commit 4bc0ea61dc46596417035b3be01000bc321d97ee Author: ilv ilv@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@something - # Should we specify gettor and torproject? - m = re.match('\w++(\w\w)@', self.to_addr) + # Look for gettor+locale@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))