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