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.AutoconfigHTTPS.Forwarded = true
873 internal.MTASTSHTTPS.Enabled = true
874 internal.MTASTSHTTPS.Port = 81
875 internal.MTASTSHTTPS.NonTLS = true
876 internal.MTASTSHTTPS.Forwarded = true
877 internal.WebserverHTTP.Enabled = true
878 internal.WebserverHTTP.Port = 81
879 }
880
881 sc.Listeners = map[string]config.Listener{
882 "public": public,
883 "internal": internal,
884 }
885 sc.Postmaster.Account = accountName
886 sc.Postmaster.Mailbox = "Postmaster"
887 sc.HostTLSRPT.Account = accountName
888 sc.HostTLSRPT.Localpart = "tlsreports"
889 sc.HostTLSRPT.Mailbox = "TLSRPT"
890
891 mox.ConfigStaticPath = filepath.FromSlash("config/mox.conf")
892 mox.ConfigDynamicPath = filepath.FromSlash("config/domains.conf")
893
894 mox.Conf.DynamicLastCheck = time.Now() // Prevent error logging by Make calls below.
895
896 accountConf := admin.MakeAccountConfig(addr)
897 const withMTASTS = true
898 confDomain, keyPaths, err := admin.MakeDomainConfig(context.Background(), domain, dnshostname, accountName, withMTASTS)
899 if err != nil {
900 fatalf("making domain config: %s", err)
901 }
902 cleanupPaths = append(cleanupPaths, keyPaths...)
903
904 dc.Domains = map[string]config.Domain{
905 domain.Name(): confDomain,
906 }
907 dc.Accounts = map[string]config.Account{
908 accountName: accountConf,
909 }
910
911 // Build config in memory, so we can easily comment out the DNSBLs config.
912 var sb strings.Builder
913 sc.CheckUpdates = true // Commented out below.
914 if err := sconf.WriteDocs(&sb, &sc); err != nil {
915 fatalf("generating static config: %v", err)
916 }
917 confstr := sb.String()
918 confstr = strings.ReplaceAll(confstr, "\nCheckUpdates: true\n", "\n#\n# RECOMMENDED: please enable to stay up to date\n#\n#CheckUpdates: true\n")
919 confstr = strings.ReplaceAll(confstr, "DNSBLs:\n", "#DNSBLs:\n")
920 for _, bl := range public.SMTP.DNSBLs {
921 confstr = strings.ReplaceAll(confstr, "- "+bl+"\n", "#- "+bl+"\n")
922 }
923 xwritefile(filepath.FromSlash("config/mox.conf"), []byte(confstr), 0660)
924
925 // Generate domains config, and add a commented out example for delivery to a mailing list.
926 var db bytes.Buffer
927 if err := sconf.WriteDocs(&db, &dc); err != nil {
928 fatalf("generating domains config: %v", err)
929 }
930
931 // This approach is a bit horrible, but it generates a convenient
932 // example that includes the comments. Though it is gone by the first
933 // write of the file by mox.
934 odests := fmt.Sprintf("\t\tDestinations:\n\t\t\t%s: nil\n", addr.String())
935 var destsExample = struct {
936 Destinations map[string]config.Destination
937 }{
938 Destinations: map[string]config.Destination{
939 addr.String(): {
940 Rulesets: []config.Ruleset{
941 {
942 VerifiedDomain: "list.example.org",
943 HeadersRegexp: map[string]string{
944 "^list-id$": `<name\.list\.example\.org>`,
945 },
946 ListAllowDomain: "list.example.org",
947 Mailbox: "Lists/Example",
948 },
949 },
950 },
951 },
952 }
953 var destBuf strings.Builder
954 if err := sconf.Describe(&destBuf, destsExample); err != nil {
955 fatalf("describing destination example: %v", err)
956 }
957 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"
958 for _, line := range strings.Split(destBuf.String(), "\n")[1:] {
959 ndests += "#\t\t" + line + "\n"
960 }
961 dconfstr := strings.ReplaceAll(db.String(), odests, ndests)
962 xwritefile(filepath.FromSlash("config/domains.conf"), []byte(dconfstr), 0660)
963
964 // Verify config.
965 loadTLSKeyCerts := !existingWebserver
966 mc, errs := mox.ParseConfig(context.Background(), c.log, filepath.FromSlash("config/mox.conf"), true, loadTLSKeyCerts, false)
967 if len(errs) > 0 {
968 if len(errs) > 1 {
969 log.Printf("checking generated config, multiple errors:")
970 for _, err := range errs {
971 log.Println(err)
972 }
973 fatalf("aborting due to multiple config errors")
974 }
975 fatalf("checking generated config: %s", errs[0])
976 }
977 mox.SetConfig(mc)
978 // NOTE: Now that we've prepared the config, we can open the account
979 // and set a passsword, and the public key for the DKIM private keys
980 // are available for generating the DKIM DNS records below.
981
982 confDomain, ok := mc.Domain(domain)
983 if !ok {
984 fatalf("cannot find domain in new config")
985 }
986
987 acc, _, _, err := store.OpenEmail(c.log, args[0], false)
988 if err != nil {
989 fatalf("open account: %s", err)
990 }
991 cleanupPaths = append(cleanupPaths, dataDir, filepath.Join(dataDir, "accounts"), filepath.Join(dataDir, "accounts", accountName), filepath.Join(dataDir, "accounts", accountName, "index.db"))
992
993 password := mox.GeneratePassword()
994
995 // Kludge to cause no logging to be printed about setting a new password.
996 loglevel := mox.Conf.Log[""]
997 mox.Conf.Log[""] = mlog.LevelWarn
998 mlog.SetConfig(mox.Conf.Log)
999 if err := acc.SetPassword(c.log, password); err != nil {
1000 fatalf("setting password: %s", err)
1001 }
1002 mox.Conf.Log[""] = loglevel
1003 mlog.SetConfig(mox.Conf.Log)
1004
1005 if err := acc.Close(); err != nil {
1006 fatalf("closing account: %s", err)
1007 }
1008 fmt.Printf("IMAP, SMTP submission and HTTP account password for %s: %s\n\n", args[0], password)
1009 fmt.Printf(`When configuring your email client, use the email address as username. If
1010autoconfig/autodiscover does not work, use these settings:
1011`)
1012 printClientConfig(domain)
1013
1014 if existingWebserver {
1015 fmt.Printf(`
1016Configuration files have been written to config/mox.conf and
1017config/domains.conf.
1018
1019Create the DNS records below, by adding them to your zone file or through the
1020web interface of your DNS operator. The admin interface can show these same
1021records, and has a page to check they have been configured correctly.
1022
1023You must configure your existing webserver to forward requests for:
1024
1025 https://mta-sts.%s/
1026 https://autoconfig.%s/
1027
1028To mox, at:
1029
1030 http://127.0.0.1:81
1031
1032If it makes it easier to get a TLS certificate for %s, you can add a
1033reverse proxy for that hostname too.
1034
1035You must edit mox.conf and configure the paths to the TLS certificates and keys.
1036The paths are relative to config/ directory that holds mox.conf! To test if your
1037config is valid, run:
1038
1039 ./mox config test
1040
1041The DNS records to add:
1042`, domain.ASCII, domain.ASCII, dnshostname.ASCII)
1043 } else {
1044 fmt.Printf(`
1045Configuration files have been written to config/mox.conf and
1046config/domains.conf. You should review them. Then create the DNS records below,
1047by adding them to your zone file or through the web interface of your DNS
1048operator. You can also skip creating the DNS records and start mox immediately.
1049The admin interface can show these same records, and has a page to check they
1050have been configured correctly. The DNS records to add:
1051`)
1052 }
1053
1054 // We do not verify the records exist: If they don't exist, we would only be
1055 // priming dns caches with negative/absent records, causing our "quick setup" to
1056 // appear to fail or take longer than "quick".
1057
1058 records, err := admin.DomainRecords(confDomain, domain, domainDNSSECResult.Authentic, "letsencrypt.org", "")
1059 if err != nil {
1060 fatalf("making required DNS records")
1061 }
1062 fmt.Print("\n\n" + strings.Join(records, "\n") + "\n\n\n\n")
1063
1064 fmt.Printf(`WARNING: The configuration and DNS records above assume you do not currently
1065have email configured for your domain. If you do already have email configured,
1066or if you are sending email for your domain from other machines/services, you
1067should understand the consequences of the DNS records above before
1068continuing!
1069`)
1070 if os.Getenv("MOX_DOCKER") == "" {
1071 fmt.Printf(`
1072You can now start mox with "./mox serve", as root.
1073`)
1074 } else {
1075 fmt.Printf(`
1076You can now start the mox container.
1077`)
1078 }
1079 fmt.Printf(`
1080File ownership and permissions are automatically set correctly by mox when
1081starting up. On linux, you may want to enable mox as a systemd service.
1082
1083`)
1084
1085 // For now, we only give service config instructions for linux when not running in docker.
1086 if runtime.GOOS == "linux" && os.Getenv("MOX_DOCKER") == "" {
1087 pwd, err := os.Getwd()
1088 if err != nil {
1089 log.Printf("current working directory: %v", err)
1090 pwd = "/home/mox"
1091 }
1092 service := strings.ReplaceAll(moxService, "/home/mox", pwd)
1093 xwritefile("mox.service", []byte(service), 0644)
1094 cleanupPaths = append(cleanupPaths, "mox.service")
1095 fmt.Printf(`See mox.service for a systemd service file. To enable and start:
1096
1097 sudo chmod 644 mox.service
1098 sudo systemctl enable $PWD/mox.service
1099 sudo systemctl start mox.service
1100 sudo journalctl -f -u mox.service # See logs
1101`)
1102 }
1103
1104 fmt.Printf(`
1105After starting mox, the web interfaces are served at:
1106
1107http://localhost/ - account (email address as username)
1108http://localhost/webmail/ - webmail (email address as username)
1109http://localhost/admin/ - admin (empty username)
1110
1111To access these from your browser, run
1112"ssh -L 8080:localhost:80 you@yourmachine" locally and open
1113http://localhost:8080/[...].
1114
1115If you run into problem, have questions/feedback or found a bug, please let us
1116know. Mox needs your help!
1117
1118Enjoy!
1119`)
1120
1121 if !existingWebserver {
1122 fmt.Printf(`
1123PS: If you want to run mox along side an existing webserver that uses port 443
1124and 80, see "mox help quickstart" with the -existing-webserver option.
1125`)
1126 }
1127
1128 cleanupPaths = nil
1129}
1130