commit 6b56b53257c2997386f2dfbeb7304556e84bc2c2 Author: ilv ilv@users.noreply.github.com Date: Wed Jul 2 00:44:14 2014 -0400
Added SMTP module skeleton. Simple stuff for now. --- src/smtp.py | 296 ++++++++++++++++++++++++++++++++++++++ src/smtp/log/all.log | 16 +++ src/smtp/log/debug.log | 14 ++ src/smtp/log/info.log | 3 + src/smtp/sample/sample-email.eml | 57 ++++++++ src/smtp_demo.py | 20 +++ 6 files changed, 406 insertions(+)
diff --git a/src/smtp.cfg b/src/smtp.cfg new file mode 100644 index 0000000..e69de29 diff --git a/src/smtp.py b/src/smtp.py new file mode 100644 index 0000000..3435412 --- /dev/null +++ b/src/smtp.py @@ -0,0 +1,296 @@ +import os +import re +import sys +import email +import logging +import ConfigParser + +import gettor + + +class SingleLevelFilter(logging.Filter): + """ + Filter logging levels to create separated logs. + + Public methods: + filter(record) + """ + + def __init__(self, passlevel, reject): + """ + Initialize a new object with level to be filtered. + + If reject value is false, all but the passlevel will be + filtered. Useful for logging in separated files. + """ + + self.passlevel = passlevel + self.reject = reject + + def filter(self, record): + """ + Do the actual filtering. + """ + if self.reject: + return (record.levelno != self.passlevel) + else: + return (record.levelno == self.passlevel) + + +class SMTP(object): + """ + Class for the GetTor's SMTP service. Provides an interface to + interact with requests received by email. + """ + + def __init__(self, config): + """ + Creates new object by reading a configuration file. + + Args: + + - config (string): the path of the file that will be used as + configuration + """ + logging.basicConfig(format='[%(levelname)s] %(asctime)s - %(message)s', + datefmt="%Y-%m-%d %H:%M:%S") + logger = logging.getLogger(__name__) + + self.delay = True + self.logdir = 'smtp/log/' + self.loglevel = 'DEBUG' + + # Better log format + string_format = '[%(levelname)7s] %(asctime)s - %(message)s' + formatter = logging.Formatter(string_format, '%Y-%m-%d %H:%M:%S') + + # Keep logs separated (and filtered) + # all.log depends on level specified on configuration file + all_log = logging.FileHandler(os.path.join(self.logdir, 'all.log'), + mode='a+') + all_log.setLevel(logging.getLevelName(self.loglevel)) + all_log.setFormatter(formatter) + + debug_log = logging.FileHandler(os.path.join(self.logdir, 'debug.log'), + mode='a+') + debug_log.setLevel('DEBUG') + debug_log.addFilter(SingleLevelFilter(logging.DEBUG, False)) + debug_log.setFormatter(formatter) + + info_log = logging.FileHandler(os.path.join(self.logdir, 'info.log'), + mode='a+') + info_log.setLevel('INFO') + info_log.addFilter(SingleLevelFilter(logging.INFO, False)) + info_log.setFormatter(formatter) + + warn_log = logging.FileHandler(os.path.join(self.logdir, 'warn.log'), + mode='a+') + warn_log.setLevel('WARNING') + warn_log.addFilter(SingleLevelFilter(logging.WARNING, False)) + warn_log.setFormatter(formatter) + + error_log = logging.FileHandler(os.path.join(self.logdir, 'error.log'), + mode='a+') + error_log.setLevel('ERROR') + error_log.addFilter(SingleLevelFilter(logging.ERROR, False)) + error_log.setFormatter(formatter) + + logger.addHandler(all_log) + logger.addHandler(info_log) + logger.addHandler(debug_log) + logger.addHandler(warn_log) + logger.addHandler(error_log) + + self.logger = logger + self.logger.setLevel(logging.getLevelName(self.loglevel)) + logger.debug('Redirecting logging to %s' % self.logdir) + + # Stop logging on stdout from now on + logger.propagate = False + self.logger.debug("New smtp object created") + + def _log_request(self): + """ + Logs a given request + + This should be called when something goes wrong. It saves the + email content that triggered the malfunctioning + + Raises: + + - RuntimeError: if something goes wrong while trying to save the + email + """ + self.logger.debug("Logging the mail content...") + + def _check_blacklist(self): + """ + Check if an email is blacklisted + + It opens the corresponding blacklist file and search for the + sender address. + + Raises: + + - BlacklistError: if the user is blacklisted. + """ + self.logger.debug("Checking if address %s is blacklisted" % + self.from_addr) + + def _get_locale(self): + """ + Get the locale from an email address + + It process the email received and look for the locale in the + recipient address (e.g. gettor+en@torproject.org) + + If no locale found, english by default + + Returns a string containing the locale + """ + self.logger.debug("Trying to obtain locale from recipient address") + + # If no match found, english by default + locale = 'en' + + # Look for word+locale@something + # Should we specify gettor and torproject? + m = re.match('\w++(\w\w)@', self.to_addr) + if m: + self.logger.debug("Request for locale %s" % m.groups()) + locale = "%s" % m.groups() + + return locale + + def _parse_email(self): + """ + Parses the email received + + It obtains the locale and parse the text for the rest of the info + + Returns a 4-tuple with locale, package, os and type + """ + self.logger.debug("Parsing email") + + locale = self._get_locale() + req_type, req_pkg, req_os = self._parse_text() + + request = {} + request['locale'] = locale + request['package'] = req_pkg + request['type'] = req_type + request['os'] = req_os + + return request + + def _parse_text(self): + """ + Parses the text part of the email received + + It tries to figure out what the user is asking, namely, the type + of request, the package and os required (if applies) + + Returns a 3-tuple with the type of request, package and os + """ + self.logger.debug("Parsing email text part") + + return ('links', 'pkg', 'linux') + + def _create_email(self, msg): + """ + Creates an email object + + This object will be used to construct the reply + + Returns the email object + """ + self.logger.debug("Creating email object for replying") + + def _send_email(self, msg): + """ + Send email with msg as content + """ + self._create_email(msg) + self.logger.debug("Email sent") + + def _send_delay(self): + """ + Send delay message + + If delay is setted on configuration, then sends a reply to the + user saying that the package is on the way + """ + self.logger.debug("Sending delay message...") + self._send_email("delay") + + def _send_links(self, links): + """ + Send the links to the user + """ + self.logger.debug("Sending links...") + self._send_email(links) + + def _send_help(self): + """ + Send help message to the user + """ + self.logger.debug("Sending help...") + self._send_email("help") + + def process_email(self, raw_msg): + """ + Process the email received. + + It create an email object from the string received. The processing + flow is as following: + - Check for blacklisted address + - Parse the email + - Check the type of request + - Send reply + + Raise: + - ValueError if the address is blacklisted, or if the request + asks for unsupported locales and/or operating systems, or if + it's not possible to recognize what type of request (help, links) + the user is asking + + - InternalError if something goes wrong while trying to obtain + the links from the Core + """ + self.parsed_msg = email.message_from_string(raw_msg) + # Just for easy access + # Normalize pending + self.from_addr = self.parsed_msg['From'] + self.to_addr = self.parsed_msg['To'] + + # We have the info we need on self.parsed_msg + try: + self._check_blacklist() + except ValueError as e: + raise ValueError("The address %s is blacklisted!" % + self.from_addr) + + # Try to figure out what the user is asking + request = self._parse_email() + + # Two possible options: asking for help or for the links + # If not, it means malformed message, and no default values + self.logger.info("New request for %s" % request['type']) + if request['type'] == 'help': + self._send_help(request['locale']) + elif request['type'] == 'links': + if self.delay: + self._send_delay() + + try: + self.logger.info("Asking Core for links in %s for %s" % + (request['locale'], request['os'])) + # links = self.core.get_links(request['os'], request['locale']) + links = "dummy links" + self._send_links(links) + except ValueError as e: + raise ValueError(str(e)) + except RuntimeError as e: + raise RuntimeError(str(e)) + else: + raise ValueError("Malformed message. No default values either") diff --git a/src/smtp/log/all.log b/src/smtp/log/all.log new file mode 100644 index 0000000..21e5f3b --- /dev/null +++ b/src/smtp/log/all.log @@ -0,0 +1,16 @@ + +[ DEBUG] 2014-07-01 19:32:28 - Redirecting logging to smtp/log/ +[ DEBUG] 2014-07-01 19:32:28 - New smtp object created +[ DEBUG] 2014-07-01 19:32:28 - Checking if address "Jacob Applebaum" ioerror@gmail.com is blacklisted +[ DEBUG] 2014-07-01 19:32:28 - Parsing email +[ DEBUG] 2014-07-01 19:32:28 - Trying to obtain locale from recipient address +[ DEBUG] 2014-07-01 19:32:28 - Request for locale en +[ DEBUG] 2014-07-01 19:32:28 - Parsing email text part +[ INFO] 2014-07-01 19:32:28 - New request for links +[ DEBUG] 2014-07-01 19:32:28 - Sending delay message... +[ DEBUG] 2014-07-01 19:32:28 - Creating email object for replying +[ DEBUG] 2014-07-01 19:32:28 - Email sent +[ INFO] 2014-07-01 19:32:28 - Asking Core for links in en for linux +[ DEBUG] 2014-07-01 19:32:28 - Sending links... +[ DEBUG] 2014-07-01 19:32:28 - Creating email object for replying +[ DEBUG] 2014-07-01 19:32:28 - Email sent diff --git a/src/smtp/log/debug.log b/src/smtp/log/debug.log new file mode 100644 index 0000000..bef7c7a --- /dev/null +++ b/src/smtp/log/debug.log @@ -0,0 +1,14 @@ + +[ DEBUG] 2014-07-01 19:32:28 - Redirecting logging to smtp/log/ +[ DEBUG] 2014-07-01 19:32:28 - New smtp object created +[ DEBUG] 2014-07-01 19:32:28 - Checking if address "Jacob Applebaum" ioerror@gmail.com is blacklisted +[ DEBUG] 2014-07-01 19:32:28 - Parsing email +[ DEBUG] 2014-07-01 19:32:28 - Trying to obtain locale from recipient address +[ DEBUG] 2014-07-01 19:32:28 - Request for locale en +[ DEBUG] 2014-07-01 19:32:28 - Parsing email text part +[ DEBUG] 2014-07-01 19:32:28 - Sending delay message... +[ DEBUG] 2014-07-01 19:32:28 - Creating email object for replying +[ DEBUG] 2014-07-01 19:32:28 - Email sent +[ DEBUG] 2014-07-01 19:32:28 - Sending links... +[ DEBUG] 2014-07-01 19:32:28 - Creating email object for replying +[ DEBUG] 2014-07-01 19:32:28 - Email sent diff --git a/src/smtp/log/error.log b/src/smtp/log/error.log new file mode 100644 index 0000000..e69de29 diff --git a/src/smtp/log/info.log b/src/smtp/log/info.log new file mode 100644 index 0000000..66d40f1 --- /dev/null +++ b/src/smtp/log/info.log @@ -0,0 +1,3 @@ + +[ INFO] 2014-07-01 19:32:28 - New request for links +[ INFO] 2014-07-01 19:32:28 - Asking Core for links in en for linux diff --git a/src/smtp/log/warn.log b/src/smtp/log/warn.log new file mode 100644 index 0000000..e69de29 diff --git a/src/smtp/sample/sample-email.eml b/src/smtp/sample/sample-email.eml new file mode 100644 index 0000000..1f13b08 --- /dev/null +++ b/src/smtp/sample/sample-email.eml @@ -0,0 +1,57 @@ +X-Account-Key: account6 +X-UIDL: 1214981061.25808.faustus,S=2285 +X-Mozilla-Status: 0001 +X-Mozilla-Status2: 02000000 +Return-Path: ioerror@gmail.com +Delivered-To: jpopped@appelbaum.net +Received: (qmail 25806 invoked by uid 89); 2 Jul 2008 06:44:21 -0000 +Delivered-To: appelbaum.net-jacob@appelbaum.net +Received: (qmail 25804 invoked by uid 89); 2 Jul 2008 06:44:21 -0000 +Received: from unknown (HELO wa-out-1112.google.com) (209.85.146.180) + by 0 with SMTP; 2 Jul 2008 06:44:21 -0000 +Received-SPF: pass (0: SPF record at _spf.google.com designates 209.85.146.180 as permitted sender) +Received: by wa-out-1112.google.com with SMTP id j40so170432wah.1 + for jacob@appelbaum.net; Tue, 01 Jul 2008 23:42:01 -0700 (PDT) +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=gmail.com; s=gamma; + h=domainkey-signature:received:received:message-id:date:from:to + :subject:mime-version:content-type; + bh=IvFqNkffeoST7vamh2ytuq/b7GpLhg2hStTrQq3I3rE=; + b=xQR0hE/J4AXpAqH1UDXTtDrU9Izc6WM8vtFudRBzldWYyRx3Vvfh2I2Opu8+O6wbAv + jlDi18anUMbZqlIGSgGOxvXW4CltpX/SFZm1aGL4AisQ1Bi5xEqlrc8OnX3sA2xKeM3g + KWsWm+GVSMI4XAqnY9FYAfPx4DmOAfkdMyWCU= +DomainKey-Signature: a=rsa-sha1; c=nofws; + d=gmail.com; s=gamma; + h=message-id:date:from:to:subject:mime-version:content-type; + b=kyzDtGRDbiC5y4Bz/ylQjyHOChiOP2A6QDzybsVXc0C1hjHLImOQYR8gOxcRY+mRkN + 1xpBaEF4UloZAxTb79khRRp4TWmjT1DagtLx2MFzIj/F6awtdE/9U3p4QyKr8S43tGcE + ET26BSfT5u9zrXblVVAP3JedMPZ8mlIGQxyDs= +Received: by 10.115.90.1 with SMTP id s1mr6711509wal.51.1214980921268; + Tue, 01 Jul 2008 23:42:01 -0700 (PDT) +Received: by 10.114.184.16 with HTTP; Tue, 1 Jul 2008 23:41:57 -0700 (PDT) +Message-ID: 7fadd8130807012341n3b3af401mbdb4a29c80310bd3@mail.gmail.com +Date: Tue, 1 Jul 2008 23:41:57 -0700 +From: "Jacob Applebaum" ioerror@gmail.com +To: gettor+en@torproject.org +Subject: Positive DKIM header +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="----=_Part_462_28562233.1214980917793" + +------=_Part_462_28562233.1214980917793 +Content-Type: text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +This email should have a positive DKIM header. + +------=_Part_462_28562233.1214980917793 +Content-Type: text/html; charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +This email should have a positive DKIM header.<br> + +------=_Part_462_28562233.1214980917793-- + + diff --git a/src/smtp_demo.py b/src/smtp_demo.py new file mode 100644 index 0000000..390377c --- /dev/null +++ b/src/smtp_demo.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +import sys + +import smtp + +service = smtp.SMTP('smtp.cfg') + +# For now we simulate mails reading from stdin +# In linux test as follows: +# $ python smtp_demo.py < email.eml + +incoming = sys.stdin.read() +try: + print "Email received!" + service.process_email(incoming) + print "Email sent!" +except ValueError as e: + print "Value error: " + str(e) +except RuntimeError as e: + print "Runtime error: " + str(e)
tor-commits@lists.torproject.org