9 cryptorand "crypto/rand"
28 "golang.org/x/crypto/bcrypt"
30 "github.com/mjl-/sconf"
32 "github.com/mjl-/mox/admin"
33 "github.com/mjl-/mox/config"
34 "github.com/mjl-/mox/dns"
35 "github.com/mjl-/mox/dnsbl"
36 "github.com/mjl-/mox/mlog"
37 "github.com/mjl-/mox/mox-"
38 "github.com/mjl-/mox/smtp"
39 "github.com/mjl-/mox/store"
46 chars := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*-_;:,<.>/"
48 buf := make([]byte, 1)
49 for i := 0; i < 12; i++ {
53 if i+len(chars) > 255 {
54 continue // Prevent bias.
56 s += string(chars[i%len(chars)])
63func cmdQuickstart(c *cmd) {
64 c.params = "[-skipdial] [-existing-webserver] [-hostname host] user@domain [user | uid]"
65 c.help = `Quickstart generates configuration files and prints instructions to quickly set up a mox instance.
67Quickstart writes configuration files, prints initial admin and account
68passwords, DNS records you should create. If you run it on Linux it writes a
69systemd service file and prints commands to enable and start mox as service.
71All output is written to quickstart.log for later reference.
73The user or uid is optional, defaults to "mox", and is the user or uid/gid mox
74will run as after initialization.
76Quickstart assumes mox will run on the machine you run quickstart on and uses
77its host name and public IPs. On many systems the hostname is not a fully
78qualified domain name, but only the first dns "label", e.g. "mail" in case of
79"mail.example.org". If so, quickstart does a reverse DNS lookup to find the
80hostname, and as fallback uses the label plus the domain of the email address
81you specified. Use flag -hostname to explicitly specify the hostname mox will
84Mox is by far easiest to operate if you let it listen on port 443 (HTTPS) and
8580 (HTTP). TLS will be fully automatic with ACME with Let's Encrypt.
87You can run mox along with an existing webserver, but because of MTA-STS and
88autoconfig, you'll need to forward HTTPS traffic for two domains to mox. Run
89"mox quickstart -existing-webserver ..." to generate configuration files and
90instructions for configuring mox along with an existing webserver.
92But please first consider configuring mox on port 443. It can itself serve
93domains with HTTP/HTTPS, including with automatic TLS with ACME, is easily
94configured through both configuration files and admin web interface, and can act
95as a reverse proxy (and static file server for that matter), so you can forward
96traffic to your existing backend applications. Look for "WebHandlers:" in the
97output of "mox config describe-domains" and see the output of
98"mox config example webhandlers".
100 var existingWebserver bool
103 c.flag.BoolVar(&existingWebserver, "existing-webserver", false, "use if a webserver is already running, so mox won't listen on port 80 and 443; you'll have to provide tls certificates/keys, and configure the existing webserver as reverse proxy, forwarding requests to mox.")
104 c.flag.StringVar(&hostname, "hostname", "", "hostname mox will run on, by default the hostname of the machine quickstart runs on; if specified, the IPs for the hostname are configured for the public listener")
105 c.flag.BoolVar(&skipDial, "skipdial", false, "skip check for outgoing smtp (port 25) connectivity")
107 if len(args) != 1 && len(args) != 2 {
111 // Write all output to quickstart.log.
112 logfile, err := os.Create("quickstart.log")
113 xcheckf(err, "creating quickstart.log")
115 origStdout := os.Stdout
116 origStderr := os.Stderr
117 piper, pipew, err := os.Pipe()
118 xcheckf(err, "creating pipe for logging to logfile")
119 pipec := make(chan struct{})
121 io.Copy(io.MultiWriter(origStdout, logfile), piper)
124 // A single pipe, so writes to stdout and stderr don't get interleaved.
130 os.Stdout = origStdout
131 os.Stderr = origStderr
132 err := logfile.Close()
133 xcheckf(err, "closing quickstart.log")
136 log.SetOutput(os.Stdout)
137 fmt.Printf("(output is also written to quickstart.log)\n\n")
138 defer fmt.Printf("\n(output is also written to quickstart.log)\n")
140 // We take care to cleanup created files when we error out.
141 // We don't want to get a new user into trouble with half of the files
142 // after encountering an error.
144 // We use fatalf instead of log.Fatal* to cleanup files.
145 var cleanupPaths []string
146 fatalf := func(format string, args ...any) {
147 // We remove in reverse order because dirs would have been created first and must
148 // be removed last, after their files have been removed.
149 for i := len(cleanupPaths) - 1; i >= 0; i-- {
151 if err := os.Remove(p); err != nil {
152 log.Printf("cleaning up %q: %s", p, err)
156 log.Printf(format, args...)
161 xwritefile := func(path string, data []byte, perm os.FileMode) {
162 os.MkdirAll(filepath.Dir(path), 0770)
163 f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm)
165 fatalf("creating file %q: %s", path, err)
167 cleanupPaths = append(cleanupPaths, path)
168 _, err = f.Write(data)
173 fatalf("writing file %q: %s", path, err)
177 addr, err := smtp.ParseAddress(args[0])
179 fatalf("parsing email address: %s", err)
181 accountName := addr.Localpart.String()
182 domain := addr.Domain
184 for _, c := range accountName {
186 fmt.Printf(`NOTE: Username %q is not ASCII-only. It is recommended you also configure an
187ASCII-only alias. Both for delivery of email from other systems, and for
195 resolver := dns.StrictResolver{}
196 // We don't want to spend too much total time on the DNS lookups. Because DNS may
197 // not work during quickstart, and we don't want to loop doing requests and having
198 // to wait for a timeout each time.
199 resolveCtx, resolveCancel := context.WithTimeout(context.Background(), 10*time.Second)
200 defer resolveCancel()
202 // Some DNSSEC-verifying resolvers return unauthentic data for ".", so we check "com".
203 fmt.Printf("Checking if DNS resolvers are DNSSEC-verifying...")
204 _, resolverDNSSECResult, err := resolver.LookupNS(resolveCtx, "com.")
207 fatalf("checking dnssec support in resolver: %v", err)
208 } else if !resolverDNSSECResult.Authentic {
211WARNING: It looks like the DNS resolvers configured on your system do not
212verify DNSSEC, or aren't trusted (by having loopback IPs or through "options
213trust-ad" in /etc/resolv.conf). Without DNSSEC, outbound delivery with SMTP
214used unprotected MX records, and SMTP STARTTLS connections cannot verify the TLS
215certificate with DANE (based on a public key in DNS), and will fall back to
216either MTA-STS for verification, or use "opportunistic TLS" with no certificate
219Recommended action: Install unbound, a DNSSEC-verifying recursive DNS resolver,
220ensure it has DNSSEC root keys (see unbound-anchor), and enable support for
221"extended dns errors" (EDE, available since unbound v1.16.0, see below; not
222required, but it gives helpful error messages about DNSSEC failures instead of
223generic DNS SERVFAIL errors). Test with "dig com. ns" and look for "ad"
224(authentic data) in response "flags".
226cat <<EOF >/etc/unbound/unbound.conf.d/ede.conf
232Troubleshooting hints:
233- Ensure /etc/resolv.conf has "nameserver 127.0.0.1". If the IP is 127.0.0.53,
234 DNS resolving is done by systemd-resolved. Make sure "resolvconf" isn't
235 overwriting /etc/resolv.conf (Debian has a package "openresolv" that makes this
236 easier). "dig" also shows to which IP the DNS request was sent.
237- Ensure unbound has DNSSEC root keys available. See unbound config option
238 "auto-trust-anchor-file" and the unbound-anchor command. Ensure the file exists.
239- Run "./mox dns lookup ns com." to simulate the DNSSEC check done by mox. The
240 output should say "with dnssec".
241- The "delv" command can check whether a domain is DNSSEC-signed, but it does
242 its own DNSSEC verification instead of relying on the resolver, so you cannot
243 use it to check whether unbound is verifying DNSSEC correctly.
244- Increase logging in unbound, see options "verbosity" and "log-queries".
251 // We are going to find the (public) IPs to listen on and possibly the host name.
253 // Start with reasonable defaults. We'll replace them specific IPs, if we can find them.
254 privateListenerIPs := []string{"127.0.0.1", "::1"}
255 publicListenerIPs := []string{"0.0.0.0", "::"}
256 var publicNATIPs []string // Actual public IP, but when it is NATed and machine doesn't have direct access.
257 defaultPublicListenerIPs := true
259 // If we find IPs based on network interfaces, {public,private}ListenerIPs are set
260 // based on these values.
261 var loopbackIPs, privateIPs, publicIPs []string
263 // Gather IP addresses for public and private listeners.
264 // We look at each network interface. If an interface has a private address, we
265 // conservatively assume all addresses on that interface are private.
266 ifaces, err := net.Interfaces()
268 fatalf("listing network interfaces: %s", err)
270 parseAddrIP := func(s string) net.IP {
271 if strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]") {
274 ip, _, _ := net.ParseCIDR(s)
277 for _, iface := range ifaces {
278 if iface.Flags&net.FlagUp == 0 {
281 addrs, err := iface.Addrs()
283 fatalf("listing address for network interface: %s", err)
289 // todo: should we detect temporary/ephemeral ipv6 addresses and not add them?
291 for _, addr := range addrs {
292 ip := parseAddrIP(addr.String())
293 if ip.IsInterfaceLocalMulticast() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() || ip.IsMulticast() {
296 if ip.IsLoopback() || ip.IsPrivate() {
302 for _, addr := range addrs {
303 ip := parseAddrIP(addr.String())
307 if ip.IsInterfaceLocalMulticast() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() || ip.IsMulticast() {
312 loopbackIPs = append(loopbackIPs, ip.String())
314 privateIPs = append(privateIPs, ip.String())
317 publicIPs = append(publicIPs, ip.String())
322 var dnshostname dns.Domain
324 hostnameStr, err := os.Hostname()
326 fatalf("hostname: %s", err)
328 if strings.Contains(hostnameStr, ".") {
329 dnshostname, err = dns.ParseDomain(hostnameStr)
331 fatalf("parsing hostname: %v", err)
334 // It seems Linux machines don't have a single FQDN configured. E.g. /etc/hostname
335 // is just the name without domain. We'll look up the names for all IPs, and hope
336 // to find a single FQDN name (with at least 1 dot).
337 names := map[string]struct{}{}
338 if len(publicIPs) > 0 {
339 fmt.Printf("Trying to find hostname by reverse lookup of public IPs %s...", strings.Join(publicIPs, ", "))
342 warnf := func(format string, args ...any) {
344 fmt.Printf("\n%s", fmt.Sprintf(format, args...))
346 for _, ip := range publicIPs {
347 revctx, revcancel := context.WithTimeout(resolveCtx, 5*time.Second)
349 l, _, err := resolver.LookupAddr(revctx, ip)
351 warnf("WARNING: looking up reverse name(s) for %s: %v", ip, err)
353 for _, name := range l {
354 if strings.Contains(name, ".") {
355 names[name] = struct{}{}
359 var nameList []string
360 for k := range names {
361 nameList = append(nameList, strings.TrimRight(k, "."))
363 sort.Slice(nameList, func(i, j int) bool {
364 return nameList[i] < nameList[j]
366 if len(nameList) == 0 {
367 dnshostname, err = dns.ParseDomain(hostnameStr + "." + domain.Name())
370 fatalf("parsing hostname: %v", err)
372 warnf(`WARNING: cannot determine hostname because the system name is not an FQDN and
373no public IPs resolving to an FQDN were found. Quickstart guessed the host name
374below. If it is not correct, please remove the generated config files and run
375quickstart again with the -hostname flag.
380 if len(nameList) > 1 {
381 warnf(`WARNING: multiple hostnames found for the public IPs, using the first of: %s
382If this is not correct, remove the generated config files and run quickstart
383again with the -hostname flag.
384`, strings.Join(nameList, ", "))
386 dnshostname, err = dns.ParseDomain(nameList[0])
389 fatalf("parsing hostname %s: %v", nameList[0], err)
395 fmt.Printf(" found %s\n", dnshostname)
399 // Host name was explicitly configured on command-line. We'll try to use its public
402 dnshostname, err = dns.ParseDomain(hostname)
404 fatalf("parsing hostname: %v", err)
408 fmt.Printf("Looking up IPs for hostname %s...", dnshostname)
409 ipctx, ipcancel := context.WithTimeout(resolveCtx, 5*time.Second)
411 ips, domainDNSSECResult, err := resolver.LookupIPAddr(ipctx, dnshostname.ASCII+".")
413 var xips []net.IPAddr
416 hostPrivate := len(ips) > 0
417 for _, ip := range ips {
418 if !ip.IP.IsPrivate() {
421 // During linux install, you may get an alias for you full hostname in /etc/hosts
422 // resolving to 127.0.1.1, which would result in a false positive about the
423 // hostname having a record. Filter it out. It is a bit surprising that hosts don't
424 // otherwise know their FQDN.
425 if ip.IP.IsLoopback() {
427 fmt.Printf("\n\nWARNING: Your hostname is resolving to a loopback IP address %s. This likely breaks email delivery to local accounts. /etc/hosts likely contains a line like %q. Either replace it with your actual IP(s), or remove the line.\n", ip.IP, fmt.Sprintf("%s %s", ip.IP, dnshostname.ASCII))
430 xips = append(xips, ip)
431 hostIPs = append(hostIPs, ip.String())
433 if err == nil && len(xips) == 0 {
434 // todo: possibly check this by trying to resolve without using /etc/hosts?
435 err = errors.New("hostname not in dns, probably only in /etc/hosts")
439 // We may have found private and public IPs on the machine, and IPs for the host
440 // name we think we should use. They may not match with each other. E.g. the public
441 // IPs on interfaces could be different from the IPs for the host. We don't try to
442 // detect all possible configs, but just generate what makes sense given whether we
443 // found public/private/hostname IPs. If the user is doing sensible things, it
444 // should be correct. But they should be checking the generated config file anyway.
445 // And we do log which host name we are using, and whether we detected a NAT setup.
446 // In the future, we may do an interactive setup that can guide the user better.
448 if !hostPrivate && len(publicIPs) == 0 && len(privateIPs) > 0 {
449 // We only have private IPs, assume we are behind a NAT and put the IPs of the host in NATIPs.
450 publicListenerIPs = privateIPs
451 publicNATIPs = hostIPs
452 defaultPublicListenerIPs = false
453 if len(loopbackIPs) > 0 {
454 privateListenerIPs = loopbackIPs
457 if len(hostIPs) > 0 {
458 publicListenerIPs = hostIPs
459 defaultPublicListenerIPs = false
461 // Only keep private IPs that are not in host-based publicListenerIPs. For
462 // internal-only setups, including integration tests.
463 m := map[string]bool{}
464 for _, ip := range hostIPs {
468 for _, ip := range privateIPs {
470 npriv = append(npriv, ip)
475 } else if len(publicIPs) > 0 {
476 publicListenerIPs = publicIPs
477 defaultPublicListenerIPs = false
478 hostIPs = publicIPs // For DNSBL check below.
480 if len(privateIPs) > 0 {
481 privateListenerIPs = append(privateIPs, loopbackIPs...)
482 } else if len(loopbackIPs) > 0 {
483 privateListenerIPs = loopbackIPs
492WARNING: Quickstart assumed the hostname of this machine is %s and generates a
493config for that host, but could not retrieve that name from DNS:
497This likely means one of two things:
4991. You don't have any DNS records for this machine at all. You should add them
5012. The hostname mentioned is not the correct host name of this machine. You will
502 have to replace the hostname in the suggested DNS records and generated
503 config/mox.conf file. Make sure your hostname resolves to your public IPs, and
504 your public IPs resolve back (reverse) to your hostname.
508 } else if !domainDNSSECResult.Authentic {
514NOTE: It looks like the DNS records of your domain (zone) are not DNSSEC-signed.
515Mail servers that send email to your domain, or receive email from your domain,
516cannot verify that the MX/SPF/DKIM/DMARC/MTA-STS records they receive are
517authentic. DANE, for authenticated delivery without relying on a pool of
518certificate authorities, requires DNSSEC, so will not be configured at this
520Recommended action: Continue now, but consider enabling DNSSEC for your domain
521later at your DNS operator, and adding DANE records for protecting incoming
536 results := make(chan result)
537 for _, ip := range ips {
541 revctx, revcancel := context.WithTimeout(resolveCtx, 5*time.Second)
543 addrs, _, err := resolver.LookupAddr(revctx, s)
544 results <- result{s, addrs, err}
547 fmt.Printf("Looking up reverse names for IP(s) %s...", strings.Join(l, ", "))
549 warnf := func(format string, args ...any) {
550 fmt.Printf("\nWARNING: %s", fmt.Sprintf(format, args...))
553 for i := 0; i < len(ips); i++ {
556 warnf("looking up reverse name for %s: %v", r.IP, r.Err)
559 if len(r.Addrs) != 1 {
560 warnf("expected exactly 1 name for %s, got %d (%v)", r.IP, len(r.Addrs), r.Addrs)
563 for i, a := range r.Addrs {
564 a = strings.TrimRight(a, ".")
565 r.Addrs[i] = a // For potential error message below.
566 d, err := dns.ParseDomain(a)
568 warnf("parsing reverse name %q for %s: %v", a, r.IP, err)
570 if d == dnshostname {
575 warnf("reverse name(s) %s for ip %s do not match hostname %s, which will cause other mail servers to reject incoming messages from this IP", strings.Join(r.Addrs, ","), r.IP, dnshostname)
585 // Check outgoing SMTP connectivity.
587 fmt.Printf("Checking if outgoing smtp connections can be made by connecting to gmail.com mx on port 25...")
588 mxctx, mxcancel := context.WithTimeout(context.Background(), 5*time.Second)
589 mx, _, err := resolver.LookupMX(mxctx, "gmail.com.")
591 if err == nil && len(mx) == 0 {
592 err = errors.New("no mx records")
596 fmt.Printf("\n\nERROR: looking up gmail.com mx record: %s\n", err)
598 dialctx, dialcancel := context.WithTimeout(context.Background(), 10*time.Second)
600 addr := net.JoinHostPort(mx[0].Host, "25")
601 conn, err := d.DialContext(dialctx, "tcp", addr)
604 fmt.Printf("\n\nERROR: connecting to %s: %s\n", addr, err)
613WARNING: Could not verify outgoing smtp connections can be made, outgoing
614delivery may not be working. Many providers block outgoing smtp connections by
615default, requiring an explicit request or a cooldown period before allowing
616outgoing smtp connections. To send through a smarthost, configure a "Transport"
617in mox.conf and use it in "Routes" in domains.conf. See
618"mox config example transport".
624 zones := []dns.Domain{
625 {ASCII: "sbl.spamhaus.org"},
626 {ASCII: "bl.spamcop.net"},
628 if len(hostIPs) > 0 {
629 fmt.Printf("Checking whether host name IPs are listed in popular DNS block lists...")
631 for _, zone := range zones {
632 for _, ip := range hostIPs {
633 dnsblctx, dnsblcancel := context.WithTimeout(context.Background(), 5*time.Second)
634 status, expl, err := dnsbl.Lookup(dnsblctx, c.log.Logger, resolver, zone, net.ParseIP(ip))
636 if status == dnsbl.StatusPass {
641 errstr = fmt.Sprintf(" (%s)", err)
643 fmt.Printf("\nWARNING: checking your public IP %s in DNS block list %s: %v %s%s", ip, zone.Name(), status, expl, errstr)
649Other mail servers are likely to reject email from IPs that are in a blocklist.
650If all your IPs are in block lists, you will encounter problems delivering
651email. Your IP may be in block lists only temporarily. To see if your IPs are
652listed in more DNS block lists, visit:
655 for _, ip := range hostIPs {
656 fmt.Printf("- https://multirbl.valli.org/lookup/%s.html\n", url.PathEscape(ip))
664 if defaultPublicListenerIPs {
666WARNING: Could not find your public IP address(es). The "public" listener is
667configured to listen on 0.0.0.0 (IPv4) and :: (IPv6). If you don't change these
668to your actual public IP addresses, you will likely get "address in use" errors
669when starting mox because the "internal" listener binds to a specific IP
670address on the same port(s). If you are behind a NAT, instead configure the
671actual public IPs in the listener's "NATIPs" option.
675 if len(publicNATIPs) > 0 {
677NOTE: Quickstart used the IPs of the host name of the mail server, but only
678found private IPs on the machine. This indicates this machine is behind a NAT,
679so the host IPs were configured in the NATIPs field of the public listeners. If
680you are behind a NAT that does not preserve the remote IPs of connections, you
681will likely experience problems accepting email due to IP-based policies. For
682example, SPF is a mechanism that checks if an IP address is allowed to send
683email for a domain, and mox uses IP-based (non)junk classification, and IP-based
684rate-limiting both for accepting email and blocking bad actors (such as with too
685many authentication failures).
697 dc := config.Dynamic{}
699 DataDir: filepath.FromSlash("../data"),
701 LogLevel: "debug", // Help new users, they'll bring it back to info when it all works.
702 Hostname: dnshostname.Name(),
703 AdminPasswordFile: "adminpasswd",
706 // todo: let user specify an alternative fallback address?
707 // Don't attempt to use a non-ascii localpart with Let's Encrypt, it won't work.
708 // Messages to postmaster will get to the account too.
709 var contactEmail string
710 if addr.Localpart.IsInternational() {
711 contactEmail = smtp.NewAddress("postmaster", addr.Domain).Pack(false)
713 contactEmail = addr.Pack(false)
715 if !existingWebserver {
716 sc.ACME = map[string]config.ACME{
718 DirectoryURL: "https://acme-v02.api.letsencrypt.org/directory",
719 ContactEmail: contactEmail,
720 IssuerDomainName: "letsencrypt.org",
725 dataDir := "data" // ../data is relative to config/
726 os.MkdirAll(dataDir, 0770)
728 adminpwhash, err := bcrypt.GenerateFromPassword([]byte(adminpw), bcrypt.DefaultCost)
730 fatalf("generating hash for generated admin password: %s", err)
732 xwritefile(filepath.Join("config", sc.AdminPasswordFile), adminpwhash, 0660)
733 fmt.Printf("Admin password: %s\n", adminpw)
735 public := config.Listener{
736 IPs: publicListenerIPs,
737 NATIPs: publicNATIPs,
739 public.SMTP.Enabled = true
740 public.Submissions.Enabled = true
741 public.IMAPS.Enabled = true
743 if existingWebserver {
744 hostbase := filepath.FromSlash("path/to/" + dnshostname.Name())
745 mtastsbase := filepath.FromSlash("path/to/mta-sts." + domain.Name())
746 autoconfigbase := filepath.FromSlash("path/to/autoconfig." + domain.Name())
747 mailbase := filepath.FromSlash("path/to/mail." + domain.Name())
748 public.TLS = &config.TLS{
749 KeyCerts: []config.KeyCert{
750 {CertFile: hostbase + "-chain.crt.pem", KeyFile: hostbase + ".key.pem"},
751 {CertFile: mtastsbase + "-chain.crt.pem", KeyFile: mtastsbase + ".key.pem"},
752 {CertFile: autoconfigbase + "-chain.crt.pem", KeyFile: autoconfigbase + ".key.pem"},
755 if mailbase != hostbase {
756 public.TLS.KeyCerts = append(public.TLS.KeyCerts, config.KeyCert{CertFile: mailbase + "-chain.crt.pem", KeyFile: mailbase + ".key.pem"})
760 `Placeholder paths to TLS certificates to be provided by the existing webserver
761have been placed in config/mox.conf and need to be edited.
763No private keys for the public listener have been generated for use with DANE.
764To configure DANE (which requires DNSSEC), set config field HostPrivateKeyFiles
765in the "public" Listener to both RSA 2048-bit and ECDSA P-256 private key files
766and check the admin page for the needed DNS records.`)
769 // todo: we may want to generate a second set of keys, make the user already add it to the DNS, but keep the private key offline. would require config option to specify a public key only, so the dane records can be generated.
770 hostRSAPrivateKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
772 fatalf("generating rsa private key for host: %s", err)
774 hostECDSAPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
776 fatalf("generating ecsa private key for host: %s", err)
779 timestamp := now.Format("20060102T150405")
780 hostRSAPrivateKeyFile := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", dnshostname.Name(), timestamp, "rsa2048"))
781 hostECDSAPrivateKeyFile := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", dnshostname.Name(), timestamp, "ecdsap256"))
782 xwritehostkeyfile := func(path string, key crypto.Signer) {
783 buf, err := x509.MarshalPKCS8PrivateKey(key)
785 fatalf("marshaling host private key to pkcs8 for %s: %s", path, err)
792 err = pem.Encode(&b, &block)
794 fatalf("pem-encoding host private key file for %s: %s", path, err)
796 xwritefile(path, b.Bytes(), 0600)
798 xwritehostkeyfile(filepath.Join("config", hostRSAPrivateKeyFile), hostRSAPrivateKey)
799 xwritehostkeyfile(filepath.Join("config", hostECDSAPrivateKeyFile), hostECDSAPrivateKey)
801 public.TLS = &config.TLS{
803 HostPrivateKeyFiles: []string{
804 hostRSAPrivateKeyFile,
805 hostECDSAPrivateKeyFile,
807 HostPrivateRSA2048Keys: []crypto.Signer{hostRSAPrivateKey},
808 HostPrivateECDSAP256Keys: []crypto.Signer{hostECDSAPrivateKey},
810 public.AutoconfigHTTPS.Enabled = true
811 public.MTASTSHTTPS.Enabled = true
812 public.WebserverHTTP.Enabled = true
813 public.WebserverHTTPS.Enabled = true
816 // Suggest blocklists, but we'll comment them out after generating the config.
817 for _, zone := range zones {
818 public.SMTP.DNSBLs = append(public.SMTP.DNSBLs, zone.Name())
821 // Monitor DNSBLs by default, without using them for incoming deliveries.
822 for _, zone := range zones {
823 dc.MonitorDNSBLs = append(dc.MonitorDNSBLs, zone.Name())
826 internal := config.Listener{
827 IPs: privateListenerIPs,
828 Hostname: "localhost",
830 internal.AccountHTTP.Enabled = true
831 internal.AdminHTTP.Enabled = true
832 internal.WebmailHTTP.Enabled = true
833 internal.WebAPIHTTP.Enabled = true
834 internal.MetricsHTTP.Enabled = true
835 if existingWebserver {
836 internal.AccountHTTP.Port = 1080
837 internal.AccountHTTP.Forwarded = true
838 internal.AdminHTTP.Port = 1080
839 internal.AdminHTTP.Forwarded = true
840 internal.WebmailHTTP.Port = 1080
841 internal.WebmailHTTP.Forwarded = true
842 internal.WebAPIHTTP.Port = 1080
843 internal.WebAPIHTTP.Forwarded = true
844 internal.AutoconfigHTTPS.Enabled = true
845 internal.AutoconfigHTTPS.Port = 81
846 internal.AutoconfigHTTPS.NonTLS = true
847 internal.MTASTSHTTPS.Enabled = true
848 internal.MTASTSHTTPS.Port = 81
849 internal.MTASTSHTTPS.NonTLS = true
850 internal.WebserverHTTP.Enabled = true
851 internal.WebserverHTTP.Port = 81
854 sc.Listeners = map[string]config.Listener{
856 "internal": internal,
858 sc.Postmaster.Account = accountName
859 sc.Postmaster.Mailbox = "Postmaster"
860 sc.HostTLSRPT.Account = accountName
861 sc.HostTLSRPT.Localpart = "tls-reports"
862 sc.HostTLSRPT.Mailbox = "TLSRPT"
864 mox.ConfigStaticPath = filepath.FromSlash("config/mox.conf")
865 mox.ConfigDynamicPath = filepath.FromSlash("config/domains.conf")
867 mox.Conf.DynamicLastCheck = time.Now() // Prevent error logging by Make calls below.
869 accountConf := admin.MakeAccountConfig(addr)
870 const withMTASTS = true
871 confDomain, keyPaths, err := admin.MakeDomainConfig(context.Background(), domain, dnshostname, accountName, withMTASTS)
873 fatalf("making domain config: %s", err)
875 cleanupPaths = append(cleanupPaths, keyPaths...)
877 dc.Domains = map[string]config.Domain{
878 domain.Name(): confDomain,
880 dc.Accounts = map[string]config.Account{
881 accountName: accountConf,
884 // Build config in memory, so we can easily comment out the DNSBLs config.
885 var sb strings.Builder
886 sc.CheckUpdates = true // Commented out below.
887 if err := sconf.WriteDocs(&sb, &sc); err != nil {
888 fatalf("generating static config: %v", err)
890 confstr := sb.String()
891 confstr = strings.ReplaceAll(confstr, "\nCheckUpdates: true\n", "\n#\n# RECOMMENDED: please enable to stay up to date\n#\n#CheckUpdates: true\n")
892 confstr = strings.ReplaceAll(confstr, "DNSBLs:\n", "#DNSBLs:\n")
893 for _, bl := range public.SMTP.DNSBLs {
894 confstr = strings.ReplaceAll(confstr, "- "+bl+"\n", "#- "+bl+"\n")
896 xwritefile(filepath.FromSlash("config/mox.conf"), []byte(confstr), 0660)
898 // Generate domains config, and add a commented out example for delivery to a mailing list.
900 if err := sconf.WriteDocs(&db, &dc); err != nil {
901 fatalf("generating domains config: %v", err)
904 // This approach is a bit horrible, but it generates a convenient
905 // example that includes the comments. Though it is gone by the first
906 // write of the file by mox.
907 odests := fmt.Sprintf("\t\tDestinations:\n\t\t\t%s: nil\n", addr.String())
908 var destsExample = struct {
909 Destinations map[string]config.Destination
911 Destinations: map[string]config.Destination{
913 Rulesets: []config.Ruleset{
915 VerifiedDomain: "list.example.org",
916 HeadersRegexp: map[string]string{
917 "^list-id$": `<name\.list\.example\.org>`,
919 ListAllowDomain: "list.example.org",
920 Mailbox: "Lists/Example",
926 var destBuf strings.Builder
927 if err := sconf.Describe(&destBuf, destsExample); err != nil {
928 fatalf("describing destination example: %v", err)
930 ndests := odests + "# If you receive email from mailing lists, you may want to configure them like the\n# example below (remove the empty/false SMTPMailRegexp and IsForward).\n# If you are receiving forwarded email, see the IsForwarded option in a Ruleset.\n"
931 for _, line := range strings.Split(destBuf.String(), "\n")[1:] {
932 ndests += "#\t\t" + line + "\n"
934 dconfstr := strings.ReplaceAll(db.String(), odests, ndests)
935 xwritefile(filepath.FromSlash("config/domains.conf"), []byte(dconfstr), 0660)
938 loadTLSKeyCerts := !existingWebserver
939 mc, errs := mox.ParseConfig(context.Background(), c.log, filepath.FromSlash("config/mox.conf"), true, loadTLSKeyCerts, false)
942 log.Printf("checking generated config, multiple errors:")
943 for _, err := range errs {
946 fatalf("aborting due to multiple config errors")
948 fatalf("checking generated config: %s", errs[0])
951 // NOTE: Now that we've prepared the config, we can open the account
952 // and set a passsword, and the public key for the DKIM private keys
953 // are available for generating the DKIM DNS records below.
955 confDomain, ok := mc.Domain(domain)
957 fatalf("cannot find domain in new config")
960 acc, _, err := store.OpenEmail(c.log, args[0])
962 fatalf("open account: %s", err)
964 cleanupPaths = append(cleanupPaths, dataDir, filepath.Join(dataDir, "accounts"), filepath.Join(dataDir, "accounts", accountName), filepath.Join(dataDir, "accounts", accountName, "index.db"))
968 // Kludge to cause no logging to be printed about setting a new password.
969 loglevel := mox.Conf.Log[""]
970 mox.Conf.Log[""] = mlog.LevelWarn
971 mlog.SetConfig(mox.Conf.Log)
972 if err := acc.SetPassword(c.log, password); err != nil {
973 fatalf("setting password: %s", err)
975 mox.Conf.Log[""] = loglevel
976 mlog.SetConfig(mox.Conf.Log)
978 if err := acc.Close(); err != nil {
979 fatalf("closing account: %s", err)
981 fmt.Printf("IMAP, SMTP submission and HTTP account password for %s: %s\n\n", args[0], password)
982 fmt.Printf(`When configuring your email client, use the email address as username. If
983autoconfig/autodiscover does not work, use these settings:
985 printClientConfig(domain)
987 if existingWebserver {
989Configuration files have been written to config/mox.conf and
992Create the DNS records below, by adding them to your zone file or through the
993web interface of your DNS operator. The admin interface can show these same
994records, and has a page to check they have been configured correctly.
996You must configure your existing webserver to forward requests for:
999 https://autoconfig.%s/
1005If it makes it easier to get a TLS certificate for %s, you can add a
1006reverse proxy for that hostname too.
1008You must edit mox.conf and configure the paths to the TLS certificates and keys.
1009The paths are relative to config/ directory that holds mox.conf! To test if your
1010config is valid, run:
1014The DNS records to add:
1015`, domain.ASCII, domain.ASCII, dnshostname.ASCII)
1018Configuration files have been written to config/mox.conf and
1019config/domains.conf. You should review them. Then create the DNS records below,
1020by adding them to your zone file or through the web interface of your DNS
1021operator. You can also skip creating the DNS records and start mox immediately.
1022The admin interface can show these same records, and has a page to check they
1023have been configured correctly. The DNS records to add:
1027 // We do not verify the records exist: If they don't exist, we would only be
1028 // priming dns caches with negative/absent records, causing our "quick setup" to
1029 // appear to fail or take longer than "quick".
1031 records, err := admin.DomainRecords(confDomain, domain, domainDNSSECResult.Authentic, "letsencrypt.org", "")
1033 fatalf("making required DNS records")
1035 fmt.Print("\n\n" + strings.Join(records, "\n") + "\n\n\n\n")
1037 fmt.Printf(`WARNING: The configuration and DNS records above assume you do not currently
1038have email configured for your domain. If you do already have email configured,
1039or if you are sending email for your domain from other machines/services, you
1040should understand the consequences of the DNS records above before
1043 if os.Getenv("MOX_DOCKER") == "" {
1045You can now start mox with "./mox serve", as root.
1049You can now start the mox container.
1053File ownership and permissions are automatically set correctly by mox when
1054starting up. On linux, you may want to enable mox as a systemd service.
1058 // For now, we only give service config instructions for linux when not running in docker.
1059 if runtime.GOOS == "linux" && os.Getenv("MOX_DOCKER") == "" {
1060 pwd, err := os.Getwd()
1062 log.Printf("current working directory: %v", err)
1065 service := strings.ReplaceAll(moxService, "/home/mox", pwd)
1066 xwritefile("mox.service", []byte(service), 0644)
1067 cleanupPaths = append(cleanupPaths, "mox.service")
1068 fmt.Printf(`See mox.service for a systemd service file. To enable and start:
1070 sudo chmod 644 mox.service
1071 sudo systemctl enable $PWD/mox.service
1072 sudo systemctl start mox.service
1073 sudo journalctl -f -u mox.service # See logs
1078After starting mox, the web interfaces are served at:
1080http://localhost/ - account (email address as username)
1081http://localhost/webmail/ - webmail (email address as username)
1082http://localhost/admin/ - admin (empty username)
1084To access these from your browser, run
1085"ssh -L 8080:localhost:80 you@yourmachine" locally and open
1086http://localhost:8080/[...].
1088If you run into problem, have questions/feedback or found a bug, please let us
1089know. Mox needs your help!
1094 if !existingWebserver {
1096PS: If you want to run mox along side an existing webserver that uses port 443
1097and 80, see "mox help quickstart" with the -existing-webserver option.