--- a/ForgeMail/forgemail/app/command.py
+++ b/ForgeMail/forgemail/app/command.py
@@ -1,44 +1,109 @@
-import os
-import sys
-import time
-import json
-import logging
-from pkg_resources import iter_entry_points
-from multiprocessing import Process
+import smtpd
+import asyncore
+import email.feedparser
from pprint import pformat
-import ming
+import tg
import pylons
from paste.script import command
-from paste.deploy import appconfig
-from carrot.connection import BrokerConnection
-from carrot.messaging import Consumer, ConsumerSet
-from pyforge.config.environment import load_environment
-from pyforge.command import Command
+import pyforge.command
+from pyforge.lib.helpers import find_project
+from pyforge.command import base
-log=None
-M=None
+M = None
-class Command(command.Command):
- min_args = max_args = 1
+class SMTPServerCommand(pyforge.command.Command):
+ min_args=1
+ max_args=1
usage = 'NAME <ini file>'
- group_name = 'PyForge'
+ summary = 'Handle incoming emails, routing them to RabbitMQ'
+ parser = command.Command.standard_parser(verbose=True)
+ parser.add_option('-c', '--context', dest='context',
+ help=('The context of the message (path to the project'
+ ' and/or plugin'))
- def basic_setup(self):
- global log, M
- conf = appconfig('config:%s' % self.args[0],relative_to=os.getcwd())
- logging.config.fileConfig(self.args[0])
+ def command(self):
+ global M
+ self.basic_setup()
from pyforge import model
- M=model
- log = logging.getLogger(__name__)
- log.info('Initialize reactor with config %r', self.args[0])
- load_environment(conf.global_conf, conf.local_conf)
- pylons.c._push_object(EmptyClass())
- from pyforge.lib.app_globals import Globals
- pylons.g._push_object(Globals())
- ming.configure(**conf)
- self.plugins = [
- (ep.name, ep.load()) for ep in iter_entry_points('pyforge') ]
- log.info('Loaded plugins')
+ M = model
+ server = MailServer((tg.config.get('forgemail.host', '0.0.0.0'),
+ tg.config.get('forgemail.port', 8825)),
+ None)
+ asyncore.loop()
+class MailServer(smtpd.SMTPServer):
+
+ def process_message(self, peer, mailfrom, rcpttos, data):
+ '''Route messages according to their destination:
+
+ <topic>@<mount_point>.<subproj2>.<subproj1>.<project>.projects.sourceforge.net
+ goes to the audit with routing ID
+ <plugin name>.<topic>
+ '''
+ base.log.info('Msg Received from %s for %s', mailfrom, rcpttos)
+ base.log.info('%s', data)
+ common_suffix = tg.config.get('forgemail.domain', '.sourceforge.net')
+ parsed_message = parse_message(data)
+ sending_user = identify_sender(peer, mailfrom, parsed_message)
+ base.log.info('Sender: %s', sending_user)
+ base.log.info('Message headers:\n%s', pformat(parsed_message['headers']))
+ if parsed_message['multipart']:
+ for part in parsed_message['parts']:
+ base.log.info('Message part:\n%s', part['payload'])
+ else:
+ base.log.info('Message payload:\n%s', parsed_message['payload'])
+ for addr in rcpttos:
+ try:
+ user, domain = addr.split('@')
+ # remove common domain suffix
+ if not domain.endswith(common_suffix):
+ base.log.warning(
+ 'Unknown domain, dropping message: %s', domain)
+ continue
+ domain = domain[:-len(common_suffix)]
+ path = list(reversed(domain.split('.')))
+ project, mount_point = find_project(path)
+ if project is None:
+ base.log.warning('Unknown project at %s', domain)
+ continue
+ if len(mount_point) != 1:
+ base.log.warning('Unknown plugin at %s', domain)
+ continue
+ pylons.c.project = project
+ pylons.c.app = app = project.app_instance(mount_point[0])
+ topic = '%s.%s' % (app.config.plugin_name, user)
+ pylons.g.publish('audit', topic, dict(parsed_message,
+ user_id=str(sending_user._id)),
+ serializer='yaml')
+ except:
+ base.log.exception('Error handling mail to %s', addr)
+
+def parse_message(data):
+ # Parse the email to its constituent parts
+ parser = email.feedparser.FeedParser()
+ parser.feed(data)
+ msg = parser.close()
+ # Extract relevant data
+ result = {}
+ result['multipart'] = multipart = msg.is_multipart()
+ result['headers'] = dict(msg)
+ if multipart:
+ result['parts'] = [
+ dict(headers=dict(subpart),
+ payload=subpart.get_payload())
+ for subpart in msg.walk() ]
+ else:
+ result['payload'] = msg.get_payload()
+ return result
+
+def identify_sender(peer, email_address, msg):
+ base.log.info('Trying ID sender for addr %s', email_address)
+ # Dumb ID -- just look for email address claimed by a particular user
+ addr = M.EmailAddress.query.get(_id=M.EmailAddress.canonical(email_address))
+ if addr and addr.claimed_by_user_id:
+ return addr.claimed_by_user()
+ # TODO: look at the From: header, maybe?
+ return None
+