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/publicsuffix"
39 "github.com/mjl-/mox/rdap"
40 "github.com/mjl-/mox/smtp"
41 "github.com/mjl-/mox/store"
48func cmdQuickstart(c *cmd) {
49 c.params = "[-skipdial] [-existing-webserver] [-hostname host] user@domain [user | uid]"
50 c.help = `Quickstart generates configuration files and prints instructions to quickly set up a mox instance.
52Quickstart writes configuration files, prints initial admin and account
53passwords, DNS records you should create. If you run it on Linux it writes a
54systemd service file and prints commands to enable and start mox as service.
56All output is written to quickstart.log for later reference.
58The user or uid is optional, defaults to "mox", and is the user or uid/gid mox
59will run as after initialization.
61Quickstart assumes mox will run on the machine you run quickstart on and uses
62its host name and public IPs. On many systems the hostname is not a fully
63qualified domain name, but only the first dns "label", e.g. "mail" in case of
64"mail.example.org". If so, quickstart does a reverse DNS lookup to find the
65hostname, and as fallback uses the label plus the domain of the email address
66you specified. Use flag -hostname to explicitly specify the hostname mox will
69Mox is by far easiest to operate if you let it listen on port 443 (HTTPS) and
7080 (HTTP). TLS will be fully automatic with ACME with Let's Encrypt.
72You can run mox along with an existing webserver, but because of MTA-STS and
73autoconfig, you'll need to forward HTTPS traffic for two domains to mox. Run
74"mox quickstart -existing-webserver ..." to generate configuration files and
75instructions for configuring mox along with an existing webserver.
77But please first consider configuring mox on port 443. It can itself serve
78domains with HTTP/HTTPS, including with automatic TLS with ACME, is easily
79configured through both configuration files and admin web interface, and can act
80as a reverse proxy (and static file server for that matter), so you can forward
81traffic to your existing backend applications. Look for "WebHandlers:" in the
82output of "mox config describe-domains" and see the output of
83"mox config example webhandlers".
85 var existingWebserver bool
88 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.")
89 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")
90 c.flag.BoolVar(&skipDial, "skipdial", false, "skip check for outgoing smtp (port 25) connectivity or for domain age with rdap")
92 if len(args) != 1 && len(args) != 2 {
96 // Write all output to quickstart.log.
97 logfile, err := os.Create("quickstart.log")
98 xcheckf(err, "creating quickstart.log")
100 origStdout := os.Stdout
101 origStderr := os.Stderr
102 piper, pipew, err := os.Pipe()
103 xcheckf(err, "creating pipe for logging to logfile")
104 pipec := make(chan struct{})
106 io.Copy(io.MultiWriter(origStdout, logfile), piper)
108 if err := piper.Close(); err != nil {
109 log.Printf("close pipe: %v", err)
112 // A single pipe, so writes to stdout and stderr don't get interleaved.
116 if err := pipew.Close(); err != nil {
117 log.Printf("close pipe: %v", err)
120 os.Stdout = origStdout
121 os.Stderr = origStderr
122 err := logfile.Close()
123 xcheckf(err, "closing quickstart.log")
126 log.SetOutput(os.Stdout)
127 fmt.Printf("(output is also written to quickstart.log)\n\n")
128 defer fmt.Printf("\n(output is also written to quickstart.log)\n")
130 // We take care to cleanup created files when we error out.
131 // We don't want to get a new user into trouble with half of the files
132 // after encountering an error.
134 // We use fatalf instead of log.Fatal* to cleanup files.
135 var cleanupPaths []string
136 fatalf := func(format string, args ...any) {
137 // We remove in reverse order because dirs would have been created first and must
138 // be removed last, after their files have been removed.
139 for i := len(cleanupPaths) - 1; i >= 0; i-- {
141 if err := os.Remove(p); err != nil {
142 log.Printf("cleaning up %q: %s", p, err)
146 log.Printf(format, args...)
151 xwritefile := func(path string, data []byte, perm os.FileMode) {
152 os.MkdirAll(filepath.Dir(path), 0770)
153 f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm)
155 fatalf("creating file %q: %s", path, err)
157 cleanupPaths = append(cleanupPaths, path)
158 _, err = f.Write(data)
163 fatalf("writing file %q: %s", path, err)
167 addr, err := smtp.ParseAddress(args[0])
169 fatalf("parsing email address: %s", err)
171 accountName := addr.Localpart.String()
172 domain := addr.Domain
174 for _, c := range accountName {
176 fmt.Printf(`NOTE: Username %q is not ASCII-only. It is recommended you also configure an
177ASCII-only alias. Both for delivery of email from other systems, and for
185 resolver := dns.StrictResolver{}
186 // We don't want to spend too much total time on the DNS lookups. Because DNS may
187 // not work during quickstart, and we don't want to loop doing requests and having
188 // to wait for a timeout each time.
189 resolveCtx, resolveCancel := context.WithTimeout(context.Background(), 10*time.Second)
190 defer resolveCancel()
192 // Some DNSSEC-verifying resolvers return unauthentic data for ".", so we check "com".
193 fmt.Printf("Checking if DNS resolvers are DNSSEC-verifying...")
194 _, resolverDNSSECResult, err := resolver.LookupNS(resolveCtx, "com.")
197 fatalf("checking dnssec support in resolver: %v", err)
198 } else if !resolverDNSSECResult.Authentic {
201WARNING: It looks like the DNS resolvers configured on your system do not
202verify DNSSEC, or aren't trusted (by having loopback IPs or through "options
203trust-ad" in /etc/resolv.conf). Without DNSSEC, outbound delivery with SMTP
204used unprotected MX records, and SMTP STARTTLS connections cannot verify the TLS
205certificate with DANE (based on a public key in DNS), and will fall back to
206either MTA-STS for verification, or use "opportunistic TLS" with no certificate
209Recommended action: Install unbound, a DNSSEC-verifying recursive DNS resolver,
210ensure it has DNSSEC root keys (see unbound-anchor), and enable support for
211"extended dns errors" (EDE, available since unbound v1.16.0, see below; not
212required, but it gives helpful error messages about DNSSEC failures instead of
213generic DNS SERVFAIL errors). Test with "dig com. ns" and look for "ad"
214(authentic data) in response "flags".
216cat <<EOF >/etc/unbound/unbound.conf.d/ede.conf
222Troubleshooting hints:
223- Ensure /etc/resolv.conf has "nameserver 127.0.0.1". If the IP is 127.0.0.53,
224 DNS resolving is done by systemd-resolved. Make sure "resolvconf" isn't
225 overwriting /etc/resolv.conf (Debian has a package "openresolv" that makes this
226 easier). "dig" also shows to which IP the DNS request was sent.
227- Ensure unbound has DNSSEC root keys available. See unbound config option
228 "auto-trust-anchor-file" and the unbound-anchor command. Ensure the file exists.
229- Run "./mox dns lookup ns com." to simulate the DNSSEC check done by mox. The
230 output should say "with dnssec".
231- The "delv" command can check whether a domain is DNSSEC-signed, but it does
232 its own DNSSEC verification instead of relying on the resolver, so you cannot
233 use it to check whether unbound is verifying DNSSEC correctly.
234- Increase logging in unbound, see options "verbosity" and "log-queries".
241 // We are going to find the (public) IPs to listen on and possibly the host name.
243 // Start with reasonable defaults. We'll replace them specific IPs, if we can find them.
244 privateListenerIPs := []string{"127.0.0.1", "::1"}
245 publicListenerIPs := []string{"0.0.0.0", "::"}
246 var publicNATIPs []string // Actual public IP, but when it is NATed and machine doesn't have direct access.
247 defaultPublicListenerIPs := true
249 // If we find IPs based on network interfaces, {public,private}ListenerIPs are set
250 // based on these values.
251 var loopbackIPs, privateIPs, publicIPs []string
253 // Gather IP addresses for public and private listeners.
254 // We look at each network interface. If an interface has a private address, we
255 // conservatively assume all addresses on that interface are private.
256 ifaces, err := net.Interfaces()
258 fatalf("listing network interfaces: %s", err)
260 parseAddrIP := func(s string) net.IP {
261 if strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]") {
264 ip, _, _ := net.ParseCIDR(s)
267 for _, iface := range ifaces {
268 if iface.Flags&net.FlagUp == 0 {
271 addrs, err := iface.Addrs()
273 fatalf("listing address for network interface: %s", err)
279 // todo: should we detect temporary/ephemeral ipv6 addresses and not add them?
281 for _, addr := range addrs {
282 ip := parseAddrIP(addr.String())
283 if ip.IsInterfaceLocalMulticast() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() || ip.IsMulticast() {
286 if ip.IsLoopback() || ip.IsPrivate() {
292 for _, addr := range addrs {
293 ip := parseAddrIP(addr.String())
297 if ip.IsInterfaceLocalMulticast() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() || ip.IsMulticast() {
302 loopbackIPs = append(loopbackIPs, ip.String())
304 privateIPs = append(privateIPs, ip.String())
307 publicIPs = append(publicIPs, ip.String())
312 var dnshostname dns.Domain
314 hostnameStr, err := os.Hostname()
316 fatalf("hostname: %s", err)
318 if strings.Contains(hostnameStr, ".") {
319 dnshostname, err = dns.ParseDomain(hostnameStr)
321 fatalf("parsing hostname: %v", err)
324 // It seems Linux machines don't have a single FQDN configured. E.g. /etc/hostname
325 // is just the name without domain. We'll look up the names for all IPs, and hope
326 // to find a single FQDN name (with at least 1 dot).
327 names := map[string]struct{}{}
328 if len(publicIPs) > 0 {
329 fmt.Printf("Trying to find hostname by reverse lookup of public IPs %s...", strings.Join(publicIPs, ", "))
332 warnf := func(format string, args ...any) {
334 fmt.Printf("\n%s", fmt.Sprintf(format, args...))
336 for _, ip := range publicIPs {
337 revctx, revcancel := context.WithTimeout(resolveCtx, 5*time.Second)
339 l, _, err := resolver.LookupAddr(revctx, ip)
341 warnf("WARNING: looking up reverse name(s) for %s: %v", ip, err)
343 for _, name := range l {
344 if strings.Contains(name, ".") {
345 names[name] = struct{}{}
349 var nameList []string
350 for k := range names {
351 nameList = append(nameList, strings.TrimRight(k, "."))
353 slices.Sort(nameList)
354 if len(nameList) == 0 {
355 dnshostname, err = dns.ParseDomain(hostnameStr + "." + domain.Name())
358 fatalf("parsing hostname: %v", err)
360 warnf(`WARNING: cannot determine hostname because the system name is not an FQDN and
361no public IPs resolving to an FQDN were found. Quickstart guessed the host name
362below. If it is not correct, please remove the generated config files and run
363quickstart again with the -hostname flag.
368 if len(nameList) > 1 {
369 warnf(`WARNING: multiple hostnames found for the public IPs, using the first of: %s
370If this is not correct, remove the generated config files and run quickstart
371again with the -hostname flag.
372`, strings.Join(nameList, ", "))
374 dnshostname, err = dns.ParseDomain(nameList[0])
377 fatalf("parsing hostname %s: %v", nameList[0], err)
383 fmt.Printf(" found %s\n", dnshostname)
387 // Host name was explicitly configured on command-line. We'll try to use its public
390 dnshostname, err = dns.ParseDomain(hostname)
392 fatalf("parsing hostname: %v", err)
396 fmt.Printf("Looking up IPs for hostname %s...", dnshostname)
397 ipctx, ipcancel := context.WithTimeout(resolveCtx, 5*time.Second)
399 ips, domainDNSSECResult, err := resolver.LookupIPAddr(ipctx, dnshostname.ASCII+".")
401 var xips []net.IPAddr
404 hostPrivate := len(ips) > 0
405 for _, ip := range ips {
406 if !ip.IP.IsPrivate() {
409 // During linux install, you may get an alias for you full hostname in /etc/hosts
410 // resolving to 127.0.1.1, which would result in a false positive about the
411 // hostname having a record. Filter it out. It is a bit surprising that hosts don't
412 // otherwise know their FQDN.
413 if ip.IP.IsLoopback() {
415 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))
418 xips = append(xips, ip)
419 hostIPs = append(hostIPs, ip.String())
421 if err == nil && len(xips) == 0 {
422 // todo: possibly check this by trying to resolve without using /etc/hosts?
423 err = errors.New("hostname not in dns, probably only in /etc/hosts")
427 // We may have found private and public IPs on the machine, and IPs for the host
428 // name we think we should use. They may not match with each other. E.g. the public
429 // IPs on interfaces could be different from the IPs for the host. We don't try to
430 // detect all possible configs, but just generate what makes sense given whether we
431 // found public/private/hostname IPs. If the user is doing sensible things, it
432 // should be correct. But they should be checking the generated config file anyway.
433 // And we do log which host name we are using, and whether we detected a NAT setup.
434 // In the future, we may do an interactive setup that can guide the user better.
436 if !hostPrivate && len(publicIPs) == 0 && len(privateIPs) > 0 {
437 // We only have private IPs, assume we are behind a NAT and put the IPs of the host in NATIPs.
438 publicListenerIPs = privateIPs
439 publicNATIPs = hostIPs
440 defaultPublicListenerIPs = false
441 if len(loopbackIPs) > 0 {
442 privateListenerIPs = loopbackIPs
445 if len(hostIPs) > 0 {
446 publicListenerIPs = hostIPs
447 defaultPublicListenerIPs = false
449 // Only keep private IPs that are not in host-based publicListenerIPs. For
450 // internal-only setups, including integration tests.
451 m := map[string]bool{}
452 for _, ip := range hostIPs {
456 for _, ip := range privateIPs {
458 npriv = append(npriv, ip)
463 } else if len(publicIPs) > 0 {
464 publicListenerIPs = publicIPs
465 defaultPublicListenerIPs = false
466 hostIPs = publicIPs // For DNSBL check below.
468 if len(privateIPs) > 0 {
469 privateListenerIPs = append(privateIPs, loopbackIPs...)
470 } else if len(loopbackIPs) > 0 {
471 privateListenerIPs = loopbackIPs
480WARNING: Quickstart assumed the hostname of this machine is %s and generates a
481config for that host, but could not retrieve that name from DNS:
485This likely means one of two things:
4871. You don't have any DNS records for this machine at all. You should add them
4892. The hostname mentioned is not the correct host name of this machine. You will
490 have to replace the hostname in the suggested DNS records and generated
491 config/mox.conf file. Make sure your hostname resolves to your public IPs, and
492 your public IPs resolve back (reverse) to your hostname.
496 } else if !domainDNSSECResult.Authentic {
502NOTE: It looks like the DNS records of your domain (zone) are not DNSSEC-signed.
503Mail servers that send email to your domain, or receive email from your domain,
504cannot verify that the MX/SPF/DKIM/DMARC/MTA-STS records they receive are
505authentic. DANE, for authenticated delivery without relying on a pool of
506certificate authorities, requires DNSSEC, so will not be configured at this
508Recommended action: Continue now, but consider enabling DNSSEC for your domain
509later at your DNS operator, and adding DANE records for protecting incoming
524 results := make(chan result)
525 for _, ip := range ips {
529 revctx, revcancel := context.WithTimeout(resolveCtx, 5*time.Second)
531 addrs, _, err := resolver.LookupAddr(revctx, s)
532 results <- result{s, addrs, err}
535 fmt.Printf("Looking up reverse names for IP(s) %s...", strings.Join(l, ", "))
537 warnf := func(format string, args ...any) {
538 fmt.Printf("\nWARNING: %s", fmt.Sprintf(format, args...))
544 warnf("looking up reverse name for %s: %v", r.IP, r.Err)
547 if len(r.Addrs) != 1 {
548 warnf("expected exactly 1 name for %s, got %d (%v)", r.IP, len(r.Addrs), r.Addrs)
551 for i, a := range r.Addrs {
552 a = strings.TrimRight(a, ".")
553 r.Addrs[i] = a // For potential error message below.
554 d, err := dns.ParseDomain(a)
556 warnf("parsing reverse name %q for %s: %v", a, r.IP, err)
558 if d == dnshostname {
563 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)
574 // Check outgoing SMTP connectivity.
575 fmt.Printf("Checking if outgoing smtp connections can be made by connecting to gmail.com mx on port 25...")
576 mxctx, mxcancel := context.WithTimeout(context.Background(), 5*time.Second)
577 mx, _, err := resolver.LookupMX(mxctx, "gmail.com.")
579 if err == nil && len(mx) == 0 {
580 err = errors.New("no mx records")
584 fmt.Printf("\n\nERROR: looking up gmail.com mx record: %s\n", err)
586 dialctx, dialcancel := context.WithTimeout(context.Background(), 10*time.Second)
588 addr := net.JoinHostPort(mx[0].Host, "25")
589 conn, err := d.DialContext(dialctx, "tcp", addr)
592 fmt.Printf("\n\nERROR: connecting to %s: %s\n", addr, err)
594 if err := conn.Close(); err != nil {
595 log.Printf("closing smtp connection: %v", err)
603WARNING: Could not verify outgoing smtp connections can be made, outgoing
604delivery may not be working. Many providers block outgoing smtp connections by
605default, requiring an explicit request or a cooldown period before allowing
606outgoing smtp connections. To send through a smarthost, configure a "Transport"
607in mox.conf and use it in "Routes" in domains.conf. See
608"mox config example transport".
613 // Check if domain is recently registered.
614 rdapctx, rdapcancel := context.WithTimeout(context.Background(), 10*time.Second)
616 orgdom := publicsuffix.Lookup(rdapctx, c.log.Logger, domain)
617 fmt.Printf("\nChecking if domain %s was registered recently...", orgdom)
618 registration, err := rdap.LookupLastDomainRegistration(rdapctx, c.log, orgdom)
621 fmt.Printf(" error: %s (continuing)\n\n", err)
623 age := time.Since(registration)
624 const day = 24 * time.Hour
625 const year = 365 * day
627 days := (age - years*year) / day
631 } else if years > 0 {
632 s = fmt.Sprintf("%d years, ", years)
637 s += fmt.Sprintf("%d days", days)
640 // 6 weeks is a guess, mail servers/service providers will have different policies.
642 fmt.Printf(" (recent!)\nWARNING: Mail servers may treat messages coming from recently registered domains\n(in the order of weeks to months) with suspicion, with higher probability of\nmessages being classified as junk.\n\n")
644 fmt.Printf(" OK\n\n")
649 zones := []dns.Domain{
650 {ASCII: "sbl.spamhaus.org"},
651 {ASCII: "bl.spamcop.net"},
653 if len(hostIPs) > 0 {
654 fmt.Printf("Checking whether host name IPs are listed in popular DNS block lists...")
656 for _, zone := range zones {
657 for _, ip := range hostIPs {
658 dnsblctx, dnsblcancel := context.WithTimeout(context.Background(), 5*time.Second)
659 status, expl, err := dnsbl.Lookup(dnsblctx, c.log.Logger, resolver, zone, net.ParseIP(ip))
661 if status == dnsbl.StatusPass {
666 errstr = fmt.Sprintf(" (%s)", err)
668 fmt.Printf("\nWARNING: checking your public IP %s in DNS block list %s: %v %s%s", ip, zone.Name(), status, expl, errstr)
674Other mail servers are likely to reject email from IPs that are in a blocklist.
675If all your IPs are in block lists, you will encounter problems delivering
676email. Your IP may be in block lists only temporarily. To see if your IPs are
677listed in more DNS block lists, visit:
680 for _, ip := range hostIPs {
681 fmt.Printf("- https://multirbl.valli.org/lookup/%s.html\n", url.PathEscape(ip))
689 if defaultPublicListenerIPs {
691WARNING: Could not find your public IP address(es). The "public" listener is
692configured to listen on 0.0.0.0 (IPv4) and :: (IPv6). If you don't change these
693to your actual public IP addresses, you will likely get "address in use" errors
694when starting mox because the "internal" listener binds to a specific IP
695address on the same port(s). If you are behind a NAT, instead configure the
696actual public IPs in the listener's "NATIPs" option.
700 if len(publicNATIPs) > 0 {
702NOTE: Quickstart used the IPs of the host name of the mail server, but only
703found private IPs on the machine. This indicates this machine is behind a NAT,
704so the host IPs were configured in the NATIPs field of the public listeners. If
705you are behind a NAT that does not preserve the remote IPs of connections, you
706will likely experience problems accepting email due to IP-based policies. For
707example, SPF is a mechanism that checks if an IP address is allowed to send
708email for a domain, and mox uses IP-based (non)junk classification, and IP-based
709rate-limiting both for accepting email and blocking bad actors (such as with too
710many authentication failures).
722 dc := config.Dynamic{}
724 DataDir: filepath.FromSlash("../data"),
726 LogLevel: "debug", // Help new users, they'll bring it back to info when it all works.
727 Hostname: dnshostname.Name(),
728 AdminPasswordFile: "adminpasswd",
731 // todo: let user specify an alternative fallback address?
732 // Don't attempt to use a non-ascii localpart with Let's Encrypt, it won't work.
733 // Messages to postmaster will get to the account too.
734 var contactEmail string
735 if addr.Localpart.IsInternational() {
736 contactEmail = smtp.NewAddress("postmaster", addr.Domain).Pack(false)
738 contactEmail = addr.Pack(false)
740 if !existingWebserver {
741 sc.ACME = map[string]config.ACME{
743 DirectoryURL: "https://acme-v02.api.letsencrypt.org/directory",
744 ContactEmail: contactEmail,
745 IssuerDomainName: "letsencrypt.org",
750 dataDir := "data" // ../data is relative to config/
751 os.MkdirAll(dataDir, 0770)
752 adminpw := mox.GeneratePassword()
753 adminpwhash, err := bcrypt.GenerateFromPassword([]byte(adminpw), bcrypt.DefaultCost)
755 fatalf("generating hash for generated admin password: %s", err)
757 xwritefile(filepath.Join("config", sc.AdminPasswordFile), adminpwhash, 0660)
758 fmt.Printf("Admin password: %s\n", adminpw)
760 public := config.Listener{
761 IPs: publicListenerIPs,
762 NATIPs: publicNATIPs,
764 public.SMTP.Enabled = true
765 public.Submissions.Enabled = true
766 public.IMAPS.Enabled = true
768 if existingWebserver {
769 hostbase := filepath.FromSlash("path/to/" + dnshostname.Name())
770 mtastsbase := filepath.FromSlash("path/to/mta-sts." + domain.Name())
771 autoconfigbase := filepath.FromSlash("path/to/autoconfig." + domain.Name())
772 mailbase := filepath.FromSlash("path/to/mail." + domain.Name())
773 public.TLS = &config.TLS{
774 KeyCerts: []config.KeyCert{
775 {CertFile: hostbase + "-chain.crt.pem", KeyFile: hostbase + ".key.pem"},
776 {CertFile: mtastsbase + "-chain.crt.pem", KeyFile: mtastsbase + ".key.pem"},
777 {CertFile: autoconfigbase + "-chain.crt.pem", KeyFile: autoconfigbase + ".key.pem"},
780 if mailbase != hostbase {
781 public.TLS.KeyCerts = append(public.TLS.KeyCerts, config.KeyCert{CertFile: mailbase + "-chain.crt.pem", KeyFile: mailbase + ".key.pem"})
785 `Placeholder paths to TLS certificates to be provided by the existing webserver
786have been placed in config/mox.conf and need to be edited.
788No private keys for the public listener have been generated for use with DANE.
789To configure DANE (which requires DNSSEC), set config field HostPrivateKeyFiles
790in the "public" Listener to both RSA 2048-bit and ECDSA P-256 private key files
791and check the admin page for the needed DNS records.`)
794 // 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.
795 hostRSAPrivateKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
797 fatalf("generating rsa private key for host: %s", err)
799 hostECDSAPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
801 fatalf("generating ecsa private key for host: %s", err)
804 timestamp := now.Format("20060102T150405")
805 hostRSAPrivateKeyFile := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", dnshostname.Name(), timestamp, "rsa2048"))
806 hostECDSAPrivateKeyFile := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", dnshostname.Name(), timestamp, "ecdsap256"))
807 xwritehostkeyfile := func(path string, key crypto.Signer) {
808 buf, err := x509.MarshalPKCS8PrivateKey(key)
810 fatalf("marshaling host private key to pkcs8 for %s: %s", path, err)
817 err = pem.Encode(&b, &block)
819 fatalf("pem-encoding host private key file for %s: %s", path, err)
821 xwritefile(path, b.Bytes(), 0600)
823 xwritehostkeyfile(filepath.Join("config", hostRSAPrivateKeyFile), hostRSAPrivateKey)
824 xwritehostkeyfile(filepath.Join("config", hostECDSAPrivateKeyFile), hostECDSAPrivateKey)
826 public.TLS = &config.TLS{
828 HostPrivateKeyFiles: []string{
829 hostRSAPrivateKeyFile,
830 hostECDSAPrivateKeyFile,
832 HostPrivateRSA2048Keys: []crypto.Signer{hostRSAPrivateKey},
833 HostPrivateECDSAP256Keys: []crypto.Signer{hostECDSAPrivateKey},
835 public.AutoconfigHTTPS.Enabled = true
836 public.MTASTSHTTPS.Enabled = true
837 public.WebserverHTTP.Enabled = true
838 public.WebserverHTTPS.Enabled = true
841 // Suggest blocklists, but we'll comment them out after generating the config.
842 for _, zone := range zones {
843 public.SMTP.DNSBLs = append(public.SMTP.DNSBLs, zone.Name())
846 // Monitor DNSBLs by default, without using them for incoming deliveries.
847 for _, zone := range zones {
848 dc.MonitorDNSBLs = append(dc.MonitorDNSBLs, zone.Name())
851 internal := config.Listener{
852 IPs: privateListenerIPs,
853 Hostname: "localhost",
855 internal.AccountHTTP.Enabled = true
856 internal.AdminHTTP.Enabled = true
857 internal.WebmailHTTP.Enabled = true
858 internal.WebAPIHTTP.Enabled = true
859 internal.MetricsHTTP.Enabled = true
860 if existingWebserver {
861 internal.AccountHTTP.Port = 1080
862 internal.AccountHTTP.Forwarded = true
863 internal.AdminHTTP.Port = 1080
864 internal.AdminHTTP.Forwarded = true
865 internal.WebmailHTTP.Port = 1080
866 internal.WebmailHTTP.Forwarded = true
867 internal.WebAPIHTTP.Port = 1080
868 internal.WebAPIHTTP.Forwarded = true
869 internal.AutoconfigHTTPS.Enabled = true
870 internal.AutoconfigHTTPS.Port = 81
871 internal.AutoconfigHTTPS.NonTLS = true
872 internal.MTASTSHTTPS.Enabled = true
873 internal.MTASTSHTTPS.Port = 81
874 internal.MTASTSHTTPS.NonTLS = true
875 internal.WebserverHTTP.Enabled = true
876 internal.WebserverHTTP.Port = 81
879 sc.Listeners = map[string]config.Listener{
881 "internal": internal,
883 sc.Postmaster.Account = accountName
884 sc.Postmaster.Mailbox = "Postmaster"
885 sc.HostTLSRPT.Account = accountName
886 sc.HostTLSRPT.Localpart = "tls-reports"
887 sc.HostTLSRPT.Mailbox = "TLSRPT"
889 mox.ConfigStaticPath = filepath.FromSlash("config/mox.conf")
890 mox.ConfigDynamicPath = filepath.FromSlash("config/domains.conf")
892 mox.Conf.DynamicLastCheck = time.Now() // Prevent error logging by Make calls below.
894 accountConf := admin.MakeAccountConfig(addr)
895 const withMTASTS = true
896 confDomain, keyPaths, err := admin.MakeDomainConfig(context.Background(), domain, dnshostname, accountName, withMTASTS)
898 fatalf("making domain config: %s", err)
900 cleanupPaths = append(cleanupPaths, keyPaths...)
902 dc.Domains = map[string]config.Domain{
903 domain.Name(): confDomain,
905 dc.Accounts = map[string]config.Account{
906 accountName: accountConf,
909 // Build config in memory, so we can easily comment out the DNSBLs config.
910 var sb strings.Builder
911 sc.CheckUpdates = true // Commented out below.
912 if err := sconf.WriteDocs(&sb, &sc); err != nil {
913 fatalf("generating static config: %v", err)
915 confstr := sb.String()
916 confstr = strings.ReplaceAll(confstr, "\nCheckUpdates: true\n", "\n#\n# RECOMMENDED: please enable to stay up to date\n#\n#CheckUpdates: true\n")
917 confstr = strings.ReplaceAll(confstr, "DNSBLs:\n", "#DNSBLs:\n")
918 for _, bl := range public.SMTP.DNSBLs {
919 confstr = strings.ReplaceAll(confstr, "- "+bl+"\n", "#- "+bl+"\n")
921 xwritefile(filepath.FromSlash("config/mox.conf"), []byte(confstr), 0660)
923 // Generate domains config, and add a commented out example for delivery to a mailing list.
925 if err := sconf.WriteDocs(&db, &dc); err != nil {
926 fatalf("generating domains config: %v", err)
929 // This approach is a bit horrible, but it generates a convenient
930 // example that includes the comments. Though it is gone by the first
931 // write of the file by mox.
932 odests := fmt.Sprintf("\t\tDestinations:\n\t\t\t%s: nil\n", addr.String())
933 var destsExample = struct {
934 Destinations map[string]config.Destination
936 Destinations: map[string]config.Destination{
938 Rulesets: []config.Ruleset{
940 VerifiedDomain: "list.example.org",
941 HeadersRegexp: map[string]string{
942 "^list-id$": `<name\.list\.example\.org>`,
944 ListAllowDomain: "list.example.org",
945 Mailbox: "Lists/Example",
951 var destBuf strings.Builder
952 if err := sconf.Describe(&destBuf, destsExample); err != nil {
953 fatalf("describing destination example: %v", err)
955 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"
956 for _, line := range strings.Split(destBuf.String(), "\n")[1:] {
957 ndests += "#\t\t" + line + "\n"
959 dconfstr := strings.ReplaceAll(db.String(), odests, ndests)
960 xwritefile(filepath.FromSlash("config/domains.conf"), []byte(dconfstr), 0660)
963 loadTLSKeyCerts := !existingWebserver
964 mc, errs := mox.ParseConfig(context.Background(), c.log, filepath.FromSlash("config/mox.conf"), true, loadTLSKeyCerts, false)
967 log.Printf("checking generated config, multiple errors:")
968 for _, err := range errs {
971 fatalf("aborting due to multiple config errors")
973 fatalf("checking generated config: %s", errs[0])
976 // NOTE: Now that we've prepared the config, we can open the account
977 // and set a passsword, and the public key for the DKIM private keys
978 // are available for generating the DKIM DNS records below.
980 confDomain, ok := mc.Domain(domain)
982 fatalf("cannot find domain in new config")
985 acc, _, _, err := store.OpenEmail(c.log, args[0], false)
987 fatalf("open account: %s", err)
989 cleanupPaths = append(cleanupPaths, dataDir, filepath.Join(dataDir, "accounts"), filepath.Join(dataDir, "accounts", accountName), filepath.Join(dataDir, "accounts", accountName, "index.db"))
991 password := mox.GeneratePassword()
993 // Kludge to cause no logging to be printed about setting a new password.
994 loglevel := mox.Conf.Log[""]
995 mox.Conf.Log[""] = mlog.LevelWarn
996 mlog.SetConfig(mox.Conf.Log)
997 if err := acc.SetPassword(c.log, password); err != nil {
998 fatalf("setting password: %s", err)
1000 mox.Conf.Log[""] = loglevel
1001 mlog.SetConfig(mox.Conf.Log)
1003 if err := acc.Close(); err != nil {
1004 fatalf("closing account: %s", err)
1006 fmt.Printf("IMAP, SMTP submission and HTTP account password for %s: %s\n\n", args[0], password)
1007 fmt.Printf(`When configuring your email client, use the email address as username. If
1008autoconfig/autodiscover does not work, use these settings:
1010 printClientConfig(domain)
1012 if existingWebserver {
1014Configuration files have been written to config/mox.conf and
1017Create the DNS records below, by adding them to your zone file or through the
1018web interface of your DNS operator. The admin interface can show these same
1019records, and has a page to check they have been configured correctly.
1021You must configure your existing webserver to forward requests for:
1024 https://autoconfig.%s/
1030If it makes it easier to get a TLS certificate for %s, you can add a
1031reverse proxy for that hostname too.
1033You must edit mox.conf and configure the paths to the TLS certificates and keys.
1034The paths are relative to config/ directory that holds mox.conf! To test if your
1035config is valid, run:
1039The DNS records to add:
1040`, domain.ASCII, domain.ASCII, dnshostname.ASCII)
1043Configuration files have been written to config/mox.conf and
1044config/domains.conf. You should review them. Then create the DNS records below,
1045by adding them to your zone file or through the web interface of your DNS
1046operator. You can also skip creating the DNS records and start mox immediately.
1047The admin interface can show these same records, and has a page to check they
1048have been configured correctly. The DNS records to add:
1052 // We do not verify the records exist: If they don't exist, we would only be
1053 // priming dns caches with negative/absent records, causing our "quick setup" to
1054 // appear to fail or take longer than "quick".
1056 records, err := admin.DomainRecords(confDomain, domain, domainDNSSECResult.Authentic, "letsencrypt.org", "")
1058 fatalf("making required DNS records")
1060 fmt.Print("\n\n" + strings.Join(records, "\n") + "\n\n\n\n")
1062 fmt.Printf(`WARNING: The configuration and DNS records above assume you do not currently
1063have email configured for your domain. If you do already have email configured,
1064or if you are sending email for your domain from other machines/services, you
1065should understand the consequences of the DNS records above before
1068 if os.Getenv("MOX_DOCKER") == "" {
1070You can now start mox with "./mox serve", as root.
1074You can now start the mox container.
1078File ownership and permissions are automatically set correctly by mox when
1079starting up. On linux, you may want to enable mox as a systemd service.
1083 // For now, we only give service config instructions for linux when not running in docker.
1084 if runtime.GOOS == "linux" && os.Getenv("MOX_DOCKER") == "" {
1085 pwd, err := os.Getwd()
1087 log.Printf("current working directory: %v", err)
1090 service := strings.ReplaceAll(moxService, "/home/mox", pwd)
1091 xwritefile("mox.service", []byte(service), 0644)
1092 cleanupPaths = append(cleanupPaths, "mox.service")
1093 fmt.Printf(`See mox.service for a systemd service file. To enable and start:
1095 sudo chmod 644 mox.service
1096 sudo systemctl enable $PWD/mox.service
1097 sudo systemctl start mox.service
1098 sudo journalctl -f -u mox.service # See logs
1103After starting mox, the web interfaces are served at:
1105http://localhost/ - account (email address as username)
1106http://localhost/webmail/ - webmail (email address as username)
1107http://localhost/admin/ - admin (empty username)
1109To access these from your browser, run
1110"ssh -L 8080:localhost:80 you@yourmachine" locally and open
1111http://localhost:8080/[...].
1113If you run into problem, have questions/feedback or found a bug, please let us
1114know. Mox needs your help!
1119 if !existingWebserver {
1121PS: If you want to run mox along side an existing webserver that uses port 443
1122and 80, see "mox help quickstart" with the -existing-webserver option.