Reject unsafe command arguments and cleartext AUTH
Two things the client got wrong, both of which put something on the wire
that the caller did not ask for.
`mailFrom`, `rcptTo`, `mailFromUtf8` and `hello` interpolated their
argument straight into the command line, so an address carrying CR or LF
ended the line early and everything after it was read by the server as
further SMTP commands -- `bob@example.net>\r\nRCPT TO:<victim@example.net`
delivered to two people. Those four now check the argument first and
return `error.UnsafeArgument` rather than send it, as does AUTH PLAIN,
where the byte that matters is NUL: it separates the three fields, so one
hidden inside a field moves the boundary and authenticates as somebody
else. The check is `protocol.isSafeArgument`, and it is deliberately
framing only -- CR, LF and NUL and nothing else -- because the RFC 5321
path grammar rejects addresses that real deployments carry every day, and
a client that refused them would be the wrong tool.
`authenticate` preferred AUTH PLAIN unconditionally, which sent the
password in the clear whenever the transport was. The client cannot tell
on its own -- it is handed a reader and a writer and has no idea what is
under them -- so it now assumes the worst and takes the answer from the
caller: `setTransport` records it for a STARTTLS upgrade, and a session
that speaks TLS from the first byte sets `security` itself. PLAIN and
LOGIN return `error.InsecureTransport` on a plaintext transport, and
`authenticate` inverts its preference there to CRAM-MD5, the one
mechanism of the three that never puts the password on the wire.
`allow_cleartext_auth` is the way past that for a connection protected by
something this library cannot see -- a unix socket, an SSH tunnel, a
loopback test -- and `zsmtp send --allow-cleartext-auth` exposes it.
The interop test grew the case that matters: the same delivery to exim
fails without the opt-in and succeeds over STARTTLS without one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDrB41sGu5k1ubD1ufbxqC