move config-changing code from package mox-/ to admin/

needed for upcoming changes, where (now) package admin needs to import package
store. before, because package store imports mox- (for accessing the active
config), that would lead to a cyclic import. package mox- keeps its active
config, package admin has the higher-level config-changing functions.
This commit is contained in:
Mechiel Lukkien
2024-12-02 22:03:18 +01:00
parent de435fceba
commit 5f7831a7f0
18 changed files with 805 additions and 756 deletions

File diff suppressed because it is too large Load Diff

View File

@ -86,11 +86,13 @@ type Config struct {
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.
accountDestinations map[string]AccountDestination
// Like accountDestinations, but for aliases.
// Domains are IDNA names in utf8. Dynamic config lock must be held when accessing.
AccountDestinationsLocked map[string]AccountDestination
// Like AccountDestinationsLocked, but for aliases.
aliases map[string]config.Alias
}
@ -142,9 +144,11 @@ func (c *Config) LogLevels() map[string]slog.Level {
return c.copyLogLevels()
}
func (c *Config) withDynamicLock(fn func()) {
// DynamicLockUnlock locks the dynamic config, will try updating the latest state
// from disk, and return an unlock function. Should be called as "defer
// Conf.DynamicLockUnlock()()".
func (c *Config) DynamicLockUnlock() func() {
c.dynamicMutex.Lock()
defer c.dynamicMutex.Unlock()
now := time.Now()
if now.Sub(c.DynamicLastCheck) > time.Second {
c.DynamicLastCheck = now
@ -159,6 +163,11 @@ func (c *Config) withDynamicLock(fn func()) {
}
}
}
return c.dynamicMutex.Unlock
}
func (c *Config) withDynamicLock(fn func()) {
defer c.DynamicLockUnlock()()
fn()
}
@ -170,7 +179,7 @@ func (c *Config) loadDynamic() []error {
}
c.Dynamic = d
c.dynamicMtime = mtime
c.accountDestinations = accDests
c.AccountDestinationsLocked = accDests
c.aliases = aliases
c.allowACMEHosts(pkglog, true)
return nil
@ -213,7 +222,7 @@ func (c *Config) DomainLocalparts(d dns.Domain) (map[string]string, map[string]c
m := map[string]string{}
aliases := map[string]config.Alias{}
c.withDynamicLock(func() {
for addr, ad := range c.accountDestinations {
for addr, ad := range c.AccountDestinationsLocked {
if strings.HasSuffix(addr, suffix) {
if ad.Catchall {
m[""] = ad.Account
@ -247,7 +256,7 @@ func (c *Config) Account(name string) (acc config.Account, ok bool) {
func (c *Config) AccountDestination(addr string) (accDest AccountDestination, alias *config.Alias, ok bool) {
c.withDynamicLock(func() {
accDest, ok = c.accountDestinations[addr]
accDest, ok = c.AccountDestinationsLocked[addr]
if !ok {
var a config.Alias
a, ok = c.aliases[addr]
@ -345,9 +354,13 @@ func (c *Config) allowACMEHosts(log mlog.Log, checkACMEHosts bool) {
// 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.
// WriteDynamicLocked prepares an updated internal state for the new dynamic
// config, then writes it to disk and activates it.
//
// Returns ErrConfig if the configuration is not valid.
func writeDynamic(ctx context.Context, log mlog.Log, c config.Dynamic) error {
//
// Must be called with config lock held.
func WriteDynamicLocked(ctx context.Context, log mlog.Log, c config.Dynamic) error {
accDests, aliases, errs := prepareDynamicConfig(ctx, log, ConfigDynamicPath, Conf.Static, &c)
if len(errs) > 0 {
errstrs := make([]string, len(errs))
@ -399,7 +412,7 @@ func writeDynamic(ctx context.Context, log mlog.Log, c config.Dynamic) error {
Conf.dynamicMtime = fi.ModTime()
Conf.DynamicLastCheck = time.Now()
Conf.Dynamic = c
Conf.accountDestinations = accDests
Conf.AccountDestinationsLocked = accDests
Conf.aliases = aliases
Conf.allowACMEHosts(log, true)
@ -440,7 +453,7 @@ func LoadConfig(ctx context.Context, log mlog.Log, doLoadTLSKeyCerts, checkACMEH
// 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, c.aliases}
Conf = Config{c.Static, sync.Mutex{}, c.Log, sync.Mutex{}, c.Dynamic, c.dynamicMtime, c.DynamicLastCheck, c.AccountDestinationsLocked, c.aliases}
// If we have non-standard CA roots, use them for all HTTPS requests.
if Conf.Static.TLS.CertPool != nil {
@ -491,7 +504,7 @@ func ParseConfig(ctx context.Context, log mlog.Log, p string, checkOnly, doLoadT
}
pp := filepath.Join(filepath.Dir(p), "domains.conf")
c.Dynamic, c.dynamicMtime, c.accountDestinations, c.aliases, errs = ParseDynamicConfig(ctx, log, pp, c.Static)
c.Dynamic, c.dynamicMtime, c.AccountDestinationsLocked, c.aliases, errs = ParseDynamicConfig(ctx, log, pp, c.Static)
if !checkOnly {
c.allowACMEHosts(log, checkACMEHosts)

View File

@ -5,11 +5,18 @@ import (
)
// ConfigDirPath returns the path to "f". Either f itself when absolute, or
// interpreted relative to the directory of the current config file.
// interpreted relative to the directory of the static configuration file
// (mox.conf).
func ConfigDirPath(f string) string {
return configDirPath(ConfigStaticPath, f)
}
// Like ConfigDirPath, but relative paths are interpreted relative to the directory
// of the dynamic configuration file (domains.conf).
func ConfigDynamicDirPath(f string) string {
return configDirPath(ConfigDynamicPath, f)
}
// DataDirPath returns to the path to "f". Either f itself when absolute, or
// interpreted relative to the data directory from the currently active
// configuration.

View File

@ -1,6 +1,9 @@
package mox
import (
"context"
"fmt"
"log/slog"
"net"
)
@ -19,3 +22,109 @@ func Network(ip string) string {
}
return "tcp6"
}
// DomainSPFIPs returns IPs to include in SPF records for domains. It includes the
// IPs on listeners that have SMTP enabled, and includes IPs configured for SOCKS
// transports.
func DomainSPFIPs() (ips []net.IP) {
for _, l := range Conf.Static.Listeners {
if !l.SMTP.Enabled || l.IPsNATed {
continue
}
ipstrs := l.IPs
if len(l.NATIPs) > 0 {
ipstrs = l.NATIPs
}
for _, ipstr := range ipstrs {
ip := net.ParseIP(ipstr)
if ip.IsUnspecified() {
continue
}
ips = append(ips, ip)
}
}
for _, t := range Conf.Static.Transports {
if t.Socks != nil {
ips = append(ips, t.Socks.IPs...)
}
}
return ips
}
// IPs returns ip addresses we may be listening/receiving mail on or
// connecting/sending from to the outside.
func IPs(ctx context.Context, receiveOnly bool) ([]net.IP, error) {
log := pkglog.WithContext(ctx)
// Try to gather all IPs we are listening on by going through the config.
// If we encounter 0.0.0.0 or ::, we'll gather all local IPs afterwards.
var ips []net.IP
var ipv4all, ipv6all bool
for _, l := range Conf.Static.Listeners {
// If NATed, we don't know our external IPs.
if l.IPsNATed {
return nil, nil
}
check := l.IPs
if len(l.NATIPs) > 0 {
check = l.NATIPs
}
for _, s := range check {
ip := net.ParseIP(s)
if ip.IsUnspecified() {
if ip.To4() != nil {
ipv4all = true
} else {
ipv6all = true
}
continue
}
ips = append(ips, ip)
}
}
// We'll list the IPs on the interfaces. How useful is this? There is a good chance
// we're listening on all addresses because of a load balancer/firewall.
if ipv4all || ipv6all {
ifaces, err := net.Interfaces()
if err != nil {
return nil, fmt.Errorf("listing network interfaces: %v", err)
}
for _, iface := range ifaces {
if iface.Flags&net.FlagUp == 0 {
continue
}
addrs, err := iface.Addrs()
if err != nil {
return nil, fmt.Errorf("listing addresses for network interface: %v", err)
}
if len(addrs) == 0 {
continue
}
for _, addr := range addrs {
ip, _, err := net.ParseCIDR(addr.String())
if err != nil {
log.Errorx("bad interface addr", err, slog.Any("address", addr))
continue
}
v4 := ip.To4() != nil
if ipv4all && v4 || ipv6all && !v4 {
ips = append(ips, ip)
}
}
}
}
if receiveOnly {
return ips, nil
}
for _, t := range Conf.Static.Transports {
if t.Socks != nil {
ips = append(ips, t.Socks.IPs...)
}
}
return ips, nil
}

View File

@ -1,51 +0,0 @@
package mox
import (
"fmt"
"os"
"strings"
"time"
"github.com/mjl-/mox/moxvar"
"github.com/mjl-/mox/updates"
)
// StoreLastKnown stores the the last known version. Future update checks compare
// against it, or the currently running version, whichever is newer.
func StoreLastKnown(v updates.Version) error {
return os.WriteFile(DataDirPath("lastknownversion"), []byte(v.String()), 0660)
}
// LastKnown returns the last known version that has been mentioned in an update
// email, or the current application.
func LastKnown() (current, lastknown updates.Version, mtime time.Time, rerr error) {
curv, curerr := updates.ParseVersion(moxvar.VersionBare)
p := DataDirPath("lastknownversion")
fi, _ := os.Stat(p)
if fi != nil {
mtime = fi.ModTime()
}
vbuf, err := os.ReadFile(p)
if err != nil && !os.IsNotExist(err) {
return curv, updates.Version{}, mtime, err
}
lastknown, lasterr := updates.ParseVersion(strings.TrimSpace(string(vbuf)))
if curerr == nil && lasterr == nil {
if curv.After(lastknown) {
return curv, curv, mtime, nil
}
return curv, lastknown, mtime, nil
} else if curerr == nil {
return curv, curv, mtime, nil
} else if lasterr == nil {
return curv, lastknown, mtime, nil
}
if strings.HasPrefix(moxvar.Version, "(devel)") {
return curv, updates.Version{}, mtime, fmt.Errorf("development version")
}
return curv, updates.Version{}, mtime, fmt.Errorf("parsing version: %w", err)
}

24
mox-/txt.go Normal file
View File

@ -0,0 +1,24 @@
package mox
// TXTStrings returns a TXT record value as one or more quoted strings, each max
// 100 characters. In case of multiple strings, a multi-line record is returned.
func TXTStrings(s string) string {
if len(s) <= 100 {
return `"` + s + `"`
}
r := "(\n"
for len(s) > 0 {
n := len(s)
if n > 100 {
n = 100
}
if r != "" {
r += " "
}
r += "\t\t\"" + s[:n] + "\"\n"
s = s[n:]
}
r += "\t)"
return r
}