Commit 4bc0ea61 authored by ilv's avatar ilv
Browse files

SMTP module. It recognizes requests in different languages and requests for...

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
parent f42ca8d4
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))
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment