diff options
Diffstat (limited to 'src/acit/imapplugin.py')
| -rw-r--r-- | src/acit/imapplugin.py | 160 |
1 files changed, 156 insertions, 4 deletions
diff --git a/src/acit/imapplugin.py b/src/acit/imapplugin.py index a73a2de..9a6e8df 100644 --- a/src/acit/imapplugin.py +++ b/src/acit/imapplugin.py @@ -10,6 +10,8 @@ import re from imap_tools import MailBox, MailMessage +from mariadb import Connection, Cursor + from .imap_pool import MailBoxPool, PoolEmpty from .db import DBPoolManager @@ -136,14 +138,17 @@ class ImapPlugin(plugins.SimplePlugin): cur.execute("SELECT tracker,bugid FROM msgindex WHERE messageid=? LIMIT 1",(messageid,)) return cur.fetchone() - def format_emailaddr(self,project=None,bugid=None,subject=None): + def format_emailaddr(self,project=None,bugid=None,subject=None,headers={}): + from urllib.parse import quote as quote email=self.addr_format.format(proj=project, bug=bugid) email=email.replace("#None",'') email=email.replace("+None",'') if subject: - from urllib.parse import quote as quote email+='?subject=' email+=quote(subject, safe='') + for key,value in headers.items(): + email+='?%s=%s'%(key,quote(value,safe='')) + return email @@ -216,7 +221,10 @@ class ImapPlugin(plugins.SimplePlugin): except Exception: import traceback self.mlog("Error processing email: ",traceback.format_exc()) + self.mlog("Message-ID:",msg.headers["message-id"] if "message-id" in msg.headers else "None") self.mlog("Moving mail to INBOX/Errors.") + if "message-id" in msg.headers: + self.mail_error(msg=msg,notice="There was an error processing your email with message id:\n "+msg.headers["message-id"]+"\nPlease contact the instance administrator.") self.move_errored_mail(mailbox,msg) # block: update all webpages that received new mail @@ -302,16 +310,50 @@ class ImapPlugin(plugins.SimplePlugin): cur.execute("DELETE FROM subscribers WHERE tracker=? AND bugid=? AND email=?)",(proj,bug,msg.from_)) mailbox.delete([msg.uid]) return + + # block: handle secure email which uses $token$ as projectname + if proj.startswith('$token$='): + token=proj.removeprefix('$token$=') + self.mlog("This email is sent to a secure token, resolving (matching against %s)."%token) + with self.dbpool.get_connection() as conn, conn.cursor() as cur: + cur.execute("SELECT email,tracker,bugid FROM tokens WHERE token=? AND usedin IS NULL",(token,)) + data=cur.fetchone() + if data and data[0]==msg.from_: + self.mlog("Resolved to",data) + proj=data[1] + bug=data[2] + try: + self.secure_process_commands(mailbox,msg,conn,cur,proj,bug) + except Exception as e: + self.mlog("Error. IDK.",traceback=True) + self.mail_error("Error processing commands:\n "+repr(e)+"\nPlease contact a site admin.") + cur.execute("UPDATE tokens SET usedin=? WHERE token=?",(msg.headers["message-id"] if "message-id" else "nomessageid"),token) + conn.commit() + else: + self.mlog("Couldn't resolve.") + # end block + + # block: handle in-reply-to headers if "in-reply-to" in msg.headers: self.mlog("Using In-Reply-To header to figure out meta") replyid=msg.headers["in-reply-to"] data=self.find_in_reply_to(replyid) if data: proj,bug=data + + elif proj and bug: + pass # just ignore this email if we already found a project and bug + + elif "RETRYING" in msg.flags: + self.mail_error(msg,"No such email: "+msg.headers["in-reply-to"]) + self.move_errored_mail(mailbox,msg) + else: # try again later, maybe we need to index first or handle some other email first + mailbox.flag([msg.uid],"RETRYING") self.mlog("Message-ID not in index, trying again next round") return + # end block # block: make sure a project was specified if not proj: @@ -379,9 +421,16 @@ class ImapPlugin(plugins.SimplePlugin): if not msg.from_ in bugobj.subscribers: bugobj.addsubscriber(msg.from_) + if msg.subject.startswith("SECURE"): + self.secure_generate_token(mailbox,msg,proj,bug) + mailbox.delete([msg.uid]) + return (proj,bug) + if not "in-reply-to" in msg.headers: replyto=self.get_newest_from_mailbox(mailbox,proj,bug) + + replyto=self.get_newest_from_mailbox(mailbox,proj,bug) self.fwd_to_list(msg,bugobj,replyto=replyto) except Exception: self.mlog("Error processing email '%s' for %s/%d"%(msg.subject,proj,bug),traceback=True) @@ -434,13 +483,116 @@ class ImapPlugin(plugins.SimplePlugin): self.mlog("Bcc:",bcc) + from_=self.format_emailaddr(bug.tracker,bug.bugid) + self.smtp.sendmail( - from_=self.format_emailaddr(bug.tracker,bug.bugid), + from_=from_, bcc=bcc, tosend=msg, - replyto=replyto + replyto=replyto, + extraheaders={"Reply-To":from_} + ) + + def secure_generate_token(self,mailbox:MailBox,msg:MailMessage,proj:str,bugid:int): + self.mlog("Generating a secure token for",msg.from_) + import random, string + token=''.join(( random.choice(string.ascii_letters) for i in range(40))) + with self.dbpool.get_connection() as conn, conn.cursor() as cur: + cur.execute("SELECT email FROM permissiontable WHERE email=? AND (tracker=? OR tracker IS NULL) AND (bugid=? OR bugid IS NULL)",(msg.from_,proj,bugid)) + if not cur.fetchall(): + self.mail_error(msg,notice="You do not have permission to do that here.") + return + + cur.execute("INSERT INTO tokens (email,token,tracker,bugid) VALUES (?,?,?,?)",(msg.from_,token,proj,bugid)) + conn.commit() + + from_=self.format_emailaddr(project='$token$='+token) + + headers={} + self.smtp.format_reply_headers(replyto=self.get_newest_from_mailbox(mailbox,proj,bugid), tosend=headers) + + self.smtp.sendmail( + from_=from_, + to=msg.from_, + subject="Secure token generated for %s/%d"%(proj,bugid), + save=False, + body="Please reply to this email to use your token, or use the email address:\n "+from_, + extraheaders=headers ) + + def secure_process_commands(self,mailbox:MailBox,msg:MailMessage,conn:Connection,cur:Cursor,proj:str,bugid:int): + from .emailcommands import findcommands + from .types import BUGSTATUS, BUGTYPES + + commandmatches=findcommands(msg.text) + + bug=self.site.getbug(tracker=proj,bugid=bugid) + + for match in commandmatches: + text=match.group() + text=text.replace("\r\n","\n") # dos2unix so we can forget about \r + text=text.lstrip('\n') + text=text.rstrip('\n') + + self.mlog(text) + + if text.startswith('SUBJECT '): + bug.subject=text.removeprefix("SUBJECT ") + self.mlog("Subject changed to:",bug.subject) + elif text.startswith("STATUS ") or text.startswith("TYPE "): + key,data=text.split(sep=' ',maxsplit=1) + possibles={ + "STATUS":BUGSTATUS, + "TYPE":BUGTYPES + } + if not data in possibles[key]: # no need to errorcheck this, the check above implies key can only be STATUS or TYPE + continue + + self.mlog("Set",key,"to",data) + if key=="STATUS": + bug.status=data + elif key=="TYPE": + bug.type=data + + elif text.startswith("DESCRIPTION"): + self.mlog("Description command") + cmdline,text=text.split('\n',1) + text=text.removesuffix("\nDESCRIPTION END") + m=re.match(r'DESCRIPTION @([0-9]*)(:[0-9])?',cmdline) + start=None + end=None + self.mlog(m) + if m: + try: + start=int(m[1]) + if m[1]: + end=int(m[2].lstrip(":")) + else: + end=start+1 + except ValueError as e: + self.mlog("Error converting description range to int:",repr(e)) + continue + + replacement=text.split("\n") + lines=bug.description.split("\n") + self.mlog("Replacing: ",repr("\n".join(lines[start:end])),"with",repr("\n".join(replacement))) + lines[start:end]=replacement + bug.description="\n".join(lines) + + elif text.startswith("PERMIT "): + account=text.removeprefix("PERMIT ") + cur.execute("INSERT INTO permissiontable VALUES (?,?,?)",(account,proj,bugid)) + conn.commit() + elif text.startswith("UNPERMIT "): + account=text.removeprefix("UNPERMIT ") + cur.execute("DELETE FROM permissiontable WHERE email=? AND tracker=? AND bugid=?)",(account,proj,bugid)) + conn.commit() + + else: + self.mlog("Not yet implemented command: '%s'"%text) + + def stop(self): "Sets stopping signal, waits for all threads to stop" self.mlog("Stopping. This can take a while.") |
