16 "github.com/mjl-/mox/config"
17 "github.com/mjl-/mox/dns"
18 "github.com/mjl-/mox/dsn"
19 "github.com/mjl-/mox/mlog"
20 "github.com/mjl-/mox/mox-"
21 "github.com/mjl-/mox/sasl"
22 "github.com/mjl-/mox/smtp"
23 "github.com/mjl-/mox/smtpclient"
24 "github.com/mjl-/mox/store"
27// todo: reuse connection? do fewer concurrently (other than with direct delivery).
29// deliver via another SMTP server, e.g. relaying to a smart host, possibly
30// with authentication (submission).
31func deliverSubmit(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer, msgs []*Msg, backoff time.Duration, transportName string, transport *config.TransportSMTP, dialTLS bool, defaultPort int) {
32 // todo: configurable timeouts
34 // For convenience, all messages share the same relevant values.
37 port := transport.Port
42 tlsMode := smtpclient.TLSRequiredStartTLS
45 tlsMode = smtpclient.TLSImmediate
46 } else if transport.STARTTLSInsecureSkipVerify {
47 tlsMode = smtpclient.TLSRequiredStartTLS
49 } else if transport.NoSTARTTLS {
50 tlsMode = smtpclient.TLSSkip
54 // Prepare values for logging/metrics. They are updated for various error
55 // conditions later on.
57 var submiterr error // Of whole operation, nil for partial failure/success.
59 failed := len(msgs) // Reset and updated after smtp transaction.
61 r := deliveryResult(submiterr, delivered, failed)
62 d := float64(time.Since(start)) / float64(time.Second)
63 metricDelivery.WithLabelValues(fmt.Sprintf("%d", m0.Attempts), transportName, string(tlsMode), r).Observe(d)
65 qlog.Debugx("queue deliversubmit result", submiterr,
66 slog.Any("host", transport.DNSHost),
67 slog.Int("port", port),
68 slog.Int("attempt", m0.Attempts),
69 slog.String("result", r),
70 slog.Int("delivered", delivered),
71 slog.Int("failed", failed),
72 slog.Any("tlsmode", tlsMode),
73 slog.Bool("tlspkix", tlsPKIX),
74 slog.Duration("duration", time.Since(start)))
77 // todo: SMTP-DANE should be used when relaying on port 25.
80 // todo: for submission, understand SRV records, and even DANE.
84 // If submit was done with REQUIRETLS extension for SMTP, we must verify TLS
85 // certificates. If our submission connection is not configured that way, abort.
86 requireTLS := m0.RequireTLS != nil && *m0.RequireTLS
87 if requireTLS && (tlsMode != smtpclient.TLSRequiredStartTLS && tlsMode != smtpclient.TLSImmediate || !tlsPKIX) {
88 submiterr = smtpclient.Error{
90 Code: smtp.C554TransactionFailed,
91 Secode: smtp.SePol7MissingReqTLS30,
92 Err: fmt.Errorf("transport %s: message requires verified tls but transport does not verify tls", transportName),
94 fail(ctx, qlog, msgs, m0.DialedIPs, backoff, dsn.NameIP{}, submiterr)
98 dialctx, dialcancel := context.WithTimeout(ctx, 30*time.Second)
100 if msgs[0].DialedIPs == nil {
101 msgs[0].DialedIPs = map[string][]net.IP{}
104 _, _, _, ips, _, err := smtpclient.GatherIPs(dialctx, qlog.Logger, resolver, dns.IPDomain{Domain: transport.DNSHost}, m0.DialedIPs)
107 conn, _, err = smtpclient.Dial(dialctx, qlog.Logger, dialer, dns.IPDomain{Domain: transport.DNSHost}, ips, port, m0.DialedIPs, mox.Conf.Static.SpecifiedSMTPListenIPs)
109 addr := net.JoinHostPort(transport.Host, fmt.Sprintf("%d", port))
114 case errors.Is(err, os.ErrDeadlineExceeded), errors.Is(err, context.DeadlineExceeded):
116 case errors.Is(err, context.Canceled):
121 metricConnection.WithLabelValues(result).Inc()
125 qlog.Check(err, "closing connection")
127 qlog.Errorx("dialing for submission", err, slog.String("remote", addr))
128 submiterr = fmt.Errorf("transport %s: dialing %s for submission: %w", transportName, addr, err)
129 fail(ctx, qlog, msgs, m0.DialedIPs, backoff, dsn.NameIP{}, submiterr)
134 var auth func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error)
135 if transport.Auth != nil {
137 auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
138 var supportsscramsha1plus, supportsscramsha256plus bool
139 for _, mech := range a.EffectiveMechanisms {
140 if !slices.Contains(mechanisms, mech) {
142 case "SCRAM-SHA-1-PLUS":
143 supportsscramsha1plus = cs != nil
144 case "SCRAM-SHA-256-PLUS":
145 supportsscramsha256plus = cs != nil
149 if mech == "SCRAM-SHA-256-PLUS" && cs != nil {
150 return sasl.NewClientSCRAMSHA256PLUS(a.Username, a.Password, *cs), nil
151 } else if mech == "SCRAM-SHA-256" {
152 return sasl.NewClientSCRAMSHA256(a.Username, a.Password, supportsscramsha256plus), nil
153 } else if mech == "SCRAM-SHA-1-PLUS" && cs != nil {
154 return sasl.NewClientSCRAMSHA1PLUS(a.Username, a.Password, *cs), nil
155 } else if mech == "SCRAM-SHA-1" {
156 return sasl.NewClientSCRAMSHA1(a.Username, a.Password, supportsscramsha1plus), nil
157 } else if mech == "CRAM-MD5" {
158 return sasl.NewClientCRAMMD5(a.Username, a.Password), nil
159 } else if mech == "PLAIN" {
160 return sasl.NewClientPlain(a.Username, a.Password), nil
162 return nil, fmt.Errorf("internal error: unrecognized authentication mechanism %q for transport %s", mech, transportName)
165 // No mutually supported algorithm.
169 clientctx, clientcancel := context.WithTimeout(context.Background(), 60*time.Second)
171 opts := smtpclient.Opts{
173 RootCAs: mox.Conf.Static.TLS.CertPool,
175 client, err := smtpclient.New(clientctx, qlog.Logger, conn, tlsMode, tlsPKIX, mox.Conf.Static.HostnameDomain, transport.DNSHost, opts)
177 smtperr, ok := err.(smtpclient.Error)
178 var remoteMTA dsn.NameIP
179 submiterr = fmt.Errorf("transport %s: establishing smtp session with %s for submission: %w", transportName, addr, err)
181 remoteMTA.Name = transport.Host
182 smtperr.Err = submiterr
185 qlog.Errorx("establishing smtp session for submission", submiterr, slog.String("remote", addr))
186 fail(ctx, qlog, msgs, m0.DialedIPs, backoff, remoteMTA, submiterr)
190 err := client.Close()
191 qlog.Check(err, "closing smtp client after delivery")
195 var msgr io.ReadCloser
197 var req8bit, reqsmtputf8 bool
198 if len(m0.DSNUTF8) > 0 && client.SupportsSMTPUTF8() {
199 msgr = io.NopCloser(bytes.NewReader(m0.DSNUTF8))
201 size = int64(len(m0.DSNUTF8))
203 req8bit = m0.Has8bit // todo: not require this, but just try to submit?
206 p := m0.MessagePath()
209 qlog.Errorx("opening message for delivery", err, slog.String("remote", addr), slog.String("path", p))
210 submiterr = fmt.Errorf("transport %s: opening message file for submission: %w", transportName, err)
211 fail(ctx, qlog, msgs, m0.DialedIPs, backoff, dsn.NameIP{}, submiterr)
214 msgr = store.FileMsgReader(m0.MsgPrefix, f)
217 qlog.Check(err, "closing message after delivery attempt")
221 deliverctx, delivercancel := context.WithTimeout(context.Background(), time.Duration(60+size/(1024*1024))*time.Second)
222 defer delivercancel()
223 rcpts := make([]string, len(msgs))
224 for i, m := range msgs {
225 rcpts[i] = m.Recipient().String()
227 rcptErrs, submiterr := client.DeliverMultiple(deliverctx, m0.Sender().String(), rcpts, size, msgr, req8bit, reqsmtputf8, requireTLS)
228 if submiterr != nil {
229 qlog.Infox("smtp transaction for delivery failed", submiterr)
231 failed = 0 // Reset, we are looking at the SMTP results below.
233 for i, m := range msgs {
235 slog.Int64("msgid", m.ID),
236 slog.Any("recipient", m.Recipient()))
239 if err == nil && len(rcptErrs) > i {
240 if rcptErrs[i].Code != smtp.C250Completed {
241 err = smtpclient.Error(rcptErrs[i])
245 smtperr, ok := err.(smtpclient.Error)
246 err = fmt.Errorf("transport %s: submitting message to %s: %w", transportName, addr, err)
247 var remoteMTA dsn.NameIP
249 remoteMTA.Name = transport.Host
253 qmlog.Errorx("submitting message", err, slog.String("remote", addr))
254 fail(ctx, qmlog, []*Msg{m}, m0.DialedIPs, backoff, remoteMTA, err)
257 delIDs = append(delIDs, m.ID)
258 qmlog.Info("delivered from queue with transport")
263 if err := queueDelete(context.Background(), delIDs...); err != nil {
264 qlog.Errorx("deleting message from queue after delivery", err)