pythonsmtplibaiosmtpd

Python 3 SMTP aiosmtpd proxy/relay


I am trying to make an open SMTP relay using the new aiosmtpd library that replaces smtpd. The program below instantiates a Proxy handler that is passed onto the mail controller, which is started afterwards in the background. A message is then created with a standard smtplib client that connects to the relay.

All is good until the SMTP conversation between the client and the relay ends with the message never leaving the relay. The relay never replies with a 250 OK\r\n and a ctrl+c shows that sendmail is waiting for a reply.

Any ideas? Is the script missing something?

Edit: mail.example.com is only an example server. An smtpd DebuggingServer prints nothing upon execution of the script with relay = aiosmtpd.handlers.Proxy("localhost", 1025).

$ python3.6 -m smtpd -n -c DebuggingServer -d localhost:1025
DebuggingServer started at Fri Apr  7 18:41:09 2017
    Local addr: ('localhost', 1025)
    Remote addr:('localhost', 25)
...nothing printed out...

Script:

from aiosmtpd.handlers import Debugging, Proxy
from aiosmtpd.controller import Controller
from smtplib import SMTP

# relay = aiosmtpd.handlers.Debugging()
relay = aiosmtpd.handlers.Proxy("localhost", 1025)
# relay = aiosmtpd.handlers.Proxy("mail.example.com", 25)
controller = Controller(relay)
controller.start()
print(controller, controller.hostname, controller.port)

input("ready... press enter to continue")

print("creating SMTP with debug")
client = SMTP()
client.set_debuglevel(1)
print("connecting to the SMTP server")
client.connect(controller.hostname, controller.port)

print("sending message")
client.sendmail('alice@example.com',
                ['bob@example.com'], """\
From: Alice <alice@example.com>
To: Bob <bob@example.com>
Subject: Title

Body.
""")

print("stopping controller")
controller.stop()
print("checking if controller really stopped")
client.connect(controller.hostname, controller.port)

Here is the output of the script:

 $ python3.6 relay.py
<aiosmtpd.controller.Controller object at 0x10199f710> ::0 8025
ready... press enter to continue
creating SMTP with debug
connecting to the SMTP server
connect: ('::0', 8025)
connect: to ('::0', 8025) None
reply: b'220 localhost Python SMTP 1.0a4\r\n'
reply: retcode (220); Msg: b'localhost Python SMTP 1.0a4'
connect: b'localhost Python SMTP 1.0a4'
sending message
send: 'ehlo localhost\r\n'
reply: b'250-localhost\r\n'
reply: b'250-SIZE 33554432\r\n'
reply: b'250-8BITMIME\r\n'
reply: b'250 HELP\r\n'
reply: retcode (250); Msg: b'localhost\nSIZE 33554432\n8BITMIME\nHELP'
send: 'mail FROM:<alice@example.com> size=85\r\n'
reply: b'250 OK\r\n'
reply: retcode (250); Msg: b'OK'
send: 'rcpt TO:<bob@example.com>\r\n'
reply: b'250 OK\r\n'
reply: retcode (250); Msg: b'OK'
send: 'data\r\n'
reply: b'354 End data with <CR><LF>.<CR><LF>\r\n'
reply: retcode (354); Msg: b'End data with <CR><LF>.<CR><LF>'
data: (354, b'End data with <CR><LF>.<CR><LF>')
send: b'From: Alice <alice@example.com>\r\nTo: Bob <bob@example.com>\r\nSubject: Title\r\n\r\nBody.\r\n.\r\n'

...nothing happens at this point...

^CTraceback (most recent call last):
  File "relay.py", line 49, in <module>
    """)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/smtplib.py", line 881, in sendmail
    (code, resp) = self.data(msg)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/smtplib.py", line 568, in data
    (code, msg) = self.getreply()
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/smtplib.py", line 386, in getreply
    line = self.file.readline(_MAXLINE + 1)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/socket.py", line 586, in readinto
    return self._sock.recv_into(b)
KeyboardInterrupt

For comparison, here is the output using the debug handler:

 $ python3.6 relay.py
<aiosmtpd.controller.Controller object at 0x10189f710> ::0 8025
ready... press enter to continue
creating SMTP with debug
connecting to the SMTP server
connect: ('::0', 8025)
connect: to ('::0', 8025) None
reply: b'220 localhost Python SMTP 1.0a4\r\n'
reply: retcode (220); Msg: b'localhost Python SMTP 1.0a4'
connect: b'localhost Python SMTP 1.0a4'
sending message
send: 'ehlo localhost\r\n'
reply: b'250-localhost\r\n'
reply: b'250-SIZE 33554432\r\n'
reply: b'250-8BITMIME\r\n'
reply: b'250 HELP\r\n'
reply: retcode (250); Msg: b'localhost\nSIZE 33554432\n8BITMIME\nHELP'
send: 'mail FROM:<alice@example.com> size=85\r\n'
reply: b'250 OK\r\n'
reply: retcode (250); Msg: b'OK'
send: 'rcpt TO:<bob@example.com>\r\n'
reply: b'250 OK\r\n'
reply: retcode (250); Msg: b'OK'
send: 'data\r\n'
reply: b'354 End data with <CR><LF>.<CR><LF>\r\n'
reply: retcode (354); Msg: b'End data with <CR><LF>.<CR><LF>'
data: (354, b'End data with <CR><LF>.<CR><LF>')
send: b'From: Alice <alice@example.com>\r\nTo: Bob <bob@example.com>\r\nSubject: Title\r\n\r\nBody.\r\n.\r\n'
---------- MESSAGE FOLLOWS ----------
mail options: ['SIZE=85']
rcpt options: []

From: Alice <alice@example.com>
To: Bob <bob@example.com>
Subject: Title
X-Peer: ('::1', 64397, 0, 0)

Body.
------------ END MESSAGE ------------
reply: b'250 OK\r\n'
reply: retcode (250); Msg: b'OK'
data: (250, b'OK')
stopping controller
checking if controller really stopped
connect: ('::0', 8025)
connect: to ('::0', 8025) None
Traceback (most recent call last):
  File "relay.py", line 51, in <module>
    client.connect(controller.hostname, controller.port)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/smtplib.py", line 335, in connect
    self.sock = self._get_socket(host, port, self.timeout)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/smtplib.py", line 306, in _get_socket
    self.source_address)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/socket.py", line 722, in create_connection
    raise err
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/socket.py", line 713, in create_connection
    sock.connect(sa)
ConnectionRefusedError: [Errno 61] Connection refused

Solution

  • EDIT: The bug has been fixed in aiosmtpd 1.0b1, so an upgrade should resolve the issue.

    In aiosmtpd 1.0a4, an uncaught exception in Proxy.handle_DATA (using data as a str instead of bytes) causes the asyncio task to stop, but the exception is never propagated.

    If you upgrade to 1.0a5 you'll get the exception printed properly: "Error: (TypeError) cannot use a string pattern on a bytes-like object". The problem is that as of 1.0a5, Proxy in aiosmtpd is designed for use in a Controller subclass that sets decode_data=True on the aiosmtpd.smtp.SMTP object, but by default, decode_data=False which causes the error. (In my opinion Proxy should be changed to work with decode_data=False, so I've opened PR #74.)

    Thus, to use Proxy in aiosmtpd 1.0a4 or 1.0a5 you need to copy and use the UTF8Controller subclass from aiosmtpd/tests/test_handlers.py. I've tested the following script with both 1.0a4 and 1.0a5:

    from aiosmtpd.handlers import Debugging, Proxy
    from aiosmtpd.controller import Controller
    from aiosmtpd.smtp import SMTP as SMTPServer
    from smtplib import SMTP as SMTPClient
    
    class UTF8Controller(Controller):
        def factory(self):
            return SMTPServer(self.handler, decode_data=True)
    
    # relay = Debugging()
    relay = Proxy("localhost", 1025)
    # relay = Proxy("mail.example.com", 25)
    controller = UTF8Controller(relay)
    controller.start()
    print(controller, controller.hostname, controller.port)
    
    input("ready... press enter to continue")
    
    print("creating SMTP with debug")
    client = SMTPClient()
    client.set_debuglevel(1)
    print("connecting to the SMTP server")
    client.connect(controller.hostname, controller.port)
    
    print("sending message")
    client.sendmail('alice@example.com',
                    ['bob@example.com'], """\
    From: Alice <alice@example.com>
    To: Bob <bob@example.com>
    Subject: Title
    
    Body.
    """)
    
    print("stopping controller")
    controller.stop()
    print("checking if controller really stopped")
    client.connect(controller.hostname, controller.port)