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