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