Implement REQUIRETLS, and say why the other four stay out
The five "niche extensions" were one bullet, which hid that they are two
different kinds of thing and that four of them were already answered
correctly.
MT-PRIORITY, DELIVERBY, FUTURERELEASE and ETRN are queue features: one
orders a queue, one bounces from it on a deadline, one holds in it until a
time, one flushes it on demand. This library has no queue, so their wire
syntax without their semantics would advertise something nothing here
could honour. They are already answered right -- 555 for a parameter
never advertised, per RFC 5321 §4.1.1.11, and 500 for a command from an
extension never offered -- and the README now says that rather than
listing them as an oversight. Ignoring them is the one answer that would
be wrong.
REQUIRETLS is different in kind, which is why it is implemented: it is a
security feature, not a queue feature, and the sender's meaning is
"bounce this rather than let it travel in the clear".
Advertising it is a promise, and the code says so out loud. RFC 8689
requires a server offering the keyword to honour the requirement, and a
client that does not see it must quit and try another MX, refusing the
domain outright if none offers it -- so the keyword is load-bearing in a
way most are not. This library can keep no part of that promise itself;
it does not relay. `Options.requiretls` is therefore the caller
undertaking to honour it, defaults false, and is advertised only while
the session employs TLS. A parameter arriving where it was never offered
gets 555 rather than being quietly disregarded: accepting and ignoring it
would turn a sender's refusal to be downgraded into a downgrade.
The client refuses to send it over a session it does not believe is
encrypted. The rest of §4.1's preconditions -- a certificate validated by
a trust chain or DANE, an MX vouched for by DNSSEC or MTA-STS -- are not
visible from a library that is handed a reader and a writer and resolves
nothing, so they are documented as the caller's. The demo CLI knows more
than the library does and refuses --requiretls beside --insecure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDrB41sGu5k1ubD1ufbxqC
Implement REQUIRETLS, and say why the other four stay out
The five "niche extensions" were one bullet, which hid that they are two
different kinds of thing and that four of them were already answered
correctly.
MT-PRIORITY, DELIVERBY, FUTURERELEASE and ETRN are queue features: one
orders a queue, one bounces from it on a deadline, one holds in it until a
time, one flushes it on demand. This library has no queue, so their wire
syntax without their semantics would advertise something nothing here
could honour. They are already answered right -- 555 for a parameter
never advertised, per RFC 5321 §4.1.1.11, and 500 for a command from an
extension never offered -- and the README now says that rather than
listing them as an oversight. Ignoring them is the one answer that would
be wrong.
REQUIRETLS is different in kind, which is why it is implemented: it is a
security feature, not a queue feature, and the sender's meaning is
"bounce this rather than let it travel in the clear".
Advertising it is a promise, and the code says so out loud. RFC 8689
requires a server offering the keyword to honour the requirement, and a
client that does not see it must quit and try another MX, refusing the
domain outright if none offers it -- so the keyword is load-bearing in a
way most are not. This library can keep no part of that promise itself;
it does not relay. `Options.requiretls` is therefore the caller
undertaking to honour it, defaults false, and is advertised only while
the session employs TLS. A parameter arriving where it was never offered
gets 555 rather than being quietly disregarded: accepting and ignoring it
would turn a sender's refusal to be downgraded into a downgrade.
The client refuses to send it over a session it does not believe is
encrypted. The rest of §4.1's preconditions -- a certificate validated by
a trust chain or DANE, an MX vouched for by DNSSEC or MTA-STS -- are not
visible from a library that is handed a reader and a writer and resolves
nothing, so they are documented as the caller's. The demo CLI knows more
than the library does and refuses --requiretls beside --insecure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDrB41sGu5k1ubD1ufbxqC
Implement REQUIRETLS, and say why the other four stay out
The five "niche extensions" were one bullet, which hid that they are two
different kinds of thing and that four of them were already answered
correctly.
MT-PRIORITY, DELIVERBY, FUTURERELEASE and ETRN are queue features: one
orders a queue, one bounces from it on a deadline, one holds in it until a
time, one flushes it on demand. This library has no queue, so their wire
syntax without their semantics would advertise something nothing here
could honour. They are already answered right -- 555 for a parameter
never advertised, per RFC 5321 §4.1.1.11, and 500 for a command from an
extension never offered -- and the README now says that rather than
listing them as an oversight. Ignoring them is the one answer that would
be wrong.
REQUIRETLS is different in kind, which is why it is implemented: it is a
security feature, not a queue feature, and the sender's meaning is
"bounce this rather than let it travel in the clear".
Advertising it is a promise, and the code says so out loud. RFC 8689
requires a server offering the keyword to honour the requirement, and a
client that does not see it must quit and try another MX, refusing the
domain outright if none offers it -- so the keyword is load-bearing in a
way most are not. This library can keep no part of that promise itself;
it does not relay. `Options.requiretls` is therefore the caller
undertaking to honour it, defaults false, and is advertised only while
the session employs TLS. A parameter arriving where it was never offered
gets 555 rather than being quietly disregarded: accepting and ignoring it
would turn a sender's refusal to be downgraded into a downgrade.
The client refuses to send it over a session it does not believe is
encrypted. The rest of §4.1's preconditions -- a certificate validated by
a trust chain or DANE, an MX vouched for by DNSSEC or MTA-STS -- are not
visible from a library that is handed a reader and a writer and resolves
nothing, so they are documented as the caller's. The demo CLI knows more
than the library does and refuses --requiretls beside --insecure.
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