1package main
2
3import (
4 "bufio"
5 "bytes"
6 "context"
7 "crypto"
8 "crypto/ecdsa"
9 "crypto/ed25519"
10 "crypto/elliptic"
11 cryptorand "crypto/rand"
12 "crypto/rsa"
13 "crypto/sha256"
14 "crypto/sha512"
15 "crypto/x509"
16 "encoding/base64"
17 "encoding/json"
18 "encoding/pem"
19 "errors"
20 "flag"
21 "fmt"
22 "io"
23 "io/fs"
24 "log"
25 "log/slog"
26 "net"
27 "net/http"
28 "net/url"
29 "os"
30 "path/filepath"
31 "reflect"
32 "runtime"
33 "slices"
34 "strconv"
35 "strings"
36 "time"
37
38 "golang.org/x/crypto/bcrypt"
39 "golang.org/x/text/secure/precis"
40
41 "github.com/mjl-/adns"
42
43 "github.com/mjl-/autocert"
44 "github.com/mjl-/bstore"
45 "github.com/mjl-/sconf"
46 "github.com/mjl-/sherpa"
47
48 "github.com/mjl-/mox/config"
49 "github.com/mjl-/mox/dane"
50 "github.com/mjl-/mox/dkim"
51 "github.com/mjl-/mox/dmarc"
52 "github.com/mjl-/mox/dmarcdb"
53 "github.com/mjl-/mox/dmarcrpt"
54 "github.com/mjl-/mox/dns"
55 "github.com/mjl-/mox/dnsbl"
56 "github.com/mjl-/mox/message"
57 "github.com/mjl-/mox/mlog"
58 "github.com/mjl-/mox/mox-"
59 "github.com/mjl-/mox/moxio"
60 "github.com/mjl-/mox/moxvar"
61 "github.com/mjl-/mox/mtasts"
62 "github.com/mjl-/mox/publicsuffix"
63 "github.com/mjl-/mox/queue"
64 "github.com/mjl-/mox/smtp"
65 "github.com/mjl-/mox/smtpclient"
66 "github.com/mjl-/mox/spf"
67 "github.com/mjl-/mox/store"
68 "github.com/mjl-/mox/tlsrpt"
69 "github.com/mjl-/mox/tlsrptdb"
70 "github.com/mjl-/mox/updates"
71 "github.com/mjl-/mox/webadmin"
72 "github.com/mjl-/mox/webapi"
73)
74
75var (
76 changelogDomain = "xmox.nl"
77 changelogURL = "https://updates.xmox.nl/changelog"
78 changelogPubKey = base64Decode("sPNiTDQzvb4FrytNEiebJhgyQzn57RwEjNbGWMM/bDY=")
79)
80
81func base64Decode(s string) []byte {
82 buf, err := base64.StdEncoding.DecodeString(s)
83 if err != nil {
84 panic(err)
85 }
86 return buf
87}
88
89func envString(k, def string) string {
90 s := os.Getenv(k)
91 if s == "" {
92 return def
93 }
94 return s
95}
96
97var commands = []struct {
98 cmd string
99 fn func(c *cmd)
100}{
101 {"serve", cmdServe},
102 {"quickstart", cmdQuickstart},
103 {"stop", cmdStop},
104 {"setaccountpassword", cmdSetaccountpassword},
105 {"setadminpassword", cmdSetadminpassword},
106 {"loglevels", cmdLoglevels},
107 {"queue holdrules list", cmdQueueHoldrulesList},
108 {"queue holdrules add", cmdQueueHoldrulesAdd},
109 {"queue holdrules remove", cmdQueueHoldrulesRemove},
110 {"queue list", cmdQueueList},
111 {"queue hold", cmdQueueHold},
112 {"queue unhold", cmdQueueUnhold},
113 {"queue schedule", cmdQueueSchedule},
114 {"queue transport", cmdQueueTransport},
115 {"queue requiretls", cmdQueueRequireTLS},
116 {"queue fail", cmdQueueFail},
117 {"queue drop", cmdQueueDrop},
118 {"queue dump", cmdQueueDump},
119 {"queue retired list", cmdQueueRetiredList},
120 {"queue retired print", cmdQueueRetiredPrint},
121 {"queue suppress list", cmdQueueSuppressList},
122 {"queue suppress add", cmdQueueSuppressAdd},
123 {"queue suppress remove", cmdQueueSuppressRemove},
124 {"queue suppress lookup", cmdQueueSuppressLookup},
125 {"queue webhook list", cmdQueueHookList},
126 {"queue webhook schedule", cmdQueueHookSchedule},
127 {"queue webhook cancel", cmdQueueHookCancel},
128 {"queue webhook print", cmdQueueHookPrint},
129 {"queue webhook retired list", cmdQueueHookRetiredList},
130 {"queue webhook retired print", cmdQueueHookRetiredPrint},
131 {"import maildir", cmdImportMaildir},
132 {"import mbox", cmdImportMbox},
133 {"export maildir", cmdExportMaildir},
134 {"export mbox", cmdExportMbox},
135 {"localserve", cmdLocalserve},
136 {"help", cmdHelp},
137 {"backup", cmdBackup},
138 {"verifydata", cmdVerifydata},
139
140 {"config test", cmdConfigTest},
141 {"config dnscheck", cmdConfigDNSCheck},
142 {"config dnsrecords", cmdConfigDNSRecords},
143 {"config describe-domains", cmdConfigDescribeDomains},
144 {"config describe-static", cmdConfigDescribeStatic},
145 {"config account add", cmdConfigAccountAdd},
146 {"config account rm", cmdConfigAccountRemove},
147 {"config address add", cmdConfigAddressAdd},
148 {"config address rm", cmdConfigAddressRemove},
149 {"config domain add", cmdConfigDomainAdd},
150 {"config domain rm", cmdConfigDomainRemove},
151 {"config alias list", cmdConfigAliasList},
152 {"config alias print", cmdConfigAliasPrint},
153 {"config alias add", cmdConfigAliasAdd},
154 {"config alias update", cmdConfigAliasUpdate},
155 {"config alias rm", cmdConfigAliasRemove},
156 {"config alias addaddr", cmdConfigAliasAddaddr},
157 {"config alias rmaddr", cmdConfigAliasRemoveaddr},
158
159 {"config describe-sendmail", cmdConfigDescribeSendmail},
160 {"config printservice", cmdConfigPrintservice},
161 {"config ensureacmehostprivatekeys", cmdConfigEnsureACMEHostprivatekeys},
162 {"config example", cmdConfigExample},
163
164 {"checkupdate", cmdCheckupdate},
165 {"cid", cmdCid},
166 {"clientconfig", cmdClientConfig},
167 {"deliver", cmdDeliver},
168 // todo: turn cmdDANEDialmx into a regular "dialmx" command that follows mta-sts policy, with options to require dane, mta-sts or requiretls. the code will be similar to queue/direct.go
169 {"dane dial", cmdDANEDial},
170 {"dane dialmx", cmdDANEDialmx},
171 {"dane makerecord", cmdDANEMakeRecord},
172 {"dns lookup", cmdDNSLookup},
173 {"dkim gened25519", cmdDKIMGened25519},
174 {"dkim genrsa", cmdDKIMGenrsa},
175 {"dkim lookup", cmdDKIMLookup},
176 {"dkim txt", cmdDKIMTXT},
177 {"dkim verify", cmdDKIMVerify},
178 {"dkim sign", cmdDKIMSign},
179 {"dmarc lookup", cmdDMARCLookup},
180 {"dmarc parsereportmsg", cmdDMARCParsereportmsg},
181 {"dmarc verify", cmdDMARCVerify},
182 {"dmarc checkreportaddrs", cmdDMARCCheckreportaddrs},
183 {"dnsbl check", cmdDNSBLCheck},
184 {"dnsbl checkhealth", cmdDNSBLCheckhealth},
185 {"mtasts lookup", cmdMTASTSLookup},
186 {"retrain", cmdRetrain},
187 {"sendmail", cmdSendmail},
188 {"spf check", cmdSPFCheck},
189 {"spf lookup", cmdSPFLookup},
190 {"spf parse", cmdSPFParse},
191 {"tlsrpt lookup", cmdTLSRPTLookup},
192 {"tlsrpt parsereportmsg", cmdTLSRPTParsereportmsg},
193 {"version", cmdVersion},
194 {"webapi", cmdWebapi},
195
196 {"example", cmdExample},
197 {"bumpuidvalidity", cmdBumpUIDValidity},
198 {"reassignuids", cmdReassignUIDs},
199 {"fixuidmeta", cmdFixUIDMeta},
200 {"fixmsgsize", cmdFixmsgsize},
201 {"reparse", cmdReparse},
202 {"ensureparsed", cmdEnsureParsed},
203 {"recalculatemailboxcounts", cmdRecalculateMailboxCounts},
204 {"message parse", cmdMessageParse},
205 {"reassignthreads", cmdReassignthreads},
206
207 // Not listed.
208 {"helpall", cmdHelpall},
209 {"junk analyze", cmdJunkAnalyze},
210 {"junk check", cmdJunkCheck},
211 {"junk play", cmdJunkPlay},
212 {"junk test", cmdJunkTest},
213 {"junk train", cmdJunkTrain},
214 {"dmarcdb addreport", cmdDMARCDBAddReport},
215 {"tlsrptdb addreport", cmdTLSRPTDBAddReport},
216 {"updates addsigned", cmdUpdatesAddSigned},
217 {"updates genkey", cmdUpdatesGenkey},
218 {"updates pubkey", cmdUpdatesPubkey},
219 {"updates serve", cmdUpdatesServe},
220 {"updates verify", cmdUpdatesVerify},
221 {"gentestdata", cmdGentestdata},
222 {"ximport maildir", cmdXImportMaildir},
223 {"ximport mbox", cmdXImportMbox},
224 {"openaccounts", cmdOpenaccounts},
225 {"readmessages", cmdReadmessages},
226 {"queuefillretired", cmdQueueFillRetired},
227}
228
229var cmds []cmd
230
231func init() {
232 for _, xc := range commands {
233 c := cmd{words: strings.Split(xc.cmd, " "), fn: xc.fn}
234 cmds = append(cmds, c)
235 }
236}
237
238type cmd struct {
239 words []string
240 fn func(c *cmd)
241
242 // Set before calling command.
243 flag *flag.FlagSet
244 flagArgs []string
245 _gather bool // Set when using Parse to gather usage for a command.
246
247 // Set by invoked command or Parse.
248 unlisted bool // If set, command is not listed until at least some words are matched from command.
249 params string // Arguments to command. Multiple lines possible.
250 help string // Additional explanation. First line is synopsis, the rest is only printed for an explicit help/usage for that command.
251 args []string
252
253 log mlog.Log
254}
255
256func (c *cmd) Parse() []string {
257 // To gather params and usage information, we just run the command but cause this
258 // panic after the command has registered its flags and set its params and help
259 // information. This is then caught and that info printed.
260 if c._gather {
261 panic("gather")
262 }
263
264 c.flag.Usage = c.Usage
265 c.flag.Parse(c.flagArgs)
266 c.args = c.flag.Args()
267 return c.args
268}
269
270func (c *cmd) gather() {
271 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
272 c._gather = true
273 defer func() {
274 x := recover()
275 // panic generated by Parse.
276 if x != "gather" {
277 panic(x)
278 }
279 }()
280 c.fn(c)
281}
282
283func (c *cmd) makeUsage() string {
284 var r strings.Builder
285 cs := "mox " + strings.Join(c.words, " ")
286 for i, line := range strings.Split(strings.TrimSpace(c.params), "\n") {
287 s := ""
288 if i == 0 {
289 s = "usage:"
290 }
291 if line != "" {
292 line = " " + line
293 }
294 fmt.Fprintf(&r, "%6s %s%s\n", s, cs, line)
295 }
296 c.flag.SetOutput(&r)
297 c.flag.PrintDefaults()
298 return r.String()
299}
300
301func (c *cmd) printUsage() {
302 fmt.Fprint(os.Stderr, c.makeUsage())
303 if c.help != "" {
304 fmt.Fprint(os.Stderr, "\n"+c.help+"\n")
305 }
306}
307
308func (c *cmd) Usage() {
309 c.printUsage()
310 os.Exit(2)
311}
312
313func cmdHelp(c *cmd) {
314 c.params = "[command ...]"
315 c.help = `Prints help about matching commands.
316
317If multiple commands match, they are listed along with the first line of their help text.
318If a single command matches, its usage and full help text is printed.
319`
320 args := c.Parse()
321 if len(args) == 0 {
322 c.Usage()
323 }
324
325 prefix := func(l, pre []string) bool {
326 if len(pre) > len(l) {
327 return false
328 }
329 return slices.Equal(pre, l[:len(pre)])
330 }
331
332 var partial []cmd
333 for _, c := range cmds {
334 if slices.Equal(c.words, args) {
335 c.gather()
336 fmt.Print(c.makeUsage())
337 if c.help != "" {
338 fmt.Print("\n" + c.help + "\n")
339 }
340 return
341 } else if prefix(c.words, args) {
342 partial = append(partial, c)
343 }
344 }
345 if len(partial) == 0 {
346 fmt.Fprintf(os.Stderr, "%s: unknown command\n", strings.Join(args, " "))
347 os.Exit(2)
348 }
349 for _, c := range partial {
350 c.gather()
351 line := "mox " + strings.Join(c.words, " ")
352 fmt.Printf("%s\n", line)
353 if c.help != "" {
354 fmt.Printf("\t%s\n", strings.Split(c.help, "\n")[0])
355 }
356 }
357}
358
359func cmdHelpall(c *cmd) {
360 c.unlisted = true
361 c.help = `Print all detailed usage and help information for all listed commands.
362
363Used to generate documentation.
364`
365 args := c.Parse()
366 if len(args) != 0 {
367 c.Usage()
368 }
369
370 n := 0
371 for _, c := range cmds {
372 c.gather()
373 if c.unlisted {
374 continue
375 }
376 if n > 0 {
377 fmt.Fprintf(os.Stderr, "\n")
378 }
379 n++
380
381 fmt.Fprintf(os.Stderr, "# mox %s\n\n", strings.Join(c.words, " "))
382 if c.help != "" {
383 fmt.Fprintln(os.Stderr, c.help+"\n")
384 }
385 s := c.makeUsage()
386 s = "\t" + strings.ReplaceAll(s, "\n", "\n\t")
387 fmt.Fprintln(os.Stderr, s)
388 }
389}
390
391func usage(l []cmd, unlisted bool) {
392 var lines []string
393 if !unlisted {
394 lines = append(lines, "mox [-config config/mox.conf] [-pedantic] ...")
395 }
396 for _, c := range l {
397 c.gather()
398 if c.unlisted && !unlisted {
399 continue
400 }
401 for _, line := range strings.Split(c.params, "\n") {
402 x := append([]string{"mox"}, c.words...)
403 if line != "" {
404 x = append(x, line)
405 }
406 lines = append(lines, strings.Join(x, " "))
407 }
408 }
409 for i, line := range lines {
410 pre := " "
411 if i == 0 {
412 pre = "usage: "
413 }
414 fmt.Fprintln(os.Stderr, pre+line)
415 }
416 os.Exit(2)
417}
418
419var loglevel string // Empty will be interpreted as info, except by localserve.
420var pedantic bool
421
422// subcommands that are not "serve" should use this function to load the config, it
423// restores any loglevel specified on the command-line, instead of using the
424// loglevels from the config file and it does not load files like TLS keys/certs.
425func mustLoadConfig() {
426 mox.MustLoadConfig(false, false)
427 ll := loglevel
428 if ll == "" {
429 ll = "info"
430 }
431 if level, ok := mlog.Levels[ll]; ok {
432 mox.Conf.Log[""] = level
433 mlog.SetConfig(mox.Conf.Log)
434 } else {
435 log.Fatal("unknown loglevel", slog.String("loglevel", loglevel))
436 }
437 if pedantic {
438 mox.SetPedantic(true)
439 }
440}
441
442func main() {
443 // CheckConsistencyOnClose is true by default, for all the test packages. A regular
444 // mox server should never use it. But integration tests enable it again with a
445 // flag.
446 store.CheckConsistencyOnClose = false
447
448 ctxbg := context.Background()
449 mox.Shutdown = ctxbg
450 mox.Context = ctxbg
451
452 log.SetFlags(0)
453
454 // If invoked as sendmail, e.g. /usr/sbin/sendmail, we do enough so cron can get a
455 // message sent using smtp submission to a configured server.
456 if len(os.Args) > 0 && filepath.Base(os.Args[0]) == "sendmail" {
457 c := &cmd{
458 flag: flag.NewFlagSet("sendmail", flag.ExitOnError),
459 flagArgs: os.Args[1:],
460 log: mlog.New("sendmail", nil),
461 }
462 cmdSendmail(c)
463 return
464 }
465
466 flag.StringVar(&mox.ConfigStaticPath, "config", envString("MOXCONF", filepath.FromSlash("config/mox.conf")), "configuration file, other config files are looked up in the same directory, defaults to $MOXCONF with a fallback to mox.conf")
467 flag.StringVar(&loglevel, "loglevel", "", "if non-empty, this log level is set early in startup")
468 flag.BoolVar(&pedantic, "pedantic", false, "protocol violations result in errors instead of accepting/working around them")
469 flag.BoolVar(&store.CheckConsistencyOnClose, "checkconsistency", false, "dangerous option for testing only, enables data checks that abort/panic when inconsistencies are found")
470
471 var cpuprofile, memprofile, tracefile string
472 flag.StringVar(&cpuprofile, "cpuprof", "", "store cpu profile to file")
473 flag.StringVar(&memprofile, "memprof", "", "store mem profile to file")
474 flag.StringVar(&tracefile, "trace", "", "store execution trace to file")
475
476 flag.Usage = func() { usage(cmds, false) }
477 flag.Parse()
478 args := flag.Args()
479 if len(args) == 0 {
480 usage(cmds, false)
481 }
482
483 if tracefile != "" {
484 defer traceExecution(tracefile)()
485 }
486 defer profile(cpuprofile, memprofile)()
487
488 if pedantic {
489 mox.SetPedantic(true)
490 }
491
492 mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
493 ll := loglevel
494 if ll == "" {
495 ll = "info"
496 }
497 if level, ok := mlog.Levels[ll]; ok {
498 mox.Conf.Log[""] = level
499 mlog.SetConfig(mox.Conf.Log)
500 // note: SetConfig may be called again when subcommands loads config.
501 } else {
502 log.Fatalf("unknown loglevel %q", loglevel)
503 }
504
505 var partial []cmd
506next:
507 for _, c := range cmds {
508 for i, w := range c.words {
509 if i >= len(args) || w != args[i] {
510 if i > 0 {
511 partial = append(partial, c)
512 }
513 continue next
514 }
515 }
516 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
517 c.flagArgs = args[len(c.words):]
518 c.log = mlog.New(strings.Join(c.words, ""), nil)
519 c.fn(&c)
520 return
521 }
522 if len(partial) > 0 {
523 usage(partial, true)
524 }
525 usage(cmds, false)
526}
527
528func xcheckf(err error, format string, args ...any) {
529 if err == nil {
530 return
531 }
532 msg := fmt.Sprintf(format, args...)
533 log.Fatalf("%s: %s", msg, err)
534}
535
536func xparseIP(s, what string) net.IP {
537 ip := net.ParseIP(s)
538 if ip == nil {
539 log.Fatalf("invalid %s: %q", what, s)
540 }
541 return ip
542}
543
544func xparseDomain(s, what string) dns.Domain {
545 d, err := dns.ParseDomain(s)
546 xcheckf(err, "parsing %s %q", what, s)
547 return d
548}
549
550func cmdClientConfig(c *cmd) {
551 c.params = "domain"
552 c.help = `Print the configuration for email clients for a domain.
553
554Sending email is typically not done on the SMTP port 25, but on submission
555ports 465 (with TLS) and 587 (without initial TLS, but usually added to the
556connection with STARTTLS). For IMAP, the port with TLS is 993 and without is
557143.
558
559Without TLS/STARTTLS, passwords are sent in clear text, which should only be
560configured over otherwise secured connections, like a VPN.
561`
562 args := c.Parse()
563 if len(args) != 1 {
564 c.Usage()
565 }
566 d := xparseDomain(args[0], "domain")
567 mustLoadConfig()
568 printClientConfig(d)
569}
570
571func printClientConfig(d dns.Domain) {
572 cc, err := mox.ClientConfigsDomain(d)
573 xcheckf(err, "getting client config")
574 fmt.Printf("%-20s %-30s %5s %-15s %s\n", "Protocol", "Host", "Port", "Listener", "Note")
575 for _, e := range cc.Entries {
576 fmt.Printf("%-20s %-30s %5d %-15s %s\n", e.Protocol, e.Host, e.Port, e.Listener, e.Note)
577 }
578 fmt.Printf(`
579To prevent authentication mechanism downgrade attempts that may result in
580clients sending plain text passwords to a MitM, clients should always be
581explicitly configured with the most secure authentication mechanism supported,
582the first of: SCRAM-SHA-256-PLUS, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, SCRAM-SHA-1,
583CRAM-MD5.
584`)
585}
586
587func cmdConfigTest(c *cmd) {
588 c.help = `Parses and validates the configuration files.
589
590If valid, the command exits with status 0. If not valid, all errors encountered
591are printed.
592`
593 args := c.Parse()
594 if len(args) != 0 {
595 c.Usage()
596 }
597
598 mox.FilesImmediate = true
599
600 _, errs := mox.ParseConfig(context.Background(), c.log, mox.ConfigStaticPath, true, true, false)
601 if len(errs) > 1 {
602 log.Printf("multiple errors:")
603 for _, err := range errs {
604 log.Printf("%s", err)
605 }
606 os.Exit(1)
607 } else if len(errs) == 1 {
608 log.Fatalf("%s", errs[0])
609 os.Exit(1)
610 }
611 fmt.Println("config OK")
612}
613
614func cmdConfigDescribeStatic(c *cmd) {
615 c.params = ">mox.conf"
616 c.help = `Prints an annotated empty configuration for use as mox.conf.
617
618The static configuration file cannot be reloaded while mox is running. Mox has
619to be restarted for changes to the static configuration file to take effect.
620
621This configuration file needs modifications to make it valid. For example, it
622may contain unfinished list items.
623`
624 if len(c.Parse()) != 0 {
625 c.Usage()
626 }
627
628 var sc config.Static
629 err := sconf.Describe(os.Stdout, &sc)
630 xcheckf(err, "describing config")
631}
632
633func cmdConfigDescribeDomains(c *cmd) {
634 c.params = ">domains.conf"
635 c.help = `Prints an annotated empty configuration for use as domains.conf.
636
637The domains configuration file contains the domains and their configuration,
638and accounts and their configuration. This includes the configured email
639addresses. The mox admin web interface, and the mox command line interface, can
640make changes to this file. Mox automatically reloads this file when it changes.
641
642Like the static configuration, the example domains.conf printed by this command
643needs modifications to make it valid.
644`
645 if len(c.Parse()) != 0 {
646 c.Usage()
647 }
648
649 var dc config.Dynamic
650 err := sconf.Describe(os.Stdout, &dc)
651 xcheckf(err, "describing config")
652}
653
654func cmdConfigPrintservice(c *cmd) {
655 c.params = ">mox.service"
656 c.help = `Prints a systemd unit service file for mox.
657
658This is the same file as generated using quickstart. If the systemd service file
659has changed with a newer version of mox, use this command to generate an up to
660date version.
661`
662 if len(c.Parse()) != 0 {
663 c.Usage()
664 }
665
666 pwd, err := os.Getwd()
667 if err != nil {
668 log.Printf("current working directory: %v", err)
669 pwd = "/home/mox"
670 }
671 service := strings.ReplaceAll(moxService, "/home/mox", pwd)
672 fmt.Print(service)
673}
674
675func cmdConfigDomainAdd(c *cmd) {
676 c.params = "domain account [localpart]"
677 c.help = `Adds a new domain to the configuration and reloads the configuration.
678
679The account is used for the postmaster mailboxes the domain, including as DMARC and
680TLS reporting. Localpart is the "username" at the domain for this account. If
681must be set if and only if account does not yet exist.
682`
683 args := c.Parse()
684 if len(args) != 2 && len(args) != 3 {
685 c.Usage()
686 }
687
688 d := xparseDomain(args[0], "domain")
689 mustLoadConfig()
690 var localpart smtp.Localpart
691 if len(args) == 3 {
692 var err error
693 localpart, err = smtp.ParseLocalpart(args[2])
694 xcheckf(err, "parsing localpart")
695 }
696 ctlcmdConfigDomainAdd(xctl(), d, args[1], localpart)
697}
698
699func ctlcmdConfigDomainAdd(ctl *ctl, domain dns.Domain, account string, localpart smtp.Localpart) {
700 ctl.xwrite("domainadd")
701 ctl.xwrite(domain.Name())
702 ctl.xwrite(account)
703 ctl.xwrite(string(localpart))
704 ctl.xreadok()
705 fmt.Printf("domain added, remember to add dns records, see:\n\nmox config dnsrecords %s\nmox config dnscheck %s\n", domain.Name(), domain.Name())
706}
707
708func cmdConfigDomainRemove(c *cmd) {
709 c.params = "domain"
710 c.help = `Remove a domain from the configuration and reload the configuration.
711
712This is a dangerous operation. Incoming email delivery for this domain will be
713rejected.
714`
715 args := c.Parse()
716 if len(args) != 1 {
717 c.Usage()
718 }
719
720 d := xparseDomain(args[0], "domain")
721 mustLoadConfig()
722 ctlcmdConfigDomainRemove(xctl(), d)
723}
724
725func ctlcmdConfigDomainRemove(ctl *ctl, d dns.Domain) {
726 ctl.xwrite("domainrm")
727 ctl.xwrite(d.Name())
728 ctl.xreadok()
729 fmt.Printf("domain removed, remember to remove dns records for %s\n", d)
730}
731
732func cmdConfigAliasList(c *cmd) {
733 c.params = "domain"
734 c.help = `List aliases for domain.`
735 args := c.Parse()
736 if len(args) != 1 {
737 c.Usage()
738 }
739
740 mustLoadConfig()
741 ctlcmdConfigAliasList(xctl(), args[0])
742}
743
744func ctlcmdConfigAliasList(ctl *ctl, address string) {
745 ctl.xwrite("aliaslist")
746 ctl.xwrite(address)
747 ctl.xreadok()
748 ctl.xstreamto(os.Stdout)
749}
750
751func cmdConfigAliasPrint(c *cmd) {
752 c.params = "alias"
753 c.help = `Print settings and members of alias.`
754 args := c.Parse()
755 if len(args) != 1 {
756 c.Usage()
757 }
758
759 mustLoadConfig()
760 ctlcmdConfigAliasPrint(xctl(), args[0])
761}
762
763func ctlcmdConfigAliasPrint(ctl *ctl, address string) {
764 ctl.xwrite("aliasprint")
765 ctl.xwrite(address)
766 ctl.xreadok()
767 ctl.xstreamto(os.Stdout)
768}
769
770func cmdConfigAliasAdd(c *cmd) {
771 c.params = "alias@domain rcpt1@domain ..."
772 c.help = `Add new alias with one or more addresses.`
773 args := c.Parse()
774 if len(args) < 2 {
775 c.Usage()
776 }
777
778 alias := config.Alias{Addresses: args[1:]}
779
780 mustLoadConfig()
781 ctlcmdConfigAliasAdd(xctl(), args[0], alias)
782}
783
784func ctlcmdConfigAliasAdd(ctl *ctl, address string, alias config.Alias) {
785 ctl.xwrite("aliasadd")
786 ctl.xwrite(address)
787 xctlwriteJSON(ctl, alias)
788 ctl.xreadok()
789}
790
791func cmdConfigAliasUpdate(c *cmd) {
792 c.params = "alias@domain [-postpublic false|true -listmembers false|true -allowmsgfrom false|true]"
793 c.help = `Update alias configuration.`
794 var postpublic, listmembers, allowmsgfrom string
795 c.flag.StringVar(&postpublic, "postpublic", "", "whether anyone or only list members can post")
796 c.flag.StringVar(&listmembers, "listmembers", "", "whether list members can list members")
797 c.flag.StringVar(&allowmsgfrom, "allowmsgfrom", "", "whether alias address can be used in message from header")
798 args := c.Parse()
799 if len(args) != 1 {
800 c.Usage()
801 }
802
803 alias := args[0]
804 mustLoadConfig()
805 ctlcmdConfigAliasUpdate(xctl(), alias, postpublic, listmembers, allowmsgfrom)
806}
807
808func ctlcmdConfigAliasUpdate(ctl *ctl, alias, postpublic, listmembers, allowmsgfrom string) {
809 ctl.xwrite("aliasupdate")
810 ctl.xwrite(alias)
811 ctl.xwrite(postpublic)
812 ctl.xwrite(listmembers)
813 ctl.xwrite(allowmsgfrom)
814 ctl.xreadok()
815}
816
817func cmdConfigAliasRemove(c *cmd) {
818 c.params = "alias@domain"
819 c.help = "Remove alias."
820 args := c.Parse()
821 if len(args) != 1 {
822 c.Usage()
823 }
824
825 mustLoadConfig()
826 ctlcmdConfigAliasRemove(xctl(), args[0])
827}
828
829func ctlcmdConfigAliasRemove(ctl *ctl, alias string) {
830 ctl.xwrite("aliasrm")
831 ctl.xwrite(alias)
832 ctl.xreadok()
833}
834
835func cmdConfigAliasAddaddr(c *cmd) {
836 c.params = "alias@domain rcpt1@domain ..."
837 c.help = `Add addresses to alias.`
838 args := c.Parse()
839 if len(args) < 2 {
840 c.Usage()
841 }
842
843 mustLoadConfig()
844 ctlcmdConfigAliasAddaddr(xctl(), args[0], args[1:])
845}
846
847func ctlcmdConfigAliasAddaddr(ctl *ctl, alias string, addresses []string) {
848 ctl.xwrite("aliasaddaddr")
849 ctl.xwrite(alias)
850 xctlwriteJSON(ctl, addresses)
851 ctl.xreadok()
852}
853
854func cmdConfigAliasRemoveaddr(c *cmd) {
855 c.params = "alias@domain rcpt1@domain ..."
856 c.help = `Remove addresses from alias.`
857 args := c.Parse()
858 if len(args) < 2 {
859 c.Usage()
860 }
861
862 mustLoadConfig()
863 ctlcmdConfigAliasRmaddr(xctl(), args[0], args[1:])
864}
865
866func ctlcmdConfigAliasRmaddr(ctl *ctl, alias string, addresses []string) {
867 ctl.xwrite("aliasrmaddr")
868 ctl.xwrite(alias)
869 xctlwriteJSON(ctl, addresses)
870 ctl.xreadok()
871}
872
873func cmdConfigAccountAdd(c *cmd) {
874 c.params = "account address"
875 c.help = `Add an account with an email address and reload the configuration.
876
877Email can be delivered to this address/account. A password has to be configured
878explicitly, see the setaccountpassword command.
879`
880 args := c.Parse()
881 if len(args) != 2 {
882 c.Usage()
883 }
884
885 mustLoadConfig()
886 ctlcmdConfigAccountAdd(xctl(), args[0], args[1])
887}
888
889func ctlcmdConfigAccountAdd(ctl *ctl, account, address string) {
890 ctl.xwrite("accountadd")
891 ctl.xwrite(account)
892 ctl.xwrite(address)
893 ctl.xreadok()
894 fmt.Printf("account added, set a password with \"mox setaccountpassword %s\"\n", account)
895}
896
897func cmdConfigAccountRemove(c *cmd) {
898 c.params = "account"
899 c.help = `Remove an account and reload the configuration.
900
901Email addresses for this account will also be removed, and incoming email for
902these addresses will be rejected.
903
904All data for the account will be removed.
905`
906 args := c.Parse()
907 if len(args) != 1 {
908 c.Usage()
909 }
910
911 mustLoadConfig()
912 ctlcmdConfigAccountRemove(xctl(), args[0])
913}
914
915func ctlcmdConfigAccountRemove(ctl *ctl, account string) {
916 ctl.xwrite("accountrm")
917 ctl.xwrite(account)
918 ctl.xreadok()
919 fmt.Println("account removed")
920}
921
922func cmdConfigAddressAdd(c *cmd) {
923 c.params = "address account"
924 c.help = `Adds an address to an account and reloads the configuration.
925
926If address starts with a @ (i.e. a missing localpart), this is a catchall
927address for the domain.
928`
929 args := c.Parse()
930 if len(args) != 2 {
931 c.Usage()
932 }
933
934 mustLoadConfig()
935 ctlcmdConfigAddressAdd(xctl(), args[0], args[1])
936}
937
938func ctlcmdConfigAddressAdd(ctl *ctl, address, account string) {
939 ctl.xwrite("addressadd")
940 ctl.xwrite(address)
941 ctl.xwrite(account)
942 ctl.xreadok()
943 fmt.Println("address added")
944}
945
946func cmdConfigAddressRemove(c *cmd) {
947 c.params = "address"
948 c.help = `Remove an address and reload the configuration.
949
950Incoming email for this address will be rejected after removing an address.
951`
952 args := c.Parse()
953 if len(args) != 1 {
954 c.Usage()
955 }
956
957 mustLoadConfig()
958 ctlcmdConfigAddressRemove(xctl(), args[0])
959}
960
961func ctlcmdConfigAddressRemove(ctl *ctl, address string) {
962 ctl.xwrite("addressrm")
963 ctl.xwrite(address)
964 ctl.xreadok()
965 fmt.Println("address removed")
966}
967
968func cmdConfigDNSRecords(c *cmd) {
969 c.params = "domain"
970 c.help = `Prints annotated DNS records as zone file that should be created for the domain.
971
972The zone file can be imported into existing DNS software. You should review the
973DNS records, especially if your domain previously/currently has email
974configured.
975`
976 args := c.Parse()
977 if len(args) != 1 {
978 c.Usage()
979 }
980
981 d := xparseDomain(args[0], "domain")
982 mustLoadConfig()
983 domConf, ok := mox.Conf.Domain(d)
984 if !ok {
985 log.Fatalf("unknown domain")
986 }
987
988 resolver := dns.StrictResolver{Pkg: "main"}
989 _, result, err := resolver.LookupTXT(context.Background(), d.ASCII+".")
990 if !dns.IsNotFound(err) {
991 xcheckf(err, "looking up record for dnssec-status")
992 }
993
994 var certIssuerDomainName, acmeAccountURI string
995 public := mox.Conf.Static.Listeners["public"]
996 if public.TLS != nil && public.TLS.ACME != "" {
997 acme, ok := mox.Conf.Static.ACME[public.TLS.ACME]
998 if ok && acme.Manager.Manager.Client != nil {
999 certIssuerDomainName = acme.IssuerDomainName
1000 acc, err := acme.Manager.Manager.Client.GetReg(context.Background(), "")
1001 c.log.Check(err, "get public acme account")
1002 if err == nil {
1003 acmeAccountURI = acc.URI
1004 }
1005 }
1006 }
1007
1008 records, err := mox.DomainRecords(domConf, d, result.Authentic, certIssuerDomainName, acmeAccountURI)
1009 xcheckf(err, "records")
1010 fmt.Print(strings.Join(records, "\n") + "\n")
1011}
1012
1013func cmdConfigDNSCheck(c *cmd) {
1014 c.params = "domain"
1015 c.help = "Check the DNS records with the configuration for the domain, and print any errors/warnings."
1016 args := c.Parse()
1017 if len(args) != 1 {
1018 c.Usage()
1019 }
1020
1021 d := xparseDomain(args[0], "domain")
1022 mustLoadConfig()
1023 _, ok := mox.Conf.Domain(d)
1024 if !ok {
1025 log.Fatalf("unknown domain")
1026 }
1027
1028 // todo future: move http.Admin.CheckDomain to mox- and make it return a regular error.
1029 defer func() {
1030 x := recover()
1031 if x == nil {
1032 return
1033 }
1034 err, ok := x.(*sherpa.Error)
1035 if !ok {
1036 panic(x)
1037 }
1038 log.Fatalf("%s", err)
1039 }()
1040
1041 printResult := func(name string, r webadmin.Result) {
1042 if len(r.Errors) == 0 && len(r.Warnings) == 0 {
1043 return
1044 }
1045 fmt.Printf("# %s\n", name)
1046 for _, s := range r.Errors {
1047 fmt.Printf("error: %s\n", s)
1048 }
1049 for _, s := range r.Warnings {
1050 fmt.Printf("warning: %s\n", s)
1051 }
1052 }
1053
1054 result := webadmin.Admin{}.CheckDomain(context.Background(), args[0])
1055 printResult("DNSSEC", result.DNSSEC.Result)
1056 printResult("IPRev", result.IPRev.Result)
1057 printResult("MX", result.MX.Result)
1058 printResult("TLS", result.TLS.Result)
1059 printResult("DANE", result.DANE.Result)
1060 printResult("SPF", result.SPF.Result)
1061 printResult("DKIM", result.DKIM.Result)
1062 printResult("DMARC", result.DMARC.Result)
1063 printResult("Host TLSRPT", result.HostTLSRPT.Result)
1064 printResult("Domain TLSRPT", result.DomainTLSRPT.Result)
1065 printResult("MTASTS", result.MTASTS.Result)
1066 printResult("SRV conf", result.SRVConf.Result)
1067 printResult("Autoconf", result.Autoconf.Result)
1068 printResult("Autodiscover", result.Autodiscover.Result)
1069}
1070
1071func cmdConfigEnsureACMEHostprivatekeys(c *cmd) {
1072 c.params = ""
1073 c.help = `Ensure host private keys exist for TLS listeners with ACME.
1074
1075In mox.conf, each listener can have TLS configured. Long-lived private key files
1076can be specified, which will be used when requesting ACME certificates.
1077Configuring these private keys makes it feasible to publish DANE TLSA records
1078for the corresponding public keys in DNS, protected with DNSSEC, allowing TLS
1079certificate verification without depending on a list of Certificate Authorities
1080(CAs). Previous versions of mox did not pre-generate private keys for use with
1081ACME certificates, but would generate private keys on-demand. By explicitly
1082configuring private keys, they will not change automatedly with new
1083certificates, and the DNS TLSA records stay valid.
1084
1085This command looks for listeners in mox.conf with TLS with ACME configured. For
1086each missing host private key (of type rsa-2048 and ecdsa-p256) a key is written
1087to config/hostkeys/. If a certificate exists in the ACME "cache", its private
1088key is copied. Otherwise a new private key is generated. Snippets for manually
1089updating/editing mox.conf are printed.
1090
1091After running this command, and updating mox.conf, run "mox config dnsrecords"
1092for a domain and create the TLSA DNS records it suggests to enable DANE.
1093`
1094 args := c.Parse()
1095 if len(args) != 0 {
1096 c.Usage()
1097 }
1098
1099 // Load a private key from p, in various forms. We only look at the first PEM
1100 // block. Files with only a private key, or with multiple blocks but private key
1101 // first like autocert does, can be loaded.
1102 loadPrivateKey := func(f *os.File) (any, error) {
1103 buf, err := io.ReadAll(f)
1104 if err != nil {
1105 return nil, fmt.Errorf("reading private key file: %v", err)
1106 }
1107 block, _ := pem.Decode(buf)
1108 if block == nil {
1109 return nil, fmt.Errorf("no pem block found in pem file")
1110 }
1111 var privKey any
1112 switch block.Type {
1113 case "EC PRIVATE KEY":
1114 privKey, err = x509.ParseECPrivateKey(block.Bytes)
1115 case "RSA PRIVATE KEY":
1116 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
1117 case "PRIVATE KEY":
1118 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
1119 default:
1120 return nil, fmt.Errorf("unrecognized pem block type %q", block.Type)
1121 }
1122 if err != nil {
1123 return nil, fmt.Errorf("parsing private key of type %q: %v", block.Type, err)
1124 }
1125 return privKey, nil
1126 }
1127
1128 // Either load a private key from file, or if it doesn't exist generate a new
1129 // private key.
1130 xtryLoadPrivateKey := func(kt autocert.KeyType, p string) any {
1131 f, err := os.Open(p)
1132 if err != nil && errors.Is(err, fs.ErrNotExist) {
1133 switch kt {
1134 case autocert.KeyRSA2048:
1135 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1136 xcheckf(err, "generating new 2048-bit rsa private key")
1137 return privKey
1138 case autocert.KeyECDSAP256:
1139 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1140 xcheckf(err, "generating new ecdsa p-256 private key")
1141 return privKey
1142 }
1143 log.Fatalf("unexpected keytype %v", kt)
1144 return nil
1145 }
1146 xcheckf(err, "%s: open acme key and certificate file", p)
1147
1148 // Load private key from file. autocert stores a PEM file that starts with a
1149 // private key, followed by certificate(s). So we can just read it and should find
1150 // the private key we are looking for.
1151 privKey, err := loadPrivateKey(f)
1152 if xerr := f.Close(); xerr != nil {
1153 log.Printf("closing private key file: %v", xerr)
1154 }
1155 xcheckf(err, "parsing private key from acme key and certificate file")
1156
1157 switch k := privKey.(type) {
1158 case *rsa.PrivateKey:
1159 if k.N.BitLen() == 2048 {
1160 return privKey
1161 }
1162 log.Printf("warning: rsa private key in %s has %d bits, skipping and generating new 2048-bit rsa private key", p, k.N.BitLen())
1163 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1164 xcheckf(err, "generating new 2048-bit rsa private key")
1165 return privKey
1166 case *ecdsa.PrivateKey:
1167 if k.Curve == elliptic.P256() {
1168 return privKey
1169 }
1170 log.Printf("warning: ecdsa private key in %s has curve %v, skipping and generating new p-256 ecdsa key", p, k.Curve.Params().Name)
1171 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1172 xcheckf(err, "generating new ecdsa p-256 private key")
1173 return privKey
1174 default:
1175 log.Fatalf("%s: unexpected private key file of type %T", p, privKey)
1176 return nil
1177 }
1178 }
1179
1180 // Write privKey as PKCS#8 private key to p. Only if file does not yet exist.
1181 writeHostPrivateKey := func(privKey any, p string) error {
1182 os.MkdirAll(filepath.Dir(p), 0700)
1183 f, err := os.OpenFile(p, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
1184 if err != nil {
1185 return fmt.Errorf("create: %v", err)
1186 }
1187 defer func() {
1188 if f != nil {
1189 if err := f.Close(); err != nil {
1190 log.Printf("closing new hostkey file %s after error: %v", p, err)
1191 }
1192 if err := os.Remove(p); err != nil {
1193 log.Printf("removing new hostkey file %s after error: %v", p, err)
1194 }
1195 }
1196 }()
1197 buf, err := x509.MarshalPKCS8PrivateKey(privKey)
1198 if err != nil {
1199 return fmt.Errorf("marshal private host key: %v", err)
1200 }
1201 block := pem.Block{
1202 Type: "PRIVATE KEY",
1203 Bytes: buf,
1204 }
1205 if err := pem.Encode(f, &block); err != nil {
1206 return fmt.Errorf("write as pem: %v", err)
1207 }
1208 if err := f.Close(); err != nil {
1209 return fmt.Errorf("close: %v", err)
1210 }
1211 f = nil
1212 return nil
1213 }
1214
1215 mustLoadConfig()
1216 timestamp := time.Now().Format("20060102T150405")
1217 didCreate := false
1218 for listenerName, l := range mox.Conf.Static.Listeners {
1219 if l.TLS == nil || l.TLS.ACME == "" {
1220 continue
1221 }
1222 haveKeyTypes := map[autocert.KeyType]bool{}
1223 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
1224 p := mox.ConfigDirPath(privKeyFile)
1225 f, err := os.Open(p)
1226 xcheckf(err, "open host private key")
1227 privKey, err := loadPrivateKey(f)
1228 if err := f.Close(); err != nil {
1229 log.Printf("closing host private key file: %v", err)
1230 }
1231 xcheckf(err, "loading host private key")
1232 switch k := privKey.(type) {
1233 case *rsa.PrivateKey:
1234 if k.N.BitLen() == 2048 {
1235 haveKeyTypes[autocert.KeyRSA2048] = true
1236 }
1237 case *ecdsa.PrivateKey:
1238 if k.Curve == elliptic.P256() {
1239 haveKeyTypes[autocert.KeyECDSAP256] = true
1240 }
1241 }
1242 }
1243 created := []string{}
1244 for _, kt := range []autocert.KeyType{autocert.KeyRSA2048, autocert.KeyECDSAP256} {
1245 if haveKeyTypes[kt] {
1246 continue
1247 }
1248 // Lookup key in ACME cache.
1249 host := l.HostnameDomain
1250 if host.ASCII == "" {
1251 host = mox.Conf.Static.HostnameDomain
1252 }
1253 filename := host.ASCII
1254 kind := "ecdsap256"
1255 if kt == autocert.KeyRSA2048 {
1256 filename += "+rsa"
1257 kind = "rsa2048"
1258 }
1259 p := mox.DataDirPath(filepath.Join("acme", "keycerts", l.TLS.ACME, filename))
1260 privKey := xtryLoadPrivateKey(kt, p)
1261
1262 relPath := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", host.Name(), timestamp, kind))
1263 destPath := mox.ConfigDirPath(relPath)
1264 err := writeHostPrivateKey(privKey, destPath)
1265 xcheckf(err, "writing host private key file to %s: %v", destPath, err)
1266 created = append(created, relPath)
1267 fmt.Printf("Wrote host private key: %s\n", destPath)
1268 }
1269 didCreate = didCreate || len(created) > 0
1270 if len(created) > 0 {
1271 tls := config.TLS{
1272 HostPrivateKeyFiles: append(l.TLS.HostPrivateKeyFiles, created...),
1273 }
1274 fmt.Printf("\nEnsure Listener %q in %s has the following in its TLS section, below \"ACME: %s\" (don't forget to indent with tabs):\n\n", listenerName, mox.ConfigStaticPath, l.TLS.ACME)
1275 err := sconf.Write(os.Stdout, tls)
1276 xcheckf(err, "writing new TLS.HostPrivateKeyFiles section")
1277 fmt.Println()
1278 }
1279 }
1280 if didCreate {
1281 fmt.Printf(`
1282After updating mox.conf and restarting, run "mox config dnsrecords" for a
1283domain and create the TLSA DNS records it suggests to enable DANE.
1284`)
1285 }
1286}
1287
1288func cmdLoglevels(c *cmd) {
1289 c.params = "[level [pkg]]"
1290 c.help = `Print the log levels, or set a new default log level, or a level for the given package.
1291
1292By default, a single log level applies to all logging in mox. But for each
1293"pkg", an overriding log level can be configured. Examples of packages:
1294smtpserver, smtpclient, queue, imapserver, spf, dkim, dmarc, junk, message,
1295etc.
1296
1297Specify a pkg and an empty level to clear the configured level for a package.
1298
1299Valid labels: error, info, debug, trace, traceauth, tracedata.
1300`
1301 args := c.Parse()
1302 if len(args) > 2 {
1303 c.Usage()
1304 }
1305 mustLoadConfig()
1306
1307 if len(args) == 0 {
1308 ctlcmdLoglevels(xctl())
1309 } else {
1310 var pkg string
1311 if len(args) == 2 {
1312 pkg = args[1]
1313 }
1314 ctlcmdSetLoglevels(xctl(), pkg, args[0])
1315 }
1316}
1317
1318func ctlcmdLoglevels(ctl *ctl) {
1319 ctl.xwrite("loglevels")
1320 ctl.xreadok()
1321 ctl.xstreamto(os.Stdout)
1322}
1323
1324func ctlcmdSetLoglevels(ctl *ctl, pkg, level string) {
1325 ctl.xwrite("setloglevels")
1326 ctl.xwrite(pkg)
1327 ctl.xwrite(level)
1328 ctl.xreadok()
1329}
1330
1331func cmdStop(c *cmd) {
1332 c.help = `Shut mox down, giving connections maximum 3 seconds to stop before closing them.
1333
1334While shutting down, new IMAP and SMTP connections will get a status response
1335indicating temporary unavailability. Existing connections will get a 3 second
1336period to finish their transaction and shut down. Under normal circumstances,
1337only IMAP has long-living connections, with the IDLE command to get notified of
1338new mail deliveries.
1339`
1340 if len(c.Parse()) != 0 {
1341 c.Usage()
1342 }
1343 mustLoadConfig()
1344
1345 ctl := xctl()
1346 ctl.xwrite("stop")
1347 // Read will hang until remote has shut down.
1348 buf := make([]byte, 128)
1349 n, err := ctl.conn.Read(buf)
1350 if err == nil {
1351 log.Fatalf("expected eof after graceful shutdown, got data %q", buf[:n])
1352 } else if err != io.EOF {
1353 log.Fatalf("expected eof after graceful shutdown, got error %v", err)
1354 }
1355 fmt.Println("mox stopped")
1356}
1357
1358func cmdBackup(c *cmd) {
1359 c.params = "dest-dir"
1360 c.help = `Creates a backup of the data directory.
1361
1362Backup creates consistent snapshots of the databases and message files and
1363copies other files in the data directory. Empty directories are not copied.
1364These files can then be stored elsewhere for long-term storage, or used to fall
1365back to should an upgrade fail. Simply copying files in the data directory
1366while mox is running can result in unusable database files.
1367
1368Message files never change (they are read-only, though can be removed) and are
1369hard-linked so they don't consume additional space. If hardlinking fails, for
1370example when the backup destination directory is on a different file system, a
1371regular copy is made. Using a destination directory like "data/tmp/backup"
1372increases the odds hardlinking succeeds: the default systemd service file
1373specifically mounts the data directory, causing attempts to hardlink outside it
1374to fail with an error about cross-device linking.
1375
1376All files in the data directory that aren't recognized (i.e. other than known
1377database files, message files, an acme directory, the "tmp" directory, etc),
1378are stored, but with a warning.
1379
1380Remove files in the destination directory before doing another backup. The
1381backup command will not overwrite files, but print and return errors.
1382
1383Exit code 0 indicates the backup was successful. A clean successful backup does
1384not print any output, but may print warnings. Use the -verbose flag for
1385details, including timing.
1386
1387To restore a backup, first shut down mox, move away the old data directory and
1388move an earlier backed up directory in its place, run "mox verifydata",
1389possibly with the "-fix" option, and restart mox. After the restore, you may
1390also want to run "mox bumpuidvalidity" for each account for which messages in a
1391mailbox changed, to force IMAP clients to synchronize mailbox state.
1392
1393Before upgrading, to check if the upgrade will likely succeed, first make a
1394backup, then use the new mox binary to run "mox verifydata" on the backup. This
1395can change the backup files (e.g. upgrade database files, move away
1396unrecognized message files), so you should make a new backup before actually
1397upgrading.
1398`
1399
1400 var verbose bool
1401 c.flag.BoolVar(&verbose, "verbose", false, "print progress")
1402 args := c.Parse()
1403 if len(args) != 1 {
1404 c.Usage()
1405 }
1406 mustLoadConfig()
1407
1408 dstDataDir, err := filepath.Abs(args[0])
1409 xcheckf(err, "making path absolute")
1410
1411 ctlcmdBackup(xctl(), dstDataDir, verbose)
1412}
1413
1414func ctlcmdBackup(ctl *ctl, dstDataDir string, verbose bool) {
1415 ctl.xwrite("backup")
1416 ctl.xwrite(dstDataDir)
1417 if verbose {
1418 ctl.xwrite("verbose")
1419 } else {
1420 ctl.xwrite("")
1421 }
1422 ctl.xstreamto(os.Stdout)
1423 ctl.xreadok()
1424}
1425
1426func cmdSetadminpassword(c *cmd) {
1427 c.help = `Set a new admin password, for the web interface.
1428
1429The password is read from stdin. Its bcrypt hash is stored in a file named
1430"adminpasswd" in the configuration directory.
1431`
1432 if len(c.Parse()) != 0 {
1433 c.Usage()
1434 }
1435 mustLoadConfig()
1436
1437 path := mox.ConfigDirPath(mox.Conf.Static.AdminPasswordFile)
1438 if path == "" {
1439 log.Fatal("no admin password file configured")
1440 }
1441
1442 pw := xreadpassword()
1443 pw, err := precis.OpaqueString.String(pw)
1444 xcheckf(err, `checking password with "precis" requirements`)
1445 hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
1446 xcheckf(err, "generating hash for password")
1447 err = os.WriteFile(path, hash, 0660)
1448 xcheckf(err, "writing hash to admin password file")
1449}
1450
1451func xreadpassword() string {
1452 fmt.Printf(`
1453Type new password. Password WILL echo.
1454
1455WARNING: Bots will try to bruteforce your password. Connections with failed
1456authentication attempts will be rate limited but attackers WILL find weak
1457passwords. If your account is compromised, spammers are likely to abuse your
1458system, spamming your address and the wider internet in your name. So please
1459pick a random, unguessable password, preferably at least 12 characters.
1460
1461`)
1462 fmt.Printf("password: ")
1463 buf := make([]byte, 64)
1464 n, err := os.Stdin.Read(buf)
1465 xcheckf(err, "reading stdin")
1466 pw := string(buf[:n])
1467 pw = strings.TrimSuffix(strings.TrimSuffix(pw, "\r\n"), "\n")
1468 if len(pw) < 8 {
1469 log.Fatal("password must be at least 8 characters")
1470 }
1471 return pw
1472}
1473
1474func cmdSetaccountpassword(c *cmd) {
1475 c.params = "account"
1476 c.help = `Set new password an account.
1477
1478The password is read from stdin. Secrets derived from the password, but not the
1479password itself, are stored in the account database. The stored secrets are for
1480authentication with: scram-sha-256, scram-sha-1, cram-md5, plain text (bcrypt
1481hash).
1482
1483The parameter is an account name, as configured under Accounts in domains.conf
1484and as present in the data/accounts/ directory, not a configured email address
1485for an account.
1486`
1487 args := c.Parse()
1488 if len(args) != 1 {
1489 c.Usage()
1490 }
1491 mustLoadConfig()
1492
1493 pw := xreadpassword()
1494
1495 ctlcmdSetaccountpassword(xctl(), args[0], pw)
1496}
1497
1498func ctlcmdSetaccountpassword(ctl *ctl, account, password string) {
1499 ctl.xwrite("setaccountpassword")
1500 ctl.xwrite(account)
1501 ctl.xwrite(password)
1502 ctl.xreadok()
1503}
1504
1505func cmdDeliver(c *cmd) {
1506 c.unlisted = true
1507 c.params = "address < message"
1508 c.help = "Deliver message to address."
1509 args := c.Parse()
1510 if len(args) != 1 {
1511 c.Usage()
1512 }
1513 mustLoadConfig()
1514 ctlcmdDeliver(xctl(), args[0])
1515}
1516
1517func ctlcmdDeliver(ctl *ctl, address string) {
1518 ctl.xwrite("deliver")
1519 ctl.xwrite(address)
1520 ctl.xreadok()
1521 ctl.xstreamfrom(os.Stdin)
1522 line := ctl.xread()
1523 if line == "ok" {
1524 fmt.Println("message delivered")
1525 } else {
1526 log.Fatalf("deliver: %s", line)
1527 }
1528}
1529
1530func cmdDKIMGenrsa(c *cmd) {
1531 c.params = ">$selector._domainkey.$domain.rsa2048.privatekey.pkcs8.pem"
1532 c.help = `Generate a new 2048 bit RSA private key for use with DKIM.
1533
1534The generated file is in PEM format, and has a comment it is generated for use
1535with DKIM, by mox.
1536`
1537 if len(c.Parse()) != 0 {
1538 c.Usage()
1539 }
1540
1541 buf, err := mox.MakeDKIMRSAKey(dns.Domain{}, dns.Domain{})
1542 xcheckf(err, "making rsa private key")
1543 _, err = os.Stdout.Write(buf)
1544 xcheckf(err, "writing rsa private key")
1545}
1546
1547func cmdDANEDial(c *cmd) {
1548 c.params = "host:port"
1549 var usages string
1550 c.flag.StringVar(&usages, "usages", "pkix-ta,pkix-ee,dane-ta,dane-ee", "allowed usages for dane, comma-separated list")
1551 c.help = `Dial the address using TLS with certificate verification using DANE.
1552
1553Data is copied between connection and stdin/stdout until either side closes the
1554connection.
1555`
1556 args := c.Parse()
1557 if len(args) != 1 {
1558 c.Usage()
1559 }
1560
1561 allowedUsages := []adns.TLSAUsage{}
1562 if usages != "" {
1563 for _, s := range strings.Split(usages, ",") {
1564 var usage adns.TLSAUsage
1565 switch strings.ToLower(s) {
1566 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
1567 usage = adns.TLSAUsagePKIXTA
1568 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
1569 usage = adns.TLSAUsagePKIXEE
1570 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
1571 usage = adns.TLSAUsageDANETA
1572 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
1573 usage = adns.TLSAUsageDANEEE
1574 default:
1575 log.Fatalf("unknown dane usage %q", s)
1576 }
1577 allowedUsages = append(allowedUsages, usage)
1578 }
1579 }
1580
1581 pkixRoots, err := x509.SystemCertPool()
1582 xcheckf(err, "get system pkix certificate pool")
1583
1584 resolver := dns.StrictResolver{Pkg: "danedial"}
1585 conn, record, err := dane.Dial(context.Background(), c.log.Logger, resolver, "tcp", args[0], allowedUsages, pkixRoots)
1586 xcheckf(err, "dial")
1587 log.Printf("(connected, verified with %s)", record)
1588
1589 go func() {
1590 _, err := io.Copy(os.Stdout, conn)
1591 xcheckf(err, "copy from connection to stdout")
1592 conn.Close()
1593 }()
1594 _, err = io.Copy(conn, os.Stdin)
1595 xcheckf(err, "copy from stdin to connection")
1596}
1597
1598func cmdDANEDialmx(c *cmd) {
1599 c.params = "domain [destination-host]"
1600 var ehloHostname string
1601 c.flag.StringVar(&ehloHostname, "ehlohostname", "localhost", "hostname to send in smtp ehlo command")
1602 c.help = `Connect to MX server for domain using STARTTLS verified with DANE.
1603
1604If no destination host is specified, regular delivery logic is used to find the
1605hosts to attempt delivery too. This involves following CNAMEs for the domain,
1606looking up MX records, and possibly falling back to the domain name itself as
1607host.
1608
1609If a destination host is specified, that is the only candidate host considered
1610for dialing.
1611
1612With a list of destinations gathered, each is dialed until a successful SMTP
1613session verified with DANE has been initialized, including EHLO and STARTTLS
1614commands.
1615
1616Once connected, data is copied between connection and stdin/stdout, until
1617either side closes the connection.
1618
1619This command follows the same logic as delivery attempts made from the queue,
1620sharing most of its code.
1621`
1622 args := c.Parse()
1623 if len(args) != 1 && len(args) != 2 {
1624 c.Usage()
1625 }
1626
1627 ehloDomain, err := dns.ParseDomain(ehloHostname)
1628 xcheckf(err, "parsing ehlo hostname")
1629
1630 origNextHop, err := dns.ParseDomain(args[0])
1631 xcheckf(err, "parse domain")
1632
1633 ctxbg := context.Background()
1634
1635 resolver := dns.StrictResolver{}
1636 var haveMX bool
1637 var origNextHopAuthentic, expandedNextHopAuthentic bool
1638 var expandedNextHop dns.Domain
1639 var hosts []dns.IPDomain
1640 if len(args) == 1 {
1641 var permanent bool
1642 haveMX, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hosts, permanent, err = smtpclient.GatherDestinations(ctxbg, c.log.Logger, resolver, dns.IPDomain{Domain: origNextHop})
1643 status := "temporary"
1644 if permanent {
1645 status = "permanent"
1646 }
1647 if err != nil {
1648 log.Fatalf("gathering destinations: %v (%s)", err, status)
1649 }
1650 if expandedNextHop != origNextHop {
1651 log.Printf("followed cnames to %s", expandedNextHop)
1652 }
1653 if haveMX {
1654 log.Printf("found mx record, trying mx hosts")
1655 } else {
1656 log.Printf("no mx record found, will try to connect to domain directly")
1657 }
1658 if !origNextHopAuthentic {
1659 log.Fatalf("error: initial domain not dnssec-secure")
1660 }
1661 if !expandedNextHopAuthentic {
1662 log.Fatalf("error: expanded domain not dnssec-secure")
1663 }
1664
1665 l := []string{}
1666 for _, h := range hosts {
1667 l = append(l, h.String())
1668 }
1669 log.Printf("destinations: %s", strings.Join(l, ", "))
1670 } else {
1671 d, err := dns.ParseDomain(args[1])
1672 if err != nil {
1673 log.Fatalf("parsing destination host: %v", err)
1674 }
1675 log.Printf("skipping domain mx/cname lookups, assuming domain is dnssec-protected")
1676
1677 origNextHopAuthentic = true
1678 expandedNextHopAuthentic = true
1679 expandedNextHop = d
1680 hosts = []dns.IPDomain{{Domain: d}}
1681 }
1682
1683 dialedIPs := map[string][]net.IP{}
1684 for _, host := range hosts {
1685 // It should not be possible for hosts to have IP addresses: They are not
1686 // allowed by dns.ParseDomain, and MX records cannot contain them.
1687 if host.IsIP() {
1688 log.Fatalf("unexpected IP address for destination host")
1689 }
1690
1691 log.Printf("attempting to connect to %s", host)
1692
1693 authentic, expandedAuthentic, expandedHost, ips, _, err := smtpclient.GatherIPs(ctxbg, c.log.Logger, resolver, "ip", host, dialedIPs)
1694 if err != nil {
1695 log.Printf("resolving ips for %s: %v, skipping", host, err)
1696 continue
1697 }
1698 if !authentic {
1699 log.Printf("no dnssec for ips of %s, skipping", host)
1700 continue
1701 }
1702 if !expandedAuthentic {
1703 log.Printf("no dnssec for cname-followed ips of %s, skipping", host)
1704 continue
1705 }
1706 if expandedHost != host.Domain {
1707 log.Printf("host %s cname-expanded to %s", host, expandedHost)
1708 }
1709 log.Printf("host %s resolved to ips %s, looking up tlsa records", host, ips)
1710
1711 daneRequired, daneRecords, tlsaBaseDomain, err := smtpclient.GatherTLSA(ctxbg, c.log.Logger, resolver, host.Domain, expandedAuthentic, expandedHost)
1712 if err != nil {
1713 log.Printf("looking up tlsa records: %s, skipping", err)
1714 continue
1715 }
1716 tlsMode := smtpclient.TLSRequiredStartTLS
1717 if len(daneRecords) == 0 {
1718 if !daneRequired {
1719 log.Printf("host %s has no tlsa records, skipping", expandedHost)
1720 continue
1721 }
1722 log.Printf("warning: only unusable tlsa records found, continuing with required tls without certificate verification")
1723 daneRecords = nil
1724 } else {
1725 var l []string
1726 for _, r := range daneRecords {
1727 l = append(l, r.String())
1728 }
1729 log.Printf("tlsa records: %s", strings.Join(l, "; "))
1730 }
1731
1732 tlsHostnames := smtpclient.GatherTLSANames(haveMX, expandedNextHopAuthentic, expandedAuthentic, origNextHop, expandedNextHop, host.Domain, tlsaBaseDomain)
1733 var l []string
1734 for _, name := range tlsHostnames {
1735 l = append(l, name.String())
1736 }
1737 log.Printf("gathered valid tls certificate names for potential verification with dane-ta: %s", strings.Join(l, ", "))
1738
1739 dialer := &net.Dialer{Timeout: 5 * time.Second}
1740 conn, _, err := smtpclient.Dial(ctxbg, c.log.Logger, dialer, dns.IPDomain{Domain: expandedHost}, ips, 25, dialedIPs, nil)
1741 if err != nil {
1742 log.Printf("dial %s: %v, skipping", expandedHost, err)
1743 continue
1744 }
1745 log.Printf("connected to %s, %s, starting smtp session with ehlo and starttls with dane verification", expandedHost, conn.RemoteAddr())
1746
1747 var verifiedRecord adns.TLSA
1748 opts := smtpclient.Opts{
1749 DANERecords: daneRecords,
1750 DANEMoreHostnames: tlsHostnames[1:],
1751 DANEVerifiedRecord: &verifiedRecord,
1752 RootCAs: mox.Conf.Static.TLS.CertPool,
1753 }
1754 tlsPKIX := false
1755 sc, err := smtpclient.New(ctxbg, c.log.Logger, conn, tlsMode, tlsPKIX, ehloDomain, tlsHostnames[0], opts)
1756 if err != nil {
1757 log.Printf("setting up smtp session: %v, skipping", err)
1758 conn.Close()
1759 continue
1760 }
1761
1762 smtpConn, err := sc.Conn()
1763 if err != nil {
1764 log.Fatalf("error: taking over smtp connection: %s", err)
1765 }
1766 log.Printf("tls verified with tlsa record: %s", verifiedRecord)
1767 log.Printf("smtp session initialized and connected to stdin/stdout")
1768
1769 go func() {
1770 _, err := io.Copy(os.Stdout, smtpConn)
1771 xcheckf(err, "copy from connection to stdout")
1772 smtpConn.Close()
1773 }()
1774 _, err = io.Copy(smtpConn, os.Stdin)
1775 xcheckf(err, "copy from stdin to connection")
1776 }
1777
1778 log.Fatalf("no remaining destinations")
1779}
1780
1781func cmdDANEMakeRecord(c *cmd) {
1782 c.params = "usage selector matchtype [certificate.pem | publickey.pem | privatekey.pem]"
1783 c.help = `Print TLSA record for given certificate/key and parameters.
1784
1785Valid values:
1786- usage: pkix-ta (0), pkix-ee (1), dane-ta (2), dane-ee (3)
1787- selector: cert (0), spki (1)
1788- matchtype: full (0), sha2-256 (1), sha2-512 (2)
1789
1790Common DANE TLSA record parameters are: dane-ee spki sha2-256, or 3 1 1,
1791followed by a sha2-256 hash of the DER-encoded "SPKI" (subject public key info)
1792from the certificate. An example DNS zone file entry:
1793
1794 _25._tcp.example.com. TLSA 3 1 1 133b919c9d65d8b1488157315327334ead8d83372db57465ecabf53ee5748aee
1795
1796The first usable information from the pem file is used to compose the TLSA
1797record. In case of selector "cert", a certificate is required. Otherwise the
1798"subject public key info" (spki) of the first certificate or public or private
1799key (pkcs#8, pkcs#1 or ec private key) is used.
1800`
1801
1802 args := c.Parse()
1803 if len(args) != 4 {
1804 c.Usage()
1805 }
1806
1807 var usage adns.TLSAUsage
1808 switch strings.ToLower(args[0]) {
1809 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
1810 usage = adns.TLSAUsagePKIXTA
1811 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
1812 usage = adns.TLSAUsagePKIXEE
1813 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
1814 usage = adns.TLSAUsageDANETA
1815 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
1816 usage = adns.TLSAUsageDANEEE
1817 default:
1818 if v, err := strconv.ParseUint(args[0], 10, 16); err != nil {
1819 log.Fatalf("bad usage %q", args[0])
1820 } else {
1821 // Does not influence certificate association data, so we can accept other numbers.
1822 log.Printf("warning: continuing with unrecognized tlsa usage %d", v)
1823 usage = adns.TLSAUsage(v)
1824 }
1825 }
1826
1827 var selector adns.TLSASelector
1828 switch strings.ToLower(args[1]) {
1829 case "cert", strconv.Itoa(int(adns.TLSASelectorCert)):
1830 selector = adns.TLSASelectorCert
1831 case "spki", strconv.Itoa(int(adns.TLSASelectorSPKI)):
1832 selector = adns.TLSASelectorSPKI
1833 default:
1834 log.Fatalf("bad selector %q", args[1])
1835 }
1836
1837 var matchType adns.TLSAMatchType
1838 switch strings.ToLower(args[2]) {
1839 case "full", strconv.Itoa(int(adns.TLSAMatchTypeFull)):
1840 matchType = adns.TLSAMatchTypeFull
1841 case "sha2-256", strconv.Itoa(int(adns.TLSAMatchTypeSHA256)):
1842 matchType = adns.TLSAMatchTypeSHA256
1843 case "sha2-512", strconv.Itoa(int(adns.TLSAMatchTypeSHA512)):
1844 matchType = adns.TLSAMatchTypeSHA512
1845 default:
1846 log.Fatalf("bad matchtype %q", args[2])
1847 }
1848
1849 buf, err := os.ReadFile(args[3])
1850 xcheckf(err, "reading certificate")
1851 for {
1852 var block *pem.Block
1853 block, buf = pem.Decode(buf)
1854 if block == nil {
1855 extra := ""
1856 if len(buf) > 0 {
1857 extra = " (with leftover data from pem file)"
1858 }
1859 if selector == adns.TLSASelectorCert {
1860 log.Fatalf("no certificate found in pem file%s", extra)
1861 } else {
1862 log.Fatalf("no certificate or public or private key found in pem file%s", extra)
1863 }
1864 }
1865 var cert *x509.Certificate
1866 var data []byte
1867 if block.Type == "CERTIFICATE" {
1868 cert, err = x509.ParseCertificate(block.Bytes)
1869 xcheckf(err, "parse certificate")
1870 switch selector {
1871 case adns.TLSASelectorCert:
1872 data = cert.Raw
1873 case adns.TLSASelectorSPKI:
1874 data = cert.RawSubjectPublicKeyInfo
1875 }
1876 } else if selector == adns.TLSASelectorCert {
1877 // We need a certificate, just a public/private key won't do.
1878 log.Printf("skipping pem type %q, certificate is required", block.Type)
1879 continue
1880 } else {
1881 var privKey, pubKey any
1882 var err error
1883 switch block.Type {
1884 case "PUBLIC KEY":
1885 _, err := x509.ParsePKIXPublicKey(block.Bytes)
1886 xcheckf(err, "parse pkix subject public key info (spki)")
1887 data = block.Bytes
1888 case "EC PRIVATE KEY":
1889 privKey, err = x509.ParseECPrivateKey(block.Bytes)
1890 xcheckf(err, "parse ec private key")
1891 case "RSA PRIVATE KEY":
1892 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
1893 xcheckf(err, "parse pkcs#1 rsa private key")
1894 case "RSA PUBLIC KEY":
1895 pubKey, err = x509.ParsePKCS1PublicKey(block.Bytes)
1896 xcheckf(err, "parse pkcs#1 rsa public key")
1897 case "PRIVATE KEY":
1898 // PKCS#8 private key
1899 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
1900 xcheckf(err, "parse pkcs#8 private key")
1901 default:
1902 log.Printf("skipping unrecognized pem type %q", block.Type)
1903 continue
1904 }
1905 if data == nil {
1906 if pubKey == nil && privKey != nil {
1907 if signer, ok := privKey.(crypto.Signer); !ok {
1908 log.Fatalf("private key of type %T is not a signer, cannot get public key", privKey)
1909 } else {
1910 pubKey = signer.Public()
1911 }
1912 }
1913 if pubKey == nil {
1914 // Should not happen.
1915 log.Fatalf("internal error: did not find private or public key")
1916 }
1917 data, err = x509.MarshalPKIXPublicKey(pubKey)
1918 xcheckf(err, "marshal pkix subject public key info (spki)")
1919 }
1920 }
1921
1922 switch matchType {
1923 case adns.TLSAMatchTypeFull:
1924 case adns.TLSAMatchTypeSHA256:
1925 p := sha256.Sum256(data)
1926 data = p[:]
1927 case adns.TLSAMatchTypeSHA512:
1928 p := sha512.Sum512(data)
1929 data = p[:]
1930 }
1931 fmt.Printf("%d %d %d %x\n", usage, selector, matchType, data)
1932 break
1933 }
1934}
1935
1936func cmdDNSLookup(c *cmd) {
1937 c.params = "[ptr | mx | cname | ips | a | aaaa | ns | txt | srv | tlsa] name"
1938 c.help = `Lookup DNS name of given type.
1939
1940Lookup always prints whether the response was DNSSEC-protected.
1941
1942Examples:
1943
1944mox dns lookup ptr 1.1.1.1
1945mox dns lookup mx xmox.nl
1946mox dns lookup txt _dmarc.xmox.nl.
1947mox dns lookup tlsa _25._tcp.xmox.nl
1948`
1949 args := c.Parse()
1950
1951 if len(args) != 2 {
1952 c.Usage()
1953 }
1954
1955 resolver := dns.StrictResolver{Pkg: "dns"}
1956
1957 // like xparseDomain, but treat unparseable domain as an ASCII name so names with
1958 // underscores are still looked up, e,g <selector>._domainkey.<host>.
1959 xdomain := func(s string) dns.Domain {
1960 d, err := dns.ParseDomain(s)
1961 if err != nil {
1962 return dns.Domain{ASCII: strings.TrimSuffix(s, ".")}
1963 }
1964 return d
1965 }
1966
1967 cmd, name := args[0], args[1]
1968
1969 switch cmd {
1970 case "ptr":
1971 ip := xparseIP(name, "ip")
1972 ptrs, result, err := resolver.LookupAddr(context.Background(), ip.String())
1973 if err != nil {
1974 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
1975 }
1976 fmt.Printf("names (%d, %s):\n", len(ptrs), dnssecStatus(result.Authentic))
1977 for _, ptr := range ptrs {
1978 fmt.Printf("- %s\n", ptr)
1979 }
1980
1981 case "mx":
1982 name := xdomain(name)
1983 mxl, result, err := resolver.LookupMX(context.Background(), name.ASCII+".")
1984 if err != nil {
1985 log.Printf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
1986 // We can still have valid records...
1987 }
1988 fmt.Printf("mx records (%d, %s):\n", len(mxl), dnssecStatus(result.Authentic))
1989 for _, mx := range mxl {
1990 fmt.Printf("- %s, preference %d\n", mx.Host, mx.Pref)
1991 }
1992
1993 case "cname":
1994 name := xdomain(name)
1995 target, result, err := resolver.LookupCNAME(context.Background(), name.ASCII+".")
1996 if err != nil {
1997 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
1998 }
1999 fmt.Printf("%s (%s)\n", target, dnssecStatus(result.Authentic))
2000
2001 case "ips", "a", "aaaa":
2002 network := "ip"
2003 if cmd == "a" {
2004 network = "ip4"
2005 } else if cmd == "aaaa" {
2006 network = "ip6"
2007 }
2008 name := xdomain(name)
2009 ips, result, err := resolver.LookupIP(context.Background(), network, name.ASCII+".")
2010 if err != nil {
2011 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2012 }
2013 fmt.Printf("records (%d, %s):\n", len(ips), dnssecStatus(result.Authentic))
2014 for _, ip := range ips {
2015 fmt.Printf("- %s\n", ip)
2016 }
2017
2018 case "ns":
2019 name := xdomain(name)
2020 nsl, result, err := resolver.LookupNS(context.Background(), name.ASCII+".")
2021 if err != nil {
2022 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2023 }
2024 fmt.Printf("ns records (%d, %s):\n", len(nsl), dnssecStatus(result.Authentic))
2025 for _, ns := range nsl {
2026 fmt.Printf("- %s\n", ns)
2027 }
2028
2029 case "txt":
2030 host := xdomain(name)
2031 l, result, err := resolver.LookupTXT(context.Background(), host.ASCII+".")
2032 if err != nil {
2033 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2034 }
2035 fmt.Printf("txt records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2036 for _, txt := range l {
2037 fmt.Printf("- %s\n", txt)
2038 }
2039
2040 case "srv":
2041 host := xdomain(name)
2042 _, l, result, err := resolver.LookupSRV(context.Background(), "", "", host.ASCII+".")
2043 if err != nil {
2044 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2045 }
2046 fmt.Printf("srv records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2047 for _, srv := range l {
2048 fmt.Printf("- host %s, port %d, priority %d, weight %d\n", srv.Target, srv.Port, srv.Priority, srv.Weight)
2049 }
2050
2051 case "tlsa":
2052 host := xdomain(name)
2053 l, result, err := resolver.LookupTLSA(context.Background(), 0, "", host.ASCII+".")
2054 if err != nil {
2055 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2056 }
2057 fmt.Printf("tlsa records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2058 for _, tlsa := range l {
2059 fmt.Printf("- usage %q (%d), selector %q (%d), matchtype %q (%d), certificate association data %x\n", tlsa.Usage, tlsa.Usage, tlsa.Selector, tlsa.Selector, tlsa.MatchType, tlsa.MatchType, tlsa.CertAssoc)
2060 }
2061 default:
2062 log.Fatalf("unknown record type %q", args[0])
2063 }
2064}
2065
2066func cmdDKIMGened25519(c *cmd) {
2067 c.params = ">$selector._domainkey.$domain.ed25519.privatekey.pkcs8.pem"
2068 c.help = `Generate a new ed25519 key for use with DKIM.
2069
2070Ed25519 keys are much smaller than RSA keys of comparable cryptographic
2071strength. This is convenient because of maximum DNS message sizes. At the time
2072of writing, not many mail servers appear to support ed25519 DKIM keys though,
2073so it is recommended to sign messages with both RSA and ed25519 keys.
2074`
2075 if len(c.Parse()) != 0 {
2076 c.Usage()
2077 }
2078
2079 buf, err := mox.MakeDKIMEd25519Key(dns.Domain{}, dns.Domain{})
2080 xcheckf(err, "making dkim ed25519 key")
2081 _, err = os.Stdout.Write(buf)
2082 xcheckf(err, "writing dkim ed25519 key")
2083}
2084
2085func cmdDKIMTXT(c *cmd) {
2086 c.params = "<$selector._domainkey.$domain.key.pkcs8.pem"
2087 c.help = `Print a DKIM DNS TXT record with the public key derived from the private key read from stdin.
2088
2089The DNS should be configured as a TXT record at $selector._domainkey.$domain.
2090`
2091 if len(c.Parse()) != 0 {
2092 c.Usage()
2093 }
2094
2095 privKey, err := parseDKIMKey(os.Stdin)
2096 xcheckf(err, "reading dkim private key from stdin")
2097
2098 r := dkim.Record{
2099 Version: "DKIM1",
2100 Hashes: []string{"sha256"},
2101 Flags: []string{"s"},
2102 }
2103
2104 switch key := privKey.(type) {
2105 case *rsa.PrivateKey:
2106 r.PublicKey = key.Public()
2107 case ed25519.PrivateKey:
2108 r.PublicKey = key.Public()
2109 r.Key = "ed25519"
2110 default:
2111 log.Fatalf("unsupported private key type %T, must be rsa or ed25519", privKey)
2112 }
2113
2114 record, err := r.Record()
2115 xcheckf(err, "making record")
2116 fmt.Print("<selector>._domainkey.<your.domain.> TXT ")
2117 for record != "" {
2118 s := record
2119 if len(s) > 100 {
2120 s, record = record[:100], record[100:]
2121 } else {
2122 record = ""
2123 }
2124 fmt.Printf(`"%s" `, s)
2125 }
2126 fmt.Println("")
2127}
2128
2129func parseDKIMKey(r io.Reader) (any, error) {
2130 buf, err := io.ReadAll(r)
2131 if err != nil {
2132 return nil, fmt.Errorf("reading pem from stdin: %v", err)
2133 }
2134 b, _ := pem.Decode(buf)
2135 if b == nil {
2136 return nil, fmt.Errorf("decoding pem: %v", err)
2137 }
2138 privKey, err := x509.ParsePKCS8PrivateKey(b.Bytes)
2139 if err != nil {
2140 return nil, fmt.Errorf("parsing private key: %v", err)
2141 }
2142 return privKey, nil
2143}
2144
2145func cmdDKIMVerify(c *cmd) {
2146 c.params = "message"
2147 c.help = `Verify the DKIM signatures in a message and print the results.
2148
2149The message is parsed, and the DKIM-Signature headers are validated. Validation
2150of older messages may fail because the DNS records have been removed or changed
2151by now, or because the signature header may have specified an expiration time
2152that was passed.
2153`
2154 args := c.Parse()
2155 if len(args) != 1 {
2156 c.Usage()
2157 }
2158
2159 msgf, err := os.Open(args[0])
2160 xcheckf(err, "open message")
2161
2162 results, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, false, dkim.DefaultPolicy, msgf, true)
2163 xcheckf(err, "dkim verify")
2164
2165 for _, result := range results {
2166 var sigh string
2167 if result.Sig == nil {
2168 log.Printf("warning: could not parse signature")
2169 } else {
2170 sigh, err = result.Sig.Header()
2171 if err != nil {
2172 log.Printf("warning: packing signature: %s", err)
2173 }
2174 }
2175 var txt string
2176 if result.Record == nil {
2177 log.Printf("warning: missing DNS record")
2178 } else {
2179 txt, err = result.Record.Record()
2180 if err != nil {
2181 log.Printf("warning: packing record: %s", err)
2182 }
2183 }
2184 fmt.Printf("status %q, err %v\nrecord %q\nheader %s\n", result.Status, result.Err, txt, sigh)
2185 }
2186}
2187
2188func cmdDKIMSign(c *cmd) {
2189 c.params = "message"
2190 c.help = `Sign a message, adding DKIM-Signature headers based on the domain in the From header.
2191
2192The message is parsed, the domain looked up in the configuration files, and
2193DKIM-Signature headers generated. The message is printed with the DKIM-Signature
2194headers prepended.
2195`
2196 args := c.Parse()
2197 if len(args) != 1 {
2198 c.Usage()
2199 }
2200
2201 msgf, err := os.Open(args[0])
2202 xcheckf(err, "open message")
2203 defer msgf.Close()
2204
2205 p, err := message.Parse(c.log.Logger, true, msgf)
2206 xcheckf(err, "parsing message")
2207
2208 if len(p.Envelope.From) != 1 {
2209 log.Fatalf("found %d from headers, need exactly 1", len(p.Envelope.From))
2210 }
2211 localpart, err := smtp.ParseLocalpart(p.Envelope.From[0].User)
2212 xcheckf(err, "parsing localpart of address in from-header")
2213 dom, err := dns.ParseDomain(p.Envelope.From[0].Host)
2214 xcheckf(err, "parsing domain of address in from-header")
2215
2216 mustLoadConfig()
2217
2218 domConf, ok := mox.Conf.Domain(dom)
2219 if !ok {
2220 log.Fatalf("domain %s not configured", dom)
2221 }
2222
2223 selectors := mox.DKIMSelectors(domConf.DKIM)
2224 headers, err := dkim.Sign(context.Background(), c.log.Logger, localpart, dom, selectors, false, msgf)
2225 xcheckf(err, "signing message with dkim")
2226 if headers == "" {
2227 log.Fatalf("no DKIM configured for domain %s", dom)
2228 }
2229 _, err = fmt.Fprint(os.Stdout, headers)
2230 xcheckf(err, "write headers")
2231 _, err = io.Copy(os.Stdout, msgf)
2232 xcheckf(err, "write message")
2233}
2234
2235func cmdDKIMLookup(c *cmd) {
2236 c.params = "selector domain"
2237 c.help = "Lookup and print the DKIM record for the selector at the domain."
2238 args := c.Parse()
2239 if len(args) != 2 {
2240 c.Usage()
2241 }
2242
2243 selector := xparseDomain(args[0], "selector")
2244 domain := xparseDomain(args[1], "domain")
2245
2246 status, record, txt, authentic, err := dkim.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, selector, domain)
2247 if err != nil {
2248 fmt.Printf("error: %s\n", err)
2249 }
2250 if status != dkim.StatusNeutral {
2251 fmt.Printf("status: %s\n", status)
2252 }
2253 if txt != "" {
2254 fmt.Printf("TXT record: %s\n", txt)
2255 }
2256 if authentic {
2257 fmt.Println("dnssec-signed: yes")
2258 } else {
2259 fmt.Println("dnssec-signed: no")
2260 }
2261 if record != nil {
2262 fmt.Printf("Record:\n")
2263 pairs := []any{
2264 "version", record.Version,
2265 "hashes", record.Hashes,
2266 "key", record.Key,
2267 "notes", record.Notes,
2268 "services", record.Services,
2269 "flags", record.Flags,
2270 }
2271 for i := 0; i < len(pairs); i += 2 {
2272 fmt.Printf("\t%s: %v\n", pairs[i], pairs[i+1])
2273 }
2274 }
2275}
2276
2277func cmdDMARCLookup(c *cmd) {
2278 c.params = "domain"
2279 c.help = "Lookup dmarc policy for domain, a DNS TXT record at _dmarc.<domain>, validate and print it."
2280 args := c.Parse()
2281 if len(args) != 1 {
2282 c.Usage()
2283 }
2284
2285 fromdomain := xparseDomain(args[0], "domain")
2286 _, domain, _, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, fromdomain)
2287 xcheckf(err, "dmarc lookup domain %s", fromdomain)
2288 fmt.Printf("dmarc record at domain %s: %s\n", domain, txt)
2289 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2290}
2291
2292func dnssecStatus(v bool) string {
2293 if v {
2294 return "with dnssec"
2295 }
2296 return "without dnssec"
2297}
2298
2299func cmdDMARCVerify(c *cmd) {
2300 c.params = "remoteip mailfromaddress helodomain < message"
2301 c.help = `Parse an email message and evaluate it against the DMARC policy of the domain in the From-header.
2302
2303mailfromaddress and helodomain are used for SPF validation. If both are empty,
2304SPF validation is skipped.
2305
2306mailfromaddress should be the address used as MAIL FROM in the SMTP session.
2307For DSN messages, that address may be empty. The helo domain was specified at
2308the beginning of the SMTP transaction that delivered the message. These values
2309can be found in message headers.
2310`
2311 args := c.Parse()
2312 if len(args) != 3 {
2313 c.Usage()
2314 }
2315
2316 var heloDomain *dns.Domain
2317
2318 remoteIP := xparseIP(args[0], "remoteip")
2319
2320 var mailfrom *smtp.Address
2321 if args[1] != "" {
2322 a, err := smtp.ParseAddress(args[1])
2323 xcheckf(err, "parsing mailfrom address")
2324 mailfrom = &a
2325 }
2326 if args[2] != "" {
2327 d := xparseDomain(args[2], "helo domain")
2328 heloDomain = &d
2329 }
2330 var received *spf.Received
2331 spfStatus := spf.StatusNone
2332 var spfIdentity *dns.Domain
2333 if mailfrom != nil || heloDomain != nil {
2334 spfArgs := spf.Args{
2335 RemoteIP: remoteIP,
2336 LocalIP: net.ParseIP("127.0.0.1"),
2337 LocalHostname: dns.Domain{ASCII: "localhost"},
2338 }
2339 if mailfrom != nil {
2340 spfArgs.MailFromLocalpart = mailfrom.Localpart
2341 spfArgs.MailFromDomain = mailfrom.Domain
2342 }
2343 if heloDomain != nil {
2344 spfArgs.HelloDomain = dns.IPDomain{Domain: *heloDomain}
2345 }
2346 rspf, spfDomain, expl, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfArgs)
2347 if err != nil {
2348 log.Printf("spf verify: %v (explanation: %q, authentic %v)", err, expl, authentic)
2349 } else {
2350 received = &rspf
2351 spfStatus = received.Result
2352 // todo: should probably potentially do two separate spf validations
2353 if mailfrom != nil {
2354 spfIdentity = &mailfrom.Domain
2355 } else {
2356 spfIdentity = heloDomain
2357 }
2358 fmt.Printf("spf result: %s: %s (%s)\n", spfDomain, spfStatus, dnssecStatus(authentic))
2359 }
2360 }
2361
2362 data, err := io.ReadAll(os.Stdin)
2363 xcheckf(err, "read message")
2364 dmarcFrom, _, _, err := message.From(c.log.Logger, false, bytes.NewReader(data), nil)
2365 xcheckf(err, "extract dmarc from message")
2366
2367 const ignoreTestMode = false
2368 dkimResults, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, true, func(*dkim.Sig) error { return nil }, bytes.NewReader(data), ignoreTestMode)
2369 xcheckf(err, "dkim verify")
2370 for _, r := range dkimResults {
2371 fmt.Printf("dkim result: %q (err %v)\n", r.Status, r.Err)
2372 }
2373
2374 _, result := dmarc.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, dmarcFrom.Domain, dkimResults, spfStatus, spfIdentity, false)
2375 xcheckf(result.Err, "dmarc verify")
2376 fmt.Printf("dmarc from: %s\ndmarc status: %q\ndmarc reject: %v\ncmarc record: %s\n", dmarcFrom, result.Status, result.Reject, result.Record)
2377}
2378
2379func cmdDMARCCheckreportaddrs(c *cmd) {
2380 c.params = "domain"
2381 c.help = `For each reporting address in the domain's DMARC record, check if it has opted into receiving reports (if needed).
2382
2383A DMARC record can request reports about DMARC evaluations to be sent to an
2384email/http address. If the organizational domains of that of the DMARC record
2385and that of the report destination address do not match, the destination
2386address must opt-in to receiving DMARC reports by creating a DMARC record at
2387<dmarcdomain>._report._dmarc.<reportdestdomain>.
2388`
2389 args := c.Parse()
2390 if len(args) != 1 {
2391 c.Usage()
2392 }
2393
2394 dom := xparseDomain(args[0], "domain")
2395 _, domain, record, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dom)
2396 xcheckf(err, "dmarc lookup domain %s", dom)
2397 fmt.Printf("dmarc record at domain %s: %q\n", domain, txt)
2398 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2399
2400 check := func(kind, addr string) {
2401 var authentic bool
2402
2403 printResult := func(format string, args ...any) {
2404 fmt.Printf("%s %s: %s (%s)\n", kind, addr, fmt.Sprintf(format, args...), dnssecStatus(authentic))
2405 }
2406
2407 u, err := url.Parse(addr)
2408 if err != nil {
2409 printResult("parsing uri: %v (skipping)", addr, err)
2410 return
2411 }
2412 var destdom dns.Domain
2413 switch u.Scheme {
2414 case "mailto":
2415 a, err := smtp.ParseAddress(u.Opaque)
2416 if err != nil {
2417 printResult("parsing destination email address %s: %v (skipping)", u.Opaque, err)
2418 return
2419 }
2420 destdom = a.Domain
2421 default:
2422 printResult("unrecognized scheme in reporting address %s (skipping)", u.Scheme)
2423 return
2424 }
2425
2426 if publicsuffix.Lookup(context.Background(), c.log.Logger, dom) == publicsuffix.Lookup(context.Background(), c.log.Logger, destdom) {
2427 printResult("pass (same organizational domain)")
2428 return
2429 }
2430
2431 accepts, status, _, txts, authentic, err := dmarc.LookupExternalReportsAccepted(context.Background(), c.log.Logger, dns.StrictResolver{}, domain, destdom)
2432 var txtstr string
2433 txtaddr := fmt.Sprintf("%s._report._dmarc.%s", domain.ASCII, destdom.ASCII)
2434 if len(txts) == 0 {
2435 txtstr = fmt.Sprintf(" (no txt records %s)", txtaddr)
2436 } else {
2437 txtstr = fmt.Sprintf(" (txt record %s: %q)", txtaddr, txts)
2438 }
2439 if status != dmarc.StatusNone {
2440 printResult("fail: %s%s", err, txtstr)
2441 } else if accepts {
2442 printResult("pass%s", txtstr)
2443 } else if err != nil {
2444 printResult("fail: %s%s", err, txtstr)
2445 } else {
2446 printResult("fail%s", txtstr)
2447 }
2448 }
2449
2450 for _, uri := range record.AggregateReportAddresses {
2451 check("aggregate reporting", uri.Address)
2452 }
2453 for _, uri := range record.FailureReportAddresses {
2454 check("failure reporting", uri.Address)
2455 }
2456}
2457
2458func cmdDMARCParsereportmsg(c *cmd) {
2459 c.params = "message ..."
2460 c.help = `Parse a DMARC report from an email message, and print its extracted details.
2461
2462DMARC reports are periodically mailed, if requested in the DMARC DNS record of
2463a domain. Reports are sent by mail servers that received messages with our
2464domain in a From header. This may or may not be legatimate email. DMARC reports
2465contain summaries of evaluations of DMARC and DKIM/SPF, which can help
2466understand email deliverability problems.
2467`
2468 args := c.Parse()
2469 if len(args) == 0 {
2470 c.Usage()
2471 }
2472
2473 for _, arg := range args {
2474 f, err := os.Open(arg)
2475 xcheckf(err, "open %q", arg)
2476 feedback, err := dmarcrpt.ParseMessageReport(c.log.Logger, f)
2477 xcheckf(err, "parse report in %q", arg)
2478 meta := feedback.ReportMetadata
2479 fmt.Printf("Report: period %s-%s, organisation %q, reportID %q, %s\n", time.Unix(meta.DateRange.Begin, 0).UTC().String(), time.Unix(meta.DateRange.End, 0).UTC().String(), meta.OrgName, meta.ReportID, meta.Email)
2480 if len(meta.Errors) > 0 {
2481 fmt.Printf("Errors:\n")
2482 for _, s := range meta.Errors {
2483 fmt.Printf("\t- %s\n", s)
2484 }
2485 }
2486 pol := feedback.PolicyPublished
2487 fmt.Printf("Policy: domain %q, policy %q, subdomainpolicy %q, dkim %q, spf %q, percentage %d, options %q\n", pol.Domain, pol.Policy, pol.SubdomainPolicy, pol.ADKIM, pol.ASPF, pol.Percentage, pol.ReportingOptions)
2488 for _, record := range feedback.Records {
2489 idents := record.Identifiers
2490 fmt.Printf("\theaderfrom %q, envelopes from %q, to %q\n", idents.HeaderFrom, idents.EnvelopeFrom, idents.EnvelopeTo)
2491 eval := record.Row.PolicyEvaluated
2492 var reasons string
2493 for _, reason := range eval.Reasons {
2494 reasons += "; " + string(reason.Type)
2495 if reason.Comment != "" {
2496 reasons += fmt.Sprintf(": %q", reason.Comment)
2497 }
2498 }
2499 fmt.Printf("\tresult %s: dkim %s, spf %s; sourceIP %s, count %d%s\n", eval.Disposition, eval.DKIM, eval.SPF, record.Row.SourceIP, record.Row.Count, reasons)
2500 for _, dkim := range record.AuthResults.DKIM {
2501 var result string
2502 if dkim.HumanResult != "" {
2503 result = fmt.Sprintf(": %q", dkim.HumanResult)
2504 }
2505 fmt.Printf("\t\tdkim %s; domain %q selector %q%s\n", dkim.Result, dkim.Domain, dkim.Selector, result)
2506 }
2507 for _, spf := range record.AuthResults.SPF {
2508 fmt.Printf("\t\tspf %s; domain %q scope %q\n", spf.Result, spf.Domain, spf.Scope)
2509 }
2510 }
2511 }
2512}
2513
2514func cmdDMARCDBAddReport(c *cmd) {
2515 c.unlisted = true
2516 c.params = "fromdomain < message"
2517 c.help = "Add a DMARC report to the database."
2518 args := c.Parse()
2519 if len(args) != 1 {
2520 c.Usage()
2521 }
2522
2523 mustLoadConfig()
2524
2525 fromdomain := xparseDomain(args[0], "domain")
2526 fmt.Fprintln(os.Stderr, "reading report message from stdin")
2527 report, err := dmarcrpt.ParseMessageReport(c.log.Logger, os.Stdin)
2528 xcheckf(err, "parse message")
2529 err = dmarcdb.AddReport(context.Background(), report, fromdomain)
2530 xcheckf(err, "add dmarc report")
2531}
2532
2533func cmdTLSRPTLookup(c *cmd) {
2534 c.params = "domain"
2535 c.help = `Lookup the TLSRPT record for the domain.
2536
2537A TLSRPT record typically contains an email address where reports about TLS
2538connectivity should be sent. Mail servers attempting delivery to our domain
2539should attempt to use TLS. TLSRPT lets them report how many connection
2540successfully used TLS, and how what kind of errors occurred otherwise.
2541`
2542 args := c.Parse()
2543 if len(args) != 1 {
2544 c.Usage()
2545 }
2546
2547 d := xparseDomain(args[0], "domain")
2548 _, txt, err := tlsrpt.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, d)
2549 xcheckf(err, "tlsrpt lookup for %s", d)
2550 fmt.Println(txt)
2551}
2552
2553func cmdTLSRPTParsereportmsg(c *cmd) {
2554 c.params = "message ..."
2555 c.help = `Parse and print the TLSRPT in the message.
2556
2557The report is printed in formatted JSON.
2558`
2559 args := c.Parse()
2560 if len(args) == 0 {
2561 c.Usage()
2562 }
2563
2564 for _, arg := range args {
2565 f, err := os.Open(arg)
2566 xcheckf(err, "open %q", arg)
2567 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, f)
2568 xcheckf(err, "parse report in %q", arg)
2569 // todo future: only print the highlights?
2570 enc := json.NewEncoder(os.Stdout)
2571 enc.SetIndent("", "\t")
2572 enc.SetEscapeHTML(false)
2573 err = enc.Encode(reportJSON)
2574 xcheckf(err, "write report")
2575 }
2576}
2577
2578func cmdSPFCheck(c *cmd) {
2579 c.params = "domain ip"
2580 c.help = `Check the status of IP for the policy published in DNS for the domain.
2581
2582IPs may be allowed to send for a domain, or disallowed, and several shades in
2583between. If not allowed, an explanation may be provided by the policy. If so,
2584the explanation is printed. The SPF mechanism that matched (if any) is also
2585printed.
2586`
2587 args := c.Parse()
2588 if len(args) != 2 {
2589 c.Usage()
2590 }
2591
2592 domain := xparseDomain(args[0], "domain")
2593
2594 ip := xparseIP(args[1], "ip")
2595
2596 spfargs := spf.Args{
2597 RemoteIP: ip,
2598 MailFromLocalpart: "user",
2599 MailFromDomain: domain,
2600 HelloDomain: dns.IPDomain{Domain: domain},
2601 LocalIP: net.ParseIP("127.0.0.1"),
2602 LocalHostname: dns.Domain{ASCII: "localhost"},
2603 }
2604 r, _, explanation, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfargs)
2605 if err != nil {
2606 fmt.Printf("error: %s\n", err)
2607 }
2608 if explanation != "" {
2609 fmt.Printf("explanation: %s\n", explanation)
2610 }
2611 fmt.Printf("status: %s (%s)\n", r.Result, dnssecStatus(authentic))
2612 if r.Mechanism != "" {
2613 fmt.Printf("mechanism: %s\n", r.Mechanism)
2614 }
2615}
2616
2617func cmdSPFParse(c *cmd) {
2618 c.params = "txtrecord"
2619 c.help = "Parse the record as SPF record. If valid, nothing is printed."
2620 args := c.Parse()
2621 if len(args) != 1 {
2622 c.Usage()
2623 }
2624
2625 _, _, err := spf.ParseRecord(args[0])
2626 xcheckf(err, "parsing record")
2627}
2628
2629func cmdSPFLookup(c *cmd) {
2630 c.params = "domain"
2631 c.help = "Lookup the SPF record for the domain and print it."
2632 args := c.Parse()
2633 if len(args) != 1 {
2634 c.Usage()
2635 }
2636
2637 domain := xparseDomain(args[0], "domain")
2638 _, txt, _, authentic, err := spf.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
2639 xcheckf(err, "spf lookup for %s", domain)
2640 fmt.Println(txt)
2641 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2642}
2643
2644func cmdMTASTSLookup(c *cmd) {
2645 c.params = "domain"
2646 c.help = `Lookup the MTASTS record and policy for the domain.
2647
2648MTA-STS is a mechanism for a domain to specify if it requires TLS connections
2649for delivering email. If a domain has a valid MTA-STS DNS TXT record at
2650_mta-sts.<domain> it signals it implements MTA-STS. A policy can then be
2651fetched at https://mta-sts.<domain>/.well-known/mta-sts.txt. The policy
2652specifies the mode (enforce, testing, none), which MX servers support TLS and
2653should be used, and how long the policy can be cached.
2654`
2655 args := c.Parse()
2656 if len(args) != 1 {
2657 c.Usage()
2658 }
2659
2660 domain := xparseDomain(args[0], "domain")
2661
2662 record, policy, _, err := mtasts.Get(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
2663 if err != nil {
2664 fmt.Printf("error: %s\n", err)
2665 }
2666 if record != nil {
2667 fmt.Printf("DNS TXT record _mta-sts.%s: %s\n", domain.ASCII, record.String())
2668 }
2669 if policy != nil {
2670 fmt.Println("")
2671 fmt.Printf("policy at https://mta-sts.%s/.well-known/mta-sts.txt:\n", domain.ASCII)
2672 fmt.Printf("%s", policy.String())
2673 }
2674}
2675
2676func cmdRetrain(c *cmd) {
2677 c.params = "accountname"
2678 c.help = `Recreate and retrain the junk filter for the account.
2679
2680Useful after having made changes to the junk filter configuration, or if the
2681implementation has changed.
2682`
2683 args := c.Parse()
2684 if len(args) != 1 {
2685 c.Usage()
2686 }
2687
2688 mustLoadConfig()
2689 ctlcmdRetrain(xctl(), args[0])
2690}
2691
2692func ctlcmdRetrain(ctl *ctl, account string) {
2693 ctl.xwrite("retrain")
2694 ctl.xwrite(account)
2695 ctl.xreadok()
2696}
2697
2698func cmdTLSRPTDBAddReport(c *cmd) {
2699 c.unlisted = true
2700 c.params = "< message"
2701 c.help = "Parse a TLS report from the message and add it to the database."
2702 var hostReport bool
2703 c.flag.BoolVar(&hostReport, "hostreport", false, "report for a host instead of domain")
2704 args := c.Parse()
2705 if len(args) != 0 {
2706 c.Usage()
2707 }
2708
2709 mustLoadConfig()
2710
2711 // First read message, to get the From-header. Then parse it as TLSRPT.
2712 fmt.Fprintln(os.Stderr, "reading report message from stdin")
2713 buf, err := io.ReadAll(os.Stdin)
2714 xcheckf(err, "reading message")
2715 part, err := message.Parse(c.log.Logger, true, bytes.NewReader(buf))
2716 xcheckf(err, "parsing message")
2717 if part.Envelope == nil || len(part.Envelope.From) != 1 {
2718 log.Fatalf("message must have one From-header")
2719 }
2720 from := part.Envelope.From[0]
2721 domain := xparseDomain(from.Host, "domain")
2722
2723 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, bytes.NewReader(buf))
2724 xcheckf(err, "parsing tls report in message")
2725
2726 mailfrom := from.User + "@" + from.Host // todo future: should escape and such
2727 report := reportJSON.Convert()
2728 err = tlsrptdb.AddReport(context.Background(), c.log, domain, mailfrom, hostReport, &report)
2729 xcheckf(err, "add tls report to database")
2730}
2731
2732func cmdDNSBLCheck(c *cmd) {
2733 c.params = "zone ip"
2734 c.help = `Test if IP is in the DNS blocklist of the zone, e.g. bl.spamcop.net.
2735
2736If the IP is in the blocklist, an explanation is printed. This is typically a
2737URL with more information.
2738`
2739 args := c.Parse()
2740 if len(args) != 2 {
2741 c.Usage()
2742 }
2743
2744 zone := xparseDomain(args[0], "zone")
2745 ip := xparseIP(args[1], "ip")
2746
2747 status, explanation, err := dnsbl.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, zone, ip)
2748 fmt.Printf("status: %s\n", status)
2749 if status == dnsbl.StatusFail {
2750 fmt.Printf("explanation: %q\n", explanation)
2751 }
2752 if err != nil {
2753 fmt.Printf("error: %s\n", err)
2754 }
2755}
2756
2757func cmdDNSBLCheckhealth(c *cmd) {
2758 c.params = "zone"
2759 c.help = `Check the health of the DNS blocklist represented by zone, e.g. bl.spamcop.net.
2760
2761The health of a DNS blocklist can be checked by querying for 127.0.0.1 and
2762127.0.0.2. The second must and the first must not be present.
2763`
2764 args := c.Parse()
2765 if len(args) != 1 {
2766 c.Usage()
2767 }
2768
2769 zone := xparseDomain(args[0], "zone")
2770 err := dnsbl.CheckHealth(context.Background(), c.log.Logger, dns.StrictResolver{}, zone)
2771 xcheckf(err, "unhealthy")
2772 fmt.Println("healthy")
2773}
2774
2775func cmdCheckupdate(c *cmd) {
2776 c.help = `Check if a newer version of mox is available.
2777
2778A single DNS TXT lookup to _updates.xmox.nl tells if a new version is
2779available. If so, a changelog is fetched from https://updates.xmox.nl, and the
2780individual entries verified with a builtin public key. The changelog is
2781printed.
2782`
2783 if len(c.Parse()) != 0 {
2784 c.Usage()
2785 }
2786 mustLoadConfig()
2787
2788 current, lastknown, _, err := mox.LastKnown()
2789 if err != nil {
2790 log.Printf("getting last known version: %s", err)
2791 } else {
2792 fmt.Printf("last known version: %s\n", lastknown)
2793 fmt.Printf("current version: %s\n", current)
2794 }
2795 latest, _, err := updates.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dns.Domain{ASCII: changelogDomain})
2796 xcheckf(err, "lookup of latest version")
2797 fmt.Printf("latest version: %s\n", latest)
2798
2799 if latest.After(current) {
2800 changelog, err := updates.FetchChangelog(context.Background(), c.log.Logger, changelogURL, current, changelogPubKey)
2801 xcheckf(err, "fetching changelog")
2802 if len(changelog.Changes) == 0 {
2803 log.Printf("no changes in changelog")
2804 return
2805 }
2806 fmt.Println("Changelog")
2807 for _, c := range changelog.Changes {
2808 fmt.Println("\n" + strings.TrimSpace(c.Text))
2809 }
2810 }
2811}
2812
2813func cmdCid(c *cmd) {
2814 c.params = "cid"
2815 c.help = `Turn an ID from a Received header into a cid, for looking up in logs.
2816
2817A cid is essentially a connection counter initialized when mox starts. Each log
2818line contains a cid. Received headers added by mox contain a unique ID that can
2819be decrypted to a cid by admin of a mox instance only.
2820`
2821 args := c.Parse()
2822 if len(args) != 1 {
2823 c.Usage()
2824 }
2825
2826 mustLoadConfig()
2827 recvidpath := mox.DataDirPath("receivedid.key")
2828 recvidbuf, err := os.ReadFile(recvidpath)
2829 xcheckf(err, "reading %s", recvidpath)
2830 if len(recvidbuf) != 16+8 {
2831 log.Fatalf("bad data in %s: got %d bytes, expect 16+8=24", recvidpath, len(recvidbuf))
2832 }
2833 err = mox.ReceivedIDInit(recvidbuf[:16], recvidbuf[16:])
2834 xcheckf(err, "init receivedid")
2835
2836 cid, err := mox.ReceivedToCid(args[0])
2837 xcheckf(err, "received id to cid")
2838 fmt.Printf("%x\n", cid)
2839}
2840
2841func cmdVersion(c *cmd) {
2842 c.help = "Prints this mox version."
2843 if len(c.Parse()) != 0 {
2844 c.Usage()
2845 }
2846 fmt.Println(moxvar.Version)
2847 fmt.Printf("%s %s/%s\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
2848}
2849
2850func cmdWebapi(c *cmd) {
2851 c.params = "[method [baseurl-with-credentials]"
2852 c.help = "Lists available methods, prints request/response parameters for method, or calls a method with a request read from standard input."
2853 args := c.Parse()
2854 if len(args) > 2 {
2855 c.Usage()
2856 }
2857
2858 t := reflect.TypeOf((*webapi.Methods)(nil)).Elem()
2859 methods := map[string]reflect.Type{}
2860 var ml []string
2861 for i := 0; i < t.NumMethod(); i++ {
2862 mt := t.Method(i)
2863 methods[mt.Name] = mt.Type
2864 ml = append(ml, mt.Name)
2865 }
2866
2867 if len(args) == 0 {
2868 fmt.Println(strings.Join(ml, "\n"))
2869 return
2870 }
2871
2872 mt, ok := methods[args[0]]
2873 if !ok {
2874 log.Fatalf("unknown method %q", args[0])
2875 }
2876 resultNotJSON := mt.Out(0).Kind() == reflect.Interface
2877
2878 if len(args) == 1 {
2879 fmt.Println("# Example request")
2880 fmt.Println()
2881 printJSON("\t", mox.FillExample(nil, reflect.New(mt.In(1))).Interface())
2882 fmt.Println()
2883 if resultNotJSON {
2884 fmt.Println("Output is non-JSON data.")
2885 return
2886 }
2887 fmt.Println("# Example response")
2888 fmt.Println()
2889 printJSON("\t", mox.FillExample(nil, reflect.New(mt.Out(0))).Interface())
2890 return
2891 }
2892
2893 var response any
2894 if !resultNotJSON {
2895 response = reflect.New(mt.Out(0))
2896 }
2897
2898 fmt.Fprintln(os.Stderr, "reading request from stdin...")
2899 request, err := io.ReadAll(os.Stdin)
2900 xcheckf(err, "read message")
2901
2902 dec := json.NewDecoder(bytes.NewReader(request))
2903 dec.DisallowUnknownFields()
2904 err = dec.Decode(reflect.New(mt.In(1)).Interface())
2905 xcheckf(err, "parsing request")
2906
2907 resp, err := http.PostForm(args[1]+args[0], url.Values{"request": []string{string(request)}})
2908 xcheckf(err, "http post")
2909 defer resp.Body.Close()
2910 if resp.StatusCode == http.StatusBadRequest {
2911 buf, err := io.ReadAll(&moxio.LimitReader{R: resp.Body, Limit: 10 * 1024})
2912 xcheckf(err, "reading response for 400 bad request error")
2913 err = json.Unmarshal(buf, &response)
2914 if err == nil {
2915 printJSON("", response)
2916 } else {
2917 fmt.Fprintf(os.Stderr, "(not json)\n")
2918 os.Stderr.Write(buf)
2919 }
2920 os.Exit(1)
2921 } else if resp.StatusCode != http.StatusOK {
2922 fmt.Fprintf(os.Stderr, "http response %s\n", resp.Status)
2923 _, err := io.Copy(os.Stderr, resp.Body)
2924 xcheckf(err, "copy body")
2925 } else {
2926 err := json.NewDecoder(resp.Body).Decode(&resp)
2927 xcheckf(err, "unmarshal response")
2928 printJSON("", response)
2929 }
2930}
2931
2932func printJSON(indent string, v any) {
2933 fmt.Printf("%s", indent)
2934 enc := json.NewEncoder(os.Stdout)
2935 enc.SetIndent(indent, "\t")
2936 enc.SetEscapeHTML(false)
2937 err := enc.Encode(v)
2938 xcheckf(err, "encode json")
2939}
2940
2941// todo: should make it possible to run this command against a running mox. it should disconnect existing clients for accounts with a bumped uidvalidity, so they will reconnect and refetch the data.
2942func cmdBumpUIDValidity(c *cmd) {
2943 c.params = "account [mailbox]"
2944 c.help = `Change the IMAP UID validity of the mailbox, causing IMAP clients to refetch messages.
2945
2946This can be useful after manually repairing metadata about the account/mailbox.
2947
2948Opens account database file directly. Ensure mox does not have the account
2949open, or is not running.
2950`
2951 args := c.Parse()
2952 if len(args) != 1 && len(args) != 2 {
2953 c.Usage()
2954 }
2955
2956 mustLoadConfig()
2957 a, err := store.OpenAccount(c.log, args[0])
2958 xcheckf(err, "open account")
2959 defer func() {
2960 if err := a.Close(); err != nil {
2961 log.Printf("closing account: %v", err)
2962 }
2963 }()
2964
2965 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
2966 uidvalidity, err := a.NextUIDValidity(tx)
2967 if err != nil {
2968 return fmt.Errorf("assigning next uid validity: %v", err)
2969 }
2970
2971 q := bstore.QueryTx[store.Mailbox](tx)
2972 if len(args) == 2 {
2973 q.FilterEqual("Name", args[1])
2974 }
2975 mbl, err := q.SortAsc("Name").List()
2976 if err != nil {
2977 return fmt.Errorf("looking up mailbox: %v", err)
2978 }
2979 if len(args) == 2 && len(mbl) != 1 {
2980 return fmt.Errorf("looking up mailbox %q, found %d mailboxes", args[1], len(mbl))
2981 }
2982 for _, mb := range mbl {
2983 mb.UIDValidity = uidvalidity
2984 err = tx.Update(&mb)
2985 if err != nil {
2986 return fmt.Errorf("updating uid validity for mailbox: %v", err)
2987 }
2988 fmt.Printf("uid validity for %q updated to %d\n", mb.Name, uidvalidity)
2989 }
2990 return nil
2991 })
2992 xcheckf(err, "updating database")
2993}
2994
2995func cmdReassignUIDs(c *cmd) {
2996 c.params = "account [mailboxid]"
2997 c.help = `Reassign UIDs in one mailbox or all mailboxes in an account and bump UID validity, causing IMAP clients to refetch messages.
2998
2999Opens account database file directly. Ensure mox does not have the account
3000open, or is not running.
3001`
3002 args := c.Parse()
3003 if len(args) != 1 && len(args) != 2 {
3004 c.Usage()
3005 }
3006
3007 var mailboxID int64
3008 if len(args) == 2 {
3009 var err error
3010 mailboxID, err = strconv.ParseInt(args[1], 10, 64)
3011 xcheckf(err, "parsing mailbox id")
3012 }
3013
3014 mustLoadConfig()
3015 a, err := store.OpenAccount(c.log, args[0])
3016 xcheckf(err, "open account")
3017 defer func() {
3018 if err := a.Close(); err != nil {
3019 log.Printf("closing account: %v", err)
3020 }
3021 }()
3022
3023 // Gather the last-assigned UIDs per mailbox.
3024 uidlasts := map[int64]store.UID{}
3025
3026 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3027 // Reassign UIDs, going per mailbox. We assign starting at 1, only changing the
3028 // message if it isn't already at the intended UID. Doing it in this order ensures
3029 // we don't get into trouble with duplicate UIDs for a mailbox. We assign a new
3030 // modseq. Not strictly needed, for doesn't hurt.
3031 modseq, err := a.NextModSeq(tx)
3032 xcheckf(err, "assigning next modseq")
3033
3034 q := bstore.QueryTx[store.Message](tx)
3035 if len(args) == 2 {
3036 q.FilterNonzero(store.Message{MailboxID: mailboxID})
3037 }
3038 q.SortAsc("MailboxID", "UID")
3039 err = q.ForEach(func(m store.Message) error {
3040 uidlasts[m.MailboxID]++
3041 uid := uidlasts[m.MailboxID]
3042 if m.UID != uid {
3043 m.UID = uid
3044 m.ModSeq = modseq
3045 if err := tx.Update(&m); err != nil {
3046 return fmt.Errorf("updating uid for message: %v", err)
3047 }
3048 }
3049 return nil
3050 })
3051 if err != nil {
3052 return fmt.Errorf("reading through messages: %v", err)
3053 }
3054
3055 // Now update the uidnext and uidvalidity for each mailbox.
3056 err = bstore.QueryTx[store.Mailbox](tx).ForEach(func(mb store.Mailbox) error {
3057 // Assign each mailbox a completely new uidvalidity.
3058 uidvalidity, err := a.NextUIDValidity(tx)
3059 if err != nil {
3060 return fmt.Errorf("assigning next uid validity: %v", err)
3061 }
3062
3063 if mb.UIDValidity >= uidvalidity {
3064 // This should not happen, but since we're fixing things up after a hypothetical
3065 // mishap, might as well account for inconsistent uidvalidity.
3066 next := store.NextUIDValidity{ID: 1, Next: mb.UIDValidity + 2}
3067 if err := tx.Update(&next); err != nil {
3068 log.Printf("updating nextuidvalidity: %v, continuing", err)
3069 }
3070 mb.UIDValidity++
3071 } else {
3072 mb.UIDValidity = uidvalidity
3073 }
3074 mb.UIDNext = uidlasts[mb.ID] + 1
3075 if err := tx.Update(&mb); err != nil {
3076 return fmt.Errorf("updating uidvalidity and uidnext for mailbox: %v", err)
3077 }
3078 return nil
3079 })
3080 if err != nil {
3081 return fmt.Errorf("updating mailboxes: %v", err)
3082 }
3083 return nil
3084 })
3085 xcheckf(err, "updating database")
3086}
3087
3088func cmdFixUIDMeta(c *cmd) {
3089 c.params = "account"
3090 c.help = `Fix inconsistent UIDVALIDITY and UIDNEXT in messages/mailboxes/account.
3091
3092The next UID to use for a message in a mailbox should always be higher than any
3093existing message UID in the mailbox. If it is not, the mailbox UIDNEXT is
3094updated.
3095
3096Each mailbox has a UIDVALIDITY sequence number, which should always be lower
3097than the per-account next UIDVALIDITY to use. If it is not, the account next
3098UIDVALIDITY is updated.
3099
3100Opens account database file directly. Ensure mox does not have the account
3101open, or is not running.
3102`
3103 args := c.Parse()
3104 if len(args) != 1 {
3105 c.Usage()
3106 }
3107
3108 mustLoadConfig()
3109 a, err := store.OpenAccount(c.log, args[0])
3110 xcheckf(err, "open account")
3111 defer func() {
3112 if err := a.Close(); err != nil {
3113 log.Printf("closing account: %v", err)
3114 }
3115 }()
3116
3117 var maxUIDValidity uint32
3118
3119 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3120 // We look at each mailbox, retrieve its max UID and compare against the mailbox
3121 // UIDNEXT.
3122 err := bstore.QueryTx[store.Mailbox](tx).ForEach(func(mb store.Mailbox) error {
3123 if mb.UIDValidity > maxUIDValidity {
3124 maxUIDValidity = mb.UIDValidity
3125 }
3126 m, err := bstore.QueryTx[store.Message](tx).FilterNonzero(store.Message{MailboxID: mb.ID}).SortDesc("UID").Limit(1).Get()
3127 if err == bstore.ErrAbsent || err == nil && m.UID < mb.UIDNext {
3128 return nil
3129 } else if err != nil {
3130 return fmt.Errorf("finding message with max uid in mailbox: %w", err)
3131 }
3132 olduidnext := mb.UIDNext
3133 mb.UIDNext = m.UID + 1
3134 log.Printf("fixing uidnext to %d (max uid is %d, old uidnext was %d) for mailbox %q (id %d)", mb.UIDNext, m.UID, olduidnext, mb.Name, mb.ID)
3135 if err := tx.Update(&mb); err != nil {
3136 return fmt.Errorf("updating mailbox uidnext: %v", err)
3137 }
3138 return nil
3139 })
3140 if err != nil {
3141 return fmt.Errorf("processing mailboxes: %v", err)
3142 }
3143
3144 uidvalidity := store.NextUIDValidity{ID: 1}
3145 if err := tx.Get(&uidvalidity); err != nil {
3146 return fmt.Errorf("reading account next uidvalidity: %v", err)
3147 }
3148 if maxUIDValidity >= uidvalidity.Next {
3149 log.Printf("account next uidvalidity %d <= highest uidvalidity %d found in mailbox, resetting account next uidvalidity to %d", uidvalidity.Next, maxUIDValidity, maxUIDValidity+1)
3150 uidvalidity.Next = maxUIDValidity + 1
3151 if err := tx.Update(&uidvalidity); err != nil {
3152 return fmt.Errorf("updating account next uidvalidity: %v", err)
3153 }
3154 }
3155
3156 return nil
3157 })
3158 xcheckf(err, "updating database")
3159}
3160
3161func cmdFixmsgsize(c *cmd) {
3162 c.params = "[account]"
3163 c.help = `Ensure message sizes in the database matching the sum of the message prefix length and on-disk file size.
3164
3165Messages with an inconsistent size are also parsed again.
3166
3167If an inconsistency is found, you should probably also run "mox
3168bumpuidvalidity" on the mailboxes or entire account to force IMAP clients to
3169refetch messages.
3170`
3171 args := c.Parse()
3172 if len(args) > 1 {
3173 c.Usage()
3174 }
3175
3176 mustLoadConfig()
3177 var account string
3178 if len(args) == 1 {
3179 account = args[0]
3180 }
3181 ctlcmdFixmsgsize(xctl(), account)
3182}
3183
3184func ctlcmdFixmsgsize(ctl *ctl, account string) {
3185 ctl.xwrite("fixmsgsize")
3186 ctl.xwrite(account)
3187 ctl.xreadok()
3188 ctl.xstreamto(os.Stdout)
3189}
3190
3191func cmdReparse(c *cmd) {
3192 c.params = "[account]"
3193 c.help = `Parse all messages in the account or all accounts again.
3194
3195Can be useful after upgrading mox with improved message parsing. Messages are
3196parsed in batches, so other access to the mailboxes/messages are not blocked
3197while reparsing all messages.
3198`
3199 args := c.Parse()
3200 if len(args) > 1 {
3201 c.Usage()
3202 }
3203
3204 mustLoadConfig()
3205 var account string
3206 if len(args) == 1 {
3207 account = args[0]
3208 }
3209 ctlcmdReparse(xctl(), account)
3210}
3211
3212func ctlcmdReparse(ctl *ctl, account string) {
3213 ctl.xwrite("reparse")
3214 ctl.xwrite(account)
3215 ctl.xreadok()
3216 ctl.xstreamto(os.Stdout)
3217}
3218
3219func cmdEnsureParsed(c *cmd) {
3220 c.params = "account"
3221 c.help = "Ensure messages in the database have a pre-parsed MIME form in the database."
3222 var all bool
3223 c.flag.BoolVar(&all, "all", false, "store new parsed message for all messages")
3224 args := c.Parse()
3225 if len(args) != 1 {
3226 c.Usage()
3227 }
3228
3229 mustLoadConfig()
3230 a, err := store.OpenAccount(c.log, args[0])
3231 xcheckf(err, "open account")
3232 defer func() {
3233 if err := a.Close(); err != nil {
3234 log.Printf("closing account: %v", err)
3235 }
3236 }()
3237
3238 n := 0
3239 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3240 q := bstore.QueryTx[store.Message](tx)
3241 q.FilterEqual("Expunged", false)
3242 q.FilterFn(func(m store.Message) bool {
3243 return all || m.ParsedBuf == nil
3244 })
3245 l, err := q.List()
3246 if err != nil {
3247 return fmt.Errorf("list messages: %v", err)
3248 }
3249 for _, m := range l {
3250 mr := a.MessageReader(m)
3251 p, err := message.EnsurePart(c.log.Logger, false, mr, m.Size)
3252 if err != nil {
3253 log.Printf("parsing message %d: %v (continuing)", m.ID, err)
3254 }
3255 m.ParsedBuf, err = json.Marshal(p)
3256 if err != nil {
3257 return fmt.Errorf("marshal parsed message: %v", err)
3258 }
3259 if err := tx.Update(&m); err != nil {
3260 return fmt.Errorf("update message: %v", err)
3261 }
3262 n++
3263 }
3264 return nil
3265 })
3266 xcheckf(err, "update messages with parsed mime structure")
3267 fmt.Printf("%d messages updated\n", n)
3268}
3269
3270func cmdRecalculateMailboxCounts(c *cmd) {
3271 c.params = "account"
3272 c.help = `Recalculate message counts for all mailboxes in the account, and total message size for quota.
3273
3274When a message is added to/removed from a mailbox, or when message flags change,
3275the total, unread, unseen and deleted messages are accounted, the total size of
3276the mailbox, and the total message size for the account. In case of a bug in
3277this accounting, the numbers could become incorrect. This command will find, fix
3278and print them.
3279`
3280 args := c.Parse()
3281 if len(args) != 1 {
3282 c.Usage()
3283 }
3284
3285 mustLoadConfig()
3286 ctlcmdRecalculateMailboxCounts(xctl(), args[0])
3287}
3288
3289func ctlcmdRecalculateMailboxCounts(ctl *ctl, account string) {
3290 ctl.xwrite("recalculatemailboxcounts")
3291 ctl.xwrite(account)
3292 ctl.xreadok()
3293 ctl.xstreamto(os.Stdout)
3294}
3295
3296func cmdMessageParse(c *cmd) {
3297 c.params = "message.eml"
3298 c.help = "Parse message, print JSON representation."
3299
3300 var smtputf8 bool
3301 c.flag.BoolVar(&smtputf8, "smtputf8", false, "check if message needs smtputf8")
3302 args := c.Parse()
3303 if len(args) != 1 {
3304 c.Usage()
3305 }
3306
3307 f, err := os.Open(args[0])
3308 xcheckf(err, "open")
3309 defer f.Close()
3310
3311 part, err := message.Parse(c.log.Logger, false, f)
3312 xcheckf(err, "parsing message")
3313 err = part.Walk(c.log.Logger, nil)
3314 xcheckf(err, "parsing nested parts")
3315 enc := json.NewEncoder(os.Stdout)
3316 enc.SetIndent("", "\t")
3317 enc.SetEscapeHTML(false)
3318 err = enc.Encode(part)
3319 xcheckf(err, "write")
3320
3321 hasNonASCII := func(r io.Reader) bool {
3322 br := bufio.NewReader(r)
3323 for {
3324 b, err := br.ReadByte()
3325 if err == io.EOF {
3326 break
3327 }
3328 xcheckf(err, "read header")
3329 if b > 0x7f {
3330 return true
3331 }
3332 }
3333 return false
3334 }
3335
3336 var walk func(p *message.Part) bool
3337 walk = func(p *message.Part) bool {
3338 if hasNonASCII(p.HeaderReader()) {
3339 return true
3340 }
3341 for _, pp := range p.Parts {
3342 if walk(&pp) {
3343 return true
3344 }
3345 }
3346 return false
3347 }
3348 if smtputf8 {
3349 fmt.Println("message needs smtputf8:", walk(&part))
3350 }
3351}
3352
3353func cmdOpenaccounts(c *cmd) {
3354 c.unlisted = true
3355 c.params = "datadir account ..."
3356 c.help = `Open and close accounts, for triggering data upgrades, for tests.
3357
3358Opens database files directly, not going through a running mox instance.
3359`
3360
3361 args := c.Parse()
3362 if len(args) <= 1 {
3363 c.Usage()
3364 }
3365
3366 dataDir := filepath.Clean(args[0])
3367 for _, accName := range args[1:] {
3368 accDir := filepath.Join(dataDir, "accounts", accName)
3369 log.Printf("opening account %s...", accDir)
3370 a, err := store.OpenAccountDB(c.log, accDir, accName)
3371 xcheckf(err, "open account %s", accName)
3372 err = a.ThreadingWait(c.log)
3373 xcheckf(err, "wait for threading upgrade to complete for %s", accName)
3374 err = a.Close()
3375 xcheckf(err, "close account %s", accName)
3376 }
3377}
3378
3379func cmdReassignthreads(c *cmd) {
3380 c.params = "[account]"
3381 c.help = `Reassign message threads.
3382
3383For all accounts, or optionally only the specified account.
3384
3385Threading for all messages in an account is first reset, and new base subject
3386and normalized message-id saved with the message. Then all messages are
3387evaluated and matched against their parents/ancestors.
3388
3389Messages are matched based on the References header, with a fall-back to an
3390In-Reply-To header, and if neither is present/valid, based only on base
3391subject.
3392
3393A References header typically points to multiple previous messages in a
3394hierarchy. From oldest ancestor to most recent parent. An In-Reply-To header
3395would have only a message-id of the parent message.
3396
3397A message is only linked to a parent/ancestor if their base subject is the
3398same. This ensures unrelated replies, with a new subject, are placed in their
3399own thread.
3400
3401The base subject is lower cased, has whitespace collapsed to a single
3402space, and some components removed: leading "Re:", "Fwd:", "Fw:", or bracketed
3403tag (that mailing lists often add, e.g. "[listname]"), trailing "(fwd)", or
3404enclosing "[fwd: ...]".
3405
3406Messages are linked to all their ancestors. If an intermediate parent/ancestor
3407message is deleted in the future, the message can still be linked to the earlier
3408ancestors. If the direct parent already wasn't available while matching, this is
3409stored as the message having a "missing link" to its stored ancestors.
3410`
3411 args := c.Parse()
3412 if len(args) > 1 {
3413 c.Usage()
3414 }
3415
3416 mustLoadConfig()
3417 var account string
3418 if len(args) == 1 {
3419 account = args[0]
3420 }
3421 ctlcmdReassignthreads(xctl(), account)
3422}
3423
3424func ctlcmdReassignthreads(ctl *ctl, account string) {
3425 ctl.xwrite("reassignthreads")
3426 ctl.xwrite(account)
3427 ctl.xreadok()
3428 ctl.xstreamto(os.Stdout)
3429}
3430
3431func cmdReadmessages(c *cmd) {
3432 c.unlisted = true
3433 c.params = "datadir account ..."
3434 c.help = `Open account, parse several headers for all messages.
3435
3436For performance testing.
3437
3438Opens database files directly, not going through a running mox instance.
3439`
3440
3441 gomaxprocs := runtime.GOMAXPROCS(0)
3442 var procs, workqueuesize, limit int
3443 c.flag.IntVar(&procs, "procs", gomaxprocs, "number of goroutines for reading messages")
3444 c.flag.IntVar(&workqueuesize, "workqueuesize", 2*gomaxprocs, "number of messages to keep in work queue")
3445 c.flag.IntVar(&limit, "limit", 0, "number of messages to process if greater than zero")
3446 args := c.Parse()
3447 if len(args) <= 1 {
3448 c.Usage()
3449 }
3450
3451 type threadPrep struct {
3452 references []string
3453 inReplyTo []string
3454 }
3455
3456 threadingFields := [][]byte{
3457 []byte("references"),
3458 []byte("in-reply-to"),
3459 }
3460
3461 dataDir := filepath.Clean(args[0])
3462 for _, accName := range args[1:] {
3463 accDir := filepath.Join(dataDir, "accounts", accName)
3464 log.Printf("opening account %s...", accDir)
3465 a, err := store.OpenAccountDB(c.log, accDir, accName)
3466 xcheckf(err, "open account %s", accName)
3467
3468 prepareMessages := func(in, out chan moxio.Work[store.Message, threadPrep]) {
3469 headerbuf := make([]byte, 8*1024)
3470 scratch := make([]byte, 4*1024)
3471 for {
3472 w, ok := <-in
3473 if !ok {
3474 return
3475 }
3476
3477 m := w.In
3478 var partialPart struct {
3479 HeaderOffset int64
3480 BodyOffset int64
3481 }
3482 if err := json.Unmarshal(m.ParsedBuf, &partialPart); err != nil {
3483 w.Err = fmt.Errorf("unmarshal part: %v", err)
3484 } else {
3485 size := partialPart.BodyOffset - partialPart.HeaderOffset
3486 if int(size) > len(headerbuf) {
3487 headerbuf = make([]byte, size)
3488 }
3489 if size > 0 {
3490 buf := headerbuf[:int(size)]
3491 err := func() error {
3492 mr := a.MessageReader(m)
3493 defer mr.Close()
3494
3495 // ReadAt returns whole buffer or error. Single read should be fast.
3496 n, err := mr.ReadAt(buf, partialPart.HeaderOffset)
3497 if err != nil || n != len(buf) {
3498 return fmt.Errorf("read header: %v", err)
3499 }
3500 return nil
3501 }()
3502 if err != nil {
3503 w.Err = err
3504 } else if h, err := message.ParseHeaderFields(buf, scratch, threadingFields); err != nil {
3505 w.Err = err
3506 } else {
3507 w.Out.references = h["References"]
3508 w.Out.inReplyTo = h["In-Reply-To"]
3509 }
3510 }
3511 }
3512
3513 out <- w
3514 }
3515 }
3516
3517 n := 0
3518 t := time.Now()
3519 t0 := t
3520
3521 processMessage := func(m store.Message, prep threadPrep) error {
3522 if n%100000 == 0 {
3523 log.Printf("%d messages (delta %s)", n, time.Since(t))
3524 t = time.Now()
3525 }
3526 n++
3527 return nil
3528 }
3529
3530 wq := moxio.NewWorkQueue[store.Message, threadPrep](procs, workqueuesize, prepareMessages, processMessage)
3531
3532 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3533 q := bstore.QueryTx[store.Message](tx)
3534 q.FilterEqual("Expunged", false)
3535 q.SortAsc("ID")
3536 if limit > 0 {
3537 q.Limit(limit)
3538 }
3539 err = q.ForEach(wq.Add)
3540 if err == nil {
3541 err = wq.Finish()
3542 }
3543 wq.Stop()
3544
3545 return err
3546 })
3547 xcheckf(err, "processing message")
3548
3549 err = a.Close()
3550 xcheckf(err, "close account %s", accName)
3551 log.Printf("account %s, total time %s", accName, time.Since(t0))
3552 }
3553}
3554
3555func cmdQueueFillRetired(c *cmd) {
3556 c.unlisted = true
3557 c.help = `Fill retired messag and webhooks queue with testdata.
3558
3559For testing the pagination. Operates directly on queue database.
3560`
3561 var n int
3562 c.flag.IntVar(&n, "n", 10000, "retired messages and retired webhooks to insert")
3563 args := c.Parse()
3564 if len(args) != 0 {
3565 c.Usage()
3566 }
3567
3568 mustLoadConfig()
3569 err := queue.Init()
3570 xcheckf(err, "init queue")
3571 err = queue.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3572 now := time.Now()
3573
3574 // Cause autoincrement ID for queue.Msg to be forwarded, and use the reserved ID
3575 // space for inserting retired messages.
3576 fm := queue.Msg{}
3577 err = tx.Insert(&fm)
3578 xcheckf(err, "temporarily insert message to get autoincrement sequence")
3579 err = tx.Delete(&fm)
3580 xcheckf(err, "removing temporary message for resetting autoincrement sequence")
3581 fm.ID += int64(n)
3582 err = tx.Insert(&fm)
3583 xcheckf(err, "temporarily insert message to forward autoincrement sequence")
3584 err = tx.Delete(&fm)
3585 xcheckf(err, "removing temporary message after forwarding autoincrement sequence")
3586 fm.ID -= int64(n)
3587
3588 // And likewise for webhooks.
3589 fh := queue.Hook{Account: "x", URL: "x", NextAttempt: time.Now()}
3590 err = tx.Insert(&fh)
3591 xcheckf(err, "temporarily insert webhook to get autoincrement sequence")
3592 err = tx.Delete(&fh)
3593 xcheckf(err, "removing temporary webhook for resetting autoincrement sequence")
3594 fh.ID += int64(n)
3595 err = tx.Insert(&fh)
3596 xcheckf(err, "temporarily insert webhook to forward autoincrement sequence")
3597 err = tx.Delete(&fh)
3598 xcheckf(err, "removing temporary webhook after forwarding autoincrement sequence")
3599 fh.ID -= int64(n)
3600
3601 for i := 0; i < n; i++ {
3602 t0 := now.Add(-time.Duration(i) * time.Second)
3603 last := now.Add(-time.Duration(i/10) * time.Second)
3604 mr := queue.MsgRetired{
3605 ID: fm.ID + int64(i),
3606 Queued: t0,
3607 SenderAccount: "test",
3608 SenderLocalpart: "mox",
3609 SenderDomainStr: "localhost",
3610 FromID: fmt.Sprintf("%016d", i),
3611 RecipientLocalpart: "mox",
3612 RecipientDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "localhost"}},
3613 RecipientDomainStr: "localhost",
3614 Attempts: i % 6,
3615 LastAttempt: &last,
3616 Results: []queue.MsgResult{
3617 {
3618 Start: last,
3619 Duration: time.Millisecond,
3620 Success: i%10 != 0,
3621 Code: 250,
3622 },
3623 },
3624 Has8bit: i%2 == 0,
3625 SMTPUTF8: i%8 == 0,
3626 Size: int64(i * 100),
3627 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
3628 Subject: fmt.Sprintf("test message %d", i),
3629 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
3630 LastActivity: last,
3631 RecipientAddress: "mox@localhost",
3632 Success: i%10 != 0,
3633 KeepUntil: now.Add(48 * time.Hour),
3634 }
3635 err := tx.Insert(&mr)
3636 xcheckf(err, "inserting retired message")
3637 }
3638
3639 for i := 0; i < n; i++ {
3640 t0 := now.Add(-time.Duration(i) * time.Second)
3641 last := now.Add(-time.Duration(i/10) * time.Second)
3642 var event string
3643 if i%10 != 0 {
3644 event = "delivered"
3645 }
3646 hr := queue.HookRetired{
3647 ID: fh.ID + int64(i),
3648 QueueMsgID: fm.ID + int64(i),
3649 FromID: fmt.Sprintf("%016d", i),
3650 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
3651 Subject: fmt.Sprintf("test message %d", i),
3652 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
3653 Account: "test",
3654 URL: "http://localhost/hook",
3655 IsIncoming: i%10 == 0,
3656 OutgoingEvent: event,
3657 Payload: "{}",
3658
3659 Submitted: t0,
3660 Attempts: i % 6,
3661 Results: []queue.HookResult{
3662 {
3663 Start: t0,
3664 Duration: time.Millisecond,
3665 URL: "http://localhost/hook",
3666 Success: i%10 != 0,
3667 Code: 200,
3668 Response: "ok",
3669 },
3670 },
3671
3672 Success: i%10 != 0,
3673 LastActivity: last,
3674 KeepUntil: now.Add(48 * time.Hour),
3675 }
3676 err := tx.Insert(&hr)
3677 xcheckf(err, "inserting retired hook")
3678 }
3679
3680 return nil
3681 })
3682 xcheckf(err, "add to queue")
3683 log.Printf("added %d retired messages and %d retired webhooks", n, n)
3684}
3685