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