Compare commits

..

No commits in common. "main" and "v0.0.15" have entirely different histories.

34 changed files with 181 additions and 654 deletions

View File

@ -212,7 +212,7 @@ func DomainRecords(domConf config.Domain, domain dns.Domain, hasDNSSEC bool, cer
records = append(records, records = append(records,
"; Remote servers can use MTA-STS to verify our TLS certificate with the", "; Remote servers can use MTA-STS to verify our TLS certificate with the",
"; WebPKI pool of CA's (certificate authorities) when delivering over SMTP with", "; WebPKI pool of CA's (certificate authorities) when delivering over SMTP with",
"; STARTTLS.", "; STARTTLSTLS.",
fmt.Sprintf(`mta-sts.%s. CNAME %s.`, d, h), fmt.Sprintf(`mta-sts.%s. CNAME %s.`, d, h),
fmt.Sprintf(`_mta-sts.%s. TXT "v=STSv1; id=%s"`, d, sts.PolicyID), fmt.Sprintf(`_mta-sts.%s. TXT "v=STSv1; id=%s"`, d, sts.PolicyID),
"", "",

View File

@ -1,5 +0,0 @@
Below are the incompatible changes between v0.0.15 and next, per package.
# smtpclient
- GatherDestinations: changed from func(context.Context, *log/slog.Logger, github.com/mjl-/mox/dns.Resolver, github.com/mjl-/mox/dns.IPDomain) (bool, bool, bool, github.com/mjl-/mox/dns.Domain, []github.com/mjl-/mox/dns.IPDomain, bool, error) to func(context.Context, *log/slog.Logger, github.com/mjl-/mox/dns.Resolver, github.com/mjl-/mox/dns.IPDomain) (bool, bool, bool, github.com/mjl-/mox/dns.Domain, []HostPref, bool, error)

View File

@ -389,7 +389,7 @@ func xbackupctl(ctx context.Context, xctl *ctl) {
srcAcmeDir := filepath.Join(srcDataDir, "acme") srcAcmeDir := filepath.Join(srcDataDir, "acme")
if _, err := os.Stat(srcAcmeDir); err == nil { if _, err := os.Stat(srcAcmeDir); err == nil {
backupDir("acme") backupDir("acme")
} else if !os.IsNotExist(err) { } else if err != nil && !os.IsNotExist(err) {
xerrx("copying acme/", err) xerrx("copying acme/", err)
} }

View File

@ -209,14 +209,12 @@ type Listener struct {
NonTLS bool `sconf:"optional" sconf-doc:"If set, plain HTTP instead of HTTPS is spoken on the configured port. Can be useful when the mta-sts domain is reverse proxied."` NonTLS bool `sconf:"optional" sconf-doc:"If set, plain HTTP instead of HTTPS is spoken on the configured port. Can be useful when the mta-sts domain is reverse proxied."`
} `sconf:"optional" sconf-doc:"Serve MTA-STS policies describing SMTP TLS requirements. Requires a TLS config."` } `sconf:"optional" sconf-doc:"Serve MTA-STS policies describing SMTP TLS requirements. Requires a TLS config."`
WebserverHTTP struct { WebserverHTTP struct {
Enabled bool Enabled bool
Port int `sconf:"optional" sconf-doc:"Port for plain HTTP (non-TLS) webserver."` Port int `sconf:"optional" sconf-doc:"Port for plain HTTP (non-TLS) webserver."`
RateLimitDisabled bool `sconf:"optional" sconf-doc:"Disable rate limiting for all requests to this port."`
} `sconf:"optional" sconf-doc:"All configured WebHandlers will serve on an enabled listener."` } `sconf:"optional" sconf-doc:"All configured WebHandlers will serve on an enabled listener."`
WebserverHTTPS struct { WebserverHTTPS struct {
Enabled bool Enabled bool
Port int `sconf:"optional" sconf-doc:"Port for HTTPS webserver."` Port int `sconf:"optional" sconf-doc:"Port for HTTPS webserver."`
RateLimitDisabled bool `sconf:"optional" sconf-doc:"Disable rate limiting for all requests to this port."`
} `sconf:"optional" sconf-doc:"All configured WebHandlers will serve on an enabled listener. Either ACME must be configured, or for each WebHandler domain a TLS certificate must be configured."` } `sconf:"optional" sconf-doc:"All configured WebHandlers will serve on an enabled listener. Either ACME must be configured, or for each WebHandler domain a TLS certificate must be configured."`
} }
@ -237,7 +235,6 @@ type Transport struct {
SMTP *TransportSMTP `sconf:"optional" sconf-doc:"SMTP over a plain connection (possibly with STARTTLS), typically for old-fashioned unauthenticated relaying to a remote queue."` SMTP *TransportSMTP `sconf:"optional" sconf-doc:"SMTP over a plain connection (possibly with STARTTLS), typically for old-fashioned unauthenticated relaying to a remote queue."`
Socks *TransportSocks `sconf:"optional" sconf-doc:"Like regular direct delivery, but makes outgoing connections through a SOCKS proxy."` Socks *TransportSocks `sconf:"optional" sconf-doc:"Like regular direct delivery, but makes outgoing connections through a SOCKS proxy."`
Direct *TransportDirect `sconf:"optional" sconf-doc:"Like regular direct delivery, but allows to tweak outgoing connections."` Direct *TransportDirect `sconf:"optional" sconf-doc:"Like regular direct delivery, but allows to tweak outgoing connections."`
Fail *TransportFail `sconf:"optional" sconf-doc:"Immediately fails the delivery attempt."`
} }
// TransportSMTP delivers messages by "submission" (SMTP, typically // TransportSMTP delivers messages by "submission" (SMTP, typically
@ -281,16 +278,6 @@ type TransportDirect struct {
IPFamily string `sconf:"-" json:"-"` IPFamily string `sconf:"-" json:"-"`
} }
// TransportFail is a transport that fails all delivery attempts.
type TransportFail struct {
SMTPCode int `sconf:"optional" sconf-doc:"SMTP error code and optional enhanced error code to use for the failure. If empty, 554 is used (transaction failed)."`
SMTPMessage string `sconf:"optional" sconf-doc:"Message to include for the rejection. It will be shown in the DSN."`
// Effective values to use, set when parsing.
Code int `sconf:"-"`
Message string `sconf:"-"`
}
type Domain struct { type Domain struct {
Disabled bool `sconf:"optional" sconf-doc:"Disabled domains can be useful during/before migrations. Domains that are disabled can still be configured like normal, including adding addresses using the domain to accounts. However, disabled domains: 1. Do not try to fetch ACME certificates. TLS connections to host names involving the email domain will fail. A TLS certificate for the hostname (that wil be used as MX) itself will be requested. 2. Incoming deliveries over SMTP are rejected with a temporary error '450 4.2.1 recipient domain temporarily disabled'. 3. Submissions over SMTP using an (envelope) SMTP MAIL FROM address or message 'From' address of a disabled domain will be rejected with a temporary error '451 4.3.0 sender domain temporarily disabled'. Note that accounts with addresses at disabled domains can still log in and read email (unless the account itself is disabled)."` Disabled bool `sconf:"optional" sconf-doc:"Disabled domains can be useful during/before migrations. Domains that are disabled can still be configured like normal, including adding addresses using the domain to accounts. However, disabled domains: 1. Do not try to fetch ACME certificates. TLS connections to host names involving the email domain will fail. A TLS certificate for the hostname (that wil be used as MX) itself will be requested. 2. Incoming deliveries over SMTP are rejected with a temporary error '450 4.2.1 recipient domain temporarily disabled'. 3. Submissions over SMTP using an (envelope) SMTP MAIL FROM address or message 'From' address of a disabled domain will be rejected with a temporary error '451 4.3.0 sender domain temporarily disabled'. Note that accounts with addresses at disabled domains can still log in and read email (unless the account itself is disabled)."`
Description string `sconf:"optional" sconf-doc:"Free-form description of domain."` Description string `sconf:"optional" sconf-doc:"Free-form description of domain."`
@ -544,7 +531,6 @@ type TLS struct {
KeyCerts []KeyCert `sconf:"optional" sconf-doc:"Keys and certificates to use for this listener. The files are opened by the privileged root process and passed to the unprivileged mox process, so no special permissions are required on the files. If the private key will not be replaced when refreshing certificates, also consider adding the private key to HostPrivateKeyFiles and configuring DANE TLSA DNS records."` KeyCerts []KeyCert `sconf:"optional" sconf-doc:"Keys and certificates to use for this listener. The files are opened by the privileged root process and passed to the unprivileged mox process, so no special permissions are required on the files. If the private key will not be replaced when refreshing certificates, also consider adding the private key to HostPrivateKeyFiles and configuring DANE TLSA DNS records."`
MinVersion string `sconf:"optional" sconf-doc:"Minimum TLS version. Default: TLSv1.2."` MinVersion string `sconf:"optional" sconf-doc:"Minimum TLS version. Default: TLSv1.2."`
HostPrivateKeyFiles []string `sconf:"optional" sconf-doc:"Private keys used for ACME certificates. Specified explicitly so DANE TLSA DNS records can be generated, even before the certificates are requested. DANE is a mechanism to authenticate remote TLS certificates based on a public key or certificate specified in DNS, protected with DNSSEC. DANE is opportunistic and attempted when delivering SMTP with STARTTLS. The private key files must be in PEM format. PKCS8 is recommended, but PKCS1 and EC private keys are recognized as well. Only RSA 2048 bit and ECDSA P-256 keys are currently used. The first of each is used when requesting new certificates through ACME."` HostPrivateKeyFiles []string `sconf:"optional" sconf-doc:"Private keys used for ACME certificates. Specified explicitly so DANE TLSA DNS records can be generated, even before the certificates are requested. DANE is a mechanism to authenticate remote TLS certificates based on a public key or certificate specified in DNS, protected with DNSSEC. DANE is opportunistic and attempted when delivering SMTP with STARTTLS. The private key files must be in PEM format. PKCS8 is recommended, but PKCS1 and EC private keys are recognized as well. Only RSA 2048 bit and ECDSA P-256 keys are currently used. The first of each is used when requesting new certificates through ACME."`
ClientAuthDisabled bool `sconf:"optional" sconf-doc:"Disable TLS client authentication with certificates/keys, preventing the TLS server from requesting a TLS certificate from clients. Useful for working around clients that don't handle TLS client authentication well."`
Config *tls.Config `sconf:"-" json:"-"` // TLS config for non-ACME-verification connections, i.e. SMTP and IMAP, and not port 443. Connections without SNI will use a certificate for the hostname of the listener, connections with an SNI hostname that isn't allowed will be rejected. Config *tls.Config `sconf:"-" json:"-"` // TLS config for non-ACME-verification connections, i.e. SMTP and IMAP, and not port 443. Connections without SNI will use a certificate for the hostname of the listener, connections with an SNI hostname that isn't allowed will be rejected.
ConfigFallback *tls.Config `sconf:"-" json:"-"` // Like Config, but uses the certificate for the listener hostname when the requested SNI hostname is not allowed, instead of causing the connection to fail. ConfigFallback *tls.Config `sconf:"-" json:"-"` // Like Config, but uses the certificate for the listener hostname when the requested SNI hostname is not allowed, instead of causing the connection to fail.

View File

@ -217,11 +217,6 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
HostPrivateKeyFiles: HostPrivateKeyFiles:
- -
# Disable TLS client authentication with certificates/keys, preventing the TLS
# server from requesting a TLS certificate from clients. Useful for working around
# clients that don't handle TLS client authentication well. (optional)
ClientAuthDisabled: false
# Maximum size in bytes for incoming and outgoing messages. Default is 100MB. # Maximum size in bytes for incoming and outgoing messages. Default is 100MB.
# (optional) # (optional)
SMTPMaxMessageSize: 0 SMTPMaxMessageSize: 0
@ -519,9 +514,6 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
# Port for plain HTTP (non-TLS) webserver. (optional) # Port for plain HTTP (non-TLS) webserver. (optional)
Port: 0 Port: 0
# Disable rate limiting for all requests to this port. (optional)
RateLimitDisabled: false
# All configured WebHandlers will serve on an enabled listener. Either ACME must # All configured WebHandlers will serve on an enabled listener. Either ACME must
# be configured, or for each WebHandler domain a TLS certificate must be # be configured, or for each WebHandler domain a TLS certificate must be
# configured. (optional) # configured. (optional)
@ -531,9 +523,6 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
# Port for HTTPS webserver. (optional) # Port for HTTPS webserver. (optional)
Port: 0 Port: 0
# Disable rate limiting for all requests to this port. (optional)
RateLimitDisabled: false
# Destination for emails delivered to postmaster addresses: a plain 'postmaster' # Destination for emails delivered to postmaster addresses: a plain 'postmaster'
# without domain, 'postmaster@<hostname>' (also for each listener with SMTP # without domain, 'postmaster@<hostname>' (also for each listener with SMTP
# enabled), and as fallback for each domain without explicitly configured # enabled), and as fallback for each domain without explicitly configured
@ -736,16 +725,6 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
# remote SMTP servers. (optional) # remote SMTP servers. (optional)
DisableIPv6: false DisableIPv6: false
# Immediately fails the delivery attempt. (optional)
Fail:
# SMTP error code and optional enhanced error code to use for the failure. If
# empty, 554 is used (transaction failed). (optional)
SMTPCode: 0
# Message to include for the rejection. It will be shown in the DSN. (optional)
SMTPMessage:
# Do not send DMARC reports (aggregate only). By default, aggregate reports on # Do not send DMARC reports (aggregate only). By default, aggregate reports on
# DMARC evaluations are sent to domains if their DMARC policy requests them. # DMARC evaluations are sent to domains if their DMARC policy requests them.
# Reports are sent at whole hours, with a minimum of 1 hour and maximum of 24 # Reports are sent at whole hours, with a minimum of 1 hour and maximum of 24

View File

@ -1,14 +0,0 @@
//go:build !go1.24
package main
import (
"crypto/tls"
)
var curvesList = []tls.CurveID{
tls.CurveP256,
tls.CurveP384,
tls.CurveP521,
tls.X25519,
}

View File

@ -1,15 +0,0 @@
//go:build go1.24
package main
import (
"crypto/tls"
)
var curvesList = []tls.CurveID{
tls.CurveP256,
tls.CurveP384,
tls.CurveP521,
tls.X25519,
tls.X25519MLKEM768,
}

View File

@ -10,7 +10,7 @@
// looked up with an DNS "A" lookup of a name similar to an IPv4 address, but with // looked up with an DNS "A" lookup of a name similar to an IPv4 address, but with
// 4-bit hexadecimal dot-separated characters, in reverse. // 4-bit hexadecimal dot-separated characters, in reverse.
// //
// The health of a DNSBL "zone" can be checked through a lookup of 127.0.0.1 // The health of a DNSBL "zone" can be check through a lookup of 127.0.0.1
// (must not be present) and 127.0.0.2 (must be present). // (must not be present) and 127.0.0.2 (must be present).
package dnsbl package dnsbl

46
doc.go
View File

@ -114,7 +114,6 @@ any parameters. Followed by the help and usage information for each command.
mox rdap domainage domain mox rdap domainage domain
mox retrain [accountname] mox retrain [accountname]
mox sendmail [-Fname] [ignoredflags] [-t] [<message] mox sendmail [-Fname] [ignoredflags] [-t] [<message]
mox smtp dial host[:port]
mox spf check domain ip mox spf check domain ip
mox spf lookup domain mox spf lookup domain
mox spf parse txtrecord mox spf parse txtrecord
@ -1528,51 +1527,6 @@ binary should be setgid that group:
usage: mox sendmail [-Fname] [ignoredflags] [-t] [<message] usage: mox sendmail [-Fname] [ignoredflags] [-t] [<message]
# mox smtp dial
Dial the address, initialize the SMTP session, including using STARTTLS to enable TLS if the server supports it.
If no port is specified, SMTP port 25 is used.
Data is copied between connection and stdin/stdout until either side closes the
connection.
The flags influence the TLS configuration, useful for debugging interoperability
issues.
No MTA-STS or DANE verification is done.
Hint: Use "mox -loglevel trace smtp dial ..." to see the protocol messages
exchanged during connection set up.
usage: mox smtp dial host[:port]
-ehlohostname string
our hostname to use during the SMTP EHLO command
-forcetls
use TLS, even if remote SMTP server does not announce STARTTLS extension
-notls
do not use TLS
-remotehostname string
remote hostname to use for TLS verification, if enabled; the hostname from the parameter is used by default
-tlscerts string
path to root ca certificates in pem form, for verification
-tlsciphersuites string
ciphersuites to allow, comma-separated, order is ignored, only for TLS 1.2 and earlier, empty value uses TLS stack defaults; values: tls_ecdhe_ecdsa_with_aes_128_cbc_sha, tls_ecdhe_ecdsa_with_aes_128_gcm_sha256, tls_ecdhe_ecdsa_with_aes_256_cbc_sha, tls_ecdhe_ecdsa_with_aes_256_gcm_sha384, tls_ecdhe_ecdsa_with_chacha20_poly1305_sha256, tls_ecdhe_rsa_with_aes_128_cbc_sha, tls_ecdhe_rsa_with_aes_128_gcm_sha256, tls_ecdhe_rsa_with_aes_256_cbc_sha, tls_ecdhe_rsa_with_aes_256_gcm_sha384, tls_ecdhe_rsa_with_chacha20_poly1305_sha256, and insecure: tls_ecdhe_ecdsa_with_aes_128_cbc_sha256, tls_ecdhe_ecdsa_with_rc4_128_sha, tls_ecdhe_rsa_with_3des_ede_cbc_sha, tls_ecdhe_rsa_with_aes_128_cbc_sha256, tls_ecdhe_rsa_with_rc4_128_sha, tls_rsa_with_3des_ede_cbc_sha, tls_rsa_with_aes_128_cbc_sha, tls_rsa_with_aes_128_cbc_sha256, tls_rsa_with_aes_128_gcm_sha256, tls_rsa_with_aes_256_cbc_sha, tls_rsa_with_aes_256_gcm_sha384, tls_rsa_with_rc4_128_sha
-tlscurves string
tls ecc key exchange mechanisms to allow, comma-separated, order is ignored, empty value uses TLS stack defaults; values: curvep256, curvep384, curvep521, x25519, x25519mlkem768
-tlsnodynamicrecordsizing
disable TLS dynamic record sizing
-tlsnosessiontickets
disable TLS session tickets
-tlsrenegotiation string
when to allow renegotiation; only applies to tls1.2 and earlier, not tls1.3; values: never, once, always (default "never")
-tlsverify
verify remote hostname during TLS
-tlsversionmax string
maximum TLS version, empty value uses TLS stack default; values: tls1.2, etc.
-tlsversionmin string
minimum TLS version, empty value uses TLS stack default; values: tls1.2, etc.
# mox spf check # mox spf check
Check the status of IP for the policy published in DNS for the domain. Check the status of IP for the policy published in DNS for the domain.

View File

@ -1,12 +1,13 @@
version: '3.7'
services: services:
mox: mox:
build: build:
context: . context: .
dockerfile: Dockerfile.moximaptest dockerfile: Dockerfile.moximaptest
volumes: volumes:
- ./testdata/imaptest/config:/mox/config:z - ./testdata/imaptest/config:/mox/config
- ./testdata/imaptest/data:/mox/data:z - ./testdata/imaptest/data:/mox/data
- ./testdata/imaptest/imaptest.mbox:/mox/imaptest.mbox:z - ./testdata/imaptest/imaptest.mbox:/mox/imaptest.mbox
working_dir: /mox working_dir: /mox
tty: true # For job control with set -m. tty: true # For job control with set -m.
command: sh -c 'set -m; mox serve & sleep 1; echo testtest | mox setaccountpassword mjl; fg' command: sh -c 'set -m; mox serve & sleep 1; echo testtest | mox setaccountpassword mjl; fg'
@ -23,7 +24,7 @@ services:
command: host=mox port=1143 'user=mjl@mox.example' pass=testtest mbox=/imaptest/imaptest.mbox command: host=mox port=1143 'user=mjl@mox.example' pass=testtest mbox=/imaptest/imaptest.mbox
working_dir: /imaptest working_dir: /imaptest
volumes: volumes:
- ./testdata/imaptest:/imaptest:z - ./testdata/imaptest:/imaptest
depends_on: depends_on:
mox: mox:
condition: service_healthy condition: service_healthy

View File

@ -1,3 +1,4 @@
version: '3.7'
services: services:
# We run integration_test.go from this container, it connects to the other mox instances. # We run integration_test.go from this container, it connects to the other mox instances.
test: test:
@ -8,11 +9,11 @@ services:
# dials in integration_test.go succeed. # dials in integration_test.go succeed.
command: ["sh", "-c", "set -ex; cat /integration/tmp-pebble-ca.pem /integration/tls/ca.pem >>/etc/ssl/certs/ca-certificates.crt; go test -tags integration"] command: ["sh", "-c", "set -ex; cat /integration/tmp-pebble-ca.pem /integration/tls/ca.pem >>/etc/ssl/certs/ca-certificates.crt; go test -tags integration"]
volumes: volumes:
- ./.go:/.go:z - ./.go:/.go
- ./testdata/integration/resolv.conf:/etc/resolv.conf:z - ./testdata/integration/resolv.conf:/etc/resolv.conf
- ./testdata/integration:/integration:z - ./testdata/integration:/integration
- ./testdata/integration/moxsubmit.conf:/etc/moxsubmit.conf:z - ./testdata/integration/moxsubmit.conf:/etc/moxsubmit.conf
- .:/mox:z - .:/mox
environment: environment:
GOCACHE: /.go/.cache/go-build GOCACHE: /.go/.cache/go-build
depends_on: depends_on:
@ -40,8 +41,8 @@ services:
MOX_UID: "${MOX_UID}" MOX_UID: "${MOX_UID}"
command: ["sh", "-c", "/integration/moxacmepebble.sh"] command: ["sh", "-c", "/integration/moxacmepebble.sh"]
volumes: volumes:
- ./testdata/integration/resolv.conf:/etc/resolv.conf:z - ./testdata/integration/resolv.conf:/etc/resolv.conf
- ./testdata/integration:/integration:z - ./testdata/integration:/integration
healthcheck: healthcheck:
test: netstat -nlt | grep ':25 ' test: netstat -nlt | grep ':25 '
interval: 1s interval: 1s
@ -65,8 +66,8 @@ services:
MOX_UID: "${MOX_UID}" MOX_UID: "${MOX_UID}"
command: ["sh", "-c", "/integration/moxmail2.sh"] command: ["sh", "-c", "/integration/moxmail2.sh"]
volumes: volumes:
- ./testdata/integration/resolv.conf:/etc/resolv.conf:z - ./testdata/integration/resolv.conf:/etc/resolv.conf
- ./testdata/integration:/integration:z - ./testdata/integration:/integration
healthcheck: healthcheck:
test: netstat -nlt | grep ':25 ' test: netstat -nlt | grep ':25 '
interval: 1s interval: 1s
@ -93,8 +94,8 @@ services:
MOX_UID: "${MOX_UID}" MOX_UID: "${MOX_UID}"
command: ["sh", "-c", "/integration/moxacmepebblealpn.sh"] command: ["sh", "-c", "/integration/moxacmepebblealpn.sh"]
volumes: volumes:
- ./testdata/integration/resolv.conf:/etc/resolv.conf:z - ./testdata/integration/resolv.conf:/etc/resolv.conf
- ./testdata/integration:/integration:z - ./testdata/integration:/integration
healthcheck: healthcheck:
test: netstat -nlt | grep ':25 ' test: netstat -nlt | grep ':25 '
interval: 1s interval: 1s
@ -115,9 +116,9 @@ services:
image: mox_integration_moxmail image: mox_integration_moxmail
command: ["sh", "-c", "set -e; chmod o+r /etc/resolv.conf; mox -checkconsistency localserve -ip 172.28.1.60"] command: ["sh", "-c", "set -e; chmod o+r /etc/resolv.conf; mox -checkconsistency localserve -ip 172.28.1.60"]
volumes: volumes:
- ./.go:/.go:z - ./.go:/.go
- ./testdata/integration/resolv.conf:/etc/resolv.conf:z - ./testdata/integration/resolv.conf:/etc/resolv.conf
- .:/mox:z - .:/mox
environment: environment:
GOCACHE: /.go/.cache/go-build GOCACHE: /.go/.cache/go-build
healthcheck: healthcheck:
@ -140,7 +141,7 @@ services:
context: testdata/integration context: testdata/integration
volumes: volumes:
# todo: figure out how to mount files with a uid that the process in the container can read... # todo: figure out how to mount files with a uid that the process in the container can read...
- ./testdata/integration/resolv.conf:/etc/resolv.conf:z - ./testdata/integration/resolv.conf:/etc/resolv.conf
command: ["sh", "-c", "set -e; chmod o+r /etc/resolv.conf; (echo 'maillog_file = /dev/stdout'; echo 'mydestination = $$myhostname, localhost.$$mydomain, localhost, $$mydomain'; echo 'smtp_tls_security_level = may') >>/etc/postfix/main.cf; echo 'root: postfix@mox1.example' >>/etc/postfix/aliases; newaliases; postfix start-fg"] command: ["sh", "-c", "set -e; chmod o+r /etc/resolv.conf; (echo 'maillog_file = /dev/stdout'; echo 'mydestination = $$myhostname, localhost.$$mydomain, localhost, $$mydomain'; echo 'smtp_tls_security_level = may') >>/etc/postfix/main.cf; echo 'root: postfix@mox1.example' >>/etc/postfix/aliases; newaliases; postfix start-fg"]
healthcheck: healthcheck:
test: netstat -nlt | grep ':25 ' test: netstat -nlt | grep ':25 '
@ -161,8 +162,8 @@ services:
# todo: figure out how to build from dockerfile with empty context without creating empty dirs in file system. # todo: figure out how to build from dockerfile with empty context without creating empty dirs in file system.
context: testdata/integration context: testdata/integration
volumes: volumes:
- ./testdata/integration/resolv.conf:/etc/resolv.conf:z - ./testdata/integration/resolv.conf:/etc/resolv.conf
- ./testdata/integration:/integration:z - ./testdata/integration:/integration
# We start with a base example.zone, but moxacmepebble appends its records, # We start with a base example.zone, but moxacmepebble appends its records,
# followed by moxmail2. They restart unbound after appending records. # followed by moxmail2. They restart unbound after appending records.
command: ["sh", "-c", "set -ex; ls -l /etc/resolv.conf; chmod o+r /etc/resolv.conf; install -m 640 -o unbound /integration/unbound.conf /etc/unbound/; chmod 755 /integration; chmod 644 /integration/*.zone; cp /integration/example.zone /integration/example-integration.zone; ls -ld /integration /integration/reverse.zone; unbound -d -p -v"] command: ["sh", "-c", "set -ex; ls -l /etc/resolv.conf; chmod o+r /etc/resolv.conf; install -m 640 -o unbound /integration/unbound.conf /etc/unbound/; chmod 755 /integration; chmod 644 /integration/*.zone; cp /integration/example.zone /integration/example-integration.zone; ls -ld /integration /integration/reverse.zone; unbound -d -p -v"]
@ -182,8 +183,8 @@ services:
hostname: acmepebble.example hostname: acmepebble.example
image: docker.io/letsencrypt/pebble:v2.3.1@sha256:fc5a537bf8fbc7cc63aa24ec3142283aa9b6ba54529f86eb8ff31fbde7c5b258 image: docker.io/letsencrypt/pebble:v2.3.1@sha256:fc5a537bf8fbc7cc63aa24ec3142283aa9b6ba54529f86eb8ff31fbde7c5b258
volumes: volumes:
- ./testdata/integration/resolv.conf:/etc/resolv.conf:z - ./testdata/integration/resolv.conf:/etc/resolv.conf
- ./testdata/integration:/integration:z - ./testdata/integration:/integration
command: ["sh", "-c", "set -ex; mount; ls -l /etc/resolv.conf; chmod o+r /etc/resolv.conf; pebble -config /integration/pebble-config.json"] command: ["sh", "-c", "set -ex; mount; ls -l /etc/resolv.conf; chmod o+r /etc/resolv.conf; pebble -config /integration/pebble-config.json"]
ports: ports:
- 14000:14000 # ACME port - 14000:14000 # ACME port

View File

@ -27,6 +27,7 @@
# The -ip flag ensures connections to the published ports make it to mox, and it # The -ip flag ensures connections to the published ports make it to mox, and it
# prevents listening on ::1 (IPv6 is not enabled in docker by default). # prevents listening on ::1 (IPv6 is not enabled in docker by default).
version: '3.7'
services: services:
mox: mox:
# Replace "latest" with the version you want to run, see https://r.xmox.nl/r/mox/. # Replace "latest" with the version you want to run, see https://r.xmox.nl/r/mox/.
@ -38,11 +39,11 @@ services:
# machine, and the IPs of incoming connections for spam filtering. # machine, and the IPs of incoming connections for spam filtering.
network_mode: 'host' network_mode: 'host'
volumes: volumes:
- ./config:/mox/config:z - ./config:/mox/config
- ./data:/mox/data:z - ./data:/mox/data
# web is optional but recommended to bind in, useful for serving static files with # web is optional but recommended to bind in, useful for serving static files with
# the webserver. # the webserver.
- ./web:/mox/web:z - ./web:/mox/web
working_dir: /mox working_dir: /mox
restart: on-failure restart: on-failure
healthcheck: healthcheck:

View File

@ -352,7 +352,7 @@ func (w *loggingWriter) Done() {
slog.Any("remoteaddr", w.R.RemoteAddr), slog.Any("remoteaddr", w.R.RemoteAddr),
slog.String("tlsinfo", tlsinfo), slog.String("tlsinfo", tlsinfo),
slog.String("useragent", w.R.Header.Get("User-Agent")), slog.String("useragent", w.R.Header.Get("User-Agent")),
slog.String("referer", w.R.Header.Get("Referer")), slog.String("referrr", w.R.Header.Get("Referrer")),
} }
if w.WebsocketRequest { if w.WebsocketRequest {
attrs = append(attrs, attrs = append(attrs,
@ -386,14 +386,11 @@ type pathHandler struct {
Path string // Path to register, like on http.ServeMux. Path string // Path to register, like on http.ServeMux.
Handler http.Handler Handler http.Handler
} }
type serve struct { type serve struct {
Kinds []string // Type of handler and protocol (e.g. acme-tls-alpn-01, account-http, admin-https, imap-https, smtp-https). Kinds []string // Type of handler and protocol (e.g. acme-tls-alpn-01, account-http, admin-https, imap-https, smtp-https).
TLSConfig *tls.Config TLSConfig *tls.Config
NextProto tlsNextProtoMap // For HTTP server, when we do submission/imap with ALPN over the HTTPS port. NextProto tlsNextProtoMap // For HTTP server, when we do submission/imap with ALPN over the HTTPS port.
Favicon bool Favicon bool
Forwarded bool // Requests are coming from a reverse proxy, we'll use X-Forwarded-For for the IP address to ratelimit.
RateLimitDisabled bool // Don't apply ratelimiting.
// SystemHandlers are for MTA-STS, autoconfig, ACME validation. They can't be // SystemHandlers are for MTA-STS, autoconfig, ACME validation. They can't be
// overridden by WebHandlers. WebHandlers are evaluated next, and the internal // overridden by WebHandlers. WebHandlers are evaluated next, and the internal
@ -440,41 +437,23 @@ var (
// metrics. // metrics.
func (s *serve) ServeHTTP(xw http.ResponseWriter, r *http.Request) { func (s *serve) ServeHTTP(xw http.ResponseWriter, r *http.Request) {
now := time.Now() now := time.Now()
// Rate limiting as early as possible.
ipstr, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
pkglog.Debugx("split host:port client remoteaddr", err, slog.Any("remoteaddr", r.RemoteAddr))
} else if ip := net.ParseIP(ipstr); ip == nil {
pkglog.Debug("parsing ip for client remoteaddr", slog.Any("remoteaddr", r.RemoteAddr))
} else if !limiterConnectionrate.Add(ip, now, 1) {
method := metricHTTPMethod(r.Method)
proto := "http"
if r.TLS != nil {
proto = "https"
}
metricRequest.WithLabelValues("(ratelimited)", proto, method, "429").Observe(0)
// No logging, that's just noise.
// Rate limiting as early as possible, if enabled. http.Error(xw, "429 - too many auth attempts", http.StatusTooManyRequests)
if !s.RateLimitDisabled { return
// If requests are coming from a reverse proxy, use the IP from X-Forwarded-For.
// Otherwise the remote IP for this connection.
var ipstr string
if s.Forwarded {
s := r.Header.Get("X-Forwarded-For")
ipstr = strings.TrimSpace(strings.Split(s, ",")[0])
if ipstr == "" {
pkglog.Debug("ratelimit: no ip address in X-Forwarded-For header")
}
} else {
var err error
ipstr, _, err = net.SplitHostPort(r.RemoteAddr)
if err != nil {
pkglog.Debugx("ratelimit: parsing remote address", err, slog.String("remoteaddr", r.RemoteAddr))
}
}
ip := net.ParseIP(ipstr)
if ip == nil && ipstr != "" {
pkglog.Debug("ratelimit: invalid ip", slog.String("ip", ipstr))
}
if ip != nil && !limiterConnectionrate.Add(ip, now, 1) {
method := metricHTTPMethod(r.Method)
proto := "http"
if r.TLS != nil {
proto = "https"
}
metricRequest.WithLabelValues("(ratelimited)", proto, method, "429").Observe(0)
// No logging, that's just noise.
http.Error(xw, "429 - too many auth attempts", http.StatusTooManyRequests)
return
}
} }
ctx := context.WithValue(r.Context(), mlog.CidKey, mox.Cid()) ctx := context.WithValue(r.Context(), mlog.CidKey, mox.Cid())
@ -604,11 +583,11 @@ func portServes(name string, l config.Listener) map[int]*serve {
return mox.Conf.IsClientSettingsDomain(host.Domain) return mox.Conf.IsClientSettingsDomain(host.Domain)
} }
var ensureServe func(https, forwarded, noRateLimiting bool, port int, kind string, favicon bool) *serve var ensureServe func(https bool, port int, kind string, favicon bool) *serve
ensureServe = func(https, forwarded, rateLimitDisabled bool, port int, kind string, favicon bool) *serve { ensureServe = func(https bool, port int, kind string, favicon bool) *serve {
s := portServe[port] s := portServe[port]
if s == nil { if s == nil {
s = &serve{nil, nil, tlsNextProtoMap{}, false, false, false, nil, false, nil} s = &serve{nil, nil, tlsNextProtoMap{}, false, nil, false, nil}
portServe[port] = s portServe[port] = s
} }
s.Kinds = append(s.Kinds, kind) s.Kinds = append(s.Kinds, kind)
@ -616,8 +595,6 @@ func portServes(name string, l config.Listener) map[int]*serve {
s.ServiceHandle("favicon", accountHostMatch, "/favicon.ico", mox.SafeHeaders(http.HandlerFunc(faviconHandle))) s.ServiceHandle("favicon", accountHostMatch, "/favicon.ico", mox.SafeHeaders(http.HandlerFunc(faviconHandle)))
s.Favicon = true s.Favicon = true
} }
s.Forwarded = s.Forwarded || forwarded
s.RateLimitDisabled = s.RateLimitDisabled || rateLimitDisabled
// We clone TLS configs because we may modify it later on for this server, for // We clone TLS configs because we may modify it later on for this server, for
// ALPN. And we need copies because multiple listeners on http.Server where the // ALPN. And we need copies because multiple listeners on http.Server where the
@ -627,7 +604,7 @@ func portServes(name string, l config.Listener) map[int]*serve {
tlsport := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443) tlsport := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
if portServe[tlsport] == nil || !slices.Contains(portServe[tlsport].Kinds, "acme-tls-alpn-01") { if portServe[tlsport] == nil || !slices.Contains(portServe[tlsport].Kinds, "acme-tls-alpn-01") {
ensureServe(true, false, false, tlsport, "acme-tls-alpn-01", false) ensureServe(true, tlsport, "acme-tls-alpn-01", false)
} }
} else if https { } else if https {
s.TLSConfig = l.TLS.Config.Clone() s.TLSConfig = l.TLS.Config.Clone()
@ -647,10 +624,10 @@ func portServes(name string, l config.Listener) map[int]*serve {
if l.TLS != nil && l.TLS.ACME != "" && (l.SMTP.Enabled && !l.SMTP.NoSTARTTLS || l.Submissions.Enabled || l.IMAPS.Enabled) { if l.TLS != nil && l.TLS.ACME != "" && (l.SMTP.Enabled && !l.SMTP.NoSTARTTLS || l.Submissions.Enabled || l.IMAPS.Enabled) {
port := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443) port := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
ensureServe(true, false, false, port, "acme-tls-alpn-01", false) ensureServe(true, port, "acme-tls-alpn-01", false)
} }
if l.Submissions.Enabled && l.Submissions.EnabledOnHTTPS { if l.Submissions.Enabled && l.Submissions.EnabledOnHTTPS {
s := ensureServe(true, false, false, 443, "smtp-https", false) s := ensureServe(true, 443, "smtp-https", false)
hostname := mox.Conf.Static.HostnameDomain hostname := mox.Conf.Static.HostnameDomain
if l.Hostname != "" { if l.Hostname != "" {
hostname = l.HostnameDomain hostname = l.HostnameDomain
@ -667,7 +644,7 @@ func portServes(name string, l config.Listener) map[int]*serve {
} }
} }
if l.IMAPS.Enabled && l.IMAPS.EnabledOnHTTPS { if l.IMAPS.Enabled && l.IMAPS.EnabledOnHTTPS {
s := ensureServe(true, false, false, 443, "imap-https", false) s := ensureServe(true, 443, "imap-https", false)
s.NextProto["imap"] = func(_ *http.Server, conn *tls.Conn, _ http.Handler) { s.NextProto["imap"] = func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
imapserver.ServeTLSConn(name, conn, s.TLSConfig) imapserver.ServeTLSConn(name, conn, s.TLSConfig)
} }
@ -678,7 +655,7 @@ func portServes(name string, l config.Listener) map[int]*serve {
if l.AccountHTTP.Path != "" { if l.AccountHTTP.Path != "" {
path = l.AccountHTTP.Path path = l.AccountHTTP.Path
} }
srv := ensureServe(false, l.AccountHTTP.Forwarded, false, port, "account-http at "+path, true) srv := ensureServe(false, port, "account-http at "+path, true)
handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webaccount.Handler(path, l.AccountHTTP.Forwarded)))) handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webaccount.Handler(path, l.AccountHTTP.Forwarded))))
srv.ServiceHandle("account", accountHostMatch, path, handler) srv.ServiceHandle("account", accountHostMatch, path, handler)
redirectToTrailingSlash(srv, accountHostMatch, "account", path) redirectToTrailingSlash(srv, accountHostMatch, "account", path)
@ -690,7 +667,7 @@ func portServes(name string, l config.Listener) map[int]*serve {
if l.AccountHTTPS.Path != "" { if l.AccountHTTPS.Path != "" {
path = l.AccountHTTPS.Path path = l.AccountHTTPS.Path
} }
srv := ensureServe(true, l.AccountHTTPS.Forwarded, false, port, "account-https at "+path, true) srv := ensureServe(true, port, "account-https at "+path, true)
handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webaccount.Handler(path, l.AccountHTTPS.Forwarded)))) handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webaccount.Handler(path, l.AccountHTTPS.Forwarded))))
srv.ServiceHandle("account", accountHostMatch, path, handler) srv.ServiceHandle("account", accountHostMatch, path, handler)
redirectToTrailingSlash(srv, accountHostMatch, "account", path) redirectToTrailingSlash(srv, accountHostMatch, "account", path)
@ -702,7 +679,7 @@ func portServes(name string, l config.Listener) map[int]*serve {
if l.AdminHTTP.Path != "" { if l.AdminHTTP.Path != "" {
path = l.AdminHTTP.Path path = l.AdminHTTP.Path
} }
srv := ensureServe(false, l.AdminHTTP.Forwarded, false, port, "admin-http at "+path, true) srv := ensureServe(false, port, "admin-http at "+path, true)
handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webadmin.Handler(path, l.AdminHTTP.Forwarded)))) handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webadmin.Handler(path, l.AdminHTTP.Forwarded))))
srv.ServiceHandle("admin", listenerHostMatch, path, handler) srv.ServiceHandle("admin", listenerHostMatch, path, handler)
redirectToTrailingSlash(srv, listenerHostMatch, "admin", path) redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
@ -714,7 +691,7 @@ func portServes(name string, l config.Listener) map[int]*serve {
if l.AdminHTTPS.Path != "" { if l.AdminHTTPS.Path != "" {
path = l.AdminHTTPS.Path path = l.AdminHTTPS.Path
} }
srv := ensureServe(true, l.AdminHTTPS.Forwarded, false, port, "admin-https at "+path, true) srv := ensureServe(true, port, "admin-https at "+path, true)
handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webadmin.Handler(path, l.AdminHTTPS.Forwarded)))) handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webadmin.Handler(path, l.AdminHTTPS.Forwarded))))
srv.ServiceHandle("admin", listenerHostMatch, path, handler) srv.ServiceHandle("admin", listenerHostMatch, path, handler)
redirectToTrailingSlash(srv, listenerHostMatch, "admin", path) redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
@ -731,7 +708,7 @@ func portServes(name string, l config.Listener) map[int]*serve {
if l.WebAPIHTTP.Path != "" { if l.WebAPIHTTP.Path != "" {
path = l.WebAPIHTTP.Path path = l.WebAPIHTTP.Path
} }
srv := ensureServe(false, l.WebAPIHTTP.Forwarded, false, port, "webapi-http at "+path, true) srv := ensureServe(false, port, "webapi-http at "+path, true)
handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTP.Forwarded))) handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTP.Forwarded)))
srv.ServiceHandle("webapi", accountHostMatch, path, handler) srv.ServiceHandle("webapi", accountHostMatch, path, handler)
redirectToTrailingSlash(srv, accountHostMatch, "webapi", path) redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
@ -743,7 +720,7 @@ func portServes(name string, l config.Listener) map[int]*serve {
if l.WebAPIHTTPS.Path != "" { if l.WebAPIHTTPS.Path != "" {
path = l.WebAPIHTTPS.Path path = l.WebAPIHTTPS.Path
} }
srv := ensureServe(true, l.WebAPIHTTPS.Forwarded, false, port, "webapi-https at "+path, true) srv := ensureServe(true, port, "webapi-https at "+path, true)
handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTPS.Forwarded))) handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTPS.Forwarded)))
srv.ServiceHandle("webapi", accountHostMatch, path, handler) srv.ServiceHandle("webapi", accountHostMatch, path, handler)
redirectToTrailingSlash(srv, accountHostMatch, "webapi", path) redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
@ -755,7 +732,7 @@ func portServes(name string, l config.Listener) map[int]*serve {
if l.WebmailHTTP.Path != "" { if l.WebmailHTTP.Path != "" {
path = l.WebmailHTTP.Path path = l.WebmailHTTP.Path
} }
srv := ensureServe(false, l.WebmailHTTP.Forwarded, false, port, "webmail-http at "+path, true) srv := ensureServe(false, port, "webmail-http at "+path, true)
var accountPath string var accountPath string
if l.AccountHTTP.Enabled { if l.AccountHTTP.Enabled {
accountPath = "/" accountPath = "/"
@ -774,7 +751,7 @@ func portServes(name string, l config.Listener) map[int]*serve {
if l.WebmailHTTPS.Path != "" { if l.WebmailHTTPS.Path != "" {
path = l.WebmailHTTPS.Path path = l.WebmailHTTPS.Path
} }
srv := ensureServe(true, l.WebmailHTTPS.Forwarded, false, port, "webmail-https at "+path, true) srv := ensureServe(true, port, "webmail-https at "+path, true)
var accountPath string var accountPath string
if l.AccountHTTPS.Enabled { if l.AccountHTTPS.Enabled {
accountPath = "/" accountPath = "/"
@ -789,7 +766,7 @@ func portServes(name string, l config.Listener) map[int]*serve {
if l.MetricsHTTP.Enabled { if l.MetricsHTTP.Enabled {
port := config.Port(l.MetricsHTTP.Port, 8010) port := config.Port(l.MetricsHTTP.Port, 8010)
srv := ensureServe(false, false, false, port, "metrics-http", false) srv := ensureServe(false, port, "metrics-http", false)
srv.SystemHandle("metrics", nil, "/metrics", mox.SafeHeaders(promhttp.Handler())) srv.SystemHandle("metrics", nil, "/metrics", mox.SafeHeaders(promhttp.Handler()))
srv.SystemHandle("metrics", nil, "/", mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv.SystemHandle("metrics", nil, "/", mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" { if r.URL.Path != "/" {
@ -805,7 +782,7 @@ func portServes(name string, l config.Listener) map[int]*serve {
} }
if l.AutoconfigHTTPS.Enabled { if l.AutoconfigHTTPS.Enabled {
port := config.Port(l.AutoconfigHTTPS.Port, 443) port := config.Port(l.AutoconfigHTTPS.Port, 443)
srv := ensureServe(!l.AutoconfigHTTPS.NonTLS, false, false, port, "autoconfig-https", false) srv := ensureServe(!l.AutoconfigHTTPS.NonTLS, port, "autoconfig-https", false)
if l.AutoconfigHTTPS.NonTLS { if l.AutoconfigHTTPS.NonTLS {
ensureACMEHTTP01(srv) ensureACMEHTTP01(srv)
} }
@ -835,7 +812,7 @@ func portServes(name string, l config.Listener) map[int]*serve {
} }
if l.MTASTSHTTPS.Enabled { if l.MTASTSHTTPS.Enabled {
port := config.Port(l.MTASTSHTTPS.Port, 443) port := config.Port(l.MTASTSHTTPS.Port, 443)
srv := ensureServe(!l.MTASTSHTTPS.NonTLS, false, false, port, "mtasts-https", false) srv := ensureServe(!l.MTASTSHTTPS.NonTLS, port, "mtasts-https", false)
if l.MTASTSHTTPS.NonTLS { if l.MTASTSHTTPS.NonTLS {
ensureACMEHTTP01(srv) ensureACMEHTTP01(srv)
} }
@ -855,19 +832,19 @@ func portServes(name string, l config.Listener) map[int]*serve {
if _, ok := portServe[port]; ok { if _, ok := portServe[port]; ok {
pkglog.Fatal("cannot serve pprof on same endpoint as other http services") pkglog.Fatal("cannot serve pprof on same endpoint as other http services")
} }
srv := &serve{[]string{"pprof-http"}, nil, nil, false, false, false, nil, false, nil} srv := &serve{[]string{"pprof-http"}, nil, nil, false, nil, false, nil}
portServe[port] = srv portServe[port] = srv
srv.SystemHandle("pprof", nil, "/", http.DefaultServeMux) srv.SystemHandle("pprof", nil, "/", http.DefaultServeMux)
} }
if l.WebserverHTTP.Enabled { if l.WebserverHTTP.Enabled {
port := config.Port(l.WebserverHTTP.Port, 80) port := config.Port(l.WebserverHTTP.Port, 80)
srv := ensureServe(false, false, l.WebserverHTTP.RateLimitDisabled, port, "webserver-http", false) srv := ensureServe(false, port, "webserver-http", false)
srv.Webserver = true srv.Webserver = true
ensureACMEHTTP01(srv) ensureACMEHTTP01(srv)
} }
if l.WebserverHTTPS.Enabled { if l.WebserverHTTPS.Enabled {
port := config.Port(l.WebserverHTTPS.Port, 443) port := config.Port(l.WebserverHTTPS.Port, 443)
srv := ensureServe(true, false, l.WebserverHTTPS.RateLimitDisabled, port, "webserver-https", false) srv := ensureServe(true, port, "webserver-https", false)
srv.Webserver = true srv.Webserver = true
} }

View File

@ -362,7 +362,7 @@ func TestAuthenticateTLSClientCert(t *testing.T) {
cid := connCounter cid := connCounter
go func() { go func() {
defer serverConn.Close() defer serverConn.Close()
serve("test", cid, &serverConfig, serverConn, true, false, false, false, "") serve("test", cid, &serverConfig, serverConn, true, false, false, "")
close(done) close(done)
}() }()

View File

@ -144,7 +144,7 @@ func FuzzServer(f *testing.F) {
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, nil, serverConn, false, false, true, false, "") serve("test", cid, nil, serverConn, false, true, false, "")
cid++ cid++
} }

View File

@ -192,10 +192,9 @@ type conn struct {
cid int64 cid int64
state state state state
conn net.Conn conn net.Conn
connBroken bool // Once broken, we won't flush any more data. connBroken bool // Once broken, we won't flush any more data.
tls bool // Whether TLS has been initialized. tls bool // Whether TLS has been initialized.
viaHTTPS bool // Whether this connection came in via HTTPS (using TLS ALPN). viaHTTPS bool // Whether this connection came in via HTTPS (using TLS ALPN).
noTLSClientAuth bool
br *bufio.Reader // From remote, with TLS unwrapped in case of TLS, and possibly wrapping inflate. br *bufio.Reader // From remote, with TLS unwrapped in case of TLS, and possibly wrapping inflate.
tr *moxio.TraceReader // Kept to change trace level when reading/writing cmd/auth/data. tr *moxio.TraceReader // Kept to change trace level when reading/writing cmd/auth/data.
line chan lineErr // If set, instead of reading from br, a line is read from this channel. For reading a line in IDLE while also waiting for mailbox/account updates. line chan lineErr // If set, instead of reading from br, a line is read from this channel. For reading a line in IDLE while also waiting for mailbox/account updates.
@ -385,23 +384,21 @@ func Listen() {
listener := mox.Conf.Static.Listeners[name] listener := mox.Conf.Static.Listeners[name]
var tlsConfig *tls.Config var tlsConfig *tls.Config
var noTLSClientAuth bool
if listener.TLS != nil { if listener.TLS != nil {
tlsConfig = listener.TLS.Config tlsConfig = listener.TLS.Config
noTLSClientAuth = listener.TLS.ClientAuthDisabled
} }
if listener.IMAP.Enabled { if listener.IMAP.Enabled {
port := config.Port(listener.IMAP.Port, 143) port := config.Port(listener.IMAP.Port, 143)
for _, ip := range listener.IPs { for _, ip := range listener.IPs {
listen1("imap", name, ip, port, tlsConfig, false, noTLSClientAuth, listener.IMAP.NoRequireSTARTTLS) listen1("imap", name, ip, port, tlsConfig, false, listener.IMAP.NoRequireSTARTTLS)
} }
} }
if listener.IMAPS.Enabled { if listener.IMAPS.Enabled {
port := config.Port(listener.IMAPS.Port, 993) port := config.Port(listener.IMAPS.Port, 993)
for _, ip := range listener.IPs { for _, ip := range listener.IPs {
listen1("imaps", name, ip, port, tlsConfig, true, noTLSClientAuth, false) listen1("imaps", name, ip, port, tlsConfig, true, false)
} }
} }
} }
@ -409,7 +406,7 @@ func Listen() {
var servers []func() var servers []func()
func listen1(protocol, listenerName, ip string, port int, tlsConfig *tls.Config, xtls, noTLSClientAuth, noRequireSTARTTLS bool) { func listen1(protocol, listenerName, ip string, port int, tlsConfig *tls.Config, xtls, noRequireSTARTTLS bool) {
log := mlog.New("imapserver", nil) log := mlog.New("imapserver", nil)
addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port)) addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
if os.Getuid() == 0 { if os.Getuid() == 0 {
@ -442,7 +439,7 @@ func listen1(protocol, listenerName, ip string, port int, tlsConfig *tls.Config,
} }
metricIMAPConnection.WithLabelValues(protocol).Inc() metricIMAPConnection.WithLabelValues(protocol).Inc()
go serve(listenerName, mox.Cid(), tlsConfig, conn, xtls, noTLSClientAuth, noRequireSTARTTLS, false, "") go serve(listenerName, mox.Cid(), tlsConfig, conn, xtls, noRequireSTARTTLS, false, "")
} }
} }
@ -451,11 +448,11 @@ func listen1(protocol, listenerName, ip string, port int, tlsConfig *tls.Config,
// ServeTLSConn serves IMAP on a TLS connection. // ServeTLSConn serves IMAP on a TLS connection.
func ServeTLSConn(listenerName string, conn *tls.Conn, tlsConfig *tls.Config) { func ServeTLSConn(listenerName string, conn *tls.Conn, tlsConfig *tls.Config) {
serve(listenerName, mox.Cid(), tlsConfig, conn, true, true, false, true, "") serve(listenerName, mox.Cid(), tlsConfig, conn, true, false, true, "")
} }
func ServeConnPreauth(listenerName string, cid int64, conn net.Conn, preauthAddress string) { func ServeConnPreauth(listenerName string, cid int64, conn net.Conn, preauthAddress string) {
serve(listenerName, cid, nil, conn, false, true, true, false, preauthAddress) serve(listenerName, cid, nil, conn, false, true, false, preauthAddress)
} }
// Serve starts serving on all listeners, launching a goroutine per listener. // Serve starts serving on all listeners, launching a goroutine per listener.
@ -769,7 +766,7 @@ var cleanClose struct{} // Sentinel value for panic/recover indicating clean clo
// preauthenticated. // preauthenticated.
// //
// The connection is closed before returning. // The connection is closed before returning.
func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, xtls, noTLSClientAuth, noRequireSTARTTLS, viaHTTPS bool, preauthAddress string) { func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, xtls, noRequireSTARTTLS, viaHTTPS bool, preauthAddress string) {
var remoteIP net.IP var remoteIP net.IP
if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok { if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
remoteIP = a.IP remoteIP = a.IP
@ -783,7 +780,6 @@ func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, x
conn: nc, conn: nc,
tls: xtls, tls: xtls,
viaHTTPS: viaHTTPS, viaHTTPS: viaHTTPS,
noTLSClientAuth: noTLSClientAuth,
lastlog: time.Now(), lastlog: time.Now(),
baseTLSConfig: tlsConfig, baseTLSConfig: tlsConfig,
remoteIP: remoteIP, remoteIP: remoteIP,
@ -994,10 +990,6 @@ func (c *conn) makeTLSConfig() *tls.Config {
// config, so they can be used for this connection too. // config, so they can be used for this connection too.
tlsConf := c.baseTLSConfig.Clone() tlsConf := c.baseTLSConfig.Clone()
if c.noTLSClientAuth {
return tlsConf
}
// Allow client certificate authentication, for use with the sasl "external" // Allow client certificate authentication, for use with the sasl "external"
// authentication mechanism. // authentication mechanism.
tlsConf.ClientAuth = tls.RequestClientCert tlsConf.ClientAuth = tls.RequestClientCert
@ -1097,7 +1089,7 @@ func (c *conn) tlsClientAuthVerifyPeerCertParsed(cert *x509.Certificate) error {
if err == bstore.ErrAbsent { if err == bstore.ErrAbsent {
c.loginAttempt.Result = store.AuthBadCredentials c.loginAttempt.Result = store.AuthBadCredentials
} }
return fmt.Errorf("looking up tls public key with fingerprint %s, subject %q, issuer %q: %v", fp, cert.Subject, cert.Issuer, err) return fmt.Errorf("looking up tls public key with fingerprint %s: %v", fp, err)
} }
c.loginAttempt.LoginAddress = pubKey.LoginAddress c.loginAttempt.LoginAddress = pubKey.LoginAddress
@ -1160,7 +1152,7 @@ func (c *conn) xtlsHandshakeAndAuthenticate(conn net.Conn) {
cancel() cancel()
cs := tlsConn.ConnectionState() cs := tlsConn.ConnectionState()
if cs.DidResume && len(cs.PeerCertificates) > 0 && !c.noTLSClientAuth { if cs.DidResume && len(cs.PeerCertificates) > 0 {
// Verify client after session resumption. // Verify client after session resumption.
err := c.tlsClientAuthVerifyPeerCertParsed(cs.PeerCertificates[0]) err := c.tlsClientAuthVerifyPeerCertParsed(cs.PeerCertificates[0])
if err != nil { if err != nil {
@ -1175,7 +1167,6 @@ func (c *conn) xtlsHandshakeAndAuthenticate(conn net.Conn) {
slog.String("ciphersuite", ciphersuite), slog.String("ciphersuite", ciphersuite),
slog.String("sni", cs.ServerName), slog.String("sni", cs.ServerName),
slog.Bool("resumed", cs.DidResume), slog.Bool("resumed", cs.DidResume),
slog.Bool("notlsclientauth", c.noTLSClientAuth),
slog.Int("clientcerts", len(cs.PeerCertificates)), slog.Int("clientcerts", len(cs.PeerCertificates)),
} }
if c.account != nil { if c.account != nil {
@ -2399,7 +2390,7 @@ func (c *conn) capabilities() string {
} else { } else {
caps += " LOGINDISABLED" caps += " LOGINDISABLED"
} }
if c.tls && len(c.conn.(*tls.Conn).ConnectionState().PeerCertificates) > 0 && !c.viaHTTPS && !c.noTLSClientAuth { if c.tls && len(c.conn.(*tls.Conn).ConnectionState().PeerCertificates) > 0 && !c.viaHTTPS {
caps += " AUTH=EXTERNAL" caps += " AUTH=EXTERNAL"
} }
return caps return caps

View File

@ -556,7 +556,7 @@ func startArgsMore(t *testing.T, uidonly, first, immediateTLS bool, serverConfig
cid := connCounter - 1 cid := connCounter - 1
go func() { go func() {
const viaHTTPS = false const viaHTTPS = false
serve("test", cid, serverConfig, serverConn, immediateTLS, false, allowLoginWithoutTLS, viaHTTPS, "") serve("test", cid, serverConfig, serverConn, immediateTLS, allowLoginWithoutTLS, viaHTTPS, "")
close(done) close(done)
}() }()
var tc *testconn var tc *testconn

243
main.go
View File

@ -12,7 +12,6 @@ import (
"crypto/rsa" "crypto/rsa"
"crypto/sha256" "crypto/sha256"
"crypto/sha512" "crypto/sha512"
"crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
@ -24,7 +23,6 @@ import (
"io/fs" "io/fs"
"log" "log"
"log/slog" "log/slog"
"maps"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
@ -203,7 +201,6 @@ var commands = []struct {
{"rdap domainage", cmdRDAPDomainage}, {"rdap domainage", cmdRDAPDomainage},
{"retrain", cmdRetrain}, {"retrain", cmdRetrain},
{"sendmail", cmdSendmail}, {"sendmail", cmdSendmail},
{"smtp dial", cmdSMTPDial},
{"spf check", cmdSPFCheck}, {"spf check", cmdSPFCheck},
{"spf lookup", cmdSPFLookup}, {"spf lookup", cmdSPFLookup},
{"spf parse", cmdSPFParse}, {"spf parse", cmdSPFParse},
@ -1900,220 +1897,6 @@ with DKIM, by mox.
xcheckf(err, "writing rsa private key") xcheckf(err, "writing rsa private key")
} }
// todo: options for specifying the domain this is the mx host of, and enabling dane and/or mta-sts verification
func cmdSMTPDial(c *cmd) {
c.params = "host[:port]"
var tlsCerts, tlsCiphersuites, tlsCurves, tlsVersionMin, tlsVersionMax, tlsRenegotiation string
var tlsVerify, noTLS, forceTLS, tlsNoSessionTickets, tlsNoDynamicRecordSizing bool
var ehloHostnameStr, remoteHostnameStr string
ciphersuites := map[string]*tls.CipherSuite{}
ciphersuitesInsecure := map[string]*tls.CipherSuite{}
for _, v := range tls.CipherSuites() {
if slices.Contains(v.SupportedVersions, tls.VersionTLS10) || slices.Contains(v.SupportedVersions, tls.VersionTLS11) || slices.Contains(v.SupportedVersions, tls.VersionTLS12) {
ciphersuites[strings.ToLower(v.Name)] = v
}
}
for _, v := range tls.InsecureCipherSuites() {
if slices.Contains(v.SupportedVersions, tls.VersionTLS10) || slices.Contains(v.SupportedVersions, tls.VersionTLS11) || slices.Contains(v.SupportedVersions, tls.VersionTLS12) {
ciphersuitesInsecure[strings.ToLower(v.Name)] = v
}
}
curves := map[string]tls.CurveID{}
for _, a := range curvesList {
curves[strings.ToLower(a.String())] = a
}
c.flag.StringVar(&tlsCiphersuites, "tlsciphersuites", "", "ciphersuites to allow, comma-separated, order is ignored, only for TLS 1.2 and earlier, empty value uses TLS stack defaults; values: "+strings.Join(slices.Sorted(maps.Keys(ciphersuites)), ", ")+", and insecure: "+strings.Join(slices.Sorted(maps.Keys(ciphersuitesInsecure)), ", "))
c.flag.StringVar(&tlsCurves, "tlscurves", "", "tls ecc key exchange mechanisms to allow, comma-separated, order is ignored, empty value uses TLS stack defaults; values: curvep256, curvep384, curvep521, x25519, x25519mlkem768")
c.flag.StringVar(&tlsCerts, "tlscerts", "", "path to root ca certificates in pem form, for verification")
c.flag.StringVar(&tlsVersionMin, "tlsversionmin", "", "minimum TLS version, empty value uses TLS stack default; values: tls1.2, etc.")
c.flag.StringVar(&tlsVersionMax, "tlsversionmax", "", "maximum TLS version, empty value uses TLS stack default; values: tls1.2, etc.")
c.flag.BoolVar(&tlsVerify, "tlsverify", false, "verify remote hostname during TLS")
c.flag.BoolVar(&tlsNoSessionTickets, "tlsnosessiontickets", false, "disable TLS session tickets")
c.flag.BoolVar(&tlsNoDynamicRecordSizing, "tlsnodynamicrecordsizing", false, "disable TLS dynamic record sizing")
c.flag.BoolVar(&noTLS, "notls", false, "do not use TLS")
c.flag.BoolVar(&forceTLS, "forcetls", false, "use TLS, even if remote SMTP server does not announce STARTTLS extension")
c.flag.StringVar(&tlsRenegotiation, "tlsrenegotiation", "never", "when to allow renegotiation; only applies to tls1.2 and earlier, not tls1.3; values: never, once, always")
c.flag.StringVar(&ehloHostnameStr, "ehlohostname", "", "our hostname to use during the SMTP EHLO command")
c.flag.StringVar(&remoteHostnameStr, "remotehostname", "", "remote hostname to use for TLS verification, if enabled; the hostname from the parameter is used by default")
c.help = `Dial the address, initialize the SMTP session, including using STARTTLS to enable TLS if the server supports it.
If no port is specified, SMTP port 25 is used.
Data is copied between connection and stdin/stdout until either side closes the
connection.
The flags influence the TLS configuration, useful for debugging interoperability
issues.
No MTA-STS or DANE verification is done.
Hint: Use "mox -loglevel trace smtp dial ..." to see the protocol messages
exchanged during connection set up.
`
args := c.Parse()
if len(args) != 1 {
c.Usage()
}
if noTLS && forceTLS {
log.Fatalf("cannot have both -notls and -forcetls")
}
parseTLSVersion := func(s string) uint16 {
switch s {
case "tls1.0":
return tls.VersionTLS10
case "tls1.1":
return tls.VersionTLS11
case "tls1.2":
return tls.VersionTLS12
case "tls1.3":
return tls.VersionTLS13
case "":
return 0
default:
log.Fatalf("invalid tls version %q", s)
panic("not reached")
}
}
tlsConfig := tls.Config{
MinVersion: parseTLSVersion(tlsVersionMin),
MaxVersion: parseTLSVersion(tlsVersionMax),
InsecureSkipVerify: !tlsVerify,
SessionTicketsDisabled: tlsNoSessionTickets,
DynamicRecordSizingDisabled: tlsNoDynamicRecordSizing,
}
switch tlsRenegotiation {
case "never":
tlsConfig.Renegotiation = tls.RenegotiateNever
case "once":
tlsConfig.Renegotiation = tls.RenegotiateOnceAsClient
case "always":
tlsConfig.Renegotiation = tls.RenegotiateFreelyAsClient
default:
log.Fatalf("invalid value %q for -tlsrenegotation", tlsRenegotiation)
}
if tlsCerts != "" {
pool := x509.NewCertPool()
pembuf, err := os.ReadFile(tlsCerts)
xcheckf(err, "reading tls certificates")
ok := pool.AppendCertsFromPEM(pembuf)
if !ok {
c.log.Warn("no tls certificates found", slog.String("path", tlsCerts))
}
tlsConfig.RootCAs = pool
}
if tlsCiphersuites != "" {
for _, s := range strings.Split(tlsCiphersuites, ",") {
s = strings.TrimSpace(s)
c, ok := ciphersuites[s]
if !ok {
c, ok = ciphersuitesInsecure[s]
}
if !ok {
log.Fatalf("unknown ciphersuite %q", s)
}
tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, c.ID)
}
}
if tlsCurves != "" {
for _, s := range strings.Split(tlsCurves, ",") {
s = strings.TrimSpace(s)
if c, ok := curves[s]; !ok {
log.Fatalf("unknown ecc key exchange algorithm %q", s)
} else {
tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, c)
}
}
}
var host, portStr string
var err error
host, portStr, err = net.SplitHostPort(args[0])
if err != nil {
host = args[0]
portStr = "25"
}
port, err := strconv.ParseInt(portStr, 10, 64)
xcheckf(err, "parsing port %q", portStr)
if remoteHostnameStr == "" {
remoteHostnameStr = host
}
remoteHostname, err := dns.ParseDomain(remoteHostnameStr)
xcheckf(err, "parsing remote host")
tlsConfig.ServerName = remoteHostname.Name()
resolver := dns.StrictResolver{Pkg: "smtpdial"}
_, _, _, ips, _, err := smtpclient.GatherIPs(context.Background(), c.log.Logger, resolver, "ip", dns.IPDomain{Domain: remoteHostname}, nil)
xcheckf(err, "resolve host")
c.log.Info("resolved remote address", slog.Any("ips", ips))
dialer := &net.Dialer{Timeout: 5 * time.Second}
dialedIPs := map[string][]net.IP{}
conn, ip, err := smtpclient.Dial(context.Background(), c.log.Logger, dialer, dns.IPDomain{Domain: remoteHostname}, ips, int(port), dialedIPs, nil)
xcheckf(err, "dial")
c.log.Info("connected to remote host", slog.Any("ip", ip))
tlsMode := smtpclient.TLSOpportunistic
if forceTLS {
tlsMode = smtpclient.TLSRequiredStartTLS
} else if noTLS {
tlsMode = smtpclient.TLSSkip
}
var ehloHostname dns.Domain
if ehloHostnameStr == "" {
name, err := os.Hostname()
xcheckf(err, "get hostname")
ehloHostnameStr = name
}
ehloHostname, err = dns.ParseDomain(ehloHostnameStr)
xcheckf(err, "parse hostname")
opts := smtpclient.Opts{
TLSConfig: &tlsConfig,
}
client, err := smtpclient.New(context.Background(), c.log.Logger, conn, tlsMode, false, ehloHostname, dns.Domain{}, opts)
xcheckf(err, "new smtp client")
cs := client.TLSConnectionState()
if cs == nil {
c.log.Info("smtp initialized without tls")
} else {
c.log.Info("smtp initialized with tls",
slog.String("version", tls.VersionName(cs.Version)),
slog.String("ciphersuite", strings.ToLower(tls.CipherSuiteName(cs.CipherSuite))),
slog.String("sni", cs.ServerName),
)
for _, chain := range cs.VerifiedChains {
var l []string
for _, cert := range chain {
s := fmt.Sprintf("dns names %q, common name %q, %s - %s, issuer %q)", strings.Join(cert.DNSNames, ","), cert.Subject.CommonName, cert.NotBefore.Format("2006-01-02T15:04:05"), cert.NotAfter.Format("2006-01-02T15:04:05"), cert.Issuer.CommonName)
l = append(l, s)
}
c.log.Info("tls certificate verification chain", slog.String("chain", strings.Join(l, "; ")))
}
}
conn, err = client.Conn()
xcheckf(err, "get smtp session connection")
go func() {
_, err := io.Copy(os.Stdout, conn)
xcheckf(err, "copy from connection to stdout")
err = conn.Close()
c.log.Check(err, "closing connection")
}()
_, err = io.Copy(conn, os.Stdin)
xcheckf(err, "copy from stdin to connection")
}
func cmdDANEDial(c *cmd) { func cmdDANEDial(c *cmd) {
c.params = "host:port" c.params = "host:port"
var usages string var usages string
@ -2204,12 +1987,12 @@ sharing most of its code.
var haveMX bool var haveMX bool
var expandedNextHopAuthentic bool var expandedNextHopAuthentic bool
var expandedNextHop dns.Domain var expandedNextHop dns.Domain
var hostPrefs []smtpclient.HostPref var hosts []dns.IPDomain
if len(args) == 1 { if len(args) == 1 {
var permanent bool var permanent bool
var origNextHopAuthentic bool var origNextHopAuthentic bool
var err error var err error
haveMX, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hostPrefs, permanent, err = smtpclient.GatherDestinations(ctxbg, c.log.Logger, resolver, dns.IPDomain{Domain: origNextHop}) haveMX, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hosts, permanent, err = smtpclient.GatherDestinations(ctxbg, c.log.Logger, resolver, dns.IPDomain{Domain: origNextHop})
status := "temporary" status := "temporary"
if permanent { if permanent {
status = "permanent" status = "permanent"
@ -2233,12 +2016,8 @@ sharing most of its code.
} }
l := []string{} l := []string{}
for _, hp := range hostPrefs { for _, h := range hosts {
s := hp.Host.String() l = append(l, h.String())
if hp.Pref >= 0 {
s += fmt.Sprintf(" (pref %d)", hp.Pref)
}
l = append(l, s)
} }
log.Printf("destinations: %s", strings.Join(l, ", ")) log.Printf("destinations: %s", strings.Join(l, ", "))
} else { } else {
@ -2247,14 +2026,18 @@ sharing most of its code.
expandedNextHopAuthentic = true expandedNextHopAuthentic = true
expandedNextHop = d expandedNextHop = d
hostPrefs = []smtpclient.HostPref{{Host: dns.IPDomain{Domain: d}, Pref: -1}} hosts = []dns.IPDomain{{Domain: d}}
} }
dialedIPs := map[string][]net.IP{} dialedIPs := map[string][]net.IP{}
for _, hp := range hostPrefs { for _, host := range hosts {
host := hp.Host // It should not be possible for hosts to have IP addresses: They are not
// allowed by dns.ParseDomain, and MX records cannot contain them.
if host.IsIP() {
log.Fatalf("unexpected IP address for destination host")
}
log.Printf("attempting to connect to %s (pref %d)", host, hp.Pref) log.Printf("attempting to connect to %s", host)
authentic, expandedAuthentic, expandedHost, ips, _, err := smtpclient.GatherIPs(ctxbg, c.log.Logger, resolver, "ip", host, dialedIPs) authentic, expandedAuthentic, expandedHost, ips, _, err := smtpclient.GatherIPs(ctxbg, c.log.Logger, resolver, "ip", host, dialedIPs)
if err != nil { if err != nil {
@ -4191,7 +3974,7 @@ Opens database files directly, not going through a running mox instance.
return nil return nil
} }
wq := moxio.NewWorkQueue(procs, workqueuesize, prepareMessages, processMessage) wq := moxio.NewWorkQueue[store.Message, threadPrep](procs, workqueuesize, prepareMessages, processMessage)
err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error { err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
q := bstore.QueryTx[store.Message](tx) q := bstore.QueryTx[store.Message](tx)

View File

@ -1058,33 +1058,6 @@ func PrepareStaticConfig(ctx context.Context, log mlog.Log, configFile string, c
} }
} }
checkTransportFail := func(name string, t *config.TransportFail) {
addTransportErrorf := func(format string, args ...any) {
addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
}
if t.SMTPCode == 0 {
t.Code = smtp.C554TransactionFailed
} else if t.SMTPCode/100 != 4 && t.SMTPCode/100 != 5 {
addTransportErrorf("smtp code %d must be 4xx or 5xx", t.SMTPCode/100)
} else {
t.Code = t.SMTPCode
}
if len(t.SMTPMessage) > 256 {
addTransportErrorf("message must be <= 256 characters")
}
for _, c := range t.SMTPMessage {
if c < ' ' || c >= 0x7f {
addTransportErrorf("message cannot contain control characters including newlines, and must be ascii-only")
}
}
t.Message = t.SMTPMessage
if t.Message == "" {
t.Message = "transport fail: explicit immediate delivery failure per configuration"
}
}
for name, t := range c.Transports { for name, t := range c.Transports {
addTransportErrorf := func(format string, args ...any) { addTransportErrorf := func(format string, args ...any) {
addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...)) addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
@ -1111,10 +1084,6 @@ func PrepareStaticConfig(ctx context.Context, log mlog.Log, configFile string, c
n++ n++
checkTransportDirect(name, t.Direct) checkTransportDirect(name, t.Direct)
} }
if t.Fail != nil {
n++
checkTransportFail(name, t.Fail)
}
if n > 1 { if n > 1 {
addTransportErrorf("cannot have multiple methods in a transport") addTransportErrorf("cannot have multiple methods in a transport")
} }

View File

@ -139,7 +139,7 @@ func deliverDirect(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Diale
// directly. // directly.
origNextHop := m0.RecipientDomain.Domain origNextHop := m0.RecipientDomain.Domain
ctx := mox.Shutdown ctx := mox.Shutdown
haveMX, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hostPrefs, permanent, err := smtpclient.GatherDestinations(ctx, qlog.Logger, resolver, m0.RecipientDomain) haveMX, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hosts, permanent, err := smtpclient.GatherDestinations(ctx, qlog.Logger, resolver, m0.RecipientDomain)
if err != nil { if err != nil {
// If this is a DNSSEC authentication error, we'll collect it for TLS reporting. // If this is a DNSSEC authentication error, we'll collect it for TLS reporting.
// Hopefully it's a temporary misconfiguration that is solve before we try to send // Hopefully it's a temporary misconfiguration that is solve before we try to send
@ -196,8 +196,7 @@ func deliverDirect(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Diale
var lastErr = errors.New("no error") // Can be smtpclient.Error. var lastErr = errors.New("no error") // Can be smtpclient.Error.
nmissingRequireTLS := 0 nmissingRequireTLS := 0
// todo: should make distinction between host permanently not accepting the message, and the message not being deliverable permanently. e.g. a mx host may have a size limit, or not accept 8bitmime, while another host in the list does accept the message. same for smtputf8, ../rfc/6531:555 // todo: should make distinction between host permanently not accepting the message, and the message not being deliverable permanently. e.g. a mx host may have a size limit, or not accept 8bitmime, while another host in the list does accept the message. same for smtputf8, ../rfc/6531:555
for _, hp := range hostPrefs { for _, h := range hosts {
h := hp.Host
// ../rfc/8461:913 // ../rfc/8461:913
if policy != nil && policy.Mode != mtasts.ModeNone && !policy.Matches(h.Domain) { if policy != nil && policy.Mode != mtasts.ModeNone && !policy.Matches(h.Domain) {
// todo: perhaps only send tlsrpt failure if none of the mx hosts matched? reporting about each mismatch seems useful for domain owners, to discover mtasts policies they didn't update after changing mx. there is a risk a domain owner intentionally didn't put all mx'es in the mtasts policy, but they probably won't mind being reported about that. // todo: perhaps only send tlsrpt failure if none of the mx hosts matched? reporting about each mismatch seems useful for domain owners, to discover mtasts policies they didn't update after changing mx. there is a risk a domain owner intentionally didn't put all mx'es in the mtasts policy, but they probably won't mind being reported about that.
@ -349,7 +348,7 @@ func deliverDirect(qlog mlog.Log, resolver dns.Resolver, dialer smtpclient.Diale
// If we failed due to requiretls not being satisfied, make the delivery permanent. // 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 // It is unlikely the recipient domain will implement requiretls during our retry
// period. Best to let the sender know immediately. // period. Best to let the sender know immediately.
if len(hostPrefs) > 0 && nmissingRequireTLS == len(hostPrefs) { if len(hosts) > 0 && nmissingRequireTLS == len(hosts) {
qlog.Info("marking delivery as permanently failed because recipient domain does not implement requiretls") qlog.Info("marking delivery as permanently failed because recipient domain does not implement requiretls")
err := smtpclient.Error{ err := smtpclient.Error{
Permanent: true, Permanent: true,

View File

@ -1528,18 +1528,6 @@ func deliver(log mlog.Log, resolver dns.Resolver, m0 Msg) {
qlog.Debug("delivering to single recipient", slog.Any("msgid", m0.ID), slog.Any("recipient", m0.Recipient())) qlog.Debug("delivering to single recipient", slog.Any("msgid", m0.ID), slog.Any("recipient", m0.Recipient()))
} }
// Test for "Fail" transport before Localserve.
if transport.Fail != nil {
err := smtpclient.Error{
Permanent: transport.Fail.Code/100 == 5,
Code: transport.Fail.Code,
Secode: smtp.SePol7Other0,
Err: fmt.Errorf("%s", transport.Fail.Message),
}
failMsgsDB(qlog, msgs, msgs[0].DialedIPs, backoff, dsn.NameIP{}, err)
return
}
if Localserve { if Localserve {
deliverLocalserve(ctx, qlog, msgs, backoff) deliverLocalserve(ctx, qlog, msgs, backoff)
return return

View File

@ -114,7 +114,6 @@ type Client struct {
daneMoreHostnames []dns.Domain // Additional allowed names in TLS certificate for DANE-TA. daneMoreHostnames []dns.Domain // Additional allowed names in TLS certificate for DANE-TA.
daneVerifiedRecord *adns.TLSA // If non-nil, then will be set to verified DANE record if any. daneVerifiedRecord *adns.TLSA // If non-nil, then will be set to verified DANE record if any.
clientCert *tls.Certificate // If non-nil, tls client authentication is done. clientCert *tls.Certificate // If non-nil, tls client authentication is done.
tlsConfigOpts *tls.Config // If non-nil, tls config to use.
// TLS connection success/failure are added. These are always non-nil, regardless // TLS connection success/failure are added. These are always non-nil, regardless
// of what was passed in opts. It lets us unconditionally dereference them. // of what was passed in opts. It lets us unconditionally dereference them.
@ -236,12 +235,6 @@ type Opts struct {
// tracked. // tracked.
RecipientDomainResult *tlsrpt.Result // MTA-STS or no policy. RecipientDomainResult *tlsrpt.Result // MTA-STS or no policy.
HostResult *tlsrpt.Result // DANE or no policy. HostResult *tlsrpt.Result // DANE or no policy.
// If not nil, the TLS config to use instead of the default. Useful for custom
// certificate verification or TLS parameters. The other DANE/TLS/certificate
// fields in [Opts], and the tlsVerifyPKIX and remoteHostname parameters to [New]
// have no effect when TLSConfig is set.
TLSConfig *tls.Config
} }
// New initializes an SMTP session on the given connection, returning a client that // New initializes an SMTP session on the given connection, returning a client that
@ -297,7 +290,6 @@ func New(ctx context.Context, elog *slog.Logger, conn net.Conn, tlsMode TLSMode,
cmds: []string{"(none)"}, cmds: []string{"(none)"},
recipientDomainResult: ensureResult(opts.RecipientDomainResult), recipientDomainResult: ensureResult(opts.RecipientDomainResult),
hostResult: ensureResult(opts.HostResult), hostResult: ensureResult(opts.HostResult),
tlsConfigOpts: opts.TLSConfig,
} }
c.log = mlog.New("smtpclient", elog).WithFunc(func() []slog.Attr { c.log = mlog.New("smtpclient", elog).WithFunc(func() []slog.Attr {
now := time.Now() now := time.Now()
@ -362,10 +354,6 @@ func (c *Client) tlsConfig() *tls.Config {
// failures. And we may have to verify both PKIX and DANE, record errors for // failures. And we may have to verify both PKIX and DANE, record errors for
// each, and possibly ignore the errors. // each, and possibly ignore the errors.
if c.tlsConfigOpts != nil {
return c.tlsConfigOpts
}
verifyConnection := func(cs tls.ConnectionState) error { verifyConnection := func(cs tls.ConnectionState) error {
// Collect verification errors. If there are none at the end, TLS validation // Collect verification errors. If there are none at the end, TLS validation
// succeeded. We may find validation problems below, record them for a TLS report // succeeded. We may find validation problems below, record them for a TLS report
@ -1465,10 +1453,9 @@ func (c *Client) Close() (rerr error) {
return return
} }
// Conn returns the connection with the initialized SMTP session, possibly wrapping // Conn returns the connection with initialized SMTP session. Once the caller uses
// a TLS connection, and handling protocol trace logging. Once the caller uses this // this connection it is in control, and responsible for closing the connection,
// connection it is in control, and responsible for closing the connection, and // and other functions on the client must not be called anymore.
// other functions on the client must not be called anymore.
func (c *Client) Conn() (net.Conn, error) { func (c *Client) Conn() (net.Conn, error) {
if err := c.conn.SetDeadline(time.Time{}); err != nil { if err := c.conn.SetDeadline(time.Time{}); err != nil {
return nil, fmt.Errorf("clearing io deadlines: %w", err) return nil, fmt.Errorf("clearing io deadlines: %w", err)

View File

@ -26,12 +26,6 @@ var (
errNoMail = errors.New("domain does not accept email as indicated with single dot for mx record") errNoMail = errors.New("domain does not accept email as indicated with single dot for mx record")
) )
// HostPref is a host for delivery, with preference for MX records.
type HostPref struct {
Host dns.IPDomain
Pref int // -1 when not an MX record.
}
// GatherDestinations looks up the hosts to deliver email to a domain ("next-hop"). // GatherDestinations looks up the hosts to deliver email to a domain ("next-hop").
// If it is an IP address, it is the only destination to try. Otherwise CNAMEs of // If it is an IP address, it is the only destination to try. Otherwise CNAMEs of
// the domain are followed. Then MX records for the expanded CNAME are looked up. // the domain are followed. Then MX records for the expanded CNAME are looked up.
@ -52,14 +46,14 @@ type HostPref struct {
// were found, both the original and expanded next-hops must be authentic for DANE // were found, both the original and expanded next-hops must be authentic for DANE
// to be option. For a non-IP with no MX records found, the authentic result can // to be option. For a non-IP with no MX records found, the authentic result can
// be used to decide which of the names to use as TLSA base domain. // be used to decide which of the names to use as TLSA base domain.
func GatherDestinations(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, origNextHop dns.IPDomain) (haveMX, origNextHopAuthentic, expandedNextHopAuthentic bool, expandedNextHop dns.Domain, hostPrefs []HostPref, permanent bool, err error) { func GatherDestinations(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, origNextHop dns.IPDomain) (haveMX, origNextHopAuthentic, expandedNextHopAuthentic bool, expandedNextHop dns.Domain, hosts []dns.IPDomain, permanent bool, err error) {
// ../rfc/5321:3824 // ../rfc/5321:3824
log := mlog.New("smtpclient", elog) log := mlog.New("smtpclient", elog)
// IP addresses are dialed directly, and don't have TLSA records. // IP addresses are dialed directly, and don't have TLSA records.
if len(origNextHop.IP) > 0 { if len(origNextHop.IP) > 0 {
return false, false, false, expandedNextHop, []HostPref{{origNextHop, -1}}, false, nil return false, false, false, expandedNextHop, []dns.IPDomain{origNextHop}, false, nil
} }
// We start out assuming the result is authentic. Updated with each lookup. // We start out assuming the result is authentic. Updated with each lookup.
@ -139,8 +133,8 @@ func GatherDestinations(ctx context.Context, elog *slog.Logger, resolver dns.Res
} }
// No MX record, attempt delivery directly to host. ../rfc/5321:3842 // No MX record, attempt delivery directly to host. ../rfc/5321:3842
hostPrefs = []HostPref{{dns.IPDomain{Domain: expandedNextHop}, -1}} hosts = []dns.IPDomain{{Domain: expandedNextHop}}
return false, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hostPrefs, false, nil return false, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hosts, false, nil
} else if err != nil { } else if err != nil {
log.Infox("mx record has some invalid records, keeping only the valid mx records", err) log.Infox("mx record has some invalid records, keeping only the valid mx records", err)
} }
@ -164,12 +158,12 @@ func GatherDestinations(ctx context.Context, elog *slog.Logger, resolver dns.Res
err = fmt.Errorf("%w: invalid host name in mx record %q: %v", errDNS, mx.Host, err) err = fmt.Errorf("%w: invalid host name in mx record %q: %v", errDNS, mx.Host, err)
return true, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, nil, true, err return true, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, nil, true, err
} }
hostPrefs = append(hostPrefs, HostPref{dns.IPDomain{Domain: host}, int(mx.Pref)}) hosts = append(hosts, dns.IPDomain{Domain: host})
} }
if len(hostPrefs) > 0 { if len(hosts) > 0 {
err = nil err = nil
} }
return true, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hostPrefs, false, err return true, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hosts, false, err
} }
} }

View File

@ -35,11 +35,11 @@ func ipdomain(s string) dns.IPDomain {
return dns.IPDomain{Domain: d} return dns.IPDomain{Domain: d}
} }
func hostprefs(pref int, names ...string) (l []HostPref) { func ipdomains(s ...string) (l []dns.IPDomain) {
for _, s := range names { for _, e := range s {
l = append(l, HostPref{Host: ipdomain(s), Pref: pref}) l = append(l, ipdomain(e))
} }
return l return
} }
// Test basic MX lookup case, but also following CNAME, detecting CNAME loops and // Test basic MX lookup case, but also following CNAME, detecting CNAME loops and
@ -86,10 +86,10 @@ func TestGatherDestinations(t *testing.T) {
resolver.CNAME[s] = next resolver.CNAME[s] = next
} }
test := func(ipd dns.IPDomain, expHostPrefs []HostPref, expDomain dns.Domain, expPerm, expAuthic, expExpAuthic bool, expErr error) { test := func(ipd dns.IPDomain, expHosts []dns.IPDomain, expDomain dns.Domain, expPerm, expAuthic, expExpAuthic bool, expErr error) {
t.Helper() t.Helper()
_, authic, authicExp, ed, hostPrefs, perm, err := GatherDestinations(ctxbg, log.Logger, resolver, ipd) _, authic, authicExp, ed, hosts, perm, err := GatherDestinations(ctxbg, log.Logger, resolver, ipd)
if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) { if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
// todo: could also check the individual errors? code currently does not have structured errors. // todo: could also check the individual errors? code currently does not have structured errors.
t.Fatalf("gather hosts: %v, expected %v", err, expErr) t.Fatalf("gather hosts: %v, expected %v", err, expErr)
@ -97,8 +97,8 @@ func TestGatherDestinations(t *testing.T) {
if err != nil { if err != nil {
return return
} }
if !reflect.DeepEqual(hostPrefs, expHostPrefs) || ed != expDomain || perm != expPerm || authic != expAuthic || authicExp != expExpAuthic { if !reflect.DeepEqual(hosts, expHosts) || ed != expDomain || perm != expPerm || authic != expAuthic || authicExp != expExpAuthic {
t.Fatalf("got hosts %#v, effectiveDomain %#v, permanent %#v, authic %v %v, expected %#v %#v %#v %v %v", hostPrefs, ed, perm, authic, authicExp, expHostPrefs, expDomain, expPerm, expAuthic, expExpAuthic) t.Fatalf("got hosts %#v, effectiveDomain %#v, permanent %#v, authic %v %v, expected %#v %#v %#v %v %v", hosts, ed, perm, authic, authicExp, expHosts, expDomain, expPerm, expAuthic, expExpAuthic)
} }
} }
@ -108,18 +108,18 @@ func TestGatherDestinations(t *testing.T) {
authic := i == 1 authic := i == 1
resolver.AllAuthentic = authic resolver.AllAuthentic = authic
// Basic with simple MX. // Basic with simple MX.
test(ipdomain("basic.example"), hostprefs(10, "mail.basic.example"), domain("basic.example"), false, authic, authic, nil) test(ipdomain("basic.example"), ipdomains("mail.basic.example"), domain("basic.example"), false, authic, authic, nil)
test(ipdomain("multimx.example"), hostprefs(10, "mail1.multimx.example", "mail2.multimx.example"), domain("multimx.example"), false, authic, authic, nil) test(ipdomain("multimx.example"), ipdomains("mail1.multimx.example", "mail2.multimx.example"), domain("multimx.example"), false, authic, authic, nil)
// Only an A record. // Only an A record.
test(ipdomain("justhost.example"), hostprefs(-1, "justhost.example"), domain("justhost.example"), false, authic, authic, nil) test(ipdomain("justhost.example"), ipdomains("justhost.example"), domain("justhost.example"), false, authic, authic, nil)
// Only an AAAA record. // Only an AAAA record.
test(ipdomain("justhost6.example"), hostprefs(-1, "justhost6.example"), domain("justhost6.example"), false, authic, authic, nil) test(ipdomain("justhost6.example"), ipdomains("justhost6.example"), domain("justhost6.example"), false, authic, authic, nil)
// Follow CNAME. // Follow CNAME.
test(ipdomain("cname.example"), hostprefs(10, "mail.basic.example"), domain("basic.example"), false, authic, authic, nil) test(ipdomain("cname.example"), ipdomains("mail.basic.example"), domain("basic.example"), false, authic, authic, nil)
// No MX/CNAME, non-existence of host will be found out later. // No MX/CNAME, non-existence of host will be found out later.
test(ipdomain("absent.example"), hostprefs(-1, "absent.example"), domain("absent.example"), false, authic, authic, nil) test(ipdomain("absent.example"), ipdomains("absent.example"), domain("absent.example"), false, authic, authic, nil)
// Followed CNAME, has no MX, non-existence of host will be found out later. // Followed CNAME, has no MX, non-existence of host will be found out later.
test(ipdomain("danglingcname.example"), hostprefs(-1, "absent.example"), domain("absent.example"), false, authic, authic, nil) test(ipdomain("danglingcname.example"), ipdomains("absent.example"), domain("absent.example"), false, authic, authic, nil)
test(ipdomain("cnamelimit1.example"), nil, zerodom, true, authic, authic, errCNAMELimit) test(ipdomain("cnamelimit1.example"), nil, zerodom, true, authic, authic, errCNAMELimit)
test(ipdomain("cnameloop.example"), nil, zerodom, true, authic, authic, errCNAMELoop) test(ipdomain("cnameloop.example"), nil, zerodom, true, authic, authic, errCNAMELoop)
test(ipdomain("nullmx.example"), nil, zerodom, true, authic, authic, errNoMail) test(ipdomain("nullmx.example"), nil, zerodom, true, authic, authic, errNoMail)
@ -127,9 +127,9 @@ func TestGatherDestinations(t *testing.T) {
test(ipdomain("temperror-cname.example"), nil, zerodom, false, authic, authic, errDNS) test(ipdomain("temperror-cname.example"), nil, zerodom, false, authic, authic, errDNS)
} }
test(ipdomain("10.0.0.1"), hostprefs(-1, "10.0.0.1"), zerodom, false, false, false, nil) test(ipdomain("10.0.0.1"), ipdomains("10.0.0.1"), zerodom, false, false, false, nil)
test(ipdomain("cnameinauthentic.example"), hostprefs(10, "mail.basic.example"), domain("basic.example"), false, false, false, nil) test(ipdomain("cnameinauthentic.example"), ipdomains("mail.basic.example"), domain("basic.example"), false, false, false, nil)
test(ipdomain("cname-to-inauthentic.example"), hostprefs(10, "mail.basic.example"), domain("basic.example"), false, true, false, nil) test(ipdomain("cname-to-inauthentic.example"), ipdomains("mail.basic.example"), domain("basic.example"), false, true, false, nil)
} }
func TestGatherIPs(t *testing.T) { func TestGatherIPs(t *testing.T) {

View File

@ -113,7 +113,7 @@ func FuzzServer(f *testing.F) {
const viaHTTPS = false const viaHTTPS = 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, viaHTTPS, false, 100<<10, false, false, false, nil, 0) serve("test", cid, dns.Domain{ASCII: "mox.example"}, nil, serverConn, resolver, submission, false, viaHTTPS, 100<<10, false, false, false, nil, 0)
cid++ cid++
} }

View File

@ -206,14 +206,12 @@ func Listen() {
listener := mox.Conf.Static.Listeners[name] listener := mox.Conf.Static.Listeners[name]
var tlsConfig, tlsConfigDelivery *tls.Config var tlsConfig, tlsConfigDelivery *tls.Config
var noTLSClientAuth bool
if listener.TLS != nil { if listener.TLS != nil {
tlsConfig = listener.TLS.Config tlsConfig = listener.TLS.Config
// For SMTP delivery, if we get a TLS handshake for an SNI hostname that we don't // For SMTP delivery, if we get a TLS handshake for an SNI hostname that we don't
// allow, we'll fallback to a certificate for the listener hostname instead of // allow, we'll fallback to a certificate for the listener hostname instead of
// causing the connection to fail. May improve interoperability. // causing the connection to fail. May improve interoperability.
tlsConfigDelivery = listener.TLS.ConfigFallback tlsConfigDelivery = listener.TLS.ConfigFallback
noTLSClientAuth = listener.TLS.ClientAuthDisabled
} }
maxMsgSize := listener.SMTPMaxMessageSize maxMsgSize := listener.SMTPMaxMessageSize
@ -236,7 +234,7 @@ func Listen() {
// https://github.com/golang/go/issues/70232. // https://github.com/golang/go/issues/70232.
tlsConfigDelivery.SessionTicketsDisabled = listener.SMTP.TLSSessionTicketsDisabled == nil || *listener.SMTP.TLSSessionTicketsDisabled tlsConfigDelivery.SessionTicketsDisabled = listener.SMTP.TLSSessionTicketsDisabled == nil || *listener.SMTP.TLSSessionTicketsDisabled
} }
listen1("smtp", name, ip, port, hostname, tlsConfigDelivery, false, false, noTLSClientAuth, maxMsgSize, false, listener.SMTP.RequireSTARTTLS, !listener.SMTP.NoRequireTLS, listener.SMTP.DNSBLZones, firstTimeSenderDelay) listen1("smtp", name, ip, port, hostname, tlsConfigDelivery, false, false, maxMsgSize, false, listener.SMTP.RequireSTARTTLS, !listener.SMTP.NoRequireTLS, listener.SMTP.DNSBLZones, firstTimeSenderDelay)
} }
} }
if listener.Submission.Enabled { if listener.Submission.Enabled {
@ -246,7 +244,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, noTLSClientAuth, maxMsgSize, !listener.Submission.NoRequireSTARTTLS, !listener.Submission.NoRequireSTARTTLS, true, nil, 0) listen1("submission", name, ip, port, hostname, tlsConfig, true, false, maxMsgSize, !listener.Submission.NoRequireSTARTTLS, !listener.Submission.NoRequireSTARTTLS, true, nil, 0)
} }
} }
@ -257,7 +255,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, noTLSClientAuth, maxMsgSize, true, true, true, nil, 0) listen1("submissions", name, ip, port, hostname, tlsConfig, true, true, maxMsgSize, true, true, true, nil, 0)
} }
} }
} }
@ -265,7 +263,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, noTLSClientAuth bool, maxMessageSize int64, requireTLSForAuth, requireTLSForDelivery, requireTLS 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) {
log := mlog.New("smtpserver", nil) log := mlog.New("smtpserver", nil)
addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port)) addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
if os.Getuid() == 0 { if os.Getuid() == 0 {
@ -299,7 +297,7 @@ func listen1(protocol, name, ip string, port int, hostname dns.Domain, tlsConfig
// Package is set on the resolver by the dkim/spf/dmarc/etc packages. // Package is set on the resolver by the dkim/spf/dmarc/etc packages.
resolver := dns.StrictResolver{Log: log.Logger} resolver := dns.StrictResolver{Log: log.Logger}
go serve(name, mox.Cid(), hostname, tlsConfig, conn, resolver, submission, xtls, false, noTLSClientAuth, maxMessageSize, requireTLSForAuth, requireTLSForDelivery, requireTLS, dnsBLs, firstTimeSenderDelay) go serve(name, mox.Cid(), hostname, tlsConfig, conn, resolver, submission, xtls, false, maxMessageSize, requireTLSForAuth, requireTLSForDelivery, requireTLS, dnsBLs, firstTimeSenderDelay)
} }
} }
@ -323,11 +321,10 @@ type conn struct {
origConn net.Conn origConn net.Conn
conn net.Conn conn net.Conn
tls bool tls bool
extRequireTLS bool // Whether to announce and allow the REQUIRETLS extension. extRequireTLS bool // Whether to announce and allow the REQUIRETLS extension.
viaHTTPS bool // Whether the connection came in via the HTTPS port (using TLS ALPN). viaHTTPS bool // Whether the connection came in via the HTTPS port (using TLS ALPN).
noTLSClientAuth bool resolver dns.Resolver
resolver dns.Resolver
// The "x" in the readers and writes indicate Read and Write errors use panic to // The "x" in the readers and writes indicate Read and Write errors use panic to
// propagate the error. // propagate the error.
xbr *bufio.Reader xbr *bufio.Reader
@ -438,7 +435,7 @@ func (c *conn) loginAttempt(useTLS bool, authMech string) store.LoginAttempt {
// makeTLSConfig makes a new tls config that is bound to the connection for // makeTLSConfig makes a new tls config that is bound to the connection for
// possible client certificate authentication in case of submission. // possible client certificate authentication in case of submission.
func (c *conn) makeTLSConfig() *tls.Config { func (c *conn) makeTLSConfig() *tls.Config {
if !c.submission || c.noTLSClientAuth { if !c.submission {
return c.baseTLSConfig return c.baseTLSConfig
} }
@ -543,7 +540,7 @@ func (c *conn) tlsClientAuthVerifyPeerCertParsed(cert *x509.Certificate) error {
if err == bstore.ErrAbsent { if err == bstore.ErrAbsent {
la.Result = store.AuthBadCredentials la.Result = store.AuthBadCredentials
} }
return fmt.Errorf("looking up tls public key with fingerprint %s, subject %q, issuer %q: %v", fp, cert.Subject, cert.Issuer, err) return fmt.Errorf("looking up tls public key with fingerprint %s: %v", fp, err)
} }
la.LoginAddress = pubKey.LoginAddress la.LoginAddress = pubKey.LoginAddress
@ -623,7 +620,7 @@ func (c *conn) xtlsHandshakeAndAuthenticate(conn net.Conn) {
cancel() cancel()
cs := tlsConn.ConnectionState() cs := tlsConn.ConnectionState()
if cs.DidResume && len(cs.PeerCertificates) > 0 && !c.noTLSClientAuth { if cs.DidResume && len(cs.PeerCertificates) > 0 {
// Verify client after session resumption. // Verify client after session resumption.
err := c.tlsClientAuthVerifyPeerCertParsed(cs.PeerCertificates[0]) err := c.tlsClientAuthVerifyPeerCertParsed(cs.PeerCertificates[0])
if err != nil { if err != nil {
@ -637,7 +634,6 @@ func (c *conn) xtlsHandshakeAndAuthenticate(conn net.Conn) {
slog.String("ciphersuite", ciphersuite), slog.String("ciphersuite", ciphersuite),
slog.String("sni", cs.ServerName), slog.String("sni", cs.ServerName),
slog.Bool("resumed", cs.DidResume), slog.Bool("resumed", cs.DidResume),
slog.Bool("notlsclientauth", c.noTLSClientAuth),
slog.Int("clientcerts", len(cs.PeerCertificates)), slog.Int("clientcerts", len(cs.PeerCertificates)),
} }
if c.account != nil { if c.account != nil {
@ -864,10 +860,10 @@ var cleanClose struct{} // Sentinel value for panic/recover indicating clean clo
func ServeTLSConn(listenerName string, hostname dns.Domain, conn *tls.Conn, tlsConfig *tls.Config, submission, viaHTTPS bool, maxMsgSize int64, requireTLS bool) { func ServeTLSConn(listenerName string, hostname dns.Domain, conn *tls.Conn, tlsConfig *tls.Config, submission, viaHTTPS bool, maxMsgSize int64, requireTLS bool) {
log := mlog.New("smtpserver", nil) log := mlog.New("smtpserver", nil)
resolver := dns.StrictResolver{Log: log.Logger} resolver := dns.StrictResolver{Log: log.Logger}
serve(listenerName, mox.Cid(), hostname, tlsConfig, conn, resolver, submission, true, viaHTTPS, true, maxMsgSize, true, true, requireTLS, nil, 0) serve(listenerName, mox.Cid(), hostname, tlsConfig, conn, resolver, submission, true, viaHTTPS, maxMsgSize, true, true, requireTLS, nil, 0)
} }
func serve(listenerName string, cid int64, hostname dns.Domain, tlsConfig *tls.Config, nc net.Conn, resolver dns.Resolver, submission, xtls, viaHTTPS, noTLSClientAuth bool, maxMessageSize int64, requireTLSForAuth, requireTLSForDelivery, requireTLS 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, xtls, viaHTTPS 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
@ -894,7 +890,6 @@ func serve(listenerName string, cid int64, hostname dns.Domain, tlsConfig *tls.C
submission: submission, submission: submission,
tls: xtls, tls: xtls,
viaHTTPS: viaHTTPS, viaHTTPS: viaHTTPS,
noTLSClientAuth: noTLSClientAuth,
extRequireTLS: requireTLS, extRequireTLS: requireTLS,
resolver: resolver, resolver: resolver,
lastlog: time.Now(), lastlog: time.Now(),
@ -1214,7 +1209,7 @@ func (c *conn) cmdHello(p *parser, ehlo bool) {
// case, or it would trigger the mechanism downgrade detection. // case, or it would trigger the mechanism downgrade detection.
mechs = "SCRAM-SHA-256-PLUS SCRAM-SHA-256 SCRAM-SHA-1-PLUS SCRAM-SHA-1 CRAM-MD5 PLAIN LOGIN" mechs = "SCRAM-SHA-256-PLUS SCRAM-SHA-256 SCRAM-SHA-1-PLUS SCRAM-SHA-1 CRAM-MD5 PLAIN LOGIN"
} }
if c.tls && len(c.conn.(*tls.Conn).ConnectionState().PeerCertificates) > 0 && !c.viaHTTPS && !c.noTLSClientAuth { if c.tls && len(c.conn.(*tls.Conn).ConnectionState().PeerCertificates) > 0 && !c.viaHTTPS {
mechs = "EXTERNAL " + mechs mechs = "EXTERNAL " + mechs
} }
c.xbwritelinef("250-AUTH %s", mechs) c.xbwritelinef("250-AUTH %s", mechs)

View File

@ -257,7 +257,7 @@ func (ts *testserver) runRaw(fn func(clientConn net.Conn)) {
defer func() { <-serverdone }() defer func() { <-serverdone }()
go func() { go func() {
serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, ts.serverConfig, serverConn, ts.resolver, ts.submission, ts.immediateTLS, false, false, 100<<20, false, false, ts.requiretls, ts.dnsbls, 0) serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, ts.serverConfig, serverConn, ts.resolver, ts.submission, ts.immediateTLS, false, 100<<20, false, false, ts.requiretls, ts.dnsbls, 0)
close(serverdone) close(serverdone)
}() }()
@ -476,7 +476,7 @@ func TestSubmission(t *testing.T) {
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
Certificates: []tls.Certificate{fakeCert(ts.t, false)}, Certificates: []tls.Certificate{fakeCert(ts.t, false)},
} }
serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, tlsConfig, serverConn, ts.resolver, ts.submission, ts.immediateTLS, false, false, 100<<20, false, false, false, ts.dnsbls, 0) serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, tlsConfig, serverConn, ts.resolver, ts.submission, ts.immediateTLS, false, 100<<20, false, false, false, ts.dnsbls, 0)
close(serverdone) close(serverdone)
}() }()
@ -1426,7 +1426,7 @@ func TestNonSMTP(t *testing.T) {
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
Certificates: []tls.Certificate{fakeCert(ts.t, false)}, Certificates: []tls.Certificate{fakeCert(ts.t, false)},
} }
serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, tlsConfig, serverConn, ts.resolver, ts.submission, false, false, false, 100<<20, false, false, false, ts.dnsbls, 0) serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, tlsConfig, serverConn, ts.resolver, ts.submission, false, false, 100<<20, false, false, false, ts.dnsbls, 0)
close(serverdone) close(serverdone)
}() }()

View File

@ -348,7 +348,7 @@ possibly making them potentially no longer readable by the previous version.
if du.MessageSize != totalSize { if du.MessageSize != totalSize {
checkf(errors.New(`wrong total message size, see mox recalculatemailboxcounts"`), dbpath, "account has wrong total message size %d, should be %d", du.MessageSize, totalSize) checkf(errors.New(`wrong total message size, see mox recalculatemailboxcounts"`), dbpath, "account has wrong total message size %d, should be %d", du.MessageSize, totalSize)
} }
} else if !errors.Is(err, bstore.ErrAbsent) { } else if err != nil && !errors.Is(err, bstore.ErrAbsent) {
checkf(err, dbpath, "get disk usage") checkf(err, dbpath, "get disk usage")
} }
} }

View File

@ -263,7 +263,7 @@ var api;
AuthResult["AuthError"] = "error"; AuthResult["AuthError"] = "error";
AuthResult["AuthAborted"] = "aborted"; AuthResult["AuthAborted"] = "aborted";
})(AuthResult = api.AuthResult || (api.AuthResult = {})); })(AuthResult = api.AuthResult || (api.AuthResult = {}));
api.structTypes = { "Account": true, "Address": true, "AddressAlias": true, "Alias": true, "AliasAddress": true, "AuthResults": true, "AutoconfCheckResult": true, "AutodiscoverCheckResult": true, "AutodiscoverSRV": true, "AutomaticJunkFlags": true, "Canonicalization": true, "CheckResult": true, "ClientConfigs": true, "ClientConfigsEntry": true, "ConfigDomain": true, "DANECheckResult": true, "DKIM": true, "DKIMAuthResult": true, "DKIMCheckResult": true, "DKIMRecord": true, "DMARC": true, "DMARCCheckResult": true, "DMARCRecord": true, "DMARCSummary": true, "DNSSECResult": true, "DateRange": true, "Destination": true, "Directive": true, "Domain": true, "DomainFeedback": true, "Dynamic": true, "Evaluation": true, "EvaluationStat": true, "Extension": true, "FailureDetails": true, "Filter": true, "HoldRule": true, "Hook": true, "HookFilter": true, "HookResult": true, "HookRetired": true, "HookRetiredFilter": true, "HookRetiredSort": true, "HookSort": true, "IPDomain": true, "IPRevCheckResult": true, "Identifiers": true, "IncomingWebhook": true, "JunkFilter": true, "LoginAttempt": true, "MTASTS": true, "MTASTSCheckResult": true, "MTASTSRecord": true, "MX": true, "MXCheckResult": true, "Modifier": true, "Msg": true, "MsgResult": true, "MsgRetired": true, "OutgoingWebhook": true, "Pair": true, "Policy": true, "PolicyEvaluated": true, "PolicyOverrideReason": true, "PolicyPublished": true, "PolicyRecord": true, "Record": true, "Report": true, "ReportMetadata": true, "ReportRecord": true, "Result": true, "ResultPolicy": true, "RetiredFilter": true, "RetiredSort": true, "Reverse": true, "Route": true, "Row": true, "Ruleset": true, "SMTPAuth": true, "SPFAuthResult": true, "SPFCheckResult": true, "SPFRecord": true, "SRV": true, "SRVConfCheckResult": true, "STSMX": true, "Selector": true, "Sort": true, "SubjectPass": true, "Summary": true, "SuppressAddress": true, "TLSCheckResult": true, "TLSPublicKey": true, "TLSRPT": true, "TLSRPTCheckResult": true, "TLSRPTDateRange": true, "TLSRPTRecord": true, "TLSRPTSummary": true, "TLSRPTSuppressAddress": true, "TLSReportRecord": true, "TLSResult": true, "Transport": true, "TransportDirect": true, "TransportFail": true, "TransportSMTP": true, "TransportSocks": true, "URI": true, "WebForward": true, "WebHandler": true, "WebInternal": true, "WebRedirect": true, "WebStatic": true, "WebserverConfig": true }; api.structTypes = { "Account": true, "Address": true, "AddressAlias": true, "Alias": true, "AliasAddress": true, "AuthResults": true, "AutoconfCheckResult": true, "AutodiscoverCheckResult": true, "AutodiscoverSRV": true, "AutomaticJunkFlags": true, "Canonicalization": true, "CheckResult": true, "ClientConfigs": true, "ClientConfigsEntry": true, "ConfigDomain": true, "DANECheckResult": true, "DKIM": true, "DKIMAuthResult": true, "DKIMCheckResult": true, "DKIMRecord": true, "DMARC": true, "DMARCCheckResult": true, "DMARCRecord": true, "DMARCSummary": true, "DNSSECResult": true, "DateRange": true, "Destination": true, "Directive": true, "Domain": true, "DomainFeedback": true, "Dynamic": true, "Evaluation": true, "EvaluationStat": true, "Extension": true, "FailureDetails": true, "Filter": true, "HoldRule": true, "Hook": true, "HookFilter": true, "HookResult": true, "HookRetired": true, "HookRetiredFilter": true, "HookRetiredSort": true, "HookSort": true, "IPDomain": true, "IPRevCheckResult": true, "Identifiers": true, "IncomingWebhook": true, "JunkFilter": true, "LoginAttempt": true, "MTASTS": true, "MTASTSCheckResult": true, "MTASTSRecord": true, "MX": true, "MXCheckResult": true, "Modifier": true, "Msg": true, "MsgResult": true, "MsgRetired": true, "OutgoingWebhook": true, "Pair": true, "Policy": true, "PolicyEvaluated": true, "PolicyOverrideReason": true, "PolicyPublished": true, "PolicyRecord": true, "Record": true, "Report": true, "ReportMetadata": true, "ReportRecord": true, "Result": true, "ResultPolicy": true, "RetiredFilter": true, "RetiredSort": true, "Reverse": true, "Route": true, "Row": true, "Ruleset": true, "SMTPAuth": true, "SPFAuthResult": true, "SPFCheckResult": true, "SPFRecord": true, "SRV": true, "SRVConfCheckResult": true, "STSMX": true, "Selector": true, "Sort": true, "SubjectPass": true, "Summary": true, "SuppressAddress": true, "TLSCheckResult": true, "TLSPublicKey": true, "TLSRPT": true, "TLSRPTCheckResult": true, "TLSRPTDateRange": true, "TLSRPTRecord": true, "TLSRPTSummary": true, "TLSRPTSuppressAddress": true, "TLSReportRecord": true, "TLSResult": true, "Transport": true, "TransportDirect": true, "TransportSMTP": true, "TransportSocks": true, "URI": true, "WebForward": true, "WebHandler": true, "WebInternal": true, "WebRedirect": true, "WebStatic": true, "WebserverConfig": true };
api.stringsTypes = { "Align": true, "AuthResult": true, "CSRFToken": true, "DMARCPolicy": true, "IP": true, "Localpart": true, "Mode": true, "RUA": true }; api.stringsTypes = { "Align": true, "AuthResult": true, "CSRFToken": true, "DMARCPolicy": true, "IP": true, "Localpart": true, "Mode": true, "RUA": true };
api.intsTypes = {}; api.intsTypes = {};
api.types = { api.types = {
@ -365,12 +365,11 @@ var api;
"WebRedirect": { "Name": "WebRedirect", "Docs": "", "Fields": [{ "Name": "BaseURL", "Docs": "", "Typewords": ["string"] }, { "Name": "OrigPathRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "ReplacePath", "Docs": "", "Typewords": ["string"] }, { "Name": "StatusCode", "Docs": "", "Typewords": ["int32"] }] }, "WebRedirect": { "Name": "WebRedirect", "Docs": "", "Fields": [{ "Name": "BaseURL", "Docs": "", "Typewords": ["string"] }, { "Name": "OrigPathRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "ReplacePath", "Docs": "", "Typewords": ["string"] }, { "Name": "StatusCode", "Docs": "", "Typewords": ["int32"] }] },
"WebForward": { "Name": "WebForward", "Docs": "", "Fields": [{ "Name": "StripPath", "Docs": "", "Typewords": ["bool"] }, { "Name": "URL", "Docs": "", "Typewords": ["string"] }, { "Name": "ResponseHeaders", "Docs": "", "Typewords": ["{}", "string"] }] }, "WebForward": { "Name": "WebForward", "Docs": "", "Fields": [{ "Name": "StripPath", "Docs": "", "Typewords": ["bool"] }, { "Name": "URL", "Docs": "", "Typewords": ["string"] }, { "Name": "ResponseHeaders", "Docs": "", "Typewords": ["{}", "string"] }] },
"WebInternal": { "Name": "WebInternal", "Docs": "", "Fields": [{ "Name": "BasePath", "Docs": "", "Typewords": ["string"] }, { "Name": "Service", "Docs": "", "Typewords": ["string"] }] }, "WebInternal": { "Name": "WebInternal", "Docs": "", "Fields": [{ "Name": "BasePath", "Docs": "", "Typewords": ["string"] }, { "Name": "Service", "Docs": "", "Typewords": ["string"] }] },
"Transport": { "Name": "Transport", "Docs": "", "Fields": [{ "Name": "Submissions", "Docs": "", "Typewords": ["nullable", "TransportSMTP"] }, { "Name": "Submission", "Docs": "", "Typewords": ["nullable", "TransportSMTP"] }, { "Name": "SMTP", "Docs": "", "Typewords": ["nullable", "TransportSMTP"] }, { "Name": "Socks", "Docs": "", "Typewords": ["nullable", "TransportSocks"] }, { "Name": "Direct", "Docs": "", "Typewords": ["nullable", "TransportDirect"] }, { "Name": "Fail", "Docs": "", "Typewords": ["nullable", "TransportFail"] }] }, "Transport": { "Name": "Transport", "Docs": "", "Fields": [{ "Name": "Submissions", "Docs": "", "Typewords": ["nullable", "TransportSMTP"] }, { "Name": "Submission", "Docs": "", "Typewords": ["nullable", "TransportSMTP"] }, { "Name": "SMTP", "Docs": "", "Typewords": ["nullable", "TransportSMTP"] }, { "Name": "Socks", "Docs": "", "Typewords": ["nullable", "TransportSocks"] }, { "Name": "Direct", "Docs": "", "Typewords": ["nullable", "TransportDirect"] }] },
"TransportSMTP": { "Name": "TransportSMTP", "Docs": "", "Fields": [{ "Name": "Host", "Docs": "", "Typewords": ["string"] }, { "Name": "Port", "Docs": "", "Typewords": ["int32"] }, { "Name": "STARTTLSInsecureSkipVerify", "Docs": "", "Typewords": ["bool"] }, { "Name": "NoSTARTTLS", "Docs": "", "Typewords": ["bool"] }, { "Name": "Auth", "Docs": "", "Typewords": ["nullable", "SMTPAuth"] }] }, "TransportSMTP": { "Name": "TransportSMTP", "Docs": "", "Fields": [{ "Name": "Host", "Docs": "", "Typewords": ["string"] }, { "Name": "Port", "Docs": "", "Typewords": ["int32"] }, { "Name": "STARTTLSInsecureSkipVerify", "Docs": "", "Typewords": ["bool"] }, { "Name": "NoSTARTTLS", "Docs": "", "Typewords": ["bool"] }, { "Name": "Auth", "Docs": "", "Typewords": ["nullable", "SMTPAuth"] }] },
"SMTPAuth": { "Name": "SMTPAuth", "Docs": "", "Fields": [{ "Name": "Username", "Docs": "", "Typewords": ["string"] }, { "Name": "Password", "Docs": "", "Typewords": ["string"] }, { "Name": "Mechanisms", "Docs": "", "Typewords": ["[]", "string"] }] }, "SMTPAuth": { "Name": "SMTPAuth", "Docs": "", "Fields": [{ "Name": "Username", "Docs": "", "Typewords": ["string"] }, { "Name": "Password", "Docs": "", "Typewords": ["string"] }, { "Name": "Mechanisms", "Docs": "", "Typewords": ["[]", "string"] }] },
"TransportSocks": { "Name": "TransportSocks", "Docs": "", "Fields": [{ "Name": "Address", "Docs": "", "Typewords": ["string"] }, { "Name": "RemoteIPs", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "RemoteHostname", "Docs": "", "Typewords": ["string"] }] }, "TransportSocks": { "Name": "TransportSocks", "Docs": "", "Fields": [{ "Name": "Address", "Docs": "", "Typewords": ["string"] }, { "Name": "RemoteIPs", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "RemoteHostname", "Docs": "", "Typewords": ["string"] }] },
"TransportDirect": { "Name": "TransportDirect", "Docs": "", "Fields": [{ "Name": "DisableIPv4", "Docs": "", "Typewords": ["bool"] }, { "Name": "DisableIPv6", "Docs": "", "Typewords": ["bool"] }] }, "TransportDirect": { "Name": "TransportDirect", "Docs": "", "Fields": [{ "Name": "DisableIPv4", "Docs": "", "Typewords": ["bool"] }, { "Name": "DisableIPv6", "Docs": "", "Typewords": ["bool"] }] },
"TransportFail": { "Name": "TransportFail", "Docs": "", "Fields": [{ "Name": "SMTPCode", "Docs": "", "Typewords": ["int32"] }, { "Name": "SMTPMessage", "Docs": "", "Typewords": ["string"] }, { "Name": "Code", "Docs": "", "Typewords": ["int32"] }, { "Name": "Message", "Docs": "", "Typewords": ["string"] }] },
"EvaluationStat": { "Name": "EvaluationStat", "Docs": "", "Fields": [{ "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "Dispositions", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Count", "Docs": "", "Typewords": ["int32"] }, { "Name": "SendReport", "Docs": "", "Typewords": ["bool"] }] }, "EvaluationStat": { "Name": "EvaluationStat", "Docs": "", "Fields": [{ "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "Dispositions", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Count", "Docs": "", "Typewords": ["int32"] }, { "Name": "SendReport", "Docs": "", "Typewords": ["bool"] }] },
"Evaluation": { "Name": "Evaluation", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "PolicyDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "Evaluated", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "Optional", "Docs": "", "Typewords": ["bool"] }, { "Name": "IntervalHours", "Docs": "", "Typewords": ["int32"] }, { "Name": "Addresses", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "PolicyPublished", "Docs": "", "Typewords": ["PolicyPublished"] }, { "Name": "SourceIP", "Docs": "", "Typewords": ["string"] }, { "Name": "Disposition", "Docs": "", "Typewords": ["string"] }, { "Name": "AlignedDKIMPass", "Docs": "", "Typewords": ["bool"] }, { "Name": "AlignedSPFPass", "Docs": "", "Typewords": ["bool"] }, { "Name": "OverrideReasons", "Docs": "", "Typewords": ["[]", "PolicyOverrideReason"] }, { "Name": "EnvelopeTo", "Docs": "", "Typewords": ["string"] }, { "Name": "EnvelopeFrom", "Docs": "", "Typewords": ["string"] }, { "Name": "HeaderFrom", "Docs": "", "Typewords": ["string"] }, { "Name": "DKIMResults", "Docs": "", "Typewords": ["[]", "DKIMAuthResult"] }, { "Name": "SPFResults", "Docs": "", "Typewords": ["[]", "SPFAuthResult"] }] }, "Evaluation": { "Name": "Evaluation", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "PolicyDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "Evaluated", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "Optional", "Docs": "", "Typewords": ["bool"] }, { "Name": "IntervalHours", "Docs": "", "Typewords": ["int32"] }, { "Name": "Addresses", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "PolicyPublished", "Docs": "", "Typewords": ["PolicyPublished"] }, { "Name": "SourceIP", "Docs": "", "Typewords": ["string"] }, { "Name": "Disposition", "Docs": "", "Typewords": ["string"] }, { "Name": "AlignedDKIMPass", "Docs": "", "Typewords": ["bool"] }, { "Name": "AlignedSPFPass", "Docs": "", "Typewords": ["bool"] }, { "Name": "OverrideReasons", "Docs": "", "Typewords": ["[]", "PolicyOverrideReason"] }, { "Name": "EnvelopeTo", "Docs": "", "Typewords": ["string"] }, { "Name": "EnvelopeFrom", "Docs": "", "Typewords": ["string"] }, { "Name": "HeaderFrom", "Docs": "", "Typewords": ["string"] }, { "Name": "DKIMResults", "Docs": "", "Typewords": ["[]", "DKIMAuthResult"] }, { "Name": "SPFResults", "Docs": "", "Typewords": ["[]", "SPFAuthResult"] }] },
"SuppressAddress": { "Name": "SuppressAddress", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Inserted", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "ReportingAddress", "Docs": "", "Typewords": ["string"] }, { "Name": "Until", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "Comment", "Docs": "", "Typewords": ["string"] }] }, "SuppressAddress": { "Name": "SuppressAddress", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Inserted", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "ReportingAddress", "Docs": "", "Typewords": ["string"] }, { "Name": "Until", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "Comment", "Docs": "", "Typewords": ["string"] }] },
@ -492,7 +491,6 @@ var api;
SMTPAuth: (v) => api.parse("SMTPAuth", v), SMTPAuth: (v) => api.parse("SMTPAuth", v),
TransportSocks: (v) => api.parse("TransportSocks", v), TransportSocks: (v) => api.parse("TransportSocks", v),
TransportDirect: (v) => api.parse("TransportDirect", v), TransportDirect: (v) => api.parse("TransportDirect", v),
TransportFail: (v) => api.parse("TransportFail", v),
EvaluationStat: (v) => api.parse("EvaluationStat", v), EvaluationStat: (v) => api.parse("EvaluationStat", v),
Evaluation: (v) => api.parse("Evaluation", v), Evaluation: (v) => api.parse("Evaluation", v),
SuppressAddress: (v) => api.parse("SuppressAddress", v), SuppressAddress: (v) => api.parse("SuppressAddress", v),

View File

@ -6901,14 +6901,6 @@
"nullable", "nullable",
"TransportDirect" "TransportDirect"
] ]
},
{
"Name": "Fail",
"Docs": "",
"Typewords": [
"nullable",
"TransportFail"
]
} }
] ]
}, },
@ -7030,40 +7022,6 @@
} }
] ]
}, },
{
"Name": "TransportFail",
"Docs": "TransportFail is a transport that fails all delivery attempts.",
"Fields": [
{
"Name": "SMTPCode",
"Docs": "",
"Typewords": [
"int32"
]
},
{
"Name": "SMTPMessage",
"Docs": "",
"Typewords": [
"string"
]
},
{
"Name": "Code",
"Docs": "Effective values to use, set when parsing.",
"Typewords": [
"int32"
]
},
{
"Name": "Message",
"Docs": "",
"Typewords": [
"string"
]
}
]
},
{ {
"Name": "EvaluationStat", "Name": "EvaluationStat",
"Docs": "EvaluationStat summarizes stored evaluations, for inclusion in an upcoming\naggregate report, for a domain.", "Docs": "EvaluationStat summarizes stored evaluations, for inclusion in an upcoming\naggregate report, for a domain.",

View File

@ -948,7 +948,6 @@ export interface Transport {
SMTP?: TransportSMTP | null SMTP?: TransportSMTP | null
Socks?: TransportSocks | null Socks?: TransportSocks | null
Direct?: TransportDirect | null Direct?: TransportDirect | null
Fail?: TransportFail | null
} }
// TransportSMTP delivers messages by "submission" (SMTP, typically // TransportSMTP delivers messages by "submission" (SMTP, typically
@ -981,14 +980,6 @@ export interface TransportDirect {
DisableIPv6: boolean DisableIPv6: boolean
} }
// TransportFail is a transport that fails all delivery attempts.
export interface TransportFail {
SMTPCode: number
SMTPMessage: string
Code: number // Effective values to use, set when parsing.
Message: string
}
// EvaluationStat summarizes stored evaluations, for inclusion in an upcoming // EvaluationStat summarizes stored evaluations, for inclusion in an upcoming
// aggregate report, for a domain. // aggregate report, for a domain.
export interface EvaluationStat { export interface EvaluationStat {
@ -1160,7 +1151,7 @@ export enum AuthResult {
AuthAborted = "aborted", AuthAborted = "aborted",
} }
export const structTypes: {[typename: string]: boolean} = {"Account":true,"Address":true,"AddressAlias":true,"Alias":true,"AliasAddress":true,"AuthResults":true,"AutoconfCheckResult":true,"AutodiscoverCheckResult":true,"AutodiscoverSRV":true,"AutomaticJunkFlags":true,"Canonicalization":true,"CheckResult":true,"ClientConfigs":true,"ClientConfigsEntry":true,"ConfigDomain":true,"DANECheckResult":true,"DKIM":true,"DKIMAuthResult":true,"DKIMCheckResult":true,"DKIMRecord":true,"DMARC":true,"DMARCCheckResult":true,"DMARCRecord":true,"DMARCSummary":true,"DNSSECResult":true,"DateRange":true,"Destination":true,"Directive":true,"Domain":true,"DomainFeedback":true,"Dynamic":true,"Evaluation":true,"EvaluationStat":true,"Extension":true,"FailureDetails":true,"Filter":true,"HoldRule":true,"Hook":true,"HookFilter":true,"HookResult":true,"HookRetired":true,"HookRetiredFilter":true,"HookRetiredSort":true,"HookSort":true,"IPDomain":true,"IPRevCheckResult":true,"Identifiers":true,"IncomingWebhook":true,"JunkFilter":true,"LoginAttempt":true,"MTASTS":true,"MTASTSCheckResult":true,"MTASTSRecord":true,"MX":true,"MXCheckResult":true,"Modifier":true,"Msg":true,"MsgResult":true,"MsgRetired":true,"OutgoingWebhook":true,"Pair":true,"Policy":true,"PolicyEvaluated":true,"PolicyOverrideReason":true,"PolicyPublished":true,"PolicyRecord":true,"Record":true,"Report":true,"ReportMetadata":true,"ReportRecord":true,"Result":true,"ResultPolicy":true,"RetiredFilter":true,"RetiredSort":true,"Reverse":true,"Route":true,"Row":true,"Ruleset":true,"SMTPAuth":true,"SPFAuthResult":true,"SPFCheckResult":true,"SPFRecord":true,"SRV":true,"SRVConfCheckResult":true,"STSMX":true,"Selector":true,"Sort":true,"SubjectPass":true,"Summary":true,"SuppressAddress":true,"TLSCheckResult":true,"TLSPublicKey":true,"TLSRPT":true,"TLSRPTCheckResult":true,"TLSRPTDateRange":true,"TLSRPTRecord":true,"TLSRPTSummary":true,"TLSRPTSuppressAddress":true,"TLSReportRecord":true,"TLSResult":true,"Transport":true,"TransportDirect":true,"TransportFail":true,"TransportSMTP":true,"TransportSocks":true,"URI":true,"WebForward":true,"WebHandler":true,"WebInternal":true,"WebRedirect":true,"WebStatic":true,"WebserverConfig":true} export const structTypes: {[typename: string]: boolean} = {"Account":true,"Address":true,"AddressAlias":true,"Alias":true,"AliasAddress":true,"AuthResults":true,"AutoconfCheckResult":true,"AutodiscoverCheckResult":true,"AutodiscoverSRV":true,"AutomaticJunkFlags":true,"Canonicalization":true,"CheckResult":true,"ClientConfigs":true,"ClientConfigsEntry":true,"ConfigDomain":true,"DANECheckResult":true,"DKIM":true,"DKIMAuthResult":true,"DKIMCheckResult":true,"DKIMRecord":true,"DMARC":true,"DMARCCheckResult":true,"DMARCRecord":true,"DMARCSummary":true,"DNSSECResult":true,"DateRange":true,"Destination":true,"Directive":true,"Domain":true,"DomainFeedback":true,"Dynamic":true,"Evaluation":true,"EvaluationStat":true,"Extension":true,"FailureDetails":true,"Filter":true,"HoldRule":true,"Hook":true,"HookFilter":true,"HookResult":true,"HookRetired":true,"HookRetiredFilter":true,"HookRetiredSort":true,"HookSort":true,"IPDomain":true,"IPRevCheckResult":true,"Identifiers":true,"IncomingWebhook":true,"JunkFilter":true,"LoginAttempt":true,"MTASTS":true,"MTASTSCheckResult":true,"MTASTSRecord":true,"MX":true,"MXCheckResult":true,"Modifier":true,"Msg":true,"MsgResult":true,"MsgRetired":true,"OutgoingWebhook":true,"Pair":true,"Policy":true,"PolicyEvaluated":true,"PolicyOverrideReason":true,"PolicyPublished":true,"PolicyRecord":true,"Record":true,"Report":true,"ReportMetadata":true,"ReportRecord":true,"Result":true,"ResultPolicy":true,"RetiredFilter":true,"RetiredSort":true,"Reverse":true,"Route":true,"Row":true,"Ruleset":true,"SMTPAuth":true,"SPFAuthResult":true,"SPFCheckResult":true,"SPFRecord":true,"SRV":true,"SRVConfCheckResult":true,"STSMX":true,"Selector":true,"Sort":true,"SubjectPass":true,"Summary":true,"SuppressAddress":true,"TLSCheckResult":true,"TLSPublicKey":true,"TLSRPT":true,"TLSRPTCheckResult":true,"TLSRPTDateRange":true,"TLSRPTRecord":true,"TLSRPTSummary":true,"TLSRPTSuppressAddress":true,"TLSReportRecord":true,"TLSResult":true,"Transport":true,"TransportDirect":true,"TransportSMTP":true,"TransportSocks":true,"URI":true,"WebForward":true,"WebHandler":true,"WebInternal":true,"WebRedirect":true,"WebStatic":true,"WebserverConfig":true}
export const stringsTypes: {[typename: string]: boolean} = {"Align":true,"AuthResult":true,"CSRFToken":true,"DMARCPolicy":true,"IP":true,"Localpart":true,"Mode":true,"RUA":true} export const stringsTypes: {[typename: string]: boolean} = {"Align":true,"AuthResult":true,"CSRFToken":true,"DMARCPolicy":true,"IP":true,"Localpart":true,"Mode":true,"RUA":true}
export const intsTypes: {[typename: string]: boolean} = {} export const intsTypes: {[typename: string]: boolean} = {}
export const types: TypenameMap = { export const types: TypenameMap = {
@ -1262,12 +1253,11 @@ export const types: TypenameMap = {
"WebRedirect": {"Name":"WebRedirect","Docs":"","Fields":[{"Name":"BaseURL","Docs":"","Typewords":["string"]},{"Name":"OrigPathRegexp","Docs":"","Typewords":["string"]},{"Name":"ReplacePath","Docs":"","Typewords":["string"]},{"Name":"StatusCode","Docs":"","Typewords":["int32"]}]}, "WebRedirect": {"Name":"WebRedirect","Docs":"","Fields":[{"Name":"BaseURL","Docs":"","Typewords":["string"]},{"Name":"OrigPathRegexp","Docs":"","Typewords":["string"]},{"Name":"ReplacePath","Docs":"","Typewords":["string"]},{"Name":"StatusCode","Docs":"","Typewords":["int32"]}]},
"WebForward": {"Name":"WebForward","Docs":"","Fields":[{"Name":"StripPath","Docs":"","Typewords":["bool"]},{"Name":"URL","Docs":"","Typewords":["string"]},{"Name":"ResponseHeaders","Docs":"","Typewords":["{}","string"]}]}, "WebForward": {"Name":"WebForward","Docs":"","Fields":[{"Name":"StripPath","Docs":"","Typewords":["bool"]},{"Name":"URL","Docs":"","Typewords":["string"]},{"Name":"ResponseHeaders","Docs":"","Typewords":["{}","string"]}]},
"WebInternal": {"Name":"WebInternal","Docs":"","Fields":[{"Name":"BasePath","Docs":"","Typewords":["string"]},{"Name":"Service","Docs":"","Typewords":["string"]}]}, "WebInternal": {"Name":"WebInternal","Docs":"","Fields":[{"Name":"BasePath","Docs":"","Typewords":["string"]},{"Name":"Service","Docs":"","Typewords":["string"]}]},
"Transport": {"Name":"Transport","Docs":"","Fields":[{"Name":"Submissions","Docs":"","Typewords":["nullable","TransportSMTP"]},{"Name":"Submission","Docs":"","Typewords":["nullable","TransportSMTP"]},{"Name":"SMTP","Docs":"","Typewords":["nullable","TransportSMTP"]},{"Name":"Socks","Docs":"","Typewords":["nullable","TransportSocks"]},{"Name":"Direct","Docs":"","Typewords":["nullable","TransportDirect"]},{"Name":"Fail","Docs":"","Typewords":["nullable","TransportFail"]}]}, "Transport": {"Name":"Transport","Docs":"","Fields":[{"Name":"Submissions","Docs":"","Typewords":["nullable","TransportSMTP"]},{"Name":"Submission","Docs":"","Typewords":["nullable","TransportSMTP"]},{"Name":"SMTP","Docs":"","Typewords":["nullable","TransportSMTP"]},{"Name":"Socks","Docs":"","Typewords":["nullable","TransportSocks"]},{"Name":"Direct","Docs":"","Typewords":["nullable","TransportDirect"]}]},
"TransportSMTP": {"Name":"TransportSMTP","Docs":"","Fields":[{"Name":"Host","Docs":"","Typewords":["string"]},{"Name":"Port","Docs":"","Typewords":["int32"]},{"Name":"STARTTLSInsecureSkipVerify","Docs":"","Typewords":["bool"]},{"Name":"NoSTARTTLS","Docs":"","Typewords":["bool"]},{"Name":"Auth","Docs":"","Typewords":["nullable","SMTPAuth"]}]}, "TransportSMTP": {"Name":"TransportSMTP","Docs":"","Fields":[{"Name":"Host","Docs":"","Typewords":["string"]},{"Name":"Port","Docs":"","Typewords":["int32"]},{"Name":"STARTTLSInsecureSkipVerify","Docs":"","Typewords":["bool"]},{"Name":"NoSTARTTLS","Docs":"","Typewords":["bool"]},{"Name":"Auth","Docs":"","Typewords":["nullable","SMTPAuth"]}]},
"SMTPAuth": {"Name":"SMTPAuth","Docs":"","Fields":[{"Name":"Username","Docs":"","Typewords":["string"]},{"Name":"Password","Docs":"","Typewords":["string"]},{"Name":"Mechanisms","Docs":"","Typewords":["[]","string"]}]}, "SMTPAuth": {"Name":"SMTPAuth","Docs":"","Fields":[{"Name":"Username","Docs":"","Typewords":["string"]},{"Name":"Password","Docs":"","Typewords":["string"]},{"Name":"Mechanisms","Docs":"","Typewords":["[]","string"]}]},
"TransportSocks": {"Name":"TransportSocks","Docs":"","Fields":[{"Name":"Address","Docs":"","Typewords":["string"]},{"Name":"RemoteIPs","Docs":"","Typewords":["[]","string"]},{"Name":"RemoteHostname","Docs":"","Typewords":["string"]}]}, "TransportSocks": {"Name":"TransportSocks","Docs":"","Fields":[{"Name":"Address","Docs":"","Typewords":["string"]},{"Name":"RemoteIPs","Docs":"","Typewords":["[]","string"]},{"Name":"RemoteHostname","Docs":"","Typewords":["string"]}]},
"TransportDirect": {"Name":"TransportDirect","Docs":"","Fields":[{"Name":"DisableIPv4","Docs":"","Typewords":["bool"]},{"Name":"DisableIPv6","Docs":"","Typewords":["bool"]}]}, "TransportDirect": {"Name":"TransportDirect","Docs":"","Fields":[{"Name":"DisableIPv4","Docs":"","Typewords":["bool"]},{"Name":"DisableIPv6","Docs":"","Typewords":["bool"]}]},
"TransportFail": {"Name":"TransportFail","Docs":"","Fields":[{"Name":"SMTPCode","Docs":"","Typewords":["int32"]},{"Name":"SMTPMessage","Docs":"","Typewords":["string"]},{"Name":"Code","Docs":"","Typewords":["int32"]},{"Name":"Message","Docs":"","Typewords":["string"]}]},
"EvaluationStat": {"Name":"EvaluationStat","Docs":"","Fields":[{"Name":"Domain","Docs":"","Typewords":["Domain"]},{"Name":"Dispositions","Docs":"","Typewords":["[]","string"]},{"Name":"Count","Docs":"","Typewords":["int32"]},{"Name":"SendReport","Docs":"","Typewords":["bool"]}]}, "EvaluationStat": {"Name":"EvaluationStat","Docs":"","Fields":[{"Name":"Domain","Docs":"","Typewords":["Domain"]},{"Name":"Dispositions","Docs":"","Typewords":["[]","string"]},{"Name":"Count","Docs":"","Typewords":["int32"]},{"Name":"SendReport","Docs":"","Typewords":["bool"]}]},
"Evaluation": {"Name":"Evaluation","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"PolicyDomain","Docs":"","Typewords":["string"]},{"Name":"Evaluated","Docs":"","Typewords":["timestamp"]},{"Name":"Optional","Docs":"","Typewords":["bool"]},{"Name":"IntervalHours","Docs":"","Typewords":["int32"]},{"Name":"Addresses","Docs":"","Typewords":["[]","string"]},{"Name":"PolicyPublished","Docs":"","Typewords":["PolicyPublished"]},{"Name":"SourceIP","Docs":"","Typewords":["string"]},{"Name":"Disposition","Docs":"","Typewords":["string"]},{"Name":"AlignedDKIMPass","Docs":"","Typewords":["bool"]},{"Name":"AlignedSPFPass","Docs":"","Typewords":["bool"]},{"Name":"OverrideReasons","Docs":"","Typewords":["[]","PolicyOverrideReason"]},{"Name":"EnvelopeTo","Docs":"","Typewords":["string"]},{"Name":"EnvelopeFrom","Docs":"","Typewords":["string"]},{"Name":"HeaderFrom","Docs":"","Typewords":["string"]},{"Name":"DKIMResults","Docs":"","Typewords":["[]","DKIMAuthResult"]},{"Name":"SPFResults","Docs":"","Typewords":["[]","SPFAuthResult"]}]}, "Evaluation": {"Name":"Evaluation","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"PolicyDomain","Docs":"","Typewords":["string"]},{"Name":"Evaluated","Docs":"","Typewords":["timestamp"]},{"Name":"Optional","Docs":"","Typewords":["bool"]},{"Name":"IntervalHours","Docs":"","Typewords":["int32"]},{"Name":"Addresses","Docs":"","Typewords":["[]","string"]},{"Name":"PolicyPublished","Docs":"","Typewords":["PolicyPublished"]},{"Name":"SourceIP","Docs":"","Typewords":["string"]},{"Name":"Disposition","Docs":"","Typewords":["string"]},{"Name":"AlignedDKIMPass","Docs":"","Typewords":["bool"]},{"Name":"AlignedSPFPass","Docs":"","Typewords":["bool"]},{"Name":"OverrideReasons","Docs":"","Typewords":["[]","PolicyOverrideReason"]},{"Name":"EnvelopeTo","Docs":"","Typewords":["string"]},{"Name":"EnvelopeFrom","Docs":"","Typewords":["string"]},{"Name":"HeaderFrom","Docs":"","Typewords":["string"]},{"Name":"DKIMResults","Docs":"","Typewords":["[]","DKIMAuthResult"]},{"Name":"SPFResults","Docs":"","Typewords":["[]","SPFAuthResult"]}]},
"SuppressAddress": {"Name":"SuppressAddress","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"Inserted","Docs":"","Typewords":["timestamp"]},{"Name":"ReportingAddress","Docs":"","Typewords":["string"]},{"Name":"Until","Docs":"","Typewords":["timestamp"]},{"Name":"Comment","Docs":"","Typewords":["string"]}]}, "SuppressAddress": {"Name":"SuppressAddress","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"Inserted","Docs":"","Typewords":["timestamp"]},{"Name":"ReportingAddress","Docs":"","Typewords":["string"]},{"Name":"Until","Docs":"","Typewords":["timestamp"]},{"Name":"Comment","Docs":"","Typewords":["string"]}]},
@ -1390,7 +1380,6 @@ export const parser = {
SMTPAuth: (v: any) => parse("SMTPAuth", v) as SMTPAuth, SMTPAuth: (v: any) => parse("SMTPAuth", v) as SMTPAuth,
TransportSocks: (v: any) => parse("TransportSocks", v) as TransportSocks, TransportSocks: (v: any) => parse("TransportSocks", v) as TransportSocks,
TransportDirect: (v: any) => parse("TransportDirect", v) as TransportDirect, TransportDirect: (v: any) => parse("TransportDirect", v) as TransportDirect,
TransportFail: (v: any) => parse("TransportFail", v) as TransportFail,
EvaluationStat: (v: any) => parse("EvaluationStat", v) as EvaluationStat, EvaluationStat: (v: any) => parse("EvaluationStat", v) as EvaluationStat,
Evaluation: (v: any) => parse("Evaluation", v) as Evaluation, Evaluation: (v: any) => parse("Evaluation", v) as Evaluation,
SuppressAddress: (v: any) => parse("SuppressAddress", v) as SuppressAddress, SuppressAddress: (v: any) => parse("SuppressAddress", v) as SuppressAddress,

View File

@ -18,6 +18,7 @@ import (
"log/slog" "log/slog"
"mime" "mime"
"mime/multipart" "mime/multipart"
"net"
"net/http" "net/http"
"net/textproto" "net/textproto"
"os" "os"
@ -421,7 +422,7 @@ func (s server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Check(werr, "writing error response") log.Check(werr, "writing error response")
} }
la := loginAttempt(remoteIP.String(), r, "webapi", "httpbasic") la := loginAttempt(r, "webapi", "httpbasic")
la.LoginAddress = email la.LoginAddress = email
defer func() { defer func() {
store.LoginAttemptAdd(context.Background(), log, la) store.LoginAttemptAdd(context.Background(), log, la)
@ -529,7 +530,12 @@ func (s server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// loginAttempt initializes a store.LoginAttempt, for adding to the store after // loginAttempt initializes a store.LoginAttempt, for adding to the store after
// filling in the results and other details. // filling in the results and other details.
func loginAttempt(remoteIP string, r *http.Request, protocol, authMech string) store.LoginAttempt { func loginAttempt(r *http.Request, protocol, authMech string) store.LoginAttempt {
remoteIP, _, _ := net.SplitHostPort(r.RemoteAddr)
if remoteIP == "" {
remoteIP = r.RemoteAddr
}
return store.LoginAttempt{ return store.LoginAttempt{
RemoteIP: remoteIP, RemoteIP: remoteIP,
TLS: store.LoginAttemptTLS(r.TLS), TLS: store.LoginAttemptTLS(r.TLS),

View File

@ -80,7 +80,12 @@ type SessionAuth interface {
} }
// loginAttempt initializes a loginAttempt, for adding to the store after filling in the results and other details. // loginAttempt initializes a loginAttempt, for adding to the store after filling in the results and other details.
func loginAttempt(remoteIP string, r *http.Request, protocol, authMech string) store.LoginAttempt { func loginAttempt(r *http.Request, protocol, authMech string) store.LoginAttempt {
remoteIP, _, _ := net.SplitHostPort(r.RemoteAddr)
if remoteIP == "" {
remoteIP = r.RemoteAddr
}
return store.LoginAttempt{ return store.LoginAttempt{
RemoteIP: remoteIP, RemoteIP: remoteIP,
TLS: store.LoginAttemptTLS(r.TLS), TLS: store.LoginAttemptTLS(r.TLS),
@ -158,7 +163,7 @@ func Check(ctx context.Context, log mlog.Log, sessionAuth SessionAuth, kind stri
return return
} }
la := loginAttempt(ip.String(), r, kind, "websession") la := loginAttempt(r, kind, "websession")
defer func() { defer func() {
store.LoginAttemptAdd(context.Background(), log, la) store.LoginAttemptAdd(context.Background(), log, la)
}() }()
@ -266,7 +271,7 @@ func Login(ctx context.Context, log mlog.Log, sessionAuth SessionAuth, kind, coo
username = norm.NFC.String(username) username = norm.NFC.String(username)
valid, disabled, accountName, err := sessionAuth.login(ctx, log, username, password) valid, disabled, accountName, err := sessionAuth.login(ctx, log, username, password)
la := loginAttempt(ip.String(), r, kind, "weblogin") la := loginAttempt(r, kind, "weblogin")
la.LoginAddress = username la.LoginAddress = username
la.AccountName = accountName la.AccountName = accountName
defer func() { defer func() {

View File

@ -1717,7 +1717,7 @@ func recipientSecurity(ctx context.Context, log mlog.Log, resolver dns.Resolver,
defer logPanic(ctx) defer logPanic(ctx)
defer wg.Done() defer wg.Done()
_, origNextHopAuthentic, expandedNextHopAuthentic, _, hostPrefs, _, err := smtpclient.GatherDestinations(ctx, log.Logger, resolver, dns.IPDomain{Domain: addr.Domain}) _, origNextHopAuthentic, expandedNextHopAuthentic, _, hosts, _, err := smtpclient.GatherDestinations(ctx, log.Logger, resolver, dns.IPDomain{Domain: addr.Domain})
if err != nil { if err != nil {
rs.DNSSEC = SecurityResultError rs.DNSSEC = SecurityResultError
return return
@ -1734,10 +1734,10 @@ func recipientSecurity(ctx context.Context, log mlog.Log, resolver dns.Resolver,
} }
// We're only looking at the first host to deliver to (typically first mx destination). // We're only looking at the first host to deliver to (typically first mx destination).
if len(hostPrefs) == 0 || hostPrefs[0].Host.Domain.IsZero() { if len(hosts) == 0 || hosts[0].Domain.IsZero() {
return // Should not happen. return // Should not happen.
} }
host := hostPrefs[0].Host host := hosts[0]
// Resolve the IPs. Required for DANE to prevent bad DNS servers from causing an // Resolve the IPs. Required for DANE to prevent bad DNS servers from causing an
// error result instead of no-DANE result. // error result instead of no-DANE result.