Use PIPELINING on both sides
RFC 2920 was advertised and parsed by both sides and used by neither.
On the client, `envelope` sends MAIL FROM and every RCPT TO as one group
and reads all of their replies, turning an envelope of n recipients from
n+1 round trips into one. `hello` sets `pipelining` from the EHLO
response, and clears it after a HELO fallback, because §3.1 allows
pipelining only against a server that said it could take it; when it is
false the same call waits for each reply and produces the same result.
`sendMail` goes through it and keeps its all-or-nothing contract: a
refused recipient means RSET and an error, not a delivery to the rest.
DATA is deliberately not in the group even though §3.1 allows it as the
last command of one. After a 354 the transaction is committed and the
only ways out are to send the message or to send an empty one to whoever
was accepted -- so stopping before DATA keeps that choice with the caller
and costs one round trip out of the n+1 saved.
Reading a group needed a way to keep a reply: they arrive one after
another into a single buffer, so the failing one is gone by the time the
group has been drained. `discardReply` reads a reply without touching
that buffer, which lets the first refusal stay in `last_reply` while the
rest of the group is drained -- and the same trick makes LMTP's `end`
able to report which verdict failed, which the last commit said it could
not.
On the server the rule is §3.2's: hold back the replies to RSET, MAIL
FROM and RCPT TO, and send everything pending the moment the input is
empty. That condition is the whole safety argument -- a reply is only
ever held while another command is already waiting to be answered, so the
client is never left waiting for something sitting in a buffer -- and the
commands whose replies must never be held (EHLO, DATA, VRFY, EXPN, TURN,
QUIT, NOOP) are exactly the ones still using the unconditional `reply`.
A test writer that records its flush boundaries pins it down: eight
replies leave in five writes, with the three envelope replies and the 354
as one of them.
Every reference the README cites is now filed in the Zotero library as
well, RFCs by their DOIs so that none of the metadata is typed by hand.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDrB41sGu5k1ubD1ufbxqC