Re-inventing the wheel: A logging development SMTP server
I found myself in a spot of trouble while trying to debug some newsletter sending plugin in Plone called Singing & Dancing. The problem was that I had made a local installation of the application server and the Python version I had to run it on had no support for TLS/SSL. This meant my ordinary outgoing mailserver wouldn’t work.
Well anyway that would have been a bad solution to send “for real” since I just needed to see that the mails looked good and where addressed to the right people. So now I probably should have done the obvious and googled a bit, there seems to be quite a few simple SMTP servers, but I alas I did not. What I did was to re-invent the wheel a bit.
Python has some nice SMTP handling modules both for sending and receiving. There is already a debugging SMTP server that just prints any received message. You can start it directly with:
$ python -m smtpd -n -c DebuggingServer localhost:2525
This was nearly what I needed, the only problem was that the mails where Base64 encoded so it wasn’t obvious that they where correct.
Wouldn’t it be nice if we could view the mails directly in a email application? Well with a little tinkering we can. Again, the standard library in Python to the rescue. This time the mailbox module. The mailbox module can write email messages to mbox files or maildirs, both old and tried and ancient formats for mail box storage. The great thing about a maildir is that several mail readers can parse it, like evolution (but not Thunderbird! AFAIK)
To read mails in a maildir in evolution you just add a new account (it’s under settings) and then select the maildir format. It will then ask you for a folder, the script takes arguments to define it but the default is ~/.devbox
To start the server (I called it devbox, as in a “development inbox”):
$ devbox --port=2525 --maildir=foobar/mymail
Use -h to get help of the options and remember that if you need to bind against port 25 you need to be root or use sudo. Heres the code, and as you can see one third is GPL preamble, the other third is the option parsing and it’s only really one small class that does the trivial job of passing on the mail between the SMTPServer and the Maildir. I love Python standard library!
#!/usr/bin/python
# Copyright 2010 David Jensen
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <>>.
import mailbox
import smtpd
import asyncore
from optparse import OptionParser
class DevBox(smtpd.SMTPServer):
def __init__(self,port,maildir):
smtpd.SMTPServer.__init__(self,('localhost',port),('localhost',25))
self.mbox = mailbox.Maildir(maildir)
def process_message(self, peer, mailfrom, rcpttos, data):
self.mbox.lock()
self.mbox.add(data)
self.mbox.flush()
self.mbox.unlock()
print "Added a msg from ",mailfrom
if __name__ == '__main__':
usage=""" %prog [options]
%prog is debugging SMTP server bound to localhost. Useful
for when you need to test outgoing mail but do not want any
mail actually sent. All incoming messages are saved in a maildir
for later perusal regardless of any possible address.
What comes in won't come out."""
parser = OptionParser(usage,version="%prog 0.1")
parser.add_option("-m", "--maildir",action="store", type="string",
dest="maildir",default='~/.devbox',
help='sets the path to the maildir [default: %default]')
parser.add_option("-p", "--port",action="store", type="int",
dest="port",default=25,
help='sets the port the server listens to [default: %default]')
(options, args) = parser.parse_args()
print "Starting server on localhost:%i, with maildir %s" % (options.port,options.maildir)
dn = DevBox(options.port,options.maildir)
try:
asyncore.loop()
except KeyboardInterrupt:
pass