Take the server's AUTH mechanisms from zig-sasl too
`Handler.authenticate` is gone. `fn (username, password) bool` could
carry PLAIN and LOGIN and nothing else, because it assumed the server
held something a password could be compared against -- so the server
offered exactly those two and could never offer a third.
`Options.auth_mechanisms` replaces it: a list of `sasl.Server`,
advertised by name in the EHLO response and driven by the loop that is
left here, which is the SMTP part -- the 334 challenges, the `*` that
cancels, 235, and the 504 for a name nothing answers to. The server can
now do CRAM-MD5, which there is a test for against RFC 2195's published
response, and EXTERNAL, and anything else zig-sasl grows.
Where the credential comes from moved to the mechanism, which is the
whole reason this works. PLAIN and LOGIN share a `PasswordCheck` and are
told only whether a password was right; CRAM-MD5 needs a
`PasswordLookup` and gets the password itself, because it must compute
the same HMAC the client did. That has always been the argument against
offering CRAM-MD5 and it is now visible in the types.
The mechanisms hold per-exchange state, so a session needs its own set
rather than a shared one -- the demo server builds a fresh CRAM-MD5
challenge per connection for exactly that reason.
`Envelope.authenticated_as` is new, and had to be: the identity used to
reach the handler because the handler did the checking, and now the
mechanism does. It is what the mechanism reported rather than what the
client typed, which for PLAIN's authorization identity is not the same
thing, and it is what a handler deciding whether to relay actually wants.
Verified end to end: the demo server advertises PLAIN LOGIN CRAM-MD5, a
CRAM-MD5 login succeeds over a plaintext session with no cleartext
opt-in, swaks still logs in with LOGIN, and the whole interop suite
passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDrB41sGu5k1ubD1ufbxqC