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