Commit d87388ac authored by Enrico Zini's avatar Enrico Zini
Browse files

archive-process-email now only uses Delivered-To and only looks up active processes

parent 71664a3b
......@@ -30,6 +30,9 @@ class LookupError(Exception):
Q = lambda s: s
class umask_override(object):
"""
Context manager that temporarily overrides the umask during its lifetime
"""
def __init__(self, umask):
self.new_umask = umask
self.old_umask = None
......@@ -46,8 +49,9 @@ class umask_override(object):
class Dispatcher(object):
re_dest = re.compile("^archive-(.+)@nm.debian.org$")
def __init__(self, infd=sys.stdin, destdir=DEFAULT_DESTDIR):
self.re_dest = re.compile("^archive-(.+)@nm.debian.org$")
self.destdir = destdir
self.infd = infd
self._db = None
......@@ -76,24 +80,35 @@ class Dispatcher(object):
self.msg.add_header("NM-Archive-Lookup-History", msg)
def get_dest_key(self):
to = self.msg.get("Delivered-To", None)
if to is None:
self.log_lookup("Delivered-To not found in mail")
return None
"""
Lookup the archive-(?P<key>.+) destination key in the Delivered-To mail
header, extract the key and return it.
Raises LookupError if no parsable Delivered-To header is found.
"""
dests = self.msg.get_all("Delivered-To")
if dests is None:
raise LookupError("No Delivered-To header found")
for dest in dests:
if dest == "archive@nm.debian.org":
self.log_lookup("ignoring {} as destination".format(dest))
continue
if to == "archive@nm.debian.org":
self.log_lookup("no dest key")
return None
mo = self.re_dest.match(dest)
if mo is None:
self.log_lookup("delivered-to '{}' does not match any known format".format(dest))
continue
mo = self.re_dest.match(to)
if mo is None:
self.log_lookup("invalid delivered-to: '%s'" % to)
return None
self.log_lookup("found destination key: '%s'" % mo.group(1))
return mo.group(1)
self.log_lookup("dest key: '%s'" % mo.group(1))
return mo.group(1)
raise LookupError("No valid Delivered-To headers found")
def deliver_to_failsafe(self, reason=None, exc=None):
"""
Deliver the message to the failsafe mailbox
"""
if reason is None:
reason = "exception %s: %s" % (exc.__class__.__name__, str(exc))
self.msg["NM-Archive-Failsafe-Reason"] = reason
......@@ -115,16 +130,18 @@ class Dispatcher(object):
SELECT pr.archive_key
FROM person p
JOIN process pr ON pr.person_id = p.id
WHERE pr.is_active = 1
"""
if '=' in dest_key:
# Lookup email
email = dest_key.replace("=", "@")
self.log_lookup("lookup by email '%s'" % email)
cur.execute(Q(query + "WHERE p.email=%s"), (email,))
cur.execute(Q(query + "AND p.email=%s"), (email,))
else:
# Lookup uid
self.log_lookup("lookup by uid '%s'" % dest_key)
cur.execute(Q(query + "WHERE p.uid=%s"), (dest_key,))
cur.execute(Q(query + "AND p.uid=%s"), (dest_key,))
# Get the person ID
arc_key = None
......@@ -136,102 +153,9 @@ class Dispatcher(object):
return arc_key
def emails_to_person_ids(self, emails):
if not emails: return []
uids = []
for email in emails:
if email.endswith("@debian.org"):
uids.append(email[:-11])
where = [
"email IN (%s)" % (", ".join(("%s",) * len(emails)))
]
if uids:
where.append("uid IN (%s)" % (", ".join(("%s",) * len(uids))))
query = "SELECT id FROM person WHERE (%s)" % (" OR ".join(where))
cur = self.db.cursor()
cur.execute(Q(query), emails + uids)
pids = []
for pid, in cur:
pids.append(pid)
return pids
def list_open_processes(self):
"""
Return a list of (archive_key, AM preson ID, NM person ID) tuples
for all active processes in the database
"""
query = """
SELECT pr.archive_key, a.person_id, pr.person_id
FROM process pr
JOIN am a ON pr.manager_id = a.id
WHERE pr.is_active = true
"""
cur = self.db.cursor()
cur.execute(Q(query))
return list(cur)
def fields_to_person_ids(self, *names):
"""
Merge the contents of all the named fields in the emails, parse the
emails they contain and convert them to person IDs, returning the
result as a frozenset
"""
emails = []
for name in names:
emails += self.msg.get_all(name, [])
emails = [x[1] for x in getaddresses(emails)]
return frozenset(self.emails_to_person_ids(emails))
def archive_keys_from_headers(self):
# Get all Person ID given emails in From and To and Cc addresses
fpids = self.fields_to_person_ids("from")
tpids = self.fields_to_person_ids("to", "resent-to", "cc", "resent-cc")
if not fpids and not tpids:
raise LookupError("No known people recognised in message headers")
# Get a list of all open processes
procs = self.list_open_processes()
def archive_keys(procs):
"Return a list of archive keys from a procs-like list"
return [x[0] for x in procs]
# List of processes where the email is from an applicant
cand = [p for p in procs if p[2] in fpids]
if cand:
# If only one active process matches, we're done
if len(cand) == 1: return archive_keys(cand)
# Try to keep only those to the AM
filtered = [p for p in cand if p[1] in tpids]
return archive_keys(filtered if filtered else cand)
# List of processes where the email is to an applicant
cand = [p for p in procs if p[2] in tpids]
if cand:
# If only one active process matches, we're done
if len(cand) == 1: return archive_keys(cand)
# Try to keep only those from the AM
filtered = [p for p in cand if p[1] in fpids]
return archive_keys(filtered if filtered else cand)
# List of processes where the email is from or to an AM, but neither
# from nor to applicants: send to all processes for the AM
cand = [p for p in procs if p[1] in fpids or p[1] in tpids]
if cand: return archive_keys(cand)
# If nothing matched so far, fail
raise LookupError("No active processes found matching the email headers")
def get_arc_keys(self):
def get_arc_key(self):
dest_key = self.get_dest_key()
if dest_key is not None:
return [self.archive_key_from_dest_key(dest_key)]
return self.archive_keys_from_headers()
return self.archive_key_from_dest_key(dest_key)
def main():
......@@ -249,8 +173,6 @@ def main():
parser = Parser(usage="usage: %prog [options]",
version="%prog "+ VERSION,
description="Dispatch NM mails Cc-ed to the archive address")
#parser.add_option("-q", "--quiet", action="store_true", help="quiet mode: only output fatal errors")
#parser.add_option("-v", "--verbose", action="store_true", help="verbose mode")
parser.add_option("--dest", action="store", default=DEFAULT_DESTDIR, help="destination directory (default: %default)")
parser.add_option("--dry-run", action="store_true", help="print destinations instead of delivering mails")
(opts, args) = parser.parse_args()
......@@ -259,16 +181,16 @@ def main():
if opts.dry_run:
msgid = dispatcher.msg.get("message-id", "(no message id)")
try:
for arc_key in dispatcher.get_arc_keys():
print(msgid, arc_key)
arc_key = dispatcher.get_arc_key()
print(msgid, arc_key)
return 0
except Exception as e:
print(msgid, "failsafe")
return 1
else:
try:
for arc_key in dispatcher.get_arc_keys():
dispatcher.deliver_to_archive_key(arc_key)
arc_key = dispatcher.get_arc_key()
dispatcher.deliver_to_archive_key(arc_key)
return 0
except Exception as e:
dispatcher.deliver_to_failsafe(exc=e)
......
......@@ -59,46 +59,17 @@ class TestLookup(unittest.TestCase):
dest = self.proc.person.email.replace("@", "=")
d = self.make_dispatcher(dk=dest)
key = d.get_dest_key()
self.assertEquals(key, dest)
self.assertEqual(key, dest)
arc_key = d.archive_key_from_dest_key(key)
self.assertEquals(arc_key, self.proc.archive_key)
self.assertEqual(arc_key, self.proc.archive_key)
def testDestkeyUid(self):
dest = self.proc.person.uid
d = self.make_dispatcher(dk=dest)
key = d.get_dest_key()
self.assertEquals(key, dest)
self.assertEqual(key, dest)
arc_key = d.archive_key_from_dest_key(key)
self.assertEquals(arc_key, self.proc.archive_key)
def testInfer(self):
d = self.make_dispatcher(src="nm", dst="am")
self.assertEquals(d.get_arc_keys(), [self.proc.archive_key])
self.assertNotIn("lookup by distinct nm and am gave 0 results", d.lookup_attempts)
d = self.make_dispatcher(src="am", dst="nm")
self.assertEquals(d.get_arc_keys(), [self.proc.archive_key])
self.assertNotIn("lookup by distinct nm and am gave 0 results", d.lookup_attempts)
d = self.make_dispatcher(src="fd", dst="nm")
self.assertEquals(d.get_arc_keys(), [self.proc.archive_key])
def testInferMultidest(self):
# Find an AM with multiple NMs
am = None
procs = None
for a in bmodels.AM.objects.all():
procs = a.processed.filter(is_active=True)
if procs.count() > 1:
am = a
procs = list(procs)
break
assert am
# AM sends an email to all their applicants
d = self.make_dispatcher(src=[am.person], dst=[p.person for p in procs])
self.assertEquals(sorted(d.get_arc_keys()), sorted(p.archive_key for p in procs))
self.assertNotIn("lookup by distinct nm and am gave 0 results", d.lookup_attempts)
self.assertEqual(arc_key, self.proc.archive_key)
if __name__ == '__main__':
unittest.main()
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment