Hyppää sisältöön

Ei vielä käännetty

Tätä sivua ei ole vielä käännetty suomeksi, joten se näytetään englanniksi. Auta kääntämään

smtpd Module Complexity

The smtpd module is an SMTP server built on asyncore and asynchat. An SMTPServer listens for connections and builds one SMTPChannel per client; the channel reads commands a line at a time, collects a message's data until the closing . line, and hands the whole message to the server's process_message(). Everything runs inside the asyncore loop, so a slow step in one connection holds up every other.

Removed in Python 3.12

Deprecated since Python 3.6 and removed in Python 3.12 by PEP 594. The examples need Python 3.11; new code should build on asyncio.

n is the bytes of one message's data, k is the characters of one command line, p is the recipients accepted for the current message, and s is the server's data_size_limit. The bounds count Python-level work and the bytes held; they exclude the kernel's cost for each system call, the resolver lookups named in the rows, and the cost of your own process_message(). The per-pass cost of the loop itself is on the asyncore page.

Complexity Reference

SMTPServer

Operation Time Space Notes
smtpd.SMTPServer(localaddr, remoteaddr, data_size_limit=33554432, map=None, enable_SMTPUTF8=False, decode_data=False) O(1) O(1) Resolves localaddr with getaddrinfo(), binds, listens and registers itself in map, or in asyncore's global map. enable_SMTPUTF8 and decode_data together raise ValueError
SMTPServer.process_message(peer, mailfrom, rcpttos, data, **kwargs) O(1) O(1) Must be overridden; the default raises NotImplementedError. Called once per message, inside the loop. With decode_data=False, the default, data is bytes and mail_options and rcpt_options arrive as keyword arguments, so an override that does not accept them, with **kwargs or by name, raises TypeError and the channel closes. Return None to reply 250 OK, or a reply line
SMTPServer.channel_class O(1) O(1) SMTPChannel; set a subclass to change how connections are handled
SMTPServer.handle_accepted(conn, addr) O(1) O(1) Builds one channel_class per accepted connection, in the server's map

SMTPChannel

Operation Time Space Notes
smtpd.SMTPChannel(server, conn, addr, data_size_limit=33554432, map=None, enable_SMTPUTF8=False, decode_data=False) O(1) O(1) Built by the server for each connection. Calls socket.getfqdn(), a resolver lookup the whole loop waits for, and queues the 220 greeting
SMTPChannel.collect_incoming_data(data) O(1) amortized O(1) Keeps each piece in received_lines until the terminator, and drops it once the line or the message is past its limit. In command state it also reads max_command_size_limit; decode_data=True decodes the piece first, in O(len(data))
SMTPChannel.found_terminator() O(k) per command, O(n) per message O(k) per command, O(n) per message Joins the pieces. A command line goes to its smtp_<COMMAND>() method, priced in its own row; a message has the leading dot removed from dot-stuffed lines and CRLF turned into LF, goes to process_message(), and gets its return value as the reply
SMTPChannel.push(msg) O(len(msg)) O(len(msg)) Encodes a reply line as ASCII, or UTF-8 for a message sent with SMTPUTF8, adds CRLF and queues it
SMTPChannel.smtp_HELO(arg), SMTPChannel.smtp_EHLO(arg) O(1) O(1) Start a new transaction and record seen_greeting; a second greeting gets 503. EHLO turns on SIZE when there is a limit, 8BITMIME unless decode_data, and SMTPUTF8 when enabled
SMTPChannel.smtp_MAIL(arg) O(k) O(k) Parses the address and parameters; refuses a declared SIZE over data_size_limit with 552
SMTPChannel.smtp_RCPT(arg) O(k + p) O(k + p) Parses the address and appends it to rcpttos, which has no limit. It also formats the whole list for DEBUGSTREAM, discarded or not, so accepting p recipients costs O(p²)
SMTPChannel.smtp_DATA(arg) O(1) O(1) Needs a greeting and a recipient; switches the terminator to \r\n.\r\n
SMTPChannel.smtp_RSET(arg) O(1) O(1) Clears the sender, the recipients and received_data, keeping the greeting
SMTPChannel.smtp_VRFY(arg) O(k) O(k) Parses the address and replies 252 without checking it
SMTPChannel.smtp_NOOP(arg), SMTPChannel.smtp_EXPN(arg) O(1) O(1) One fixed reply each; EXPN is always 502
SMTPChannel.smtp_HELP(arg) O(k) O(k) Upper-cases the argument and replies with that command's syntax, or the command list
SMTPChannel.smtp_QUIT(arg) O(1) O(1) Replies 221 and closes the channel once the reply is sent
SMTPChannel.smtp_server, SMTPChannel.conn, SMTPChannel.addr, SMTPChannel.peer, SMTPChannel.fqdn O(1) O(1) The server, the socket and its address, the address getpeername() returned, and the name getfqdn() returned
SMTPChannel.smtp_state, SMTPChannel.COMMAND, SMTPChannel.DATA O(1) O(1) smtp_state is COMMAND (0) or, after DATA, DATA (1)
SMTPChannel.seen_greeting, SMTPChannel.mailfrom O(1) O(1) The HELO/EHLO argument and the sender; '' and None until set
SMTPChannel.rcpttos O(1) O(1) The recipient list; process_message() receives this list itself, and the channel starts a new one for the next message
SMTPChannel.received_lines O(1) O(1) The pieces of the line or message being read
SMTPChannel.received_data O(1) O(1) The last message's data, the object process_message() received. It stays until RSET or the next message, so an idle channel keeps its last message
SMTPChannel.data_size_limit, SMTPChannel.enable_SMTPUTF8 O(1) O(1) Copied from the server; None or 0 means no limit on a message
SMTPChannel.command_size_limit O(1) O(1) 512, the limit on a command line unless command_size_limits gives the command a larger one; a longer line gets 500 Error: line too long
SMTPChannel.command_size_limits O(1) O(1) A class-wide dict of per-command limits, shared by every channel and emptied whenever one is built. EHLO raises MAIL's limit by 26 for SIZE and 10 for SMTPUTF8
SMTPChannel.max_command_size_limit O(u) O(1) u = entries in command_size_limits; the largest, or 512 when it is empty

DebuggingServer

Operation Time Space Notes
smtpd.DebuggingServer(localaddr, remoteaddr, ...) O(1) O(1) An SMTPServer that prints each message instead of delivering it
DebuggingServer.process_message(peer, mailfrom, rcpttos, data, **kwargs) O(n) O(n) Prints the message to standard output, with an X-Peer header at the first blank line; bytes data is printed one repr() per line

PureProxy

Operation Time Space Notes
smtpd.PureProxy(localaddr, remoteaddr, ...) O(1) O(1) Relays to remoteaddr. enable_SMTPUTF8=True raises ValueError. Pass decode_data=True: its process_message() takes no keyword arguments, so without it every message raises TypeError and the channel closes
PureProxy.process_message(peer, mailfrom, rcpttos, data) O(n + p) O(n + p) Adds an X-Peer header and sends the message with a new smtplib.SMTP connection, one per message, with one RCPT per recipient; the loop waits for the whole session. Refusals and connection failures are not reported: the client still gets 250 OK

Module constants

Operation Time Space Notes
smtpd.DATA_SIZE_DEFAULT O(1) O(1) 33554432 bytes (32 MiB), the default data_size_limit
smtpd.DEBUGSTREAM O(1) O(1) Where the module writes its trace of commands and messages; it discards them unless you assign a file such as sys.stderr

Receiving Messages

Subclass SMTPServer and override process_message(). The channel holds a message's pieces until the closing . line and then hands over the whole message at once, as bytes with LF line endings unless decode_data=True. The override runs inside the loop: while it runs, no other connection is served.

import asyncore
import smtpd
import smtplib
import threading

class Collector(smtpd.SMTPServer):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)  # O(1) - binds and listens
        self.messages = []

    def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
        self.messages.append((mailfrom, rcpttos, data, kwargs))  # O(1)
        return None  # 250 OK

channels = {}
server = Collector(("127.0.0.1", 0), None, map=channels)
port = server.socket.getsockname()[1]

def send():
    with smtplib.SMTP("127.0.0.1", port) as client:
        client.sendmail("a@example.org", ["b@example.org"], "Subject: hi\r\n\r\n.dot\r\n")

sender = threading.Thread(target=send)
sender.start()
while sender.is_alive():
    asyncore.loop(timeout=0.05, map=channels, count=1)  # O(n) for the message
sender.join()

mailfrom, rcpttos, data, options = server.messages[0]
assert (mailfrom, rcpttos) == ("a@example.org", ["b@example.org"])
assert data == b"Subject: hi\n\n.dot"  # bytes, LF endings, dot-stuffing undone
assert options == {"mail_options": ["SIZE=21"], "rcpt_options": []}

asyncore.close_all(channels)

The Message Size Limit

data_size_limit bounds what one message can hold: past it the channel drops what arrives, so memory stays near s however much the client sends. A client that declares the size after EHLO, as smtplib does, is refused at MAIL before it sends anything. One that sends the data anyway gets 552 at the closing . line, and the channel then stays in data state: what the client sends next is read as another message, so close the connection after that reply.

import asyncore
import smtpd
import socket

class Server:
    def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
        raise AssertionError("an oversized message was delivered")

channels = {}
ours, theirs = socket.socketpair()
channel = smtpd.SMTPChannel(Server(), ours, ("client", 0), data_size_limit=1000, map=channels)
assert theirs.recv(1024).startswith(b"220 ")

theirs.sendall(
    b"HELO me\r\nMAIL FROM:<a@x.org>\r\nRCPT TO:<b@x.org>\r\nDATA\r\n"
    + b"x" * 5_000 + b"\r\n.\r\n"
)
asyncore.loop(timeout=1, map=channels, count=1)  # O(n) time, O(s) memory
replies = theirs.recv(4096).decode().splitlines()
assert replies[-1] == "552 Error: Too much mail data"
assert channel.smtp_state == channel.DATA  # still reading message data

theirs.sendall(b"RSET\r\n")
asyncore.loop(timeout=1, map=channels, count=1)
assert channel.received_lines == [b"RSET"]  # taken as data, not as a command

asyncore.close_all(channels)
theirs.close()

Relaying With PureProxy

PureProxy delivers each message itself, opening a new smtplib connection to the upstream server and waiting for the whole session before process_message() returns. Every other client of the proxy waits with it. What the upstream server answers is not passed on.

import asyncore
import smtpd
import threading

class Upstream(smtpd.SMTPServer):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.messages = []

    def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
        self.messages.append((rcpttos, data))

upstream_map = {}
upstream = Upstream(("127.0.0.1", 0), None, map=upstream_map)
done = threading.Event()

def serve():
    while not done.is_set():
        asyncore.loop(timeout=0.05, map=upstream_map, count=1)
    asyncore.close_all(upstream_map)

thread = threading.Thread(target=serve)
thread.start()

proxy = smtpd.PureProxy(
    ("127.0.0.1", 0), upstream.socket.getsockname(), map={}, decode_data=True
)
proxy.process_message(("192.0.2.7", 2525), "a@x.org", ["b@x.org"], "Subject: hi\n\nbody")
# O(n + p), and the upstream session has finished before it returns
assert upstream.messages == [(["b@x.org"], b"Subject: hi\nX-Peer: 192.0.2.7\n\nbody")]

done.set()
thread.join()
proxy.close()

Performance Best Practices

✅ Do:

  • Accept **kwargs in process_message(), or pass decode_data=True; an override that refuses mail_options and rcpt_options closes the channel on every message
  • Keep process_message() short, or hand the message to a thread or queue: the loop serves no one else while it runs
  • Keep a data_size_limit: it is what bounds one message's memory
  • Close the connection after a 552 at the end of DATA

❌ Avoid:

  • PureProxy for any volume of mail - one blocking upstream session per message, with failures reported to no one
  • data_size_limit=None or 0 with clients you do not control - a message is then held whole
  • Messages with thousands of recipients - accepting p recipients costs O(p²)
  • smtpd in new code - it does not exist from Python 3.12

Version Notes

  • Python 3.6+: Deprecated
  • Python 3.10+: Importing the module emits a DeprecationWarning
  • Python 3.11+: MailmanProxy removed
  • Python 3.12+: Removed by PEP 594; import smtpd raises ModuleNotFoundError
  • smtplib - the client side, and what PureProxy relays through
  • asyncore - the loop and dispatcher base class SMTPServer builds on
  • asynchat - the async_chat base class that splits a channel's input at its terminator
  • email - parse the bytes process_message() receives with email.message_from_bytes()
  • asyncio - the replacement for the loop all of this runs in