[tor-commits] [gettor/master] Added SMTP module skeleton. Simple stuff for now.

ilv at torproject.org ilv at torproject.org
Tue Sep 22 23:39:11 UTC 2015


commit 6b56b53257c2997386f2dfbeb7304556e84bc2c2
Author: ilv <ilv at 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 at 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 at 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 at 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 at 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 at gmail.com>
+Delivered-To: jpopped at appelbaum.net
+Received: (qmail 25806 invoked by uid 89); 2 Jul 2008 06:44:21 -0000
+Delivered-To: appelbaum.net-jacob at 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 at 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 at mail.gmail.com>
+Date: Tue, 1 Jul 2008 23:41:57 -0700
+From: "Jacob Applebaum" <ioerror at gmail.com>
+To: gettor+en at 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)





More information about the tor-commits mailing list