Read enhanced status codes, and check that we emit them
`Reply.enhanced` parses the class.subject.detail code RFC 3463 defines
and RFC 2034 puts at the front of a reply's text; `Reply.message` gives
the text without it. 550 is "no", where 5.1.1 is "no, that mailbox does
not exist" and 5.7.1 is "no, and not because of anything about the
address" -- a difference a caller can act on and the three digits cannot
express.
The parser is strict on purpose, because the failure mode of a loose one
is misreading an ordinary message that happens to start with digits. The
class must be one of the three RFC 3463 defines, each field is one to
three digits with no leading zeros, and the code must be followed by a
space or be the whole text. "2.1 GB is too large" is not a status code
and does not parse as one.
`agrees` is there because nothing else checks it: a 250 carrying a 5.x.x
code is a server contradicting itself, RFC 3463 does not say what a
receiver should do about that, and a caller that reads only one half will
believe the wrong one.
The other half of this is a test that walks a session touching most of
the command table and checks every reply against RFC 2034's rule --
prefaced with a code whose class agrees, except the greeting, the EHLO
response and any 3xx, which must *not* carry one. It passes, and it is
not vacuous: deleting the code from one reply makes it fail naming that
line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDrB41sGu5k1ubD1ufbxqC
Decline EXPN rather than disown it
EXPN fell through to the unknown-command arm and answered 500, which
claims never to have heard of a command this server can perfectly well
parse. RFC 5321 §4.2.4 permits either 500 or 502 for a command that is
not implemented, and 502 is the one that is true here: the verb was
recognized and the service is not offered. A client can tell the
difference and act on it -- 500 means stop asking, 502 means this server
in this configuration.
VRFY keeps its 252, which is the compliant answer for a server that will
not check an address in advance but will accept the mail, and which
§4.5.1 requires of it -- 500 or 502 there would put this out of
compliance, since VRFY is one of the commands a server must support.
Both now require their argument, which the ABNF makes mandatory: `vrfy =
"VRFY" SP String CRLF`, and EXPN the same shape. Neither has anything to
act on without one, so a bare VRFY is a syntax error rather than a
command with an empty operand.
The RFC 2034 conformance walk covers EXPN now too, so its 502 is checked
for a status code whose class agrees, along with everything else.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDrB41sGu5k1ubD1ufbxqC
Read enhanced status codes, and check that we emit them
`Reply.enhanced` parses the class.subject.detail code RFC 3463 defines
and RFC 2034 puts at the front of a reply's text; `Reply.message` gives
the text without it. 550 is "no", where 5.1.1 is "no, that mailbox does
not exist" and 5.7.1 is "no, and not because of anything about the
address" -- a difference a caller can act on and the three digits cannot
express.
The parser is strict on purpose, because the failure mode of a loose one
is misreading an ordinary message that happens to start with digits. The
class must be one of the three RFC 3463 defines, each field is one to
three digits with no leading zeros, and the code must be followed by a
space or be the whole text. "2.1 GB is too large" is not a status code
and does not parse as one.
`agrees` is there because nothing else checks it: a 250 carrying a 5.x.x
code is a server contradicting itself, RFC 3463 does not say what a
receiver should do about that, and a caller that reads only one half will
believe the wrong one.
The other half of this is a test that walks a session touching most of
the command table and checks every reply against RFC 2034's rule --
prefaced with a code whose class agrees, except the greeting, the EHLO
response and any 3xx, which must *not* carry one. It passes, and it is
not vacuous: deleting the code from one reply makes it fail naming that
line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDrB41sGu5k1ubD1ufbxqC
Decline EXPN rather than disown it
EXPN fell through to the unknown-command arm and answered 500, which
claims never to have heard of a command this server can perfectly well
parse. RFC 5321 §4.2.4 permits either 500 or 502 for a command that is
not implemented, and 502 is the one that is true here: the verb was
recognized and the service is not offered. A client can tell the
difference and act on it -- 500 means stop asking, 502 means this server
in this configuration.
VRFY keeps its 252, which is the compliant answer for a server that will
not check an address in advance but will accept the mail, and which
§4.5.1 requires of it -- 500 or 502 there would put this out of
compliance, since VRFY is one of the commands a server must support.
Both now require their argument, which the ABNF makes mandatory: `vrfy =
"VRFY" SP String CRLF`, and EXPN the same shape. Neither has anything to
act on without one, so a bare VRFY is a syntax error rather than a
command with an empty operand.
The RFC 2034 conformance walk covers EXPN now too, so its 502 is checked
for a status code whose class agrees, along with everything else.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDrB41sGu5k1ubD1ufbxqC
Rename the project to zig-smtp
It sits beside zig-pop3, zig-ftp, zig-scram and zig-sasl, and "zsmtp" was
the odd one out.
Three names come out of it rather than one, following what those siblings
do. The repository and the package are `zig-smtp`; the Zig module is
`smtp`, so callers write `@import("smtp")` the way zig-pop3's callers
write `@import("pop3")`; and the demo CLI is `zig-smtp`, matching zig-ftp
rather than pop3's bare `pop3`, because `smtp` is too generic a name to
put on somebody's PATH.
The manifest fingerprint had to change with the package name -- Zig
derives it from that name and refuses the old one -- so this is a new
package as far as the package manager is concerned, not a renamed one.
The Radicle identity was renamed in place, so the RID is unchanged and
every `rad clone rad:z3ZKHgoDKEue8FT7sV6fHZdtjxRx1` in the wild still
works. The Tangled mirror could not be renamed and was recreated.
The published documentation moves with it, from jeff.jcollie.page/zsmtp/
to jeff.jcollie.page/zig-smtp/.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDrB41sGu5k1ubD1ufbxqC