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.SplitSeq(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 cryptorand.Read(seed)
1230 privKey := ed25519.NewKeyFromSeed(seed)
1231 privKeyBuf, err := x509.MarshalPKCS8PrivateKey(privKey)
1232 xcheckf(err, "marshal private key as pkcs8")
1233 var b bytes.Buffer
1234 err = pem.Encode(&b, &pem.Block{Type: "PRIVATE KEY", Bytes: privKeyBuf})
1235 xcheckf(err, "marshal pkcs8 private key to pem")
1236 privKeyBufPEM := b.Bytes()
1237
1238 certBuf, tlsCert := xminimalCert(privKey)
1239 b = bytes.Buffer{}
1240 err = pem.Encode(&b, &pem.Block{Type: "CERTIFICATE", Bytes: certBuf})
1241 xcheckf(err, "marshal certificate to pem")
1242 certBufPEM := b.Bytes()
1243
1244 xwriteFile := func(p string, data []byte, what string) {
1245 log.Printf("writing %s", p)
1246 err = os.WriteFile(p, data, 0600)
1247 xcheckf(err, "writing %s file: %v", what, err)
1248 }
1249
1250 xwriteFile(prefix+".ed25519privatekey.pkcs8.pem", privKeyBufPEM, "private key")
1251 xwriteFile(prefix+".certificate.pem", certBufPEM, "certificate")
1252 combinedPEM := slices.Concat(privKeyBufPEM, certBufPEM)
1253 xwriteFile(prefix+".ed25519privatekey-certificate.pem", combinedPEM, "combined private key and certificate")
1254
1255 shabuf := sha256.Sum256(tlsCert.Leaf.RawSubjectPublicKeyInfo)
1256
1257 _, err = fmt.Fprintf(os.Stderr, "ed25519 private key as raw-url-base64: %s\ned25519 public key fingerprint: %s\n",
1258 base64.RawURLEncoding.EncodeToString(seed),
1259 base64.RawURLEncoding.EncodeToString(shabuf[:]),
1260 )
1261 xcheckf(err, "write private key and public key fingerprint")
1262}
1263
1264func cmdConfigAddressAdd(c *cmd) {
1265 c.params = "address account"
1266 c.help = `Adds an address to an account and reloads the configuration.
1267
1268If address starts with a @ (i.e. a missing localpart), this is a catchall
1269address for the domain.
1270`
1271 args := c.Parse()
1272 if len(args) != 2 {
1273 c.Usage()
1274 }
1275
1276 mustLoadConfig()
1277 ctlcmdConfigAddressAdd(xctl(), args[0], args[1])
1278}
1279
1280func ctlcmdConfigAddressAdd(ctl *ctl, address, account string) {
1281 ctl.xwrite("addressadd")
1282 ctl.xwrite(address)
1283 ctl.xwrite(account)
1284 ctl.xreadok()
1285 fmt.Println("address added")
1286}
1287
1288func cmdConfigAddressRemove(c *cmd) {
1289 c.params = "address"
1290 c.help = `Remove an address and reload the configuration.
1291
1292Incoming email for this address will be rejected after removing an address.
1293`
1294 args := c.Parse()
1295 if len(args) != 1 {
1296 c.Usage()
1297 }
1298
1299 mustLoadConfig()
1300 ctlcmdConfigAddressRemove(xctl(), args[0])
1301}
1302
1303func ctlcmdConfigAddressRemove(ctl *ctl, address string) {
1304 ctl.xwrite("addressrm")
1305 ctl.xwrite(address)
1306 ctl.xreadok()
1307 fmt.Println("address removed")
1308}
1309
1310func cmdConfigDNSRecords(c *cmd) {
1311 c.params = "domain"
1312 c.help = `Prints annotated DNS records as zone file that should be created for the domain.
1313
1314The zone file can be imported into existing DNS software. You should review the
1315DNS records, especially if your domain previously/currently has email
1316configured.
1317`
1318 args := c.Parse()
1319 if len(args) != 1 {
1320 c.Usage()
1321 }
1322
1323 d := xparseDomain(args[0], "domain")
1324 mustLoadConfig()
1325 domConf, ok := mox.Conf.Domain(d)
1326 if !ok {
1327 log.Fatalf("unknown domain")
1328 }
1329
1330 resolver := dns.StrictResolver{Pkg: "main"}
1331 _, result, err := resolver.LookupTXT(context.Background(), d.ASCII+".")
1332 if !dns.IsNotFound(err) {
1333 xcheckf(err, "looking up record for dnssec-status")
1334 }
1335
1336 var certIssuerDomainName, acmeAccountURI string
1337 public := mox.Conf.Static.Listeners["public"]
1338 if public.TLS != nil && public.TLS.ACME != "" {
1339 acme, ok := mox.Conf.Static.ACME[public.TLS.ACME]
1340 if ok && acme.Manager.Manager.Client != nil {
1341 certIssuerDomainName = acme.IssuerDomainName
1342 acc, err := acme.Manager.Manager.Client.GetReg(context.Background(), "")
1343 c.log.Check(err, "get public acme account")
1344 if err == nil {
1345 acmeAccountURI = acc.URI
1346 }
1347 }
1348 }
1349
1350 records, err := admin.DomainRecords(domConf, d, result.Authentic, certIssuerDomainName, acmeAccountURI)
1351 xcheckf(err, "records")
1352 fmt.Print(strings.Join(records, "\n") + "\n")
1353}
1354
1355func cmdConfigDNSCheck(c *cmd) {
1356 c.params = "domain"
1357 c.help = "Check the DNS records with the configuration for the domain, and print any errors/warnings."
1358 args := c.Parse()
1359 if len(args) != 1 {
1360 c.Usage()
1361 }
1362
1363 d := xparseDomain(args[0], "domain")
1364 mustLoadConfig()
1365 _, ok := mox.Conf.Domain(d)
1366 if !ok {
1367 log.Fatalf("unknown domain")
1368 }
1369
1370 // todo future: move http.Admin.CheckDomain to mox- and make it return a regular error.
1371 defer func() {
1372 x := recover()
1373 if x == nil {
1374 return
1375 }
1376 err, ok := x.(*sherpa.Error)
1377 if !ok {
1378 panic(x)
1379 }
1380 log.Fatalf("%s", err)
1381 }()
1382
1383 printResult := func(name string, r webadmin.Result) {
1384 if len(r.Errors) == 0 && len(r.Warnings) == 0 {
1385 return
1386 }
1387 fmt.Printf("# %s\n", name)
1388 for _, s := range r.Errors {
1389 fmt.Printf("error: %s\n", s)
1390 }
1391 for _, s := range r.Warnings {
1392 fmt.Printf("warning: %s\n", s)
1393 }
1394 }
1395
1396 result := webadmin.Admin{}.CheckDomain(context.Background(), args[0])
1397 printResult("DNSSEC", result.DNSSEC.Result)
1398 printResult("IPRev", result.IPRev.Result)
1399 printResult("MX", result.MX.Result)
1400 printResult("TLS", result.TLS.Result)
1401 printResult("DANE", result.DANE.Result)
1402 printResult("SPF", result.SPF.Result)
1403 printResult("DKIM", result.DKIM.Result)
1404 printResult("DMARC", result.DMARC.Result)
1405 printResult("Host TLSRPT", result.HostTLSRPT.Result)
1406 printResult("Domain TLSRPT", result.DomainTLSRPT.Result)
1407 printResult("MTASTS", result.MTASTS.Result)
1408 printResult("SRV conf", result.SRVConf.Result)
1409 printResult("Autoconf", result.Autoconf.Result)
1410 printResult("Autodiscover", result.Autodiscover.Result)
1411}
1412
1413func cmdConfigEnsureACMEHostprivatekeys(c *cmd) {
1414 c.params = ""
1415 c.help = `Ensure host private keys exist for TLS listeners with ACME.
1416
1417In mox.conf, each listener can have TLS configured. Long-lived private key files
1418can be specified, which will be used when requesting ACME certificates.
1419Configuring these private keys makes it feasible to publish DANE TLSA records
1420for the corresponding public keys in DNS, protected with DNSSEC, allowing TLS
1421certificate verification without depending on a list of Certificate Authorities
1422(CAs). Previous versions of mox did not pre-generate private keys for use with
1423ACME certificates, but would generate private keys on-demand. By explicitly
1424configuring private keys, they will not change automatedly with new
1425certificates, and the DNS TLSA records stay valid.
1426
1427This command looks for listeners in mox.conf with TLS with ACME configured. For
1428each missing host private key (of type rsa-2048 and ecdsa-p256) a key is written
1429to config/hostkeys/. If a certificate exists in the ACME "cache", its private
1430key is copied. Otherwise a new private key is generated. Snippets for manually
1431updating/editing mox.conf are printed.
1432
1433After running this command, and updating mox.conf, run "mox config dnsrecords"
1434for a domain and create the TLSA DNS records it suggests to enable DANE.
1435`
1436 args := c.Parse()
1437 if len(args) != 0 {
1438 c.Usage()
1439 }
1440
1441 // Load a private key from p, in various forms. We only look at the first PEM
1442 // block. Files with only a private key, or with multiple blocks but private key
1443 // first like autocert does, can be loaded.
1444 loadPrivateKey := func(f *os.File) (any, error) {
1445 buf, err := io.ReadAll(f)
1446 if err != nil {
1447 return nil, fmt.Errorf("reading private key file: %v", err)
1448 }
1449 block, _ := pem.Decode(buf)
1450 if block == nil {
1451 return nil, fmt.Errorf("no pem block found in pem file")
1452 }
1453 var privKey any
1454 switch block.Type {
1455 case "EC PRIVATE KEY":
1456 privKey, err = x509.ParseECPrivateKey(block.Bytes)
1457 case "RSA PRIVATE KEY":
1458 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
1459 case "PRIVATE KEY":
1460 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
1461 default:
1462 return nil, fmt.Errorf("unrecognized pem block type %q", block.Type)
1463 }
1464 if err != nil {
1465 return nil, fmt.Errorf("parsing private key of type %q: %v", block.Type, err)
1466 }
1467 return privKey, nil
1468 }
1469
1470 // Either load a private key from file, or if it doesn't exist generate a new
1471 // private key.
1472 xtryLoadPrivateKey := func(kt autocert.KeyType, p string) any {
1473 f, err := os.Open(p)
1474 if err != nil && errors.Is(err, fs.ErrNotExist) {
1475 switch kt {
1476 case autocert.KeyRSA2048:
1477 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1478 xcheckf(err, "generating new 2048-bit rsa private key")
1479 return privKey
1480 case autocert.KeyECDSAP256:
1481 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1482 xcheckf(err, "generating new ecdsa p-256 private key")
1483 return privKey
1484 }
1485 log.Fatalf("unexpected keytype %v", kt)
1486 return nil
1487 }
1488 xcheckf(err, "%s: open acme key and certificate file", p)
1489
1490 // Load private key from file. autocert stores a PEM file that starts with a
1491 // private key, followed by certificate(s). So we can just read it and should find
1492 // the private key we are looking for.
1493 privKey, err := loadPrivateKey(f)
1494 if xerr := f.Close(); xerr != nil {
1495 log.Printf("closing private key file: %v", xerr)
1496 }
1497 xcheckf(err, "parsing private key from acme key and certificate file")
1498
1499 switch k := privKey.(type) {
1500 case *rsa.PrivateKey:
1501 if k.N.BitLen() == 2048 {
1502 return privKey
1503 }
1504 log.Printf("warning: rsa private key in %s has %d bits, skipping and generating new 2048-bit rsa private key", p, k.N.BitLen())
1505 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1506 xcheckf(err, "generating new 2048-bit rsa private key")
1507 return privKey
1508 case *ecdsa.PrivateKey:
1509 if k.Curve == elliptic.P256() {
1510 return privKey
1511 }
1512 log.Printf("warning: ecdsa private key in %s has curve %v, skipping and generating new p-256 ecdsa key", p, k.Curve.Params().Name)
1513 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1514 xcheckf(err, "generating new ecdsa p-256 private key")
1515 return privKey
1516 default:
1517 log.Fatalf("%s: unexpected private key file of type %T", p, privKey)
1518 return nil
1519 }
1520 }
1521
1522 // Write privKey as PKCS#8 private key to p. Only if file does not yet exist.
1523 writeHostPrivateKey := func(privKey any, p string) error {
1524 os.MkdirAll(filepath.Dir(p), 0700)
1525 f, err := os.OpenFile(p, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
1526 if err != nil {
1527 return fmt.Errorf("create: %v", err)
1528 }
1529 defer func() {
1530 if f != nil {
1531 if err := f.Close(); err != nil {
1532 log.Printf("closing new hostkey file %s after error: %v", p, err)
1533 }
1534 if err := os.Remove(p); err != nil {
1535 log.Printf("removing new hostkey file %s after error: %v", p, err)
1536 }
1537 }
1538 }()
1539 buf, err := x509.MarshalPKCS8PrivateKey(privKey)
1540 if err != nil {
1541 return fmt.Errorf("marshal private host key: %v", err)
1542 }
1543 block := pem.Block{
1544 Type: "PRIVATE KEY",
1545 Bytes: buf,
1546 }
1547 if err := pem.Encode(f, &block); err != nil {
1548 return fmt.Errorf("write as pem: %v", err)
1549 }
1550 if err := f.Close(); err != nil {
1551 return fmt.Errorf("close: %v", err)
1552 }
1553 f = nil
1554 return nil
1555 }
1556
1557 mustLoadConfig()
1558 timestamp := time.Now().Format("20060102T150405")
1559 didCreate := false
1560 for listenerName, l := range mox.Conf.Static.Listeners {
1561 if l.TLS == nil || l.TLS.ACME == "" {
1562 continue
1563 }
1564 haveKeyTypes := map[autocert.KeyType]bool{}
1565 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
1566 p := mox.ConfigDirPath(privKeyFile)
1567 f, err := os.Open(p)
1568 xcheckf(err, "open host private key")
1569 privKey, err := loadPrivateKey(f)
1570 if err := f.Close(); err != nil {
1571 log.Printf("closing host private key file: %v", err)
1572 }
1573 xcheckf(err, "loading host private key")
1574 switch k := privKey.(type) {
1575 case *rsa.PrivateKey:
1576 if k.N.BitLen() == 2048 {
1577 haveKeyTypes[autocert.KeyRSA2048] = true
1578 }
1579 case *ecdsa.PrivateKey:
1580 if k.Curve == elliptic.P256() {
1581 haveKeyTypes[autocert.KeyECDSAP256] = true
1582 }
1583 }
1584 }
1585 created := []string{}
1586 for _, kt := range []autocert.KeyType{autocert.KeyRSA2048, autocert.KeyECDSAP256} {
1587 if haveKeyTypes[kt] {
1588 continue
1589 }
1590 // Lookup key in ACME cache.
1591 host := l.HostnameDomain
1592 if host.ASCII == "" {
1593 host = mox.Conf.Static.HostnameDomain
1594 }
1595 filename := host.ASCII
1596 kind := "ecdsap256"
1597 if kt == autocert.KeyRSA2048 {
1598 filename += "+rsa"
1599 kind = "rsa2048"
1600 }
1601 p := mox.DataDirPath(filepath.Join("acme", "keycerts", l.TLS.ACME, filename))
1602 privKey := xtryLoadPrivateKey(kt, p)
1603
1604 relPath := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", host.Name(), timestamp, kind))
1605 destPath := mox.ConfigDirPath(relPath)
1606 err := writeHostPrivateKey(privKey, destPath)
1607 xcheckf(err, "writing host private key file to %s: %v", destPath, err)
1608 created = append(created, relPath)
1609 fmt.Printf("Wrote host private key: %s\n", destPath)
1610 }
1611 didCreate = didCreate || len(created) > 0
1612 if len(created) > 0 {
1613 tls := config.TLS{
1614 HostPrivateKeyFiles: append(l.TLS.HostPrivateKeyFiles, created...),
1615 }
1616 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)
1617 err := sconf.Write(os.Stdout, tls)
1618 xcheckf(err, "writing new TLS.HostPrivateKeyFiles section")
1619 fmt.Println()
1620 }
1621 }
1622 if didCreate {
1623 fmt.Printf(`
1624After updating mox.conf and restarting, run "mox config dnsrecords" for a
1625domain and create the TLSA DNS records it suggests to enable DANE.
1626`)
1627 }
1628}
1629
1630func cmdLoglevels(c *cmd) {
1631 c.params = "[level [pkg]]"
1632 c.help = `Print the log levels, or set a new default log level, or a level for the given package.
1633
1634By default, a single log level applies to all logging in mox. But for each
1635"pkg", an overriding log level can be configured. Examples of packages:
1636smtpserver, smtpclient, queue, imapserver, spf, dkim, dmarc, junk, message,
1637etc.
1638
1639Specify a pkg and an empty level to clear the configured level for a package.
1640
1641Valid labels: error, info, debug, trace, traceauth, tracedata.
1642`
1643 args := c.Parse()
1644 if len(args) > 2 {
1645 c.Usage()
1646 }
1647 mustLoadConfig()
1648
1649 if len(args) == 0 {
1650 ctlcmdLoglevels(xctl())
1651 } else {
1652 var pkg string
1653 if len(args) == 2 {
1654 pkg = args[1]
1655 }
1656 ctlcmdSetLoglevels(xctl(), pkg, args[0])
1657 }
1658}
1659
1660func ctlcmdLoglevels(ctl *ctl) {
1661 ctl.xwrite("loglevels")
1662 ctl.xreadok()
1663 ctl.xstreamto(os.Stdout)
1664}
1665
1666func ctlcmdSetLoglevels(ctl *ctl, pkg, level string) {
1667 ctl.xwrite("setloglevels")
1668 ctl.xwrite(pkg)
1669 ctl.xwrite(level)
1670 ctl.xreadok()
1671}
1672
1673func cmdStop(c *cmd) {
1674 c.help = `Shut mox down, giving connections maximum 3 seconds to stop before closing them.
1675
1676While shutting down, new IMAP and SMTP connections will get a status response
1677indicating temporary unavailability. Existing connections will get a 3 second
1678period to finish their transaction and shut down. Under normal circumstances,
1679only IMAP has long-living connections, with the IDLE command to get notified of
1680new mail deliveries.
1681`
1682 if len(c.Parse()) != 0 {
1683 c.Usage()
1684 }
1685 mustLoadConfig()
1686
1687 xctl := xctl()
1688 xctl.xwrite("stop")
1689 // Read will hang until remote has shut down.
1690 buf := make([]byte, 128)
1691 n, err := xctl.conn.Read(buf)
1692 if err == nil {
1693 log.Fatalf("expected eof after graceful shutdown, got data %q", buf[:n])
1694 } else if err != io.EOF {
1695 log.Fatalf("expected eof after graceful shutdown, got error %v", err)
1696 }
1697 fmt.Println("mox stopped")
1698}
1699
1700func cmdBackup(c *cmd) {
1701 c.params = "destdir"
1702 c.help = `Creates a backup of the config and data directory.
1703
1704Backup copies the config directory to <destdir>/config, and creates
1705<destdir>/data with a consistent snapshot of the databases and message files
1706and copies other files from the data directory. Empty directories are not
1707copied. The backup can then be stored elsewhere for long-term storage, or used
1708to fall back to should an upgrade fail. Simply copying files in the data
1709directory while mox is running can result in unusable database files.
1710
1711Message files never change (they are read-only, though can be removed) and are
1712hard-linked so they don't consume additional space. If hardlinking fails, for
1713example when the backup destination directory is on a different file system, a
1714regular copy is made. Using a destination directory like "data/tmp/backup"
1715increases the odds hardlinking succeeds: the default systemd service file
1716specifically mounts the data directory, causing attempts to hardlink outside it
1717to fail with an error about cross-device linking.
1718
1719All files in the data directory that aren't recognized (i.e. other than known
1720database files, message files, an acme directory, the "tmp" directory, etc),
1721are stored, but with a warning.
1722
1723Remove files in the destination directory before doing another backup. The
1724backup command will not overwrite files, but print and return errors.
1725
1726Exit code 0 indicates the backup was successful. A clean successful backup does
1727not print any output, but may print warnings. Use the -verbose flag for
1728details, including timing.
1729
1730To restore a backup, first shut down mox, move away the old data directory and
1731move an earlier backed up directory in its place, run "mox verifydata
1732<datadir>", possibly with the "-fix" option, and restart mox. After the
1733restore, you may also want to run "mox bumpuidvalidity" for each account for
1734which messages in a mailbox changed, to force IMAP clients to synchronize
1735mailbox state.
1736
1737Before upgrading, to check if the upgrade will likely succeed, first make a
1738backup, then use the new mox binary to run "mox verifydata <backupdir>/data".
1739This can change the backup files (e.g. upgrade database files, move away
1740unrecognized message files), so you should make a new backup before actually
1741upgrading.
1742`
1743
1744 var verbose bool
1745 c.flag.BoolVar(&verbose, "verbose", false, "print progress")
1746 args := c.Parse()
1747 if len(args) != 1 {
1748 c.Usage()
1749 }
1750 mustLoadConfig()
1751
1752 dstDataDir, err := filepath.Abs(args[0])
1753 xcheckf(err, "making path absolute")
1754
1755 ctlcmdBackup(xctl(), dstDataDir, verbose)
1756}
1757
1758func ctlcmdBackup(ctl *ctl, dstDataDir string, verbose bool) {
1759 ctl.xwrite("backup")
1760 ctl.xwrite(dstDataDir)
1761 if verbose {
1762 ctl.xwrite("verbose")
1763 } else {
1764 ctl.xwrite("")
1765 }
1766 ctl.xstreamto(os.Stdout)
1767 ctl.xreadok()
1768}
1769
1770func cmdSetadminpassword(c *cmd) {
1771 c.help = `Set a new admin password, for the web interface.
1772
1773The password is read from stdin. Its bcrypt hash is stored in a file named
1774"adminpasswd" in the configuration directory.
1775`
1776 if len(c.Parse()) != 0 {
1777 c.Usage()
1778 }
1779 mustLoadConfig()
1780
1781 path := mox.ConfigDirPath(mox.Conf.Static.AdminPasswordFile)
1782 if path == "" {
1783 log.Fatal("no admin password file configured")
1784 }
1785
1786 pw := xreadpassword()
1787 pw, err := precis.OpaqueString.String(pw)
1788 xcheckf(err, `checking password with "precis" requirements`)
1789 hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
1790 xcheckf(err, "generating hash for password")
1791 err = os.WriteFile(path, hash, 0660)
1792 xcheckf(err, "writing hash to admin password file")
1793}
1794
1795func xreadpassword() string {
1796 fmt.Printf(`
1797Type new password. Password WILL echo.
1798
1799WARNING: Bots will try to bruteforce your password. Connections with failed
1800authentication attempts will be rate limited but attackers WILL find passwords
1801reused at other services and weak passwords. If your account is compromised,
1802spammers are likely to abuse your system, spamming your address and the wider
1803internet in your name. So please pick a random, unguessable password, preferably
1804at least 12 characters.
1805
1806`)
1807 fmt.Printf("password: ")
1808 scanner := bufio.NewScanner(os.Stdin)
1809 // The default splitter for scanners is one that splits by lines, so we
1810 // don't have to set up another one here.
1811
1812 // We discard the return value of Scan() since failing to tokenize could
1813 // either mean reaching EOF but no newline (which can be legitimate if the
1814 // CLI was programatically called to set the password, but with no trailing
1815 // newline), or an actual error. We can distinguish between the two by
1816 // calling Err() since it will return nil if it were EOF, but the actual
1817 // error if not.
1818 scanner.Scan()
1819 xcheckf(scanner.Err(), "reading stdin")
1820 // No need to trim, the scanner does not return the token in the output.
1821 pw := scanner.Text()
1822 if len(pw) < 8 {
1823 log.Fatal("password must be at least 8 characters")
1824 }
1825 return pw
1826}
1827
1828func cmdSetaccountpassword(c *cmd) {
1829 c.params = "account"
1830 c.help = `Set new password an account.
1831
1832The password is read from stdin. Secrets derived from the password, but not the
1833password itself, are stored in the account database. The stored secrets are for
1834authentication with: scram-sha-256, scram-sha-1, cram-md5, plain text (bcrypt
1835hash).
1836
1837The parameter is an account name, as configured under Accounts in domains.conf
1838and as present in the data/accounts/ directory, not a configured email address
1839for an account.
1840`
1841 args := c.Parse()
1842 if len(args) != 1 {
1843 c.Usage()
1844 }
1845 mustLoadConfig()
1846
1847 pw := xreadpassword()
1848
1849 ctlcmdSetaccountpassword(xctl(), args[0], pw)
1850}
1851
1852func ctlcmdSetaccountpassword(ctl *ctl, account, password string) {
1853 ctl.xwrite("setaccountpassword")
1854 ctl.xwrite(account)
1855 ctl.xwrite(password)
1856 ctl.xreadok()
1857}
1858
1859func cmdDeliver(c *cmd) {
1860 c.unlisted = true
1861 c.params = "address < message"
1862 c.help = "Deliver message to address."
1863 args := c.Parse()
1864 if len(args) != 1 {
1865 c.Usage()
1866 }
1867 mustLoadConfig()
1868 ctlcmdDeliver(xctl(), args[0])
1869}
1870
1871func ctlcmdDeliver(ctl *ctl, address string) {
1872 ctl.xwrite("deliver")
1873 ctl.xwrite(address)
1874 ctl.xreadok()
1875 ctl.xstreamfrom(os.Stdin)
1876 line := ctl.xread()
1877 if line == "ok" {
1878 fmt.Println("message delivered")
1879 } else {
1880 log.Fatalf("deliver: %s", line)
1881 }
1882}
1883
1884func cmdDKIMGenrsa(c *cmd) {
1885 c.params = ">$selector._domainkey.$domain.rsa2048.privatekey.pkcs8.pem"
1886 c.help = `Generate a new 2048 bit RSA private key for use with DKIM.
1887
1888The generated file is in PEM format, and has a comment it is generated for use
1889with DKIM, by mox.
1890`
1891 if len(c.Parse()) != 0 {
1892 c.Usage()
1893 }
1894
1895 buf, err := admin.MakeDKIMRSAKey(dns.Domain{}, dns.Domain{})
1896 xcheckf(err, "making rsa private key")
1897 _, err = os.Stdout.Write(buf)
1898 xcheckf(err, "writing rsa private key")
1899}
1900
1901// todo: options for specifying the domain this is the mx host of, and enabling dane and/or mta-sts verification
1902func cmdSMTPDial(c *cmd) {
1903 c.params = "host[:port]"
1904
1905 var tlsCerts, tlsCiphersuites, tlsCurves, tlsVersionMin, tlsVersionMax, tlsRenegotiation string
1906 var tlsVerify, noTLS, forceTLS, tlsNoSessionTickets, tlsNoDynamicRecordSizing bool
1907 var ehloHostnameStr, remoteHostnameStr string
1908
1909 ciphersuites := map[string]*tls.CipherSuite{}
1910 ciphersuitesInsecure := map[string]*tls.CipherSuite{}
1911 for _, v := range tls.CipherSuites() {
1912 if slices.Contains(v.SupportedVersions, tls.VersionTLS10) || slices.Contains(v.SupportedVersions, tls.VersionTLS11) || slices.Contains(v.SupportedVersions, tls.VersionTLS12) {
1913 ciphersuites[strings.ToLower(v.Name)] = v
1914 }
1915 }
1916 for _, v := range tls.InsecureCipherSuites() {
1917 if slices.Contains(v.SupportedVersions, tls.VersionTLS10) || slices.Contains(v.SupportedVersions, tls.VersionTLS11) || slices.Contains(v.SupportedVersions, tls.VersionTLS12) {
1918 ciphersuitesInsecure[strings.ToLower(v.Name)] = v
1919 }
1920 }
1921
1922 curves := map[string]tls.CurveID{}
1923 for _, a := range curvesList {
1924 curves[strings.ToLower(a.String())] = a
1925 }
1926
1927 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)), ", "))
1928 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")
1929 c.flag.StringVar(&tlsCerts, "tlscerts", "", "path to root ca certificates in pem form, for verification")
1930 c.flag.StringVar(&tlsVersionMin, "tlsversionmin", "", "minimum TLS version, empty value uses TLS stack default; values: tls1.2, etc.")
1931 c.flag.StringVar(&tlsVersionMax, "tlsversionmax", "", "maximum TLS version, empty value uses TLS stack default; values: tls1.2, etc.")
1932 c.flag.BoolVar(&tlsVerify, "tlsverify", false, "verify remote hostname during TLS")
1933 c.flag.BoolVar(&tlsNoSessionTickets, "tlsnosessiontickets", false, "disable TLS session tickets")
1934 c.flag.BoolVar(&tlsNoDynamicRecordSizing, "tlsnodynamicrecordsizing", false, "disable TLS dynamic record sizing")
1935 c.flag.BoolVar(&noTLS, "notls", false, "do not use TLS")
1936 c.flag.BoolVar(&forceTLS, "forcetls", false, "use TLS, even if remote SMTP server does not announce STARTTLS extension")
1937 c.flag.StringVar(&tlsRenegotiation, "tlsrenegotiation", "never", "when to allow renegotiation; only applies to tls1.2 and earlier, not tls1.3; values: never, once, always")
1938 c.flag.StringVar(&ehloHostnameStr, "ehlohostname", "", "our hostname to use during the SMTP EHLO command")
1939 c.flag.StringVar(&remoteHostnameStr, "remotehostname", "", "remote hostname to use for TLS verification, if enabled; the hostname from the parameter is used by default")
1940
1941 c.help = `Dial the address, initialize the SMTP session, including using STARTTLS to enable TLS if the server supports it.
1942
1943If no port is specified, SMTP port 25 is used.
1944
1945Data is copied between connection and stdin/stdout until either side closes the
1946connection.
1947
1948The flags influence the TLS configuration, useful for debugging interoperability
1949issues.
1950
1951No MTA-STS or DANE verification is done.
1952
1953Hint: Use "mox -loglevel trace smtp dial ..." to see the protocol messages
1954exchanged during connection set up.
1955`
1956 args := c.Parse()
1957 if len(args) != 1 {
1958 c.Usage()
1959 }
1960
1961 if noTLS && forceTLS {
1962 log.Fatalf("cannot have both -notls and -forcetls")
1963 }
1964
1965 parseTLSVersion := func(s string) uint16 {
1966 switch s {
1967 case "tls1.0":
1968 return tls.VersionTLS10
1969 case "tls1.1":
1970 return tls.VersionTLS11
1971 case "tls1.2":
1972 return tls.VersionTLS12
1973 case "tls1.3":
1974 return tls.VersionTLS13
1975 case "":
1976 return 0
1977 default:
1978 log.Fatalf("invalid tls version %q", s)
1979 panic("not reached")
1980 }
1981 }
1982 tlsConfig := tls.Config{
1983 MinVersion: parseTLSVersion(tlsVersionMin),
1984 MaxVersion: parseTLSVersion(tlsVersionMax),
1985 InsecureSkipVerify: !tlsVerify,
1986 SessionTicketsDisabled: tlsNoSessionTickets,
1987 DynamicRecordSizingDisabled: tlsNoDynamicRecordSizing,
1988 }
1989
1990 switch tlsRenegotiation {
1991 case "never":
1992 tlsConfig.Renegotiation = tls.RenegotiateNever
1993 case "once":
1994 tlsConfig.Renegotiation = tls.RenegotiateOnceAsClient
1995 case "always":
1996 tlsConfig.Renegotiation = tls.RenegotiateFreelyAsClient
1997 default:
1998 log.Fatalf("invalid value %q for -tlsrenegotation", tlsRenegotiation)
1999 }
2000 if tlsCerts != "" {
2001 pool := x509.NewCertPool()
2002 pembuf, err := os.ReadFile(tlsCerts)
2003 xcheckf(err, "reading tls certificates")
2004 ok := pool.AppendCertsFromPEM(pembuf)
2005 if !ok {
2006 c.log.Warn("no tls certificates found", slog.String("path", tlsCerts))
2007 }
2008 tlsConfig.RootCAs = pool
2009 }
2010 if tlsCiphersuites != "" {
2011 for s := range strings.SplitSeq(tlsCiphersuites, ",") {
2012 s = strings.TrimSpace(s)
2013 c, ok := ciphersuites[s]
2014 if !ok {
2015 c, ok = ciphersuitesInsecure[s]
2016 }
2017 if !ok {
2018 log.Fatalf("unknown ciphersuite %q", s)
2019 }
2020 tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, c.ID)
2021 }
2022 }
2023 if tlsCurves != "" {
2024 for s := range strings.SplitSeq(tlsCurves, ",") {
2025 s = strings.TrimSpace(s)
2026 if c, ok := curves[s]; !ok {
2027 log.Fatalf("unknown ecc key exchange algorithm %q", s)
2028 } else {
2029 tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, c)
2030 }
2031 }
2032 }
2033
2034 var host, portStr string
2035 var err error
2036 host, portStr, err = net.SplitHostPort(args[0])
2037 if err != nil {
2038 host = args[0]
2039 portStr = "25"
2040 }
2041 port, err := strconv.ParseInt(portStr, 10, 64)
2042 xcheckf(err, "parsing port %q", portStr)
2043
2044 if remoteHostnameStr == "" {
2045 remoteHostnameStr = host
2046 }
2047 remoteHostname, err := dns.ParseDomain(remoteHostnameStr)
2048 xcheckf(err, "parsing remote host")
2049 tlsConfig.ServerName = remoteHostname.Name()
2050
2051 resolver := dns.StrictResolver{Pkg: "smtpdial"}
2052 _, _, _, ips, _, err := smtpclient.GatherIPs(context.Background(), c.log.Logger, resolver, "ip", dns.IPDomain{Domain: remoteHostname}, nil)
2053 xcheckf(err, "resolve host")
2054 c.log.Info("resolved remote address", slog.Any("ips", ips))
2055
2056 dialer := &net.Dialer{Timeout: 5 * time.Second}
2057 dialedIPs := map[string][]net.IP{}
2058 conn, ip, err := smtpclient.Dial(context.Background(), c.log.Logger, dialer, dns.IPDomain{Domain: remoteHostname}, ips, int(port), dialedIPs, nil)
2059 xcheckf(err, "dial")
2060 c.log.Info("connected to remote host", slog.Any("ip", ip))
2061
2062 tlsMode := smtpclient.TLSOpportunistic
2063 if forceTLS {
2064 tlsMode = smtpclient.TLSRequiredStartTLS
2065 } else if noTLS {
2066 tlsMode = smtpclient.TLSSkip
2067 }
2068 var ehloHostname dns.Domain
2069 if ehloHostnameStr == "" {
2070 name, err := os.Hostname()
2071 xcheckf(err, "get hostname")
2072 ehloHostnameStr = name
2073 }
2074 ehloHostname, err = dns.ParseDomain(ehloHostnameStr)
2075 xcheckf(err, "parse hostname")
2076
2077 opts := smtpclient.Opts{
2078 TLSConfig: &tlsConfig,
2079 }
2080 client, err := smtpclient.New(context.Background(), c.log.Logger, conn, tlsMode, false, ehloHostname, dns.Domain{}, opts)
2081 xcheckf(err, "new smtp client")
2082
2083 cs := client.TLSConnectionState()
2084 if cs == nil {
2085 c.log.Info("smtp initialized without tls")
2086 } else {
2087 c.log.Info("smtp initialized with tls",
2088 slog.String("version", tls.VersionName(cs.Version)),
2089 slog.String("ciphersuite", strings.ToLower(tls.CipherSuiteName(cs.CipherSuite))),
2090 slog.String("sni", cs.ServerName),
2091 )
2092 for _, chain := range cs.VerifiedChains {
2093 var l []string
2094 for _, cert := range chain {
2095 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)
2096 l = append(l, s)
2097 }
2098 c.log.Info("tls certificate verification chain", slog.String("chain", strings.Join(l, "; ")))
2099 }
2100 }
2101
2102 conn, err = client.Conn()
2103 xcheckf(err, "get smtp session connection")
2104
2105 go func() {
2106 _, err := io.Copy(os.Stdout, conn)
2107 xcheckf(err, "copy from connection to stdout")
2108 err = conn.Close()
2109 c.log.Check(err, "closing connection")
2110 }()
2111 _, err = io.Copy(conn, os.Stdin)
2112 xcheckf(err, "copy from stdin to connection")
2113}
2114
2115func cmdDANEDial(c *cmd) {
2116 c.params = "host:port"
2117 var usages string
2118 c.flag.StringVar(&usages, "usages", "pkix-ta,pkix-ee,dane-ta,dane-ee", "allowed usages for dane, comma-separated list")
2119 c.help = `Dial the address using TLS with certificate verification using DANE.
2120
2121Data is copied between connection and stdin/stdout until either side closes the
2122connection.
2123`
2124 args := c.Parse()
2125 if len(args) != 1 {
2126 c.Usage()
2127 }
2128
2129 allowedUsages := []adns.TLSAUsage{}
2130 if usages != "" {
2131 for s := range strings.SplitSeq(usages, ",") {
2132 var usage adns.TLSAUsage
2133 switch strings.ToLower(s) {
2134 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
2135 usage = adns.TLSAUsagePKIXTA
2136 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
2137 usage = adns.TLSAUsagePKIXEE
2138 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
2139 usage = adns.TLSAUsageDANETA
2140 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
2141 usage = adns.TLSAUsageDANEEE
2142 default:
2143 log.Fatalf("unknown dane usage %q", s)
2144 }
2145 allowedUsages = append(allowedUsages, usage)
2146 }
2147 }
2148
2149 pkixRoots, err := x509.SystemCertPool()
2150 xcheckf(err, "get system pkix certificate pool")
2151
2152 resolver := dns.StrictResolver{Pkg: "danedial"}
2153 conn, record, err := dane.Dial(context.Background(), c.log.Logger, resolver, "tcp", args[0], allowedUsages, pkixRoots)
2154 xcheckf(err, "dial")
2155 log.Printf("(connected, verified with %s)", record)
2156
2157 go func() {
2158 _, err := io.Copy(os.Stdout, conn)
2159 xcheckf(err, "copy from connection to stdout")
2160 err = conn.Close()
2161 c.log.Check(err, "closing connection")
2162 }()
2163 _, err = io.Copy(conn, os.Stdin)
2164 xcheckf(err, "copy from stdin to connection")
2165}
2166
2167func cmdDANEDialmx(c *cmd) {
2168 c.params = "domain [destination-host]"
2169 var ehloHostname string
2170 c.flag.StringVar(&ehloHostname, "ehlohostname", "localhost", "hostname to send in smtp ehlo command")
2171 c.help = `Connect to MX server for domain using STARTTLS verified with DANE.
2172
2173If no destination host is specified, regular delivery logic is used to find the
2174hosts to attempt delivery too. This involves following CNAMEs for the domain,
2175looking up MX records, and possibly falling back to the domain name itself as
2176host.
2177
2178If a destination host is specified, that is the only candidate host considered
2179for dialing.
2180
2181With a list of destinations gathered, each is dialed until a successful SMTP
2182session verified with DANE has been initialized, including EHLO and STARTTLS
2183commands.
2184
2185Once connected, data is copied between connection and stdin/stdout, until
2186either side closes the connection.
2187
2188This command follows the same logic as delivery attempts made from the queue,
2189sharing most of its code.
2190`
2191 args := c.Parse()
2192 if len(args) != 1 && len(args) != 2 {
2193 c.Usage()
2194 }
2195
2196 ehloDomain := xparseDomain(ehloHostname, "ehlo host name")
2197 origNextHop := xparseDomain(args[0], "domain")
2198
2199 ctxbg := context.Background()
2200
2201 resolver := dns.StrictResolver{}
2202 var haveMX bool
2203 var expandedNextHopAuthentic bool
2204 var expandedNextHop dns.Domain
2205 var hostPrefs []smtpclient.HostPref
2206 if len(args) == 1 {
2207 var permanent bool
2208 var origNextHopAuthentic bool
2209 var err error
2210 haveMX, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hostPrefs, permanent, err = smtpclient.GatherDestinations(ctxbg, c.log.Logger, resolver, dns.IPDomain{Domain: origNextHop})
2211 status := "temporary"
2212 if permanent {
2213 status = "permanent"
2214 }
2215 if err != nil {
2216 log.Fatalf("gathering destinations: %v (%s)", err, status)
2217 }
2218 if expandedNextHop != origNextHop {
2219 log.Printf("followed cnames to %s", expandedNextHop)
2220 }
2221 if haveMX {
2222 log.Printf("found mx record, trying mx hosts")
2223 } else {
2224 log.Printf("no mx record found, will try to connect to domain directly")
2225 }
2226 if !origNextHopAuthentic {
2227 log.Fatalf("error: initial domain not dnssec-secure")
2228 }
2229 if !expandedNextHopAuthentic {
2230 log.Fatalf("error: expanded domain not dnssec-secure")
2231 }
2232
2233 l := []string{}
2234 for _, hp := range hostPrefs {
2235 s := hp.Host.String()
2236 if hp.Pref >= 0 {
2237 s += fmt.Sprintf(" (pref %d)", hp.Pref)
2238 }
2239 l = append(l, s)
2240 }
2241 log.Printf("destinations: %s", strings.Join(l, ", "))
2242 } else {
2243 d := xparseDomain(args[1], "destination host")
2244 log.Printf("skipping domain mx/cname lookups, assuming domain is dnssec-protected")
2245
2246 expandedNextHopAuthentic = true
2247 expandedNextHop = d
2248 hostPrefs = []smtpclient.HostPref{{Host: dns.IPDomain{Domain: d}, Pref: -1}}
2249 }
2250
2251 dialedIPs := map[string][]net.IP{}
2252 for _, hp := range hostPrefs {
2253 host := hp.Host
2254
2255 log.Printf("attempting to connect to %s (pref %d)", host, hp.Pref)
2256
2257 authentic, expandedAuthentic, expandedHost, ips, _, err := smtpclient.GatherIPs(ctxbg, c.log.Logger, resolver, "ip", host, dialedIPs)
2258 if err != nil {
2259 log.Printf("resolving ips for %s: %v, skipping", host, err)
2260 continue
2261 }
2262 if !authentic {
2263 log.Printf("no dnssec for ips of %s, skipping", host)
2264 continue
2265 }
2266 if !expandedAuthentic {
2267 log.Printf("no dnssec for cname-followed ips of %s, skipping", host)
2268 continue
2269 }
2270 if expandedHost != host.Domain {
2271 log.Printf("host %s cname-expanded to %s", host, expandedHost)
2272 }
2273 log.Printf("host %s resolved to ips %s, looking up tlsa records", host, ips)
2274
2275 daneRequired, daneRecords, tlsaBaseDomain, err := smtpclient.GatherTLSA(ctxbg, c.log.Logger, resolver, host.Domain, expandedAuthentic, expandedHost)
2276 if err != nil {
2277 log.Printf("looking up tlsa records: %s, skipping", err)
2278 continue
2279 }
2280 tlsMode := smtpclient.TLSRequiredStartTLS
2281 if len(daneRecords) == 0 {
2282 if !daneRequired {
2283 log.Printf("host %s has no tlsa records, skipping", expandedHost)
2284 continue
2285 }
2286 log.Printf("warning: only unusable tlsa records found, continuing with required tls without certificate verification")
2287 daneRecords = nil
2288 } else {
2289 var l []string
2290 for _, r := range daneRecords {
2291 l = append(l, r.String())
2292 }
2293 log.Printf("tlsa records: %s", strings.Join(l, "; "))
2294 }
2295
2296 tlsHostnames := smtpclient.GatherTLSANames(haveMX, expandedNextHopAuthentic, expandedAuthentic, origNextHop, expandedNextHop, host.Domain, tlsaBaseDomain)
2297 var l []string
2298 for _, name := range tlsHostnames {
2299 l = append(l, name.String())
2300 }
2301 log.Printf("gathered valid tls certificate names for potential verification with dane-ta: %s", strings.Join(l, ", "))
2302
2303 dialer := &net.Dialer{Timeout: 5 * time.Second}
2304 conn, _, err := smtpclient.Dial(ctxbg, c.log.Logger, dialer, dns.IPDomain{Domain: expandedHost}, ips, 25, dialedIPs, nil)
2305 if err != nil {
2306 log.Printf("dial %s: %v, skipping", expandedHost, err)
2307 continue
2308 }
2309 log.Printf("connected to %s, %s, starting smtp session with ehlo and starttls with dane verification", expandedHost, conn.RemoteAddr())
2310
2311 var verifiedRecord adns.TLSA
2312 opts := smtpclient.Opts{
2313 DANERecords: daneRecords,
2314 DANEMoreHostnames: tlsHostnames[1:],
2315 DANEVerifiedRecord: &verifiedRecord,
2316 RootCAs: mox.Conf.Static.TLS.CertPool,
2317 }
2318 tlsPKIX := false
2319 sc, err := smtpclient.New(ctxbg, c.log.Logger, conn, tlsMode, tlsPKIX, ehloDomain, tlsHostnames[0], opts)
2320 if err != nil {
2321 log.Printf("setting up smtp session: %v, skipping", err)
2322 if xerr := conn.Close(); xerr != nil {
2323 log.Printf("closing connection: %v", xerr)
2324 }
2325 continue
2326 }
2327
2328 smtpConn, err := sc.Conn()
2329 if err != nil {
2330 log.Fatalf("error: taking over smtp connection: %s", err)
2331 }
2332 log.Printf("tls verified with tlsa record: %s", verifiedRecord)
2333 log.Printf("smtp session initialized and connected to stdin/stdout")
2334
2335 go func() {
2336 _, err := io.Copy(os.Stdout, smtpConn)
2337 xcheckf(err, "copy from connection to stdout")
2338 if err := smtpConn.Close(); err != nil {
2339 log.Printf("closing smtp connection: %v", err)
2340 }
2341 }()
2342 _, err = io.Copy(smtpConn, os.Stdin)
2343 xcheckf(err, "copy from stdin to connection")
2344 }
2345
2346 log.Fatalf("no remaining destinations")
2347}
2348
2349func cmdDANEMakeRecord(c *cmd) {
2350 c.params = "usage selector matchtype [certificate.pem | publickey.pem | privatekey.pem]"
2351 c.help = `Print TLSA record for given certificate/key and parameters.
2352
2353Valid values:
2354- usage: pkix-ta (0), pkix-ee (1), dane-ta (2), dane-ee (3)
2355- selector: cert (0), spki (1)
2356- matchtype: full (0), sha2-256 (1), sha2-512 (2)
2357
2358Common DANE TLSA record parameters are: dane-ee spki sha2-256, or 3 1 1,
2359followed by a sha2-256 hash of the DER-encoded "SPKI" (subject public key info)
2360from the certificate. An example DNS zone file entry:
2361
2362 _25._tcp.example.com. TLSA 3 1 1 133b919c9d65d8b1488157315327334ead8d83372db57465ecabf53ee5748aee
2363
2364The first usable information from the pem file is used to compose the TLSA
2365record. In case of selector "cert", a certificate is required. Otherwise the
2366"subject public key info" (spki) of the first certificate or public or private
2367key (pkcs#8, pkcs#1 or ec private key) is used.
2368`
2369
2370 args := c.Parse()
2371 if len(args) != 4 {
2372 c.Usage()
2373 }
2374
2375 var usage adns.TLSAUsage
2376 switch strings.ToLower(args[0]) {
2377 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
2378 usage = adns.TLSAUsagePKIXTA
2379 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
2380 usage = adns.TLSAUsagePKIXEE
2381 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
2382 usage = adns.TLSAUsageDANETA
2383 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
2384 usage = adns.TLSAUsageDANEEE
2385 default:
2386 if v, err := strconv.ParseUint(args[0], 10, 16); err != nil {
2387 log.Fatalf("bad usage %q", args[0])
2388 } else {
2389 // Does not influence certificate association data, so we can accept other numbers.
2390 log.Printf("warning: continuing with unrecognized tlsa usage %d", v)
2391 usage = adns.TLSAUsage(v)
2392 }
2393 }
2394
2395 var selector adns.TLSASelector
2396 switch strings.ToLower(args[1]) {
2397 case "cert", strconv.Itoa(int(adns.TLSASelectorCert)):
2398 selector = adns.TLSASelectorCert
2399 case "spki", strconv.Itoa(int(adns.TLSASelectorSPKI)):
2400 selector = adns.TLSASelectorSPKI
2401 default:
2402 log.Fatalf("bad selector %q", args[1])
2403 }
2404
2405 var matchType adns.TLSAMatchType
2406 switch strings.ToLower(args[2]) {
2407 case "full", strconv.Itoa(int(adns.TLSAMatchTypeFull)):
2408 matchType = adns.TLSAMatchTypeFull
2409 case "sha2-256", strconv.Itoa(int(adns.TLSAMatchTypeSHA256)):
2410 matchType = adns.TLSAMatchTypeSHA256
2411 case "sha2-512", strconv.Itoa(int(adns.TLSAMatchTypeSHA512)):
2412 matchType = adns.TLSAMatchTypeSHA512
2413 default:
2414 log.Fatalf("bad matchtype %q", args[2])
2415 }
2416
2417 buf, err := os.ReadFile(args[3])
2418 xcheckf(err, "reading certificate")
2419 for {
2420 var block *pem.Block
2421 block, buf = pem.Decode(buf)
2422 if block == nil {
2423 extra := ""
2424 if len(buf) > 0 {
2425 extra = " (with leftover data from pem file)"
2426 }
2427 if selector == adns.TLSASelectorCert {
2428 log.Fatalf("no certificate found in pem file%s", extra)
2429 } else {
2430 log.Fatalf("no certificate or public or private key found in pem file%s", extra)
2431 }
2432 }
2433 var cert *x509.Certificate
2434 var data []byte
2435 if block.Type == "CERTIFICATE" {
2436 cert, err = x509.ParseCertificate(block.Bytes)
2437 xcheckf(err, "parse certificate")
2438 switch selector {
2439 case adns.TLSASelectorCert:
2440 data = cert.Raw
2441 case adns.TLSASelectorSPKI:
2442 data = cert.RawSubjectPublicKeyInfo
2443 }
2444 } else if selector == adns.TLSASelectorCert {
2445 // We need a certificate, just a public/private key won't do.
2446 log.Printf("skipping pem type %q, certificate is required", block.Type)
2447 continue
2448 } else {
2449 var privKey, pubKey any
2450 var err error
2451 switch block.Type {
2452 case "PUBLIC KEY":
2453 _, err := x509.ParsePKIXPublicKey(block.Bytes)
2454 xcheckf(err, "parse pkix subject public key info (spki)")
2455 data = block.Bytes
2456 case "EC PRIVATE KEY":
2457 privKey, err = x509.ParseECPrivateKey(block.Bytes)
2458 xcheckf(err, "parse ec private key")
2459 case "RSA PRIVATE KEY":
2460 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
2461 xcheckf(err, "parse pkcs#1 rsa private key")
2462 case "RSA PUBLIC KEY":
2463 pubKey, err = x509.ParsePKCS1PublicKey(block.Bytes)
2464 xcheckf(err, "parse pkcs#1 rsa public key")
2465 case "PRIVATE KEY":
2466 // PKCS#8 private key
2467 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
2468 xcheckf(err, "parse pkcs#8 private key")
2469 default:
2470 log.Printf("skipping unrecognized pem type %q", block.Type)
2471 continue
2472 }
2473 if data == nil {
2474 if pubKey == nil && privKey != nil {
2475 if signer, ok := privKey.(crypto.Signer); !ok {
2476 log.Fatalf("private key of type %T is not a signer, cannot get public key", privKey)
2477 } else {
2478 pubKey = signer.Public()
2479 }
2480 }
2481 if pubKey == nil {
2482 // Should not happen.
2483 log.Fatalf("internal error: did not find private or public key")
2484 }
2485 data, err = x509.MarshalPKIXPublicKey(pubKey)
2486 xcheckf(err, "marshal pkix subject public key info (spki)")
2487 }
2488 }
2489
2490 switch matchType {
2491 case adns.TLSAMatchTypeFull:
2492 case adns.TLSAMatchTypeSHA256:
2493 p := sha256.Sum256(data)
2494 data = p[:]
2495 case adns.TLSAMatchTypeSHA512:
2496 p := sha512.Sum512(data)
2497 data = p[:]
2498 }
2499 fmt.Printf("%d %d %d %x\n", usage, selector, matchType, data)
2500 break
2501 }
2502}
2503
2504func cmdDNSLookup(c *cmd) {
2505 c.params = "[ptr | mx | cname | ips | a | aaaa | ns | txt | srv | tlsa] name"
2506 c.help = `Lookup DNS name of given type.
2507
2508Lookup always prints whether the response was DNSSEC-protected.
2509
2510Examples:
2511
2512mox dns lookup ptr 1.1.1.1
2513mox dns lookup mx xmox.nl
2514mox dns lookup txt _dmarc.xmox.nl.
2515mox dns lookup tlsa _25._tcp.xmox.nl
2516`
2517 args := c.Parse()
2518
2519 if len(args) != 2 {
2520 c.Usage()
2521 }
2522
2523 resolver := dns.StrictResolver{Pkg: "dns"}
2524
2525 // like xparseDomain, but treat unparseable domain as an ASCII name so names with
2526 // underscores are still looked up, e,g <selector>._domainkey.<host>.
2527 xdomain := func(s string) dns.Domain {
2528 d, err := dns.ParseDomain(s)
2529 if err != nil {
2530 return dns.Domain{ASCII: strings.TrimSuffix(s, ".")}
2531 }
2532 return d
2533 }
2534
2535 cmd, name := args[0], args[1]
2536
2537 switch cmd {
2538 case "ptr":
2539 ip := xparseIP(name, "ip")
2540 ptrs, result, err := resolver.LookupAddr(context.Background(), ip.String())
2541 if err != nil {
2542 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2543 }
2544 fmt.Printf("names (%d, %s):\n", len(ptrs), dnssecStatus(result.Authentic))
2545 for _, ptr := range ptrs {
2546 fmt.Printf("- %s\n", ptr)
2547 }
2548
2549 case "mx":
2550 name := xdomain(name)
2551 mxl, result, err := resolver.LookupMX(context.Background(), name.ASCII+".")
2552 if err != nil {
2553 log.Printf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2554 // We can still have valid records...
2555 }
2556 fmt.Printf("mx records (%d, %s):\n", len(mxl), dnssecStatus(result.Authentic))
2557 for _, mx := range mxl {
2558 fmt.Printf("- %s, preference %d\n", mx.Host, mx.Pref)
2559 }
2560
2561 case "cname":
2562 name := xdomain(name)
2563 target, result, err := resolver.LookupCNAME(context.Background(), name.ASCII+".")
2564 if err != nil {
2565 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2566 }
2567 fmt.Printf("%s (%s)\n", target, dnssecStatus(result.Authentic))
2568
2569 case "ips", "a", "aaaa":
2570 network := "ip"
2571 if cmd == "a" {
2572 network = "ip4"
2573 } else if cmd == "aaaa" {
2574 network = "ip6"
2575 }
2576 name := xdomain(name)
2577 ips, result, err := resolver.LookupIP(context.Background(), network, name.ASCII+".")
2578 if err != nil {
2579 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2580 }
2581 fmt.Printf("records (%d, %s):\n", len(ips), dnssecStatus(result.Authentic))
2582 for _, ip := range ips {
2583 fmt.Printf("- %s\n", ip)
2584 }
2585
2586 case "ns":
2587 name := xdomain(name)
2588 nsl, result, err := resolver.LookupNS(context.Background(), name.ASCII+".")
2589 if err != nil {
2590 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2591 }
2592 fmt.Printf("ns records (%d, %s):\n", len(nsl), dnssecStatus(result.Authentic))
2593 for _, ns := range nsl {
2594 fmt.Printf("- %s\n", ns)
2595 }
2596
2597 case "txt":
2598 host := xdomain(name)
2599 l, result, err := resolver.LookupTXT(context.Background(), host.ASCII+".")
2600 if err != nil {
2601 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2602 }
2603 fmt.Printf("txt records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2604 for _, txt := range l {
2605 fmt.Printf("- %s\n", txt)
2606 }
2607
2608 case "srv":
2609 host := xdomain(name)
2610 _, l, result, err := resolver.LookupSRV(context.Background(), "", "", host.ASCII+".")
2611 if err != nil {
2612 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2613 }
2614 fmt.Printf("srv records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2615 for _, srv := range l {
2616 fmt.Printf("- host %s, port %d, priority %d, weight %d\n", srv.Target, srv.Port, srv.Priority, srv.Weight)
2617 }
2618
2619 case "tlsa":
2620 host := xdomain(name)
2621 l, result, err := resolver.LookupTLSA(context.Background(), 0, "", host.ASCII+".")
2622 if err != nil {
2623 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2624 }
2625 fmt.Printf("tlsa records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2626 for _, tlsa := range l {
2627 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)
2628 }
2629 default:
2630 log.Fatalf("unknown record type %q", args[0])
2631 }
2632}
2633
2634func cmdDKIMGened25519(c *cmd) {
2635 c.params = ">$selector._domainkey.$domain.ed25519.privatekey.pkcs8.pem"
2636 c.help = `Generate a new ed25519 key for use with DKIM.
2637
2638Ed25519 keys are much smaller than RSA keys of comparable cryptographic
2639strength. This is convenient because of maximum DNS message sizes. At the time
2640of writing, not many mail servers appear to support ed25519 DKIM keys though,
2641so it is recommended to sign messages with both RSA and ed25519 keys.
2642`
2643 if len(c.Parse()) != 0 {
2644 c.Usage()
2645 }
2646
2647 buf, err := admin.MakeDKIMEd25519Key(dns.Domain{}, dns.Domain{})
2648 xcheckf(err, "making dkim ed25519 key")
2649 _, err = os.Stdout.Write(buf)
2650 xcheckf(err, "writing dkim ed25519 key")
2651}
2652
2653func cmdDKIMTXT(c *cmd) {
2654 c.params = "<$selector._domainkey.$domain.key.pkcs8.pem"
2655 c.help = `Print a DKIM DNS TXT record with the public key derived from the private key read from stdin.
2656
2657The DNS should be configured as a TXT record at $selector._domainkey.$domain.
2658`
2659 if len(c.Parse()) != 0 {
2660 c.Usage()
2661 }
2662
2663 privKey, err := parseDKIMKey(os.Stdin)
2664 xcheckf(err, "reading dkim private key from stdin")
2665
2666 r := dkim.Record{
2667 Version: "DKIM1",
2668 Hashes: []string{"sha256"},
2669 Flags: []string{"s"},
2670 }
2671
2672 switch key := privKey.(type) {
2673 case *rsa.PrivateKey:
2674 r.PublicKey = key.Public()
2675 case ed25519.PrivateKey:
2676 r.PublicKey = key.Public()
2677 r.Key = "ed25519"
2678 default:
2679 log.Fatalf("unsupported private key type %T, must be rsa or ed25519", privKey)
2680 }
2681
2682 record, err := r.Record()
2683 xcheckf(err, "making record")
2684 fmt.Print("<selector>._domainkey.<your.domain.> TXT ")
2685 for record != "" {
2686 s := record
2687 if len(s) > 100 {
2688 s, record = record[:100], record[100:]
2689 } else {
2690 record = ""
2691 }
2692 fmt.Printf(`"%s" `, s)
2693 }
2694 fmt.Println("")
2695}
2696
2697func parseDKIMKey(r io.Reader) (any, error) {
2698 buf, err := io.ReadAll(r)
2699 if err != nil {
2700 return nil, fmt.Errorf("reading pem from stdin: %v", err)
2701 }
2702 b, _ := pem.Decode(buf)
2703 if b == nil {
2704 return nil, fmt.Errorf("decoding pem: %v", err)
2705 }
2706 privKey, err := x509.ParsePKCS8PrivateKey(b.Bytes)
2707 if err != nil {
2708 return nil, fmt.Errorf("parsing private key: %v", err)
2709 }
2710 return privKey, nil
2711}
2712
2713func cmdDKIMVerify(c *cmd) {
2714 c.params = "message"
2715 c.help = `Verify the DKIM signatures in a message and print the results.
2716
2717The message is parsed, and the DKIM-Signature headers are validated. Validation
2718of older messages may fail because the DNS records have been removed or changed
2719by now, or because the signature header may have specified an expiration time
2720that was passed.
2721`
2722 args := c.Parse()
2723 if len(args) != 1 {
2724 c.Usage()
2725 }
2726
2727 msgf, err := os.Open(args[0])
2728 xcheckf(err, "open message")
2729
2730 results, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, false, dkim.DefaultPolicy, msgf, true)
2731 xcheckf(err, "dkim verify")
2732
2733 for _, result := range results {
2734 var sigh string
2735 if result.Sig == nil {
2736 log.Printf("warning: could not parse signature")
2737 } else {
2738 sigh, err = result.Sig.Header()
2739 if err != nil {
2740 log.Printf("warning: packing signature: %s", err)
2741 }
2742 }
2743 var txt string
2744 if result.Record == nil {
2745 log.Printf("warning: missing DNS record")
2746 } else {
2747 txt, err = result.Record.Record()
2748 if err != nil {
2749 log.Printf("warning: packing record: %s", err)
2750 }
2751 }
2752 fmt.Printf("status %q, err %v\nrecord %q\nheader %s\n", result.Status, result.Err, txt, sigh)
2753 }
2754}
2755
2756func cmdDKIMSign(c *cmd) {
2757 c.params = "message"
2758 c.help = `Sign a message, adding DKIM-Signature headers based on the domain in the From header.
2759
2760The message is parsed, the domain looked up in the configuration files, and
2761DKIM-Signature headers generated. The message is printed with the DKIM-Signature
2762headers prepended.
2763`
2764 args := c.Parse()
2765 if len(args) != 1 {
2766 c.Usage()
2767 }
2768
2769 msgf, err := os.Open(args[0])
2770 xcheckf(err, "open message")
2771 defer func() {
2772 if err := msgf.Close(); err != nil {
2773 log.Printf("closing message file: %v", err)
2774 }
2775 }()
2776
2777 p, err := message.Parse(c.log.Logger, true, msgf)
2778 xcheckf(err, "parsing message")
2779
2780 if len(p.Envelope.From) != 1 {
2781 log.Fatalf("found %d from headers, need exactly 1", len(p.Envelope.From))
2782 }
2783 localpart, err := smtp.ParseLocalpart(p.Envelope.From[0].User)
2784 xcheckf(err, "parsing localpart of address in from-header")
2785 dom := xparseDomain(p.Envelope.From[0].Host, "domain of address in from-header")
2786
2787 mustLoadConfig()
2788
2789 domConf, ok := mox.Conf.Domain(dom)
2790 if !ok {
2791 log.Fatalf("domain %s not configured", dom)
2792 }
2793
2794 selectors := mox.DKIMSelectors(domConf.DKIM)
2795 headers, err := dkim.Sign(context.Background(), c.log.Logger, localpart, dom, selectors, false, msgf)
2796 xcheckf(err, "signing message with dkim")
2797 if headers == "" {
2798 log.Fatalf("no DKIM configured for domain %s", dom)
2799 }
2800 _, err = fmt.Fprint(os.Stdout, headers)
2801 xcheckf(err, "write headers")
2802 _, err = io.Copy(os.Stdout, msgf)
2803 xcheckf(err, "write message")
2804}
2805
2806func cmdDKIMLookup(c *cmd) {
2807 c.params = "selector domain"
2808 c.help = "Lookup and print the DKIM record for the selector at the domain."
2809 args := c.Parse()
2810 if len(args) != 2 {
2811 c.Usage()
2812 }
2813
2814 selector := xparseDomain(args[0], "selector")
2815 domain := xparseDomain(args[1], "domain")
2816
2817 status, record, txt, authentic, err := dkim.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, selector, domain)
2818 if err != nil {
2819 fmt.Printf("error: %s\n", err)
2820 }
2821 if status != dkim.StatusNeutral {
2822 fmt.Printf("status: %s\n", status)
2823 }
2824 if txt != "" {
2825 fmt.Printf("TXT record: %s\n", txt)
2826 }
2827 if authentic {
2828 fmt.Println("dnssec-signed: yes")
2829 } else {
2830 fmt.Println("dnssec-signed: no")
2831 }
2832 if record != nil {
2833 fmt.Printf("Record:\n")
2834 pairs := []any{
2835 "version", record.Version,
2836 "hashes", record.Hashes,
2837 "key", record.Key,
2838 "notes", record.Notes,
2839 "services", record.Services,
2840 "flags", record.Flags,
2841 }
2842 for i := 0; i < len(pairs); i += 2 {
2843 fmt.Printf("\t%s: %v\n", pairs[i], pairs[i+1])
2844 }
2845 }
2846}
2847
2848func cmdDMARCLookup(c *cmd) {
2849 c.params = "domain"
2850 c.help = "Lookup dmarc policy for domain, a DNS TXT record at _dmarc.<domain>, validate and print it."
2851 args := c.Parse()
2852 if len(args) != 1 {
2853 c.Usage()
2854 }
2855
2856 fromdomain := xparseDomain(args[0], "domain")
2857 _, domain, _, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, fromdomain)
2858 xcheckf(err, "dmarc lookup domain %s", fromdomain)
2859 fmt.Printf("dmarc record at domain %s: %s\n", domain, txt)
2860 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2861}
2862
2863func dnssecStatus(v bool) string {
2864 if v {
2865 return "with dnssec"
2866 }
2867 return "without dnssec"
2868}
2869
2870func cmdDMARCVerify(c *cmd) {
2871 c.params = "remoteip mailfromaddress helodomain < message"
2872 c.help = `Parse an email message and evaluate it against the DMARC policy of the domain in the From-header.
2873
2874mailfromaddress and helodomain are used for SPF validation. If both are empty,
2875SPF validation is skipped.
2876
2877mailfromaddress should be the address used as MAIL FROM in the SMTP session.
2878For DSN messages, that address may be empty. The helo domain was specified at
2879the beginning of the SMTP transaction that delivered the message. These values
2880can be found in message headers.
2881`
2882 args := c.Parse()
2883 if len(args) != 3 {
2884 c.Usage()
2885 }
2886
2887 var heloDomain *dns.Domain
2888
2889 remoteIP := xparseIP(args[0], "remoteip")
2890
2891 var mailfrom *smtp.Address
2892 if args[1] != "" {
2893 a, err := smtp.ParseAddress(args[1])
2894 xcheckf(err, "parsing mailfrom address")
2895 mailfrom = &a
2896 }
2897 if args[2] != "" {
2898 d := xparseDomain(args[2], "helo domain")
2899 heloDomain = &d
2900 }
2901 var received *spf.Received
2902 spfStatus := spf.StatusNone
2903 var spfIdentity *dns.Domain
2904 if mailfrom != nil || heloDomain != nil {
2905 spfArgs := spf.Args{
2906 RemoteIP: remoteIP,
2907 LocalIP: net.ParseIP("127.0.0.1"),
2908 LocalHostname: dns.Domain{ASCII: "localhost"},
2909 }
2910 if mailfrom != nil {
2911 spfArgs.MailFromLocalpart = mailfrom.Localpart
2912 spfArgs.MailFromDomain = mailfrom.Domain
2913 }
2914 if heloDomain != nil {
2915 spfArgs.HelloDomain = dns.IPDomain{Domain: *heloDomain}
2916 }
2917 rspf, spfDomain, expl, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfArgs)
2918 if err != nil {
2919 log.Printf("spf verify: %v (explanation: %q, authentic %v)", err, expl, authentic)
2920 } else {
2921 received = &rspf
2922 spfStatus = received.Result
2923 // todo: should probably potentially do two separate spf validations
2924 if mailfrom != nil {
2925 spfIdentity = &mailfrom.Domain
2926 } else {
2927 spfIdentity = heloDomain
2928 }
2929 fmt.Printf("spf result: %s: %s (%s)\n", spfDomain, spfStatus, dnssecStatus(authentic))
2930 }
2931 }
2932
2933 data, err := io.ReadAll(os.Stdin)
2934 xcheckf(err, "read message")
2935 dmarcFrom, _, _, err := message.From(c.log.Logger, false, bytes.NewReader(data), nil)
2936 xcheckf(err, "extract dmarc from message")
2937
2938 const ignoreTestMode = false
2939 dkimResults, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, true, func(*dkim.Sig) error { return nil }, bytes.NewReader(data), ignoreTestMode)
2940 xcheckf(err, "dkim verify")
2941 for _, r := range dkimResults {
2942 fmt.Printf("dkim result: %q (err %v)\n", r.Status, r.Err)
2943 }
2944
2945 _, result := dmarc.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, dmarcFrom.Domain, dkimResults, spfStatus, spfIdentity, false)
2946 xcheckf(result.Err, "dmarc verify")
2947 fmt.Printf("dmarc from: %s\ndmarc status: %q\ndmarc reject: %v\ncmarc record: %s\n", dmarcFrom, result.Status, result.Reject, result.Record)
2948}
2949
2950func cmdDMARCCheckreportaddrs(c *cmd) {
2951 c.params = "domain"
2952 c.help = `For each reporting address in the domain's DMARC record, check if it has opted into receiving reports (if needed).
2953
2954A DMARC record can request reports about DMARC evaluations to be sent to an
2955email/http address. If the organizational domains of that of the DMARC record
2956and that of the report destination address do not match, the destination
2957address must opt-in to receiving DMARC reports by creating a DMARC record at
2958<dmarcdomain>._report._dmarc.<reportdestdomain>.
2959`
2960 args := c.Parse()
2961 if len(args) != 1 {
2962 c.Usage()
2963 }
2964
2965 dom := xparseDomain(args[0], "domain")
2966 _, domain, record, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dom)
2967 xcheckf(err, "dmarc lookup domain %s", dom)
2968 fmt.Printf("dmarc record at domain %s: %q\n", domain, txt)
2969 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2970
2971 check := func(kind, addr string) {
2972 var authentic bool
2973
2974 printResult := func(format string, args ...any) {
2975 fmt.Printf("%s %s: %s (%s)\n", kind, addr, fmt.Sprintf(format, args...), dnssecStatus(authentic))
2976 }
2977
2978 u, err := url.Parse(addr)
2979 if err != nil {
2980 printResult("parsing uri: %v (skipping)", addr, err)
2981 return
2982 }
2983 var destdom dns.Domain
2984 switch u.Scheme {
2985 case "mailto":
2986 a, err := smtp.ParseAddress(u.Opaque)
2987 if err != nil {
2988 printResult("parsing destination email address %s: %v (skipping)", u.Opaque, err)
2989 return
2990 }
2991 destdom = a.Domain
2992 default:
2993 printResult("unrecognized scheme in reporting address %s (skipping)", u.Scheme)
2994 return
2995 }
2996
2997 if publicsuffix.Lookup(context.Background(), c.log.Logger, dom) == publicsuffix.Lookup(context.Background(), c.log.Logger, destdom) {
2998 printResult("pass (same organizational domain)")
2999 return
3000 }
3001
3002 accepts, status, _, txts, authentic, err := dmarc.LookupExternalReportsAccepted(context.Background(), c.log.Logger, dns.StrictResolver{}, domain, destdom)
3003 var txtstr string
3004 txtaddr := fmt.Sprintf("%s._report._dmarc.%s", domain.ASCII, destdom.ASCII)
3005 if len(txts) == 0 {
3006 txtstr = fmt.Sprintf(" (no txt records %s)", txtaddr)
3007 } else {
3008 txtstr = fmt.Sprintf(" (txt record %s: %q)", txtaddr, txts)
3009 }
3010 if status != dmarc.StatusNone {
3011 printResult("fail: %s%s", err, txtstr)
3012 } else if accepts {
3013 printResult("pass%s", txtstr)
3014 } else if err != nil {
3015 printResult("fail: %s%s", err, txtstr)
3016 } else {
3017 printResult("fail%s", txtstr)
3018 }
3019 }
3020
3021 for _, uri := range record.AggregateReportAddresses {
3022 check("aggregate reporting", uri.Address)
3023 }
3024 for _, uri := range record.FailureReportAddresses {
3025 check("failure reporting", uri.Address)
3026 }
3027}
3028
3029func cmdDMARCParsereportmsg(c *cmd) {
3030 c.params = "message ..."
3031 c.help = `Parse a DMARC report from an email message, and print its extracted details.
3032
3033DMARC reports are periodically mailed, if requested in the DMARC DNS record of
3034a domain. Reports are sent by mail servers that received messages with our
3035domain in a From header. This may or may not be legatimate email. DMARC reports
3036contain summaries of evaluations of DMARC and DKIM/SPF, which can help
3037understand email deliverability problems.
3038`
3039 args := c.Parse()
3040 if len(args) == 0 {
3041 c.Usage()
3042 }
3043
3044 for _, arg := range args {
3045 f, err := os.Open(arg)
3046 xcheckf(err, "open %q", arg)
3047 feedback, err := dmarcrpt.ParseMessageReport(c.log.Logger, f)
3048 xcheckf(err, "parse report in %q", arg)
3049 meta := feedback.ReportMetadata
3050 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)
3051 if len(meta.Errors) > 0 {
3052 fmt.Printf("Errors:\n")
3053 for _, s := range meta.Errors {
3054 fmt.Printf("\t- %s\n", s)
3055 }
3056 }
3057 pol := feedback.PolicyPublished
3058 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)
3059 for _, record := range feedback.Records {
3060 idents := record.Identifiers
3061 fmt.Printf("\theaderfrom %q, envelopes from %q, to %q\n", idents.HeaderFrom, idents.EnvelopeFrom, idents.EnvelopeTo)
3062 eval := record.Row.PolicyEvaluated
3063 var reasons string
3064 for _, reason := range eval.Reasons {
3065 reasons += "; " + string(reason.Type)
3066 if reason.Comment != "" {
3067 reasons += fmt.Sprintf(": %q", reason.Comment)
3068 }
3069 }
3070 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)
3071 for _, dkim := range record.AuthResults.DKIM {
3072 var result string
3073 if dkim.HumanResult != "" {
3074 result = fmt.Sprintf(": %q", dkim.HumanResult)
3075 }
3076 fmt.Printf("\t\tdkim %s; domain %q selector %q%s\n", dkim.Result, dkim.Domain, dkim.Selector, result)
3077 }
3078 for _, spf := range record.AuthResults.SPF {
3079 fmt.Printf("\t\tspf %s; domain %q scope %q\n", spf.Result, spf.Domain, spf.Scope)
3080 }
3081 }
3082 }
3083}
3084
3085func cmdDMARCDBAddReport(c *cmd) {
3086 c.unlisted = true
3087 c.params = "fromdomain < message"
3088 c.help = "Add a DMARC report to the database."
3089 args := c.Parse()
3090 if len(args) != 1 {
3091 c.Usage()
3092 }
3093
3094 mustLoadConfig()
3095
3096 fromdomain := xparseDomain(args[0], "domain")
3097 fmt.Fprintln(os.Stderr, "reading report message from stdin")
3098 report, err := dmarcrpt.ParseMessageReport(c.log.Logger, os.Stdin)
3099 xcheckf(err, "parse message")
3100 err = dmarcdb.AddReport(context.Background(), report, fromdomain)
3101 xcheckf(err, "add dmarc report")
3102}
3103
3104func cmdTLSRPTLookup(c *cmd) {
3105 c.params = "domain"
3106 c.help = `Lookup the TLSRPT record for the domain.
3107
3108A TLSRPT record typically contains an email address where reports about TLS
3109connectivity should be sent. Mail servers attempting delivery to our domain
3110should attempt to use TLS. TLSRPT lets them report how many connection
3111successfully used TLS, and how what kind of errors occurred otherwise.
3112`
3113 args := c.Parse()
3114 if len(args) != 1 {
3115 c.Usage()
3116 }
3117
3118 d := xparseDomain(args[0], "domain")
3119 _, txt, err := tlsrpt.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, d)
3120 xcheckf(err, "tlsrpt lookup for %s", d)
3121 fmt.Println(txt)
3122}
3123
3124func cmdTLSRPTParsereportmsg(c *cmd) {
3125 c.params = "message ..."
3126 c.help = `Parse and print the TLSRPT in the message.
3127
3128The report is printed in formatted JSON.
3129`
3130 args := c.Parse()
3131 if len(args) == 0 {
3132 c.Usage()
3133 }
3134
3135 for _, arg := range args {
3136 f, err := os.Open(arg)
3137 xcheckf(err, "open %q", arg)
3138 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, f)
3139 xcheckf(err, "parse report in %q", arg)
3140 // todo future: only print the highlights?
3141 enc := json.NewEncoder(os.Stdout)
3142 enc.SetIndent("", "\t")
3143 enc.SetEscapeHTML(false)
3144 err = enc.Encode(reportJSON)
3145 xcheckf(err, "write report")
3146 }
3147}
3148
3149func cmdSPFCheck(c *cmd) {
3150 c.params = "domain ip"
3151 c.help = `Check the status of IP for the policy published in DNS for the domain.
3152
3153IPs may be allowed to send for a domain, or disallowed, and several shades in
3154between. If not allowed, an explanation may be provided by the policy. If so,
3155the explanation is printed. The SPF mechanism that matched (if any) is also
3156printed.
3157`
3158 args := c.Parse()
3159 if len(args) != 2 {
3160 c.Usage()
3161 }
3162
3163 domain := xparseDomain(args[0], "domain")
3164
3165 ip := xparseIP(args[1], "ip")
3166
3167 spfargs := spf.Args{
3168 RemoteIP: ip,
3169 MailFromLocalpart: "user",
3170 MailFromDomain: domain,
3171 HelloDomain: dns.IPDomain{Domain: domain},
3172 LocalIP: net.ParseIP("127.0.0.1"),
3173 LocalHostname: dns.Domain{ASCII: "localhost"},
3174 }
3175 r, _, explanation, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfargs)
3176 if err != nil {
3177 fmt.Printf("error: %s\n", err)
3178 }
3179 if explanation != "" {
3180 fmt.Printf("explanation: %s\n", explanation)
3181 }
3182 fmt.Printf("status: %s (%s)\n", r.Result, dnssecStatus(authentic))
3183 if r.Mechanism != "" {
3184 fmt.Printf("mechanism: %s\n", r.Mechanism)
3185 }
3186}
3187
3188func cmdSPFParse(c *cmd) {
3189 c.params = "txtrecord"
3190 c.help = "Parse the record as SPF record. If valid, nothing is printed."
3191 args := c.Parse()
3192 if len(args) != 1 {
3193 c.Usage()
3194 }
3195
3196 _, _, err := spf.ParseRecord(args[0])
3197 xcheckf(err, "parsing record")
3198}
3199
3200func cmdSPFLookup(c *cmd) {
3201 c.params = "domain"
3202 c.help = "Lookup the SPF record for the domain and print it."
3203 args := c.Parse()
3204 if len(args) != 1 {
3205 c.Usage()
3206 }
3207
3208 domain := xparseDomain(args[0], "domain")
3209 _, txt, _, authentic, err := spf.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
3210 xcheckf(err, "spf lookup for %s", domain)
3211 fmt.Println(txt)
3212 fmt.Printf("(%s)\n", dnssecStatus(authentic))
3213}
3214
3215func cmdMTASTSLookup(c *cmd) {
3216 c.params = "domain"
3217 c.help = `Lookup the MTASTS record and policy for the domain.
3218
3219MTA-STS is a mechanism for a domain to specify if it requires TLS connections
3220for delivering email. If a domain has a valid MTA-STS DNS TXT record at
3221_mta-sts.<domain> it signals it implements MTA-STS. A policy can then be
3222fetched at https://mta-sts.<domain>/.well-known/mta-sts.txt. The policy
3223specifies the mode (enforce, testing, none), which MX servers support TLS and
3224should be used, and how long the policy can be cached.
3225`
3226 args := c.Parse()
3227 if len(args) != 1 {
3228 c.Usage()
3229 }
3230
3231 domain := xparseDomain(args[0], "domain")
3232
3233 record, policy, _, err := mtasts.Get(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
3234 if err != nil {
3235 fmt.Printf("error: %s\n", err)
3236 }
3237 if record != nil {
3238 fmt.Printf("DNS TXT record _mta-sts.%s: %s\n", domain.ASCII, record.String())
3239 }
3240 if policy != nil {
3241 fmt.Println("")
3242 fmt.Printf("policy at https://mta-sts.%s/.well-known/mta-sts.txt:\n", domain.ASCII)
3243 fmt.Printf("%s", policy.String())
3244 }
3245}
3246
3247func cmdRDAPDomainage(c *cmd) {
3248 c.params = "domain"
3249 c.help = `Lookup the age of domain in RDAP based on latest registration.
3250
3251RDAP is the registration data access protocol. Registries run RDAP services for
3252their top level domains, providing information such as the registration date of
3253domains. This command looks up the "age" of a domain by looking at the most
3254recent "registration", "reregistration" or "reinstantiation" event.
3255
3256Email messages from recently registered domains are often treated with
3257suspicion, and some mail systems are more likely to classify them as junk.
3258
3259On each invocation, a bootstrap file with a list of registries (of top-level
3260domains) is retrieved, without caching. Do not run this command too often with
3261automation.
3262`
3263 args := c.Parse()
3264 if len(args) != 1 {
3265 c.Usage()
3266 }
3267
3268 domain := xparseDomain(args[0], "domain")
3269
3270 registration, err := rdap.LookupLastDomainRegistration(context.Background(), c.log, domain)
3271 xcheckf(err, "looking up domain in rdap")
3272
3273 age := time.Since(registration)
3274 const day = 24 * time.Hour
3275 const year = 365 * day
3276 years := age / year
3277 days := (age - years*year) / day
3278 var s string
3279 if years == 1 {
3280 s = "1 year, "
3281 } else if years > 0 {
3282 s = fmt.Sprintf("%d years, ", years)
3283 }
3284 if days == 1 {
3285 s += "1 day"
3286 } else {
3287 s += fmt.Sprintf("%d days", days)
3288 }
3289 fmt.Println(s)
3290}
3291
3292func cmdRetrain(c *cmd) {
3293 c.params = "[accountname]"
3294 c.help = `Recreate and retrain the junk filter for the account or all accounts.
3295
3296Useful after having made changes to the junk filter configuration, or if the
3297implementation has changed.
3298`
3299 args := c.Parse()
3300 if len(args) > 1 {
3301 c.Usage()
3302 }
3303 var account string
3304 if len(args) == 1 {
3305 account = args[0]
3306 }
3307
3308 mustLoadConfig()
3309 ctlcmdRetrain(xctl(), account)
3310}
3311
3312func ctlcmdRetrain(ctl *ctl, account string) {
3313 ctl.xwrite("retrain")
3314 ctl.xwrite(account)
3315 ctl.xreadok()
3316}
3317
3318func cmdTLSRPTDBAddReport(c *cmd) {
3319 c.unlisted = true
3320 c.params = "< message"
3321 c.help = "Parse a TLS report from the message and add it to the database."
3322 var hostReport bool
3323 c.flag.BoolVar(&hostReport, "hostreport", false, "report for a host instead of domain")
3324 args := c.Parse()
3325 if len(args) != 0 {
3326 c.Usage()
3327 }
3328
3329 mustLoadConfig()
3330
3331 // First read message, to get the From-header. Then parse it as TLSRPT.
3332 fmt.Fprintln(os.Stderr, "reading report message from stdin")
3333 buf, err := io.ReadAll(os.Stdin)
3334 xcheckf(err, "reading message")
3335 part, err := message.Parse(c.log.Logger, true, bytes.NewReader(buf))
3336 xcheckf(err, "parsing message")
3337 if part.Envelope == nil || len(part.Envelope.From) != 1 {
3338 log.Fatalf("message must have one From-header")
3339 }
3340 from := part.Envelope.From[0]
3341 domain := xparseDomain(from.Host, "domain")
3342
3343 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, bytes.NewReader(buf))
3344 xcheckf(err, "parsing tls report in message")
3345
3346 mailfrom := from.User + "@" + from.Host // todo future: should escape and such
3347 report := reportJSON.Convert()
3348 err = tlsrptdb.AddReport(context.Background(), c.log, domain, mailfrom, hostReport, &report)
3349 xcheckf(err, "add tls report to database")
3350}
3351
3352func cmdDNSBLCheck(c *cmd) {
3353 c.params = "zone ip"
3354 c.help = `Test if IP is in the DNS blocklist of the zone, e.g. bl.spamcop.net.
3355
3356If the IP is in the blocklist, an explanation is printed. This is typically a
3357URL with more information.
3358`
3359 args := c.Parse()
3360 if len(args) != 2 {
3361 c.Usage()
3362 }
3363
3364 zone := xparseDomain(args[0], "zone")
3365 ip := xparseIP(args[1], "ip")
3366
3367 status, explanation, err := dnsbl.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, zone, ip)
3368 fmt.Printf("status: %s\n", status)
3369 if status == dnsbl.StatusFail {
3370 fmt.Printf("explanation: %q\n", explanation)
3371 }
3372 if err != nil {
3373 fmt.Printf("error: %s\n", err)
3374 }
3375}
3376
3377func cmdDNSBLCheckhealth(c *cmd) {
3378 c.params = "zone"
3379 c.help = `Check the health of the DNS blocklist represented by zone, e.g. bl.spamcop.net.
3380
3381The health of a DNS blocklist can be checked by querying for 127.0.0.1 and
3382127.0.0.2. The second must and the first must not be present.
3383`
3384 args := c.Parse()
3385 if len(args) != 1 {
3386 c.Usage()
3387 }
3388
3389 zone := xparseDomain(args[0], "zone")
3390 err := dnsbl.CheckHealth(context.Background(), c.log.Logger, dns.StrictResolver{}, zone)
3391 xcheckf(err, "unhealthy")
3392 fmt.Println("healthy")
3393}
3394
3395func cmdCheckupdate(c *cmd) {
3396 c.help = `Check if a newer version of mox is available.
3397
3398A single DNS TXT lookup to _updates.xmox.nl tells if a new version is
3399available. If so, a changelog is fetched from https://updates.xmox.nl, and the
3400individual entries verified with a builtin public key. The changelog is
3401printed.
3402`
3403 if len(c.Parse()) != 0 {
3404 c.Usage()
3405 }
3406 mustLoadConfig()
3407
3408 current, lastknown, _, err := store.LastKnown()
3409 if err != nil {
3410 log.Printf("getting last known version: %s", err)
3411 } else {
3412 fmt.Printf("last known version: %s\n", lastknown)
3413 fmt.Printf("current version: %s\n", current)
3414 }
3415 latest, _, err := updates.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dns.Domain{ASCII: changelogDomain})
3416 xcheckf(err, "lookup of latest version")
3417 fmt.Printf("latest version: %s\n", latest)
3418
3419 if latest.After(current) {
3420 changelog, err := updates.FetchChangelog(context.Background(), c.log.Logger, changelogURL, current, changelogPubKey)
3421 xcheckf(err, "fetching changelog")
3422 if len(changelog.Changes) == 0 {
3423 log.Printf("no changes in changelog")
3424 return
3425 }
3426 fmt.Println("Changelog")
3427 for _, c := range changelog.Changes {
3428 fmt.Println("\n" + strings.TrimSpace(c.Text))
3429 }
3430 }
3431}
3432
3433func cmdCid(c *cmd) {
3434 c.params = "cid"
3435 c.help = `Turn an ID from a Received header into a cid, for looking up in logs.
3436
3437A cid is essentially a connection counter initialized when mox starts. Each log
3438line contains a cid. Received headers added by mox contain a unique ID that can
3439be decrypted to a cid by admin of a mox instance only.
3440`
3441 args := c.Parse()
3442 if len(args) != 1 {
3443 c.Usage()
3444 }
3445
3446 mustLoadConfig()
3447 recvidpath := mox.DataDirPath("receivedid.key")
3448 recvidbuf, err := os.ReadFile(recvidpath)
3449 xcheckf(err, "reading %s", recvidpath)
3450 if len(recvidbuf) != 16+8 {
3451 log.Fatalf("bad data in %s: got %d bytes, expect 16+8=24", recvidpath, len(recvidbuf))
3452 }
3453 err = mox.ReceivedIDInit(recvidbuf[:16], recvidbuf[16:])
3454 xcheckf(err, "init receivedid")
3455
3456 cid, err := mox.ReceivedToCid(args[0])
3457 xcheckf(err, "received id to cid")
3458 fmt.Printf("%x\n", cid)
3459}
3460
3461func cmdVersion(c *cmd) {
3462 c.help = "Prints this mox version."
3463 if len(c.Parse()) != 0 {
3464 c.Usage()
3465 }
3466 fmt.Println(moxvar.Version)
3467 fmt.Printf("%s/%s\n", runtime.GOOS, runtime.GOARCH)
3468}
3469
3470func cmdWebapi(c *cmd) {
3471 c.params = "[method [baseurl-with-credentials]"
3472 c.help = "Lists available methods, prints request/response parameters for method, or calls a method with a request read from standard input."
3473 args := c.Parse()
3474 if len(args) > 2 {
3475 c.Usage()
3476 }
3477
3478 t := reflect.TypeFor[webapi.Methods]()
3479 methods := map[string]reflect.Type{}
3480 var ml []string
3481 for i := range t.NumMethod() {
3482 mt := t.Method(i)
3483 methods[mt.Name] = mt.Type
3484 ml = append(ml, mt.Name)
3485 }
3486
3487 if len(args) == 0 {
3488 fmt.Println(strings.Join(ml, "\n"))
3489 return
3490 }
3491
3492 mt, ok := methods[args[0]]
3493 if !ok {
3494 log.Fatalf("unknown method %q", args[0])
3495 }
3496 resultNotJSON := mt.Out(0).Kind() == reflect.Interface
3497
3498 if len(args) == 1 {
3499 fmt.Println("# Example request")
3500 fmt.Println()
3501 printJSON("\t", mox.FillExample(nil, reflect.New(mt.In(1))).Interface())
3502 fmt.Println()
3503 if resultNotJSON {
3504 fmt.Println("Output is non-JSON data.")
3505 return
3506 }
3507 fmt.Println("# Example response")
3508 fmt.Println()
3509 printJSON("\t", mox.FillExample(nil, reflect.New(mt.Out(0))).Interface())
3510 return
3511 }
3512
3513 var response any
3514 if !resultNotJSON {
3515 response = reflect.New(mt.Out(0))
3516 }
3517
3518 fmt.Fprintln(os.Stderr, "reading request from stdin...")
3519 request, err := io.ReadAll(os.Stdin)
3520 xcheckf(err, "read message")
3521
3522 dec := json.NewDecoder(bytes.NewReader(request))
3523 dec.DisallowUnknownFields()
3524 err = dec.Decode(reflect.New(mt.In(1)).Interface())
3525 xcheckf(err, "parsing request")
3526
3527 resp, err := http.PostForm(args[1]+args[0], url.Values{"request": []string{string(request)}})
3528 xcheckf(err, "http post")
3529 defer func() {
3530 if err := resp.Body.Close(); err != nil {
3531 log.Printf("closing http response body: %v", err)
3532 }
3533 }()
3534 if resp.StatusCode == http.StatusBadRequest {
3535 buf, err := io.ReadAll(&moxio.LimitReader{R: resp.Body, Limit: 10 * 1024})
3536 xcheckf(err, "reading response for 400 bad request error")
3537 err = json.Unmarshal(buf, &response)
3538 if err == nil {
3539 printJSON("", response)
3540 } else {
3541 fmt.Fprintf(os.Stderr, "(not json)\n")
3542 os.Stderr.Write(buf)
3543 }
3544 os.Exit(1)
3545 } else if resp.StatusCode != http.StatusOK {
3546 fmt.Fprintf(os.Stderr, "http response %s\n", resp.Status)
3547 _, err := io.Copy(os.Stderr, resp.Body)
3548 xcheckf(err, "copy body")
3549 } else {
3550 err := json.NewDecoder(resp.Body).Decode(&resp)
3551 xcheckf(err, "unmarshal response")
3552 printJSON("", response)
3553 }
3554}
3555
3556func printJSON(indent string, v any) {
3557 fmt.Printf("%s", indent)
3558 enc := json.NewEncoder(os.Stdout)
3559 enc.SetIndent(indent, "\t")
3560 enc.SetEscapeHTML(false)
3561 err := enc.Encode(v)
3562 xcheckf(err, "encode json")
3563}
3564
3565// 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.
3566func cmdBumpUIDValidity(c *cmd) {
3567 c.params = "account [mailbox]"
3568 c.help = `Change the IMAP UID validity of the mailbox, causing IMAP clients to refetch messages.
3569
3570This can be useful after manually repairing metadata about the account/mailbox.
3571
3572Opens account database file directly. Ensure mox does not have the account
3573open, or is not running.
3574`
3575 args := c.Parse()
3576 if len(args) != 1 && len(args) != 2 {
3577 c.Usage()
3578 }
3579
3580 mustLoadConfig()
3581 a, err := store.OpenAccount(c.log, args[0], false)
3582 xcheckf(err, "open account")
3583 defer func() {
3584 if err := a.Close(); err != nil {
3585 log.Printf("closing account: %v", err)
3586 }
3587 }()
3588
3589 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3590 uidvalidity, err := a.NextUIDValidity(tx)
3591 if err != nil {
3592 return fmt.Errorf("assigning next uid validity: %v", err)
3593 }
3594
3595 q := bstore.QueryTx[store.Mailbox](tx)
3596 q.FilterEqual("Expunged", false)
3597 if len(args) == 2 {
3598 q.FilterEqual("Name", args[1])
3599 }
3600 mbl, err := q.SortAsc("Name").List()
3601 if err != nil {
3602 return fmt.Errorf("looking up mailbox: %v", err)
3603 }
3604 if len(args) == 2 && len(mbl) != 1 {
3605 return fmt.Errorf("looking up mailbox %q, found %d mailboxes", args[1], len(mbl))
3606 }
3607 for _, mb := range mbl {
3608 mb.UIDValidity = uidvalidity
3609 err = tx.Update(&mb)
3610 if err != nil {
3611 return fmt.Errorf("updating uid validity for mailbox: %v", err)
3612 }
3613 fmt.Printf("uid validity for %q updated to %d\n", mb.Name, uidvalidity)
3614 }
3615 return nil
3616 })
3617 xcheckf(err, "updating database")
3618}
3619
3620func cmdReassignUIDs(c *cmd) {
3621 c.params = "account [mailboxid]"
3622 c.help = `Reassign UIDs in one mailbox or all mailboxes in an account and bump UID validity, causing IMAP clients to refetch messages.
3623
3624Opens account database file directly. Ensure mox does not have the account
3625open, or is not running.
3626`
3627 args := c.Parse()
3628 if len(args) != 1 && len(args) != 2 {
3629 c.Usage()
3630 }
3631
3632 var mailboxID int64
3633 if len(args) == 2 {
3634 var err error
3635 mailboxID, err = strconv.ParseInt(args[1], 10, 64)
3636 xcheckf(err, "parsing mailbox id")
3637 }
3638
3639 mustLoadConfig()
3640 a, err := store.OpenAccount(c.log, args[0], false)
3641 xcheckf(err, "open account")
3642 defer func() {
3643 if err := a.Close(); err != nil {
3644 log.Printf("closing account: %v", err)
3645 }
3646 }()
3647
3648 // Gather the last-assigned UIDs per mailbox.
3649 uidlasts := map[int64]store.UID{}
3650
3651 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3652 // Reassign UIDs, going per mailbox. We assign starting at 1, only changing the
3653 // message if it isn't already at the intended UID. Doing it in this order ensures
3654 // we don't get into trouble with duplicate UIDs for a mailbox. We assign a new
3655 // modseq. Not strictly needed, but doesn't hurt. It's also why we assign a UID to
3656 // expunged messages.
3657 modseq, err := a.NextModSeq(tx)
3658 xcheckf(err, "assigning next modseq")
3659
3660 q := bstore.QueryTx[store.Message](tx)
3661 if len(args) == 2 {
3662 q.FilterNonzero(store.Message{MailboxID: mailboxID})
3663 }
3664 q.SortAsc("MailboxID", "UID")
3665 err = q.ForEach(func(m store.Message) error {
3666 uidlasts[m.MailboxID]++
3667 uid := uidlasts[m.MailboxID]
3668 if m.UID != uid {
3669 m.UID = uid
3670 m.ModSeq = modseq
3671 if err := tx.Update(&m); err != nil {
3672 return fmt.Errorf("updating uid for message: %v", err)
3673 }
3674 }
3675 return nil
3676 })
3677 if err != nil {
3678 return fmt.Errorf("reading through messages: %v", err)
3679 }
3680
3681 // Now update the uidnext, uidvalidity and modseq for each mailbox.
3682 err = bstore.QueryTx[store.Mailbox](tx).FilterEqual("Expunged", false).ForEach(func(mb store.Mailbox) error {
3683 // Assign each mailbox a completely new uidvalidity.
3684 uidvalidity, err := a.NextUIDValidity(tx)
3685 if err != nil {
3686 return fmt.Errorf("assigning next uid validity: %v", err)
3687 }
3688
3689 if mb.UIDValidity >= uidvalidity {
3690 // This should not happen, but since we're fixing things up after a hypothetical
3691 // mishap, might as well account for inconsistent uidvalidity.
3692 next := store.NextUIDValidity{ID: 1, Next: mb.UIDValidity + 2}
3693 if err := tx.Update(&next); err != nil {
3694 log.Printf("updating nextuidvalidity: %v, continuing", err)
3695 }
3696 mb.UIDValidity++
3697 } else {
3698 mb.UIDValidity = uidvalidity
3699 }
3700 mb.UIDNext = uidlasts[mb.ID] + 1
3701 mb.ModSeq = modseq
3702 if err := tx.Update(&mb); err != nil {
3703 return fmt.Errorf("updating uidvalidity and uidnext for mailbox: %v", err)
3704 }
3705 return nil
3706 })
3707 if err != nil {
3708 return fmt.Errorf("updating mailboxes: %v", err)
3709 }
3710 return nil
3711 })
3712 xcheckf(err, "updating database")
3713}
3714
3715func cmdFixUIDMeta(c *cmd) {
3716 c.params = "account"
3717 c.help = `Fix inconsistent UIDVALIDITY and UIDNEXT in messages/mailboxes/account.
3718
3719The next UID to use for a message in a mailbox should always be higher than any
3720existing message UID in the mailbox. If it is not, the mailbox UIDNEXT is
3721updated.
3722
3723Each mailbox has a UIDVALIDITY sequence number, which should always be lower
3724than the per-account next UIDVALIDITY to use. If it is not, the account next
3725UIDVALIDITY is updated.
3726
3727Opens account database file directly. Ensure mox does not have the account
3728open, or is not running.
3729`
3730 args := c.Parse()
3731 if len(args) != 1 {
3732 c.Usage()
3733 }
3734
3735 mustLoadConfig()
3736 a, err := store.OpenAccount(c.log, args[0], false)
3737 xcheckf(err, "open account")
3738 defer func() {
3739 if err := a.Close(); err != nil {
3740 log.Printf("closing account: %v", err)
3741 }
3742 }()
3743
3744 var maxUIDValidity uint32
3745
3746 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3747 // We look at each mailbox, retrieve its max UID and compare against the mailbox
3748 // UIDNEXT.
3749 err := bstore.QueryTx[store.Mailbox](tx).FilterEqual("Expunged", false).ForEach(func(mb store.Mailbox) error {
3750 if mb.UIDValidity > maxUIDValidity {
3751 maxUIDValidity = mb.UIDValidity
3752 }
3753 m, err := bstore.QueryTx[store.Message](tx).FilterNonzero(store.Message{MailboxID: mb.ID}).SortDesc("UID").Limit(1).Get()
3754 if err == bstore.ErrAbsent || err == nil && m.UID < mb.UIDNext {
3755 return nil
3756 } else if err != nil {
3757 return fmt.Errorf("finding message with max uid in mailbox: %w", err)
3758 }
3759 olduidnext := mb.UIDNext
3760 mb.UIDNext = m.UID + 1
3761 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)
3762 if err := tx.Update(&mb); err != nil {
3763 return fmt.Errorf("updating mailbox uidnext: %v", err)
3764 }
3765 return nil
3766 })
3767 if err != nil {
3768 return fmt.Errorf("processing mailboxes: %v", err)
3769 }
3770
3771 uidvalidity := store.NextUIDValidity{ID: 1}
3772 if err := tx.Get(&uidvalidity); err != nil {
3773 return fmt.Errorf("reading account next uidvalidity: %v", err)
3774 }
3775 if maxUIDValidity >= uidvalidity.Next {
3776 log.Printf("account next uidvalidity %d <= highest uidvalidity %d found in mailbox, resetting account next uidvalidity to %d", uidvalidity.Next, maxUIDValidity, maxUIDValidity+1)
3777 uidvalidity.Next = maxUIDValidity + 1
3778 if err := tx.Update(&uidvalidity); err != nil {
3779 return fmt.Errorf("updating account next uidvalidity: %v", err)
3780 }
3781 }
3782
3783 return nil
3784 })
3785 xcheckf(err, "updating database")
3786}
3787
3788func cmdFixmsgsize(c *cmd) {
3789 c.params = "[account]"
3790 c.help = `Ensure message sizes in the database matching the sum of the message prefix length and on-disk file size.
3791
3792Messages with an inconsistent size are also parsed again.
3793
3794If an inconsistency is found, you should probably also run "mox
3795bumpuidvalidity" on the mailboxes or entire account to force IMAP clients to
3796refetch messages.
3797`
3798 args := c.Parse()
3799 if len(args) > 1 {
3800 c.Usage()
3801 }
3802
3803 mustLoadConfig()
3804 var account string
3805 if len(args) == 1 {
3806 account = args[0]
3807 }
3808 ctlcmdFixmsgsize(xctl(), account)
3809}
3810
3811func ctlcmdFixmsgsize(ctl *ctl, account string) {
3812 ctl.xwrite("fixmsgsize")
3813 ctl.xwrite(account)
3814 ctl.xreadok()
3815 ctl.xstreamto(os.Stdout)
3816}
3817
3818func cmdReparse(c *cmd) {
3819 c.params = "[account]"
3820 c.help = `Parse all messages in the account or all accounts again.
3821
3822Can be useful after upgrading mox with improved message parsing. Messages are
3823parsed in batches, so other access to the mailboxes/messages are not blocked
3824while reparsing all messages.
3825`
3826 args := c.Parse()
3827 if len(args) > 1 {
3828 c.Usage()
3829 }
3830
3831 mustLoadConfig()
3832 var account string
3833 if len(args) == 1 {
3834 account = args[0]
3835 }
3836 ctlcmdReparse(xctl(), account)
3837}
3838
3839func ctlcmdReparse(ctl *ctl, account string) {
3840 ctl.xwrite("reparse")
3841 ctl.xwrite(account)
3842 ctl.xreadok()
3843 ctl.xstreamto(os.Stdout)
3844}
3845
3846func cmdEnsureParsed(c *cmd) {
3847 c.params = "account"
3848 c.help = "Ensure messages in the database have a pre-parsed MIME form in the database."
3849 var all bool
3850 c.flag.BoolVar(&all, "all", false, "store new parsed message for all messages")
3851 args := c.Parse()
3852 if len(args) != 1 {
3853 c.Usage()
3854 }
3855
3856 mustLoadConfig()
3857 a, err := store.OpenAccount(c.log, args[0], false)
3858 xcheckf(err, "open account")
3859 defer func() {
3860 if err := a.Close(); err != nil {
3861 log.Printf("closing account: %v", err)
3862 }
3863 }()
3864
3865 n := 0
3866 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3867 q := bstore.QueryTx[store.Message](tx)
3868 q.FilterEqual("Expunged", false)
3869 q.FilterFn(func(m store.Message) bool {
3870 return all || m.ParsedBuf == nil
3871 })
3872 l, err := q.List()
3873 if err != nil {
3874 return fmt.Errorf("list messages: %v", err)
3875 }
3876 for _, m := range l {
3877 mr := a.MessageReader(m)
3878 p, err := message.EnsurePart(c.log.Logger, false, mr, m.Size)
3879 if err != nil {
3880 log.Printf("parsing message %d: %v (continuing)", m.ID, err)
3881 }
3882 m.ParsedBuf, err = json.Marshal(p)
3883 if err != nil {
3884 return fmt.Errorf("marshal parsed message: %v", err)
3885 }
3886 if err := tx.Update(&m); err != nil {
3887 return fmt.Errorf("update message: %v", err)
3888 }
3889 n++
3890 }
3891 return nil
3892 })
3893 xcheckf(err, "update messages with parsed mime structure")
3894 fmt.Printf("%d messages updated\n", n)
3895}
3896
3897func cmdRecalculateMailboxCounts(c *cmd) {
3898 c.params = "account"
3899 c.help = `Recalculate message counts for all mailboxes in the account, and total message size for quota.
3900
3901When a message is added to/removed from a mailbox, or when message flags change,
3902the total, unread, unseen and deleted messages are accounted, the total size of
3903the mailbox, and the total message size for the account. In case of a bug in
3904this accounting, the numbers could become incorrect. This command will find, fix
3905and print them.
3906`
3907 args := c.Parse()
3908 if len(args) != 1 {
3909 c.Usage()
3910 }
3911
3912 mustLoadConfig()
3913 ctlcmdRecalculateMailboxCounts(xctl(), args[0])
3914}
3915
3916func ctlcmdRecalculateMailboxCounts(ctl *ctl, account string) {
3917 ctl.xwrite("recalculatemailboxcounts")
3918 ctl.xwrite(account)
3919 ctl.xreadok()
3920 ctl.xstreamto(os.Stdout)
3921}
3922
3923func cmdMessageParse(c *cmd) {
3924 c.params = "message.eml"
3925 c.help = "Parse message, print JSON representation."
3926
3927 var smtputf8 bool
3928 c.flag.BoolVar(&smtputf8, "smtputf8", false, "check if message needs smtputf8")
3929 args := c.Parse()
3930 if len(args) != 1 {
3931 c.Usage()
3932 }
3933
3934 f, err := os.Open(args[0])
3935 xcheckf(err, "open")
3936 defer func() {
3937 if err := f.Close(); err != nil {
3938 log.Printf("closing message file: %v", err)
3939 }
3940 }()
3941
3942 part, err := message.Parse(c.log.Logger, false, f)
3943 xcheckf(err, "parsing message")
3944 err = part.Walk(c.log.Logger, nil)
3945 xcheckf(err, "parsing nested parts")
3946 enc := json.NewEncoder(os.Stdout)
3947 enc.SetIndent("", "\t")
3948 enc.SetEscapeHTML(false)
3949 err = enc.Encode(part)
3950 xcheckf(err, "write")
3951
3952 if smtputf8 {
3953 needs, err := part.NeedsSMTPUTF8()
3954 xcheckf(err, "checking if message needs smtputf8")
3955 fmt.Println("message needs smtputf8:", needs)
3956 }
3957}
3958
3959func cmdOpenaccounts(c *cmd) {
3960 c.unlisted = true
3961 c.params = "datadir account ..."
3962 c.help = `Open and close accounts, for triggering data upgrades, for tests.
3963
3964Opens database files directly, not going through a running mox instance.
3965`
3966
3967 args := c.Parse()
3968 if len(args) <= 1 {
3969 c.Usage()
3970 }
3971
3972 dataDir := filepath.Clean(args[0])
3973 for _, accName := range args[1:] {
3974 accDir := filepath.Join(dataDir, "accounts", accName)
3975 log.Printf("opening account %s...", accDir)
3976 a, err := store.OpenAccountDB(c.log, accDir, accName)
3977 xcheckf(err, "open account %s", accName)
3978 err = a.ThreadingWait(c.log)
3979 xcheckf(err, "wait for threading upgrade to complete for %s", accName)
3980 err = a.Close()
3981 xcheckf(err, "close account %s", accName)
3982 }
3983}
3984
3985func cmdReassignthreads(c *cmd) {
3986 c.params = "[account]"
3987 c.help = `Reassign message threads.
3988
3989For all accounts, or optionally only the specified account.
3990
3991Threading for all messages in an account is first reset, and new base subject
3992and normalized message-id saved with the message. Then all messages are
3993evaluated and matched against their parents/ancestors.
3994
3995Messages are matched based on the References header, with a fall-back to an
3996In-Reply-To header, and if neither is present/valid, based only on base
3997subject.
3998
3999A References header typically points to multiple previous messages in a
4000hierarchy. From oldest ancestor to most recent parent. An In-Reply-To header
4001would have only a message-id of the parent message.
4002
4003A message is only linked to a parent/ancestor if their base subject is the
4004same. This ensures unrelated replies, with a new subject, are placed in their
4005own thread.
4006
4007The base subject is lower cased, has whitespace collapsed to a single
4008space, and some components removed: leading "Re:", "Fwd:", "Fw:", or bracketed
4009tag (that mailing lists often add, e.g. "[listname]"), trailing "(fwd)", or
4010enclosing "[fwd: ...]".
4011
4012Messages are linked to all their ancestors. If an intermediate parent/ancestor
4013message is deleted in the future, the message can still be linked to the earlier
4014ancestors. If the direct parent already wasn't available while matching, this is
4015stored as the message having a "missing link" to its stored ancestors.
4016`
4017 args := c.Parse()
4018 if len(args) > 1 {
4019 c.Usage()
4020 }
4021
4022 mustLoadConfig()
4023 var account string
4024 if len(args) == 1 {
4025 account = args[0]
4026 }
4027 ctlcmdReassignthreads(xctl(), account)
4028}
4029
4030func ctlcmdReassignthreads(ctl *ctl, account string) {
4031 ctl.xwrite("reassignthreads")
4032 ctl.xwrite(account)
4033 ctl.xreadok()
4034 ctl.xstreamto(os.Stdout)
4035}
4036
4037func cmdIMAPServe(c *cmd) {
4038 c.params = "preauth-address"
4039 c.help = `Initiate a preauthenticated IMAP connection on file descriptor 0.
4040
4041For use with tools that can do IMAP over tunneled connections, e.g. with SSH
4042during migrations. TLS is not possible on the connection, and authentication
4043does not require TLS.
4044`
4045 var fd0 bool
4046 c.flag.BoolVar(&fd0, "fd0", false, "write IMAP to file descriptor 0 instead of stdout")
4047 args := c.Parse()
4048 if len(args) != 1 {
4049 c.Usage()
4050 }
4051
4052 address := args[0]
4053 output := os.Stdout
4054 if fd0 {
4055 output = os.Stdout
4056 }
4057 ctlcmdIMAPServe(xctl(), address, os.Stdin, output)
4058}
4059
4060func ctlcmdIMAPServe(ctl *ctl, address string, input io.ReadCloser, output io.WriteCloser) {
4061 ctl.xwrite("imapserve")
4062 ctl.xwrite(address)
4063 ctl.xreadok()
4064
4065 done := make(chan struct{}, 1)
4066 go func() {
4067 defer func() {
4068 done <- struct{}{}
4069 }()
4070 _, err := io.Copy(output, ctl.conn)
4071 if err == nil {
4072 err = io.EOF
4073 }
4074 log.Printf("reading from imap: %v", err)
4075 }()
4076 go func() {
4077 defer func() {
4078 done <- struct{}{}
4079 }()
4080 _, err := io.Copy(ctl.conn, input)
4081 if err == nil {
4082 err = io.EOF
4083 }
4084 log.Printf("writing to imap: %v", err)
4085 }()
4086 <-done
4087}
4088
4089func cmdReadmessages(c *cmd) {
4090 c.unlisted = true
4091 c.params = "datadir account ..."
4092 c.help = `Open account, parse several headers for all messages.
4093
4094For performance testing.
4095
4096Opens database files directly, not going through a running mox instance.
4097`
4098
4099 gomaxprocs := runtime.GOMAXPROCS(0)
4100 var procs, workqueuesize, limit int
4101 c.flag.IntVar(&procs, "procs", gomaxprocs, "number of goroutines for reading messages")
4102 c.flag.IntVar(&workqueuesize, "workqueuesize", 2*gomaxprocs, "number of messages to keep in work queue")
4103 c.flag.IntVar(&limit, "limit", 0, "number of messages to process if greater than zero")
4104 args := c.Parse()
4105 if len(args) <= 1 {
4106 c.Usage()
4107 }
4108
4109 type threadPrep struct {
4110 references []string
4111 inReplyTo []string
4112 }
4113
4114 threadingFields := [][]byte{
4115 []byte("references"),
4116 []byte("in-reply-to"),
4117 }
4118
4119 dataDir := filepath.Clean(args[0])
4120 for _, accName := range args[1:] {
4121 accDir := filepath.Join(dataDir, "accounts", accName)
4122 log.Printf("opening account %s...", accDir)
4123 a, err := store.OpenAccountDB(c.log, accDir, accName)
4124 xcheckf(err, "open account %s", accName)
4125
4126 prepareMessages := func(in, out chan moxio.Work[store.Message, threadPrep]) {
4127 headerbuf := make([]byte, 8*1024)
4128 scratch := make([]byte, 4*1024)
4129 for {
4130 w, ok := <-in
4131 if !ok {
4132 return
4133 }
4134
4135 m := w.In
4136 var partialPart struct {
4137 HeaderOffset int64
4138 BodyOffset int64
4139 }
4140 if err := json.Unmarshal(m.ParsedBuf, &partialPart); err != nil {
4141 w.Err = fmt.Errorf("unmarshal part: %v", err)
4142 } else {
4143 size := partialPart.BodyOffset - partialPart.HeaderOffset
4144 if int(size) > len(headerbuf) {
4145 headerbuf = make([]byte, size)
4146 }
4147 if size > 0 {
4148 buf := headerbuf[:int(size)]
4149 err := func() error {
4150 mr := a.MessageReader(m)
4151 defer func() {
4152 if err := mr.Close(); err != nil {
4153 log.Printf("closing message reader: %v", err)
4154 }
4155 }()
4156
4157 // ReadAt returns whole buffer or error. Single read should be fast.
4158 n, err := mr.ReadAt(buf, partialPart.HeaderOffset)
4159 if err != nil || n != len(buf) {
4160 return fmt.Errorf("read header: %v", err)
4161 }
4162 return nil
4163 }()
4164 if err != nil {
4165 w.Err = err
4166 } else if h, err := message.ParseHeaderFields(buf, scratch, threadingFields); err != nil {
4167 w.Err = err
4168 } else {
4169 w.Out.references = h["References"]
4170 w.Out.inReplyTo = h["In-Reply-To"]
4171 }
4172 }
4173 }
4174
4175 out <- w
4176 }
4177 }
4178
4179 n := 0
4180 t := time.Now()
4181 t0 := t
4182
4183 processMessage := func(m store.Message, prep threadPrep) error {
4184 if n%100000 == 0 {
4185 log.Printf("%d messages (delta %s)", n, time.Since(t))
4186 t = time.Now()
4187 }
4188 n++
4189 return nil
4190 }
4191
4192 wq := moxio.NewWorkQueue(procs, workqueuesize, prepareMessages, processMessage)
4193
4194 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
4195 q := bstore.QueryTx[store.Message](tx)
4196 q.FilterEqual("Expunged", false)
4197 q.SortAsc("ID")
4198 if limit > 0 {
4199 q.Limit(limit)
4200 }
4201 err = q.ForEach(wq.Add)
4202 if err == nil {
4203 err = wq.Finish()
4204 }
4205 wq.Stop()
4206
4207 return err
4208 })
4209 xcheckf(err, "processing message")
4210
4211 err = a.Close()
4212 xcheckf(err, "close account %s", accName)
4213 log.Printf("account %s, total time %s", accName, time.Since(t0))
4214 }
4215}
4216
4217func cmdQueueFillRetired(c *cmd) {
4218 c.unlisted = true
4219 c.help = `Fill retired messag and webhooks queue with testdata.
4220
4221For testing the pagination. Operates directly on queue database.
4222`
4223 var n int
4224 c.flag.IntVar(&n, "n", 10000, "retired messages and retired webhooks to insert")
4225 args := c.Parse()
4226 if len(args) != 0 {
4227 c.Usage()
4228 }
4229
4230 mustLoadConfig()
4231 err := queue.Init()
4232 xcheckf(err, "init queue")
4233 err = queue.DB.Write(context.Background(), func(tx *bstore.Tx) error {
4234 now := time.Now()
4235
4236 // Cause autoincrement ID for queue.Msg to be forwarded, and use the reserved ID
4237 // space for inserting retired messages.
4238 fm := queue.Msg{}
4239 err = tx.Insert(&fm)
4240 xcheckf(err, "temporarily insert message to get autoincrement sequence")
4241 err = tx.Delete(&fm)
4242 xcheckf(err, "removing temporary message for resetting autoincrement sequence")
4243 fm.ID += int64(n)
4244 err = tx.Insert(&fm)
4245 xcheckf(err, "temporarily insert message to forward autoincrement sequence")
4246 err = tx.Delete(&fm)
4247 xcheckf(err, "removing temporary message after forwarding autoincrement sequence")
4248 fm.ID -= int64(n)
4249
4250 // And likewise for webhooks.
4251 fh := queue.Hook{Account: "x", URL: "x", NextAttempt: time.Now()}
4252 err = tx.Insert(&fh)
4253 xcheckf(err, "temporarily insert webhook to get autoincrement sequence")
4254 err = tx.Delete(&fh)
4255 xcheckf(err, "removing temporary webhook for resetting autoincrement sequence")
4256 fh.ID += int64(n)
4257 err = tx.Insert(&fh)
4258 xcheckf(err, "temporarily insert webhook to forward autoincrement sequence")
4259 err = tx.Delete(&fh)
4260 xcheckf(err, "removing temporary webhook after forwarding autoincrement sequence")
4261 fh.ID -= int64(n)
4262
4263 for i := range n {
4264 t0 := now.Add(-time.Duration(i) * time.Second)
4265 last := now.Add(-time.Duration(i/10) * time.Second)
4266 mr := queue.MsgRetired{
4267 ID: fm.ID + int64(i),
4268 Queued: t0,
4269 SenderAccount: "test",
4270 SenderLocalpart: "mox",
4271 SenderDomainStr: "localhost",
4272 FromID: fmt.Sprintf("%016d", i),
4273 RecipientLocalpart: "mox",
4274 RecipientDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "localhost"}},
4275 RecipientDomainStr: "localhost",
4276 Attempts: i % 6,
4277 LastAttempt: &last,
4278 Results: []queue.MsgResult{
4279 {
4280 Start: last,
4281 Duration: time.Millisecond,
4282 Success: i%10 != 0,
4283 Code: 250,
4284 },
4285 },
4286 Has8bit: i%2 == 0,
4287 SMTPUTF8: i%8 == 0,
4288 Size: int64(i * 100),
4289 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
4290 Subject: fmt.Sprintf("test message %d", i),
4291 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
4292 LastActivity: last,
4293 RecipientAddress: "mox@localhost",
4294 Success: i%10 != 0,
4295 KeepUntil: now.Add(48 * time.Hour),
4296 }
4297 err := tx.Insert(&mr)
4298 xcheckf(err, "inserting retired message")
4299 }
4300
4301 for i := range n {
4302 t0 := now.Add(-time.Duration(i) * time.Second)
4303 last := now.Add(-time.Duration(i/10) * time.Second)
4304 var event string
4305 if i%10 != 0 {
4306 event = "delivered"
4307 }
4308 hr := queue.HookRetired{
4309 ID: fh.ID + int64(i),
4310 QueueMsgID: fm.ID + int64(i),
4311 FromID: fmt.Sprintf("%016d", i),
4312 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
4313 Subject: fmt.Sprintf("test message %d", i),
4314 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
4315 Account: "test",
4316 URL: "http://localhost/hook",
4317 IsIncoming: i%10 == 0,
4318 OutgoingEvent: event,
4319 Payload: "{}",
4320
4321 Submitted: t0,
4322 Attempts: i % 6,
4323 Results: []queue.HookResult{
4324 {
4325 Start: t0,
4326 Duration: time.Millisecond,
4327 URL: "http://localhost/hook",
4328 Success: i%10 != 0,
4329 Code: 200,
4330 Response: "ok",
4331 },
4332 },
4333
4334 Success: i%10 != 0,
4335 LastActivity: last,
4336 KeepUntil: now.Add(48 * time.Hour),
4337 }
4338 err := tx.Insert(&hr)
4339 xcheckf(err, "inserting retired hook")
4340 }
4341
4342 return nil
4343 })
4344 xcheckf(err, "add to queue")
4345 log.Printf("added %d retired messages and %d retired webhooks", n, n)
4346}
4347