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