10 cryptorand "crypto/rand"
35 "golang.org/x/crypto/bcrypt"
36 "golang.org/x/text/secure/precis"
38 "github.com/mjl-/adns"
40 "github.com/mjl-/autocert"
41 "github.com/mjl-/bstore"
42 "github.com/mjl-/sconf"
43 "github.com/mjl-/sherpa"
45 "github.com/mjl-/mox/config"
46 "github.com/mjl-/mox/dane"
47 "github.com/mjl-/mox/dkim"
48 "github.com/mjl-/mox/dmarc"
49 "github.com/mjl-/mox/dmarcdb"
50 "github.com/mjl-/mox/dmarcrpt"
51 "github.com/mjl-/mox/dns"
52 "github.com/mjl-/mox/dnsbl"
53 "github.com/mjl-/mox/message"
54 "github.com/mjl-/mox/mlog"
55 "github.com/mjl-/mox/mox-"
56 "github.com/mjl-/mox/moxio"
57 "github.com/mjl-/mox/moxvar"
58 "github.com/mjl-/mox/mtasts"
59 "github.com/mjl-/mox/publicsuffix"
60 "github.com/mjl-/mox/smtp"
61 "github.com/mjl-/mox/smtpclient"
62 "github.com/mjl-/mox/spf"
63 "github.com/mjl-/mox/store"
64 "github.com/mjl-/mox/tlsrpt"
65 "github.com/mjl-/mox/tlsrptdb"
66 "github.com/mjl-/mox/updates"
67 "github.com/mjl-/mox/webadmin"
71 changelogDomain = "xmox.nl"
72 changelogURL = "https://updates.xmox.nl/changelog"
73 changelogPubKey = base64Decode("sPNiTDQzvb4FrytNEiebJhgyQzn57RwEjNbGWMM/bDY=")
76func base64Decode(s string) []byte {
77 buf, err := base64.StdEncoding.DecodeString(s)
84func envString(k, def string) string {
92var commands = []struct {
97 {"quickstart", cmdQuickstart},
99 {"setaccountpassword", cmdSetaccountpassword},
100 {"setadminpassword", cmdSetadminpassword},
101 {"loglevels", cmdLoglevels},
102 {"queue list", cmdQueueList},
103 {"queue kick", cmdQueueKick},
104 {"queue drop", cmdQueueDrop},
105 {"queue dump", cmdQueueDump},
106 {"import maildir", cmdImportMaildir},
107 {"import mbox", cmdImportMbox},
108 {"export maildir", cmdExportMaildir},
109 {"export mbox", cmdExportMbox},
110 {"localserve", cmdLocalserve},
112 {"backup", cmdBackup},
113 {"verifydata", cmdVerifydata},
115 {"config test", cmdConfigTest},
116 {"config dnscheck", cmdConfigDNSCheck},
117 {"config dnsrecords", cmdConfigDNSRecords},
118 {"config describe-domains", cmdConfigDescribeDomains},
119 {"config describe-static", cmdConfigDescribeStatic},
120 {"config account add", cmdConfigAccountAdd},
121 {"config account rm", cmdConfigAccountRemove},
122 {"config address add", cmdConfigAddressAdd},
123 {"config address rm", cmdConfigAddressRemove},
124 {"config domain add", cmdConfigDomainAdd},
125 {"config domain rm", cmdConfigDomainRemove},
126 {"config describe-sendmail", cmdConfigDescribeSendmail},
127 {"config printservice", cmdConfigPrintservice},
128 {"config ensureacmehostprivatekeys", cmdConfigEnsureACMEHostprivatekeys},
129 {"example", cmdExample},
131 {"checkupdate", cmdCheckupdate},
133 {"clientconfig", cmdClientConfig},
134 {"deliver", cmdDeliver},
135 // 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
136 {"dane dial", cmdDANEDial},
137 {"dane dialmx", cmdDANEDialmx},
138 {"dane makerecord", cmdDANEMakeRecord},
139 {"dns lookup", cmdDNSLookup},
140 {"dkim gened25519", cmdDKIMGened25519},
141 {"dkim genrsa", cmdDKIMGenrsa},
142 {"dkim lookup", cmdDKIMLookup},
143 {"dkim txt", cmdDKIMTXT},
144 {"dkim verify", cmdDKIMVerify},
145 {"dkim sign", cmdDKIMSign},
146 {"dmarc lookup", cmdDMARCLookup},
147 {"dmarc parsereportmsg", cmdDMARCParsereportmsg},
148 {"dmarc verify", cmdDMARCVerify},
149 {"dmarc checkreportaddrs", cmdDMARCCheckreportaddrs},
150 {"dnsbl check", cmdDNSBLCheck},
151 {"dnsbl checkhealth", cmdDNSBLCheckhealth},
152 {"mtasts lookup", cmdMTASTSLookup},
153 {"retrain", cmdRetrain},
154 {"sendmail", cmdSendmail},
155 {"spf check", cmdSPFCheck},
156 {"spf lookup", cmdSPFLookup},
157 {"spf parse", cmdSPFParse},
158 {"tlsrpt lookup", cmdTLSRPTLookup},
159 {"tlsrpt parsereportmsg", cmdTLSRPTParsereportmsg},
160 {"version", cmdVersion},
162 {"bumpuidvalidity", cmdBumpUIDValidity},
163 {"reassignuids", cmdReassignUIDs},
164 {"fixuidmeta", cmdFixUIDMeta},
165 {"fixmsgsize", cmdFixmsgsize},
166 {"reparse", cmdReparse},
167 {"ensureparsed", cmdEnsureParsed},
168 {"recalculatemailboxcounts", cmdRecalculateMailboxCounts},
169 {"message parse", cmdMessageParse},
170 {"reassignthreads", cmdReassignthreads},
173 {"helpall", cmdHelpall},
174 {"junk analyze", cmdJunkAnalyze},
175 {"junk check", cmdJunkCheck},
176 {"junk play", cmdJunkPlay},
177 {"junk test", cmdJunkTest},
178 {"junk train", cmdJunkTrain},
179 {"dmarcdb addreport", cmdDMARCDBAddReport},
180 {"tlsrptdb addreport", cmdTLSRPTDBAddReport},
181 {"updates addsigned", cmdUpdatesAddSigned},
182 {"updates genkey", cmdUpdatesGenkey},
183 {"updates pubkey", cmdUpdatesPubkey},
184 {"updates serve", cmdUpdatesServe},
185 {"updates verify", cmdUpdatesVerify},
186 {"gentestdata", cmdGentestdata},
187 {"ximport maildir", cmdXImportMaildir},
188 {"ximport mbox", cmdXImportMbox},
189 {"openaccounts", cmdOpenaccounts},
190 {"readmessages", cmdReadmessages},
196 for _, xc := range commands {
197 c := cmd{words: strings.Split(xc.cmd, " "), fn: xc.fn}
198 cmds = append(cmds, c)
206 // Set before calling command.
209 _gather bool // Set when using Parse to gather usage for a command.
211 // Set by invoked command or Parse.
212 unlisted bool // If set, command is not listed until at least some words are matched from command.
213 params string // Arguments to command. Multiple lines possible.
214 help string // Additional explanation. First line is synopsis, the rest is only printed for an explicit help/usage for that command.
220func (c *cmd) Parse() []string {
221 // To gather params and usage information, we just run the command but cause this
222 // panic after the command has registered its flags and set its params and help
223 // information. This is then caught and that info printed.
228 c.flag.Usage = c.Usage
229 c.flag.Parse(c.flagArgs)
230 c.args = c.flag.Args()
234func (c *cmd) gather() {
235 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
239 // panic generated by Parse.
247func (c *cmd) makeUsage() string {
248 var r strings.Builder
249 cs := "mox " + strings.Join(c.words, " ")
250 for i, line := range strings.Split(strings.TrimSpace(c.params), "\n") {
258 fmt.Fprintf(&r, "%6s %s%s\n", s, cs, line)
261 c.flag.PrintDefaults()
265func (c *cmd) printUsage() {
266 fmt.Fprint(os.Stderr, c.makeUsage())
268 fmt.Fprint(os.Stderr, "\n"+c.help+"\n")
272func (c *cmd) Usage() {
277func cmdHelp(c *cmd) {
278 c.params = "[command ...]"
279 c.help = `Prints help about matching commands.
281If multiple commands match, they are listed along with the first line of their help text.
282If a single command matches, its usage and full help text is printed.
289 prefix := func(l, pre []string) bool {
290 if len(pre) > len(l) {
293 return slices.Equal(pre, l[:len(pre)])
297 for _, c := range cmds {
298 if slices.Equal(c.words, args) {
300 fmt.Print(c.makeUsage())
302 fmt.Print("\n" + c.help + "\n")
305 } else if prefix(c.words, args) {
306 partial = append(partial, c)
309 if len(partial) == 0 {
310 fmt.Fprintf(os.Stderr, "%s: unknown command\n", strings.Join(args, " "))
313 for _, c := range partial {
315 line := "mox " + strings.Join(c.words, " ")
316 fmt.Printf("%s\n", line)
318 fmt.Printf("\t%s\n", strings.Split(c.help, "\n")[0])
323func cmdHelpall(c *cmd) {
325 c.help = `Print all detailed usage and help information for all listed commands.
327Used to generate documentation.
335 for _, c := range cmds {
341 fmt.Fprintf(os.Stderr, "\n")
345 fmt.Fprintf(os.Stderr, "# mox %s\n\n", strings.Join(c.words, " "))
347 fmt.Fprintln(os.Stderr, c.help+"\n")
350 s = "\t" + strings.ReplaceAll(s, "\n", "\n\t")
351 fmt.Fprintln(os.Stderr, s)
355func usage(l []cmd, unlisted bool) {
358 lines = append(lines, "mox [-config config/mox.conf] [-pedantic] ...")
360 for _, c := range l {
362 if c.unlisted && !unlisted {
365 for _, line := range strings.Split(c.params, "\n") {
366 x := append([]string{"mox"}, c.words...)
370 lines = append(lines, strings.Join(x, " "))
373 for i, line := range lines {
378 fmt.Fprintln(os.Stderr, pre+line)
386// subcommands that are not "serve" should use this function to load the config, it
387// restores any loglevel specified on the command-line, instead of using the
388// loglevels from the config file and it does not load files like TLS keys/certs.
389func mustLoadConfig() {
390 mox.MustLoadConfig(false, false)
391 if level, ok := mlog.Levels[loglevel]; loglevel != "" && ok {
392 mox.Conf.Log[""] = level
393 mlog.SetConfig(mox.Conf.Log)
394 } else if loglevel != "" && !ok {
395 log.Fatal("unknown loglevel", slog.String("loglevel", loglevel))
398 mox.SetPedantic(true)
403 // CheckConsistencyOnClose is true by default, for all the test packages. A regular
404 // mox server should never use it. But integration tests enable it again with a
406 store.CheckConsistencyOnClose = false
408 ctxbg := context.Background()
414 // If invoked as sendmail, e.g. /usr/sbin/sendmail, we do enough so cron can get a
415 // message sent using smtp submission to a configured server.
416 if len(os.Args) > 0 && filepath.Base(os.Args[0]) == "sendmail" {
418 flag: flag.NewFlagSet("sendmail", flag.ExitOnError),
419 flagArgs: os.Args[1:],
420 log: mlog.New("sendmail", nil),
426 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")
427 flag.StringVar(&loglevel, "loglevel", "", "if non-empty, this log level is set early in startup")
428 flag.BoolVar(&pedantic, "pedantic", false, "protocol violations result in errors instead of accepting/working around them")
429 flag.BoolVar(&store.CheckConsistencyOnClose, "checkconsistency", false, "dangerous option for testing only, enables data checks that abort/panic when inconsistencies are found")
431 var cpuprofile, memprofile, tracefile string
432 flag.StringVar(&cpuprofile, "cpuprof", "", "store cpu profile to file")
433 flag.StringVar(&memprofile, "memprof", "", "store mem profile to file")
434 flag.StringVar(&tracefile, "trace", "", "store execution trace to file")
436 flag.Usage = func() { usage(cmds, false) }
444 defer traceExecution(tracefile)()
446 defer profile(cpuprofile, memprofile)()
449 mox.SetPedantic(true)
452 mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
453 if level, ok := mlog.Levels[loglevel]; ok && loglevel != "" {
454 mox.Conf.Log[""] = level
455 mlog.SetConfig(mox.Conf.Log)
456 // note: SetConfig may be called again when subcommands loads config.
461 for _, c := range cmds {
462 for i, w := range c.words {
463 if i >= len(args) || w != args[i] {
465 partial = append(partial, c)
470 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
471 c.flagArgs = args[len(c.words):]
472 c.log = mlog.New(strings.Join(c.words, ""), nil)
476 if len(partial) > 0 {
482func xcheckf(err error, format string, args ...any) {
486 msg := fmt.Sprintf(format, args...)
487 log.Fatalf("%s: %s", msg, err)
490func xparseIP(s, what string) net.IP {
493 log.Fatalf("invalid %s: %q", what, s)
498func xparseDomain(s, what string) dns.Domain {
499 d, err := dns.ParseDomain(s)
500 xcheckf(err, "parsing %s %q", what, s)
504func cmdClientConfig(c *cmd) {
506 c.help = `Print the configuration for email clients for a domain.
508Sending email is typically not done on the SMTP port 25, but on submission
509ports 465 (with TLS) and 587 (without initial TLS, but usually added to the
510connection with STARTTLS). For IMAP, the port with TLS is 993 and without is
513Without TLS/STARTTLS, passwords are sent in clear text, which should only be
514configured over otherwise secured connections, like a VPN.
520 d := xparseDomain(args[0], "domain")
525func printClientConfig(d dns.Domain) {
526 cc, err := mox.ClientConfigsDomain(d)
527 xcheckf(err, "getting client config")
528 fmt.Printf("%-20s %-30s %5s %-15s %s\n", "Protocol", "Host", "Port", "Listener", "Note")
529 for _, e := range cc.Entries {
530 fmt.Printf("%-20s %-30s %5d %-15s %s\n", e.Protocol, e.Host, e.Port, e.Listener, e.Note)
533To prevent authentication mechanism downgrade attempts that may result in
534clients sending plain text passwords to a MitM, clients should always be
535explicitly configured with the most secure authentication mechanism supported,
536the first of: SCRAM-SHA-256-PLUS, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, SCRAM-SHA-1,
541func cmdConfigTest(c *cmd) {
542 c.help = `Parses and validates the configuration files.
544If valid, the command exits with status 0. If not valid, all errors encountered
552 mox.FilesImmediate = true
554 _, errs := mox.ParseConfig(context.Background(), c.log, mox.ConfigStaticPath, true, true, false)
556 log.Printf("multiple errors:")
557 for _, err := range errs {
558 log.Printf("%s", err)
561 } else if len(errs) == 1 {
562 log.Fatalf("%s", errs[0])
565 fmt.Println("config OK")
568func cmdConfigDescribeStatic(c *cmd) {
569 c.params = ">mox.conf"
570 c.help = `Prints an annotated empty configuration for use as mox.conf.
572The static configuration file cannot be reloaded while mox is running. Mox has
573to be restarted for changes to the static configuration file to take effect.
575This configuration file needs modifications to make it valid. For example, it
576may contain unfinished list items.
578 if len(c.Parse()) != 0 {
583 err := sconf.Describe(os.Stdout, &sc)
584 xcheckf(err, "describing config")
587func cmdConfigDescribeDomains(c *cmd) {
588 c.params = ">domains.conf"
589 c.help = `Prints an annotated empty configuration for use as domains.conf.
591The domains configuration file contains the domains and their configuration,
592and accounts and their configuration. This includes the configured email
593addresses. The mox admin web interface, and the mox command line interface, can
594make changes to this file. Mox automatically reloads this file when it changes.
596Like the static configuration, the example domains.conf printed by this command
597needs modifications to make it valid.
599 if len(c.Parse()) != 0 {
603 var dc config.Dynamic
604 err := sconf.Describe(os.Stdout, &dc)
605 xcheckf(err, "describing config")
608func cmdConfigPrintservice(c *cmd) {
609 c.params = ">mox.service"
610 c.help = `Prints a systemd unit service file for mox.
612This is the same file as generated using quickstart. If the systemd service file
613has changed with a newer version of mox, use this command to generate an up to
616 if len(c.Parse()) != 0 {
620 pwd, err := os.Getwd()
622 log.Printf("current working directory: %v", err)
625 service := strings.ReplaceAll(moxService, "/home/mox", pwd)
629func cmdConfigDomainAdd(c *cmd) {
630 c.params = "domain account [localpart]"
631 c.help = `Adds a new domain to the configuration and reloads the configuration.
633The account is used for the postmaster mailboxes the domain, including as DMARC and
634TLS reporting. Localpart is the "username" at the domain for this account. If
635must be set if and only if account does not yet exist.
638 if len(args) != 2 && len(args) != 3 {
642 d := xparseDomain(args[0], "domain")
644 var localpart smtp.Localpart
647 localpart, err = smtp.ParseLocalpart(args[2])
648 xcheckf(err, "parsing localpart")
650 ctlcmdConfigDomainAdd(xctl(), d, args[1], localpart)
653func ctlcmdConfigDomainAdd(ctl *ctl, domain dns.Domain, account string, localpart smtp.Localpart) {
654 ctl.xwrite("domainadd")
655 ctl.xwrite(domain.Name())
657 ctl.xwrite(string(localpart))
659 fmt.Printf("domain added, remember to add dns records, see:\n\nmox config dnsrecords %s\nmox config dnscheck %s\n", domain.Name(), domain.Name())
662func cmdConfigDomainRemove(c *cmd) {
664 c.help = `Remove a domain from the configuration and reload the configuration.
666This is a dangerous operation. Incoming email delivery for this domain will be
674 d := xparseDomain(args[0], "domain")
676 ctlcmdConfigDomainRemove(xctl(), d)
679func ctlcmdConfigDomainRemove(ctl *ctl, d dns.Domain) {
680 ctl.xwrite("domainrm")
683 fmt.Printf("domain removed, remember to remove dns records for %s\n", d)
686func cmdConfigAccountAdd(c *cmd) {
687 c.params = "account address"
688 c.help = `Add an account with an email address and reload the configuration.
690Email can be delivered to this address/account. A password has to be configured
691explicitly, see the setaccountpassword command.
699 ctlcmdConfigAccountAdd(xctl(), args[0], args[1])
702func ctlcmdConfigAccountAdd(ctl *ctl, account, address string) {
703 ctl.xwrite("accountadd")
707 fmt.Printf("account added, set a password with \"mox setaccountpassword %s\"\n", account)
710func cmdConfigAccountRemove(c *cmd) {
712 c.help = `Remove an account and reload the configuration.
714Email addresses for this account will also be removed, and incoming email for
715these addresses will be rejected.
723 ctlcmdConfigAccountRemove(xctl(), args[0])
726func ctlcmdConfigAccountRemove(ctl *ctl, account string) {
727 ctl.xwrite("accountrm")
730 fmt.Println("account removed")
733func cmdConfigAddressAdd(c *cmd) {
734 c.params = "address account"
735 c.help = `Adds an address to an account and reloads the configuration.
737If address starts with a @ (i.e. a missing localpart), this is a catchall
738address for the domain.
746 ctlcmdConfigAddressAdd(xctl(), args[0], args[1])
749func ctlcmdConfigAddressAdd(ctl *ctl, address, account string) {
750 ctl.xwrite("addressadd")
754 fmt.Println("address added")
757func cmdConfigAddressRemove(c *cmd) {
759 c.help = `Remove an address and reload the configuration.
761Incoming email for this address will be rejected after removing an address.
769 ctlcmdConfigAddressRemove(xctl(), args[0])
772func ctlcmdConfigAddressRemove(ctl *ctl, address string) {
773 ctl.xwrite("addressrm")
776 fmt.Println("address removed")
779func cmdConfigDNSRecords(c *cmd) {
781 c.help = `Prints annotated DNS records as zone file that should be created for the domain.
783The zone file can be imported into existing DNS software. You should review the
784DNS records, especially if your domain previously/currently has email
792 d := xparseDomain(args[0], "domain")
794 domConf, ok := mox.Conf.Domain(d)
796 log.Fatalf("unknown domain")
799 resolver := dns.StrictResolver{Pkg: "main"}
800 _, result, err := resolver.LookupTXT(context.Background(), d.ASCII+".")
801 if !dns.IsNotFound(err) {
802 xcheckf(err, "looking up record for dnssec-status")
805 var certIssuerDomainName, acmeAccountURI string
806 public := mox.Conf.Static.Listeners["public"]
807 if public.TLS != nil && public.TLS.ACME != "" {
808 acme, ok := mox.Conf.Static.ACME[public.TLS.ACME]
809 if ok && acme.Manager.Manager.Client != nil {
810 certIssuerDomainName = acme.IssuerDomainName
811 acc, err := acme.Manager.Manager.Client.GetReg(context.Background(), "")
812 c.log.Check(err, "get public acme account")
814 acmeAccountURI = acc.URI
819 records, err := mox.DomainRecords(domConf, d, result.Authentic, certIssuerDomainName, acmeAccountURI)
820 xcheckf(err, "records")
821 fmt.Print(strings.Join(records, "\n") + "\n")
824func cmdConfigDNSCheck(c *cmd) {
826 c.help = "Check the DNS records with the configuration for the domain, and print any errors/warnings."
832 d := xparseDomain(args[0], "domain")
834 _, ok := mox.Conf.Domain(d)
836 log.Fatalf("unknown domain")
839 // todo future: move http.Admin.CheckDomain to mox- and make it return a regular error.
845 err, ok := x.(*sherpa.Error)
849 log.Fatalf("%s", err)
852 printResult := func(name string, r webadmin.Result) {
853 if len(r.Errors) == 0 && len(r.Warnings) == 0 {
856 fmt.Printf("# %s\n", name)
857 for _, s := range r.Errors {
858 fmt.Printf("error: %s\n", s)
860 for _, s := range r.Warnings {
861 fmt.Printf("warning: %s\n", s)
865 result := webadmin.Admin{}.CheckDomain(context.Background(), args[0])
866 printResult("DNSSEC", result.DNSSEC.Result)
867 printResult("IPRev", result.IPRev.Result)
868 printResult("MX", result.MX.Result)
869 printResult("TLS", result.TLS.Result)
870 printResult("DANE", result.DANE.Result)
871 printResult("SPF", result.SPF.Result)
872 printResult("DKIM", result.DKIM.Result)
873 printResult("DMARC", result.DMARC.Result)
874 printResult("Host TLSRPT", result.HostTLSRPT.Result)
875 printResult("Domain TLSRPT", result.DomainTLSRPT.Result)
876 printResult("MTASTS", result.MTASTS.Result)
877 printResult("SRV conf", result.SRVConf.Result)
878 printResult("Autoconf", result.Autoconf.Result)
879 printResult("Autodiscover", result.Autodiscover.Result)
882func cmdConfigEnsureACMEHostprivatekeys(c *cmd) {
884 c.help = `Ensure host private keys exist for TLS listeners with ACME.
886In mox.conf, each listener can have TLS configured. Long-lived private key files
887can be specified, which will be used when requesting ACME certificates.
888Configuring these private keys makes it feasible to publish DANE TLSA records
889for the corresponding public keys in DNS, protected with DNSSEC, allowing TLS
890certificate verification without depending on a list of Certificate Authorities
891(CAs). Previous versions of mox did not pre-generate private keys for use with
892ACME certificates, but would generate private keys on-demand. By explicitly
893configuring private keys, they will not change automatedly with new
894certificates, and the DNS TLSA records stay valid.
896This command looks for listeners in mox.conf with TLS with ACME configured. For
897each missing host private key (of type rsa-2048 and ecdsa-p256) a key is written
898to config/hostkeys/. If a certificate exists in the ACME "cache", its private
899key is copied. Otherwise a new private key is generated. Snippets for manually
900updating/editing mox.conf are printed.
902After running this command, and updating mox.conf, run "mox config dnsrecords"
903for a domain and create the TLSA DNS records it suggests to enable DANE.
910 // Load a private key from p, in various forms. We only look at the first PEM
911 // block. Files with only a private key, or with multiple blocks but private key
912 // first like autocert does, can be loaded.
913 loadPrivateKey := func(f *os.File) (any, error) {
914 buf, err := io.ReadAll(f)
916 return nil, fmt.Errorf("reading private key file: %v", err)
918 block, _ := pem.Decode(buf)
920 return nil, fmt.Errorf("no pem block found in pem file")
924 case "EC PRIVATE KEY":
925 privKey, err = x509.ParseECPrivateKey(block.Bytes)
926 case "RSA PRIVATE KEY":
927 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
929 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
931 return nil, fmt.Errorf("unrecognized pem block type %q", block.Type)
934 return nil, fmt.Errorf("parsing private key of type %q: %v", block.Type, err)
939 // Either load a private key from file, or if it doesn't exist generate a new
941 xtryLoadPrivateKey := func(kt autocert.KeyType, p string) any {
943 if err != nil && errors.Is(err, fs.ErrNotExist) {
945 case autocert.KeyRSA2048:
946 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
947 xcheckf(err, "generating new 2048-bit rsa private key")
949 case autocert.KeyECDSAP256:
950 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
951 xcheckf(err, "generating new ecdsa p-256 private key")
954 log.Fatalf("unexpected keytype %v", kt)
957 xcheckf(err, "%s: open acme key and certificate file", p)
959 // Load private key from file. autocert stores a PEM file that starts with a
960 // private key, followed by certificate(s). So we can just read it and should find
961 // the private key we are looking for.
962 privKey, err := loadPrivateKey(f)
963 if xerr := f.Close(); xerr != nil {
964 log.Printf("closing private key file: %v", xerr)
966 xcheckf(err, "parsing private key from acme key and certificate file")
968 switch k := privKey.(type) {
969 case *rsa.PrivateKey:
970 if k.N.BitLen() == 2048 {
973 log.Printf("warning: rsa private key in %s has %d bits, skipping and generating new 2048-bit rsa private key", p, k.N.BitLen())
974 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
975 xcheckf(err, "generating new 2048-bit rsa private key")
977 case *ecdsa.PrivateKey:
978 if k.Curve == elliptic.P256() {
981 log.Printf("warning: ecdsa private key in %s has curve %v, skipping and generating new p-256 ecdsa key", p, k.Curve.Params().Name)
982 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
983 xcheckf(err, "generating new ecdsa p-256 private key")
986 log.Fatalf("%s: unexpected private key file of type %T", p, privKey)
991 // Write privKey as PKCS#8 private key to p. Only if file does not yet exist.
992 writeHostPrivateKey := func(privKey any, p string) error {
993 os.MkdirAll(filepath.Dir(p), 0700)
994 f, err := os.OpenFile(p, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
996 return fmt.Errorf("create: %v", err)
1000 if err := f.Close(); err != nil {
1001 log.Printf("closing new hostkey file %s after error: %v", p, err)
1003 if err := os.Remove(p); err != nil {
1004 log.Printf("removing new hostkey file %s after error: %v", p, err)
1008 buf, err := x509.MarshalPKCS8PrivateKey(privKey)
1010 return fmt.Errorf("marshal private host key: %v", err)
1013 Type: "PRIVATE KEY",
1016 if err := pem.Encode(f, &block); err != nil {
1017 return fmt.Errorf("write as pem: %v", err)
1019 if err := f.Close(); err != nil {
1020 return fmt.Errorf("close: %v", err)
1027 timestamp := time.Now().Format("20060102T150405")
1029 for listenerName, l := range mox.Conf.Static.Listeners {
1030 if l.TLS == nil || l.TLS.ACME == "" {
1033 haveKeyTypes := map[autocert.KeyType]bool{}
1034 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
1035 p := mox.ConfigDirPath(privKeyFile)
1036 f, err := os.Open(p)
1037 xcheckf(err, "open host private key")
1038 privKey, err := loadPrivateKey(f)
1039 if err := f.Close(); err != nil {
1040 log.Printf("closing host private key file: %v", err)
1042 xcheckf(err, "loading host private key")
1043 switch k := privKey.(type) {
1044 case *rsa.PrivateKey:
1045 if k.N.BitLen() == 2048 {
1046 haveKeyTypes[autocert.KeyRSA2048] = true
1048 case *ecdsa.PrivateKey:
1049 if k.Curve == elliptic.P256() {
1050 haveKeyTypes[autocert.KeyECDSAP256] = true
1054 created := []string{}
1055 for _, kt := range []autocert.KeyType{autocert.KeyRSA2048, autocert.KeyECDSAP256} {
1056 if haveKeyTypes[kt] {
1059 // Lookup key in ACME cache.
1060 host := l.HostnameDomain
1061 if host.ASCII == "" {
1062 host = mox.Conf.Static.HostnameDomain
1064 filename := host.ASCII
1066 if kt == autocert.KeyRSA2048 {
1070 p := mox.DataDirPath(filepath.Join("acme", "keycerts", l.TLS.ACME, filename))
1071 privKey := xtryLoadPrivateKey(kt, p)
1073 relPath := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", host.Name(), timestamp, kind))
1074 destPath := mox.ConfigDirPath(relPath)
1075 err := writeHostPrivateKey(privKey, destPath)
1076 xcheckf(err, "writing host private key file to %s: %v", destPath, err)
1077 created = append(created, relPath)
1078 fmt.Printf("Wrote host private key: %s\n", destPath)
1080 didCreate = didCreate || len(created) > 0
1081 if len(created) > 0 {
1083 HostPrivateKeyFiles: append(l.TLS.HostPrivateKeyFiles, created...),
1085 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)
1086 err := sconf.Write(os.Stdout, tls)
1087 xcheckf(err, "writing new TLS.HostPrivateKeyFiles section")
1093After updating mox.conf and restarting, run "mox config dnsrecords" for a
1094domain and create the TLSA DNS records it suggests to enable DANE.
1099func cmdLoglevels(c *cmd) {
1100 c.params = "[level [pkg]]"
1101 c.help = `Print the log levels, or set a new default log level, or a level for the given package.
1103By default, a single log level applies to all logging in mox. But for each
1104"pkg", an overriding log level can be configured. Examples of packages:
1105smtpserver, smtpclient, queue, imapserver, spf, dkim, dmarc, junk, message,
1108Specify a pkg and an empty level to clear the configured level for a package.
1110Valid labels: error, info, debug, trace, traceauth, tracedata.
1119 ctlcmdLoglevels(xctl())
1125 ctlcmdSetLoglevels(xctl(), pkg, args[0])
1129func ctlcmdLoglevels(ctl *ctl) {
1130 ctl.xwrite("loglevels")
1132 ctl.xstreamto(os.Stdout)
1135func ctlcmdSetLoglevels(ctl *ctl, pkg, level string) {
1136 ctl.xwrite("setloglevels")
1142func cmdStop(c *cmd) {
1143 c.help = `Shut mox down, giving connections maximum 3 seconds to stop before closing them.
1145While shutting down, new IMAP and SMTP connections will get a status response
1146indicating temporary unavailability. Existing connections will get a 3 second
1147period to finish their transaction and shut down. Under normal circumstances,
1148only IMAP has long-living connections, with the IDLE command to get notified of
1151 if len(c.Parse()) != 0 {
1158 // Read will hang until remote has shut down.
1159 buf := make([]byte, 128)
1160 n, err := ctl.conn.Read(buf)
1162 log.Fatalf("expected eof after graceful shutdown, got data %q", buf[:n])
1163 } else if err != io.EOF {
1164 log.Fatalf("expected eof after graceful shutdown, got error %v", err)
1166 fmt.Println("mox stopped")
1169func cmdBackup(c *cmd) {
1170 c.params = "dest-dir"
1171 c.help = `Creates a backup of the data directory.
1173Backup creates consistent snapshots of the databases and message files and
1174copies other files in the data directory. Empty directories are not copied.
1175These files can then be stored elsewhere for long-term storage, or used to fall
1176back to should an upgrade fail. Simply copying files in the data directory
1177while mox is running can result in unusable database files.
1179Message files never change (they are read-only, though can be removed) and are
1180hard-linked so they don't consume additional space. If hardlinking fails, for
1181example when the backup destination directory is on a different file system, a
1182regular copy is made. Using a destination directory like "data/tmp/backup"
1183increases the odds hardlinking succeeds: the default systemd service file
1184specifically mounts the data directory, causing attempts to hardlink outside it
1185to fail with an error about cross-device linking.
1187All files in the data directory that aren't recognized (i.e. other than known
1188database files, message files, an acme directory, the "tmp" directory, etc),
1189are stored, but with a warning.
1191A clean successful backup does not print any output by default. Use the
1192-verbose flag for details, including timing.
1194To restore a backup, first shut down mox, move away the old data directory and
1195move an earlier backed up directory in its place, run "mox verifydata",
1196possibly with the "-fix" option, and restart mox. After the restore, you may
1197also want to run "mox bumpuidvalidity" for each account for which messages in a
1198mailbox changed, to force IMAP clients to synchronize mailbox state.
1200Before upgrading, to check if the upgrade will likely succeed, first make a
1201backup, then use the new mox binary to run "mox verifydata" on the backup. This
1202can change the backup files (e.g. upgrade database files, move away
1203unrecognized message files), so you should make a new backup before actually
1208 c.flag.BoolVar(&verbose, "verbose", false, "print progress")
1215 dstDataDir, err := filepath.Abs(args[0])
1216 xcheckf(err, "making path absolute")
1218 ctlcmdBackup(xctl(), dstDataDir, verbose)
1221func ctlcmdBackup(ctl *ctl, dstDataDir string, verbose bool) {
1222 ctl.xwrite("backup")
1223 ctl.xwrite(dstDataDir)
1225 ctl.xwrite("verbose")
1229 ctl.xstreamto(os.Stdout)
1233func cmdSetadminpassword(c *cmd) {
1234 c.help = `Set a new admin password, for the web interface.
1236The password is read from stdin. Its bcrypt hash is stored in a file named
1237"adminpasswd" in the configuration directory.
1239 if len(c.Parse()) != 0 {
1244 path := mox.ConfigDirPath(mox.Conf.Static.AdminPasswordFile)
1246 log.Fatal("no admin password file configured")
1249 pw := xreadpassword()
1250 pw, err := precis.OpaqueString.String(pw)
1251 xcheckf(err, `checking password with "precis" requirements`)
1252 hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
1253 xcheckf(err, "generating hash for password")
1254 err = os.WriteFile(path, hash, 0660)
1255 xcheckf(err, "writing hash to admin password file")
1258func xreadpassword() string {
1260Type new password. Password WILL echo.
1262WARNING: Bots will try to bruteforce your password. Connections with failed
1263authentication attempts will be rate limited but attackers WILL find weak
1264passwords. If your account is compromised, spammers are likely to abuse your
1265system, spamming your address and the wider internet in your name. So please
1266pick a random, unguessable password, preferably at least 12 characters.
1269 fmt.Printf("password: ")
1270 buf := make([]byte, 64)
1271 n, err := os.Stdin.Read(buf)
1272 xcheckf(err, "reading stdin")
1273 pw := string(buf[:n])
1274 pw = strings.TrimSuffix(strings.TrimSuffix(pw, "\r\n"), "\n")
1276 log.Fatal("password must be at least 8 characters")
1281func cmdSetaccountpassword(c *cmd) {
1282 c.params = "account"
1283 c.help = `Set new password an account.
1285The password is read from stdin. Secrets derived from the password, but not the
1286password itself, are stored in the account database. The stored secrets are for
1287authentication with: scram-sha-256, scram-sha-1, cram-md5, plain text (bcrypt
1290The parameter is an account name, as configured under Accounts in domains.conf
1291and as present in the data/accounts/ directory, not a configured email address
1300 pw := xreadpassword()
1302 ctlcmdSetaccountpassword(xctl(), args[0], pw)
1305func ctlcmdSetaccountpassword(ctl *ctl, account, password string) {
1306 ctl.xwrite("setaccountpassword")
1308 ctl.xwrite(password)
1312func cmdDeliver(c *cmd) {
1314 c.params = "address < message"
1315 c.help = "Deliver message to address."
1321 ctlcmdDeliver(xctl(), args[0])
1324func ctlcmdDeliver(ctl *ctl, address string) {
1325 ctl.xwrite("deliver")
1328 ctl.xstreamfrom(os.Stdin)
1331 fmt.Println("message delivered")
1333 log.Fatalf("deliver: %s", line)
1337func cmdQueueList(c *cmd) {
1338 c.help = `List messages in the delivery queue.
1340This prints the message with its ID, last and next delivery attempts, last
1343 if len(c.Parse()) != 0 {
1347 ctlcmdQueueList(xctl())
1350func ctlcmdQueueList(ctl *ctl) {
1353 if _, err := io.Copy(os.Stdout, ctl.reader()); err != nil {
1354 log.Fatalf("%s", err)
1358func cmdQueueKick(c *cmd) {
1359 c.params = "[-id id] [-todomain domain] [-recipient address] [-transport transport]"
1360 c.help = `Schedule matching messages in the queue for immediate delivery.
1362Messages deliveries are normally attempted with exponential backoff. The first
1363retry after 7.5 minutes, and doubling each time. Kicking messages sets their
1364next scheduled attempt to now, it can cause delivery to fail earlier than
1365without rescheduling.
1367With the -transport flag, future delivery attempts are done using the specified
1368transport. Transports can be configured in mox.conf, e.g. to submit to a remote
1372 var todomain, recipient, transport string
1373 c.flag.Int64Var(&id, "id", 0, "id of message in queue")
1374 c.flag.StringVar(&todomain, "todomain", "", "destination domain of messages")
1375 c.flag.StringVar(&recipient, "recipient", "", "recipient email address")
1376 c.flag.StringVar(&transport, "transport", "", "transport to use for the next delivery")
1377 if len(c.Parse()) != 0 {
1381 ctlcmdQueueKick(xctl(), id, todomain, recipient, transport)
1384func ctlcmdQueueKick(ctl *ctl, id int64, todomain, recipient, transport string) {
1385 ctl.xwrite("queuekick")
1386 ctl.xwrite(fmt.Sprintf("%d", id))
1387 ctl.xwrite(todomain)
1388 ctl.xwrite(recipient)
1389 ctl.xwrite(transport)
1390 count := ctl.xread()
1393 fmt.Printf("%s messages scheduled\n", count)
1395 log.Fatalf("scheduling messages for immediate delivery: %s", line)
1399func cmdQueueDrop(c *cmd) {
1400 c.params = "[-id id] [-todomain domain] [-recipient address]"
1401 c.help = `Remove matching messages from the queue.
1403Dangerous operation, this completely removes the message. If you want to store
1404the message, use "queue dump" before removing.
1407 var todomain, recipient string
1408 c.flag.Int64Var(&id, "id", 0, "id of message in queue")
1409 c.flag.StringVar(&todomain, "todomain", "", "destination domain of messages")
1410 c.flag.StringVar(&recipient, "recipient", "", "recipient email address")
1411 if len(c.Parse()) != 0 {
1415 ctlcmdQueueDrop(xctl(), id, todomain, recipient)
1418func ctlcmdQueueDrop(ctl *ctl, id int64, todomain, recipient string) {
1419 ctl.xwrite("queuedrop")
1420 ctl.xwrite(fmt.Sprintf("%d", id))
1421 ctl.xwrite(todomain)
1422 ctl.xwrite(recipient)
1423 count := ctl.xread()
1426 fmt.Printf("%s messages dropped\n", count)
1428 log.Fatalf("scheduling messages for immediate delivery: %s", line)
1432func cmdQueueDump(c *cmd) {
1434 c.help = `Dump a message from the queue.
1436The message is printed to stdout and is in standard internet mail format.
1443 ctlcmdQueueDump(xctl(), args[0])
1446func ctlcmdQueueDump(ctl *ctl, id string) {
1447 ctl.xwrite("queuedump")
1450 if _, err := io.Copy(os.Stdout, ctl.reader()); err != nil {
1451 log.Fatalf("%s", err)
1455func cmdDKIMGenrsa(c *cmd) {
1456 c.params = ">$selector._domainkey.$domain.rsa2048.privatekey.pkcs8.pem"
1457 c.help = `Generate a new 2048 bit RSA private key for use with DKIM.
1459The generated file is in PEM format, and has a comment it is generated for use
1462 if len(c.Parse()) != 0 {
1466 buf, err := mox.MakeDKIMRSAKey(dns.Domain{}, dns.Domain{})
1467 xcheckf(err, "making rsa private key")
1468 _, err = os.Stdout.Write(buf)
1469 xcheckf(err, "writing rsa private key")
1472func cmdDANEDial(c *cmd) {
1473 c.params = "host:port"
1475 c.flag.StringVar(&usages, "usages", "pkix-ta,pkix-ee,dane-ta,dane-ee", "allowed usages for dane, comma-separated list")
1476 c.help = `Dial the address using TLS with certificate verification using DANE.
1478Data is copied between connection and stdin/stdout until either side closes the
1486 allowedUsages := []adns.TLSAUsage{}
1488 for _, s := range strings.Split(usages, ",") {
1489 var usage adns.TLSAUsage
1490 switch strings.ToLower(s) {
1491 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
1492 usage = adns.TLSAUsagePKIXTA
1493 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
1494 usage = adns.TLSAUsagePKIXEE
1495 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
1496 usage = adns.TLSAUsageDANETA
1497 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
1498 usage = adns.TLSAUsageDANEEE
1500 log.Fatalf("unknown dane usage %q", s)
1502 allowedUsages = append(allowedUsages, usage)
1506 pkixRoots, err := x509.SystemCertPool()
1507 xcheckf(err, "get system pkix certificate pool")
1509 resolver := dns.StrictResolver{Pkg: "danedial"}
1510 conn, record, err := dane.Dial(context.Background(), c.log.Logger, resolver, "tcp", args[0], allowedUsages, pkixRoots)
1511 xcheckf(err, "dial")
1512 log.Printf("(connected, verified with %s)", record)
1515 _, err := io.Copy(os.Stdout, conn)
1516 xcheckf(err, "copy from connection to stdout")
1519 _, err = io.Copy(conn, os.Stdin)
1520 xcheckf(err, "copy from stdin to connection")
1523func cmdDANEDialmx(c *cmd) {
1524 c.params = "domain [destination-host]"
1525 var ehloHostname string
1526 c.flag.StringVar(&ehloHostname, "ehlohostname", "localhost", "hostname to send in smtp ehlo command")
1527 c.help = `Connect to MX server for domain using STARTTLS verified with DANE.
1529If no destination host is specified, regular delivery logic is used to find the
1530hosts to attempt delivery too. This involves following CNAMEs for the domain,
1531looking up MX records, and possibly falling back to the domain name itself as
1534If a destination host is specified, that is the only candidate host considered
1537With a list of destinations gathered, each is dialed until a successful SMTP
1538session verified with DANE has been initialized, including EHLO and STARTTLS
1541Once connected, data is copied between connection and stdin/stdout, until
1542either side closes the connection.
1544This command follows the same logic as delivery attempts made from the queue,
1545sharing most of its code.
1548 if len(args) != 1 && len(args) != 2 {
1552 ehloDomain, err := dns.ParseDomain(ehloHostname)
1553 xcheckf(err, "parsing ehlo hostname")
1555 origNextHop, err := dns.ParseDomain(args[0])
1556 xcheckf(err, "parse domain")
1558 ctxbg := context.Background()
1560 resolver := dns.StrictResolver{}
1562 var origNextHopAuthentic, expandedNextHopAuthentic bool
1563 var expandedNextHop dns.Domain
1564 var hosts []dns.IPDomain
1567 haveMX, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hosts, permanent, err = smtpclient.GatherDestinations(ctxbg, c.log.Logger, resolver, dns.IPDomain{Domain: origNextHop})
1568 status := "temporary"
1570 status = "permanent"
1573 log.Fatalf("gathering destinations: %v (%s)", err, status)
1575 if expandedNextHop != origNextHop {
1576 log.Printf("followed cnames to %s", expandedNextHop)
1579 log.Printf("found mx record, trying mx hosts")
1581 log.Printf("no mx record found, will try to connect to domain directly")
1583 if !origNextHopAuthentic {
1584 log.Fatalf("error: initial domain not dnssec-secure")
1586 if !expandedNextHopAuthentic {
1587 log.Fatalf("error: expanded domain not dnssec-secure")
1591 for _, h := range hosts {
1592 l = append(l, h.String())
1594 log.Printf("destinations: %s", strings.Join(l, ", "))
1596 d, err := dns.ParseDomain(args[1])
1598 log.Fatalf("parsing destination host: %v", err)
1600 log.Printf("skipping domain mx/cname lookups, assuming domain is dnssec-protected")
1602 origNextHopAuthentic = true
1603 expandedNextHopAuthentic = true
1605 hosts = []dns.IPDomain{{Domain: d}}
1608 dialedIPs := map[string][]net.IP{}
1609 for _, host := range hosts {
1610 // It should not be possible for hosts to have IP addresses: They are not
1611 // allowed by dns.ParseDomain, and MX records cannot contain them.
1613 log.Fatalf("unexpected IP address for destination host")
1616 log.Printf("attempting to connect to %s", host)
1618 authentic, expandedAuthentic, expandedHost, ips, _, err := smtpclient.GatherIPs(ctxbg, c.log.Logger, resolver, host, dialedIPs)
1620 log.Printf("resolving ips for %s: %v, skipping", host, err)
1624 log.Printf("no dnssec for ips of %s, skipping", host)
1627 if !expandedAuthentic {
1628 log.Printf("no dnssec for cname-followed ips of %s, skipping", host)
1631 if expandedHost != host.Domain {
1632 log.Printf("host %s cname-expanded to %s", host, expandedHost)
1634 log.Printf("host %s resolved to ips %s, looking up tlsa records", host, ips)
1636 daneRequired, daneRecords, tlsaBaseDomain, err := smtpclient.GatherTLSA(ctxbg, c.log.Logger, resolver, host.Domain, expandedAuthentic, expandedHost)
1638 log.Printf("looking up tlsa records: %s, skipping", err)
1641 tlsMode := smtpclient.TLSRequiredStartTLS
1642 if len(daneRecords) == 0 {
1644 log.Printf("host %s has no tlsa records, skipping", expandedHost)
1647 log.Printf("warning: only unusable tlsa records found, continuing with required tls without certificate verification")
1651 for _, r := range daneRecords {
1652 l = append(l, r.String())
1654 log.Printf("tlsa records: %s", strings.Join(l, "; "))
1657 tlsHostnames := smtpclient.GatherTLSANames(haveMX, expandedNextHopAuthentic, expandedAuthentic, origNextHop, expandedNextHop, host.Domain, tlsaBaseDomain)
1659 for _, name := range tlsHostnames {
1660 l = append(l, name.String())
1662 log.Printf("gathered valid tls certificate names for potential verification with dane-ta: %s", strings.Join(l, ", "))
1664 dialer := &net.Dialer{Timeout: 5 * time.Second}
1665 conn, _, err := smtpclient.Dial(ctxbg, c.log.Logger, dialer, dns.IPDomain{Domain: expandedHost}, ips, 25, dialedIPs, nil)
1667 log.Printf("dial %s: %v, skipping", expandedHost, err)
1670 log.Printf("connected to %s, %s, starting smtp session with ehlo and starttls with dane verification", expandedHost, conn.RemoteAddr())
1672 var verifiedRecord adns.TLSA
1673 opts := smtpclient.Opts{
1674 DANERecords: daneRecords,
1675 DANEMoreHostnames: tlsHostnames[1:],
1676 DANEVerifiedRecord: &verifiedRecord,
1677 RootCAs: mox.Conf.Static.TLS.CertPool,
1680 sc, err := smtpclient.New(ctxbg, c.log.Logger, conn, tlsMode, tlsPKIX, ehloDomain, tlsHostnames[0], opts)
1682 log.Printf("setting up smtp session: %v, skipping", err)
1687 smtpConn, err := sc.Conn()
1689 log.Fatalf("error: taking over smtp connection: %s", err)
1691 log.Printf("tls verified with tlsa record: %s", verifiedRecord)
1692 log.Printf("smtp session initialized and connected to stdin/stdout")
1695 _, err := io.Copy(os.Stdout, smtpConn)
1696 xcheckf(err, "copy from connection to stdout")
1699 _, err = io.Copy(smtpConn, os.Stdin)
1700 xcheckf(err, "copy from stdin to connection")
1703 log.Fatalf("no remaining destinations")
1706func cmdDANEMakeRecord(c *cmd) {
1707 c.params = "usage selector matchtype [certificate.pem | publickey.pem | privatekey.pem]"
1708 c.help = `Print TLSA record for given certificate/key and parameters.
1711- usage: pkix-ta (0), pkix-ee (1), dane-ta (2), dane-ee (3)
1712- selector: cert (0), spki (1)
1713- matchtype: full (0), sha2-256 (1), sha2-512 (2)
1715Common DANE TLSA record parameters are: dane-ee spki sha2-256, or 3 1 1,
1716followed by a sha2-256 hash of the DER-encoded "SPKI" (subject public key info)
1717from the certificate. An example DNS zone file entry:
1719 _25._tcp.example.com. TLSA 3 1 1 133b919c9d65d8b1488157315327334ead8d83372db57465ecabf53ee5748aee
1721The first usable information from the pem file is used to compose the TLSA
1722record. In case of selector "cert", a certificate is required. Otherwise the
1723"subject public key info" (spki) of the first certificate or public or private
1724key (pkcs#8, pkcs#1 or ec private key) is used.
1732 var usage adns.TLSAUsage
1733 switch strings.ToLower(args[0]) {
1734 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
1735 usage = adns.TLSAUsagePKIXTA
1736 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
1737 usage = adns.TLSAUsagePKIXEE
1738 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
1739 usage = adns.TLSAUsageDANETA
1740 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
1741 usage = adns.TLSAUsageDANEEE
1743 if v, err := strconv.ParseUint(args[0], 10, 16); err != nil {
1744 log.Fatalf("bad usage %q", args[0])
1746 // Does not influence certificate association data, so we can accept other numbers.
1747 log.Printf("warning: continuing with unrecognized tlsa usage %d", v)
1748 usage = adns.TLSAUsage(v)
1752 var selector adns.TLSASelector
1753 switch strings.ToLower(args[1]) {
1754 case "cert", strconv.Itoa(int(adns.TLSASelectorCert)):
1755 selector = adns.TLSASelectorCert
1756 case "spki", strconv.Itoa(int(adns.TLSASelectorSPKI)):
1757 selector = adns.TLSASelectorSPKI
1759 log.Fatalf("bad selector %q", args[1])
1762 var matchType adns.TLSAMatchType
1763 switch strings.ToLower(args[2]) {
1764 case "full", strconv.Itoa(int(adns.TLSAMatchTypeFull)):
1765 matchType = adns.TLSAMatchTypeFull
1766 case "sha2-256", strconv.Itoa(int(adns.TLSAMatchTypeSHA256)):
1767 matchType = adns.TLSAMatchTypeSHA256
1768 case "sha2-512", strconv.Itoa(int(adns.TLSAMatchTypeSHA512)):
1769 matchType = adns.TLSAMatchTypeSHA512
1771 log.Fatalf("bad matchtype %q", args[2])
1774 buf, err := os.ReadFile(args[3])
1775 xcheckf(err, "reading certificate")
1777 var block *pem.Block
1778 block, buf = pem.Decode(buf)
1782 extra = " (with leftover data from pem file)"
1784 if selector == adns.TLSASelectorCert {
1785 log.Fatalf("no certificate found in pem file%s", extra)
1787 log.Fatalf("no certificate or public or private key found in pem file%s", extra)
1790 var cert *x509.Certificate
1792 if block.Type == "CERTIFICATE" {
1793 cert, err = x509.ParseCertificate(block.Bytes)
1794 xcheckf(err, "parse certificate")
1796 case adns.TLSASelectorCert:
1798 case adns.TLSASelectorSPKI:
1799 data = cert.RawSubjectPublicKeyInfo
1801 } else if selector == adns.TLSASelectorCert {
1802 // We need a certificate, just a public/private key won't do.
1803 log.Printf("skipping pem type %q, certificate is required", block.Type)
1806 var privKey, pubKey any
1810 _, err := x509.ParsePKIXPublicKey(block.Bytes)
1811 xcheckf(err, "parse pkix subject public key info (spki)")
1813 case "EC PRIVATE KEY":
1814 privKey, err = x509.ParseECPrivateKey(block.Bytes)
1815 xcheckf(err, "parse ec private key")
1816 case "RSA PRIVATE KEY":
1817 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
1818 xcheckf(err, "parse pkcs#1 rsa private key")
1819 case "RSA PUBLIC KEY":
1820 pubKey, err = x509.ParsePKCS1PublicKey(block.Bytes)
1821 xcheckf(err, "parse pkcs#1 rsa public key")
1823 // PKCS#8 private key
1824 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
1825 xcheckf(err, "parse pkcs#8 private key")
1827 log.Printf("skipping unrecognized pem type %q", block.Type)
1831 if pubKey == nil && privKey != nil {
1832 if signer, ok := privKey.(crypto.Signer); !ok {
1833 log.Fatalf("private key of type %T is not a signer, cannot get public key", privKey)
1835 pubKey = signer.Public()
1839 // Should not happen.
1840 log.Fatalf("internal error: did not find private or public key")
1842 data, err = x509.MarshalPKIXPublicKey(pubKey)
1843 xcheckf(err, "marshal pkix subject public key info (spki)")
1848 case adns.TLSAMatchTypeFull:
1849 case adns.TLSAMatchTypeSHA256:
1850 p := sha256.Sum256(data)
1852 case adns.TLSAMatchTypeSHA512:
1853 p := sha512.Sum512(data)
1856 fmt.Printf("%d %d %d %x\n", usage, selector, matchType, data)
1861func cmdDNSLookup(c *cmd) {
1862 c.params = "[ptr | mx | cname | ips | a | aaaa | ns | txt | srv | tlsa] name"
1863 c.help = `Lookup DNS name of given type.
1865Lookup always prints whether the response was DNSSEC-protected.
1869mox dns lookup ptr 1.1.1.1
1870mox dns lookup mx xmox.nl
1871mox dns lookup txt _dmarc.xmox.nl.
1872mox dns lookup tlsa _25._tcp.xmox.nl
1880 resolver := dns.StrictResolver{Pkg: "dns"}
1882 // like xparseDomain, but treat unparseable domain as an ASCII name so names with
1883 // underscores are still looked up, e,g <selector>._domainkey.<host>.
1884 xdomain := func(s string) dns.Domain {
1885 d, err := dns.ParseDomain(s)
1887 return dns.Domain{ASCII: strings.TrimSuffix(s, ".")}
1892 cmd, name := args[0], args[1]
1896 ip := xparseIP(name, "ip")
1897 ptrs, result, err := resolver.LookupAddr(context.Background(), ip.String())
1899 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
1901 fmt.Printf("names (%d, %s):\n", len(ptrs), dnssecStatus(result.Authentic))
1902 for _, ptr := range ptrs {
1903 fmt.Printf("- %s\n", ptr)
1907 name := xdomain(name)
1908 mxl, result, err := resolver.LookupMX(context.Background(), name.ASCII+".")
1910 log.Printf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
1911 // We can still have valid records...
1913 fmt.Printf("mx records (%d, %s):\n", len(mxl), dnssecStatus(result.Authentic))
1914 for _, mx := range mxl {
1915 fmt.Printf("- %s, preference %d\n", mx.Host, mx.Pref)
1919 name := xdomain(name)
1920 target, result, err := resolver.LookupCNAME(context.Background(), name.ASCII+".")
1922 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
1924 fmt.Printf("%s (%s)\n", target, dnssecStatus(result.Authentic))
1926 case "ips", "a", "aaaa":
1930 } else if cmd == "aaaa" {
1933 name := xdomain(name)
1934 ips, result, err := resolver.LookupIP(context.Background(), network, name.ASCII+".")
1936 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
1938 fmt.Printf("records (%d, %s):\n", len(ips), dnssecStatus(result.Authentic))
1939 for _, ip := range ips {
1940 fmt.Printf("- %s\n", ip)
1944 name := xdomain(name)
1945 nsl, result, err := resolver.LookupNS(context.Background(), name.ASCII+".")
1947 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
1949 fmt.Printf("ns records (%d, %s):\n", len(nsl), dnssecStatus(result.Authentic))
1950 for _, ns := range nsl {
1951 fmt.Printf("- %s\n", ns)
1955 host := xdomain(name)
1956 l, result, err := resolver.LookupTXT(context.Background(), host.ASCII+".")
1958 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
1960 fmt.Printf("txt records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
1961 for _, txt := range l {
1962 fmt.Printf("- %s\n", txt)
1966 host := xdomain(name)
1967 _, l, result, err := resolver.LookupSRV(context.Background(), "", "", host.ASCII+".")
1969 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
1971 fmt.Printf("srv records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
1972 for _, srv := range l {
1973 fmt.Printf("- host %s, port %d, priority %d, weight %d\n", srv.Target, srv.Port, srv.Priority, srv.Weight)
1977 host := xdomain(name)
1978 l, result, err := resolver.LookupTLSA(context.Background(), 0, "", host.ASCII+".")
1980 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
1982 fmt.Printf("tlsa records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
1983 for _, tlsa := range l {
1984 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)
1987 log.Fatalf("unknown record type %q", args[0])
1991func cmdDKIMGened25519(c *cmd) {
1992 c.params = ">$selector._domainkey.$domain.ed25519.privatekey.pkcs8.pem"
1993 c.help = `Generate a new ed25519 key for use with DKIM.
1995Ed25519 keys are much smaller than RSA keys of comparable cryptographic
1996strength. This is convenient because of maximum DNS message sizes. At the time
1997of writing, not many mail servers appear to support ed25519 DKIM keys though,
1998so it is recommended to sign messages with both RSA and ed25519 keys.
2000 if len(c.Parse()) != 0 {
2004 buf, err := mox.MakeDKIMEd25519Key(dns.Domain{}, dns.Domain{})
2005 xcheckf(err, "making dkim ed25519 key")
2006 _, err = os.Stdout.Write(buf)
2007 xcheckf(err, "writing dkim ed25519 key")
2010func cmdDKIMTXT(c *cmd) {
2011 c.params = "<$selector._domainkey.$domain.key.pkcs8.pem"
2012 c.help = `Print a DKIM DNS TXT record with the public key derived from the private key read from stdin.
2014The DNS should be configured as a TXT record at $selector._domainkey.$domain.
2016 if len(c.Parse()) != 0 {
2020 privKey, err := parseDKIMKey(os.Stdin)
2021 xcheckf(err, "reading dkim private key from stdin")
2025 Hashes: []string{"sha256"},
2026 Flags: []string{"s"},
2029 switch key := privKey.(type) {
2030 case *rsa.PrivateKey:
2031 r.PublicKey = key.Public()
2032 case ed25519.PrivateKey:
2033 r.PublicKey = key.Public()
2036 log.Fatalf("unsupported private key type %T, must be rsa or ed25519", privKey)
2039 record, err := r.Record()
2040 xcheckf(err, "making record")
2041 fmt.Print("<selector>._domainkey.<your.domain.> TXT ")
2045 s, record = record[:100], record[100:]
2049 fmt.Printf(`"%s" `, s)
2054func parseDKIMKey(r io.Reader) (any, error) {
2055 buf, err := io.ReadAll(r)
2057 return nil, fmt.Errorf("reading pem from stdin: %v", err)
2059 b, _ := pem.Decode(buf)
2061 return nil, fmt.Errorf("decoding pem: %v", err)
2063 privKey, err := x509.ParsePKCS8PrivateKey(b.Bytes)
2065 return nil, fmt.Errorf("parsing private key: %v", err)
2070func cmdDKIMVerify(c *cmd) {
2071 c.params = "message"
2072 c.help = `Verify the DKIM signatures in a message and print the results.
2074The message is parsed, and the DKIM-Signature headers are validated. Validation
2075of older messages may fail because the DNS records have been removed or changed
2076by now, or because the signature header may have specified an expiration time
2084 msgf, err := os.Open(args[0])
2085 xcheckf(err, "open message")
2087 results, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, false, dkim.DefaultPolicy, msgf, true)
2088 xcheckf(err, "dkim verify")
2090 for _, result := range results {
2092 if result.Sig == nil {
2093 log.Printf("warning: could not parse signature")
2095 sigh, err = result.Sig.Header()
2097 log.Printf("warning: packing signature: %s", err)
2101 if result.Record == nil {
2102 log.Printf("warning: missing DNS record")
2104 txt, err = result.Record.Record()
2106 log.Printf("warning: packing record: %s", err)
2109 fmt.Printf("status %q, err %v\nrecord %q\nheader %s\n", result.Status, result.Err, txt, sigh)
2113func cmdDKIMSign(c *cmd) {
2114 c.params = "message"
2115 c.help = `Sign a message, adding DKIM-Signature headers based on the domain in the From header.
2117The message is parsed, the domain looked up in the configuration files, and
2118DKIM-Signature headers generated. The message is printed with the DKIM-Signature
2126 msgf, err := os.Open(args[0])
2127 xcheckf(err, "open message")
2130 p, err := message.Parse(c.log.Logger, true, msgf)
2131 xcheckf(err, "parsing message")
2133 if len(p.Envelope.From) != 1 {
2134 log.Fatalf("found %d from headers, need exactly 1", len(p.Envelope.From))
2136 localpart, err := smtp.ParseLocalpart(p.Envelope.From[0].User)
2137 xcheckf(err, "parsing localpart of address in from-header")
2138 dom, err := dns.ParseDomain(p.Envelope.From[0].Host)
2139 xcheckf(err, "parsing domain of address in from-header")
2143 domConf, ok := mox.Conf.Domain(dom)
2145 log.Fatalf("domain %s not configured", dom)
2148 selectors := mox.DKIMSelectors(domConf.DKIM)
2149 headers, err := dkim.Sign(context.Background(), c.log.Logger, localpart, dom, selectors, false, msgf)
2150 xcheckf(err, "signing message with dkim")
2152 log.Fatalf("no DKIM configured for domain %s", dom)
2154 _, err = fmt.Fprint(os.Stdout, headers)
2155 xcheckf(err, "write headers")
2156 _, err = io.Copy(os.Stdout, msgf)
2157 xcheckf(err, "write message")
2160func cmdDKIMLookup(c *cmd) {
2161 c.params = "selector domain"
2162 c.help = "Lookup and print the DKIM record for the selector at the domain."
2168 selector := xparseDomain(args[0], "selector")
2169 domain := xparseDomain(args[1], "domain")
2171 status, record, txt, authentic, err := dkim.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, selector, domain)
2173 fmt.Printf("error: %s\n", err)
2175 if status != dkim.StatusNeutral {
2176 fmt.Printf("status: %s\n", status)
2179 fmt.Printf("TXT record: %s\n", txt)
2182 fmt.Println("dnssec-signed: yes")
2184 fmt.Println("dnssec-signed: no")
2187 fmt.Printf("Record:\n")
2189 "version", record.Version,
2190 "hashes", record.Hashes,
2192 "notes", record.Notes,
2193 "services", record.Services,
2194 "flags", record.Flags,
2196 for i := 0; i < len(pairs); i += 2 {
2197 fmt.Printf("\t%s: %v\n", pairs[i], pairs[i+1])
2202func cmdDMARCLookup(c *cmd) {
2204 c.help = "Lookup dmarc policy for domain, a DNS TXT record at _dmarc.<domain>, validate and print it."
2210 fromdomain := xparseDomain(args[0], "domain")
2211 _, domain, _, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, fromdomain)
2212 xcheckf(err, "dmarc lookup domain %s", fromdomain)
2213 fmt.Printf("dmarc record at domain %s: %s\n", domain, txt)
2214 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2217func dnssecStatus(v bool) string {
2219 return "with dnssec"
2221 return "without dnssec"
2224func cmdDMARCVerify(c *cmd) {
2225 c.params = "remoteip mailfromaddress helodomain < message"
2226 c.help = `Parse an email message and evaluate it against the DMARC policy of the domain in the From-header.
2228mailfromaddress and helodomain are used for SPF validation. If both are empty,
2229SPF validation is skipped.
2231mailfromaddress should be the address used as MAIL FROM in the SMTP session.
2232For DSN messages, that address may be empty. The helo domain was specified at
2233the beginning of the SMTP transaction that delivered the message. These values
2234can be found in message headers.
2241 var heloDomain *dns.Domain
2243 remoteIP := xparseIP(args[0], "remoteip")
2245 var mailfrom *smtp.Address
2247 a, err := smtp.ParseAddress(args[1])
2248 xcheckf(err, "parsing mailfrom address")
2252 d := xparseDomain(args[2], "helo domain")
2255 var received *spf.Received
2256 spfStatus := spf.StatusNone
2257 var spfIdentity *dns.Domain
2258 if mailfrom != nil || heloDomain != nil {
2259 spfArgs := spf.Args{
2261 LocalIP: net.ParseIP("127.0.0.1"),
2262 LocalHostname: dns.Domain{ASCII: "localhost"},
2264 if mailfrom != nil {
2265 spfArgs.MailFromLocalpart = mailfrom.Localpart
2266 spfArgs.MailFromDomain = mailfrom.Domain
2268 if heloDomain != nil {
2269 spfArgs.HelloDomain = dns.IPDomain{Domain: *heloDomain}
2271 rspf, spfDomain, expl, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfArgs)
2273 log.Printf("spf verify: %v (explanation: %q, authentic %v)", err, expl, authentic)
2276 spfStatus = received.Result
2277 // todo: should probably potentially do two separate spf validations
2278 if mailfrom != nil {
2279 spfIdentity = &mailfrom.Domain
2281 spfIdentity = heloDomain
2283 fmt.Printf("spf result: %s: %s (%s)\n", spfDomain, spfStatus, dnssecStatus(authentic))
2287 data, err := io.ReadAll(os.Stdin)
2288 xcheckf(err, "read message")
2289 dmarcFrom, _, _, err := message.From(c.log.Logger, false, bytes.NewReader(data))
2290 xcheckf(err, "extract dmarc from message")
2292 const ignoreTestMode = false
2293 dkimResults, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, true, func(*dkim.Sig) error { return nil }, bytes.NewReader(data), ignoreTestMode)
2294 xcheckf(err, "dkim verify")
2295 for _, r := range dkimResults {
2296 fmt.Printf("dkim result: %q (err %v)\n", r.Status, r.Err)
2299 _, result := dmarc.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, dmarcFrom.Domain, dkimResults, spfStatus, spfIdentity, false)
2300 xcheckf(result.Err, "dmarc verify")
2301 fmt.Printf("dmarc from: %s\ndmarc status: %q\ndmarc reject: %v\ncmarc record: %s\n", dmarcFrom, result.Status, result.Reject, result.Record)
2304func cmdDMARCCheckreportaddrs(c *cmd) {
2306 c.help = `For each reporting address in the domain's DMARC record, check if it has opted into receiving reports (if needed).
2308A DMARC record can request reports about DMARC evaluations to be sent to an
2309email/http address. If the organizational domains of that of the DMARC record
2310and that of the report destination address do not match, the destination
2311address must opt-in to receiving DMARC reports by creating a DMARC record at
2312<dmarcdomain>._report._dmarc.<reportdestdomain>.
2319 dom := xparseDomain(args[0], "domain")
2320 _, domain, record, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dom)
2321 xcheckf(err, "dmarc lookup domain %s", dom)
2322 fmt.Printf("dmarc record at domain %s: %q\n", domain, txt)
2323 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2325 check := func(kind, addr string) {
2328 printResult := func(format string, args ...any) {
2329 fmt.Printf("%s %s: %s (%s)\n", kind, addr, fmt.Sprintf(format, args...), dnssecStatus(authentic))
2332 u, err := url.Parse(addr)
2334 printResult("parsing uri: %v (skipping)", addr, err)
2337 var destdom dns.Domain
2340 a, err := smtp.ParseAddress(u.Opaque)
2342 printResult("parsing destination email address %s: %v (skipping)", u.Opaque, err)
2347 printResult("unrecognized scheme in reporting address %s (skipping)", u.Scheme)
2351 if publicsuffix.Lookup(context.Background(), c.log.Logger, dom) == publicsuffix.Lookup(context.Background(), c.log.Logger, destdom) {
2352 printResult("pass (same organizational domain)")
2356 accepts, status, _, txts, authentic, err := dmarc.LookupExternalReportsAccepted(context.Background(), c.log.Logger, dns.StrictResolver{}, domain, destdom)
2358 txtaddr := fmt.Sprintf("%s._report._dmarc.%s", domain.ASCII, destdom.ASCII)
2360 txtstr = fmt.Sprintf(" (no txt records %s)", txtaddr)
2362 txtstr = fmt.Sprintf(" (txt record %s: %q)", txtaddr, txts)
2364 if status != dmarc.StatusNone {
2365 printResult("fail: %s%s", err, txtstr)
2367 printResult("pass%s", txtstr)
2368 } else if err != nil {
2369 printResult("fail: %s%s", err, txtstr)
2371 printResult("fail%s", txtstr)
2375 for _, uri := range record.AggregateReportAddresses {
2376 check("aggregate reporting", uri.Address)
2378 for _, uri := range record.FailureReportAddresses {
2379 check("failure reporting", uri.Address)
2383func cmdDMARCParsereportmsg(c *cmd) {
2384 c.params = "message ..."
2385 c.help = `Parse a DMARC report from an email message, and print its extracted details.
2387DMARC reports are periodically mailed, if requested in the DMARC DNS record of
2388a domain. Reports are sent by mail servers that received messages with our
2389domain in a From header. This may or may not be legatimate email. DMARC reports
2390contain summaries of evaluations of DMARC and DKIM/SPF, which can help
2391understand email deliverability problems.
2398 for _, arg := range args {
2399 f, err := os.Open(arg)
2400 xcheckf(err, "open %q", arg)
2401 feedback, err := dmarcrpt.ParseMessageReport(c.log.Logger, f)
2402 xcheckf(err, "parse report in %q", arg)
2403 meta := feedback.ReportMetadata
2404 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)
2405 if len(meta.Errors) > 0 {
2406 fmt.Printf("Errors:\n")
2407 for _, s := range meta.Errors {
2408 fmt.Printf("\t- %s\n", s)
2411 pol := feedback.PolicyPublished
2412 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)
2413 for _, record := range feedback.Records {
2414 idents := record.Identifiers
2415 fmt.Printf("\theaderfrom %q, envelopes from %q, to %q\n", idents.HeaderFrom, idents.EnvelopeFrom, idents.EnvelopeTo)
2416 eval := record.Row.PolicyEvaluated
2418 for _, reason := range eval.Reasons {
2419 reasons += "; " + string(reason.Type)
2420 if reason.Comment != "" {
2421 reasons += fmt.Sprintf(": %q", reason.Comment)
2424 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)
2425 for _, dkim := range record.AuthResults.DKIM {
2427 if dkim.HumanResult != "" {
2428 result = fmt.Sprintf(": %q", dkim.HumanResult)
2430 fmt.Printf("\t\tdkim %s; domain %q selector %q%s\n", dkim.Result, dkim.Domain, dkim.Selector, result)
2432 for _, spf := range record.AuthResults.SPF {
2433 fmt.Printf("\t\tspf %s; domain %q scope %q\n", spf.Result, spf.Domain, spf.Scope)
2439func cmdDMARCDBAddReport(c *cmd) {
2441 c.params = "fromdomain < message"
2442 c.help = "Add a DMARC report to the database."
2450 fromdomain := xparseDomain(args[0], "domain")
2451 fmt.Fprintln(os.Stderr, "reading report message from stdin")
2452 report, err := dmarcrpt.ParseMessageReport(c.log.Logger, os.Stdin)
2453 xcheckf(err, "parse message")
2454 err = dmarcdb.AddReport(context.Background(), report, fromdomain)
2455 xcheckf(err, "add dmarc report")
2458func cmdTLSRPTLookup(c *cmd) {
2460 c.help = `Lookup the TLSRPT record for the domain.
2462A TLSRPT record typically contains an email address where reports about TLS
2463connectivity should be sent. Mail servers attempting delivery to our domain
2464should attempt to use TLS. TLSRPT lets them report how many connection
2465successfully used TLS, and how what kind of errors occurred otherwise.
2472 d := xparseDomain(args[0], "domain")
2473 _, txt, err := tlsrpt.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, d)
2474 xcheckf(err, "tlsrpt lookup for %s", d)
2478func cmdTLSRPTParsereportmsg(c *cmd) {
2479 c.params = "message ..."
2480 c.help = `Parse and print the TLSRPT in the message.
2482The report is printed in formatted JSON.
2489 for _, arg := range args {
2490 f, err := os.Open(arg)
2491 xcheckf(err, "open %q", arg)
2492 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, f)
2493 xcheckf(err, "parse report in %q", arg)
2494 // todo future: only print the highlights?
2495 enc := json.NewEncoder(os.Stdout)
2496 enc.SetIndent("", "\t")
2497 err = enc.Encode(reportJSON)
2498 xcheckf(err, "write report")
2502func cmdSPFCheck(c *cmd) {
2503 c.params = "domain ip"
2504 c.help = `Check the status of IP for the policy published in DNS for the domain.
2506IPs may be allowed to send for a domain, or disallowed, and several shades in
2507between. If not allowed, an explanation may be provided by the policy. If so,
2508the explanation is printed. The SPF mechanism that matched (if any) is also
2516 domain := xparseDomain(args[0], "domain")
2518 ip := xparseIP(args[1], "ip")
2520 spfargs := spf.Args{
2522 MailFromLocalpart: "user",
2523 MailFromDomain: domain,
2524 HelloDomain: dns.IPDomain{Domain: domain},
2525 LocalIP: net.ParseIP("127.0.0.1"),
2526 LocalHostname: dns.Domain{ASCII: "localhost"},
2528 r, _, explanation, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfargs)
2530 fmt.Printf("error: %s\n", err)
2532 if explanation != "" {
2533 fmt.Printf("explanation: %s\n", explanation)
2535 fmt.Printf("status: %s (%s)\n", r.Result, dnssecStatus(authentic))
2536 if r.Mechanism != "" {
2537 fmt.Printf("mechanism: %s\n", r.Mechanism)
2541func cmdSPFParse(c *cmd) {
2542 c.params = "txtrecord"
2543 c.help = "Parse the record as SPF record. If valid, nothing is printed."
2549 _, _, err := spf.ParseRecord(args[0])
2550 xcheckf(err, "parsing record")
2553func cmdSPFLookup(c *cmd) {
2555 c.help = "Lookup the SPF record for the domain and print it."
2561 domain := xparseDomain(args[0], "domain")
2562 _, txt, _, authentic, err := spf.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
2563 xcheckf(err, "spf lookup for %s", domain)
2565 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2568func cmdMTASTSLookup(c *cmd) {
2570 c.help = `Lookup the MTASTS record and policy for the domain.
2572MTA-STS is a mechanism for a domain to specify if it requires TLS connections
2573for delivering email. If a domain has a valid MTA-STS DNS TXT record at
2574_mta-sts.<domain> it signals it implements MTA-STS. A policy can then be
2575fetched at https://mta-sts.<domain>/.well-known/mta-sts.txt. The policy
2576specifies the mode (enforce, testing, none), which MX servers support TLS and
2577should be used, and how long the policy can be cached.
2584 domain := xparseDomain(args[0], "domain")
2586 record, policy, _, err := mtasts.Get(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
2588 fmt.Printf("error: %s\n", err)
2591 fmt.Printf("DNS TXT record _mta-sts.%s: %s\n", domain.ASCII, record.String())
2595 fmt.Printf("policy at https://mta-sts.%s/.well-known/mta-sts.txt:\n", domain.ASCII)
2596 fmt.Printf("%s", policy.String())
2600func cmdRetrain(c *cmd) {
2601 c.params = "accountname"
2602 c.help = `Recreate and retrain the junk filter for the account.
2604Useful after having made changes to the junk filter configuration, or if the
2605implementation has changed.
2613 ctlcmdRetrain(xctl(), args[0])
2616func ctlcmdRetrain(ctl *ctl, account string) {
2617 ctl.xwrite("retrain")
2622func cmdTLSRPTDBAddReport(c *cmd) {
2624 c.params = "< message"
2625 c.help = "Parse a TLS report from the message and add it to the database."
2627 c.flag.BoolVar(&hostReport, "hostreport", false, "report for a host instead of domain")
2635 // First read message, to get the From-header. Then parse it as TLSRPT.
2636 fmt.Fprintln(os.Stderr, "reading report message from stdin")
2637 buf, err := io.ReadAll(os.Stdin)
2638 xcheckf(err, "reading message")
2639 part, err := message.Parse(c.log.Logger, true, bytes.NewReader(buf))
2640 xcheckf(err, "parsing message")
2641 if part.Envelope == nil || len(part.Envelope.From) != 1 {
2642 log.Fatalf("message must have one From-header")
2644 from := part.Envelope.From[0]
2645 domain := xparseDomain(from.Host, "domain")
2647 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, bytes.NewReader(buf))
2648 xcheckf(err, "parsing tls report in message")
2650 mailfrom := from.User + "@" + from.Host // todo future: should escape and such
2651 report := reportJSON.Convert()
2652 err = tlsrptdb.AddReport(context.Background(), c.log, domain, mailfrom, hostReport, &report)
2653 xcheckf(err, "add tls report to database")
2656func cmdDNSBLCheck(c *cmd) {
2657 c.params = "zone ip"
2658 c.help = `Test if IP is in the DNS blocklist of the zone, e.g. bl.spamcop.net.
2660If the IP is in the blocklist, an explanation is printed. This is typically a
2661URL with more information.
2668 zone := xparseDomain(args[0], "zone")
2669 ip := xparseIP(args[1], "ip")
2671 status, explanation, err := dnsbl.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, zone, ip)
2672 fmt.Printf("status: %s\n", status)
2673 if status == dnsbl.StatusFail {
2674 fmt.Printf("explanation: %q\n", explanation)
2677 fmt.Printf("error: %s\n", err)
2681func cmdDNSBLCheckhealth(c *cmd) {
2683 c.help = `Check the health of the DNS blocklist represented by zone, e.g. bl.spamcop.net.
2685The health of a DNS blocklist can be checked by querying for 127.0.0.1 and
2686127.0.0.2. The second must and the first must not be present.
2693 zone := xparseDomain(args[0], "zone")
2694 err := dnsbl.CheckHealth(context.Background(), c.log.Logger, dns.StrictResolver{}, zone)
2695 xcheckf(err, "unhealthy")
2696 fmt.Println("healthy")
2699func cmdCheckupdate(c *cmd) {
2700 c.help = `Check if a newer version of mox is available.
2702A single DNS TXT lookup to _updates.xmox.nl tells if a new version is
2703available. If so, a changelog is fetched from https://updates.xmox.nl, and the
2704individual entries verified with a builtin public key. The changelog is
2707 if len(c.Parse()) != 0 {
2712 current, lastknown, _, err := mox.LastKnown()
2714 log.Printf("getting last known version: %s", err)
2716 fmt.Printf("last known version: %s\n", lastknown)
2717 fmt.Printf("current version: %s\n", current)
2719 latest, _, err := updates.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dns.Domain{ASCII: changelogDomain})
2720 xcheckf(err, "lookup of latest version")
2721 fmt.Printf("latest version: %s\n", latest)
2723 if latest.After(current) {
2724 changelog, err := updates.FetchChangelog(context.Background(), c.log.Logger, changelogURL, current, changelogPubKey)
2725 xcheckf(err, "fetching changelog")
2726 if len(changelog.Changes) == 0 {
2727 log.Printf("no changes in changelog")
2730 fmt.Println("Changelog")
2731 for _, c := range changelog.Changes {
2732 fmt.Println("\n" + strings.TrimSpace(c.Text))
2737func cmdCid(c *cmd) {
2739 c.help = `Turn an ID from a Received header into a cid, for looking up in logs.
2741A cid is essentially a connection counter initialized when mox starts. Each log
2742line contains a cid. Received headers added by mox contain a unique ID that can
2743be decrypted to a cid by admin of a mox instance only.
2751 recvidpath := mox.DataDirPath("receivedid.key")
2752 recvidbuf, err := os.ReadFile(recvidpath)
2753 xcheckf(err, "reading %s", recvidpath)
2754 if len(recvidbuf) != 16+8 {
2755 log.Fatalf("bad data in %s: got %d bytes, expect 16+8=24", recvidpath, len(recvidbuf))
2757 err = mox.ReceivedIDInit(recvidbuf[:16], recvidbuf[16:])
2758 xcheckf(err, "init receivedid")
2760 cid, err := mox.ReceivedToCid(args[0])
2761 xcheckf(err, "received id to cid")
2762 fmt.Printf("%x\n", cid)
2765func cmdVersion(c *cmd) {
2766 c.help = "Prints this mox version."
2767 if len(c.Parse()) != 0 {
2770 fmt.Println(moxvar.Version)
2771 fmt.Printf("%s %s/%s\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
2774// 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.
2775func cmdBumpUIDValidity(c *cmd) {
2776 c.params = "account [mailbox]"
2777 c.help = `Change the IMAP UID validity of the mailbox, causing IMAP clients to refetch messages.
2779This can be useful after manually repairing metadata about the account/mailbox.
2781Opens account database file directly. Ensure mox does not have the account
2782open, or is not running.
2785 if len(args) != 1 && len(args) != 2 {
2790 a, err := store.OpenAccount(c.log, args[0])
2791 xcheckf(err, "open account")
2793 if err := a.Close(); err != nil {
2794 log.Printf("closing account: %v", err)
2798 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
2799 uidvalidity, err := a.NextUIDValidity(tx)
2801 return fmt.Errorf("assigning next uid validity: %v", err)
2804 q := bstore.QueryTx[store.Mailbox](tx)
2806 q.FilterEqual("Name", args[1])
2808 mbl, err := q.SortAsc("Name").List()
2810 return fmt.Errorf("looking up mailbox: %v", err)
2812 if len(args) == 2 && len(mbl) != 1 {
2813 return fmt.Errorf("looking up mailbox %q, found %d mailboxes", args[1], len(mbl))
2815 for _, mb := range mbl {
2816 mb.UIDValidity = uidvalidity
2817 err = tx.Update(&mb)
2819 return fmt.Errorf("updating uid validity for mailbox: %v", err)
2821 fmt.Printf("uid validity for %q updated to %d\n", mb.Name, uidvalidity)
2825 xcheckf(err, "updating database")
2828func cmdReassignUIDs(c *cmd) {
2829 c.params = "account [mailboxid]"
2830 c.help = `Reassign UIDs in one mailbox or all mailboxes in an account and bump UID validity, causing IMAP clients to refetch messages.
2832Opens account database file directly. Ensure mox does not have the account
2833open, or is not running.
2836 if len(args) != 1 && len(args) != 2 {
2843 mailboxID, err = strconv.ParseInt(args[1], 10, 64)
2844 xcheckf(err, "parsing mailbox id")
2848 a, err := store.OpenAccount(c.log, args[0])
2849 xcheckf(err, "open account")
2851 if err := a.Close(); err != nil {
2852 log.Printf("closing account: %v", err)
2856 // Gather the last-assigned UIDs per mailbox.
2857 uidlasts := map[int64]store.UID{}
2859 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
2860 // Reassign UIDs, going per mailbox. We assign starting at 1, only changing the
2861 // message if it isn't already at the intended UID. Doing it in this order ensures
2862 // we don't get into trouble with duplicate UIDs for a mailbox. We assign a new
2863 // modseq. Not strictly needed, for doesn't hurt.
2864 modseq, err := a.NextModSeq(tx)
2865 xcheckf(err, "assigning next modseq")
2867 q := bstore.QueryTx[store.Message](tx)
2869 q.FilterNonzero(store.Message{MailboxID: mailboxID})
2871 q.SortAsc("MailboxID", "UID")
2872 err = q.ForEach(func(m store.Message) error {
2873 uidlasts[m.MailboxID]++
2874 uid := uidlasts[m.MailboxID]
2878 if err := tx.Update(&m); err != nil {
2879 return fmt.Errorf("updating uid for message: %v", err)
2885 return fmt.Errorf("reading through messages: %v", err)
2888 // Now update the uidnext and uidvalidity for each mailbox.
2889 err = bstore.QueryTx[store.Mailbox](tx).ForEach(func(mb store.Mailbox) error {
2890 // Assign each mailbox a completely new uidvalidity.
2891 uidvalidity, err := a.NextUIDValidity(tx)
2893 return fmt.Errorf("assigning next uid validity: %v", err)
2896 if mb.UIDValidity >= uidvalidity {
2897 // This should not happen, but since we're fixing things up after a hypothetical
2898 // mishap, might as well account for inconsistent uidvalidity.
2899 next := store.NextUIDValidity{ID: 1, Next: mb.UIDValidity + 2}
2900 if err := tx.Update(&next); err != nil {
2901 log.Printf("updating nextuidvalidity: %v, continuing", err)
2905 mb.UIDValidity = uidvalidity
2907 mb.UIDNext = uidlasts[mb.ID] + 1
2908 if err := tx.Update(&mb); err != nil {
2909 return fmt.Errorf("updating uidvalidity and uidnext for mailbox: %v", err)
2914 return fmt.Errorf("updating mailboxes: %v", err)
2918 xcheckf(err, "updating database")
2921func cmdFixUIDMeta(c *cmd) {
2922 c.params = "account"
2923 c.help = `Fix inconsistent UIDVALIDITY and UIDNEXT in messages/mailboxes/account.
2925The next UID to use for a message in a mailbox should always be higher than any
2926existing message UID in the mailbox. If it is not, the mailbox UIDNEXT is
2929Each mailbox has a UIDVALIDITY sequence number, which should always be lower
2930than the per-account next UIDVALIDITY to use. If it is not, the account next
2931UIDVALIDITY is updated.
2933Opens account database file directly. Ensure mox does not have the account
2934open, or is not running.
2942 a, err := store.OpenAccount(c.log, args[0])
2943 xcheckf(err, "open account")
2945 if err := a.Close(); err != nil {
2946 log.Printf("closing account: %v", err)
2950 var maxUIDValidity uint32
2952 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
2953 // We look at each mailbox, retrieve its max UID and compare against the mailbox
2955 err := bstore.QueryTx[store.Mailbox](tx).ForEach(func(mb store.Mailbox) error {
2956 if mb.UIDValidity > maxUIDValidity {
2957 maxUIDValidity = mb.UIDValidity
2959 m, err := bstore.QueryTx[store.Message](tx).FilterNonzero(store.Message{MailboxID: mb.ID}).SortDesc("UID").Limit(1).Get()
2960 if err == bstore.ErrAbsent || err == nil && m.UID < mb.UIDNext {
2962 } else if err != nil {
2963 return fmt.Errorf("finding message with max uid in mailbox: %w", err)
2965 olduidnext := mb.UIDNext
2966 mb.UIDNext = m.UID + 1
2967 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)
2968 if err := tx.Update(&mb); err != nil {
2969 return fmt.Errorf("updating mailbox uidnext: %v", err)
2974 return fmt.Errorf("processing mailboxes: %v", err)
2977 uidvalidity := store.NextUIDValidity{ID: 1}
2978 if err := tx.Get(&uidvalidity); err != nil {
2979 return fmt.Errorf("reading account next uidvalidity: %v", err)
2981 if maxUIDValidity >= uidvalidity.Next {
2982 log.Printf("account next uidvalidity %d <= highest uidvalidity %d found in mailbox, resetting account next uidvalidity to %d", uidvalidity.Next, maxUIDValidity, maxUIDValidity+1)
2983 uidvalidity.Next = maxUIDValidity + 1
2984 if err := tx.Update(&uidvalidity); err != nil {
2985 return fmt.Errorf("updating account next uidvalidity: %v", err)
2991 xcheckf(err, "updating database")
2994func cmdFixmsgsize(c *cmd) {
2995 c.params = "[account]"
2996 c.help = `Ensure message sizes in the database matching the sum of the message prefix length and on-disk file size.
2998Messages with an inconsistent size are also parsed again.
3000If an inconsistency is found, you should probably also run "mox
3001bumpuidvalidity" on the mailboxes or entire account to force IMAP clients to
3014 ctlcmdFixmsgsize(xctl(), account)
3017func ctlcmdFixmsgsize(ctl *ctl, account string) {
3018 ctl.xwrite("fixmsgsize")
3021 ctl.xstreamto(os.Stdout)
3024func cmdReparse(c *cmd) {
3025 c.params = "[account]"
3026 c.help = `Parse all messages in the account or all accounts again
3028Can be useful after upgrading mox with improved message parsing. Messages are
3029parsed in batches, so other access to the mailboxes/messages are not blocked
3030while reparsing all messages.
3042 ctlcmdReparse(xctl(), account)
3045func ctlcmdReparse(ctl *ctl, account string) {
3046 ctl.xwrite("reparse")
3049 ctl.xstreamto(os.Stdout)
3052func cmdEnsureParsed(c *cmd) {
3053 c.params = "account"
3054 c.help = "Ensure messages in the database have a pre-parsed MIME form in the database."
3056 c.flag.BoolVar(&all, "all", false, "store new parsed message for all messages")
3063 a, err := store.OpenAccount(c.log, args[0])
3064 xcheckf(err, "open account")
3066 if err := a.Close(); err != nil {
3067 log.Printf("closing account: %v", err)
3072 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3073 q := bstore.QueryTx[store.Message](tx)
3074 q.FilterEqual("Expunged", false)
3075 q.FilterFn(func(m store.Message) bool {
3076 return all || m.ParsedBuf == nil
3080 return fmt.Errorf("list messages: %v", err)
3082 for _, m := range l {
3083 mr := a.MessageReader(m)
3084 p, err := message.EnsurePart(c.log.Logger, false, mr, m.Size)
3086 log.Printf("parsing message %d: %v (continuing)", m.ID, err)
3088 m.ParsedBuf, err = json.Marshal(p)
3090 return fmt.Errorf("marshal parsed message: %v", err)
3092 if err := tx.Update(&m); err != nil {
3093 return fmt.Errorf("update message: %v", err)
3099 xcheckf(err, "update messages with parsed mime structure")
3100 fmt.Printf("%d messages updated\n", n)
3103func cmdRecalculateMailboxCounts(c *cmd) {
3104 c.params = "account"
3105 c.help = `Recalculate message counts for all mailboxes in the account, and total message size for quota.
3107When a message is added to/removed from a mailbox, or when message flags change,
3108the total, unread, unseen and deleted messages are accounted, the total size of
3109the mailbox, and the total message size for the account. In case of a bug in
3110this accounting, the numbers could become incorrect. This command will find, fix
3119 ctlcmdRecalculateMailboxCounts(xctl(), args[0])
3122func ctlcmdRecalculateMailboxCounts(ctl *ctl, account string) {
3123 ctl.xwrite("recalculatemailboxcounts")
3126 ctl.xstreamto(os.Stdout)
3129func cmdMessageParse(c *cmd) {
3130 c.params = "message.eml"
3131 c.help = "Parse message, print JSON representation."
3138 f, err := os.Open(args[0])
3139 xcheckf(err, "open")
3142 part, err := message.Parse(c.log.Logger, false, f)
3143 xcheckf(err, "parsing message")
3144 err = part.Walk(c.log.Logger, nil)
3145 xcheckf(err, "parsing nested parts")
3146 enc := json.NewEncoder(os.Stdout)
3147 enc.SetIndent("", "\t")
3148 err = enc.Encode(part)
3149 xcheckf(err, "write")
3152func cmdOpenaccounts(c *cmd) {
3154 c.params = "datadir account ..."
3155 c.help = `Open and close accounts, for triggering data upgrades, for tests.
3157Opens database files directly, not going through a running mox instance.
3165 dataDir := filepath.Clean(args[0])
3166 for _, accName := range args[1:] {
3167 accDir := filepath.Join(dataDir, "accounts", accName)
3168 log.Printf("opening account %s...", accDir)
3169 a, err := store.OpenAccountDB(c.log, accDir, accName)
3170 xcheckf(err, "open account %s", accName)
3171 err = a.ThreadingWait(c.log)
3172 xcheckf(err, "wait for threading upgrade to complete for %s", accName)
3174 xcheckf(err, "close account %s", accName)
3178func cmdReassignthreads(c *cmd) {
3179 c.params = "[account]"
3180 c.help = `Reassign message threads.
3182For all accounts, or optionally only the specified account.
3184Threading for all messages in an account is first reset, and new base subject
3185and normalized message-id saved with the message. Then all messages are
3186evaluated and matched against their parents/ancestors.
3188Messages are matched based on the References header, with a fall-back to an
3189In-Reply-To header, and if neither is present/valid, based only on base
3192A References header typically points to multiple previous messages in a
3193hierarchy. From oldest ancestor to most recent parent. An In-Reply-To header
3194would have only a message-id of the parent message.
3196A message is only linked to a parent/ancestor if their base subject is the
3197same. This ensures unrelated replies, with a new subject, are placed in their
3200The base subject is lower cased, has whitespace collapsed to a single
3201space, and some components removed: leading "Re:", "Fwd:", "Fw:", or bracketed
3202tag (that mailing lists often add, e.g. "[listname]"), trailing "(fwd)", or
3203enclosing "[fwd: ...]".
3205Messages are linked to all their ancestors. If an intermediate parent/ancestor
3206message is deleted in the future, the message can still be linked to the earlier
3207ancestors. If the direct parent already wasn't available while matching, this is
3208stored as the message having a "missing link" to its stored ancestors.
3220 ctlcmdReassignthreads(xctl(), account)
3223func ctlcmdReassignthreads(ctl *ctl, account string) {
3224 ctl.xwrite("reassignthreads")
3227 ctl.xstreamto(os.Stdout)
3230func cmdReadmessages(c *cmd) {
3232 c.params = "datadir account ..."
3233 c.help = `Open account, parse several headers for all messages.
3235For performance testing.
3237Opens database files directly, not going through a running mox instance.
3240 gomaxprocs := runtime.GOMAXPROCS(0)
3241 var procs, workqueuesize, limit int
3242 c.flag.IntVar(&procs, "procs", gomaxprocs, "number of goroutines for reading messages")
3243 c.flag.IntVar(&workqueuesize, "workqueuesize", 2*gomaxprocs, "number of messages to keep in work queue")
3244 c.flag.IntVar(&limit, "limit", 0, "number of messages to process if greater than zero")
3250 type threadPrep struct {
3255 threadingFields := [][]byte{
3256 []byte("references"),
3257 []byte("in-reply-to"),
3260 dataDir := filepath.Clean(args[0])
3261 for _, accName := range args[1:] {
3262 accDir := filepath.Join(dataDir, "accounts", accName)
3263 log.Printf("opening account %s...", accDir)
3264 a, err := store.OpenAccountDB(c.log, accDir, accName)
3265 xcheckf(err, "open account %s", accName)
3267 prepareMessages := func(in, out chan moxio.Work[store.Message, threadPrep]) {
3268 headerbuf := make([]byte, 8*1024)
3269 scratch := make([]byte, 4*1024)
3277 var partialPart struct {
3281 if err := json.Unmarshal(m.ParsedBuf, &partialPart); err != nil {
3282 w.Err = fmt.Errorf("unmarshal part: %v", err)
3284 size := partialPart.BodyOffset - partialPart.HeaderOffset
3285 if int(size) > len(headerbuf) {
3286 headerbuf = make([]byte, size)
3289 buf := headerbuf[:int(size)]
3290 err := func() error {
3291 mr := a.MessageReader(m)
3294 // ReadAt returns whole buffer or error. Single read should be fast.
3295 n, err := mr.ReadAt(buf, partialPart.HeaderOffset)
3296 if err != nil || n != len(buf) {
3297 return fmt.Errorf("read header: %v", err)
3303 } else if h, err := message.ParseHeaderFields(buf, scratch, threadingFields); err != nil {
3306 w.Out.references = h["References"]
3307 w.Out.inReplyTo = h["In-Reply-To"]
3320 processMessage := func(m store.Message, prep threadPrep) error {
3322 log.Printf("%d messages (delta %s)", n, time.Since(t))
3329 wq := moxio.NewWorkQueue[store.Message, threadPrep](procs, workqueuesize, prepareMessages, processMessage)
3331 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3332 q := bstore.QueryTx[store.Message](tx)
3333 q.FilterEqual("Expunged", false)
3338 err = q.ForEach(wq.Add)
3346 xcheckf(err, "processing message")
3349 xcheckf(err, "close account %s", accName)
3350 log.Printf("account %s, total time %s", accName, time.Since(t0))