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