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