14 "github.com/prometheus/client_golang/prometheus"
15 "github.com/prometheus/client_golang/prometheus/promauto"
17 "github.com/mjl-/adns"
18 "github.com/mjl-/bstore"
20 "github.com/mjl-/mox/dns"
21 "github.com/mjl-/mox/dsn"
22 "github.com/mjl-/mox/mlog"
23 "github.com/mjl-/mox/mox-"
24 "github.com/mjl-/mox/mtasts"
25 "github.com/mjl-/mox/mtastsdb"
26 "github.com/mjl-/mox/smtp"
27 "github.com/mjl-/mox/smtpclient"
28 "github.com/mjl-/mox/store"
29 "github.com/mjl-/mox/tlsrpt"
33 metricDestinations = promauto.NewCounter(
34 prometheus.CounterOpts{
35 Name: "mox_queue_destinations_total",
36 Help: "Total destination (e.g. MX) lookups for delivery attempts, including those in mox_smtpclient_destinations_authentic_total.",
39 metricDestinationsAuthentic = promauto.NewCounter(
40 prometheus.CounterOpts{
41 Name: "mox_queue_destinations_authentic_total",
42 Help: "Destination (e.g. MX) lookups for delivery attempts authenticated with DNSSEC so they are candidates for DANE verification.",
45 metricDestinationDANERequired = promauto.NewCounter(
46 prometheus.CounterOpts{
47 Name: "mox_queue_destination_dane_required_total",
48 Help: "Total number of connections to hosts with valid TLSA records making DANE required.",
51 metricDestinationDANESTARTTLSUnverified = promauto.NewCounter(
52 prometheus.CounterOpts{
53 Name: "mox_queue_destination_dane_starttlsunverified_total",
54 Help: "Total number of connections with required DANE where all TLSA records were unusable.",
57 metricDestinationDANEGatherTLSAErrors = promauto.NewCounter(
58 prometheus.CounterOpts{
59 Name: "mox_queue_destination_dane_gathertlsa_errors_total",
60 Help: "Total number of connections where looking up TLSA records resulted in an error.",
63 // todo: recognize when "tls-required-no" message header caused a non-verifying certificate to be overridden. requires doing our own certificate validation after having set tls.Config.InsecureSkipVerify due to tls-required-no.
64 metricTLSRequiredNoIgnored = promauto.NewCounterVec(
65 prometheus.CounterOpts{
66 Name: "mox_queue_tlsrequiredno_ignored_total",
67 Help: "Delivery attempts with TLS policy findings ignored due to message with TLS-Required: No header. Does not cover case where TLS certificate cannot be PKIX-verified.",
70 "ignored", // mtastspolicy (error getting policy), mtastsmx (mx host not allowed in policy), badtls (error negotiating tls), badtlsa (error fetching dane tlsa records)
73 metricRequireTLSUnsupported = promauto.NewCounterVec(
74 prometheus.CounterOpts{
75 Name: "mox_queue_requiretls_unsupported_total",
76 Help: "Delivery attempts that failed due to message with REQUIRETLS.",
79 "reason", // nopolicy (no mta-sts and no dane), norequiretls (smtp server does not support requiretls)
82 metricPlaintextFallback = promauto.NewCounter(
83 prometheus.CounterOpts{
84 Name: "mox_queue_plaintext_fallback_total",
85 Help: "Delivery attempts with fallback to plain text delivery.",
90// todo: rename function, perhaps put some of the params in a delivery struct so we don't pass all the params all the time?
91func fail(qlog *mlog.Log, m Msg, backoff time.Duration, permanent bool, remoteMTA dsn.NameIP, secodeOpt, errmsg string) {
92 // 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
93 // 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?
94 // todo future: when we implement smtp dsn extension, parameter RET=FULL must be disregarded for messages with REQUIRETLS.
../rfc/8689:379
96 if permanent || m.MaxAttempts == 0 && m.Attempts >= 8 || m.MaxAttempts > 0 && m.Attempts >= m.MaxAttempts {
97 qlog.Errorx("permanent failure delivering from queue", errors.New(errmsg))
98 deliverDSNFailure(qlog, m, remoteMTA, secodeOpt, errmsg)
100 if err := queueDelete(context.Background(), m.ID); err != nil {
101 qlog.Errorx("deleting message from queue after permanent failure", err)
106 qup := bstore.QueryDB[Msg](context.Background(), DB)
108 if _, err := qup.UpdateNonzero(Msg{LastError: errmsg, DialedIPs: m.DialedIPs}); err != nil {
109 qlog.Errorx("storing delivery error", err, mlog.Field("deliveryerror", errmsg))
113 // We've attempted deliveries at these intervals: 0, 7.5m, 15m, 30m, 1h, 2u.
114 // Let sender know delivery is delayed.
115 qlog.Errorx("temporary failure delivering from queue, sending delayed dsn", errors.New(errmsg), mlog.Field("backoff", backoff))
117 retryUntil := m.LastAttempt.Add((4 + 8 + 16) * time.Hour)
118 deliverDSNDelay(qlog, m, remoteMTA, secodeOpt, errmsg, retryUntil)
120 qlog.Errorx("temporary failure delivering from queue", errors.New(errmsg), mlog.Field("backoff", backoff), mlog.Field("nextattempt", m.NextAttempt))
124// Delivery by directly dialing (MX) hosts for destination domain of message.
126// The returned results are for use in a TLSRPT report, it holds success/failure
127// counts and failure details for delivery/connection attempts. The
128// recipientDomainResult is for policies/counts/failures about the whole recipient
129// domain (MTA-STS), its policy type can be empty, in which case there is no
130// information (e.g. internal failure). hostResults are per-host details (DANE, one
132func deliverDirect(cid int64, qlog *mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer, ourHostname dns.Domain, transportName string, m Msg, backoff time.Duration) (recipientDomainResult tlsrpt.Result, hostResults []tlsrpt.Result) {
133 // High-level approach:
134 // - Resolve domain to deliver to (CNAME), and determine hosts to try to deliver to (MX)
135 // - Get MTA-STS policy for domain (optional). If present, only deliver to its
136 // allowlisted hosts and verify TLS against CA pool.
137 // - For each host, attempt delivery. If the attempt results in a permanent failure
138 // (as claimed by remote with a 5xx SMTP response, or perhaps decided by us), the
139 // attempt can be aborted. Other errors are often temporary and may result in later
140 // successful delivery. But hopefully the delivery just succeeds. For each host:
141 // - If there is an MTA-STS policy, we only connect to allow-listed hosts.
142 // - We try to lookup DANE records (optional) and verify them if present.
143 // - If RequireTLS is true, we only deliver if the remote SMTP server implements it.
144 // - If RequireTLS is false, we'll fall back to regular delivery attempts without
145 // TLS verification and possibly without TLS at all, ignoring recipient domain/host
146 // MTA-STS and DANE policies.
148 // Resolve domain and hosts to attempt delivery to.
149 // These next-hop names are often the name under which we find MX records. The
150 // expanded name is different from the original if the original was a CNAME,
151 // possibly a chain. If there are no MX records, it can be an IP or the host
153 origNextHop := m.RecipientDomain.Domain
154 ctx := context.WithValue(mox.Context, mlog.CidKey, cid)
155 haveMX, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hosts, permanent, err := smtpclient.GatherDestinations(ctx, qlog, resolver, m.RecipientDomain)
157 // If this is a DNSSEC authentication error, we'll collect it for TLS reporting.
158 // Hopefully it's a temporary misconfiguration that is solve before we try to send
159 // our report. We don't report as "dnssec-invalid", because that is defined as
161 var errCode adns.ErrorCode
162 if errors.As(err, &errCode) && errCode.IsAuthentication() {
164 reasonCode := fmt.Sprintf("dns-extended-error-%d-%s", errCode, strings.ReplaceAll(errCode.String(), " ", "-"))
165 fd := tlsrpt.Details(tlsrpt.ResultValidationFailure, reasonCode)
166 recipientDomainResult = tlsrpt.MakeResult(tlsrpt.NoPolicyFound, origNextHop, fd)
167 recipientDomainResult.Summary.TotalFailureSessionCount++
170 fail(qlog, m, backoff, permanent, dsn.NameIP{}, "", err.Error())
174 tlsRequiredNo := m.RequireTLS != nil && !*m.RequireTLS
176 // Check for MTA-STS policy and enforce it if needed.
177 // We must check at the original next-hop, i.e. recipient domain, not following any
178 // CNAMEs. If we were to follow CNAMEs and ask for MTA-STS at that domain, it
179 // would only take a single CNAME DNS response to direct us to an unrelated domain.
180 var policy *mtasts.Policy // Policy can have mode enforce, testing and none.
181 if !origNextHop.IsZero() {
182 cidctx := context.WithValue(mox.Shutdown, mlog.CidKey, cid)
183 policy, recipientDomainResult, _, err = mtastsdb.Get(cidctx, resolver, origNextHop)
186 qlog.Infox("mtasts lookup temporary error, continuing due to tls-required-no message header", err, mlog.Field("domain", origNextHop))
187 metricTLSRequiredNoIgnored.WithLabelValues("mtastspolicy").Inc()
189 qlog.Infox("mtasts lookup temporary error, aborting delivery attempt", err, mlog.Field("domain", origNextHop))
190 recipientDomainResult.Summary.TotalFailureSessionCount++
191 fail(qlog, m, backoff, false, dsn.NameIP{}, "", err.Error())
195 // note: policy can be nil, if a domain does not implement MTA-STS or it's the
196 // first time we fetch the policy and if we encountered an error.
199 // We try delivery to each host until we have success or a permanent failure. So
200 // for transient errors, we'll try the next host. For MX records pointing to a
201 // dual stack host, we turn a permanent failure due to policy on the first delivery
202 // attempt into a temporary failure and make sure to try the other address family
203 // the next attempt. This should reduce issues due to one of our IPs being on a
204 // block list. We won't try multiple IPs of the same address family. Surprisingly,
205 // RFC 5321 does not specify a clear algorithm, but common practice is probably
207 var remoteMTA dsn.NameIP
208 var secodeOpt, errmsg string
210 nmissingRequireTLS := 0
211 // 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
212 for _, h := range hosts {
214 if policy != nil && policy.Mode != mtasts.ModeNone && !policy.Matches(h.Domain) {
215 // todo: perhaps only send tlsrpt failure if none of the mx hosts matched? reporting about each mismatch seems useful for domain owners, to discover mtasts policies they didn't update after changing mx. there is a risk a domain owner intentionally didn't put all mx'es in the mtasts policy, but they probably won't mind being reported about that.
216 // Other error: Surprising that TLSRPT doesn't have an MTA-STS specific error code
219 fd := tlsrpt.Details(tlsrpt.ResultValidationFailure, "mtasts-policy-mx-mismatch")
220 fd.ReceivingMXHostname = h.Domain.ASCII
221 recipientDomainResult.Add(0, 0, fd)
223 var policyHosts []string
224 for _, mx := range policy.MX {
225 policyHosts = append(policyHosts, mx.LogString())
227 if policy.Mode == mtasts.ModeEnforce {
229 qlog.Info("mx host does not match mta-sts policy in mode enforce, ignoring due to tls-required-no message header", mlog.Field("host", h.Domain), mlog.Field("policyhosts", policyHosts))
230 metricTLSRequiredNoIgnored.WithLabelValues("mtastsmx").Inc()
232 errmsg = fmt.Sprintf("mx host %s does not match enforced mta-sts policy with hosts %s", h.Domain, strings.Join(policyHosts, ","))
233 qlog.Error("mx host does not match mta-sts policy in mode enforce, skipping", mlog.Field("host", h.Domain), mlog.Field("policyhosts", policyHosts))
234 recipientDomainResult.Summary.TotalFailureSessionCount++
238 qlog.Error("mx host does not match mta-sts policy, but it is not enforced, continuing", mlog.Field("host", h.Domain), mlog.Field("policyhosts", policyHosts))
242 qlog.Info("delivering to remote", mlog.Field("remote", h), mlog.Field("queuecid", cid))
244 nqlog := qlog.WithCid(cid)
247 enforceMTASTS := policy != nil && policy.Mode == mtasts.ModeEnforce
248 tlsMode := smtpclient.TLSOpportunistic
251 tlsMode = smtpclient.TLSRequiredStartTLS
253 // note: smtpclient will still go through PKIX verification, and report about it, but not fail the connection if not passing.
256 // Try to deliver to host. We can get various errors back. Like permanent failure
257 // response codes, TCP, DNSSEC, TLS (opportunistic, i.e. optional with fallback to
258 // without), etc. It's a balancing act to handle these situations correctly. We
259 // don't want to bounce unnecessarily. But also not keep trying if there is no
260 // chance of success.
262 // deliverHost will report generic TLS and MTA-STS-specific failures in
263 // recipientDomainResult. If DANE is encountered, it will add a DANE reporting
264 // result for generic TLS and DANE-specific errors.
266 // Set if TLSA records were found. Means TLS is required for this host, usually
267 // with verification of the certificate, and that we cannot fall back to
268 // opportunistic TLS.
272 var hostResult tlsrpt.Result
273 permanent, tlsDANE, badTLS, secodeOpt, remoteIP, errmsg, hostResult, ok = deliverHost(nqlog, resolver, dialer, cid, ourHostname, transportName, h, enforceMTASTS, haveMX, origNextHopAuthentic, origNextHop, expandedNextHopAuthentic, expandedNextHop, &m, tlsMode, tlsPKIX, &recipientDomainResult)
275 var zerotype tlsrpt.PolicyType
276 if hostResult.Policy.Type != zerotype {
277 hostResults = append(hostResults, hostResult)
280 // If we had a TLS-related failure when doing TLS, and we don't have a requirement
281 // for MTA-STS/DANE, we try again without TLS. This could be an old server that
282 // only does ancient TLS versions, or has a misconfiguration. Note that
283 // opportunistic TLS does not do regular certificate verification, so that can't be
287 // We queue outgoing TLS reports with tlsRequiredNo, so reports can be delivered in
288 // case of broken TLS.
289 if !ok && badTLS && (!enforceMTASTS && tlsMode == smtpclient.TLSOpportunistic && !tlsDANE && !m.IsDMARCReport || tlsRequiredNo) {
290 metricPlaintextFallback.Inc()
292 metricTLSRequiredNoIgnored.WithLabelValues("badtls").Inc()
295 // todo future: add a configuration option to not fall back?
296 nqlog.Info("connecting again for delivery attempt without tls", mlog.Field("enforcemtasts", enforceMTASTS), mlog.Field("tlsdane", tlsDANE), mlog.Field("requiretls", m.RequireTLS))
297 permanent, _, _, secodeOpt, remoteIP, errmsg, _, ok = deliverHost(nqlog, resolver, dialer, cid, ourHostname, transportName, h, enforceMTASTS, haveMX, origNextHopAuthentic, origNextHop, expandedNextHopAuthentic, expandedNextHop, &m, smtpclient.TLSSkip, false, &tlsrpt.Result{})
301 nqlog.Info("delivered from queue")
302 if err := queueDelete(context.Background(), m.ID); err != nil {
303 nqlog.Errorx("deleting message from queue after delivery", err)
307 remoteMTA = dsn.NameIP{Name: h.XString(false), IP: remoteIP}
311 if secodeOpt == smtp.SePol7MissingReqTLS {
316 // In theory, we could make a failure permanent if we didn't find any mx host
317 // matching the mta-sts policy AND the policy is fresh AND all DNS records leading
318 // to the MX targets (including CNAME) have a TTL that is beyond the latest
319 // possible delivery attempt. Until that time, configuration problems can be
320 // corrected through DNS or policy update. Not sure if worth it in practice, there
321 // is a good chance the MX records can still change, at least on initial delivery
323 // todo: possibly detect that future deliveries will fail due to long ttl's of cached records that are preventing delivery.
325 // If we failed due to requiretls not being satisfied, make the delivery permanent.
326 // It is unlikely the recipient domain will implement requiretls during our retry
327 // period. Best to let the sender know immediately.
328 if !permanent && nmissingRequireTLS > 0 && nmissingRequireTLS == len(hosts) {
329 qlog.Info("marking delivery as permanently failed because recipient domain does not implement requiretls")
333 fail(qlog, m, backoff, permanent, remoteMTA, secodeOpt, errmsg)
337// deliverHost attempts to deliver m to host. Depending on tlsMode we'll do
338// opportunistic or required STARTTLS or skip TLS entirely. Based on tlsPKIX we do
339// PKIX/WebPKI verification (for MTA-STS). If we encounter DANE records, we verify
340// those. If the message has a message header "TLS-Required: No", we ignore TLS
341// verification errors.
343// deliverHost updates m.DialedIPs, which must be saved in case of failure to
346// The haveMX and next-hop-authentic fields are used to determine if DANE is
347// applicable. The next-hop fields themselves are used to determine valid names
348// during DANE TLS certificate verification.
350// The returned hostResult holds TLSRPT reporting results for the connection
351// attempt. Its policy type can be the zero value, indicating there was no finding
352// (e.g. internal error).
353func deliverHost(log *mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer, cid int64, 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) {
356 tlsRequiredNo := m.RequireTLS != nil && !*m.RequireTLS
359 var deliveryResult string
361 mode := string(tlsMode)
368 metricDelivery.WithLabelValues(fmt.Sprintf("%d", m.Attempts), transportName, mode, deliveryResult).Observe(float64(time.Since(start)) / float64(time.Second))
369 log.Debug("queue deliverhost result",
370 mlog.Field("host", host),
371 mlog.Field("attempt", m.Attempts),
372 mlog.Field("tlsmode", tlsMode),
373 mlog.Field("tlspkix", tlsPKIX),
374 mlog.Field("tlsdane", tlsDANE),
375 mlog.Field("tlsrequiredno", tlsRequiredNo),
376 mlog.Field("permanent", permanent),
377 mlog.Field("badtls", badTLS),
378 mlog.Field("secodeopt", secodeOpt),
379 mlog.Field("errmsg", errmsg),
380 mlog.Field("ok", ok),
381 mlog.Field("duration", time.Since(start)))
384 // Open message to deliver.
385 f, err := os.Open(m.MessagePath())
387 return false, false, false, "", nil, fmt.Sprintf("open message file: %s", err), hostResult, false
389 msgr := store.FileMsgReader(m.MsgPrefix, f)
392 log.Check(err, "closing message after delivery attempt")
395 cidctx := context.WithValue(mox.Context, mlog.CidKey, cid)
396 ctx, cancel := context.WithTimeout(cidctx, 30*time.Second)
399 // We must lookup the IPs for the host name before checking DANE TLSA records. And
400 // only check TLSA records for secure responses. This prevents problems with old
401 // name servers returning an error for TLSA requests or letting it timeout (not
403 var daneRecords []adns.TLSA
404 var tlsHostnames []dns.Domain
406 tlsHostnames = []dns.Domain{host.Domain}
408 if m.DialedIPs == nil {
409 m.DialedIPs = map[string][]net.IP{}
412 countResultFailure := func() {
413 recipientDomainResult.Summary.TotalFailureSessionCount++
414 hostResult.Summary.TotalFailureSessionCount++
417 metricDestinations.Inc()
418 authentic, expandedAuthentic, expandedHost, ips, dualstack, err := smtpclient.GatherIPs(ctx, log, resolver, host, m.DialedIPs)
419 destAuthentic := err == nil && authentic && origNextHopAuthentic && (!haveMX || expandedNextHopAuthentic) && host.IsDomain()
421 log.Debugx("not attempting verification with dane", err, mlog.Field("authentic", authentic), mlog.Field("expandedauthentic", expandedAuthentic))
423 // Track a DNSSEC error if found.
424 var errCode adns.ErrorCode
426 if errors.As(err, &errCode) && errCode.IsAuthentication() {
428 reasonCode := fmt.Sprintf("dns-extended-error-%d-%s", errCode, strings.ReplaceAll(errCode.String(), " ", "-"))
429 fd := tlsrpt.Details(tlsrpt.ResultValidationFailure, reasonCode)
430 hostResult = tlsrpt.MakeResult(tlsrpt.TLSA, host.Domain, fd)
434 // todo: we could lookup tlsa records, and log an error when they are not dnssec-signed. this should be interpreted simply as "not doing dane", but it could be useful to warn domain owners about, they may be under the impression they are dane-protected.
435 hostResult = tlsrpt.MakeResult(tlsrpt.NoPolicyFound, host.Domain)
437 } else if tlsMode == smtpclient.TLSSkip {
438 metricDestinationsAuthentic.Inc()
440 // TLSSkip is used to fallback to plaintext, which is used with a TLS-Required: No
441 // header to ignore the recipient domain's DANE policy.
443 // possible err is propagated to below.
445 metricDestinationsAuthentic.Inc()
447 // Look for TLSA records in either the expandedHost, or otherwise the original
449 var tlsaBaseDomain dns.Domain
450 tlsDANE, daneRecords, tlsaBaseDomain, err = smtpclient.GatherTLSA(ctx, log, resolver, host.Domain, expandedNextHopAuthentic && expandedAuthentic, expandedHost)
452 metricDestinationDANERequired.Inc()
455 metricDestinationDANEGatherTLSAErrors.Inc()
457 if err == nil && tlsDANE {
458 tlsMode = smtpclient.TLSRequiredStartTLS
459 hostResult = tlsrpt.Result{Policy: tlsrpt.TLSAPolicy(daneRecords, tlsaBaseDomain)}
460 if len(daneRecords) == 0 {
461 // If there are no usable DANE records, we still have to use TLS, but without
462 // verifying its certificate. At least when there is no MTA-STS. Why? Perhaps to
463 // prevent ossification? The SMTP TLSA specification has different behaviour than
464 // the generic TLSA. "Usable" means different things in different places.
466 log.Debug("no usable dane records, requiring starttls but not verifying with dane")
467 metricDestinationDANESTARTTLSUnverified.Inc()
470 hostResult.FailureDetails = []tlsrpt.FailureDetails{
472 ResultType: tlsrpt.ResultTLSAInvalid,
473 ReceivingMXHostname: host.XString(false),
474 FailureReasonCode: "all-unusable-records+ignored",
478 log.Debug("delivery with required starttls with dane verification", mlog.Field("allowedtlshostnames", tlsHostnames))
480 // Based on CNAMEs followed and DNSSEC-secure status, we must allow up to 4 host
482 tlsHostnames = smtpclient.GatherTLSANames(haveMX, expandedNextHopAuthentic, expandedAuthentic, origNextHop, expandedNextHop, host.Domain, tlsaBaseDomain)
484 log.Debugx("not doing opportunistic dane after gathering tlsa records", err)
486 hostResult = tlsrpt.MakeResult(tlsrpt.NoPolicyFound, tlsaBaseDomain)
487 } else if err != nil {
488 fd := tlsrpt.Details(tlsrpt.ResultTLSAInvalid, "")
489 var errCode adns.ErrorCode
490 if errors.As(err, &errCode) {
491 fd.FailureReasonCode = fmt.Sprintf("extended-dns-error-%d-%s", errCode, strings.ReplaceAll(errCode.String(), " ", "-"))
492 if errCode.IsAuthentication() {
494 fd.ResultType = tlsrpt.ResultDNSSECInvalid
498 hostResult = tlsrpt.Result{
499 Policy: tlsrpt.TLSAPolicy(daneRecords, tlsaBaseDomain),
500 FailureDetails: []tlsrpt.FailureDetails{fd},
504 log.Debugx("error gathering dane tlsa records with dane required, but continuing without validation due to tls-required-no message header", err)
506 metricTLSRequiredNoIgnored.WithLabelValues("badtlsa").Inc()
509 // else, err is propagated below.
512 // todo: for requiretls, should an MTA-STS policy in mode testing be treated as good enough for requiretls? let's be strict and assume not.
513 // todo:
../rfc/8689:276 seems to specify stricter requirements on name in certificate than DANE (which allows original recipient domain name and cname-expanded name, and hints at following CNAME for MX targets as well, allowing both their original and expanded names too). perhaps the intent was just to say the name must be validated according to the relevant specifications?
514 // todo: for requiretls, should we allow no usable dane records with requiretls? dane allows it, but doesn't seem in spirit of requiretls, so not allowing it.
515 if err == nil && m.RequireTLS != nil && *m.RequireTLS && !(tlsDANE && len(daneRecords) > 0) && !enforceMTASTS {
516 log.Info("verified tls is required, but destination has no usable dane records and no mta-sts policy, canceling delivery attempt to host")
517 metricRequireTLSUnsupported.WithLabelValues("nopolicy").Inc()
519 return false, tlsDANE, false, smtp.SePol7MissingReqTLS, remoteIP, "missing required tls verification mechanism", hostResult, false
522 // Dial the remote host given the IPs if no error yet.
525 if m.DialedIPs == nil {
526 m.DialedIPs = map[string][]net.IP{}
528 conn, remoteIP, err = smtpclient.Dial(ctx, log, dialer, host, ips, 25, m.DialedIPs)
532 // Set error for metrics.
537 case errors.Is(err, os.ErrDeadlineExceeded), errors.Is(err, context.DeadlineExceeded):
539 case errors.Is(err, context.Canceled):
544 metricConnection.WithLabelValues(result).Inc()
546 log.Debugx("connecting to remote smtp", err, mlog.Field("host", host))
547 return false, tlsDANE, false, "", remoteIP, fmt.Sprintf("dialing smtp server: %v", err), hostResult, false
551 if m.SenderLocalpart != "" || !m.SenderDomain.IsZero() {
552 mailFrom = m.Sender().XString(m.SMTPUTF8)
554 rcptTo := m.Recipient().XString(m.SMTPUTF8)
557 log = log.Fields(mlog.Field("remoteip", remoteIP))
558 ctx, cancel = context.WithTimeout(cidctx, 30*time.Minute)
560 mox.Connections.Register(conn, "smtpclient", "queue")
562 // Initialize SMTP session, sending EHLO/HELO and STARTTLS with specified tls mode.
563 var firstHost dns.Domain
564 var moreHosts []dns.Domain
565 if len(tlsHostnames) > 0 {
566 // For use with DANE-TA.
567 firstHost = tlsHostnames[0]
568 moreHosts = tlsHostnames[1:]
570 var verifiedRecord adns.TLSA
571 opts := smtpclient.Opts{
572 IgnoreTLSVerifyErrors: tlsRequiredNo,
573 RootCAs: mox.Conf.Static.TLS.CertPool,
574 DANERecords: daneRecords,
575 DANEMoreHostnames: moreHosts,
576 DANEVerifiedRecord: &verifiedRecord,
577 RecipientDomainResult: recipientDomainResult,
578 HostResult: &hostResult,
580 sc, err := smtpclient.New(ctx, log, conn, tlsMode, tlsPKIX, ourHostname, firstHost, opts)
587 mox.Connections.Unregister(conn)
589 if err == nil && m.SenderAccount != "" {
590 // Remember the STARTTLS and REQUIRETLS support for this recipient domain.
591 // It is used in the webmail client, to show the recipient domain security mechanisms.
592 // We always save only the last connection we actually encountered. There may be
593 // multiple MX hosts, perhaps only some support STARTTLS and REQUIRETLS. We may not
594 // be accurate for the whole domain, but we're only storing a hint.
595 rdt := store.RecipientDomainTLS{
596 Domain: m.RecipientDomain.Domain.Name(),
597 STARTTLS: sc.TLSEnabled(),
598 RequireTLS: sc.SupportsRequireTLS(),
600 if err = updateRecipientDomainTLS(ctx, m.SenderAccount, rdt); err != nil {
601 err = fmt.Errorf("storing recipient domain tls status: %w", err)
605 // SMTP session is ready. Finally try to actually deliver.
607 smtputf8 := m.SMTPUTF8
608 var msg io.Reader = msgr
610 if m.DSNUTF8 != nil && sc.Supports8BITMIME() && sc.SupportsSMTPUTF8() {
613 size = int64(len(m.DSNUTF8))
614 msg = bytes.NewReader(m.DSNUTF8)
616 err = sc.Deliver(ctx, mailFrom, rcptTo, size, msg, has8bit, smtputf8, m.RequireTLS != nil && *m.RequireTLS)
619 log.Infox("delivery failed", err)
621 var cerr smtpclient.Error
624 deliveryResult = "ok"
625 case errors.Is(err, os.ErrDeadlineExceeded), errors.Is(err, context.DeadlineExceeded):
626 deliveryResult = "timeout"
627 case errors.Is(err, context.Canceled):
628 deliveryResult = "canceled"
629 case errors.As(err, &cerr):
630 deliveryResult = "temperror"
632 deliveryResult = "permerror"
635 deliveryResult = "error"
638 return false, tlsDANE, false, "", remoteIP, "", hostResult, true
639 } else if cerr, ok := err.(smtpclient.Error); ok {
640 // If we are being rejected due to policy reasons on the first
641 // attempt and remote has both IPv4 and IPv6, we'll give it
642 // another try. Our first IP may be in a block list, the address for
643 // the other family perhaps is not.
644 permanent := cerr.Permanent
645 if permanent && m.Attempts == 1 && dualstack && strings.HasPrefix(cerr.Secode, "7.") {
649 secode := cerr.Secode
650 if errors.Is(cerr.Err, smtpclient.ErrRequireTLSUnsupported) {
651 secode = smtp.SePol7MissingReqTLS
652 metricRequireTLSUnsupported.WithLabelValues("norequiretls").Inc()
654 return permanent, tlsDANE, errors.Is(cerr, smtpclient.ErrTLS), secode, remoteIP, cerr.Error(), hostResult, false
656 return false, tlsDANE, errors.Is(cerr, smtpclient.ErrTLS), "", remoteIP, err.Error(), hostResult, false
660// Update (overwite) last known starttls/requiretls support for recipient domain.
661func updateRecipientDomainTLS(ctx context.Context, senderAccount string, rdt store.RecipientDomainTLS) error {
662 acc, err := store.OpenAccount(senderAccount)
664 return fmt.Errorf("open account: %w", err)
666 err = acc.DB.Write(ctx, func(tx *bstore.Tx) error {
667 // First delete any existing record.
668 if err := tx.Delete(&store.RecipientDomainTLS{Domain: rdt.Domain}); err != nil && err != bstore.ErrAbsent {
669 return fmt.Errorf("removing previous recipient domain tls status: %w", err)
671 // Insert new record.
672 return tx.Insert(&rdt)
675 return fmt.Errorf("adding recipient domain tls status to account database: %w", err)