Take the AUTH mechanisms from zig-sasl
PLAIN, LOGIN and CRAM-MD5 are gone from the client. They were three of
the four copies in this tree -- zig-pop3 has its own, an IMAP library
would have made a third, and zig-scram's SCRAM was reachable from none of
them. They live in zig-sasl now, re-exported here as `zsmtp.sasl` so a
caller does not need a second import to say `sasl.Plain`.
What is left behind is the part that was ever specific to SMTP, and it is
one loop: the AUTH command, the 334 challenges, the `*` that cancels, the
235 that ends it. `authenticate` takes a `sasl.Client` and drives it.
`Extensions.auth` is now the mechanism names as the server sent them,
which is what `sasl.Client.selectFromList` reads -- so the preference
order and the don't-send-a-password-in-the-clear rule are one
implementation instead of one per protocol.
Two things the old code could not express now work. A mechanism may
answer a challenge with nothing, which is how SCRAM acknowledges the
server's proof and how XOAUTH2 acknowledges a failure report. And
`error.ServerNotAuthenticated` is the server reporting success while the
mechanism says it never finished proving what it set out to -- for SCRAM,
a peer that took the client's proof and offered none of its own, which is
precisely what something in the middle without the verifier would do.
Nothing here could tell that from a real success before.
A mechanism failing mid-exchange now cancels with `*` and reads the 501,
rather than leaving the server waiting for a line that is not coming.
The server side is unchanged: it still implements PLAIN and LOGIN itself,
against a plaintext password, because zig-sasl's server side does not yet
reach past PLAIN. That is the next thing.
Verified against postfix, exim and dovecot: the whole interop suite
passes, including AUTH PLAIN and LOGIN to exim over both a cleartext
opt-in and STARTTLS.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDrB41sGu5k1ubD1ufbxqC
Implement BINARYMIME, the other half of RFC 3030
CHUNKING was here and BINARYMIME was refused with 555, which left the
framing without the thing it exists to frame. BDAT carries a length, so
it can carry content that holds what would otherwise be a terminator;
that is the whole reason RFC 3030 defines the two together and says
BINARYMIME can only be used with CHUNKING.
The server advertises it beside CHUNKING, takes `BODY=BINARYMIME`, and
answers DATA for such a message with 503 as §3 requires -- binary has no
line structure, so a line holding a single dot cannot mean the end of it.
Nothing had to change in the receive path: BDAT already copied octets
without touching line endings, which is what "preserve all bits in each
octet" asks for, and there is now a test that walks all 256 byte values
through it to keep that true.
The client gained `MailOptions.body`, so `BODY=` is sayable at all --
7BIT and 8BITMIME as well, which the client could parse off EHLO and
never send. Declaring `.binary_mime` commits the transaction to BDAT, and
`data` refuses it with `error.BinaryRequiresChunking` rather than making
the round trip to be told 503. The demo CLI exposes it as --binarymime,
which implies --chunking because there is no other way to send it.
`Envelope.Body` moved to `protocol.Body` and lost its `unspecified`
variant in favour of `?protocol.Body`, since the client needs the same
enum and the envelope's other optional parameters are already spelled
that way. That is the breaking part.
The torture dialogue asserted 555 for BODY=BINARYMIME, which was exim's
answer and is no longer ours; those cases now use BODY=BINARY, which is
still not a body-value, so they go on testing what they were written to
test.
Verified against postfix and exim, neither of which offers BINARYMIME:
the client refuses to send binary to them at all, which is what RFC 3030
demands without qualification. Bit-exactness is checked end to end
through the CLI, all 256 octets plus a bare CR and a lone dot line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDrB41sGu5k1ubD1ufbxqC
Take the AUTH mechanisms from zig-sasl
PLAIN, LOGIN and CRAM-MD5 are gone from the client. They were three of
the four copies in this tree -- zig-pop3 has its own, an IMAP library
would have made a third, and zig-scram's SCRAM was reachable from none of
them. They live in zig-sasl now, re-exported here as `zsmtp.sasl` so a
caller does not need a second import to say `sasl.Plain`.
What is left behind is the part that was ever specific to SMTP, and it is
one loop: the AUTH command, the 334 challenges, the `*` that cancels, the
235 that ends it. `authenticate` takes a `sasl.Client` and drives it.
`Extensions.auth` is now the mechanism names as the server sent them,
which is what `sasl.Client.selectFromList` reads -- so the preference
order and the don't-send-a-password-in-the-clear rule are one
implementation instead of one per protocol.
Two things the old code could not express now work. A mechanism may
answer a challenge with nothing, which is how SCRAM acknowledges the
server's proof and how XOAUTH2 acknowledges a failure report. And
`error.ServerNotAuthenticated` is the server reporting success while the
mechanism says it never finished proving what it set out to -- for SCRAM,
a peer that took the client's proof and offered none of its own, which is
precisely what something in the middle without the verifier would do.
Nothing here could tell that from a real success before.
A mechanism failing mid-exchange now cancels with `*` and reads the 501,
rather than leaving the server waiting for a line that is not coming.
The server side is unchanged: it still implements PLAIN and LOGIN itself,
against a plaintext password, because zig-sasl's server side does not yet
reach past PLAIN. That is the next thing.
Verified against postfix, exim and dovecot: the whole interop suite
passes, including AUTH PLAIN and LOGIN to exim over both a cleartext
opt-in and STARTTLS.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDrB41sGu5k1ubD1ufbxqC
Implement BINARYMIME, the other half of RFC 3030
CHUNKING was here and BINARYMIME was refused with 555, which left the
framing without the thing it exists to frame. BDAT carries a length, so
it can carry content that holds what would otherwise be a terminator;
that is the whole reason RFC 3030 defines the two together and says
BINARYMIME can only be used with CHUNKING.
The server advertises it beside CHUNKING, takes `BODY=BINARYMIME`, and
answers DATA for such a message with 503 as §3 requires -- binary has no
line structure, so a line holding a single dot cannot mean the end of it.
Nothing had to change in the receive path: BDAT already copied octets
without touching line endings, which is what "preserve all bits in each
octet" asks for, and there is now a test that walks all 256 byte values
through it to keep that true.
The client gained `MailOptions.body`, so `BODY=` is sayable at all --
7BIT and 8BITMIME as well, which the client could parse off EHLO and
never send. Declaring `.binary_mime` commits the transaction to BDAT, and
`data` refuses it with `error.BinaryRequiresChunking` rather than making
the round trip to be told 503. The demo CLI exposes it as --binarymime,
which implies --chunking because there is no other way to send it.
`Envelope.Body` moved to `protocol.Body` and lost its `unspecified`
variant in favour of `?protocol.Body`, since the client needs the same
enum and the envelope's other optional parameters are already spelled
that way. That is the breaking part.
The torture dialogue asserted 555 for BODY=BINARYMIME, which was exim's
answer and is no longer ours; those cases now use BODY=BINARY, which is
still not a body-value, so they go on testing what they were written to
test.
Verified against postfix and exim, neither of which offers BINARYMIME:
the client refuses to send binary to them at all, which is what RFC 3030
demands without qualification. Bit-exactness is checked end to end
through the CLI, all 256 octets plus a bare CR and a lone dot line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDrB41sGu5k1ubD1ufbxqC
Take the AUTH mechanisms from zig-sasl
PLAIN, LOGIN and CRAM-MD5 are gone from the client. They were three of
the four copies in this tree -- zig-pop3 has its own, an IMAP library
would have made a third, and zig-scram's SCRAM was reachable from none of
them. They live in zig-sasl now, re-exported here as `zsmtp.sasl` so a
caller does not need a second import to say `sasl.Plain`.
What is left behind is the part that was ever specific to SMTP, and it is
one loop: the AUTH command, the 334 challenges, the `*` that cancels, the
235 that ends it. `authenticate` takes a `sasl.Client` and drives it.
`Extensions.auth` is now the mechanism names as the server sent them,
which is what `sasl.Client.selectFromList` reads -- so the preference
order and the don't-send-a-password-in-the-clear rule are one
implementation instead of one per protocol.
Two things the old code could not express now work. A mechanism may
answer a challenge with nothing, which is how SCRAM acknowledges the
server's proof and how XOAUTH2 acknowledges a failure report. And
`error.ServerNotAuthenticated` is the server reporting success while the
mechanism says it never finished proving what it set out to -- for SCRAM,
a peer that took the client's proof and offered none of its own, which is
precisely what something in the middle without the verifier would do.
Nothing here could tell that from a real success before.
A mechanism failing mid-exchange now cancels with `*` and reads the 501,
rather than leaving the server waiting for a line that is not coming.
The server side is unchanged: it still implements PLAIN and LOGIN itself,
against a plaintext password, because zig-sasl's server side does not yet
reach past PLAIN. That is the next thing.
Verified against postfix, exim and dovecot: the whole interop suite
passes, including AUTH PLAIN and LOGIN to exim over both a cleartext
opt-in and STARTTLS.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDrB41sGu5k1ubD1ufbxqC