include full smtp response in dsn on errors

we now keep track of the full smtp error responses, potentially multi-line. and
we include it in a dsn in the first free-form human-readable text.

it can have multiple lines in practice, e.g. when a destination mail server
tries to be helpful in explaining what the problem is.
This commit is contained in:
Mechiel Lukkien 2024-02-14 23:37:43 +01:00
parent 39bfa4338a
commit 50c13965a7
No known key found for this signature in database
5 changed files with 116 additions and 100 deletions

View file

@ -89,14 +89,14 @@ var (
) )
// todo: rename function, perhaps put some of the params in a delivery struct so we don't pass all the params all the time? // todo: rename function, perhaps put some of the params in a delivery struct so we don't pass all the params all the time?
func fail(ctx context.Context, qlog mlog.Log, m Msg, backoff time.Duration, permanent bool, remoteMTA dsn.NameIP, secodeOpt, errmsg string) { func fail(ctx context.Context, qlog mlog.Log, m Msg, backoff time.Duration, permanent bool, remoteMTA dsn.NameIP, secodeOpt, errmsg string, moreLines []string) {
// todo future: when we implement relaying, we should be able to send DSNs to non-local users. and possibly specify a null mailfrom. ../rfc/5321:1503 // todo future: when we implement relaying, we should be able to send DSNs to non-local users. and possibly specify a null mailfrom. ../rfc/5321:1503
// todo future: when we implement relaying, and a dsn cannot be delivered, and requiretls was active, we cannot drop the message. instead deliver to local postmaster? though ../rfc/8689:383 may intend to say the dsn should be delivered without requiretls? // todo future: when we implement relaying, and a dsn cannot be delivered, and requiretls was active, we cannot drop the message. instead deliver to local postmaster? though ../rfc/8689:383 may intend to say the dsn should be delivered without requiretls?
// todo future: when we implement smtp dsn extension, parameter RET=FULL must be disregarded for messages with REQUIRETLS. ../rfc/8689:379 // todo future: when we implement smtp dsn extension, parameter RET=FULL must be disregarded for messages with REQUIRETLS. ../rfc/8689:379
if permanent || m.MaxAttempts == 0 && m.Attempts >= 8 || m.MaxAttempts > 0 && m.Attempts >= m.MaxAttempts { if permanent || m.MaxAttempts == 0 && m.Attempts >= 8 || m.MaxAttempts > 0 && m.Attempts >= m.MaxAttempts {
qlog.Errorx("permanent failure delivering from queue", errors.New(errmsg)) qlog.Errorx("permanent failure delivering from queue", errors.New(errmsg))
deliverDSNFailure(ctx, qlog, m, remoteMTA, secodeOpt, errmsg) deliverDSNFailure(ctx, qlog, m, remoteMTA, secodeOpt, errmsg, moreLines)
if err := queueDelete(context.Background(), m.ID); err != nil { if err := queueDelete(context.Background(), m.ID); err != nil {
qlog.Errorx("deleting message from queue after permanent failure", err) qlog.Errorx("deleting message from queue after permanent failure", err)
@ -116,7 +116,7 @@ func fail(ctx context.Context, qlog mlog.Log, m Msg, backoff time.Duration, perm
qlog.Errorx("temporary failure delivering from queue, sending delayed dsn", errors.New(errmsg), slog.Duration("backoff", backoff)) qlog.Errorx("temporary failure delivering from queue, sending delayed dsn", errors.New(errmsg), slog.Duration("backoff", backoff))
retryUntil := m.LastAttempt.Add((4 + 8 + 16) * time.Hour) retryUntil := m.LastAttempt.Add((4 + 8 + 16) * time.Hour)
deliverDSNDelay(ctx, qlog, m, remoteMTA, secodeOpt, errmsg, retryUntil) deliverDSNDelay(ctx, qlog, m, remoteMTA, secodeOpt, errmsg, moreLines, retryUntil)
} else { } else {
qlog.Errorx("temporary failure delivering from queue", errors.New(errmsg), slog.Duration("backoff", backoff), slog.Time("nextattempt", m.NextAttempt)) qlog.Errorx("temporary failure delivering from queue", errors.New(errmsg), slog.Duration("backoff", backoff), slog.Time("nextattempt", m.NextAttempt))
} }
@ -168,7 +168,7 @@ func deliverDirect(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Diale
recipientDomainResult.Summary.TotalFailureSessionCount++ recipientDomainResult.Summary.TotalFailureSessionCount++
} }
fail(ctx, qlog, m, backoff, permanent, dsn.NameIP{}, "", err.Error()) fail(ctx, qlog, m, backoff, permanent, dsn.NameIP{}, "", err.Error(), nil)
return return
} }
@ -188,7 +188,7 @@ func deliverDirect(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Diale
} else { } else {
qlog.Infox("mtasts lookup temporary error, aborting delivery attempt", err, slog.Any("domain", origNextHop)) qlog.Infox("mtasts lookup temporary error, aborting delivery attempt", err, slog.Any("domain", origNextHop))
recipientDomainResult.Summary.TotalFailureSessionCount++ recipientDomainResult.Summary.TotalFailureSessionCount++
fail(ctx, qlog, m, backoff, false, dsn.NameIP{}, "", err.Error()) fail(ctx, qlog, m, backoff, false, dsn.NameIP{}, "", err.Error(), nil)
return return
} }
} }
@ -206,6 +206,7 @@ func deliverDirect(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Diale
// ../rfc/3974:268. // ../rfc/3974:268.
var remoteMTA dsn.NameIP var remoteMTA dsn.NameIP
var secodeOpt, errmsg string var secodeOpt, errmsg string
var moreLines []string // For additional SMTP response lines, included in DSN.
permanent = false permanent = false
nmissingRequireTLS := 0 nmissingRequireTLS := 0
// todo: should make distinction between host permanently not accepting the message, and the message not being deliverable permanently. e.g. a mx host may have a size limit, or not accept 8bitmime, while another host in the list does accept the message. same for smtputf8, ../rfc/6531:555 // todo: should make distinction between host permanently not accepting the message, and the message not being deliverable permanently. e.g. a mx host may have a size limit, or not accept 8bitmime, while another host in the list does accept the message. same for smtputf8, ../rfc/6531:555
@ -230,6 +231,7 @@ func deliverDirect(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Diale
metricTLSRequiredNoIgnored.WithLabelValues("mtastsmx").Inc() metricTLSRequiredNoIgnored.WithLabelValues("mtastsmx").Inc()
} else { } else {
errmsg = fmt.Sprintf("mx host %s does not match enforced mta-sts policy with hosts %s", h.Domain, strings.Join(policyHosts, ",")) errmsg = fmt.Sprintf("mx host %s does not match enforced mta-sts policy with hosts %s", h.Domain, strings.Join(policyHosts, ","))
moreLines = nil
qlog.Error("mx host does not match mta-sts policy in mode enforce, skipping", slog.Any("host", h.Domain), slog.Any("policyhosts", policyHosts)) qlog.Error("mx host does not match mta-sts policy in mode enforce, skipping", slog.Any("host", h.Domain), slog.Any("policyhosts", policyHosts))
recipientDomainResult.Summary.TotalFailureSessionCount++ recipientDomainResult.Summary.TotalFailureSessionCount++
continue continue
@ -269,7 +271,7 @@ func deliverDirect(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Diale
var badTLS, ok bool var badTLS, ok bool
var hostResult tlsrpt.Result var hostResult tlsrpt.Result
permanent, tlsDANE, badTLS, secodeOpt, remoteIP, errmsg, hostResult, ok = deliverHost(nqlog, resolver, dialer, ourHostname, transportName, h, enforceMTASTS, haveMX, origNextHopAuthentic, origNextHop, expandedNextHopAuthentic, expandedNextHop, &m, tlsMode, tlsPKIX, &recipientDomainResult) permanent, tlsDANE, badTLS, secodeOpt, remoteIP, errmsg, moreLines, hostResult, ok = deliverHost(nqlog, resolver, dialer, ourHostname, transportName, h, enforceMTASTS, haveMX, origNextHopAuthentic, origNextHop, expandedNextHopAuthentic, expandedNextHop, &m, tlsMode, tlsPKIX, &recipientDomainResult)
var zerotype tlsrpt.PolicyType var zerotype tlsrpt.PolicyType
if hostResult.Policy.Type != zerotype { if hostResult.Policy.Type != zerotype {
@ -296,7 +298,7 @@ func deliverDirect(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Diale
slog.Bool("enforcemtasts", enforceMTASTS), slog.Bool("enforcemtasts", enforceMTASTS),
slog.Bool("tlsdane", tlsDANE), slog.Bool("tlsdane", tlsDANE),
slog.Any("requiretls", m.RequireTLS)) slog.Any("requiretls", m.RequireTLS))
permanent, _, _, secodeOpt, remoteIP, errmsg, _, ok = deliverHost(nqlog, resolver, dialer, ourHostname, transportName, h, enforceMTASTS, haveMX, origNextHopAuthentic, origNextHop, expandedNextHopAuthentic, expandedNextHop, &m, smtpclient.TLSSkip, false, &tlsrpt.Result{}) permanent, _, _, secodeOpt, remoteIP, errmsg, moreLines, _, ok = deliverHost(nqlog, resolver, dialer, ourHostname, transportName, h, enforceMTASTS, haveMX, origNextHopAuthentic, origNextHop, expandedNextHopAuthentic, expandedNextHop, &m, smtpclient.TLSSkip, false, &tlsrpt.Result{})
} }
if ok { if ok {
@ -332,7 +334,7 @@ func deliverDirect(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Diale
permanent = true permanent = true
} }
fail(ctx, qlog, m, backoff, permanent, remoteMTA, secodeOpt, errmsg) fail(ctx, qlog, m, backoff, permanent, remoteMTA, secodeOpt, errmsg, moreLines)
return return
} }
@ -352,7 +354,7 @@ func deliverDirect(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Diale
// The returned hostResult holds TLSRPT reporting results for the connection // The returned hostResult holds TLSRPT reporting results for the connection
// attempt. Its policy type can be the zero value, indicating there was no finding // attempt. Its policy type can be the zero value, indicating there was no finding
// (e.g. internal error). // (e.g. internal error).
func deliverHost(log mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer, ourHostname dns.Domain, transportName string, host dns.IPDomain, enforceMTASTS, haveMX, origNextHopAuthentic bool, origNextHop dns.Domain, expandedNextHopAuthentic bool, expandedNextHop dns.Domain, m *Msg, tlsMode smtpclient.TLSMode, tlsPKIX bool, recipientDomainResult *tlsrpt.Result) (permanent, tlsDANE, badTLS bool, secodeOpt string, remoteIP net.IP, errmsg string, hostResult tlsrpt.Result, ok bool) { func deliverHost(log mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer, ourHostname dns.Domain, transportName string, host dns.IPDomain, enforceMTASTS, haveMX, origNextHopAuthentic bool, origNextHop dns.Domain, expandedNextHopAuthentic bool, expandedNextHop dns.Domain, m *Msg, tlsMode smtpclient.TLSMode, tlsPKIX bool, recipientDomainResult *tlsrpt.Result) (permanent, tlsDANE, badTLS bool, secodeOpt string, remoteIP net.IP, errmsg string, moreLines []string, hostResult tlsrpt.Result, ok bool) {
// About attempting delivery to multiple addresses of a host: ../rfc/5321:3898 // About attempting delivery to multiple addresses of a host: ../rfc/5321:3898
tlsRequiredNo := m.RequireTLS != nil && !*m.RequireTLS tlsRequiredNo := m.RequireTLS != nil && !*m.RequireTLS
@ -386,7 +388,7 @@ func deliverHost(log mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer,
// Open message to deliver. // Open message to deliver.
f, err := os.Open(m.MessagePath()) f, err := os.Open(m.MessagePath())
if err != nil { if err != nil {
return false, false, false, "", nil, fmt.Sprintf("open message file: %s", err), hostResult, false return false, false, false, "", nil, fmt.Sprintf("open message file: %s", err), nil, hostResult, false
} }
msgr := store.FileMsgReader(m.MsgPrefix, f) msgr := store.FileMsgReader(m.MsgPrefix, f)
defer func() { defer func() {
@ -517,7 +519,7 @@ func deliverHost(log mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer,
log.Info("verified tls is required, but destination has no usable dane records and no mta-sts policy, canceling delivery attempt to host") log.Info("verified tls is required, but destination has no usable dane records and no mta-sts policy, canceling delivery attempt to host")
metricRequireTLSUnsupported.WithLabelValues("nopolicy").Inc() metricRequireTLSUnsupported.WithLabelValues("nopolicy").Inc()
// Resond with proper enhanced status code. ../rfc/8689:301 // Resond with proper enhanced status code. ../rfc/8689:301
return false, tlsDANE, false, smtp.SePol7MissingReqTLS, remoteIP, "missing required tls verification mechanism", hostResult, false return false, tlsDANE, false, smtp.SePol7MissingReqTLS, remoteIP, "missing required tls verification mechanism", nil, hostResult, false
} }
// Dial the remote host given the IPs if no error yet. // Dial the remote host given the IPs if no error yet.
@ -545,7 +547,7 @@ func deliverHost(log mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer,
metricConnection.WithLabelValues(result).Inc() metricConnection.WithLabelValues(result).Inc()
if err != nil { if err != nil {
log.Debugx("connecting to remote smtp", err, slog.Any("host", host)) log.Debugx("connecting to remote smtp", err, slog.Any("host", host))
return false, tlsDANE, false, "", remoteIP, fmt.Sprintf("dialing smtp server: %v", err), hostResult, false return false, tlsDANE, false, "", remoteIP, fmt.Sprintf("dialing smtp server: %v", err), nil, hostResult, false
} }
var mailFrom string var mailFrom string
@ -636,7 +638,7 @@ func deliverHost(log mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer,
deliveryResult = "error" deliveryResult = "error"
} }
if err == nil { if err == nil {
return false, tlsDANE, false, "", remoteIP, "", hostResult, true return false, tlsDANE, false, "", remoteIP, "", nil, hostResult, true
} else if cerr, ok := err.(smtpclient.Error); ok { } else if cerr, ok := err.(smtpclient.Error); ok {
// If we are being rejected due to policy reasons on the first // If we are being rejected due to policy reasons on the first
// attempt and remote has both IPv4 and IPv6, we'll give it // attempt and remote has both IPv4 and IPv6, we'll give it
@ -652,9 +654,9 @@ func deliverHost(log mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer,
secode = smtp.SePol7MissingReqTLS secode = smtp.SePol7MissingReqTLS
metricRequireTLSUnsupported.WithLabelValues("norequiretls").Inc() metricRequireTLSUnsupported.WithLabelValues("norequiretls").Inc()
} }
return permanent, tlsDANE, errors.Is(cerr, smtpclient.ErrTLS), secode, remoteIP, cerr.Error(), hostResult, false return permanent, tlsDANE, errors.Is(cerr, smtpclient.ErrTLS), secode, remoteIP, cerr.Error(), cerr.MoreLines, hostResult, false
} else { } else {
return false, tlsDANE, errors.Is(cerr, smtpclient.ErrTLS), "", remoteIP, err.Error(), hostResult, false return false, tlsDANE, errors.Is(cerr, smtpclient.ErrTLS), "", remoteIP, err.Error(), nil, hostResult, false
} }
} }

View file

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"os" "os"
"strings"
"time" "time"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
@ -29,7 +30,7 @@ var (
) )
) )
func deliverDSNFailure(ctx context.Context, log mlog.Log, m Msg, remoteMTA dsn.NameIP, secodeOpt, errmsg string) { func deliverDSNFailure(ctx context.Context, log mlog.Log, m Msg, remoteMTA dsn.NameIP, secodeOpt, errmsg string, moreLines []string) {
const subject = "mail delivery failed" const subject = "mail delivery failed"
message := fmt.Sprintf(` message := fmt.Sprintf(`
Delivery has failed permanently for your email to: Delivery has failed permanently for your email to:
@ -41,12 +42,12 @@ No further deliveries will be attempted.
Error during the last delivery attempt: Error during the last delivery attempt:
%s %s
`, m.Recipient().XString(m.SMTPUTF8), errmsg) `, m.Recipient().XString(m.SMTPUTF8), strings.Join(append([]string{errmsg}, moreLines...), "\n\t"))
deliverDSN(ctx, log, m, remoteMTA, secodeOpt, errmsg, true, nil, subject, message) deliverDSN(ctx, log, m, remoteMTA, secodeOpt, errmsg, true, nil, subject, message)
} }
func deliverDSNDelay(ctx context.Context, log mlog.Log, m Msg, remoteMTA dsn.NameIP, secodeOpt, errmsg string, retryUntil time.Time) { func deliverDSNDelay(ctx context.Context, log mlog.Log, m Msg, remoteMTA dsn.NameIP, secodeOpt, errmsg string, moreLines []string, retryUntil time.Time) {
// Should not happen, but doesn't hurt to prevent sending delayed delivery // Should not happen, but doesn't hurt to prevent sending delayed delivery
// notifications for DMARC reports. We don't want to waste postmaster attention. // notifications for DMARC reports. We don't want to waste postmaster attention.
if m.IsDMARCReport { if m.IsDMARCReport {
@ -65,7 +66,7 @@ If these attempts all fail, you will receive a notice.
Error during the last delivery attempt: Error during the last delivery attempt:
%s %s
`, m.Recipient().XString(false), errmsg) `, m.Recipient().XString(false), strings.Join(append([]string{errmsg}, moreLines...), "\n\t"))
deliverDSN(ctx, log, m, remoteMTA, secodeOpt, errmsg, false, &retryUntil, subject, message) deliverDSN(ctx, log, m, remoteMTA, secodeOpt, errmsg, false, &retryUntil, subject, message)
} }

View file

@ -581,7 +581,7 @@ func deliver(log mlog.Log, resolver dns.Resolver, m Msg) {
transport, ok = mox.Conf.Static.Transports[m.Transport] transport, ok = mox.Conf.Static.Transports[m.Transport]
if !ok { if !ok {
var remoteMTA dsn.NameIP // Zero value, will not be included in DSN. ../rfc/3464:1027 var remoteMTA dsn.NameIP // Zero value, will not be included in DSN. ../rfc/3464:1027
fail(ctx, qlog, m, backoff, false, remoteMTA, "", fmt.Sprintf("cannot find transport %q", m.Transport)) fail(ctx, qlog, m, backoff, false, remoteMTA, "", fmt.Sprintf("cannot find transport %q", m.Transport), nil)
return return
} }
transportName = m.Transport transportName = m.Transport
@ -692,10 +692,10 @@ func deliver(log mlog.Log, resolver dns.Resolver, m Msg) {
if transport.Socks != nil { if transport.Socks != nil {
socksdialer, err := proxy.SOCKS5("tcp", transport.Socks.Address, nil, &net.Dialer{}) socksdialer, err := proxy.SOCKS5("tcp", transport.Socks.Address, nil, &net.Dialer{})
if err != nil { if err != nil {
fail(ctx, qlog, m, backoff, false, dsn.NameIP{}, "", fmt.Sprintf("socks dialer: %v", err)) fail(ctx, qlog, m, backoff, false, dsn.NameIP{}, "", fmt.Sprintf("socks dialer: %v", err), nil)
return return
} else if d, ok := socksdialer.(smtpclient.Dialer); !ok { } else if d, ok := socksdialer.(smtpclient.Dialer); !ok {
fail(ctx, qlog, m, backoff, false, dsn.NameIP{}, "", "socks dialer is not a contextdialer") fail(ctx, qlog, m, backoff, false, dsn.NameIP{}, "", "socks dialer is not a contextdialer", nil)
return return
} else { } else {
dialer = d dialer = d

View file

@ -78,7 +78,7 @@ func deliverSubmit(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Diale
requireTLS := m.RequireTLS != nil && *m.RequireTLS requireTLS := m.RequireTLS != nil && *m.RequireTLS
if requireTLS && (tlsMode != smtpclient.TLSRequiredStartTLS && tlsMode != smtpclient.TLSImmediate || !tlsPKIX) { if requireTLS && (tlsMode != smtpclient.TLSRequiredStartTLS && tlsMode != smtpclient.TLSImmediate || !tlsPKIX) {
errmsg = fmt.Sprintf("transport %s: message requires verified tls but transport does not verify tls", transportName) errmsg = fmt.Sprintf("transport %s: message requires verified tls but transport does not verify tls", transportName)
fail(ctx, qlog, m, backoff, true, dsn.NameIP{}, smtp.SePol7MissingReqTLS, errmsg) fail(ctx, qlog, m, backoff, true, dsn.NameIP{}, smtp.SePol7MissingReqTLS, errmsg, nil)
return return
} }
@ -115,7 +115,7 @@ func deliverSubmit(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Diale
} }
qlog.Errorx("dialing for submission", err, slog.String("remote", addr)) qlog.Errorx("dialing for submission", err, slog.String("remote", addr))
errmsg = fmt.Sprintf("transport %s: dialing %s for submission: %v", transportName, addr, err) errmsg = fmt.Sprintf("transport %s: dialing %s for submission: %v", transportName, addr, err)
fail(ctx, qlog, m, backoff, false, dsn.NameIP{}, "", errmsg) fail(ctx, qlog, m, backoff, false, dsn.NameIP{}, "", errmsg, nil)
return return
} }
dialcancel() dialcancel()
@ -171,7 +171,7 @@ func deliverSubmit(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Diale
qlog.Errorx("establishing smtp session for submission", err, slog.String("remote", addr)) qlog.Errorx("establishing smtp session for submission", err, slog.String("remote", addr))
errmsg = fmt.Sprintf("transport %s: establishing smtp session with %s for submission: %v", transportName, addr, err) errmsg = fmt.Sprintf("transport %s: establishing smtp session with %s for submission: %v", transportName, addr, err)
secodeOpt = smtperr.Secode secodeOpt = smtperr.Secode
fail(ctx, qlog, m, backoff, false, remoteMTA, secodeOpt, errmsg) fail(ctx, qlog, m, backoff, false, remoteMTA, secodeOpt, errmsg, smtperr.MoreLines)
return return
} }
defer func() { defer func() {
@ -196,7 +196,7 @@ func deliverSubmit(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Diale
if err != nil { if err != nil {
qlog.Errorx("opening message for delivery", err, slog.String("remote", addr), slog.String("path", p)) qlog.Errorx("opening message for delivery", err, slog.String("remote", addr), slog.String("path", p))
errmsg = fmt.Sprintf("transport %s: opening message file for submission: %v", transportName, err) errmsg = fmt.Sprintf("transport %s: opening message file for submission: %v", transportName, err)
fail(ctx, qlog, m, backoff, false, dsn.NameIP{}, "", errmsg) fail(ctx, qlog, m, backoff, false, dsn.NameIP{}, "", errmsg, nil)
return return
} }
msgr = store.FileMsgReader(m.MsgPrefix, f) msgr = store.FileMsgReader(m.MsgPrefix, f)
@ -239,7 +239,7 @@ func deliverSubmit(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Diale
permanent = smtperr.Permanent permanent = smtperr.Permanent
secodeOpt = smtperr.Secode secodeOpt = smtperr.Secode
errmsg = fmt.Sprintf("transport %s: submitting email to %s: %v", transportName, addr, err) errmsg = fmt.Sprintf("transport %s: submitting email to %s: %v", transportName, addr, err)
fail(ctx, qlog, m, backoff, permanent, remoteMTA, secodeOpt, errmsg) fail(ctx, qlog, m, backoff, permanent, remoteMTA, secodeOpt, errmsg, smtperr.MoreLines)
return return
} }
qlog.Info("delivered from queue with transport") qlog.Info("delivered from queue with transport")

View file

@ -163,6 +163,9 @@ type Error struct {
// For errors due to SMTP responses, the full SMTP line excluding CRLF that caused // For errors due to SMTP responses, the full SMTP line excluding CRLF that caused
// the error. Typically the last line read. // the error. Typically the last line read.
Line string Line string
// Optional additional lines in case of multi-line SMTP response. Most SMTP
// responses are single-line, leaving this field empty.
MoreLines []string
// Underlying error, e.g. one of the Err variables in this package, or io errors. // Underlying error, e.g. one of the Err variables in this package, or io errors.
Err error Err error
} }
@ -418,27 +421,27 @@ func (c *Client) tlsConfig() *tls.Config {
// xbotchf generates a temporary error and marks the client as botched. e.g. for // xbotchf generates a temporary error and marks the client as botched. e.g. for
// i/o errors or invalid protocol messages. // i/o errors or invalid protocol messages.
func (c *Client) xbotchf(code int, secode string, lastLine, format string, args ...any) { func (c *Client) xbotchf(code int, secode string, firstLine string, moreLines []string, format string, args ...any) {
panic(c.botchf(code, secode, lastLine, format, args...)) panic(c.botchf(code, secode, firstLine, moreLines, format, args...))
} }
// botchf generates a temporary error and marks the client as botched. e.g. for // botchf generates a temporary error and marks the client as botched. e.g. for
// i/o errors or invalid protocol messages. // i/o errors or invalid protocol messages.
func (c *Client) botchf(code int, secode string, lastLine, format string, args ...any) error { func (c *Client) botchf(code int, secode string, firstLine string, moreLines []string, format string, args ...any) error {
c.botched = true c.botched = true
return c.errorf(false, code, secode, lastLine, format, args...) return c.errorf(false, code, secode, firstLine, moreLines, format, args...)
} }
func (c *Client) errorf(permanent bool, code int, secode, lastLine, format string, args ...any) error { func (c *Client) errorf(permanent bool, code int, secode, firstLine string, moreLines []string, format string, args ...any) error {
var cmd string var cmd string
if len(c.cmds) > 0 { if len(c.cmds) > 0 {
cmd = c.cmds[0] cmd = c.cmds[0]
} }
return Error{permanent, code, secode, cmd, lastLine, fmt.Errorf(format, args...)} return Error{permanent, code, secode, cmd, firstLine, moreLines, fmt.Errorf(format, args...)}
} }
func (c *Client) xerrorf(permanent bool, code int, secode, lastLine, format string, args ...any) { func (c *Client) xerrorf(permanent bool, code int, secode, firstLine string, moreLines []string, format string, args ...any) {
panic(c.errorf(permanent, code, secode, lastLine, format, args...)) panic(c.errorf(permanent, code, secode, firstLine, moreLines, format, args...))
} }
// timeoutWriter passes each Write on to conn after setting a write deadline on conn based on // timeoutWriter passes each Write on to conn after setting a write deadline on conn based on
@ -477,7 +480,7 @@ func (c *Client) readline() (string, error) {
c.tlsResultAddFailureDetails(-1, 1, c.tlsrptFailureDetails(resultType, reasonCode)) c.tlsResultAddFailureDetails(-1, 1, c.tlsrptFailureDetails(resultType, reasonCode))
} }
return line, c.botchf(0, "", "", "%s: %w", strings.Join(c.cmds, ","), err) return line, c.botchf(0, "", "", nil, "%s: %w", strings.Join(c.cmds, ","), err)
} }
c.firstReadAfterHandshake = false c.firstReadAfterHandshake = false
return line, nil return line, nil
@ -511,45 +514,55 @@ func (c *Client) xbwritelinef(format string, args ...any) {
func (c *Client) xbwriteline(line string) { func (c *Client) xbwriteline(line string) {
_, err := fmt.Fprintf(c.w, "%s\r\n", line) _, err := fmt.Fprintf(c.w, "%s\r\n", line)
if err != nil { if err != nil {
c.xbotchf(0, "", "", "write: %w", err) c.xbotchf(0, "", "", nil, "write: %w", err)
} }
} }
func (c *Client) xflush() { func (c *Client) xflush() {
err := c.w.Flush() err := c.w.Flush()
if err != nil { if err != nil {
c.xbotchf(0, "", "", "writes: %w", err) c.xbotchf(0, "", "", nil, "writes: %w", err)
} }
} }
// read response, possibly multiline, with supporting extended codes based on configuration in client. // read response, possibly multiline, with supporting extended codes based on configuration in client.
func (c *Client) xread() (code int, secode, lastLine string, texts []string) { func (c *Client) xread() (code int, secode, firstLine string, moreLines []string) {
var err error var err error
code, secode, lastLine, texts, err = c.read() code, secode, firstLine, moreLines, err = c.read()
if err != nil { if err != nil {
panic(err) panic(err)
} }
return return
} }
func (c *Client) read() (code int, secode, lastLine string, texts []string, rerr error) { func (c *Client) read() (code int, secode, firstLine string, moreLines []string, rerr error) {
return c.readecode(c.extEcodes) code, secode, _, firstLine, moreLines, _, rerr = c.readecode(c.extEcodes)
return
} }
// read response, possibly multiline. // read response, possibly multiline.
// if ecodes, extended codes are parsed. // if ecodes, extended codes are parsed.
func (c *Client) readecode(ecodes bool) (code int, secode, lastLine string, texts []string, rerr error) { func (c *Client) readecode(ecodes bool) (code int, secode, lastText, firstLine string, moreLines, moreTexts []string, rerr error) {
first := true
for { for {
co, sec, text, line, last, err := c.read1(ecodes) co, sec, text, line, last, err := c.read1(ecodes)
if first {
firstLine = line
first = false
} else if line != "" {
moreLines = append(moreLines, line)
if text != "" {
moreTexts = append(moreTexts, text)
}
}
if err != nil { if err != nil {
rerr = err rerr = err
return return
} }
texts = append(texts, text)
if code != 0 && co != code { if code != 0 && co != code {
// ../rfc/5321:2771 // ../rfc/5321:2771
err := c.botchf(0, "", line, "%w: multiline response with different codes, previous %d, last %d", ErrProtocol, code, co) err := c.botchf(0, "", firstLine, moreLines, "%w: multiline response with different codes, previous %d, last %d", ErrProtocol, code, co)
return 0, "", "", nil, err return 0, "", "", "", nil, nil, err
} }
code = co code = co
if last { if last {
@ -569,14 +582,14 @@ func (c *Client) readecode(ecodes bool) (code int, secode, lastLine string, text
slog.String("secode", sec), slog.String("secode", sec),
slog.Duration("duration", time.Since(c.cmdStart))) slog.Duration("duration", time.Since(c.cmdStart)))
} }
return co, sec, line, texts, nil return co, sec, text, firstLine, moreLines, moreTexts, nil
} }
} }
} }
func (c *Client) xreadecode(ecodes bool) (code int, secode, lastLine string, texts []string) { func (c *Client) xreadecode(ecodes bool) (code int, secode, lastText, firstLine string, moreLines, moreTexts []string) {
var err error var err error
code, secode, lastLine, texts, err = c.readecode(ecodes) code, secode, lastText, firstLine, moreLines, moreTexts, err = c.readecode(ecodes)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -594,12 +607,12 @@ func (c *Client) read1(ecodes bool) (code int, secode, text, line string, last b
for ; i < len(line) && line[i] >= '0' && line[i] <= '9'; i++ { for ; i < len(line) && line[i] >= '0' && line[i] <= '9'; i++ {
} }
if i != 3 { if i != 3 {
rerr = c.botchf(0, "", line, "%w: expected response code: %s", ErrProtocol, line) rerr = c.botchf(0, "", line, nil, "%w: expected response code: %s", ErrProtocol, line)
return return
} }
v, err := strconv.ParseInt(line[:i], 10, 32) v, err := strconv.ParseInt(line[:i], 10, 32)
if err != nil { if err != nil {
rerr = c.botchf(0, "", line, "%w: bad response code (%s): %s", ErrProtocol, err, line) rerr = c.botchf(0, "", line, nil, "%w: bad response code (%s): %s", ErrProtocol, err, line)
return return
} }
code = int(v) code = int(v)
@ -612,7 +625,7 @@ func (c *Client) read1(ecodes bool) (code int, secode, text, line string, last b
// Allow missing space. ../rfc/5321:2570 ../rfc/5321:2612 // Allow missing space. ../rfc/5321:2570 ../rfc/5321:2612
last = true last = true
} else { } else {
rerr = c.botchf(0, "", line, "%w: expected space or dash after response code: %s", ErrProtocol, line) rerr = c.botchf(0, "", line, nil, "%w: expected space or dash after response code: %s", ErrProtocol, line)
return return
} }
@ -684,28 +697,28 @@ func (c *Client) hello(ctx context.Context, tlsMode TLSMode, ehloHostname dns.Do
c.cmdStart = time.Now() c.cmdStart = time.Now()
// Syntax: ../rfc/5321:1827 // Syntax: ../rfc/5321:1827
c.xwritelinef("EHLO %s", ehloHostname.ASCII) c.xwritelinef("EHLO %s", ehloHostname.ASCII)
code, _, lastLine, remains := c.xreadecode(false) code, _, _, firstLine, moreLines, moreTexts := c.xreadecode(false)
switch code { switch code {
// ../rfc/5321:997 // ../rfc/5321:997
// ../rfc/5321:3098 // ../rfc/5321:3098
case smtp.C500BadSyntax, smtp.C501BadParamSyntax, smtp.C502CmdNotImpl, smtp.C503BadCmdSeq, smtp.C504ParamNotImpl: case smtp.C500BadSyntax, smtp.C501BadParamSyntax, smtp.C502CmdNotImpl, smtp.C503BadCmdSeq, smtp.C504ParamNotImpl:
if !heloOK { if !heloOK {
c.xerrorf(true, code, "", lastLine, "%w: remote claims ehlo is not supported", ErrProtocol) c.xerrorf(true, code, "", firstLine, moreLines, "%w: remote claims ehlo is not supported", ErrProtocol)
} }
// ../rfc/5321:996 // ../rfc/5321:996
c.cmds[0] = "helo" c.cmds[0] = "helo"
c.cmdStart = time.Now() c.cmdStart = time.Now()
c.xwritelinef("HELO %s", ehloHostname.ASCII) c.xwritelinef("HELO %s", ehloHostname.ASCII)
code, _, lastLine, _ = c.xreadecode(false) code, _, _, firstLine, _, _ = c.xreadecode(false)
if code != smtp.C250Completed { if code != smtp.C250Completed {
c.xerrorf(code/100 == 5, code, "", lastLine, "%w: expected 250 to HELO, got %d", ErrStatus, code) c.xerrorf(code/100 == 5, code, "", firstLine, moreLines, "%w: expected 250 to HELO, got %d", ErrStatus, code)
} }
return return
case smtp.C250Completed: case smtp.C250Completed:
default: default:
c.xerrorf(code/100 == 5, code, "", lastLine, "%w: expected 250, got %d", ErrStatus, code) c.xerrorf(code/100 == 5, code, "", firstLine, moreLines, "%w: expected 250, got %d", ErrStatus, code)
} }
for _, s := range remains[1:] { for _, s := range moreTexts {
// ../rfc/5321:1869 // ../rfc/5321:1869
s = strings.ToUpper(strings.TrimSpace(s)) s = strings.ToUpper(strings.TrimSpace(s))
switch s { switch s {
@ -739,12 +752,12 @@ func (c *Client) hello(ctx context.Context, tlsMode TLSMode, ehloHostname dns.Do
// Read greeting. // Read greeting.
c.cmds = []string{"(greeting)"} c.cmds = []string{"(greeting)"}
c.cmdStart = time.Now() c.cmdStart = time.Now()
code, _, lastLine, lines := c.xreadecode(false) code, _, _, firstLine, moreLines, _ := c.xreadecode(false)
if code != smtp.C220ServiceReady { if code != smtp.C220ServiceReady {
c.xerrorf(code/100 == 5, code, "", lastLine, "%w: expected 220, got %d", ErrStatus, code) c.xerrorf(code/100 == 5, code, "", firstLine, moreLines, "%w: expected 220, got %d", ErrStatus, code)
} }
// ../rfc/5321:2588 // ../rfc/5321:2588
c.remoteHelo, _, _ = strings.Cut(lines[0], " ") _, c.remoteHelo, _ = strings.Cut(firstLine, " ")
// Write EHLO, falling back to HELO if server doesn't appear to support it. // Write EHLO, falling back to HELO if server doesn't appear to support it.
hello(true) hello(true)
@ -755,11 +768,11 @@ func (c *Client) hello(ctx context.Context, tlsMode TLSMode, ehloHostname dns.Do
c.cmds[0] = "starttls" c.cmds[0] = "starttls"
c.cmdStart = time.Now() c.cmdStart = time.Now()
c.xwritelinef("STARTTLS") c.xwritelinef("STARTTLS")
code, secode, lastLine, _ := c.xread() code, secode, firstLine, _ := c.xread()
// ../rfc/3207:107 // ../rfc/3207:107
if code != smtp.C220ServiceReady { if code != smtp.C220ServiceReady {
c.tlsResultAddFailureDetails(0, 1, c.tlsrptFailureDetails(tlsrpt.ResultSTARTTLSNotSupported, fmt.Sprintf("smtp-starttls-reply-code-%d", code))) c.tlsResultAddFailureDetails(0, 1, c.tlsrptFailureDetails(tlsrpt.ResultSTARTTLSNotSupported, fmt.Sprintf("smtp-starttls-reply-code-%d", code)))
c.xerrorf(code/100 == 5, code, secode, lastLine, "%w: STARTTLS: got %d, expected 220", ErrTLS, code) c.xerrorf(code/100 == 5, code, secode, firstLine, moreLines, "%w: STARTTLS: got %d, expected 220", ErrTLS, code)
} }
// We don't want to do TLS on top of c.r because it also prints protocol traces: We // We don't want to do TLS on top of c.r because it also prints protocol traces: We
@ -786,7 +799,7 @@ func (c *Client) hello(ctx context.Context, tlsMode TLSMode, ehloHostname dns.Do
// multiple MX targets, we may add multiple failures, and delivery may succeed with // multiple MX targets, we may add multiple failures, and delivery may succeed with
// a later MX target with which we can do STARTTLS. ../rfc/8460:524 // a later MX target with which we can do STARTTLS. ../rfc/8460:524
c.tlsResultAdd(0, 1, err) c.tlsResultAdd(0, 1, err)
c.xerrorf(false, 0, "", "", "%w: STARTTLS TLS handshake: %s", ErrTLS, err) c.xerrorf(false, 0, "", "", nil, "%w: STARTTLS TLS handshake: %s", ErrTLS, err)
} }
c.firstReadAfterHandshake = true c.firstReadAfterHandshake = true
cancel() cancel()
@ -878,9 +891,9 @@ func (c *Client) auth(auth func(mechanisms []string, cs *tls.ConnectionState) (s
} }
a, err := auth(mechanisms, c.TLSConnectionState()) a, err := auth(mechanisms, c.TLSConnectionState())
if err != nil { if err != nil {
c.xerrorf(true, 0, "", "", "get authentication mechanism: %s, server supports %s", err, strings.Join(c.extAuthMechanisms, ", ")) c.xerrorf(true, 0, "", "", nil, "get authentication mechanism: %s, server supports %s", err, strings.Join(c.extAuthMechanisms, ", "))
} else if a == nil { } else if a == nil {
c.xerrorf(true, 0, "", "", "no matching authentication mechanisms, server supports %s", strings.Join(c.extAuthMechanisms, ", ")) c.xerrorf(true, 0, "", "", nil, "no matching authentication mechanisms, server supports %s", strings.Join(c.extAuthMechanisms, ", "))
} }
name, cleartextCreds := a.Info() name, cleartextCreds := a.Info()
@ -889,16 +902,16 @@ func (c *Client) auth(auth func(mechanisms []string, cs *tls.ConnectionState) (s
c.xwriteline("*") c.xwriteline("*")
// Server must respond with 501. // ../rfc/4954:195 // Server must respond with 501. // ../rfc/4954:195
code, secode, lastline, _ := c.xread() code, secode, firstLine, _ := c.xread()
if code != smtp.C501BadParamSyntax { if code != smtp.C501BadParamSyntax {
c.botched = true c.botched = true
} }
return code, secode, lastline return code, secode, firstLine
} }
toserver, last, err := a.Next(nil) toserver, last, err := a.Next(nil)
if err != nil { if err != nil {
c.xerrorf(false, 0, "", "", "initial step in auth mechanism %s: %w", name, err) c.xerrorf(false, 0, "", "", nil, "initial step in auth mechanism %s: %w", name, err)
} }
if cleartextCreds { if cleartextCreds {
defer c.xtrace(mlog.LevelTraceauth)() defer c.xtrace(mlog.LevelTraceauth)()
@ -915,36 +928,36 @@ func (c *Client) auth(auth func(mechanisms []string, cs *tls.ConnectionState) (s
c.xtrace(mlog.LevelTrace) // Restore. c.xtrace(mlog.LevelTrace) // Restore.
} }
code, secode, lastLine, texts := c.xreadecode(last) code, secode, lastText, firstLine, moreLines, _ := c.xreadecode(last)
if code == smtp.C235AuthSuccess { if code == smtp.C235AuthSuccess {
if !last { if !last {
c.xerrorf(false, code, secode, lastLine, "server completed authentication earlier than client expected") c.xerrorf(false, code, secode, firstLine, moreLines, "server completed authentication earlier than client expected")
} }
return nil return nil
} else if code == smtp.C334ContinueAuth { } else if code == smtp.C334ContinueAuth {
if last { if last {
c.xerrorf(false, code, secode, lastLine, "server requested unexpected continuation of authentication") c.xerrorf(false, code, secode, firstLine, moreLines, "server requested unexpected continuation of authentication")
} }
if len(texts) != 1 { if len(moreLines) > 0 {
abort() abort()
c.xerrorf(false, code, secode, lastLine, "server responded with multiline contination") c.xerrorf(false, code, secode, firstLine, moreLines, "server responded with multiline contination")
} }
fromserver, err := base64.StdEncoding.DecodeString(texts[0]) fromserver, err := base64.StdEncoding.DecodeString(lastText)
if err != nil { if err != nil {
abort() abort()
c.xerrorf(false, code, secode, lastLine, "malformed base64 data in authentication continuation response") c.xerrorf(false, code, secode, firstLine, moreLines, "malformed base64 data in authentication continuation response")
} }
toserver, last, err = a.Next(fromserver) toserver, last, err = a.Next(fromserver)
if err != nil { if err != nil {
// For failing SCRAM, the client stops due to message about invalid proof. The // For failing SCRAM, the client stops due to message about invalid proof. The
// server still sends an authentication result (it probably should send 501 // server still sends an authentication result (it probably should send 501
// instead). // instead).
xcode, xsecode, lastline := abort() xcode, xsecode, firstLine := abort()
c.xerrorf(false, xcode, xsecode, lastline, "client aborted authentication: %w", err) c.xerrorf(false, xcode, xsecode, firstLine, moreLines, "client aborted authentication: %w", err)
} }
c.xwriteline(base64.StdEncoding.EncodeToString(toserver)) c.xwriteline(base64.StdEncoding.EncodeToString(toserver))
} else { } else {
c.xerrorf(code/100 == 5, code, secode, lastLine, "unexpected response during authentication, expected 334 continue or 235 auth success") c.xerrorf(code/100 == 5, code, secode, firstLine, moreLines, "unexpected response during authentication, expected 334 continue or 235 auth success")
} }
} }
} }
@ -1022,19 +1035,19 @@ func (c *Client) Deliver(ctx context.Context, mailFrom string, rcptTo string, ms
// Temporary error, e.g. OpenBSD spamd does not announce 8bitmime support, but once // Temporary error, e.g. OpenBSD spamd does not announce 8bitmime support, but once
// you get through, the mail server behind it probably does. Just needs a few // you get through, the mail server behind it probably does. Just needs a few
// retries. // retries.
c.xerrorf(false, 0, "", "", "%w", Err8bitmimeUnsupported) c.xerrorf(false, 0, "", "", nil, "%w", Err8bitmimeUnsupported)
} }
if !c.extSMTPUTF8 && reqSMTPUTF8 { if !c.extSMTPUTF8 && reqSMTPUTF8 {
// ../rfc/6531:313 // ../rfc/6531:313
c.xerrorf(false, 0, "", "", "%w", ErrSMTPUTF8Unsupported) c.xerrorf(false, 0, "", "", nil, "%w", ErrSMTPUTF8Unsupported)
} }
if !c.extRequireTLS && requireTLS { if !c.extRequireTLS && requireTLS {
c.xerrorf(false, 0, "", "", "%w", ErrRequireTLSUnsupported) c.xerrorf(false, 0, "", "", nil, "%w", ErrRequireTLSUnsupported)
} }
// Max size enforced, only when not zero. ../rfc/1870:79 // Max size enforced, only when not zero. ../rfc/1870:79
if c.extSize && c.maxSize > 0 && msgSize > c.maxSize { if c.extSize && c.maxSize > 0 && msgSize > c.maxSize {
c.xerrorf(true, 0, "", "", "%w: message is %d bytes, remote has a %d bytes maximum size", ErrSize, msgSize, c.maxSize) c.xerrorf(true, 0, "", "", nil, "%w: message is %d bytes, remote has a %d bytes maximum size", ErrSize, msgSize, c.maxSize)
} }
var mailSize, bodyType string var mailSize, bodyType string
@ -1084,48 +1097,48 @@ func (c *Client) Deliver(ctx context.Context, mailFrom string, rcptTo string, ms
// cause a read error as it would result in an unhelpful error message and a // cause a read error as it would result in an unhelpful error message and a
// temporary instead of permanent error code. // temporary instead of permanent error code.
mfcode, mfsecode, mflastline, _ := c.xread() mfcode, mfsecode, mffirstLine, mfmoreLines := c.xread()
rtcode, rtsecode, rtlastline, _, rterr := c.read() rtcode, rtsecode, rtfirstLine, rtmoreLines, rterr := c.read()
datacode, datasecode, datalastline, _, dataerr := c.read() datacode, datasecode, datafirstLine, datamoreLines, dataerr := c.read()
if mfcode != smtp.C250Completed { if mfcode != smtp.C250Completed {
c.xerrorf(mfcode/100 == 5, mfcode, mfsecode, mflastline, "%w: got %d, expected 2xx", ErrStatus, mfcode) c.xerrorf(mfcode/100 == 5, mfcode, mfsecode, mffirstLine, mfmoreLines, "%w: got %d, expected 2xx", ErrStatus, mfcode)
} }
if rterr != nil { if rterr != nil {
panic(rterr) panic(rterr)
} }
if rtcode != smtp.C250Completed { if rtcode != smtp.C250Completed {
c.xerrorf(rtcode/100 == 5, rtcode, rtsecode, rtlastline, "%w: got %d, expected 2xx", ErrStatus, rtcode) c.xerrorf(rtcode/100 == 5, rtcode, rtsecode, rtfirstLine, rtmoreLines, "%w: got %d, expected 2xx", ErrStatus, rtcode)
} }
if dataerr != nil { if dataerr != nil {
panic(dataerr) panic(dataerr)
} }
if datacode != smtp.C354Continue { if datacode != smtp.C354Continue {
c.xerrorf(datacode/100 == 5, datacode, datasecode, datalastline, "%w: got %d, expected 354", ErrStatus, datacode) c.xerrorf(datacode/100 == 5, datacode, datasecode, datafirstLine, datamoreLines, "%w: got %d, expected 354", ErrStatus, datacode)
} }
} else { } else {
c.cmds[0] = "mailfrom" c.cmds[0] = "mailfrom"
c.cmdStart = time.Now() c.cmdStart = time.Now()
c.xwriteline(lineMailFrom) c.xwriteline(lineMailFrom)
code, secode, lastline, _ := c.xread() code, secode, firstLine, moreLines := c.xread()
if code != smtp.C250Completed { if code != smtp.C250Completed {
c.xerrorf(code/100 == 5, code, secode, lastline, "%w: got %d, expected 2xx", ErrStatus, code) c.xerrorf(code/100 == 5, code, secode, firstLine, moreLines, "%w: got %d, expected 2xx", ErrStatus, code)
} }
c.cmds[0] = "rcptto" c.cmds[0] = "rcptto"
c.cmdStart = time.Now() c.cmdStart = time.Now()
c.xwriteline(lineRcptTo) c.xwriteline(lineRcptTo)
code, secode, lastline, _ = c.xread() code, secode, firstLine, moreLines = c.xread()
if code != smtp.C250Completed { if code != smtp.C250Completed {
c.xerrorf(code/100 == 5, code, secode, lastline, "%w: got %d, expected 2xx", ErrStatus, code) c.xerrorf(code/100 == 5, code, secode, firstLine, moreLines, "%w: got %d, expected 2xx", ErrStatus, code)
} }
c.cmds[0] = "data" c.cmds[0] = "data"
c.cmdStart = time.Now() c.cmdStart = time.Now()
c.xwriteline("DATA") c.xwriteline("DATA")
code, secode, lastline, _ = c.xread() code, secode, firstLine, moreLines = c.xread()
if code != smtp.C354Continue { if code != smtp.C354Continue {
c.xerrorf(code/100 == 5, code, secode, lastline, "%w: got %d, expected 354", ErrStatus, code) c.xerrorf(code/100 == 5, code, secode, firstLine, moreLines, "%w: got %d, expected 354", ErrStatus, code)
} }
} }
@ -1134,13 +1147,13 @@ func (c *Client) Deliver(ctx context.Context, mailFrom string, rcptTo string, ms
defer c.xtrace(mlog.LevelTracedata)() defer c.xtrace(mlog.LevelTracedata)()
err := smtp.DataWrite(c.w, msg) err := smtp.DataWrite(c.w, msg)
if err != nil { if err != nil {
c.xbotchf(0, "", "", "writing message as smtp data: %w", err) c.xbotchf(0, "", "", nil, "writing message as smtp data: %w", err)
} }
c.xflush() c.xflush()
c.xtrace(mlog.LevelTrace) // Restore. c.xtrace(mlog.LevelTrace) // Restore.
code, secode, lastline, _ := c.xread() code, secode, firstLine, moreLines := c.xread()
if code != smtp.C250Completed { if code != smtp.C250Completed {
c.xerrorf(code/100 == 5, code, secode, lastline, "%w: got %d, expected 2xx", ErrStatus, code) c.xerrorf(code/100 == 5, code, secode, firstLine, moreLines, "%w: got %d, expected 2xx", ErrStatus, code)
} }
c.needRset = false c.needRset = false
@ -1162,9 +1175,9 @@ func (c *Client) Reset() (rerr error) {
c.cmds[0] = "rset" c.cmds[0] = "rset"
c.cmdStart = time.Now() c.cmdStart = time.Now()
c.xwriteline("RSET") c.xwriteline("RSET")
code, secode, lastline, _ := c.xread() code, secode, firstLine, moreLines := c.xread()
if code != smtp.C250Completed { if code != smtp.C250Completed {
c.xerrorf(code/100 == 5, code, secode, lastline, "%w: got %d, expected 2xx", ErrStatus, code) c.xerrorf(code/100 == 5, code, secode, firstLine, moreLines, "%w: got %d, expected 2xx", ErrStatus, code)
} }
c.needRset = false c.needRset = false
return return