Fixed email plugin's use of globals

The email plugin was still using globals for tracking
the check_email_delay as well as the config.  This
patch creates a new singleton thread safe mechanism for
check_email_delay with the EmailInfo class.
This commit is contained in:
Hemna 2021-10-04 11:36:13 -04:00
parent 270be947b5
commit a6ed7b894b
1 changed files with 82 additions and 54 deletions

View File

@ -5,6 +5,7 @@ import imaplib
import logging
import re
import smtplib
import threading
import time
import imapclient
@ -15,9 +16,45 @@ from aprsd import messaging, plugin, stats, threads, trace
LOG = logging.getLogger("APRSD")
# This gets forced set from main.py prior to being used internally
CONFIG = {}
check_email_delay = 60
class EmailInfo:
"""A singleton thread safe mechanism for the global check_email_delay.
This has to be done because we have 2 separate threads that access
the delay value.
1) when EmailPlugin runs from a user message and
2) when the background EmailThread runs to check email.
Access the check email delay with
EmailInfo().delay
Set it with
EmailInfo().delay = 100
or
EmailInfo().delay += 10
"""
_instance = None
def __new__(cls, *args, **kwargs):
"""This magic turns this into a singleton."""
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.lock = threading.Lock()
cls._instance._delay = 60
return cls._instance
@property
def delay(self):
with self.lock:
return self._delay
@delay.setter
def delay(self, val):
with self.lock:
self._delay = val
class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
@ -34,8 +71,6 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
def setup(self):
"""Ensure that email is enabled and start the thread."""
global CONFIG
CONFIG = self.config
email_enabled = self.config["aprsd"]["email"].get("enabled", False)
validation = self.config["aprsd"]["email"].get("validate", False)
@ -81,7 +116,7 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
r = re.search("^-([0-9])[0-9]*$", message)
if r is not None:
LOG.debug("RESEND EMAIL")
resend_email(r.group(1), fromcall)
resend_email(self.config, r.group(1), fromcall)
reply = messaging.NULL_MESSAGE
# -user@address.com body of email
elif re.search(r"^-([A-Za-z0-9_\-\.@]+) (.*)", message):
@ -91,7 +126,7 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
to_addr = a.group(1)
content = a.group(2)
email_address = get_email_from_shortcut(to_addr)
email_address = get_email_from_shortcut(self.config, to_addr)
if not email_address:
reply = "Bad email address"
return reply
@ -114,7 +149,7 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
too_soon = 1
if not too_soon or ack == 0:
LOG.info(f"Send email '{content}'")
send_result = email.send_email(to_addr, content)
send_result = send_email(self.config, to_addr, content)
reply = messaging.NULL_MESSAGE
if send_result != 0:
reply = f"-{to_addr} failed"
@ -143,10 +178,9 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
return reply
def _imap_connect():
global CONFIG
imap_port = CONFIG["aprsd"]["email"]["imap"].get("port", 143)
use_ssl = CONFIG["aprsd"]["email"]["imap"].get("use_ssl", False)
def _imap_connect(config):
imap_port = config["aprsd"]["email"]["imap"].get("port", 143)
use_ssl = config["aprsd"]["email"]["imap"].get("use_ssl", False)
# host = CONFIG["aprsd"]["email"]["imap"]["host"]
# msg = "{}{}:{}".format("TLS " if use_ssl else "", host, imap_port)
# LOG.debug("Connect to IMAP host {} with user '{}'".
@ -154,7 +188,7 @@ def _imap_connect():
try:
server = imapclient.IMAPClient(
CONFIG["aprsd"]["email"]["imap"]["host"],
config["aprsd"]["email"]["imap"]["host"],
port=imap_port,
use_uid=True,
ssl=use_ssl,
@ -166,8 +200,8 @@ def _imap_connect():
try:
server.login(
CONFIG["aprsd"]["email"]["imap"]["login"],
CONFIG["aprsd"]["email"]["imap"]["password"],
config["aprsd"]["email"]["imap"]["login"],
config["aprsd"]["email"]["imap"]["password"],
)
except (imaplib.IMAP4.error, Exception) as e:
msg = getattr(e, "message", repr(e))
@ -183,15 +217,15 @@ def _imap_connect():
return server
def _smtp_connect():
host = CONFIG["aprsd"]["email"]["smtp"]["host"]
smtp_port = CONFIG["aprsd"]["email"]["smtp"]["port"]
use_ssl = CONFIG["aprsd"]["email"]["smtp"].get("use_ssl", False)
def _smtp_connect(config):
host = config["aprsd"]["email"]["smtp"]["host"]
smtp_port = config["aprsd"]["email"]["smtp"]["port"]
use_ssl = config["aprsd"]["email"]["smtp"].get("use_ssl", False)
msg = "{}{}:{}".format("SSL " if use_ssl else "", host, smtp_port)
LOG.debug(
"Connect to SMTP host {} with user '{}'".format(
msg,
CONFIG["aprsd"]["email"]["imap"]["login"],
config["aprsd"]["email"]["imap"]["login"],
),
)
@ -214,15 +248,15 @@ def _smtp_connect():
LOG.debug(f"Connected to smtp host {msg}")
debug = CONFIG["aprsd"]["email"]["smtp"].get("debug", False)
debug = config["aprsd"]["email"]["smtp"].get("debug", False)
if debug:
server.set_debuglevel(5)
server.sendmail = trace.trace(server.sendmail)
try:
server.login(
CONFIG["aprsd"]["email"]["smtp"]["login"],
CONFIG["aprsd"]["email"]["smtp"]["password"],
config["aprsd"]["email"]["smtp"]["login"],
config["aprsd"]["email"]["smtp"]["password"],
)
except Exception:
LOG.error("Couldn't connect to SMTP Server")
@ -273,9 +307,9 @@ def validate_shortcuts(config):
)
def get_email_from_shortcut(addr):
if CONFIG["aprsd"]["email"].get("shortcuts", False):
return CONFIG["aprsd"]["email"]["shortcuts"].get(addr, addr)
def get_email_from_shortcut(config, addr):
if config["aprsd"]["email"].get("shortcuts", False):
return config["aprsd"]["email"]["shortcuts"].get(addr, addr)
else:
return addr
@ -286,9 +320,9 @@ def validate_email_config(config, disable_validation=False):
This helps with failing early during startup.
"""
LOG.info("Checking IMAP configuration")
imap_server = _imap_connect()
imap_server = _imap_connect(config)
LOG.info("Checking SMTP configuration")
smtp_server = _smtp_connect()
smtp_server = _smtp_connect(config)
# Now validate and flag any shortcuts as invalid
if not disable_validation:
@ -398,34 +432,32 @@ def parse_email(msgid, data, server):
@trace.trace
def send_email(to_addr, content):
global check_email_delay
shortcuts = CONFIG["aprsd"]["email"]["shortcuts"]
email_address = get_email_from_shortcut(to_addr)
def send_email(config, to_addr, content):
shortcuts = config["aprsd"]["email"]["shortcuts"]
email_address = get_email_from_shortcut(config, to_addr)
LOG.info("Sending Email_________________")
if to_addr in shortcuts:
LOG.info("To : " + to_addr)
to_addr = email_address
LOG.info(" (" + to_addr + ")")
subject = CONFIG["ham"]["callsign"]
subject = config["ham"]["callsign"]
# content = content + "\n\n(NOTE: reply with one line)"
LOG.info("Subject : " + subject)
LOG.info("Body : " + content)
# check email more often since there's activity right now
check_email_delay = 60
EmailInfo().delay = 60
msg = MIMEText(content)
msg["Subject"] = subject
msg["From"] = CONFIG["aprsd"]["email"]["smtp"]["login"]
msg["From"] = config["aprsd"]["email"]["smtp"]["login"]
msg["To"] = to_addr
server = _smtp_connect()
if server:
try:
server.sendmail(
CONFIG["aprsd"]["email"]["smtp"]["login"],
config["aprsd"]["email"]["smtp"]["login"],
[to_addr],
msg.as_string(),
)
@ -440,20 +472,19 @@ def send_email(to_addr, content):
@trace.trace
def resend_email(count, fromcall):
global check_email_delay
def resend_email(config, count, fromcall):
date = datetime.datetime.now()
month = date.strftime("%B")[:3] # Nov, Mar, Apr
day = date.day
year = date.year
today = f"{day}-{month}-{year}"
shortcuts = CONFIG["aprsd"]["email"]["shortcuts"]
shortcuts = config["aprsd"]["email"]["shortcuts"]
# swap key/value
shortcuts_inverted = {v: k for k, v in shortcuts.items()}
try:
server = _imap_connect()
server = _imap_connect(config)
except Exception as e:
LOG.exception("Failed to Connect to IMAP. Cannot resend email ", e)
return
@ -493,7 +524,7 @@ def resend_email(count, fromcall):
reply = "-" + from_addr + " * " + body.decode(errors="ignore")
# messaging.send_message(fromcall, reply)
msg = messaging.TextMessage(
CONFIG["aprs"]["login"],
config["aprs"]["login"],
fromcall,
reply,
)
@ -515,11 +546,11 @@ def resend_email(count, fromcall):
str(s).zfill(2),
)
# messaging.send_message(fromcall, reply)
msg = messaging.TextMessage(CONFIG["aprs"]["login"], fromcall, reply)
msg = messaging.TextMessage(config["aprs"]["login"], fromcall, reply)
msg.send()
# check email more often since we're resending one now
check_email_delay = 60
EmailInfo().delay = 60
server.logout()
# end resend_email()
@ -533,27 +564,24 @@ class APRSDEmailThread(threads.APRSDThread):
self.past = datetime.datetime.now()
def loop(self):
global check_email_delay
check_email_delay = 60
time.sleep(5)
stats.APRSDStats().email_thread_update()
# always sleep for 5 seconds and see if we need to check email
# This allows CTRL-C to stop the execution of this loop sooner
# than check_email_delay time
now = datetime.datetime.now()
if now - self.past > datetime.timedelta(seconds=check_email_delay):
if now - self.past > datetime.timedelta(seconds=EmailInfo().delay):
# It's time to check email
# slowly increase delay every iteration, max out at 300 seconds
# any send/receive/resend activity will reset this to 60 seconds
if check_email_delay < 300:
check_email_delay += 1
if EmailInfo().delay < 300:
EmailInfo().delay += 1
LOG.debug(
"check_email_delay is " + str(check_email_delay) + " seconds",
f"check_email_delay is {EmailInfo().delay} seconds ",
)
shortcuts = CONFIG["aprsd"]["email"]["shortcuts"]
shortcuts = self.config["aprsd"]["email"]["shortcuts"]
# swap key/value
shortcuts_inverted = {v: k for k, v in shortcuts.items()}
@ -564,7 +592,7 @@ class APRSDEmailThread(threads.APRSDThread):
today = f"{day}-{month}-{year}"
try:
server = _imap_connect()
server = _imap_connect(self.config)
except Exception as e:
LOG.exception("IMAP failed to connect.", e)
return True
@ -658,7 +686,7 @@ class APRSDEmailThread(threads.APRSDThread):
LOG.exception("Couldn't remove seen flag from email", e)
# check email more often since we just received an email
check_email_delay = 60
EmailInfo().delay = 60
# reset clock
LOG.debug("Done looping over Server.fetch, logging out.")