2023-01-30 16:27:06 +03:00
package mox
import (
"bytes"
"context"
"crypto/ed25519"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"net"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"sync"
"time"
"golang.org/x/text/unicode/norm"
"github.com/mjl-/sconf"
"github.com/mjl-/mox/autotls"
"github.com/mjl-/mox/config"
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/mlog"
"github.com/mjl-/mox/moxio"
"github.com/mjl-/mox/mtasts"
"github.com/mjl-/mox/smtp"
)
var xlog = mlog . New ( "mox" )
// Config paths are set early in program startup. They will point to files in
// the same directory.
var (
ConfigStaticPath string
ConfigDynamicPath string
Conf = Config { Log : map [ string ] mlog . Level { "" : mlog . LevelError } }
)
// Config as used in the code, a processed version of what is in the config file.
//
// Use methods to lookup a domain/account/address in the dynamic configuration.
type Config struct {
Static config . Static // Does not change during the lifetime of a running instance.
logMutex sync . Mutex // For accessing the log levels.
Log map [ string ] mlog . Level
dynamicMutex sync . Mutex
Dynamic config . Dynamic // Can only be accessed directly by tests. Use methods on Config for locked access.
dynamicMtime time . Time
DynamicLastCheck time . Time // For use by quickstart only to skip checks.
// From correctly-cased full address (localpart@domain) to account and
// address. Domains are IDNA names in utf8.
accountDestinations map [ string ] AccountDestination
}
type AccountDestination struct {
Localpart smtp . Localpart
Account string
Destination config . Destination
}
// SetLogLevel sets a new log level for pkg. An empty pkg sets the default log
// value that is used if no explicit log level is configured for a package.
// This change is ephemeral, no config file is changed.
func ( c * Config ) SetLogLevel ( pkg string , level mlog . Level ) {
c . logMutex . Lock ( )
defer c . logMutex . Unlock ( )
l := c . copyLogLevels ( )
l [ pkg ] = level
c . Log = l
xlog . Print ( "log level changed" , mlog . Field ( "pkg" , pkg ) , mlog . Field ( "level" , mlog . LevelStrings [ level ] ) )
mlog . SetConfig ( c . Log )
}
// copyLogLevels returns a copy of c.Log, for modifications.
// must be called with log lock held.
func ( c * Config ) copyLogLevels ( ) map [ string ] mlog . Level {
m := map [ string ] mlog . Level { }
for pkg , level := range c . Log {
m [ pkg ] = level
}
return m
}
// LogLevels returns a copy of the current log levels.
func ( c * Config ) LogLevels ( ) map [ string ] mlog . Level {
c . logMutex . Lock ( )
defer c . logMutex . Unlock ( )
return c . copyLogLevels ( )
}
func ( c * Config ) withDynamicLock ( fn func ( ) ) {
c . dynamicMutex . Lock ( )
defer c . dynamicMutex . Unlock ( )
now := time . Now ( )
if now . Sub ( c . DynamicLastCheck ) > time . Second {
c . DynamicLastCheck = now
if fi , err := os . Stat ( ConfigDynamicPath ) ; err != nil {
xlog . Errorx ( "stat domains config" , err )
} else if ! fi . ModTime ( ) . Equal ( c . dynamicMtime ) {
if errs := c . loadDynamic ( ) ; len ( errs ) > 0 {
xlog . Errorx ( "loading domains config" , errs [ 0 ] , mlog . Field ( "errors" , errs ) )
} else {
xlog . Info ( "domains config reloaded" )
c . dynamicMtime = fi . ModTime ( )
}
}
}
fn ( )
}
// must be called with dynamic lock held.
func ( c * Config ) loadDynamic ( ) [ ] error {
d , mtime , accDests , err := ParseDynamicConfig ( context . Background ( ) , ConfigDynamicPath , c . Static )
if err != nil {
return err
}
c . Dynamic = d
c . dynamicMtime = mtime
c . accountDestinations = accDests
return nil
}
func ( c * Config ) Domains ( ) ( l [ ] string ) {
c . withDynamicLock ( func ( ) {
for name := range c . Dynamic . Domains {
l = append ( l , name )
}
} )
sort . Slice ( l , func ( i , j int ) bool {
return l [ i ] < l [ j ]
} )
return l
}
func ( c * Config ) Accounts ( ) ( l [ ] string ) {
c . withDynamicLock ( func ( ) {
for name := range c . Dynamic . Accounts {
l = append ( l , name )
}
} )
return
}
func ( c * Config ) DomainLocalparts ( d dns . Domain ) map [ smtp . Localpart ] string {
suffix := "@" + d . Name ( )
m := map [ smtp . Localpart ] string { }
c . withDynamicLock ( func ( ) {
for addr , ad := range c . accountDestinations {
if strings . HasSuffix ( addr , suffix ) {
m [ ad . Localpart ] = ad . Account
}
}
} )
return m
}
func ( c * Config ) Domain ( d dns . Domain ) ( dom config . Domain , ok bool ) {
c . withDynamicLock ( func ( ) {
dom , ok = c . Dynamic . Domains [ d . Name ( ) ]
} )
return
}
func ( c * Config ) Account ( name string ) ( acc config . Account , ok bool ) {
c . withDynamicLock ( func ( ) {
acc , ok = c . Dynamic . Accounts [ name ]
} )
return
}
func ( c * Config ) AccountDestination ( addr string ) ( accDests AccountDestination , ok bool ) {
c . withDynamicLock ( func ( ) {
accDests , ok = c . accountDestinations [ addr ]
} )
return
}
func ( c * Config ) allowACMEHosts ( ) {
// todo future: reset the allowed hosts for autoconfig & mtasts when loading new list.
for _ , l := range c . Static . Listeners {
if l . TLS == nil || l . TLS . ACME == "" {
continue
}
m := c . Static . ACME [ l . TLS . ACME ] . Manager
for _ , dom := range c . Dynamic . Domains {
if l . AutoconfigHTTPS . Enabled {
2023-02-03 19:53:45 +03:00
if d , err := dns . ParseDomain ( "autoconfig." + dom . Domain . ASCII ) ; err != nil {
2023-01-30 16:27:06 +03:00
xlog . Errorx ( "parsing autoconfig domain" , err , mlog . Field ( "domain" , dom . Domain ) )
2023-02-03 19:53:45 +03:00
} else {
m . AllowHostname ( d )
}
if d , err := dns . ParseDomain ( "autodiscover." + dom . Domain . ASCII ) ; err != nil {
xlog . Errorx ( "parsing autodiscover domain" , err , mlog . Field ( "domain" , dom . Domain ) )
} else {
m . AllowHostname ( d )
2023-01-30 16:27:06 +03:00
}
}
if l . MTASTSHTTPS . Enabled && dom . MTASTS != nil {
d , err := dns . ParseDomain ( "mta-sts." + dom . Domain . ASCII )
if err != nil {
xlog . Errorx ( "parsing mta-sts domain" , err , mlog . Field ( "domain" , dom . Domain ) )
continue
}
m . AllowHostname ( d )
}
}
}
}
// todo future: write config parsing & writing code that can read a config and remembers the exact tokens including newlines and comments, and can write back a modified file. the goal is to be able to write a config file automatically (after changing fields through the ui), but not loose comments and whitespace, to still get useful diffs for storing the config in a version control system.
// must be called with lock held.
func writeDynamic ( ctx context . Context , c config . Dynamic ) error {
accDests , errs := prepareDynamicConfig ( ctx , ConfigDynamicPath , Conf . Static , & c )
if len ( errs ) > 0 {
return errs [ 0 ]
}
var b bytes . Buffer
err := sconf . Write ( & b , c )
if err != nil {
return err
}
f , err := os . OpenFile ( ConfigDynamicPath , os . O_WRONLY , 0660 )
if err != nil {
return err
}
defer func ( ) {
if f != nil {
f . Close ( )
}
} ( )
buf := b . Bytes ( )
if _ , err := f . Write ( buf ) ; err != nil {
return fmt . Errorf ( "write domains.conf: %v" , err )
}
if err := f . Truncate ( int64 ( len ( buf ) ) ) ; err != nil {
return fmt . Errorf ( "truncate domains.conf after write: %v" , err )
}
if err := f . Sync ( ) ; err != nil {
return fmt . Errorf ( "sync domains.conf after write: %v" , err )
}
if err := moxio . SyncDir ( filepath . Dir ( ConfigDynamicPath ) ) ; err != nil {
return fmt . Errorf ( "sync dir of domains.conf after write: %v" , err )
}
fi , err := f . Stat ( )
if err != nil {
return fmt . Errorf ( "stat after writing domains.conf: %v" , err )
}
Conf . dynamicMtime = fi . ModTime ( )
Conf . DynamicLastCheck = time . Now ( )
Conf . Dynamic = c
Conf . accountDestinations = accDests
Conf . allowACMEHosts ( )
return nil
}
// MustLoadConfig loads the config, quitting on errors.
func MustLoadConfig ( ) {
errs := LoadConfig ( context . Background ( ) )
if len ( errs ) > 1 {
xlog . Error ( "loading config file: multiple errors" )
for _ , err := range errs {
xlog . Errorx ( "config error" , err )
}
xlog . Fatal ( "stopping after multiple config errors" )
} else if len ( errs ) == 1 {
xlog . Fatalx ( "loading config file" , errs [ 0 ] )
}
}
// LoadConfig attempts to parse and load a config, returning any errors
// encountered.
func LoadConfig ( ctx context . Context ) [ ] error {
c , errs := ParseConfig ( ctx , ConfigStaticPath , false )
if len ( errs ) > 0 {
return errs
}
mlog . SetConfig ( c . Log )
SetConfig ( c )
return nil
}
// SetConfig sets a new config. Not to be used during normal operation.
func SetConfig ( c * Config ) {
// Cannot just assign *c to Conf, it would copy the mutex.
Conf = Config { c . Static , sync . Mutex { } , c . Log , sync . Mutex { } , c . Dynamic , c . dynamicMtime , c . DynamicLastCheck , c . accountDestinations }
}
// ParseConfig parses the static config at path p. If checkOnly is true, no
// changes are made, such as registering ACME identities.
func ParseConfig ( ctx context . Context , p string , checkOnly bool ) ( c * Config , errs [ ] error ) {
c = & Config {
Static : config . Static {
DataDir : "." ,
} ,
}
f , err := os . Open ( p )
if err != nil {
if os . IsNotExist ( err ) && os . Getenv ( "MOXCONF" ) == "" {
return nil , [ ] error { fmt . Errorf ( "open config file: %v (hint: use mox -config ... or set MOXCONF=...)" , err ) }
}
return nil , [ ] error { fmt . Errorf ( "open config file: %v" , err ) }
}
defer f . Close ( )
if err := sconf . Parse ( f , & c . Static ) ; err != nil {
return nil , [ ] error { fmt . Errorf ( "parsing %s: %v" , p , err ) }
}
if xerrs := PrepareStaticConfig ( ctx , p , c , checkOnly ) ; len ( xerrs ) > 0 {
return nil , xerrs
}
pp := filepath . Join ( filepath . Dir ( p ) , "domains.conf" )
c . Dynamic , c . dynamicMtime , c . accountDestinations , errs = ParseDynamicConfig ( ctx , pp , c . Static )
if ! checkOnly {
c . allowACMEHosts ( )
}
return c , errs
}
// PrepareStaticConfig parses the static config file and prepares data structures
// for starting mox. If checkOnly is set no substantial changes are made, like
// creating an ACME registration.
func PrepareStaticConfig ( ctx context . Context , configFile string , config * Config , checkOnly bool ) ( errs [ ] error ) {
addErrorf := func ( format string , args ... any ) {
errs = append ( errs , fmt . Errorf ( format , args ... ) )
}
c := & config . Static
// check that mailbox is in unicode NFC normalized form.
checkMailboxNormf := func ( mailbox string , format string , args ... any ) {
s := norm . NFC . String ( mailbox )
if mailbox != s {
msg := fmt . Sprintf ( format , args ... )
addErrorf ( "%s: mailbox %q is not in NFC normalized form, should be %q" , msg , mailbox , s )
}
}
// Post-process logging config.
if logLevel , ok := mlog . Levels [ c . LogLevel ] ; ok {
config . Log = map [ string ] mlog . Level { "" : logLevel }
} else {
addErrorf ( "invalid log level %q" , c . LogLevel )
}
for pkg , s := range c . PackageLogLevels {
if logLevel , ok := mlog . Levels [ s ] ; ok {
config . Log [ pkg ] = logLevel
} else {
addErrorf ( "invalid package log level %q" , s )
}
}
hostname , err := dns . ParseDomain ( c . Hostname )
if err != nil {
addErrorf ( "parsing hostname: %s" , err )
} else if hostname . Name ( ) != c . Hostname {
addErrorf ( "hostname must be in IDNA form %q" , hostname . Name ( ) )
}
c . HostnameDomain = hostname
for name , acme := range c . ACME {
if checkOnly {
continue
}
acmeDir := dataDirPath ( configFile , c . DataDir , "acme" )
os . MkdirAll ( acmeDir , 0770 )
manager , err := autotls . Load ( name , acmeDir , acme . ContactEmail , acme . DirectoryURL , Shutdown )
if err != nil {
addErrorf ( "loading ACME identity for %q: %s" , name , err )
}
acme . Manager = manager
c . ACME [ name ] = acme
}
var haveUnspecifiedSMTPListener bool
for name , l := range c . Listeners {
if l . Hostname != "" {
d , err := dns . ParseDomain ( l . Hostname )
if err != nil {
addErrorf ( "bad listener hostname %q: %s" , l . Hostname , err )
}
l . HostnameDomain = d
}
if l . TLS != nil {
if l . TLS . ACME != "" && len ( l . TLS . KeyCerts ) != 0 {
addErrorf ( "listener %q: cannot have ACME and static key/certificates" , name )
} else if l . TLS . ACME != "" {
acme , ok := c . ACME [ l . TLS . ACME ]
if ! ok {
addErrorf ( "listener %q: unknown ACME provider %q" , name , l . TLS . ACME )
}
// If only checking, we don't have an acme manager, so set an empty tls config to
// continue without errors.
var tlsconfig * tls . Config
if checkOnly {
tlsconfig = & tls . Config { }
} else {
tlsconfig = acme . Manager . TLSConfig . Clone ( )
l . TLS . ACMEConfig = acme . Manager . ACMETLSConfig
// SMTP STARTTLS connections are commonly made without SNI, because certificates
// often aren't validated.
hostname := c . HostnameDomain
if l . Hostname != "" {
hostname = l . HostnameDomain
}
getCert := tlsconfig . GetCertificate
tlsconfig . GetCertificate = func ( hello * tls . ClientHelloInfo ) ( * tls . Certificate , error ) {
if hello . ServerName == "" {
hello . ServerName = hostname . ASCII
}
return getCert ( hello )
}
}
l . TLS . Config = tlsconfig
} else if len ( l . TLS . KeyCerts ) != 0 {
if err := loadTLSKeyCerts ( configFile , "listener " + name , l . TLS ) ; err != nil {
addErrorf ( "%w" , err )
}
} else {
addErrorf ( "listener %q: cannot have TLS config without ACME and without static keys/certificates" , name )
}
// TLS 1.2 was introduced in 2008. TLS <1.2 was deprecated by ../rfc/8996:31 and ../rfc/8997:66 in 2021.
var minVersion uint16 = tls . VersionTLS12
if l . TLS . MinVersion != "" {
versions := map [ string ] uint16 {
"TLSv1.0" : tls . VersionTLS10 ,
"TLSv1.1" : tls . VersionTLS11 ,
"TLSv1.2" : tls . VersionTLS12 ,
"TLSv1.3" : tls . VersionTLS13 ,
}
v , ok := versions [ l . TLS . MinVersion ]
if ! ok {
addErrorf ( "listener %q: unknown TLS mininum version %q" , name , l . TLS . MinVersion )
}
minVersion = v
}
if l . TLS . Config != nil {
l . TLS . Config . MinVersion = minVersion
}
if l . TLS . ACMEConfig != nil {
l . TLS . ACMEConfig . MinVersion = minVersion
}
} else if l . IMAPS . Enabled || l . SMTP . Enabled && ! l . SMTP . NoSTARTTLS || l . Submissions . Enabled || l . Submission . Enabled && ! l . Submission . NoRequireSTARTTLS || l . AdminHTTPS . Enabled || l . AutoconfigHTTPS . Enabled || l . MTASTSHTTPS . Enabled {
addErrorf ( "listener %q requires TLS, but does not specify tls config" , name )
}
if l . AutoconfigHTTPS . Enabled && ( ! l . IMAP . Enabled && ! l . IMAPS . Enabled || ! l . Submission . Enabled && ! l . Submissions . Enabled ) {
addErrorf ( "listener %q with autoconfig enabled must have SMTP submission or submissions and IMAP or IMAPS enabled" , name )
}
if l . SMTP . Enabled {
if len ( l . IPs ) == 0 {
haveUnspecifiedSMTPListener = true
}
for _ , ipstr := range l . IPs {
ip := net . ParseIP ( ipstr )
if ip == nil {
addErrorf ( "listener %q has invalid IP %q" , name , ipstr )
continue
}
if ip . IsUnspecified ( ) {
haveUnspecifiedSMTPListener = true
break
}
if len ( c . SpecifiedSMTPListenIPs ) >= 2 {
haveUnspecifiedSMTPListener = true
} else if len ( c . SpecifiedSMTPListenIPs ) > 0 && ( c . SpecifiedSMTPListenIPs [ 0 ] . To4 ( ) == nil ) == ( ip . To4 ( ) == nil ) {
haveUnspecifiedSMTPListener = true
} else {
c . SpecifiedSMTPListenIPs = append ( c . SpecifiedSMTPListenIPs , ip )
}
}
}
for _ , s := range l . SMTP . DNSBLs {
d , err := dns . ParseDomain ( s )
if err != nil {
addErrorf ( "listener %q has invalid DNSBL zone %q" , name , s )
continue
}
l . SMTP . DNSBLZones = append ( l . SMTP . DNSBLZones , d )
}
c . Listeners [ name ] = l
}
if haveUnspecifiedSMTPListener {
c . SpecifiedSMTPListenIPs = nil
}
for _ , mb := range c . DefaultMailboxes {
checkMailboxNormf ( mb , "default mailbox" )
}
// Load CA certificate pool.
if c . TLS . CA != nil {
if c . TLS . CA . AdditionalToSystem {
var err error
c . TLS . CertPool , err = x509 . SystemCertPool ( )
if err != nil {
addErrorf ( "fetching system CA cert pool: %v" , err )
}
} else {
c . TLS . CertPool = x509 . NewCertPool ( )
}
for _ , certfile := range c . TLS . CA . CertFiles {
p := configDirPath ( configFile , certfile )
pemBuf , err := os . ReadFile ( p )
if err != nil {
addErrorf ( "reading TLS CA cert file: %v" , err )
continue
} else if ! c . TLS . CertPool . AppendCertsFromPEM ( pemBuf ) {
// todo: can we check more fully if we're getting some useful data back?
addErrorf ( "no CA certs added from %q" , p )
}
}
}
return
}
// PrepareDynamicConfig parses the dynamic config file given a static file.
func ParseDynamicConfig ( ctx context . Context , dynamicPath string , static config . Static ) ( c config . Dynamic , mtime time . Time , accDests map [ string ] AccountDestination , errs [ ] error ) {
addErrorf := func ( format string , args ... any ) {
errs = append ( errs , fmt . Errorf ( format , args ... ) )
}
f , err := os . Open ( dynamicPath )
if err != nil {
addErrorf ( "parsing domains config: %v" , err )
return
}
defer f . Close ( )
fi , err := f . Stat ( )
if err != nil {
addErrorf ( "stat domains config: %v" , err )
}
if err := sconf . Parse ( f , & c ) ; err != nil {
addErrorf ( "parsing dynamic config file: %v" , err )
return
}
accDests , errs = prepareDynamicConfig ( ctx , dynamicPath , static , & c )
return c , fi . ModTime ( ) , accDests , errs
}
func prepareDynamicConfig ( ctx context . Context , dynamicPath string , static config . Static , c * config . Dynamic ) ( accDests map [ string ] AccountDestination , errs [ ] error ) {
log := xlog . WithContext ( ctx )
addErrorf := func ( format string , args ... any ) {
errs = append ( errs , fmt . Errorf ( format , args ... ) )
}
// check that mailbox is in unicode NFC normalized form.
checkMailboxNormf := func ( mailbox string , format string , args ... any ) {
s := norm . NFC . String ( mailbox )
if mailbox != s {
msg := fmt . Sprintf ( format , args ... )
addErrorf ( "%s: mailbox %q is not in NFC normalized form, should be %q" , msg , mailbox , s )
}
}
// Validate postmaster account exists.
if _ , ok := c . Accounts [ static . Postmaster . Account ] ; ! ok {
addErrorf ( "postmaster account %q does not exist" , static . Postmaster . Account )
}
checkMailboxNormf ( static . Postmaster . Mailbox , "postmaster mailbox" )
var haveSTSListener bool
for _ , l := range static . Listeners {
if l . MTASTSHTTPS . Enabled {
haveSTSListener = true
break
}
}
// Validate domains.
for d , domain := range c . Domains {
dnsdomain , err := dns . ParseDomain ( d )
if err != nil {
addErrorf ( "bad domain %q: %s" , d , err )
} else if dnsdomain . Name ( ) != d {
addErrorf ( "domain %q must be specified in IDNA form, %q" , d , dnsdomain . Name ( ) )
}
domain . Domain = dnsdomain
for _ , sign := range domain . DKIM . Sign {
if _ , ok := domain . DKIM . Selectors [ sign ] ; ! ok {
addErrorf ( "selector %q for signing is missing in domain %q" , sign , d )
}
}
for name , sel := range domain . DKIM . Selectors {
seld , err := dns . ParseDomain ( name )
if err != nil {
addErrorf ( "bad selector %q: %s" , name , err )
} else if seld . Name ( ) != name {
addErrorf ( "selector %q must be specified in IDNA form, %q" , name , seld . Name ( ) )
}
sel . Domain = seld
if sel . Expiration != "" {
exp , err := time . ParseDuration ( sel . Expiration )
if err != nil {
addErrorf ( "selector %q has invalid expiration %q: %v" , name , sel . Expiration , err )
} else {
sel . ExpirationSeconds = int ( exp / time . Second )
}
}
sel . HashEffective = sel . Hash
switch sel . HashEffective {
case "" :
sel . HashEffective = "sha256"
case "sha1" :
log . Error ( "using sha1 with DKIM is deprecated as not secure enough, switch to sha256" )
case "sha256" :
default :
addErrorf ( "unsupported hash %q for selector %q in domain %q" , sel . HashEffective , name , d )
}
pemBuf , err := os . ReadFile ( configDirPath ( dynamicPath , sel . PrivateKeyFile ) )
if err != nil {
addErrorf ( "reading private key for selector %q in domain %q: %s" , name , d , err )
continue
}
p , _ := pem . Decode ( pemBuf )
if p == nil {
addErrorf ( "private key for selector %q in domain %q has no PEM block" , name , d )
continue
}
key , err := x509 . ParsePKCS8PrivateKey ( p . Bytes )
if err != nil {
addErrorf ( "parsing private key for selector %q in domain %q: %s" , name , d , err )
continue
}
switch k := key . ( type ) {
case * rsa . PrivateKey :
if k . N . BitLen ( ) < 1024 {
// ../rfc/6376:757
// Let's help user do the right thing.
addErrorf ( "rsa keys should be >= 1024 bits" )
}
sel . Key = k
case ed25519 . PrivateKey :
if sel . HashEffective != "sha256" {
addErrorf ( "hash algorithm %q is not supported with ed25519, only sha256 is" , sel . HashEffective )
}
sel . Key = k
default :
addErrorf ( "private key type %T not yet supported, at selector %q in domain %q" , key , name , d )
}
if len ( sel . Headers ) == 0 {
// ../rfc/6376:2139
// ../rfc/6376:2203
// ../rfc/6376:2212
// By default we seal signed headers, and we sign user-visible headers to
// prevent/limit reuse of previously signed messages: All addressing fields, date
// and subject, message-referencing fields, parsing instructions (content-type).
sel . HeadersEffective = strings . Split ( "From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-Id,Content-Type" , "," )
} else {
var from bool
for _ , h := range sel . Headers {
from = from || strings . EqualFold ( h , "From" )
// ../rfc/6376:2269
if strings . EqualFold ( h , "DKIM-Signature" ) || strings . EqualFold ( h , "Received" ) || strings . EqualFold ( h , "Return-Path" ) {
log . Error ( "DKIM-signing header %q is recommended against as it may be modified in transit" )
}
}
if ! from {
addErrorf ( "From-field must always be DKIM-signed" )
}
sel . HeadersEffective = sel . Headers
}
domain . DKIM . Selectors [ name ] = sel
}
if domain . MTASTS != nil {
if ! haveSTSListener {
addErrorf ( "MTA-STS enabled for domain %q, but there is no listener for MTASTS" , d )
}
sts := domain . MTASTS
if sts . PolicyID == "" {
addErrorf ( "invalid empty MTA-STS PolicyID" )
}
switch sts . Mode {
case mtasts . ModeNone , mtasts . ModeTesting , mtasts . ModeEnforce :
default :
addErrorf ( "invalid mtasts mode %q" , sts . Mode )
}
}
c . Domains [ d ] = domain
}
// Post-process email addresses for fast lookups.
accDests = map [ string ] AccountDestination { }
for accName , acc := range c . Accounts {
var err error
acc . DNSDomain , err = dns . ParseDomain ( acc . Domain )
if err != nil {
addErrorf ( "parsing domain %q for account %q: %s" , acc . Domain , accName , err )
}
c . Accounts [ accName ] = acc
if strings . EqualFold ( acc . RejectsMailbox , "Inbox" ) {
addErrorf ( "account %q: cannot set RejectsMailbox to inbox" , accName )
}
checkMailboxNormf ( acc . RejectsMailbox , "account %q" , accName )
for addrName , dest := range acc . Destinations {
checkMailboxNormf ( dest . Mailbox , "account %q, destination %q" , accName , addrName )
for i , rs := range dest . Rulesets {
checkMailboxNormf ( rs . Mailbox , "account %q, destination %q, ruleset %d" , accName , addrName , i + 1 )
n := 0
if rs . SMTPMailFromRegexp != "" {
n ++
r , err := regexp . Compile ( rs . SMTPMailFromRegexp )
if err != nil {
addErrorf ( "invalid SMTPMailFrom regular expression: %v" , err )
}
c . Accounts [ accName ] . Destinations [ addrName ] . Rulesets [ i ] . SMTPMailFromRegexpCompiled = r
}
if rs . VerifiedDomain != "" {
n ++
d , err := dns . ParseDomain ( rs . VerifiedDomain )
if err != nil {
addErrorf ( "invalid VerifiedDomain: %v" , err )
}
c . Accounts [ accName ] . Destinations [ addrName ] . Rulesets [ i ] . VerifiedDNSDomain = d
}
var hdr [ ] [ 2 ] * regexp . Regexp
for k , v := range rs . HeadersRegexp {
n ++
if strings . ToLower ( k ) != k {
addErrorf ( "header field %q must only have lower case characters" , k )
}
if strings . ToLower ( v ) != v {
addErrorf ( "header value %q must only have lower case characters" , v )
}
rk , err := regexp . Compile ( k )
if err != nil {
addErrorf ( "invalid rule header regexp %q: %v" , k , err )
}
rv , err := regexp . Compile ( v )
if err != nil {
addErrorf ( "invalid rule header regexp %q: %v" , v , err )
}
hdr = append ( hdr , [ ... ] * regexp . Regexp { rk , rv } )
}
c . Accounts [ accName ] . Destinations [ addrName ] . Rulesets [ i ] . HeadersRegexpCompiled = hdr
if n == 0 {
addErrorf ( "ruleset must have at least one rule" )
}
if rs . ListAllowDomain != "" {
d , err := dns . ParseDomain ( rs . ListAllowDomain )
if err != nil {
addErrorf ( "invalid ListAllowDomain %q: %v" , rs . ListAllowDomain , err )
}
c . Accounts [ accName ] . Destinations [ addrName ] . Rulesets [ i ] . ListAllowDNSDomain = d
}
}
var address smtp . Address
localpart , err := smtp . ParseLocalpart ( addrName )
if err != nil && errors . Is ( err , smtp . ErrBadLocalpart ) {
address , err = smtp . ParseAddress ( addrName )
if err != nil {
addErrorf ( "invalid email address %q in account %q" , addrName , accName )
continue
} else if _ , ok := c . Domains [ address . Domain . Name ( ) ] ; ! ok {
addErrorf ( "unknown domain for address %q in account %q" , addrName , accName )
continue
}
} else {
if err != nil {
addErrorf ( "invalid localpart %q in account %q" , addrName , accName )
continue
}
address = smtp . NewAddress ( localpart , acc . DNSDomain )
if _ , ok := c . Domains [ acc . DNSDomain . Name ( ) ] ; ! ok {
addErrorf ( "unknown domain %q for account %q" , acc . DNSDomain . Name ( ) , accName )
continue
}
}
addrFull := address . Pack ( true )
if _ , ok := accDests [ addrFull ] ; ok {
addErrorf ( "duplicate destination address %q" , addrFull )
}
accDests [ addrFull ] = AccountDestination { address . Localpart , accName , dest }
}
}
// Set DMARC destinations.
for d , domain := range c . Domains {
dmarc := domain . DMARC
if dmarc == nil {
continue
}
if _ , ok := c . Accounts [ dmarc . Account ] ; ! ok {
addErrorf ( "DMARC account %q does not exist" , dmarc . Account )
}
lp , err := smtp . ParseLocalpart ( dmarc . Localpart )
if err != nil {
addErrorf ( "invalid DMARC localpart %q: %s" , dmarc . Localpart , err )
}
if lp . IsInternational ( ) {
// ../rfc/8616:234
addErrorf ( "DMARC localpart %q is an internationalized address, only conventional ascii-only address possible for interopability" , lp )
}
domain . DMARC . ParsedLocalpart = lp
c . Domains [ d ] = domain
addrFull := smtp . NewAddress ( lp , domain . Domain ) . String ( )
dest := config . Destination {
Mailbox : dmarc . Mailbox ,
DMARCReports : true ,
}
checkMailboxNormf ( dmarc . Mailbox , "DMARC mailbox for account %q" , dmarc . Account )
accDests [ addrFull ] = AccountDestination { lp , dmarc . Account , dest }
}
// Set TLSRPT destinations.
for d , domain := range c . Domains {
tlsrpt := domain . TLSRPT
if tlsrpt == nil {
continue
}
if _ , ok := c . Accounts [ tlsrpt . Account ] ; ! ok {
addErrorf ( "TLSRPT account %q does not exist" , tlsrpt . Account )
}
lp , err := smtp . ParseLocalpart ( tlsrpt . Localpart )
if err != nil {
addErrorf ( "invalid TLSRPT localpart %q: %s" , tlsrpt . Localpart , err )
}
if lp . IsInternational ( ) {
// Does not appear documented in ../rfc/8460, but similar to DMARC it makes sense
// to keep this ascii-only addresses.
addErrorf ( "TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability" , lp )
}
domain . TLSRPT . ParsedLocalpart = lp
c . Domains [ d ] = domain
addrFull := smtp . NewAddress ( lp , domain . Domain ) . String ( )
dest := config . Destination {
Mailbox : tlsrpt . Mailbox ,
TLSReports : true ,
}
checkMailboxNormf ( tlsrpt . Mailbox , "TLSRPT mailbox for account %q" , tlsrpt . Account )
accDests [ addrFull ] = AccountDestination { lp , tlsrpt . Account , dest }
}
return
}
func loadTLSKeyCerts ( configFile , kind string , ctls * config . TLS ) error {
certs := [ ] tls . Certificate { }
for _ , kp := range ctls . KeyCerts {
certPath := configDirPath ( configFile , kp . CertFile )
keyPath := configDirPath ( configFile , kp . KeyFile )
cert , err := tls . LoadX509KeyPair ( certPath , keyPath )
if err != nil {
return fmt . Errorf ( "tls config for %q: parsing x509 key pair: %v" , kind , err )
}
certs = append ( certs , cert )
}
ctls . Config = & tls . Config {
Certificates : certs ,
}
return nil
}