1// Package smtpclient is an SMTP client, used by the queue for sending outgoing messages.
2package smtpclient
3
4import (
5 "bufio"
6 "context"
7 "crypto/tls"
8 "crypto/x509"
9 "encoding/base64"
10 "errors"
11 "fmt"
12 "io"
13 "net"
14 "reflect"
15 "strconv"
16 "strings"
17 "time"
18
19 "github.com/prometheus/client_golang/prometheus"
20 "github.com/prometheus/client_golang/prometheus/promauto"
21
22 "github.com/mjl-/adns"
23
24 "github.com/mjl-/mox/dane"
25 "github.com/mjl-/mox/dns"
26 "github.com/mjl-/mox/metrics"
27 "github.com/mjl-/mox/mlog"
28 "github.com/mjl-/mox/mox-"
29 "github.com/mjl-/mox/moxio"
30 "github.com/mjl-/mox/sasl"
31 "github.com/mjl-/mox/smtp"
32 "github.com/mjl-/mox/tlsrpt"
33)
34
35// 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
36
37var (
38 metricCommands = promauto.NewHistogramVec(
39 prometheus.HistogramOpts{
40 Name: "mox_smtpclient_command_duration_seconds",
41 Help: "SMTP client command duration and result codes in seconds.",
42 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
43 },
44 []string{
45 "cmd",
46 "code",
47 "secode",
48 },
49 )
50 metricTLSRequiredNoIgnored = promauto.NewCounterVec(
51 prometheus.CounterOpts{
52 Name: "mox_smtpclient_tlsrequiredno_ignored_total",
53 Help: "Connection attempts with TLS policy findings ignored due to message with TLS-Required: No header. Does not cover case where TLS certificate cannot be PKIX-verified.",
54 },
55 []string{
56 "ignored", // daneverification (no matching tlsa record)
57 },
58 )
59)
60
61var (
62 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.
63 Err8bitmimeUnsupported = errors.New("remote smtp server does not implement 8bitmime extension, required by message")
64 ErrSMTPUTF8Unsupported = errors.New("remote smtp server does not implement smtputf8 extension, required by message")
65 ErrRequireTLSUnsupported = errors.New("remote smtp server does not implement requiretls extension, required for delivery")
66 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.
67 ErrProtocol = errors.New("smtp protocol error") // After a malformed SMTP response or inconsistent multi-line response.
68 ErrTLS = errors.New("tls error") // E.g. handshake failure, or hostname verification was required and failed.
69 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.
70 ErrClosed = errors.New("client is closed")
71)
72
73// TLSMode indicates if TLS must, should or must not be used.
74type TLSMode string
75
76const (
77 // TLS immediately ("implicit TLS"), directly starting TLS on the TCP connection,
78 // so not using STARTTLS. Whether PKIX and/or DANE is verified is specified
79 // separately.
80 TLSImmediate TLSMode = "immediate"
81
82 // Required TLS with STARTTLS for SMTP servers. The STARTTLS command is always
83 // executed, even if the server does not announce support.
84 // Whether PKIX and/or DANE is verified is specified separately.
85 TLSRequiredStartTLS TLSMode = "requiredstarttls"
86
87 // Use TLS with STARTTLS if remote claims to support it.
88 TLSOpportunistic TLSMode = "opportunistic"
89
90 // TLS must not be attempted, e.g. due to earlier TLS handshake error.
91 TLSSkip TLSMode = "skip"
92)
93
94// Client is an SMTP client that can deliver messages to a mail server.
95//
96// Use New to make a new client.
97type Client struct {
98 // OrigConn is the original (TCP) connection. We'll read from/write to conn, which
99 // can be wrapped in a tls.Client. We close origConn instead of conn because
100 // closing the TLS connection would send a TLS close notification, which may block
101 // for 5s if the server isn't reading it (because it is also sending it).
102 origConn net.Conn
103 conn net.Conn
104 tlsVerifyPKIX bool
105 ignoreTLSVerifyErrors bool
106 rootCAs *x509.CertPool
107 remoteHostname dns.Domain // TLS with SNI and name verification.
108 daneRecords []adns.TLSA // For authenticating (START)TLS connection.
109 daneMoreHostnames []dns.Domain // Additional allowed names in TLS certificate for DANE-TA.
110 daneVerifiedRecord *adns.TLSA // If non-nil, then will be set to verified DANE record if any.
111
112 // TLS connection success/failure are added. These are always non-nil, regardless
113 // of what was passed in opts. It lets us unconditionally dereference them.
114 recipientDomainResult *tlsrpt.Result // Either "sts" or "no-policy-found".
115 hostResult *tlsrpt.Result // Either "dane" or "no-policy-found".
116
117 r *bufio.Reader
118 w *bufio.Writer
119 tr *moxio.TraceReader // Kept for changing trace levels between cmd/auth/data.
120 tw *moxio.TraceWriter
121 log *mlog.Log
122 lastlog time.Time // For adding delta timestamps between log lines.
123 cmds []string // Last or active command, for generating errors and metrics.
124 cmdStart time.Time // Start of command.
125 tls bool // Whether connection is TLS protected.
126 firstReadAfterHandshake bool // To detect TLS alert error from remote just after handshake.
127
128 botched bool // If set, protocol is out of sync and no further commands can be sent.
129 needRset bool // If set, a new delivery requires an RSET command.
130
131 remoteHelo string // From 220 greeting line.
132 extEcodes bool // Remote server supports sending extended error codes.
133 extStartTLS bool // Remote server supports STARTTLS.
134 ext8bitmime bool
135 extSize bool // Remote server supports SIZE parameter.
136 maxSize int64 // Max size of email message.
137 extPipelining bool // Remote server supports command pipelining.
138 extSMTPUTF8 bool // Remote server supports SMTPUTF8 extension.
139 extAuthMechanisms []string // Supported authentication mechanisms.
140 extRequireTLS bool // Remote supports REQUIRETLS extension.
141}
142
143// Error represents a failure to deliver a message.
144//
145// Code, Secode, Command and Line are only set for SMTP-level errors, and are zero
146// values otherwise.
147type Error struct {
148 // Whether failure is permanent, typically because of 5xx response.
149 Permanent bool
150 // SMTP response status, e.g. 2xx for success, 4xx for transient error and 5xx for
151 // permanent failure.
152 Code int
153 // Short enhanced status, minus first digit and dot. Can be empty, e.g. for io
154 // errors or if remote does not send enhanced status codes. If remote responds with
155 // "550 5.7.1 ...", the Secode will be "7.1".
156 Secode string
157 // SMTP command causing failure.
158 Command string
159 // For errors due to SMTP responses, the full SMTP line excluding CRLF that caused
160 // the error. Typically the last line read.
161 Line string
162 // Underlying error, e.g. one of the Err variables in this package, or io errors.
163 Err error
164}
165
166// Unwrap returns the underlying Err.
167func (e Error) Unwrap() error {
168 return e.Err
169}
170
171// Error returns a readable error string.
172func (e Error) Error() string {
173 s := ""
174 if e.Err != nil {
175 s = e.Err.Error() + ", "
176 }
177 if e.Permanent {
178 s += "permanent"
179 } else {
180 s += "transient"
181 }
182 if e.Line != "" {
183 s += ": " + e.Line
184 }
185 return s
186}
187
188// Opts influence behaviour of Client.
189type Opts struct {
190 // If auth is non-empty, authentication will be done with the first algorithm
191 // supported by the server. If none of the algorithms are supported, an error is
192 // returned.
193 Auth []sasl.Client
194
195 DANERecords []adns.TLSA // If not nil, DANE records to verify.
196 DANEMoreHostnames []dns.Domain // For use with DANE, where additional certificate host names are allowed.
197 DANEVerifiedRecord *adns.TLSA // If non-empty, set to the DANE record that verified the TLS connection.
198
199 // If set, TLS verification errors (for DANE or PKIX) are ignored. Useful for
200 // delivering messages with message header "TLS-Required: No".
201 // Certificates are still verified, and results are still tracked for TLS
202 // reporting, but the connections will continue.
203 IgnoreTLSVerifyErrors bool
204
205 // If not nil, used instead of the system default roots for TLS PKIX verification.
206 RootCAs *x509.CertPool
207
208 // TLS verification successes/failures is added to these TLS reporting results.
209 // Once the STARTTLS handshake is attempted, a successful/failed connection is
210 // tracked.
211 RecipientDomainResult *tlsrpt.Result // MTA-STS or no policy.
212 HostResult *tlsrpt.Result // DANE or no policy.
213}
214
215// New initializes an SMTP session on the given connection, returning a client that
216// can be used to deliver messages.
217//
218// New optionally starts TLS (for submission), reads the server greeting,
219// identifies itself with a HELO or EHLO command, initializes TLS with STARTTLS if
220// remote supports it and optionally authenticates. If successful, a client is
221// returned on which eventually Close must be called. Otherwise an error is
222// returned and the caller is responsible for closing the connection.
223//
224// Connecting to the correct host is outside the scope of the client. The queue
225// managing outgoing messages decides which host to deliver to, taking multiple MX
226// records with preferences, other DNS records, MTA-STS, retries and special
227// cases into account.
228//
229// tlsMode indicates if and how TLS may/must (not) be used. tlsVerifyPKIX
230// indicates if TLS certificates must be validated against the PKIX/WebPKI
231// certificate authorities (if TLS is done). DANE-verification is done when
232// opts.DANERecords is not nil. TLS verification errors will be ignored if
233// opts.IgnoreTLSVerification is set. If TLS is done, PKIX verification is
234// always performed for tracking the results for TLS reporting, but if
235// tlsVerifyPKIX is false, the verification result does not affect the
236// connection. At the time of writing, delivery of email on the internet is done
237// with opportunistic TLS without PKIX verification by default. Recipient domains
238// can opt-in to PKIX verification by publishing an MTA-STS policy, or opt-in to
239// DANE verification by publishing DNSSEC-protected TLSA records in DNS.
240func New(ctx context.Context, log *mlog.Log, conn net.Conn, tlsMode TLSMode, tlsVerifyPKIX bool, ehloHostname, remoteHostname dns.Domain, opts Opts) (*Client, error) {
241 ensureResult := func(r *tlsrpt.Result) *tlsrpt.Result {
242 if r == nil {
243 return &tlsrpt.Result{}
244 }
245 return r
246 }
247
248 c := &Client{
249 origConn: conn,
250 tlsVerifyPKIX: tlsVerifyPKIX,
251 ignoreTLSVerifyErrors: opts.IgnoreTLSVerifyErrors,
252 rootCAs: opts.RootCAs,
253 remoteHostname: remoteHostname,
254 daneRecords: opts.DANERecords,
255 daneMoreHostnames: opts.DANEMoreHostnames,
256 daneVerifiedRecord: opts.DANEVerifiedRecord,
257 lastlog: time.Now(),
258 cmds: []string{"(none)"},
259 recipientDomainResult: ensureResult(opts.RecipientDomainResult),
260 hostResult: ensureResult(opts.HostResult),
261 }
262 c.log = log.Fields(mlog.Field("smtpclient", "")).MoreFields(func() []mlog.Pair {
263 now := time.Now()
264 l := []mlog.Pair{
265 mlog.Field("delta", now.Sub(c.lastlog)),
266 }
267 c.lastlog = now
268 return l
269 })
270
271 if tlsMode == TLSImmediate {
272 config := c.tlsConfig()
273 tlsconn := tls.Client(conn, config)
274 // The tlsrpt tracking isn't used by caller, but won't hurt.
275 if err := tlsconn.HandshakeContext(ctx); err != nil {
276 c.tlsResultAdd(0, 1, err)
277 return nil, err
278 }
279 c.firstReadAfterHandshake = true
280 c.tlsResultAdd(1, 0, nil)
281 c.conn = tlsconn
282 tlsversion, ciphersuite := mox.TLSInfo(tlsconn)
283 c.log.Debug("tls client handshake done", mlog.Field("tls", tlsversion), mlog.Field("ciphersuite", ciphersuite), mlog.Field("servername", remoteHostname))
284 c.tls = true
285 } else {
286 c.conn = conn
287 }
288
289 // We don't wrap reads in a timeoutReader for fear of an optional TLS wrapper doing
290 // reads without the client asking for it. Such reads could result in a timeout
291 // error.
292 c.tr = moxio.NewTraceReader(c.log, "RS: ", c.conn)
293 c.r = bufio.NewReader(c.tr)
294 // We use a single write timeout of 30 seconds.
295 // todo future: use different timeouts ../rfc/5321:3610
296 c.tw = moxio.NewTraceWriter(c.log, "LC: ", timeoutWriter{c.conn, 30 * time.Second, c.log})
297 c.w = bufio.NewWriter(c.tw)
298
299 if err := c.hello(ctx, tlsMode, ehloHostname, opts.Auth); err != nil {
300 return nil, err
301 }
302 return c, nil
303}
304
305// reportedError wraps an error while indicating it was already tracked for TLS
306// reporting.
307type reportedError struct{ err error }
308
309func (e reportedError) Error() string {
310 return e.err.Error()
311}
312
313func (e reportedError) Unwrap() error {
314 return e.err
315}
316
317func (c *Client) tlsConfig() *tls.Config {
318 // We always manage verification ourselves: We need to report in detail about
319 // failures. And we may have to verify both PKIX and DANE, record errors for
320 // each, and possibly ignore the errors.
321
322 verifyConnection := func(cs tls.ConnectionState) error {
323 // Collect verification errors. If there are none at the end, TLS validation
324 // succeeded. We may find validation problems below, record them for a TLS report
325 // but continue due to policies. We track the TLS reporting result in this
326 // function, wrapping errors in a reportedError.
327 var daneErr, pkixErr error
328
329 // DANE verification.
330 // daneRecords can be non-nil and empty, that's intended.
331 if c.daneRecords != nil {
332 verified, record, err := dane.Verify(c.log, c.daneRecords, cs, c.remoteHostname, c.daneMoreHostnames)
333 c.log.Debugx("dane verification", err, mlog.Field("verified", verified), mlog.Field("record", record))
334 if verified {
335 if c.daneVerifiedRecord != nil {
336 *c.daneVerifiedRecord = record
337 }
338 } else {
339 // Track error for reports.
340 // 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
341 fd := c.tlsrptFailureDetails(tlsrpt.ResultValidationFailure, "dane-no-match")
342 if err != nil {
343 // 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.
344
345 // We may have encountered errors while evaluation some of the TLSA records.
346 fd.FailureReasonCode += "+errors"
347 }
348 c.hostResult.Add(0, 0, fd)
349
350 if c.ignoreTLSVerifyErrors {
351 // We ignore the failure and continue the connection.
352 c.log.Infox("verifying dane failed, continuing with connection", err)
353 metricTLSRequiredNoIgnored.WithLabelValues("daneverification").Inc()
354 } else {
355 // This connection will fail.
356 daneErr = dane.ErrNoMatch
357 }
358 }
359 }
360
361 // PKIX verification.
362 opts := x509.VerifyOptions{
363 DNSName: cs.ServerName,
364 Intermediates: x509.NewCertPool(),
365 Roots: c.rootCAs,
366 }
367 for _, cert := range cs.PeerCertificates[1:] {
368 opts.Intermediates.AddCert(cert)
369 }
370 if _, err := cs.PeerCertificates[0].Verify(opts); err != nil {
371 resultType, reasonCode := tlsrpt.TLSFailureDetails(err)
372 fd := c.tlsrptFailureDetails(resultType, reasonCode)
373 c.recipientDomainResult.Add(0, 0, fd)
374
375 if c.tlsVerifyPKIX && !c.ignoreTLSVerifyErrors {
376 pkixErr = err
377 }
378 }
379
380 if daneErr != nil && pkixErr != nil {
381 return reportedError{errors.Join(daneErr, pkixErr)}
382 } else if daneErr != nil {
383 return reportedError{daneErr}
384 } else if pkixErr != nil {
385 return reportedError{pkixErr}
386 }
387 return nil
388 }
389
390 return &tls.Config{
391 ServerName: c.remoteHostname.ASCII, // For SNI.
392 // todo: possibly accept older TLS versions for TLSOpportunistic? or would our private key be at risk?
393 MinVersion: tls.VersionTLS12, // ../rfc/8996:31 ../rfc/8997:66
394 InsecureSkipVerify: true, // VerifyConnection below is called and will do all verification.
395 VerifyConnection: verifyConnection,
396 }
397}
398
399// xbotchf generates a temporary error and marks the client as botched. e.g. for
400// i/o errors or invalid protocol messages.
401func (c *Client) xbotchf(code int, secode string, lastLine, format string, args ...any) {
402 panic(c.botchf(code, secode, lastLine, format, args...))
403}
404
405// botchf generates a temporary error and marks the client as botched. e.g. for
406// i/o errors or invalid protocol messages.
407func (c *Client) botchf(code int, secode string, lastLine, format string, args ...any) error {
408 c.botched = true
409 return c.errorf(false, code, secode, lastLine, format, args...)
410}
411
412func (c *Client) errorf(permanent bool, code int, secode, lastLine, format string, args ...any) error {
413 var cmd string
414 if len(c.cmds) > 0 {
415 cmd = c.cmds[0]
416 }
417 return Error{permanent, code, secode, cmd, lastLine, fmt.Errorf(format, args...)}
418}
419
420func (c *Client) xerrorf(permanent bool, code int, secode, lastLine, format string, args ...any) {
421 panic(c.errorf(permanent, code, secode, lastLine, format, args...))
422}
423
424// timeoutWriter passes each Write on to conn after setting a write deadline on conn based on
425// timeout.
426type timeoutWriter struct {
427 conn net.Conn
428 timeout time.Duration
429 log *mlog.Log
430}
431
432func (w timeoutWriter) Write(buf []byte) (int, error) {
433 if err := w.conn.SetWriteDeadline(time.Now().Add(w.timeout)); err != nil {
434 w.log.Errorx("setting write deadline", err)
435 }
436
437 return w.conn.Write(buf)
438}
439
440var bufs = moxio.NewBufpool(8, 2*1024)
441
442func (c *Client) readline() (string, error) {
443 // todo: could have per-operation timeouts. and rfc suggests higher minimum timeouts. ../rfc/5321:3610
444 if err := c.conn.SetReadDeadline(time.Now().Add(30 * time.Second)); err != nil {
445 c.log.Errorx("setting read deadline", err)
446 }
447
448 line, err := bufs.Readline(c.r)
449 if err != nil {
450 // See if this is a TLS alert from remote, and one other than 0 (which notifies
451 // that the connection is being closed. If so, we register a TLS connection
452 // failure. This handles TLS alerts that happen just after a successful handshake.
453 var netErr *net.OpError
454 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 {
455 resultType, reasonCode := tlsrpt.TLSFailureDetails(err)
456 // We count -1 success to compensate for the assumed success right after the handshake.
457 c.tlsResultAddFailureDetails(-1, 1, c.tlsrptFailureDetails(resultType, reasonCode))
458 }
459
460 return line, c.botchf(0, "", "", "%s: %w", strings.Join(c.cmds, ","), err)
461 }
462 c.firstReadAfterHandshake = false
463 return line, nil
464}
465
466func (c *Client) xtrace(level mlog.Level) func() {
467 c.xflush()
468 c.tr.SetTrace(level)
469 c.tw.SetTrace(level)
470 return func() {
471 c.xflush()
472 c.tr.SetTrace(mlog.LevelTrace)
473 c.tw.SetTrace(mlog.LevelTrace)
474 }
475}
476
477func (c *Client) xwritelinef(format string, args ...any) {
478 c.xbwritelinef(format, args...)
479 c.xflush()
480}
481
482func (c *Client) xwriteline(line string) {
483 c.xbwriteline(line)
484 c.xflush()
485}
486
487func (c *Client) xbwritelinef(format string, args ...any) {
488 c.xbwriteline(fmt.Sprintf(format, args...))
489}
490
491func (c *Client) xbwriteline(line string) {
492 _, err := fmt.Fprintf(c.w, "%s\r\n", line)
493 if err != nil {
494 c.xbotchf(0, "", "", "write: %w", err)
495 }
496}
497
498func (c *Client) xflush() {
499 err := c.w.Flush()
500 if err != nil {
501 c.xbotchf(0, "", "", "writes: %w", err)
502 }
503}
504
505// read response, possibly multiline, with supporting extended codes based on configuration in client.
506func (c *Client) xread() (code int, secode, lastLine string, texts []string) {
507 var err error
508 code, secode, lastLine, texts, err = c.read()
509 if err != nil {
510 panic(err)
511 }
512 return
513}
514
515func (c *Client) read() (code int, secode, lastLine string, texts []string, rerr error) {
516 return c.readecode(c.extEcodes)
517}
518
519// read response, possibly multiline.
520// if ecodes, extended codes are parsed.
521func (c *Client) readecode(ecodes bool) (code int, secode, lastLine string, texts []string, rerr error) {
522 for {
523 co, sec, text, line, last, err := c.read1(ecodes)
524 if err != nil {
525 rerr = err
526 return
527 }
528 texts = append(texts, text)
529 if code != 0 && co != code {
530 // ../rfc/5321:2771
531 err := c.botchf(0, "", line, "%w: multiline response with different codes, previous %d, last %d", ErrProtocol, code, co)
532 return 0, "", "", nil, err
533 }
534 code = co
535 if last {
536 if code != smtp.C334ContinueAuth {
537 cmd := ""
538 if len(c.cmds) > 0 {
539 cmd = c.cmds[0]
540 // We only keep the last, so we're not creating new slices all the time.
541 if len(c.cmds) > 1 {
542 c.cmds = c.cmds[1:]
543 }
544 }
545 metricCommands.WithLabelValues(cmd, fmt.Sprintf("%d", co), sec).Observe(float64(time.Since(c.cmdStart)) / float64(time.Second))
546 c.log.Debug("smtpclient command result", mlog.Field("cmd", cmd), mlog.Field("code", co), mlog.Field("secode", sec), mlog.Field("duration", time.Since(c.cmdStart)))
547 }
548 return co, sec, line, texts, nil
549 }
550 }
551}
552
553func (c *Client) xreadecode(ecodes bool) (code int, secode, lastLine string, texts []string) {
554 var err error
555 code, secode, lastLine, texts, err = c.readecode(ecodes)
556 if err != nil {
557 panic(err)
558 }
559 return
560}
561
562// read single response line.
563// if ecodes, extended codes are parsed.
564func (c *Client) read1(ecodes bool) (code int, secode, text, line string, last bool, rerr error) {
565 line, rerr = c.readline()
566 if rerr != nil {
567 return
568 }
569 i := 0
570 for ; i < len(line) && line[i] >= '0' && line[i] <= '9'; i++ {
571 }
572 if i != 3 {
573 rerr = c.botchf(0, "", line, "%w: expected response code: %s", ErrProtocol, line)
574 return
575 }
576 v, err := strconv.ParseInt(line[:i], 10, 32)
577 if err != nil {
578 rerr = c.botchf(0, "", line, "%w: bad response code (%s): %s", ErrProtocol, err, line)
579 return
580 }
581 code = int(v)
582 major := code / 100
583 s := line[3:]
584 if strings.HasPrefix(s, "-") || strings.HasPrefix(s, " ") {
585 last = s[0] == ' '
586 s = s[1:]
587 } else if s == "" {
588 // Allow missing space. ../rfc/5321:2570 ../rfc/5321:2612
589 last = true
590 } else {
591 rerr = c.botchf(0, "", line, "%w: expected space or dash after response code: %s", ErrProtocol, line)
592 return
593 }
594
595 if ecodes {
596 secode, s = parseEcode(major, s)
597 }
598
599 return code, secode, s, line, last, nil
600}
601
602func parseEcode(major int, s string) (secode string, remain string) {
603 o := 0
604 bad := false
605 take := func(need bool, a, b byte) bool {
606 if !bad && o < len(s) && s[o] >= a && s[o] <= b {
607 o++
608 return true
609 }
610 bad = bad || need
611 return false
612 }
613 digit := func(need bool) bool {
614 return take(need, '0', '9')
615 }
616 dot := func() bool {
617 return take(true, '.', '.')
618 }
619
620 digit(true)
621 dot()
622 xo := o
623 digit(true)
624 for digit(false) {
625 }
626 dot()
627 digit(true)
628 for digit(false) {
629 }
630 secode = s[xo:o]
631 take(false, ' ', ' ')
632 if bad || int(s[0])-int('0') != major {
633 return "", s
634 }
635 return secode, s[o:]
636}
637
638func (c *Client) recover(rerr *error) {
639 x := recover()
640 if x == nil {
641 return
642 }
643 cerr, ok := x.(Error)
644 if !ok {
645 metrics.PanicInc(metrics.Smtpclient)
646 panic(x)
647 }
648 *rerr = cerr
649}
650
651func (c *Client) hello(ctx context.Context, tlsMode TLSMode, ehloHostname dns.Domain, auth []sasl.Client) (rerr error) {
652 defer c.recover(&rerr)
653
654 // perform EHLO handshake, falling back to HELO if server does not appear to
655 // implement EHLO.
656 hello := func(heloOK bool) {
657 // Write EHLO and parse the supported extensions.
658 // ../rfc/5321:987
659 c.cmds[0] = "ehlo"
660 c.cmdStart = time.Now()
661 // Syntax: ../rfc/5321:1827
662 c.xwritelinef("EHLO %s", ehloHostname.ASCII)
663 code, _, lastLine, remains := c.xreadecode(false)
664 switch code {
665 // ../rfc/5321:997
666 // ../rfc/5321:3098
667 case smtp.C500BadSyntax, smtp.C501BadParamSyntax, smtp.C502CmdNotImpl, smtp.C503BadCmdSeq, smtp.C504ParamNotImpl:
668 if !heloOK {
669 c.xerrorf(true, code, "", lastLine, "%w: remote claims ehlo is not supported", ErrProtocol)
670 }
671 // ../rfc/5321:996
672 c.cmds[0] = "helo"
673 c.cmdStart = time.Now()
674 c.xwritelinef("HELO %s", ehloHostname.ASCII)
675 code, _, lastLine, _ = c.xreadecode(false)
676 if code != smtp.C250Completed {
677 c.xerrorf(code/100 == 5, code, "", lastLine, "%w: expected 250 to HELO, got %d", ErrStatus, code)
678 }
679 return
680 case smtp.C250Completed:
681 default:
682 c.xerrorf(code/100 == 5, code, "", lastLine, "%w: expected 250, got %d", ErrStatus, code)
683 }
684 for _, s := range remains[1:] {
685 // ../rfc/5321:1869
686 s = strings.ToUpper(strings.TrimSpace(s))
687 switch s {
688 case "STARTTLS":
689 c.extStartTLS = true
690 case "ENHANCEDSTATUSCODES":
691 c.extEcodes = true
692 case "8BITMIME":
693 c.ext8bitmime = true
694 case "PIPELINING":
695 c.extPipelining = true
696 case "REQUIRETLS":
697 c.extRequireTLS = true
698 default:
699 // For SMTPUTF8 we must ignore any parameter. ../rfc/6531:207
700 if s == "SMTPUTF8" || strings.HasPrefix(s, "SMTPUTF8 ") {
701 c.extSMTPUTF8 = true
702 } else if strings.HasPrefix(s, "SIZE ") {
703 c.extSize = true
704 if v, err := strconv.ParseInt(s[len("SIZE "):], 10, 64); err == nil {
705 c.maxSize = v
706 }
707 } else if strings.HasPrefix(s, "AUTH ") {
708 c.extAuthMechanisms = strings.Split(s[len("AUTH "):], " ")
709 }
710 }
711 }
712 }
713
714 // Read greeting.
715 c.cmds = []string{"(greeting)"}
716 c.cmdStart = time.Now()
717 code, _, lastLine, lines := c.xreadecode(false)
718 if code != smtp.C220ServiceReady {
719 c.xerrorf(code/100 == 5, code, "", lastLine, "%w: expected 220, got %d", ErrStatus, code)
720 }
721 // ../rfc/5321:2588
722 c.remoteHelo, _, _ = strings.Cut(lines[0], " ")
723
724 // Write EHLO, falling back to HELO if server doesn't appear to support it.
725 hello(true)
726
727 // Attempt TLS if remote understands STARTTLS and we aren't doing immediate TLS or if caller requires it.
728 if c.extStartTLS && tlsMode == TLSOpportunistic || tlsMode == TLSRequiredStartTLS {
729 c.log.Debug("starting tls client", mlog.Field("tlsmode", tlsMode), mlog.Field("servername", c.remoteHostname))
730 c.cmds[0] = "starttls"
731 c.cmdStart = time.Now()
732 c.xwritelinef("STARTTLS")
733 code, secode, lastLine, _ := c.xread()
734 // ../rfc/3207:107
735 if code != smtp.C220ServiceReady {
736 c.tlsResultAddFailureDetails(0, 1, c.tlsrptFailureDetails(tlsrpt.ResultSTARTTLSNotSupported, fmt.Sprintf("smtp-starttls-reply-code-%d", code)))
737 c.xerrorf(code/100 == 5, code, secode, lastLine, "%w: STARTTLS: got %d, expected 220", ErrTLS, code)
738 }
739
740 // We don't want to do TLS on top of c.r because it also prints protocol traces: We
741 // don't want to log the TLS stream. So we'll do TLS on the underlying connection,
742 // but make sure any bytes already read and in the buffer are used for the TLS
743 // handshake.
744 conn := c.conn
745 if n := c.r.Buffered(); n > 0 {
746 conn = &moxio.PrefixConn{
747 PrefixReader: io.LimitReader(c.r, int64(n)),
748 Conn: conn,
749 }
750 }
751
752 tlsConfig := c.tlsConfig()
753 nconn := tls.Client(conn, tlsConfig)
754 c.conn = nconn
755
756 nctx, cancel := context.WithTimeout(ctx, time.Minute)
757 defer cancel()
758 err := nconn.HandshakeContext(nctx)
759 if err != nil {
760 // For each STARTTLS failure, we track a failed TLS session. For deliveries with
761 // multiple MX targets, we may add multiple failures, and delivery may succeed with
762 // a later MX target with which we can do STARTTLS. ../rfc/8460:524
763 c.tlsResultAdd(0, 1, err)
764 c.xerrorf(false, 0, "", "", "%w: STARTTLS TLS handshake: %s", ErrTLS, err)
765 }
766 c.firstReadAfterHandshake = true
767 cancel()
768 c.tr = moxio.NewTraceReader(c.log, "RS: ", c.conn)
769 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.
770 c.r = bufio.NewReader(c.tr)
771 c.w = bufio.NewWriter(c.tw)
772
773 tlsversion, ciphersuite := mox.TLSInfo(nconn)
774 c.log.Debug("starttls client handshake done",
775 mlog.Field("tlsmode", tlsMode),
776 mlog.Field("verifypkix", c.tlsVerifyPKIX),
777 mlog.Field("verifydane", c.daneRecords != nil),
778 mlog.Field("ignoretlsverifyerrors", c.ignoreTLSVerifyErrors),
779 mlog.Field("tls", tlsversion),
780 mlog.Field("ciphersuite", ciphersuite),
781 mlog.Field("servername", c.remoteHostname),
782 mlog.Field("danerecord", c.daneVerifiedRecord))
783 c.tls = true
784 // Track successful TLS connection. ../rfc/8460:515
785 c.tlsResultAdd(1, 0, nil)
786
787 hello(false)
788 } else if tlsMode == TLSOpportunistic {
789 // Result: ../rfc/8460:538
790 c.tlsResultAddFailureDetails(0, 0, c.tlsrptFailureDetails(tlsrpt.ResultSTARTTLSNotSupported, ""))
791 }
792
793 if len(auth) > 0 {
794 return c.auth(auth)
795 }
796 return
797}
798
799func addrIP(addr net.Addr) string {
800 if t, ok := addr.(*net.TCPAddr); ok {
801 return t.IP.String()
802 }
803 host, _, _ := net.SplitHostPort(addr.String())
804 ip := net.ParseIP(host)
805 if ip == nil {
806 return "" // For pipe during tests.
807 }
808 return ip.String()
809}
810
811// tlsrptFailureDetails returns FailureDetails with connection details (such as
812// IP addresses) for inclusion in a TLS report.
813func (c *Client) tlsrptFailureDetails(resultType tlsrpt.ResultType, reasonCode string) tlsrpt.FailureDetails {
814 return tlsrpt.FailureDetails{
815 ResultType: resultType,
816 SendingMTAIP: addrIP(c.origConn.LocalAddr()),
817 ReceivingMXHostname: c.remoteHostname.ASCII,
818 ReceivingMXHelo: c.remoteHelo,
819 ReceivingIP: addrIP(c.origConn.RemoteAddr()),
820 FailedSessionCount: 1,
821 FailureReasonCode: reasonCode,
822 }
823}
824
825// tlsResultAdd adds TLS success/failure to all results.
826func (c *Client) tlsResultAdd(success, failure int64, err error) {
827 // Only track failure if not already done so in tls.Config.VerifyConnection.
828 var fds []tlsrpt.FailureDetails
829 var repErr reportedError
830 if err != nil && !errors.As(err, &repErr) {
831 resultType, reasonCode := tlsrpt.TLSFailureDetails(err)
832 fd := c.tlsrptFailureDetails(resultType, reasonCode)
833 fds = []tlsrpt.FailureDetails{fd}
834 }
835 c.tlsResultAddFailureDetails(success, failure, fds...)
836}
837
838func (c *Client) tlsResultAddFailureDetails(success, failure int64, fds ...tlsrpt.FailureDetails) {
839 c.recipientDomainResult.Add(success, failure, fds...)
840 c.hostResult.Add(success, failure, fds...)
841}
842
843// ../rfc/4954:139
844func (c *Client) auth(auth []sasl.Client) (rerr error) {
845 defer c.recover(&rerr)
846
847 c.cmds[0] = "auth"
848 c.cmdStart = time.Now()
849
850 var a sasl.Client
851 var name string
852 var cleartextCreds bool
853 for _, x := range auth {
854 name, cleartextCreds = x.Info()
855 for _, s := range c.extAuthMechanisms {
856 if s == name {
857 a = x
858 break
859 }
860 }
861 }
862 if a == nil {
863 c.xerrorf(true, 0, "", "", "no matching authentication mechanisms, server supports %s", strings.Join(c.extAuthMechanisms, ", "))
864 }
865
866 abort := func() (int, string, string) {
867 // Abort authentication. ../rfc/4954:193
868 c.xwriteline("*")
869
870 // Server must respond with 501. // ../rfc/4954:195
871 code, secode, lastline, _ := c.xread()
872 if code != smtp.C501BadParamSyntax {
873 c.botched = true
874 }
875 return code, secode, lastline
876 }
877
878 toserver, last, err := a.Next(nil)
879 if err != nil {
880 c.xerrorf(false, 0, "", "", "initial step in auth mechanism %s: %w", name, err)
881 }
882 if cleartextCreds {
883 defer c.xtrace(mlog.LevelTraceauth)()
884 }
885 if toserver == nil {
886 c.xwriteline("AUTH " + name)
887 } else if len(toserver) == 0 {
888 c.xwriteline("AUTH " + name + " =") // ../rfc/4954:214
889 } else {
890 c.xwriteline("AUTH " + name + " " + base64.StdEncoding.EncodeToString(toserver))
891 }
892 for {
893 if cleartextCreds && last {
894 c.xtrace(mlog.LevelTrace) // Restore.
895 }
896
897 code, secode, lastLine, texts := c.xreadecode(last)
898 if code == smtp.C235AuthSuccess {
899 if !last {
900 c.xerrorf(false, code, secode, lastLine, "server completed authentication earlier than client expected")
901 }
902 return nil
903 } else if code == smtp.C334ContinueAuth {
904 if last {
905 c.xerrorf(false, code, secode, lastLine, "server requested unexpected continuation of authentication")
906 }
907 if len(texts) != 1 {
908 abort()
909 c.xerrorf(false, code, secode, lastLine, "server responded with multiline contination")
910 }
911 fromserver, err := base64.StdEncoding.DecodeString(texts[0])
912 if err != nil {
913 abort()
914 c.xerrorf(false, code, secode, lastLine, "malformed base64 data in authentication continuation response")
915 }
916 toserver, last, err = a.Next(fromserver)
917 if err != nil {
918 // For failing SCRAM, the client stops due to message about invalid proof. The
919 // server still sends an authentication result (it probably should send 501
920 // instead).
921 xcode, xsecode, lastline := abort()
922 c.xerrorf(false, xcode, xsecode, lastline, "client aborted authentication: %w", err)
923 }
924 c.xwriteline(base64.StdEncoding.EncodeToString(toserver))
925 } else {
926 c.xerrorf(code/100 == 5, code, secode, lastLine, "unexpected response during authentication, expected 334 continue or 235 auth success")
927 }
928 }
929}
930
931// Supports8BITMIME returns whether the SMTP server supports the 8BITMIME
932// extension, needed for sending data with non-ASCII bytes.
933func (c *Client) Supports8BITMIME() bool {
934 return c.ext8bitmime
935}
936
937// SupportsSMTPUTF8 returns whether the SMTP server supports the SMTPUTF8
938// extension, needed for sending messages with UTF-8 in headers or in an (SMTP)
939// address.
940func (c *Client) SupportsSMTPUTF8() bool {
941 return c.extSMTPUTF8
942}
943
944// SupportsStartTLS returns whether the SMTP server supports the STARTTLS
945// extension.
946func (c *Client) SupportsStartTLS() bool {
947 return c.extStartTLS
948}
949
950// SupportsRequireTLS returns whether the SMTP server supports the REQUIRETLS
951// extension. The REQUIRETLS extension is only announced after enabling
952// STARTTLS.
953func (c *Client) SupportsRequireTLS() bool {
954 return c.extRequireTLS
955}
956
957// TLSEnabled returns whether TLS is enabled for this connection.
958func (c *Client) TLSEnabled() bool {
959 return c.tls
960}
961
962// Deliver attempts to deliver a message to a mail server.
963//
964// mailFrom must be an email address, or empty in case of a DSN. rcptTo must be
965// an email address.
966//
967// If the message contains bytes with the high bit set, req8bitmime must be true. If
968// set, the remote server must support the 8BITMIME extension or delivery will
969// fail.
970//
971// If the message is internationalized, e.g. when headers contain non-ASCII
972// character, or when UTF-8 is used in a localpart, reqSMTPUTF8 must be true. If set,
973// the remote server must support the SMTPUTF8 extension or delivery will fail.
974//
975// If requireTLS is true, the remote server must support the REQUIRETLS
976// extension, or delivery will fail.
977//
978// Deliver uses the following SMTP extensions if the remote server supports them:
979// 8BITMIME, SMTPUTF8, SIZE, PIPELINING, ENHANCEDSTATUSCODES, STARTTLS.
980//
981// Returned errors can be of type Error, one of the Err-variables in this package
982// or other underlying errors, e.g. for i/o. Use errors.Is to check.
983func (c *Client) Deliver(ctx context.Context, mailFrom string, rcptTo string, msgSize int64, msg io.Reader, req8bitmime, reqSMTPUTF8, requireTLS bool) (rerr error) {
984 defer c.recover(&rerr)
985
986 if c.origConn == nil {
987 return ErrClosed
988 } else if c.botched {
989 return ErrBotched
990 } else if c.needRset {
991 if err := c.Reset(); err != nil {
992 return err
993 }
994 }
995
996 if !c.ext8bitmime && req8bitmime {
997 // Temporary error, e.g. OpenBSD spamd does not announce 8bitmime support, but once
998 // you get through, the mail server behind it probably does. Just needs a few
999 // retries.
1000 c.xerrorf(false, 0, "", "", "%w", Err8bitmimeUnsupported)
1001 }
1002 if !c.extSMTPUTF8 && reqSMTPUTF8 {
1003 // ../rfc/6531:313
1004 c.xerrorf(false, 0, "", "", "%w", ErrSMTPUTF8Unsupported)
1005 }
1006 if !c.extRequireTLS && requireTLS {
1007 c.xerrorf(false, 0, "", "", "%w", ErrRequireTLSUnsupported)
1008 }
1009
1010 if c.extSize && msgSize > c.maxSize {
1011 c.xerrorf(true, 0, "", "", "%w: message is %d bytes, remote has a %d bytes maximum size", ErrSize, msgSize, c.maxSize)
1012 }
1013
1014 var mailSize, bodyType string
1015 if c.extSize {
1016 mailSize = fmt.Sprintf(" SIZE=%d", msgSize)
1017 }
1018 if c.ext8bitmime {
1019 if req8bitmime {
1020 bodyType = " BODY=8BITMIME"
1021 } else {
1022 bodyType = " BODY=7BIT"
1023 }
1024 }
1025 var smtputf8Arg string
1026 if reqSMTPUTF8 {
1027 // ../rfc/6531:213
1028 smtputf8Arg = " SMTPUTF8"
1029 }
1030 var requiretlsArg string
1031 if requireTLS {
1032 // ../rfc/8689:155
1033 requiretlsArg = " REQUIRETLS"
1034 }
1035
1036 // Transaction overview: ../rfc/5321:1015
1037 // MAIL FROM: ../rfc/5321:1879
1038 // RCPT TO: ../rfc/5321:1916
1039 // DATA: ../rfc/5321:1992
1040 lineMailFrom := fmt.Sprintf("MAIL FROM:<%s>%s%s%s%s", mailFrom, mailSize, bodyType, smtputf8Arg, requiretlsArg)
1041 lineRcptTo := fmt.Sprintf("RCPT TO:<%s>", rcptTo)
1042
1043 // We are going into a transaction. We'll clear this when done.
1044 c.needRset = true
1045
1046 if c.extPipelining {
1047 c.cmds = []string{"mailfrom", "rcptto", "data"}
1048 c.cmdStart = time.Now()
1049 // 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?
1050 c.xbwriteline(lineMailFrom)
1051 c.xbwriteline(lineRcptTo)
1052 c.xbwriteline("DATA")
1053 c.xflush()
1054
1055 // We read the response to RCPT TO and DATA without panic on read error. Servers
1056 // may be aborting the connection after a failed MAIL FROM, e.g. outlook when it
1057 // has blocklisted your IP. We don't want the read for the response to RCPT TO to
1058 // cause a read error as it would result in an unhelpful error message and a
1059 // temporary instead of permanent error code.
1060
1061 mfcode, mfsecode, mflastline, _ := c.xread()
1062 rtcode, rtsecode, rtlastline, _, rterr := c.read()
1063 datacode, datasecode, datalastline, _, dataerr := c.read()
1064
1065 if mfcode != smtp.C250Completed {
1066 c.xerrorf(mfcode/100 == 5, mfcode, mfsecode, mflastline, "%w: got %d, expected 2xx", ErrStatus, mfcode)
1067 }
1068 if rterr != nil {
1069 panic(rterr)
1070 }
1071 if rtcode != smtp.C250Completed {
1072 c.xerrorf(rtcode/100 == 5, rtcode, rtsecode, rtlastline, "%w: got %d, expected 2xx", ErrStatus, rtcode)
1073 }
1074 if dataerr != nil {
1075 panic(dataerr)
1076 }
1077 if datacode != smtp.C354Continue {
1078 c.xerrorf(datacode/100 == 5, datacode, datasecode, datalastline, "%w: got %d, expected 354", ErrStatus, datacode)
1079 }
1080 } else {
1081 c.cmds[0] = "mailfrom"
1082 c.cmdStart = time.Now()
1083 c.xwriteline(lineMailFrom)
1084 code, secode, lastline, _ := c.xread()
1085 if code != smtp.C250Completed {
1086 c.xerrorf(code/100 == 5, code, secode, lastline, "%w: got %d, expected 2xx", ErrStatus, code)
1087 }
1088
1089 c.cmds[0] = "rcptto"
1090 c.cmdStart = time.Now()
1091 c.xwriteline(lineRcptTo)
1092 code, secode, lastline, _ = c.xread()
1093 if code != smtp.C250Completed {
1094 c.xerrorf(code/100 == 5, code, secode, lastline, "%w: got %d, expected 2xx", ErrStatus, code)
1095 }
1096
1097 c.cmds[0] = "data"
1098 c.cmdStart = time.Now()
1099 c.xwriteline("DATA")
1100 code, secode, lastline, _ = c.xread()
1101 if code != smtp.C354Continue {
1102 c.xerrorf(code/100 == 5, code, secode, lastline, "%w: got %d, expected 354", ErrStatus, code)
1103 }
1104 }
1105
1106 // For a DATA write, the suggested timeout is 3 minutes, we use 30 seconds for all
1107 // writes through timeoutWriter. ../rfc/5321:3651
1108 defer c.xtrace(mlog.LevelTracedata)()
1109 err := smtp.DataWrite(c.w, msg)
1110 if err != nil {
1111 c.xbotchf(0, "", "", "writing message as smtp data: %w", err)
1112 }
1113 c.xflush()
1114 c.xtrace(mlog.LevelTrace) // Restore.
1115 code, secode, lastline, _ := c.xread()
1116 if code != smtp.C250Completed {
1117 c.xerrorf(code/100 == 5, code, secode, lastline, "%w: got %d, expected 2xx", ErrStatus, code)
1118 }
1119
1120 c.needRset = false
1121 return
1122}
1123
1124// Reset sends an SMTP RSET command to reset the message transaction state. Deliver
1125// automatically sends it if needed.
1126func (c *Client) Reset() (rerr error) {
1127 if c.origConn == nil {
1128 return ErrClosed
1129 } else if c.botched {
1130 return ErrBotched
1131 }
1132
1133 defer c.recover(&rerr)
1134
1135 // ../rfc/5321:2079
1136 c.cmds[0] = "rset"
1137 c.cmdStart = time.Now()
1138 c.xwriteline("RSET")
1139 code, secode, lastline, _ := c.xread()
1140 if code != smtp.C250Completed {
1141 c.xerrorf(code/100 == 5, code, secode, lastline, "%w: got %d, expected 2xx", ErrStatus, code)
1142 }
1143 c.needRset = false
1144 return
1145}
1146
1147// Botched returns whether this connection is botched, e.g. a protocol error
1148// occurred and the connection is in unknown state, and cannot be used for message
1149// delivery.
1150func (c *Client) Botched() bool {
1151 return c.botched || c.origConn == nil
1152}
1153
1154// Close cleans up the client, closing the underlying connection.
1155//
1156// If the connection is in initialized and not botched, a QUIT command is sent and
1157// the response read with a short timeout before closing the underlying connection.
1158//
1159// Close returns any error encountered during QUIT and closing.
1160func (c *Client) Close() (rerr error) {
1161 if c.origConn == nil {
1162 return ErrClosed
1163 }
1164
1165 defer c.recover(&rerr)
1166
1167 if !c.botched {
1168 // ../rfc/5321:2205
1169 c.cmds[0] = "quit"
1170 c.cmdStart = time.Now()
1171 c.xwriteline("QUIT")
1172 if err := c.conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
1173 c.log.Infox("setting read deadline for reading quit response", err)
1174 } else if _, err := bufs.Readline(c.r); err != nil {
1175 rerr = fmt.Errorf("reading response to quit command: %v", err)
1176 c.log.Debugx("reading quit response", err)
1177 }
1178 }
1179
1180 err := c.origConn.Close()
1181 if c.conn != c.origConn {
1182 // This is the TLS connection. Close will attempt to write a close notification.
1183 // But it will fail quickly because the underlying socket was closed.
1184 c.conn.Close()
1185 }
1186 c.origConn = nil
1187 c.conn = nil
1188 if rerr != nil {
1189 rerr = err
1190 }
1191 return
1192}
1193
1194// Conn returns the connection with initialized SMTP session. Once the caller uses
1195// this connection it is in control, and responsible for closing the connection,
1196// and other functions on the client must not be called anymore.
1197func (c *Client) Conn() (net.Conn, error) {
1198 if err := c.conn.SetDeadline(time.Time{}); err != nil {
1199 return nil, fmt.Errorf("clearing io deadlines: %w", err)
1200 }
1201 return c.conn, nil
1202}
1203