9 cryptorand "crypto/rand"
27 "golang.org/x/crypto/bcrypt"
29 "github.com/mjl-/sconf"
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"
44 chars := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*-_;:,<.>/"
46 buf := make([]byte, 1)
47 for i := 0; i < 12; i++ {
51 if i+len(chars) > 255 {
52 continue // Prevent bias.
54 s += string(chars[i%len(chars)])
61func cmdQuickstart(c *cmd) {
62 c.params = "[-existing-webserver] [-hostname host] user@domain [user | uid]"
63 c.help = `Quickstart generates configuration files and prints instructions to quickly set up a mox instance.
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.
69The user or uid is optional, defaults to "mox", and is the user or uid/gid mox
70will run as after initialization.
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
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.
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.
88But please first consider configuring mox on port 443. It can itself serve
89domains with HTTP/HTTPS, including with automatic TLS with ACME, is easily
90configured through both configuration files and admin web interface, and can act
91as a reverse proxy (and static file server for that matter), so you can forward
92traffic to your existing backend applications. Look for "WebHandlers:" in the
93output of "mox config describe-domains" and see the output of "mox example
96 var existingWebserver bool
98 c.flag.BoolVar(&existingWebserver, "existing-webserver", false, "use if a webserver is already running, so mox won't listen on port 80 and 443; you'll have to provide tls certificates/keys, and configure the existing webserver as reverse proxy, forwarding requests to mox.")
99 c.flag.StringVar(&hostname, "hostname", "", "hostname mox will run on, by default the hostname of the machine quickstart runs on; if specified, the IPs for the hostname are configured for the public listener")
101 if len(args) != 1 && len(args) != 2 {
105 // We take care to cleanup created files when we error out.
106 // We don't want to get a new user into trouble with half of the files
107 // after encountering an error.
109 // We use fatalf instead of log.Fatal* to cleanup files.
110 var cleanupPaths []string
111 fatalf := func(format string, args ...any) {
112 // We remove in reverse order because dirs would have been created first and must
113 // be removed last, after their files have been removed.
114 for i := len(cleanupPaths) - 1; i >= 0; i-- {
116 if err := os.Remove(p); err != nil {
117 log.Printf("cleaning up %q: %s", p, err)
121 log.Fatalf(format, args...)
124 xwritefile := func(path string, data []byte, perm os.FileMode) {
125 os.MkdirAll(filepath.Dir(path), 0770)
126 f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm)
128 fatalf("creating file %q: %s", path, err)
130 cleanupPaths = append(cleanupPaths, path)
131 _, err = f.Write(data)
136 fatalf("writing file %q: %s", path, err)
140 addr, err := smtp.ParseAddress(args[0])
142 fatalf("parsing email address: %s", err)
144 accountName := addr.Localpart.String()
145 domain := addr.Domain
147 for _, c := range accountName {
149 fmt.Printf(`NOTE: Username %q is not ASCII-only. It is recommended you also configure an
150ASCII-only alias. Both for delivery of email from other systems, and for
158 resolver := dns.StrictResolver{}
159 // We don't want to spend too much total time on the DNS lookups. Because DNS may
160 // not work during quickstart, and we don't want to loop doing requests and having
161 // to wait for a timeout each time.
162 resolveCtx, resolveCancel := context.WithTimeout(context.Background(), 10*time.Second)
163 defer resolveCancel()
165 // Some DNSSEC-verifying resolvers return unauthentic data for ".", so we check "com".
166 fmt.Printf("Checking if DNS resolvers are DNSSEC-verifying...")
167 _, resolverDNSSECResult, err := resolver.LookupNS(resolveCtx, "com.")
170 fatalf("checking dnssec support in resolver: %v", err)
171 } else if !resolverDNSSECResult.Authentic {
174WARNING: It looks like the DNS resolvers configured on your system do not
175verify DNSSEC, or aren't trusted (by having loopback IPs or through "options
176trust-ad" in /etc/resolv.conf). Without DNSSEC, outbound delivery with SMTP
177used unprotected MX records, and SMTP STARTTLS connections cannot verify the TLS
178certificate with DANE (based on a public key in DNS), and will fall back to
179either MTA-STS for verification, or use "opportunistic TLS" with no certificate
182Recommended action: Install unbound, a DNSSEC-verifying recursive DNS resolver,
183ensure it has DNSSEC root keys (see unbound-anchor), and enable support for
184"extended dns errors" (EDE, available since unbound v1.16.0). Test with
185"dig com. ns" and look for "ad" (authentic data) in response "flags".
187cat <<EOF >/etc/unbound/unbound.conf.d/ede.conf
198 // We are going to find the (public) IPs to listen on and possibly the host name.
200 // Start with reasonable defaults. We'll replace them specific IPs, if we can find them.
201 privateListenerIPs := []string{"127.0.0.1", "::1"}
202 publicListenerIPs := []string{"0.0.0.0", "::"}
203 var publicNATIPs []string // Actual public IP, but when it is NATed and machine doesn't have direct access.
204 defaultPublicListenerIPs := true
206 // If we find IPs based on network interfaces, {public,private}ListenerIPs are set
207 // based on these values.
208 var loopbackIPs, privateIPs, publicIPs []string
210 // Gather IP addresses for public and private listeners.
211 // We look at each network interface. If an interface has a private address, we
212 // conservatively assume all addresses on that interface are private.
213 ifaces, err := net.Interfaces()
215 fatalf("listing network interfaces: %s", err)
217 parseAddrIP := func(s string) net.IP {
218 if strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]") {
221 ip, _, _ := net.ParseCIDR(s)
224 for _, iface := range ifaces {
225 if iface.Flags&net.FlagUp == 0 {
228 addrs, err := iface.Addrs()
230 fatalf("listing address for network interface: %s", err)
236 // todo: should we detect temporary/ephemeral ipv6 addresses and not add them?
238 for _, addr := range addrs {
239 ip := parseAddrIP(addr.String())
240 if ip.IsInterfaceLocalMulticast() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() || ip.IsMulticast() {
243 if ip.IsLoopback() || ip.IsPrivate() {
249 for _, addr := range addrs {
250 ip := parseAddrIP(addr.String())
254 if ip.IsInterfaceLocalMulticast() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() || ip.IsMulticast() {
259 loopbackIPs = append(loopbackIPs, ip.String())
261 privateIPs = append(privateIPs, ip.String())
264 publicIPs = append(publicIPs, ip.String())
269 var dnshostname dns.Domain
271 hostnameStr, err := os.Hostname()
273 fatalf("hostname: %s", err)
275 if strings.Contains(hostnameStr, ".") {
276 dnshostname, err = dns.ParseDomain(hostnameStr)
278 fatalf("parsing hostname: %v", err)
281 // It seems Linux machines don't have a single FQDN configured. E.g. /etc/hostname
282 // is just the name without domain. We'll look up the names for all IPs, and hope
283 // to find a single FQDN name (with at least 1 dot).
284 names := map[string]struct{}{}
285 if len(publicIPs) > 0 {
286 fmt.Printf("Trying to find hostname by reverse lookup of public IPs %s...", strings.Join(publicIPs, ", "))
289 warnf := func(format string, args ...any) {
291 fmt.Printf("\n%s", fmt.Sprintf(format, args...))
293 for _, ip := range publicIPs {
294 revctx, revcancel := context.WithTimeout(resolveCtx, 5*time.Second)
296 l, _, err := resolver.LookupAddr(revctx, ip)
298 warnf("WARNING: looking up reverse name(s) for %s: %v", ip, err)
300 for _, name := range l {
301 if strings.Contains(name, ".") {
302 names[name] = struct{}{}
306 var nameList []string
307 for k := range names {
308 nameList = append(nameList, strings.TrimRight(k, "."))
310 sort.Slice(nameList, func(i, j int) bool {
311 return nameList[i] < nameList[j]
313 if len(nameList) == 0 {
314 dnshostname, err = dns.ParseDomain(hostnameStr + "." + domain.Name())
317 fatalf("parsing hostname: %v", err)
319 warnf(`WARNING: cannot determine hostname because the system name is not an FQDN and
320no public IPs resolving to an FQDN were found. Quickstart guessed the host name
321below. If it is not correct, please remove the generated config files and run
322quickstart again with the -hostname flag.
327 if len(nameList) > 1 {
328 warnf(`WARNING: multiple hostnames found for the public IPs, using the first of: %s
329If this is not correct, remove the generated config files and run quickstart
330again with the -hostname flag.
331`, strings.Join(nameList, ", "))
333 dnshostname, err = dns.ParseDomain(nameList[0])
336 fatalf("parsing hostname %s: %v", nameList[0], err)
342 fmt.Printf(" found %s\n", dnshostname)
346 // Host name was explicitly configured on command-line. We'll try to use its public
349 dnshostname, err = dns.ParseDomain(hostname)
351 fatalf("parsing hostname: %v", err)
355 fmt.Printf("Looking up IPs for hostname %s...", dnshostname)
356 ipctx, ipcancel := context.WithTimeout(resolveCtx, 5*time.Second)
358 ips, domainDNSSECResult, err := resolver.LookupIPAddr(ipctx, dnshostname.ASCII+".")
360 var xips []net.IPAddr
363 hostPrivate := len(ips) > 0
364 for _, ip := range ips {
365 if !ip.IP.IsPrivate() {
368 // During linux install, you may get an alias for you full hostname in /etc/hosts
369 // resolving to 127.0.1.1, which would result in a false positive about the
370 // hostname having a record. Filter it out. It is a bit surprising that hosts don't
371 // otherwise know their FQDN.
372 if ip.IP.IsLoopback() {
374 fmt.Printf("\n\nWARNING: Your hostname is resolving to a loopback IP address %s. This likely breaks email delivery to local accounts. /etc/hosts likely contains a line like %q. Either replace it with your actual IP(s), or remove the line.\n", ip.IP, fmt.Sprintf("%s %s", ip.IP, dnshostname.ASCII))
377 xips = append(xips, ip)
378 hostIPs = append(hostIPs, ip.String())
380 if err == nil && len(xips) == 0 {
381 // todo: possibly check this by trying to resolve without using /etc/hosts?
382 err = errors.New("hostname not in dns, probably only in /etc/hosts")
386 // We may have found private and public IPs on the machine, and IPs for the host
387 // name we think we should use. They may not match with each other. E.g. the public
388 // IPs on interfaces could be different from the IPs for the host. We don't try to
389 // detect all possible configs, but just generate what makes sense given whether we
390 // found public/private/hostname IPs. If the user is doing sensible things, it
391 // should be correct. But they should be checking the generated config file anyway.
392 // And we do log which host name we are using, and whether we detected a NAT setup.
393 // In the future, we may do an interactive setup that can guide the user better.
395 if !hostPrivate && len(publicIPs) == 0 && len(privateIPs) > 0 {
396 // We only have private IPs, assume we are behind a NAT and put the IPs of the host in NATIPs.
397 publicListenerIPs = privateIPs
398 publicNATIPs = hostIPs
399 defaultPublicListenerIPs = false
400 if len(loopbackIPs) > 0 {
401 privateListenerIPs = loopbackIPs
404 if len(hostIPs) > 0 {
405 publicListenerIPs = hostIPs
406 defaultPublicListenerIPs = false
408 // Only keep private IPs that are not in host-based publicListenerIPs. For
409 // internal-only setups, including integration tests.
410 m := map[string]bool{}
411 for _, ip := range hostIPs {
415 for _, ip := range privateIPs {
417 npriv = append(npriv, ip)
422 } else if len(publicIPs) > 0 {
423 publicListenerIPs = publicIPs
424 defaultPublicListenerIPs = false
425 hostIPs = publicIPs // For DNSBL check below.
427 if len(privateIPs) > 0 {
428 privateListenerIPs = append(privateIPs, loopbackIPs...)
429 } else if len(loopbackIPs) > 0 {
430 privateListenerIPs = loopbackIPs
439WARNING: Quickstart assumed the hostname of this machine is %s and generates a
440config for that host, but could not retrieve that name from DNS:
444This likely means one of two things:
4461. You don't have any DNS records for this machine at all. You should add them
4482. The hostname mentioned is not the correct host name of this machine. You will
449 have to replace the hostname in the suggested DNS records and generated
450 config/mox.conf file. Make sure your hostname resolves to your public IPs, and
451 your public IPs resolve back (reverse) to your hostname.
455 } else if !domainDNSSECResult.Authentic {
461NOTE: It looks like the DNS records of your domain (zone) are not DNSSEC-signed.
462Mail servers that send email to your domain, or receive email from your domain,
463cannot verify that the MX/SPF/DKIM/DMARC/MTA-STS records they receive are
464authentic. DANE, for authenticated delivery without relying on a pool of
465certificate authorities, requires DNSSEC, so will not be configured at this
467Recommended action: Continue now, but consider enabling DNSSEC for your domain
468later at your DNS operator, and adding DANE records for protecting incoming
483 results := make(chan result)
484 for _, ip := range ips {
488 revctx, revcancel := context.WithTimeout(resolveCtx, 5*time.Second)
490 addrs, _, err := resolver.LookupAddr(revctx, s)
491 results <- result{s, addrs, err}
494 fmt.Printf("Looking up reverse names for IP(s) %s...", strings.Join(l, ", "))
496 warnf := func(format string, args ...any) {
497 fmt.Printf("\nWARNING: %s", fmt.Sprintf(format, args...))
500 for i := 0; i < len(ips); i++ {
503 warnf("looking up reverse name for %s: %v", r.IP, r.Err)
506 if len(r.Addrs) != 1 {
507 warnf("expected exactly 1 name for %s, got %d (%v)", r.IP, len(r.Addrs), r.Addrs)
510 for i, a := range r.Addrs {
511 a = strings.TrimRight(a, ".")
512 r.Addrs[i] = a // For potential error message below.
513 d, err := dns.ParseDomain(a)
515 warnf("parsing reverse name %q for %s: %v", a, r.IP, err)
517 if d == dnshostname {
522 warnf("reverse name(s) %s for ip %s do not match hostname %s, which will cause other mail servers to reject incoming messages from this IP", strings.Join(r.Addrs, ","), r.IP, dnshostname)
532 zones := []dns.Domain{
533 {ASCII: "sbl.spamhaus.org"},
534 {ASCII: "bl.spamcop.net"},
536 if len(hostIPs) > 0 {
537 fmt.Printf("Checking whether host name IPs are listed in popular DNS block lists...")
539 for _, zone := range zones {
540 for _, ip := range hostIPs {
541 dnsblctx, dnsblcancel := context.WithTimeout(resolveCtx, 5*time.Second)
542 status, expl, err := dnsbl.Lookup(dnsblctx, c.log.Logger, resolver, zone, net.ParseIP(ip))
544 if status == dnsbl.StatusPass {
549 errstr = fmt.Sprintf(" (%s)", err)
551 fmt.Printf("\nWARNING: checking your public IP %s in DNS block list %s: %v %s%s", ip, zone.Name(), status, expl, errstr)
557Other mail servers are likely to reject email from IPs that are in a blocklist.
558If all your IPs are in block lists, you will encounter problems delivering
559email. Your IP may be in block lists only temporarily. To see if your IPs are
560listed in more DNS block lists, visit:
563 for _, ip := range hostIPs {
564 fmt.Printf("- https://multirbl.valli.org/lookup/%s.html\n", url.PathEscape(ip))
572 if defaultPublicListenerIPs {
574WARNING: Could not find your public IP address(es). The "public" listener is
575configured to listen on 0.0.0.0 (IPv4) and :: (IPv6). If you don't change these
576to your actual public IP addresses, you will likely get "address in use" errors
577when starting mox because the "internal" listener binds to a specific IP
578address on the same port(s). If you are behind a NAT, instead configure the
579actual public IPs in the listener's "NATIPs" option.
583 if len(publicNATIPs) > 0 {
585NOTE: Quickstart used the IPs of the host name of the mail server, but only
586found private IPs on the machine. This indicates this machine is behind a NAT,
587so the host IPs were configured in the NATIPs field of the public listeners. If
588you are behind a NAT that does not preserve the remote IPs of connections, you
589will likely experience problems accepting email due to IP-based policies. For
590example, SPF is a mechanism that checks if an IP address is allowed to send
591email for a domain, and mox uses IP-based (non)junk classification, and IP-based
592rate-limiting both for accepting email and blocking bad actors (such as with too
593many authentication failures).
605 dc := config.Dynamic{}
607 DataDir: filepath.FromSlash("../data"),
609 LogLevel: "debug", // Help new users, they'll bring it back to info when it all works.
610 Hostname: dnshostname.Name(),
611 AdminPasswordFile: "adminpasswd",
613 if !existingWebserver {
614 sc.ACME = map[string]config.ACME{
616 DirectoryURL: "https://acme-v02.api.letsencrypt.org/directory",
617 ContactEmail: args[0], // todo: let user specify an alternative fallback address?
618 IssuerDomainName: "letsencrypt.org",
622 dataDir := "data" // ../data is relative to config/
623 os.MkdirAll(dataDir, 0770)
625 adminpwhash, err := bcrypt.GenerateFromPassword([]byte(adminpw), bcrypt.DefaultCost)
627 fatalf("generating hash for generated admin password: %s", err)
629 xwritefile(filepath.Join("config", sc.AdminPasswordFile), adminpwhash, 0660)
630 fmt.Printf("Admin password: %s\n", adminpw)
632 public := config.Listener{
633 IPs: publicListenerIPs,
634 NATIPs: publicNATIPs,
636 public.SMTP.Enabled = true
637 public.Submissions.Enabled = true
638 public.IMAPS.Enabled = true
640 if existingWebserver {
641 hostbase := filepath.FromSlash("path/to/" + dnshostname.Name())
642 mtastsbase := filepath.FromSlash("path/to/mta-sts." + domain.Name())
643 autoconfigbase := filepath.FromSlash("path/to/autoconfig." + domain.Name())
644 public.TLS = &config.TLS{
645 KeyCerts: []config.KeyCert{
646 {CertFile: hostbase + "-chain.crt.pem", KeyFile: hostbase + ".key.pem"},
647 {CertFile: mtastsbase + "-chain.crt.pem", KeyFile: mtastsbase + ".key.pem"},
648 {CertFile: autoconfigbase + "-chain.crt.pem", KeyFile: autoconfigbase + ".key.pem"},
653 `Placeholder paths to TLS certificates to be provided by the existing webserver
654have been placed in config/mox.conf and need to be edited.
656No private keys for the public listener have been generated for use with DANE.
657To configure DANE (which requires DNSSEC), set config field HostPrivateKeyFiles
658in the "public" Listener to both RSA 2048-bit and ECDSA P-256 private key files
659and check the admin page for the needed DNS records.`)
662 // todo: we may want to generate a second set of keys, make the user already add it to the DNS, but keep the private key offline. would require config option to specify a public key only, so the dane records can be generated.
663 hostRSAPrivateKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
665 fatalf("generating rsa private key for host: %s", err)
667 hostECDSAPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
669 fatalf("generating ecsa private key for host: %s", err)
672 timestamp := now.Format("20060102T150405")
673 hostRSAPrivateKeyFile := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", dnshostname.Name(), timestamp, "rsa2048"))
674 hostECDSAPrivateKeyFile := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", dnshostname.Name(), timestamp, "ecdsap256"))
675 xwritehostkeyfile := func(path string, key crypto.Signer) {
676 buf, err := x509.MarshalPKCS8PrivateKey(key)
678 fatalf("marshaling host private key to pkcs8 for %s: %s", path, err)
685 err = pem.Encode(&b, &block)
687 fatalf("pem-encoding host private key file for %s: %s", path, err)
689 xwritefile(path, b.Bytes(), 0600)
691 xwritehostkeyfile(filepath.Join("config", hostRSAPrivateKeyFile), hostRSAPrivateKey)
692 xwritehostkeyfile(filepath.Join("config", hostECDSAPrivateKeyFile), hostECDSAPrivateKey)
694 public.TLS = &config.TLS{
696 HostPrivateKeyFiles: []string{
697 hostRSAPrivateKeyFile,
698 hostECDSAPrivateKeyFile,
700 HostPrivateRSA2048Keys: []crypto.Signer{hostRSAPrivateKey},
701 HostPrivateECDSAP256Keys: []crypto.Signer{hostECDSAPrivateKey},
703 public.AutoconfigHTTPS.Enabled = true
704 public.MTASTSHTTPS.Enabled = true
705 public.WebserverHTTP.Enabled = true
706 public.WebserverHTTPS.Enabled = true
709 // Suggest blocklists, but we'll comment them out after generating the config.
710 for _, zone := range zones {
711 public.SMTP.DNSBLs = append(public.SMTP.DNSBLs, zone.Name())
714 // Monitor DNSBLs by default, without using them for incoming deliveries.
715 for _, zone := range zones {
716 dc.MonitorDNSBLs = append(dc.MonitorDNSBLs, zone.Name())
719 internal := config.Listener{
720 IPs: privateListenerIPs,
721 Hostname: "localhost",
723 internal.AccountHTTP.Enabled = true
724 internal.AdminHTTP.Enabled = true
725 internal.WebmailHTTP.Enabled = true
726 internal.MetricsHTTP.Enabled = true
727 if existingWebserver {
728 internal.AccountHTTP.Port = 1080
729 internal.AccountHTTP.Forwarded = true
730 internal.AdminHTTP.Port = 1080
731 internal.AdminHTTP.Forwarded = true
732 internal.WebmailHTTP.Port = 1080
733 internal.WebmailHTTP.Forwarded = true
734 internal.AutoconfigHTTPS.Enabled = true
735 internal.AutoconfigHTTPS.Port = 81
736 internal.AutoconfigHTTPS.NonTLS = true
737 internal.MTASTSHTTPS.Enabled = true
738 internal.MTASTSHTTPS.Port = 81
739 internal.MTASTSHTTPS.NonTLS = true
740 internal.WebserverHTTP.Enabled = true
741 internal.WebserverHTTP.Port = 81
744 sc.Listeners = map[string]config.Listener{
746 "internal": internal,
748 sc.Postmaster.Account = accountName
749 sc.Postmaster.Mailbox = "Postmaster"
750 sc.HostTLSRPT.Account = accountName
751 sc.HostTLSRPT.Localpart = "tls-reports"
752 sc.HostTLSRPT.Mailbox = "TLSRPT"
754 mox.ConfigStaticPath = filepath.FromSlash("config/mox.conf")
755 mox.ConfigDynamicPath = filepath.FromSlash("config/domains.conf")
757 mox.Conf.DynamicLastCheck = time.Now() // Prevent error logging by Make calls below.
759 accountConf := mox.MakeAccountConfig(addr)
760 const withMTASTS = true
761 confDomain, keyPaths, err := mox.MakeDomainConfig(context.Background(), domain, dnshostname, accountName, withMTASTS)
763 fatalf("making domain config: %s", err)
765 cleanupPaths = append(cleanupPaths, keyPaths...)
767 dc.Domains = map[string]config.Domain{
768 domain.Name(): confDomain,
770 dc.Accounts = map[string]config.Account{
771 accountName: accountConf,
774 // Build config in memory, so we can easily comment out the DNSBLs config.
775 var sb strings.Builder
776 sc.CheckUpdates = true // Commented out below.
777 if err := sconf.WriteDocs(&sb, &sc); err != nil {
778 fatalf("generating static config: %v", err)
780 confstr := sb.String()
781 confstr = strings.ReplaceAll(confstr, "\nCheckUpdates: true\n", "\n#\n# RECOMMENDED: please enable to stay up to date\n#\n#CheckUpdates: true\n")
782 confstr = strings.ReplaceAll(confstr, "DNSBLs:\n", "#DNSBLs:\n")
783 for _, bl := range public.SMTP.DNSBLs {
784 confstr = strings.ReplaceAll(confstr, "- "+bl+"\n", "#- "+bl+"\n")
786 xwritefile(filepath.FromSlash("config/mox.conf"), []byte(confstr), 0660)
788 // Generate domains config, and add a commented out example for delivery to a mailing list.
790 if err := sconf.WriteDocs(&db, &dc); err != nil {
791 fatalf("generating domains config: %v", err)
794 // This approach is a bit horrible, but it generates a convenient
795 // example that includes the comments. Though it is gone by the first
796 // write of the file by mox.
797 odests := fmt.Sprintf("\t\tDestinations:\n\t\t\t%s: nil\n", addr.String())
798 var destsExample = struct {
799 Destinations map[string]config.Destination
801 Destinations: map[string]config.Destination{
803 Rulesets: []config.Ruleset{
805 VerifiedDomain: "list.example.org",
806 HeadersRegexp: map[string]string{
807 "^list-id$": `<name\.list\.example\.org>`,
809 ListAllowDomain: "list.example.org",
810 Mailbox: "Lists/Example",
816 var destBuf strings.Builder
817 if err := sconf.Describe(&destBuf, destsExample); err != nil {
818 fatalf("describing destination example: %v", err)
820 ndests := odests + "# If you receive email from mailing lists, you may want to configure them like the\n# example below (remove the empty/false SMTPMailRegexp and IsForward).\n# If you are receiving forwarded email, see the IsForwarded option in a Ruleset.\n"
821 for _, line := range strings.Split(destBuf.String(), "\n")[1:] {
822 ndests += "#\t\t" + line + "\n"
824 dconfstr := strings.ReplaceAll(db.String(), odests, ndests)
825 xwritefile(filepath.FromSlash("config/domains.conf"), []byte(dconfstr), 0660)
828 loadTLSKeyCerts := !existingWebserver
829 mc, errs := mox.ParseConfig(context.Background(), c.log, filepath.FromSlash("config/mox.conf"), true, loadTLSKeyCerts, false)
832 log.Printf("checking generated config, multiple errors:")
833 for _, err := range errs {
836 fatalf("aborting due to multiple config errors")
838 fatalf("checking generated config: %s", errs[0])
841 // NOTE: Now that we've prepared the config, we can open the account
842 // and set a passsword, and the public key for the DKIM private keys
843 // are available for generating the DKIM DNS records below.
845 confDomain, ok := mc.Domain(domain)
847 fatalf("cannot find domain in new config")
850 acc, _, err := store.OpenEmail(c.log, args[0])
852 fatalf("open account: %s", err)
854 cleanupPaths = append(cleanupPaths, dataDir, filepath.Join(dataDir, "accounts"), filepath.Join(dataDir, "accounts", accountName), filepath.Join(dataDir, "accounts", accountName, "index.db"))
858 // Kludge to cause no logging to be printed about setting a new password.
859 loglevel := mox.Conf.Log[""]
860 mox.Conf.Log[""] = mlog.LevelWarn
861 mlog.SetConfig(mox.Conf.Log)
862 if err := acc.SetPassword(c.log, password); err != nil {
863 fatalf("setting password: %s", err)
865 mox.Conf.Log[""] = loglevel
866 mlog.SetConfig(mox.Conf.Log)
868 if err := acc.Close(); err != nil {
869 fatalf("closing account: %s", err)
871 fmt.Printf("IMAP, SMTP submission and HTTP account password for %s: %s\n\n", args[0], password)
872 fmt.Printf(`When configuring your email client, use the email address as username. If
873autoconfig/autodiscover does not work, use these settings:
875 printClientConfig(domain)
877 if existingWebserver {
879Configuration files have been written to config/mox.conf and
882Create the DNS records below, by adding them to your zone file or through the
883web interface of your DNS operator. The admin interface can show these same
884records, and has a page to check they have been configured correctly.
886You must configure your existing webserver to forward requests for:
889 https://autoconfig.%s/
895If it makes it easier to get a TLS certificate for %s, you can add a
896reverse proxy for that hostname too.
898You must edit mox.conf and configure the paths to the TLS certificates and keys.
899The paths are relative to config/ directory that holds mox.conf! To test if your
904The DNS records to add:
905`, domain.ASCII, domain.ASCII, dnshostname.ASCII)
908Configuration files have been written to config/mox.conf and
909config/domains.conf. You should review them. Then create the DNS records below,
910by adding them to your zone file or through the web interface of your DNS
911operator. You can also skip creating the DNS records and start mox immediately.
912The admin interface can show these same records, and has a page to check they
913have been configured correctly. The DNS records to add:
917 // We do not verify the records exist: If they don't exist, we would only be
918 // priming dns caches with negative/absent records, causing our "quick setup" to
919 // appear to fail or take longer than "quick".
921 records, err := mox.DomainRecords(confDomain, domain, domainDNSSECResult.Authentic, "letsencrypt.org", "")
923 fatalf("making required DNS records")
925 fmt.Print("\n\n" + strings.Join(records, "\n") + "\n\n\n\n")
927 fmt.Printf(`WARNING: The configuration and DNS records above assume you do not currently
928have email configured for your domain. If you do already have email configured,
929or if you are sending email for your domain from other machines/services, you
930should understand the consequences of the DNS records above before
933 if os.Getenv("MOX_DOCKER") == "" {
935You can now start mox with "./mox serve", as root.
939You can now start the mox container.
943File ownership and permissions are automatically set correctly by mox when
944starting up. On linux, you may want to enable mox as a systemd service.
948 // For now, we only give service config instructions for linux when not running in docker.
949 if runtime.GOOS == "linux" && os.Getenv("MOX_DOCKER") == "" {
950 pwd, err := os.Getwd()
952 log.Printf("current working directory: %v", err)
955 service := strings.ReplaceAll(moxService, "/home/mox", pwd)
956 xwritefile("mox.service", []byte(service), 0644)
957 cleanupPaths = append(cleanupPaths, "mox.service")
958 fmt.Printf(`See mox.service for a systemd service file. To enable and start:
960 sudo chmod 644 mox.service
961 sudo systemctl enable $PWD/mox.service
962 sudo systemctl start mox.service
963 sudo journalctl -f -u mox.service # See logs
968After starting mox, the web interfaces are served at:
970http://localhost/ - account (email address as username)
971http://localhost/webmail/ - webmail (email address as username)
972http://localhost/admin/ - admin (empty username)
974To access these from your browser, run
975"ssh -L 8080:localhost:80 you@yourmachine" locally and open
976http://localhost:8080/[...].
978If you run into problem, have questions/feedback or found a bug, please let us
979know. Mox needs your help!
984 if !existingWebserver {
986PS: If you want to run mox along side an existing webserver that uses port 443
987and 80, see "mox help quickstart" with the -existing-webserver option.