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)