1// Package smtpclient is an SMTP client, for submitting to an SMTP server or
 
2// delivering from a queue.
 
4// Email clients can submit a message to SMTP server, after which the server is
 
5// responsible for delivery to the final destination. A submission client
 
6// typically connects with TLS, and PKIX-verifies the server's certificate. The
 
7// client then authenticates using a SASL mechanism.
 
9// Email servers manage a message queue, from which they will try to deliver
 
10// messages. In case of temporary failures, the message is kept in the queue and
 
11// tried again later. For delivery, no authentication is done. TLS is opportunistic
 
12// by default (TLS certificates not verified), but TLS and certificate verification
 
13// can be opted into by domains by specifying an MTA-STS policy for the domain, or
 
14// DANE TLSA records for their MX hosts.
 
16// Delivering a message from a queue would involve:
 
17//  1. Looking up an MTA-STS policy, through a cache.
 
18//  2. Resolving the MX targets for a domain, through smtpclient.GatherDestinations,
 
19//     and for each destination try delivery through:
 
20//  3. Looking up IP addresses for the destination, with smtpclient.GatherIPs.
 
21//  4. Looking up TLSA records for DANE, in case of authentic DNS responses
 
22//     (DNSSEC), with smtpclient.GatherTLSA.
 
23//  5. Dialing the MX target with smtpclient.Dial.
 
24//  6. Initializing a SMTP session with smtpclient.New, with proper TLS
 
25//     configuration based on discovered MTA-STS and DANE policies, and finally calling
 
44	"golang.org/x/exp/slog"
 
46	"github.com/mjl-/adns"
 
48	"github.com/mjl-/mox/dane"
 
49	"github.com/mjl-/mox/dns"
 
50	"github.com/mjl-/mox/mlog"
 
51	"github.com/mjl-/mox/moxio"
 
52	"github.com/mjl-/mox/sasl"
 
53	"github.com/mjl-/mox/smtp"
 
54	"github.com/mjl-/mox/stub"
 
55	"github.com/mjl-/mox/tlsrpt"
 
58// todo future: add function to deliver message to multiple recipients. requires more elaborate return value, indicating success per message: some recipients may succeed, others may fail, and we should still deliver. to prevent backscatter, we also sometimes don't allow multiple recipients. 
../rfc/5321:1144 
61	MetricCommands             stub.HistogramVec = stub.HistogramVecIgnore{}
 
62	MetricTLSRequiredNoIgnored stub.CounterVec   = stub.CounterVecIgnore{}
 
63	MetricPanicInc                               = func() {}
 
67	ErrSize                  = errors.New("message too large for remote smtp server") // SMTP server announced a maximum message size and the message to be delivered exceeds it.
 
68	Err8bitmimeUnsupported   = errors.New("remote smtp server does not implement 8bitmime extension, required by message")
 
69	ErrSMTPUTF8Unsupported   = errors.New("remote smtp server does not implement smtputf8 extension, required by message")
 
70	ErrRequireTLSUnsupported = errors.New("remote smtp server does not implement requiretls extension, required for delivery")
 
71	ErrStatus                = errors.New("remote smtp server sent unexpected response status code") // Relatively common, e.g. when a 250 OK was expected and server sent 451 temporary error.
 
72	ErrProtocol              = errors.New("smtp protocol error")                                     // After a malformed SMTP response or inconsistent multi-line response.
 
73	ErrTLS                   = errors.New("tls error")                                               // E.g. handshake failure, or hostname verification was required and failed.
 
74	ErrBotched               = errors.New("smtp connection is botched")                              // Set on a client, and returned for new operations, after an i/o error or malformed SMTP response.
 
75	ErrClosed                = errors.New("client is closed")
 
78// TLSMode indicates if TLS must, should or must not be used.
 
82	// TLS immediately ("implicit TLS"), directly starting TLS on the TCP connection,
 
83	// so not using STARTTLS. Whether PKIX and/or DANE is verified is specified
 
85	TLSImmediate TLSMode = "immediate"
 
87	// Required TLS with STARTTLS for SMTP servers. The STARTTLS command is always
 
88	// executed, even if the server does not announce support.
 
89	// Whether PKIX and/or DANE is verified is specified separately.
 
90	TLSRequiredStartTLS TLSMode = "requiredstarttls"
 
92	// Use TLS with STARTTLS if remote claims to support it.
 
93	TLSOpportunistic TLSMode = "opportunistic"
 
95	// TLS must not be attempted, e.g. due to earlier TLS handshake error.
 
96	TLSSkip TLSMode = "skip"
 
99// Client is an SMTP client that can deliver messages to a mail server.
 
101// Use New to make a new client.
 
103	// OrigConn is the original (TCP) connection. We'll read from/write to conn, which
 
104	// can be wrapped in a tls.Client. We close origConn instead of conn because
 
105	// closing the TLS connection would send a TLS close notification, which may block
 
106	// for 5s if the server isn't reading it (because it is also sending it).
 
110	ignoreTLSVerifyErrors bool
 
111	rootCAs               *x509.CertPool
 
112	remoteHostname        dns.Domain   // TLS with SNI and name verification.
 
113	daneRecords           []adns.TLSA  // For authenticating (START)TLS connection.
 
114	daneMoreHostnames     []dns.Domain // Additional allowed names in TLS certificate for DANE-TA.
 
115	daneVerifiedRecord    *adns.TLSA   // If non-nil, then will be set to verified DANE record if any.
 
117	// TLS connection success/failure are added. These are always non-nil, regardless
 
118	// of what was passed in opts. It lets us unconditionally dereference them.
 
119	recipientDomainResult *tlsrpt.Result // Either "sts" or "no-policy-found".
 
120	hostResult            *tlsrpt.Result // Either "dane" or "no-policy-found".
 
124	tr                      *moxio.TraceReader // Kept for changing trace levels between cmd/auth/data.
 
125	tw                      *moxio.TraceWriter
 
127	lastlog                 time.Time // For adding delta timestamps between log lines.
 
128	cmds                    []string  // Last or active command, for generating errors and metrics.
 
129	cmdStart                time.Time // Start of command.
 
130	tls                     bool      // Whether connection is TLS protected.
 
131	firstReadAfterHandshake bool      // To detect TLS alert error from remote just after handshake.
 
133	botched  bool // If set, protocol is out of sync and no further commands can be sent.
 
134	needRset bool // If set, a new delivery requires an RSET command.
 
136	remoteHelo        string // From 220 greeting line.
 
137	extEcodes         bool   // Remote server supports sending extended error codes.
 
138	extStartTLS       bool   // Remote server supports STARTTLS.
 
140	extSize           bool     // Remote server supports SIZE parameter.
 
141	maxSize           int64    // Max size of email message.
 
142	extPipelining     bool     // Remote server supports command pipelining.
 
143	extSMTPUTF8       bool     // Remote server supports SMTPUTF8 extension.
 
144	extAuthMechanisms []string // Supported authentication mechanisms.
 
145	extRequireTLS     bool     // Remote supports REQUIRETLS extension.
 
148// Error represents a failure to deliver a message.
 
150// Code, Secode, Command and Line are only set for SMTP-level errors, and are zero
 
153	// Whether failure is permanent, typically because of 5xx response.
 
155	// SMTP response status, e.g. 2xx for success, 4xx for transient error and 5xx for
 
156	// permanent failure.
 
158	// Short enhanced status, minus first digit and dot. Can be empty, e.g. for io
 
159	// errors or if remote does not send enhanced status codes. If remote responds with
 
160	// "550 5.7.1 ...", the Secode will be "7.1".
 
162	// SMTP command causing failure.
 
164	// For errors due to SMTP responses, the full SMTP line excluding CRLF that caused
 
165	// the error. Typically the last line read.
 
167	// Underlying error, e.g. one of the Err variables in this package, or io errors.
 
171// Unwrap returns the underlying Err.
 
172func (e Error) Unwrap() error {
 
176// Error returns a readable error string.
 
177func (e Error) Error() string {
 
180		s = e.Err.Error() + ", "
 
193// Opts influence behaviour of Client.
 
195	// If auth is non-nil, authentication will be done with the returned sasl client.
 
196	// The function should select the preferred mechanism. Mechanisms are in upper
 
199	// The TLS connection state can be used for the SCRAM PLUS mechanisms, binding the
 
200	// authentication exchange to a TLS connection. It is only present for TLS
 
203	// If no mechanism is supported, a nil client and nil error can be returned, and
 
204	// the connection will fail.
 
205	Auth func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error)
 
207	DANERecords        []adns.TLSA  // If not nil, DANE records to verify.
 
208	DANEMoreHostnames  []dns.Domain // For use with DANE, where additional certificate host names are allowed.
 
209	DANEVerifiedRecord *adns.TLSA   // If non-empty, set to the DANE record that verified the TLS connection.
 
211	// If set, TLS verification errors (for DANE or PKIX) are ignored. Useful for
 
212	// delivering messages with message header "TLS-Required: No".
 
213	// Certificates are still verified, and results are still tracked for TLS
 
214	// reporting, but the connections will continue.
 
215	IgnoreTLSVerifyErrors bool
 
217	// If not nil, used instead of the system default roots for TLS PKIX verification.
 
218	RootCAs *x509.CertPool
 
220	// TLS verification successes/failures is added to these TLS reporting results.
 
221	// Once the STARTTLS handshake is attempted, a successful/failed connection is
 
223	RecipientDomainResult *tlsrpt.Result // MTA-STS or no policy.
 
224	HostResult            *tlsrpt.Result // DANE or no policy.
 
227// New initializes an SMTP session on the given connection, returning a client that
 
228// can be used to deliver messages.
 
230// New optionally starts TLS (for submission), reads the server greeting,
 
231// identifies itself with a HELO or EHLO command, initializes TLS with STARTTLS if
 
232// remote supports it and optionally authenticates. If successful, a client is
 
233// returned on which eventually Close must be called. Otherwise an error is
 
234// returned and the caller is responsible for closing the connection.
 
236// Connecting to the correct host for delivery can be done using the Gather
 
237// functions, and with Dial. The queue managing outgoing messages typically decides
 
238// which host to deliver to, taking multiple MX records with preferences, other DNS
 
239// records, MTA-STS, retries and special cases into account.
 
241// tlsMode indicates if and how TLS may/must (not) be used.
 
243// tlsVerifyPKIX indicates if TLS certificates must be validated against the
 
244// PKIX/WebPKI certificate authorities (if TLS is done).
 
246// DANE-verification is done when opts.DANERecords is not nil.
 
248// TLS verification errors will be ignored if opts.IgnoreTLSVerification is set.
 
250// If TLS is done, PKIX verification is always performed for tracking the results
 
251// for TLS reporting, but if tlsVerifyPKIX is false, the verification result does
 
252// not affect the connection.
 
254// At the time of writing, delivery of email on the internet is done with
 
255// opportunistic TLS without PKIX verification by default. Recipient domains can
 
256// opt-in to PKIX verification by publishing an MTA-STS policy, or opt-in to DANE
 
257// verification by publishing DNSSEC-protected TLSA records in DNS.
 
258func New(ctx context.Context, elog *slog.Logger, conn net.Conn, tlsMode TLSMode, tlsVerifyPKIX bool, ehloHostname, remoteHostname dns.Domain, opts Opts) (*Client, error) {
 
259	ensureResult := func(r *tlsrpt.Result) *tlsrpt.Result {
 
261			return &tlsrpt.Result{}
 
268		tlsVerifyPKIX:         tlsVerifyPKIX,
 
269		ignoreTLSVerifyErrors: opts.IgnoreTLSVerifyErrors,
 
270		rootCAs:               opts.RootCAs,
 
271		remoteHostname:        remoteHostname,
 
272		daneRecords:           opts.DANERecords,
 
273		daneMoreHostnames:     opts.DANEMoreHostnames,
 
274		daneVerifiedRecord:    opts.DANEVerifiedRecord,
 
276		cmds:                  []string{"(none)"},
 
277		recipientDomainResult: ensureResult(opts.RecipientDomainResult),
 
278		hostResult:            ensureResult(opts.HostResult),
 
280	c.log = mlog.New("smtpclient", elog).WithFunc(func() []slog.Attr {
 
283			slog.Duration("delta", now.Sub(c.lastlog)),
 
289	if tlsMode == TLSImmediate {
 
290		config := c.tlsConfig()
 
291		tlsconn := tls.Client(conn, config)
 
292		// The tlsrpt tracking isn't used by caller, but won't hurt.
 
293		if err := tlsconn.HandshakeContext(ctx); err != nil {
 
294			c.tlsResultAdd(0, 1, err)
 
297		c.firstReadAfterHandshake = true
 
298		c.tlsResultAdd(1, 0, nil)
 
300		tlsversion, ciphersuite := moxio.TLSInfo(tlsconn)
 
301		c.log.Debug("tls client handshake done",
 
302			slog.String("tls", tlsversion),
 
303			slog.String("ciphersuite", ciphersuite),
 
304			slog.Any("servername", remoteHostname))
 
310	// We don't wrap reads in a timeoutReader for fear of an optional TLS wrapper doing
 
311	// reads without the client asking for it. Such reads could result in a timeout
 
313	c.tr = moxio.NewTraceReader(c.log, "RS: ", c.conn)
 
314	c.r = bufio.NewReader(c.tr)
 
315	// We use a single write timeout of 30 seconds.
 
317	c.tw = moxio.NewTraceWriter(c.log, "LC: ", timeoutWriter{c.conn, 30 * time.Second, c.log})
 
318	c.w = bufio.NewWriter(c.tw)
 
320	if err := c.hello(ctx, tlsMode, ehloHostname, opts.Auth); err != nil {
 
326// reportedError wraps an error while indicating it was already tracked for TLS
 
328type reportedError struct{ err error }
 
330func (e reportedError) Error() string {
 
334func (e reportedError) Unwrap() error {
 
338func (c *Client) tlsConfig() *tls.Config {
 
339	// We always manage verification ourselves: We need to report in detail about
 
340	// failures. And we may have to verify both PKIX and DANE, record errors for
 
341	// each, and possibly ignore the errors.
 
343	verifyConnection := func(cs tls.ConnectionState) error {
 
344		// Collect verification errors. If there are none at the end, TLS validation
 
345		// succeeded. We may find validation problems below, record them for a TLS report
 
346		// but continue due to policies. We track the TLS reporting result in this
 
347		// function, wrapping errors in a reportedError.
 
348		var daneErr, pkixErr error
 
350		// DANE verification.
 
351		// daneRecords can be non-nil and empty, that's intended.
 
352		if c.daneRecords != nil {
 
353			verified, record, err := dane.Verify(c.log.Logger, c.daneRecords, cs, c.remoteHostname, c.daneMoreHostnames, c.rootCAs)
 
354			c.log.Debugx("dane verification", err, slog.Bool("verified", verified), slog.Any("record", record))
 
356				if c.daneVerifiedRecord != nil {
 
357					*c.daneVerifiedRecord = record
 
360				// Track error for reports.
 
361				// todo spec: may want to propose adding a result for no-dane-match. dane allows multiple records, some mismatching/failing isn't fatal and reporting on each record is probably not productive. 
../rfc/8460:541 
362				fd := c.tlsrptFailureDetails(tlsrpt.ResultValidationFailure, "dane-no-match")
 
364					// todo future: potentially add more details. e.g. dane-ta verification errors. tlsrpt does not have "result types" to indicate those kinds of errors. we would probably have to pass c.daneResult to dane.Verify.
 
366					// We may have encountered errors while evaluation some of the TLSA records.
 
367					fd.FailureReasonCode += "+errors"
 
369				c.hostResult.Add(0, 0, fd)
 
371				if c.ignoreTLSVerifyErrors {
 
372					// We ignore the failure and continue the connection.
 
373					c.log.Infox("verifying dane failed, continuing with connection", err)
 
374					MetricTLSRequiredNoIgnored.IncLabels("daneverification")
 
376					// This connection will fail.
 
377					daneErr = dane.ErrNoMatch
 
382		// PKIX verification.
 
383		opts := x509.VerifyOptions{
 
384			DNSName:       cs.ServerName,
 
385			Intermediates: x509.NewCertPool(),
 
388		for _, cert := range cs.PeerCertificates[1:] {
 
389			opts.Intermediates.AddCert(cert)
 
391		if _, err := cs.PeerCertificates[0].Verify(opts); err != nil {
 
392			resultType, reasonCode := tlsrpt.TLSFailureDetails(err)
 
393			fd := c.tlsrptFailureDetails(resultType, reasonCode)
 
394			c.recipientDomainResult.Add(0, 0, fd)
 
396			if c.tlsVerifyPKIX && !c.ignoreTLSVerifyErrors {
 
401		if daneErr != nil && pkixErr != nil {
 
402			return reportedError{errors.Join(daneErr, pkixErr)}
 
403		} else if daneErr != nil {
 
404			return reportedError{daneErr}
 
405		} else if pkixErr != nil {
 
406			return reportedError{pkixErr}
 
412		ServerName: c.remoteHostname.ASCII, // For SNI.
 
413		// todo: possibly accept older TLS versions for TLSOpportunistic? or would our private key be at risk?
 
415		InsecureSkipVerify: true,             // VerifyConnection below is called and will do all verification.
 
416		VerifyConnection:   verifyConnection,
 
420// xbotchf generates a temporary error and marks the client as botched. e.g. for
 
421// i/o errors or invalid protocol messages.
 
422func (c *Client) xbotchf(code int, secode string, lastLine, format string, args ...any) {
 
423	panic(c.botchf(code, secode, lastLine, format, args...))
 
426// botchf generates a temporary error and marks the client as botched. e.g. for
 
427// i/o errors or invalid protocol messages.
 
428func (c *Client) botchf(code int, secode string, lastLine, format string, args ...any) error {
 
430	return c.errorf(false, code, secode, lastLine, format, args...)
 
433func (c *Client) errorf(permanent bool, code int, secode, lastLine, format string, args ...any) error {
 
438	return Error{permanent, code, secode, cmd, lastLine, fmt.Errorf(format, args...)}
 
441func (c *Client) xerrorf(permanent bool, code int, secode, lastLine, format string, args ...any) {
 
442	panic(c.errorf(permanent, code, secode, lastLine, format, args...))
 
445// timeoutWriter passes each Write on to conn after setting a write deadline on conn based on
 
447type timeoutWriter struct {
 
449	timeout time.Duration
 
453func (w timeoutWriter) Write(buf []byte) (int, error) {
 
454	if err := w.conn.SetWriteDeadline(time.Now().Add(w.timeout)); err != nil {
 
455		w.log.Errorx("setting write deadline", err)
 
458	return w.conn.Write(buf)
 
461var bufs = moxio.NewBufpool(8, 2*1024)
 
463func (c *Client) readline() (string, error) {
 
464	// todo: could have per-operation timeouts. and rfc suggests higher minimum timeouts. 
../rfc/5321:3610 
465	if err := c.conn.SetReadDeadline(time.Now().Add(30 * time.Second)); err != nil {
 
466		c.log.Errorx("setting read deadline", err)
 
469	line, err := bufs.Readline(c.log, c.r)
 
471		// See if this is a TLS alert from remote, and one other than 0 (which notifies
 
472		// that the connection is being closed. If so, we register a TLS connection
 
473		// failure. This handles TLS alerts that happen just after a successful handshake.
 
474		var netErr *net.OpError
 
475		if c.firstReadAfterHandshake && errors.As(err, &netErr) && netErr.Op == "remote error" && netErr.Err != nil && reflect.ValueOf(netErr.Err).Kind() == reflect.Uint8 && reflect.ValueOf(netErr.Err).Uint() != 0 {
 
476			resultType, reasonCode := tlsrpt.TLSFailureDetails(err)
 
477			// We count -1 success to compensate for the assumed success right after the handshake.
 
478			c.tlsResultAddFailureDetails(-1, 1, c.tlsrptFailureDetails(resultType, reasonCode))
 
481		return line, c.botchf(0, "", "", "%s: %w", strings.Join(c.cmds, ","), err)
 
483	c.firstReadAfterHandshake = false
 
487func (c *Client) xtrace(level slog.Level) func() {
 
493		c.tr.SetTrace(mlog.LevelTrace)
 
494		c.tw.SetTrace(mlog.LevelTrace)
 
498func (c *Client) xwritelinef(format string, args ...any) {
 
499	c.xbwritelinef(format, args...)
 
503func (c *Client) xwriteline(line string) {
 
508func (c *Client) xbwritelinef(format string, args ...any) {
 
509	c.xbwriteline(fmt.Sprintf(format, args...))
 
512func (c *Client) xbwriteline(line string) {
 
513	_, err := fmt.Fprintf(c.w, "%s\r\n", line)
 
515		c.xbotchf(0, "", "", "write: %w", err)
 
519func (c *Client) xflush() {
 
522		c.xbotchf(0, "", "", "writes: %w", err)
 
526// read response, possibly multiline, with supporting extended codes based on configuration in client.
 
527func (c *Client) xread() (code int, secode, lastLine string, texts []string) {
 
529	code, secode, lastLine, texts, err = c.read()
 
536func (c *Client) read() (code int, secode, lastLine string, texts []string, rerr error) {
 
537	return c.readecode(c.extEcodes)
 
540// read response, possibly multiline.
 
541// if ecodes, extended codes are parsed.
 
542func (c *Client) readecode(ecodes bool) (code int, secode, lastLine string, texts []string, rerr error) {
 
544		co, sec, text, line, last, err := c.read1(ecodes)
 
549		texts = append(texts, text)
 
550		if code != 0 && co != code {
 
552			err := c.botchf(0, "", line, "%w: multiline response with different codes, previous %d, last %d", ErrProtocol, code, co)
 
553			return 0, "", "", nil, err
 
557			if code != smtp.C334ContinueAuth {
 
561					// We only keep the last, so we're not creating new slices all the time.
 
566				MetricCommands.ObserveLabels(float64(time.Since(c.cmdStart))/float64(time.Second), cmd, fmt.Sprintf("%d", co), sec)
 
567				c.log.Debug("smtpclient command result",
 
568					slog.String("cmd", cmd),
 
569					slog.Int("code", co),
 
570					slog.String("secode", sec),
 
571					slog.Duration("duration", time.Since(c.cmdStart)))
 
573			return co, sec, line, texts, nil
 
578func (c *Client) xreadecode(ecodes bool) (code int, secode, lastLine string, texts []string) {
 
580	code, secode, lastLine, texts, err = c.readecode(ecodes)
 
587// read single response line.
 
588// if ecodes, extended codes are parsed.
 
589func (c *Client) read1(ecodes bool) (code int, secode, text, line string, last bool, rerr error) {
 
590	line, rerr = c.readline()
 
595	for ; i < len(line) && line[i] >= '0' && line[i] <= '9'; i++ {
 
598		rerr = c.botchf(0, "", line, "%w: expected response code: %s", ErrProtocol, line)
 
601	v, err := strconv.ParseInt(line[:i], 10, 32)
 
603		rerr = c.botchf(0, "", line, "%w: bad response code (%s): %s", ErrProtocol, err, line)
 
609	if strings.HasPrefix(s, "-") || strings.HasPrefix(s, " ") {
 
616		rerr = c.botchf(0, "", line, "%w: expected space or dash after response code: %s", ErrProtocol, line)
 
621		secode, s = parseEcode(major, s)
 
624	return code, secode, s, line, last, nil
 
627func parseEcode(major int, s string) (secode string, remain string) {
 
630	take := func(need bool, a, b byte) bool {
 
631		if !bad && o < len(s) && s[o] >= a && s[o] <= b {
 
638	digit := func(need bool) bool {
 
639		return take(need, '0', '9')
 
642		return take(true, '.', '.')
 
656	take(false, ' ', ' ')
 
657	if bad || int(s[0])-int('0') != major {
 
663func (c *Client) recover(rerr *error) {
 
668	cerr, ok := x.(Error)
 
676func (c *Client) hello(ctx context.Context, tlsMode TLSMode, ehloHostname dns.Domain, auth func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error)) (rerr error) {
 
677	defer c.recover(&rerr)
 
679	// perform EHLO handshake, falling back to HELO if server does not appear to
 
681	hello := func(heloOK bool) {
 
682		// Write EHLO and parse the supported extensions.
 
685		c.cmdStart = time.Now()
 
687		c.xwritelinef("EHLO %s", ehloHostname.ASCII)
 
688		code, _, lastLine, remains := c.xreadecode(false)
 
692		case smtp.C500BadSyntax, smtp.C501BadParamSyntax, smtp.C502CmdNotImpl, smtp.C503BadCmdSeq, smtp.C504ParamNotImpl:
 
694				c.xerrorf(true, code, "", lastLine, "%w: remote claims ehlo is not supported", ErrProtocol)
 
698			c.cmdStart = time.Now()
 
699			c.xwritelinef("HELO %s", ehloHostname.ASCII)
 
700			code, _, lastLine, _ = c.xreadecode(false)
 
701			if code != smtp.C250Completed {
 
702				c.xerrorf(code/100 == 5, code, "", lastLine, "%w: expected 250 to HELO, got %d", ErrStatus, code)
 
705		case smtp.C250Completed:
 
707			c.xerrorf(code/100 == 5, code, "", lastLine, "%w: expected 250, got %d", ErrStatus, code)
 
709		for _, s := range remains[1:] {
 
711			s = strings.ToUpper(strings.TrimSpace(s))
 
715			case "ENHANCEDSTATUSCODES":
 
720				c.extPipelining = true
 
722				c.extRequireTLS = true
 
725				if s == "SMTPUTF8" || strings.HasPrefix(s, "SMTPUTF8 ") {
 
727				} else if strings.HasPrefix(s, "SIZE ") {
 
729					if v, err := strconv.ParseInt(s[len("SIZE "):], 10, 64); err == nil {
 
732				} else if strings.HasPrefix(s, "AUTH ") {
 
733					c.extAuthMechanisms = strings.Split(s[len("AUTH "):], " ")
 
740	c.cmds = []string{"(greeting)"}
 
741	c.cmdStart = time.Now()
 
742	code, _, lastLine, lines := c.xreadecode(false)
 
743	if code != smtp.C220ServiceReady {
 
744		c.xerrorf(code/100 == 5, code, "", lastLine, "%w: expected 220, got %d", ErrStatus, code)
 
747	c.remoteHelo, _, _ = strings.Cut(lines[0], " ")
 
749	// Write EHLO, falling back to HELO if server doesn't appear to support it.
 
752	// Attempt TLS if remote understands STARTTLS and we aren't doing immediate TLS or if caller requires it.
 
753	if c.extStartTLS && tlsMode == TLSOpportunistic || tlsMode == TLSRequiredStartTLS {
 
754		c.log.Debug("starting tls client", slog.Any("tlsmode", tlsMode), slog.Any("servername", c.remoteHostname))
 
755		c.cmds[0] = "starttls"
 
756		c.cmdStart = time.Now()
 
757		c.xwritelinef("STARTTLS")
 
758		code, secode, lastLine, _ := c.xread()
 
760		if code != smtp.C220ServiceReady {
 
761			c.tlsResultAddFailureDetails(0, 1, c.tlsrptFailureDetails(tlsrpt.ResultSTARTTLSNotSupported, fmt.Sprintf("smtp-starttls-reply-code-%d", code)))
 
762			c.xerrorf(code/100 == 5, code, secode, lastLine, "%w: STARTTLS: got %d, expected 220", ErrTLS, code)
 
765		// We don't want to do TLS on top of c.r because it also prints protocol traces: We
 
766		// don't want to log the TLS stream. So we'll do TLS on the underlying connection,
 
767		// but make sure any bytes already read and in the buffer are used for the TLS
 
770		if n := c.r.Buffered(); n > 0 {
 
771			conn = &moxio.PrefixConn{
 
772				PrefixReader: io.LimitReader(c.r, int64(n)),
 
777		tlsConfig := c.tlsConfig()
 
778		nconn := tls.Client(conn, tlsConfig)
 
781		nctx, cancel := context.WithTimeout(ctx, time.Minute)
 
783		err := nconn.HandshakeContext(nctx)
 
785			// For each STARTTLS failure, we track a failed TLS session. For deliveries with
 
786			// multiple MX targets, we may add multiple failures, and delivery may succeed with
 
788			c.tlsResultAdd(0, 1, err)
 
789			c.xerrorf(false, 0, "", "", "%w: STARTTLS TLS handshake: %s", ErrTLS, err)
 
791		c.firstReadAfterHandshake = true
 
793		c.tr = moxio.NewTraceReader(c.log, "RS: ", c.conn)
 
794		c.tw = moxio.NewTraceWriter(c.log, "LC: ", c.conn) // No need to wrap in timeoutWriter, it would just set the timeout on the underlying connection, which is still active.
 
795		c.r = bufio.NewReader(c.tr)
 
796		c.w = bufio.NewWriter(c.tw)
 
798		tlsversion, ciphersuite := moxio.TLSInfo(nconn)
 
799		c.log.Debug("starttls client handshake done",
 
800			slog.Any("tlsmode", tlsMode),
 
801			slog.Bool("verifypkix", c.tlsVerifyPKIX),
 
802			slog.Bool("verifydane", c.daneRecords != nil),
 
803			slog.Bool("ignoretlsverifyerrors", c.ignoreTLSVerifyErrors),
 
804			slog.String("tls", tlsversion),
 
805			slog.String("ciphersuite", ciphersuite),
 
806			slog.Any("servername", c.remoteHostname),
 
807			slog.Any("danerecord", c.daneVerifiedRecord))
 
810		c.tlsResultAdd(1, 0, nil)
 
813	} else if tlsMode == TLSOpportunistic {
 
815		c.tlsResultAddFailureDetails(0, 0, c.tlsrptFailureDetails(tlsrpt.ResultSTARTTLSNotSupported, ""))
 
824func addrIP(addr net.Addr) string {
 
825	if t, ok := addr.(*net.TCPAddr); ok {
 
828	host, _, _ := net.SplitHostPort(addr.String())
 
829	ip := net.ParseIP(host)
 
831		return "" // For pipe during tests.
 
836// tlsrptFailureDetails returns FailureDetails with connection details (such as
 
837// IP addresses) for inclusion in a TLS report.
 
838func (c *Client) tlsrptFailureDetails(resultType tlsrpt.ResultType, reasonCode string) tlsrpt.FailureDetails {
 
839	return tlsrpt.FailureDetails{
 
840		ResultType:          resultType,
 
841		SendingMTAIP:        addrIP(c.origConn.LocalAddr()),
 
842		ReceivingMXHostname: c.remoteHostname.ASCII,
 
843		ReceivingMXHelo:     c.remoteHelo,
 
844		ReceivingIP:         addrIP(c.origConn.RemoteAddr()),
 
845		FailedSessionCount:  1,
 
846		FailureReasonCode:   reasonCode,
 
850// tlsResultAdd adds TLS success/failure to all results.
 
851func (c *Client) tlsResultAdd(success, failure int64, err error) {
 
852	// Only track failure if not already done so in tls.Config.VerifyConnection.
 
853	var fds []tlsrpt.FailureDetails
 
854	var repErr reportedError
 
855	if err != nil && !errors.As(err, &repErr) {
 
856		resultType, reasonCode := tlsrpt.TLSFailureDetails(err)
 
857		fd := c.tlsrptFailureDetails(resultType, reasonCode)
 
858		fds = []tlsrpt.FailureDetails{fd}
 
860	c.tlsResultAddFailureDetails(success, failure, fds...)
 
863func (c *Client) tlsResultAddFailureDetails(success, failure int64, fds ...tlsrpt.FailureDetails) {
 
864	c.recipientDomainResult.Add(success, failure, fds...)
 
865	c.hostResult.Add(success, failure, fds...)
 
869func (c *Client) auth(auth func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error)) (rerr error) {
 
870	defer c.recover(&rerr)
 
873	c.cmdStart = time.Now()
 
875	mechanisms := make([]string, len(c.extAuthMechanisms))
 
876	for i, m := range c.extAuthMechanisms {
 
877		mechanisms[i] = strings.ToUpper(m)
 
879	a, err := auth(mechanisms, c.TLSConnectionState())
 
881		c.xerrorf(true, 0, "", "", "get authentication mechanism: %s, server supports %s", err, strings.Join(c.extAuthMechanisms, ", "))
 
883		c.xerrorf(true, 0, "", "", "no matching authentication mechanisms, server supports %s", strings.Join(c.extAuthMechanisms, ", "))
 
885	name, cleartextCreds := a.Info()
 
887	abort := func() (int, string, string) {
 
892		code, secode, lastline, _ := c.xread()
 
893		if code != smtp.C501BadParamSyntax {
 
896		return code, secode, lastline
 
899	toserver, last, err := a.Next(nil)
 
901		c.xerrorf(false, 0, "", "", "initial step in auth mechanism %s: %w", name, err)
 
904		defer c.xtrace(mlog.LevelTraceauth)()
 
907		c.xwriteline("AUTH " + name)
 
908	} else if len(toserver) == 0 {
 
911		c.xwriteline("AUTH " + name + " " + base64.StdEncoding.EncodeToString(toserver))
 
914		if cleartextCreds && last {
 
915			c.xtrace(mlog.LevelTrace) // Restore.
 
918		code, secode, lastLine, texts := c.xreadecode(last)
 
919		if code == smtp.C235AuthSuccess {
 
921				c.xerrorf(false, code, secode, lastLine, "server completed authentication earlier than client expected")
 
924		} else if code == smtp.C334ContinueAuth {
 
926				c.xerrorf(false, code, secode, lastLine, "server requested unexpected continuation of authentication")
 
930				c.xerrorf(false, code, secode, lastLine, "server responded with multiline contination")
 
932			fromserver, err := base64.StdEncoding.DecodeString(texts[0])
 
935				c.xerrorf(false, code, secode, lastLine, "malformed base64 data in authentication continuation response")
 
937			toserver, last, err = a.Next(fromserver)
 
939				// For failing SCRAM, the client stops due to message about invalid proof. The
 
940				// server still sends an authentication result (it probably should send 501
 
942				xcode, xsecode, lastline := abort()
 
943				c.xerrorf(false, xcode, xsecode, lastline, "client aborted authentication: %w", err)
 
945			c.xwriteline(base64.StdEncoding.EncodeToString(toserver))
 
947			c.xerrorf(code/100 == 5, code, secode, lastLine, "unexpected response during authentication, expected 334 continue or 235 auth success")
 
952// Supports8BITMIME returns whether the SMTP server supports the 8BITMIME
 
953// extension, needed for sending data with non-ASCII bytes.
 
954func (c *Client) Supports8BITMIME() bool {
 
958// SupportsSMTPUTF8 returns whether the SMTP server supports the SMTPUTF8
 
959// extension, needed for sending messages with UTF-8 in headers or in an (SMTP)
 
961func (c *Client) SupportsSMTPUTF8() bool {
 
965// SupportsStartTLS returns whether the SMTP server supports the STARTTLS
 
967func (c *Client) SupportsStartTLS() bool {
 
971// SupportsRequireTLS returns whether the SMTP server supports the REQUIRETLS
 
972// extension. The REQUIRETLS extension is only announced after enabling
 
974func (c *Client) SupportsRequireTLS() bool {
 
975	return c.extRequireTLS
 
978// TLSConnectionState returns TLS details if TLS is enabled, and nil otherwise.
 
979func (c *Client) TLSConnectionState() *tls.ConnectionState {
 
980	if tlsConn, ok := c.conn.(*tls.Conn); ok {
 
981		cs := tlsConn.ConnectionState()
 
987// Deliver attempts to deliver a message to a mail server.
 
989// mailFrom must be an email address, or empty in case of a DSN. rcptTo must be
 
992// If the message contains bytes with the high bit set, req8bitmime must be true. If
 
993// set, the remote server must support the 8BITMIME extension or delivery will
 
996// If the message is internationalized, e.g. when headers contain non-ASCII
 
997// character, or when UTF-8 is used in a localpart, reqSMTPUTF8 must be true. If set,
 
998// the remote server must support the SMTPUTF8 extension or delivery will fail.
 
1000// If requireTLS is true, the remote server must support the REQUIRETLS
 
1001// extension, or delivery will fail.
 
1003// Deliver uses the following SMTP extensions if the remote server supports them:
 
1004// 8BITMIME, SMTPUTF8, SIZE, PIPELINING, ENHANCEDSTATUSCODES, STARTTLS.
 
1006// Returned errors can be of type Error, one of the Err-variables in this package
 
1007// or other underlying errors, e.g. for i/o. Use errors.Is to check.
 
1008func (c *Client) Deliver(ctx context.Context, mailFrom string, rcptTo string, msgSize int64, msg io.Reader, req8bitmime, reqSMTPUTF8, requireTLS bool) (rerr error) {
 
1009	defer c.recover(&rerr)
 
1011	if c.origConn == nil {
 
1013	} else if c.botched {
 
1015	} else if c.needRset {
 
1016		if err := c.Reset(); err != nil {
 
1021	if !c.ext8bitmime && req8bitmime {
 
1022		// Temporary error, e.g. OpenBSD spamd does not announce 8bitmime support, but once
 
1023		// you get through, the mail server behind it probably does. Just needs a few
 
1025		c.xerrorf(false, 0, "", "", "%w", Err8bitmimeUnsupported)
 
1027	if !c.extSMTPUTF8 && reqSMTPUTF8 {
 
1029		c.xerrorf(false, 0, "", "", "%w", ErrSMTPUTF8Unsupported)
 
1031	if !c.extRequireTLS && requireTLS {
 
1032		c.xerrorf(false, 0, "", "", "%w", ErrRequireTLSUnsupported)
 
1035	if c.extSize && msgSize > c.maxSize {
 
1036		c.xerrorf(true, 0, "", "", "%w: message is %d bytes, remote has a %d bytes maximum size", ErrSize, msgSize, c.maxSize)
 
1039	var mailSize, bodyType string
 
1041		mailSize = fmt.Sprintf(" SIZE=%d", msgSize)
 
1045			bodyType = " BODY=8BITMIME"
 
1047			bodyType = " BODY=7BIT"
 
1050	var smtputf8Arg string
 
1053		smtputf8Arg = " SMTPUTF8"
 
1055	var requiretlsArg string
 
1058		requiretlsArg = " REQUIRETLS"
 
1065	lineMailFrom := fmt.Sprintf("MAIL FROM:<%s>%s%s%s%s", mailFrom, mailSize, bodyType, smtputf8Arg, requiretlsArg)
 
1066	lineRcptTo := fmt.Sprintf("RCPT TO:<%s>", rcptTo)
 
1068	// We are going into a transaction. We'll clear this when done.
 
1071	if c.extPipelining {
 
1072		c.cmds = []string{"mailfrom", "rcptto", "data"}
 
1073		c.cmdStart = time.Now()
 
1074		// todo future: write in a goroutine to prevent potential deadlock if remote does not consume our writes before expecting us to read. could potentially happen with greylisting and a small tcp send window?
 
1075		c.xbwriteline(lineMailFrom)
 
1076		c.xbwriteline(lineRcptTo)
 
1077		c.xbwriteline("DATA")
 
1080		// We read the response to RCPT TO and DATA without panic on read error. Servers
 
1081		// may be aborting the connection after a failed MAIL FROM, e.g. outlook when it
 
1082		// has blocklisted your IP. We don't want the read for the response to RCPT TO to
 
1083		// cause a read error as it would result in an unhelpful error message and a
 
1084		// temporary instead of permanent error code.
 
1086		mfcode, mfsecode, mflastline, _ := c.xread()
 
1087		rtcode, rtsecode, rtlastline, _, rterr := c.read()
 
1088		datacode, datasecode, datalastline, _, dataerr := c.read()
 
1090		if mfcode != smtp.C250Completed {
 
1091			c.xerrorf(mfcode/100 == 5, mfcode, mfsecode, mflastline, "%w: got %d, expected 2xx", ErrStatus, mfcode)
 
1096		if rtcode != smtp.C250Completed {
 
1097			c.xerrorf(rtcode/100 == 5, rtcode, rtsecode, rtlastline, "%w: got %d, expected 2xx", ErrStatus, rtcode)
 
1102		if datacode != smtp.C354Continue {
 
1103			c.xerrorf(datacode/100 == 5, datacode, datasecode, datalastline, "%w: got %d, expected 354", ErrStatus, datacode)
 
1106		c.cmds[0] = "mailfrom"
 
1107		c.cmdStart = time.Now()
 
1108		c.xwriteline(lineMailFrom)
 
1109		code, secode, lastline, _ := c.xread()
 
1110		if code != smtp.C250Completed {
 
1111			c.xerrorf(code/100 == 5, code, secode, lastline, "%w: got %d, expected 2xx", ErrStatus, code)
 
1114		c.cmds[0] = "rcptto"
 
1115		c.cmdStart = time.Now()
 
1116		c.xwriteline(lineRcptTo)
 
1117		code, secode, lastline, _ = c.xread()
 
1118		if code != smtp.C250Completed {
 
1119			c.xerrorf(code/100 == 5, code, secode, lastline, "%w: got %d, expected 2xx", ErrStatus, code)
 
1123		c.cmdStart = time.Now()
 
1124		c.xwriteline("DATA")
 
1125		code, secode, lastline, _ = c.xread()
 
1126		if code != smtp.C354Continue {
 
1127			c.xerrorf(code/100 == 5, code, secode, lastline, "%w: got %d, expected 354", ErrStatus, code)
 
1131	// For a DATA write, the suggested timeout is 3 minutes, we use 30 seconds for all
 
1133	defer c.xtrace(mlog.LevelTracedata)()
 
1134	err := smtp.DataWrite(c.w, msg)
 
1136		c.xbotchf(0, "", "", "writing message as smtp data: %w", err)
 
1139	c.xtrace(mlog.LevelTrace) // Restore.
 
1140	code, secode, lastline, _ := c.xread()
 
1141	if code != smtp.C250Completed {
 
1142		c.xerrorf(code/100 == 5, code, secode, lastline, "%w: got %d, expected 2xx", ErrStatus, code)
 
1149// Reset sends an SMTP RSET command to reset the message transaction state. Deliver
 
1150// automatically sends it if needed.
 
1151func (c *Client) Reset() (rerr error) {
 
1152	if c.origConn == nil {
 
1154	} else if c.botched {
 
1158	defer c.recover(&rerr)
 
1162	c.cmdStart = time.Now()
 
1163	c.xwriteline("RSET")
 
1164	code, secode, lastline, _ := c.xread()
 
1165	if code != smtp.C250Completed {
 
1166		c.xerrorf(code/100 == 5, code, secode, lastline, "%w: got %d, expected 2xx", ErrStatus, code)
 
1172// Botched returns whether this connection is botched, e.g. a protocol error
 
1173// occurred and the connection is in unknown state, and cannot be used for message
 
1175func (c *Client) Botched() bool {
 
1176	return c.botched || c.origConn == nil
 
1179// Close cleans up the client, closing the underlying connection.
 
1181// If the connection is initialized and not botched, a QUIT command is sent and the
 
1182// response read with a short timeout before closing the underlying connection.
 
1184// Close returns any error encountered during QUIT and closing.
 
1185func (c *Client) Close() (rerr error) {
 
1186	if c.origConn == nil {
 
1190	defer c.recover(&rerr)
 
1195		c.cmdStart = time.Now()
 
1196		c.xwriteline("QUIT")
 
1197		if err := c.conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
 
1198			c.log.Infox("setting read deadline for reading quit response", err)
 
1199		} else if _, err := bufs.Readline(c.log, c.r); err != nil {
 
1200			rerr = fmt.Errorf("reading response to quit command: %v", err)
 
1201			c.log.Debugx("reading quit response", err)
 
1205	err := c.origConn.Close()
 
1206	if c.conn != c.origConn {
 
1207		// This is the TLS connection. Close will attempt to write a close notification.
 
1208		// But it will fail quickly because the underlying socket was closed.
 
1219// Conn returns the connection with initialized SMTP session. Once the caller uses
 
1220// this connection it is in control, and responsible for closing the connection,
 
1221// and other functions on the client must not be called anymore.
 
1222func (c *Client) Conn() (net.Conn, error) {
 
1223	if err := c.conn.SetDeadline(time.Time{}); err != nil {
 
1224		return nil, fmt.Errorf("clearing io deadlines: %w", err)