1// Package smtpclient is an SMTP client, used by the queue for sending outgoing messages.
17 "github.com/prometheus/client_golang/prometheus"
18 "github.com/prometheus/client_golang/prometheus/promauto"
20 "github.com/mjl-/mox/dns"
21 "github.com/mjl-/mox/metrics"
22 "github.com/mjl-/mox/mlog"
23 "github.com/mjl-/mox/mox-"
24 "github.com/mjl-/mox/moxio"
25 "github.com/mjl-/mox/sasl"
26 "github.com/mjl-/mox/smtp"
29// 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
32 metricCommands = promauto.NewHistogramVec(
33 prometheus.HistogramOpts{
34 Name: "mox_smtpclient_command_duration_seconds",
35 Help: "SMTP client command duration and result codes in seconds.",
36 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
47 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.
48 Err8bitmimeUnsupported = errors.New("remote smtp server does not implement 8bitmime extension, required by message")
49 ErrSMTPUTF8Unsupported = errors.New("remote smtp server does not implement smtputf8 extension, required by message")
50 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.
51 ErrProtocol = errors.New("smtp protocol error") // After a malformed SMTP response or inconsistent multi-line response.
52 ErrTLS = errors.New("tls error") // E.g. handshake failure, or hostname validation was required and failed.
53 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.
54 ErrClosed = errors.New("client is closed")
57// TLSMode indicates if TLS must, should or must not be used.
61 // TLS with STARTTLS for MX SMTP servers, with validated certificate is required: matching name, not expired, trusted by CA.
62 TLSStrictStartTLS TLSMode = "strictstarttls"
64 // TLS immediately ("implicit TLS"), with validated certificate is required: matching name, not expired, trusted by CA.
65 TLSStrictImmediate TLSMode = "strictimmediate"
67 // Use TLS if remote claims to support it, but do not validate the certificate
68 // (not trusted by CA, different host name or expired certificate is accepted).
69 TLSOpportunistic TLSMode = "opportunistic"
71 // TLS must not be attempted, e.g. due to earlier TLS handshake error.
72 TLSSkip TLSMode = "skip"
75// Client is an SMTP client that can deliver messages to a mail server.
77// Use New to make a new client.
79 // OrigConn is the original (TCP) connection. We'll read from/write to conn, which
80 // can be wrapped in a tls.Client. We close origConn instead of conn because
81 // closing the TLS connection would send a TLS close notification, which may block
82 // for 5s if the server isn't reading it (because it is also sending it).
88 tr *moxio.TraceReader // Kept for changing trace levels between cmd/auth/data.
91 lastlog time.Time // For adding delta timestamps between log lines.
92 cmds []string // Last or active command, for generating errors and metrics.
93 cmdStart time.Time // Start of command.
95 botched bool // If set, protocol is out of sync and no further commands can be sent.
96 needRset bool // If set, a new delivery requires an RSET command.
98 extEcodes bool // Remote server supports sending extended error codes.
99 extStartTLS bool // Remote server supports STARTTLS.
101 extSize bool // Remote server supports SIZE parameter.
102 maxSize int64 // Max size of email message.
103 extPipelining bool // Remote server supports command pipelining.
104 extSMTPUTF8 bool // Remote server supports SMTPUTF8 extension.
105 extAuthMechanisms []string // Supported authentication mechanisms.
108// Error represents a failure to deliver a message.
110// Code, Secode, Command and Line are only set for SMTP-level errors, and are zero
113 // Whether failure is permanent, typically because of 5xx response.
115 // SMTP response status, e.g. 2xx for success, 4xx for transient error and 5xx for
116 // permanent failure.
118 // Short enhanced status, minus first digit and dot. Can be empty, e.g. for io
119 // errors or if remote does not send enhanced status codes. If remote responds with
120 // "550 5.7.1 ...", the Secode will be "7.1".
122 // SMTP command causing failure.
124 // For errors due to SMTP responses, the full SMTP line excluding CRLF that caused
125 // the error. Typically the last line read.
127 // Underlying error, e.g. one of the Err variables in this package, or io errors.
131// Unwrap returns the underlying Err.
132func (e Error) Unwrap() error {
136// Error returns a readable error string.
137func (e Error) Error() string {
140 s = e.Err.Error() + ", "
153// New initializes an SMTP session on the given connection, returning a client that
154// can be used to deliver messages.
156// New optionally starts TLS (for submission), reads the server greeting,
157// identifies itself with a HELO or EHLO command, initializes TLS with STARTTLS if
158// remote supports it and optionally authenticates. If successful, a client is
159// returned on which eventually Close must be called. Otherwise an error is
160// returned and the caller is responsible for closing the connection.
162// Connecting to the correct host is outside the scope of the client. The queue
163// managing outgoing messages decides which host to deliver to, taking multiple MX
164// records with preferences, other DNS records, MTA-STS, retries and special
165// cases into account.
167// tlsMode indicates if TLS is required, optional or should not be used. A
168// certificate is only validated (trusted, match remoteHostname and not expired)
169// for the strict tls modes. By default, SMTP does not verify TLS for
170// interopability reasons, but MTA-STS or DANE can require it. If opportunistic TLS
171// is used, and a TLS error is encountered, the caller may want to try again (on a
172// new connection) without TLS.
174// If auth is non-empty, authentication will be done with the first algorithm
175// supported by the server. If none of the algorithms are supported, an error is
177func New(ctx context.Context, log *mlog.Log, conn net.Conn, tlsMode TLSMode, ourHostname, remoteHostname dns.Domain, auth []sasl.Client) (*Client, error) {
181 cmds: []string{"(none)"},
183 c.log = log.Fields(mlog.Field("smtpclient", "")).MoreFields(func() []mlog.Pair {
186 mlog.Field("delta", now.Sub(c.lastlog)),
192 if tlsMode == TLSStrictImmediate {
193 tlsconfig := tls.Config{
194 ServerName: remoteHostname.ASCII,
195 RootCAs: mox.Conf.Static.TLS.CertPool,
198 tlsconn := tls.Client(conn, &tlsconfig)
199 if err := tlsconn.HandshakeContext(ctx); err != nil {
203 tlsversion, ciphersuite := mox.TLSInfo(tlsconn)
204 c.log.Debug("tls client handshake done", mlog.Field("tls", tlsversion), mlog.Field("ciphersuite", ciphersuite), mlog.Field("servername", remoteHostname))
209 // We don't wrap reads in a timeoutReader for fear of an optional TLS wrapper doing
210 // reads without the client asking for it. Such reads could result in a timeout
212 c.tr = moxio.NewTraceReader(c.log, "RS: ", c.conn)
213 c.r = bufio.NewReader(c.tr)
214 // We use a single write timeout of 30 seconds.
216 c.tw = moxio.NewTraceWriter(c.log, "LC: ", timeoutWriter{c.conn, 30 * time.Second, c.log})
217 c.w = bufio.NewWriter(c.tw)
219 if err := c.hello(ctx, tlsMode, ourHostname, remoteHostname, auth); err != nil {
225// xbotchf generates a temporary error and marks the client as botched. e.g. for
226// i/o errors or invalid protocol messages.
227func (c *Client) xbotchf(code int, secode string, lastLine, format string, args ...any) {
228 panic(c.botchf(code, secode, lastLine, format, args...))
231// botchf generates a temporary error and marks the client as botched. e.g. for
232// i/o errors or invalid protocol messages.
233func (c *Client) botchf(code int, secode string, lastLine, format string, args ...any) error {
235 return c.errorf(false, code, secode, lastLine, format, args...)
238func (c *Client) errorf(permanent bool, code int, secode, lastLine, format string, args ...any) error {
243 return Error{permanent, code, secode, cmd, lastLine, fmt.Errorf(format, args...)}
246func (c *Client) xerrorf(permanent bool, code int, secode, lastLine, format string, args ...any) {
247 panic(c.errorf(permanent, code, secode, lastLine, format, args...))
250// timeoutWriter passes each Write on to conn after setting a write deadline on conn based on
252type timeoutWriter struct {
254 timeout time.Duration
258func (w timeoutWriter) Write(buf []byte) (int, error) {
259 if err := w.conn.SetWriteDeadline(time.Now().Add(w.timeout)); err != nil {
260 w.log.Errorx("setting write deadline", err)
263 return w.conn.Write(buf)
266var bufs = moxio.NewBufpool(8, 2*1024)
268func (c *Client) readline() (string, error) {
269 // todo: could have per-operation timeouts. and rfc suggests higher minimum timeouts.
../rfc/5321:3610
270 if err := c.conn.SetReadDeadline(time.Now().Add(30 * time.Second)); err != nil {
271 c.log.Errorx("setting read deadline", err)
274 line, err := bufs.Readline(c.r)
276 return line, c.botchf(0, "", "", "%s: %w", strings.Join(c.cmds, ","), err)
281func (c *Client) xtrace(level mlog.Level) func() {
287 c.tr.SetTrace(mlog.LevelTrace)
288 c.tw.SetTrace(mlog.LevelTrace)
292func (c *Client) xwritelinef(format string, args ...any) {
293 c.xbwritelinef(format, args...)
297func (c *Client) xwriteline(line string) {
302func (c *Client) xbwritelinef(format string, args ...any) {
303 c.xbwriteline(fmt.Sprintf(format, args...))
306func (c *Client) xbwriteline(line string) {
307 _, err := fmt.Fprintf(c.w, "%s\r\n", line)
309 c.xbotchf(0, "", "", "write: %w", err)
313func (c *Client) xflush() {
316 c.xbotchf(0, "", "", "writes: %w", err)
320// read response, possibly multiline, with supporting extended codes based on configuration in client.
321func (c *Client) xread() (code int, secode, lastLine string, texts []string) {
323 code, secode, lastLine, texts, err = c.read()
330func (c *Client) read() (code int, secode, lastLine string, texts []string, rerr error) {
331 return c.readecode(c.extEcodes)
334// read response, possibly multiline.
335// if ecodes, extended codes are parsed.
336func (c *Client) readecode(ecodes bool) (code int, secode, lastLine string, texts []string, rerr error) {
338 co, sec, text, line, last, err := c.read1(ecodes)
343 texts = append(texts, text)
344 if code != 0 && co != code {
346 err := c.botchf(0, "", line, "%w: multiline response with different codes, previous %d, last %d", ErrProtocol, code, co)
347 return 0, "", "", nil, err
351 if code != smtp.C334ContinueAuth {
355 // We only keep the last, so we're not creating new slices all the time.
360 metricCommands.WithLabelValues(cmd, fmt.Sprintf("%d", co), sec).Observe(float64(time.Since(c.cmdStart)) / float64(time.Second))
361 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)))
363 return co, sec, line, texts, nil
368func (c *Client) xreadecode(ecodes bool) (code int, secode, lastLine string, texts []string) {
370 code, secode, lastLine, texts, err = c.readecode(ecodes)
377// read single response line.
378// if ecodes, extended codes are parsed.
379func (c *Client) read1(ecodes bool) (code int, secode, text, line string, last bool, rerr error) {
380 line, rerr = c.readline()
385 for ; i < len(line) && line[i] >= '0' && line[i] <= '9'; i++ {
388 rerr = c.botchf(0, "", line, "%w: expected response code: %s", ErrProtocol, line)
391 v, err := strconv.ParseInt(line[:i], 10, 32)
393 rerr = c.botchf(0, "", line, "%w: bad response code (%s): %s", ErrProtocol, err, line)
399 if strings.HasPrefix(s, "-") || strings.HasPrefix(s, " ") {
406 rerr = c.botchf(0, "", line, "%w: expected space or dash after response code: %s", ErrProtocol, line)
411 secode, s = parseEcode(major, s)
414 return code, secode, s, line, last, nil
417func parseEcode(major int, s string) (secode string, remain string) {
420 take := func(need bool, a, b byte) bool {
421 if !bad && o < len(s) && s[o] >= a && s[o] <= b {
428 digit := func(need bool) bool {
429 return take(need, '0', '9')
432 return take(true, '.', '.')
446 take(false, ' ', ' ')
447 if bad || int(s[0])-int('0') != major {
453func (c *Client) recover(rerr *error) {
458 cerr, ok := x.(Error)
460 metrics.PanicInc(metrics.Smtpclient)
466func (c *Client) hello(ctx context.Context, tlsMode TLSMode, ourHostname, remoteHostname dns.Domain, auth []sasl.Client) (rerr error) {
467 defer c.recover(&rerr)
469 // perform EHLO handshake, falling back to HELO if server does not appear to
471 hello := func(heloOK bool) {
472 // Write EHLO and parse the supported extensions.
475 c.cmdStart = time.Now()
477 c.xwritelinef("EHLO %s", ourHostname.ASCII)
478 code, _, lastLine, remains := c.xreadecode(false)
482 case smtp.C500BadSyntax, smtp.C501BadParamSyntax, smtp.C502CmdNotImpl, smtp.C503BadCmdSeq, smtp.C504ParamNotImpl:
484 c.xerrorf(true, code, "", lastLine, "%w: remote claims ehlo is not supported", ErrProtocol)
488 c.cmdStart = time.Now()
489 c.xwritelinef("HELO %s", ourHostname.ASCII)
490 code, _, lastLine, _ = c.xreadecode(false)
491 if code != smtp.C250Completed {
492 c.xerrorf(code/100 == 5, code, "", lastLine, "%w: expected 250 to HELO, got %d", ErrStatus, code)
495 case smtp.C250Completed:
497 c.xerrorf(code/100 == 5, code, "", lastLine, "%w: expected 250, got %d", ErrStatus, code)
499 for _, s := range remains[1:] {
501 s = strings.ToUpper(strings.TrimSpace(s))
505 case "ENHANCEDSTATUSCODES":
510 c.extPipelining = true
513 if s == "SMTPUTF8" || strings.HasPrefix(s, "SMTPUTF8 ") {
515 } else if strings.HasPrefix(s, "SIZE ") {
517 if v, err := strconv.ParseInt(s[len("SIZE "):], 10, 64); err == nil {
520 } else if strings.HasPrefix(s, "AUTH ") {
521 c.extAuthMechanisms = strings.Split(s[len("AUTH "):], " ")
528 c.cmds = []string{"(greeting)"}
529 c.cmdStart = time.Now()
530 code, _, lastLine, _ := c.xreadecode(false)
531 if code != smtp.C220ServiceReady {
532 c.xerrorf(code/100 == 5, code, "", lastLine, "%w: expected 220, got %d", ErrStatus, code)
535 // Write EHLO, falling back to HELO if server doesn't appear to support it.
538 // Attempt TLS if remote understands STARTTLS and we aren't doing immediate TLS or if caller requires it.
539 if c.extStartTLS && (tlsMode != TLSSkip && tlsMode != TLSStrictImmediate) || tlsMode == TLSStrictStartTLS {
540 c.log.Debug("starting tls client", mlog.Field("tlsmode", tlsMode), mlog.Field("servername", remoteHostname))
541 c.cmds[0] = "starttls"
542 c.cmdStart = time.Now()
543 c.xwritelinef("STARTTLS")
544 code, secode, lastLine, _ := c.xread()
546 if code != smtp.C220ServiceReady {
547 c.xerrorf(code/100 == 5, code, secode, lastLine, "%w: STARTTLS: got %d, expected 220", ErrTLS, code)
550 // We don't want to do TLS on top of c.r because it also prints protocol traces: We
551 // don't want to log the TLS stream. So we'll do TLS on the underlying connection,
552 // but make sure any bytes already read and in the buffer are used for the TLS
555 if n := c.r.Buffered(); n > 0 {
556 conn = &moxio.PrefixConn{
557 PrefixReader: io.LimitReader(c.r, int64(n)),
562 // For TLSStrictStartTLS, the Go TLS library performs the checks needed for MTA-STS.
564 // todo: possibly accept older TLS versions for TLSOpportunistic?
565 tlsConfig := &tls.Config{
566 ServerName: remoteHostname.ASCII,
567 RootCAs: mox.Conf.Static.TLS.CertPool,
568 InsecureSkipVerify: tlsMode != TLSStrictStartTLS,
571 nconn := tls.Client(conn, tlsConfig)
574 nctx, cancel := context.WithTimeout(ctx, time.Minute)
576 err := nconn.HandshakeContext(nctx)
578 c.xerrorf(false, 0, "", "", "%w: STARTTLS TLS handshake: %s", ErrTLS, err)
581 c.tr = moxio.NewTraceReader(c.log, "RS: ", c.conn)
582 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.
583 c.r = bufio.NewReader(c.tr)
584 c.w = bufio.NewWriter(c.tw)
586 tlsversion, ciphersuite := mox.TLSInfo(nconn)
587 c.log.Debug("starttls client handshake done", mlog.Field("tls", tlsversion), mlog.Field("ciphersuite", ciphersuite), mlog.Field("servername", remoteHostname), mlog.Field("insecureskipverify", tlsConfig.InsecureSkipVerify))
599func (c *Client) auth(auth []sasl.Client) (rerr error) {
600 defer c.recover(&rerr)
603 c.cmdStart = time.Now()
607 var cleartextCreds bool
608 for _, x := range auth {
609 name, cleartextCreds = x.Info()
610 for _, s := range c.extAuthMechanisms {
618 c.xerrorf(true, 0, "", "", "no matching authentication mechanisms, server supports %s", strings.Join(c.extAuthMechanisms, ", "))
621 abort := func() (int, string, string) {
626 code, secode, lastline, _ := c.xread()
627 if code != smtp.C501BadParamSyntax {
630 return code, secode, lastline
633 toserver, last, err := a.Next(nil)
635 c.xerrorf(false, 0, "", "", "initial step in auth mechanism %s: %w", name, err)
638 defer c.xtrace(mlog.LevelTraceauth)()
641 c.xwriteline("AUTH " + name)
642 } else if len(toserver) == 0 {
645 c.xwriteline("AUTH " + name + " " + base64.StdEncoding.EncodeToString(toserver))
648 if cleartextCreds && last {
649 c.xtrace(mlog.LevelTrace) // Restore.
652 code, secode, lastLine, texts := c.xreadecode(last)
653 if code == smtp.C235AuthSuccess {
655 c.xerrorf(false, code, secode, lastLine, "server completed authentication earlier than client expected")
658 } else if code == smtp.C334ContinueAuth {
660 c.xerrorf(false, code, secode, lastLine, "server requested unexpected continuation of authentication")
664 c.xerrorf(false, code, secode, lastLine, "server responded with multiline contination")
666 fromserver, err := base64.StdEncoding.DecodeString(texts[0])
669 c.xerrorf(false, code, secode, lastLine, "malformed base64 data in authentication continuation response")
671 toserver, last, err = a.Next(fromserver)
673 // For failing SCRAM, the client stops due to message about invalid proof. The
674 // server still sends an authentication result (it probably should send 501
676 xcode, xsecode, lastline := abort()
677 c.xerrorf(false, xcode, xsecode, lastline, "client aborted authentication: %w", err)
679 c.xwriteline(base64.StdEncoding.EncodeToString(toserver))
681 c.xerrorf(code/100 == 5, code, secode, lastLine, "unexpected response during authentication, expected 334 continue or 235 auth success")
686// Supports8BITMIME returns whether the SMTP server supports the 8BITMIME
687// extension, needed for sending data with non-ASCII bytes.
688func (c *Client) Supports8BITMIME() bool {
692// SupportsSMTPUTF8 returns whether the SMTP server supports the SMTPUTF8
693// extension, needed for sending messages with UTF-8 in headers or in an (SMTP)
695func (c *Client) SupportsSMTPUTF8() bool {
699// Deliver attempts to deliver a message to a mail server.
701// mailFrom must be an email address, or empty in case of a DSN. rcptTo must be
704// If the message contains bytes with the high bit set, req8bitmime must be true. If
705// set, the remote server must support the 8BITMIME extension or delivery will
708// If the message is internationalized, e.g. when headers contain non-ASCII
709// character, or when UTF-8 is used in a localpart, reqSMTPUTF8 must be true. If set,
710// the remote server must support the SMTPUTF8 extension or delivery will fail.
712// Deliver uses the following SMTP extensions if the remote server supports them:
713// 8BITMIME, SMTPUTF8, SIZE, PIPELINING, ENHANCEDSTATUSCODES, STARTTLS.
715// Returned errors can be of type Error, one of the Err-variables in this package
716// or other underlying errors, e.g. for i/o. Use errors.Is to check.
717func (c *Client) Deliver(ctx context.Context, mailFrom string, rcptTo string, msgSize int64, msg io.Reader, req8bitmime, reqSMTPUTF8 bool) (rerr error) {
718 defer c.recover(&rerr)
720 if c.origConn == nil {
722 } else if c.botched {
724 } else if c.needRset {
725 if err := c.Reset(); err != nil {
730 if !c.ext8bitmime && req8bitmime {
731 // Temporary error, e.g. OpenBSD spamd does not announce 8bitmime support, but once
732 // you get through, the mail server behind it probably does. Just needs a few
734 c.xerrorf(false, 0, "", "", "%w", Err8bitmimeUnsupported)
736 if !c.extSMTPUTF8 && reqSMTPUTF8 {
738 c.xerrorf(false, 0, "", "", "%w", ErrSMTPUTF8Unsupported)
741 if c.extSize && msgSize > c.maxSize {
742 c.xerrorf(true, 0, "", "", "%w: message is %d bytes, remote has a %d bytes maximum size", ErrSize, msgSize, c.maxSize)
745 var mailSize, bodyType string
747 mailSize = fmt.Sprintf(" SIZE=%d", msgSize)
751 bodyType = " BODY=8BITMIME"
753 bodyType = " BODY=7BIT"
756 var smtputf8Arg string
759 smtputf8Arg = " SMTPUTF8"
766 lineMailFrom := fmt.Sprintf("MAIL FROM:<%s>%s%s%s", mailFrom, mailSize, bodyType, smtputf8Arg)
767 lineRcptTo := fmt.Sprintf("RCPT TO:<%s>", rcptTo)
769 // We are going into a transaction. We'll clear this when done.
773 c.cmds = []string{"mailfrom", "rcptto", "data"}
774 c.cmdStart = time.Now()
775 // 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?
776 c.xbwriteline(lineMailFrom)
777 c.xbwriteline(lineRcptTo)
778 c.xbwriteline("DATA")
781 // We read the response to RCPT TO and DATA without panic on read error. Servers
782 // may be aborting the connection after a failed MAIL FROM, e.g. outlook when it
783 // has blocklisted your IP. We don't want the read for the response to RCPT TO to
784 // cause a read error as it would result in an unhelpful error message and a
785 // temporary instead of permanent error code.
787 mfcode, mfsecode, mflastline, _ := c.xread()
788 rtcode, rtsecode, rtlastline, _, rterr := c.read()
789 datacode, datasecode, datalastline, _, dataerr := c.read()
791 if mfcode != smtp.C250Completed {
792 c.xerrorf(mfcode/100 == 5, mfcode, mfsecode, mflastline, "%w: got %d, expected 2xx", ErrStatus, mfcode)
797 if rtcode != smtp.C250Completed {
798 c.xerrorf(rtcode/100 == 5, rtcode, rtsecode, rtlastline, "%w: got %d, expected 2xx", ErrStatus, rtcode)
803 if datacode != smtp.C354Continue {
804 c.xerrorf(datacode/100 == 5, datacode, datasecode, datalastline, "%w: got %d, expected 354", ErrStatus, datacode)
807 c.cmds[0] = "mailfrom"
808 c.cmdStart = time.Now()
809 c.xwriteline(lineMailFrom)
810 code, secode, lastline, _ := c.xread()
811 if code != smtp.C250Completed {
812 c.xerrorf(code/100 == 5, code, secode, lastline, "%w: got %d, expected 2xx", ErrStatus, code)
816 c.cmdStart = time.Now()
817 c.xwriteline(lineRcptTo)
818 code, secode, lastline, _ = c.xread()
819 if code != smtp.C250Completed {
820 c.xerrorf(code/100 == 5, code, secode, lastline, "%w: got %d, expected 2xx", ErrStatus, code)
824 c.cmdStart = time.Now()
826 code, secode, lastline, _ = c.xread()
827 if code != smtp.C354Continue {
828 c.xerrorf(code/100 == 5, code, secode, lastline, "%w: got %d, expected 354", ErrStatus, code)
832 // For a DATA write, the suggested timeout is 3 minutes, we use 30 seconds for all
834 defer c.xtrace(mlog.LevelTracedata)()
835 err := smtp.DataWrite(c.w, msg)
837 c.xbotchf(0, "", "", "writing message as smtp data: %w", err)
840 c.xtrace(mlog.LevelTrace) // Restore.
841 code, secode, lastline, _ := c.xread()
842 if code != smtp.C250Completed {
843 c.xerrorf(code/100 == 5, code, secode, lastline, "%w: got %d, expected 2xx", ErrStatus, code)
850// Reset sends an SMTP RSET command to reset the message transaction state. Deliver
851// automatically sends it if needed.
852func (c *Client) Reset() (rerr error) {
853 if c.origConn == nil {
855 } else if c.botched {
859 defer c.recover(&rerr)
863 c.cmdStart = time.Now()
865 code, secode, lastline, _ := c.xread()
866 if code != smtp.C250Completed {
867 c.xerrorf(code/100 == 5, code, secode, lastline, "%w: got %d, expected 2xx", ErrStatus, code)
873// Botched returns whether this connection is botched, e.g. a protocol error
874// occurred and the connection is in unknown state, and cannot be used for message
876func (c *Client) Botched() bool {
877 return c.botched || c.origConn == nil
880// Close cleans up the client, closing the underlying connection.
882// If the connection is in initialized and not botched, a QUIT command is sent and
883// the response read with a short timeout before closing the underlying connection.
885// Close returns any error encountered during QUIT and closing.
886func (c *Client) Close() (rerr error) {
887 if c.origConn == nil {
891 defer c.recover(&rerr)
896 c.cmdStart = time.Now()
898 if err := c.conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
899 c.log.Infox("setting read deadline for reading quit response", err)
900 } else if _, err := bufs.Readline(c.r); err != nil {
901 rerr = fmt.Errorf("reading response to quit command: %v", err)
902 c.log.Debugx("reading quit response", err)
906 err := c.origConn.Close()
907 if c.conn != c.origConn {
908 // This is the TLS connection. Close will attempt to write a close notification.
909 // But it will fail quickly because the underlying socket was closed.