Skip to main content

Lib/smtpd.py

Source:

cpython 3.14 @ ab2d84fe1023/Lib/smtpd.py

smtpd provides a simple SMTP server framework. It uses asynchat/asyncore for non-blocking I/O and is primarily used for testing mail handling.

Note: smtpd was deprecated in 3.6 and removed in 3.12. The annotation documents it for historical context and for projects targeting older Python versions.

Map

LinesSymbolRole
1-80SMTPChannelPer-connection state machine: EHLO/HELO, MAIL, RCPT, DATA
81-300SMTP commandssmtp_EHLO, smtp_MAIL, smtp_RCPT, smtp_DATA
301-500SMTPServerAsync TCP listener: create channel on accept
501-650DebuggingServerPrint each received message to stdout
651-800PureProxyForward mail to another SMTP server

Reading

SMTPChannel state machine

# CPython: Lib/smtpd.py:68 SMTPChannel
class SMTPChannel(asynchat.async_chat):
COMMAND = 0
DATA = 1

def __init__(self, server, conn, addr, ...):
asynchat.async_chat.__init__(self, conn)
self.set_terminator(b'\r\n') # command terminator
self._state = self.COMMAND
self._mailfrom = None
self._rcpttos = []
self._data = []
self.push('220 %s %s' % (server.fqdn, __version__))

smtp_DATA

# CPython: Lib/smtpd.py:200 smtp_DATA
def smtp_DATA(self, arg):
if not self._rcpttos:
self.push('503 Error: need RCPT command')
return
self.push('354 End data with <CR><LF>.<CR><LF>')
self._state = self.DATA
self.set_terminator(b'\r\n.\r\n') # end of message marker

When in DATA state, accumulate lines until the \r\n.\r\n terminator, then call process_message.

process_message

# CPython: Lib/smtpd.py:360 SMTPServer.process_message
def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
"""Override this method to process a received message.
peer — (host, port) of connecting client
mailfrom — MAIL FROM: address
rcpttos — list of RCPT TO: addresses
data — bytes: the complete message (headers + body)
"""
raise NotImplementedError

DebuggingServer

# CPython: Lib/smtpd.py:510 DebuggingServer
class DebuggingServer(SMTPServer):
def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
inheaders = True
lines = data.decode('utf-8', errors='replace').split('\n')
print('---------- MESSAGE FOLLOWS ----------')
for line in lines:
if inheaders and not line:
print('------------ END MESSAGE ------------')
inheaders = False
print(line)

gopy notes

smtpd is removed in Python 3.12 and not in gopy's stdlib. The annotation documents the design for reference. Modern replacements use aiosmtpd (asyncio-based) or a custom asyncio.Protocol.