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