diff -r d49cf0800c14 -r a765a06383af Lib/smtpd.py --- a/Lib/smtpd.py Mon Aug 22 09:46:56 2011 +0200 +++ b/Lib/smtpd.py Mon Aug 22 18:12:39 2011 +1000 @@ -77,6 +77,7 @@ import socket import asyncore import asynchat +import smtplib from warnings import warn __all__ = ["SMTPServer","DebuggingServer","PureProxy","MailmanProxy"] @@ -428,6 +429,10 @@ channel_class = SMTPChannel def __init__(self, localaddr, remoteaddr): + ''' + @param localaddr: I guess this is the local address to bind to? + @param remoteaddr: destination server for forwarding SMTP requests + ''' self._localaddr = localaddr self._remoteaddr = remoteaddr asyncore.dispatcher.__init__(self) @@ -493,7 +498,22 @@ class PureProxy(SMTPServer): + ''' + 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!). + ''' + + def process_message(self, peer, mailfrom, rcpttos, data): + ''' + @param peer -- is this the backend or address or something? + @param mailfrom: assume this is an email address in utf8 + @param rcpttos: assume this is a list of email addresses in utf8 + @param data: assume this is the message body + ''' + lines = data.split('\n') # Look for the last header i = 0 @@ -501,18 +521,34 @@ if not line: break i += 1 + + # Is this a requirement or debug? Pushed message into debug stream. + print("PEER: %s" % str(peer), file=DEBUGSTREAM) + 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('we got some refusals:', refused, file=DEBUGSTREAM) + if refused != {}: + # TBD: what to do with refused addresses? + # Suggest we refuse temptation to guess... :) + message = 'we got some refusals: %s' % refused + raise NotImplementedError(message) - def _deliver(self, mailfrom, rcpttos, data): - import smtplib + def _deliver(self, mailfrom, rcpttos, data): + ''' + @param mailfrom: An email address in utf8 + @param rcpttos: A list of email addresses in utf8 + @param data: Message contents. Must include the X-Peer header. + + @return: Messages regarding refusal of response + ''' refused = {} try: s = smtplib.SMTP() - s.connect(self._remoteaddr[0], self._remoteaddr[1]) + remote_hostname = self._remoteaddr[0] + remote_port = self._remoteaddr[1] + s.connect(remote_hostname, remote_port) try: refused = s.sendmail(mailfrom, rcpttos, data) finally: @@ -529,6 +565,7 @@ errmsg = getattr(e, 'smtp_error', 'ignore') for r in rcpttos: refused[r] = (errcode, errmsg) + return refused diff -r d49cf0800c14 -r a765a06383af Lib/test/test_smtpd.py --- a/Lib/test/test_smtpd.py Mon Aug 22 09:46:56 2011 +0200 +++ b/Lib/test/test_smtpd.py Mon Aug 22 18:12:39 2011 +1000 @@ -3,9 +3,25 @@ import socket import io import smtpd +import smtplib +from smtplib import SMTP import asyncore +# Want some sample message data in a few places... +SAMPLE_MESSAGE_DATA = \ +'''Subject: Sample email message to test pure proxy +MIME-Version: 1.0 +Content-Type: text +Content-Transfer-Encoding: utf8 + +I've got a lovely bunch of coconuts +''' + +# Pick a non-privileged port to bind to +SMTP_DEFAULT_PORT = 4731 + + class DummyServer(smtpd.SMTPServer): def __init__(self, localaddr, remoteaddr): smtpd.SMTPServer.__init__(self, localaddr, remoteaddr) @@ -282,8 +298,126 @@ with support.check_warnings(('', PendingDeprecationWarning)): self.channel._SMTPChannel__addr = 'spam' +class PureProxyTest(TestCase): + + def setUp(self): + ''' + Don't go round binding to actual ports during testing + ''' + smtpd.socket = asyncore.socket = mock_socket + + class MockSMTP(SMTP): + ''' + Obviously when testing delivery we don't want to get coupled + to a running remote SMTP server + ''' + raise_on_connect = None + + def connect(self, remote_hostname, remote_port): + + if MockSMTP.raise_on_connect: + raise MockSMTP.raise_on_connect + + pass + + @classmethod + def set_raise_on_connect(cls, exc): + ''' + Allow this to test both success and failure of connection + ''' + MockSMTP.raise_on_connect = exc + + smtplib.SMTP = MockSMTP + + def tearDown(self): + ''' + Leave the smtp ports as we found them + ''' + asyncore.close_all() + asyncore.socket = smtpd.socket = socket + + smtplib.SMTP = SMTP + + def test_deliver(self): + ''' + Test the delivery of messages and handling of various + kinds of response + ''' + + localaddress = ('localhost', SMTP_DEFAULT_PORT) + remoteaddress = ('localhost', SMTP_DEFAULT_PORT) + broken_remoteaddress = ('themoon', -1) + the_server = smtpd.PureProxy(localaddress, remoteaddress) + broken_remotesever = smtpd.PureProxy(localaddress, broken_remoteaddress) + + peer = '127.0.0.1' + mailfrom = 'tleeuwenburg@gmail.com' + rcpttos = 'tleeuwenburg@gmail.com' + + # Test standard message delivery + the_server._deliver(mailfrom, rcpttos, SAMPLE_MESSAGE_DATA) + + # Connection refused error. Pushed coverage into another code path + smtplib.SMTP.set_raise_on_connect( + smtplib.SMTPRecipientsRefused("Monkey Exception")) + the_server._deliver(mailfrom, rcpttos, SAMPLE_MESSAGE_DATA) + smtplib.SMTP.set_raise_on_connect(None) + + ## Test delivery to a non-existent host + broken_remotesever._deliver(mailfrom, rcpttos, SAMPLE_MESSAGE_DATA) + + def test_process_message(self): + ''' + Block off call to deliver to restrict testing to proper unit. Basically + a smoke test to make sure the call works on a couple of message types. + + Basically the only thing that process_message does is insert a mail + header and then passes off to deliver, so it doesn't matter that the test + is not very meaningful. It just needs to not explode. + ''' + + class MockPureProxy(smtpd.PureProxy): + + def _deliver(self, mailfrom, rcpttos, data): + return {} + + class RefusingPureProxy(smtpd.PureProxy): + + def _deliver(self, mailfrom, rcpttos, data): + return "A fake refusal" + + localaddress = ('localhost', SMTP_DEFAULT_PORT) + remoteaddress = ('localhost', SMTP_DEFAULT_PORT + 1) + theServer = MockPureProxy(localaddress, remoteaddress) + + # Add the X-Peer header that DebuggingServer adds + peer = socket.gethostbyname('localhost') + + # Send a mail from myself + mailfrom = 'tleeuwenburg@gmail.com' + + # To myself + rcpttos = 'tleeuwenburg@gmail.com' + empty_message_data = '' + + # Test on plausible data + theServer.process_message(peer, mailfrom, rcpttos, SAMPLE_MESSAGE_DATA) + + # Test an empty message + theServer.process_message(peer, mailfrom, rcpttos, SAMPLE_MESSAGE_DATA) + + # Test some failure modes + refusingServer = RefusingPureProxy(localaddress, remoteaddress) + + # Test exception is raised on refusal (since we have no plan for what to do) + with self.assertRaises(NotImplementedError): + refusingServer.process_message(peer, mailfrom, rcpttos, SAMPLE_MESSAGE_DATA) + + def test_main(): - support.run_unittest(SMTPDServerTest, SMTPDChannelTest) + support.run_unittest(SMTPDServerTest, + SMTPDChannelTest, + PureProxyTest) if __name__ == "__main__": test_main()