mox/mox-/config.go

1360 lines
42 KiB
Go
Raw Normal View History

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"
"io"
2023-01-30 16:27:06 +03:00
"net"
"net/http"
"net/url"
2023-01-30 16:27:06 +03:00
"os"
change mox to start as root, bind to network sockets, then drop to regular unprivileged mox user makes it easier to run on bsd's, where you cannot (easily?) let non-root users bind to ports <1024. starting as root also paves the way for future improvements with privilege separation. unfortunately, this requires changes to how you start mox. though mox will help by automatically fix up dir/file permissions/ownership. if you start mox from the systemd unit file, you should update it so it starts as root and adds a few additional capabilities: # first update the mox binary, then, as root: ./mox config printservice >mox.service systemctl daemon-reload systemctl restart mox journalctl -f -u mox & # you should see mox start up, with messages about fixing permissions on dirs/files. if you used the recommended config/ and data/ directory, in a directory just for mox, and with the mox user called "mox", this should be enough. if you don't want mox to modify dir/file permissions, set "NoFixPermissions: true" in mox.conf. if you named the mox user something else than mox, e.g. "_mox", add "User: _mox" to mox.conf. if you created a shared service user as originally suggested, you may want to get rid of that as it is no longer useful and may get in the way. e.g. if you had /home/service/mox with a "service" user, that service user can no longer access any files: only mox and root can. this also adds scripts for building mox docker images for alpine-supported platforms. the "restart" subcommand has been removed. it wasn't all that useful and got in the way. and another change: when adding a domain while mtasts isn't enabled, don't add the per-domain mtasts config, as it would cause failure to add the domain. based on report from setting up mox on openbsd from mteege. and based on issue #3. thanks for the feedback!
2023-02-27 14:19:55 +03:00
"os/user"
2023-01-30 16:27:06 +03:00
"path/filepath"
"regexp"
"sort"
change mox to start as root, bind to network sockets, then drop to regular unprivileged mox user makes it easier to run on bsd's, where you cannot (easily?) let non-root users bind to ports <1024. starting as root also paves the way for future improvements with privilege separation. unfortunately, this requires changes to how you start mox. though mox will help by automatically fix up dir/file permissions/ownership. if you start mox from the systemd unit file, you should update it so it starts as root and adds a few additional capabilities: # first update the mox binary, then, as root: ./mox config printservice >mox.service systemctl daemon-reload systemctl restart mox journalctl -f -u mox & # you should see mox start up, with messages about fixing permissions on dirs/files. if you used the recommended config/ and data/ directory, in a directory just for mox, and with the mox user called "mox", this should be enough. if you don't want mox to modify dir/file permissions, set "NoFixPermissions: true" in mox.conf. if you named the mox user something else than mox, e.g. "_mox", add "User: _mox" to mox.conf. if you created a shared service user as originally suggested, you may want to get rid of that as it is no longer useful and may get in the way. e.g. if you had /home/service/mox with a "service" user, that service user can no longer access any files: only mox and root can. this also adds scripts for building mox docker images for alpine-supported platforms. the "restart" subcommand has been removed. it wasn't all that useful and got in the way. and another change: when adding a domain while mtasts isn't enabled, don't add the per-domain mtasts config, as it would cause failure to add the domain. based on report from setting up mox on openbsd from mteege. and based on issue #3. thanks for the feedback!
2023-02-27 14:19:55 +03:00
"strconv"
2023-01-30 16:27:06 +03:00
"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/moxvar"
2023-01-30 16:27:06 +03:00
"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 canonical full address (localpart@domain, lower-cased when
// case-insensitive, stripped of catchall separator) to account and address.
// Domains are IDNA names in utf8.
2023-01-30 16:27:06 +03:00
accountDestinations map[string]AccountDestination
}
type AccountDestination struct {
Catchall bool // If catchall destination for its domain.
Localpart smtp.Localpart // In original casing as written in config file.
2023-01-30 16:27:06 +03:00
Account string
Destination config.Destination
}
// LogLevelSet sets a new log level for pkg. An empty pkg sets the default log
2023-01-30 16:27:06 +03:00
// 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) LogLevelSet(pkg string, level mlog.Level) {
2023-01-30 16:27:06 +03:00
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)
}
// LogLevelRemove removes a configured log level for a package.
func (c *Config) LogLevelRemove(pkg string) {
c.logMutex.Lock()
defer c.logMutex.Unlock()
l := c.copyLogLevels()
delete(l, pkg)
c.Log = l
xlog.Print("log level cleared", mlog.Field("pkg", pkg))
mlog.SetConfig(c.Log)
}
2023-01-30 16:27:06 +03:00
// 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
c.allowACMEHosts(true)
2023-01-30 16:27:06 +03:00
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
}
// DomainLocalparts returns a mapping of encoded localparts to account names for a
// domain. An empty localpart is a catchall destination for a domain.
func (c *Config) DomainLocalparts(d dns.Domain) map[string]string {
2023-01-30 16:27:06 +03:00
suffix := "@" + d.Name()
m := map[string]string{}
2023-01-30 16:27:06 +03:00
c.withDynamicLock(func() {
for addr, ad := range c.accountDestinations {
if strings.HasSuffix(addr, suffix) {
if ad.Catchall {
m[""] = ad.Account
} else {
m[ad.Localpart.String()] = ad.Account
}
2023-01-30 16:27:06 +03:00
}
}
})
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
}
improve webserver, add domain redirects (aliases), add tests and admin page ui to manage the config - make builtin http handlers serve on specific domains, such as for mta-sts, so e.g. /.well-known/mta-sts.txt isn't served on all domains. - add logging of a few more fields in access logging. - small tweaks/bug fixes in webserver request handling. - add config option for redirecting entire domains to another (common enough). - split httpserver metric into two: one for duration until writing header (i.e. performance of server), another for duration until full response is sent to client (i.e. performance as perceived by users). - add admin ui, a new page for managing the configs. after making changes and hitting "save", the changes take effect immediately. the page itself doesn't look very well-designed (many input fields, makes it look messy). i have an idea to improve it (explained in admin.html as todo) by making the layout look just like the config file. not urgent though. i've already changed my websites/webapps over. the idea of adding a webserver is to take away a (the) reason for folks to want to complicate their mox setup by running an other webserver on the same machine. i think the current webserver implementation can already serve most common use cases. with a few more tweaks (feedback needed!) we should be able to get to 95% of the use cases. the reverse proxy can take care of the remaining 5%. nevertheless, a next step is still to change the quickstart to make it easier for folks to run with an existing webserver, with existing tls certs/keys. that's how this relates to issue #5.
2023-03-02 20:15:54 +03:00
func (c *Config) WebServer() (r map[dns.Domain]dns.Domain, l []config.WebHandler) {
c.withDynamicLock(func() {
improve webserver, add domain redirects (aliases), add tests and admin page ui to manage the config - make builtin http handlers serve on specific domains, such as for mta-sts, so e.g. /.well-known/mta-sts.txt isn't served on all domains. - add logging of a few more fields in access logging. - small tweaks/bug fixes in webserver request handling. - add config option for redirecting entire domains to another (common enough). - split httpserver metric into two: one for duration until writing header (i.e. performance of server), another for duration until full response is sent to client (i.e. performance as perceived by users). - add admin ui, a new page for managing the configs. after making changes and hitting "save", the changes take effect immediately. the page itself doesn't look very well-designed (many input fields, makes it look messy). i have an idea to improve it (explained in admin.html as todo) by making the layout look just like the config file. not urgent though. i've already changed my websites/webapps over. the idea of adding a webserver is to take away a (the) reason for folks to want to complicate their mox setup by running an other webserver on the same machine. i think the current webserver implementation can already serve most common use cases. with a few more tweaks (feedback needed!) we should be able to get to 95% of the use cases. the reverse proxy can take care of the remaining 5%. nevertheless, a next step is still to change the quickstart to make it easier for folks to run with an existing webserver, with existing tls certs/keys. that's how this relates to issue #5.
2023-03-02 20:15:54 +03:00
r = c.Dynamic.WebDNSDomainRedirects
l = c.Dynamic.WebHandlers
})
improve webserver, add domain redirects (aliases), add tests and admin page ui to manage the config - make builtin http handlers serve on specific domains, such as for mta-sts, so e.g. /.well-known/mta-sts.txt isn't served on all domains. - add logging of a few more fields in access logging. - small tweaks/bug fixes in webserver request handling. - add config option for redirecting entire domains to another (common enough). - split httpserver metric into two: one for duration until writing header (i.e. performance of server), another for duration until full response is sent to client (i.e. performance as perceived by users). - add admin ui, a new page for managing the configs. after making changes and hitting "save", the changes take effect immediately. the page itself doesn't look very well-designed (many input fields, makes it look messy). i have an idea to improve it (explained in admin.html as todo) by making the layout look just like the config file. not urgent though. i've already changed my websites/webapps over. the idea of adding a webserver is to take away a (the) reason for folks to want to complicate their mox setup by running an other webserver on the same machine. i think the current webserver implementation can already serve most common use cases. with a few more tweaks (feedback needed!) we should be able to get to 95% of the use cases. the reverse proxy can take care of the remaining 5%. nevertheless, a next step is still to change the quickstart to make it easier for folks to run with an existing webserver, with existing tls certs/keys. that's how this relates to issue #5.
2023-03-02 20:15:54 +03:00
return r, l
}
new feature: when delivering messages from the queue, make it possible to use a "transport" the default transport is still just "direct delivery", where we connect to the destination domain's MX servers. other transports are: - regular smtp without authentication, this is relaying to a smarthost. - submission with authentication, e.g. to a third party email sending service. - direct delivery, but with with connections going through a socks proxy. this can be helpful if your ip is blocked, you need to get email out, and you have another IP that isn't blocked. keep in mind that for all of the above, appropriate SPF/DKIM settings have to be configured. the "dnscheck" for a domain does a check for any SOCKS IP in the SPF record. SPF for smtp/submission (ranges? includes?) and any DKIM requirements cannot really be checked. which transport is used can be configured through routes. routes can be set on an account, a domain, or globally. the routes are evaluated in that order, with the first match selecting the transport. these routes are evaluated for each delivery attempt. common selection criteria are recipient domain and sender domain, but also which delivery attempt this is. you could configured mox to attempt sending through a 3rd party from the 4th attempt onwards. routes and transports are optional. if no route matches, or an empty/zero transport is selected, normal direct delivery is done. we could already "submit" emails with 3rd party accounts with "sendmail". but we now support more SASL authentication mechanisms with SMTP (not only PLAIN, but also SCRAM-SHA-256, SCRAM-SHA-1 and CRAM-MD5), which sendmail now also supports. sendmail will use the most secure mechanism supported by the server, or the explicitly configured mechanism. for issue #36 by dmikushin. also based on earlier discussion on hackernews.
2023-06-16 19:38:28 +03:00
func (c *Config) Routes(accountName string, domain dns.Domain) (accountRoutes, domainRoutes, globalRoutes []config.Route) {
c.withDynamicLock(func() {
acc := c.Dynamic.Accounts[accountName]
accountRoutes = acc.Routes
dom := c.Dynamic.Domains[domain.Name()]
domainRoutes = dom.Routes
globalRoutes = c.Dynamic.Routes
})
return
}
func (c *Config) allowACMEHosts(checkACMEHosts bool) {
2023-01-30 16:27:06 +03:00
for _, l := range c.Static.Listeners {
if l.TLS == nil || l.TLS.ACME == "" {
continue
}
2023-01-30 16:27:06 +03:00
m := c.Static.ACME[l.TLS.ACME].Manager
hostnames := map[dns.Domain]struct{}{}
hostnames[c.Static.HostnameDomain] = struct{}{}
if l.HostnameDomain.ASCII != "" {
hostnames[l.HostnameDomain] = struct{}{}
}
2023-01-30 16:27:06 +03:00
for _, dom := range c.Dynamic.Domains {
if l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS {
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))
} else {
hostnames[d] = struct{}{}
}
2023-01-30 16:27:06 +03:00
}
if l.MTASTSHTTPS.Enabled && dom.MTASTS != nil && !l.MTASTSHTTPS.NonTLS {
2023-01-30 16:27:06 +03:00
d, err := dns.ParseDomain("mta-sts." + dom.Domain.ASCII)
if err != nil {
xlog.Errorx("parsing mta-sts domain", err, mlog.Field("domain", dom.Domain))
} else {
hostnames[d] = struct{}{}
2023-01-30 16:27:06 +03:00
}
}
}
if l.WebserverHTTPS.Enabled {
improve webserver, add domain redirects (aliases), add tests and admin page ui to manage the config - make builtin http handlers serve on specific domains, such as for mta-sts, so e.g. /.well-known/mta-sts.txt isn't served on all domains. - add logging of a few more fields in access logging. - small tweaks/bug fixes in webserver request handling. - add config option for redirecting entire domains to another (common enough). - split httpserver metric into two: one for duration until writing header (i.e. performance of server), another for duration until full response is sent to client (i.e. performance as perceived by users). - add admin ui, a new page for managing the configs. after making changes and hitting "save", the changes take effect immediately. the page itself doesn't look very well-designed (many input fields, makes it look messy). i have an idea to improve it (explained in admin.html as todo) by making the layout look just like the config file. not urgent though. i've already changed my websites/webapps over. the idea of adding a webserver is to take away a (the) reason for folks to want to complicate their mox setup by running an other webserver on the same machine. i think the current webserver implementation can already serve most common use cases. with a few more tweaks (feedback needed!) we should be able to get to 95% of the use cases. the reverse proxy can take care of the remaining 5%. nevertheless, a next step is still to change the quickstart to make it easier for folks to run with an existing webserver, with existing tls certs/keys. that's how this relates to issue #5.
2023-03-02 20:15:54 +03:00
for from := range c.Dynamic.WebDNSDomainRedirects {
hostnames[from] = struct{}{}
}
for _, wh := range c.Dynamic.WebHandlers {
hostnames[wh.DNSDomain] = struct{}{}
}
}
m.SetAllowedHostnames(dns.StrictResolver{Pkg: "autotls"}, hostnames, c.Static.Listeners["public"].IPs, checkACMEHosts)
2023-01-30 16:27:06 +03:00
}
}
// 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, log *mlog.Log, c config.Dynamic) error {
2023-01-30 16:27:06 +03:00
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 {
err := f.Close()
log.Check(err, "closing file after error")
2023-01-30 16:27:06 +03:00
}
}()
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)
}
if err := f.Close(); err != nil {
return fmt.Errorf("close written domains.conf: %v", err)
}
f = nil
2023-01-30 16:27:06 +03:00
Conf.dynamicMtime = fi.ModTime()
Conf.DynamicLastCheck = time.Now()
Conf.Dynamic = c
Conf.accountDestinations = accDests
Conf.allowACMEHosts(true)
2023-01-30 16:27:06 +03:00
return nil
}
// MustLoadConfig loads the config, quitting on errors.
func MustLoadConfig(doLoadTLSKeyCerts, checkACMEHosts bool) {
errs := LoadConfig(context.Background(), doLoadTLSKeyCerts, checkACMEHosts)
2023-01-30 16:27:06 +03:00
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, doLoadTLSKeyCerts, checkACMEHosts bool) []error {
Shutdown, ShutdownCancel = context.WithCancel(context.Background())
Context, ContextCancel = context.WithCancel(context.Background())
c, errs := ParseConfig(ctx, ConfigStaticPath, false, doLoadTLSKeyCerts, checkACMEHosts)
2023-01-30 16:27:06 +03:00
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}
// If we have non-standard CA roots, use them for all HTTPS requests.
if Conf.Static.TLS.CertPool != nil {
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
RootCAs: Conf.Static.TLS.CertPool,
}
}
moxvar.Pedantic = c.Static.Pedantic
2023-01-30 16:27:06 +03:00
}
// ParseConfig parses the static config at path p. If checkOnly is true, no changes
// are made, such as registering ACME identities. If doLoadTLSKeyCerts is true,
// the TLS KeyCerts configuration is loaded and checked. This is used during the
// quickstart in the case the user is going to provide their own certificates.
// If checkACMEHosts is true, the hosts allowed for acme are compared with the
// explicitly configured ips we are listening on.
func ParseConfig(ctx context.Context, p string, checkOnly, doLoadTLSKeyCerts, checkACMEHosts bool) (c *Config, errs []error) {
2023-01-30 16:27:06 +03:00
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)}
2023-01-30 16:27:06 +03:00
}
if xerrs := PrepareStaticConfig(ctx, p, c, checkOnly, doLoadTLSKeyCerts); len(xerrs) > 0 {
2023-01-30 16:27:06 +03:00
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(checkACMEHosts)
2023-01-30 16:27:06 +03:00
}
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.
new feature: when delivering messages from the queue, make it possible to use a "transport" the default transport is still just "direct delivery", where we connect to the destination domain's MX servers. other transports are: - regular smtp without authentication, this is relaying to a smarthost. - submission with authentication, e.g. to a third party email sending service. - direct delivery, but with with connections going through a socks proxy. this can be helpful if your ip is blocked, you need to get email out, and you have another IP that isn't blocked. keep in mind that for all of the above, appropriate SPF/DKIM settings have to be configured. the "dnscheck" for a domain does a check for any SOCKS IP in the SPF record. SPF for smtp/submission (ranges? includes?) and any DKIM requirements cannot really be checked. which transport is used can be configured through routes. routes can be set on an account, a domain, or globally. the routes are evaluated in that order, with the first match selecting the transport. these routes are evaluated for each delivery attempt. common selection criteria are recipient domain and sender domain, but also which delivery attempt this is. you could configured mox to attempt sending through a 3rd party from the 4th attempt onwards. routes and transports are optional. if no route matches, or an empty/zero transport is selected, normal direct delivery is done. we could already "submit" emails with 3rd party accounts with "sendmail". but we now support more SASL authentication mechanisms with SMTP (not only PLAIN, but also SCRAM-SHA-256, SCRAM-SHA-1 and CRAM-MD5), which sendmail now also supports. sendmail will use the most secure mechanism supported by the server, or the explicitly configured mechanism. for issue #36 by dmikushin. also based on earlier discussion on hackernews.
2023-06-16 19:38:28 +03:00
func PrepareStaticConfig(ctx context.Context, configFile string, conf *Config, checkOnly, doLoadTLSKeyCerts bool) (errs []error) {
2023-01-30 16:27:06 +03:00
addErrorf := func(format string, args ...any) {
errs = append(errs, fmt.Errorf(format, args...))
}
new feature: when delivering messages from the queue, make it possible to use a "transport" the default transport is still just "direct delivery", where we connect to the destination domain's MX servers. other transports are: - regular smtp without authentication, this is relaying to a smarthost. - submission with authentication, e.g. to a third party email sending service. - direct delivery, but with with connections going through a socks proxy. this can be helpful if your ip is blocked, you need to get email out, and you have another IP that isn't blocked. keep in mind that for all of the above, appropriate SPF/DKIM settings have to be configured. the "dnscheck" for a domain does a check for any SOCKS IP in the SPF record. SPF for smtp/submission (ranges? includes?) and any DKIM requirements cannot really be checked. which transport is used can be configured through routes. routes can be set on an account, a domain, or globally. the routes are evaluated in that order, with the first match selecting the transport. these routes are evaluated for each delivery attempt. common selection criteria are recipient domain and sender domain, but also which delivery attempt this is. you could configured mox to attempt sending through a 3rd party from the 4th attempt onwards. routes and transports are optional. if no route matches, or an empty/zero transport is selected, normal direct delivery is done. we could already "submit" emails with 3rd party accounts with "sendmail". but we now support more SASL authentication mechanisms with SMTP (not only PLAIN, but also SCRAM-SHA-256, SCRAM-SHA-1 and CRAM-MD5), which sendmail now also supports. sendmail will use the most secure mechanism supported by the server, or the explicitly configured mechanism. for issue #36 by dmikushin. also based on earlier discussion on hackernews.
2023-06-16 19:38:28 +03:00
c := &conf.Static
2023-01-30 16:27:06 +03:00
// 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 {
new feature: when delivering messages from the queue, make it possible to use a "transport" the default transport is still just "direct delivery", where we connect to the destination domain's MX servers. other transports are: - regular smtp without authentication, this is relaying to a smarthost. - submission with authentication, e.g. to a third party email sending service. - direct delivery, but with with connections going through a socks proxy. this can be helpful if your ip is blocked, you need to get email out, and you have another IP that isn't blocked. keep in mind that for all of the above, appropriate SPF/DKIM settings have to be configured. the "dnscheck" for a domain does a check for any SOCKS IP in the SPF record. SPF for smtp/submission (ranges? includes?) and any DKIM requirements cannot really be checked. which transport is used can be configured through routes. routes can be set on an account, a domain, or globally. the routes are evaluated in that order, with the first match selecting the transport. these routes are evaluated for each delivery attempt. common selection criteria are recipient domain and sender domain, but also which delivery attempt this is. you could configured mox to attempt sending through a 3rd party from the 4th attempt onwards. routes and transports are optional. if no route matches, or an empty/zero transport is selected, normal direct delivery is done. we could already "submit" emails with 3rd party accounts with "sendmail". but we now support more SASL authentication mechanisms with SMTP (not only PLAIN, but also SCRAM-SHA-256, SCRAM-SHA-1 and CRAM-MD5), which sendmail now also supports. sendmail will use the most secure mechanism supported by the server, or the explicitly configured mechanism. for issue #36 by dmikushin. also based on earlier discussion on hackernews.
2023-06-16 19:38:28 +03:00
conf.Log = map[string]mlog.Level{"": logLevel}
2023-01-30 16:27:06 +03:00
} else {
addErrorf("invalid log level %q", c.LogLevel)
}
for pkg, s := range c.PackageLogLevels {
if logLevel, ok := mlog.Levels[s]; ok {
new feature: when delivering messages from the queue, make it possible to use a "transport" the default transport is still just "direct delivery", where we connect to the destination domain's MX servers. other transports are: - regular smtp without authentication, this is relaying to a smarthost. - submission with authentication, e.g. to a third party email sending service. - direct delivery, but with with connections going through a socks proxy. this can be helpful if your ip is blocked, you need to get email out, and you have another IP that isn't blocked. keep in mind that for all of the above, appropriate SPF/DKIM settings have to be configured. the "dnscheck" for a domain does a check for any SOCKS IP in the SPF record. SPF for smtp/submission (ranges? includes?) and any DKIM requirements cannot really be checked. which transport is used can be configured through routes. routes can be set on an account, a domain, or globally. the routes are evaluated in that order, with the first match selecting the transport. these routes are evaluated for each delivery attempt. common selection criteria are recipient domain and sender domain, but also which delivery attempt this is. you could configured mox to attempt sending through a 3rd party from the 4th attempt onwards. routes and transports are optional. if no route matches, or an empty/zero transport is selected, normal direct delivery is done. we could already "submit" emails with 3rd party accounts with "sendmail". but we now support more SASL authentication mechanisms with SMTP (not only PLAIN, but also SCRAM-SHA-256, SCRAM-SHA-1 and CRAM-MD5), which sendmail now also supports. sendmail will use the most secure mechanism supported by the server, or the explicitly configured mechanism. for issue #36 by dmikushin. also based on earlier discussion on hackernews.
2023-06-16 19:38:28 +03:00
conf.Log[pkg] = logLevel
2023-01-30 16:27:06 +03:00
} else {
addErrorf("invalid package log level %q", s)
}
}
change mox to start as root, bind to network sockets, then drop to regular unprivileged mox user makes it easier to run on bsd's, where you cannot (easily?) let non-root users bind to ports <1024. starting as root also paves the way for future improvements with privilege separation. unfortunately, this requires changes to how you start mox. though mox will help by automatically fix up dir/file permissions/ownership. if you start mox from the systemd unit file, you should update it so it starts as root and adds a few additional capabilities: # first update the mox binary, then, as root: ./mox config printservice >mox.service systemctl daemon-reload systemctl restart mox journalctl -f -u mox & # you should see mox start up, with messages about fixing permissions on dirs/files. if you used the recommended config/ and data/ directory, in a directory just for mox, and with the mox user called "mox", this should be enough. if you don't want mox to modify dir/file permissions, set "NoFixPermissions: true" in mox.conf. if you named the mox user something else than mox, e.g. "_mox", add "User: _mox" to mox.conf. if you created a shared service user as originally suggested, you may want to get rid of that as it is no longer useful and may get in the way. e.g. if you had /home/service/mox with a "service" user, that service user can no longer access any files: only mox and root can. this also adds scripts for building mox docker images for alpine-supported platforms. the "restart" subcommand has been removed. it wasn't all that useful and got in the way. and another change: when adding a domain while mtasts isn't enabled, don't add the per-domain mtasts config, as it would cause failure to add the domain. based on report from setting up mox on openbsd from mteege. and based on issue #3. thanks for the feedback!
2023-02-27 14:19:55 +03:00
if c.User == "" {
c.User = "mox"
}
u, err := user.Lookup(c.User)
var userErr user.UnknownUserError
if err != nil && errors.As(err, &userErr) {
uid, err := strconv.ParseUint(c.User, 10, 32)
if err != nil {
addErrorf("parsing unknown user %s as uid: %v (hint: add user mox with \"useradd -d $PWD mox\" or specify a different username on the quickstart command-line)", c.User, err)
change mox to start as root, bind to network sockets, then drop to regular unprivileged mox user makes it easier to run on bsd's, where you cannot (easily?) let non-root users bind to ports <1024. starting as root also paves the way for future improvements with privilege separation. unfortunately, this requires changes to how you start mox. though mox will help by automatically fix up dir/file permissions/ownership. if you start mox from the systemd unit file, you should update it so it starts as root and adds a few additional capabilities: # first update the mox binary, then, as root: ./mox config printservice >mox.service systemctl daemon-reload systemctl restart mox journalctl -f -u mox & # you should see mox start up, with messages about fixing permissions on dirs/files. if you used the recommended config/ and data/ directory, in a directory just for mox, and with the mox user called "mox", this should be enough. if you don't want mox to modify dir/file permissions, set "NoFixPermissions: true" in mox.conf. if you named the mox user something else than mox, e.g. "_mox", add "User: _mox" to mox.conf. if you created a shared service user as originally suggested, you may want to get rid of that as it is no longer useful and may get in the way. e.g. if you had /home/service/mox with a "service" user, that service user can no longer access any files: only mox and root can. this also adds scripts for building mox docker images for alpine-supported platforms. the "restart" subcommand has been removed. it wasn't all that useful and got in the way. and another change: when adding a domain while mtasts isn't enabled, don't add the per-domain mtasts config, as it would cause failure to add the domain. based on report from setting up mox on openbsd from mteege. and based on issue #3. thanks for the feedback!
2023-02-27 14:19:55 +03:00
} else {
// We assume the same gid as uid.
c.UID = uint32(uid)
c.GID = uint32(uid)
}
} else if err != nil {
addErrorf("looking up user: %v", err)
} else {
if uid, err := strconv.ParseUint(u.Uid, 10, 32); err != nil {
addErrorf("parsing uid %s: %v", u.Uid, err)
} else {
c.UID = uint32(uid)
}
if gid, err := strconv.ParseUint(u.Gid, 10, 32); err != nil {
addErrorf("parsing gid %s: %v", u.Gid, err)
} else {
c.GID = uint32(gid)
}
}
2023-01-30 16:27:06 +03:00
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.Done())
2023-01-30 16:27:06 +03:00
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 or with missing ACME definition, we don't have an acme manager,
// so set an empty tls config to continue.
2023-01-30 16:27:06 +03:00
var tlsconfig *tls.Config
if checkOnly || acme.Manager == nil {
2023-01-30 16:27:06 +03:00
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 doLoadTLSKeyCerts {
if err := loadTLSKeyCerts(configFile, "listener "+name, l.TLS); err != nil {
addErrorf("%w", err)
}
2023-01-30 16:27:06 +03:00
}
} 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 {
var needsTLS []string
needtls := func(s string, v bool) {
if v {
needsTLS = append(needsTLS, s)
}
}
needtls("IMAPS", l.IMAPS.Enabled)
needtls("SMTP", l.SMTP.Enabled && !l.SMTP.NoSTARTTLS)
needtls("Submissions", l.Submissions.Enabled)
needtls("Submission", l.Submission.Enabled && !l.Submission.NoRequireSTARTTLS)
needtls("AccountHTTPS", l.AccountHTTPS.Enabled)
needtls("AdminHTTPS", l.AdminHTTPS.Enabled)
needtls("AutoconfigHTTPS", l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS)
needtls("MTASTSHTTPS", l.MTASTSHTTPS.Enabled && !l.MTASTSHTTPS.NonTLS)
needtls("WebserverHTTPS", l.WebserverHTTPS.Enabled)
if len(needsTLS) > 0 {
addErrorf("listener %q does not specify tls config, but requires tls for %s", name, strings.Join(needsTLS, ", "))
}
2023-01-30 16:27:06 +03:00
}
if l.AutoconfigHTTPS.Enabled && l.MTASTSHTTPS.Enabled && l.AutoconfigHTTPS.Port == l.MTASTSHTTPS.Port && l.AutoconfigHTTPS.NonTLS != l.MTASTSHTTPS.NonTLS {
addErrorf("listener %q tries to enable autoconfig and mta-sts enabled on same port but with both http and https", name)
}
2023-01-30 16:27:06 +03:00
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)
}
checkPath := func(kind string, enabled bool, path string) {
if enabled && path != "" && !strings.HasPrefix(path, "/") {
addErrorf("listener %q has %s with path %q that must start with a slash", name, kind, path)
}
}
checkPath("AccountHTTP", l.AccountHTTP.Enabled, l.AccountHTTP.Path)
checkPath("AccountHTTPS", l.AccountHTTPS.Enabled, l.AccountHTTPS.Path)
checkPath("AdminHTTP", l.AdminHTTP.Enabled, l.AdminHTTP.Path)
checkPath("AdminHTTPS", l.AdminHTTPS.Enabled, l.AdminHTTPS.Path)
2023-01-30 16:27:06 +03:00
c.Listeners[name] = l
}
if haveUnspecifiedSMTPListener {
c.SpecifiedSMTPListenIPs = nil
}
for _, mb := range c.DefaultMailboxes {
checkMailboxNormf(mb, "default mailbox")
}
new feature: when delivering messages from the queue, make it possible to use a "transport" the default transport is still just "direct delivery", where we connect to the destination domain's MX servers. other transports are: - regular smtp without authentication, this is relaying to a smarthost. - submission with authentication, e.g. to a third party email sending service. - direct delivery, but with with connections going through a socks proxy. this can be helpful if your ip is blocked, you need to get email out, and you have another IP that isn't blocked. keep in mind that for all of the above, appropriate SPF/DKIM settings have to be configured. the "dnscheck" for a domain does a check for any SOCKS IP in the SPF record. SPF for smtp/submission (ranges? includes?) and any DKIM requirements cannot really be checked. which transport is used can be configured through routes. routes can be set on an account, a domain, or globally. the routes are evaluated in that order, with the first match selecting the transport. these routes are evaluated for each delivery attempt. common selection criteria are recipient domain and sender domain, but also which delivery attempt this is. you could configured mox to attempt sending through a 3rd party from the 4th attempt onwards. routes and transports are optional. if no route matches, or an empty/zero transport is selected, normal direct delivery is done. we could already "submit" emails with 3rd party accounts with "sendmail". but we now support more SASL authentication mechanisms with SMTP (not only PLAIN, but also SCRAM-SHA-256, SCRAM-SHA-1 and CRAM-MD5), which sendmail now also supports. sendmail will use the most secure mechanism supported by the server, or the explicitly configured mechanism. for issue #36 by dmikushin. also based on earlier discussion on hackernews.
2023-06-16 19:38:28 +03:00
checkTransportSMTP := func(name string, isTLS bool, t *config.TransportSMTP) {
var err error
t.DNSHost, err = dns.ParseDomain(t.Host)
if err != nil {
addErrorf("transport %s: bad host %s: %v", name, t.Host, err)
}
if isTLS && t.STARTTLSInsecureSkipVerify {
addErrorf("transport %s: cannot have STARTTLSInsecureSkipVerify with immediate TLS")
}
if isTLS && t.NoSTARTTLS {
addErrorf("transport %s: cannot have NoSTARTTLS with immediate TLS")
}
if t.Auth == nil {
return
}
seen := map[string]bool{}
for _, m := range t.Auth.Mechanisms {
if seen[m] {
addErrorf("transport %s: duplicate authentication mechanism %s", name, m)
}
seen[m] = true
switch m {
case "SCRAM-SHA-256":
case "SCRAM-SHA-1":
case "CRAM-MD5":
case "PLAIN":
default:
addErrorf("transport %s: unknown authentication mechanism %s", name, m)
}
}
t.Auth.EffectiveMechanisms = t.Auth.Mechanisms
if len(t.Auth.EffectiveMechanisms) == 0 {
t.Auth.EffectiveMechanisms = []string{"SCRAM-SHA-256", "SCRAM-SHA-1", "CRAM-MD5"}
}
}
checkTransportSocks := func(name string, t *config.TransportSocks) {
_, _, err := net.SplitHostPort(t.Address)
if err != nil {
addErrorf("transport %s: bad address %s: %v", name, t.Address, err)
}
for _, ipstr := range t.RemoteIPs {
ip := net.ParseIP(ipstr)
if ip == nil {
addErrorf("transport %s: bad ip %s", name, ipstr)
} else {
t.IPs = append(t.IPs, ip)
}
}
t.Hostname, err = dns.ParseDomain(t.RemoteHostname)
if err != nil {
addErrorf("transport %s: bad hostname %s: %v", name, t.RemoteHostname, err)
}
}
for name, t := range c.Transports {
n := 0
if t.Submissions != nil {
n++
checkTransportSMTP(name, true, t.Submissions)
}
if t.Submission != nil {
n++
checkTransportSMTP(name, false, t.Submission)
}
if t.SMTP != nil {
n++
checkTransportSMTP(name, false, t.SMTP)
}
if t.Socks != nil {
n++
checkTransportSocks(name, t.Socks)
}
if n > 1 {
addErrorf("transport %s: cannot have multiple methods in a transport", name)
}
}
2023-01-30 16:27:06 +03:00
// 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.
2023-01-30 16:27:06 +03:00
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")
improve webserver, add domain redirects (aliases), add tests and admin page ui to manage the config - make builtin http handlers serve on specific domains, such as for mta-sts, so e.g. /.well-known/mta-sts.txt isn't served on all domains. - add logging of a few more fields in access logging. - small tweaks/bug fixes in webserver request handling. - add config option for redirecting entire domains to another (common enough). - split httpserver metric into two: one for duration until writing header (i.e. performance of server), another for duration until full response is sent to client (i.e. performance as perceived by users). - add admin ui, a new page for managing the configs. after making changes and hitting "save", the changes take effect immediately. the page itself doesn't look very well-designed (many input fields, makes it look messy). i have an idea to improve it (explained in admin.html as todo) by making the layout look just like the config file. not urgent though. i've already changed my websites/webapps over. the idea of adding a webserver is to take away a (the) reason for folks to want to complicate their mox setup by running an other webserver on the same machine. i think the current webserver implementation can already serve most common use cases. with a few more tweaks (feedback needed!) we should be able to get to 95% of the use cases. the reverse proxy can take care of the remaining 5%. nevertheless, a next step is still to change the quickstart to make it easier for folks to run with an existing webserver, with existing tls certs/keys. that's how this relates to issue #5.
2023-03-02 20:15:54 +03:00
var haveSTSListener, haveWebserverListener bool
2023-01-30 16:27:06 +03:00
for _, l := range static.Listeners {
if l.MTASTSHTTPS.Enabled {
haveSTSListener = true
improve webserver, add domain redirects (aliases), add tests and admin page ui to manage the config - make builtin http handlers serve on specific domains, such as for mta-sts, so e.g. /.well-known/mta-sts.txt isn't served on all domains. - add logging of a few more fields in access logging. - small tweaks/bug fixes in webserver request handling. - add config option for redirecting entire domains to another (common enough). - split httpserver metric into two: one for duration until writing header (i.e. performance of server), another for duration until full response is sent to client (i.e. performance as perceived by users). - add admin ui, a new page for managing the configs. after making changes and hitting "save", the changes take effect immediately. the page itself doesn't look very well-designed (many input fields, makes it look messy). i have an idea to improve it (explained in admin.html as todo) by making the layout look just like the config file. not urgent though. i've already changed my websites/webapps over. the idea of adding a webserver is to take away a (the) reason for folks to want to complicate their mox setup by running an other webserver on the same machine. i think the current webserver implementation can already serve most common use cases. with a few more tweaks (feedback needed!) we should be able to get to 95% of the use cases. the reverse proxy can take care of the remaining 5%. nevertheless, a next step is still to change the quickstart to make it easier for folks to run with an existing webserver, with existing tls certs/keys. that's how this relates to issue #5.
2023-03-02 20:15:54 +03:00
}
if l.WebserverHTTP.Enabled || l.WebserverHTTPS.Enabled {
haveWebserverListener = true
2023-01-30 16:27:06 +03:00
}
}
new feature: when delivering messages from the queue, make it possible to use a "transport" the default transport is still just "direct delivery", where we connect to the destination domain's MX servers. other transports are: - regular smtp without authentication, this is relaying to a smarthost. - submission with authentication, e.g. to a third party email sending service. - direct delivery, but with with connections going through a socks proxy. this can be helpful if your ip is blocked, you need to get email out, and you have another IP that isn't blocked. keep in mind that for all of the above, appropriate SPF/DKIM settings have to be configured. the "dnscheck" for a domain does a check for any SOCKS IP in the SPF record. SPF for smtp/submission (ranges? includes?) and any DKIM requirements cannot really be checked. which transport is used can be configured through routes. routes can be set on an account, a domain, or globally. the routes are evaluated in that order, with the first match selecting the transport. these routes are evaluated for each delivery attempt. common selection criteria are recipient domain and sender domain, but also which delivery attempt this is. you could configured mox to attempt sending through a 3rd party from the 4th attempt onwards. routes and transports are optional. if no route matches, or an empty/zero transport is selected, normal direct delivery is done. we could already "submit" emails with 3rd party accounts with "sendmail". but we now support more SASL authentication mechanisms with SMTP (not only PLAIN, but also SCRAM-SHA-256, SCRAM-SHA-1 and CRAM-MD5), which sendmail now also supports. sendmail will use the most secure mechanism supported by the server, or the explicitly configured mechanism. for issue #36 by dmikushin. also based on earlier discussion on hackernews.
2023-06-16 19:38:28 +03:00
checkRoutes := func(descr string, routes []config.Route) {
parseRouteDomains := func(l []string) []string {
var r []string
for _, e := range l {
if e == "." {
r = append(r, e)
continue
}
prefix := ""
if strings.HasPrefix(e, ".") {
prefix = "."
e = e[1:]
}
d, err := dns.ParseDomain(e)
if err != nil {
addErrorf("%s: invalid domain %s: %v", descr, e, err)
}
r = append(r, prefix+d.ASCII)
}
return r
}
for i := range routes {
routes[i].FromDomainASCII = parseRouteDomains(routes[i].FromDomain)
routes[i].ToDomainASCII = parseRouteDomains(routes[i].ToDomain)
var ok bool
routes[i].ResolvedTransport, ok = static.Transports[routes[i].Transport]
if !ok {
addErrorf("%s: route references undefined transport %s", descr, routes[i].Transport)
}
}
}
checkRoutes("global routes", c.Routes)
2023-01-30 16:27:06 +03:00
// 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 %s must be specified in IDNA form, %s", d, dnsdomain.Name())
2023-01-30 16:27:06 +03:00
}
domain.Domain = dnsdomain
for _, sign := range domain.DKIM.Sign {
if _, ok := domain.DKIM.Selectors[sign]; !ok {
addErrorf("selector %s for signing is missing in domain %s", sign, d)
2023-01-30 16:27:06 +03:00
}
}
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 %s", sel.HashEffective, name, d)
2023-01-30 16:27:06 +03:00
}
pemBuf, err := os.ReadFile(configDirPath(dynamicPath, sel.PrivateKeyFile))
if err != nil {
addErrorf("reading private key for selector %s in domain %s: %s", name, d, err)
2023-01-30 16:27:06 +03:00
continue
}
p, _ := pem.Decode(pemBuf)
if p == nil {
addErrorf("private key for selector %s in domain %s has no PEM block", name, d)
2023-01-30 16:27:06 +03:00
continue
}
key, err := x509.ParsePKCS8PrivateKey(p.Bytes)
if err != nil {
addErrorf("parsing private key for selector %s in domain %s: %s", name, d, err)
2023-01-30 16:27:06 +03:00
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 %s in domain %s", key, name, d)
2023-01-30 16:27:06 +03:00
}
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)
}
}
new feature: when delivering messages from the queue, make it possible to use a "transport" the default transport is still just "direct delivery", where we connect to the destination domain's MX servers. other transports are: - regular smtp without authentication, this is relaying to a smarthost. - submission with authentication, e.g. to a third party email sending service. - direct delivery, but with with connections going through a socks proxy. this can be helpful if your ip is blocked, you need to get email out, and you have another IP that isn't blocked. keep in mind that for all of the above, appropriate SPF/DKIM settings have to be configured. the "dnscheck" for a domain does a check for any SOCKS IP in the SPF record. SPF for smtp/submission (ranges? includes?) and any DKIM requirements cannot really be checked. which transport is used can be configured through routes. routes can be set on an account, a domain, or globally. the routes are evaluated in that order, with the first match selecting the transport. these routes are evaluated for each delivery attempt. common selection criteria are recipient domain and sender domain, but also which delivery attempt this is. you could configured mox to attempt sending through a 3rd party from the 4th attempt onwards. routes and transports are optional. if no route matches, or an empty/zero transport is selected, normal direct delivery is done. we could already "submit" emails with 3rd party accounts with "sendmail". but we now support more SASL authentication mechanisms with SMTP (not only PLAIN, but also SCRAM-SHA-256, SCRAM-SHA-1 and CRAM-MD5), which sendmail now also supports. sendmail will use the most secure mechanism supported by the server, or the explicitly configured mechanism. for issue #36 by dmikushin. also based on earlier discussion on hackernews.
2023-06-16 19:38:28 +03:00
checkRoutes("routes for domain", domain.Routes)
2023-01-30 16:27:06 +03:00
c.Domains[d] = domain
}
// Validate email addresses.
2023-01-30 16:27:06 +03:00
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 %s for account %q: %s", acc.Domain, accName, err)
2023-01-30 16:27:06 +03:00
}
if strings.EqualFold(acc.RejectsMailbox, "Inbox") {
improve training of junk filter before, we used heuristics to decide when to train/untrain a message as junk or nonjunk: the message had to be seen, be in certain mailboxes. then if a message was marked as junk, it was junk. and otherwise it was nonjunk. this wasn't good enough: you may want to keep some messages around as neither junk or nonjunk. and that wasn't possible. ideally, we would just look at the imap $Junk and $NotJunk flags. the problem is that mail clients don't set these flags, or don't make it easy. thunderbird can set the flags based on its own bayesian filter. it has a shortcut for marking Junk and moving it to the junk folder (good), but the counterpart of notjunk only marks a message as notjunk without showing in the UI that it was marked as notjunk. there is also no "move and mark as notjunk" mechanism. e.g. "archive" does not mark a message as notjunk. ios mail and mutt don't appear to have any way to see or change the $Junk and $NotJunk flags. what email clients do have is the ability to move messages to other mailboxes/folders. so mox now has a mechanism that allows you to configure mailboxes that automatically set $Junk or $NotJunk (or clear both) when a message is moved/copied/delivered to that folder. e.g. a mailbox called junk or spam or rejects marks its messags as junk. inbox, postmaster, dmarc, tlsrpt, neutral* mark their messages as neither junk or notjunk. other folders mark their messages as notjunk. e.g. list/*, archive. this functionality is optional, but enabled with the quickstart and for new accounts. also, mox now keeps track of the previous training of a message and will only untrain/train if needed. before, there probably have been duplicate or missing (un)trainings. this also includes a new subcommand "retrain" to recreate the junkfilter for an account. you should run it after updating to this version. and you should probably also modify your account config to include the AutomaticJunkFlags.
2023-02-12 01:00:12 +03:00
addErrorf("account %q: cannot set RejectsMailbox to inbox, messages will be removed automatically from the rejects mailbox", accName)
2023-01-30 16:27:06 +03:00
}
checkMailboxNormf(acc.RejectsMailbox, "account %q", accName)
improve training of junk filter before, we used heuristics to decide when to train/untrain a message as junk or nonjunk: the message had to be seen, be in certain mailboxes. then if a message was marked as junk, it was junk. and otherwise it was nonjunk. this wasn't good enough: you may want to keep some messages around as neither junk or nonjunk. and that wasn't possible. ideally, we would just look at the imap $Junk and $NotJunk flags. the problem is that mail clients don't set these flags, or don't make it easy. thunderbird can set the flags based on its own bayesian filter. it has a shortcut for marking Junk and moving it to the junk folder (good), but the counterpart of notjunk only marks a message as notjunk without showing in the UI that it was marked as notjunk. there is also no "move and mark as notjunk" mechanism. e.g. "archive" does not mark a message as notjunk. ios mail and mutt don't appear to have any way to see or change the $Junk and $NotJunk flags. what email clients do have is the ability to move messages to other mailboxes/folders. so mox now has a mechanism that allows you to configure mailboxes that automatically set $Junk or $NotJunk (or clear both) when a message is moved/copied/delivered to that folder. e.g. a mailbox called junk or spam or rejects marks its messags as junk. inbox, postmaster, dmarc, tlsrpt, neutral* mark their messages as neither junk or notjunk. other folders mark their messages as notjunk. e.g. list/*, archive. this functionality is optional, but enabled with the quickstart and for new accounts. also, mox now keeps track of the previous training of a message and will only untrain/train if needed. before, there probably have been duplicate or missing (un)trainings. this also includes a new subcommand "retrain" to recreate the junkfilter for an account. you should run it after updating to this version. and you should probably also modify your account config to include the AutomaticJunkFlags.
2023-02-12 01:00:12 +03:00
if acc.AutomaticJunkFlags.JunkMailboxRegexp != "" {
r, err := regexp.Compile(acc.AutomaticJunkFlags.JunkMailboxRegexp)
if err != nil {
addErrorf("invalid JunkMailboxRegexp regular expression: %v", err)
}
acc.JunkMailbox = r
}
if acc.AutomaticJunkFlags.NeutralMailboxRegexp != "" {
r, err := regexp.Compile(acc.AutomaticJunkFlags.NeutralMailboxRegexp)
if err != nil {
addErrorf("invalid NeutralMailboxRegexp regular expression: %v", err)
}
acc.NeutralMailbox = r
}
if acc.AutomaticJunkFlags.NotJunkMailboxRegexp != "" {
r, err := regexp.Compile(acc.AutomaticJunkFlags.NotJunkMailboxRegexp)
if err != nil {
addErrorf("invalid NotJunkMailboxRegexp regular expression: %v", err)
}
acc.NotJunkMailbox = r
}
c.Accounts[accName] = acc
// todo deprecated: only localpart as keys for Destinations, we are replacing them with full addresses. if domains.conf is written, we won't have to do this again.
replaceLocalparts := map[string]string{}
2023-01-30 16:27:06 +03:00
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
}
}
// Catchall destination for domain.
if strings.HasPrefix(addrName, "@") {
d, err := dns.ParseDomain(addrName[1:])
if err != nil {
addErrorf("parsing domain %q in account %q", addrName[1:], accName)
continue
} else if _, ok := c.Domains[d.Name()]; !ok {
addErrorf("unknown domain for address %q in account %q", addrName, accName)
continue
}
addrFull := "@" + d.Name()
if _, ok := accDests[addrFull]; ok {
addErrorf("duplicate canonicalized catchall destination address %s", addrFull)
}
accDests[addrFull] = AccountDestination{true, "", accName, dest}
continue
}
// todo deprecated: remove support for parsing destination as just a localpart instead full address.
2023-01-30 16:27:06 +03:00
var address smtp.Address
if localpart, err := smtp.ParseLocalpart(addrName); err != nil && errors.Is(err, smtp.ErrBadLocalpart) {
2023-01-30 16:27:06 +03:00
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 %s for account %q", acc.DNSDomain.Name(), accName)
2023-01-30 16:27:06 +03:00
continue
}
replaceLocalparts[addrName] = address.Pack(true)
2023-01-30 16:27:06 +03:00
}
origLP := address.Localpart
dc := c.Domains[address.Domain.Name()]
if lp, err := CanonicalLocalpart(address.Localpart, dc); err != nil {
addErrorf("canonicalizing localpart %s: %v", address.Localpart, err)
} else if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(address.Localpart), dc.LocalpartCatchallSeparator) {
addErrorf("localpart of address %s includes domain catchall separator %s", address, dc.LocalpartCatchallSeparator)
} else {
address.Localpart = lp
}
2023-01-30 16:27:06 +03:00
addrFull := address.Pack(true)
if _, ok := accDests[addrFull]; ok {
addErrorf("duplicate canonicalized destination address %s", addrFull)
2023-01-30 16:27:06 +03:00
}
accDests[addrFull] = AccountDestination{false, origLP, accName, dest}
2023-01-30 16:27:06 +03:00
}
for lp, addr := range replaceLocalparts {
dest, ok := acc.Destinations[lp]
if !ok {
addErrorf("could not find localpart %q to replace with address in destinations", lp)
} else {
log.Error(`deprecation warning: support for account destination addresses specified as just localpart ("username") instead of full email address will be removed in the future; update domains.conf, for each Account, for each Destination, ensure each key is an email address by appending "@" and the default domain for the account`, mlog.Field("localpart", lp), mlog.Field("address", addr), mlog.Field("account", accName))
acc.Destinations[addr] = dest
delete(acc.Destinations, lp)
}
}
new feature: when delivering messages from the queue, make it possible to use a "transport" the default transport is still just "direct delivery", where we connect to the destination domain's MX servers. other transports are: - regular smtp without authentication, this is relaying to a smarthost. - submission with authentication, e.g. to a third party email sending service. - direct delivery, but with with connections going through a socks proxy. this can be helpful if your ip is blocked, you need to get email out, and you have another IP that isn't blocked. keep in mind that for all of the above, appropriate SPF/DKIM settings have to be configured. the "dnscheck" for a domain does a check for any SOCKS IP in the SPF record. SPF for smtp/submission (ranges? includes?) and any DKIM requirements cannot really be checked. which transport is used can be configured through routes. routes can be set on an account, a domain, or globally. the routes are evaluated in that order, with the first match selecting the transport. these routes are evaluated for each delivery attempt. common selection criteria are recipient domain and sender domain, but also which delivery attempt this is. you could configured mox to attempt sending through a 3rd party from the 4th attempt onwards. routes and transports are optional. if no route matches, or an empty/zero transport is selected, normal direct delivery is done. we could already "submit" emails with 3rd party accounts with "sendmail". but we now support more SASL authentication mechanisms with SMTP (not only PLAIN, but also SCRAM-SHA-256, SCRAM-SHA-1 and CRAM-MD5), which sendmail now also supports. sendmail will use the most secure mechanism supported by the server, or the explicitly configured mechanism. for issue #36 by dmikushin. also based on earlier discussion on hackernews.
2023-06-16 19:38:28 +03:00
checkRoutes("routes for account", acc.Routes)
2023-01-30 16:27:06 +03:00
}
// 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{false, lp, dmarc.Account, dest}
2023-01-30 16:27:06 +03:00
}
// 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{false, lp, tlsrpt.Account, dest}
2023-01-30 16:27:06 +03:00
}
// Check webserver configs.
improve webserver, add domain redirects (aliases), add tests and admin page ui to manage the config - make builtin http handlers serve on specific domains, such as for mta-sts, so e.g. /.well-known/mta-sts.txt isn't served on all domains. - add logging of a few more fields in access logging. - small tweaks/bug fixes in webserver request handling. - add config option for redirecting entire domains to another (common enough). - split httpserver metric into two: one for duration until writing header (i.e. performance of server), another for duration until full response is sent to client (i.e. performance as perceived by users). - add admin ui, a new page for managing the configs. after making changes and hitting "save", the changes take effect immediately. the page itself doesn't look very well-designed (many input fields, makes it look messy). i have an idea to improve it (explained in admin.html as todo) by making the layout look just like the config file. not urgent though. i've already changed my websites/webapps over. the idea of adding a webserver is to take away a (the) reason for folks to want to complicate their mox setup by running an other webserver on the same machine. i think the current webserver implementation can already serve most common use cases. with a few more tweaks (feedback needed!) we should be able to get to 95% of the use cases. the reverse proxy can take care of the remaining 5%. nevertheless, a next step is still to change the quickstart to make it easier for folks to run with an existing webserver, with existing tls certs/keys. that's how this relates to issue #5.
2023-03-02 20:15:54 +03:00
if (len(c.WebDomainRedirects) > 0 || len(c.WebHandlers) > 0) && !haveWebserverListener {
addErrorf("WebDomainRedirects or WebHandlers configured but no listener with WebserverHTTP or WebserverHTTPS enabled")
}
c.WebDNSDomainRedirects = map[dns.Domain]dns.Domain{}
for from, to := range c.WebDomainRedirects {
fromdom, err := dns.ParseDomain(from)
if err != nil {
addErrorf("parsing domain for redirect %s: %v", from, err)
}
todom, err := dns.ParseDomain(to)
if err != nil {
addErrorf("parsing domain for redirect %s: %v", to, err)
} else if fromdom == todom {
addErrorf("will not redirect domain %s to itself", todom)
}
var zerodom dns.Domain
if _, ok := c.WebDNSDomainRedirects[fromdom]; ok && fromdom != zerodom {
addErrorf("duplicate redirect domain %s", from)
}
c.WebDNSDomainRedirects[fromdom] = todom
}
for i := range c.WebHandlers {
wh := &c.WebHandlers[i]
if wh.LogName == "" {
wh.Name = fmt.Sprintf("%d", i)
} else {
wh.Name = wh.LogName
}
dom, err := dns.ParseDomain(wh.Domain)
if err != nil {
addErrorf("webhandler %s %s: parsing domain: %v", wh.Domain, wh.PathRegexp, err)
}
wh.DNSDomain = dom
if !strings.HasPrefix(wh.PathRegexp, "^") {
addErrorf("webhandler %s %s: path regexp must start with a ^", wh.Domain, wh.PathRegexp)
}
re, err := regexp.Compile(wh.PathRegexp)
if err != nil {
addErrorf("webhandler %s %s: compiling regexp: %v", wh.Domain, wh.PathRegexp, err)
}
wh.Path = re
var n int
if wh.WebStatic != nil {
n++
ws := wh.WebStatic
if ws.StripPrefix != "" && !strings.HasPrefix(ws.StripPrefix, "/") {
addErrorf("webstatic %s %s: prefix to strip %s must start with a slash", wh.Domain, wh.PathRegexp, ws.StripPrefix)
}
for k := range ws.ResponseHeaders {
xk := k
k := strings.TrimSpace(xk)
if k != xk || k == "" {
addErrorf("webstatic %s %s: bad header %q", wh.Domain, wh.PathRegexp, xk)
}
}
}
if wh.WebRedirect != nil {
n++
wr := wh.WebRedirect
if wr.BaseURL != "" {
u, err := url.Parse(wr.BaseURL)
if err != nil {
addErrorf("webredirect %s %s: parsing redirect url %s: %v", wh.Domain, wh.PathRegexp, wr.BaseURL, err)
}
switch u.Path {
case "", "/":
u.Path = "/"
default:
addErrorf("webredirect %s %s: BaseURL must have empty path", wh.Domain, wh.PathRegexp, wr.BaseURL)
}
wr.URL = u
}
if wr.OrigPathRegexp != "" && wr.ReplacePath != "" {
re, err := regexp.Compile(wr.OrigPathRegexp)
if err != nil {
addErrorf("webredirect %s %s: compiling regexp %s: %v", wh.Domain, wh.PathRegexp, wr.OrigPathRegexp, err)
}
wr.OrigPath = re
} else if wr.OrigPathRegexp != "" || wr.ReplacePath != "" {
addErrorf("webredirect %s %s: must have either both OrigPathRegexp and ReplacePath, or neither", wh.Domain, wh.PathRegexp)
} else if wr.BaseURL == "" {
addErrorf("webredirect %s %s: must at least one of BaseURL and OrigPathRegexp+ReplacePath", wh.Domain, wh.PathRegexp)
}
if wr.StatusCode != 0 && (wr.StatusCode < 300 || wr.StatusCode >= 400) {
addErrorf("webredirect %s %s: invalid redirect status code %d", wh.Domain, wh.PathRegexp, wr.StatusCode)
}
}
if wh.WebForward != nil {
n++
wf := wh.WebForward
u, err := url.Parse(wf.URL)
if err != nil {
addErrorf("webforward %s %s: parsing url %s: %v", wh.Domain, wh.PathRegexp, wf.URL, err)
}
wf.TargetURL = u
for k := range wf.ResponseHeaders {
xk := k
k := strings.TrimSpace(xk)
if k != xk || k == "" {
addErrorf("webforward %s %s: bad header %q", wh.Domain, wh.PathRegexp, xk)
}
}
}
if n != 1 {
addErrorf("webhandler %s %s: must have exactly one handler, not %d", wh.Domain, wh.PathRegexp, n)
}
}
2023-01-30 16:27:06 +03:00
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 := loadX509KeyPairPrivileged(certPath, keyPath)
2023-01-30 16:27:06 +03:00
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
}
// load x509 key/cert files from file descriptor possibly passed in by privileged
// process.
func loadX509KeyPairPrivileged(certPath, keyPath string) (tls.Certificate, error) {
certBuf, err := readFilePrivileged(certPath)
if err != nil {
return tls.Certificate{}, fmt.Errorf("reading tls certificate: %v", err)
}
keyBuf, err := readFilePrivileged(keyPath)
if err != nil {
return tls.Certificate{}, fmt.Errorf("reading tls key: %v", err)
}
return tls.X509KeyPair(certBuf, keyBuf)
}
// like os.ReadFile, but open privileged file possibly passed in by root process.
func readFilePrivileged(path string) ([]byte, error) {
f, err := OpenPrivileged(path)
if err != nil {
return nil, err
}
defer f.Close()
return io.ReadAll(f)
}