[tor-commits] [gettor/master] SMTP module. It recognizes requests in different languages and requests for help/links. Send three type of emails: delay, help, links. Still various error check pending

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


commit 4bc0ea61dc46596417035b3be01000bc321d97ee
Author: ilv <ilv at users.noreply.github.com>
Date:   Fri Jul 18 23:21:46 2014 -0400

    SMTP module. It recognizes requests in different languages and requests for help/links. Send three type of emails: delay, help, links. Still various error check pending
---
 src/smtp.py |  310 +++++++++++++++++++++++++++++++++++++++++++++++++----------
 1 file changed, 259 insertions(+), 51 deletions(-)

diff --git a/src/smtp.py b/src/smtp.py
index 3435412..e29aa2d 100644
--- a/src/smtp.py
+++ b/src/smtp.py
@@ -1,7 +1,10 @@
 import os
 import re
 import sys
+import time
 import email
+import gettext
+import hashlib
 import logging
 import ConfigParser
 
@@ -29,7 +32,7 @@ class SingleLevelFilter(logging.Filter):
 
     def filter(self, record):
         """
-            Do the actual filtering.
+        Do the actual filtering.
         """
         if self.reject:
             return (record.levelno != self.passlevel)
@@ -43,9 +46,9 @@ class SMTP(object):
     interact with requests received by email.
     """
 
-    def __init__(self, config):
+    def __init__(self, config_file):
     	"""
-        Creates new object by reading a configuration file.
+        Create new object by reading a configuration file.
 
         Args:
 
@@ -55,10 +58,61 @@ class SMTP(object):
         logging.basicConfig(format='[%(levelname)s] %(asctime)s - %(message)s',
                             datefmt="%Y-%m-%d %H:%M:%S")
         logger = logging.getLogger(__name__)
+        config = ConfigParser.ConfigParser()
 
-        self.delay = True
-        self.logdir = 'smtp/log/'
-        self.loglevel = 'DEBUG'
+        if os.path.isfile(config_file):
+            logger.info("Reading configuration from %s" % config_file)
+            config.read(config_file)
+        else:
+            logger.error("Error while trying to read %s" % config_file)
+            raise RuntimeError("Couldn't read the configuration file %s"
+                               % config_file)
+
+        # Handle the gets internally to catch proper exceptions
+        try:
+            self.basedir = self._get_config_option('general',
+                                                   'basedir', config)
+        except RuntimeError as e:
+            logger.warning("%s misconfigured. %s" % (config_file, str(e)))
+
+        try:
+            self.delay = self._get_config_option('general',
+                                                 'delay', config)
+            # There has to be a better way for this...
+            if self.delay == 'False':
+                self.delay = False
+
+        except RuntimeError as e:
+            logger.warning("%s misconfigured. %s" % (config_file, str(e)))
+
+        try:
+            self.our_addr = self._get_config_option('general',
+                                                    'our_addr', config)
+        except RuntimeError as e:
+            logger.warning("%s misconfigured. %s" % (config_file, str(e)))
+
+        try:
+            self.logdir = self._get_config_option('log',
+                                                  'dir', config)
+            self.logdir = os.path.join(self.basedir, self.logdir)
+        except RuntimeError as e:
+            logger.warning("%s misconfigured. %s" % (config_file, str(e)))
+
+        try:
+            self.logdir_emails = self._get_config_option('log',
+                                                         'emails_dir',
+                                                         config)
+            self.logdir_emails = os.path.join(self.logdir, self.logdir_emails)
+        except RuntimeError as e:
+            logger.warning("%s misconfigured. %s" % (config_file, str(e)))
+
+        try:
+            self.loglevel = self._get_config_option('log',
+                                                    'level', config)
+        except RuntimeError as e:
+            logger.warning("%s misconfigured. %s" % (config_file, str(e)))
+
+        self.core = gettor.Core('gettor.cfg')
 
         # Better log format
         string_format = '[%(levelname)7s] %(asctime)s - %(message)s'
@@ -109,9 +163,19 @@ class SMTP(object):
         logger.propagate = False
         self.logger.debug("New smtp object created")
 
-    def _log_request(self):
+    def _get_sha1(self, string):
+        """
+        Get the sha1 of a string
+
+        Used whenever we want to do things with addresses (log, blacklist, etc)
+
+        Returns a string
         """
-        Logs a given request
+        return str(hashlib.sha1(string).hexdigest())
+
+    def _log_request(self, addr, content):
+        """
+        Log a given request
 
         This should be called when something goes wrong. It saves the
         email content that triggered the malfunctioning
@@ -121,9 +185,22 @@ class SMTP(object):
         - RuntimeError: if something goes wrong while trying to save the
                         email
         """
-        self.logger.debug("Logging the mail content...")
-
-    def _check_blacklist(self):
+        # We don't store the original address, but rather its sha1 digest
+        # in order to know when some specific addresses are doing weird
+        # requests
+        log_addr = self._get_sha1(addr)
+        filename = str(time.time()) + '.log'
+        path = self.logdir_emails + filename
+        abs_path = os.path.abspath(path)
+
+        log_file = open(abs_path, 'w+')
+        log_file.write(content)
+        log_file.close()
+
+        self.logger.debug("Logging request from %s in %s"
+                          % (log_addr, abs_path))
+
+    def _check_blacklist(self, addr):
         """
         Check if an email is blacklisted
 
@@ -134,8 +211,9 @@ class SMTP(object):
 
         - BlacklistError: if the user is blacklisted.
         """
+        anon_addr = self._get_sha1(addr)
         self.logger.debug("Checking if address %s is blacklisted" %
-                          self.from_addr)
+                          anon_addr)
 
     def _get_locale(self):
         """
@@ -153,102 +231,195 @@ class SMTP(object):
         # 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)
+        # Look for gettor+locale at torproject.org
+        m = re.match('gettor\+(\w\w)@torproject\.org', self.to_addr)
         if m:
             self.logger.debug("Request for locale %s" % m.groups())
             locale = "%s" % m.groups()
 
         return locale
 
+    def _get_normalized_address(self, addr):
+        """
+        Get normalized address
+
+        It looks for anything inside the last '<' and '>'. Code taken
+        from the old GetTor (utils.py)
+
+        On success, returns the normalized address
+        On failure, returns ValueError
+        """
+        if '<' in addr:
+            idx = addr.rindex('<')
+            addr = addr[idx:]
+            m = re.search(r'<([^>]*)>', addr)
+            if m is None:
+                raise ValueError("Couldn't extract normalized address from %s"
+                                 % addr)
+            addr = m.group(1)
+        return addr
+
     def _parse_email(self):
         """
-        Parses the email received
+        Parse 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
+        Returns a 3-tuple with locale, os and type
         """
         self.logger.debug("Parsing email")
 
         locale = self._get_locale()
-        req_type, req_pkg, req_os = self._parse_text()
-
-        request = {}
+        request = self._parse_text()
         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
+        Parse 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
+        Returns a tuple with the type of request and os (None if request
+        is for help)
         """
         self.logger.debug("Parsing email text part")
 
-        return ('links', 'pkg', 'linux')
+        # By default we asume the request is asking for links
+        request = {}
+        request['type'] = 'links'
+        request['os'] = None
+
+        # The core knows what OS are supported
+        supported_os = self.core.get_supported_os()
+
+        lines = self.raw_msg.split('\n')
+        found_os = False
+        for line in lines:
+            # Check for help request
+            if re.match('.*help.*', line, re.IGNORECASE):
+                request['type'] = 'help'
+                break
+            # Check for os
+            for supported in supported_os:
+                p = '.*' + supported + '.*'
+                if re.match(p, line, re.IGNORECASE):
+                    request['os'] = supported
+                    found_os = True
+            if found_os:
+                break
+
+            if request['type'] == 'links' and not request['os']:
+                # Windows by default?
+                request['os'] = 'windows'
+
+        return request
 
-    def _create_email(self, msg):
+    def _create_email(self, from_addr, to_addr, subject, msg):
         """
-        Creates an email object
+        Create an email object
 
-        This object will be used to construct the reply
+        This object will be used to construct the reply. Comment lines
+        331-334, 339, and uncomment lines 336, 337, 340 to test it
+        without having an SMTP server
 
         Returns the email object
         """
         self.logger.debug("Creating email object for replying")
+        # email_obj = MIMEtext(msg)
+        # email_obj['Subject'] = subject
+        # email_obj['From'] = from_addr
+        # email_obj['To'] = to_addr
 
-    def _send_email(self, msg):
+        reply = "From: " + from_addr + ", To: " + to_addr
+        reply = reply + ", Subject: " + subject + "\n\n" + msg
+
+        # return email_obj
+        return reply
+
+    def _send_email(self, from_addr, to_addr, subject, msg):
         """
-        Send email with msg as content
+        Send an email
+
+        It takes a from and to addresses, a subject and the content, creates
+        an email and send it. Comment lines 350-352 and uncomment line 353
+        to test it without having an SMTP server
         """
-        self._create_email(msg)
+        email_obj = self._create_email(from_addr, to_addr, subject, msg)
+        # s = smtplib.SMTP("localhost")
+        # s.sendmail(from_addr, to_addr, msg.as_string())
+        # s.quit()
+        print email_obj
         self.logger.debug("Email sent")
 
-    def _send_delay(self):
+    def _send_delay(self, locale, from_addr, to_addr):
         """
         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")
+        self.logger.debug("Delay is setted. Sending a delay message.")
 
-    def _send_links(self, links):
+        # Obtain the content in the proper language and send it
+        t = gettext.translation(locale, './i18n', languages=[locale])
+        _ = t.ugettext
+
+        delay_msg = _('delay_msg')
+        delay_subject = _('delay_subject')
+        self._send_email(from_addr, to_addr, delay_subject, delay_msg)
+
+    def _send_links(self, links, locale, from_addr, to_addr):
         """
         Send the links to the user
+
+        It gets the message in the proper language (according to the
+        locale), replace variables in that message and call to send the
+        email
         """
-        self.logger.debug("Sending links...")
-        self._send_email(links)
+        self.logger.debug("Request for links in %s" % locale)
 
-    def _send_help(self):
+        # Obtain the content in the proper language and send it
+        t = gettext.translation(locale, './i18n', languages=[locale])
+        _ = t.ugettext
+
+        links_msg = _('links_msg')
+        links_subject = _('links_subject')
+        links_msg = links_msg % ('linux', locale, links, links)
+        self._send_email(from_addr, to_addr, links_subject, links_msg)
+
+    def _send_help(self, locale, from_addr, to_addr):
         """
         Send help message to the user
+
+        It gets the message in the proper language (according to the
+        locale), replace variables in that message (if any) and call to send
+        the email
         """
-        self.logger.debug("Sending help...")
-        self._send_email("help")
+        self.logger.debug("Request for help in %s" % locale)
+
+        # Obtain the content in the proper language and send it
+        t = gettext.translation(locale, './i18n', languages=[locale])
+        _ = t.ugettext
+
+        help_msg = _('help_msg')
+        help_subject = _('help_subject')
+        self._send_email(from_addr, to_addr, help_subject, help_msg)
 
     def process_email(self, raw_msg):
         """
         Process the email received.
 
-        It create an email object from the string received. The processing
+        It creates 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:
+        Raises:
             - 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)
@@ -257,18 +428,19 @@ class SMTP(object):
             - InternalError if something goes wrong while trying to obtain
             the links from the Core
         """
+        self.raw_msg = raw_msg
         self.parsed_msg = email.message_from_string(raw_msg)
         # Just for easy access
-        # Normalize pending
         self.from_addr = self.parsed_msg['From']
+        self.norm_from_addr = self._get_normalized_address(self.from_addr)
         self.to_addr = self.parsed_msg['To']
 
         # We have the info we need on self.parsed_msg
         try:
-            self._check_blacklist()
+            self._check_blacklist(self._get_sha1(self.from_addr))
         except ValueError as e:
             raise ValueError("The address %s is blacklisted!" %
-                             self.from_addr)
+                             self._get_sha1(self.from_addr))
 
         # Try to figure out what the user is asking
         request = self._parse_email()
@@ -277,20 +449,56 @@ class SMTP(object):
         # 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'])
+            self._send_help(request['locale'], self.our_addr,
+                            self.norm_from_addr)
         elif request['type'] == 'links':
             if self.delay:
-                self._send_delay()
+                self._send_delay(request['locale'], self.our_addr,
+                                 self.norm_from_addr)
 
             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)
+
+                links = self.core.get_links('SMTP', request['os'],
+                                            request['locale'])
+
+                self._send_links(links, request['locale'], self.our_addr,
+                                 self.norm_from_addr)
             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")
+
+    def _get_config_option(self, section, option, config):
+        """
+            Private method to get configuration options.
+
+            It tries to obtain a value from a section in config using
+            ConfigParser. It catches possible exceptions and raises
+            RuntimeError if something goes wrong.
+
+            Arguments:
+                config: ConfigParser object
+                section: section inside config
+                option: option inside section
+
+            Returns the value of the option inside the section in the
+            config object.
+        """
+
+        try:
+            value = config.get(section, option)
+            return value
+        # This exceptions should appear when messing with the configuration
+        except (ConfigParser.NoSectionError,
+                ConfigParser.NoOptionError,
+                ConfigParser.InterpolationError,
+                ConfigParser.MissingSectionHeaderError,
+                ConfigParser.ParsingError) as e:
+            raise RuntimeError("%s" % str(e))
+        # No other errors should occurr, unless something's terribly wrong
+        except ConfigParser.Error as e:
+            raise RuntimeError("Unexpected error: %s" % str(e))





More information about the tor-commits mailing list