mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 16:33:47 +03:00
add reverse ip checks during quickstart and in "check dns" admin page/subcommand
- and don't have a global variable "d" in the big checkDomain function in http/admin.go. - and set loglevel from command-line flag again after loading the config file, for all subcommands except "serve".
This commit is contained in:
parent
8bbaa38c74
commit
c21b8c0d54
8 changed files with 300 additions and 89 deletions
|
@ -51,7 +51,7 @@ type Static struct {
|
|||
DefaultMailboxes []string `sconf:"optional" sconf-doc:"Mailboxes to create when adding an account. Inbox is always created. If no mailboxes are specified, the following are automatically created: Sent, Archive, Trash, Drafts and Junk."`
|
||||
|
||||
// All IPs that were explicitly listen on for external SMTP. Only set when there
|
||||
// are no unspecified external SMTP listeners and there is at most 1 for IPv4 and
|
||||
// are no unspecified external SMTP listeners and there is at most one for IPv4 and
|
||||
// at most one for IPv6. Used for setting the local address when making outgoing
|
||||
// connections. Those IPs are assumed to be in an SPF record for the domain,
|
||||
// potentially unlike other IPs on the machine. If there is only one address
|
||||
|
|
150
http/admin.go
150
http/admin.go
|
@ -183,6 +183,12 @@ type TLSCheckResult struct {
|
|||
Result
|
||||
}
|
||||
|
||||
type IPRevCheckResult struct {
|
||||
Hostname dns.Domain // This hostname, IPs must resolve back to this.
|
||||
IPNames map[string][]string // IP to names.
|
||||
Result
|
||||
}
|
||||
|
||||
type MX struct {
|
||||
Host string
|
||||
Pref int
|
||||
|
@ -275,6 +281,7 @@ type AutodiscoverCheckResult struct {
|
|||
// (e.g. DNS records), and warnings and errors encountered.
|
||||
type CheckResult struct {
|
||||
Domain string
|
||||
IPRev IPRevCheckResult
|
||||
MX MXCheckResult
|
||||
TLS TLSCheckResult
|
||||
SPF SPFCheckResult
|
||||
|
@ -319,10 +326,10 @@ func (Admin) CheckDomain(ctx context.Context, domainName string) (r CheckResult)
|
|||
}
|
||||
|
||||
func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer, domainName string) (r CheckResult) {
|
||||
d, err := dns.ParseDomain(domainName)
|
||||
domain, err := dns.ParseDomain(domainName)
|
||||
xcheckf(ctx, err, "parsing domain")
|
||||
|
||||
domain, ok := mox.Conf.Domain(d)
|
||||
domConf, ok := mox.Conf.Domain(domain)
|
||||
if !ok {
|
||||
panic(&sherpa.Error{Code: "user:notFound", Message: "domain not found"})
|
||||
}
|
||||
|
@ -384,15 +391,88 @@ func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer,
|
|||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// IPRev
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer logPanic(ctx)
|
||||
defer wg.Done()
|
||||
|
||||
// For each mox.Conf.SpecifiedSMTPListenIPs, and each address for
|
||||
// mox.Conf.HostnameDomain, check if they resolve back to the host name.
|
||||
var ips []net.IP
|
||||
ips, err := resolver.LookupIP(ctx, "ip", mox.Conf.Static.HostnameDomain.ASCII+".")
|
||||
if err != nil {
|
||||
addf(&r.IPRev.Errors, "Looking up IPs for hostname: %s", err)
|
||||
}
|
||||
nextip:
|
||||
for _, ip := range mox.Conf.Static.SpecifiedSMTPListenIPs {
|
||||
for _, xip := range ips {
|
||||
if ip.Equal(xip) {
|
||||
continue nextip
|
||||
}
|
||||
}
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
|
||||
type result struct {
|
||||
IP string
|
||||
Addrs []string
|
||||
Err error
|
||||
}
|
||||
results := make(chan result)
|
||||
var ipstrs []string
|
||||
for _, ip := range ips {
|
||||
s := ip.String()
|
||||
go func() {
|
||||
addrs, err := resolver.LookupAddr(ctx, s)
|
||||
results <- result{s, addrs, err}
|
||||
}()
|
||||
ipstrs = append(ipstrs, s)
|
||||
}
|
||||
r.IPRev.IPNames = map[string][]string{}
|
||||
for i := 0; i < len(ips); i++ {
|
||||
lr := <-results
|
||||
addrs, ip, err := lr.Addrs, lr.IP, lr.Err
|
||||
if err != nil {
|
||||
addf(&r.IPRev.Errors, "Looking up reverse name for %s: %v", ip, err)
|
||||
continue
|
||||
}
|
||||
if len(addrs) != 1 {
|
||||
addf(&r.IPRev.Errors, "Expected exactly 1 name for %s, got %d (%v)", ip, len(addrs), addrs)
|
||||
}
|
||||
var match bool
|
||||
for i, a := range addrs {
|
||||
a = strings.TrimRight(a, ".")
|
||||
addrs[i] = a
|
||||
ad, err := dns.ParseDomain(a)
|
||||
if err != nil {
|
||||
addf(&r.IPRev.Errors, "Parsing reverse name %q for %s: %v", a, ip, err)
|
||||
}
|
||||
if ad == mox.Conf.Static.HostnameDomain {
|
||||
match = true
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
addf(&r.IPRev.Errors, "Reverse name(s) %s for ip %s do not match hostname %s, which will cause other mail servers to reject incoming messages from this IP.", strings.Join(addrs, ","), ip, mox.Conf.Static.HostnameDomain)
|
||||
}
|
||||
r.IPRev.IPNames[ip] = addrs
|
||||
}
|
||||
|
||||
r.IPRev.Hostname = mox.Conf.Static.HostnameDomain
|
||||
r.IPRev.Instructions = []string{
|
||||
fmt.Sprintf("Ensure IPs %s have reverse address %s.", strings.Join(ipstrs, ", "), mox.Conf.Static.HostnameDomain.ASCII),
|
||||
}
|
||||
}()
|
||||
|
||||
// MX
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer logPanic(ctx)
|
||||
defer wg.Done()
|
||||
|
||||
mxs, err := resolver.LookupMX(ctx, d.ASCII+".")
|
||||
mxs, err := resolver.LookupMX(ctx, domain.ASCII+".")
|
||||
if err != nil {
|
||||
addf(&r.MX.Errors, "Looking up MX records for %q: %s", d, err)
|
||||
addf(&r.MX.Errors, "Looking up MX records for %q: %s", domain, err)
|
||||
}
|
||||
r.MX.Records = make([]MX, len(mxs))
|
||||
for i, mx := range mxs {
|
||||
|
@ -416,7 +496,7 @@ func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer,
|
|||
|
||||
}
|
||||
r.MX.Instructions = []string{
|
||||
fmt.Sprintf("Ensure a DNS MX record like the following exists:\n\n\t%s MX 10 %s\n\nWithout the trailing dot, the name would be interpreted as relative to the domain.", d.ASCII+".", mox.Conf.Static.HostnameDomain.ASCII+"."),
|
||||
fmt.Sprintf("Ensure a DNS MX record like the following exists:\n\n\t%s MX 10 %s\n\nWithout the trailing dot, the name would be interpreted as relative to the domain.", domain.ASCII+".", mox.Conf.Static.HostnameDomain.ASCII+"."),
|
||||
}
|
||||
}()
|
||||
|
||||
|
@ -490,7 +570,7 @@ func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer,
|
|||
|
||||
checkSMTPSTARTTLS := func() {
|
||||
// Initial errors are ignored, will already have been warned about by MX checks.
|
||||
mxs, err := resolver.LookupMX(ctx, d.ASCII+".")
|
||||
mxs, err := resolver.LookupMX(ctx, domain.ASCII+".")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -573,7 +653,7 @@ func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer,
|
|||
|
||||
// Check SPF record for domain.
|
||||
var dspfr spf.Record
|
||||
r.SPF.DomainTXT, r.SPF.DomainRecord, dspfr = verifySPF("domain", d)
|
||||
r.SPF.DomainTXT, r.SPF.DomainRecord, dspfr = verifySPF("domain", domain)
|
||||
// todo: possibly check all hosts for MX records? assuming they are also sending mail servers.
|
||||
r.SPF.HostTXT, r.SPF.HostRecord, _ = verifySPF("host", mox.Conf.Static.HostnameDomain)
|
||||
|
||||
|
@ -581,7 +661,7 @@ func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer,
|
|||
if err != nil {
|
||||
addf(&r.SPF.Errors, "Making SPF record for instructions: %s", err)
|
||||
}
|
||||
domainspf := fmt.Sprintf("%s IN TXT %s", d.ASCII+".", mox.TXTStrings(dtxt))
|
||||
domainspf := fmt.Sprintf("%s IN TXT %s", domain.ASCII+".", mox.TXTStrings(dtxt))
|
||||
|
||||
// Check SPF record for sending host. ../rfc/7208:2263 ../rfc/7208:2287
|
||||
hostspf := fmt.Sprintf(`%s IN TXT "v=spf1 a -all"`, mox.Conf.Static.HostnameDomain.ASCII+".")
|
||||
|
@ -597,12 +677,12 @@ func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer,
|
|||
|
||||
var missing []string
|
||||
var haveEd25519 bool
|
||||
for sel, selc := range domain.DKIM.Selectors {
|
||||
for sel, selc := range domConf.DKIM.Selectors {
|
||||
if _, ok := selc.Key.(ed25519.PrivateKey); ok {
|
||||
haveEd25519 = true
|
||||
}
|
||||
|
||||
_, record, txt, err := dkim.Lookup(ctx, resolver, selc.Domain, d)
|
||||
_, record, txt, err := dkim.Lookup(ctx, resolver, selc.Domain, domain)
|
||||
if err != nil {
|
||||
missing = append(missing, sel)
|
||||
if errors.Is(err, dkim.ErrNoRecord) {
|
||||
|
@ -638,7 +718,7 @@ func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer,
|
|||
}
|
||||
}
|
||||
}
|
||||
if len(domain.DKIM.Selectors) == 0 {
|
||||
if len(domConf.DKIM.Selectors) == 0 {
|
||||
addf(&r.DKIM.Errors, "No DKIM configuration, add a key to the configuration file, and instructions for DNS records will appear here.")
|
||||
} else if !haveEd25519 {
|
||||
addf(&r.DKIM.Warnings, "Consider adding an ed25519 key: the keys are smaller, the cryptography faster and more modern.")
|
||||
|
@ -648,7 +728,7 @@ func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer,
|
|||
dkimr := dkim.Record{
|
||||
Version: "DKIM1",
|
||||
Hashes: []string{"sha256"},
|
||||
PublicKey: domain.DKIM.Selectors[sel].Key.Public(),
|
||||
PublicKey: domConf.DKIM.Selectors[sel].Key.Public(),
|
||||
}
|
||||
switch dkimr.PublicKey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
|
@ -676,7 +756,7 @@ func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer,
|
|||
defer logPanic(ctx)
|
||||
defer wg.Done()
|
||||
|
||||
_, dmarcDomain, record, txt, err := dmarc.Lookup(ctx, resolver, d)
|
||||
_, dmarcDomain, record, txt, err := dmarc.Lookup(ctx, resolver, domain)
|
||||
if err != nil {
|
||||
addf(&r.DMARC.Errors, "Looking up DMARC record: %s", err)
|
||||
} else if record == nil {
|
||||
|
@ -697,8 +777,8 @@ func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer,
|
|||
addf(&r.DMARC.Warnings, "It is recommended you specify you would like aggregate reports about delivery success in the DMARC record, see instructions.")
|
||||
}
|
||||
localpart := smtp.Localpart("dmarc-reports")
|
||||
if domain.DMARC != nil {
|
||||
localpart = domain.DMARC.ParsedLocalpart
|
||||
if domConf.DMARC != nil {
|
||||
localpart = domConf.DMARC.ParsedLocalpart
|
||||
} else {
|
||||
addf(&r.DMARC.Instructions, `Configure a DMARC destination in domain in config file. Localpart could be %q.`, localpart)
|
||||
}
|
||||
|
@ -706,7 +786,7 @@ func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer,
|
|||
Version: "DMARC1",
|
||||
Policy: "reject",
|
||||
AggregateReportAddresses: []dmarc.URI{
|
||||
{Address: fmt.Sprintf("mailto:%s!10m", smtp.NewAddress(localpart, d).Pack(false))},
|
||||
{Address: fmt.Sprintf("mailto:%s!10m", smtp.NewAddress(localpart, domain).Pack(false))},
|
||||
},
|
||||
AggregateReportingInterval: 86400,
|
||||
Percentage: 100,
|
||||
|
@ -721,7 +801,7 @@ func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer,
|
|||
defer logPanic(ctx)
|
||||
defer wg.Done()
|
||||
|
||||
record, txt, err := tlsrpt.Lookup(ctx, resolver, d)
|
||||
record, txt, err := tlsrpt.Lookup(ctx, resolver, domain)
|
||||
if err != nil {
|
||||
addf(&r.TLSRPT.Errors, "Looking up TLSRPT record: %s", err)
|
||||
}
|
||||
|
@ -731,15 +811,15 @@ func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer,
|
|||
}
|
||||
|
||||
localpart := smtp.Localpart("tls-reports")
|
||||
if domain.TLSRPT != nil {
|
||||
localpart = domain.TLSRPT.ParsedLocalpart
|
||||
if domConf.TLSRPT != nil {
|
||||
localpart = domConf.TLSRPT.ParsedLocalpart
|
||||
} else {
|
||||
addf(&r.TLSRPT.Errors, `Configure a TLSRPT destination in domain in config file. Localpart could be %q.`, localpart)
|
||||
}
|
||||
tlsrptr := &tlsrpt.Record{
|
||||
Version: "TLSRPTv1",
|
||||
// todo: should URI-encode the URI, including ',', '!' and ';'.
|
||||
RUAs: [][]string{{fmt.Sprintf("mailto:%s", smtp.NewAddress(localpart, d).Pack(false))}},
|
||||
RUAs: [][]string{{fmt.Sprintf("mailto:%s", smtp.NewAddress(localpart, domain).Pack(false))}},
|
||||
}
|
||||
instr := fmt.Sprintf(`TLSRPT is an opt-in mechanism to request feedback about TLS connectivity from remote SMTP servers when they connect to us. It allows detecting delivery problems and unwanted downgrades to plaintext SMTP connections. With TLSRPT you configure an email address to which reports should be sent. Remote SMTP servers will send a report once a day with the number of successful connections, and the number of failed connections including details that should help debugging/resolving any issues.
|
||||
|
||||
|
@ -756,7 +836,7 @@ Ensure a DNS TXT record like the following exists:
|
|||
defer logPanic(ctx)
|
||||
defer wg.Done()
|
||||
|
||||
record, txt, cnames, err := mtasts.LookupRecord(ctx, resolver, d)
|
||||
record, txt, cnames, err := mtasts.LookupRecord(ctx, resolver, domain)
|
||||
if err != nil {
|
||||
addf(&r.MTASTS.Errors, "Looking up MTA-STS record: %s", err)
|
||||
}
|
||||
|
@ -770,7 +850,7 @@ Ensure a DNS TXT record like the following exists:
|
|||
r.MTASTS.Record = &MTASTSRecord{*record}
|
||||
}
|
||||
|
||||
policy, text, err := mtasts.FetchPolicy(ctx, d)
|
||||
policy, text, err := mtasts.FetchPolicy(ctx, domain)
|
||||
if err != nil {
|
||||
addf(&r.MTASTS.Errors, "Fetching MTA-STS policy: %s", err)
|
||||
} else if policy.Mode == mtasts.ModeNone {
|
||||
|
@ -788,16 +868,16 @@ Ensure a DNS TXT record like the following exists:
|
|||
addf(&r.MTASTS.Warnings, "Policy has a MaxAge of less than 1 day. For stable configurations, the recommended period is in weeks.")
|
||||
}
|
||||
|
||||
mxl, _ := resolver.LookupMX(ctx, d.ASCII+".")
|
||||
mxl, _ := resolver.LookupMX(ctx, domain.ASCII+".")
|
||||
// We do not check for errors, the MX check will complain about mx errors, we assume we will get the same error here.
|
||||
mxs := map[dns.Domain]struct{}{}
|
||||
for _, mx := range mxl {
|
||||
domain, err := dns.ParseDomain(strings.TrimSuffix(mx.Host, "."))
|
||||
d, err := dns.ParseDomain(strings.TrimSuffix(mx.Host, "."))
|
||||
if err != nil {
|
||||
addf(&r.MTASTS.Warnings, "MX record %q is invalid: %s", mx.Host, err)
|
||||
continue
|
||||
}
|
||||
mxs[domain] = struct{}{}
|
||||
mxs[d] = struct{}{}
|
||||
}
|
||||
for mx := range mxs {
|
||||
if !policy.Matches(mx) {
|
||||
|
@ -830,14 +910,14 @@ When enabling MTA-STS, or updating a policy, always update the policy first (thr
|
|||
|
||||
addf(&r.MTASTS.Instructions, `Enable a policy through the configuration file. For new deployments, it is best to start with mode "testing" while enabling TLSRPT. Start with a short "max_age", so updates to your policy are picked up quickly. When confidence in the deployment is high enough, switch to "enforce" mode and a longer "max age". A max age in the order of weeks is recommended. If you foresee a change to your setup in the future, requiring different policies or MX records, you may want to dial back the "max age" ahead of time, similar to how you would handle TTL's in DNS record updates.`)
|
||||
|
||||
host := fmt.Sprintf("Ensure DNS CNAME/A/AAAA records exist that resolve mta-sts.%s to this mail server. For example:\n\n\t%s IN CNAME %s\n\n", d.ASCII, "mta-sts."+d.ASCII+".", mox.Conf.Static.HostnameDomain.ASCII+".")
|
||||
host := fmt.Sprintf("Ensure DNS CNAME/A/AAAA records exist that resolve mta-sts.%s to this mail server. For example:\n\n\t%s IN CNAME %s\n\n", domain.ASCII, "mta-sts."+domain.ASCII+".", mox.Conf.Static.HostnameDomain.ASCII+".")
|
||||
addf(&r.MTASTS.Instructions, host)
|
||||
|
||||
mtastsr := mtasts.Record{
|
||||
Version: "STSv1",
|
||||
ID: time.Now().Format("20060102T150405"),
|
||||
}
|
||||
dns := fmt.Sprintf("Ensure a DNS TXT record like the following exists:\n\n\t_mta-sts IN TXT %s\n\nConfigure the ID in the configuration file, it must be of the form [a-zA-Z0-9]{1,31}. It represents the version of the policy. For each policy change, you must change the ID to a new unique value. You could use a timestamp like 20220621T123000. When this field exists, an SMTP server will fetch a policy at https://mta-sts.%s/.well-known/mta-sts.txt. This policy is served by mox.", mox.TXTStrings(mtastsr.String()), d.Name())
|
||||
dns := fmt.Sprintf("Ensure a DNS TXT record like the following exists:\n\n\t_mta-sts IN TXT %s\n\nConfigure the ID in the configuration file, it must be of the form [a-zA-Z0-9]{1,31}. It represents the version of the policy. For each policy change, you must change the ID to a new unique value. You could use a timestamp like 20220621T123000. When this field exists, an SMTP server will fetch a policy at https://mta-sts.%s/.well-known/mta-sts.txt. This policy is served by mox.", mox.TXTStrings(mtastsr.String()), domain.Name())
|
||||
addf(&r.MTASTS.Instructions, dns)
|
||||
}()
|
||||
|
||||
|
@ -885,7 +965,7 @@ When enabling MTA-STS, or updating a policy, always update the policy first (thr
|
|||
for i := range reqs {
|
||||
go func(i int) {
|
||||
defer srvwg.Done()
|
||||
_, reqs[i].srvs, reqs[i].err = resolver.LookupSRV(ctx, reqs[i].name[1:], "tcp", d.ASCII+".")
|
||||
_, reqs[i].srvs, reqs[i].err = resolver.LookupSRV(ctx, reqs[i].name[1:], "tcp", domain.ASCII+".")
|
||||
}(i)
|
||||
}
|
||||
srvwg.Wait()
|
||||
|
@ -893,8 +973,8 @@ When enabling MTA-STS, or updating a policy, always update the policy first (thr
|
|||
instr := "Ensure DNS records like the following exist:\n\n"
|
||||
r.SRVConf.SRVs = map[string][]*net.SRV{}
|
||||
for _, req := range reqs {
|
||||
name := req.name + "_.tcp." + d.ASCII
|
||||
instr += fmt.Sprintf("\t%s._tcp.%s IN SRV 0 1 %d %s\n", req.name, d.ASCII+".", req.port, req.host)
|
||||
name := req.name + "_.tcp." + domain.ASCII
|
||||
instr += fmt.Sprintf("\t%s._tcp.%s IN SRV 0 1 %d %s\n", req.name, domain.ASCII+".", req.port, req.host)
|
||||
r.SRVConf.SRVs[req.name] = req.srvs
|
||||
if err != nil {
|
||||
addf(&r.SRVConf.Errors, "Looking up SRV record %q: %s", name, err)
|
||||
|
@ -913,9 +993,9 @@ When enabling MTA-STS, or updating a policy, always update the policy first (thr
|
|||
defer logPanic(ctx)
|
||||
defer wg.Done()
|
||||
|
||||
addf(&r.Autoconf.Instructions, "Ensure a DNS CNAME record like the following exists:\n\n\tautoconfig.%s IN CNAME %s\n\nNote: the trailing dot is relevant, it makes the host name absolute instead of relative to the domain name.", d.ASCII+".", mox.Conf.Static.HostnameDomain.ASCII+".")
|
||||
addf(&r.Autoconf.Instructions, "Ensure a DNS CNAME record like the following exists:\n\n\tautoconfig.%s IN CNAME %s\n\nNote: the trailing dot is relevant, it makes the host name absolute instead of relative to the domain name.", domain.ASCII+".", mox.Conf.Static.HostnameDomain.ASCII+".")
|
||||
|
||||
host := "autoconfig." + d.ASCII + "."
|
||||
host := "autoconfig." + domain.ASCII + "."
|
||||
ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autoconf.Errors, host)
|
||||
if err != nil {
|
||||
addf(&r.Autoconf.Errors, "Looking up autoconfig host: %s", err)
|
||||
|
@ -929,7 +1009,7 @@ When enabling MTA-STS, or updating a policy, always update the policy first (thr
|
|||
addf(&r.Autoconf.Errors, "Autoconfig does not point to some IP addresses that are not ours: %v", notOurIPs)
|
||||
}
|
||||
|
||||
checkTLS(&r.Autoconf.Errors, "autoconfig."+d.ASCII, ips, "443")
|
||||
checkTLS(&r.Autoconf.Errors, "autoconfig."+domain.ASCII, ips, "443")
|
||||
}()
|
||||
|
||||
// Autodiscover
|
||||
|
@ -938,9 +1018,9 @@ When enabling MTA-STS, or updating a policy, always update the policy first (thr
|
|||
defer logPanic(ctx)
|
||||
defer wg.Done()
|
||||
|
||||
addf(&r.Autodiscover.Instructions, "Ensure DNS records like the following exist:\n\n\t_autodiscover._tcp.%s IN SRV 0 1 443 autoconfig.%s\n\tautoconfig.%s IN CNAME %s\n\nNote: the trailing dots are relevant, it makes the host names absolute instead of relative to the domain name.", d.ASCII+".", d.ASCII+".", d.ASCII+".", mox.Conf.Static.HostnameDomain.ASCII+".")
|
||||
addf(&r.Autodiscover.Instructions, "Ensure DNS records like the following exist:\n\n\t_autodiscover._tcp.%s IN SRV 0 1 443 autoconfig.%s\n\tautoconfig.%s IN CNAME %s\n\nNote: the trailing dots are relevant, it makes the host names absolute instead of relative to the domain name.", domain.ASCII+".", domain.ASCII+".", domain.ASCII+".", mox.Conf.Static.HostnameDomain.ASCII+".")
|
||||
|
||||
_, srvs, err := resolver.LookupSRV(ctx, "autodiscover", "tcp", d.ASCII+".")
|
||||
_, srvs, err := resolver.LookupSRV(ctx, "autodiscover", "tcp", domain.ASCII+".")
|
||||
if err != nil {
|
||||
addf(&r.Autodiscover.Errors, "Looking up SRV record %q: %s", "autodiscover", err)
|
||||
return
|
||||
|
|
|
@ -722,6 +722,15 @@ const domainDNSCheck = async (d) => {
|
|||
]
|
||||
}
|
||||
|
||||
const detailsIPRev = !checks.IPRev.IPNames || !Object.entries(checks.IPRev.IPNames).length ? [] : [
|
||||
dom.div('Hostname: ' + domainString(checks.IPRev.Hostname)),
|
||||
dom.table(
|
||||
dom.tr(dom.th('IP'), dom.th('Addresses')),
|
||||
Object.entries(checks.IPRev.IPNames).sort().map(t =>
|
||||
dom.tr(dom.td(t[0]), dom.td(t[1])),
|
||||
)
|
||||
),
|
||||
]
|
||||
const detailsMX = empty(checks.MX.Records) ? [] : [
|
||||
dom.table(
|
||||
dom.tr(dom.th('Preference'), dom.th('Host'), dom.th('IPs')),
|
||||
|
@ -787,6 +796,7 @@ const domainDNSCheck = async (d) => {
|
|||
'Check DNS',
|
||||
),
|
||||
dom.h1('DNS records and domain configuration check'),
|
||||
resultSection('IPRev', checks.IPRev, detailsIPRev),
|
||||
resultSection('MX', checks.MX, detailsMX),
|
||||
resultSection('TLS', checks.TLS, detailsTLS),
|
||||
resultSection('SPF', checks.SPF, detailsSPF),
|
||||
|
|
|
@ -592,6 +592,13 @@
|
|||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "IPRev",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"IPRevCheckResult"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "MX",
|
||||
"Docs": "",
|
||||
|
@ -664,6 +671,72 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "IPRevCheckResult",
|
||||
"Docs": "",
|
||||
"Fields": [
|
||||
{
|
||||
"Name": "Hostname",
|
||||
"Docs": "This hostname, IPs must resolve back to this.",
|
||||
"Typewords": [
|
||||
"Domain"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "IPNames",
|
||||
"Docs": "IP to names.",
|
||||
"Typewords": [
|
||||
"{}",
|
||||
"[]",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Errors",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"[]",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Warnings",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"[]",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Instructions",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"[]",
|
||||
"string"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Domain",
|
||||
"Docs": "Domain is a domain name, with one or more labels, with at least an ASCII\nrepresentation, and for IDNA non-ASCII domains a unicode representation.\nThe ASCII string must be used for DNS lookups.",
|
||||
"Fields": [
|
||||
{
|
||||
"Name": "ASCII",
|
||||
"Docs": "A non-unicode domain, e.g. with A-labels (xn--...) or NR-LDH (non-reserved letters/digits/hyphens) labels. Always in lower case.",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Unicode",
|
||||
"Docs": "Name as U-labels. Empty if this is an ASCII-only domain.",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "MXCheckResult",
|
||||
"Docs": "",
|
||||
|
@ -1498,26 +1571,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Domain",
|
||||
"Docs": "Domain is a domain name, with one or more labels, with at least an ASCII\nrepresentation, and for IDNA non-ASCII domains a unicode representation.\nThe ASCII string must be used for DNS lookups.",
|
||||
"Fields": [
|
||||
{
|
||||
"Name": "ASCII",
|
||||
"Docs": "A non-unicode domain, e.g. with A-labels (xn--...) or NR-LDH (non-reserved letters/digits/hyphens) labels. Always in lower case.",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Unicode",
|
||||
"Docs": "Name as U-labels. Empty if this is an ASCII-only domain.",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "SRVConfCheckResult",
|
||||
"Docs": "",
|
||||
|
|
|
@ -19,7 +19,6 @@ import (
|
|||
"github.com/mjl-/mox/message"
|
||||
"github.com/mjl-/mox/metrics"
|
||||
"github.com/mjl-/mox/mlog"
|
||||
"github.com/mjl-/mox/mox-"
|
||||
"github.com/mjl-/mox/store"
|
||||
)
|
||||
|
||||
|
@ -86,7 +85,7 @@ func xcmdImport(mbox, train, markRead bool, args []string, c *cmd) {
|
|||
c.Usage()
|
||||
}
|
||||
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
|
||||
account := args[0]
|
||||
mailbox := args[1]
|
||||
|
|
68
main.go
68
main.go
|
@ -346,6 +346,19 @@ func usage(l []cmd, unlisted bool) {
|
|||
os.Exit(2)
|
||||
}
|
||||
|
||||
var loglevel string
|
||||
|
||||
// subcommands that are not "serve" should use this function to load the config, it
|
||||
// restores any loglevel specified on the command-line, instead of using the
|
||||
// loglevels from the config file.
|
||||
func mustLoadConfig() {
|
||||
mox.MustLoadConfig()
|
||||
if level, ok := mlog.Levels[loglevel]; ok && loglevel != "" {
|
||||
mox.Conf.Log[""] = level
|
||||
mlog.SetConfig(mox.Conf.Log)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
|
||||
|
@ -360,9 +373,8 @@ func main() {
|
|||
return
|
||||
}
|
||||
|
||||
var loglevel string
|
||||
flag.StringVar(&mox.ConfigStaticPath, "config", envString("MOXCONF", "mox.conf"), "configuration file, other config files are looked up in the same directory, defaults to $MOXCONF with a fallback to mox.conf")
|
||||
flag.StringVar(&loglevel, "loglevel", "", "if non-empty, this debug level is set early in startup")
|
||||
flag.StringVar(&loglevel, "loglevel", "", "if non-empty, this log level is set early in startup")
|
||||
|
||||
flag.Usage = func() { usage(cmds, false) }
|
||||
flag.Parse()
|
||||
|
@ -375,6 +387,7 @@ func main() {
|
|||
if level, ok := mlog.Levels[loglevel]; ok && loglevel != "" {
|
||||
mox.Conf.Log[""] = level
|
||||
mlog.SetConfig(mox.Conf.Log)
|
||||
// note: SetConfig may be called again when subcommands loads config.
|
||||
}
|
||||
|
||||
var partial []cmd
|
||||
|
@ -438,7 +451,7 @@ configured over otherwise secured connections, like a VPN.
|
|||
c.Usage()
|
||||
}
|
||||
d := xparseDomain(args[0], "domain")
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
printClientConfig(d)
|
||||
}
|
||||
|
||||
|
@ -530,7 +543,7 @@ must be set if and only if account does not yet exist.
|
|||
}
|
||||
|
||||
d := xparseDomain(args[0], "domain")
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
|
||||
if len(args) == 2 {
|
||||
args = append(args, "")
|
||||
|
@ -557,7 +570,7 @@ rejected.
|
|||
}
|
||||
|
||||
d := xparseDomain(args[0], "domain")
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
ctl := xctl()
|
||||
ctl.xwrite("domainrm")
|
||||
ctl.xwrite(args[0])
|
||||
|
@ -577,7 +590,7 @@ explicitly, see the setaccountpassword command.
|
|||
c.Usage()
|
||||
}
|
||||
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
ctl := xctl()
|
||||
ctl.xwrite("accountadd")
|
||||
for _, s := range args {
|
||||
|
@ -599,7 +612,7 @@ these addresses will be rejected.
|
|||
c.Usage()
|
||||
}
|
||||
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
ctl := xctl()
|
||||
ctl.xwrite("accountrm")
|
||||
ctl.xwrite(args[0])
|
||||
|
@ -615,7 +628,7 @@ func cmdConfigAddressAdd(c *cmd) {
|
|||
c.Usage()
|
||||
}
|
||||
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
ctl := xctl()
|
||||
ctl.xwrite("addressadd")
|
||||
for _, s := range args {
|
||||
|
@ -636,7 +649,7 @@ Incoming email for this address will be rejected.
|
|||
c.Usage()
|
||||
}
|
||||
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
ctl := xctl()
|
||||
ctl.xwrite("addressrm")
|
||||
ctl.xwrite(args[0])
|
||||
|
@ -658,7 +671,7 @@ configured.
|
|||
}
|
||||
|
||||
d := xparseDomain(args[0], "domain")
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
domConf, ok := mox.Conf.Domain(d)
|
||||
if !ok {
|
||||
log.Fatalf("unknown domain")
|
||||
|
@ -677,7 +690,7 @@ func cmdConfigDNSCheck(c *cmd) {
|
|||
}
|
||||
|
||||
d := xparseDomain(args[0], "domain")
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
_, ok := mox.Conf.Domain(d)
|
||||
if !ok {
|
||||
log.Fatalf("unknown domain")
|
||||
|
@ -710,6 +723,7 @@ func cmdConfigDNSCheck(c *cmd) {
|
|||
}
|
||||
|
||||
result := http.Admin{}.CheckDomain(context.Background(), args[0])
|
||||
printResult("IPRev", result.IPRev.Result)
|
||||
printResult("MX", result.MX.Result)
|
||||
printResult("TLS", result.TLS.Result)
|
||||
printResult("SPF", result.SPF.Result)
|
||||
|
@ -737,7 +751,7 @@ Valid labels: error, info, debug, trace.
|
|||
if len(args) > 2 {
|
||||
c.Usage()
|
||||
}
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
|
||||
if len(args) == 0 {
|
||||
ctl := xctl()
|
||||
|
@ -770,7 +784,7 @@ new mail deliveries.
|
|||
if len(c.Parse()) != 0 {
|
||||
c.Usage()
|
||||
}
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
|
||||
ctl := xctl()
|
||||
ctl.xwrite("stop")
|
||||
|
@ -800,7 +814,7 @@ Like stop, existing connections get a 3 second period for graceful shutdown.
|
|||
if len(c.Parse()) != 0 {
|
||||
c.Usage()
|
||||
}
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
|
||||
ctl := xctl()
|
||||
ctl.xwrite("restart")
|
||||
|
@ -830,7 +844,7 @@ The password is read from stdin. Its bcrypt hash is stored in a file named
|
|||
if len(c.Parse()) != 0 {
|
||||
c.Usage()
|
||||
}
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
|
||||
path := mox.ConfigDirPath(mox.Conf.Static.AdminPasswordFile)
|
||||
if path == "" {
|
||||
|
@ -871,7 +885,7 @@ Any email address configured for the account can be used.
|
|||
if len(args) != 1 {
|
||||
c.Usage()
|
||||
}
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
|
||||
acc, _, err := store.OpenEmail(args[0])
|
||||
xcheckf(err, "open account")
|
||||
|
@ -892,7 +906,7 @@ func cmdDeliver(c *cmd) {
|
|||
if len(args) != 1 {
|
||||
c.Usage()
|
||||
}
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
|
||||
ctl := xctl()
|
||||
ctl.xwrite("deliver")
|
||||
|
@ -916,7 +930,7 @@ error.
|
|||
if len(c.Parse()) != 0 {
|
||||
c.Usage()
|
||||
}
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
|
||||
ctl := xctl()
|
||||
ctl.xwrite("queue")
|
||||
|
@ -943,7 +957,7 @@ without rescheduling.
|
|||
if len(c.Parse()) != 0 {
|
||||
c.Usage()
|
||||
}
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
|
||||
ctl := xctl()
|
||||
ctl.xwrite("queuekick")
|
||||
|
@ -974,7 +988,7 @@ the message, use "queue dump" before removing.
|
|||
if len(c.Parse()) != 0 {
|
||||
c.Usage()
|
||||
}
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
|
||||
ctl := xctl()
|
||||
ctl.xwrite("queuedrop")
|
||||
|
@ -1000,7 +1014,7 @@ The message is printed to stdout and is in standard internet mail format.
|
|||
if len(args) != 1 {
|
||||
c.Usage()
|
||||
}
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
|
||||
ctl := xctl()
|
||||
ctl.xwrite("queuedump")
|
||||
|
@ -1346,7 +1360,7 @@ func cmdDMARCDBAddReport(c *cmd) {
|
|||
c.Usage()
|
||||
}
|
||||
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
|
||||
fromdomain := xparseDomain(args[0], "domain")
|
||||
fmt.Fprintln(os.Stderr, "reading report message from stdin")
|
||||
|
@ -1506,7 +1520,7 @@ func cmdTLSRPTDBAddReport(c *cmd) {
|
|||
c.Usage()
|
||||
}
|
||||
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
|
||||
// First read message, to get the From-header. Then parse it as TLSRPT.
|
||||
fmt.Fprintln(os.Stderr, "reading report message from stdin")
|
||||
|
@ -1582,7 +1596,7 @@ printed.
|
|||
if len(c.Parse()) != 0 {
|
||||
c.Usage()
|
||||
}
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
|
||||
current, lastknown, _, err := mox.LastKnown()
|
||||
if err != nil {
|
||||
|
@ -1616,7 +1630,7 @@ be decrypted to a cid by admin of a mox instance only.
|
|||
c.Usage()
|
||||
}
|
||||
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
recvidpath := mox.DataDirPath("receivedid.key")
|
||||
recvidbuf, err := os.ReadFile(recvidpath)
|
||||
xcheckf(err, "reading %s", recvidpath)
|
||||
|
@ -1650,7 +1664,7 @@ func cmdEnsureParsed(c *cmd) {
|
|||
c.Usage()
|
||||
}
|
||||
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
a, err := store.OpenAccount(args[0])
|
||||
xcheckf(err, "open account")
|
||||
defer a.Close()
|
||||
|
@ -1695,7 +1709,7 @@ func cmdBumpUIDValidity(c *cmd) {
|
|||
c.Usage()
|
||||
}
|
||||
|
||||
mox.MustLoadConfig()
|
||||
mustLoadConfig()
|
||||
a, err := store.OpenAccount(args[0])
|
||||
xcheckf(err, "open account")
|
||||
defer a.Close()
|
||||
|
|
|
@ -399,7 +399,11 @@ func DomainRecords(domConf config.Domain, domain dns.Domain) ([]string, error) {
|
|||
"$TTL 300",
|
||||
"",
|
||||
|
||||
"; Deliver email to this host.",
|
||||
"; For the machine, only needs to be created for the first domain added.",
|
||||
fmt.Sprintf(`%-*s IN TXT "v=spf1 a -all"`, 20+len(d), h+"."), // ../rfc/7208:2263 ../rfc/7208:2287
|
||||
"",
|
||||
|
||||
"; Deliver email for the domain to this host.",
|
||||
fmt.Sprintf("%s. MX 10 %s.", d, h),
|
||||
"",
|
||||
|
||||
|
@ -447,8 +451,6 @@ func DomainRecords(domConf config.Domain, domain dns.Domain) ([]string, error) {
|
|||
"; ~all means softfail for anything else, which is done instead of -all to prevent older",
|
||||
"; mail servers from rejecting the message because they never get to looking for a dkim/dmarc pass.",
|
||||
fmt.Sprintf(`%s. IN TXT "v=spf1 mx ~all"`, d),
|
||||
"; The next record may already exist if you have more domains configured.",
|
||||
fmt.Sprintf(`%-*s IN TXT "v=spf1 a -all"`, 20+len(d), h+"."), // ../rfc/7208:2263 ../rfc/7208:2287
|
||||
"",
|
||||
|
||||
"; Emails that fail the DMARC check (without DKIM and without SPF) should be rejected, and request reports.",
|
||||
|
|
|
@ -122,11 +122,64 @@ permissions, and if you run it on Linux it prints a systemd service file.
|
|||
fmt.Printf("Looking up hostname %q...", hostname)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
_, err = dns.StrictResolver{}.LookupIPAddr(ctx, hostname.ASCII+".")
|
||||
resolver := dns.StrictResolver{}
|
||||
ips, err := resolver.LookupIPAddr(ctx, hostname.ASCII+".")
|
||||
if err != nil {
|
||||
fmt.Printf("\n\nWARNING: Quickstart assumes hostname %q and generates a config for that host,\nbut could not retrieve that name from DNS:\n\n\t%s\n\n", hostname, err)
|
||||
} else {
|
||||
fmt.Printf(" OK\n")
|
||||
|
||||
var l []string
|
||||
type result struct {
|
||||
IP string
|
||||
Addrs []string
|
||||
Err error
|
||||
}
|
||||
results := make(chan result)
|
||||
for _, ip := range ips {
|
||||
s := ip.String()
|
||||
l = append(l, s)
|
||||
go func() {
|
||||
addrs, err := resolver.LookupAddr(ctx, s)
|
||||
results <- result{s, addrs, err}
|
||||
}()
|
||||
}
|
||||
fmt.Printf("Looking up reverse names for IP(s) %s...", strings.Join(l, ", "))
|
||||
var warned bool
|
||||
warnf := func(format string, args ...any) {
|
||||
fmt.Printf("\nWARNING: %s", fmt.Sprintf(format, args...))
|
||||
warned = true
|
||||
}
|
||||
for i := 0; i < len(ips); i++ {
|
||||
r := <-results
|
||||
if r.Err != nil {
|
||||
warnf("looking up reverse name for %s: %v", r.IP, r.Err)
|
||||
continue
|
||||
}
|
||||
if len(r.Addrs) != 1 {
|
||||
warnf("expected exactly 1 name for %s, got %d (%v)", r.IP, len(r.Addrs), r.Addrs)
|
||||
}
|
||||
var match bool
|
||||
for i, a := range r.Addrs {
|
||||
a = strings.TrimRight(a, ".")
|
||||
r.Addrs[i] = a // For potential error message below.
|
||||
d, err := dns.ParseDomain(a)
|
||||
if err != nil {
|
||||
warnf("parsing reverse name %q for %s: %v", a, r.IP, err)
|
||||
}
|
||||
if d == hostname {
|
||||
match = true
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
warnf("reverse name(s) %s for ip %s do not match hostname %s, which will cause other mail servers to reject incoming messages from this IP", strings.Join(r.Addrs, ","), r.IP, hostname)
|
||||
}
|
||||
}
|
||||
if warned {
|
||||
fmt.Printf("\n\n")
|
||||
} else {
|
||||
fmt.Printf(" OK\n")
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
|
||||
|
|
Loading…
Reference in a new issue