1// Package smtpclient is an SMTP client, for submitting to an SMTP server or
2// delivering from a queue.
3//
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.
8//
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.
15//
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
26// client.Deliver.
27package smtpclient
28
29import (
30 "bufio"
31 "bytes"
32 "context"
33 "crypto/tls"
34 "crypto/x509"
35 "encoding/base64"
36 "errors"
37 "fmt"
38 "io"
39 "log/slog"
40 "net"
41 "reflect"
42 "strconv"
43 "strings"
44 "time"
45
46 "github.com/mjl-/adns"
47
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"
56)
57
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
59
60var (
61 MetricCommands stub.HistogramVec = stub.HistogramVecIgnore{}
62 MetricTLSRequiredNoIgnored stub.CounterVec = stub.CounterVecIgnore{}
63 MetricPanicInc = func() {}
64)
65
66var (
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")
76)
77
78// TLSMode indicates if TLS must, should or must not be used.
79type TLSMode string
80
81const (
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
84 // separately.
85 TLSImmediate TLSMode = "immediate"
86
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"
91
92 // Use TLS with STARTTLS if remote claims to support it.
93 TLSOpportunistic TLSMode = "opportunistic"
94
95 // TLS must not be attempted, e.g. due to earlier TLS handshake error.
96 TLSSkip TLSMode = "skip"
97)
98
99// Client is an SMTP client that can deliver messages to a mail server.
100//
101// Use New to make a new client.
102type Client struct {
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).
107 origConn net.Conn
108 conn net.Conn
109 tlsVerifyPKIX bool
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.
116 clientCert *tls.Certificate // If non-nil, tls client authentication is done.
117 tlsConfigOpts *tls.Config // If non-nil, tls config to use.
118
119 // TLS connection success/failure are added. These are always non-nil, regardless
120 // of what was passed in opts. It lets us unconditionally dereference them.
121 recipientDomainResult *tlsrpt.Result // Either "sts" or "no-policy-found".
122 hostResult *tlsrpt.Result // Either "dane" or "no-policy-found".
123
124 r *bufio.Reader
125 w *bufio.Writer
126 tr *moxio.TraceReader // Kept for changing trace levels between cmd/auth/data.
127 tw *moxio.TraceWriter
128 log mlog.Log
129 lastlog time.Time // For adding delta timestamps between log lines.
130 cmds []string // Last or active command, for generating errors and metrics.
131 cmdStart time.Time // Start of command.
132 tls bool // Whether connection is TLS protected.
133 firstReadAfterHandshake bool // To detect TLS alert error from remote just after handshake.
134
135 botched bool // If set, protocol is out of sync and no further commands can be sent.
136 needRset bool // If set, a new delivery requires an RSET command.
137
138 remoteHelo string // From 220 greeting line.
139 extEcodes bool // Remote server supports sending extended error codes.
140 extStartTLS bool // Remote server supports STARTTLS.
141 ext8bitmime bool
142 extSize bool // Remote server supports SIZE parameter. Must only be used if > 0.
143 maxSize int64 // Max size of email message.
144 extPipelining bool // Remote server supports command pipelining.
145 extSMTPUTF8 bool // Remote server supports SMTPUTF8 extension.
146 extAuthMechanisms []string // Supported authentication mechanisms.
147 extRequireTLS bool // Remote supports REQUIRETLS extension.
148 ExtLimits map[string]string // For LIMITS extension, only if present and valid, with uppercase keys.
149 ExtLimitMailMax int // Max "MAIL" commands in a connection, if > 0.
150 ExtLimitRcptMax int // Max "RCPT" commands in a transaction, if > 0.
151 ExtLimitRcptDomainMax int // Max unique domains in a connection, if > 0.
152}
153
154// Error represents a failure to deliver a message.
155//
156// Code, Secode, Command and Line are only set for SMTP-level errors, and are zero
157// values otherwise.
158type Error struct {
159 // Whether failure is permanent, typically because of 5xx response.
160 Permanent bool
161 // SMTP response status, e.g. 2xx for success, 4xx for transient error and 5xx for
162 // permanent failure.
163 Code int
164 // Short enhanced status, minus first digit and dot. Can be empty, e.g. for io
165 // errors or if remote does not send enhanced status codes. If remote responds with
166 // "550 5.7.1 ...", the Secode will be "7.1".
167 Secode string
168 // SMTP command causing failure.
169 Command string
170 // For errors due to SMTP responses, the full SMTP line excluding CRLF that caused
171 // the error. First line of a multi-line response.
172 Line string
173 // Optional additional lines in case of multi-line SMTP response. Most SMTP
174 // responses are single-line, leaving this field empty.
175 MoreLines []string
176 // Underlying error, e.g. one of the Err variables in this package, or io errors.
177 Err error
178}
179
180type Response Error
181
182// Unwrap returns the underlying Err.
183func (e Error) Unwrap() error {
184 return e.Err
185}
186
187// Error returns a readable error string.
188func (e Error) Error() string {
189 s := ""
190 if e.Err != nil {
191 s = e.Err.Error() + ", "
192 }
193 if e.Permanent {
194 s += "permanent"
195 } else {
196 s += "transient"
197 }
198 if e.Line != "" {
199 s += ": " + e.Line
200 }
201 return s
202}
203
204// Opts influence behaviour of Client.
205type Opts struct {
206 // If auth is non-nil, authentication will be done with the returned sasl client.
207 // The function should select the preferred mechanism. Mechanisms are in upper
208 // case.
209 //
210 // The TLS connection state can be used for the SCRAM PLUS mechanisms, binding the
211 // authentication exchange to a TLS connection. It is only present for TLS
212 // connections.
213 //
214 // If no mechanism is supported, a nil client and nil error can be returned, and
215 // the connection will fail.
216 Auth func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error)
217
218 DANERecords []adns.TLSA // If not nil, DANE records to verify.
219 DANEMoreHostnames []dns.Domain // For use with DANE, where additional certificate host names are allowed.
220 DANEVerifiedRecord *adns.TLSA // If non-empty, set to the DANE record that verified the TLS connection.
221
222 // If set, TLS verification errors (for DANE or PKIX) are ignored. Useful for
223 // delivering messages with message header "TLS-Required: No".
224 // Certificates are still verified, and results are still tracked for TLS
225 // reporting, but the connections will continue.
226 IgnoreTLSVerifyErrors bool
227
228 // If not nil, used instead of the system default roots for TLS PKIX verification.
229 RootCAs *x509.CertPool
230
231 // If set, the TLS client certificate authentication is done.
232 ClientCert *tls.Certificate
233
234 // TLS verification successes/failures is added to these TLS reporting results.
235 // Once the STARTTLS handshake is attempted, a successful/failed connection is
236 // tracked.
237 RecipientDomainResult *tlsrpt.Result // MTA-STS or no policy.
238 HostResult *tlsrpt.Result // DANE or no policy.
239
240 // If not nil, the TLS config to use instead of the default. Useful for custom
241 // certificate verification or TLS parameters. The other DANE/TLS/certificate
242 // fields in [Opts], and the tlsVerifyPKIX and remoteHostname parameters to [New]
243 // have no effect when TLSConfig is set.
244 TLSConfig *tls.Config
245}
246
247// New initializes an SMTP session on the given connection, returning a client that
248// can be used to deliver messages.
249//
250// New optionally starts TLS (for submission), reads the server greeting,
251// identifies itself with a HELO or EHLO command, initializes TLS with STARTTLS if
252// remote supports it and optionally authenticates. If successful, a client is
253// returned on which eventually Close must be called. Otherwise an error is
254// returned and the caller is responsible for closing the connection.
255//
256// Connecting to the correct host for delivery can be done using the Gather
257// functions, and with Dial. The queue managing outgoing messages typically decides
258// which host to deliver to, taking multiple MX records with preferences, other DNS
259// records, MTA-STS, retries and special cases into account.
260//
261// tlsMode indicates if and how TLS may/must (not) be used.
262//
263// tlsVerifyPKIX indicates if TLS certificates must be validated against the
264// PKIX/WebPKI certificate authorities (if TLS is done).
265//
266// DANE-verification is done when opts.DANERecords is not nil.
267//
268// TLS verification errors will be ignored if opts.IgnoreTLSVerification is set.
269//
270// If TLS is done, PKIX verification is always performed for tracking the results
271// for TLS reporting, but if tlsVerifyPKIX is false, the verification result does
272// not affect the connection.
273//
274// At the time of writing, delivery of email on the internet is done with
275// opportunistic TLS without PKIX verification by default. Recipient domains can
276// opt-in to PKIX verification by publishing an MTA-STS policy, or opt-in to DANE
277// verification by publishing DNSSEC-protected TLSA records in DNS.
278func New(ctx context.Context, elog *slog.Logger, conn net.Conn, tlsMode TLSMode, tlsVerifyPKIX bool, ehloHostname, remoteHostname dns.Domain, opts Opts) (*Client, error) {
279 ensureResult := func(r *tlsrpt.Result) *tlsrpt.Result {
280 if r == nil {
281 return &tlsrpt.Result{}
282 }
283 return r
284 }
285
286 c := &Client{
287 origConn: conn,
288 tlsVerifyPKIX: tlsVerifyPKIX,
289 ignoreTLSVerifyErrors: opts.IgnoreTLSVerifyErrors,
290 rootCAs: opts.RootCAs,
291 remoteHostname: remoteHostname,
292 daneRecords: opts.DANERecords,
293 daneMoreHostnames: opts.DANEMoreHostnames,
294 daneVerifiedRecord: opts.DANEVerifiedRecord,
295 clientCert: opts.ClientCert,
296 lastlog: time.Now(),
297 cmds: []string{"(none)"},
298 recipientDomainResult: ensureResult(opts.RecipientDomainResult),
299 hostResult: ensureResult(opts.HostResult),
300 tlsConfigOpts: opts.TLSConfig,
301 }
302 c.log = mlog.New("smtpclient", elog).WithFunc(func() []slog.Attr {
303 now := time.Now()
304 l := []slog.Attr{
305 slog.Duration("delta", now.Sub(c.lastlog)),
306 }
307 c.lastlog = now
308 return l
309 })
310
311 if tlsMode == TLSImmediate {
312 config := c.tlsConfig()
313 tlsconn := tls.Client(conn, config)
314 // The tlsrpt tracking isn't used by caller, but won't hurt.
315 if err := tlsconn.HandshakeContext(ctx); err != nil {
316 c.tlsResultAdd(0, 1, err)
317 return nil, err
318 }
319 c.firstReadAfterHandshake = true
320 c.tlsResultAdd(1, 0, nil)
321 c.conn = tlsconn
322 version, ciphersuite := moxio.TLSInfo(tlsconn.ConnectionState())
323 c.log.Debug("tls client handshake done",
324 slog.String("version", version),
325 slog.String("ciphersuite", ciphersuite),
326 slog.Any("servername", remoteHostname))
327 c.tls = true
328 } else {
329 c.conn = conn
330 }
331
332 // We don't wrap reads in a timeoutReader for fear of an optional TLS wrapper doing
333 // reads without the client asking for it. Such reads could result in a timeout
334 // error.
335 c.tr = moxio.NewTraceReader(c.log, "RS: ", c.conn)
336 c.r = bufio.NewReader(c.tr)
337 // We use a single write timeout of 30 seconds.
338 // todo future: use different timeouts ../rfc/5321:3610
339 c.tw = moxio.NewTraceWriter(c.log, "LC: ", timeoutWriter{c.conn, 30 * time.Second, c.log})
340 c.w = bufio.NewWriter(c.tw)
341
342 if err := c.hello(ctx, tlsMode, ehloHostname, opts.Auth); err != nil {
343 return nil, err
344 }
345 return c, nil
346}
347
348// reportedError wraps an error while indicating it was already tracked for TLS
349// reporting.
350type reportedError struct{ err error }
351
352func (e reportedError) Error() string {
353 return e.err.Error()
354}
355
356func (e reportedError) Unwrap() error {
357 return e.err
358}
359
360func (c *Client) tlsConfig() *tls.Config {
361 // We always manage verification ourselves: We need to report in detail about
362 // failures. And we may have to verify both PKIX and DANE, record errors for
363 // each, and possibly ignore the errors.
364
365 if c.tlsConfigOpts != nil {
366 return c.tlsConfigOpts
367 }
368
369 verifyConnection := func(cs tls.ConnectionState) error {
370 // Collect verification errors. If there are none at the end, TLS validation
371 // succeeded. We may find validation problems below, record them for a TLS report
372 // but continue due to policies. We track the TLS reporting result in this
373 // function, wrapping errors in a reportedError.
374 var daneErr, pkixErr error
375
376 // DANE verification.
377 // daneRecords can be non-nil and empty, that's intended.
378 if c.daneRecords != nil {
379 verified, record, err := dane.Verify(c.log.Logger, c.daneRecords, cs, c.remoteHostname, c.daneMoreHostnames, c.rootCAs)
380 c.log.Debugx("dane verification", err, slog.Bool("verified", verified), slog.Any("record", record))
381 if verified {
382 if c.daneVerifiedRecord != nil {
383 *c.daneVerifiedRecord = record
384 }
385 } else {
386 // Track error for reports.
387 // 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
388 fd := c.tlsrptFailureDetails(tlsrpt.ResultValidationFailure, "dane-no-match")
389 if err != nil {
390 // 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.
391
392 // We may have encountered errors while evaluation some of the TLSA records.
393 fd.FailureReasonCode += "+errors"
394 }
395 c.hostResult.Add(0, 0, fd)
396
397 if c.ignoreTLSVerifyErrors {
398 // We ignore the failure and continue the connection.
399 c.log.Infox("verifying dane failed, continuing with connection", err)
400 MetricTLSRequiredNoIgnored.IncLabels("daneverification")
401 } else {
402 // This connection will fail.
403 daneErr = dane.ErrNoMatch
404 }
405 }
406 }
407
408 // PKIX verification.
409 opts := x509.VerifyOptions{
410 DNSName: cs.ServerName,
411 Intermediates: x509.NewCertPool(),
412 Roots: c.rootCAs,
413 }
414 for _, cert := range cs.PeerCertificates[1:] {
415 opts.Intermediates.AddCert(cert)
416 }
417 if _, err := cs.PeerCertificates[0].Verify(opts); err != nil {
418 resultType, reasonCode := tlsrpt.TLSFailureDetails(err)
419 fd := c.tlsrptFailureDetails(resultType, reasonCode)
420 c.recipientDomainResult.Add(0, 0, fd)
421
422 if c.tlsVerifyPKIX && !c.ignoreTLSVerifyErrors {
423 pkixErr = err
424 }
425 }
426
427 if daneErr != nil && pkixErr != nil {
428 return reportedError{errors.Join(daneErr, pkixErr)}
429 } else if daneErr != nil {
430 return reportedError{daneErr}
431 } else if pkixErr != nil {
432 return reportedError{pkixErr}
433 }
434 return nil
435 }
436
437 var certs []tls.Certificate
438 if c.clientCert != nil {
439 certs = []tls.Certificate{*c.clientCert}
440 }
441
442 return &tls.Config{
443 ServerName: c.remoteHostname.ASCII, // For SNI.
444 // todo: possibly accept older TLS versions for TLSOpportunistic? or would our private key be at risk?
445 MinVersion: tls.VersionTLS12, // ../rfc/8996:31 ../rfc/8997:66
446 InsecureSkipVerify: true, // VerifyConnection below is called and will do all verification.
447 VerifyConnection: verifyConnection,
448 Certificates: certs,
449 }
450}
451
452// xbotchf generates a temporary error and marks the client as botched. e.g. for
453// i/o errors or invalid protocol messages.
454func (c *Client) xbotchf(code int, secode string, firstLine string, moreLines []string, format string, args ...any) {
455 panic(c.botchf(code, secode, firstLine, moreLines, format, args...))
456}
457
458// botchf generates a temporary error and marks the client as botched. e.g. for
459// i/o errors or invalid protocol messages.
460func (c *Client) botchf(code int, secode string, firstLine string, moreLines []string, format string, args ...any) error {
461 c.botched = true
462 return c.errorf(false, code, secode, firstLine, moreLines, format, args...)
463}
464
465func (c *Client) errorf(permanent bool, code int, secode, firstLine string, moreLines []string, format string, args ...any) error {
466 var cmd string
467 if len(c.cmds) > 0 {
468 cmd = c.cmds[0]
469 }
470 return Error{permanent, code, secode, cmd, firstLine, moreLines, fmt.Errorf(format, args...)}
471}
472
473func (c *Client) xerrorf(permanent bool, code int, secode, firstLine string, moreLines []string, format string, args ...any) {
474 panic(c.errorf(permanent, code, secode, firstLine, moreLines, format, args...))
475}
476
477// timeoutWriter passes each Write on to conn after setting a write deadline on conn based on
478// timeout.
479type timeoutWriter struct {
480 conn net.Conn
481 timeout time.Duration
482 log mlog.Log
483}
484
485func (w timeoutWriter) Write(buf []byte) (int, error) {
486 if err := w.conn.SetWriteDeadline(time.Now().Add(w.timeout)); err != nil {
487 w.log.Errorx("setting write deadline", err)
488 }
489
490 return w.conn.Write(buf)
491}
492
493var bufs = moxio.NewBufpool(8, 2*1024)
494
495func (c *Client) readline() (string, error) {
496 // todo: could have per-operation timeouts. and rfc suggests higher minimum timeouts. ../rfc/5321:3610
497 if err := c.conn.SetReadDeadline(time.Now().Add(30 * time.Second)); err != nil {
498 c.log.Errorx("setting read deadline", err)
499 }
500
501 line, err := bufs.Readline(c.log, c.r)
502 if err != nil {
503 // See if this is a TLS alert from remote, and one other than 0 (which notifies
504 // that the connection is being closed. If so, we register a TLS connection
505 // failure. This handles TLS alerts that happen just after a successful handshake.
506 var netErr *net.OpError
507 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 {
508 resultType, reasonCode := tlsrpt.TLSFailureDetails(err)
509 // We count -1 success to compensate for the assumed success right after the handshake.
510 c.tlsResultAddFailureDetails(-1, 1, c.tlsrptFailureDetails(resultType, reasonCode))
511 }
512
513 return line, c.botchf(0, "", "", nil, "%s: %w", strings.Join(c.cmds, ","), err)
514 }
515 c.firstReadAfterHandshake = false
516 return line, nil
517}
518
519func (c *Client) xtrace(level slog.Level) func() {
520 c.xflush()
521 c.tr.SetTrace(level)
522 c.tw.SetTrace(level)
523 return func() {
524 c.xflush()
525 c.tr.SetTrace(mlog.LevelTrace)
526 c.tw.SetTrace(mlog.LevelTrace)
527 }
528}
529
530func (c *Client) xwritelinef(format string, args ...any) {
531 c.xbwritelinef(format, args...)
532 c.xflush()
533}
534
535func (c *Client) xwriteline(line string) {
536 c.xbwriteline(line)
537 c.xflush()
538}
539
540func (c *Client) xbwritelinef(format string, args ...any) {
541 c.xbwriteline(fmt.Sprintf(format, args...))
542}
543
544func (c *Client) xbwriteline(line string) {
545 _, err := fmt.Fprintf(c.w, "%s\r\n", line)
546 if err != nil {
547 c.xbotchf(0, "", "", nil, "write: %w", err)
548 }
549}
550
551func (c *Client) xflush() {
552 err := c.w.Flush()
553 if err != nil {
554 c.xbotchf(0, "", "", nil, "writes: %w", err)
555 }
556}
557
558// read response, possibly multiline, with supporting extended codes based on configuration in client.
559func (c *Client) xread() (code int, secode, firstLine string, moreLines []string) {
560 var err error
561 code, secode, firstLine, moreLines, err = c.read()
562 if err != nil {
563 panic(err)
564 }
565 return
566}
567
568func (c *Client) read() (code int, secode, firstLine string, moreLines []string, rerr error) {
569 code, secode, _, firstLine, moreLines, _, rerr = c.readecode(c.extEcodes)
570 return
571}
572
573// read response, possibly multiline.
574// if ecodes, extended codes are parsed.
575func (c *Client) readecode(ecodes bool) (code int, secode, lastText, firstLine string, moreLines, moreTexts []string, rerr error) {
576 first := true
577 for {
578 co, sec, text, line, last, err := c.read1(ecodes)
579 if first {
580 firstLine = line
581 first = false
582 } else if line != "" {
583 moreLines = append(moreLines, line)
584 if text != "" {
585 moreTexts = append(moreTexts, text)
586 }
587 }
588 if err != nil {
589 rerr = err
590 return
591 }
592 if code != 0 && co != code {
593 // ../rfc/5321:2771
594 err := c.botchf(0, "", firstLine, moreLines, "%w: multiline response with different codes, previous %d, last %d", ErrProtocol, code, co)
595 return 0, "", "", "", nil, nil, err
596 }
597 code = co
598 if last {
599 if code != smtp.C334ContinueAuth {
600 cmd := ""
601 if len(c.cmds) > 0 {
602 cmd = c.cmds[0]
603 // We only keep the last, so we're not creating new slices all the time.
604 if len(c.cmds) > 1 {
605 c.cmds = c.cmds[1:]
606 }
607 }
608 MetricCommands.ObserveLabels(float64(time.Since(c.cmdStart))/float64(time.Second), cmd, fmt.Sprintf("%d", co), sec)
609 c.log.Debug("smtpclient command result",
610 slog.String("cmd", cmd),
611 slog.Int("code", co),
612 slog.String("secode", sec),
613 slog.Duration("duration", time.Since(c.cmdStart)))
614 }
615 return co, sec, text, firstLine, moreLines, moreTexts, nil
616 }
617 }
618}
619
620func (c *Client) xreadecode(ecodes bool) (code int, secode, lastText, firstLine string, moreLines, moreTexts []string) {
621 var err error
622 code, secode, lastText, firstLine, moreLines, moreTexts, err = c.readecode(ecodes)
623 if err != nil {
624 panic(err)
625 }
626 return
627}
628
629// read single response line.
630// if ecodes, extended codes are parsed.
631func (c *Client) read1(ecodes bool) (code int, secode, text, line string, last bool, rerr error) {
632 line, rerr = c.readline()
633 if rerr != nil {
634 return
635 }
636 i := 0
637 for ; i < len(line) && line[i] >= '0' && line[i] <= '9'; i++ {
638 }
639 if i != 3 {
640 rerr = c.botchf(0, "", line, nil, "%w: expected response code: %s", ErrProtocol, line)
641 return
642 }
643 v, err := strconv.ParseInt(line[:i], 10, 32)
644 if err != nil {
645 rerr = c.botchf(0, "", line, nil, "%w: bad response code (%s): %s", ErrProtocol, err, line)
646 return
647 }
648 code = int(v)
649 major := code / 100
650 s := line[3:]
651 if strings.HasPrefix(s, "-") || strings.HasPrefix(s, " ") {
652 last = s[0] == ' '
653 s = s[1:]
654 } else if s == "" {
655 // Allow missing space. ../rfc/5321:2570 ../rfc/5321:2612
656 last = true
657 } else {
658 rerr = c.botchf(0, "", line, nil, "%w: expected space or dash after response code: %s", ErrProtocol, line)
659 return
660 }
661
662 if ecodes {
663 secode, s = parseEcode(major, s)
664 }
665
666 return code, secode, s, line, last, nil
667}
668
669func parseEcode(major int, s string) (secode string, remain string) {
670 o := 0
671 bad := false
672 take := func(need bool, a, b byte) bool {
673 if !bad && o < len(s) && s[o] >= a && s[o] <= b {
674 o++
675 return true
676 }
677 bad = bad || need
678 return false
679 }
680 digit := func(need bool) bool {
681 return take(need, '0', '9')
682 }
683 dot := func() bool {
684 return take(true, '.', '.')
685 }
686
687 digit(true)
688 dot()
689 xo := o
690 digit(true)
691 for digit(false) {
692 }
693 dot()
694 digit(true)
695 for digit(false) {
696 }
697 secode = s[xo:o]
698 take(false, ' ', ' ')
699 if bad || int(s[0])-int('0') != major {
700 return "", s
701 }
702 return secode, s[o:]
703}
704
705func (c *Client) recover(rerr *error) {
706 x := recover()
707 if x == nil {
708 return
709 }
710 cerr, ok := x.(Error)
711 if !ok {
712 MetricPanicInc()
713 panic(x)
714 }
715 *rerr = cerr
716}
717
718func (c *Client) hello(ctx context.Context, tlsMode TLSMode, ehloHostname dns.Domain, auth func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error)) (rerr error) {
719 defer c.recover(&rerr)
720
721 // perform EHLO handshake, falling back to HELO if server does not appear to
722 // implement EHLO.
723 hello := func(heloOK bool) {
724 // Write EHLO and parse the supported extensions.
725 // ../rfc/5321:987
726 c.cmds[0] = "ehlo"
727 c.cmdStart = time.Now()
728 // Syntax: ../rfc/5321:1827
729 c.xwritelinef("EHLO %s", ehloHostname.ASCII)
730 code, _, _, firstLine, moreLines, moreTexts := c.xreadecode(false)
731 switch code {
732 // ../rfc/5321:997
733 // ../rfc/5321:3098
734 case smtp.C500BadSyntax, smtp.C501BadParamSyntax, smtp.C502CmdNotImpl, smtp.C503BadCmdSeq, smtp.C504ParamNotImpl:
735 if !heloOK {
736 c.xerrorf(true, code, "", firstLine, moreLines, "%w: remote claims ehlo is not supported", ErrProtocol)
737 }
738 // ../rfc/5321:996
739 c.cmds[0] = "helo"
740 c.cmdStart = time.Now()
741 c.xwritelinef("HELO %s", ehloHostname.ASCII)
742 code, _, _, firstLine, _, _ = c.xreadecode(false)
743 if code != smtp.C250Completed {
744 c.xerrorf(code/100 == 5, code, "", firstLine, moreLines, "%w: expected 250 to HELO, got %d", ErrStatus, code)
745 }
746 return
747 case smtp.C250Completed:
748 default:
749 c.xerrorf(code/100 == 5, code, "", firstLine, moreLines, "%w: expected 250, got %d", ErrStatus, code)
750 }
751 for _, s := range moreTexts {
752 // ../rfc/5321:1869
753 s = strings.ToUpper(strings.TrimSpace(s))
754 switch s {
755 case "STARTTLS":
756 c.extStartTLS = true
757 case "ENHANCEDSTATUSCODES":
758 c.extEcodes = true
759 case "8BITMIME":
760 c.ext8bitmime = true
761 case "PIPELINING":
762 c.extPipelining = true
763 case "REQUIRETLS":
764 c.extRequireTLS = true
765 default:
766 // For SMTPUTF8 we must ignore any parameter. ../rfc/6531:207
767 if s == "SMTPUTF8" || strings.HasPrefix(s, "SMTPUTF8 ") {
768 c.extSMTPUTF8 = true
769 } else if strings.HasPrefix(s, "SIZE ") {
770 // ../rfc/1870:77
771 c.extSize = true
772 if v, err := strconv.ParseInt(s[len("SIZE "):], 10, 64); err == nil {
773 c.maxSize = v
774 }
775 } else if strings.HasPrefix(s, "AUTH ") {
776 c.extAuthMechanisms = strings.Split(s[len("AUTH "):], " ")
777 } else if strings.HasPrefix(s, "LIMITS ") {
778 c.ExtLimits, c.ExtLimitMailMax, c.ExtLimitRcptMax, c.ExtLimitRcptDomainMax = parseLimits([]byte(s[len("LIMITS"):]))
779 }
780 }
781 }
782 }
783
784 // Read greeting.
785 c.cmds = []string{"(greeting)"}
786 c.cmdStart = time.Now()
787 code, _, _, firstLine, moreLines, _ := c.xreadecode(false)
788 if code != smtp.C220ServiceReady {
789 c.xerrorf(code/100 == 5, code, "", firstLine, moreLines, "%w: expected 220, got %d", ErrStatus, code)
790 }
791 // ../rfc/5321:2588
792 _, c.remoteHelo, _ = strings.Cut(firstLine, " ")
793
794 // Write EHLO, falling back to HELO if server doesn't appear to support it.
795 hello(true)
796
797 // Attempt TLS if remote understands STARTTLS and we aren't doing immediate TLS or if caller requires it.
798 if c.extStartTLS && tlsMode == TLSOpportunistic || tlsMode == TLSRequiredStartTLS {
799 c.log.Debug("starting tls client", slog.Any("tlsmode", tlsMode), slog.Any("servername", c.remoteHostname))
800 c.cmds[0] = "starttls"
801 c.cmdStart = time.Now()
802 c.xwritelinef("STARTTLS")
803 code, secode, firstLine, _ := c.xread()
804 // ../rfc/3207:107
805 if code != smtp.C220ServiceReady {
806 c.tlsResultAddFailureDetails(0, 1, c.tlsrptFailureDetails(tlsrpt.ResultSTARTTLSNotSupported, fmt.Sprintf("smtp-starttls-reply-code-%d", code)))
807 c.xerrorf(code/100 == 5, code, secode, firstLine, moreLines, "%w: STARTTLS: got %d, expected 220", ErrTLS, code)
808 }
809
810 // We don't want to do TLS on top of c.r because it also prints protocol traces: We
811 // don't want to log the TLS stream. So we'll do TLS on the underlying connection,
812 // but make sure any bytes already read and in the buffer are used for the TLS
813 // handshake.
814 conn := c.conn
815 if n := c.r.Buffered(); n > 0 {
816 conn = &moxio.PrefixConn{
817 PrefixReader: io.LimitReader(c.r, int64(n)),
818 Conn: conn,
819 }
820 }
821
822 tlsConfig := c.tlsConfig()
823 nconn := tls.Client(conn, tlsConfig)
824 c.conn = nconn
825
826 nctx, cancel := context.WithTimeout(ctx, time.Minute)
827 defer cancel()
828 err := nconn.HandshakeContext(nctx)
829 if err != nil {
830 // For each STARTTLS failure, we track a failed TLS session. For deliveries with
831 // multiple MX targets, we may add multiple failures, and delivery may succeed with
832 // a later MX target with which we can do STARTTLS. ../rfc/8460:524
833 c.tlsResultAdd(0, 1, err)
834 c.xerrorf(false, 0, "", "", nil, "%w: STARTTLS TLS handshake: %s", ErrTLS, err)
835 }
836 c.firstReadAfterHandshake = true
837 cancel()
838 c.tr = moxio.NewTraceReader(c.log, "RS: ", c.conn)
839 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.
840 c.r = bufio.NewReader(c.tr)
841 c.w = bufio.NewWriter(c.tw)
842
843 version, ciphersuite := moxio.TLSInfo(nconn.ConnectionState())
844 c.log.Debug("starttls client handshake done",
845 slog.Any("tlsmode", tlsMode),
846 slog.Bool("verifypkix", c.tlsVerifyPKIX),
847 slog.Bool("verifydane", c.daneRecords != nil),
848 slog.Bool("ignoretlsverifyerrors", c.ignoreTLSVerifyErrors),
849 slog.String("version", version),
850 slog.String("ciphersuite", ciphersuite),
851 slog.Any("servername", c.remoteHostname),
852 slog.Any("danerecord", c.daneVerifiedRecord))
853 c.tls = true
854 // Track successful TLS connection. ../rfc/8460:515
855 c.tlsResultAdd(1, 0, nil)
856
857 hello(false)
858 } else if tlsMode == TLSOpportunistic {
859 // Result: ../rfc/8460:538
860 c.tlsResultAddFailureDetails(0, 0, c.tlsrptFailureDetails(tlsrpt.ResultSTARTTLSNotSupported, ""))
861 }
862
863 if auth != nil {
864 return c.auth(auth)
865 }
866 return
867}
868
869// parse text after "LIMITS", including leading space.
870func parseLimits(b []byte) (map[string]string, int, int, int) {
871 // ../rfc/9422:150
872 var o int
873 // Read next " name=value".
874 pair := func() ([]byte, []byte) {
875 if o >= len(b) || b[o] != ' ' {
876 return nil, nil
877 }
878 o++
879
880 ns := o
881 for o < len(b) {
882 c := b[o]
883 if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '-' || c == '_' {
884 o++
885 } else {
886 break
887 }
888 }
889 es := o
890 if ns == es || o >= len(b) || b[o] != '=' {
891 return nil, nil
892 }
893 o++
894 vs := o
895 for o < len(b) {
896 c := b[o]
897 if c > 0x20 && c < 0x7f && c != ';' {
898 o++
899 } else {
900 break
901 }
902 }
903 if vs == o {
904 return nil, nil
905 }
906 return b[ns:es], b[vs:o]
907 }
908 limits := map[string]string{}
909 var mailMax, rcptMax, rcptDomainMax int
910 for o < len(b) {
911 name, value := pair()
912 if name == nil {
913 // We skip the entire LIMITS extension for syntax errors. ../rfc/9422:232
914 return nil, 0, 0, 0
915 }
916 k := strings.ToUpper(string(name))
917 if _, ok := limits[k]; ok {
918 // Not specified, but we treat duplicates as error.
919 return nil, 0, 0, 0
920 }
921 limits[k] = string(value)
922 // For individual value syntax errors, we skip that value, leaving the default 0.
923 // ../rfc/9422:254
924 switch string(name) {
925 case "MAILMAX":
926 if v, err := strconv.Atoi(string(value)); err == nil && v > 0 && len(value) <= 6 {
927 mailMax = v
928 }
929 case "RCPTMAX":
930 if v, err := strconv.Atoi(string(value)); err == nil && v > 0 && len(value) <= 6 {
931 rcptMax = v
932 }
933 case "RCPTDOMAINMAX":
934 if v, err := strconv.Atoi(string(value)); err == nil && v > 0 && len(value) <= 6 {
935 rcptDomainMax = v
936 }
937 }
938 }
939 return limits, mailMax, rcptMax, rcptDomainMax
940}
941
942func addrIP(addr net.Addr) string {
943 if t, ok := addr.(*net.TCPAddr); ok {
944 return t.IP.String()
945 }
946 host, _, _ := net.SplitHostPort(addr.String())
947 ip := net.ParseIP(host)
948 if ip == nil {
949 return "" // For pipe during tests.
950 }
951 return ip.String()
952}
953
954// tlsrptFailureDetails returns FailureDetails with connection details (such as
955// IP addresses) for inclusion in a TLS report.
956func (c *Client) tlsrptFailureDetails(resultType tlsrpt.ResultType, reasonCode string) tlsrpt.FailureDetails {
957 return tlsrpt.FailureDetails{
958 ResultType: resultType,
959 SendingMTAIP: addrIP(c.origConn.LocalAddr()),
960 ReceivingMXHostname: c.remoteHostname.ASCII,
961 ReceivingMXHelo: c.remoteHelo,
962 ReceivingIP: addrIP(c.origConn.RemoteAddr()),
963 FailedSessionCount: 1,
964 FailureReasonCode: reasonCode,
965 }
966}
967
968// tlsResultAdd adds TLS success/failure to all results.
969func (c *Client) tlsResultAdd(success, failure int64, err error) {
970 // Only track failure if not already done so in tls.Config.VerifyConnection.
971 var fds []tlsrpt.FailureDetails
972 var repErr reportedError
973 if err != nil && !errors.As(err, &repErr) {
974 resultType, reasonCode := tlsrpt.TLSFailureDetails(err)
975 fd := c.tlsrptFailureDetails(resultType, reasonCode)
976 fds = []tlsrpt.FailureDetails{fd}
977 }
978 c.tlsResultAddFailureDetails(success, failure, fds...)
979}
980
981func (c *Client) tlsResultAddFailureDetails(success, failure int64, fds ...tlsrpt.FailureDetails) {
982 c.recipientDomainResult.Add(success, failure, fds...)
983 c.hostResult.Add(success, failure, fds...)
984}
985
986// ../rfc/4954:139
987func (c *Client) auth(auth func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error)) (rerr error) {
988 defer c.recover(&rerr)
989
990 c.cmds[0] = "auth"
991 c.cmdStart = time.Now()
992
993 mechanisms := make([]string, len(c.extAuthMechanisms))
994 for i, m := range c.extAuthMechanisms {
995 mechanisms[i] = strings.ToUpper(m)
996 }
997 a, err := auth(mechanisms, c.TLSConnectionState())
998 if err != nil {
999 c.xerrorf(true, 0, "", "", nil, "get authentication mechanism: %s, server supports %s", err, strings.Join(c.extAuthMechanisms, ", "))
1000 } else if a == nil {
1001 c.xerrorf(true, 0, "", "", nil, "no matching authentication mechanisms, server supports %s", strings.Join(c.extAuthMechanisms, ", "))
1002 }
1003 name, cleartextCreds := a.Info()
1004
1005 abort := func() (int, string, string, []string) {
1006 // Abort authentication. ../rfc/4954:193
1007 c.xwriteline("*")
1008
1009 // Server must respond with 501. // ../rfc/4954:195
1010 code, secode, firstLine, moreLines := c.xread()
1011 if code != smtp.C501BadParamSyntax {
1012 c.botched = true
1013 }
1014 return code, secode, firstLine, moreLines
1015 }
1016
1017 toserver, last, err := a.Next(nil)
1018 if err != nil {
1019 c.xerrorf(false, 0, "", "", nil, "initial step in auth mechanism %s: %w", name, err)
1020 }
1021 if cleartextCreds {
1022 defer c.xtrace(mlog.LevelTraceauth)()
1023 }
1024 if toserver == nil {
1025 c.xwriteline("AUTH " + name)
1026 } else if len(toserver) == 0 {
1027 c.xwriteline("AUTH " + name + " =") // ../rfc/4954:214
1028 } else {
1029 c.xwriteline("AUTH " + name + " " + base64.StdEncoding.EncodeToString(toserver))
1030 }
1031 for {
1032 if cleartextCreds && last {
1033 c.xtrace(mlog.LevelTrace) // Restore.
1034 }
1035
1036 code, secode, lastText, firstLine, moreLines, _ := c.xreadecode(last)
1037 if code == smtp.C235AuthSuccess {
1038 if !last {
1039 c.xerrorf(false, code, secode, firstLine, moreLines, "server completed authentication earlier than client expected")
1040 }
1041 return nil
1042 } else if code == smtp.C334ContinueAuth {
1043 if last {
1044 c.xerrorf(false, code, secode, firstLine, moreLines, "server requested unexpected continuation of authentication")
1045 }
1046 if len(moreLines) > 0 {
1047 abort()
1048 c.xerrorf(false, code, secode, firstLine, moreLines, "server responded with multiline contination")
1049 }
1050 fromserver, err := base64.StdEncoding.DecodeString(lastText)
1051 if err != nil {
1052 abort()
1053 c.xerrorf(false, code, secode, firstLine, moreLines, "malformed base64 data in authentication continuation response")
1054 }
1055 toserver, last, err = a.Next(fromserver)
1056 if err != nil {
1057 // For failing SCRAM, the client stops due to message about invalid proof. The
1058 // server still sends an authentication result (it probably should send 501
1059 // instead).
1060 xcode, xsecode, xfirstLine, xmoreLines := abort()
1061 c.xerrorf(false, xcode, xsecode, xfirstLine, xmoreLines, "client aborted authentication: %w", err)
1062 }
1063 c.xwriteline(base64.StdEncoding.EncodeToString(toserver))
1064 } else {
1065 c.xerrorf(code/100 == 5, code, secode, firstLine, moreLines, "unexpected response during authentication, expected 334 continue or 235 auth success")
1066 }
1067 }
1068}
1069
1070// Supports8BITMIME returns whether the SMTP server supports the 8BITMIME
1071// extension, needed for sending data with non-ASCII bytes.
1072func (c *Client) Supports8BITMIME() bool {
1073 return c.ext8bitmime
1074}
1075
1076// SupportsSMTPUTF8 returns whether the SMTP server supports the SMTPUTF8
1077// extension, needed for sending messages with UTF-8 in headers or in an (SMTP)
1078// address.
1079func (c *Client) SupportsSMTPUTF8() bool {
1080 return c.extSMTPUTF8
1081}
1082
1083// SupportsStartTLS returns whether the SMTP server supports the STARTTLS
1084// extension.
1085func (c *Client) SupportsStartTLS() bool {
1086 return c.extStartTLS
1087}
1088
1089// SupportsRequireTLS returns whether the SMTP server supports the REQUIRETLS
1090// extension. The REQUIRETLS extension is only announced after enabling
1091// STARTTLS.
1092func (c *Client) SupportsRequireTLS() bool {
1093 return c.extRequireTLS
1094}
1095
1096// TLSConnectionState returns TLS details if TLS is enabled, and nil otherwise.
1097func (c *Client) TLSConnectionState() *tls.ConnectionState {
1098 if tlsConn, ok := c.conn.(*tls.Conn); ok {
1099 cs := tlsConn.ConnectionState()
1100 return &cs
1101 }
1102 return nil
1103}
1104
1105// Deliver attempts to deliver a message to a mail server.
1106//
1107// mailFrom must be an email address, or empty in case of a DSN. rcptTo must be
1108// an email address.
1109//
1110// If the message contains bytes with the high bit set, req8bitmime should be true.
1111// If set, the remote server must support the 8BITMIME extension or delivery will
1112// fail.
1113//
1114// If the message is internationalized, e.g. when headers contain non-ASCII
1115// character, or when UTF-8 is used in a localpart, reqSMTPUTF8 must be true. If set,
1116// the remote server must support the SMTPUTF8 extension or delivery will fail.
1117//
1118// If requireTLS is true, the remote server must support the REQUIRETLS
1119// extension, or delivery will fail.
1120//
1121// Deliver uses the following SMTP extensions if the remote server supports them:
1122// 8BITMIME, SMTPUTF8, SIZE, PIPELINING, ENHANCEDSTATUSCODES, STARTTLS.
1123//
1124// Returned errors can be of type Error, one of the Err-variables in this package
1125// or other underlying errors, e.g. for i/o. Use errors.Is to check.
1126func (c *Client) Deliver(ctx context.Context, mailFrom string, rcptTo string, msgSize int64, msg io.Reader, req8bitmime, reqSMTPUTF8, requireTLS bool) (rerr error) {
1127 _, err := c.DeliverMultiple(ctx, mailFrom, []string{rcptTo}, msgSize, msg, req8bitmime, reqSMTPUTF8, requireTLS)
1128 return err
1129}
1130
1131var errNoRecipientsPipelined = errors.New("no recipients accepted in pipelined transaction")
1132var errNoRecipients = errors.New("no recipients accepted in transaction")
1133
1134// DeliverMultiple is like Deliver, but attempts to deliver a message to multiple
1135// recipients. Errors about the entire transaction, such as i/o errors or error
1136// responses to the MAIL FROM or DATA commands, are returned by a non-nil rerr. If
1137// rcptTo has a single recipient, an error to the RCPT TO command is returned in
1138// rerr instead of rcptResps. Otherwise, the SMTP response for each recipient is
1139// returned in rcptResps.
1140//
1141// The caller should take extLimit* into account when sending. And recognize
1142// recipient response code "452" to mean that a recipient limit was reached,
1143// another transaction can be attempted immediately after instead of marking the
1144// delivery attempt as failed. Also code "552" must be treated like temporary error
1145// code "452" for historic reasons.
1146func (c *Client) DeliverMultiple(ctx context.Context, mailFrom string, rcptTo []string, msgSize int64, msg io.Reader, req8bitmime, reqSMTPUTF8, requireTLS bool) (rcptResps []Response, rerr error) {
1147 defer c.recover(&rerr)
1148
1149 if len(rcptTo) == 0 {
1150 return nil, fmt.Errorf("need at least one recipient")
1151 }
1152
1153 if c.origConn == nil {
1154 return nil, ErrClosed
1155 } else if c.botched {
1156 return nil, ErrBotched
1157 } else if c.needRset {
1158 if err := c.Reset(); err != nil {
1159 return nil, err
1160 }
1161 }
1162
1163 if !c.ext8bitmime && req8bitmime {
1164 c.xerrorf(true, 0, "", "", nil, "%w", Err8bitmimeUnsupported)
1165 }
1166 if !c.extSMTPUTF8 && reqSMTPUTF8 {
1167 // ../rfc/6531:313
1168 c.xerrorf(false, 0, "", "", nil, "%w", ErrSMTPUTF8Unsupported)
1169 }
1170 if !c.extRequireTLS && requireTLS {
1171 c.xerrorf(false, 0, "", "", nil, "%w", ErrRequireTLSUnsupported)
1172 }
1173
1174 // Max size enforced, only when not zero. ../rfc/1870:79
1175 if c.extSize && c.maxSize > 0 && msgSize > c.maxSize {
1176 c.xerrorf(true, 0, "", "", nil, "%w: message is %d bytes, remote has a %d bytes maximum size", ErrSize, msgSize, c.maxSize)
1177 }
1178
1179 var mailSize, bodyType string
1180 if c.extSize {
1181 mailSize = fmt.Sprintf(" SIZE=%d", msgSize)
1182 }
1183 if c.ext8bitmime {
1184 if req8bitmime {
1185 bodyType = " BODY=8BITMIME"
1186 } else {
1187 bodyType = " BODY=7BIT"
1188 }
1189 }
1190 var smtputf8Arg string
1191 if reqSMTPUTF8 {
1192 // ../rfc/6531:213
1193 smtputf8Arg = " SMTPUTF8"
1194 }
1195 var requiretlsArg string
1196 if requireTLS {
1197 // ../rfc/8689:155
1198 requiretlsArg = " REQUIRETLS"
1199 }
1200
1201 // Transaction overview: ../rfc/5321:1015
1202 // MAIL FROM: ../rfc/5321:1879
1203 // RCPT TO: ../rfc/5321:1916
1204 // DATA: ../rfc/5321:1992
1205 lineMailFrom := fmt.Sprintf("MAIL FROM:<%s>%s%s%s%s", mailFrom, mailSize, bodyType, smtputf8Arg, requiretlsArg)
1206
1207 // We are going into a transaction. We'll clear this when done.
1208 c.needRset = true
1209
1210 if c.extPipelining {
1211 c.cmds = make([]string, 1+len(rcptTo)+1)
1212 c.cmds[0] = "mailfrom"
1213 for i := range rcptTo {
1214 c.cmds[1+i] = "rcptto"
1215 }
1216 c.cmds[len(c.cmds)-1] = "data"
1217 c.cmdStart = time.Now()
1218
1219 // Write and read in separte goroutines. Otherwise, writing a large recipient list
1220 // could block when a server doesn't read more commands before we read their
1221 // response.
1222 errc := make(chan error, 1)
1223 // Make sure we don't return before we're done writing to the connection.
1224 defer func() {
1225 if errc != nil {
1226 <-errc
1227 }
1228 }()
1229 go func() {
1230 var b bytes.Buffer
1231 b.WriteString(lineMailFrom)
1232 b.WriteString("\r\n")
1233 for _, rcpt := range rcptTo {
1234 b.WriteString("RCPT TO:<")
1235 b.WriteString(rcpt)
1236 b.WriteString(">\r\n")
1237 }
1238 b.WriteString("DATA\r\n")
1239 _, err := c.w.Write(b.Bytes())
1240 if err == nil {
1241 err = c.w.Flush()
1242 }
1243 errc <- err
1244 }()
1245
1246 // Read response to MAIL FROM.
1247 mfcode, mfsecode, mffirstLine, mfmoreLines := c.xread()
1248
1249 // We read the response to RCPT TOs and DATA without panic on read error. Servers
1250 // may be aborting the connection after a failed MAIL FROM, e.g. outlook when it
1251 // has blocklisted your IP. We don't want the read for the response to RCPT TO to
1252 // cause a read error as it would result in an unhelpful error message and a
1253 // temporary instead of permanent error code.
1254
1255 // Read responses to RCPT TO.
1256 rcptResps = make([]Response, len(rcptTo))
1257 nok := 0
1258 for i := range rcptTo {
1259 code, secode, firstLine, moreLines, err := c.read()
1260 // 552 should be treated as temporary historically, ../rfc/5321:3576
1261 permanent := code/100 == 5 && code != smtp.C552MailboxFull
1262 rcptResps[i] = Response{permanent, code, secode, "rcptto", firstLine, moreLines, err}
1263 if code == smtp.C250Completed {
1264 nok++
1265 }
1266 }
1267
1268 // Read response to DATA.
1269 datacode, datasecode, datafirstLine, datamoreLines, dataerr := c.read()
1270
1271 writeerr := <-errc
1272 errc = nil
1273
1274 // If MAIL FROM failed, it's an error for the entire transaction. We may have been
1275 // blocked.
1276 if mfcode != smtp.C250Completed {
1277 if writeerr != nil || dataerr != nil {
1278 c.botched = true
1279 }
1280 c.xerrorf(mfcode/100 == 5, mfcode, mfsecode, mffirstLine, mfmoreLines, "%w: got %d, expected 2xx", ErrStatus, mfcode)
1281 }
1282
1283 // If there was an i/o error writing the commands, there is no point continuing.
1284 if writeerr != nil {
1285 c.xbotchf(0, "", "", nil, "writing pipelined mail/rcpt/data: %w", writeerr)
1286 }
1287
1288 // If remote closed the connection before writing a DATA response, and the RCPT
1289 // TO's failed (e.g. after deciding we're on a blocklist), use the last response
1290 // for a rcptto as result.
1291 if dataerr != nil && errors.Is(dataerr, io.ErrUnexpectedEOF) && nok == 0 {
1292 c.botched = true
1293 r := rcptResps[len(rcptResps)-1]
1294 c.xerrorf(r.Permanent, r.Code, r.Secode, r.Line, r.MoreLines, "%w: server closed connection just before responding to data command", ErrStatus)
1295 }
1296
1297 // If the data command had an i/o or protocol error, it's also a failure for the
1298 // entire transaction.
1299 if dataerr != nil {
1300 panic(dataerr)
1301 }
1302
1303 // If we didn't have any successful recipient, there is no point in continuing.
1304 if nok == 0 {
1305 // Servers may return success for a DATA without valid recipients. Write a dot to
1306 // end DATA and restore the connection to a known state.
1307 // ../rfc/2920:328
1308 if datacode == smtp.C354Continue {
1309 _, doterr := fmt.Fprintf(c.w, ".\r\n")
1310 if doterr == nil {
1311 doterr = c.w.Flush()
1312 }
1313 if doterr == nil {
1314 _, _, _, _, doterr = c.read()
1315 }
1316 if doterr != nil {
1317 c.botched = true
1318 }
1319 }
1320
1321 if len(rcptTo) == 1 {
1322 panic(Error(rcptResps[0]))
1323 }
1324 c.xerrorf(false, 0, "", "", nil, "%w", errNoRecipientsPipelined)
1325 }
1326
1327 if datacode != smtp.C354Continue {
1328 c.xerrorf(datacode/100 == 5, datacode, datasecode, datafirstLine, datamoreLines, "%w: got %d, expected 354", ErrStatus, datacode)
1329 }
1330
1331 } else {
1332 c.cmds[0] = "mailfrom"
1333 c.cmdStart = time.Now()
1334 c.xwriteline(lineMailFrom)
1335 code, secode, firstLine, moreLines := c.xread()
1336 if code != smtp.C250Completed {
1337 c.xerrorf(code/100 == 5, code, secode, firstLine, moreLines, "%w: got %d, expected 2xx", ErrStatus, code)
1338 }
1339
1340 rcptResps = make([]Response, len(rcptTo))
1341 nok := 0
1342 for i, rcpt := range rcptTo {
1343 c.cmds[0] = "rcptto"
1344 c.cmdStart = time.Now()
1345 c.xwriteline(fmt.Sprintf("RCPT TO:<%s>", rcpt))
1346 code, secode, firstLine, moreLines = c.xread()
1347 if i > 0 && (code == smtp.C452StorageFull || code == smtp.C552MailboxFull) {
1348 // Remote doesn't accept more recipients for this transaction. Don't send more, give
1349 // remaining recipients the same error result.
1350 for j := i; j < len(rcptTo); j++ {
1351 rcptResps[j] = Response{false, code, secode, "rcptto", firstLine, moreLines, fmt.Errorf("no more recipients accepted in transaction")}
1352 }
1353 break
1354 }
1355 var err error
1356 if code == smtp.C250Completed {
1357 nok++
1358 } else {
1359 err = fmt.Errorf("%w: got %d, expected 2xx", ErrStatus, code)
1360 }
1361 rcptResps[i] = Response{code/100 == 5, code, secode, "rcptto", firstLine, moreLines, err}
1362 }
1363
1364 if nok == 0 {
1365 if len(rcptTo) == 1 {
1366 panic(Error(rcptResps[0]))
1367 }
1368 c.xerrorf(false, 0, "", "", nil, "%w", errNoRecipients)
1369 }
1370
1371 c.cmds[0] = "data"
1372 c.cmdStart = time.Now()
1373 c.xwriteline("DATA")
1374 code, secode, firstLine, moreLines = c.xread()
1375 if code != smtp.C354Continue {
1376 c.xerrorf(code/100 == 5, code, secode, firstLine, moreLines, "%w: got %d, expected 354", ErrStatus, code)
1377 }
1378 }
1379
1380 // For a DATA write, the suggested timeout is 3 minutes, we use 30 seconds for all
1381 // writes through timeoutWriter. ../rfc/5321:3651
1382 defer c.xtrace(mlog.LevelTracedata)()
1383 err := smtp.DataWrite(c.w, msg)
1384 if err != nil {
1385 c.xbotchf(0, "", "", nil, "writing message as smtp data: %w", err)
1386 }
1387 c.xflush()
1388 c.xtrace(mlog.LevelTrace) // Restore.
1389 code, secode, firstLine, moreLines := c.xread()
1390 if code != smtp.C250Completed {
1391 c.xerrorf(code/100 == 5, code, secode, firstLine, moreLines, "%w: got %d, expected 2xx", ErrStatus, code)
1392 }
1393
1394 c.needRset = false
1395 return
1396}
1397
1398// Reset sends an SMTP RSET command to reset the message transaction state. Deliver
1399// automatically sends it if needed.
1400func (c *Client) Reset() (rerr error) {
1401 if c.origConn == nil {
1402 return ErrClosed
1403 } else if c.botched {
1404 return ErrBotched
1405 }
1406
1407 defer c.recover(&rerr)
1408
1409 // ../rfc/5321:2079
1410 c.cmds[0] = "rset"
1411 c.cmdStart = time.Now()
1412 c.xwriteline("RSET")
1413 code, secode, firstLine, moreLines := c.xread()
1414 if code != smtp.C250Completed {
1415 c.xerrorf(code/100 == 5, code, secode, firstLine, moreLines, "%w: got %d, expected 2xx", ErrStatus, code)
1416 }
1417 c.needRset = false
1418 return
1419}
1420
1421// Botched returns whether this connection is botched, e.g. a protocol error
1422// occurred and the connection is in unknown state, and cannot be used for message
1423// delivery.
1424func (c *Client) Botched() bool {
1425 return c.botched || c.origConn == nil
1426}
1427
1428// Close cleans up the client, closing the underlying connection.
1429//
1430// If the connection is initialized and not botched, a QUIT command is sent and the
1431// response read with a short timeout before closing the underlying connection.
1432//
1433// Close returns any error encountered during QUIT and closing.
1434func (c *Client) Close() (rerr error) {
1435 if c.origConn == nil {
1436 return ErrClosed
1437 }
1438
1439 defer c.recover(&rerr)
1440
1441 if !c.botched {
1442 // ../rfc/5321:2205
1443 c.cmds[0] = "quit"
1444 c.cmdStart = time.Now()
1445 c.xwriteline("QUIT")
1446 if err := c.conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
1447 c.log.Infox("setting read deadline for reading quit response", err)
1448 } else if _, err := bufs.Readline(c.log, c.r); err != nil {
1449 rerr = fmt.Errorf("reading response to quit command: %v", err)
1450 c.log.Debugx("reading quit response", err)
1451 }
1452 }
1453
1454 err := c.origConn.Close()
1455 if c.conn != c.origConn {
1456 // This is the TLS connection. Close will attempt to write a close notification.
1457 // But it will fail quickly because the underlying socket was closed.
1458 c.conn.Close()
1459 }
1460 c.origConn = nil
1461 c.conn = nil
1462 if rerr != nil {
1463 rerr = err
1464 }
1465 return
1466}
1467
1468// Conn returns the connection with the initialized SMTP session, possibly wrapping
1469// a TLS connection, and handling protocol trace logging. Once the caller uses this
1470// connection it is in control, and responsible for closing the connection, and
1471// other functions on the client must not be called anymore.
1472func (c *Client) Conn() (net.Conn, error) {
1473 if err := c.conn.SetDeadline(time.Time{}); err != nil {
1474 return nil, fmt.Errorf("clearing io deadlines: %w", err)
1475 }
1476 return c.conn, nil
1477}
1478