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