commit 39e3cf66a2324e262ae1fc617f4f2d0e79c0bf49 Merge: 63c32b5 f6f1bd8 Author: Isis Lovecruft isis@torproject.org Date: Thu Apr 17 04:21:05 2014 +0000
Merge branch 'fix/11522-exc-in-email-dist' into develop
Conflicts: lib/bridgedb/EmailServer.py
lib/bridgedb/Dist.py | 117 ++----------- lib/bridgedb/EmailServer.py | 293 ++++++++++++++------------------- lib/bridgedb/Tests.py | 9 +- lib/bridgedb/crypto.py | 179 ++++++++++++++++++++ lib/bridgedb/parse/addr.py | 171 ++++++++++++++++++- lib/bridgedb/test/test_EmailServer.py | 278 ++++++++++++++++++++----------- 6 files changed, 673 insertions(+), 374 deletions(-)
diff --cc lib/bridgedb/EmailServer.py index c58ec53,022dd41..a1980d1 --- a/lib/bridgedb/EmailServer.py +++ b/lib/bridgedb/EmailServer.py @@@ -29,28 -28,19 +28,19 @@@ from zope.interface import implement from bridgedb import Dist from bridgedb import I18n from bridgedb import safelog +from bridgedb import translations + from bridgedb.crypto import getGPGContext + from bridgedb.crypto import gpgSignMessage from bridgedb.Filters import filterBridgesByIP6 from bridgedb.Filters import filterBridgesByIP4 from bridgedb.Filters import filterBridgesByTransport from bridgedb.Filters import filterBridgesByNotBlockedIn + from bridgedb.parse import addr + from bridgedb.parse.addr import BadEmail + from bridgedb.parse.addr import UnsupportedDomain + from bridgedb.parse.addr import canonicalizeEmailDomain
- class MailFile: - """A file-like object used to hand rfc822.Message a list of lines - as though it were reading them from a file.""" - def __init__(self, lines): - self.lines = lines - self.idx = 0 - def readline(self): - try : - line = self.lines[self.idx] - self.idx += 1 - return line - except IndexError: - return "" -- def getBridgeDBEmailAddrFromList(ctx, address_list): """Loop through a list of (full name, email address) pairs and look up our mail address. If our address isn't found (which can't happen), return @@@ -75,46 -65,67 +65,67 @@@ def getMailResponse(lines, ctx) will receive the response, and a readable filelike object containing the response. Return None,None if we shouldn't answer. """ + raw = io.StringIO() + raw.writelines([unicode('{0}\n'.format(line)) for line in lines]) + raw.seek(0) + + msg = smtp.rfc822.Message(raw) # Extract data from the headers. - msg = rfc822.Message(MailFile(lines)) - subject = msg.getheader("Subject", None) - if not subject: subject = "[no subject]" - clientFromAddr = msg.getaddr("From") - clientSenderAddr = msg.getaddr("Sender") - # RFC822 requires at least one 'To' address - clientToList = msg.getaddrlist("To") - clientToaddr = getBridgeDBEmailAddrFromList(ctx, clientToList) msgID = msg.getheader("Message-ID", None) - if clientSenderAddr and clientSenderAddr[1]: - clientAddr = clientSenderAddr[1] - elif clientFromAddr and clientFromAddr[1]: - clientAddr = clientFromAddr[1] - else: - logging.info("No From or Sender header on incoming mail.") - return None,None + subject = msg.getheader("Subject", None) or "[no subject]"
- # Look up the locale part in the 'To:' address, if there is one and get - # the appropriate Translation object - lang = translations.getLocaleFromPlusAddr(clientToaddr) - t = translations.installTranslations(lang) + fromHeader = msg.getaddr("From") + senderHeader = msg.getaddr("Sender")
+ clientAddrHeader = None try: - _, addrdomain = Dist.extractAddrSpec(clientAddr.lower()) - except BadEmail: - logging.info("Ignoring bad address on incoming email.") + clientAddrHeader = fromHeader[1] + except (IndexError, TypeError, AttributeError): + pass + + if not clientAddrHeader: + logging.warn("No From header on incoming mail.") + try: + clientAddrHeader = senderHeader[1] + except (IndexError, TypeError, AttributeError): + pass + + if not clientAddrHeader: + logging.warn("No Sender header on incoming mail.") return None, None
- if not addrdomain: - logging.info("Couldn't parse domain from %r" % clientAddr) + try: + clientAddr = addr.normalizeEmail(clientAddrHeader, + ctx.cfg.EMAIL_DOMAIN_MAP, + ctx.cfg.EMAIL_DOMAIN_RULES) + except (UnsupportedDomain, BadEmail) as error: + logging.warn(error) + return None, None
- if addrdomain and ctx.cfg.EMAIL_DOMAIN_MAP: - addrdomain = ctx.cfg.EMAIL_DOMAIN_MAP.get(addrdomain, addrdomain) + # RFC822 requires at least one 'To' address + clientToList = msg.getaddrlist("To") + clientToAddr = getBridgeDBEmailAddrFromList(ctx, clientToList) + + # Look up the locale part in the 'To:' address, if there is one and get + # the appropriate Translation object - lang = getLocaleFromPlusAddr(clientToAddr) - t = I18n.getLang(lang) ++ lang = translations.getLocaleFromPlusAddr(clientToAddr) ++ t = translations.installTranslations(lang)
- if addrdomain not in ctx.cfg.EMAIL_DOMAINS: - logging.warn("Unrecognized email domain %r", addrdomain) + canon = ctx.cfg.EMAIL_DOMAIN_MAP + for domain, rule in ctx.cfg.EMAIL_DOMAIN_RULES.items(): + if domain not in canon.keys(): + canon[domain] = domain + for domain in ctx.cfg.EMAIL_DOMAINS: + canon[domain] = domain + + try: + _, clientDomain = addr.extractEmailAddress(clientAddr.lower()) + canonical = canonicalizeEmailDomain(clientDomain, canon) + except (UnsupportedDomain, BadEmail) as error: + logging.warn(error) return None, None
- rules = ctx.cfg.EMAIL_DOMAIN_RULES.get(addrdomain, []) + rules = ctx.cfg.EMAIL_DOMAIN_RULES.get(canonical, [])
if 'dkim' in rules: # getheader() returns the last of a given kind of header; we want @@@ -287,7 -297,71 +297,46 @@@ def replyToMail(lines, ctx) reactor.connectTCP(ctx.smtpServer, ctx.smtpPort, factory) return d
-def getLocaleFromPlusAddr(address): - """See whether the user sent his email to a 'plus' address, for - instance to bridgedb+fa@tpo. Plus addresses are the current - mechanism to set the reply language - """ - replyLocale = "en" - r = '.*(<)?(\w++(\w+)@\w+(?:.\w+)+)(?(1)>)' - match = re.match(r, address) - if match: - replyLocale = match.group(3) - - return replyLocale - -def getLocaleFromRequest(request): - # See if we did get a request for a certain locale, otherwise fall back - # to 'en': - # Try evaluating the path /foo first, then check if we got a ?lang=foo - default_lang = lang = "en" - if len(request.path) > 1: - lang = request.path[1:] - if lang == default_lang: - lang = request.args.get("lang", [default_lang]) - lang = lang[0] - return I18n.getLang(lang) - + def composeEmail(fromAddr, clientAddr, subject, body, + msgID=None, gpgContext=None): + + if not subject.startswith("Re:"): + subject = "Re: %s" % subject + + msg = smtp.rfc822.Message(io.StringIO()) + msg.setdefault("From", fromAddr) + msg.setdefault("To", clientAddr) + msg.setdefault("Message-ID", smtp.messageid()) + msg.setdefault("Subject", subject) + if msgID: + msg.setdefault("In-Reply-To", msgID) + msg.setdefault("Date", smtp.rfc822date()) + msg.setdefault('Content-Type', 'text/plain; charset="utf-8"') + headers = [': '.join(m) for m in msg.items()] + + mail = io.BytesIO() + mail.writelines(buffer("\r\n".join(headers))) + mail.writelines(buffer("\r\n")) + mail.writelines(buffer("\r\n")) + + if not gpgContext: + mail.write(buffer(body)) + else: + signature, siglist = gpgSignMessage(gpgContext, body) + if signature: + mail.writelines(buffer(signature)) + mail.seek(0) + + # Only log the email text (including all headers) if SAFE_LOGGING is + # disabled: + if not safelog.safe_logging: + logging.debug("Email contents:\n\n%s" % mail.read()) + mail.seek(0) + else: + logging.debug("Email text for %r created." % clientAddr) + + return clientAddr, mail +
class MailContext(object): """Helper object that holds information used by email subsystem."""