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