mirror of
https://github.com/mjl-/mox.git
synced 2025-07-15 02:14:36 +03:00
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:
1655
mox-/admin.go
1655
mox-/admin.go
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
|
@ -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.
|
||||
|
109
mox-/ip.go
109
mox-/ip.go
@ -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
|
||||
}
|
||||
|
@ -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
24
mox-/txt.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user