]> granicus.if.org Git - python/commitdiff
Implementation for issue 4184
authorRichard Jones <richard@commonground.com.au>
Sat, 24 Jul 2010 09:51:40 +0000 (09:51 +0000)
committerRichard Jones <richard@commonground.com.au>
Sat, 24 Jul 2010 09:51:40 +0000 (09:51 +0000)
Changes the previously private attributes to make them public, increasing the potential for extending the library in user code. Backward-compatible and documented.

Doc/library/smtpd.rst
Lib/smtpd.py

index 276751634d1abaff6c3565580f72c9bcbf1440f9..df67de943398e8c7308fe2ede7080e49ec70c55c 100644 (file)
 
 
 
-This module offers several classes to implement SMTP servers.  One is a generic
+This module offers several classes to implement SMTP (email) servers.
+
+Several server implementations are present; one is a generic
 do-nothing implementation, which can be overridden, while the other two offer
 specific mail-sending strategies.
 
+Additionally the SMTPChannel may be extended to implement very specific
+interaction behaviour with SMTP clients.
 
 SMTPServer Objects
 ------------------
@@ -26,7 +30,6 @@ SMTPServer Objects
    inherits from :class:`asyncore.dispatcher`, and so will insert itself into
    :mod:`asyncore`'s event loop on instantiation.
 
-
    .. method:: process_message(peer, mailfrom, rcpttos, data)
 
       Raise :exc:`NotImplementedError` exception. Override this in subclasses to
@@ -37,6 +40,11 @@ SMTPServer Objects
       containing the contents of the e-mail (which should be in :rfc:`2822`
       format).
 
+   .. attribute:: channel_class
+
+      Override this in subclasses to use a custom :class:`SMTPChannel` for
+      managing SMTP clients.
+
 
 DebuggingServer Objects
 -----------------------
@@ -71,3 +79,91 @@ MailmanProxy Objects
    running this has a good chance to make you into an open relay, so please be
    careful.
 
+SMTPChannel Objects
+-------------------
+
+.. class:: SMTPChannel(server, conn, addr)
+
+   Create a new :class:`SMTPChannel` object which manages the communication
+   between the server and a single SMTP client.
+
+   To use a custom SMTPChannel implementation you need to override the
+   :attr:`SMTPServer.channel_class` of your :class:`SMTPServer`.
+
+   The :class:`SMTPChannel` has the following instance variables:
+
+   .. attribute:: smtp_server
+
+      Holds the :class:`SMTPServer` that spawned this channel.
+
+   .. attribute:: conn
+
+      Holds the socket object connecting to the client.
+
+   .. attribute:: addr
+
+      Holds the address of the client, the second value returned by
+      socket.accept()
+
+   .. attribute:: received_lines
+
+      Holds a list of the line strings (decoded using UTF-8) received from
+      the client. The lines have their "\r\n" line ending translated to "\n".
+
+   .. attribute:: smtp_state
+
+      Holds the current state of the channel. This will be either
+      :attr:`COMMAND` initially and then :attr:`DATA` after the client sends
+      a "DATA" line.
+
+   .. attribute:: seen_greeting
+
+      Holds a string containing the greeting sent by the client in its "HELO".
+
+   .. attribute:: mailfrom
+
+      Holds a string containing the address identified in the "MAIL FROM:" line
+      from the client.
+
+   .. attribute:: rcpttos
+
+      Holds a list of strings containing the addresses identified in the
+      "RCPT TO:" lines from the client.
+
+   .. attribute:: received_data
+
+      Holds a string containing all of the data sent by the client during the
+      DATA state, up to but not including the terminating "\r\n.\r\n".
+
+   .. attribute:: fqdn
+
+      Holds the fully-qualified domain name of the server as returned by
+      ``socket.getfqdn()``.
+
+   .. attribute:: peer
+
+      Holds the name of the client peer as returned by ``conn.getpeername()``
+      where ``conn`` is :attr:`conn`.
+
+   The :class:`SMTPChannel` operates by invoking methods named ``smtp_<command>``
+   upon reception of a command line from the client. Built into the base
+   :class:`SMTPChannel` class are methods for handling the following commands
+   (and responding to them appropriately):
+
+   ======== ===================================================================
+   Command  Action taken
+   ======== ===================================================================
+   HELO     Accepts the greeting from the client and stores it in
+            :attr:`seen_greeting`.
+   NOOP     Takes no action.
+   QUIT     Closes the connection cleanly.
+   MAIL     Accepts the "MAIL FROM:" syntax and stores the supplied address as
+            :attr:`mailfrom`.
+   RCPT     Accepts the "RCPT TO:" syntax and stores the supplied addresses in
+            the :attr:`rcpttos` list.
+   RSET     Resets the :attr:`mailfrom`, :attr:`rcpttos`, and
+            :attr:`received_data`, but not the greeting.
+   DATA     Sets the internal state to :attr:`DATA` and stores remaining lines
+            from the client in :attr:`received_data` until the terminator
+            "\r\n.\r\n" is received.
+   ======== ===================================================================
\ No newline at end of file
index d7c5c93814005ee12207b2657dab72732b9f98f4..dd8398836ff96c0af9172c5438ed939ac4654778 100755 (executable)
@@ -78,6 +78,7 @@ import time
 import socket
 import asyncore
 import asynchat
+from warnings import warn
 
 __all__ = ["SMTPServer","DebuggingServer","PureProxy","MailmanProxy"]
 
@@ -111,35 +112,157 @@ class SMTPChannel(asynchat.async_chat):
 
     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('Peer:', repr(self.__peer), file=DEBUGSTREAM)
-        self.push('220 %s %s' % (self.__fqdn, __version__))
+        self.smtp_server = server
+        self.conn = conn
+        self.addr = addr
+        self.received_lines = []
+        self.smtp_state = self.COMMAND
+        self.seen_greeting = ''
+        self.mailfrom = None
+        self.rcpttos = []
+        self.received_data = ''
+        self.fqdn = socket.getfqdn()
+        self.peer = conn.getpeername()
+        print('Peer:', repr(self.peer), file=DEBUGSTREAM)
+        self.push('220 %s %s' % (self.fqdn, __version__))
         self.set_terminator(b'\r\n')
 
+    # properties for backwards-compatibility
+    @property
+    def __server(self):
+        warn("Access to __server attribute on SMTPChannel is deprecated, "
+            "use 'smtp_server' instead", PendingDeprecationWarning, 2)
+        return self.smtp_server
+    @__server.setter
+    def __server(self, value):
+        warn("Setting __server attribute on SMTPChannel is deprecated, "
+            "set 'smtp_server' instead", PendingDeprecationWarning, 2)
+        self.smtp_server = value
+
+    @property
+    def __line(self):
+        warn("Access to __line attribute on SMTPChannel is deprecated, "
+            "use 'received_lines' instead", PendingDeprecationWarning, 2)
+        return self.received_lines
+    @__line.setter
+    def __line(self, value):
+        warn("Setting __line attribute on SMTPChannel is deprecated, "
+            "set 'received_lines' instead", PendingDeprecationWarning, 2)
+        self.received_lines = value
+
+    @property
+    def __state(self):
+        warn("Access to __state attribute on SMTPChannel is deprecated, "
+            "use 'smtp_state' instead", PendingDeprecationWarning, 2)
+        return self.smtp_state
+    @__state.setter
+    def __state(self, value):
+        warn("Setting __state attribute on SMTPChannel is deprecated, "
+            "set 'smtp_state' instead", PendingDeprecationWarning, 2)
+        self.smtp_state = value
+
+    @property
+    def __greeting(self):
+        warn("Access to __greeting attribute on SMTPChannel is deprecated, "
+            "use 'seen_greeting' instead", PendingDeprecationWarning, 2)
+        return self.seen_greeting
+    @__greeting.setter
+    def __greeting(self, value):
+        warn("Setting __greeting attribute on SMTPChannel is deprecated, "
+            "set 'seen_greeting' instead", PendingDeprecationWarning, 2)
+        self.seen_greeting = value
+
+    @property
+    def __mailfrom(self):
+        warn("Access to __mailfrom attribute on SMTPChannel is deprecated, "
+            "use 'mailfrom' instead", PendingDeprecationWarning, 2)
+        return self.mailfrom
+    @__mailfrom.setter
+    def __mailfrom(self, value):
+        warn("Setting __mailfrom attribute on SMTPChannel is deprecated, "
+            "set 'mailfrom' instead", PendingDeprecationWarning, 2)
+        self.mailfrom = value
+
+    @property
+    def __rcpttos(self):
+        warn("Access to __rcpttos attribute on SMTPChannel is deprecated, "
+            "use 'rcpttos' instead", PendingDeprecationWarning, 2)
+        return self.rcpttos
+    @__rcpttos.setter
+    def __rcpttos(self, value):
+        warn("Setting __rcpttos attribute on SMTPChannel is deprecated, "
+            "set 'rcpttos' instead", PendingDeprecationWarning, 2)
+        self.rcpttos = value
+
+    @property
+    def __data(self):
+        warn("Access to __data attribute on SMTPChannel is deprecated, "
+            "use 'received_data' instead", PendingDeprecationWarning, 2)
+        return self.received_data
+    @__data.setter
+    def __data(self, value):
+        warn("Setting __data attribute on SMTPChannel is deprecated, "
+            "set 'received_data' instead", PendingDeprecationWarning, 2)
+        self.received_data = value
+
+    @property
+    def __fqdn(self):
+        warn("Access to __fqdn attribute on SMTPChannel is deprecated, "
+            "use 'fqdn' instead", PendingDeprecationWarning, 2)
+        return self.fqdn
+    @__fqdn.setter
+    def __fqdn(self, value):
+        warn("Setting __fqdn attribute on SMTPChannel is deprecated, "
+            "set 'fqdn' instead", PendingDeprecationWarning, 2)
+        self.fqdn = value
+
+    @property
+    def __peer(self):
+        warn("Access to __peer attribute on SMTPChannel is deprecated, "
+            "use 'peer' instead", PendingDeprecationWarning, 2)
+        return self.peer
+    @__peer.setter
+    def __peer(self, value):
+        warn("Setting __peer attribute on SMTPChannel is deprecated, "
+            "set 'peer' instead", PendingDeprecationWarning, 2)
+        self.peer = value
+
+    @property
+    def __conn(self):
+        warn("Access to __conn attribute on SMTPChannel is deprecated, "
+            "use 'conn' instead", PendingDeprecationWarning, 2)
+        return self.conn
+    @__conn.setter
+    def __conn(self, value):
+        warn("Setting __conn attribute on SMTPChannel is deprecated, "
+            "set 'conn' instead", PendingDeprecationWarning, 2)
+        self.conn = value
+
+    @property
+    def __addr(self):
+        warn("Access to __addr attribute on SMTPChannel is deprecated, "
+            "use 'addr' instead", PendingDeprecationWarning, 2)
+        return self.addr
+    @__addr.setter
+    def __addr(self, value):
+        warn("Setting __addr attribute on SMTPChannel is deprecated, "
+            "set 'addr' instead", PendingDeprecationWarning, 2)
+        self.addr = value
+
     # Overrides base class for convenience
     def push(self, msg):
         asynchat.async_chat.push(self, bytes(msg + '\r\n', 'ascii'))
 
     # Implementation of base class abstract method
     def collect_incoming_data(self, data):
-        self.__line.append(str(data, "utf8"))
+        self.received_lines.append(str(data, "utf8"))
 
     # Implementation of base class abstract method
     def found_terminator(self):
-        line = EMPTYSTRING.join(self.__line)
+        line = EMPTYSTRING.join(self.received_lines)
         print('Data:', repr(line), file=DEBUGSTREAM)
-        self.__line = []
-        if self.__state == self.COMMAND:
+        self.received_lines = []
+        if self.smtp_state == self.COMMAND:
             if not line:
                 self.push('500 Error: bad syntax')
                 return
@@ -158,7 +281,7 @@ class SMTPChannel(asynchat.async_chat):
             method(arg)
             return
         else:
-            if self.__state != self.DATA:
+            if self.smtp_state != self.DATA:
                 self.push('451 Internal confusion')
                 return
             # Remove extraneous carriage returns and de-transparency according
@@ -169,14 +292,14 @@ class SMTPChannel(asynchat.async_chat):
                     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.received_data = NEWLINE.join(data)
+            status = self.__server.process_message(self.peer,
+                                                   self.mailfrom,
+                                                   self.rcpttos,
+                                                   self.received_data)
+            self.rcpttos = []
+            self.mailfrom = None
+            self.smtp_state = self.COMMAND
             self.set_terminator(b'\r\n')
             if not status:
                 self.push('250 Ok')
@@ -188,11 +311,11 @@ class SMTPChannel(asynchat.async_chat):
         if not arg:
             self.push('501 Syntax: HELO hostname')
             return
-        if self.__greeting:
+        if self.seen_greeting:
             self.push('503 Duplicate HELO/EHLO')
         else:
-            self.__greeting = arg
-            self.push('250 %s' % self.__fqdn)
+            self.seen_greeting = arg
+            self.push('250 %s' % self.fqdn)
 
     def smtp_NOOP(self, arg):
         if arg:
@@ -225,24 +348,24 @@ class SMTPChannel(asynchat.async_chat):
         if not address:
             self.push('501 Syntax: MAIL FROM:<address>')
             return
-        if self.__mailfrom:
+        if self.mailfrom:
             self.push('503 Error: nested MAIL command')
             return
-        self.__mailfrom = address
-        print('sender:', self.__mailfrom, file=DEBUGSTREAM)
+        self.mailfrom = address
+        print('sender:', self.mailfrom, file=DEBUGSTREAM)
         self.push('250 Ok')
 
     def smtp_RCPT(self, arg):
         print('===> RCPT', arg, file=DEBUGSTREAM)
-        if not self.__mailfrom:
+        if not self.mailfrom:
             self.push('503 Error: need MAIL command')
             return
         address = self.__getaddr('TO:', arg) if arg else None
         if not address:
             self.push('501 Syntax: RCPT TO: <address>')
             return
-        self.__rcpttos.append(address)
-        print('recips:', self.__rcpttos, file=DEBUGSTREAM)
+        self.rcpttos.append(address)
+        print('recips:', self.rcpttos, file=DEBUGSTREAM)
         self.push('250 Ok')
 
     def smtp_RSET(self, arg):
@@ -250,26 +373,29 @@ class SMTPChannel(asynchat.async_chat):
             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.mailfrom = None
+        self.rcpttos = []
+        self.received_data = ''
+        self.smtp_state = self.COMMAND
         self.push('250 Ok')
 
     def smtp_DATA(self, arg):
-        if not self.__rcpttos:
+        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.smtp_state = self.DATA
         self.set_terminator(b'\r\n.\r\n')
         self.push('354 End data with <CR><LF>.<CR><LF>')
 
 
 \f
 class SMTPServer(asyncore.dispatcher):
+    # SMTPChannel class to use for managing client connections
+    channel_class = SMTPChannel
+
     def __init__(self, localaddr, remoteaddr):
         self._localaddr = localaddr
         self._remoteaddr = remoteaddr
@@ -291,7 +417,7 @@ class SMTPServer(asyncore.dispatcher):
     def handle_accept(self):
         conn, addr = self.accept()
         print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
-        channel = SMTPChannel(self, conn, addr)
+        channel = self.channel_class(self, conn, addr)
 
     # API for "doing something useful with the message"
     def process_message(self, peer, mailfrom, rcpttos, data):