1package main
2
3import (
4 "bytes"
5 "context"
6 "crypto"
7 "crypto/ecdsa"
8 "crypto/elliptic"
9 cryptorand "crypto/rand"
10 "crypto/rsa"
11 "crypto/x509"
12 "encoding/pem"
13 "errors"
14 "fmt"
15 "io"
16 "log"
17 "net"
18 "net/url"
19 "os"
20 "path/filepath"
21 "runtime"
22 "sort"
23 "strings"
24 "time"
25
26 _ "embed"
27
28 "golang.org/x/crypto/bcrypt"
29
30 "github.com/mjl-/sconf"
31
32 "github.com/mjl-/mox/admin"
33 "github.com/mjl-/mox/config"
34 "github.com/mjl-/mox/dns"
35 "github.com/mjl-/mox/dnsbl"
36 "github.com/mjl-/mox/mlog"
37 "github.com/mjl-/mox/mox-"
38 "github.com/mjl-/mox/publicsuffix"
39 "github.com/mjl-/mox/rdap"
40 "github.com/mjl-/mox/smtp"
41 "github.com/mjl-/mox/store"
42)
43
44//go:embed mox.service
45var moxService string
46
47func cmdQuickstart(c *cmd) {
48 c.params = "[-skipdial] [-existing-webserver] [-hostname host] user@domain [user | uid]"
49 c.help = `Quickstart generates configuration files and prints instructions to quickly set up a mox instance.
50
51Quickstart writes configuration files, prints initial admin and account
52passwords, DNS records you should create. If you run it on Linux it writes a
53systemd service file and prints commands to enable and start mox as service.
54
55All output is written to quickstart.log for later reference.
56
57The user or uid is optional, defaults to "mox", and is the user or uid/gid mox
58will run as after initialization.
59
60Quickstart assumes mox will run on the machine you run quickstart on and uses
61its host name and public IPs. On many systems the hostname is not a fully
62qualified domain name, but only the first dns "label", e.g. "mail" in case of
63"mail.example.org". If so, quickstart does a reverse DNS lookup to find the
64hostname, and as fallback uses the label plus the domain of the email address
65you specified. Use flag -hostname to explicitly specify the hostname mox will
66run on.
67
68Mox is by far easiest to operate if you let it listen on port 443 (HTTPS) and
6980 (HTTP). TLS will be fully automatic with ACME with Let's Encrypt.
70
71You can run mox along with an existing webserver, but because of MTA-STS and
72autoconfig, you'll need to forward HTTPS traffic for two domains to mox. Run
73"mox quickstart -existing-webserver ..." to generate configuration files and
74instructions for configuring mox along with an existing webserver.
75
76But please first consider configuring mox on port 443. It can itself serve
77domains with HTTP/HTTPS, including with automatic TLS with ACME, is easily
78configured through both configuration files and admin web interface, and can act
79as a reverse proxy (and static file server for that matter), so you can forward
80traffic to your existing backend applications. Look for "WebHandlers:" in the
81output of "mox config describe-domains" and see the output of
82"mox config example webhandlers".
83`
84 var existingWebserver bool
85 var hostname string
86 var skipDial bool
87 c.flag.BoolVar(&existingWebserver, "existing-webserver", false, "use if a webserver is already running, so mox won't listen on port 80 and 443; you'll have to provide tls certificates/keys, and configure the existing webserver as reverse proxy, forwarding requests to mox.")
88 c.flag.StringVar(&hostname, "hostname", "", "hostname mox will run on, by default the hostname of the machine quickstart runs on; if specified, the IPs for the hostname are configured for the public listener")
89 c.flag.BoolVar(&skipDial, "skipdial", false, "skip check for outgoing smtp (port 25) connectivity or for domain age with rdap")
90 args := c.Parse()
91 if len(args) != 1 && len(args) != 2 {
92 c.Usage()
93 }
94
95 // Write all output to quickstart.log.
96 logfile, err := os.Create("quickstart.log")
97 xcheckf(err, "creating quickstart.log")
98
99 origStdout := os.Stdout
100 origStderr := os.Stderr
101 piper, pipew, err := os.Pipe()
102 xcheckf(err, "creating pipe for logging to logfile")
103 pipec := make(chan struct{})
104 go func() {
105 io.Copy(io.MultiWriter(origStdout, logfile), piper)
106 close(pipec)
107 }()
108 // A single pipe, so writes to stdout and stderr don't get interleaved.
109 os.Stdout = pipew
110 os.Stderr = pipew
111 logClose := func() {
112 pipew.Close()
113 <-pipec
114 os.Stdout = origStdout
115 os.Stderr = origStderr
116 err := logfile.Close()
117 xcheckf(err, "closing quickstart.log")
118 }
119 defer logClose()
120 log.SetOutput(os.Stdout)
121 fmt.Printf("(output is also written to quickstart.log)\n\n")
122 defer fmt.Printf("\n(output is also written to quickstart.log)\n")
123
124 // We take care to cleanup created files when we error out.
125 // We don't want to get a new user into trouble with half of the files
126 // after encountering an error.
127
128 // We use fatalf instead of log.Fatal* to cleanup files.
129 var cleanupPaths []string
130 fatalf := func(format string, args ...any) {
131 // We remove in reverse order because dirs would have been created first and must
132 // be removed last, after their files have been removed.
133 for i := len(cleanupPaths) - 1; i >= 0; i-- {
134 p := cleanupPaths[i]
135 if err := os.Remove(p); err != nil {
136 log.Printf("cleaning up %q: %s", p, err)
137 }
138 }
139
140 log.Printf(format, args...)
141 logClose()
142 os.Exit(1)
143 }
144
145 xwritefile := func(path string, data []byte, perm os.FileMode) {
146 os.MkdirAll(filepath.Dir(path), 0770)
147 f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm)
148 if err != nil {
149 fatalf("creating file %q: %s", path, err)
150 }
151 cleanupPaths = append(cleanupPaths, path)
152 _, err = f.Write(data)
153 if err == nil {
154 err = f.Close()
155 }
156 if err != nil {
157 fatalf("writing file %q: %s", path, err)
158 }
159 }
160
161 addr, err := smtp.ParseAddress(args[0])
162 if err != nil {
163 fatalf("parsing email address: %s", err)
164 }
165 accountName := addr.Localpart.String()
166 domain := addr.Domain
167
168 for _, c := range accountName {
169 if c > 0x7f {
170 fmt.Printf(`NOTE: Username %q is not ASCII-only. It is recommended you also configure an
171ASCII-only alias. Both for delivery of email from other systems, and for
172logging in with IMAP.
173
174`, accountName)
175 break
176 }
177 }
178
179 resolver := dns.StrictResolver{}
180 // We don't want to spend too much total time on the DNS lookups. Because DNS may
181 // not work during quickstart, and we don't want to loop doing requests and having
182 // to wait for a timeout each time.
183 resolveCtx, resolveCancel := context.WithTimeout(context.Background(), 10*time.Second)
184 defer resolveCancel()
185
186 // Some DNSSEC-verifying resolvers return unauthentic data for ".", so we check "com".
187 fmt.Printf("Checking if DNS resolvers are DNSSEC-verifying...")
188 _, resolverDNSSECResult, err := resolver.LookupNS(resolveCtx, "com.")
189 if err != nil {
190 fmt.Println("")
191 fatalf("checking dnssec support in resolver: %v", err)
192 } else if !resolverDNSSECResult.Authentic {
193 fmt.Printf(`
194
195WARNING: It looks like the DNS resolvers configured on your system do not
196verify DNSSEC, or aren't trusted (by having loopback IPs or through "options
197trust-ad" in /etc/resolv.conf). Without DNSSEC, outbound delivery with SMTP
198used unprotected MX records, and SMTP STARTTLS connections cannot verify the TLS
199certificate with DANE (based on a public key in DNS), and will fall back to
200either MTA-STS for verification, or use "opportunistic TLS" with no certificate
201verification.
202
203Recommended action: Install unbound, a DNSSEC-verifying recursive DNS resolver,
204ensure it has DNSSEC root keys (see unbound-anchor), and enable support for
205"extended dns errors" (EDE, available since unbound v1.16.0, see below; not
206required, but it gives helpful error messages about DNSSEC failures instead of
207generic DNS SERVFAIL errors). Test with "dig com. ns" and look for "ad"
208(authentic data) in response "flags".
209
210cat <<EOF >/etc/unbound/unbound.conf.d/ede.conf
211server:
212 ede: yes
213 val-log-level: 2
214EOF
215
216Troubleshooting hints:
217- Ensure /etc/resolv.conf has "nameserver 127.0.0.1". If the IP is 127.0.0.53,
218 DNS resolving is done by systemd-resolved. Make sure "resolvconf" isn't
219 overwriting /etc/resolv.conf (Debian has a package "openresolv" that makes this
220 easier). "dig" also shows to which IP the DNS request was sent.
221- Ensure unbound has DNSSEC root keys available. See unbound config option
222 "auto-trust-anchor-file" and the unbound-anchor command. Ensure the file exists.
223- Run "./mox dns lookup ns com." to simulate the DNSSEC check done by mox. The
224 output should say "with dnssec".
225- The "delv" command can check whether a domain is DNSSEC-signed, but it does
226 its own DNSSEC verification instead of relying on the resolver, so you cannot
227 use it to check whether unbound is verifying DNSSEC correctly.
228- Increase logging in unbound, see options "verbosity" and "log-queries".
229
230`)
231 } else {
232 fmt.Println(" OK")
233 }
234
235 // We are going to find the (public) IPs to listen on and possibly the host name.
236
237 // Start with reasonable defaults. We'll replace them specific IPs, if we can find them.
238 privateListenerIPs := []string{"127.0.0.1", "::1"}
239 publicListenerIPs := []string{"0.0.0.0", "::"}
240 var publicNATIPs []string // Actual public IP, but when it is NATed and machine doesn't have direct access.
241 defaultPublicListenerIPs := true
242
243 // If we find IPs based on network interfaces, {public,private}ListenerIPs are set
244 // based on these values.
245 var loopbackIPs, privateIPs, publicIPs []string
246
247 // Gather IP addresses for public and private listeners.
248 // We look at each network interface. If an interface has a private address, we
249 // conservatively assume all addresses on that interface are private.
250 ifaces, err := net.Interfaces()
251 if err != nil {
252 fatalf("listing network interfaces: %s", err)
253 }
254 parseAddrIP := func(s string) net.IP {
255 if strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]") {
256 s = s[1 : len(s)-1]
257 }
258 ip, _, _ := net.ParseCIDR(s)
259 return ip
260 }
261 for _, iface := range ifaces {
262 if iface.Flags&net.FlagUp == 0 {
263 continue
264 }
265 addrs, err := iface.Addrs()
266 if err != nil {
267 fatalf("listing address for network interface: %s", err)
268 }
269 if len(addrs) == 0 {
270 continue
271 }
272
273 // todo: should we detect temporary/ephemeral ipv6 addresses and not add them?
274 var nonpublic bool
275 for _, addr := range addrs {
276 ip := parseAddrIP(addr.String())
277 if ip.IsInterfaceLocalMulticast() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() || ip.IsMulticast() {
278 continue
279 }
280 if ip.IsLoopback() || ip.IsPrivate() {
281 nonpublic = true
282 break
283 }
284 }
285
286 for _, addr := range addrs {
287 ip := parseAddrIP(addr.String())
288 if ip == nil {
289 continue
290 }
291 if ip.IsInterfaceLocalMulticast() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() || ip.IsMulticast() {
292 continue
293 }
294 if nonpublic {
295 if ip.IsLoopback() {
296 loopbackIPs = append(loopbackIPs, ip.String())
297 } else {
298 privateIPs = append(privateIPs, ip.String())
299 }
300 } else {
301 publicIPs = append(publicIPs, ip.String())
302 }
303 }
304 }
305
306 var dnshostname dns.Domain
307 if hostname == "" {
308 hostnameStr, err := os.Hostname()
309 if err != nil {
310 fatalf("hostname: %s", err)
311 }
312 if strings.Contains(hostnameStr, ".") {
313 dnshostname, err = dns.ParseDomain(hostnameStr)
314 if err != nil {
315 fatalf("parsing hostname: %v", err)
316 }
317 } else {
318 // It seems Linux machines don't have a single FQDN configured. E.g. /etc/hostname
319 // is just the name without domain. We'll look up the names for all IPs, and hope
320 // to find a single FQDN name (with at least 1 dot).
321 names := map[string]struct{}{}
322 if len(publicIPs) > 0 {
323 fmt.Printf("Trying to find hostname by reverse lookup of public IPs %s...", strings.Join(publicIPs, ", "))
324 }
325 var warned bool
326 warnf := func(format string, args ...any) {
327 warned = true
328 fmt.Printf("\n%s", fmt.Sprintf(format, args...))
329 }
330 for _, ip := range publicIPs {
331 revctx, revcancel := context.WithTimeout(resolveCtx, 5*time.Second)
332 defer revcancel()
333 l, _, err := resolver.LookupAddr(revctx, ip)
334 if err != nil {
335 warnf("WARNING: looking up reverse name(s) for %s: %v", ip, err)
336 }
337 for _, name := range l {
338 if strings.Contains(name, ".") {
339 names[name] = struct{}{}
340 }
341 }
342 }
343 var nameList []string
344 for k := range names {
345 nameList = append(nameList, strings.TrimRight(k, "."))
346 }
347 sort.Slice(nameList, func(i, j int) bool {
348 return nameList[i] < nameList[j]
349 })
350 if len(nameList) == 0 {
351 dnshostname, err = dns.ParseDomain(hostnameStr + "." + domain.Name())
352 if err != nil {
353 fmt.Println()
354 fatalf("parsing hostname: %v", err)
355 }
356 warnf(`WARNING: cannot determine hostname because the system name is not an FQDN and
357no public IPs resolving to an FQDN were found. Quickstart guessed the host name
358below. If it is not correct, please remove the generated config files and run
359quickstart again with the -hostname flag.
360
361 %s
362`, dnshostname)
363 } else {
364 if len(nameList) > 1 {
365 warnf(`WARNING: multiple hostnames found for the public IPs, using the first of: %s
366If this is not correct, remove the generated config files and run quickstart
367again with the -hostname flag.
368`, strings.Join(nameList, ", "))
369 }
370 dnshostname, err = dns.ParseDomain(nameList[0])
371 if err != nil {
372 fmt.Println()
373 fatalf("parsing hostname %s: %v", nameList[0], err)
374 }
375 }
376 if warned {
377 fmt.Printf("\n\n")
378 } else {
379 fmt.Printf(" found %s\n", dnshostname)
380 }
381 }
382 } else {
383 // Host name was explicitly configured on command-line. We'll try to use its public
384 // IPs below.
385 var err error
386 dnshostname, err = dns.ParseDomain(hostname)
387 if err != nil {
388 fatalf("parsing hostname: %v", err)
389 }
390 }
391
392 fmt.Printf("Looking up IPs for hostname %s...", dnshostname)
393 ipctx, ipcancel := context.WithTimeout(resolveCtx, 5*time.Second)
394 defer ipcancel()
395 ips, domainDNSSECResult, err := resolver.LookupIPAddr(ipctx, dnshostname.ASCII+".")
396 ipcancel()
397 var xips []net.IPAddr
398 var hostIPs []string
399 var dnswarned bool
400 hostPrivate := len(ips) > 0
401 for _, ip := range ips {
402 if !ip.IP.IsPrivate() {
403 hostPrivate = false
404 }
405 // During linux install, you may get an alias for you full hostname in /etc/hosts
406 // resolving to 127.0.1.1, which would result in a false positive about the
407 // hostname having a record. Filter it out. It is a bit surprising that hosts don't
408 // otherwise know their FQDN.
409 if ip.IP.IsLoopback() {
410 dnswarned = true
411 fmt.Printf("\n\nWARNING: Your hostname is resolving to a loopback IP address %s. This likely breaks email delivery to local accounts. /etc/hosts likely contains a line like %q. Either replace it with your actual IP(s), or remove the line.\n", ip.IP, fmt.Sprintf("%s %s", ip.IP, dnshostname.ASCII))
412 continue
413 }
414 xips = append(xips, ip)
415 hostIPs = append(hostIPs, ip.String())
416 }
417 if err == nil && len(xips) == 0 {
418 // todo: possibly check this by trying to resolve without using /etc/hosts?
419 err = errors.New("hostname not in dns, probably only in /etc/hosts")
420 }
421 ips = xips
422
423 // We may have found private and public IPs on the machine, and IPs for the host
424 // name we think we should use. They may not match with each other. E.g. the public
425 // IPs on interfaces could be different from the IPs for the host. We don't try to
426 // detect all possible configs, but just generate what makes sense given whether we
427 // found public/private/hostname IPs. If the user is doing sensible things, it
428 // should be correct. But they should be checking the generated config file anyway.
429 // And we do log which host name we are using, and whether we detected a NAT setup.
430 // In the future, we may do an interactive setup that can guide the user better.
431
432 if !hostPrivate && len(publicIPs) == 0 && len(privateIPs) > 0 {
433 // We only have private IPs, assume we are behind a NAT and put the IPs of the host in NATIPs.
434 publicListenerIPs = privateIPs
435 publicNATIPs = hostIPs
436 defaultPublicListenerIPs = false
437 if len(loopbackIPs) > 0 {
438 privateListenerIPs = loopbackIPs
439 }
440 } else {
441 if len(hostIPs) > 0 {
442 publicListenerIPs = hostIPs
443 defaultPublicListenerIPs = false
444
445 // Only keep private IPs that are not in host-based publicListenerIPs. For
446 // internal-only setups, including integration tests.
447 m := map[string]bool{}
448 for _, ip := range hostIPs {
449 m[ip] = true
450 }
451 var npriv []string
452 for _, ip := range privateIPs {
453 if !m[ip] {
454 npriv = append(npriv, ip)
455 }
456 }
457 sort.Strings(npriv)
458 privateIPs = npriv
459 } else if len(publicIPs) > 0 {
460 publicListenerIPs = publicIPs
461 defaultPublicListenerIPs = false
462 hostIPs = publicIPs // For DNSBL check below.
463 }
464 if len(privateIPs) > 0 {
465 privateListenerIPs = append(privateIPs, loopbackIPs...)
466 } else if len(loopbackIPs) > 0 {
467 privateListenerIPs = loopbackIPs
468 }
469 }
470 if err != nil {
471 if !dnswarned {
472 fmt.Printf("\n")
473 }
474 dnswarned = true
475 fmt.Printf(`
476WARNING: Quickstart assumed the hostname of this machine is %s and generates a
477config for that host, but could not retrieve that name from DNS:
478
479 %s
480
481This likely means one of two things:
482
4831. You don't have any DNS records for this machine at all. You should add them
484 before continuing.
4852. The hostname mentioned is not the correct host name of this machine. You will
486 have to replace the hostname in the suggested DNS records and generated
487 config/mox.conf file. Make sure your hostname resolves to your public IPs, and
488 your public IPs resolve back (reverse) to your hostname.
489
490
491`, dnshostname, err)
492 } else if !domainDNSSECResult.Authentic {
493 if !dnswarned {
494 fmt.Printf("\n")
495 }
496 dnswarned = true
497 fmt.Printf(`
498NOTE: It looks like the DNS records of your domain (zone) are not DNSSEC-signed.
499Mail servers that send email to your domain, or receive email from your domain,
500cannot verify that the MX/SPF/DKIM/DMARC/MTA-STS records they receive are
501authentic. DANE, for authenticated delivery without relying on a pool of
502certificate authorities, requires DNSSEC, so will not be configured at this
503time.
504Recommended action: Continue now, but consider enabling DNSSEC for your domain
505later at your DNS operator, and adding DANE records for protecting incoming
506messages over SMTP.
507
508`)
509 }
510
511 if !dnswarned {
512 fmt.Printf(" OK\n")
513
514 var l []string
515 type result struct {
516 IP string
517 Addrs []string
518 Err error
519 }
520 results := make(chan result)
521 for _, ip := range ips {
522 s := ip.String()
523 l = append(l, s)
524 go func() {
525 revctx, revcancel := context.WithTimeout(resolveCtx, 5*time.Second)
526 defer revcancel()
527 addrs, _, err := resolver.LookupAddr(revctx, s)
528 results <- result{s, addrs, err}
529 }()
530 }
531 fmt.Printf("Looking up reverse names for IP(s) %s...", strings.Join(l, ", "))
532 var warned bool
533 warnf := func(format string, args ...any) {
534 fmt.Printf("\nWARNING: %s", fmt.Sprintf(format, args...))
535 warned = true
536 }
537 for i := 0; i < len(ips); i++ {
538 r := <-results
539 if r.Err != nil {
540 warnf("looking up reverse name for %s: %v", r.IP, r.Err)
541 continue
542 }
543 if len(r.Addrs) != 1 {
544 warnf("expected exactly 1 name for %s, got %d (%v)", r.IP, len(r.Addrs), r.Addrs)
545 }
546 var match bool
547 for i, a := range r.Addrs {
548 a = strings.TrimRight(a, ".")
549 r.Addrs[i] = a // For potential error message below.
550 d, err := dns.ParseDomain(a)
551 if err != nil {
552 warnf("parsing reverse name %q for %s: %v", a, r.IP, err)
553 }
554 if d == dnshostname {
555 match = true
556 }
557 }
558 if !match {
559 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, dnshostname)
560 }
561 }
562 if warned {
563 fmt.Printf("\n\n")
564 } else {
565 fmt.Printf(" OK\n")
566 }
567 }
568
569 if !skipDial {
570 // Check outgoing SMTP connectivity.
571 fmt.Printf("Checking if outgoing smtp connections can be made by connecting to gmail.com mx on port 25...")
572 mxctx, mxcancel := context.WithTimeout(context.Background(), 5*time.Second)
573 mx, _, err := resolver.LookupMX(mxctx, "gmail.com.")
574 mxcancel()
575 if err == nil && len(mx) == 0 {
576 err = errors.New("no mx records")
577 }
578 var ok bool
579 if err != nil {
580 fmt.Printf("\n\nERROR: looking up gmail.com mx record: %s\n", err)
581 } else {
582 dialctx, dialcancel := context.WithTimeout(context.Background(), 10*time.Second)
583 d := net.Dialer{}
584 addr := net.JoinHostPort(mx[0].Host, "25")
585 conn, err := d.DialContext(dialctx, "tcp", addr)
586 dialcancel()
587 if err != nil {
588 fmt.Printf("\n\nERROR: connecting to %s: %s\n", addr, err)
589 } else {
590 conn.Close()
591 fmt.Printf(" OK\n")
592 ok = true
593 }
594 }
595 if !ok {
596 fmt.Printf(`
597WARNING: Could not verify outgoing smtp connections can be made, outgoing
598delivery may not be working. Many providers block outgoing smtp connections by
599default, requiring an explicit request or a cooldown period before allowing
600outgoing smtp connections. To send through a smarthost, configure a "Transport"
601in mox.conf and use it in "Routes" in domains.conf. See
602"mox config example transport".
603
604`)
605 }
606
607 // Check if domain is recently registered.
608 rdapctx, rdapcancel := context.WithTimeout(context.Background(), 10*time.Second)
609 defer rdapcancel()
610 orgdom := publicsuffix.Lookup(rdapctx, c.log.Logger, domain)
611 fmt.Printf("\nChecking if domain %s was registered recently...", orgdom)
612 registration, err := rdap.LookupLastDomainRegistration(rdapctx, orgdom)
613 rdapcancel()
614 if err != nil {
615 fmt.Printf(" error: %s (continuing)\n\n", err)
616 } else {
617 age := time.Since(registration)
618 const day = 24 * time.Hour
619 const year = 365 * day
620 years := age / year
621 days := (age - years*year) / day
622 var s string
623 if years == 1 {
624 s = "1 year, "
625 } else if years > 0 {
626 s = fmt.Sprintf("%d years, ", years)
627 }
628 if days == 1 {
629 s += "1 day"
630 } else {
631 s += fmt.Sprintf("%d days", days)
632 }
633 fmt.Printf(" %s", s)
634 // 6 weeks is a guess, mail servers/service providers will have different policies.
635 if age < 6*7*day {
636 fmt.Printf(" (recent!)\nWARNING: Mail servers may treat messages coming from recently registered domains\n(in the order of weeks to months) with suspicion, with higher probability of\nmessages being classified as junk.\n\n")
637 } else {
638 fmt.Printf(" OK\n\n")
639 }
640 }
641 }
642
643 zones := []dns.Domain{
644 {ASCII: "sbl.spamhaus.org"},
645 {ASCII: "bl.spamcop.net"},
646 }
647 if len(hostIPs) > 0 {
648 fmt.Printf("Checking whether host name IPs are listed in popular DNS block lists...")
649 var listed bool
650 for _, zone := range zones {
651 for _, ip := range hostIPs {
652 dnsblctx, dnsblcancel := context.WithTimeout(context.Background(), 5*time.Second)
653 status, expl, err := dnsbl.Lookup(dnsblctx, c.log.Logger, resolver, zone, net.ParseIP(ip))
654 dnsblcancel()
655 if status == dnsbl.StatusPass {
656 continue
657 }
658 errstr := ""
659 if err != nil {
660 errstr = fmt.Sprintf(" (%s)", err)
661 }
662 fmt.Printf("\nWARNING: checking your public IP %s in DNS block list %s: %v %s%s", ip, zone.Name(), status, expl, errstr)
663 listed = true
664 }
665 }
666 if listed {
667 log.Printf(`
668Other mail servers are likely to reject email from IPs that are in a blocklist.
669If all your IPs are in block lists, you will encounter problems delivering
670email. Your IP may be in block lists only temporarily. To see if your IPs are
671listed in more DNS block lists, visit:
672
673`)
674 for _, ip := range hostIPs {
675 fmt.Printf("- https://multirbl.valli.org/lookup/%s.html\n", url.PathEscape(ip))
676 }
677 fmt.Printf("\n")
678 } else {
679 fmt.Printf(" OK\n")
680 }
681 }
682
683 if defaultPublicListenerIPs {
684 log.Printf(`
685WARNING: Could not find your public IP address(es). The "public" listener is
686configured to listen on 0.0.0.0 (IPv4) and :: (IPv6). If you don't change these
687to your actual public IP addresses, you will likely get "address in use" errors
688when starting mox because the "internal" listener binds to a specific IP
689address on the same port(s). If you are behind a NAT, instead configure the
690actual public IPs in the listener's "NATIPs" option.
691
692`)
693 }
694 if len(publicNATIPs) > 0 {
695 log.Printf(`
696NOTE: Quickstart used the IPs of the host name of the mail server, but only
697found private IPs on the machine. This indicates this machine is behind a NAT,
698so the host IPs were configured in the NATIPs field of the public listeners. If
699you are behind a NAT that does not preserve the remote IPs of connections, you
700will likely experience problems accepting email due to IP-based policies. For
701example, SPF is a mechanism that checks if an IP address is allowed to send
702email for a domain, and mox uses IP-based (non)junk classification, and IP-based
703rate-limiting both for accepting email and blocking bad actors (such as with too
704many authentication failures).
705
706`)
707 }
708
709 fmt.Printf("\n")
710
711 user := "mox"
712 if len(args) == 2 {
713 user = args[1]
714 }
715
716 dc := config.Dynamic{}
717 sc := config.Static{
718 DataDir: filepath.FromSlash("../data"),
719 User: user,
720 LogLevel: "debug", // Help new users, they'll bring it back to info when it all works.
721 Hostname: dnshostname.Name(),
722 AdminPasswordFile: "adminpasswd",
723 }
724
725 // todo: let user specify an alternative fallback address?
726 // Don't attempt to use a non-ascii localpart with Let's Encrypt, it won't work.
727 // Messages to postmaster will get to the account too.
728 var contactEmail string
729 if addr.Localpart.IsInternational() {
730 contactEmail = smtp.NewAddress("postmaster", addr.Domain).Pack(false)
731 } else {
732 contactEmail = addr.Pack(false)
733 }
734 if !existingWebserver {
735 sc.ACME = map[string]config.ACME{
736 "letsencrypt": {
737 DirectoryURL: "https://acme-v02.api.letsencrypt.org/directory",
738 ContactEmail: contactEmail,
739 IssuerDomainName: "letsencrypt.org",
740 },
741 }
742 }
743
744 dataDir := "data" // ../data is relative to config/
745 os.MkdirAll(dataDir, 0770)
746 adminpw := mox.GeneratePassword()
747 adminpwhash, err := bcrypt.GenerateFromPassword([]byte(adminpw), bcrypt.DefaultCost)
748 if err != nil {
749 fatalf("generating hash for generated admin password: %s", err)
750 }
751 xwritefile(filepath.Join("config", sc.AdminPasswordFile), adminpwhash, 0660)
752 fmt.Printf("Admin password: %s\n", adminpw)
753
754 public := config.Listener{
755 IPs: publicListenerIPs,
756 NATIPs: publicNATIPs,
757 }
758 public.SMTP.Enabled = true
759 public.Submissions.Enabled = true
760 public.IMAPS.Enabled = true
761
762 if existingWebserver {
763 hostbase := filepath.FromSlash("path/to/" + dnshostname.Name())
764 mtastsbase := filepath.FromSlash("path/to/mta-sts." + domain.Name())
765 autoconfigbase := filepath.FromSlash("path/to/autoconfig." + domain.Name())
766 mailbase := filepath.FromSlash("path/to/mail." + domain.Name())
767 public.TLS = &config.TLS{
768 KeyCerts: []config.KeyCert{
769 {CertFile: hostbase + "-chain.crt.pem", KeyFile: hostbase + ".key.pem"},
770 {CertFile: mtastsbase + "-chain.crt.pem", KeyFile: mtastsbase + ".key.pem"},
771 {CertFile: autoconfigbase + "-chain.crt.pem", KeyFile: autoconfigbase + ".key.pem"},
772 },
773 }
774 if mailbase != hostbase {
775 public.TLS.KeyCerts = append(public.TLS.KeyCerts, config.KeyCert{CertFile: mailbase + "-chain.crt.pem", KeyFile: mailbase + ".key.pem"})
776 }
777
778 fmt.Println(
779 `Placeholder paths to TLS certificates to be provided by the existing webserver
780have been placed in config/mox.conf and need to be edited.
781
782No private keys for the public listener have been generated for use with DANE.
783To configure DANE (which requires DNSSEC), set config field HostPrivateKeyFiles
784in the "public" Listener to both RSA 2048-bit and ECDSA P-256 private key files
785and check the admin page for the needed DNS records.`)
786
787 } else {
788 // todo: we may want to generate a second set of keys, make the user already add it to the DNS, but keep the private key offline. would require config option to specify a public key only, so the dane records can be generated.
789 hostRSAPrivateKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
790 if err != nil {
791 fatalf("generating rsa private key for host: %s", err)
792 }
793 hostECDSAPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
794 if err != nil {
795 fatalf("generating ecsa private key for host: %s", err)
796 }
797 now := time.Now()
798 timestamp := now.Format("20060102T150405")
799 hostRSAPrivateKeyFile := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", dnshostname.Name(), timestamp, "rsa2048"))
800 hostECDSAPrivateKeyFile := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", dnshostname.Name(), timestamp, "ecdsap256"))
801 xwritehostkeyfile := func(path string, key crypto.Signer) {
802 buf, err := x509.MarshalPKCS8PrivateKey(key)
803 if err != nil {
804 fatalf("marshaling host private key to pkcs8 for %s: %s", path, err)
805 }
806 var b bytes.Buffer
807 block := pem.Block{
808 Type: "PRIVATE KEY",
809 Bytes: buf,
810 }
811 err = pem.Encode(&b, &block)
812 if err != nil {
813 fatalf("pem-encoding host private key file for %s: %s", path, err)
814 }
815 xwritefile(path, b.Bytes(), 0600)
816 }
817 xwritehostkeyfile(filepath.Join("config", hostRSAPrivateKeyFile), hostRSAPrivateKey)
818 xwritehostkeyfile(filepath.Join("config", hostECDSAPrivateKeyFile), hostECDSAPrivateKey)
819
820 public.TLS = &config.TLS{
821 ACME: "letsencrypt",
822 HostPrivateKeyFiles: []string{
823 hostRSAPrivateKeyFile,
824 hostECDSAPrivateKeyFile,
825 },
826 HostPrivateRSA2048Keys: []crypto.Signer{hostRSAPrivateKey},
827 HostPrivateECDSAP256Keys: []crypto.Signer{hostECDSAPrivateKey},
828 }
829 public.AutoconfigHTTPS.Enabled = true
830 public.MTASTSHTTPS.Enabled = true
831 public.WebserverHTTP.Enabled = true
832 public.WebserverHTTPS.Enabled = true
833 }
834
835 // Suggest blocklists, but we'll comment them out after generating the config.
836 for _, zone := range zones {
837 public.SMTP.DNSBLs = append(public.SMTP.DNSBLs, zone.Name())
838 }
839
840 // Monitor DNSBLs by default, without using them for incoming deliveries.
841 for _, zone := range zones {
842 dc.MonitorDNSBLs = append(dc.MonitorDNSBLs, zone.Name())
843 }
844
845 internal := config.Listener{
846 IPs: privateListenerIPs,
847 Hostname: "localhost",
848 }
849 internal.AccountHTTP.Enabled = true
850 internal.AdminHTTP.Enabled = true
851 internal.WebmailHTTP.Enabled = true
852 internal.WebAPIHTTP.Enabled = true
853 internal.MetricsHTTP.Enabled = true
854 if existingWebserver {
855 internal.AccountHTTP.Port = 1080
856 internal.AccountHTTP.Forwarded = true
857 internal.AdminHTTP.Port = 1080
858 internal.AdminHTTP.Forwarded = true
859 internal.WebmailHTTP.Port = 1080
860 internal.WebmailHTTP.Forwarded = true
861 internal.WebAPIHTTP.Port = 1080
862 internal.WebAPIHTTP.Forwarded = true
863 internal.AutoconfigHTTPS.Enabled = true
864 internal.AutoconfigHTTPS.Port = 81
865 internal.AutoconfigHTTPS.NonTLS = true
866 internal.MTASTSHTTPS.Enabled = true
867 internal.MTASTSHTTPS.Port = 81
868 internal.MTASTSHTTPS.NonTLS = true
869 internal.WebserverHTTP.Enabled = true
870 internal.WebserverHTTP.Port = 81
871 }
872
873 sc.Listeners = map[string]config.Listener{
874 "public": public,
875 "internal": internal,
876 }
877 sc.Postmaster.Account = accountName
878 sc.Postmaster.Mailbox = "Postmaster"
879 sc.HostTLSRPT.Account = accountName
880 sc.HostTLSRPT.Localpart = "tls-reports"
881 sc.HostTLSRPT.Mailbox = "TLSRPT"
882
883 mox.ConfigStaticPath = filepath.FromSlash("config/mox.conf")
884 mox.ConfigDynamicPath = filepath.FromSlash("config/domains.conf")
885
886 mox.Conf.DynamicLastCheck = time.Now() // Prevent error logging by Make calls below.
887
888 accountConf := admin.MakeAccountConfig(addr)
889 const withMTASTS = true
890 confDomain, keyPaths, err := admin.MakeDomainConfig(context.Background(), domain, dnshostname, accountName, withMTASTS)
891 if err != nil {
892 fatalf("making domain config: %s", err)
893 }
894 cleanupPaths = append(cleanupPaths, keyPaths...)
895
896 dc.Domains = map[string]config.Domain{
897 domain.Name(): confDomain,
898 }
899 dc.Accounts = map[string]config.Account{
900 accountName: accountConf,
901 }
902
903 // Build config in memory, so we can easily comment out the DNSBLs config.
904 var sb strings.Builder
905 sc.CheckUpdates = true // Commented out below.
906 if err := sconf.WriteDocs(&sb, &sc); err != nil {
907 fatalf("generating static config: %v", err)
908 }
909 confstr := sb.String()
910 confstr = strings.ReplaceAll(confstr, "\nCheckUpdates: true\n", "\n#\n# RECOMMENDED: please enable to stay up to date\n#\n#CheckUpdates: true\n")
911 confstr = strings.ReplaceAll(confstr, "DNSBLs:\n", "#DNSBLs:\n")
912 for _, bl := range public.SMTP.DNSBLs {
913 confstr = strings.ReplaceAll(confstr, "- "+bl+"\n", "#- "+bl+"\n")
914 }
915 xwritefile(filepath.FromSlash("config/mox.conf"), []byte(confstr), 0660)
916
917 // Generate domains config, and add a commented out example for delivery to a mailing list.
918 var db bytes.Buffer
919 if err := sconf.WriteDocs(&db, &dc); err != nil {
920 fatalf("generating domains config: %v", err)
921 }
922
923 // This approach is a bit horrible, but it generates a convenient
924 // example that includes the comments. Though it is gone by the first
925 // write of the file by mox.
926 odests := fmt.Sprintf("\t\tDestinations:\n\t\t\t%s: nil\n", addr.String())
927 var destsExample = struct {
928 Destinations map[string]config.Destination
929 }{
930 Destinations: map[string]config.Destination{
931 addr.String(): {
932 Rulesets: []config.Ruleset{
933 {
934 VerifiedDomain: "list.example.org",
935 HeadersRegexp: map[string]string{
936 "^list-id$": `<name\.list\.example\.org>`,
937 },
938 ListAllowDomain: "list.example.org",
939 Mailbox: "Lists/Example",
940 },
941 },
942 },
943 },
944 }
945 var destBuf strings.Builder
946 if err := sconf.Describe(&destBuf, destsExample); err != nil {
947 fatalf("describing destination example: %v", err)
948 }
949 ndests := odests + "# If you receive email from mailing lists, you may want to configure them like the\n# example below (remove the empty/false SMTPMailRegexp and IsForward).\n# If you are receiving forwarded email, see the IsForwarded option in a Ruleset.\n"
950 for _, line := range strings.Split(destBuf.String(), "\n")[1:] {
951 ndests += "#\t\t" + line + "\n"
952 }
953 dconfstr := strings.ReplaceAll(db.String(), odests, ndests)
954 xwritefile(filepath.FromSlash("config/domains.conf"), []byte(dconfstr), 0660)
955
956 // Verify config.
957 loadTLSKeyCerts := !existingWebserver
958 mc, errs := mox.ParseConfig(context.Background(), c.log, filepath.FromSlash("config/mox.conf"), true, loadTLSKeyCerts, false)
959 if len(errs) > 0 {
960 if len(errs) > 1 {
961 log.Printf("checking generated config, multiple errors:")
962 for _, err := range errs {
963 log.Println(err)
964 }
965 fatalf("aborting due to multiple config errors")
966 }
967 fatalf("checking generated config: %s", errs[0])
968 }
969 mox.SetConfig(mc)
970 // NOTE: Now that we've prepared the config, we can open the account
971 // and set a passsword, and the public key for the DKIM private keys
972 // are available for generating the DKIM DNS records below.
973
974 confDomain, ok := mc.Domain(domain)
975 if !ok {
976 fatalf("cannot find domain in new config")
977 }
978
979 acc, _, _, err := store.OpenEmail(c.log, args[0], false)
980 if err != nil {
981 fatalf("open account: %s", err)
982 }
983 cleanupPaths = append(cleanupPaths, dataDir, filepath.Join(dataDir, "accounts"), filepath.Join(dataDir, "accounts", accountName), filepath.Join(dataDir, "accounts", accountName, "index.db"))
984
985 password := mox.GeneratePassword()
986
987 // Kludge to cause no logging to be printed about setting a new password.
988 loglevel := mox.Conf.Log[""]
989 mox.Conf.Log[""] = mlog.LevelWarn
990 mlog.SetConfig(mox.Conf.Log)
991 if err := acc.SetPassword(c.log, password); err != nil {
992 fatalf("setting password: %s", err)
993 }
994 mox.Conf.Log[""] = loglevel
995 mlog.SetConfig(mox.Conf.Log)
996
997 if err := acc.Close(); err != nil {
998 fatalf("closing account: %s", err)
999 }
1000 fmt.Printf("IMAP, SMTP submission and HTTP account password for %s: %s\n\n", args[0], password)
1001 fmt.Printf(`When configuring your email client, use the email address as username. If
1002autoconfig/autodiscover does not work, use these settings:
1003`)
1004 printClientConfig(domain)
1005
1006 if existingWebserver {
1007 fmt.Printf(`
1008Configuration files have been written to config/mox.conf and
1009config/domains.conf.
1010
1011Create the DNS records below, by adding them to your zone file or through the
1012web interface of your DNS operator. The admin interface can show these same
1013records, and has a page to check they have been configured correctly.
1014
1015You must configure your existing webserver to forward requests for:
1016
1017 https://mta-sts.%s/
1018 https://autoconfig.%s/
1019
1020To mox, at:
1021
1022 http://127.0.0.1:81
1023
1024If it makes it easier to get a TLS certificate for %s, you can add a
1025reverse proxy for that hostname too.
1026
1027You must edit mox.conf and configure the paths to the TLS certificates and keys.
1028The paths are relative to config/ directory that holds mox.conf! To test if your
1029config is valid, run:
1030
1031 ./mox config test
1032
1033The DNS records to add:
1034`, domain.ASCII, domain.ASCII, dnshostname.ASCII)
1035 } else {
1036 fmt.Printf(`
1037Configuration files have been written to config/mox.conf and
1038config/domains.conf. You should review them. Then create the DNS records below,
1039by adding them to your zone file or through the web interface of your DNS
1040operator. You can also skip creating the DNS records and start mox immediately.
1041The admin interface can show these same records, and has a page to check they
1042have been configured correctly. The DNS records to add:
1043`)
1044 }
1045
1046 // We do not verify the records exist: If they don't exist, we would only be
1047 // priming dns caches with negative/absent records, causing our "quick setup" to
1048 // appear to fail or take longer than "quick".
1049
1050 records, err := admin.DomainRecords(confDomain, domain, domainDNSSECResult.Authentic, "letsencrypt.org", "")
1051 if err != nil {
1052 fatalf("making required DNS records")
1053 }
1054 fmt.Print("\n\n" + strings.Join(records, "\n") + "\n\n\n\n")
1055
1056 fmt.Printf(`WARNING: The configuration and DNS records above assume you do not currently
1057have email configured for your domain. If you do already have email configured,
1058or if you are sending email for your domain from other machines/services, you
1059should understand the consequences of the DNS records above before
1060continuing!
1061`)
1062 if os.Getenv("MOX_DOCKER") == "" {
1063 fmt.Printf(`
1064You can now start mox with "./mox serve", as root.
1065`)
1066 } else {
1067 fmt.Printf(`
1068You can now start the mox container.
1069`)
1070 }
1071 fmt.Printf(`
1072File ownership and permissions are automatically set correctly by mox when
1073starting up. On linux, you may want to enable mox as a systemd service.
1074
1075`)
1076
1077 // For now, we only give service config instructions for linux when not running in docker.
1078 if runtime.GOOS == "linux" && os.Getenv("MOX_DOCKER") == "" {
1079 pwd, err := os.Getwd()
1080 if err != nil {
1081 log.Printf("current working directory: %v", err)
1082 pwd = "/home/mox"
1083 }
1084 service := strings.ReplaceAll(moxService, "/home/mox", pwd)
1085 xwritefile("mox.service", []byte(service), 0644)
1086 cleanupPaths = append(cleanupPaths, "mox.service")
1087 fmt.Printf(`See mox.service for a systemd service file. To enable and start:
1088
1089 sudo chmod 644 mox.service
1090 sudo systemctl enable $PWD/mox.service
1091 sudo systemctl start mox.service
1092 sudo journalctl -f -u mox.service # See logs
1093`)
1094 }
1095
1096 fmt.Printf(`
1097After starting mox, the web interfaces are served at:
1098
1099http://localhost/ - account (email address as username)
1100http://localhost/webmail/ - webmail (email address as username)
1101http://localhost/admin/ - admin (empty username)
1102
1103To access these from your browser, run
1104"ssh -L 8080:localhost:80 you@yourmachine" locally and open
1105http://localhost:8080/[...].
1106
1107If you run into problem, have questions/feedback or found a bug, please let us
1108know. Mox needs your help!
1109
1110Enjoy!
1111`)
1112
1113 if !existingWebserver {
1114 fmt.Printf(`
1115PS: If you want to run mox along side an existing webserver that uses port 443
1116and 80, see "mox help quickstart" with the -existing-webserver option.
1117`)
1118 }
1119
1120 cleanupPaths = nil
1121}
1122