aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorvosjedev <vosje+git@vosjedev.net>2025-10-09 16:30:25 +0200
committervosjedev <vosje+git@vosjedev.net>2025-10-09 16:30:25 +0200
commit2ce8748cd7e4c0df1d0394b24bc341bb57534d05 (patch)
tree0e45356936c96979cf5c5ece3ce53e4b042da391
parent67398d29c788f88012e330c9e98a886e6b05e7de (diff)
downloadacit-2ce8748cd7e4c0df1d0394b24bc341bb57534d05.tar.gz
acit-2ce8748cd7e4c0df1d0394b24bc341bb57534d05.tar.bz2
acit-2ce8748cd7e4c0df1d0394b24bc341bb57534d05.tar.xz
[mail] Add a lot of email logic, note this doesn't run yet
-rw-r--r--src/acit/imapplugin.py211
1 files changed, 211 insertions, 0 deletions
diff --git a/src/acit/imapplugin.py b/src/acit/imapplugin.py
new file mode 100644
index 0000000..56eda22
--- /dev/null
+++ b/src/acit/imapplugin.py
@@ -0,0 +1,211 @@
+
+import threading
+from os import getenv
+from queue import Queue, Empty
+
+from time import time, sleep
+
+from cherrypy.process import wspbus, plugins
+import cherrypy
+
+import re
+
+from imap_tools import MailBox, MailMessage
+
+from typing import Literal
+
+from . import html
+
+class ImapPlugin(plugins.SimplePlugin):
+ def __init__(self,bus,
+ imap_server:str,
+ imap_user:str,
+ imap_pass:str,
+ smtp_server:str,
+ smtp_user:str,
+ smtp_pass:str,
+ ):
+ plugins.SimplePlugin.__init__(self,bus)
+ self.imap_server=imap_server
+ self.imap_user=imap_user
+ self.imap_pass=imap_pass
+ self.smtp_server=smtp_server
+ self.smtp_user=smtp_user
+ self.smtp_pass=smtp_pass
+
+ self.mailin_thread=None
+ self.mailout_thread=None
+ self.stopping=threading.Event()
+
+ self.mailout_queue=Queue()
+
+ emailcache={}
+
+ def start(self):
+ self.stopping.clear()
+ self.mailin_thread=threading.Thread(target=self.in_runner)
+ self.mailout_thread=threading.Thread(target=self.out_runner)
+
+ # the following commented block was too free to be parsed. If you can get it to work, feel free to email a patch.
+ #self.addrformat=getenv("ACIT_ADDRESSFORMAT",
+ # self.imap_user.replace("@","+{proj}#{bug}@",1) # default to something like something+example#15@example.com
+ # )
+ #self.addrregex=getenv("ACIT_ADDRESSREGEX",
+ # self.addrformat.replace("+","\\+").format(proj="[^@#]*",bug="[0-9]*")
+ # # note above default only works if your email address doesn't contain any character regex picks up, and you use plus-addresses
+ # )
+
+
+ self.uses_aliases=getenv("ACIT_MAIL_USES_ALIASES",False)
+
+ name,domain=self.imap_user.rsplit("@",1)
+ self.emaildomain=getenv("ACIT_MAIL_DOMAIN",domain)
+ self.emailname=getenv("ACIT_MAIL_NAME",name)
+
+ if self.uses_aliases:
+ self.addr_format="{proj}#{bug}@"+self.emaildomain
+ self.addr_regex=r"[^@#\+]*#[0-9]*@"+ self.emaildomain.replace(".","\\.")
+
+ else:
+ self.addr_format=self.emailname+"+{proj}#{bug}@"+self.emaildomain
+ self.addr_regex=self.emailname+r"+[^@#\+]*#[0-9]*@"+self.emaildomain
+
+
+
+
+ self.mailin_thread.start()
+ self.mailout_thread.start()
+
+ self.mailbox_per_thread={}
+
+ def stripInfoFromMailAddr(self,address:str):
+ if not self.uses_aliases:
+ address=address.removesuffix(self.emailname+"+")
+
+ address=address.removesuffix("@"+self.emaildomain)
+ return address.rsplit("#",1)
+
+
+ def get_bug_folder(self,proj,bug):
+ return self.ensurefolder("bugs",proj,str(bug))
+
+
+ def get_MailBox(self):
+ tid=threading.current_thread()
+ if not tid in self.mailbox_per_thread:
+ self.mailbox_per_thread[tid]=MailBox(self.imap_server).login(self.imap_user,self.imap_pass, None)
+ return self.mailbox_per_thread[tid]
+
+
+ def generate_page(self,mailbox:MailBox,project=None,bug=None):
+ if not bug and not project:
+ return self.generate_main_page(mailbox)
+ if project and not bug:
+ return self.generate_project_page(mailbox,project)
+
+ try:
+ mailbox.folder.set(self.get_bug_folder(project,bug))
+ pagelimit=25
+ currentpage=""
+ for msg in mailbox.fetch():
+ pass
+
+
+
+ except Exception:
+ self.mlog("Error occured generating bug page for %s/%d"%(project,bug),traceback=True)
+
+
+
+ def in_runner(self):
+ with self.get_MailBox() as mailbox:
+ self.mlog("IMAP monitor thread started (connected)")
+ while not self.stopping.is_set():
+ try:
+ mailbox.folder.set("INBOX")
+
+ # block: wait 5 minutes and poll after that
+ mailbox.idle.start()
+
+ if self.stopping.wait(300): # if not stopping, this just times out after 300 seconds, so this is a nice timer
+ mailbox.idle.stop()
+ break
+
+ responses=mailbox.idle.poll(timeout=1)
+ mailbox.idle.stop()
+ # end block
+
+ if responses or mailbox.folder.status()["MESSAGES"]>0:
+ refreshable={}
+
+ for msg in mailbox.fetch():
+ for addr in msg.to + msg.cc + msg.bcc + msg.reply_to:
+ if re.fullmatch(self.addr_regex,addr):
+ proj,bug=self.stripInfoFromMailAddr(addr)
+ break
+
+ # block: parse bug id
+ try:
+ bug=int(bug)
+ except ValueError as e:
+ self.mlog("Error decoding value to int:",e,traceback=True)
+
+ self.mail_error(msg,notice="Exception while trying to convert bug number to integer",exception=e)
+ self.move_errored_mail(msg)
+ continue
+ # end block
+
+ # block: make sure project exists
+ proj=self.get_full_projectname(proj)
+ if not proj:
+ self.mlog("Received email for nonexistend project %s"%proj)
+ self.mail_error(msg,notice="Project '%s' doesn't exist"%proj)
+ self.move_errored_mail(msg)
+ continue
+ # end block
+
+ # block: move mail to folder specific for this project/bug
+ try:
+ path=self.get_bug_folder(proj,bug)
+ mailbox.move([msg.uid], path)
+ refreshable.setdefault(proj,[]).append(bug)
+ except Exception:
+ self.mlog("Error processing email '%s' for %s/%d"%(msg.subject,proj,bug),traceback=True)
+ # end block
+
+ # block: update all webpages that received new mail
+ for proj,bugs in refreshable.items():
+ self.generate_page(proj,None) # project page needs to be regenerated too (counters)
+ for bug in bugs:
+ self.generate_page(proj,bug)
+
+ self.generate_page(None,None) # main page needs to be generated too (a new project may have appeared + counters)
+ # end block
+
+
+ except Exception:
+ import traceback
+ exc=traceback.format_exc()
+ self.mlog("Exception occured:\n%s"%exc)
+ self.mlog("!! this may lead to emails not appearing or appearing later !!")
+
+ self.mlog("IMAP monitor thread stopped.")
+
+
+ def out_runner(self):
+ pass
+
+ def stop(self):
+ self.mlog("Stopping. This can take a while.")
+ self.stopping.set()
+ self.mailin_thread.join()
+ self.mailout_thread.join()
+ self.mlog("Stopped")
+
+ def mlog(self,*msg,**kwargs):
+ cherrypy.log(context="MAIL", msg=" ".join([str(i) for i in msg]),**kwargs)
+
+
+
+
+