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