16 "github.com/mjl-/bstore"
18 "github.com/mjl-/mox/config"
19 "github.com/mjl-/mox/dns"
20 "github.com/mjl-/mox/dsn"
21 "github.com/mjl-/mox/mlog"
22 "github.com/mjl-/mox/mox-"
23 "github.com/mjl-/mox/sasl"
24 "github.com/mjl-/mox/smtp"
25 "github.com/mjl-/mox/smtpclient"
26 "github.com/mjl-/mox/store"
27 "github.com/mjl-/mox/webhook"
30// todo: reuse connection? do fewer concurrently (other than with direct delivery).
32// deliver via another SMTP server, e.g. relaying to a smart host, possibly
33// with authentication (submission).
34func deliverSubmit(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer, msgs []*Msg, backoff time.Duration, transportName string, transport *config.TransportSMTP, dialTLS bool, defaultPort int) {
35 // todo: configurable timeouts
37 // For convenience, all messages share the same relevant values.
40 port := transport.Port
45 tlsMode := smtpclient.TLSRequiredStartTLS
48 tlsMode = smtpclient.TLSImmediate
49 } else if transport.STARTTLSInsecureSkipVerify {
50 tlsMode = smtpclient.TLSRequiredStartTLS
52 } else if transport.NoSTARTTLS {
53 tlsMode = smtpclient.TLSSkip
57 // Prepare values for logging/metrics. They are updated for various error
58 // conditions later on.
60 var submiterr error // Of whole operation, nil for partial failure/success.
62 failed := len(msgs) // Reset and updated after smtp transaction.
64 r := deliveryResult(submiterr, delivered, failed)
65 d := float64(time.Since(start)) / float64(time.Second)
66 metricDelivery.WithLabelValues(fmt.Sprintf("%d", m0.Attempts), transportName, string(tlsMode), r).Observe(d)
68 qlog.Debugx("queue deliversubmit result", submiterr,
69 slog.Any("host", transport.DNSHost),
70 slog.Int("port", port),
71 slog.Int("attempt", m0.Attempts),
72 slog.String("result", r),
73 slog.Int("delivered", delivered),
74 slog.Int("failed", failed),
75 slog.Any("tlsmode", tlsMode),
76 slog.Bool("tlspkix", tlsPKIX),
77 slog.Duration("duration", time.Since(start)))
80 // todo: SMTP-DANE should be used when relaying on port 25.
83 // todo: for submission, understand SRV records, and even DANE.
87 // If submit was done with REQUIRETLS extension for SMTP, we must verify TLS
88 // certificates. If our submission connection is not configured that way, abort.
89 requireTLS := m0.RequireTLS != nil && *m0.RequireTLS
90 if requireTLS && (tlsMode != smtpclient.TLSRequiredStartTLS && tlsMode != smtpclient.TLSImmediate || !tlsPKIX) {
91 submiterr = smtpclient.Error{
93 Code: smtp.C554TransactionFailed,
94 Secode: smtp.SePol7MissingReqTLS30,
95 Err: fmt.Errorf("transport %s: message requires verified tls but transport does not verify tls", transportName),
97 failMsgsDB(qlog, msgs, m0.DialedIPs, backoff, dsn.NameIP{}, submiterr)
101 dialctx, dialcancel := context.WithTimeout(ctx, 30*time.Second)
103 if msgs[0].DialedIPs == nil {
104 msgs[0].DialedIPs = map[string][]net.IP{}
107 _, _, _, ips, _, err := smtpclient.GatherIPs(dialctx, qlog.Logger, resolver, "ip", dns.IPDomain{Domain: transport.DNSHost}, m0.DialedIPs)
110 conn, _, err = smtpclient.Dial(dialctx, qlog.Logger, dialer, dns.IPDomain{Domain: transport.DNSHost}, ips, port, m0.DialedIPs, mox.Conf.Static.SpecifiedSMTPListenIPs)
112 addr := net.JoinHostPort(transport.Host, fmt.Sprintf("%d", port))
117 case errors.Is(err, os.ErrDeadlineExceeded), errors.Is(err, context.DeadlineExceeded):
119 case errors.Is(err, context.Canceled):
124 metricConnection.WithLabelValues(result).Inc()
128 qlog.Check(err, "closing connection")
130 qlog.Errorx("dialing for submission", err, slog.String("remote", addr))
131 submiterr = fmt.Errorf("transport %s: dialing %s for submission: %w", transportName, addr, err)
132 failMsgsDB(qlog, msgs, m0.DialedIPs, backoff, dsn.NameIP{}, submiterr)
137 var auth func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error)
138 if transport.Auth != nil {
140 auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
141 var supportsscramsha1plus, supportsscramsha256plus bool
142 for _, mech := range a.EffectiveMechanisms {
143 if !slices.Contains(mechanisms, mech) {
145 case "SCRAM-SHA-1-PLUS":
146 supportsscramsha1plus = cs != nil
147 case "SCRAM-SHA-256-PLUS":
148 supportsscramsha256plus = cs != nil
152 if mech == "SCRAM-SHA-256-PLUS" && cs != nil {
153 return sasl.NewClientSCRAMSHA256PLUS(a.Username, a.Password, *cs), nil
154 } else if mech == "SCRAM-SHA-256" {
155 return sasl.NewClientSCRAMSHA256(a.Username, a.Password, supportsscramsha256plus), nil
156 } else if mech == "SCRAM-SHA-1-PLUS" && cs != nil {
157 return sasl.NewClientSCRAMSHA1PLUS(a.Username, a.Password, *cs), nil
158 } else if mech == "SCRAM-SHA-1" {
159 return sasl.NewClientSCRAMSHA1(a.Username, a.Password, supportsscramsha1plus), nil
160 } else if mech == "CRAM-MD5" {
161 return sasl.NewClientCRAMMD5(a.Username, a.Password), nil
162 } else if mech == "PLAIN" {
163 return sasl.NewClientPlain(a.Username, a.Password), nil
165 return nil, fmt.Errorf("internal error: unrecognized authentication mechanism %q for transport %s", mech, transportName)
168 // No mutually supported algorithm.
172 clientctx, clientcancel := context.WithTimeout(context.Background(), 60*time.Second)
174 opts := smtpclient.Opts{
176 RootCAs: mox.Conf.Static.TLS.CertPool,
178 client, err := smtpclient.New(clientctx, qlog.Logger, conn, tlsMode, tlsPKIX, mox.Conf.Static.HostnameDomain, transport.DNSHost, opts)
180 smtperr, ok := err.(smtpclient.Error)
181 var remoteMTA dsn.NameIP
182 submiterr = fmt.Errorf("transport %s: establishing smtp session with %s for submission: %w", transportName, addr, err)
184 remoteMTA.Name = transport.Host
185 smtperr.Err = submiterr
188 qlog.Errorx("establishing smtp session for submission", submiterr, slog.String("remote", addr))
189 failMsgsDB(qlog, msgs, m0.DialedIPs, backoff, remoteMTA, submiterr)
193 err := client.Close()
194 qlog.Check(err, "closing smtp client after delivery")
198 var msgr io.ReadCloser
200 var req8bit, reqsmtputf8 bool
201 if len(m0.DSNUTF8) > 0 && client.SupportsSMTPUTF8() {
202 msgr = io.NopCloser(bytes.NewReader(m0.DSNUTF8))
204 size = int64(len(m0.DSNUTF8))
206 req8bit = m0.Has8bit // todo: not require this, but just try to submit?
209 p := m0.MessagePath()
212 qlog.Errorx("opening message for delivery", err, slog.String("remote", addr), slog.String("path", p))
213 submiterr = fmt.Errorf("transport %s: opening message file for submission: %w", transportName, err)
214 failMsgsDB(qlog, msgs, m0.DialedIPs, backoff, dsn.NameIP{}, submiterr)
217 msgr = store.FileMsgReader(m0.MsgPrefix, f)
220 qlog.Check(err, "closing message after delivery attempt")
224 deliverctx, delivercancel := context.WithTimeout(context.Background(), time.Duration(60+size/(1024*1024))*time.Second)
225 defer delivercancel()
226 rcpts := make([]string, len(msgs))
227 for i, m := range msgs {
228 rcpts[i] = m.Recipient().String()
230 rcptErrs, submiterr := client.DeliverMultiple(deliverctx, m0.Sender().String(), rcpts, size, msgr, req8bit, reqsmtputf8, requireTLS)
231 if submiterr != nil {
232 qlog.Infox("smtp transaction for delivery failed", submiterr)
234 failed, delivered = processDeliveries(qlog, m0, msgs, addr, transport.Host, backoff, rcptErrs, submiterr)
237// Process failures and successful deliveries, retiring/removing messages from
238// queue, queueing webhooks.
240// Also used by deliverLocalserve.
241func processDeliveries(qlog mlog.Log, m0 *Msg, msgs []*Msg, remoteAddr string, remoteHost string, backoff time.Duration, rcptErrs []smtpclient.Response, submiterr error) (failed, delivered int) {
243 for i, m := range msgs {
245 slog.Int64("msgid", m.ID),
246 slog.Any("recipient", m.Recipient()))
249 if err == nil && len(rcptErrs) > i {
250 if rcptErrs[i].Code != smtp.C250Completed {
251 err = smtpclient.Error(rcptErrs[i])
255 smtperr, ok := err.(smtpclient.Error)
256 err = fmt.Errorf("delivering message to %s: %w", remoteAddr, err)
257 var remoteMTA dsn.NameIP
259 remoteMTA.Name = remoteHost
263 qmlog.Errorx("submitting message", err, slog.String("remote", remoteAddr))
264 failMsgsDB(qmlog, []*Msg{m}, m0.DialedIPs, backoff, remoteMTA, err)
267 m.markResult(0, "", "", true)
268 delMsgs = append(delMsgs, *m)
269 qmlog.Info("delivered from queue with transport")
273 if len(delMsgs) > 0 {
274 err := DB.Write(context.Background(), func(tx *bstore.Tx) error {
275 return retireMsgs(qlog, tx, webhook.EventDelivered, 0, "", nil, delMsgs...)
278 qlog.Errorx("remove queue message from database after delivery", err)
279 } else if err := removeMsgsFS(qlog, delMsgs...); err != nil {
280 qlog.Errorx("remove queue message from file system after delivery", err)