import cherrypy import threading from os import getenv from queue import Queue, Empty from imap_tools import MailMessage as IMAP_Message from email.message import Message from email import message_from_bytes, utils from smtplib import SMTP_SSL as SMTP from smtplib import ( SMTPResponseException, SMTPDataError, SMTPConnectError, SMTPServerDisconnected, SMTPHeloError, SMTPAuthenticationError ) from datetime import datetime class SmtpPlugin(): def __init__(self): self.smtp_server=getenv("ACIT_SMTP_SERVER") self.smtp_user=getenv("ACIT_SMTP_USER") self.smtp_pass=getenv("ACIT_SMTP_PASS") self.smtp_port=int(getenv("ACIT_SMTP_PORT",0)) self.domain="whyisthisattributeunset.example.com" self.thread:threading.Thread=None self.stopping=threading.Event() self.que=Queue() def sendmail(self, from_:str=None, to:str|list[str]=[], subject:str=None, body:str=None, cc:list[str]=[], bcc:list[str]=[], replyto:IMAP_Message=None, tosend:IMAP_Message|Message=None, save=False, ): """ Required arguments: if ``tosend`` is unset: - body, from_ - if ``replyto`` is also unset, also: - to, subject if ``tosend`` is set: - from_ from_ (str): the email address to use for the ``From`` header. if ``tosend`` is set, used for ``Resent-From`` instead, and added to ``Cc``. to (list|str): the email address(es) to send to. when specifying multiple addrs, use a list. subject (str): the subject for the email. not needed when ``replyto`` is set, though when both are set, ``subject`` is used. body (str): the email body cc (list): email addresses to put in the cc save (bool): force adding from_ to the cclist, making ImapPlugin store the email in ``Sent`` on arrival """ if type(to)==str: to=[to] if tosend: if type(tosend)==IMAP_Message: tosend=tosend.obj tosend=message_from_bytes(tosend.as_bytes(), policy=tosend.policy) # copy message object tosend["Resent-From"]=from_ tosend["Resent-Date"]=utils.format_datetime(datetime.now()) tosend["Resent-To"]=", ".join(to) if bcc: tosend["Resent-Bcc"]=", ".join(bcc) if cc: tosend["Resent-Cc"]=", ".join(bcc) if not from_ in tosend["to"]+tosend.get("cc","")+tosend.get("resent-to",""): if "cc" in tosend: tosend["cc"]+=", "+from_ else: tosend["cc"]=from_ tosend["X-Acit-Delete-When-Sender"]=from_ else: if replyto: cc.extend(replyto.cc) cc.extend(replyto.to) to.append(replyto.headers["reply-to"] if "reply-to" in replyto.headers else replyto.from_) if not subject: subject="Re: "+replyto.subject for addr in to: if addr in cc: cc.remove(addr) for addr in cc: if addr in bcc: bcc.remove(addr) if not (to and body and from_ and subject): raise ValueError("Missing to, from_, body, or subject") tosend=Message() tosend["From"]=from_ tosend["To"]=", ".join(set(to)) # set()s can't have duplicates tosend["Subject"]=subject tosend["Cc"]=", ".join(set(cc)) tosend["Bcc"]=", ".join(set(bcc)) if replyto: if "references" in replyto.headers: tosend["References"]=replyto.obj["references"] elif "in-reply-to" in replyto.headers: tosend["References"]=replyto.obj["in-reply-to"] if "message-id" in replyto.headers: tosend["In-Reply-To"]=replyto.obj["message-id"] if "References" in tosend: tosend["References"]+=" "+replyto.obj["message-id"] else: tosend["References"]=replyto.obj["message-id"] tosend.set_payload(body) tosend["Sender"]=from_ tosend["X-Acit-Is-Outgoing"]=from_ if not "Message-ID" in tosend: tosend["Message-ID"]="<"+datetime.now().strftime("%d%m%Y%H%M%S%j")+"@"+self.domain+">" if save and not from_ in tosend["Cc"]: tosend["Cc"]+=" "+from_ self.que.put(tosend) return tosend["Message-ID"] def start(self): self.thread=threading.Thread(target=self.runner) self.thread.start() def runner(self): threading.current_thread().name="SendMailRunner" smtp=None while True: try: item=self.que.get(timeout=1) if not item: continue except Empty: if smtp: smtp.quit() smtp=None if self.stopping.is_set(): break continue try: if not smtp: self.mlog("Connecting and logging in to smtp server") smtp=SMTP(host=self.smtp_server,port=self.smtp_port) smtp.ehlo() smtp.login(user=self.smtp_user,password=self.smtp_pass) self.mlog("Sending message %s '%s'"%(item.get("message-id"),item.get("subject"))) smtp.send_message(item) except SMTPAuthenticationError: self.mlog("!!! AUTH ERROR !!! please check your config. Discarding email.") except (SMTPHeloError,SMTPResponseException,SMTPDataError,SMTPServerDisconnected,SMTPConnectError) as e: self.mlog("Error occured: %s"%repr(e)) self.que.put(item) self.mlog("Sending rescheduled.") except Exception: if smtp: smtp.quit() smtp=None import traceback self.mlog("An error occured in the SMTP runner:\n",traceback.format_exc()) self.mlog("As a result, the following email was discarded: %s '%s'"%(item.get("message-id"),item.get("subject"))) finally: self.que.task_done() def stop(self,block=True): self.mlog("Signaling stop to sendmail") self.stopping.set() if block: self.thread.join() def mlog(self,*msg,**kwargs): import traceback function=traceback.extract_stack(limit=2)[0].name cherrypy.log(context="%s>>SENDMAIL:%s"%(threading.current_thread().name, function), msg=" ".join([str(i) for i in msg]),**kwargs)