mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 16:33:47 +03:00
implement "requiretls", rfc 8689
with requiretls, the tls verification mode/rules for email deliveries can be changed by the sender/submitter. in two ways: 1. "requiretls" smtp extension to always enforce verified tls (with mta-sts or dnssec+dane), along the entire delivery path until delivery into the final destination mailbox (so entire transport is verified-tls-protected). 2. "tls-required: no" message header, to ignore any tls and tls verification errors even if the recipient domain has a policy that requires tls verification (mta-sts and/or dnssec+dane), allowing delivery of non-sensitive messages in case of misconfiguration/interoperability issues (at least useful for sending tls reports). we enable requiretls by default (only when tls is active), for smtp and submission. it can be disabled through the config. for each delivery attempt, we now store (per recipient domain, in the account of the sender) whether the smtp server supports starttls and requiretls. this support is shown (after having sent a first message) in the webmail when sending a message (the previous 3 bars under the address input field are now 5 bars, the first for starttls support, the last for requiretls support). when all recipient domains for a message are known to implement requiretls, requiretls is automatically selected for sending (instead of "default" tls behaviour). users can also select the "fallback to insecure" to add the "tls-required: no" header. new metrics are added for insight into requiretls errors and (some, not yet all) cases where tls-required-no ignored a tls/verification error. the admin can change the requiretls status for messages in the queue. so with default delivery attempts, when verified tls is required by failing, an admin could potentially change the field to "tls-required: no"-behaviour. messages received (over smtp) with the requiretls option, get a comment added to their Received header line, just before "id", after "with".
This commit is contained in:
parent
0e5e16b3d0
commit
2f5d6069bf
31 changed files with 1102 additions and 274 deletions
|
@ -20,7 +20,7 @@ See Quickstart below to get started.
|
||||||
("localparts"), and in domain names (IDNA).
|
("localparts"), and in domain names (IDNA).
|
||||||
- Automatic TLS with ACME, for use with Let's Encrypt and other CA's.
|
- Automatic TLS with ACME, for use with Let's Encrypt and other CA's.
|
||||||
- DANE and MTA-STS for inbound and outbound delivery over SMTP with STARTTLS,
|
- DANE and MTA-STS for inbound and outbound delivery over SMTP with STARTTLS,
|
||||||
with incoming TLSRPT reporting.
|
including REQUIRETLS and with incoming TLSRPT reporting.
|
||||||
- Web admin interface that helps you set up your domains and accounts
|
- Web admin interface that helps you set up your domains and accounts
|
||||||
(instructions to create DNS records, configure
|
(instructions to create DNS records, configure
|
||||||
SPF/DKIM/DMARC/TLSRPT/MTA-STS), for status information, managing
|
SPF/DKIM/DMARC/TLSRPT/MTA-STS), for status information, managing
|
||||||
|
|
|
@ -123,10 +123,14 @@ type Listener struct {
|
||||||
SMTPMaxMessageSize int64 `sconf:"optional" sconf-doc:"Maximum size in bytes for incoming and outgoing messages. Default is 100MB."`
|
SMTPMaxMessageSize int64 `sconf:"optional" sconf-doc:"Maximum size in bytes for incoming and outgoing messages. Default is 100MB."`
|
||||||
SMTP struct {
|
SMTP struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
Port int `sconf:"optional" sconf-doc:"Default 25."`
|
Port int `sconf:"optional" sconf-doc:"Default 25."`
|
||||||
NoSTARTTLS bool `sconf:"optional" sconf-doc:"Do not offer STARTTLS to secure the connection. Not recommended."`
|
NoSTARTTLS bool `sconf:"optional" sconf-doc:"Do not offer STARTTLS to secure the connection. Not recommended."`
|
||||||
RequireSTARTTLS bool `sconf:"optional" sconf-doc:"Do not accept incoming messages if STARTTLS is not active. Can be used in combination with a strict MTA-STS policy. A remote SMTP server may not support TLS and may not be able to deliver messages."`
|
RequireSTARTTLS bool `sconf:"optional" sconf-doc:"Do not accept incoming messages if STARTTLS is not active. Can be used in combination with a strict MTA-STS policy. A remote SMTP server may not support TLS and may not be able to deliver messages."`
|
||||||
DNSBLs []string `sconf:"optional" sconf-doc:"Addresses of DNS block lists for incoming messages. Block lists are only consulted for connections/messages without enough reputation to make an accept/reject decision. This prevents sending IPs of all communications to the block list provider. If any of the listed DNSBLs contains a requested IP address, the message is rejected as spam. The DNSBLs are checked for healthiness before use, at most once per 4 hours. Example DNSBLs: sbl.spamhaus.org, bl.spamcop.net. See https://www.spamhaus.org/sbl/ and https://www.spamcop.net/ for more information and terms of use."`
|
NoRequireTLS bool `sconf:"optional" sconf-doc:"Do not announce the REQUIRETLS SMTP extension. Messages delivered using the REQUIRETLS extension should only be distributed onwards to servers also implementing the REQUIRETLS extension. In some situations, such as hosting mailing lists, this may not be feasible due to lack of support for the extension by mailing list subscribers."`
|
||||||
|
// Reoriginated messages (such as messages sent to mailing list subscribers) should
|
||||||
|
// keep REQUIRETLS. ../rfc/8689:412
|
||||||
|
|
||||||
|
DNSBLs []string `sconf:"optional" sconf-doc:"Addresses of DNS block lists for incoming messages. Block lists are only consulted for connections/messages without enough reputation to make an accept/reject decision. This prevents sending IPs of all communications to the block list provider. If any of the listed DNSBLs contains a requested IP address, the message is rejected as spam. The DNSBLs are checked for healthiness before use, at most once per 4 hours. Example DNSBLs: sbl.spamhaus.org, bl.spamcop.net. See https://www.spamhaus.org/sbl/ and https://www.spamcop.net/ for more information and terms of use."`
|
||||||
|
|
||||||
FirstTimeSenderDelay *time.Duration `sconf:"optional" sconf-doc:"Delay before accepting a message from a first-time sender for the destination account. Default: 15s."`
|
FirstTimeSenderDelay *time.Duration `sconf:"optional" sconf-doc:"Delay before accepting a message from a first-time sender for the destination account. Default: 15s."`
|
||||||
|
|
||||||
|
|
|
@ -184,6 +184,13 @@ describe-static" and "mox config describe-domains":
|
||||||
# TLS and may not be able to deliver messages. (optional)
|
# TLS and may not be able to deliver messages. (optional)
|
||||||
RequireSTARTTLS: false
|
RequireSTARTTLS: false
|
||||||
|
|
||||||
|
# Do not announce the REQUIRETLS SMTP extension. Messages delivered using the
|
||||||
|
# REQUIRETLS extension should only be distributed onwards to servers also
|
||||||
|
# implementing the REQUIRETLS extension. In some situations, such as hosting
|
||||||
|
# mailing lists, this may not be feasible due to lack of support for the extension
|
||||||
|
# by mailing list subscribers. (optional)
|
||||||
|
NoRequireTLS: false
|
||||||
|
|
||||||
# Addresses of DNS block lists for incoming messages. Block lists are only
|
# Addresses of DNS block lists for incoming messages. Block lists are only
|
||||||
# consulted for connections/messages without enough reputation to make an
|
# consulted for connections/messages without enough reputation to make an
|
||||||
# accept/reject decision. This prevents sending IPs of all communications to the
|
# accept/reject decision. This prevents sending IPs of all communications to the
|
||||||
|
|
|
@ -297,7 +297,7 @@ func Dial(ctx context.Context, resolver dns.Resolver, network, address string, a
|
||||||
// verified, if any.
|
// verified, if any.
|
||||||
func TLSClientConfig(log *mlog.Log, records []adns.TLSA, allowedHost dns.Domain, moreAllowedHosts []dns.Domain, verifiedRecord *adns.TLSA) tls.Config {
|
func TLSClientConfig(log *mlog.Log, records []adns.TLSA, allowedHost dns.Domain, moreAllowedHosts []dns.Domain, verifiedRecord *adns.TLSA) tls.Config {
|
||||||
return tls.Config{
|
return tls.Config{
|
||||||
ServerName: allowedHost.ASCII,
|
ServerName: allowedHost.ASCII, // For SNI.
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true,
|
||||||
VerifyConnection: func(cs tls.ConnectionState) error {
|
VerifyConnection: func(cs tls.ConnectionState) error {
|
||||||
verified, record, err := Verify(log, records, cs, allowedHost, moreAllowedHosts)
|
verified, record, err := Verify(log, records, cs, allowedHost, moreAllowedHosts)
|
||||||
|
|
|
@ -233,7 +233,7 @@ Accounts:
|
||||||
const qmsg = "From: <test0@mox.example>\r\nTo: <other@remote.example>\r\nSubject: test\r\n\r\nthe message...\r\n"
|
const qmsg = "From: <test0@mox.example>\r\nTo: <other@remote.example>\r\nSubject: test\r\n\r\nthe message...\r\n"
|
||||||
_, err = fmt.Fprint(mf, qmsg)
|
_, err = fmt.Fprint(mf, qmsg)
|
||||||
xcheckf(err, "writing message")
|
xcheckf(err, "writing message")
|
||||||
_, err = queue.Add(ctxbg, log, "test0", mailfrom, rcptto, false, false, int64(len(qmsg)), "<test@localhost>", prefix, mf, nil)
|
_, err = queue.Add(ctxbg, log, "test0", mailfrom, rcptto, false, false, int64(len(qmsg)), "<test@localhost>", prefix, mf, nil, nil)
|
||||||
xcheckf(err, "enqueue message")
|
xcheckf(err, "enqueue message")
|
||||||
|
|
||||||
// Create three accounts.
|
// Create three accounts.
|
||||||
|
|
|
@ -131,7 +131,7 @@ This is the message.
|
||||||
auth := []sasl.Client{sasl.NewClientPlain(mailfrom, password)}
|
auth := []sasl.Client{sasl.NewClientPlain(mailfrom, password)}
|
||||||
c, err := smtpclient.New(mox.Context, xlog, conn, smtpclient.TLSSkip, ourHostname, dns.Domain{ASCII: desthost}, auth, nil, nil, nil)
|
c, err := smtpclient.New(mox.Context, xlog, conn, smtpclient.TLSSkip, ourHostname, dns.Domain{ASCII: desthost}, auth, nil, nil, nil)
|
||||||
tcheck(t, err, "smtp hello")
|
tcheck(t, err, "smtp hello")
|
||||||
err = c.Deliver(mox.Context, mailfrom, rcptto, int64(len(msg)), strings.NewReader(msg), false, false)
|
err = c.Deliver(mox.Context, mailfrom, rcptto, int64(len(msg)), strings.NewReader(msg), false, false, false)
|
||||||
tcheck(t, err, "deliver with smtp")
|
tcheck(t, err, "deliver with smtp")
|
||||||
err = c.Close()
|
err = c.Close()
|
||||||
tcheck(t, err, "close smtpclient")
|
tcheck(t, err, "close smtpclient")
|
||||||
|
|
1
main.go
1
main.go
|
@ -130,6 +130,7 @@ var commands = []struct {
|
||||||
{"cid", cmdCid},
|
{"cid", cmdCid},
|
||||||
{"clientconfig", cmdClientConfig},
|
{"clientconfig", cmdClientConfig},
|
||||||
{"deliver", cmdDeliver},
|
{"deliver", cmdDeliver},
|
||||||
|
// todo: turn cmdDANEDialmx into a regular "dialmx" command that follows mta-sts policy, with options to require dane, mta-sts or requiretls. the code will be similar to queue/direct.go
|
||||||
{"dane dial", cmdDANEDial},
|
{"dane dial", cmdDANEDial},
|
||||||
{"dane dialmx", cmdDANEDialmx},
|
{"dane dialmx", cmdDANEDialmx},
|
||||||
{"dane makerecord", cmdDANEMakeRecord},
|
{"dane makerecord", cmdDANEMakeRecord},
|
||||||
|
|
157
queue/direct.go
157
queue/direct.go
|
@ -23,6 +23,7 @@ import (
|
||||||
"github.com/mjl-/mox/mox-"
|
"github.com/mjl-/mox/mox-"
|
||||||
"github.com/mjl-/mox/mtasts"
|
"github.com/mjl-/mox/mtasts"
|
||||||
"github.com/mjl-/mox/mtastsdb"
|
"github.com/mjl-/mox/mtastsdb"
|
||||||
|
"github.com/mjl-/mox/smtp"
|
||||||
"github.com/mjl-/mox/smtpclient"
|
"github.com/mjl-/mox/smtpclient"
|
||||||
"github.com/mjl-/mox/store"
|
"github.com/mjl-/mox/store"
|
||||||
)
|
)
|
||||||
|
@ -58,13 +59,36 @@ var (
|
||||||
Help: "Total number of connections where looking up TLSA records resulted in an error.",
|
Help: "Total number of connections where looking up TLSA records resulted in an error.",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
// 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.
|
||||||
|
metricTLSRequiredNoIgnored = promauto.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "mox_queue_tlsrequiredno_ignored_total",
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"ignored", // mtastspolicy (error getting policy), mtastsmx (mx host not allowed in policy), badtls (error negotiating tls), badtlsa (error fetching dane tlsa records)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
metricRequireTLSUnsupported = promauto.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "mox_queue_requiretls_unsupported_total",
|
||||||
|
Help: "Delivery attempts that failed due to message with REQUIRETLS.",
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"reason", // nopolicy (no mta-sts and no dane), norequiretls (smtp server does not support requiretls)
|
||||||
|
},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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(qlog *mlog.Log, m Msg, backoff time.Duration, permanent bool, remoteMTA dsn.NameIP, secodeOpt, errmsg string) {
|
func fail(qlog *mlog.Log, m Msg, backoff time.Duration, permanent bool, remoteMTA dsn.NameIP, secodeOpt, errmsg 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, 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
|
||||||
|
|
||||||
if permanent || m.Attempts >= 8 {
|
if permanent || m.Attempts >= 8 {
|
||||||
qlog.Errorx("permanent failure delivering from queue", errors.New(errmsg))
|
qlog.Errorx("permanent failure delivering from queue", errors.New(errmsg))
|
||||||
queueDSNFailure(qlog, m, remoteMTA, secodeOpt, errmsg)
|
deliverDSNFailure(qlog, m, remoteMTA, secodeOpt, errmsg)
|
||||||
|
|
||||||
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)
|
||||||
|
@ -84,7 +108,7 @@ func fail(qlog *mlog.Log, m Msg, backoff time.Duration, permanent bool, remoteMT
|
||||||
qlog.Errorx("temporary failure delivering from queue, sending delayed dsn", errors.New(errmsg), mlog.Field("backoff", backoff))
|
qlog.Errorx("temporary failure delivering from queue, sending delayed dsn", errors.New(errmsg), mlog.Field("backoff", backoff))
|
||||||
|
|
||||||
retryUntil := m.LastAttempt.Add((4 + 8 + 16) * time.Hour)
|
retryUntil := m.LastAttempt.Add((4 + 8 + 16) * time.Hour)
|
||||||
queueDSNDelay(qlog, m, remoteMTA, secodeOpt, errmsg, retryUntil)
|
deliverDSNDelay(qlog, m, remoteMTA, secodeOpt, errmsg, retryUntil)
|
||||||
} else {
|
} else {
|
||||||
qlog.Errorx("temporary failure delivering from queue", errors.New(errmsg), mlog.Field("backoff", backoff), mlog.Field("nextattempt", m.NextAttempt))
|
qlog.Errorx("temporary failure delivering from queue", errors.New(errmsg), mlog.Field("backoff", backoff), mlog.Field("nextattempt", m.NextAttempt))
|
||||||
}
|
}
|
||||||
|
@ -102,6 +126,10 @@ func deliverDirect(cid int64, qlog *mlog.Log, resolver dns.Resolver, dialer smtp
|
||||||
// successful delivery. But hopefully the delivery just succeeds. For each host:
|
// successful delivery. But hopefully the delivery just succeeds. For each host:
|
||||||
// - If there is an MTA-STS policy, we only connect to allow-listed hosts.
|
// - If there is an MTA-STS policy, we only connect to allow-listed hosts.
|
||||||
// - We try to lookup DANE records (optional) and verify them if present.
|
// - We try to lookup DANE records (optional) and verify them if present.
|
||||||
|
// - If RequireTLS is true, we only deliver if the remote SMTP server implements it.
|
||||||
|
// - If RequireTLS is false, we'll fall back to regular delivery attempts without
|
||||||
|
// TLS verification and possibly without TLS at all, ignoring recipient domain/host
|
||||||
|
// MTA-STS and DANE policies.
|
||||||
|
|
||||||
// Resolve domain and hosts to attempt delivery to.
|
// Resolve domain and hosts to attempt delivery to.
|
||||||
// These next-hop names are often the name under which we find MX records. The
|
// These next-hop names are often the name under which we find MX records. The
|
||||||
|
@ -125,9 +153,14 @@ func deliverDirect(cid int64, qlog *mlog.Log, resolver dns.Resolver, dialer smtp
|
||||||
cidctx := context.WithValue(mox.Shutdown, mlog.CidKey, cid)
|
cidctx := context.WithValue(mox.Shutdown, mlog.CidKey, cid)
|
||||||
policy, _, err = mtastsdb.Get(cidctx, resolver, origNextHop)
|
policy, _, err = mtastsdb.Get(cidctx, resolver, origNextHop)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
qlog.Infox("mtasts lookup temporary error, aborting delivery attempt", err, mlog.Field("domain", origNextHop))
|
if m.RequireTLS != nil && !*m.RequireTLS {
|
||||||
fail(qlog, m, backoff, false, dsn.NameIP{}, "", err.Error())
|
qlog.Infox("mtasts lookup temporary error, continuing due to tls-required-no message header", err, mlog.Field("domain", origNextHop))
|
||||||
return
|
metricTLSRequiredNoIgnored.WithLabelValues("mtastspolicy").Inc()
|
||||||
|
} else {
|
||||||
|
qlog.Infox("mtasts lookup temporary error, aborting delivery attempt", err, mlog.Field("domain", origNextHop))
|
||||||
|
fail(qlog, m, backoff, false, dsn.NameIP{}, "", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// note: policy can be nil, if a domain does not implement MTA-STS or it's the
|
// note: policy can be nil, if a domain does not implement MTA-STS or it's the
|
||||||
// first time we fetch the policy and if we encountered an error.
|
// first time we fetch the policy and if we encountered an error.
|
||||||
|
@ -144,6 +177,7 @@ func deliverDirect(cid int64, qlog *mlog.Log, resolver dns.Resolver, dialer smtp
|
||||||
var remoteMTA dsn.NameIP
|
var remoteMTA dsn.NameIP
|
||||||
var secodeOpt, errmsg string
|
var secodeOpt, errmsg string
|
||||||
permanent = false
|
permanent = false
|
||||||
|
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
|
||||||
for _, h := range hosts {
|
for _, h := range hosts {
|
||||||
var badTLS, ok bool
|
var badTLS, ok bool
|
||||||
|
@ -155,11 +189,17 @@ func deliverDirect(cid int64, qlog *mlog.Log, resolver dns.Resolver, dialer smtp
|
||||||
policyHosts = append(policyHosts, mx.LogString())
|
policyHosts = append(policyHosts, mx.LogString())
|
||||||
}
|
}
|
||||||
if policy.Mode == mtasts.ModeEnforce {
|
if policy.Mode == mtasts.ModeEnforce {
|
||||||
errmsg = fmt.Sprintf("mx host %s does not match enforced mta-sts policy with hosts %s", h.Domain, strings.Join(policyHosts, ","))
|
if m.RequireTLS != nil && !*m.RequireTLS {
|
||||||
qlog.Error("mx host does not match mta-sts policy in mode enforce, skipping", mlog.Field("host", h.Domain), mlog.Field("policyhosts", policyHosts))
|
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))
|
||||||
continue
|
metricTLSRequiredNoIgnored.WithLabelValues("mtastsmx").Inc()
|
||||||
|
} else {
|
||||||
|
errmsg = fmt.Sprintf("mx host %s does not match enforced mta-sts policy with hosts %s", h.Domain, strings.Join(policyHosts, ","))
|
||||||
|
qlog.Error("mx host does not match mta-sts policy in mode enforce, skipping", mlog.Field("host", h.Domain), mlog.Field("policyhosts", policyHosts))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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))
|
||||||
}
|
}
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
qlog.Info("delivering to remote", mlog.Field("remote", h), mlog.Field("queuecid", cid))
|
qlog.Info("delivering to remote", mlog.Field("remote", h), mlog.Field("queuecid", cid))
|
||||||
|
@ -190,10 +230,14 @@ func deliverDirect(cid int64, qlog *mlog.Log, resolver dns.Resolver, dialer smtp
|
||||||
// old server that only does ancient TLS versions, or has a misconfiguration. Note
|
// old server that only does ancient TLS versions, or has a misconfiguration. Note
|
||||||
// that opportunistic TLS does not do regular certificate verification, so that can't
|
// that opportunistic TLS does not do regular certificate verification, so that can't
|
||||||
// be the problem.
|
// be the problem.
|
||||||
if !ok && badTLS && !enforceMTASTS && tlsMode == smtpclient.TLSOpportunistic && !daneRequired {
|
if !ok && badTLS && (!enforceMTASTS && tlsMode == smtpclient.TLSOpportunistic && !daneRequired || m.RequireTLS != nil && !*m.RequireTLS) {
|
||||||
|
if m.RequireTLS != nil && !*m.RequireTLS {
|
||||||
|
metricTLSRequiredNoIgnored.WithLabelValues("badtls").Inc()
|
||||||
|
}
|
||||||
|
|
||||||
// In case of failure with opportunistic TLS, try again without TLS. ../rfc/7435:459
|
// In case of failure with opportunistic TLS, try again without TLS. ../rfc/7435:459
|
||||||
// todo future: add a configuration option to not fall back?
|
// todo future: add a configuration option to not fall back?
|
||||||
nqlog.Info("connecting again for delivery attempt without tls")
|
nqlog.Info("connecting again for delivery attempt without tls", mlog.Field("enforcemtasts", enforceMTASTS), mlog.Field("danerequired", daneRequired), mlog.Field("requiretls", m.RequireTLS))
|
||||||
tlsMode = smtpclient.TLSSkip
|
tlsMode = smtpclient.TLSSkip
|
||||||
permanent, _, _, secodeOpt, remoteIP, errmsg, ok = deliverHost(nqlog, resolver, dialer, cid, ourHostname, transportName, h, enforceMTASTS, haveMX, origNextHopAuthentic, origNextHop, expandedNextHopAuthentic, expandedNextHop, &m, tlsMode)
|
permanent, _, _, secodeOpt, remoteIP, errmsg, ok = deliverHost(nqlog, resolver, dialer, cid, ourHostname, transportName, h, enforceMTASTS, haveMX, origNextHopAuthentic, origNextHop, expandedNextHopAuthentic, expandedNextHop, &m, tlsMode)
|
||||||
}
|
}
|
||||||
|
@ -209,6 +253,9 @@ func deliverDirect(cid int64, qlog *mlog.Log, resolver dns.Resolver, dialer smtp
|
||||||
if permanent {
|
if permanent {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
if secodeOpt == smtp.SePol7MissingReqTLS {
|
||||||
|
nmissingRequireTLS++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// In theory, we could make a failure permanent if we didn't find any mx host
|
// In theory, we could make a failure permanent if we didn't find any mx host
|
||||||
|
@ -220,13 +267,27 @@ func deliverDirect(cid int64, qlog *mlog.Log, resolver dns.Resolver, dialer smtp
|
||||||
// failures.
|
// failures.
|
||||||
// todo: possibly detect that future deliveries will fail due to long ttl's of cached records that are preventing delivery.
|
// todo: possibly detect that future deliveries will fail due to long ttl's of cached records that are preventing delivery.
|
||||||
|
|
||||||
|
// If we failed due to requiretls not being satisfied, make the delivery permanent.
|
||||||
|
// It is unlikely the recipient domain will implement requiretls during our retry
|
||||||
|
// period. Best to let the sender know immediately.
|
||||||
|
if !permanent && nmissingRequireTLS > 0 && nmissingRequireTLS == len(hosts) {
|
||||||
|
qlog.Info("marking delivery as permanently failed because recipient domain does not implement requiretls")
|
||||||
|
permanent = true
|
||||||
|
}
|
||||||
|
|
||||||
fail(qlog, m, backoff, permanent, remoteMTA, secodeOpt, errmsg)
|
fail(qlog, m, backoff, permanent, remoteMTA, secodeOpt, errmsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// deliverHost attempts to deliver m to host. Depending on tlsMode, we'll do
|
// deliverHost attempts to deliver m to host. Depending on tlsMode, we'll do
|
||||||
// required TLS with WebPKI verification (with MTA-STS), opportunistic DANE TLS
|
// required TLS with WebPKI verification (with MTA-STS), opportunistic DANE TLS
|
||||||
// (opportunistic TLS) or non-verifying TLS (opportunistic TLS) deliverHost updates
|
// (opportunistic TLS), non-verifying TLS (opportunistic TLS) or skip TLS
|
||||||
// m.DialedIPs, which must be saved in case of failure to deliver.
|
// altogether due to previous TLS errors.
|
||||||
|
//
|
||||||
|
// deliverHost updates m.DialedIPs, which must be saved in case of failure to
|
||||||
|
// deliver.
|
||||||
|
//
|
||||||
|
// With TLS-Required no header, we ignore verification failures and continue
|
||||||
|
// delivering.
|
||||||
//
|
//
|
||||||
// The haveMX and next-hop-authentic fields are used to determine if DANE is
|
// The haveMX and next-hop-authentic fields are used to determine if DANE is
|
||||||
// applicable. The next-hop fields themselves are used to determine valid names
|
// applicable. The next-hop fields themselves are used to determine valid names
|
||||||
|
@ -282,14 +343,13 @@ func deliverHost(log *mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer,
|
||||||
if err == nil && authentic && origNextHopAuthentic && (!haveMX || expandedNextHopAuthentic) && host.IsDomain() {
|
if err == nil && authentic && origNextHopAuthentic && (!haveMX || expandedNextHopAuthentic) && host.IsDomain() {
|
||||||
metricDestinationsAuthentic.Inc()
|
metricDestinationsAuthentic.Inc()
|
||||||
|
|
||||||
// Modes to skip and not verify aren't normally set when we get here. But in the
|
|
||||||
// future may perhaps be set on a message manually after delivery failures. We can
|
|
||||||
// handle them here.
|
|
||||||
switch tlsMode {
|
switch tlsMode {
|
||||||
case smtpclient.TLSSkip:
|
case smtpclient.TLSSkip:
|
||||||
// No TLS, so clearly no DANE.
|
// No TLS, so clearly no DANE. This can happen if we've dialed TLS before but a TLS
|
||||||
|
// connection couldn't be established.
|
||||||
case smtpclient.TLSUnverifiedStartTLS:
|
case smtpclient.TLSUnverifiedStartTLS:
|
||||||
// Fallback mode for DANE without usable records, so skip DANE.
|
// Fallback mode for DANE without usable records, so skip DANE.
|
||||||
|
// We shouldn't be able to get here, but no harm handling it.
|
||||||
default:
|
default:
|
||||||
// Look for TLSA records in either the expandedHost, or otherwise the original
|
// Look for TLSA records in either the expandedHost, or otherwise the original
|
||||||
// host. ../rfc/7672:912
|
// host. ../rfc/7672:912
|
||||||
|
@ -324,6 +384,11 @@ func deliverHost(log *mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer,
|
||||||
} else if !daneRequired {
|
} else if !daneRequired {
|
||||||
log.Debugx("not doing opportunistic dane after gathering tlsa records", err)
|
log.Debugx("not doing opportunistic dane after gathering tlsa records", err)
|
||||||
err = nil
|
err = nil
|
||||||
|
} else if err != nil && m.RequireTLS != nil && !*m.RequireTLS {
|
||||||
|
log.Debugx("error gathering dane tlsa records with dane required, but continuing without validation due to tls-required-no message header", err)
|
||||||
|
daneRecords = nil
|
||||||
|
err = nil
|
||||||
|
metricTLSRequiredNoIgnored.WithLabelValues("badtlsa").Inc()
|
||||||
}
|
}
|
||||||
// else, err is propagated below.
|
// else, err is propagated below.
|
||||||
}
|
}
|
||||||
|
@ -331,6 +396,16 @@ func deliverHost(log *mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer,
|
||||||
log.Debugx("not attempting verification with dane", err, mlog.Field("authentic", authentic), mlog.Field("expandedauthentic", expandedAuthentic))
|
log.Debugx("not attempting verification with dane", err, mlog.Field("authentic", authentic), mlog.Field("expandedauthentic", expandedAuthentic))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// 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?
|
||||||
|
// 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.
|
||||||
|
if err == nil && m.RequireTLS != nil && *m.RequireTLS && !(daneRequired && len(daneRecords) > 0) && !enforceMTASTS {
|
||||||
|
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()
|
||||||
|
// Resond with proper enhanced status code. ../rfc/8689:301
|
||||||
|
return false, daneRequired, false, smtp.SePol7MissingReqTLS, remoteIP, "missing required tls verification mechanism", false
|
||||||
|
}
|
||||||
|
|
||||||
// Dial the remote host given the IPs if no error yet.
|
// Dial the remote host given the IPs if no error yet.
|
||||||
var conn net.Conn
|
var conn net.Conn
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -380,6 +455,9 @@ func deliverHost(log *mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer,
|
||||||
moreHosts = tlsRemoteHostnames[1:]
|
moreHosts = tlsRemoteHostnames[1:]
|
||||||
}
|
}
|
||||||
var verifiedRecord adns.TLSA
|
var verifiedRecord adns.TLSA
|
||||||
|
if m.RequireTLS != nil && !*m.RequireTLS && tlsMode != smtpclient.TLSSkip {
|
||||||
|
tlsMode = smtpclient.TLSUnverifiedStartTLS
|
||||||
|
}
|
||||||
sc, err := smtpclient.New(ctx, log, conn, tlsMode, ourHostname, firstHost, nil, daneRecords, moreHosts, &verifiedRecord)
|
sc, err := smtpclient.New(ctx, log, conn, tlsMode, ourHostname, firstHost, nil, daneRecords, moreHosts, &verifiedRecord)
|
||||||
defer func() {
|
defer func() {
|
||||||
if sc == nil {
|
if sc == nil {
|
||||||
|
@ -389,6 +467,21 @@ func deliverHost(log *mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer,
|
||||||
}
|
}
|
||||||
mox.Connections.Unregister(conn)
|
mox.Connections.Unregister(conn)
|
||||||
}()
|
}()
|
||||||
|
if err == nil && m.SenderAccount != "" {
|
||||||
|
// Remember the STARTTLS and REQUIRETLS support for this recipient domain.
|
||||||
|
// It is used in the webmail client, to show the recipient domain security mechanisms.
|
||||||
|
// We always save only the last connection we actually encountered. There may be
|
||||||
|
// multiple MX hosts, perhaps only some support STARTTLS and REQUIRETLS. We may not
|
||||||
|
// be accurate for the whole domain, but we're only storing a hint.
|
||||||
|
rdt := store.RecipientDomainTLS{
|
||||||
|
Domain: m.RecipientDomain.Domain.Name(),
|
||||||
|
STARTTLS: sc.TLSEnabled(),
|
||||||
|
RequireTLS: sc.SupportsRequireTLS(),
|
||||||
|
}
|
||||||
|
if err = updateRecipientDomainTLS(ctx, m.SenderAccount, rdt); err != nil {
|
||||||
|
err = fmt.Errorf("storing recipient domain tls status: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// SMTP session is ready. Finally try to actually deliver.
|
// SMTP session is ready. Finally try to actually deliver.
|
||||||
has8bit := m.Has8bit
|
has8bit := m.Has8bit
|
||||||
|
@ -401,7 +494,7 @@ func deliverHost(log *mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer,
|
||||||
size = int64(len(m.DSNUTF8))
|
size = int64(len(m.DSNUTF8))
|
||||||
msg = bytes.NewReader(m.DSNUTF8)
|
msg = bytes.NewReader(m.DSNUTF8)
|
||||||
}
|
}
|
||||||
err = sc.Deliver(ctx, mailFrom, rcptTo, size, msg, has8bit, smtputf8)
|
err = sc.Deliver(ctx, mailFrom, rcptTo, size, msg, has8bit, smtputf8, m.RequireTLS != nil && *m.RequireTLS)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Infox("delivery failed", err)
|
log.Infox("delivery failed", err)
|
||||||
|
@ -433,8 +526,34 @@ func deliverHost(log *mlog.Log, resolver dns.Resolver, dialer smtpclient.Dialer,
|
||||||
if permanent && m.Attempts == 1 && dualstack && strings.HasPrefix(cerr.Secode, "7.") {
|
if permanent && m.Attempts == 1 && dualstack && strings.HasPrefix(cerr.Secode, "7.") {
|
||||||
permanent = false
|
permanent = false
|
||||||
}
|
}
|
||||||
return permanent, daneRequired, errors.Is(cerr, smtpclient.ErrTLS), cerr.Secode, remoteIP, cerr.Error(), false
|
// If server does not implement requiretls, respond with that code. ../rfc/8689:301
|
||||||
|
secode := cerr.Secode
|
||||||
|
if errors.Is(cerr.Err, smtpclient.ErrRequireTLSUnsupported) {
|
||||||
|
secode = smtp.SePol7MissingReqTLS
|
||||||
|
metricRequireTLSUnsupported.WithLabelValues("norequiretls").Inc()
|
||||||
|
}
|
||||||
|
return permanent, daneRequired, errors.Is(cerr, smtpclient.ErrTLS), secode, remoteIP, cerr.Error(), false
|
||||||
} else {
|
} else {
|
||||||
return false, daneRequired, errors.Is(cerr, smtpclient.ErrTLS), "", remoteIP, err.Error(), false
|
return false, daneRequired, errors.Is(cerr, smtpclient.ErrTLS), "", remoteIP, err.Error(), false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update (overwite) last known starttls/requiretls support for recipient domain.
|
||||||
|
func updateRecipientDomainTLS(ctx context.Context, senderAccount string, rdt store.RecipientDomainTLS) error {
|
||||||
|
acc, err := store.OpenAccount(senderAccount)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open account: %w", err)
|
||||||
|
}
|
||||||
|
err = acc.DB.Write(ctx, func(tx *bstore.Tx) error {
|
||||||
|
// First delete any existing record.
|
||||||
|
if err := tx.Delete(&store.RecipientDomainTLS{Domain: rdt.Domain}); err != nil && err != bstore.ErrAbsent {
|
||||||
|
return fmt.Errorf("removing previous recipient domain tls status: %w", err)
|
||||||
|
}
|
||||||
|
// Insert new record.
|
||||||
|
return tx.Insert(&rdt)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("adding recipient domain tls status to account database: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
11
queue/dsn.go
11
queue/dsn.go
|
@ -15,7 +15,7 @@ import (
|
||||||
"github.com/mjl-/mox/store"
|
"github.com/mjl-/mox/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
func queueDSNFailure(log *mlog.Log, m Msg, remoteMTA dsn.NameIP, secodeOpt, errmsg string) {
|
func deliverDSNFailure(log *mlog.Log, m Msg, remoteMTA dsn.NameIP, secodeOpt, errmsg 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:
|
||||||
|
@ -29,10 +29,10 @@ Error during the last delivery attempt:
|
||||||
%s
|
%s
|
||||||
`, m.Recipient().XString(m.SMTPUTF8), errmsg)
|
`, m.Recipient().XString(m.SMTPUTF8), errmsg)
|
||||||
|
|
||||||
queueDSN(log, m, remoteMTA, secodeOpt, errmsg, true, nil, subject, message)
|
deliverDSN(log, m, remoteMTA, secodeOpt, errmsg, true, nil, subject, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
func queueDSNDelay(log *mlog.Log, m Msg, remoteMTA dsn.NameIP, secodeOpt, errmsg string, retryUntil time.Time) {
|
func deliverDSNDelay(log *mlog.Log, m Msg, remoteMTA dsn.NameIP, secodeOpt, errmsg string, retryUntil time.Time) {
|
||||||
const subject = "mail delivery delayed"
|
const subject = "mail delivery delayed"
|
||||||
message := fmt.Sprintf(`
|
message := fmt.Sprintf(`
|
||||||
Delivery has been delayed of your email to:
|
Delivery has been delayed of your email to:
|
||||||
|
@ -47,15 +47,14 @@ Error during the last delivery attempt:
|
||||||
%s
|
%s
|
||||||
`, m.Recipient().XString(false), errmsg)
|
`, m.Recipient().XString(false), errmsg)
|
||||||
|
|
||||||
queueDSN(log, m, remoteMTA, secodeOpt, errmsg, false, &retryUntil, subject, message)
|
deliverDSN(log, m, remoteMTA, secodeOpt, errmsg, false, &retryUntil, subject, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We only queue DSNs for delivery failures for emails submitted by authenticated
|
// We only queue DSNs for delivery failures for emails submitted by authenticated
|
||||||
// users. So we are delivering to local users. ../rfc/5321:1466
|
// users. So we are delivering to local users. ../rfc/5321:1466
|
||||||
// ../rfc/5321:1494
|
// ../rfc/5321:1494
|
||||||
// ../rfc/7208:490
|
// ../rfc/7208:490
|
||||||
// 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
|
func deliverDSN(log *mlog.Log, m Msg, remoteMTA dsn.NameIP, secodeOpt, errmsg string, permanent bool, retryUntil *time.Time, subject, textBody string) {
|
||||||
func queueDSN(log *mlog.Log, m Msg, remoteMTA dsn.NameIP, secodeOpt, errmsg string, permanent bool, retryUntil *time.Time, subject, textBody string) {
|
|
||||||
kind := "delayed delivery"
|
kind := "delayed delivery"
|
||||||
if permanent {
|
if permanent {
|
||||||
kind = "failure"
|
kind = "failure"
|
||||||
|
|
|
@ -99,6 +99,21 @@ type Msg struct {
|
||||||
// admin interface. If empty (the default for a submitted message), regular routing
|
// admin interface. If empty (the default for a submitted message), regular routing
|
||||||
// rules apply.
|
// rules apply.
|
||||||
Transport string
|
Transport string
|
||||||
|
|
||||||
|
// RequireTLS influences TLS verification during delivery.
|
||||||
|
//
|
||||||
|
// If nil, the recipient domain policy is followed (MTA-STS and/or DANE), falling
|
||||||
|
// back to optional opportunistic non-verified STARTTLS.
|
||||||
|
//
|
||||||
|
// If RequireTLS is true (through SMTP REQUIRETLS extension or webmail submit),
|
||||||
|
// MTA-STS or DANE is required, as well as REQUIRETLS support by the next hop
|
||||||
|
// server.
|
||||||
|
//
|
||||||
|
// If RequireTLS is false (through messag header "TLS-Required: No"), the recipient
|
||||||
|
// domain's policy is ignored if it does not lead to a successful TLS connection,
|
||||||
|
// i.e. falling back to SMTP delivery with unverified STARTTLS or plain text.
|
||||||
|
RequireTLS *bool
|
||||||
|
// ../rfc/8689:250
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sender of message as used in MAIL FROM.
|
// Sender of message as used in MAIL FROM.
|
||||||
|
@ -180,7 +195,7 @@ func Count(ctx context.Context) (int, error) {
|
||||||
// this data is used as the message when delivering the DSN and the remote SMTP
|
// this data is used as the message when delivering the DSN and the remote SMTP
|
||||||
// server supports SMTPUTF8. If the remote SMTP server does not support SMTPUTF8,
|
// server supports SMTPUTF8. If the remote SMTP server does not support SMTPUTF8,
|
||||||
// the regular non-utf8 message is delivered.
|
// the regular non-utf8 message is delivered.
|
||||||
func Add(ctx context.Context, log *mlog.Log, senderAccount string, mailFrom, rcptTo smtp.Path, has8bit, smtputf8 bool, size int64, messageID string, msgPrefix []byte, msgFile *os.File, dsnutf8Opt []byte) (int64, error) {
|
func Add(ctx context.Context, log *mlog.Log, senderAccount string, mailFrom, rcptTo smtp.Path, has8bit, smtputf8 bool, size int64, messageID string, msgPrefix []byte, msgFile *os.File, dsnutf8Opt []byte, requireTLS *bool) (int64, error) {
|
||||||
// todo: Add should accept multiple rcptTo if they are for the same domain. so we can queue them for delivery in one (or just a few) session(s), transferring the data only once. ../rfc/5321:3759
|
// todo: Add should accept multiple rcptTo if they are for the same domain. so we can queue them for delivery in one (or just a few) session(s), transferring the data only once. ../rfc/5321:3759
|
||||||
|
|
||||||
if Localserve {
|
if Localserve {
|
||||||
|
@ -221,7 +236,7 @@ func Add(ctx context.Context, log *mlog.Log, senderAccount string, mailFrom, rcp
|
||||||
}()
|
}()
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
qm := Msg{0, now, senderAccount, mailFrom.Localpart, mailFrom.IPDomain, rcptTo.Localpart, rcptTo.IPDomain, formatIPDomain(rcptTo.IPDomain), 0, nil, now, nil, "", has8bit, smtputf8, size, messageID, msgPrefix, dsnutf8Opt, ""}
|
qm := Msg{0, now, senderAccount, mailFrom.Localpart, mailFrom.IPDomain, rcptTo.Localpart, rcptTo.IPDomain, formatIPDomain(rcptTo.IPDomain), 0, nil, now, nil, "", has8bit, smtputf8, size, messageID, msgPrefix, dsnutf8Opt, "", requireTLS}
|
||||||
|
|
||||||
if err := tx.Insert(&qm); err != nil {
|
if err := tx.Insert(&qm); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
|
@ -339,6 +354,18 @@ func Drop(ctx context.Context, ID int64, toDomain string, recipient string) (int
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveRequireTLS updates the RequireTLS field of the message with id.
|
||||||
|
func SaveRequireTLS(ctx context.Context, id int64, requireTLS *bool) error {
|
||||||
|
return DB.Write(ctx, func(tx *bstore.Tx) error {
|
||||||
|
m := Msg{ID: id}
|
||||||
|
if err := tx.Get(&m); err != nil {
|
||||||
|
return fmt.Errorf("get message: %w", err)
|
||||||
|
}
|
||||||
|
m.RequireTLS = requireTLS
|
||||||
|
return tx.Update(&m)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
type ReadReaderAtCloser interface {
|
type ReadReaderAtCloser interface {
|
||||||
io.ReadCloser
|
io.ReadCloser
|
||||||
io.ReaderAt
|
io.ReaderAt
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -37,9 +38,27 @@ func tcheck(t *testing.T, err error, msg string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tcompare(t *testing.T, got, exp any) {
|
||||||
|
t.Helper()
|
||||||
|
if !reflect.DeepEqual(got, exp) {
|
||||||
|
t.Fatalf("got %v, expected %v", got, exp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var keepAccount bool
|
||||||
|
|
||||||
func setup(t *testing.T) (*store.Account, func()) {
|
func setup(t *testing.T) (*store.Account, func()) {
|
||||||
// Prepare config so email can be delivered to mjl@mox.example.
|
// Prepare config so email can be delivered to mjl@mox.example.
|
||||||
os.RemoveAll("../testdata/queue/data")
|
|
||||||
|
// Don't trigger the account consistency checks. Only remove account files on first
|
||||||
|
// (of randomized) runs.
|
||||||
|
if !keepAccount {
|
||||||
|
os.RemoveAll("../testdata/queue/data")
|
||||||
|
keepAccount = true
|
||||||
|
} else {
|
||||||
|
os.RemoveAll("../testdata/queue/data/queue")
|
||||||
|
}
|
||||||
|
|
||||||
mox.Context = ctxbg
|
mox.Context = ctxbg
|
||||||
mox.ConfigStaticPath = filepath.FromSlash("../testdata/queue/mox.conf")
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/queue/mox.conf")
|
||||||
mox.MustLoadConfig(true, false)
|
mox.MustLoadConfig(true, false)
|
||||||
|
@ -91,10 +110,10 @@ func TestQueue(t *testing.T) {
|
||||||
defer os.Remove(mf.Name())
|
defer os.Remove(mf.Name())
|
||||||
defer mf.Close()
|
defer mf.Close()
|
||||||
|
|
||||||
_, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, mf, nil)
|
_, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, mf, nil, nil)
|
||||||
tcheck(t, err, "add message to queue for delivery")
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
|
|
||||||
_, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, mf, nil)
|
_, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, mf, nil, nil)
|
||||||
tcheck(t, err, "add message to queue for delivery")
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
|
|
||||||
msgs, err = List(ctxbg)
|
msgs, err = List(ctxbg)
|
||||||
|
@ -198,6 +217,10 @@ func TestQueue(t *testing.T) {
|
||||||
smtpdone := make(chan struct{})
|
smtpdone := make(chan struct{})
|
||||||
|
|
||||||
fakeSMTPServer := func(server net.Conn) {
|
fakeSMTPServer := func(server net.Conn) {
|
||||||
|
defer func() {
|
||||||
|
smtpdone <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
// We do a minimal fake smtp server. We cannot import smtpserver.Serve due to cyclic dependencies.
|
// We do a minimal fake smtp server. We cannot import smtpserver.Serve due to cyclic dependencies.
|
||||||
fmt.Fprintf(server, "220 mox.example\r\n")
|
fmt.Fprintf(server, "220 mox.example\r\n")
|
||||||
br := bufio.NewReader(server)
|
br := bufio.NewReader(server)
|
||||||
|
@ -225,14 +248,16 @@ func TestQueue(t *testing.T) {
|
||||||
writeline("250 ok")
|
writeline("250 ok")
|
||||||
readline("quit")
|
readline("quit")
|
||||||
writeline("221 ok")
|
writeline("221 ok")
|
||||||
|
|
||||||
smtpdone <- struct{}{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
goodTLSConfig := tls.Config{Certificates: []tls.Certificate{moxCert}}
|
goodTLSConfig := tls.Config{Certificates: []tls.Certificate{moxCert}}
|
||||||
makeFakeSMTPSTARTTLSServer := func(tlsConfig *tls.Config, nstarttls int) func(server net.Conn) {
|
makeFakeSMTPSTARTTLSServer := func(tlsConfig *tls.Config, nstarttls int, requiretls bool) func(server net.Conn) {
|
||||||
attempt := 0
|
attempt := 0
|
||||||
return func(server net.Conn) {
|
return func(server net.Conn) {
|
||||||
|
defer func() {
|
||||||
|
smtpdone <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
attempt++
|
attempt++
|
||||||
|
|
||||||
// We do a minimal fake smtp server. We cannot import smtpserver.Serve due to cyclic dependencies.
|
// We do a minimal fake smtp server. We cannot import smtpserver.Serve due to cyclic dependencies.
|
||||||
|
@ -264,7 +289,12 @@ func TestQueue(t *testing.T) {
|
||||||
br = bufio.NewReader(server)
|
br = bufio.NewReader(server)
|
||||||
|
|
||||||
readline("ehlo")
|
readline("ehlo")
|
||||||
writeline("250 mox.example")
|
if requiretls {
|
||||||
|
writeline("250-mox.example")
|
||||||
|
writeline("250 requiretls")
|
||||||
|
} else {
|
||||||
|
writeline("250 mox.example")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
readline("mail")
|
readline("mail")
|
||||||
writeline("250 ok")
|
writeline("250 ok")
|
||||||
|
@ -277,17 +307,19 @@ func TestQueue(t *testing.T) {
|
||||||
writeline("250 ok")
|
writeline("250 ok")
|
||||||
readline("quit")
|
readline("quit")
|
||||||
writeline("221 ok")
|
writeline("221 ok")
|
||||||
|
|
||||||
smtpdone <- struct{}{}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fakeSMTPSTARTTLSServer := makeFakeSMTPSTARTTLSServer(&goodTLSConfig, 0)
|
fakeSMTPSTARTTLSServer := makeFakeSMTPSTARTTLSServer(&goodTLSConfig, 0, true)
|
||||||
makeBadFakeSMTPSTARTTLSServer := func() func(server net.Conn) {
|
makeBadFakeSMTPSTARTTLSServer := func(requiretls bool) func(server net.Conn) {
|
||||||
return makeFakeSMTPSTARTTLSServer(&tls.Config{MaxVersion: tls.VersionTLS10, Certificates: []tls.Certificate{moxCert}}, 1)
|
return makeFakeSMTPSTARTTLSServer(&tls.Config{MaxVersion: tls.VersionTLS10, Certificates: []tls.Certificate{moxCert}}, 1, requiretls)
|
||||||
}
|
}
|
||||||
|
|
||||||
fakeSubmitServer := func(server net.Conn) {
|
fakeSubmitServer := func(server net.Conn) {
|
||||||
|
defer func() {
|
||||||
|
smtpdone <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
// We do a minimal fake smtp server. We cannot import smtpserver.Serve due to cyclic dependencies.
|
// We do a minimal fake smtp server. We cannot import smtpserver.Serve due to cyclic dependencies.
|
||||||
fmt.Fprintf(server, "220 mox.example\r\n")
|
fmt.Fprintf(server, "220 mox.example\r\n")
|
||||||
br := bufio.NewReader(server)
|
br := bufio.NewReader(server)
|
||||||
|
@ -307,11 +339,9 @@ func TestQueue(t *testing.T) {
|
||||||
fmt.Fprintf(server, "250 ok\r\n")
|
fmt.Fprintf(server, "250 ok\r\n")
|
||||||
br.ReadString('\n') // Should be QUIT.
|
br.ReadString('\n') // Should be QUIT.
|
||||||
fmt.Fprintf(server, "221 ok\r\n")
|
fmt.Fprintf(server, "221 ok\r\n")
|
||||||
|
|
||||||
smtpdone <- struct{}{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
testDeliver := func(fakeServer func(conn net.Conn)) bool {
|
testQueue := func(expectDSN bool, fakeServer func(conn net.Conn)) bool {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
var pipes []net.Conn
|
var pipes []net.Conn
|
||||||
|
@ -346,6 +376,12 @@ func TestQueue(t *testing.T) {
|
||||||
smtpclient.DialHook = nil
|
smtpclient.DialHook = nil
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
inbox, err := bstore.QueryDB[store.Mailbox](ctxbg, acc.DB).FilterNonzero(store.Mailbox{Name: "Inbox"}).Get()
|
||||||
|
tcheck(t, err, "get inbox")
|
||||||
|
|
||||||
|
inboxCount, err := bstore.QueryDB[store.Message](ctxbg, acc.DB).FilterNonzero(store.Message{MailboxID: inbox.ID}).Count()
|
||||||
|
tcheck(t, err, "querying messages in inbox")
|
||||||
|
|
||||||
waitDeliver := func() {
|
waitDeliver := func() {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
timer.Reset(time.Second)
|
timer.Reset(time.Second)
|
||||||
|
@ -358,6 +394,14 @@ func TestQueue(t *testing.T) {
|
||||||
xmsgs, err := List(ctxbg)
|
xmsgs, err := List(ctxbg)
|
||||||
tcheck(t, err, "list queue")
|
tcheck(t, err, "list queue")
|
||||||
if len(xmsgs) == 0 {
|
if len(xmsgs) == 0 {
|
||||||
|
ninbox, err := bstore.QueryDB[store.Message](ctxbg, acc.DB).FilterNonzero(store.Message{MailboxID: inbox.ID}).Count()
|
||||||
|
tcheck(t, err, "querying messages in inbox")
|
||||||
|
if expectDSN && ninbox != inboxCount+1 {
|
||||||
|
t.Fatalf("got %d messages in inbox, previously %d, expected 1 additional for dsn", ninbox, inboxCount)
|
||||||
|
} else if !expectDSN && ninbox != inboxCount {
|
||||||
|
t.Fatalf("got %d messages in inbox, previously %d, expected no additional messages", ninbox, inboxCount)
|
||||||
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
i++
|
i++
|
||||||
|
@ -379,6 +423,14 @@ func TestQueue(t *testing.T) {
|
||||||
waitDeliver()
|
waitDeliver()
|
||||||
return wasNetDialer
|
return wasNetDialer
|
||||||
}
|
}
|
||||||
|
testDeliver := func(fakeServer func(conn net.Conn)) bool {
|
||||||
|
t.Helper()
|
||||||
|
return testQueue(false, fakeServer)
|
||||||
|
}
|
||||||
|
testDSN := func(fakeServer func(conn net.Conn)) bool {
|
||||||
|
t.Helper()
|
||||||
|
return testQueue(true, fakeServer)
|
||||||
|
}
|
||||||
|
|
||||||
// Test direct delivery.
|
// Test direct delivery.
|
||||||
wasNetDialer := testDeliver(fakeSMTPServer)
|
wasNetDialer := testDeliver(fakeSMTPServer)
|
||||||
|
@ -388,7 +440,7 @@ func TestQueue(t *testing.T) {
|
||||||
|
|
||||||
// Add a message to be delivered with submit because of its route.
|
// Add a message to be delivered with submit because of its route.
|
||||||
topath := smtp.Path{Localpart: "mjl", IPDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "submit.example"}}}
|
topath := smtp.Path{Localpart: "mjl", IPDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "submit.example"}}}
|
||||||
_, err = Add(ctxbg, xlog, "mjl", path, topath, false, false, int64(len(testmsg)), "<test@localhost>", nil, mf, nil)
|
_, err = Add(ctxbg, xlog, "mjl", path, topath, false, false, int64(len(testmsg)), "<test@localhost>", nil, mf, nil, nil)
|
||||||
tcheck(t, err, "add message to queue for delivery")
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
wasNetDialer = testDeliver(fakeSubmitServer)
|
wasNetDialer = testDeliver(fakeSubmitServer)
|
||||||
if !wasNetDialer {
|
if !wasNetDialer {
|
||||||
|
@ -396,7 +448,7 @@ func TestQueue(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a message to be delivered with submit because of explicitly configured transport, that uses TLS.
|
// Add a message to be delivered with submit because of explicitly configured transport, that uses TLS.
|
||||||
msgID, err := Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, mf, nil)
|
msgID, err := Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, mf, nil, nil)
|
||||||
tcheck(t, err, "add message to queue for delivery")
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
transportSubmitTLS := "submittls"
|
transportSubmitTLS := "submittls"
|
||||||
n, err = Kick(ctxbg, msgID, "", "", &transportSubmitTLS)
|
n, err = Kick(ctxbg, msgID, "", "", &transportSubmitTLS)
|
||||||
|
@ -420,7 +472,7 @@ func TestQueue(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a message to be delivered with socks.
|
// Add a message to be delivered with socks.
|
||||||
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<socks@localhost>", nil, mf, nil)
|
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<socks@localhost>", nil, mf, nil, nil)
|
||||||
tcheck(t, err, "add message to queue for delivery")
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
transportSocks := "socks"
|
transportSocks := "socks"
|
||||||
n, err = Kick(ctxbg, msgID, "", "", &transportSocks)
|
n, err = Kick(ctxbg, msgID, "", "", &transportSocks)
|
||||||
|
@ -434,7 +486,7 @@ func TestQueue(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add message to be delivered with opportunistic TLS verification.
|
// Add message to be delivered with opportunistic TLS verification.
|
||||||
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<opportunistictls@localhost>", nil, mf, nil)
|
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<opportunistictls@localhost>", nil, mf, nil, nil)
|
||||||
tcheck(t, err, "add message to queue for delivery")
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
n, err = Kick(ctxbg, msgID, "", "", nil)
|
n, err = Kick(ctxbg, msgID, "", "", nil)
|
||||||
tcheck(t, err, "kick queue")
|
tcheck(t, err, "kick queue")
|
||||||
|
@ -444,14 +496,14 @@ func TestQueue(t *testing.T) {
|
||||||
testDeliver(fakeSMTPSTARTTLSServer)
|
testDeliver(fakeSMTPSTARTTLSServer)
|
||||||
|
|
||||||
// Test fallback to plain text with TLS handshake fails.
|
// Test fallback to plain text with TLS handshake fails.
|
||||||
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<badtls@localhost>", nil, mf, nil)
|
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<badtls@localhost>", nil, mf, nil, nil)
|
||||||
tcheck(t, err, "add message to queue for delivery")
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
n, err = Kick(ctxbg, msgID, "", "", nil)
|
n, err = Kick(ctxbg, msgID, "", "", nil)
|
||||||
tcheck(t, err, "kick queue")
|
tcheck(t, err, "kick queue")
|
||||||
if n != 1 {
|
if n != 1 {
|
||||||
t.Fatalf("kick changed %d messages, expected 1", n)
|
t.Fatalf("kick changed %d messages, expected 1", n)
|
||||||
}
|
}
|
||||||
testDeliver(makeBadFakeSMTPSTARTTLSServer())
|
testDeliver(makeBadFakeSMTPSTARTTLSServer(true))
|
||||||
|
|
||||||
// Add message to be delivered with DANE verification.
|
// Add message to be delivered with DANE verification.
|
||||||
resolver.AllAuthentic = true
|
resolver.AllAuthentic = true
|
||||||
|
@ -460,7 +512,25 @@ func TestQueue(t *testing.T) {
|
||||||
{Usage: adns.TLSAUsageDANEEE, Selector: adns.TLSASelectorSPKI, MatchType: adns.TLSAMatchTypeFull, CertAssoc: moxCert.Leaf.RawSubjectPublicKeyInfo},
|
{Usage: adns.TLSAUsageDANEEE, Selector: adns.TLSASelectorSPKI, MatchType: adns.TLSAMatchTypeFull, CertAssoc: moxCert.Leaf.RawSubjectPublicKeyInfo},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<dane@localhost>", nil, mf, nil)
|
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<dane@localhost>", nil, mf, nil, nil)
|
||||||
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
|
n, err = Kick(ctxbg, msgID, "", "", nil)
|
||||||
|
tcheck(t, err, "kick queue")
|
||||||
|
if n != 1 {
|
||||||
|
t.Fatalf("kick changed %d messages, expected 1", n)
|
||||||
|
}
|
||||||
|
testDeliver(fakeSMTPSTARTTLSServer)
|
||||||
|
|
||||||
|
// We should know starttls/requiretls by now.
|
||||||
|
rdt := store.RecipientDomainTLS{Domain: "mox.example"}
|
||||||
|
err = acc.DB.Get(ctxbg, &rdt)
|
||||||
|
tcheck(t, err, "get recipientdomaintls")
|
||||||
|
tcompare(t, rdt.STARTTLS, true)
|
||||||
|
tcompare(t, rdt.RequireTLS, true)
|
||||||
|
|
||||||
|
// Add message to be delivered with verified TLS and REQUIRETLS.
|
||||||
|
yes := true
|
||||||
|
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<opportunistictls@localhost>", nil, mf, nil, &yes)
|
||||||
tcheck(t, err, "add message to queue for delivery")
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
n, err = Kick(ctxbg, msgID, "", "", nil)
|
n, err = Kick(ctxbg, msgID, "", "", nil)
|
||||||
tcheck(t, err, "kick queue")
|
tcheck(t, err, "kick queue")
|
||||||
|
@ -475,7 +545,7 @@ func TestQueue(t *testing.T) {
|
||||||
{},
|
{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<daneunusable@localhost>", nil, mf, nil)
|
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<daneunusable@localhost>", nil, mf, nil, nil)
|
||||||
tcheck(t, err, "add message to queue for delivery")
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
n, err = Kick(ctxbg, msgID, "", "", nil)
|
n, err = Kick(ctxbg, msgID, "", "", nil)
|
||||||
tcheck(t, err, "kick queue")
|
tcheck(t, err, "kick queue")
|
||||||
|
@ -492,22 +562,74 @@ func TestQueue(t *testing.T) {
|
||||||
{Usage: adns.TLSAUsageDANEEE, Selector: adns.TLSASelectorSPKI, MatchType: adns.TLSAMatchTypeFull, CertAssoc: make([]byte, sha256.Size)},
|
{Usage: adns.TLSAUsageDANEEE, Selector: adns.TLSASelectorSPKI, MatchType: adns.TLSAMatchTypeFull, CertAssoc: make([]byte, sha256.Size)},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<daneinsecure@localhost>", nil, mf, nil)
|
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<daneinsecure@localhost>", nil, mf, nil, nil)
|
||||||
tcheck(t, err, "add message to queue for delivery")
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
n, err = Kick(ctxbg, msgID, "", "", nil)
|
n, err = Kick(ctxbg, msgID, "", "", nil)
|
||||||
tcheck(t, err, "kick queue")
|
tcheck(t, err, "kick queue")
|
||||||
if n != 1 {
|
if n != 1 {
|
||||||
t.Fatalf("kick changed %d messages, expected 1", n)
|
t.Fatalf("kick changed %d messages, expected 1", n)
|
||||||
}
|
}
|
||||||
testDeliver(makeBadFakeSMTPSTARTTLSServer())
|
testDeliver(makeBadFakeSMTPSTARTTLSServer(true))
|
||||||
resolver.Inauthentic = nil
|
resolver.Inauthentic = nil
|
||||||
|
|
||||||
|
// STARTTLS failed, so not known supported.
|
||||||
|
rdt = store.RecipientDomainTLS{Domain: "mox.example"}
|
||||||
|
err = acc.DB.Get(ctxbg, &rdt)
|
||||||
|
tcheck(t, err, "get recipientdomaintls")
|
||||||
|
tcompare(t, rdt.STARTTLS, false)
|
||||||
|
tcompare(t, rdt.RequireTLS, false)
|
||||||
|
|
||||||
|
// Check that message is delivered with TLS-Required: No and non-matching DANE record.
|
||||||
|
no := false
|
||||||
|
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<tlsrequirednostarttls@localhost>", nil, mf, nil, &no)
|
||||||
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
|
n, err = Kick(ctxbg, msgID, "", "", nil)
|
||||||
|
tcheck(t, err, "kick queue")
|
||||||
|
if n != 1 {
|
||||||
|
t.Fatalf("kick changed %d messages, expected 1", n)
|
||||||
|
}
|
||||||
|
testDeliver(fakeSMTPSTARTTLSServer)
|
||||||
|
|
||||||
|
// Check that message is delivered with TLS-Required: No and bad TLS, falling back to plain text.
|
||||||
|
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<tlsrequirednoplaintext@localhost>", nil, mf, nil, &no)
|
||||||
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
|
n, err = Kick(ctxbg, msgID, "", "", nil)
|
||||||
|
tcheck(t, err, "kick queue")
|
||||||
|
if n != 1 {
|
||||||
|
t.Fatalf("kick changed %d messages, expected 1", n)
|
||||||
|
}
|
||||||
|
testDeliver(makeBadFakeSMTPSTARTTLSServer(true))
|
||||||
|
|
||||||
|
// Add message with requiretls that fails immediately due to no REQUIRETLS support in all servers.
|
||||||
|
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<tlsrequiredunsupported@localhost>", nil, mf, nil, &yes)
|
||||||
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
|
n, err = Kick(ctxbg, msgID, "", "", nil)
|
||||||
|
tcheck(t, err, "kick queue")
|
||||||
|
if n != 1 {
|
||||||
|
t.Fatalf("kick changed %d messages, expected 1", n)
|
||||||
|
}
|
||||||
|
testDSN(makeBadFakeSMTPSTARTTLSServer(false))
|
||||||
|
|
||||||
// Restore pre-DANE behaviour.
|
// Restore pre-DANE behaviour.
|
||||||
resolver.AllAuthentic = false
|
resolver.AllAuthentic = false
|
||||||
resolver.TLSA = nil
|
resolver.TLSA = nil
|
||||||
|
|
||||||
|
// Add message with requiretls that fails immediately due to no verification policy for recipient domain.
|
||||||
|
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<tlsrequirednopolicy@localhost>", nil, mf, nil, &yes)
|
||||||
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
|
n, err = Kick(ctxbg, msgID, "", "", nil)
|
||||||
|
tcheck(t, err, "kick queue")
|
||||||
|
if n != 1 {
|
||||||
|
t.Fatalf("kick changed %d messages, expected 1", n)
|
||||||
|
}
|
||||||
|
// Based on DNS lookups, there won't be any dialing or SMTP connection.
|
||||||
|
dialed <- struct{}{}
|
||||||
|
testDSN(func(conn net.Conn) {
|
||||||
|
smtpdone <- struct{}{}
|
||||||
|
})
|
||||||
|
|
||||||
// Add another message that we'll fail to deliver entirely.
|
// Add another message that we'll fail to deliver entirely.
|
||||||
_, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, mf, nil)
|
_, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, mf, nil, nil)
|
||||||
tcheck(t, err, "add message to queue for delivery")
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
|
|
||||||
msgs, err = List(ctxbg)
|
msgs, err = List(ctxbg)
|
||||||
|
@ -666,7 +788,7 @@ func TestQueueStart(t *testing.T) {
|
||||||
mf := prepareFile(t)
|
mf := prepareFile(t)
|
||||||
defer os.Remove(mf.Name())
|
defer os.Remove(mf.Name())
|
||||||
defer mf.Close()
|
defer mf.Close()
|
||||||
_, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, mf, nil)
|
_, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, mf, nil, nil)
|
||||||
tcheck(t, err, "add message to queue for delivery")
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
checkDialed(true)
|
checkDialed(true)
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/mjl-/mox/mlog"
|
"github.com/mjl-/mox/mlog"
|
||||||
"github.com/mjl-/mox/mox-"
|
"github.com/mjl-/mox/mox-"
|
||||||
"github.com/mjl-/mox/sasl"
|
"github.com/mjl-/mox/sasl"
|
||||||
|
"github.com/mjl-/mox/smtp"
|
||||||
"github.com/mjl-/mox/smtpclient"
|
"github.com/mjl-/mox/smtpclient"
|
||||||
"github.com/mjl-/mox/store"
|
"github.com/mjl-/mox/store"
|
||||||
)
|
)
|
||||||
|
@ -51,11 +52,20 @@ func deliverSubmit(cid int64, qlog *mlog.Log, resolver dns.Resolver, dialer smtp
|
||||||
qlog.Debug("queue deliversubmit result", mlog.Field("host", transport.DNSHost), mlog.Field("port", port), mlog.Field("attempt", m.Attempts), mlog.Field("permanent", permanent), mlog.Field("secodeopt", secodeOpt), mlog.Field("errmsg", errmsg), mlog.Field("ok", success), mlog.Field("duration", time.Since(start)))
|
qlog.Debug("queue deliversubmit result", mlog.Field("host", transport.DNSHost), mlog.Field("port", port), mlog.Field("attempt", m.Attempts), mlog.Field("permanent", permanent), mlog.Field("secodeopt", secodeOpt), mlog.Field("errmsg", errmsg), mlog.Field("ok", success), mlog.Field("duration", time.Since(start)))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// We don't have to attempt SMTP-DANE for submission, since it only applies to SMTP
|
// todo: SMTP-DANE should be used when relaying on port 25.
|
||||||
// relaying on port 25. ../rfc/7672:1261
|
// ../rfc/7672:1261
|
||||||
|
|
||||||
// todo: for submission, understand SRV records, and even DANE.
|
// todo: for submission, understand SRV records, and even DANE.
|
||||||
|
|
||||||
|
// If submit was done with REQUIRETLS extension for SMTP, we must verify TLS
|
||||||
|
// certificates. If our submission connection is not configured that way, abort.
|
||||||
|
requireTLS := m.RequireTLS != nil && *m.RequireTLS
|
||||||
|
if requireTLS && tlsMode != smtpclient.TLSStrictStartTLS && tlsMode != smtpclient.TLSStrictImmediate {
|
||||||
|
errmsg = fmt.Sprintf("transport %s: message requires verified tls but transport does not verify tls", transportName)
|
||||||
|
fail(qlog, m, backoff, true, dsn.NameIP{}, smtp.SePol7MissingReqTLS, errmsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
dialctx, dialcancel := context.WithTimeout(context.Background(), 30*time.Second)
|
dialctx, dialcancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer dialcancel()
|
defer dialcancel()
|
||||||
if m.DialedIPs == nil {
|
if m.DialedIPs == nil {
|
||||||
|
@ -165,7 +175,7 @@ func deliverSubmit(cid int64, qlog *mlog.Log, resolver dns.Resolver, dialer smtp
|
||||||
|
|
||||||
deliverctx, delivercancel := context.WithTimeout(context.Background(), time.Duration(60+size/(1024*1024))*time.Second)
|
deliverctx, delivercancel := context.WithTimeout(context.Background(), time.Duration(60+size/(1024*1024))*time.Second)
|
||||||
defer delivercancel()
|
defer delivercancel()
|
||||||
err = client.Deliver(deliverctx, m.Sender().String(), m.Recipient().String(), size, msgr, req8bit, reqsmtputf8)
|
err = client.Deliver(deliverctx, m.Sender().String(), m.Recipient().String(), size, msgr, req8bit, reqsmtputf8, requireTLS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
qlog.Infox("delivery failed", err)
|
qlog.Infox("delivery failed", err)
|
||||||
}
|
}
|
||||||
|
|
40
sendmail.go
40
sendmail.go
|
@ -3,6 +3,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
@ -23,18 +24,27 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var submitconf struct {
|
var submitconf struct {
|
||||||
LocalHostname string `sconf-doc:"Hosts don't always have an FQDN, set it explicitly, for EHLO."`
|
LocalHostname string `sconf-doc:"Hosts don't always have an FQDN, set it explicitly, for EHLO."`
|
||||||
Host string `sconf-doc:"Host to dial for delivery, e.g. mail.<domain>."`
|
Host string `sconf-doc:"Host to dial for delivery, e.g. mail.<domain>."`
|
||||||
Port int `sconf-doc:"Port to dial for delivery, e.g. 465 for submissions, 587 for submission, or perhaps 25 for smtp."`
|
Port int `sconf-doc:"Port to dial for delivery, e.g. 465 for submissions, 587 for submission, or perhaps 25 for smtp."`
|
||||||
TLS bool `sconf-doc:"Connect with TLS. Usually for connections to port 465."`
|
TLS bool `sconf-doc:"Connect with TLS. Usually for connections to port 465."`
|
||||||
STARTTLS bool `sconf-doc:"After starting in plain text, use STARTTLS to enable TLS. For port 587 and 25."`
|
STARTTLS bool `sconf-doc:"After starting in plain text, use STARTTLS to enable TLS. For port 587 and 25."`
|
||||||
Username string `sconf-doc:"For SMTP authentication."`
|
Username string `sconf-doc:"For SMTP authentication."`
|
||||||
Password string `sconf-doc:"For password-based SMTP authentication, e.g. SCRAM-SHA-256, SCRAM-SHA-1, CRAM-MD5, PLAIN."`
|
Password string `sconf-doc:"For password-based SMTP authentication, e.g. SCRAM-SHA-256, SCRAM-SHA-1, CRAM-MD5, PLAIN."`
|
||||||
AuthMethod string `sconf-doc:"If set, only attempt this authentication mechanism. E.g. SCRAM-SHA-256. If not set, any mutually supported algorithm can be used, in order of most to least secure."`
|
AuthMethod string `sconf-doc:"If set, only attempt this authentication mechanism. E.g. SCRAM-SHA-256. If not set, any mutually supported algorithm can be used, in order of most to least secure."`
|
||||||
From string `sconf-doc:"Address for MAIL FROM in SMTP and From-header in message."`
|
From string `sconf-doc:"Address for MAIL FROM in SMTP and From-header in message."`
|
||||||
DefaultDestination string `sconf:"optional" sconf-doc:"Used when specified address does not contain an @ and may be a local user (eg root)."`
|
DefaultDestination string `sconf:"optional" sconf-doc:"Used when specified address does not contain an @ and may be a local user (eg root)."`
|
||||||
|
RequireTLS RequireTLSOption `sconf:"optional" sconf-doc:"If yes, submission server must implement SMTP REQUIRETLS extension, and connection to submission server must use verified TLS. If no, a TLS-Required header with value no is added to the message, allowing fallback to unverified TLS or plain text delivery despite recpient domain policies. By default, the submission server will follow the policies of the recipient domain (MTA-STS and/or DANE), and apply unverified opportunistic TLS with STARTTLS."`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RequireTLSOption string
|
||||||
|
|
||||||
|
const (
|
||||||
|
RequireTLSDefault RequireTLSOption = ""
|
||||||
|
RequireTLSYes RequireTLSOption = "yes"
|
||||||
|
RequireTLSNo RequireTLSOption = "no"
|
||||||
|
)
|
||||||
|
|
||||||
func cmdConfigDescribeSendmail(c *cmd) {
|
func cmdConfigDescribeSendmail(c *cmd) {
|
||||||
c.params = ">/etc/moxsubmit.conf"
|
c.params = ">/etc/moxsubmit.conf"
|
||||||
c.help = `Describe configuration for mox when invoked as sendmail.`
|
c.help = `Describe configuration for mox when invoked as sendmail.`
|
||||||
|
@ -157,6 +167,9 @@ binary should be setgid that group:
|
||||||
if !haveTo {
|
if !haveTo {
|
||||||
line = fmt.Sprintf("To: <%s>\r\n", recipient) + line
|
line = fmt.Sprintf("To: <%s>\r\n", recipient) + line
|
||||||
}
|
}
|
||||||
|
if submitconf.RequireTLS == RequireTLSNo {
|
||||||
|
line = "TLS-Required: No\r\n" + line
|
||||||
|
}
|
||||||
header = false
|
header = false
|
||||||
} else if header {
|
} else if header {
|
||||||
t := strings.SplitN(line, ":", 2)
|
t := strings.SplitN(line, ":", 2)
|
||||||
|
@ -199,6 +212,9 @@ binary should be setgid that group:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if header && submitconf.RequireTLS == RequireTLSNo {
|
||||||
|
sb.WriteString("TLS-Required: No\r\n")
|
||||||
|
}
|
||||||
msg := sb.String()
|
msg := sb.String()
|
||||||
|
|
||||||
if recipient == "" {
|
if recipient == "" {
|
||||||
|
@ -262,6 +278,8 @@ binary should be setgid that group:
|
||||||
tlsMode = smtpclient.TLSStrictImmediate
|
tlsMode = smtpclient.TLSStrictImmediate
|
||||||
} else if submitconf.STARTTLS {
|
} else if submitconf.STARTTLS {
|
||||||
tlsMode = smtpclient.TLSStrictStartTLS
|
tlsMode = smtpclient.TLSStrictStartTLS
|
||||||
|
} else if submitconf.RequireTLS == RequireTLSYes {
|
||||||
|
xsavecheckf(errors.New("cannot submit with requiretls enabled without tls to submission server"), "checking tls configuration")
|
||||||
}
|
}
|
||||||
|
|
||||||
ourHostname, err := dns.ParseDomain(submitconf.LocalHostname)
|
ourHostname, err := dns.ParseDomain(submitconf.LocalHostname)
|
||||||
|
@ -277,7 +295,7 @@ binary should be setgid that group:
|
||||||
client, err := smtpclient.New(ctx, mlog.New("sendmail"), conn, tlsMode, ourHostname, remoteHostname, auth, nil, nil, nil)
|
client, err := smtpclient.New(ctx, mlog.New("sendmail"), conn, tlsMode, ourHostname, remoteHostname, auth, nil, nil, nil)
|
||||||
xsavecheckf(err, "open smtp session")
|
xsavecheckf(err, "open smtp session")
|
||||||
|
|
||||||
err = client.Deliver(ctx, submitconf.From, recipient, int64(len(msg)), strings.NewReader(msg), true, false)
|
err = client.Deliver(ctx, submitconf.From, recipient, int64(len(msg)), strings.NewReader(msg), true, false, submitconf.RequireTLS == RequireTLSYes)
|
||||||
xsavecheckf(err, "submit message")
|
xsavecheckf(err, "submit message")
|
||||||
|
|
||||||
if err := client.Close(); err != nil {
|
if err := client.Close(); err != nil {
|
||||||
|
|
|
@ -44,17 +44,27 @@ var (
|
||||||
"secode",
|
"secode",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
metricTLSRequiredNoIgnored = promauto.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "mox_smtpclient_tlsrequiredno_ignored_total",
|
||||||
|
Help: "Connection 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.",
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"ignored", // daneverification (no matching tlsa record)
|
||||||
|
},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
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.
|
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.
|
||||||
Err8bitmimeUnsupported = errors.New("remote smtp server does not implement 8bitmime extension, required by message")
|
Err8bitmimeUnsupported = errors.New("remote smtp server does not implement 8bitmime extension, required by message")
|
||||||
ErrSMTPUTF8Unsupported = errors.New("remote smtp server does not implement smtputf8 extension, required by message")
|
ErrSMTPUTF8Unsupported = errors.New("remote smtp server does not implement smtputf8 extension, required by message")
|
||||||
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.
|
ErrRequireTLSUnsupported = errors.New("remote smtp server does not implement requiretls extension, required for delivery")
|
||||||
ErrProtocol = errors.New("smtp protocol error") // After a malformed SMTP response or inconsistent multi-line response.
|
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.
|
||||||
ErrTLS = errors.New("tls error") // E.g. handshake failure, or hostname verification was required and failed.
|
ErrProtocol = errors.New("smtp protocol error") // After a malformed SMTP response or inconsistent multi-line response.
|
||||||
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.
|
ErrTLS = errors.New("tls error") // E.g. handshake failure, or hostname verification was required and failed.
|
||||||
ErrClosed = errors.New("client is closed")
|
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.
|
||||||
|
ErrClosed = errors.New("client is closed")
|
||||||
)
|
)
|
||||||
|
|
||||||
// TLSMode indicates if TLS must, should or must not be used.
|
// TLSMode indicates if TLS must, should or must not be used.
|
||||||
|
@ -67,7 +77,8 @@ const (
|
||||||
|
|
||||||
// Required TLS with STARTTLS for SMTP servers, without verifiying the certificate.
|
// Required TLS with STARTTLS for SMTP servers, without verifiying the certificate.
|
||||||
// This mode is needed to fallback after only unusable DANE records were found
|
// This mode is needed to fallback after only unusable DANE records were found
|
||||||
// (e.g. with unknown parameters in the TLSA records).
|
// (e.g. with unknown parameters in the TLSA records). Also for allowing
|
||||||
|
// verification errors with DANE with message header TLS-Required no.
|
||||||
TLSUnverifiedStartTLS TLSMode = "unverifiedstarttls"
|
TLSUnverifiedStartTLS TLSMode = "unverifiedstarttls"
|
||||||
|
|
||||||
// TLS immediately ("implicit TLS"), with either verified DANE TLSA records or a
|
// TLS immediately ("implicit TLS"), with either verified DANE TLSA records or a
|
||||||
|
@ -105,6 +116,7 @@ type Client struct {
|
||||||
lastlog time.Time // For adding delta timestamps between log lines.
|
lastlog time.Time // For adding delta timestamps between log lines.
|
||||||
cmds []string // Last or active command, for generating errors and metrics.
|
cmds []string // Last or active command, for generating errors and metrics.
|
||||||
cmdStart time.Time // Start of command.
|
cmdStart time.Time // Start of command.
|
||||||
|
tls bool // Whether connection is TLS protected.
|
||||||
|
|
||||||
botched bool // If set, protocol is out of sync and no further commands can be sent.
|
botched bool // If set, protocol is out of sync and no further commands can be sent.
|
||||||
needRset bool // If set, a new delivery requires an RSET command.
|
needRset bool // If set, a new delivery requires an RSET command.
|
||||||
|
@ -117,6 +129,7 @@ type Client struct {
|
||||||
extPipelining bool // Remote server supports command pipelining.
|
extPipelining bool // Remote server supports command pipelining.
|
||||||
extSMTPUTF8 bool // Remote server supports SMTPUTF8 extension.
|
extSMTPUTF8 bool // Remote server supports SMTPUTF8 extension.
|
||||||
extAuthMechanisms []string // Supported authentication mechanisms.
|
extAuthMechanisms []string // Supported authentication mechanisms.
|
||||||
|
extRequireTLS bool // Remote supports REQUIRETLS extension.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error represents a failure to deliver a message.
|
// Error represents a failure to deliver a message.
|
||||||
|
@ -185,7 +198,9 @@ func (e Error) Error() string {
|
||||||
// verification. By default, SMTP does not verify TLS for interopability reasons,
|
// verification. By default, SMTP does not verify TLS for interopability reasons,
|
||||||
// but MTA-STS or DANE can require it. If opportunistic TLS is used, and a TLS
|
// but MTA-STS or DANE can require it. If opportunistic TLS is used, and a TLS
|
||||||
// error is encountered, the caller may want to try again (on a new connection)
|
// error is encountered, the caller may want to try again (on a new connection)
|
||||||
// without TLS.
|
// without TLS. For messages with header TLS-Required no, DANE records may be
|
||||||
|
// passed along with tlsMode TLSUnverifiedStartTLS. In that case, failing DANE
|
||||||
|
// verification causes an error to be logged, but the connection won't be aborted.
|
||||||
//
|
//
|
||||||
// If auth is non-empty, authentication will be done with the first algorithm
|
// If auth is non-empty, authentication will be done with the first algorithm
|
||||||
// supported by the server. If none of the algorithms are supported, an error is
|
// supported by the server. If none of the algorithms are supported, an error is
|
||||||
|
@ -212,13 +227,14 @@ func New(ctx context.Context, log *mlog.Log, conn net.Conn, tlsMode TLSMode, ehl
|
||||||
if tlsMode == TLSStrictImmediate {
|
if tlsMode == TLSStrictImmediate {
|
||||||
// todo: we could also verify DANE here. not applicable to SMTP delivery.
|
// todo: we could also verify DANE here. not applicable to SMTP delivery.
|
||||||
config := c.tlsConfig(tlsMode)
|
config := c.tlsConfig(tlsMode)
|
||||||
tlsconn := tls.Client(conn, &config)
|
tlsconn := tls.Client(conn, config)
|
||||||
if err := tlsconn.HandshakeContext(ctx); err != nil {
|
if err := tlsconn.HandshakeContext(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
c.conn = tlsconn
|
c.conn = tlsconn
|
||||||
tlsversion, ciphersuite := mox.TLSInfo(tlsconn)
|
tlsversion, ciphersuite := mox.TLSInfo(tlsconn)
|
||||||
c.log.Debug("tls client handshake done", mlog.Field("tls", tlsversion), mlog.Field("ciphersuite", ciphersuite), mlog.Field("servername", remoteHostname))
|
c.log.Debug("tls client handshake done", mlog.Field("tls", tlsversion), mlog.Field("ciphersuite", ciphersuite), mlog.Field("servername", remoteHostname))
|
||||||
|
c.tls = true
|
||||||
} else {
|
} else {
|
||||||
c.conn = conn
|
c.conn = conn
|
||||||
}
|
}
|
||||||
|
@ -239,12 +255,26 @@ func New(ctx context.Context, log *mlog.Log, conn net.Conn, tlsMode TLSMode, ehl
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) tlsConfig(tlsMode TLSMode) tls.Config {
|
func (c *Client) tlsConfig(tlsMode TLSMode) *tls.Config {
|
||||||
if c.daneRecords != nil {
|
if c.daneRecords != nil {
|
||||||
return dane.TLSClientConfig(c.log, c.daneRecords, c.remoteHostname, c.moreRemoteHostnames, c.verifiedRecord)
|
config := dane.TLSClientConfig(c.log, c.daneRecords, c.remoteHostname, c.moreRemoteHostnames, c.verifiedRecord)
|
||||||
|
if tlsMode == TLSUnverifiedStartTLS {
|
||||||
|
// In case of delivery with header "TLS-Required: No", the connection should not be
|
||||||
|
// aborted.
|
||||||
|
origVerify := config.VerifyConnection
|
||||||
|
config.VerifyConnection = func(cs tls.ConnectionState) error {
|
||||||
|
err := origVerify(cs)
|
||||||
|
if err != nil {
|
||||||
|
c.log.Infox("verifying dane failed, continuing due to tls mode unverified starttls, due to tls-required-no message header", err)
|
||||||
|
metricTLSRequiredNoIgnored.WithLabelValues("daneverification").Inc()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &config
|
||||||
}
|
}
|
||||||
// todo: possibly accept older TLS versions for TLSOpportunistic?
|
// todo: possibly accept older TLS versions for TLSOpportunistic?
|
||||||
return tls.Config{
|
return &tls.Config{
|
||||||
ServerName: c.remoteHostname.ASCII,
|
ServerName: c.remoteHostname.ASCII,
|
||||||
RootCAs: mox.Conf.Static.TLS.CertPool,
|
RootCAs: mox.Conf.Static.TLS.CertPool,
|
||||||
InsecureSkipVerify: tlsMode == TLSOpportunistic || tlsMode == TLSUnverifiedStartTLS,
|
InsecureSkipVerify: tlsMode == TLSOpportunistic || tlsMode == TLSUnverifiedStartTLS,
|
||||||
|
@ -538,6 +568,8 @@ func (c *Client) hello(ctx context.Context, tlsMode TLSMode, ehloHostname dns.Do
|
||||||
c.ext8bitmime = true
|
c.ext8bitmime = true
|
||||||
case "PIPELINING":
|
case "PIPELINING":
|
||||||
c.extPipelining = true
|
c.extPipelining = true
|
||||||
|
case "REQUIRETLS":
|
||||||
|
c.extRequireTLS = true
|
||||||
default:
|
default:
|
||||||
// For SMTPUTF8 we must ignore any parameter. ../rfc/6531:207
|
// For SMTPUTF8 we must ignore any parameter. ../rfc/6531:207
|
||||||
if s == "SMTPUTF8" || strings.HasPrefix(s, "SMTPUTF8 ") {
|
if s == "SMTPUTF8" || strings.HasPrefix(s, "SMTPUTF8 ") {
|
||||||
|
@ -592,7 +624,7 @@ func (c *Client) hello(ctx context.Context, tlsMode TLSMode, ehloHostname dns.Do
|
||||||
// For TLSStrictStartTLS, the Go TLS library performs the checks needed for MTA-STS.
|
// For TLSStrictStartTLS, the Go TLS library performs the checks needed for MTA-STS.
|
||||||
// ../rfc/8461:646
|
// ../rfc/8461:646
|
||||||
tlsConfig := c.tlsConfig(tlsMode)
|
tlsConfig := c.tlsConfig(tlsMode)
|
||||||
nconn := tls.Client(conn, &tlsConfig)
|
nconn := tls.Client(conn, tlsConfig)
|
||||||
c.conn = nconn
|
c.conn = nconn
|
||||||
|
|
||||||
nctx, cancel := context.WithTimeout(ctx, time.Minute)
|
nctx, cancel := context.WithTimeout(ctx, time.Minute)
|
||||||
|
@ -609,6 +641,7 @@ func (c *Client) hello(ctx context.Context, tlsMode TLSMode, ehloHostname dns.Do
|
||||||
|
|
||||||
tlsversion, ciphersuite := mox.TLSInfo(nconn)
|
tlsversion, ciphersuite := mox.TLSInfo(nconn)
|
||||||
c.log.Debug("starttls client handshake done", mlog.Field("tlsmode", tlsMode), mlog.Field("tls", tlsversion), mlog.Field("ciphersuite", ciphersuite), mlog.Field("servername", c.remoteHostname), mlog.Field("danerecord", c.verifiedRecord))
|
c.log.Debug("starttls client handshake done", mlog.Field("tlsmode", tlsMode), mlog.Field("tls", tlsversion), mlog.Field("ciphersuite", ciphersuite), mlog.Field("servername", c.remoteHostname), mlog.Field("danerecord", c.verifiedRecord))
|
||||||
|
c.tls = true
|
||||||
|
|
||||||
hello(false)
|
hello(false)
|
||||||
}
|
}
|
||||||
|
@ -720,6 +753,24 @@ func (c *Client) SupportsSMTPUTF8() bool {
|
||||||
return c.extSMTPUTF8
|
return c.extSMTPUTF8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SupportsStartTLS returns whether the SMTP server supports the STARTTLS
|
||||||
|
// extension.
|
||||||
|
func (c *Client) SupportsStartTLS() bool {
|
||||||
|
return c.extStartTLS
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportsRequireTLS returns whether the SMTP server supports the REQUIRETLS
|
||||||
|
// extension. The REQUIRETLS extension is only announced after enabling
|
||||||
|
// STARTTLS.
|
||||||
|
func (c *Client) SupportsRequireTLS() bool {
|
||||||
|
return c.extRequireTLS
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLSEnabled returns whether TLS is enabled for this connection.
|
||||||
|
func (c *Client) TLSEnabled() bool {
|
||||||
|
return c.tls
|
||||||
|
}
|
||||||
|
|
||||||
// Deliver attempts to deliver a message to a mail server.
|
// Deliver attempts to deliver a message to a mail server.
|
||||||
//
|
//
|
||||||
// mailFrom must be an email address, or empty in case of a DSN. rcptTo must be
|
// mailFrom must be an email address, or empty in case of a DSN. rcptTo must be
|
||||||
|
@ -733,12 +784,15 @@ func (c *Client) SupportsSMTPUTF8() bool {
|
||||||
// character, or when UTF-8 is used in a localpart, reqSMTPUTF8 must be true. If set,
|
// character, or when UTF-8 is used in a localpart, reqSMTPUTF8 must be true. If set,
|
||||||
// the remote server must support the SMTPUTF8 extension or delivery will fail.
|
// the remote server must support the SMTPUTF8 extension or delivery will fail.
|
||||||
//
|
//
|
||||||
|
// If requireTLS is true, the remote server must support the REQUIRETLS
|
||||||
|
// extension, or delivery will fail.
|
||||||
|
//
|
||||||
// Deliver uses the following SMTP extensions if the remote server supports them:
|
// Deliver uses the following SMTP extensions if the remote server supports them:
|
||||||
// 8BITMIME, SMTPUTF8, SIZE, PIPELINING, ENHANCEDSTATUSCODES, STARTTLS.
|
// 8BITMIME, SMTPUTF8, SIZE, PIPELINING, ENHANCEDSTATUSCODES, STARTTLS.
|
||||||
//
|
//
|
||||||
// Returned errors can be of type Error, one of the Err-variables in this package
|
// Returned errors can be of type Error, one of the Err-variables in this package
|
||||||
// or other underlying errors, e.g. for i/o. Use errors.Is to check.
|
// or other underlying errors, e.g. for i/o. Use errors.Is to check.
|
||||||
func (c *Client) Deliver(ctx context.Context, mailFrom string, rcptTo string, msgSize int64, msg io.Reader, req8bitmime, reqSMTPUTF8 bool) (rerr error) {
|
func (c *Client) Deliver(ctx context.Context, mailFrom string, rcptTo string, msgSize int64, msg io.Reader, req8bitmime, reqSMTPUTF8, requireTLS bool) (rerr error) {
|
||||||
defer c.recover(&rerr)
|
defer c.recover(&rerr)
|
||||||
|
|
||||||
if c.origConn == nil {
|
if c.origConn == nil {
|
||||||
|
@ -761,6 +815,9 @@ func (c *Client) Deliver(ctx context.Context, mailFrom string, rcptTo string, ms
|
||||||
// ../rfc/6531:313
|
// ../rfc/6531:313
|
||||||
c.xerrorf(false, 0, "", "", "%w", ErrSMTPUTF8Unsupported)
|
c.xerrorf(false, 0, "", "", "%w", ErrSMTPUTF8Unsupported)
|
||||||
}
|
}
|
||||||
|
if !c.extRequireTLS && requireTLS {
|
||||||
|
c.xerrorf(false, 0, "", "", "%w", ErrRequireTLSUnsupported)
|
||||||
|
}
|
||||||
|
|
||||||
if c.extSize && msgSize > c.maxSize {
|
if c.extSize && 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, "", "", "%w: message is %d bytes, remote has a %d bytes maximum size", ErrSize, msgSize, c.maxSize)
|
||||||
|
@ -782,12 +839,17 @@ func (c *Client) Deliver(ctx context.Context, mailFrom string, rcptTo string, ms
|
||||||
// ../rfc/6531:213
|
// ../rfc/6531:213
|
||||||
smtputf8Arg = " SMTPUTF8"
|
smtputf8Arg = " SMTPUTF8"
|
||||||
}
|
}
|
||||||
|
var requiretlsArg string
|
||||||
|
if requireTLS {
|
||||||
|
// ../rfc/8689:155
|
||||||
|
requiretlsArg = " REQUIRETLS"
|
||||||
|
}
|
||||||
|
|
||||||
// Transaction overview: ../rfc/5321:1015
|
// Transaction overview: ../rfc/5321:1015
|
||||||
// MAIL FROM: ../rfc/5321:1879
|
// MAIL FROM: ../rfc/5321:1879
|
||||||
// RCPT TO: ../rfc/5321:1916
|
// RCPT TO: ../rfc/5321:1916
|
||||||
// DATA: ../rfc/5321:1992
|
// DATA: ../rfc/5321:1992
|
||||||
lineMailFrom := fmt.Sprintf("MAIL FROM:<%s>%s%s%s", mailFrom, mailSize, bodyType, smtputf8Arg)
|
lineMailFrom := fmt.Sprintf("MAIL FROM:<%s>%s%s%s%s", mailFrom, mailSize, bodyType, smtputf8Arg, requiretlsArg)
|
||||||
lineRcptTo := fmt.Sprintf("RCPT TO:<%s>", rcptTo)
|
lineRcptTo := fmt.Sprintf("RCPT TO:<%s>", rcptTo)
|
||||||
|
|
||||||
// We are going into a transaction. We'll clear this when done.
|
// We are going into a transaction. We'll clear this when done.
|
||||||
|
|
|
@ -36,6 +36,8 @@ func TestClient(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
log := mlog.New("smtpclient")
|
log := mlog.New("smtpclient")
|
||||||
|
|
||||||
|
mlog.SetConfig(map[string]mlog.Level{"": mlog.LevelTrace})
|
||||||
|
|
||||||
type options struct {
|
type options struct {
|
||||||
pipelining bool
|
pipelining bool
|
||||||
ecodes bool
|
ecodes bool
|
||||||
|
@ -43,13 +45,15 @@ func TestClient(t *testing.T) {
|
||||||
starttls bool
|
starttls bool
|
||||||
eightbitmime bool
|
eightbitmime bool
|
||||||
smtputf8 bool
|
smtputf8 bool
|
||||||
|
requiretls bool
|
||||||
ehlo bool
|
ehlo bool
|
||||||
|
|
||||||
tlsMode TLSMode
|
tlsMode TLSMode
|
||||||
tlsHostname dns.Domain
|
tlsHostname dns.Domain
|
||||||
need8bitmime bool
|
need8bitmime bool
|
||||||
needsmtputf8 bool
|
needsmtputf8 bool
|
||||||
auths []string // Allowed mechanisms.
|
needsrequiretls bool
|
||||||
|
auths []string // Allowed mechanisms.
|
||||||
|
|
||||||
nodeliver bool // For server, whether client will attempt a delivery.
|
nodeliver bool // For server, whether client will attempt a delivery.
|
||||||
}
|
}
|
||||||
|
@ -146,6 +150,9 @@ func TestClient(t *testing.T) {
|
||||||
if opts.smtputf8 {
|
if opts.smtputf8 {
|
||||||
writeline("250-SMTPUTF8")
|
writeline("250-SMTPUTF8")
|
||||||
}
|
}
|
||||||
|
if opts.requiretls && haveTLS {
|
||||||
|
writeline("250-REQUIRETLS")
|
||||||
|
}
|
||||||
if opts.auths != nil {
|
if opts.auths != nil {
|
||||||
writeline("250-AUTH " + strings.Join(opts.auths, " "))
|
writeline("250-AUTH " + strings.Join(opts.auths, " "))
|
||||||
}
|
}
|
||||||
|
@ -260,6 +267,7 @@ func TestClient(t *testing.T) {
|
||||||
result <- nil
|
result <- nil
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// todo: should abort tests more properly. on client failures, we may be left with hanging test.
|
||||||
go func() {
|
go func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
x := recover()
|
x := recover()
|
||||||
|
@ -268,7 +276,8 @@ func TestClient(t *testing.T) {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
fail := func(format string, args ...any) {
|
fail := func(format string, args ...any) {
|
||||||
result <- fmt.Errorf("client: %w", fmt.Errorf(format, args...))
|
err := fmt.Errorf("client: %w", fmt.Errorf(format, args...))
|
||||||
|
result <- err
|
||||||
panic("stop")
|
panic("stop")
|
||||||
}
|
}
|
||||||
c, err := New(ctx, log, clientConn, opts.tlsMode, localhost, opts.tlsHostname, auths, nil, nil, nil)
|
c, err := New(ctx, log, clientConn, opts.tlsMode, localhost, opts.tlsHostname, auths, nil, nil, nil)
|
||||||
|
@ -279,7 +288,7 @@ func TestClient(t *testing.T) {
|
||||||
result <- nil
|
result <- nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = c.Deliver(ctx, "postmaster@mox.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), opts.need8bitmime, opts.needsmtputf8)
|
err = c.Deliver(ctx, "postmaster@mox.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), opts.need8bitmime, opts.needsmtputf8, opts.needsrequiretls)
|
||||||
if (err == nil) != (expDeliverErr == nil) || err != nil && !errors.Is(err, expDeliverErr) {
|
if (err == nil) != (expDeliverErr == nil) || err != nil && !errors.Is(err, expDeliverErr) {
|
||||||
fail("first deliver: got err %v, expected %v", err, expDeliverErr)
|
fail("first deliver: got err %v, expected %v", err, expDeliverErr)
|
||||||
}
|
}
|
||||||
|
@ -288,7 +297,7 @@ func TestClient(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail("reset: %v", err)
|
fail("reset: %v", err)
|
||||||
}
|
}
|
||||||
err = c.Deliver(ctx, "postmaster@mox.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), opts.need8bitmime, opts.needsmtputf8)
|
err = c.Deliver(ctx, "postmaster@mox.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), opts.need8bitmime, opts.needsmtputf8, opts.needsrequiretls)
|
||||||
if (err == nil) != (expDeliverErr == nil) || err != nil && !errors.Is(err, expDeliverErr) {
|
if (err == nil) != (expDeliverErr == nil) || err != nil && !errors.Is(err, expDeliverErr) {
|
||||||
fail("second deliver: got err %v, expected %v", err, expDeliverErr)
|
fail("second deliver: got err %v, expected %v", err, expDeliverErr)
|
||||||
}
|
}
|
||||||
|
@ -327,11 +336,13 @@ test
|
||||||
smtputf8: true,
|
smtputf8: true,
|
||||||
starttls: true,
|
starttls: true,
|
||||||
ehlo: true,
|
ehlo: true,
|
||||||
|
requiretls: true,
|
||||||
|
|
||||||
tlsMode: TLSStrictStartTLS,
|
tlsMode: TLSStrictStartTLS,
|
||||||
tlsHostname: dns.Domain{ASCII: "mox.example"},
|
tlsHostname: dns.Domain{ASCII: "mox.example"},
|
||||||
need8bitmime: true,
|
need8bitmime: true,
|
||||||
needsmtputf8: true,
|
needsmtputf8: true,
|
||||||
|
needsrequiretls: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
test(msg, options{}, nil, nil, nil, nil)
|
test(msg, options{}, nil, nil, nil, nil)
|
||||||
|
@ -346,6 +357,7 @@ test
|
||||||
test(msg, options{ehlo: true, auths: []string{"SCRAM-SHA-1"}}, []sasl.Client{sasl.NewClientSCRAMSHA1("test", "test")}, nil, nil, nil)
|
test(msg, options{ehlo: true, auths: []string{"SCRAM-SHA-1"}}, []sasl.Client{sasl.NewClientSCRAMSHA1("test", "test")}, nil, nil, nil)
|
||||||
test(msg, options{ehlo: true, auths: []string{"SCRAM-SHA-256"}}, []sasl.Client{sasl.NewClientSCRAMSHA256("test", "test")}, nil, nil, nil)
|
test(msg, options{ehlo: true, auths: []string{"SCRAM-SHA-256"}}, []sasl.Client{sasl.NewClientSCRAMSHA256("test", "test")}, nil, nil, nil)
|
||||||
// todo: add tests for failing authentication, also at various stages in SCRAM
|
// todo: add tests for failing authentication, also at various stages in SCRAM
|
||||||
|
test(msg, options{ehlo: true, requiretls: false, needsrequiretls: true, nodeliver: true}, nil, nil, ErrRequireTLSUnsupported, nil)
|
||||||
|
|
||||||
// Set an expired certificate. For non-strict TLS, we should still accept it.
|
// Set an expired certificate. For non-strict TLS, we should still accept it.
|
||||||
// ../rfc/7435:424
|
// ../rfc/7435:424
|
||||||
|
@ -441,7 +453,7 @@ func TestErrors(t *testing.T) {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
msg := ""
|
msg := ""
|
||||||
err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false)
|
err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
|
||||||
var xerr Error
|
var xerr Error
|
||||||
if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || !xerr.Permanent {
|
if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || !xerr.Permanent {
|
||||||
panic(fmt.Errorf("got %#v, expected ErrStatus with Permanent", err))
|
panic(fmt.Errorf("got %#v, expected ErrStatus with Permanent", err))
|
||||||
|
@ -461,7 +473,7 @@ func TestErrors(t *testing.T) {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
msg := ""
|
msg := ""
|
||||||
err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false)
|
err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
|
||||||
var xerr Error
|
var xerr Error
|
||||||
if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || xerr.Permanent {
|
if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || xerr.Permanent {
|
||||||
panic(fmt.Errorf("got %#v, expected ErrStatus with not-Permanent", err))
|
panic(fmt.Errorf("got %#v, expected ErrStatus with not-Permanent", err))
|
||||||
|
@ -483,7 +495,7 @@ func TestErrors(t *testing.T) {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
msg := ""
|
msg := ""
|
||||||
err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false)
|
err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
|
||||||
var xerr Error
|
var xerr Error
|
||||||
if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || xerr.Permanent {
|
if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || xerr.Permanent {
|
||||||
panic(fmt.Errorf("got %#v, expected ErrStatus with not-Permanent", err))
|
panic(fmt.Errorf("got %#v, expected ErrStatus with not-Permanent", err))
|
||||||
|
@ -507,7 +519,7 @@ func TestErrors(t *testing.T) {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
msg := ""
|
msg := ""
|
||||||
err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false)
|
err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
|
||||||
var xerr Error
|
var xerr Error
|
||||||
if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || !xerr.Permanent {
|
if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || !xerr.Permanent {
|
||||||
panic(fmt.Errorf("got %#v, expected ErrStatus with Permanent", err))
|
panic(fmt.Errorf("got %#v, expected ErrStatus with Permanent", err))
|
||||||
|
@ -543,7 +555,7 @@ func TestErrors(t *testing.T) {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
msg := ""
|
msg := ""
|
||||||
err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false)
|
err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
|
||||||
var xerr Error
|
var xerr Error
|
||||||
if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || xerr.Permanent {
|
if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || xerr.Permanent {
|
||||||
panic(fmt.Errorf("got %#v, expected ErrStatus with non-Permanent", err))
|
panic(fmt.Errorf("got %#v, expected ErrStatus with non-Permanent", err))
|
||||||
|
@ -574,14 +586,14 @@ func TestErrors(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := ""
|
msg := ""
|
||||||
err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false)
|
err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
|
||||||
var xerr Error
|
var xerr Error
|
||||||
if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || xerr.Permanent {
|
if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || xerr.Permanent {
|
||||||
panic(fmt.Errorf("got %#v, expected ErrStatus with non-Permanent", err))
|
panic(fmt.Errorf("got %#v, expected ErrStatus with non-Permanent", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Another delivery.
|
// Another delivery.
|
||||||
err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false)
|
err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
|
||||||
if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || !xerr.Permanent {
|
if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || !xerr.Permanent {
|
||||||
panic(fmt.Errorf("got %#v, expected ErrStatus with Permanent", err))
|
panic(fmt.Errorf("got %#v, expected ErrStatus with Permanent", err))
|
||||||
}
|
}
|
||||||
|
@ -604,7 +616,7 @@ func TestErrors(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := ""
|
msg := ""
|
||||||
err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false)
|
err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false)
|
||||||
var xerr Error
|
var xerr Error
|
||||||
if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || !xerr.Permanent {
|
if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || !xerr.Permanent {
|
||||||
panic(fmt.Errorf("got %#v, expected ErrStatus with Permanent", err))
|
panic(fmt.Errorf("got %#v, expected ErrStatus with Permanent", err))
|
||||||
|
|
|
@ -13,7 +13,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// compose dsn message and add it to the queue for delivery to rcptTo.
|
// compose dsn message and add it to the queue for delivery to rcptTo.
|
||||||
func queueDSN(ctx context.Context, c *conn, rcptTo smtp.Path, m dsn.Message) error {
|
func queueDSN(ctx context.Context, c *conn, rcptTo smtp.Path, m dsn.Message, requireTLS bool) error {
|
||||||
buf, err := m.Compose(c.log, false)
|
buf, err := m.Compose(c.log, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -46,7 +46,11 @@ func queueDSN(ctx context.Context, c *conn, rcptTo smtp.Path, m dsn.Message) err
|
||||||
// ../rfc/3464:433
|
// ../rfc/3464:433
|
||||||
const has8bit = false
|
const has8bit = false
|
||||||
const smtputf8 = false
|
const smtputf8 = false
|
||||||
if _, err := queue.Add(ctx, c.log, "", smtp.Path{}, rcptTo, has8bit, smtputf8, int64(len(buf)), m.MessageID, nil, f, bufUTF8); err != nil {
|
var reqTLS *bool
|
||||||
|
if requireTLS {
|
||||||
|
reqTLS = &requireTLS
|
||||||
|
}
|
||||||
|
if _, err := queue.Add(ctx, c.log, "", smtp.Path{}, rcptTo, has8bit, smtputf8, int64(len(buf)), m.MessageID, nil, f, bufUTF8, reqTLS); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -100,7 +100,7 @@ func FuzzServer(f *testing.F) {
|
||||||
const submission = false
|
const submission = false
|
||||||
err := serverConn.SetDeadline(time.Now().Add(time.Second))
|
err := serverConn.SetDeadline(time.Now().Add(time.Second))
|
||||||
flog(err, "set server deadline")
|
flog(err, "set server deadline")
|
||||||
serve("test", cid, dns.Domain{ASCII: "mox.example"}, nil, serverConn, resolver, submission, false, 100<<10, false, false, nil, 0)
|
serve("test", cid, dns.Domain{ASCII: "mox.example"}, nil, serverConn, resolver, submission, false, 100<<10, false, false, false, nil, 0)
|
||||||
cid++
|
cid++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
|
"net/textproto"
|
||||||
"os"
|
"os"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -201,7 +202,7 @@ func Listen() {
|
||||||
port := config.Port(listener.SMTP.Port, 25)
|
port := config.Port(listener.SMTP.Port, 25)
|
||||||
for _, ip := range listener.IPs {
|
for _, ip := range listener.IPs {
|
||||||
firstTimeSenderDelay := durationDefault(listener.SMTP.FirstTimeSenderDelay, firstTimeSenderDelayDefault)
|
firstTimeSenderDelay := durationDefault(listener.SMTP.FirstTimeSenderDelay, firstTimeSenderDelayDefault)
|
||||||
listen1("smtp", name, ip, port, hostname, tlsConfig, false, false, maxMsgSize, false, listener.SMTP.RequireSTARTTLS, listener.SMTP.DNSBLZones, firstTimeSenderDelay)
|
listen1("smtp", name, ip, port, hostname, tlsConfig, false, false, maxMsgSize, false, listener.SMTP.RequireSTARTTLS, !listener.SMTP.NoRequireTLS, listener.SMTP.DNSBLZones, firstTimeSenderDelay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if listener.Submission.Enabled {
|
if listener.Submission.Enabled {
|
||||||
|
@ -211,7 +212,7 @@ func Listen() {
|
||||||
}
|
}
|
||||||
port := config.Port(listener.Submission.Port, 587)
|
port := config.Port(listener.Submission.Port, 587)
|
||||||
for _, ip := range listener.IPs {
|
for _, ip := range listener.IPs {
|
||||||
listen1("submission", name, ip, port, hostname, tlsConfig, true, false, maxMsgSize, !listener.Submission.NoRequireSTARTTLS, !listener.Submission.NoRequireSTARTTLS, nil, 0)
|
listen1("submission", name, ip, port, hostname, tlsConfig, true, false, maxMsgSize, !listener.Submission.NoRequireSTARTTLS, !listener.Submission.NoRequireSTARTTLS, true, nil, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,7 +223,7 @@ func Listen() {
|
||||||
}
|
}
|
||||||
port := config.Port(listener.Submissions.Port, 465)
|
port := config.Port(listener.Submissions.Port, 465)
|
||||||
for _, ip := range listener.IPs {
|
for _, ip := range listener.IPs {
|
||||||
listen1("submissions", name, ip, port, hostname, tlsConfig, true, true, maxMsgSize, true, true, nil, 0)
|
listen1("submissions", name, ip, port, hostname, tlsConfig, true, true, maxMsgSize, true, true, true, nil, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -230,7 +231,7 @@ func Listen() {
|
||||||
|
|
||||||
var servers []func()
|
var servers []func()
|
||||||
|
|
||||||
func listen1(protocol, name, ip string, port int, hostname dns.Domain, tlsConfig *tls.Config, submission, xtls bool, maxMessageSize int64, requireTLSForAuth, requireTLSForDelivery bool, dnsBLs []dns.Domain, firstTimeSenderDelay time.Duration) {
|
func listen1(protocol, name, ip string, port int, hostname dns.Domain, tlsConfig *tls.Config, submission, xtls bool, maxMessageSize int64, requireTLSForAuth, requireTLSForDelivery, requireTLS bool, dnsBLs []dns.Domain, firstTimeSenderDelay time.Duration) {
|
||||||
addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
|
addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
|
||||||
if os.Getuid() == 0 {
|
if os.Getuid() == 0 {
|
||||||
xlog.Print("listening for smtp", mlog.Field("listener", name), mlog.Field("address", addr), mlog.Field("protocol", protocol))
|
xlog.Print("listening for smtp", mlog.Field("listener", name), mlog.Field("address", addr), mlog.Field("protocol", protocol))
|
||||||
|
@ -252,7 +253,7 @@ func listen1(protocol, name, ip string, port int, hostname dns.Domain, tlsConfig
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
resolver := dns.StrictResolver{} // By leaving Pkg empty, it'll be set by each package that uses the resolver, e.g. spf/dkim/dmarc.
|
resolver := dns.StrictResolver{} // By leaving Pkg empty, it'll be set by each package that uses the resolver, e.g. spf/dkim/dmarc.
|
||||||
go serve(name, mox.Cid(), hostname, tlsConfig, conn, resolver, submission, xtls, maxMessageSize, requireTLSForAuth, requireTLSForDelivery, dnsBLs, firstTimeSenderDelay)
|
go serve(name, mox.Cid(), hostname, tlsConfig, conn, resolver, submission, xtls, maxMessageSize, requireTLSForAuth, requireTLSForDelivery, requireTLS, dnsBLs, firstTimeSenderDelay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,6 +278,7 @@ type conn struct {
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
|
|
||||||
tls bool
|
tls bool
|
||||||
|
extRequireTLS bool // Whether to announce and allow the REQUIRETLS extension.
|
||||||
resolver dns.Resolver
|
resolver dns.Resolver
|
||||||
r *bufio.Reader
|
r *bufio.Reader
|
||||||
w *bufio.Writer
|
w *bufio.Writer
|
||||||
|
@ -316,8 +318,9 @@ type conn struct {
|
||||||
|
|
||||||
// Message transaction.
|
// Message transaction.
|
||||||
mailFrom *smtp.Path
|
mailFrom *smtp.Path
|
||||||
has8bitmime bool // If MAIL FROM parameter BODY=8BITMIME was sent. Required for SMTPUTF8.
|
requireTLS *bool // MAIL FROM with REQUIRETLS set.
|
||||||
smtputf8 bool // todo future: we should keep track of this per recipient. perhaps only a specific recipient requires smtputf8, e.g. due to a utf8 localpart. we should decide ourselves if the message needs smtputf8, e.g. due to utf8 header values.
|
has8bitmime bool // If MAIL FROM parameter BODY=8BITMIME was sent. Required for SMTPUTF8.
|
||||||
|
smtputf8 bool // todo future: we should keep track of this per recipient. perhaps only a specific recipient requires smtputf8, e.g. due to a utf8 localpart. we should decide ourselves if the message needs smtputf8, e.g. due to utf8 header values.
|
||||||
recipients []rcptAccount
|
recipients []rcptAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -353,6 +356,7 @@ func (c *conn) reset() {
|
||||||
// ../rfc/5321:2502
|
// ../rfc/5321:2502
|
||||||
func (c *conn) rset() {
|
func (c *conn) rset() {
|
||||||
c.mailFrom = nil
|
c.mailFrom = nil
|
||||||
|
c.requireTLS = nil
|
||||||
c.has8bitmime = false
|
c.has8bitmime = false
|
||||||
c.smtputf8 = false
|
c.smtputf8 = false
|
||||||
c.recipients = nil
|
c.recipients = nil
|
||||||
|
@ -524,7 +528,7 @@ func (c *conn) writelinef(format string, args ...any) {
|
||||||
|
|
||||||
var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
|
var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
|
||||||
|
|
||||||
func serve(listenerName string, cid int64, hostname dns.Domain, tlsConfig *tls.Config, nc net.Conn, resolver dns.Resolver, submission, tls bool, maxMessageSize int64, requireTLSForAuth, requireTLSForDelivery bool, dnsBLs []dns.Domain, firstTimeSenderDelay time.Duration) {
|
func serve(listenerName string, cid int64, hostname dns.Domain, tlsConfig *tls.Config, nc net.Conn, resolver dns.Resolver, submission, tls bool, maxMessageSize int64, requireTLSForAuth, requireTLSForDelivery, requireTLS bool, dnsBLs []dns.Domain, firstTimeSenderDelay time.Duration) {
|
||||||
var localIP, remoteIP net.IP
|
var localIP, remoteIP net.IP
|
||||||
if a, ok := nc.LocalAddr().(*net.TCPAddr); ok {
|
if a, ok := nc.LocalAddr().(*net.TCPAddr); ok {
|
||||||
localIP = a.IP
|
localIP = a.IP
|
||||||
|
@ -545,6 +549,7 @@ func serve(listenerName string, cid int64, hostname dns.Domain, tlsConfig *tls.C
|
||||||
conn: nc,
|
conn: nc,
|
||||||
submission: submission,
|
submission: submission,
|
||||||
tls: tls,
|
tls: tls,
|
||||||
|
extRequireTLS: requireTLS,
|
||||||
resolver: resolver,
|
resolver: resolver,
|
||||||
lastlog: time.Now(),
|
lastlog: time.Now(),
|
||||||
tlsConfig: tlsConfig,
|
tlsConfig: tlsConfig,
|
||||||
|
@ -821,6 +826,10 @@ func (c *conn) cmdHello(p *parser, ehlo bool) {
|
||||||
if !c.tls && c.tlsConfig != nil {
|
if !c.tls && c.tlsConfig != nil {
|
||||||
// ../rfc/3207:90
|
// ../rfc/3207:90
|
||||||
c.bwritelinef("250-STARTTLS")
|
c.bwritelinef("250-STARTTLS")
|
||||||
|
} else if c.extRequireTLS {
|
||||||
|
// ../rfc/8689:202
|
||||||
|
// ../rfc/8689:143
|
||||||
|
c.bwritelinef("250-REQUIRETLS")
|
||||||
}
|
}
|
||||||
if c.submission {
|
if c.submission {
|
||||||
// ../rfc/4954:123
|
// ../rfc/4954:123
|
||||||
|
@ -1285,6 +1294,15 @@ func (c *conn) cmdMail(p *parser) {
|
||||||
case "SMTPUTF8":
|
case "SMTPUTF8":
|
||||||
// ../rfc/6531:213
|
// ../rfc/6531:213
|
||||||
c.smtputf8 = true
|
c.smtputf8 = true
|
||||||
|
case "REQUIRETLS":
|
||||||
|
// ../rfc/8689:155
|
||||||
|
if !c.tls {
|
||||||
|
xsmtpUserErrorf(smtp.C530SecurityRequired, smtp.SePol7EncNeeded10, "requiretls only allowed on tls-encrypted connections")
|
||||||
|
} else if !c.extRequireTLS {
|
||||||
|
xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "REQUIRETLS not allowed for this connection")
|
||||||
|
}
|
||||||
|
v := true
|
||||||
|
c.requireTLS = &v
|
||||||
default:
|
default:
|
||||||
// ../rfc/5321:2230
|
// ../rfc/5321:2230
|
||||||
xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
|
xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
|
||||||
|
@ -1645,7 +1663,12 @@ func (c *conn) cmdData(p *parser) {
|
||||||
recvHdr := &message.HeaderWriter{}
|
recvHdr := &message.HeaderWriter{}
|
||||||
// For additional Received-header clauses, see:
|
// For additional Received-header clauses, see:
|
||||||
// https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml#table-mail-parameters-8
|
// https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml#table-mail-parameters-8
|
||||||
recvHdr.Add(" ", "Received:", "from", recvFrom, "by", recvBy, "via", "tcp", "with", with, "id", mox.ReceivedID(c.cid)) // ../rfc/5321:3158
|
withComment := ""
|
||||||
|
if c.requireTLS != nil && *c.requireTLS {
|
||||||
|
// Comment is actually part of ID ABNF rule. ../rfc/5321:3336
|
||||||
|
withComment = " (requiretls)"
|
||||||
|
}
|
||||||
|
recvHdr.Add(" ", "Received:", "from", recvFrom, "by", recvBy, "via", "tcp", "with", with+withComment, "id", mox.ReceivedID(c.cid)) // ../rfc/5321:3158
|
||||||
if c.tls {
|
if c.tls {
|
||||||
tlsConn := c.conn.(*tls.Conn)
|
tlsConn := c.conn.(*tls.Conn)
|
||||||
tlsComment := message.TLSReceivedComment(c.log, tlsConn.ConnectionState())
|
tlsComment := message.TLSReceivedComment(c.log, tlsConn.ConnectionState())
|
||||||
|
@ -1665,6 +1688,23 @@ func (c *conn) cmdData(p *parser) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if a message has unambiguous "TLS-Required: No" header. Messages must not
|
||||||
|
// contain multiple TLS-Required headers. The only valid value is "no". But we'll
|
||||||
|
// accept multiple headers as long as all they are all "no".
|
||||||
|
// ../rfc/8689:223
|
||||||
|
func hasTLSRequiredNo(h textproto.MIMEHeader) bool {
|
||||||
|
l := h.Values("Tls-Required")
|
||||||
|
if len(l) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, v := range l {
|
||||||
|
if !strings.EqualFold(v, "no") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// submit is used for mail from authenticated users that we will try to deliver.
|
// submit is used for mail from authenticated users that we will try to deliver.
|
||||||
func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, dataFile *os.File) {
|
func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, dataFile *os.File) {
|
||||||
// Similar between ../smtpserver/server.go:/submit\( and ../webmail/webmail.go:/MessageSubmit\(
|
// Similar between ../smtpserver/server.go:/submit\( and ../webmail/webmail.go:/MessageSubmit\(
|
||||||
|
@ -1692,6 +1732,14 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
|
||||||
xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "must match authenticated user")
|
xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SePol7DeliveryUnauth1, "must match authenticated user")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TLS-Required: No header makes us not enforce recipient domain's TLS policy.
|
||||||
|
// ../rfc/8689:206
|
||||||
|
// Only when requiretls smtp extension wasn't used. ../rfc/8689:246
|
||||||
|
if c.requireTLS == nil && hasTLSRequiredNo(header) {
|
||||||
|
v := false
|
||||||
|
c.requireTLS = &v
|
||||||
|
}
|
||||||
|
|
||||||
// Outgoing messages should not have a Return-Path header. The final receiving mail
|
// Outgoing messages should not have a Return-Path header. The final receiving mail
|
||||||
// server will add it.
|
// server will add it.
|
||||||
// ../rfc/5321:3233
|
// ../rfc/5321:3233
|
||||||
|
@ -1787,7 +1835,7 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
|
||||||
xmsgPrefix := append([]byte(recvHdrFor(rcptAcc.rcptTo.String())), msgPrefix...)
|
xmsgPrefix := append([]byte(recvHdrFor(rcptAcc.rcptTo.String())), msgPrefix...)
|
||||||
|
|
||||||
msgSize := int64(len(xmsgPrefix)) + msgWriter.Size
|
msgSize := int64(len(xmsgPrefix)) + msgWriter.Size
|
||||||
if _, err := queue.Add(ctx, c.log, c.account.Name, *c.mailFrom, rcptAcc.rcptTo, msgWriter.Has8bit, c.smtputf8, msgSize, messageID, xmsgPrefix, dataFile, nil); err != nil {
|
if _, err := queue.Add(ctx, c.log, c.account.Name, *c.mailFrom, rcptAcc.rcptTo, msgWriter.Has8bit, c.smtputf8, msgSize, messageID, xmsgPrefix, dataFile, nil, c.requireTLS); err != nil {
|
||||||
// Aborting the transaction is not great. But continuing and generating DSNs will
|
// Aborting the transaction is not great. But continuing and generating DSNs will
|
||||||
// probably result in errors as well...
|
// probably result in errors as well...
|
||||||
metricSubmission.WithLabelValues("queueerror").Inc()
|
metricSubmission.WithLabelValues("queueerror").Inc()
|
||||||
|
@ -1872,6 +1920,16 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
|
||||||
xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeNet4Loop6, "loop detected, more than 100 Received headers")
|
xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeNet4Loop6, "loop detected, more than 100 Received headers")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TLS-Required: No header makes us not enforce recipient domain's TLS policy.
|
||||||
|
// Since we only deliver locally at the moment, this won't influence our behaviour.
|
||||||
|
// Once we forward, it would our delivery attempts.
|
||||||
|
// ../rfc/8689:206
|
||||||
|
// Only when requiretls smtp extension wasn't used. ../rfc/8689:246
|
||||||
|
if c.requireTLS == nil && hasTLSRequiredNo(headers) {
|
||||||
|
v := false
|
||||||
|
c.requireTLS = &v
|
||||||
|
}
|
||||||
|
|
||||||
// We'll be building up an Authentication-Results header.
|
// We'll be building up an Authentication-Results header.
|
||||||
authResults := message.AuthResults{
|
authResults := message.AuthResults{
|
||||||
Hostname: mox.Conf.Static.HostnameDomain.XName(c.smtputf8),
|
Hostname: mox.Conf.Static.HostnameDomain.XName(c.smtputf8),
|
||||||
|
@ -2547,7 +2605,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
|
||||||
|
|
||||||
if Localserve {
|
if Localserve {
|
||||||
c.log.Error("not queueing dsn for incoming delivery due to localserve")
|
c.log.Error("not queueing dsn for incoming delivery due to localserve")
|
||||||
} else if err := queueDSN(context.TODO(), c, *c.mailFrom, dsnMsg); err != nil {
|
} else if err := queueDSN(context.TODO(), c, *c.mailFrom, dsnMsg, c.requireTLS != nil && *c.requireTLS); err != nil {
|
||||||
metricServerErrors.WithLabelValues("queuedsn").Inc()
|
metricServerErrors.WithLabelValues("queuedsn").Inc()
|
||||||
c.log.Errorx("queuing DSN for incoming delivery, no DSN sent", err)
|
c.log.Errorx("queuing DSN for incoming delivery, no DSN sent", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,6 +90,7 @@ type testserver struct {
|
||||||
auth []sasl.Client
|
auth []sasl.Client
|
||||||
user, pass string
|
user, pass string
|
||||||
submission bool
|
submission bool
|
||||||
|
requiretls bool
|
||||||
dnsbls []dns.Domain
|
dnsbls []dns.Domain
|
||||||
tlsmode smtpclient.TLSMode
|
tlsmode smtpclient.TLSMode
|
||||||
}
|
}
|
||||||
|
@ -145,7 +146,7 @@ func (ts *testserver) run(fn func(helloErr error, client *smtpclient.Client)) {
|
||||||
tlsConfig := &tls.Config{
|
tlsConfig := &tls.Config{
|
||||||
Certificates: []tls.Certificate{fakeCert(ts.t)},
|
Certificates: []tls.Certificate{fakeCert(ts.t)},
|
||||||
}
|
}
|
||||||
serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, tlsConfig, serverConn, ts.resolver, ts.submission, false, 100<<20, false, false, ts.dnsbls, 0)
|
serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, tlsConfig, serverConn, ts.resolver, ts.submission, false, 100<<20, false, false, ts.requiretls, ts.dnsbls, 0)
|
||||||
close(serverdone)
|
close(serverdone)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -222,7 +223,7 @@ func TestSubmission(t *testing.T) {
|
||||||
mailFrom := "mjl@mox.example"
|
mailFrom := "mjl@mox.example"
|
||||||
rcptTo := "remote@example.org"
|
rcptTo := "remote@example.org"
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
|
||||||
}
|
}
|
||||||
var cerr smtpclient.Error
|
var cerr smtpclient.Error
|
||||||
if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Secode != expErr.Secode) {
|
if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Secode != expErr.Secode) {
|
||||||
|
@ -261,7 +262,7 @@ func TestDelivery(t *testing.T) {
|
||||||
mailFrom := "remote@example.org"
|
mailFrom := "remote@example.org"
|
||||||
rcptTo := "mjl@127.0.0.10"
|
rcptTo := "mjl@127.0.0.10"
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
|
||||||
}
|
}
|
||||||
var cerr smtpclient.Error
|
var cerr smtpclient.Error
|
||||||
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
|
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
|
||||||
|
@ -273,7 +274,7 @@ func TestDelivery(t *testing.T) {
|
||||||
mailFrom := "remote@example.org"
|
mailFrom := "remote@example.org"
|
||||||
rcptTo := "mjl@test.example" // Not configured as destination.
|
rcptTo := "mjl@test.example" // Not configured as destination.
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
|
||||||
}
|
}
|
||||||
var cerr smtpclient.Error
|
var cerr smtpclient.Error
|
||||||
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
|
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
|
||||||
|
@ -285,7 +286,7 @@ func TestDelivery(t *testing.T) {
|
||||||
mailFrom := "remote@example.org"
|
mailFrom := "remote@example.org"
|
||||||
rcptTo := "unknown@mox.example" // User unknown.
|
rcptTo := "unknown@mox.example" // User unknown.
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
|
||||||
}
|
}
|
||||||
var cerr smtpclient.Error
|
var cerr smtpclient.Error
|
||||||
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
|
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
|
||||||
|
@ -297,7 +298,7 @@ func TestDelivery(t *testing.T) {
|
||||||
mailFrom := "remote@example.org"
|
mailFrom := "remote@example.org"
|
||||||
rcptTo := "mjl@mox.example"
|
rcptTo := "mjl@mox.example"
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
|
||||||
}
|
}
|
||||||
var cerr smtpclient.Error
|
var cerr smtpclient.Error
|
||||||
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
|
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
|
||||||
|
@ -311,7 +312,7 @@ func TestDelivery(t *testing.T) {
|
||||||
mailFrom := "remote@example.org"
|
mailFrom := "remote@example.org"
|
||||||
rcptTo := "mjl@mox.example"
|
rcptTo := "mjl@mox.example"
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
|
||||||
}
|
}
|
||||||
tcheck(t, err, "deliver to remote")
|
tcheck(t, err, "deliver to remote")
|
||||||
|
|
||||||
|
@ -442,7 +443,7 @@ func TestSpam(t *testing.T) {
|
||||||
mailFrom := "remote@example.org"
|
mailFrom := "remote@example.org"
|
||||||
rcptTo := "mjl@mox.example"
|
rcptTo := "mjl@mox.example"
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
|
||||||
}
|
}
|
||||||
var cerr smtpclient.Error
|
var cerr smtpclient.Error
|
||||||
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
|
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
|
||||||
|
@ -458,7 +459,7 @@ func TestSpam(t *testing.T) {
|
||||||
mailFrom := "remote@example.org"
|
mailFrom := "remote@example.org"
|
||||||
rcptTo := "mjl2@mox.example"
|
rcptTo := "mjl2@mox.example"
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage2)), strings.NewReader(deliverMessage2), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage2)), strings.NewReader(deliverMessage2), false, false, false)
|
||||||
}
|
}
|
||||||
tcheck(t, err, "deliver")
|
tcheck(t, err, "deliver")
|
||||||
|
|
||||||
|
@ -477,7 +478,7 @@ func TestSpam(t *testing.T) {
|
||||||
mailFrom := "remote@example.org"
|
mailFrom := "remote@example.org"
|
||||||
rcptTo := "mjl@mox.example"
|
rcptTo := "mjl@mox.example"
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
|
||||||
}
|
}
|
||||||
tcheck(t, err, "deliver")
|
tcheck(t, err, "deliver")
|
||||||
|
|
||||||
|
@ -499,7 +500,7 @@ func TestSpam(t *testing.T) {
|
||||||
mailFrom := "remote@example.org"
|
mailFrom := "remote@example.org"
|
||||||
rcptTo := "mjl@mox.example"
|
rcptTo := "mjl@mox.example"
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
|
||||||
}
|
}
|
||||||
var cerr smtpclient.Error
|
var cerr smtpclient.Error
|
||||||
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
|
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
|
||||||
|
@ -576,7 +577,7 @@ happens to come from forwarding mail server.
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgBad)), strings.NewReader(msgBad), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgBad)), strings.NewReader(msgBad), false, false, false)
|
||||||
tcheck(t, err, "deliver message")
|
tcheck(t, err, "deliver message")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -585,7 +586,7 @@ happens to come from forwarding mail server.
|
||||||
tcompare(t, n, 10)
|
tcompare(t, n, 10)
|
||||||
|
|
||||||
// Next delivery will fail, with negative "message From" signal.
|
// Next delivery will fail, with negative "message From" signal.
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgBad)), strings.NewReader(msgBad), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgBad)), strings.NewReader(msgBad), false, false, false)
|
||||||
var cerr smtpclient.Error
|
var cerr smtpclient.Error
|
||||||
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
|
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
|
||||||
t.Fatalf("delivery by bad sender, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
|
t.Fatalf("delivery by bad sender, got err %v, expected smtpclient.Error with code %d", err, smtp.C451LocalErr)
|
||||||
|
@ -603,7 +604,7 @@ happens to come from forwarding mail server.
|
||||||
mailFrom = "remote@good.example"
|
mailFrom = "remote@good.example"
|
||||||
}
|
}
|
||||||
|
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgOK)), strings.NewReader(msgOK), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgOK)), strings.NewReader(msgOK), false, false, false)
|
||||||
if forward {
|
if forward {
|
||||||
tcheck(t, err, "deliver")
|
tcheck(t, err, "deliver")
|
||||||
} else {
|
} else {
|
||||||
|
@ -620,7 +621,7 @@ happens to come from forwarding mail server.
|
||||||
|
|
||||||
mailFrom := "other@forward.example"
|
mailFrom := "other@forward.example"
|
||||||
|
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgOK2)), strings.NewReader(msgOK2), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msgOK2)), strings.NewReader(msgOK2), false, false, false)
|
||||||
if forward {
|
if forward {
|
||||||
tcheck(t, err, "deliver")
|
tcheck(t, err, "deliver")
|
||||||
} else {
|
} else {
|
||||||
|
@ -669,7 +670,7 @@ func TestDMARCSent(t *testing.T) {
|
||||||
mailFrom := "remote@example.org"
|
mailFrom := "remote@example.org"
|
||||||
rcptTo := "mjl@mox.example"
|
rcptTo := "mjl@mox.example"
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
|
||||||
}
|
}
|
||||||
var cerr smtpclient.Error
|
var cerr smtpclient.Error
|
||||||
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
|
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
|
||||||
|
@ -688,7 +689,7 @@ func TestDMARCSent(t *testing.T) {
|
||||||
mailFrom := "remote@example.org"
|
mailFrom := "remote@example.org"
|
||||||
rcptTo := "mjl@mox.example"
|
rcptTo := "mjl@mox.example"
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
|
||||||
}
|
}
|
||||||
tcheck(t, err, "deliver")
|
tcheck(t, err, "deliver")
|
||||||
})
|
})
|
||||||
|
@ -721,7 +722,7 @@ func TestBlocklistedSubjectpass(t *testing.T) {
|
||||||
mailFrom := "remote@example.org"
|
mailFrom := "remote@example.org"
|
||||||
rcptTo := "mjl@mox.example"
|
rcptTo := "mjl@mox.example"
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
|
||||||
}
|
}
|
||||||
var cerr smtpclient.Error
|
var cerr smtpclient.Error
|
||||||
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
|
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C451LocalErr {
|
||||||
|
@ -740,7 +741,7 @@ func TestBlocklistedSubjectpass(t *testing.T) {
|
||||||
mailFrom := "remote@example.org"
|
mailFrom := "remote@example.org"
|
||||||
rcptTo := "mjl@mox.example"
|
rcptTo := "mjl@mox.example"
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
|
||||||
}
|
}
|
||||||
var cerr smtpclient.Error
|
var cerr smtpclient.Error
|
||||||
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
|
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C550MailboxUnavail {
|
||||||
|
@ -758,7 +759,7 @@ func TestBlocklistedSubjectpass(t *testing.T) {
|
||||||
rcptTo := "mjl@mox.example"
|
rcptTo := "mjl@mox.example"
|
||||||
passMessage := strings.Replace(deliverMessage, "Subject: test", "Subject: test "+pass, 1)
|
passMessage := strings.Replace(deliverMessage, "Subject: test", "Subject: test "+pass, 1)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(passMessage)), strings.NewReader(passMessage), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(passMessage)), strings.NewReader(passMessage), false, false, false)
|
||||||
}
|
}
|
||||||
tcheck(t, err, "deliver with subjectpass")
|
tcheck(t, err, "deliver with subjectpass")
|
||||||
})
|
})
|
||||||
|
@ -800,7 +801,7 @@ func TestDMARCReport(t *testing.T) {
|
||||||
msg := msgb.String()
|
msg := msgb.String()
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
|
||||||
}
|
}
|
||||||
tcheck(t, err, "deliver")
|
tcheck(t, err, "deliver")
|
||||||
|
|
||||||
|
@ -922,7 +923,7 @@ func TestTLSReport(t *testing.T) {
|
||||||
msg = headers + msg
|
msg = headers + msg
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
|
||||||
}
|
}
|
||||||
tcheck(t, err, "deliver")
|
tcheck(t, err, "deliver")
|
||||||
|
|
||||||
|
@ -1021,11 +1022,11 @@ func TestRatelimitDelivery(t *testing.T) {
|
||||||
mailFrom := "remote@example.org"
|
mailFrom := "remote@example.org"
|
||||||
rcptTo := "mjl@mox.example"
|
rcptTo := "mjl@mox.example"
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
|
||||||
}
|
}
|
||||||
tcheck(t, err, "deliver to remote")
|
tcheck(t, err, "deliver to remote")
|
||||||
|
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
|
||||||
var cerr smtpclient.Error
|
var cerr smtpclient.Error
|
||||||
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C452StorageFull {
|
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C452StorageFull {
|
||||||
t.Fatalf("got err %v, expected smtpclient error with code 452 for storage full", err)
|
t.Fatalf("got err %v, expected smtpclient error with code 452 for storage full", err)
|
||||||
|
@ -1050,11 +1051,11 @@ func TestRatelimitDelivery(t *testing.T) {
|
||||||
mailFrom := "remote@example.org"
|
mailFrom := "remote@example.org"
|
||||||
rcptTo := "mjl@mox.example"
|
rcptTo := "mjl@mox.example"
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
|
||||||
}
|
}
|
||||||
tcheck(t, err, "deliver to remote")
|
tcheck(t, err, "deliver to remote")
|
||||||
|
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
|
||||||
var cerr smtpclient.Error
|
var cerr smtpclient.Error
|
||||||
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C452StorageFull {
|
if err == nil || !errors.As(err, &cerr) || cerr.Code != smtp.C452StorageFull {
|
||||||
t.Fatalf("got err %v, expected smtpclient error with code 452 for storage full", err)
|
t.Fatalf("got err %v, expected smtpclient error with code 452 for storage full", err)
|
||||||
|
@ -1076,7 +1077,7 @@ func TestNonSMTP(t *testing.T) {
|
||||||
tlsConfig := &tls.Config{
|
tlsConfig := &tls.Config{
|
||||||
Certificates: []tls.Certificate{fakeCert(ts.t)},
|
Certificates: []tls.Certificate{fakeCert(ts.t)},
|
||||||
}
|
}
|
||||||
serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, tlsConfig, serverConn, ts.resolver, ts.submission, false, 100<<20, false, false, ts.dnsbls, 0)
|
serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, tlsConfig, serverConn, ts.resolver, ts.submission, false, 100<<20, false, false, false, ts.dnsbls, 0)
|
||||||
close(serverdone)
|
close(serverdone)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -1123,7 +1124,7 @@ func TestLimitOutgoing(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
mailFrom := "mjl@mox.example"
|
mailFrom := "mjl@mox.example"
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
|
||||||
}
|
}
|
||||||
var cerr smtpclient.Error
|
var cerr smtpclient.Error
|
||||||
if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Secode != expErr.Secode) {
|
if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Secode != expErr.Secode) {
|
||||||
|
@ -1160,7 +1161,7 @@ func TestCatchall(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
mailFrom := "mjl@other.example"
|
mailFrom := "mjl@other.example"
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(submitMessage)), strings.NewReader(submitMessage), false, false, false)
|
||||||
}
|
}
|
||||||
var cerr smtpclient.Error
|
var cerr smtpclient.Error
|
||||||
if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Secode != expErr.Secode) {
|
if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Secode != expErr.Secode) {
|
||||||
|
@ -1253,7 +1254,7 @@ test email
|
||||||
|
|
||||||
rcptTo := "remote@example.org"
|
rcptTo := "remote@example.org"
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, false)
|
||||||
}
|
}
|
||||||
tcheck(t, err, "deliver")
|
tcheck(t, err, "deliver")
|
||||||
|
|
||||||
|
@ -1298,7 +1299,7 @@ func TestPostmaster(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
mailFrom := "mjl@other.example"
|
mailFrom := "mjl@other.example"
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
|
||||||
}
|
}
|
||||||
var cerr smtpclient.Error
|
var cerr smtpclient.Error
|
||||||
if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Code != expErr.Code || cerr.Secode != expErr.Secode) {
|
if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Code != expErr.Code || cerr.Secode != expErr.Secode) {
|
||||||
|
@ -1332,7 +1333,7 @@ func TestEmptylocalpart(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
mailFrom := `""@other.example`
|
mailFrom := `""@other.example`
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false)
|
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
|
||||||
}
|
}
|
||||||
var cerr smtpclient.Error
|
var cerr smtpclient.Error
|
||||||
if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Code != expErr.Code || cerr.Secode != expErr.Secode) {
|
if expErr == nil && err != nil || expErr != nil && (err == nil || !errors.As(err, &cerr) || cerr.Code != expErr.Code || cerr.Secode != expErr.Secode) {
|
||||||
|
@ -1343,3 +1344,98 @@ func TestEmptylocalpart(t *testing.T) {
|
||||||
|
|
||||||
testDeliver(`""@mox.example`, nil)
|
testDeliver(`""@mox.example`, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test handling REQUIRETLS and TLS-Required: No.
|
||||||
|
func TestRequireTLS(t *testing.T) {
|
||||||
|
resolver := dns.MockResolver{
|
||||||
|
A: map[string][]string{
|
||||||
|
"mox.example.": {"127.0.0.10"}, // For mx check.
|
||||||
|
},
|
||||||
|
PTR: map[string][]string{
|
||||||
|
"127.0.0.10": {"mox.example."},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
|
||||||
|
defer ts.close()
|
||||||
|
|
||||||
|
ts.submission = true
|
||||||
|
ts.requiretls = true
|
||||||
|
ts.user = "mjl@mox.example"
|
||||||
|
ts.pass = "testtest"
|
||||||
|
|
||||||
|
no := false
|
||||||
|
yes := true
|
||||||
|
|
||||||
|
msg0 := strings.ReplaceAll(`From: <mjl@mox.example>
|
||||||
|
To: <remote@example.org>
|
||||||
|
Subject: test
|
||||||
|
Message-Id: <test@mox.example>
|
||||||
|
TLS-Required: No
|
||||||
|
|
||||||
|
test email
|
||||||
|
`, "\n", "\r\n")
|
||||||
|
|
||||||
|
msg1 := strings.ReplaceAll(`From: <mjl@mox.example>
|
||||||
|
To: <remote@example.org>
|
||||||
|
Subject: test
|
||||||
|
Message-Id: <test@mox.example>
|
||||||
|
TLS-Required: No
|
||||||
|
TLS-Required: bogus
|
||||||
|
|
||||||
|
test email
|
||||||
|
`, "\n", "\r\n")
|
||||||
|
|
||||||
|
msg2 := strings.ReplaceAll(`From: <mjl@mox.example>
|
||||||
|
To: <remote@example.org>
|
||||||
|
Subject: test
|
||||||
|
Message-Id: <test@mox.example>
|
||||||
|
|
||||||
|
test email
|
||||||
|
`, "\n", "\r\n")
|
||||||
|
|
||||||
|
testSubmit := func(msg string, requiretls bool, expRequireTLS *bool) {
|
||||||
|
t.Helper()
|
||||||
|
ts.run(func(err error, client *smtpclient.Client) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
rcptTo := "remote@example.org"
|
||||||
|
if err == nil {
|
||||||
|
err = client.Deliver(ctxbg, "mjl@mox.example", rcptTo, int64(len(msg)), strings.NewReader(msg), false, false, requiretls)
|
||||||
|
}
|
||||||
|
tcheck(t, err, "deliver")
|
||||||
|
|
||||||
|
msgs, err := queue.List(ctxbg)
|
||||||
|
tcheck(t, err, "listing queue")
|
||||||
|
tcompare(t, len(msgs), 1)
|
||||||
|
tcompare(t, msgs[0].RequireTLS, expRequireTLS)
|
||||||
|
_, err = queue.Drop(ctxbg, msgs[0].ID, "", "")
|
||||||
|
tcheck(t, err, "deleting message from queue")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
testSubmit(msg0, true, &yes) // Header ignored, requiretls applied.
|
||||||
|
testSubmit(msg0, false, &no) // TLS-Required header applied.
|
||||||
|
testSubmit(msg1, true, &yes) // Bad headers ignored, requiretls applied.
|
||||||
|
testSubmit(msg1, false, nil) // Inconsistent multiple headers ignored.
|
||||||
|
testSubmit(msg2, false, nil) // Regular message, no RequireTLS setting.
|
||||||
|
testSubmit(msg2, true, &yes) // Requiretls applied.
|
||||||
|
|
||||||
|
// Check that we get an error if remote SMTP server does not support the requiretls
|
||||||
|
// extension.
|
||||||
|
ts.requiretls = false
|
||||||
|
ts.run(func(err error, client *smtpclient.Client) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
rcptTo := "remote@example.org"
|
||||||
|
if err == nil {
|
||||||
|
err = client.Deliver(ctxbg, "mjl@mox.example", rcptTo, int64(len(msg0)), strings.NewReader(msg0), false, false, true)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("delivered with requiretls to server without requiretls")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, smtpclient.ErrRequireTLSUnsupported) {
|
||||||
|
t.Fatalf("got err %v, expected ErrRequireTLSUnsupported", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -669,8 +669,17 @@ type Outgoing struct {
|
||||||
Submitted time.Time `bstore:"nonzero,default now"`
|
Submitted time.Time `bstore:"nonzero,default now"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RecipientDomainTLS stores TLS capabilities of a recipient domain as encountered
|
||||||
|
// during most recent connection (delivery attempt).
|
||||||
|
type RecipientDomainTLS struct {
|
||||||
|
Domain string // Unicode.
|
||||||
|
Updated time.Time `bstore:"default now"`
|
||||||
|
STARTTLS bool // Supports STARTTLS.
|
||||||
|
RequireTLS bool // Supports RequireTLS SMTP extension.
|
||||||
|
}
|
||||||
|
|
||||||
// Types stored in DB.
|
// Types stored in DB.
|
||||||
var DBTypes = []any{NextUIDValidity{}, Message{}, Recipient{}, Mailbox{}, Subscription{}, Outgoing{}, Password{}, Subjectpass{}, SyncState{}, Upgrade{}}
|
var DBTypes = []any{NextUIDValidity{}, Message{}, Recipient{}, Mailbox{}, Subscription{}, Outgoing{}, Password{}, Subjectpass{}, SyncState{}, Upgrade{}, RecipientDomainTLS{}}
|
||||||
|
|
||||||
// Account holds the information about a user, includings mailboxes, messages, imap subscriptions.
|
// Account holds the information about a user, includings mailboxes, messages, imap subscriptions.
|
||||||
type Account struct {
|
type Account struct {
|
||||||
|
|
|
@ -1823,6 +1823,13 @@ func (Admin) QueueDrop(ctx context.Context, id int64) {
|
||||||
xcheckf(ctx, err, "drop message from queue")
|
xcheckf(ctx, err, "drop message from queue")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QueueSaveRequireTLS updates the requiretls field for a message in the queue,
|
||||||
|
// to be used for the next delivery.
|
||||||
|
func (Admin) QueueSaveRequireTLS(ctx context.Context, id int64, requireTLS *bool) {
|
||||||
|
err := queue.SaveRequireTLS(ctx, id, requireTLS)
|
||||||
|
xcheckf(ctx, err, "update requiretls for message in queue")
|
||||||
|
}
|
||||||
|
|
||||||
// LogLevels returns the current log levels.
|
// LogLevels returns the current log levels.
|
||||||
func (Admin) LogLevels(ctx context.Context) map[string]string {
|
func (Admin) LogLevels(ctx context.Context) map[string]string {
|
||||||
m := map[string]string{}
|
m := map[string]string{}
|
||||||
|
|
|
@ -1537,7 +1537,6 @@ const queueList = async () => {
|
||||||
])
|
])
|
||||||
|
|
||||||
const nowSecs = new Date().getTime()/1000
|
const nowSecs = new Date().getTime()/1000
|
||||||
let transport
|
|
||||||
|
|
||||||
const page = document.getElementById('page')
|
const page = document.getElementById('page')
|
||||||
dom._kids(page,
|
dom._kids(page,
|
||||||
|
@ -1560,63 +1559,97 @@ const queueList = async () => {
|
||||||
dom.th('Next attempt'),
|
dom.th('Next attempt'),
|
||||||
dom.th('Last attempt'),
|
dom.th('Last attempt'),
|
||||||
dom.th('Last error'),
|
dom.th('Last error'),
|
||||||
|
dom.th('Require TLS'),
|
||||||
dom.th('Transport/Retry'),
|
dom.th('Transport/Retry'),
|
||||||
dom.th('Remove'),
|
dom.th('Remove'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
dom.tbody(
|
dom.tbody(
|
||||||
msgs.map(m => dom.tr(
|
msgs.map(m => {
|
||||||
dom.td(''+m.ID),
|
let requiretls, requiretlsFieldset, transport
|
||||||
dom.td(age(new Date(m.Queued), false, nowSecs)),
|
return dom.tr(
|
||||||
dom.td(m.SenderLocalpart+"@"+ipdomainString(m.SenderDomain)), // todo: escaping of localpart
|
dom.td(''+m.ID),
|
||||||
dom.td(m.RecipientLocalpart+"@"+ipdomainString(m.RecipientDomain)), // todo: escaping of localpart
|
dom.td(age(new Date(m.Queued), false, nowSecs)),
|
||||||
dom.td(formatSize(m.Size)),
|
dom.td(m.SenderLocalpart+"@"+ipdomainString(m.SenderDomain)), // todo: escaping of localpart
|
||||||
dom.td(''+m.Attempts),
|
dom.td(m.RecipientLocalpart+"@"+ipdomainString(m.RecipientDomain)), // todo: escaping of localpart
|
||||||
dom.td(age(new Date(m.NextAttempt), true, nowSecs)),
|
dom.td(formatSize(m.Size)),
|
||||||
dom.td(m.LastAttempt ? age(new Date(m.LastAttempt), false, nowSecs) : '-'),
|
dom.td(''+m.Attempts),
|
||||||
dom.td(m.LastError || '-'),
|
dom.td(age(new Date(m.NextAttempt), true, nowSecs)),
|
||||||
dom.td(
|
dom.td(m.LastAttempt ? age(new Date(m.LastAttempt), false, nowSecs) : '-'),
|
||||||
transport=dom.select(
|
dom.td(m.LastError || '-'),
|
||||||
attr({title: 'Transport to use for delivery attempts. The default is direct delivery, connecting to the MX hosts of the domain.'}),
|
dom.td(
|
||||||
dom.option('(default)', attr({value: ''})),
|
dom.form(
|
||||||
Object.keys(transports).sort().map(t => dom.option(t, m.Transport === t ? attr({checked: ''}) : [])),
|
requiretlsFieldset=dom.fieldset(
|
||||||
|
requiretls=dom.select(
|
||||||
|
attr({title: 'How to use TLS for message delivery over SMTP:\n\nDefault: Delivery attempts follow the policies published by the recipient domain: Verification with MTA-STS and/or DANE, or optional opportunistic unverified STARTTLS if the domain does not specify a policy.\n\nWith RequireTLS: For sensitive messages, you may want to require verified TLS. The recipient destination domain SMTP server must support the REQUIRETLS SMTP extension for delivery to succeed. It is automatically chosen when the destination domain mail servers of all recipients are known to support it.\n\nFallback to insecure: If delivery fails due to MTA-STS and/or DANE policies specified by the recipient domain, and the content is not sensitive, you may choose to ignore the recipient domain TLS policies so delivery can succeed.'}),
|
||||||
|
dom.option('Default', attr({value: ''})),
|
||||||
|
dom.option('With RequireTLS', attr({value: 'yes'}), m.RequireTLS === true ? attr({selected: ''}) : []),
|
||||||
|
dom.option('Fallback to insecure', attr({value: 'no'}), m.RequireTLS === false ? attr({selected: ''}) : []),
|
||||||
|
),
|
||||||
|
' ',
|
||||||
|
dom.button('Save'),
|
||||||
|
),
|
||||||
|
async function submit(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
try {
|
||||||
|
requiretlsFieldset.disabled = true
|
||||||
|
await api.QueueSaveRequireTLS(m.ID, requiretls.value === '' ? null : requiretls.value === 'yes')
|
||||||
|
} catch (err) {
|
||||||
|
console.log({err})
|
||||||
|
window.alert('Error: ' + err.message)
|
||||||
|
return
|
||||||
|
} finally {
|
||||||
|
requiretlsFieldset.disabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
),
|
),
|
||||||
' ',
|
dom.td(
|
||||||
dom.button('Retry now', async function click(e) {
|
dom.form(
|
||||||
e.preventDefault()
|
transport=dom.select(
|
||||||
try {
|
attr({title: 'Transport to use for delivery attempts. The default is direct delivery, connecting to the MX hosts of the domain.'}),
|
||||||
e.target.disabled = true
|
dom.option('(default)', attr({value: ''})),
|
||||||
await api.QueueKick(m.ID, transport.value)
|
Object.keys(transports).sort().map(t => dom.option(t, m.Transport === t ? attr({checked: ''}) : [])),
|
||||||
} catch (err) {
|
),
|
||||||
console.log({err})
|
' ',
|
||||||
window.alert('Error: ' + err.message)
|
dom.button('Retry now'),
|
||||||
return
|
async function submit(e) {
|
||||||
} finally {
|
e.preventDefault()
|
||||||
e.target.disabled = false
|
try {
|
||||||
}
|
e.target.disabled = true
|
||||||
window.location.reload() // todo: only refresh the list
|
await api.QueueKick(m.ID, transport.value)
|
||||||
}),
|
} catch (err) {
|
||||||
),
|
console.log({err})
|
||||||
dom.td(
|
window.alert('Error: ' + err.message)
|
||||||
dom.button('Remove', async function click(e) {
|
return
|
||||||
e.preventDefault()
|
} finally {
|
||||||
if (!window.confirm('Are you sure you want to remove this message? It will be removed completely.')) {
|
e.target.disabled = false
|
||||||
return
|
}
|
||||||
}
|
window.location.reload() // todo: only refresh the list
|
||||||
try {
|
}
|
||||||
e.target.disabled = true
|
),
|
||||||
await api.QueueDrop(m.ID)
|
),
|
||||||
} catch (err) {
|
dom.td(
|
||||||
console.log({err})
|
dom.button('Remove', async function click(e) {
|
||||||
window.alert('Error: ' + err.message)
|
e.preventDefault()
|
||||||
return
|
if (!window.confirm('Are you sure you want to remove this message? It will be removed completely.')) {
|
||||||
} finally {
|
return
|
||||||
e.target.disabled = false
|
}
|
||||||
}
|
try {
|
||||||
window.location.reload() // todo: only refresh the list
|
e.target.disabled = true
|
||||||
}),
|
await api.QueueDrop(m.ID)
|
||||||
),
|
} catch (err) {
|
||||||
)),
|
console.log({err})
|
||||||
|
window.alert('Error: ' + err.message)
|
||||||
|
return
|
||||||
|
} finally {
|
||||||
|
e.target.disabled = false
|
||||||
|
}
|
||||||
|
window.location.reload() // todo: only refresh the list
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -622,6 +622,26 @@
|
||||||
],
|
],
|
||||||
"Returns": []
|
"Returns": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"Name": "QueueSaveRequireTLS",
|
||||||
|
"Docs": "QueueSaveRequireTLS updates the requiretls field for a message in the queue,\nto be used for the next delivery.",
|
||||||
|
"Params": [
|
||||||
|
{
|
||||||
|
"Name": "id",
|
||||||
|
"Typewords": [
|
||||||
|
"int64"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "requireTLS",
|
||||||
|
"Typewords": [
|
||||||
|
"nullable",
|
||||||
|
"bool"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Returns": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Name": "LogLevels",
|
"Name": "LogLevels",
|
||||||
"Docs": "LogLevels returns the current log levels.",
|
"Docs": "LogLevels returns the current log levels.",
|
||||||
|
@ -3040,6 +3060,14 @@
|
||||||
"Typewords": [
|
"Typewords": [
|
||||||
"string"
|
"string"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "RequireTLS",
|
||||||
|
"Docs": "RequireTLS influences TLS verification during delivery. If nil, the recipient domain policy is followed (MTA-STS and/or DANE), falling back to optional opportunistic non-verified STARTTLS. If RequireTLS is true (through SMTP REQUIRETLS extension or webmail submit), MTA-STS or DANE is required, as well as REQUIRETLS support by the next hop server. If RequireTLS is false (through messag header \"TLS-Required: No\"), the recipient domain's policy is ignored if it does not lead to a successful TLS connection, i.e. falling back to SMTP delivery with unverified STARTTLS or plain text.",
|
||||||
|
"Typewords": [
|
||||||
|
"nullable",
|
||||||
|
"bool"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime/debug"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -33,6 +34,7 @@ import (
|
||||||
"github.com/mjl-/mox/dkim"
|
"github.com/mjl-/mox/dkim"
|
||||||
"github.com/mjl-/mox/dns"
|
"github.com/mjl-/mox/dns"
|
||||||
"github.com/mjl-/mox/message"
|
"github.com/mjl-/mox/message"
|
||||||
|
"github.com/mjl-/mox/metrics"
|
||||||
"github.com/mjl-/mox/mlog"
|
"github.com/mjl-/mox/mlog"
|
||||||
"github.com/mjl-/mox/mox-"
|
"github.com/mjl-/mox/mox-"
|
||||||
"github.com/mjl-/mox/moxio"
|
"github.com/mjl-/mox/moxio"
|
||||||
|
@ -162,6 +164,7 @@ type SubmitMessage struct {
|
||||||
ResponseMessageID int64 // If set, this was a reply or forward, based on IsForward.
|
ResponseMessageID int64 // If set, this was a reply or forward, based on IsForward.
|
||||||
ReplyTo string // If non-empty, Reply-To header to add to message.
|
ReplyTo string // If non-empty, Reply-To header to add to message.
|
||||||
UserAgent string // User-Agent header added if not empty.
|
UserAgent string // User-Agent header added if not empty.
|
||||||
|
RequireTLS *bool // For "Require TLS" extension during delivery.
|
||||||
}
|
}
|
||||||
|
|
||||||
// ForwardAttachments references attachments by a list of message.Part paths.
|
// ForwardAttachments references attachments by a list of message.Part paths.
|
||||||
|
@ -522,6 +525,9 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
|
||||||
if m.UserAgent != "" {
|
if m.UserAgent != "" {
|
||||||
header("User-Agent", m.UserAgent)
|
header("User-Agent", m.UserAgent)
|
||||||
}
|
}
|
||||||
|
if m.RequireTLS != nil && !*m.RequireTLS {
|
||||||
|
header("TLS-Required", "No")
|
||||||
|
}
|
||||||
header("MIME-Version", "1.0")
|
header("MIME-Version", "1.0")
|
||||||
|
|
||||||
if len(m.Attachments) > 0 || len(m.ForwardAttachments.Paths) > 0 {
|
if len(m.Attachments) > 0 || len(m.ForwardAttachments.Paths) > 0 {
|
||||||
|
@ -685,7 +691,7 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
|
||||||
Localpart: rcpt.Localpart,
|
Localpart: rcpt.Localpart,
|
||||||
IPDomain: dns.IPDomain{Domain: rcpt.Domain},
|
IPDomain: dns.IPDomain{Domain: rcpt.Domain},
|
||||||
}
|
}
|
||||||
_, err := queue.Add(ctx, log, reqInfo.AccountName, fromPath, toPath, has8bit, smtputf8, msgSize, messageID, []byte(rcptMsgPrefix), dataFile, nil)
|
_, err := queue.Add(ctx, log, reqInfo.AccountName, fromPath, toPath, has8bit, smtputf8, msgSize, messageID, []byte(rcptMsgPrefix), dataFile, nil, m.RequireTLS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
metricSubmission.WithLabelValues("queueerror").Inc()
|
metricSubmission.WithLabelValues("queueerror").Inc()
|
||||||
}
|
}
|
||||||
|
@ -1635,12 +1641,27 @@ const (
|
||||||
SecurityResultUnknown SecurityResult = "unknown"
|
SecurityResultUnknown SecurityResult = "unknown"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RecipientSecurity is a quick analysis of the security properties of delivery to the recipient (domain).
|
// RecipientSecurity is a quick analysis of the security properties of delivery to
|
||||||
// Fields are nil when an error occurred during analysis.
|
// the recipient (domain).
|
||||||
type RecipientSecurity struct {
|
type RecipientSecurity struct {
|
||||||
MTASTS SecurityResult // Whether we have a stored enforced MTA-STS policy, or domain has MTA-STS DNS record.
|
// Whether recipient domain supports (opportunistic) STARTTLS, as seen during most
|
||||||
DNSSEC SecurityResult // Whether MX lookup response was DNSSEC-signed.
|
// recent delivery attempt. Will be "unknown" if no delivery to the domain has been
|
||||||
DANE SecurityResult // Whether first delivery destination has DANE records.
|
// attempted yet.
|
||||||
|
STARTTLS SecurityResult
|
||||||
|
|
||||||
|
// Whether we have a stored enforced MTA-STS policy, or domain has MTA-STS DNS
|
||||||
|
// record.
|
||||||
|
MTASTS SecurityResult
|
||||||
|
|
||||||
|
// Whether MX lookup response was DNSSEC-signed.
|
||||||
|
DNSSEC SecurityResult
|
||||||
|
|
||||||
|
// Whether first delivery destination has DANE records.
|
||||||
|
DANE SecurityResult
|
||||||
|
|
||||||
|
// Whether recipient domain is known to implement the REQUIRETLS SMTP extension.
|
||||||
|
// Will be "unknown" if no delivery to the domain has been attempted yet.
|
||||||
|
RequireTLS SecurityResult
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecipientSecurity looks up security properties of the address in the
|
// RecipientSecurity looks up security properties of the address in the
|
||||||
|
@ -1650,6 +1671,18 @@ func (Webmail) RecipientSecurity(ctx context.Context, messageAddressee string) (
|
||||||
return recipientSecurity(ctx, resolver, messageAddressee)
|
return recipientSecurity(ctx, resolver, messageAddressee)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// logPanic can be called with a defer from a goroutine to prevent the entire program from being shutdown in case of a panic.
|
||||||
|
func logPanic(ctx context.Context) {
|
||||||
|
x := recover()
|
||||||
|
if x == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log := xlog.WithContext(ctx)
|
||||||
|
log.Error("recover from panic", mlog.Field("panic", x))
|
||||||
|
debug.PrintStack()
|
||||||
|
metrics.PanicInc(metrics.Webmail)
|
||||||
|
}
|
||||||
|
|
||||||
// separate function for testing with mocked resolver.
|
// separate function for testing with mocked resolver.
|
||||||
func recipientSecurity(ctx context.Context, resolver dns.Resolver, messageAddressee string) (RecipientSecurity, error) {
|
func recipientSecurity(ctx context.Context, resolver dns.Resolver, messageAddressee string) (RecipientSecurity, error) {
|
||||||
log := xlog.WithContext(ctx)
|
log := xlog.WithContext(ctx)
|
||||||
|
@ -1658,6 +1691,8 @@ func recipientSecurity(ctx context.Context, resolver dns.Resolver, messageAddres
|
||||||
SecurityResultUnknown,
|
SecurityResultUnknown,
|
||||||
SecurityResultUnknown,
|
SecurityResultUnknown,
|
||||||
SecurityResultUnknown,
|
SecurityResultUnknown,
|
||||||
|
SecurityResultUnknown,
|
||||||
|
SecurityResultUnknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
msgAddr, err := mail.ParseAddress(messageAddressee)
|
msgAddr, err := mail.ParseAddress(messageAddressee)
|
||||||
|
@ -1675,6 +1710,7 @@ func recipientSecurity(ctx context.Context, resolver dns.Resolver, messageAddres
|
||||||
// MTA-STS.
|
// MTA-STS.
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
|
defer logPanic(ctx)
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
policy, _, err := mtastsdb.Get(ctx, resolver, addr.Domain)
|
policy, _, err := mtastsdb.Get(ctx, resolver, addr.Domain)
|
||||||
|
@ -1690,6 +1726,7 @@ func recipientSecurity(ctx context.Context, resolver dns.Resolver, messageAddres
|
||||||
// DNSSEC and DANE.
|
// DNSSEC and DANE.
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
|
defer logPanic(ctx)
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
_, origNextHopAuthentic, expandedNextHopAuthentic, _, hosts, _, err := smtpclient.GatherDestinations(ctx, log, resolver, dns.IPDomain{Domain: addr.Domain})
|
_, origNextHopAuthentic, expandedNextHopAuthentic, _, hosts, _, err := smtpclient.GatherDestinations(ctx, log, resolver, dns.IPDomain{Domain: addr.Domain})
|
||||||
|
@ -1737,7 +1774,51 @@ func recipientSecurity(ctx context.Context, resolver dns.Resolver, messageAddres
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// STARTTLS and RequireTLS
|
||||||
|
reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
|
||||||
|
acc, err := store.OpenAccount(reqInfo.AccountName)
|
||||||
|
xcheckf(ctx, err, "open account")
|
||||||
|
defer func() {
|
||||||
|
if acc != nil {
|
||||||
|
err := acc.Close()
|
||||||
|
log.Check(err, "closing account")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = acc.DB.Read(ctx, func(tx *bstore.Tx) error {
|
||||||
|
q := bstore.QueryTx[store.RecipientDomainTLS](tx)
|
||||||
|
q.FilterNonzero(store.RecipientDomainTLS{Domain: addr.Domain.Name()})
|
||||||
|
rd, err := q.Get()
|
||||||
|
if err == bstore.ErrAbsent {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
rs.STARTTLS = SecurityResultError
|
||||||
|
rs.RequireTLS = SecurityResultError
|
||||||
|
log.Errorx("looking up recipient domain", err, mlog.Field("domain", addr.Domain))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if rd.STARTTLS {
|
||||||
|
rs.STARTTLS = SecurityResultYes
|
||||||
|
} else {
|
||||||
|
rs.STARTTLS = SecurityResultNo
|
||||||
|
}
|
||||||
|
if rd.RequireTLS {
|
||||||
|
rs.RequireTLS = SecurityResultYes
|
||||||
|
} else {
|
||||||
|
rs.RequireTLS = SecurityResultNo
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
xcheckf(ctx, err, "lookup recipient domain")
|
||||||
|
|
||||||
|
// Close account as soon as possible, not after waiting for MTA-STS/DNSSEC/DANE
|
||||||
|
// checks to complete, which can take a while.
|
||||||
|
err = acc.Close()
|
||||||
|
log.Check(err, "closing account")
|
||||||
|
acc = nil
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
return rs, nil
|
return rs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1090,6 +1090,14 @@
|
||||||
"Typewords": [
|
"Typewords": [
|
||||||
"string"
|
"string"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "RequireTLS",
|
||||||
|
"Docs": "For \"Require TLS\" extension during delivery.",
|
||||||
|
"Typewords": [
|
||||||
|
"nullable",
|
||||||
|
"bool"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -1256,8 +1264,15 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Name": "RecipientSecurity",
|
"Name": "RecipientSecurity",
|
||||||
"Docs": "RecipientSecurity is a quick analysis of the security properties of delivery to the recipient (domain).\nFields are nil when an error occurred during analysis.",
|
"Docs": "RecipientSecurity is a quick analysis of the security properties of delivery to\nthe recipient (domain).",
|
||||||
"Fields": [
|
"Fields": [
|
||||||
|
{
|
||||||
|
"Name": "STARTTLS",
|
||||||
|
"Docs": "Whether recipient domain supports (opportunistic) STARTTLS, as seen during most recent delivery attempt. Will be \"unknown\" if no delivery to the domain has been attempted yet.",
|
||||||
|
"Typewords": [
|
||||||
|
"SecurityResult"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Name": "MTASTS",
|
"Name": "MTASTS",
|
||||||
"Docs": "Whether we have a stored enforced MTA-STS policy, or domain has MTA-STS DNS record.",
|
"Docs": "Whether we have a stored enforced MTA-STS policy, or domain has MTA-STS DNS record.",
|
||||||
|
@ -1278,6 +1293,13 @@
|
||||||
"Typewords": [
|
"Typewords": [
|
||||||
"SecurityResult"
|
"SecurityResult"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "RequireTLS",
|
||||||
|
"Docs": "Whether recipient domain is known to implement the REQUIRETLS SMTP extension. Will be \"unknown\" if no delivery to the domain has been attempted yet.",
|
||||||
|
"Typewords": [
|
||||||
|
"SecurityResult"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -142,6 +142,7 @@ export interface SubmitMessage {
|
||||||
ResponseMessageID: number // If set, this was a reply or forward, based on IsForward.
|
ResponseMessageID: number // If set, this was a reply or forward, based on IsForward.
|
||||||
ReplyTo: string // If non-empty, Reply-To header to add to message.
|
ReplyTo: string // If non-empty, Reply-To header to add to message.
|
||||||
UserAgent: string // User-Agent header added if not empty.
|
UserAgent: string // User-Agent header added if not empty.
|
||||||
|
RequireTLS?: boolean | null // For "Require TLS" extension during delivery.
|
||||||
}
|
}
|
||||||
|
|
||||||
// File is a new attachment (not from an existing message that is being
|
// File is a new attachment (not from an existing message that is being
|
||||||
|
@ -177,12 +178,14 @@ export interface Mailbox {
|
||||||
Size: number // Number of bytes for all messages.
|
Size: number // Number of bytes for all messages.
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecipientSecurity is a quick analysis of the security properties of delivery to the recipient (domain).
|
// RecipientSecurity is a quick analysis of the security properties of delivery to
|
||||||
// Fields are nil when an error occurred during analysis.
|
// the recipient (domain).
|
||||||
export interface RecipientSecurity {
|
export interface RecipientSecurity {
|
||||||
|
STARTTLS: SecurityResult // Whether recipient domain supports (opportunistic) STARTTLS, as seen during most recent delivery attempt. Will be "unknown" if no delivery to the domain has been attempted yet.
|
||||||
MTASTS: SecurityResult // Whether we have a stored enforced MTA-STS policy, or domain has MTA-STS DNS record.
|
MTASTS: SecurityResult // Whether we have a stored enforced MTA-STS policy, or domain has MTA-STS DNS record.
|
||||||
DNSSEC: SecurityResult // Whether MX lookup response was DNSSEC-signed.
|
DNSSEC: SecurityResult // Whether MX lookup response was DNSSEC-signed.
|
||||||
DANE: SecurityResult // Whether first delivery destination has DANE records.
|
DANE: SecurityResult // Whether first delivery destination has DANE records.
|
||||||
|
RequireTLS: SecurityResult // Whether recipient domain is known to implement the REQUIRETLS SMTP extension. Will be "unknown" if no delivery to the domain has been attempted yet.
|
||||||
}
|
}
|
||||||
|
|
||||||
// EventStart is the first message sent on an SSE connection, giving the client
|
// EventStart is the first message sent on an SSE connection, giving the client
|
||||||
|
@ -519,11 +522,11 @@ export const types: TypenameMap = {
|
||||||
"Address": {"Name":"Address","Docs":"","Fields":[{"Name":"Name","Docs":"","Typewords":["string"]},{"Name":"User","Docs":"","Typewords":["string"]},{"Name":"Host","Docs":"","Typewords":["string"]}]},
|
"Address": {"Name":"Address","Docs":"","Fields":[{"Name":"Name","Docs":"","Typewords":["string"]},{"Name":"User","Docs":"","Typewords":["string"]},{"Name":"Host","Docs":"","Typewords":["string"]}]},
|
||||||
"MessageAddress": {"Name":"MessageAddress","Docs":"","Fields":[{"Name":"Name","Docs":"","Typewords":["string"]},{"Name":"User","Docs":"","Typewords":["string"]},{"Name":"Domain","Docs":"","Typewords":["Domain"]}]},
|
"MessageAddress": {"Name":"MessageAddress","Docs":"","Fields":[{"Name":"Name","Docs":"","Typewords":["string"]},{"Name":"User","Docs":"","Typewords":["string"]},{"Name":"Domain","Docs":"","Typewords":["Domain"]}]},
|
||||||
"Domain": {"Name":"Domain","Docs":"","Fields":[{"Name":"ASCII","Docs":"","Typewords":["string"]},{"Name":"Unicode","Docs":"","Typewords":["string"]}]},
|
"Domain": {"Name":"Domain","Docs":"","Fields":[{"Name":"ASCII","Docs":"","Typewords":["string"]},{"Name":"Unicode","Docs":"","Typewords":["string"]}]},
|
||||||
"SubmitMessage": {"Name":"SubmitMessage","Docs":"","Fields":[{"Name":"From","Docs":"","Typewords":["string"]},{"Name":"To","Docs":"","Typewords":["[]","string"]},{"Name":"Cc","Docs":"","Typewords":["[]","string"]},{"Name":"Bcc","Docs":"","Typewords":["[]","string"]},{"Name":"Subject","Docs":"","Typewords":["string"]},{"Name":"TextBody","Docs":"","Typewords":["string"]},{"Name":"Attachments","Docs":"","Typewords":["[]","File"]},{"Name":"ForwardAttachments","Docs":"","Typewords":["ForwardAttachments"]},{"Name":"IsForward","Docs":"","Typewords":["bool"]},{"Name":"ResponseMessageID","Docs":"","Typewords":["int64"]},{"Name":"ReplyTo","Docs":"","Typewords":["string"]},{"Name":"UserAgent","Docs":"","Typewords":["string"]}]},
|
"SubmitMessage": {"Name":"SubmitMessage","Docs":"","Fields":[{"Name":"From","Docs":"","Typewords":["string"]},{"Name":"To","Docs":"","Typewords":["[]","string"]},{"Name":"Cc","Docs":"","Typewords":["[]","string"]},{"Name":"Bcc","Docs":"","Typewords":["[]","string"]},{"Name":"Subject","Docs":"","Typewords":["string"]},{"Name":"TextBody","Docs":"","Typewords":["string"]},{"Name":"Attachments","Docs":"","Typewords":["[]","File"]},{"Name":"ForwardAttachments","Docs":"","Typewords":["ForwardAttachments"]},{"Name":"IsForward","Docs":"","Typewords":["bool"]},{"Name":"ResponseMessageID","Docs":"","Typewords":["int64"]},{"Name":"ReplyTo","Docs":"","Typewords":["string"]},{"Name":"UserAgent","Docs":"","Typewords":["string"]},{"Name":"RequireTLS","Docs":"","Typewords":["nullable","bool"]}]},
|
||||||
"File": {"Name":"File","Docs":"","Fields":[{"Name":"Filename","Docs":"","Typewords":["string"]},{"Name":"DataURI","Docs":"","Typewords":["string"]}]},
|
"File": {"Name":"File","Docs":"","Fields":[{"Name":"Filename","Docs":"","Typewords":["string"]},{"Name":"DataURI","Docs":"","Typewords":["string"]}]},
|
||||||
"ForwardAttachments": {"Name":"ForwardAttachments","Docs":"","Fields":[{"Name":"MessageID","Docs":"","Typewords":["int64"]},{"Name":"Paths","Docs":"","Typewords":["[]","[]","int32"]}]},
|
"ForwardAttachments": {"Name":"ForwardAttachments","Docs":"","Fields":[{"Name":"MessageID","Docs":"","Typewords":["int64"]},{"Name":"Paths","Docs":"","Typewords":["[]","[]","int32"]}]},
|
||||||
"Mailbox": {"Name":"Mailbox","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"Name","Docs":"","Typewords":["string"]},{"Name":"UIDValidity","Docs":"","Typewords":["uint32"]},{"Name":"UIDNext","Docs":"","Typewords":["UID"]},{"Name":"Archive","Docs":"","Typewords":["bool"]},{"Name":"Draft","Docs":"","Typewords":["bool"]},{"Name":"Junk","Docs":"","Typewords":["bool"]},{"Name":"Sent","Docs":"","Typewords":["bool"]},{"Name":"Trash","Docs":"","Typewords":["bool"]},{"Name":"Keywords","Docs":"","Typewords":["[]","string"]},{"Name":"HaveCounts","Docs":"","Typewords":["bool"]},{"Name":"Total","Docs":"","Typewords":["int64"]},{"Name":"Deleted","Docs":"","Typewords":["int64"]},{"Name":"Unread","Docs":"","Typewords":["int64"]},{"Name":"Unseen","Docs":"","Typewords":["int64"]},{"Name":"Size","Docs":"","Typewords":["int64"]}]},
|
"Mailbox": {"Name":"Mailbox","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"Name","Docs":"","Typewords":["string"]},{"Name":"UIDValidity","Docs":"","Typewords":["uint32"]},{"Name":"UIDNext","Docs":"","Typewords":["UID"]},{"Name":"Archive","Docs":"","Typewords":["bool"]},{"Name":"Draft","Docs":"","Typewords":["bool"]},{"Name":"Junk","Docs":"","Typewords":["bool"]},{"Name":"Sent","Docs":"","Typewords":["bool"]},{"Name":"Trash","Docs":"","Typewords":["bool"]},{"Name":"Keywords","Docs":"","Typewords":["[]","string"]},{"Name":"HaveCounts","Docs":"","Typewords":["bool"]},{"Name":"Total","Docs":"","Typewords":["int64"]},{"Name":"Deleted","Docs":"","Typewords":["int64"]},{"Name":"Unread","Docs":"","Typewords":["int64"]},{"Name":"Unseen","Docs":"","Typewords":["int64"]},{"Name":"Size","Docs":"","Typewords":["int64"]}]},
|
||||||
"RecipientSecurity": {"Name":"RecipientSecurity","Docs":"","Fields":[{"Name":"MTASTS","Docs":"","Typewords":["SecurityResult"]},{"Name":"DNSSEC","Docs":"","Typewords":["SecurityResult"]},{"Name":"DANE","Docs":"","Typewords":["SecurityResult"]}]},
|
"RecipientSecurity": {"Name":"RecipientSecurity","Docs":"","Fields":[{"Name":"STARTTLS","Docs":"","Typewords":["SecurityResult"]},{"Name":"MTASTS","Docs":"","Typewords":["SecurityResult"]},{"Name":"DNSSEC","Docs":"","Typewords":["SecurityResult"]},{"Name":"DANE","Docs":"","Typewords":["SecurityResult"]},{"Name":"RequireTLS","Docs":"","Typewords":["SecurityResult"]}]},
|
||||||
"EventStart": {"Name":"EventStart","Docs":"","Fields":[{"Name":"SSEID","Docs":"","Typewords":["int64"]},{"Name":"LoginAddress","Docs":"","Typewords":["MessageAddress"]},{"Name":"Addresses","Docs":"","Typewords":["[]","MessageAddress"]},{"Name":"DomainAddressConfigs","Docs":"","Typewords":["{}","DomainAddressConfig"]},{"Name":"MailboxName","Docs":"","Typewords":["string"]},{"Name":"Mailboxes","Docs":"","Typewords":["[]","Mailbox"]}]},
|
"EventStart": {"Name":"EventStart","Docs":"","Fields":[{"Name":"SSEID","Docs":"","Typewords":["int64"]},{"Name":"LoginAddress","Docs":"","Typewords":["MessageAddress"]},{"Name":"Addresses","Docs":"","Typewords":["[]","MessageAddress"]},{"Name":"DomainAddressConfigs","Docs":"","Typewords":["{}","DomainAddressConfig"]},{"Name":"MailboxName","Docs":"","Typewords":["string"]},{"Name":"Mailboxes","Docs":"","Typewords":["[]","Mailbox"]}]},
|
||||||
"DomainAddressConfig": {"Name":"DomainAddressConfig","Docs":"","Fields":[{"Name":"LocalpartCatchallSeparator","Docs":"","Typewords":["string"]},{"Name":"LocalpartCaseSensitive","Docs":"","Typewords":["bool"]}]},
|
"DomainAddressConfig": {"Name":"DomainAddressConfig","Docs":"","Fields":[{"Name":"LocalpartCatchallSeparator","Docs":"","Typewords":["string"]},{"Name":"LocalpartCaseSensitive","Docs":"","Typewords":["bool"]}]},
|
||||||
"EventViewErr": {"Name":"EventViewErr","Docs":"","Fields":[{"Name":"ViewID","Docs":"","Typewords":["int64"]},{"Name":"RequestID","Docs":"","Typewords":["int64"]},{"Name":"Err","Docs":"","Typewords":["string"]}]},
|
"EventViewErr": {"Name":"EventViewErr","Docs":"","Fields":[{"Name":"ViewID","Docs":"","Typewords":["int64"]},{"Name":"RequestID","Docs":"","Typewords":["int64"]},{"Name":"Err","Docs":"","Typewords":["string"]}]},
|
||||||
|
|
|
@ -366,7 +366,12 @@ func TestAPI(t *testing.T) {
|
||||||
|
|
||||||
// RecipientSecurity
|
// RecipientSecurity
|
||||||
resolver := dns.MockResolver{}
|
resolver := dns.MockResolver{}
|
||||||
rs, err := recipientSecurity(ctxbg, resolver, "mjl@a.mox.example")
|
rs, err := recipientSecurity(ctx, resolver, "mjl@a.mox.example")
|
||||||
tcompare(t, err, nil)
|
tcompare(t, err, nil)
|
||||||
tcompare(t, rs, RecipientSecurity{SecurityResultNo, SecurityResultNo, SecurityResultNo})
|
tcompare(t, rs, RecipientSecurity{SecurityResultUnknown, SecurityResultNo, SecurityResultNo, SecurityResultNo, SecurityResultUnknown})
|
||||||
|
err = acc.DB.Insert(ctx, &store.RecipientDomainTLS{Domain: "a.mox.example", STARTTLS: true, RequireTLS: false})
|
||||||
|
tcheck(t, err, "insert recipient domain tls info")
|
||||||
|
rs, err = recipientSecurity(ctx, resolver, "mjl@a.mox.example")
|
||||||
|
tcompare(t, err, nil)
|
||||||
|
tcompare(t, rs, RecipientSecurity{SecurityResultYes, SecurityResultNo, SecurityResultNo, SecurityResultNo, SecurityResultNo})
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,11 +62,11 @@ var api;
|
||||||
"Address": { "Name": "Address", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Host", "Docs": "", "Typewords": ["string"] }] },
|
"Address": { "Name": "Address", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Host", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"MessageAddress": { "Name": "MessageAddress", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }] },
|
"MessageAddress": { "Name": "MessageAddress", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }] },
|
||||||
"Domain": { "Name": "Domain", "Docs": "", "Fields": [{ "Name": "ASCII", "Docs": "", "Typewords": ["string"] }, { "Name": "Unicode", "Docs": "", "Typewords": ["string"] }] },
|
"Domain": { "Name": "Domain", "Docs": "", "Fields": [{ "Name": "ASCII", "Docs": "", "Typewords": ["string"] }, { "Name": "Unicode", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"SubmitMessage": { "Name": "SubmitMessage", "Docs": "", "Fields": [{ "Name": "From", "Docs": "", "Typewords": ["string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Cc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Bcc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "TextBody", "Docs": "", "Typewords": ["string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "File"] }, { "Name": "ForwardAttachments", "Docs": "", "Typewords": ["ForwardAttachments"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ResponseMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "UserAgent", "Docs": "", "Typewords": ["string"] }] },
|
"SubmitMessage": { "Name": "SubmitMessage", "Docs": "", "Fields": [{ "Name": "From", "Docs": "", "Typewords": ["string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Cc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Bcc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "TextBody", "Docs": "", "Typewords": ["string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "File"] }, { "Name": "ForwardAttachments", "Docs": "", "Typewords": ["ForwardAttachments"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ResponseMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "UserAgent", "Docs": "", "Typewords": ["string"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["nullable", "bool"] }] },
|
||||||
"File": { "Name": "File", "Docs": "", "Fields": [{ "Name": "Filename", "Docs": "", "Typewords": ["string"] }, { "Name": "DataURI", "Docs": "", "Typewords": ["string"] }] },
|
"File": { "Name": "File", "Docs": "", "Fields": [{ "Name": "Filename", "Docs": "", "Typewords": ["string"] }, { "Name": "DataURI", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"ForwardAttachments": { "Name": "ForwardAttachments", "Docs": "", "Fields": [{ "Name": "MessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Paths", "Docs": "", "Typewords": ["[]", "[]", "int32"] }] },
|
"ForwardAttachments": { "Name": "ForwardAttachments", "Docs": "", "Fields": [{ "Name": "MessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Paths", "Docs": "", "Typewords": ["[]", "[]", "int32"] }] },
|
||||||
"Mailbox": { "Name": "Mailbox", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "UIDValidity", "Docs": "", "Typewords": ["uint32"] }, { "Name": "UIDNext", "Docs": "", "Typewords": ["UID"] }, { "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HaveCounts", "Docs": "", "Typewords": ["bool"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] },
|
"Mailbox": { "Name": "Mailbox", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "UIDValidity", "Docs": "", "Typewords": ["uint32"] }, { "Name": "UIDNext", "Docs": "", "Typewords": ["UID"] }, { "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HaveCounts", "Docs": "", "Typewords": ["bool"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] },
|
||||||
"RecipientSecurity": { "Name": "RecipientSecurity", "Docs": "", "Fields": [{ "Name": "MTASTS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DNSSEC", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DANE", "Docs": "", "Typewords": ["SecurityResult"] }] },
|
"RecipientSecurity": { "Name": "RecipientSecurity", "Docs": "", "Fields": [{ "Name": "STARTTLS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "MTASTS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DNSSEC", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DANE", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["SecurityResult"] }] },
|
||||||
"EventStart": { "Name": "EventStart", "Docs": "", "Fields": [{ "Name": "SSEID", "Docs": "", "Typewords": ["int64"] }, { "Name": "LoginAddress", "Docs": "", "Typewords": ["MessageAddress"] }, { "Name": "Addresses", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "DomainAddressConfigs", "Docs": "", "Typewords": ["{}", "DomainAddressConfig"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailboxes", "Docs": "", "Typewords": ["[]", "Mailbox"] }] },
|
"EventStart": { "Name": "EventStart", "Docs": "", "Fields": [{ "Name": "SSEID", "Docs": "", "Typewords": ["int64"] }, { "Name": "LoginAddress", "Docs": "", "Typewords": ["MessageAddress"] }, { "Name": "Addresses", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "DomainAddressConfigs", "Docs": "", "Typewords": ["{}", "DomainAddressConfig"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailboxes", "Docs": "", "Typewords": ["[]", "Mailbox"] }] },
|
||||||
"DomainAddressConfig": { "Name": "DomainAddressConfig", "Docs": "", "Fields": [{ "Name": "LocalpartCatchallSeparator", "Docs": "", "Typewords": ["string"] }, { "Name": "LocalpartCaseSensitive", "Docs": "", "Typewords": ["bool"] }] },
|
"DomainAddressConfig": { "Name": "DomainAddressConfig", "Docs": "", "Fields": [{ "Name": "LocalpartCatchallSeparator", "Docs": "", "Typewords": ["string"] }, { "Name": "LocalpartCaseSensitive", "Docs": "", "Typewords": ["bool"] }] },
|
||||||
"EventViewErr": { "Name": "EventViewErr", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Err", "Docs": "", "Typewords": ["string"] }] },
|
"EventViewErr": { "Name": "EventViewErr", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Err", "Docs": "", "Typewords": ["string"] }] },
|
||||||
|
|
|
@ -62,11 +62,11 @@ var api;
|
||||||
"Address": { "Name": "Address", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Host", "Docs": "", "Typewords": ["string"] }] },
|
"Address": { "Name": "Address", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Host", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"MessageAddress": { "Name": "MessageAddress", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }] },
|
"MessageAddress": { "Name": "MessageAddress", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }] },
|
||||||
"Domain": { "Name": "Domain", "Docs": "", "Fields": [{ "Name": "ASCII", "Docs": "", "Typewords": ["string"] }, { "Name": "Unicode", "Docs": "", "Typewords": ["string"] }] },
|
"Domain": { "Name": "Domain", "Docs": "", "Fields": [{ "Name": "ASCII", "Docs": "", "Typewords": ["string"] }, { "Name": "Unicode", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"SubmitMessage": { "Name": "SubmitMessage", "Docs": "", "Fields": [{ "Name": "From", "Docs": "", "Typewords": ["string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Cc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Bcc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "TextBody", "Docs": "", "Typewords": ["string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "File"] }, { "Name": "ForwardAttachments", "Docs": "", "Typewords": ["ForwardAttachments"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ResponseMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "UserAgent", "Docs": "", "Typewords": ["string"] }] },
|
"SubmitMessage": { "Name": "SubmitMessage", "Docs": "", "Fields": [{ "Name": "From", "Docs": "", "Typewords": ["string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Cc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Bcc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "TextBody", "Docs": "", "Typewords": ["string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "File"] }, { "Name": "ForwardAttachments", "Docs": "", "Typewords": ["ForwardAttachments"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ResponseMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "UserAgent", "Docs": "", "Typewords": ["string"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["nullable", "bool"] }] },
|
||||||
"File": { "Name": "File", "Docs": "", "Fields": [{ "Name": "Filename", "Docs": "", "Typewords": ["string"] }, { "Name": "DataURI", "Docs": "", "Typewords": ["string"] }] },
|
"File": { "Name": "File", "Docs": "", "Fields": [{ "Name": "Filename", "Docs": "", "Typewords": ["string"] }, { "Name": "DataURI", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"ForwardAttachments": { "Name": "ForwardAttachments", "Docs": "", "Fields": [{ "Name": "MessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Paths", "Docs": "", "Typewords": ["[]", "[]", "int32"] }] },
|
"ForwardAttachments": { "Name": "ForwardAttachments", "Docs": "", "Fields": [{ "Name": "MessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Paths", "Docs": "", "Typewords": ["[]", "[]", "int32"] }] },
|
||||||
"Mailbox": { "Name": "Mailbox", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "UIDValidity", "Docs": "", "Typewords": ["uint32"] }, { "Name": "UIDNext", "Docs": "", "Typewords": ["UID"] }, { "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HaveCounts", "Docs": "", "Typewords": ["bool"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] },
|
"Mailbox": { "Name": "Mailbox", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "UIDValidity", "Docs": "", "Typewords": ["uint32"] }, { "Name": "UIDNext", "Docs": "", "Typewords": ["UID"] }, { "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HaveCounts", "Docs": "", "Typewords": ["bool"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] },
|
||||||
"RecipientSecurity": { "Name": "RecipientSecurity", "Docs": "", "Fields": [{ "Name": "MTASTS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DNSSEC", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DANE", "Docs": "", "Typewords": ["SecurityResult"] }] },
|
"RecipientSecurity": { "Name": "RecipientSecurity", "Docs": "", "Fields": [{ "Name": "STARTTLS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "MTASTS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DNSSEC", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DANE", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["SecurityResult"] }] },
|
||||||
"EventStart": { "Name": "EventStart", "Docs": "", "Fields": [{ "Name": "SSEID", "Docs": "", "Typewords": ["int64"] }, { "Name": "LoginAddress", "Docs": "", "Typewords": ["MessageAddress"] }, { "Name": "Addresses", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "DomainAddressConfigs", "Docs": "", "Typewords": ["{}", "DomainAddressConfig"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailboxes", "Docs": "", "Typewords": ["[]", "Mailbox"] }] },
|
"EventStart": { "Name": "EventStart", "Docs": "", "Fields": [{ "Name": "SSEID", "Docs": "", "Typewords": ["int64"] }, { "Name": "LoginAddress", "Docs": "", "Typewords": ["MessageAddress"] }, { "Name": "Addresses", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "DomainAddressConfigs", "Docs": "", "Typewords": ["{}", "DomainAddressConfig"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailboxes", "Docs": "", "Typewords": ["[]", "Mailbox"] }] },
|
||||||
"DomainAddressConfig": { "Name": "DomainAddressConfig", "Docs": "", "Fields": [{ "Name": "LocalpartCatchallSeparator", "Docs": "", "Typewords": ["string"] }, { "Name": "LocalpartCaseSensitive", "Docs": "", "Typewords": ["bool"] }] },
|
"DomainAddressConfig": { "Name": "DomainAddressConfig", "Docs": "", "Fields": [{ "Name": "LocalpartCatchallSeparator", "Docs": "", "Typewords": ["string"] }, { "Name": "LocalpartCaseSensitive", "Docs": "", "Typewords": ["bool"] }] },
|
||||||
"EventViewErr": { "Name": "EventViewErr", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Err", "Docs": "", "Typewords": ["string"] }] },
|
"EventViewErr": { "Name": "EventViewErr", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Err", "Docs": "", "Typewords": ["string"] }] },
|
||||||
|
|
|
@ -62,11 +62,11 @@ var api;
|
||||||
"Address": { "Name": "Address", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Host", "Docs": "", "Typewords": ["string"] }] },
|
"Address": { "Name": "Address", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Host", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"MessageAddress": { "Name": "MessageAddress", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }] },
|
"MessageAddress": { "Name": "MessageAddress", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }] },
|
||||||
"Domain": { "Name": "Domain", "Docs": "", "Fields": [{ "Name": "ASCII", "Docs": "", "Typewords": ["string"] }, { "Name": "Unicode", "Docs": "", "Typewords": ["string"] }] },
|
"Domain": { "Name": "Domain", "Docs": "", "Fields": [{ "Name": "ASCII", "Docs": "", "Typewords": ["string"] }, { "Name": "Unicode", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"SubmitMessage": { "Name": "SubmitMessage", "Docs": "", "Fields": [{ "Name": "From", "Docs": "", "Typewords": ["string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Cc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Bcc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "TextBody", "Docs": "", "Typewords": ["string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "File"] }, { "Name": "ForwardAttachments", "Docs": "", "Typewords": ["ForwardAttachments"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ResponseMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "UserAgent", "Docs": "", "Typewords": ["string"] }] },
|
"SubmitMessage": { "Name": "SubmitMessage", "Docs": "", "Fields": [{ "Name": "From", "Docs": "", "Typewords": ["string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Cc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Bcc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "TextBody", "Docs": "", "Typewords": ["string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "File"] }, { "Name": "ForwardAttachments", "Docs": "", "Typewords": ["ForwardAttachments"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ResponseMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "UserAgent", "Docs": "", "Typewords": ["string"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["nullable", "bool"] }] },
|
||||||
"File": { "Name": "File", "Docs": "", "Fields": [{ "Name": "Filename", "Docs": "", "Typewords": ["string"] }, { "Name": "DataURI", "Docs": "", "Typewords": ["string"] }] },
|
"File": { "Name": "File", "Docs": "", "Fields": [{ "Name": "Filename", "Docs": "", "Typewords": ["string"] }, { "Name": "DataURI", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"ForwardAttachments": { "Name": "ForwardAttachments", "Docs": "", "Fields": [{ "Name": "MessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Paths", "Docs": "", "Typewords": ["[]", "[]", "int32"] }] },
|
"ForwardAttachments": { "Name": "ForwardAttachments", "Docs": "", "Fields": [{ "Name": "MessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Paths", "Docs": "", "Typewords": ["[]", "[]", "int32"] }] },
|
||||||
"Mailbox": { "Name": "Mailbox", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "UIDValidity", "Docs": "", "Typewords": ["uint32"] }, { "Name": "UIDNext", "Docs": "", "Typewords": ["UID"] }, { "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HaveCounts", "Docs": "", "Typewords": ["bool"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] },
|
"Mailbox": { "Name": "Mailbox", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "UIDValidity", "Docs": "", "Typewords": ["uint32"] }, { "Name": "UIDNext", "Docs": "", "Typewords": ["UID"] }, { "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HaveCounts", "Docs": "", "Typewords": ["bool"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] },
|
||||||
"RecipientSecurity": { "Name": "RecipientSecurity", "Docs": "", "Fields": [{ "Name": "MTASTS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DNSSEC", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DANE", "Docs": "", "Typewords": ["SecurityResult"] }] },
|
"RecipientSecurity": { "Name": "RecipientSecurity", "Docs": "", "Fields": [{ "Name": "STARTTLS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "MTASTS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DNSSEC", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DANE", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["SecurityResult"] }] },
|
||||||
"EventStart": { "Name": "EventStart", "Docs": "", "Fields": [{ "Name": "SSEID", "Docs": "", "Typewords": ["int64"] }, { "Name": "LoginAddress", "Docs": "", "Typewords": ["MessageAddress"] }, { "Name": "Addresses", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "DomainAddressConfigs", "Docs": "", "Typewords": ["{}", "DomainAddressConfig"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailboxes", "Docs": "", "Typewords": ["[]", "Mailbox"] }] },
|
"EventStart": { "Name": "EventStart", "Docs": "", "Fields": [{ "Name": "SSEID", "Docs": "", "Typewords": ["int64"] }, { "Name": "LoginAddress", "Docs": "", "Typewords": ["MessageAddress"] }, { "Name": "Addresses", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "DomainAddressConfigs", "Docs": "", "Typewords": ["{}", "DomainAddressConfig"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailboxes", "Docs": "", "Typewords": ["[]", "Mailbox"] }] },
|
||||||
"DomainAddressConfig": { "Name": "DomainAddressConfig", "Docs": "", "Fields": [{ "Name": "LocalpartCatchallSeparator", "Docs": "", "Typewords": ["string"] }, { "Name": "LocalpartCaseSensitive", "Docs": "", "Typewords": ["bool"] }] },
|
"DomainAddressConfig": { "Name": "DomainAddressConfig", "Docs": "", "Fields": [{ "Name": "LocalpartCatchallSeparator", "Docs": "", "Typewords": ["string"] }, { "Name": "LocalpartCaseSensitive", "Docs": "", "Typewords": ["bool"] }] },
|
||||||
"EventViewErr": { "Name": "EventViewErr", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Err", "Docs": "", "Typewords": ["string"] }] },
|
"EventViewErr": { "Name": "EventViewErr", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Err", "Docs": "", "Typewords": ["string"] }] },
|
||||||
|
@ -1039,7 +1039,7 @@ To simulate slow API calls and SSE events:
|
||||||
|
|
||||||
Show additional headers of messages:
|
Show additional headers of messages:
|
||||||
|
|
||||||
settingsPut({...settings, showHeaders: ['User-Agent', 'X-Mailer', 'Message-Id', 'List-Id', 'List-Post', 'X-Mox-Reason']})
|
settingsPut({...settings, showHeaders: ['User-Agent', 'X-Mailer', 'Message-Id', 'List-Id', 'List-Post', 'X-Mox-Reason', 'TLS-Required']})
|
||||||
|
|
||||||
Enable logging and reload afterwards:
|
Enable logging and reload afterwards:
|
||||||
|
|
||||||
|
@ -1987,6 +1987,7 @@ const compose = (opts) => {
|
||||||
let subject;
|
let subject;
|
||||||
let body;
|
let body;
|
||||||
let attachments;
|
let attachments;
|
||||||
|
let requiretls;
|
||||||
let toBtn, ccBtn, bccBtn, replyToBtn, customFromBtn;
|
let toBtn, ccBtn, bccBtn, replyToBtn, customFromBtn;
|
||||||
let replyToCell, toCell, ccCell, bccCell; // Where we append new address views.
|
let replyToCell, toCell, ccCell, bccCell; // Where we append new address views.
|
||||||
let toRow, replyToRow, ccRow, bccRow; // We show/hide rows as needed.
|
let toRow, replyToRow, ccRow, bccRow; // We show/hide rows as needed.
|
||||||
|
@ -2035,6 +2036,7 @@ const compose = (opts) => {
|
||||||
ForwardAttachments: forwardAttachmentPaths.length === 0 ? { MessageID: 0, Paths: [] } : { MessageID: opts.attachmentsMessageItem.Message.ID, Paths: forwardAttachmentPaths },
|
ForwardAttachments: forwardAttachmentPaths.length === 0 ? { MessageID: 0, Paths: [] } : { MessageID: opts.attachmentsMessageItem.Message.ID, Paths: forwardAttachmentPaths },
|
||||||
IsForward: opts.isForward || false,
|
IsForward: opts.isForward || false,
|
||||||
ResponseMessageID: opts.responseMessageID || 0,
|
ResponseMessageID: opts.responseMessageID || 0,
|
||||||
|
RequireTLS: requiretls.value === '' ? null : requiretls.value === 'yes',
|
||||||
};
|
};
|
||||||
await client.MessageSubmit(message);
|
await client.MessageSubmit(message);
|
||||||
cmdCancel();
|
cmdCancel();
|
||||||
|
@ -2042,10 +2044,10 @@ const compose = (opts) => {
|
||||||
const cmdSend = async () => {
|
const cmdSend = async () => {
|
||||||
await withStatus('Sending email', submit(), fieldset);
|
await withStatus('Sending email', submit(), fieldset);
|
||||||
};
|
};
|
||||||
const cmdAddTo = async () => { newAddrView('', toViews, toBtn, toCell, toRow); };
|
const cmdAddTo = async () => { newAddrView('', true, toViews, toBtn, toCell, toRow); };
|
||||||
const cmdAddCc = async () => { newAddrView('', ccViews, ccBtn, ccCell, ccRow); };
|
const cmdAddCc = async () => { newAddrView('', true, ccViews, ccBtn, ccCell, ccRow); };
|
||||||
const cmdAddBcc = async () => { newAddrView('', bccViews, bccBtn, bccCell, bccRow); };
|
const cmdAddBcc = async () => { newAddrView('', true, bccViews, bccBtn, bccCell, bccRow); };
|
||||||
const cmdReplyTo = async () => { newAddrView('', replytoViews, replyToBtn, replyToCell, replyToRow, true); };
|
const cmdReplyTo = async () => { newAddrView('', false, replytoViews, replyToBtn, replyToCell, replyToRow, true); };
|
||||||
const cmdCustomFrom = async () => {
|
const cmdCustomFrom = async () => {
|
||||||
if (customFrom) {
|
if (customFrom) {
|
||||||
return;
|
return;
|
||||||
|
@ -2063,7 +2065,7 @@ const compose = (opts) => {
|
||||||
'ctrl Y': cmdReplyTo,
|
'ctrl Y': cmdReplyTo,
|
||||||
// ctrl - and ctrl = (+) not included, they are handled by keydown handlers on in the inputs they remove/add.
|
// ctrl - and ctrl = (+) not included, they are handled by keydown handlers on in the inputs they remove/add.
|
||||||
};
|
};
|
||||||
const newAddrView = (addr, views, btn, cell, row, single) => {
|
const newAddrView = (addr, isRecipient, views, btn, cell, row, single) => {
|
||||||
if (single && views.length !== 0) {
|
if (single && views.length !== 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -2096,11 +2098,13 @@ const compose = (opts) => {
|
||||||
}
|
}
|
||||||
return '#aaa';
|
return '#aaa';
|
||||||
};
|
};
|
||||||
const setBar = (c0, c1, c2) => {
|
const setBar = (c0, c1, c2, c3, c4) => {
|
||||||
const stops = [
|
const stops = [
|
||||||
c0 + ' 0%', c0 + ' 32%', 'white 32%', 'white 33%',
|
c0 + ' 0%', c0 + ' 19%', 'white 19%', 'white 20%',
|
||||||
c1 + ' 33%', c1 + ' 66%', 'white 66%', 'white 67%',
|
c1 + ' 20%', c1 + ' 39%', 'white 39%', 'white 40%',
|
||||||
c2 + ' 67%', c2 + ' 100%',
|
c2 + ' 40%', c2 + ' 59%', 'white 59%', 'white 60%',
|
||||||
|
c3 + ' 60%', c3 + ' 79%', 'white 79%', 'white 80%',
|
||||||
|
c4 + ' 80%', c4 + ' 100%',
|
||||||
].join(', ');
|
].join(', ');
|
||||||
securityBar.style.borderImage = 'linear-gradient(to right, ' + stops + ') 1';
|
securityBar.style.borderImage = 'linear-gradient(to right, ' + stops + ') 1';
|
||||||
};
|
};
|
||||||
|
@ -2108,19 +2112,56 @@ const compose = (opts) => {
|
||||||
rcptSecAborter = aborter;
|
rcptSecAborter = aborter;
|
||||||
rcptSecPromise = client.withOptions({ aborter: aborter }).RecipientSecurity(inputElem.value);
|
rcptSecPromise = client.withOptions({ aborter: aborter }).RecipientSecurity(inputElem.value);
|
||||||
rcptSecPromise.then((rs) => {
|
rcptSecPromise.then((rs) => {
|
||||||
setBar(color(rs.MTASTS), color(rs.DNSSEC), color(rs.DANE));
|
setBar(color(rs.STARTTLS), color(rs.MTASTS), color(rs.DNSSEC), color(rs.DANE), color(rs.RequireTLS));
|
||||||
|
const implemented = [];
|
||||||
|
const check = (v, s) => {
|
||||||
|
if (v) {
|
||||||
|
implemented.push(s);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
check(rs.STARTTLS === api.SecurityResult.SecurityResultYes, 'STARTTLS');
|
||||||
|
check(rs.MTASTS === api.SecurityResult.SecurityResultYes, 'MTASTS');
|
||||||
|
check(rs.DNSSEC === api.SecurityResult.SecurityResultYes, 'DNSSEC');
|
||||||
|
check(rs.DANE === api.SecurityResult.SecurityResultYes, 'DANE');
|
||||||
|
check(rs.RequireTLS === api.SecurityResult.SecurityResultYes, 'RequireTLS');
|
||||||
|
const status = 'Security mechanisms known to be implemented by the recipient domain: ' + (implemented.length === 0 ? '(none)' : implemented.join(', ')) + '.';
|
||||||
|
inputElem.setAttribute('title', status + '\n\n' + recipientSecurityTitle);
|
||||||
aborter.abort = undefined;
|
aborter.abort = undefined;
|
||||||
|
v.recipientSecurity = rs;
|
||||||
|
if (isRecipient) {
|
||||||
|
// If all recipients implement REQUIRETLS, we can enable it.
|
||||||
|
let reqtls = true;
|
||||||
|
const walk = (l) => {
|
||||||
|
for (const v of l) {
|
||||||
|
if (v.recipientSecurity?.RequireTLS !== api.SecurityResult.SecurityResultYes) {
|
||||||
|
reqtls = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk(toViews);
|
||||||
|
walk(ccViews);
|
||||||
|
walk(bccViews);
|
||||||
|
if (requiretls.value === '' || requiretls.value === 'yes') {
|
||||||
|
requiretls.value = reqtls ? 'yes' : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
}, () => {
|
}, () => {
|
||||||
setBar('#888', '#888', '#888');
|
setBar('#888', '#888', '#888', '#888', '#888');
|
||||||
|
inputElem.setAttribute('title', 'Error fetching security mechanisms known to be implemented by the recipient domain...\n\n' + recipientSecurityTitle);
|
||||||
aborter.abort = undefined;
|
aborter.abort = undefined;
|
||||||
|
if (requiretls.value === 'yes') {
|
||||||
|
requiretls.value = '';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const root = dom.span(autosizeElem = dom.span(dom._class('autosize'), inputElem = dom.input(focusPlaceholder('Jane <jane@example.org>'), style({ width: 'auto' }), attr.value(addr), newAddressComplete(), attr.title('The bars below the input field indicate security features of the recipient (domain):\n1. Delivery with STARTTLS and MTA-STS (PKIX/WebPKI) enforced.\n2. MX lookup resulted in DNSSEC-signed response.\n3. First delivery destination host has DANE, so STARTTLS is required.\n\nColors:\n- Red, not implemented/unsupported\n- Green, implemented/supported\n- Gray, error while determining\n- Absent/white, unknown or skipped (e.g. dane check skipped due to dnssec-lookup error)'), function keydown(e) {
|
const recipientSecurityTitle = 'Description of security mechanisms recipient domains may implement:\n1. STARTTLS: Opportunistic (unverified) TLS with STARTTLS, successfully negotiated during the most recent delivery attempt.\n2. MTA-STS: For PKIX/WebPKI-verified TLS.\n3. DNSSEC: MX DNS records are DNSSEC-signed.\n4. DANE: First delivery destination host implements DANE for verified TLS.\n5. RequireTLS: SMTP extension for verified TLS delivery into recipient mailbox, support detected during the most recent delivery attempt.\n\nChecks STARTTLS, DANE and RequireTLS cover the most recently used delivery path, not necessarily all possible delivery paths.\n\nThe bars below the input field indicate implementation status by the recipient domain:\n- Red, not implemented/unsupported\n- Green, implemented/supported\n- Gray, error while determining\n- Absent/white, unknown or skipped (e.g. no previous delivery attempt, or DANE check skipped due to DNSSEC-lookup error)';
|
||||||
|
const root = dom.span(autosizeElem = dom.span(dom._class('autosize'), inputElem = dom.input(focusPlaceholder('Jane <jane@example.org>'), style({ width: 'auto' }), attr.value(addr), newAddressComplete(), attr.title(recipientSecurityTitle), function keydown(e) {
|
||||||
if (e.key === '-' && e.ctrlKey) {
|
if (e.key === '-' && e.ctrlKey) {
|
||||||
remove();
|
remove();
|
||||||
}
|
}
|
||||||
else if (e.key === '=' && e.ctrlKey) {
|
else if (e.key === '=' && e.ctrlKey) {
|
||||||
newAddrView('', views, btn, cell, row, single);
|
newAddrView('', isRecipient, views, btn, cell, row, single);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
|
@ -2143,7 +2184,6 @@ const compose = (opts) => {
|
||||||
}
|
}
|
||||||
}), ' ');
|
}), ' ');
|
||||||
autosizeElem.dataset.value = inputElem.value;
|
autosizeElem.dataset.value = inputElem.value;
|
||||||
fetchRecipientSecurity();
|
|
||||||
const remove = () => {
|
const remove = () => {
|
||||||
const i = views.indexOf(v);
|
const i = views.indexOf(v);
|
||||||
views.splice(i, 1);
|
views.splice(i, 1);
|
||||||
|
@ -2170,7 +2210,8 @@ const compose = (opts) => {
|
||||||
next.focus();
|
next.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const v = { root: root, input: inputElem };
|
const v = { root: root, input: inputElem, isRecipient: isRecipient, recipientSecurity: null };
|
||||||
|
fetchRecipientSecurity();
|
||||||
views.push(v);
|
views.push(v);
|
||||||
cell.appendChild(v.root);
|
cell.appendChild(v.root);
|
||||||
row.style.display = '';
|
row.style.display = '';
|
||||||
|
@ -2255,16 +2296,16 @@ const compose = (opts) => {
|
||||||
return v;
|
return v;
|
||||||
}), dom.label(style({ color: '#666' }), dom.input(attr.type('checkbox'), function change(e) {
|
}), dom.label(style({ color: '#666' }), dom.input(attr.type('checkbox'), function change(e) {
|
||||||
forwardAttachmentViews.forEach(v => v.checkbox.checked = e.target.checked);
|
forwardAttachmentViews.forEach(v => v.checkbox.checked = e.target.checked);
|
||||||
}), ' (Toggle all)')), noAttachmentsWarning = dom.div(style({ display: 'none', backgroundColor: '#fcd284', padding: '0.15em .25em', margin: '.5em 0' }), 'Message mentions attachments, but no files are attached.'), dom.div(style({ margin: '1ex 0' }), 'Attachments ', attachments = dom.input(attr.type('file'), attr.multiple(''), function change() { checkAttachments(); })), dom.submitbutton('Send')), async function submit(e) {
|
}), ' (Toggle all)')), noAttachmentsWarning = dom.div(style({ display: 'none', backgroundColor: '#fcd284', padding: '0.15em .25em', margin: '.5em 0' }), 'Message mentions attachments, but no files are attached.'), dom.label(style({ margin: '1ex 0', display: 'block' }), 'Attachments ', attachments = dom.input(attr.type('file'), attr.multiple(''), function change() { checkAttachments(); })), dom.label(style({ margin: '1ex 0', display: 'block' }), attr.title('How to use TLS for message delivery over SMTP:\n\nDefault: Delivery attempts follow the policies published by the recipient domain: Verification with MTA-STS and/or DANE, or optional opportunistic unverified STARTTLS if the domain does not specify a policy.\n\nWith RequireTLS: For sensitive messages, you may want to require verified TLS. The recipient destination domain SMTP server must support the REQUIRETLS SMTP extension for delivery to succeed. It is automatically chosen when the destination domain mail servers of all recipients are known to support it.\n\nFallback to insecure: If delivery fails due to MTA-STS and/or DANE policies specified by the recipient domain, and the content is not sensitive, you may choose to ignore the recipient domain TLS policies so delivery can succeed.'), 'TLS ', requiretls = dom.select(dom.option(attr.value(''), 'Default'), dom.option(attr.value('yes'), 'With RequireTLS'), dom.option(attr.value('no'), 'Fallback to insecure'))), dom.div(style({ margin: '3ex 0 1ex 0', display: 'block' }), dom.submitbutton('Send'))), async function submit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
shortcutCmd(cmdSend, shortcuts);
|
shortcutCmd(cmdSend, shortcuts);
|
||||||
}));
|
}));
|
||||||
subjectAutosize.dataset.value = subject.value;
|
subjectAutosize.dataset.value = subject.value;
|
||||||
(opts.to && opts.to.length > 0 ? opts.to : ['']).forEach(s => newAddrView(s, toViews, toBtn, toCell, toRow));
|
(opts.to && opts.to.length > 0 ? opts.to : ['']).forEach(s => newAddrView(s, true, toViews, toBtn, toCell, toRow));
|
||||||
(opts.cc || []).forEach(s => newAddrView(s, ccViews, ccBtn, ccCell, ccRow));
|
(opts.cc || []).forEach(s => newAddrView(s, true, ccViews, ccBtn, ccCell, ccRow));
|
||||||
(opts.bcc || []).forEach(s => newAddrView(s, bccViews, bccBtn, bccCell, bccRow));
|
(opts.bcc || []).forEach(s => newAddrView(s, true, bccViews, bccBtn, bccCell, bccRow));
|
||||||
if (opts.replyto) {
|
if (opts.replyto) {
|
||||||
newAddrView(opts.replyto, replytoViews, replyToBtn, replyToCell, replyToRow, true);
|
newAddrView(opts.replyto, false, replytoViews, replyToBtn, replyToCell, replyToRow, true);
|
||||||
}
|
}
|
||||||
if (!opts.cc || !opts.cc.length) {
|
if (!opts.cc || !opts.cc.length) {
|
||||||
ccRow.style.display = 'none';
|
ccRow.style.display = 'none';
|
||||||
|
|
|
@ -52,7 +52,7 @@ To simulate slow API calls and SSE events:
|
||||||
|
|
||||||
Show additional headers of messages:
|
Show additional headers of messages:
|
||||||
|
|
||||||
settingsPut({...settings, showHeaders: ['User-Agent', 'X-Mailer', 'Message-Id', 'List-Id', 'List-Post', 'X-Mox-Reason']})
|
settingsPut({...settings, showHeaders: ['User-Agent', 'X-Mailer', 'Message-Id', 'List-Id', 'List-Post', 'X-Mox-Reason', 'TLS-Required']})
|
||||||
|
|
||||||
Enable logging and reload afterwards:
|
Enable logging and reload afterwards:
|
||||||
|
|
||||||
|
@ -1154,6 +1154,8 @@ const compose = (opts: ComposeOptions) => {
|
||||||
type AddrView = {
|
type AddrView = {
|
||||||
root: HTMLElement
|
root: HTMLElement
|
||||||
input: HTMLInputElement
|
input: HTMLInputElement
|
||||||
|
isRecipient: boolean
|
||||||
|
recipientSecurity: null | api.RecipientSecurity
|
||||||
}
|
}
|
||||||
|
|
||||||
let fieldset: HTMLFieldSetElement
|
let fieldset: HTMLFieldSetElement
|
||||||
|
@ -1163,6 +1165,7 @@ const compose = (opts: ComposeOptions) => {
|
||||||
let subject: HTMLInputElement
|
let subject: HTMLInputElement
|
||||||
let body: HTMLTextAreaElement
|
let body: HTMLTextAreaElement
|
||||||
let attachments: HTMLInputElement
|
let attachments: HTMLInputElement
|
||||||
|
let requiretls: HTMLSelectElement
|
||||||
|
|
||||||
let toBtn: HTMLButtonElement, ccBtn: HTMLButtonElement, bccBtn: HTMLButtonElement, replyToBtn: HTMLButtonElement, customFromBtn: HTMLButtonElement
|
let toBtn: HTMLButtonElement, ccBtn: HTMLButtonElement, bccBtn: HTMLButtonElement, replyToBtn: HTMLButtonElement, customFromBtn: HTMLButtonElement
|
||||||
let replyToCell: HTMLElement, toCell: HTMLElement, ccCell: HTMLElement, bccCell: HTMLElement // Where we append new address views.
|
let replyToCell: HTMLElement, toCell: HTMLElement, ccCell: HTMLElement, bccCell: HTMLElement // Where we append new address views.
|
||||||
|
@ -1217,6 +1220,7 @@ const compose = (opts: ComposeOptions) => {
|
||||||
ForwardAttachments: forwardAttachmentPaths.length === 0 ? {MessageID: 0, Paths: []} : {MessageID: opts.attachmentsMessageItem!.Message.ID, Paths: forwardAttachmentPaths},
|
ForwardAttachments: forwardAttachmentPaths.length === 0 ? {MessageID: 0, Paths: []} : {MessageID: opts.attachmentsMessageItem!.Message.ID, Paths: forwardAttachmentPaths},
|
||||||
IsForward: opts.isForward || false,
|
IsForward: opts.isForward || false,
|
||||||
ResponseMessageID: opts.responseMessageID || 0,
|
ResponseMessageID: opts.responseMessageID || 0,
|
||||||
|
RequireTLS: requiretls.value === '' ? null : requiretls.value === 'yes',
|
||||||
}
|
}
|
||||||
await client.MessageSubmit(message)
|
await client.MessageSubmit(message)
|
||||||
cmdCancel()
|
cmdCancel()
|
||||||
|
@ -1226,10 +1230,10 @@ const compose = (opts: ComposeOptions) => {
|
||||||
await withStatus('Sending email', submit(), fieldset)
|
await withStatus('Sending email', submit(), fieldset)
|
||||||
}
|
}
|
||||||
|
|
||||||
const cmdAddTo = async () => { newAddrView('', toViews, toBtn, toCell, toRow) }
|
const cmdAddTo = async () => { newAddrView('', true, toViews, toBtn, toCell, toRow) }
|
||||||
const cmdAddCc = async () => { newAddrView('', ccViews, ccBtn, ccCell, ccRow) }
|
const cmdAddCc = async () => { newAddrView('', true, ccViews, ccBtn, ccCell, ccRow) }
|
||||||
const cmdAddBcc = async () => { newAddrView('', bccViews, bccBtn, bccCell, bccRow) }
|
const cmdAddBcc = async () => { newAddrView('', true, bccViews, bccBtn, bccCell, bccRow) }
|
||||||
const cmdReplyTo = async () => { newAddrView('', replytoViews, replyToBtn, replyToCell, replyToRow, true) }
|
const cmdReplyTo = async () => { newAddrView('', false, replytoViews, replyToBtn, replyToCell, replyToRow, true) }
|
||||||
const cmdCustomFrom = async () => {
|
const cmdCustomFrom = async () => {
|
||||||
if (customFrom) {
|
if (customFrom) {
|
||||||
return
|
return
|
||||||
|
@ -1249,7 +1253,7 @@ const compose = (opts: ComposeOptions) => {
|
||||||
// ctrl - and ctrl = (+) not included, they are handled by keydown handlers on in the inputs they remove/add.
|
// ctrl - and ctrl = (+) not included, they are handled by keydown handlers on in the inputs they remove/add.
|
||||||
}
|
}
|
||||||
|
|
||||||
const newAddrView = (addr: string, views: AddrView[], btn: HTMLButtonElement, cell: HTMLElement, row: HTMLElement, single?: boolean) => {
|
const newAddrView = (addr: string, isRecipient: boolean, views: AddrView[], btn: HTMLButtonElement, cell: HTMLElement, row: HTMLElement, single?: boolean) => {
|
||||||
if (single && views.length !== 0) {
|
if (single && views.length !== 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1285,11 +1289,13 @@ const compose = (opts: ComposeOptions) => {
|
||||||
}
|
}
|
||||||
return '#aaa'
|
return '#aaa'
|
||||||
}
|
}
|
||||||
const setBar = (c0: string, c1: string, c2: string) => {
|
const setBar = (c0: string, c1: string, c2: string, c3: string, c4: string) => {
|
||||||
const stops = [
|
const stops = [
|
||||||
c0 + ' 0%', c0 + ' 32%', 'white 32%', 'white 33%',
|
c0 + ' 0%', c0 + ' 19%', 'white 19%', 'white 20%',
|
||||||
c1 + ' 33%', c1 + ' 66%', 'white 66%', 'white 67%',
|
c1 + ' 20%', c1 + ' 39%', 'white 39%', 'white 40%',
|
||||||
c2 + ' 67%', c2 + ' 100%',
|
c2 + ' 40%', c2 + ' 59%', 'white 59%', 'white 60%',
|
||||||
|
c3 + ' 60%', c3 + ' 79%', 'white 79%', 'white 80%',
|
||||||
|
c4 + ' 80%', c4 + ' 100%',
|
||||||
].join(', ')
|
].join(', ')
|
||||||
securityBar.style.borderImage = 'linear-gradient(to right, ' + stops + ') 1'
|
securityBar.style.borderImage = 'linear-gradient(to right, ' + stops + ') 1'
|
||||||
}
|
}
|
||||||
|
@ -1298,14 +1304,53 @@ const compose = (opts: ComposeOptions) => {
|
||||||
rcptSecAborter = aborter
|
rcptSecAborter = aborter
|
||||||
rcptSecPromise = client.withOptions({aborter: aborter}).RecipientSecurity(inputElem.value)
|
rcptSecPromise = client.withOptions({aborter: aborter}).RecipientSecurity(inputElem.value)
|
||||||
rcptSecPromise.then((rs) => {
|
rcptSecPromise.then((rs) => {
|
||||||
setBar(color(rs.MTASTS), color(rs.DNSSEC), color(rs.DANE))
|
setBar(color(rs.STARTTLS), color(rs.MTASTS), color(rs.DNSSEC), color(rs.DANE), color(rs.RequireTLS))
|
||||||
|
|
||||||
|
const implemented: string[] = []
|
||||||
|
const check = (v: boolean, s: string) => {
|
||||||
|
if (v) {
|
||||||
|
implemented.push(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check(rs.STARTTLS === api.SecurityResult.SecurityResultYes, 'STARTTLS')
|
||||||
|
check(rs.MTASTS === api.SecurityResult.SecurityResultYes, 'MTASTS')
|
||||||
|
check(rs.DNSSEC === api.SecurityResult.SecurityResultYes, 'DNSSEC')
|
||||||
|
check(rs.DANE === api.SecurityResult.SecurityResultYes, 'DANE')
|
||||||
|
check(rs.RequireTLS === api.SecurityResult.SecurityResultYes, 'RequireTLS')
|
||||||
|
const status = 'Security mechanisms known to be implemented by the recipient domain: '+ (implemented.length === 0 ? '(none)' : implemented.join(', ')) + '.'
|
||||||
|
inputElem.setAttribute('title', status+'\n\n'+recipientSecurityTitle)
|
||||||
|
|
||||||
aborter.abort = undefined
|
aborter.abort = undefined
|
||||||
|
v.recipientSecurity = rs
|
||||||
|
if (isRecipient) {
|
||||||
|
// If all recipients implement REQUIRETLS, we can enable it.
|
||||||
|
let reqtls = true
|
||||||
|
const walk = (l: AddrView[]) => {
|
||||||
|
for (const v of l) {
|
||||||
|
if (v.recipientSecurity?.RequireTLS !== api.SecurityResult.SecurityResultYes) {
|
||||||
|
reqtls = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(toViews)
|
||||||
|
walk(ccViews)
|
||||||
|
walk(bccViews)
|
||||||
|
if (requiretls.value === '' || requiretls.value === 'yes') {
|
||||||
|
requiretls.value = reqtls ? 'yes' : ''
|
||||||
|
}
|
||||||
|
}
|
||||||
}, () => {
|
}, () => {
|
||||||
setBar('#888', '#888', '#888')
|
setBar('#888', '#888', '#888', '#888', '#888')
|
||||||
|
inputElem.setAttribute('title', 'Error fetching security mechanisms known to be implemented by the recipient domain...\n\n'+recipientSecurityTitle)
|
||||||
aborter.abort = undefined
|
aborter.abort = undefined
|
||||||
|
if (requiretls.value === 'yes') {
|
||||||
|
requiretls.value = ''
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const recipientSecurityTitle = 'Description of security mechanisms recipient domains may implement:\n1. STARTTLS: Opportunistic (unverified) TLS with STARTTLS, successfully negotiated during the most recent delivery attempt.\n2. MTA-STS: For PKIX/WebPKI-verified TLS.\n3. DNSSEC: MX DNS records are DNSSEC-signed.\n4. DANE: First delivery destination host implements DANE for verified TLS.\n5. RequireTLS: SMTP extension for verified TLS delivery into recipient mailbox, support detected during the most recent delivery attempt.\n\nChecks STARTTLS, DANE and RequireTLS cover the most recently used delivery path, not necessarily all possible delivery paths.\n\nThe bars below the input field indicate implementation status by the recipient domain:\n- Red, not implemented/unsupported\n- Green, implemented/supported\n- Gray, error while determining\n- Absent/white, unknown or skipped (e.g. no previous delivery attempt, or DANE check skipped due to DNSSEC-lookup error)'
|
||||||
const root = dom.span(
|
const root = dom.span(
|
||||||
autosizeElem=dom.span(
|
autosizeElem=dom.span(
|
||||||
dom._class('autosize'),
|
dom._class('autosize'),
|
||||||
|
@ -1314,12 +1359,12 @@ const compose = (opts: ComposeOptions) => {
|
||||||
style({width: 'auto'}),
|
style({width: 'auto'}),
|
||||||
attr.value(addr),
|
attr.value(addr),
|
||||||
newAddressComplete(),
|
newAddressComplete(),
|
||||||
attr.title('The bars below the input field indicate security features of the recipient (domain):\n1. Delivery with STARTTLS and MTA-STS (PKIX/WebPKI) enforced.\n2. MX lookup resulted in DNSSEC-signed response.\n3. First delivery destination host has DANE, so STARTTLS is required.\n\nColors:\n- Red, not implemented/unsupported\n- Green, implemented/supported\n- Gray, error while determining\n- Absent/white, unknown or skipped (e.g. dane check skipped due to dnssec-lookup error)'),
|
attr.title(recipientSecurityTitle),
|
||||||
function keydown(e: KeyboardEvent) {
|
function keydown(e: KeyboardEvent) {
|
||||||
if (e.key === '-' && e.ctrlKey) {
|
if (e.key === '-' && e.ctrlKey) {
|
||||||
remove()
|
remove()
|
||||||
} else if (e.key === '=' && e.ctrlKey) {
|
} else if (e.key === '=' && e.ctrlKey) {
|
||||||
newAddrView('', views, btn, cell, row, single)
|
newAddrView('', isRecipient, views, btn, cell, row, single)
|
||||||
} else {
|
} else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1353,7 +1398,6 @@ const compose = (opts: ComposeOptions) => {
|
||||||
' ',
|
' ',
|
||||||
)
|
)
|
||||||
autosizeElem.dataset.value = inputElem.value
|
autosizeElem.dataset.value = inputElem.value
|
||||||
fetchRecipientSecurity()
|
|
||||||
|
|
||||||
const remove = () => {
|
const remove = () => {
|
||||||
const i = views.indexOf(v)
|
const i = views.indexOf(v)
|
||||||
|
@ -1383,7 +1427,10 @@ const compose = (opts: ComposeOptions) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const v: AddrView = {root: root, input: inputElem}
|
const v: AddrView = {root: root, input: inputElem, isRecipient: isRecipient, recipientSecurity: null}
|
||||||
|
|
||||||
|
fetchRecipientSecurity()
|
||||||
|
|
||||||
views.push(v)
|
views.push(v)
|
||||||
cell.appendChild(v.root)
|
cell.appendChild(v.root)
|
||||||
row.style.display = ''
|
row.style.display = ''
|
||||||
|
@ -1541,8 +1588,21 @@ const compose = (opts: ComposeOptions) => {
|
||||||
}), ' (Toggle all)')
|
}), ' (Toggle all)')
|
||||||
),
|
),
|
||||||
noAttachmentsWarning=dom.div(style({display: 'none', backgroundColor: '#fcd284', padding: '0.15em .25em', margin: '.5em 0'}), 'Message mentions attachments, but no files are attached.'),
|
noAttachmentsWarning=dom.div(style({display: 'none', backgroundColor: '#fcd284', padding: '0.15em .25em', margin: '.5em 0'}), 'Message mentions attachments, but no files are attached.'),
|
||||||
dom.div(style({margin: '1ex 0'}), 'Attachments ', attachments=dom.input(attr.type('file'), attr.multiple(''), function change() { checkAttachments() })),
|
dom.label(style({margin: '1ex 0', display: 'block'}), 'Attachments ', attachments=dom.input(attr.type('file'), attr.multiple(''), function change() { checkAttachments() })),
|
||||||
dom.submitbutton('Send'),
|
dom.label(
|
||||||
|
style({margin: '1ex 0', display: 'block'}),
|
||||||
|
attr.title('How to use TLS for message delivery over SMTP:\n\nDefault: Delivery attempts follow the policies published by the recipient domain: Verification with MTA-STS and/or DANE, or optional opportunistic unverified STARTTLS if the domain does not specify a policy.\n\nWith RequireTLS: For sensitive messages, you may want to require verified TLS. The recipient destination domain SMTP server must support the REQUIRETLS SMTP extension for delivery to succeed. It is automatically chosen when the destination domain mail servers of all recipients are known to support it.\n\nFallback to insecure: If delivery fails due to MTA-STS and/or DANE policies specified by the recipient domain, and the content is not sensitive, you may choose to ignore the recipient domain TLS policies so delivery can succeed.'),
|
||||||
|
'TLS ',
|
||||||
|
requiretls=dom.select(
|
||||||
|
dom.option(attr.value(''), 'Default'),
|
||||||
|
dom.option(attr.value('yes'), 'With RequireTLS'),
|
||||||
|
dom.option(attr.value('no'), 'Fallback to insecure'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dom.div(
|
||||||
|
style({margin: '3ex 0 1ex 0', display: 'block'}),
|
||||||
|
dom.submitbutton('Send'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
async function submit(e: SubmitEvent) {
|
async function submit(e: SubmitEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
@ -1553,11 +1613,11 @@ const compose = (opts: ComposeOptions) => {
|
||||||
|
|
||||||
subjectAutosize.dataset.value = subject.value
|
subjectAutosize.dataset.value = subject.value
|
||||||
|
|
||||||
;(opts.to && opts.to.length > 0 ? opts.to : ['']).forEach(s => newAddrView(s, toViews, toBtn, toCell, toRow))
|
;(opts.to && opts.to.length > 0 ? opts.to : ['']).forEach(s => newAddrView(s, true, toViews, toBtn, toCell, toRow))
|
||||||
;(opts.cc || []).forEach(s => newAddrView(s, ccViews, ccBtn, ccCell, ccRow))
|
;(opts.cc || []).forEach(s => newAddrView(s,true, ccViews, ccBtn, ccCell, ccRow))
|
||||||
;(opts.bcc || []).forEach(s => newAddrView(s, bccViews, bccBtn, bccCell, bccRow))
|
;(opts.bcc || []).forEach(s => newAddrView(s, true, bccViews, bccBtn, bccCell, bccRow))
|
||||||
if (opts.replyto) {
|
if (opts.replyto) {
|
||||||
newAddrView(opts.replyto, replytoViews, replyToBtn, replyToCell, replyToRow, true)
|
newAddrView(opts.replyto, false, replytoViews, replyToBtn, replyToCell, replyToRow, true)
|
||||||
}
|
}
|
||||||
if (!opts.cc || !opts.cc.length) {
|
if (!opts.cc || !opts.cc.length) {
|
||||||
ccRow.style.display = 'none'
|
ccRow.style.display = 'none'
|
||||||
|
|
Loading…
Reference in a new issue