aboutsummaryrefslogtreecommitdiffstats
path: root/src/acit/imapplugin.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/acit/imapplugin.py')
-rw-r--r--src/acit/imapplugin.py160
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.")