diff options
| author | vosjedev <vosje+git@vosjedev.net> | 2025-10-09 16:30:25 +0200 |
|---|---|---|
| committer | vosjedev <vosje+git@vosjedev.net> | 2025-10-09 16:30:25 +0200 |
| commit | 2ce8748cd7e4c0df1d0394b24bc341bb57534d05 (patch) | |
| tree | 0e45356936c96979cf5c5ece3ce53e4b042da391 | |
| parent | 67398d29c788f88012e330c9e98a886e6b05e7de (diff) | |
| download | acit-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.py | 211 |
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) + + + + + |
