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