Sunday, October 19, 2008

PyMOTW: smtpd


smtpd – Sample SMTP Servers











Purpose:Includes classes for implementing SMTP servers.
Python Version:2.1 and later

The smtpd module includes classes for building simple mail transport protocol servers. It is the server-side of the protocol used by smtplib.



SMTPServer


The base class for all of the provided example servers is SMTPServer. It handles communicating with the client, receiving the data, and provides a convenient hook to override to handle the message once it is fully available.


The constructor arguments are the local address to listen for connections and the remote address for proxying. The method process_message() is provided as a hook to be overridden by your derived class. It is called when the message is completely received, and given these arguments:



peer

The client’s address, a tuple containing IP and incoming port.

mailfrom

The “from” information out of the message envelope, given to the server by the client when the message is delivered. This does not necessarily match the From header in all cases.

rcpttos

The list of recipients from the message envelope. Again, this does not always match the To header, especially if someone is blind carbon copied.

data

The full RFC 2822 message body.


Since the default implementation of process_message() raises NotImplementedError, to demonstrate using SMTPServer we need to create a subclass and provide a useful implementation. Let’s create a simple server to print information about the messages recieved.


import smtpd
import asyncore

class CustomSMTPServer(smtpd.SMTPServer):

def process_message(self, peer, mailfrom, rcpttos, data):
print 'Receiving message from:', peer
print 'Message addressed from:', mailfrom
print 'Message addressed to :', rcpttos
print 'Message length :', len(data)
return

server = CustomSMTPServer(('127.0.0.1', 1025), None)

asyncore.loop()

Since SMTPServer uses asyncore, to run the server we run the asyncore.loop() function.


Now, we need a client to send data. By adapting one of the examples from the smtplib page, we can set up a client to send data to our test server running locally on port 1025.


import smtplib
import email.utils
from email.mime.text import MIMEText

# Create the message
msg = MIMEText('This is the body of the message.')
msg['To'] = email.utils.formataddr(('Recipient', 'recipient@example.com'))
msg['From'] = email.utils.formataddr(('Author', 'author@example.com'))
msg['Subject'] = 'Simple test message'

server = smtplib.SMTP('127.0.0.1', 1025)
server.set_debuglevel(True) # show communication with the server
try:
server.sendmail('author@example.com', ['recipient@example.com'], msg.as_string())
finally:
server.quit()

Now if we run smtpd_custom.py in one terminal, and smtpd_senddata.py in another, we should see:


$ python smtpd_senddata.py
send: 'ehlo farnsworth.local\r\n'
reply: '502 Error: command "EHLO" not implemented\r\n'
reply: retcode (502); Msg: Error: command "EHLO" not implemented
send: 'helo farnsworth.local\r\n'
reply: '250 farnsworth.local\r\n'
reply: retcode (250); Msg: farnsworth.local
send: 'mail FROM:<author@example.com>\r\n'
reply: '250 Ok\r\n'
reply: retcode (250); Msg: Ok
send: 'rcpt TO:<recipient@example.com>\r\n'
reply: '250 Ok\r\n'
reply: retcode (250); Msg: Ok
send: 'data\r\n'
reply: '354 End data with <CR><LF>.<CR><LF>\r\n'
reply: retcode (354); Msg: End data with <CR><LF>.<CR><LF>
data: (354, 'End data with <CR><LF>.<CR><LF>')
send: 'Content-Type: text/plain; charset="us-ascii"\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: 7bit\r\nTo: Recipient <recipient@example.com>\r\nFrom: Author <author@example.com>\r\nSubject: Simple test message\r\n\r\nThis is the body of the message.\r\n.\r\n'
reply: '250 Ok\r\n'
reply: retcode (250); Msg: Ok
data: (250, 'Ok')
send: 'quit\r\n'
reply: '221 Bye\r\n'
reply: retcode (221); Msg: Bye

and


$ python smtpd_custom.py
Receiving message from: ('127.0.0.1', 58541)
Message addressed from: author@example.com
Message addressed to : ['recipient@example.com']
Message length : 229

The port number for the incoming message will vary, of course. Notice that the rcpttos argument is a list of values and mailfrom is a single string.



Note


To stop the server, press Ctrl-C.





DebuggingServer


The simplistic example above shows the arguments to process_message(), but smtpd also includes a server specifically designed for debugging, called DebuggingServer. It prints the entire incoming message to stdout and then stops processing it (it does not proxy the message to a real mail server).


import smtpd
import asyncore

server = smtpd.DebuggingServer(('127.0.0.1', 1025), None)

asyncore.loop()

Using the smtpd_senddata.py client program from above, the output of the DebuggingServer is:


$ python smtpd_debug.py
---------- MESSAGE FOLLOWS ----------
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
To: Recipient <recipient@example.com>
From: Author <author@example.com>
Subject: Simple test message
X-Peer: 127.0.0.1

This is the body of the message.
------------ END MESSAGE ------------



PureProxy


The PureProxy class implements a straightforward proxy server. Incoming messages are forwarded upstream to the server given as argument to the constructor.



Warning


The stdlib docs say, “running this has a good chance to make you into an open relay, so please be careful.”



Setting up the proxy server is just as easy as the debug server:


import smtpd
import asyncore

server = smtpd.PureProxy(('127.0.0.1', 1025), ('mail', 25))

asyncore.loop()

It prints no output, though, so to verify that it is working we need to look at the mail server logs.


Oct 19 19:16:34 homer sendmail[6785]: m9JNGXJb006785: from=<author@example.com>, size=248, class=0, nrcpts=1, msgid=<200810192316.m9JNGXJb006785@homer.example.com>, proto=ESMTP, daemon=MTA, relay=[192.168.1.17]



MailmanProxy


smtpd also includes a special proxy that acts as a front-end for Mailman. If the local Mailman configuration recognizes the address, it is handled directly. Otherwise the message is delivered to the proxy.




References


Standard library documentation: smtpd


The smtplib module provides a client interface.


See also email and asyncore.


RFC 2822


GNU Mailman mailing list software


PyMOTW Home


Download Sample Code



5 comments:

Richard Jones said...

The smptd module is pretty nice, but has one fatal flaw: the implementation uses __private variables for important things which means you can't subclass it to override eg. the RCPT handler method without reverting to crap like
self._SMTPChannel__rcpttos.append(address)

Eugh. Private attributes were such a bad idea.

Doug Hellmann said...

That's a good point. I rarely, if ever, use private variables in my own code for that very reason.

Have you considered fixing it and submitting a patch for the Python 3.1 release?

Richard Jones said...

Considered, yes. Found time to do it? Not so much ;)

I am trying though.

Richard Jones said...

And done. http://bugs.python.org/issue4184

Doug Hellmann said...

Hey, great! I'll keep an eye on the ticket's progress through the bug tracker.