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