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