commit 4bc0ea61dc46596417035b3be01000bc321d97ee
Author: ilv <ilv(a)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(a)torproject.org
+ m = re.match('gettor\+(\w\w)(a)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))