Commits

Anonymous committed a66f9ec

Renamed the SMTPSession to CommandParser and the SMTPProcessor to SMTPSession

Comments (0)

Files changed (7)

pymta/__init__.py

 from pymta.default_policy import *
 from pymta.model import *
 from pymta.mta import *
-from pymta.processor import *
-from pymta.smtp_session import *
+from pymta.command_parser import *
+from pymta.session import *
 
 

pymta/command_parser.py

+# -*- coding: UTF-8 -*-
+
+import asynchat
+
+from pymta.session import SMTPSession
+
+__all__ = ['SMTPCommandParser']
+
+class SMTPCommandParser(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
+    primary_hostname = property(primary_hostname)
+    
+    
+    # -------------------------------------------------------------------------
+    # Communication helper methods
+    
+    def push(self, code, msg=None):
+        """Send a message to the peer (using the correct SMTP line terminators
+        (usually only called from the SMTPProcessor)."""
+        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)
+    
+    def new_message_received(self, msg):
+        """Called from the SMTPProcessor when a new message was received 
+        successfully."""
+        self._server.new_message_received(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>')
+
+
 import socket
 
 from pymta.smtpd import SMTPServer
-from pymta.smtp_session import SMTPSession
+from pymta.command_parser import SMTPCommandParser
 
 __all__ = ['PythonMTA']
 
         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)
+        SMTPCommandParser(self, connection, remote_ip_and_port, policy)
 
     
     def primary_hostname(self):

pymta/processor.py

-# -*- coding: UTF-8 -*-
-
-from sets import Set
-
-from repoze.workflow.statemachine import StateMachine, StateMachineError
-
-from pymta.model import Message, Peer
-
-__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._message = 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 _add_noop_and_quit_transitions(self):
-        """NOOP and QUIT should be possible from everywhere so we need to add 
-        these transitions to all states configured so far."""
-        states = Set()
-        for key in self.state.states:
-            new_state = self.state.states[key]
-            state_name = new_state[0]
-            if state_name not in ['new', 'finished']:
-                states.add(state_name)
-        for state in states:
-            self._add_state(state, 'NOOP',  state)
-            self._add_state(state, 'QUIT',  'finished')
-        
-    
-    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('identify', 'MAIL FROM',  'sender_known')
-        self._add_state('sender_known', 'RCPT TO',  'recipient_known')
-        self._add_state('recipient_known', 'DATA',  'identify')
-        # How to add commands?
-        self._add_noop_and_quit_transitions()
-        self.valid_commands = [command for from_state, command in self.state.states]
-    
-    
-    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().replace(' ', '_')
-        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._state = 'new'
-        self._message = Message(Peer(remote_ip, remote_port))
-        
-        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:
-            if command not in self.valid_commands:
-                self.reply(500, 'unrecognized command "%s"' % smtp_command)
-            else:
-                msg = 'Command "%s" is not allowed here' % smtp_command
-                allowed_transitions = self.state.transitions(self)
-                if len(allowed_transitions) > 0:
-                      msg += ', expected on of %s' % allowed_transitions
-                self.reply(503, msg)
-        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(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."""
-        primary_hostname = self._session.primary_hostname
-        reply_text = '%s Hello %s' % (primary_hostname, self._message.peer.remote_ip)
-        self.reply(220, reply_text)
-    
-    def smtp_quit(self):
-        primary_hostname = self._session.primary_hostname
-        reply_text = '%s closing connection' % primary_hostname
-        self.reply(221, reply_text)
-        self._session.close_when_done()
-    
-    def smtp_noop(self):
-        self.reply(250, 'OK')
-    
-    def smtp_helo(self):
-        helo_string = self._command_arguments
-        self._message.smtp_helo = helo_string
-        primary_hostname = self._session.primary_hostname
-        self.reply(250, primary_hostname)
-    
-    def smtp_mail_from(self):
-        # TODO: Check for good email address!
-        # TODO: Check for single email address!
-        # TODO: Policy
-        self._message.smtp_from = self._command_arguments
-        self.reply(250, 'OK')
-    
-    def smtp_rcpt_to(self):
-        # TODO: Check for good email address!
-        # TODO: Handle multiple arguments
-        # TODO: Policy
-        self._message.smtp_to = self._command_arguments
-        self.reply(250, 'OK')
-    
-    def smtp_data(self):
-        msg_data = self._command_arguments
-        # TODO: Policy check
-        self._message.msg_data = msg_data
-        self._session.new_message_received(self._message)
-        self._message = None
-        self.reply(250, 'OK')
-        # Now we must not loose the message anymore!
-
-
+# -*- coding: UTF-8 -*-
+
+from sets import Set
+
+from repoze.workflow.statemachine import StateMachine, StateMachineError
+
+from pymta.model import Message, Peer
+
+__all__ = ['SMTPSession']
+
+
+class SMTPSession(object):
+    """The SMTPSession 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, command_parser, policy=None):
+        self._command_parser = command_parser
+        self._policy = policy
+        
+        self._command_arguments = None
+        self._message = 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 _add_noop_and_quit_transitions(self):
+        """NOOP and QUIT should be possible from everywhere so we need to add 
+        these transitions to all states configured so far."""
+        states = Set()
+        for key in self.state.states:
+            new_state = self.state.states[key]
+            state_name = new_state[0]
+            if state_name not in ['new', 'finished']:
+                states.add(state_name)
+        for state in states:
+            self._add_state(state, 'NOOP',  state)
+            self._add_state(state, 'QUIT',  'finished')
+        
+    
+    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('identify', 'MAIL FROM',  'sender_known')
+        self._add_state('sender_known', 'RCPT TO',  'recipient_known')
+        self._add_state('recipient_known', 'DATA',  'identify')
+        # How to add commands?
+        self._add_noop_and_quit_transitions()
+        self.valid_commands = [command for from_state, command in self.state.states]
+    
+    
+    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().replace(' ', '_')
+        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._state = 'new'
+        self._message = Message(Peer(remote_ip, remote_port))
+        
+        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:
+            if command not in self.valid_commands:
+                self.reply(500, 'unrecognized command "%s"' % smtp_command)
+            else:
+                msg = 'Command "%s" is not allowed here' % smtp_command
+                allowed_transitions = self.state.transitions(self)
+                if len(allowed_transitions) > 0:
+                      msg += ', expected on of %s' % allowed_transitions
+                self.reply(503, msg)
+        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._command_parser.push(code, text)
+    
+    
+    def close_connection(self):
+        "Request a connection close from the SMTP session handling instance."
+        self._command_parser.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."""
+        primary_hostname = self._command_parser.primary_hostname
+        reply_text = '%s Hello %s' % (primary_hostname, self._message.peer.remote_ip)
+        self.reply(220, reply_text)
+    
+    def smtp_quit(self):
+        primary_hostname = self._command_parser.primary_hostname
+        reply_text = '%s closing connection' % primary_hostname
+        self.reply(221, reply_text)
+        self._command_parser.close_when_done()
+    
+    def smtp_noop(self):
+        self.reply(250, 'OK')
+    
+    def smtp_helo(self):
+        helo_string = self._command_arguments
+        self._message.smtp_helo = helo_string
+        primary_hostname = self._command_parser.primary_hostname
+        self.reply(250, primary_hostname)
+    
+    def smtp_mail_from(self):
+        # TODO: Check for good email address!
+        # TODO: Check for single email address!
+        # TODO: Policy
+        self._message.smtp_from = self._command_arguments
+        self.reply(250, 'OK')
+    
+    def smtp_rcpt_to(self):
+        # TODO: Check for good email address!
+        # TODO: Handle multiple arguments
+        # TODO: Policy
+        self._message.smtp_to = self._command_arguments
+        self.reply(250, 'OK')
+    
+    def smtp_data(self):
+        msg_data = self._command_arguments
+        # TODO: Policy check
+        self._message.msg_data = msg_data
+        self._command_parser.new_message_received(self._message)
+        self._message = None
+        self.reply(250, 'OK')
+        # Now we must not loose the message anymore!
+
+

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
-    primary_hostname = property(primary_hostname)
-    
-    
-    # -------------------------------------------------------------------------
-    # Communication helper methods
-    
-    def push(self, code, msg=None):
-        """Send a message to the peer (using the correct SMTP line terminators
-        (usually only called from the SMTPProcessor)."""
-        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)
-    
-    def new_message_received(self, msg):
-        """Called from the SMTPProcessor when a new message was received 
-        successfully."""
-        self._server.new_message_received(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>')
-
-

tests/basic_message_sending_test.py

 
 from unittest import TestCase
 
-from pymta import Peer, SMTPProcessor
+from pymta import Peer, SMTPSession
 
 
-class MockSession(object):
+class MockCommandParser(object):
     primary_hostname = 'localhost'
     
     def __init__(self, server):
 
     def setUp(self):
         self.server = MockServer()
-        self.session = MockSession(self.server)
-        self.processor = SMTPProcessor(session=self.session)
-        self.processor.new_connection('127.0.0.1', 4567)
+        self.command_parser = MockCommandParser(self.server)
+        self.session = SMTPSession(command_parser=self.command_parser)
+        self.session.new_connection('127.0.0.1', 4567)
     
     def _send(self, command, data=None):
-        number_replies_before = len(self.session.replies)
-        self.processor.handle_input(command, data)
-        self.assertEqual(number_replies_before + 1, len(self.session.replies))
-        code, reply_text = self.session.replies[-1]
+        number_replies_before = len(self.command_parser.replies)
+        self.session.handle_input(command, data)
+        self.assertEqual(number_replies_before + 1, len(self.command_parser.replies))
+        code, reply_text = self.command_parser.replies[-1]
         self.assertEqual('2', str(code)[0], "%s %s" % (code, reply_text))
     
     def _close_connection(self):
         self._send('quit')
-        code, reply_text = self.session.replies[-1]
+        code, reply_text = self.command_parser.replies[-1]
         self.assertTrue(221, code)
         self.assertEqual('localhost closing connection', reply_text)
-        self.assertEqual(False, self.session.open)
+        self.assertEqual(False, self.command_parser.open)
     
     def test_new_connection(self):
-        self.assertEqual(1, len(self.session.replies))
-        code, reply_text = self.session.replies[-1]
+        self.assertEqual(1, len(self.command_parser.replies))
+        code, reply_text = self.command_parser.replies[-1]
         self.assertEqual(220, code)
         self.assertEqual('localhost Hello 127.0.0.1', reply_text)
         self._close_connection()
     
     def test_send_helo(self):
         self._send('helo', 'foo.example.com')
-        self.assertEqual(2, len(self.session.replies))
-        code, reply_text = self.session.replies[-1]
+        self.assertEqual(2, len(self.command_parser.replies))
+        code, reply_text = self.command_parser.replies[-1]
         self.assertEqual(250, code)
         self.assertEqual('localhost', reply_text)
         self._close_connection()
 
     def test_reject_duplicated_helo(self):
         self._send('helo', 'foo.example.com')
-        self.processor.handle_input('helo', 'foo.example.com')
-        self.assertEqual(3, len(self.session.replies))
-        code, reply_text = self.session.replies[-1]
+        self.session.handle_input('helo', 'foo.example.com')
+        self.assertEqual(3, len(self.command_parser.replies))
+        code, reply_text = self.command_parser.replies[-1]
         self.assertEqual(503, code)
         expected_message = 'Command "helo" is not allowed here'
         self.assertTrue(reply_text.startswith(expected_message), reply_text)
         self._close_connection()
 
     def test_invalid_commands_are_recognized(self):
-        self.processor.handle_input('invalid')
-        self.assertEqual(2, len(self.session.replies))
-        code, reply_text = self.session.replies[-1]
+        self.session.handle_input('invalid')
+        self.assertEqual(2, len(self.command_parser.replies))
+        code, reply_text = self.command_parser.replies[-1]
         self.assertEqual(500, code)
         self.assertEqual('unrecognized command "invalid"', reply_text)
         self._close_connection()