Skip to content

Send one LMTP DATA reply per recipient (RFC 2033 4.2) - #648

Draft
aiolibsbot wants to merge 1 commit into
aio-libs:masterfrom
aiolibsbot:koan/lmtp-per-rcpt-data-replies
Draft

aiolibsbot wants to merge 1 commit into
aio-libs:masterfrom
aiolibsbot:koan/lmtp-per-rcpt-data-replies

Conversation

@aiolibsbot

@aiolibsbot aiolibsbot commented Sep 30, 2026 •

Copy link
Copy Markdown

What

LMTP now sends one DATA reply per accepted recipient, as RFC 2033 § 4.2 requires, instead of a single reply for the whole message.

Why

LMTP exists precisely because delivery is per-mailbox: it can succeed for one recipient and fail for another, so the protocol mandates one reply per RCPT that was accepted, in order. aiosmtpd sent exactly one — a conformant LMTP client (Postfix's, Mailman's) reads the first reply and then finds the remaining ones sitting in the stream, desynchronising the session. Closes #517.

How

smtp_DATA() no longer pushes the terminal reply itself; it maps the DATA hook's return value through a new overridable SMTP._data_replies() seam and pushes whatever comes back. The seam is evaluated before the post-DATA state reset, so it can still see envelope.rcpt_tos.

  • SMTP._data_replies() — unchanged behaviour: a one-element list.
  • LMTP._data_replies() — repeats a single status once per recipient, or accepts a list/tuple of per-recipient statuses from handle_DATA so a handler can report each mailbox's real outcome. A length mismatch raises ValueError, which the session surfaces as a 500 rather than silently misattributing statuses.

No new constructor flag: LMTP was simply non-conformant here, and no existing handler can have depended on the truncated reply stream.

Testing

5 new tests in test_lmtp.py covering: two recipients → two replies; one recipient → one reply and a session still usable afterwards; a single status repeated; per-recipient statuses delivered in RCPT order; and the count-mismatch error path. Full suite: 560 passed. flake8 clean, mypy adds no new errors, and the sphinx HTML build is warning-free for the touched pages.


Quality Report

Changes: 6 files changed, 177 insertions(+), 7 deletions(-)

Code scan: clean

Tests: skipped

Branch hygiene: clean

Generated by Kōan

LMTP requires the server to answer DATA once for every recipient that a
RCPT command accepted, in order, because delivery is per-mailbox and can
succeed for one recipient while failing for another.  aiosmtpd sent a
single reply, desynchronising conformant LMTP clients.

smtp_DATA now maps the DATA hook's return value through an overridable
_data_replies() seam, evaluated while the envelope is still intact.  SMTP
keeps its single reply; LMTP repeats the status once per recipient, or
uses a per-recipient list if handle_DATA returns one.

Closes aio-libs#517

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LMTP multi-rcpt data response

2 participants