Commits

Anonymous committed 2439e61

initial commit

Comments (0)

Files changed (9)

+syntax: glob
+*.pyc
+
+
+import asyncore
+
+
+from pymta import PythonMTA, DefaultMTAPolicy
+
+
+if __name__ == '__main__':
+    server = PythonMTA(('localhost', 8025), (None, None), DefaultMTAPolicy)
+    try:
+        asyncore.loop()
+    except KeyboardInterrupt:
+        pass
+
+

pymta/__init__.py

+# -*- coding: UTF-8 -*-
+
+from pymta.default_policy import *
+from pymta.mta import *
+from pymta.processor import *
+from pymta.smtp_session import *
+
+
+# -*- coding: UTF-8 -*-
+
+

pymta/default_policy.py

+# -*- coding: UTF-8 -*-
+
+__all__ = ['DefaultMTAPolicy']
+
+
+class DefaultMTAPolicy(object):
+    """This is the default policy which just accepts everything."""
+    
+    def accept_new_connection(self, remote_ip_string, remote_port):
+        return True
+
+
+
+# -*- coding: UTF-8 -*-
+
+import socket
+
+from pymta.smtpd import SMTPServer
+from pymta.smtp_session import SMTPSession
+
+__all__ = ['PythonMTA']
+
+
+class PythonMTA(SMTPServer):
+    version='0.1'
+
+    def __init__(self, localaddr, remoteaddr, policy_class):
+        SMTPServer.__init__(self, localaddr, remoteaddr)
+        self._policy_class = policy_class
+        self._primary_hostname = socket.getfqdn()
+    
+    
+    def handle_accept(self):
+        connection, remote_ip_and_port = self.accept()
+        remote_ip_string, port = remote_ip_and_port
+        policy = self._policy_class()
+        SMTPSession(self, connection, remote_ip_and_port, policy)
+
+    
+    def primary_hostname(self):
+        return self._primary_hostname
+    primary_hostname = property(primary_hostname)
+    
+    
+    # Do something with the gathered message
+    # TODO: Rewrite!
+    def process_message(self, peer, mailfrom, rcpttos, data):
+        inheaders = True
+        lines = data.split('\n')
+        print '---------- MESSAGE FOLLOWS ----------'
+        for line in lines:
+            # headers first
+            if inheaders and not line:
+                print 'X-Peer:', peer[0]
+                inheaders = False
+            print line
+        print '------------ END MESSAGE ------------'
+
+

pymta/processor.py

+# -*- coding: UTF-8 -*-
+
+from repoze.workflow.statemachine import StateMachine, StateMachineError
+
+__all__ = ['SMTPProcessor']
+
+
+class SMTPProcessor(object):
+    """The SMTPProcessor processes all input data which were extracted from 
+    sockets previously. The idea behind is that this class is decoupled from 
+    asynchat as much as possible and make it really testable."""
+    
+    def __init__(self, session, policy=None):
+        self._session = session
+        self._policy = policy
+        
+        self._command_arguments = None
+        self.remote_ip_string = None
+        self.remote_port = None
+        self._build_state_machine()
+        
+    
+    # -------------------------------------------------------------------------
+    
+    def _add_state(self, from_state, smtp_command, to_state):
+        handler_function = self._dispatch_commands
+        self.state.add(from_state, smtp_command, to_state, handler_function)
+    
+    
+    def _build_state_machine(self):
+        # This will implicitely declare an instance variable '_state' with the
+        # initial state
+        self.state = StateMachine('_state', initial_state='new')
+        self._add_state('new',     'GREET', 'greeted')
+        self._add_state('greeted', 'HELO',  'identify')
+        self._add_state('greeted', 'EHLO',  'identify')
+        self._add_state('greeted', 'QUIT',  'finished')
+    
+    
+    def _dispatch_commands(self, from_state, to_state, smtp_command, ob):
+        """This method dispatches a SMTP command to the appropriate handler 
+        method. It is called after a new command was received and a valid 
+        transition was found."""
+        print from_state, ' -> ', to_state, ':', smtp_command
+        name_handler_method = 'smtp_%s' % smtp_command.lower()
+        try:
+            handler_method = getattr(self, name_handler_method)
+        except AttributeError:
+            base_msg = 'No handler for %s though transition is defined (no method %s)'
+            print base_msg % (smtp_command, name_handler_method)
+            self.reply(451, 'Temporary Local Problem: Please come back later')
+        else:
+            handler_method()
+    
+    # -------------------------------------------------------------------------
+    
+    def new_connection(self, remote_ip, remote_port):
+        """This method is called when a new SMTP session is opened.
+        [PUBLIC API]
+        """
+        self.remote_ip_string = remote_ip
+        self.remote_port = remote_port
+        self._state = 'new'
+        
+        if (self._policy != None) and \
+            (not self._policy.accept_new_connection(self.remote_ip_string, self.remote_port)):
+            self.reply(554, 'SMTP service not available')
+            self.close_connection()
+        else:
+            self.handle_input('greet')
+    
+    
+    def handle_input(self, smtp_command, data=None):
+        """Processes the given SMTP command with the (optional data).
+        [PUBLIC API]
+        """
+        self._command_arguments = data
+        command = smtp_command.upper()
+        try:
+            # SMTP commands must be treated as case-insensitive
+            self.state.execute(self, command)
+        except StateMachineError:
+            base_msg = 'Command "%s" is not allowed here, expected on of %s'
+            msg = base_msg % (command, self.state.transitions(self))
+            print msg
+            self.reply(502, 'Error: %s not allowed here' % smtp_command)
+        self._command_arguments = None
+    
+    
+    def reply(self, code, text):
+        """This method returns a message to the client (actually the session 
+        object is responsible of actually pushing the bits)."""
+        self._session.push('%s %s' % (code, text))
+    
+    
+    def close_connection(self):
+        "Request a connection close from the SMTP session handling instance."
+        self._session.close_when_done()
+        self.remote_ip_string = None
+        self.remote_port = None
+    
+    
+    # -------------------------------------------------------------------------
+    # Protocol handling functions (not public)
+    
+    def smtp_greet(self):
+        """This method handles not a real smtp command. It is called when a new
+        connection was accepted by the server."""
+        self.reply(220, '%s %s' % (self._fqdn, self._server.version))
+
+

pymta/smtp_session.py

+# -*- coding: UTF-8 -*-
+
+import asynchat
+
+from pymta.processor import SMTPProcessor
+
+__all__ = ['SMTPSession']
+
+class SMTPSession(asynchat.async_chat):
+    """This class handles only the actual communication with the client. As soon
+    as a complete command is received, this class will hand everything over to
+    the SMTPProcessor.
+    
+    In the original 'SMTPChannel' class from Python.org this class handled 
+    all communication with asynchat, implemented a extremly simple state machine
+    and processed the data. Implementing hooks in that design (or adding 
+    fine-grained policies) was not possible at all with the previous design."""
+    LINE_TERMINATOR = '\r\n'
+
+    def __init__(self, server, connection, remote_ip_and_port, policy):
+        self.COMMAND = 0
+        self.DATA = 1
+        asynchat.async_chat.__init__(self, connection)
+        self.set_terminator(self.LINE_TERMINATOR)
+        
+        self._server = server
+        
+        self._connection = connection
+        
+        self._peer = connection.getpeername()
+        
+        self.processor = SMTPProcessor(session=self, policy=policy)
+        remote_ip_string, remote_port = remote_ip_and_port
+        self.processor.new_connection(remote_ip_string, remote_port)
+        
+        self._line = []
+        self._old_state = self.COMMAND
+        self._greeting = 0
+        self._mailfrom = None
+        self._rcpttos = []
+        self._data = ''
+    
+    
+    def primary_hostname(self):
+        return self._server.primary_hostname
+    _fqdn = property(primary_hostname)
+    
+    
+    # -------------------------------------------------------------------------
+    # Communication helper methods
+    
+    def push(self, code, msg=None):
+        "Send a message to the peer (using the correct SMTP line terminators."
+        if msg == None:
+            msg = code
+        else:
+            msg = "%s %s" % (str(code), msg)
+        
+        if not msg.endswith(self.LINE_TERMINATOR):
+            msg += self.LINE_TERMINATOR
+        asynchat.async_chat.push(self, msg)
+    
+    # Implementation of base class abstract method
+    # TODO: Rewrite!
+    def collect_incoming_data(self, data):
+        print 'collect_incoming_data', data
+        self._line.append(data)
+
+    # Implementation of base class abstract method
+    # TODO: Rewrite!
+    def found_terminator(self):
+        line = ''.join(self._line)
+        print 'Data:', repr(line)
+        self._line = []
+        if self._old_state == self.COMMAND:
+            if not line:
+                self.push('500 Error: bad syntax')
+                return
+            method = None
+            i = line.find(' ')
+            if i < 0:
+                command = line
+                arg = None
+            else:
+                command = line[:i]
+                arg = line[i+1:].strip()
+            print 'command is ', command
+            
+            self.processor.handle_input(command, arg)
+            return
+        else:
+            if self._old_state != self.DATA:
+                self.push('451 Internal confusion')
+                return
+            # Remove extraneous carriage returns and de-transparency according
+            # to RFC 821, Section 4.5.2.
+            data = []
+            for text in line.split('\r\n'):
+                if text and text[0] == '.':
+                    data.append(text[1:])
+                else:
+                    data.append(text)
+            self._data = '\n'.join(data)
+            status = self._server.process_message(self._peer,
+                                                   self._mailfrom,
+                                                   self._rcpttos,
+                                                   self._data)
+            self._rcpttos = []
+            self._mailfrom = None
+            self._old_state = self.COMMAND
+            self.set_terminator('\r\n')
+            if not status:
+                self.push('250 Ok')
+            else:
+                self.push(status)
+    
+    # TODO: Rewrite!
+    # factored
+    def __getaddr(self, keyword, arg):
+        address = None
+        keylen = len(keyword)
+        if arg[:keylen].upper() == keyword:
+            address = arg[keylen:].strip()
+            if not address:
+                pass
+            elif address[0] == '<' and address[-1] == '>' and address != '<>':
+                # Addresses can be in the form <person@dom.com> but watch out
+                # for null address, e.g. <>
+                address = address[1:-1]
+        return address
+
+    # -------------------------------------------------------------------------
+    # Internal methods for sending data to the client (easy subclassing with
+    # different behavior)
+
+    def smtp_helo(self):
+        if self.command_arguments in [None, '']:
+            self.push('501 Syntax: HELO hostname')
+        else:
+            self._greeting = self.command_arguments
+            self.push('250 %s' % self._fqdn)        
+
+    # -------------------------------------------------------------------------
+    # Methods that call policy checks
+    
+
+    # SMTP and ESMTP commands
+    def smtp_HELO(self, arg):
+        print 'helo', repr(self._greeting)
+        if not arg:
+            self.push('501 Syntax: HELO hostname')
+            return
+        if self._greeting:
+            self.push('503 Duplicate HELO/EHLO')
+        else:
+            print 'sending ', '250 %s' % self._fqdn
+            self._greeting = arg
+            self.push('250 %s' % self._fqdn)
+    
+    def smtp_QUIT(self, arg):
+        # args is ignored
+        self.push('221 Bye')
+        self.close_when_done()
+
+    def smtp_MAIL(self, arg):
+        print '===> MAIL', arg
+        address = self.__getaddr('FROM:', arg)
+        if not address:
+            self.push('501 Syntax: MAIL FROM:<address>')
+            return
+        if self._mailfrom:
+            self.push('503 Error: nested MAIL command')
+            return
+        self._mailfrom = address
+        print 'sender:', self._mailfrom
+        self.push('250 Ok')
+
+    def smtp_RCPT(self, arg):
+        print '===> RCPT', arg
+        if not self._mailfrom:
+            self.push('503 Error: need MAIL command')
+            return
+        address = self.__getaddr('TO:', arg)
+        if not address:
+            self.push('501 Syntax: RCPT TO: <address>')
+            return
+        self._rcpttos.append(address)
+        print 'recips:', self._rcpttos
+        self.push('250 Ok')
+
+    def smtp_RSET(self, arg):
+        if arg:
+            self.push('501 Syntax: RSET')
+            return
+        # Resets the sender, recipients, and data, but not the greeting
+        self._mailfrom = None
+        self._rcpttos = []
+        self._data = ''
+        self._old_state = self.COMMAND
+        self.push('250 Ok')
+
+    def smtp_DATA(self, arg):
+        if not self._rcpttos:
+            self.push('503 Error: need RCPT command')
+            return
+        if arg:
+            self.push('501 Syntax: DATA')
+            return
+        self._old_state = self.DATA
+        self.set_terminator('\r\n.\r\n')
+        self.push('354 End data with <CR><LF>.<CR><LF>')
+
+
+#! /usr/bin/env python2.5
+"""An RFC 2821 smtp proxy.
+
+Usage: %(program)s [options] [localhost:localport [remotehost:remoteport]]
+
+Options:
+
+    --nosetuid
+    -n
+        This program generally tries to setuid `nobody', unless this flag is
+        set.  The setuid call will fail if this program is not run as root (in
+        which case, use this flag).
+
+    --version
+    -V
+        Print the version number and exit.
+
+    --class classname
+    -c classname
+        Use `classname' as the concrete SMTP proxy class.  Uses `PureProxy' by
+        default.
+
+    --debug
+    -d
+        Turn on debugging prints.
+
+    --help
+    -h
+        Print this message and exit.
+
+Version: %(__version__)s
+
+If localhost is not given then `localhost' is used, and if localport is not
+given then 8025 is used.  If remotehost is not given then `localhost' is used,
+and if remoteport is not given, then 25 is used.
+"""
+
+
+# Overview:
+#
+# This file implements the minimal SMTP protocol as defined in RFC 821.  It
+# has a hierarchy of classes which implement the backend functionality for the
+# smtpd.  A number of classes are provided:
+#
+#   SMTPServer - the base class for the backend.  Raises NotImplementedError
+#   if you try to use it.
+#
+#   DebuggingServer - simply prints each message it receives on stdout.
+#
+#   PureProxy - Proxies all messages to a real smtpd which does final
+#   delivery.  One known problem with this class is that it doesn't handle
+#   SMTP errors from the backend server at all.  This should be fixed
+#   (contributions are welcome!).
+#
+#   MailmanProxy - An experimental hack to work with GNU Mailman
+#   <www.list.org>.  Using this server as your real incoming smtpd, your
+#   mailhost will automatically recognize and accept mail destined to Mailman
+#   lists when those lists are created.  Every message not destined for a list
+#   gets forwarded to a real backend smtpd, as with PureProxy.  Again, errors
+#   are not handled correctly yet.
+#
+# Please note that this script requires Python 2.0
+#
+# Author: Barry Warsaw <barry@python.org>
+#
+# TODO:
+#
+# - support mailbox delivery
+# - alias files
+# - ESMTP
+# - handle error codes from the backend smtpd
+
+import sys
+import os
+import errno
+import getopt
+import time
+import socket
+import asyncore
+import asynchat
+
+__all__ = ["SMTPServer","DebuggingServer","PureProxy","MailmanProxy"]
+
+program = sys.argv[0]
+__version__ = 'Python SMTP proxy version 0.2'
+
+
+class Devnull:
+    def write(self, msg): pass
+    def flush(self): pass
+
+
+DEBUGSTREAM = Devnull()
+NEWLINE = '\n'
+EMPTYSTRING = ''
+COMMASPACE = ', '
+
+
+
+def usage(code, msg=''):
+    print >> sys.stderr, __doc__ % globals()
+    if msg:
+        print >> sys.stderr, msg
+    sys.exit(code)
+
+
+
+class SMTPChannel(asynchat.async_chat):
+    COMMAND = 0
+    DATA = 1
+
+    def __init__(self, server, conn, addr):
+        asynchat.async_chat.__init__(self, conn)
+        self.__server = server
+        self.__conn = conn
+        self.__addr = addr
+        self.__line = []
+        self.__state = self.COMMAND
+        self.__greeting = 0
+        self.__mailfrom = None
+        self.__rcpttos = []
+        self.__data = ''
+        self.__fqdn = socket.getfqdn()
+        self.__peer = conn.getpeername()
+        print >> DEBUGSTREAM, 'Peer:', repr(self.__peer)
+        self.push('220 %s %s' % (self.__fqdn, __version__))
+        self.set_terminator('\r\n')
+
+    # Overrides base class for convenience
+    def push(self, msg):
+        asynchat.async_chat.push(self, msg + '\r\n')
+
+    # Implementation of base class abstract method
+    def collect_incoming_data(self, data):
+        self.__line.append(data)
+
+    # Implementation of base class abstract method
+    def found_terminator(self):
+        line = EMPTYSTRING.join(self.__line)
+        print >> DEBUGSTREAM, 'Data:', repr(line)
+        self.__line = []
+        if self.__state == self.COMMAND:
+            if not line:
+                self.push('500 Error: bad syntax')
+                return
+            method = None
+            i = line.find(' ')
+            if i < 0:
+                command = line.upper()
+                arg = None
+            else:
+                command = line[:i].upper()
+                arg = line[i+1:].strip()
+            method = getattr(self, 'smtp_' + command, None)
+            if not method:
+                self.push('502 Error: command "%s" not implemented' % command)
+                return
+            method(arg)
+            return
+        else:
+            if self.__state != self.DATA:
+                self.push('451 Internal confusion')
+                return
+            # Remove extraneous carriage returns and de-transparency according
+            # to RFC 821, Section 4.5.2.
+            data = []
+            for text in line.split('\r\n'):
+                if text and text[0] == '.':
+                    data.append(text[1:])
+                else:
+                    data.append(text)
+            self.__data = NEWLINE.join(data)
+            status = self.__server.process_message(self.__peer,
+                                                   self.__mailfrom,
+                                                   self.__rcpttos,
+                                                   self.__data)
+            self.__rcpttos = []
+            self.__mailfrom = None
+            self.__state = self.COMMAND
+            self.set_terminator('\r\n')
+            if not status:
+                self.push('250 Ok')
+            else:
+                self.push(status)
+
+    # SMTP and ESMTP commands
+    def smtp_HELO(self, arg):
+        if not arg:
+            self.push('501 Syntax: HELO hostname')
+            return
+        if self.__greeting:
+            self.push('503 Duplicate HELO/EHLO')
+        else:
+            self.__greeting = arg
+            self.push('250 %s' % self.__fqdn)
+
+    def smtp_NOOP(self, arg):
+        if arg:
+            self.push('501 Syntax: NOOP')
+        else:
+            self.push('250 Ok')
+
+    def smtp_QUIT(self, arg):
+        # args is ignored
+        self.push('221 Bye')
+        self.close_when_done()
+
+    # factored
+    def __getaddr(self, keyword, arg):
+        address = None
+        keylen = len(keyword)
+        if arg[:keylen].upper() == keyword:
+            address = arg[keylen:].strip()
+            if not address:
+                pass
+            elif address[0] == '<' and address[-1] == '>' and address != '<>':
+                # Addresses can be in the form <person@dom.com> but watch out
+                # for null address, e.g. <>
+                address = address[1:-1]
+        return address
+
+    def smtp_MAIL(self, arg):
+        print >> DEBUGSTREAM, '===> MAIL', arg
+        address = self.__getaddr('FROM:', arg)
+        if not address:
+            self.push('501 Syntax: MAIL FROM:<address>')
+            return
+        if self.__mailfrom:
+            self.push('503 Error: nested MAIL command')
+            return
+        self.__mailfrom = address
+        print >> DEBUGSTREAM, 'sender:', self.__mailfrom
+        self.push('250 Ok')
+
+    def smtp_RCPT(self, arg):
+        print >> DEBUGSTREAM, '===> RCPT', arg
+        if not self.__mailfrom:
+            self.push('503 Error: need MAIL command')
+            return
+        address = self.__getaddr('TO:', arg)
+        if not address:
+            self.push('501 Syntax: RCPT TO: <address>')
+            return
+        self.__rcpttos.append(address)
+        print >> DEBUGSTREAM, 'recips:', self.__rcpttos
+        self.push('250 Ok')
+
+    def smtp_RSET(self, arg):
+        if arg:
+            self.push('501 Syntax: RSET')
+            return
+        # Resets the sender, recipients, and data, but not the greeting
+        self.__mailfrom = None
+        self.__rcpttos = []
+        self.__data = ''
+        self.__state = self.COMMAND
+        self.push('250 Ok')
+
+    def smtp_DATA(self, arg):
+        if not self.__rcpttos:
+            self.push('503 Error: need RCPT command')
+            return
+        if arg:
+            self.push('501 Syntax: DATA')
+            return
+        self.__state = self.DATA
+        self.set_terminator('\r\n.\r\n')
+        self.push('354 End data with <CR><LF>.<CR><LF>')
+
+
+
+class SMTPServer(asyncore.dispatcher):
+    def __init__(self, localaddr, remoteaddr):
+        self._localaddr = localaddr
+        self._remoteaddr = remoteaddr
+        asyncore.dispatcher.__init__(self)
+        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
+        # try to re-use a server port if possible
+        self.set_reuse_addr()
+        self.bind(localaddr)
+        self.listen(5)
+        print >> DEBUGSTREAM, \
+              '%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
+            self.__class__.__name__, time.ctime(time.time()),
+            localaddr, remoteaddr)
+
+    def handle_accept(self):
+        conn, addr = self.accept()
+        print >> DEBUGSTREAM, 'Incoming connection from %s' % repr(addr)
+        channel = SMTPChannel(self, conn, addr)
+
+    # API for "doing something useful with the message"
+    def process_message(self, peer, mailfrom, rcpttos, data):
+        """Override this abstract method to handle messages from the client.
+
+        peer is a tuple containing (ipaddr, port) of the client that made the
+        socket connection to our smtp port.
+
+        mailfrom is the raw address the client claims the message is coming
+        from.
+
+        rcpttos is a list of raw addresses the client wishes to deliver the
+        message to.
+
+        data is a string containing the entire full text of the message,
+        headers (if supplied) and all.  It has been `de-transparencied'
+        according to RFC 821, Section 4.5.2.  In other words, a line
+        containing a `.' followed by other text has had the leading dot
+        removed.
+
+        This function should return None, for a normal `250 Ok' response;
+        otherwise it returns the desired response string in RFC 821 format.
+
+        """
+        raise NotImplementedError
+
+
+
+class DebuggingServer(SMTPServer):
+    # Do something with the gathered message
+    def process_message(self, peer, mailfrom, rcpttos, data):
+        inheaders = 1
+        lines = data.split('\n')
+        print '---------- MESSAGE FOLLOWS ----------'
+        for line in lines:
+            # headers first
+            if inheaders and not line:
+                print 'X-Peer:', peer[0]
+                inheaders = 0
+            print line
+        print '------------ END MESSAGE ------------'
+
+
+
+class PureProxy(SMTPServer):
+    def process_message(self, peer, mailfrom, rcpttos, data):
+        lines = data.split('\n')
+        # Look for the last header
+        i = 0
+        for line in lines:
+            if not line:
+                break
+            i += 1
+        lines.insert(i, 'X-Peer: %s' % peer[0])
+        data = NEWLINE.join(lines)
+        refused = self._deliver(mailfrom, rcpttos, data)
+        # TBD: what to do with refused addresses?
+        print >> DEBUGSTREAM, 'we got some refusals:', refused
+
+    def _deliver(self, mailfrom, rcpttos, data):
+        import smtplib
+        refused = {}
+        try:
+            s = smtplib.SMTP()
+            s.connect(self._remoteaddr[0], self._remoteaddr[1])
+            try:
+                refused = s.sendmail(mailfrom, rcpttos, data)
+            finally:
+                s.quit()
+        except smtplib.SMTPRecipientsRefused, e:
+            print >> DEBUGSTREAM, 'got SMTPRecipientsRefused'
+            refused = e.recipients
+        except (socket.error, smtplib.SMTPException), e:
+            print >> DEBUGSTREAM, 'got', e.__class__
+            # All recipients were refused.  If the exception had an associated
+            # error code, use it.  Otherwise,fake it with a non-triggering
+            # exception code.
+            errcode = getattr(e, 'smtp_code', -1)
+            errmsg = getattr(e, 'smtp_error', 'ignore')
+            for r in rcpttos:
+                refused[r] = (errcode, errmsg)
+        return refused
+
+
+
+class MailmanProxy(PureProxy):
+    def process_message(self, peer, mailfrom, rcpttos, data):
+        from cStringIO import StringIO
+        from Mailman import Utils
+        from Mailman import Message
+        from Mailman import MailList
+        # If the message is to a Mailman mailing list, then we'll invoke the
+        # Mailman script directly, without going through the real smtpd.
+        # Otherwise we'll forward it to the local proxy for disposition.
+        listnames = []
+        for rcpt in rcpttos:
+            local = rcpt.lower().split('@')[0]
+            # We allow the following variations on the theme
+            #   listname
+            #   listname-admin
+            #   listname-owner
+            #   listname-request
+            #   listname-join
+            #   listname-leave
+            parts = local.split('-')
+            if len(parts) > 2:
+                continue
+            listname = parts[0]
+            if len(parts) == 2:
+                command = parts[1]
+            else:
+                command = ''
+            if not Utils.list_exists(listname) or command not in (
+                    '', 'admin', 'owner', 'request', 'join', 'leave'):
+                continue
+            listnames.append((rcpt, listname, command))
+        # Remove all list recipients from rcpttos and forward what we're not
+        # going to take care of ourselves.  Linear removal should be fine
+        # since we don't expect a large number of recipients.
+        for rcpt, listname, command in listnames:
+            rcpttos.remove(rcpt)
+        # If there's any non-list destined recipients left,
+        print >> DEBUGSTREAM, 'forwarding recips:', ' '.join(rcpttos)
+        if rcpttos:
+            refused = self._deliver(mailfrom, rcpttos, data)
+            # TBD: what to do with refused addresses?
+            print >> DEBUGSTREAM, 'we got refusals:', refused
+        # Now deliver directly to the list commands
+        mlists = {}
+        s = StringIO(data)
+        msg = Message.Message(s)
+        # These headers are required for the proper execution of Mailman.  All
+        # MTAs in existance seem to add these if the original message doesn't
+        # have them.
+        if not msg.getheader('from'):
+            msg['From'] = mailfrom
+        if not msg.getheader('date'):
+            msg['Date'] = time.ctime(time.time())
+        for rcpt, listname, command in listnames:
+            print >> DEBUGSTREAM, 'sending message to', rcpt
+            mlist = mlists.get(listname)
+            if not mlist:
+                mlist = MailList.MailList(listname, lock=0)
+                mlists[listname] = mlist
+            # dispatch on the type of command
+            if command == '':
+                # post
+                msg.Enqueue(mlist, tolist=1)
+            elif command == 'admin':
+                msg.Enqueue(mlist, toadmin=1)
+            elif command == 'owner':
+                msg.Enqueue(mlist, toowner=1)
+            elif command == 'request':
+                msg.Enqueue(mlist, torequest=1)
+            elif command in ('join', 'leave'):
+                # TBD: this is a hack!
+                if command == 'join':
+                    msg['Subject'] = 'subscribe'
+                else:
+                    msg['Subject'] = 'unsubscribe'
+                msg.Enqueue(mlist, torequest=1)
+
+
+
+class Options:
+    setuid = 1
+    classname = 'PureProxy'
+
+
+
+def parseargs():
+    global DEBUGSTREAM
+    try:
+        opts, args = getopt.getopt(
+            sys.argv[1:], 'nVhc:d',
+            ['class=', 'nosetuid', 'version', 'help', 'debug'])
+    except getopt.error, e:
+        usage(1, e)
+
+    options = Options()
+    for opt, arg in opts:
+        if opt in ('-h', '--help'):
+            usage(0)
+        elif opt in ('-V', '--version'):
+            print >> sys.stderr, __version__
+            sys.exit(0)
+        elif opt in ('-n', '--nosetuid'):
+            options.setuid = 0
+        elif opt in ('-c', '--class'):
+            options.classname = arg
+        elif opt in ('-d', '--debug'):
+            DEBUGSTREAM = sys.stderr
+
+    # parse the rest of the arguments
+    if len(args) < 1:
+        localspec = 'localhost:8025'
+        remotespec = 'localhost:25'
+    elif len(args) < 2:
+        localspec = args[0]
+        remotespec = 'localhost:25'
+    elif len(args) < 3:
+        localspec = args[0]
+        remotespec = args[1]
+    else:
+        usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
+
+    # split into host/port pairs
+    i = localspec.find(':')
+    if i < 0:
+        usage(1, 'Bad local spec: %s' % localspec)
+    options.localhost = localspec[:i]
+    try:
+        options.localport = int(localspec[i+1:])
+    except ValueError:
+        usage(1, 'Bad local port: %s' % localspec)
+    i = remotespec.find(':')
+    if i < 0:
+        usage(1, 'Bad remote spec: %s' % remotespec)
+    options.remotehost = remotespec[:i]
+    try:
+        options.remoteport = int(remotespec[i+1:])
+    except ValueError:
+        usage(1, 'Bad remote port: %s' % remotespec)
+    return options
+
+
+
+if __name__ == '__main__':
+    options = parseargs()
+    # Become nobody
+    if options.setuid:
+        try:
+            import pwd
+        except ImportError:
+            print >> sys.stderr, \
+                  'Cannot import module "pwd"; try running with -n option.'
+            sys.exit(1)
+        nobody = pwd.getpwnam('nobody')[2]
+        try:
+            os.setuid(nobody)
+        except OSError, e:
+            if e.errno != errno.EPERM: raise
+            print >> sys.stderr, \
+                  'Cannot setuid "nobody"; try running with -n option.'
+            sys.exit(1)
+    classname = options.classname
+    if "." in classname:
+        lastdot = classname.rfind(".")
+        mod = __import__(classname[:lastdot], globals(), locals(), [""])
+        classname = classname[lastdot+1:]
+    else:
+        import __main__ as mod
+    class_ = getattr(mod, classname)
+    proxy = class_((options.localhost, options.localport),
+                   (options.remotehost, options.remoteport))
+    try:
+        asyncore.loop()
+    except KeyboardInterrupt:
+        pass