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