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