I have the following server taken almost directly from the aiosmtpd docs:
import asyncio
import ssl
from aiosmtpd.controller import Controller
class ExampleHandler:
async def handle_RCPT(self, server, session, envelope, address, rcpt_options):
if not address.endswith('@example.com'):
return '550 not relaying to that domain'
envelope.rcpt_tos.append(address)
return '250 OK'
async def handle_DATA(self, server, session, envelope):
print(f'Message from {envelope.mail_from}')
print(f'Message for {envelope.rcpt_tos}')
print(f'Message data:\n{envelope.content.decode("utf8", errors="replace")}')
print('End of message')
return '250 Message accepted for delivery'
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
controller = Controller(ExampleHandler(), port=8026, ssl_context=context)
controller.start()
input('Press enter to stop')
controller.stop()
However, when I start this server and try to send an email to it using swaks:
echo 'Testing' | swaks --to wayne@example.com --from "something@example.org" --server localhost --port 8026 -tls
It times out after 30s. If I remove the ssl_context=context
from the server and -tls
from the client then it sends the mail fine.
Additionally, when I try to connect via telnet and just send EHLO whatever
then the server actually closes the connection.
What's the correct way to implement an aiosmtpd server that supports tls?
Building upon Wayne's own answer, here's how to create a STARTTLS server with aiosmtpd.
For testing, use the following command to generate a self-signed certificate for localhost
:
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -subj '/CN=localhost'
Load it into Python using the ssl
module:
import ssl
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain('cert.pem', 'key.pem')
Create a subclass of aiosmtpd's Controller that passes this context as the tls_context
to SMTP
:
from aiosmtpd.smtp import SMTP
from aiosmtpd.controller import Controller
class ControllerTls(Controller):
def factory(self):
return SMTP(self.handler, require_starttls=True, tls_context=context)
Instantiate this controller with a handler and start it. Here, I use aiosmtpd's own Debugging
handler:
from aiosmtpd.handlers import Debugging
controller = ControllerTls(Debugging(), port=1025)
controller.start()
input('Press enter to stop')
controller.stop()
Either configure a local mail client to send to localhost:1025
, or use swaks
:
swaks -tls -t test --server localhost:1025
... or use openssl s_client
to talk to the server after the initial STARTTLS
command has been issued:
openssl s_client -crlf -CAfile cert.pem -connect localhost:1025 -starttls smtp
The code below additionally tests the server using swaks, and it also shows how to create a TLS-on-connect server (as in Wayne's answer).
import os
import ssl
import subprocess
from aiosmtpd.smtp import SMTP
from aiosmtpd.controller import Controller
from aiosmtpd.handlers import Debugging
# Create cert and key if they don't exist
if not os.path.exists('cert.pem') and not os.path.exists('key.pem'):
subprocess.call('openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem ' +
'-days 365 -nodes -subj "/CN=localhost"', shell=True)
# Load SSL context
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain('cert.pem', 'key.pem')
# Pass SSL context to aiosmtpd
class ControllerStarttls(Controller):
def factory(self):
return SMTP(self.handler, require_starttls=True, tls_context=context)
# Start server
controller = ControllerStarttls(Debugging(), port=1025)
controller.start()
# Test using swaks (if available)
subprocess.call('swaks -tls -t test --server localhost:1025', shell=True)
input('Running STARTTLS server. Press enter to stop.\n')
controller.stop()
# Alternatively: Use TLS-on-connect
controller = Controller(Debugging(), port=1025, ssl_context=context)
controller.start()
# Test using swaks (if available)
subprocess.call('swaks -tlsc -t test --server localhost:1025', shell=True)
input('Running TLSC server. Press enter to stop.\n')
controller.stop()