aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorVosjedev <vosje@vosjedev.net>2025-11-06 15:33:30 +0100
committerVosjedev <vosje@vosjedev.net>2025-11-06 15:33:30 +0100
commit8d8375b2c309c54a734e2d3b8cbd9b35ac664e74 (patch)
tree1fe6549cf19855d4d3cce1c8df6abff6127fcbdc
parentf13fc2943d1292bf31a9fe8112dc3729e29280eb (diff)
downloadacit-8d8375b2c309c54a734e2d3b8cbd9b35ac664e74.tar.gz
acit-8d8375b2c309c54a734e2d3b8cbd9b35ac664e74.tar.bz2
acit-8d8375b2c309c54a734e2d3b8cbd9b35ac664e74.tar.xz
smtp: allow sending email using an smtp queue
-rw-r--r--src/acit/smtpplugin.py207
1 files changed, 207 insertions, 0 deletions
diff --git a/src/acit/smtpplugin.py b/src/acit/smtpplugin.py
new file mode 100644
index 0000000..3b9cc81
--- /dev/null
+++ b/src/acit/smtpplugin.py
@@ -0,0 +1,207 @@
+
+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)
+