11 cryptorand "crypto/rand"
38 "golang.org/x/crypto/bcrypt"
39 "golang.org/x/text/secure/precis"
41 "github.com/mjl-/adns"
43 "github.com/mjl-/autocert"
44 "github.com/mjl-/bstore"
45 "github.com/mjl-/sconf"
46 "github.com/mjl-/sherpa"
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"
76 changelogDomain = "xmox.nl"
77 changelogURL = "https://updates.xmox.nl/changelog"
78 changelogPubKey = base64Decode("sPNiTDQzvb4FrytNEiebJhgyQzn57RwEjNbGWMM/bDY=")
81func base64Decode(s string) []byte {
82 buf, err := base64.StdEncoding.DecodeString(s)
89func envString(k, def string) string {
97var commands = []struct {
102 {"quickstart", cmdQuickstart},
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},
137 {"backup", cmdBackup},
138 {"verifydata", cmdVerifydata},
140 {"config test", cmdConfigTest},
141 {"config dnscheck", cmdConfigDNSCheck},
142 {"config dnsrecords", cmdConfigDNSRecords},
143 {"config describe-domains", cmdConfigDescribeDomains},
144 {"config describe-static", cmdConfigDescribeStatic},
145 {"config account add", cmdConfigAccountAdd},
146 {"config account rm", cmdConfigAccountRemove},
147 {"config address add", cmdConfigAddressAdd},
148 {"config address rm", cmdConfigAddressRemove},
149 {"config domain add", cmdConfigDomainAdd},
150 {"config domain rm", cmdConfigDomainRemove},
151 {"config alias list", cmdConfigAliasList},
152 {"config alias print", cmdConfigAliasPrint},
153 {"config alias add", cmdConfigAliasAdd},
154 {"config alias update", cmdConfigAliasUpdate},
155 {"config alias rm", cmdConfigAliasRemove},
156 {"config alias addaddr", cmdConfigAliasAddaddr},
157 {"config alias rmaddr", cmdConfigAliasRemoveaddr},
159 {"config describe-sendmail", cmdConfigDescribeSendmail},
160 {"config printservice", cmdConfigPrintservice},
161 {"config ensureacmehostprivatekeys", cmdConfigEnsureACMEHostprivatekeys},
162 {"config example", cmdConfigExample},
164 {"checkupdate", cmdCheckupdate},
166 {"clientconfig", cmdClientConfig},
167 {"deliver", cmdDeliver},
168 // 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
169 {"dane dial", cmdDANEDial},
170 {"dane dialmx", cmdDANEDialmx},
171 {"dane makerecord", cmdDANEMakeRecord},
172 {"dns lookup", cmdDNSLookup},
173 {"dkim gened25519", cmdDKIMGened25519},
174 {"dkim genrsa", cmdDKIMGenrsa},
175 {"dkim lookup", cmdDKIMLookup},
176 {"dkim txt", cmdDKIMTXT},
177 {"dkim verify", cmdDKIMVerify},
178 {"dkim sign", cmdDKIMSign},
179 {"dmarc lookup", cmdDMARCLookup},
180 {"dmarc parsereportmsg", cmdDMARCParsereportmsg},
181 {"dmarc verify", cmdDMARCVerify},
182 {"dmarc checkreportaddrs", cmdDMARCCheckreportaddrs},
183 {"dnsbl check", cmdDNSBLCheck},
184 {"dnsbl checkhealth", cmdDNSBLCheckhealth},
185 {"mtasts lookup", cmdMTASTSLookup},
186 {"retrain", cmdRetrain},
187 {"sendmail", cmdSendmail},
188 {"spf check", cmdSPFCheck},
189 {"spf lookup", cmdSPFLookup},
190 {"spf parse", cmdSPFParse},
191 {"tlsrpt lookup", cmdTLSRPTLookup},
192 {"tlsrpt parsereportmsg", cmdTLSRPTParsereportmsg},
193 {"version", cmdVersion},
194 {"webapi", cmdWebapi},
196 {"example", cmdExample},
197 {"bumpuidvalidity", cmdBumpUIDValidity},
198 {"reassignuids", cmdReassignUIDs},
199 {"fixuidmeta", cmdFixUIDMeta},
200 {"fixmsgsize", cmdFixmsgsize},
201 {"reparse", cmdReparse},
202 {"ensureparsed", cmdEnsureParsed},
203 {"recalculatemailboxcounts", cmdRecalculateMailboxCounts},
204 {"message parse", cmdMessageParse},
205 {"reassignthreads", cmdReassignthreads},
208 {"helpall", cmdHelpall},
209 {"junk analyze", cmdJunkAnalyze},
210 {"junk check", cmdJunkCheck},
211 {"junk play", cmdJunkPlay},
212 {"junk test", cmdJunkTest},
213 {"junk train", cmdJunkTrain},
214 {"dmarcdb addreport", cmdDMARCDBAddReport},
215 {"tlsrptdb addreport", cmdTLSRPTDBAddReport},
216 {"updates addsigned", cmdUpdatesAddSigned},
217 {"updates genkey", cmdUpdatesGenkey},
218 {"updates pubkey", cmdUpdatesPubkey},
219 {"updates serve", cmdUpdatesServe},
220 {"updates verify", cmdUpdatesVerify},
221 {"gentestdata", cmdGentestdata},
222 {"ximport maildir", cmdXImportMaildir},
223 {"ximport mbox", cmdXImportMbox},
224 {"openaccounts", cmdOpenaccounts},
225 {"readmessages", cmdReadmessages},
226 {"queuefillretired", cmdQueueFillRetired},
232 for _, xc := range commands {
233 c := cmd{words: strings.Split(xc.cmd, " "), fn: xc.fn}
234 cmds = append(cmds, c)
242 // Set before calling command.
245 _gather bool // Set when using Parse to gather usage for a command.
247 // Set by invoked command or Parse.
248 unlisted bool // If set, command is not listed until at least some words are matched from command.
249 params string // Arguments to command. Multiple lines possible.
250 help string // Additional explanation. First line is synopsis, the rest is only printed for an explicit help/usage for that command.
256func (c *cmd) Parse() []string {
257 // To gather params and usage information, we just run the command but cause this
258 // panic after the command has registered its flags and set its params and help
259 // information. This is then caught and that info printed.
264 c.flag.Usage = c.Usage
265 c.flag.Parse(c.flagArgs)
266 c.args = c.flag.Args()
270func (c *cmd) gather() {
271 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
275 // panic generated by Parse.
283func (c *cmd) makeUsage() string {
284 var r strings.Builder
285 cs := "mox " + strings.Join(c.words, " ")
286 for i, line := range strings.Split(strings.TrimSpace(c.params), "\n") {
294 fmt.Fprintf(&r, "%6s %s%s\n", s, cs, line)
297 c.flag.PrintDefaults()
301func (c *cmd) printUsage() {
302 fmt.Fprint(os.Stderr, c.makeUsage())
304 fmt.Fprint(os.Stderr, "\n"+c.help+"\n")
308func (c *cmd) Usage() {
313func cmdHelp(c *cmd) {
314 c.params = "[command ...]"
315 c.help = `Prints help about matching commands.
317If multiple commands match, they are listed along with the first line of their help text.
318If a single command matches, its usage and full help text is printed.
325 prefix := func(l, pre []string) bool {
326 if len(pre) > len(l) {
329 return slices.Equal(pre, l[:len(pre)])
333 for _, c := range cmds {
334 if slices.Equal(c.words, args) {
336 fmt.Print(c.makeUsage())
338 fmt.Print("\n" + c.help + "\n")
341 } else if prefix(c.words, args) {
342 partial = append(partial, c)
345 if len(partial) == 0 {
346 fmt.Fprintf(os.Stderr, "%s: unknown command\n", strings.Join(args, " "))
349 for _, c := range partial {
351 line := "mox " + strings.Join(c.words, " ")
352 fmt.Printf("%s\n", line)
354 fmt.Printf("\t%s\n", strings.Split(c.help, "\n")[0])
359func cmdHelpall(c *cmd) {
361 c.help = `Print all detailed usage and help information for all listed commands.
363Used to generate documentation.
371 for _, c := range cmds {
377 fmt.Fprintf(os.Stderr, "\n")
381 fmt.Fprintf(os.Stderr, "# mox %s\n\n", strings.Join(c.words, " "))
383 fmt.Fprintln(os.Stderr, c.help+"\n")
386 s = "\t" + strings.ReplaceAll(s, "\n", "\n\t")
387 fmt.Fprintln(os.Stderr, s)
391func usage(l []cmd, unlisted bool) {
394 lines = append(lines, "mox [-config config/mox.conf] [-pedantic] ...")
396 for _, c := range l {
398 if c.unlisted && !unlisted {
401 for _, line := range strings.Split(c.params, "\n") {
402 x := append([]string{"mox"}, c.words...)
406 lines = append(lines, strings.Join(x, " "))
409 for i, line := range lines {
414 fmt.Fprintln(os.Stderr, pre+line)
419var loglevel string // Empty will be interpreted as info, except by localserve.
422// subcommands that are not "serve" should use this function to load the config, it
423// restores any loglevel specified on the command-line, instead of using the
424// loglevels from the config file and it does not load files like TLS keys/certs.
425func mustLoadConfig() {
426 mox.MustLoadConfig(false, false)
431 if level, ok := mlog.Levels[ll]; ok {
432 mox.Conf.Log[""] = level
433 mlog.SetConfig(mox.Conf.Log)
435 log.Fatal("unknown loglevel", slog.String("loglevel", loglevel))
438 mox.SetPedantic(true)
443 // CheckConsistencyOnClose is true by default, for all the test packages. A regular
444 // mox server should never use it. But integration tests enable it again with a
446 store.CheckConsistencyOnClose = false
448 ctxbg := context.Background()
454 // If invoked as sendmail, e.g. /usr/sbin/sendmail, we do enough so cron can get a
455 // message sent using smtp submission to a configured server.
456 if len(os.Args) > 0 && filepath.Base(os.Args[0]) == "sendmail" {
458 flag: flag.NewFlagSet("sendmail", flag.ExitOnError),
459 flagArgs: os.Args[1:],
460 log: mlog.New("sendmail", nil),
466 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")
467 flag.StringVar(&loglevel, "loglevel", "", "if non-empty, this log level is set early in startup")
468 flag.BoolVar(&pedantic, "pedantic", false, "protocol violations result in errors instead of accepting/working around them")
469 flag.BoolVar(&store.CheckConsistencyOnClose, "checkconsistency", false, "dangerous option for testing only, enables data checks that abort/panic when inconsistencies are found")
471 var cpuprofile, memprofile, tracefile string
472 flag.StringVar(&cpuprofile, "cpuprof", "", "store cpu profile to file")
473 flag.StringVar(&memprofile, "memprof", "", "store mem profile to file")
474 flag.StringVar(&tracefile, "trace", "", "store execution trace to file")
476 flag.Usage = func() { usage(cmds, false) }
484 defer traceExecution(tracefile)()
486 defer profile(cpuprofile, memprofile)()
489 mox.SetPedantic(true)
492 mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
497 if level, ok := mlog.Levels[ll]; ok {
498 mox.Conf.Log[""] = level
499 mlog.SetConfig(mox.Conf.Log)
500 // note: SetConfig may be called again when subcommands loads config.
502 log.Fatalf("unknown loglevel %q", loglevel)
507 for _, c := range cmds {
508 for i, w := range c.words {
509 if i >= len(args) || w != args[i] {
511 partial = append(partial, c)
516 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
517 c.flagArgs = args[len(c.words):]
518 c.log = mlog.New(strings.Join(c.words, ""), nil)
522 if len(partial) > 0 {
528func xcheckf(err error, format string, args ...any) {
532 msg := fmt.Sprintf(format, args...)
533 log.Fatalf("%s: %s", msg, err)
536func xparseIP(s, what string) net.IP {
539 log.Fatalf("invalid %s: %q", what, s)
544func xparseDomain(s, what string) dns.Domain {
545 d, err := dns.ParseDomain(s)
546 xcheckf(err, "parsing %s %q", what, s)
550func cmdClientConfig(c *cmd) {
552 c.help = `Print the configuration for email clients for a domain.
554Sending email is typically not done on the SMTP port 25, but on submission
555ports 465 (with TLS) and 587 (without initial TLS, but usually added to the
556connection with STARTTLS). For IMAP, the port with TLS is 993 and without is
559Without TLS/STARTTLS, passwords are sent in clear text, which should only be
560configured over otherwise secured connections, like a VPN.
566 d := xparseDomain(args[0], "domain")
571func printClientConfig(d dns.Domain) {
572 cc, err := mox.ClientConfigsDomain(d)
573 xcheckf(err, "getting client config")
574 fmt.Printf("%-20s %-30s %5s %-15s %s\n", "Protocol", "Host", "Port", "Listener", "Note")
575 for _, e := range cc.Entries {
576 fmt.Printf("%-20s %-30s %5d %-15s %s\n", e.Protocol, e.Host, e.Port, e.Listener, e.Note)
579To prevent authentication mechanism downgrade attempts that may result in
580clients sending plain text passwords to a MitM, clients should always be
581explicitly configured with the most secure authentication mechanism supported,
582the first of: SCRAM-SHA-256-PLUS, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, SCRAM-SHA-1,
587func cmdConfigTest(c *cmd) {
588 c.help = `Parses and validates the configuration files.
590If valid, the command exits with status 0. If not valid, all errors encountered
598 mox.FilesImmediate = true
600 _, errs := mox.ParseConfig(context.Background(), c.log, mox.ConfigStaticPath, true, true, false)
602 log.Printf("multiple errors:")
603 for _, err := range errs {
604 log.Printf("%s", err)
607 } else if len(errs) == 1 {
608 log.Fatalf("%s", errs[0])
611 fmt.Println("config OK")
614func cmdConfigDescribeStatic(c *cmd) {
615 c.params = ">mox.conf"
616 c.help = `Prints an annotated empty configuration for use as mox.conf.
618The static configuration file cannot be reloaded while mox is running. Mox has
619to be restarted for changes to the static configuration file to take effect.
621This configuration file needs modifications to make it valid. For example, it
622may contain unfinished list items.
624 if len(c.Parse()) != 0 {
629 err := sconf.Describe(os.Stdout, &sc)
630 xcheckf(err, "describing config")
633func cmdConfigDescribeDomains(c *cmd) {
634 c.params = ">domains.conf"
635 c.help = `Prints an annotated empty configuration for use as domains.conf.
637The domains configuration file contains the domains and their configuration,
638and accounts and their configuration. This includes the configured email
639addresses. The mox admin web interface, and the mox command line interface, can
640make changes to this file. Mox automatically reloads this file when it changes.
642Like the static configuration, the example domains.conf printed by this command
643needs modifications to make it valid.
645 if len(c.Parse()) != 0 {
649 var dc config.Dynamic
650 err := sconf.Describe(os.Stdout, &dc)
651 xcheckf(err, "describing config")
654func cmdConfigPrintservice(c *cmd) {
655 c.params = ">mox.service"
656 c.help = `Prints a systemd unit service file for mox.
658This is the same file as generated using quickstart. If the systemd service file
659has changed with a newer version of mox, use this command to generate an up to
662 if len(c.Parse()) != 0 {
666 pwd, err := os.Getwd()
668 log.Printf("current working directory: %v", err)
671 service := strings.ReplaceAll(moxService, "/home/mox", pwd)
675func cmdConfigDomainAdd(c *cmd) {
676 c.params = "domain account [localpart]"
677 c.help = `Adds a new domain to the configuration and reloads the configuration.
679The account is used for the postmaster mailboxes the domain, including as DMARC and
680TLS reporting. Localpart is the "username" at the domain for this account. If
681must be set if and only if account does not yet exist.
684 if len(args) != 2 && len(args) != 3 {
688 d := xparseDomain(args[0], "domain")
690 var localpart smtp.Localpart
693 localpart, err = smtp.ParseLocalpart(args[2])
694 xcheckf(err, "parsing localpart")
696 ctlcmdConfigDomainAdd(xctl(), d, args[1], localpart)
699func ctlcmdConfigDomainAdd(ctl *ctl, domain dns.Domain, account string, localpart smtp.Localpart) {
700 ctl.xwrite("domainadd")
701 ctl.xwrite(domain.Name())
703 ctl.xwrite(string(localpart))
705 fmt.Printf("domain added, remember to add dns records, see:\n\nmox config dnsrecords %s\nmox config dnscheck %s\n", domain.Name(), domain.Name())
708func cmdConfigDomainRemove(c *cmd) {
710 c.help = `Remove a domain from the configuration and reload the configuration.
712This is a dangerous operation. Incoming email delivery for this domain will be
720 d := xparseDomain(args[0], "domain")
722 ctlcmdConfigDomainRemove(xctl(), d)
725func ctlcmdConfigDomainRemove(ctl *ctl, d dns.Domain) {
726 ctl.xwrite("domainrm")
729 fmt.Printf("domain removed, remember to remove dns records for %s\n", d)
732func cmdConfigAliasList(c *cmd) {
734 c.help = `List aliases for domain.`
741 ctlcmdConfigAliasList(xctl(), args[0])
744func ctlcmdConfigAliasList(ctl *ctl, address string) {
745 ctl.xwrite("aliaslist")
748 ctl.xstreamto(os.Stdout)
751func cmdConfigAliasPrint(c *cmd) {
753 c.help = `Print settings and members of alias.`
760 ctlcmdConfigAliasPrint(xctl(), args[0])
763func ctlcmdConfigAliasPrint(ctl *ctl, address string) {
764 ctl.xwrite("aliasprint")
767 ctl.xstreamto(os.Stdout)
770func cmdConfigAliasAdd(c *cmd) {
771 c.params = "alias@domain rcpt1@domain ..."
772 c.help = `Add new alias with one or more addresses.`
778 alias := config.Alias{Addresses: args[1:]}
781 ctlcmdConfigAliasAdd(xctl(), args[0], alias)
784func ctlcmdConfigAliasAdd(ctl *ctl, address string, alias config.Alias) {
785 ctl.xwrite("aliasadd")
787 xctlwriteJSON(ctl, alias)
791func cmdConfigAliasUpdate(c *cmd) {
792 c.params = "alias@domain [-postpublic false|true -listmembers false|true -allowmsgfrom false|true]"
793 c.help = `Update alias configuration.`
794 var postpublic, listmembers, allowmsgfrom string
795 c.flag.StringVar(&postpublic, "postpublic", "", "whether anyone or only list members can post")
796 c.flag.StringVar(&listmembers, "listmembers", "", "whether list members can list members")
797 c.flag.StringVar(&allowmsgfrom, "allowmsgfrom", "", "whether alias address can be used in message from header")
805 ctlcmdConfigAliasUpdate(xctl(), alias, postpublic, listmembers, allowmsgfrom)
808func ctlcmdConfigAliasUpdate(ctl *ctl, alias, postpublic, listmembers, allowmsgfrom string) {
809 ctl.xwrite("aliasupdate")
811 ctl.xwrite(postpublic)
812 ctl.xwrite(listmembers)
813 ctl.xwrite(allowmsgfrom)
817func cmdConfigAliasRemove(c *cmd) {
818 c.params = "alias@domain"
819 c.help = "Remove alias."
826 ctlcmdConfigAliasRemove(xctl(), args[0])
829func ctlcmdConfigAliasRemove(ctl *ctl, alias string) {
830 ctl.xwrite("aliasrm")
835func cmdConfigAliasAddaddr(c *cmd) {
836 c.params = "alias@domain rcpt1@domain ..."
837 c.help = `Add addresses to alias.`
844 ctlcmdConfigAliasAddaddr(xctl(), args[0], args[1:])
847func ctlcmdConfigAliasAddaddr(ctl *ctl, alias string, addresses []string) {
848 ctl.xwrite("aliasaddaddr")
850 xctlwriteJSON(ctl, addresses)
854func cmdConfigAliasRemoveaddr(c *cmd) {
855 c.params = "alias@domain rcpt1@domain ..."
856 c.help = `Remove addresses from alias.`
863 ctlcmdConfigAliasRmaddr(xctl(), args[0], args[1:])
866func ctlcmdConfigAliasRmaddr(ctl *ctl, alias string, addresses []string) {
867 ctl.xwrite("aliasrmaddr")
869 xctlwriteJSON(ctl, addresses)
873func cmdConfigAccountAdd(c *cmd) {
874 c.params = "account address"
875 c.help = `Add an account with an email address and reload the configuration.
877Email can be delivered to this address/account. A password has to be configured
878explicitly, see the setaccountpassword command.
886 ctlcmdConfigAccountAdd(xctl(), args[0], args[1])
889func ctlcmdConfigAccountAdd(ctl *ctl, account, address string) {
890 ctl.xwrite("accountadd")
894 fmt.Printf("account added, set a password with \"mox setaccountpassword %s\"\n", account)
897func cmdConfigAccountRemove(c *cmd) {
899 c.help = `Remove an account and reload the configuration.
901Email addresses for this account will also be removed, and incoming email for
902these addresses will be rejected.
904All data for the account will be removed.
912 ctlcmdConfigAccountRemove(xctl(), args[0])
915func ctlcmdConfigAccountRemove(ctl *ctl, account string) {
916 ctl.xwrite("accountrm")
919 fmt.Println("account removed")
922func cmdConfigAddressAdd(c *cmd) {
923 c.params = "address account"
924 c.help = `Adds an address to an account and reloads the configuration.
926If address starts with a @ (i.e. a missing localpart), this is a catchall
927address for the domain.
935 ctlcmdConfigAddressAdd(xctl(), args[0], args[1])
938func ctlcmdConfigAddressAdd(ctl *ctl, address, account string) {
939 ctl.xwrite("addressadd")
943 fmt.Println("address added")
946func cmdConfigAddressRemove(c *cmd) {
948 c.help = `Remove an address and reload the configuration.
950Incoming email for this address will be rejected after removing an address.
958 ctlcmdConfigAddressRemove(xctl(), args[0])
961func ctlcmdConfigAddressRemove(ctl *ctl, address string) {
962 ctl.xwrite("addressrm")
965 fmt.Println("address removed")
968func cmdConfigDNSRecords(c *cmd) {
970 c.help = `Prints annotated DNS records as zone file that should be created for the domain.
972The zone file can be imported into existing DNS software. You should review the
973DNS records, especially if your domain previously/currently has email
981 d := xparseDomain(args[0], "domain")
983 domConf, ok := mox.Conf.Domain(d)
985 log.Fatalf("unknown domain")
988 resolver := dns.StrictResolver{Pkg: "main"}
989 _, result, err := resolver.LookupTXT(context.Background(), d.ASCII+".")
990 if !dns.IsNotFound(err) {
991 xcheckf(err, "looking up record for dnssec-status")
994 var certIssuerDomainName, acmeAccountURI string
995 public := mox.Conf.Static.Listeners["public"]
996 if public.TLS != nil && public.TLS.ACME != "" {
997 acme, ok := mox.Conf.Static.ACME[public.TLS.ACME]
998 if ok && acme.Manager.Manager.Client != nil {
999 certIssuerDomainName = acme.IssuerDomainName
1000 acc, err := acme.Manager.Manager.Client.GetReg(context.Background(), "")
1001 c.log.Check(err, "get public acme account")
1003 acmeAccountURI = acc.URI
1008 records, err := mox.DomainRecords(domConf, d, result.Authentic, certIssuerDomainName, acmeAccountURI)
1009 xcheckf(err, "records")
1010 fmt.Print(strings.Join(records, "\n") + "\n")
1013func cmdConfigDNSCheck(c *cmd) {
1015 c.help = "Check the DNS records with the configuration for the domain, and print any errors/warnings."
1021 d := xparseDomain(args[0], "domain")
1023 _, ok := mox.Conf.Domain(d)
1025 log.Fatalf("unknown domain")
1028 // todo future: move http.Admin.CheckDomain to mox- and make it return a regular error.
1034 err, ok := x.(*sherpa.Error)
1038 log.Fatalf("%s", err)
1041 printResult := func(name string, r webadmin.Result) {
1042 if len(r.Errors) == 0 && len(r.Warnings) == 0 {
1045 fmt.Printf("# %s\n", name)
1046 for _, s := range r.Errors {
1047 fmt.Printf("error: %s\n", s)
1049 for _, s := range r.Warnings {
1050 fmt.Printf("warning: %s\n", s)
1054 result := webadmin.Admin{}.CheckDomain(context.Background(), args[0])
1055 printResult("DNSSEC", result.DNSSEC.Result)
1056 printResult("IPRev", result.IPRev.Result)
1057 printResult("MX", result.MX.Result)
1058 printResult("TLS", result.TLS.Result)
1059 printResult("DANE", result.DANE.Result)
1060 printResult("SPF", result.SPF.Result)
1061 printResult("DKIM", result.DKIM.Result)
1062 printResult("DMARC", result.DMARC.Result)
1063 printResult("Host TLSRPT", result.HostTLSRPT.Result)
1064 printResult("Domain TLSRPT", result.DomainTLSRPT.Result)
1065 printResult("MTASTS", result.MTASTS.Result)
1066 printResult("SRV conf", result.SRVConf.Result)
1067 printResult("Autoconf", result.Autoconf.Result)
1068 printResult("Autodiscover", result.Autodiscover.Result)
1071func cmdConfigEnsureACMEHostprivatekeys(c *cmd) {
1073 c.help = `Ensure host private keys exist for TLS listeners with ACME.
1075In mox.conf, each listener can have TLS configured. Long-lived private key files
1076can be specified, which will be used when requesting ACME certificates.
1077Configuring these private keys makes it feasible to publish DANE TLSA records
1078for the corresponding public keys in DNS, protected with DNSSEC, allowing TLS
1079certificate verification without depending on a list of Certificate Authorities
1080(CAs). Previous versions of mox did not pre-generate private keys for use with
1081ACME certificates, but would generate private keys on-demand. By explicitly
1082configuring private keys, they will not change automatedly with new
1083certificates, and the DNS TLSA records stay valid.
1085This command looks for listeners in mox.conf with TLS with ACME configured. For
1086each missing host private key (of type rsa-2048 and ecdsa-p256) a key is written
1087to config/hostkeys/. If a certificate exists in the ACME "cache", its private
1088key is copied. Otherwise a new private key is generated. Snippets for manually
1089updating/editing mox.conf are printed.
1091After running this command, and updating mox.conf, run "mox config dnsrecords"
1092for a domain and create the TLSA DNS records it suggests to enable DANE.
1099 // Load a private key from p, in various forms. We only look at the first PEM
1100 // block. Files with only a private key, or with multiple blocks but private key
1101 // first like autocert does, can be loaded.
1102 loadPrivateKey := func(f *os.File) (any, error) {
1103 buf, err := io.ReadAll(f)
1105 return nil, fmt.Errorf("reading private key file: %v", err)
1107 block, _ := pem.Decode(buf)
1109 return nil, fmt.Errorf("no pem block found in pem file")
1113 case "EC PRIVATE KEY":
1114 privKey, err = x509.ParseECPrivateKey(block.Bytes)
1115 case "RSA PRIVATE KEY":
1116 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
1118 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
1120 return nil, fmt.Errorf("unrecognized pem block type %q", block.Type)
1123 return nil, fmt.Errorf("parsing private key of type %q: %v", block.Type, err)
1128 // Either load a private key from file, or if it doesn't exist generate a new
1130 xtryLoadPrivateKey := func(kt autocert.KeyType, p string) any {
1131 f, err := os.Open(p)
1132 if err != nil && errors.Is(err, fs.ErrNotExist) {
1134 case autocert.KeyRSA2048:
1135 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1136 xcheckf(err, "generating new 2048-bit rsa private key")
1138 case autocert.KeyECDSAP256:
1139 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1140 xcheckf(err, "generating new ecdsa p-256 private key")
1143 log.Fatalf("unexpected keytype %v", kt)
1146 xcheckf(err, "%s: open acme key and certificate file", p)
1148 // Load private key from file. autocert stores a PEM file that starts with a
1149 // private key, followed by certificate(s). So we can just read it and should find
1150 // the private key we are looking for.
1151 privKey, err := loadPrivateKey(f)
1152 if xerr := f.Close(); xerr != nil {
1153 log.Printf("closing private key file: %v", xerr)
1155 xcheckf(err, "parsing private key from acme key and certificate file")
1157 switch k := privKey.(type) {
1158 case *rsa.PrivateKey:
1159 if k.N.BitLen() == 2048 {
1162 log.Printf("warning: rsa private key in %s has %d bits, skipping and generating new 2048-bit rsa private key", p, k.N.BitLen())
1163 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1164 xcheckf(err, "generating new 2048-bit rsa private key")
1166 case *ecdsa.PrivateKey:
1167 if k.Curve == elliptic.P256() {
1170 log.Printf("warning: ecdsa private key in %s has curve %v, skipping and generating new p-256 ecdsa key", p, k.Curve.Params().Name)
1171 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1172 xcheckf(err, "generating new ecdsa p-256 private key")
1175 log.Fatalf("%s: unexpected private key file of type %T", p, privKey)
1180 // Write privKey as PKCS#8 private key to p. Only if file does not yet exist.
1181 writeHostPrivateKey := func(privKey any, p string) error {
1182 os.MkdirAll(filepath.Dir(p), 0700)
1183 f, err := os.OpenFile(p, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
1185 return fmt.Errorf("create: %v", err)
1189 if err := f.Close(); err != nil {
1190 log.Printf("closing new hostkey file %s after error: %v", p, err)
1192 if err := os.Remove(p); err != nil {
1193 log.Printf("removing new hostkey file %s after error: %v", p, err)
1197 buf, err := x509.MarshalPKCS8PrivateKey(privKey)
1199 return fmt.Errorf("marshal private host key: %v", err)
1202 Type: "PRIVATE KEY",
1205 if err := pem.Encode(f, &block); err != nil {
1206 return fmt.Errorf("write as pem: %v", err)
1208 if err := f.Close(); err != nil {
1209 return fmt.Errorf("close: %v", err)
1216 timestamp := time.Now().Format("20060102T150405")
1218 for listenerName, l := range mox.Conf.Static.Listeners {
1219 if l.TLS == nil || l.TLS.ACME == "" {
1222 haveKeyTypes := map[autocert.KeyType]bool{}
1223 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
1224 p := mox.ConfigDirPath(privKeyFile)
1225 f, err := os.Open(p)
1226 xcheckf(err, "open host private key")
1227 privKey, err := loadPrivateKey(f)
1228 if err := f.Close(); err != nil {
1229 log.Printf("closing host private key file: %v", err)
1231 xcheckf(err, "loading host private key")
1232 switch k := privKey.(type) {
1233 case *rsa.PrivateKey:
1234 if k.N.BitLen() == 2048 {
1235 haveKeyTypes[autocert.KeyRSA2048] = true
1237 case *ecdsa.PrivateKey:
1238 if k.Curve == elliptic.P256() {
1239 haveKeyTypes[autocert.KeyECDSAP256] = true
1243 created := []string{}
1244 for _, kt := range []autocert.KeyType{autocert.KeyRSA2048, autocert.KeyECDSAP256} {
1245 if haveKeyTypes[kt] {
1248 // Lookup key in ACME cache.
1249 host := l.HostnameDomain
1250 if host.ASCII == "" {
1251 host = mox.Conf.Static.HostnameDomain
1253 filename := host.ASCII
1255 if kt == autocert.KeyRSA2048 {
1259 p := mox.DataDirPath(filepath.Join("acme", "keycerts", l.TLS.ACME, filename))
1260 privKey := xtryLoadPrivateKey(kt, p)
1262 relPath := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", host.Name(), timestamp, kind))
1263 destPath := mox.ConfigDirPath(relPath)
1264 err := writeHostPrivateKey(privKey, destPath)
1265 xcheckf(err, "writing host private key file to %s: %v", destPath, err)
1266 created = append(created, relPath)
1267 fmt.Printf("Wrote host private key: %s\n", destPath)
1269 didCreate = didCreate || len(created) > 0
1270 if len(created) > 0 {
1272 HostPrivateKeyFiles: append(l.TLS.HostPrivateKeyFiles, created...),
1274 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)
1275 err := sconf.Write(os.Stdout, tls)
1276 xcheckf(err, "writing new TLS.HostPrivateKeyFiles section")
1282After updating mox.conf and restarting, run "mox config dnsrecords" for a
1283domain and create the TLSA DNS records it suggests to enable DANE.
1288func cmdLoglevels(c *cmd) {
1289 c.params = "[level [pkg]]"
1290 c.help = `Print the log levels, or set a new default log level, or a level for the given package.
1292By default, a single log level applies to all logging in mox. But for each
1293"pkg", an overriding log level can be configured. Examples of packages:
1294smtpserver, smtpclient, queue, imapserver, spf, dkim, dmarc, junk, message,
1297Specify a pkg and an empty level to clear the configured level for a package.
1299Valid labels: error, info, debug, trace, traceauth, tracedata.
1308 ctlcmdLoglevels(xctl())
1314 ctlcmdSetLoglevels(xctl(), pkg, args[0])
1318func ctlcmdLoglevels(ctl *ctl) {
1319 ctl.xwrite("loglevels")
1321 ctl.xstreamto(os.Stdout)
1324func ctlcmdSetLoglevels(ctl *ctl, pkg, level string) {
1325 ctl.xwrite("setloglevels")
1331func cmdStop(c *cmd) {
1332 c.help = `Shut mox down, giving connections maximum 3 seconds to stop before closing them.
1334While shutting down, new IMAP and SMTP connections will get a status response
1335indicating temporary unavailability. Existing connections will get a 3 second
1336period to finish their transaction and shut down. Under normal circumstances,
1337only IMAP has long-living connections, with the IDLE command to get notified of
1340 if len(c.Parse()) != 0 {
1347 // Read will hang until remote has shut down.
1348 buf := make([]byte, 128)
1349 n, err := ctl.conn.Read(buf)
1351 log.Fatalf("expected eof after graceful shutdown, got data %q", buf[:n])
1352 } else if err != io.EOF {
1353 log.Fatalf("expected eof after graceful shutdown, got error %v", err)
1355 fmt.Println("mox stopped")
1358func cmdBackup(c *cmd) {
1359 c.params = "dest-dir"
1360 c.help = `Creates a backup of the data directory.
1362Backup creates consistent snapshots of the databases and message files and
1363copies other files in the data directory. Empty directories are not copied.
1364These files can then be stored elsewhere for long-term storage, or used to fall
1365back to should an upgrade fail. Simply copying files in the data directory
1366while mox is running can result in unusable database files.
1368Message files never change (they are read-only, though can be removed) and are
1369hard-linked so they don't consume additional space. If hardlinking fails, for
1370example when the backup destination directory is on a different file system, a
1371regular copy is made. Using a destination directory like "data/tmp/backup"
1372increases the odds hardlinking succeeds: the default systemd service file
1373specifically mounts the data directory, causing attempts to hardlink outside it
1374to fail with an error about cross-device linking.
1376All files in the data directory that aren't recognized (i.e. other than known
1377database files, message files, an acme directory, the "tmp" directory, etc),
1378are stored, but with a warning.
1380Remove files in the destination directory before doing another backup. The
1381backup command will not overwrite files, but print and return errors.
1383Exit code 0 indicates the backup was successful. A clean successful backup does
1384not print any output, but may print warnings. Use the -verbose flag for
1385details, including timing.
1387To restore a backup, first shut down mox, move away the old data directory and
1388move an earlier backed up directory in its place, run "mox verifydata",
1389possibly with the "-fix" option, and restart mox. After the restore, you may
1390also want to run "mox bumpuidvalidity" for each account for which messages in a
1391mailbox changed, to force IMAP clients to synchronize mailbox state.
1393Before upgrading, to check if the upgrade will likely succeed, first make a
1394backup, then use the new mox binary to run "mox verifydata" on the backup. This
1395can change the backup files (e.g. upgrade database files, move away
1396unrecognized message files), so you should make a new backup before actually
1401 c.flag.BoolVar(&verbose, "verbose", false, "print progress")
1408 dstDataDir, err := filepath.Abs(args[0])
1409 xcheckf(err, "making path absolute")
1411 ctlcmdBackup(xctl(), dstDataDir, verbose)
1414func ctlcmdBackup(ctl *ctl, dstDataDir string, verbose bool) {
1415 ctl.xwrite("backup")
1416 ctl.xwrite(dstDataDir)
1418 ctl.xwrite("verbose")
1422 ctl.xstreamto(os.Stdout)
1426func cmdSetadminpassword(c *cmd) {
1427 c.help = `Set a new admin password, for the web interface.
1429The password is read from stdin. Its bcrypt hash is stored in a file named
1430"adminpasswd" in the configuration directory.
1432 if len(c.Parse()) != 0 {
1437 path := mox.ConfigDirPath(mox.Conf.Static.AdminPasswordFile)
1439 log.Fatal("no admin password file configured")
1442 pw := xreadpassword()
1443 pw, err := precis.OpaqueString.String(pw)
1444 xcheckf(err, `checking password with "precis" requirements`)
1445 hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
1446 xcheckf(err, "generating hash for password")
1447 err = os.WriteFile(path, hash, 0660)
1448 xcheckf(err, "writing hash to admin password file")
1451func xreadpassword() string {
1453Type new password. Password WILL echo.
1455WARNING: Bots will try to bruteforce your password. Connections with failed
1456authentication attempts will be rate limited but attackers WILL find weak
1457passwords. If your account is compromised, spammers are likely to abuse your
1458system, spamming your address and the wider internet in your name. So please
1459pick a random, unguessable password, preferably at least 12 characters.
1462 fmt.Printf("password: ")
1463 buf := make([]byte, 64)
1464 n, err := os.Stdin.Read(buf)
1465 xcheckf(err, "reading stdin")
1466 pw := string(buf[:n])
1467 pw = strings.TrimSuffix(strings.TrimSuffix(pw, "\r\n"), "\n")
1469 log.Fatal("password must be at least 8 characters")
1474func cmdSetaccountpassword(c *cmd) {
1475 c.params = "account"
1476 c.help = `Set new password an account.
1478The password is read from stdin. Secrets derived from the password, but not the
1479password itself, are stored in the account database. The stored secrets are for
1480authentication with: scram-sha-256, scram-sha-1, cram-md5, plain text (bcrypt
1483The parameter is an account name, as configured under Accounts in domains.conf
1484and as present in the data/accounts/ directory, not a configured email address
1493 pw := xreadpassword()
1495 ctlcmdSetaccountpassword(xctl(), args[0], pw)
1498func ctlcmdSetaccountpassword(ctl *ctl, account, password string) {
1499 ctl.xwrite("setaccountpassword")
1501 ctl.xwrite(password)
1505func cmdDeliver(c *cmd) {
1507 c.params = "address < message"
1508 c.help = "Deliver message to address."
1514 ctlcmdDeliver(xctl(), args[0])
1517func ctlcmdDeliver(ctl *ctl, address string) {
1518 ctl.xwrite("deliver")
1521 ctl.xstreamfrom(os.Stdin)
1524 fmt.Println("message delivered")
1526 log.Fatalf("deliver: %s", line)
1530func cmdDKIMGenrsa(c *cmd) {
1531 c.params = ">$selector._domainkey.$domain.rsa2048.privatekey.pkcs8.pem"
1532 c.help = `Generate a new 2048 bit RSA private key for use with DKIM.
1534The generated file is in PEM format, and has a comment it is generated for use
1537 if len(c.Parse()) != 0 {
1541 buf, err := mox.MakeDKIMRSAKey(dns.Domain{}, dns.Domain{})
1542 xcheckf(err, "making rsa private key")
1543 _, err = os.Stdout.Write(buf)
1544 xcheckf(err, "writing rsa private key")
1547func cmdDANEDial(c *cmd) {
1548 c.params = "host:port"
1550 c.flag.StringVar(&usages, "usages", "pkix-ta,pkix-ee,dane-ta,dane-ee", "allowed usages for dane, comma-separated list")
1551 c.help = `Dial the address using TLS with certificate verification using DANE.
1553Data is copied between connection and stdin/stdout until either side closes the
1561 allowedUsages := []adns.TLSAUsage{}
1563 for _, s := range strings.Split(usages, ",") {
1564 var usage adns.TLSAUsage
1565 switch strings.ToLower(s) {
1566 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
1567 usage = adns.TLSAUsagePKIXTA
1568 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
1569 usage = adns.TLSAUsagePKIXEE
1570 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
1571 usage = adns.TLSAUsageDANETA
1572 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
1573 usage = adns.TLSAUsageDANEEE
1575 log.Fatalf("unknown dane usage %q", s)
1577 allowedUsages = append(allowedUsages, usage)
1581 pkixRoots, err := x509.SystemCertPool()
1582 xcheckf(err, "get system pkix certificate pool")
1584 resolver := dns.StrictResolver{Pkg: "danedial"}
1585 conn, record, err := dane.Dial(context.Background(), c.log.Logger, resolver, "tcp", args[0], allowedUsages, pkixRoots)
1586 xcheckf(err, "dial")
1587 log.Printf("(connected, verified with %s)", record)
1590 _, err := io.Copy(os.Stdout, conn)
1591 xcheckf(err, "copy from connection to stdout")
1594 _, err = io.Copy(conn, os.Stdin)
1595 xcheckf(err, "copy from stdin to connection")
1598func cmdDANEDialmx(c *cmd) {
1599 c.params = "domain [destination-host]"
1600 var ehloHostname string
1601 c.flag.StringVar(&ehloHostname, "ehlohostname", "localhost", "hostname to send in smtp ehlo command")
1602 c.help = `Connect to MX server for domain using STARTTLS verified with DANE.
1604If no destination host is specified, regular delivery logic is used to find the
1605hosts to attempt delivery too. This involves following CNAMEs for the domain,
1606looking up MX records, and possibly falling back to the domain name itself as
1609If a destination host is specified, that is the only candidate host considered
1612With a list of destinations gathered, each is dialed until a successful SMTP
1613session verified with DANE has been initialized, including EHLO and STARTTLS
1616Once connected, data is copied between connection and stdin/stdout, until
1617either side closes the connection.
1619This command follows the same logic as delivery attempts made from the queue,
1620sharing most of its code.
1623 if len(args) != 1 && len(args) != 2 {
1627 ehloDomain, err := dns.ParseDomain(ehloHostname)
1628 xcheckf(err, "parsing ehlo hostname")
1630 origNextHop, err := dns.ParseDomain(args[0])
1631 xcheckf(err, "parse domain")
1633 ctxbg := context.Background()
1635 resolver := dns.StrictResolver{}
1637 var origNextHopAuthentic, expandedNextHopAuthentic bool
1638 var expandedNextHop dns.Domain
1639 var hosts []dns.IPDomain
1642 haveMX, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hosts, permanent, err = smtpclient.GatherDestinations(ctxbg, c.log.Logger, resolver, dns.IPDomain{Domain: origNextHop})
1643 status := "temporary"
1645 status = "permanent"
1648 log.Fatalf("gathering destinations: %v (%s)", err, status)
1650 if expandedNextHop != origNextHop {
1651 log.Printf("followed cnames to %s", expandedNextHop)
1654 log.Printf("found mx record, trying mx hosts")
1656 log.Printf("no mx record found, will try to connect to domain directly")
1658 if !origNextHopAuthentic {
1659 log.Fatalf("error: initial domain not dnssec-secure")
1661 if !expandedNextHopAuthentic {
1662 log.Fatalf("error: expanded domain not dnssec-secure")
1666 for _, h := range hosts {
1667 l = append(l, h.String())
1669 log.Printf("destinations: %s", strings.Join(l, ", "))
1671 d, err := dns.ParseDomain(args[1])
1673 log.Fatalf("parsing destination host: %v", err)
1675 log.Printf("skipping domain mx/cname lookups, assuming domain is dnssec-protected")
1677 origNextHopAuthentic = true
1678 expandedNextHopAuthentic = true
1680 hosts = []dns.IPDomain{{Domain: d}}
1683 dialedIPs := map[string][]net.IP{}
1684 for _, host := range hosts {
1685 // It should not be possible for hosts to have IP addresses: They are not
1686 // allowed by dns.ParseDomain, and MX records cannot contain them.
1688 log.Fatalf("unexpected IP address for destination host")
1691 log.Printf("attempting to connect to %s", host)
1693 authentic, expandedAuthentic, expandedHost, ips, _, err := smtpclient.GatherIPs(ctxbg, c.log.Logger, resolver, "ip", host, dialedIPs)
1695 log.Printf("resolving ips for %s: %v, skipping", host, err)
1699 log.Printf("no dnssec for ips of %s, skipping", host)
1702 if !expandedAuthentic {
1703 log.Printf("no dnssec for cname-followed ips of %s, skipping", host)
1706 if expandedHost != host.Domain {
1707 log.Printf("host %s cname-expanded to %s", host, expandedHost)
1709 log.Printf("host %s resolved to ips %s, looking up tlsa records", host, ips)
1711 daneRequired, daneRecords, tlsaBaseDomain, err := smtpclient.GatherTLSA(ctxbg, c.log.Logger, resolver, host.Domain, expandedAuthentic, expandedHost)
1713 log.Printf("looking up tlsa records: %s, skipping", err)
1716 tlsMode := smtpclient.TLSRequiredStartTLS
1717 if len(daneRecords) == 0 {
1719 log.Printf("host %s has no tlsa records, skipping", expandedHost)
1722 log.Printf("warning: only unusable tlsa records found, continuing with required tls without certificate verification")
1726 for _, r := range daneRecords {
1727 l = append(l, r.String())
1729 log.Printf("tlsa records: %s", strings.Join(l, "; "))
1732 tlsHostnames := smtpclient.GatherTLSANames(haveMX, expandedNextHopAuthentic, expandedAuthentic, origNextHop, expandedNextHop, host.Domain, tlsaBaseDomain)
1734 for _, name := range tlsHostnames {
1735 l = append(l, name.String())
1737 log.Printf("gathered valid tls certificate names for potential verification with dane-ta: %s", strings.Join(l, ", "))
1739 dialer := &net.Dialer{Timeout: 5 * time.Second}
1740 conn, _, err := smtpclient.Dial(ctxbg, c.log.Logger, dialer, dns.IPDomain{Domain: expandedHost}, ips, 25, dialedIPs, nil)
1742 log.Printf("dial %s: %v, skipping", expandedHost, err)
1745 log.Printf("connected to %s, %s, starting smtp session with ehlo and starttls with dane verification", expandedHost, conn.RemoteAddr())
1747 var verifiedRecord adns.TLSA
1748 opts := smtpclient.Opts{
1749 DANERecords: daneRecords,
1750 DANEMoreHostnames: tlsHostnames[1:],
1751 DANEVerifiedRecord: &verifiedRecord,
1752 RootCAs: mox.Conf.Static.TLS.CertPool,
1755 sc, err := smtpclient.New(ctxbg, c.log.Logger, conn, tlsMode, tlsPKIX, ehloDomain, tlsHostnames[0], opts)
1757 log.Printf("setting up smtp session: %v, skipping", err)
1762 smtpConn, err := sc.Conn()
1764 log.Fatalf("error: taking over smtp connection: %s", err)
1766 log.Printf("tls verified with tlsa record: %s", verifiedRecord)
1767 log.Printf("smtp session initialized and connected to stdin/stdout")
1770 _, err := io.Copy(os.Stdout, smtpConn)
1771 xcheckf(err, "copy from connection to stdout")
1774 _, err = io.Copy(smtpConn, os.Stdin)
1775 xcheckf(err, "copy from stdin to connection")
1778 log.Fatalf("no remaining destinations")
1781func cmdDANEMakeRecord(c *cmd) {
1782 c.params = "usage selector matchtype [certificate.pem | publickey.pem | privatekey.pem]"
1783 c.help = `Print TLSA record for given certificate/key and parameters.
1786- usage: pkix-ta (0), pkix-ee (1), dane-ta (2), dane-ee (3)
1787- selector: cert (0), spki (1)
1788- matchtype: full (0), sha2-256 (1), sha2-512 (2)
1790Common DANE TLSA record parameters are: dane-ee spki sha2-256, or 3 1 1,
1791followed by a sha2-256 hash of the DER-encoded "SPKI" (subject public key info)
1792from the certificate. An example DNS zone file entry:
1794 _25._tcp.example.com. TLSA 3 1 1 133b919c9d65d8b1488157315327334ead8d83372db57465ecabf53ee5748aee
1796The first usable information from the pem file is used to compose the TLSA
1797record. In case of selector "cert", a certificate is required. Otherwise the
1798"subject public key info" (spki) of the first certificate or public or private
1799key (pkcs#8, pkcs#1 or ec private key) is used.
1807 var usage adns.TLSAUsage
1808 switch strings.ToLower(args[0]) {
1809 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
1810 usage = adns.TLSAUsagePKIXTA
1811 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
1812 usage = adns.TLSAUsagePKIXEE
1813 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
1814 usage = adns.TLSAUsageDANETA
1815 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
1816 usage = adns.TLSAUsageDANEEE
1818 if v, err := strconv.ParseUint(args[0], 10, 16); err != nil {
1819 log.Fatalf("bad usage %q", args[0])
1821 // Does not influence certificate association data, so we can accept other numbers.
1822 log.Printf("warning: continuing with unrecognized tlsa usage %d", v)
1823 usage = adns.TLSAUsage(v)
1827 var selector adns.TLSASelector
1828 switch strings.ToLower(args[1]) {
1829 case "cert", strconv.Itoa(int(adns.TLSASelectorCert)):
1830 selector = adns.TLSASelectorCert
1831 case "spki", strconv.Itoa(int(adns.TLSASelectorSPKI)):
1832 selector = adns.TLSASelectorSPKI
1834 log.Fatalf("bad selector %q", args[1])
1837 var matchType adns.TLSAMatchType
1838 switch strings.ToLower(args[2]) {
1839 case "full", strconv.Itoa(int(adns.TLSAMatchTypeFull)):
1840 matchType = adns.TLSAMatchTypeFull
1841 case "sha2-256", strconv.Itoa(int(adns.TLSAMatchTypeSHA256)):
1842 matchType = adns.TLSAMatchTypeSHA256
1843 case "sha2-512", strconv.Itoa(int(adns.TLSAMatchTypeSHA512)):
1844 matchType = adns.TLSAMatchTypeSHA512
1846 log.Fatalf("bad matchtype %q", args[2])
1849 buf, err := os.ReadFile(args[3])
1850 xcheckf(err, "reading certificate")
1852 var block *pem.Block
1853 block, buf = pem.Decode(buf)
1857 extra = " (with leftover data from pem file)"
1859 if selector == adns.TLSASelectorCert {
1860 log.Fatalf("no certificate found in pem file%s", extra)
1862 log.Fatalf("no certificate or public or private key found in pem file%s", extra)
1865 var cert *x509.Certificate
1867 if block.Type == "CERTIFICATE" {
1868 cert, err = x509.ParseCertificate(block.Bytes)
1869 xcheckf(err, "parse certificate")
1871 case adns.TLSASelectorCert:
1873 case adns.TLSASelectorSPKI:
1874 data = cert.RawSubjectPublicKeyInfo
1876 } else if selector == adns.TLSASelectorCert {
1877 // We need a certificate, just a public/private key won't do.
1878 log.Printf("skipping pem type %q, certificate is required", block.Type)
1881 var privKey, pubKey any
1885 _, err := x509.ParsePKIXPublicKey(block.Bytes)
1886 xcheckf(err, "parse pkix subject public key info (spki)")
1888 case "EC PRIVATE KEY":
1889 privKey, err = x509.ParseECPrivateKey(block.Bytes)
1890 xcheckf(err, "parse ec private key")
1891 case "RSA PRIVATE KEY":
1892 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
1893 xcheckf(err, "parse pkcs#1 rsa private key")
1894 case "RSA PUBLIC KEY":
1895 pubKey, err = x509.ParsePKCS1PublicKey(block.Bytes)
1896 xcheckf(err, "parse pkcs#1 rsa public key")
1898 // PKCS#8 private key
1899 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
1900 xcheckf(err, "parse pkcs#8 private key")
1902 log.Printf("skipping unrecognized pem type %q", block.Type)
1906 if pubKey == nil && privKey != nil {
1907 if signer, ok := privKey.(crypto.Signer); !ok {
1908 log.Fatalf("private key of type %T is not a signer, cannot get public key", privKey)
1910 pubKey = signer.Public()
1914 // Should not happen.
1915 log.Fatalf("internal error: did not find private or public key")
1917 data, err = x509.MarshalPKIXPublicKey(pubKey)
1918 xcheckf(err, "marshal pkix subject public key info (spki)")
1923 case adns.TLSAMatchTypeFull:
1924 case adns.TLSAMatchTypeSHA256:
1925 p := sha256.Sum256(data)
1927 case adns.TLSAMatchTypeSHA512:
1928 p := sha512.Sum512(data)
1931 fmt.Printf("%d %d %d %x\n", usage, selector, matchType, data)
1936func cmdDNSLookup(c *cmd) {
1937 c.params = "[ptr | mx | cname | ips | a | aaaa | ns | txt | srv | tlsa] name"
1938 c.help = `Lookup DNS name of given type.
1940Lookup always prints whether the response was DNSSEC-protected.
1944mox dns lookup ptr 1.1.1.1
1945mox dns lookup mx xmox.nl
1946mox dns lookup txt _dmarc.xmox.nl.
1947mox dns lookup tlsa _25._tcp.xmox.nl
1955 resolver := dns.StrictResolver{Pkg: "dns"}
1957 // like xparseDomain, but treat unparseable domain as an ASCII name so names with
1958 // underscores are still looked up, e,g <selector>._domainkey.<host>.
1959 xdomain := func(s string) dns.Domain {
1960 d, err := dns.ParseDomain(s)
1962 return dns.Domain{ASCII: strings.TrimSuffix(s, ".")}
1967 cmd, name := args[0], args[1]
1971 ip := xparseIP(name, "ip")
1972 ptrs, result, err := resolver.LookupAddr(context.Background(), ip.String())
1974 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
1976 fmt.Printf("names (%d, %s):\n", len(ptrs), dnssecStatus(result.Authentic))
1977 for _, ptr := range ptrs {
1978 fmt.Printf("- %s\n", ptr)
1982 name := xdomain(name)
1983 mxl, result, err := resolver.LookupMX(context.Background(), name.ASCII+".")
1985 log.Printf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
1986 // We can still have valid records...
1988 fmt.Printf("mx records (%d, %s):\n", len(mxl), dnssecStatus(result.Authentic))
1989 for _, mx := range mxl {
1990 fmt.Printf("- %s, preference %d\n", mx.Host, mx.Pref)
1994 name := xdomain(name)
1995 target, result, err := resolver.LookupCNAME(context.Background(), name.ASCII+".")
1997 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
1999 fmt.Printf("%s (%s)\n", target, dnssecStatus(result.Authentic))
2001 case "ips", "a", "aaaa":
2005 } else if cmd == "aaaa" {
2008 name := xdomain(name)
2009 ips, result, err := resolver.LookupIP(context.Background(), network, name.ASCII+".")
2011 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2013 fmt.Printf("records (%d, %s):\n", len(ips), dnssecStatus(result.Authentic))
2014 for _, ip := range ips {
2015 fmt.Printf("- %s\n", ip)
2019 name := xdomain(name)
2020 nsl, result, err := resolver.LookupNS(context.Background(), name.ASCII+".")
2022 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2024 fmt.Printf("ns records (%d, %s):\n", len(nsl), dnssecStatus(result.Authentic))
2025 for _, ns := range nsl {
2026 fmt.Printf("- %s\n", ns)
2030 host := xdomain(name)
2031 l, result, err := resolver.LookupTXT(context.Background(), host.ASCII+".")
2033 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2035 fmt.Printf("txt records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2036 for _, txt := range l {
2037 fmt.Printf("- %s\n", txt)
2041 host := xdomain(name)
2042 _, l, result, err := resolver.LookupSRV(context.Background(), "", "", host.ASCII+".")
2044 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2046 fmt.Printf("srv records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2047 for _, srv := range l {
2048 fmt.Printf("- host %s, port %d, priority %d, weight %d\n", srv.Target, srv.Port, srv.Priority, srv.Weight)
2052 host := xdomain(name)
2053 l, result, err := resolver.LookupTLSA(context.Background(), 0, "", host.ASCII+".")
2055 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2057 fmt.Printf("tlsa records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2058 for _, tlsa := range l {
2059 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)
2062 log.Fatalf("unknown record type %q", args[0])
2066func cmdDKIMGened25519(c *cmd) {
2067 c.params = ">$selector._domainkey.$domain.ed25519.privatekey.pkcs8.pem"
2068 c.help = `Generate a new ed25519 key for use with DKIM.
2070Ed25519 keys are much smaller than RSA keys of comparable cryptographic
2071strength. This is convenient because of maximum DNS message sizes. At the time
2072of writing, not many mail servers appear to support ed25519 DKIM keys though,
2073so it is recommended to sign messages with both RSA and ed25519 keys.
2075 if len(c.Parse()) != 0 {
2079 buf, err := mox.MakeDKIMEd25519Key(dns.Domain{}, dns.Domain{})
2080 xcheckf(err, "making dkim ed25519 key")
2081 _, err = os.Stdout.Write(buf)
2082 xcheckf(err, "writing dkim ed25519 key")
2085func cmdDKIMTXT(c *cmd) {
2086 c.params = "<$selector._domainkey.$domain.key.pkcs8.pem"
2087 c.help = `Print a DKIM DNS TXT record with the public key derived from the private key read from stdin.
2089The DNS should be configured as a TXT record at $selector._domainkey.$domain.
2091 if len(c.Parse()) != 0 {
2095 privKey, err := parseDKIMKey(os.Stdin)
2096 xcheckf(err, "reading dkim private key from stdin")
2100 Hashes: []string{"sha256"},
2101 Flags: []string{"s"},
2104 switch key := privKey.(type) {
2105 case *rsa.PrivateKey:
2106 r.PublicKey = key.Public()
2107 case ed25519.PrivateKey:
2108 r.PublicKey = key.Public()
2111 log.Fatalf("unsupported private key type %T, must be rsa or ed25519", privKey)
2114 record, err := r.Record()
2115 xcheckf(err, "making record")
2116 fmt.Print("<selector>._domainkey.<your.domain.> TXT ")
2120 s, record = record[:100], record[100:]
2124 fmt.Printf(`"%s" `, s)
2129func parseDKIMKey(r io.Reader) (any, error) {
2130 buf, err := io.ReadAll(r)
2132 return nil, fmt.Errorf("reading pem from stdin: %v", err)
2134 b, _ := pem.Decode(buf)
2136 return nil, fmt.Errorf("decoding pem: %v", err)
2138 privKey, err := x509.ParsePKCS8PrivateKey(b.Bytes)
2140 return nil, fmt.Errorf("parsing private key: %v", err)
2145func cmdDKIMVerify(c *cmd) {
2146 c.params = "message"
2147 c.help = `Verify the DKIM signatures in a message and print the results.
2149The message is parsed, and the DKIM-Signature headers are validated. Validation
2150of older messages may fail because the DNS records have been removed or changed
2151by now, or because the signature header may have specified an expiration time
2159 msgf, err := os.Open(args[0])
2160 xcheckf(err, "open message")
2162 results, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, false, dkim.DefaultPolicy, msgf, true)
2163 xcheckf(err, "dkim verify")
2165 for _, result := range results {
2167 if result.Sig == nil {
2168 log.Printf("warning: could not parse signature")
2170 sigh, err = result.Sig.Header()
2172 log.Printf("warning: packing signature: %s", err)
2176 if result.Record == nil {
2177 log.Printf("warning: missing DNS record")
2179 txt, err = result.Record.Record()
2181 log.Printf("warning: packing record: %s", err)
2184 fmt.Printf("status %q, err %v\nrecord %q\nheader %s\n", result.Status, result.Err, txt, sigh)
2188func cmdDKIMSign(c *cmd) {
2189 c.params = "message"
2190 c.help = `Sign a message, adding DKIM-Signature headers based on the domain in the From header.
2192The message is parsed, the domain looked up in the configuration files, and
2193DKIM-Signature headers generated. The message is printed with the DKIM-Signature
2201 msgf, err := os.Open(args[0])
2202 xcheckf(err, "open message")
2205 p, err := message.Parse(c.log.Logger, true, msgf)
2206 xcheckf(err, "parsing message")
2208 if len(p.Envelope.From) != 1 {
2209 log.Fatalf("found %d from headers, need exactly 1", len(p.Envelope.From))
2211 localpart, err := smtp.ParseLocalpart(p.Envelope.From[0].User)
2212 xcheckf(err, "parsing localpart of address in from-header")
2213 dom, err := dns.ParseDomain(p.Envelope.From[0].Host)
2214 xcheckf(err, "parsing domain of address in from-header")
2218 domConf, ok := mox.Conf.Domain(dom)
2220 log.Fatalf("domain %s not configured", dom)
2223 selectors := mox.DKIMSelectors(domConf.DKIM)
2224 headers, err := dkim.Sign(context.Background(), c.log.Logger, localpart, dom, selectors, false, msgf)
2225 xcheckf(err, "signing message with dkim")
2227 log.Fatalf("no DKIM configured for domain %s", dom)
2229 _, err = fmt.Fprint(os.Stdout, headers)
2230 xcheckf(err, "write headers")
2231 _, err = io.Copy(os.Stdout, msgf)
2232 xcheckf(err, "write message")
2235func cmdDKIMLookup(c *cmd) {
2236 c.params = "selector domain"
2237 c.help = "Lookup and print the DKIM record for the selector at the domain."
2243 selector := xparseDomain(args[0], "selector")
2244 domain := xparseDomain(args[1], "domain")
2246 status, record, txt, authentic, err := dkim.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, selector, domain)
2248 fmt.Printf("error: %s\n", err)
2250 if status != dkim.StatusNeutral {
2251 fmt.Printf("status: %s\n", status)
2254 fmt.Printf("TXT record: %s\n", txt)
2257 fmt.Println("dnssec-signed: yes")
2259 fmt.Println("dnssec-signed: no")
2262 fmt.Printf("Record:\n")
2264 "version", record.Version,
2265 "hashes", record.Hashes,
2267 "notes", record.Notes,
2268 "services", record.Services,
2269 "flags", record.Flags,
2271 for i := 0; i < len(pairs); i += 2 {
2272 fmt.Printf("\t%s: %v\n", pairs[i], pairs[i+1])
2277func cmdDMARCLookup(c *cmd) {
2279 c.help = "Lookup dmarc policy for domain, a DNS TXT record at _dmarc.<domain>, validate and print it."
2285 fromdomain := xparseDomain(args[0], "domain")
2286 _, domain, _, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, fromdomain)
2287 xcheckf(err, "dmarc lookup domain %s", fromdomain)
2288 fmt.Printf("dmarc record at domain %s: %s\n", domain, txt)
2289 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2292func dnssecStatus(v bool) string {
2294 return "with dnssec"
2296 return "without dnssec"
2299func cmdDMARCVerify(c *cmd) {
2300 c.params = "remoteip mailfromaddress helodomain < message"
2301 c.help = `Parse an email message and evaluate it against the DMARC policy of the domain in the From-header.
2303mailfromaddress and helodomain are used for SPF validation. If both are empty,
2304SPF validation is skipped.
2306mailfromaddress should be the address used as MAIL FROM in the SMTP session.
2307For DSN messages, that address may be empty. The helo domain was specified at
2308the beginning of the SMTP transaction that delivered the message. These values
2309can be found in message headers.
2316 var heloDomain *dns.Domain
2318 remoteIP := xparseIP(args[0], "remoteip")
2320 var mailfrom *smtp.Address
2322 a, err := smtp.ParseAddress(args[1])
2323 xcheckf(err, "parsing mailfrom address")
2327 d := xparseDomain(args[2], "helo domain")
2330 var received *spf.Received
2331 spfStatus := spf.StatusNone
2332 var spfIdentity *dns.Domain
2333 if mailfrom != nil || heloDomain != nil {
2334 spfArgs := spf.Args{
2336 LocalIP: net.ParseIP("127.0.0.1"),
2337 LocalHostname: dns.Domain{ASCII: "localhost"},
2339 if mailfrom != nil {
2340 spfArgs.MailFromLocalpart = mailfrom.Localpart
2341 spfArgs.MailFromDomain = mailfrom.Domain
2343 if heloDomain != nil {
2344 spfArgs.HelloDomain = dns.IPDomain{Domain: *heloDomain}
2346 rspf, spfDomain, expl, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfArgs)
2348 log.Printf("spf verify: %v (explanation: %q, authentic %v)", err, expl, authentic)
2351 spfStatus = received.Result
2352 // todo: should probably potentially do two separate spf validations
2353 if mailfrom != nil {
2354 spfIdentity = &mailfrom.Domain
2356 spfIdentity = heloDomain
2358 fmt.Printf("spf result: %s: %s (%s)\n", spfDomain, spfStatus, dnssecStatus(authentic))
2362 data, err := io.ReadAll(os.Stdin)
2363 xcheckf(err, "read message")
2364 dmarcFrom, _, _, err := message.From(c.log.Logger, false, bytes.NewReader(data), nil)
2365 xcheckf(err, "extract dmarc from message")
2367 const ignoreTestMode = false
2368 dkimResults, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, true, func(*dkim.Sig) error { return nil }, bytes.NewReader(data), ignoreTestMode)
2369 xcheckf(err, "dkim verify")
2370 for _, r := range dkimResults {
2371 fmt.Printf("dkim result: %q (err %v)\n", r.Status, r.Err)
2374 _, result := dmarc.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, dmarcFrom.Domain, dkimResults, spfStatus, spfIdentity, false)
2375 xcheckf(result.Err, "dmarc verify")
2376 fmt.Printf("dmarc from: %s\ndmarc status: %q\ndmarc reject: %v\ncmarc record: %s\n", dmarcFrom, result.Status, result.Reject, result.Record)
2379func cmdDMARCCheckreportaddrs(c *cmd) {
2381 c.help = `For each reporting address in the domain's DMARC record, check if it has opted into receiving reports (if needed).
2383A DMARC record can request reports about DMARC evaluations to be sent to an
2384email/http address. If the organizational domains of that of the DMARC record
2385and that of the report destination address do not match, the destination
2386address must opt-in to receiving DMARC reports by creating a DMARC record at
2387<dmarcdomain>._report._dmarc.<reportdestdomain>.
2394 dom := xparseDomain(args[0], "domain")
2395 _, domain, record, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dom)
2396 xcheckf(err, "dmarc lookup domain %s", dom)
2397 fmt.Printf("dmarc record at domain %s: %q\n", domain, txt)
2398 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2400 check := func(kind, addr string) {
2403 printResult := func(format string, args ...any) {
2404 fmt.Printf("%s %s: %s (%s)\n", kind, addr, fmt.Sprintf(format, args...), dnssecStatus(authentic))
2407 u, err := url.Parse(addr)
2409 printResult("parsing uri: %v (skipping)", addr, err)
2412 var destdom dns.Domain
2415 a, err := smtp.ParseAddress(u.Opaque)
2417 printResult("parsing destination email address %s: %v (skipping)", u.Opaque, err)
2422 printResult("unrecognized scheme in reporting address %s (skipping)", u.Scheme)
2426 if publicsuffix.Lookup(context.Background(), c.log.Logger, dom) == publicsuffix.Lookup(context.Background(), c.log.Logger, destdom) {
2427 printResult("pass (same organizational domain)")
2431 accepts, status, _, txts, authentic, err := dmarc.LookupExternalReportsAccepted(context.Background(), c.log.Logger, dns.StrictResolver{}, domain, destdom)
2433 txtaddr := fmt.Sprintf("%s._report._dmarc.%s", domain.ASCII, destdom.ASCII)
2435 txtstr = fmt.Sprintf(" (no txt records %s)", txtaddr)
2437 txtstr = fmt.Sprintf(" (txt record %s: %q)", txtaddr, txts)
2439 if status != dmarc.StatusNone {
2440 printResult("fail: %s%s", err, txtstr)
2442 printResult("pass%s", txtstr)
2443 } else if err != nil {
2444 printResult("fail: %s%s", err, txtstr)
2446 printResult("fail%s", txtstr)
2450 for _, uri := range record.AggregateReportAddresses {
2451 check("aggregate reporting", uri.Address)
2453 for _, uri := range record.FailureReportAddresses {
2454 check("failure reporting", uri.Address)
2458func cmdDMARCParsereportmsg(c *cmd) {
2459 c.params = "message ..."
2460 c.help = `Parse a DMARC report from an email message, and print its extracted details.
2462DMARC reports are periodically mailed, if requested in the DMARC DNS record of
2463a domain. Reports are sent by mail servers that received messages with our
2464domain in a From header. This may or may not be legatimate email. DMARC reports
2465contain summaries of evaluations of DMARC and DKIM/SPF, which can help
2466understand email deliverability problems.
2473 for _, arg := range args {
2474 f, err := os.Open(arg)
2475 xcheckf(err, "open %q", arg)
2476 feedback, err := dmarcrpt.ParseMessageReport(c.log.Logger, f)
2477 xcheckf(err, "parse report in %q", arg)
2478 meta := feedback.ReportMetadata
2479 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)
2480 if len(meta.Errors) > 0 {
2481 fmt.Printf("Errors:\n")
2482 for _, s := range meta.Errors {
2483 fmt.Printf("\t- %s\n", s)
2486 pol := feedback.PolicyPublished
2487 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)
2488 for _, record := range feedback.Records {
2489 idents := record.Identifiers
2490 fmt.Printf("\theaderfrom %q, envelopes from %q, to %q\n", idents.HeaderFrom, idents.EnvelopeFrom, idents.EnvelopeTo)
2491 eval := record.Row.PolicyEvaluated
2493 for _, reason := range eval.Reasons {
2494 reasons += "; " + string(reason.Type)
2495 if reason.Comment != "" {
2496 reasons += fmt.Sprintf(": %q", reason.Comment)
2499 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)
2500 for _, dkim := range record.AuthResults.DKIM {
2502 if dkim.HumanResult != "" {
2503 result = fmt.Sprintf(": %q", dkim.HumanResult)
2505 fmt.Printf("\t\tdkim %s; domain %q selector %q%s\n", dkim.Result, dkim.Domain, dkim.Selector, result)
2507 for _, spf := range record.AuthResults.SPF {
2508 fmt.Printf("\t\tspf %s; domain %q scope %q\n", spf.Result, spf.Domain, spf.Scope)
2514func cmdDMARCDBAddReport(c *cmd) {
2516 c.params = "fromdomain < message"
2517 c.help = "Add a DMARC report to the database."
2525 fromdomain := xparseDomain(args[0], "domain")
2526 fmt.Fprintln(os.Stderr, "reading report message from stdin")
2527 report, err := dmarcrpt.ParseMessageReport(c.log.Logger, os.Stdin)
2528 xcheckf(err, "parse message")
2529 err = dmarcdb.AddReport(context.Background(), report, fromdomain)
2530 xcheckf(err, "add dmarc report")
2533func cmdTLSRPTLookup(c *cmd) {
2535 c.help = `Lookup the TLSRPT record for the domain.
2537A TLSRPT record typically contains an email address where reports about TLS
2538connectivity should be sent. Mail servers attempting delivery to our domain
2539should attempt to use TLS. TLSRPT lets them report how many connection
2540successfully used TLS, and how what kind of errors occurred otherwise.
2547 d := xparseDomain(args[0], "domain")
2548 _, txt, err := tlsrpt.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, d)
2549 xcheckf(err, "tlsrpt lookup for %s", d)
2553func cmdTLSRPTParsereportmsg(c *cmd) {
2554 c.params = "message ..."
2555 c.help = `Parse and print the TLSRPT in the message.
2557The report is printed in formatted JSON.
2564 for _, arg := range args {
2565 f, err := os.Open(arg)
2566 xcheckf(err, "open %q", arg)
2567 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, f)
2568 xcheckf(err, "parse report in %q", arg)
2569 // todo future: only print the highlights?
2570 enc := json.NewEncoder(os.Stdout)
2571 enc.SetIndent("", "\t")
2572 enc.SetEscapeHTML(false)
2573 err = enc.Encode(reportJSON)
2574 xcheckf(err, "write report")
2578func cmdSPFCheck(c *cmd) {
2579 c.params = "domain ip"
2580 c.help = `Check the status of IP for the policy published in DNS for the domain.
2582IPs may be allowed to send for a domain, or disallowed, and several shades in
2583between. If not allowed, an explanation may be provided by the policy. If so,
2584the explanation is printed. The SPF mechanism that matched (if any) is also
2592 domain := xparseDomain(args[0], "domain")
2594 ip := xparseIP(args[1], "ip")
2596 spfargs := spf.Args{
2598 MailFromLocalpart: "user",
2599 MailFromDomain: domain,
2600 HelloDomain: dns.IPDomain{Domain: domain},
2601 LocalIP: net.ParseIP("127.0.0.1"),
2602 LocalHostname: dns.Domain{ASCII: "localhost"},
2604 r, _, explanation, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfargs)
2606 fmt.Printf("error: %s\n", err)
2608 if explanation != "" {
2609 fmt.Printf("explanation: %s\n", explanation)
2611 fmt.Printf("status: %s (%s)\n", r.Result, dnssecStatus(authentic))
2612 if r.Mechanism != "" {
2613 fmt.Printf("mechanism: %s\n", r.Mechanism)
2617func cmdSPFParse(c *cmd) {
2618 c.params = "txtrecord"
2619 c.help = "Parse the record as SPF record. If valid, nothing is printed."
2625 _, _, err := spf.ParseRecord(args[0])
2626 xcheckf(err, "parsing record")
2629func cmdSPFLookup(c *cmd) {
2631 c.help = "Lookup the SPF record for the domain and print it."
2637 domain := xparseDomain(args[0], "domain")
2638 _, txt, _, authentic, err := spf.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
2639 xcheckf(err, "spf lookup for %s", domain)
2641 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2644func cmdMTASTSLookup(c *cmd) {
2646 c.help = `Lookup the MTASTS record and policy for the domain.
2648MTA-STS is a mechanism for a domain to specify if it requires TLS connections
2649for delivering email. If a domain has a valid MTA-STS DNS TXT record at
2650_mta-sts.<domain> it signals it implements MTA-STS. A policy can then be
2651fetched at https://mta-sts.<domain>/.well-known/mta-sts.txt. The policy
2652specifies the mode (enforce, testing, none), which MX servers support TLS and
2653should be used, and how long the policy can be cached.
2660 domain := xparseDomain(args[0], "domain")
2662 record, policy, _, err := mtasts.Get(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
2664 fmt.Printf("error: %s\n", err)
2667 fmt.Printf("DNS TXT record _mta-sts.%s: %s\n", domain.ASCII, record.String())
2671 fmt.Printf("policy at https://mta-sts.%s/.well-known/mta-sts.txt:\n", domain.ASCII)
2672 fmt.Printf("%s", policy.String())
2676func cmdRetrain(c *cmd) {
2677 c.params = "accountname"
2678 c.help = `Recreate and retrain the junk filter for the account.
2680Useful after having made changes to the junk filter configuration, or if the
2681implementation has changed.
2689 ctlcmdRetrain(xctl(), args[0])
2692func ctlcmdRetrain(ctl *ctl, account string) {
2693 ctl.xwrite("retrain")
2698func cmdTLSRPTDBAddReport(c *cmd) {
2700 c.params = "< message"
2701 c.help = "Parse a TLS report from the message and add it to the database."
2703 c.flag.BoolVar(&hostReport, "hostreport", false, "report for a host instead of domain")
2711 // First read message, to get the From-header. Then parse it as TLSRPT.
2712 fmt.Fprintln(os.Stderr, "reading report message from stdin")
2713 buf, err := io.ReadAll(os.Stdin)
2714 xcheckf(err, "reading message")
2715 part, err := message.Parse(c.log.Logger, true, bytes.NewReader(buf))
2716 xcheckf(err, "parsing message")
2717 if part.Envelope == nil || len(part.Envelope.From) != 1 {
2718 log.Fatalf("message must have one From-header")
2720 from := part.Envelope.From[0]
2721 domain := xparseDomain(from.Host, "domain")
2723 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, bytes.NewReader(buf))
2724 xcheckf(err, "parsing tls report in message")
2726 mailfrom := from.User + "@" + from.Host // todo future: should escape and such
2727 report := reportJSON.Convert()
2728 err = tlsrptdb.AddReport(context.Background(), c.log, domain, mailfrom, hostReport, &report)
2729 xcheckf(err, "add tls report to database")
2732func cmdDNSBLCheck(c *cmd) {
2733 c.params = "zone ip"
2734 c.help = `Test if IP is in the DNS blocklist of the zone, e.g. bl.spamcop.net.
2736If the IP is in the blocklist, an explanation is printed. This is typically a
2737URL with more information.
2744 zone := xparseDomain(args[0], "zone")
2745 ip := xparseIP(args[1], "ip")
2747 status, explanation, err := dnsbl.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, zone, ip)
2748 fmt.Printf("status: %s\n", status)
2749 if status == dnsbl.StatusFail {
2750 fmt.Printf("explanation: %q\n", explanation)
2753 fmt.Printf("error: %s\n", err)
2757func cmdDNSBLCheckhealth(c *cmd) {
2759 c.help = `Check the health of the DNS blocklist represented by zone, e.g. bl.spamcop.net.
2761The health of a DNS blocklist can be checked by querying for 127.0.0.1 and
2762127.0.0.2. The second must and the first must not be present.
2769 zone := xparseDomain(args[0], "zone")
2770 err := dnsbl.CheckHealth(context.Background(), c.log.Logger, dns.StrictResolver{}, zone)
2771 xcheckf(err, "unhealthy")
2772 fmt.Println("healthy")
2775func cmdCheckupdate(c *cmd) {
2776 c.help = `Check if a newer version of mox is available.
2778A single DNS TXT lookup to _updates.xmox.nl tells if a new version is
2779available. If so, a changelog is fetched from https://updates.xmox.nl, and the
2780individual entries verified with a builtin public key. The changelog is
2783 if len(c.Parse()) != 0 {
2788 current, lastknown, _, err := mox.LastKnown()
2790 log.Printf("getting last known version: %s", err)
2792 fmt.Printf("last known version: %s\n", lastknown)
2793 fmt.Printf("current version: %s\n", current)
2795 latest, _, err := updates.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dns.Domain{ASCII: changelogDomain})
2796 xcheckf(err, "lookup of latest version")
2797 fmt.Printf("latest version: %s\n", latest)
2799 if latest.After(current) {
2800 changelog, err := updates.FetchChangelog(context.Background(), c.log.Logger, changelogURL, current, changelogPubKey)
2801 xcheckf(err, "fetching changelog")
2802 if len(changelog.Changes) == 0 {
2803 log.Printf("no changes in changelog")
2806 fmt.Println("Changelog")
2807 for _, c := range changelog.Changes {
2808 fmt.Println("\n" + strings.TrimSpace(c.Text))
2813func cmdCid(c *cmd) {
2815 c.help = `Turn an ID from a Received header into a cid, for looking up in logs.
2817A cid is essentially a connection counter initialized when mox starts. Each log
2818line contains a cid. Received headers added by mox contain a unique ID that can
2819be decrypted to a cid by admin of a mox instance only.
2827 recvidpath := mox.DataDirPath("receivedid.key")
2828 recvidbuf, err := os.ReadFile(recvidpath)
2829 xcheckf(err, "reading %s", recvidpath)
2830 if len(recvidbuf) != 16+8 {
2831 log.Fatalf("bad data in %s: got %d bytes, expect 16+8=24", recvidpath, len(recvidbuf))
2833 err = mox.ReceivedIDInit(recvidbuf[:16], recvidbuf[16:])
2834 xcheckf(err, "init receivedid")
2836 cid, err := mox.ReceivedToCid(args[0])
2837 xcheckf(err, "received id to cid")
2838 fmt.Printf("%x\n", cid)
2841func cmdVersion(c *cmd) {
2842 c.help = "Prints this mox version."
2843 if len(c.Parse()) != 0 {
2846 fmt.Println(moxvar.Version)
2847 fmt.Printf("%s %s/%s\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
2850func cmdWebapi(c *cmd) {
2851 c.params = "[method [baseurl-with-credentials]"
2852 c.help = "Lists available methods, prints request/response parameters for method, or calls a method with a request read from standard input."
2858 t := reflect.TypeOf((*webapi.Methods)(nil)).Elem()
2859 methods := map[string]reflect.Type{}
2861 for i := 0; i < t.NumMethod(); i++ {
2863 methods[mt.Name] = mt.Type
2864 ml = append(ml, mt.Name)
2868 fmt.Println(strings.Join(ml, "\n"))
2872 mt, ok := methods[args[0]]
2874 log.Fatalf("unknown method %q", args[0])
2876 resultNotJSON := mt.Out(0).Kind() == reflect.Interface
2879 fmt.Println("# Example request")
2881 printJSON("\t", mox.FillExample(nil, reflect.New(mt.In(1))).Interface())
2884 fmt.Println("Output is non-JSON data.")
2887 fmt.Println("# Example response")
2889 printJSON("\t", mox.FillExample(nil, reflect.New(mt.Out(0))).Interface())
2895 response = reflect.New(mt.Out(0))
2898 fmt.Fprintln(os.Stderr, "reading request from stdin...")
2899 request, err := io.ReadAll(os.Stdin)
2900 xcheckf(err, "read message")
2902 dec := json.NewDecoder(bytes.NewReader(request))
2903 dec.DisallowUnknownFields()
2904 err = dec.Decode(reflect.New(mt.In(1)).Interface())
2905 xcheckf(err, "parsing request")
2907 resp, err := http.PostForm(args[1]+args[0], url.Values{"request": []string{string(request)}})
2908 xcheckf(err, "http post")
2909 defer resp.Body.Close()
2910 if resp.StatusCode == http.StatusBadRequest {
2911 buf, err := io.ReadAll(&moxio.LimitReader{R: resp.Body, Limit: 10 * 1024})
2912 xcheckf(err, "reading response for 400 bad request error")
2913 err = json.Unmarshal(buf, &response)
2915 printJSON("", response)
2917 fmt.Fprintf(os.Stderr, "(not json)\n")
2918 os.Stderr.Write(buf)
2921 } else if resp.StatusCode != http.StatusOK {
2922 fmt.Fprintf(os.Stderr, "http response %s\n", resp.Status)
2923 _, err := io.Copy(os.Stderr, resp.Body)
2924 xcheckf(err, "copy body")
2926 err := json.NewDecoder(resp.Body).Decode(&resp)
2927 xcheckf(err, "unmarshal response")
2928 printJSON("", response)
2932func printJSON(indent string, v any) {
2933 fmt.Printf("%s", indent)
2934 enc := json.NewEncoder(os.Stdout)
2935 enc.SetIndent(indent, "\t")
2936 enc.SetEscapeHTML(false)
2937 err := enc.Encode(v)
2938 xcheckf(err, "encode json")
2941// 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.
2942func cmdBumpUIDValidity(c *cmd) {
2943 c.params = "account [mailbox]"
2944 c.help = `Change the IMAP UID validity of the mailbox, causing IMAP clients to refetch messages.
2946This can be useful after manually repairing metadata about the account/mailbox.
2948Opens account database file directly. Ensure mox does not have the account
2949open, or is not running.
2952 if len(args) != 1 && len(args) != 2 {
2957 a, err := store.OpenAccount(c.log, args[0])
2958 xcheckf(err, "open account")
2960 if err := a.Close(); err != nil {
2961 log.Printf("closing account: %v", err)
2965 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
2966 uidvalidity, err := a.NextUIDValidity(tx)
2968 return fmt.Errorf("assigning next uid validity: %v", err)
2971 q := bstore.QueryTx[store.Mailbox](tx)
2973 q.FilterEqual("Name", args[1])
2975 mbl, err := q.SortAsc("Name").List()
2977 return fmt.Errorf("looking up mailbox: %v", err)
2979 if len(args) == 2 && len(mbl) != 1 {
2980 return fmt.Errorf("looking up mailbox %q, found %d mailboxes", args[1], len(mbl))
2982 for _, mb := range mbl {
2983 mb.UIDValidity = uidvalidity
2984 err = tx.Update(&mb)
2986 return fmt.Errorf("updating uid validity for mailbox: %v", err)
2988 fmt.Printf("uid validity for %q updated to %d\n", mb.Name, uidvalidity)
2992 xcheckf(err, "updating database")
2995func cmdReassignUIDs(c *cmd) {
2996 c.params = "account [mailboxid]"
2997 c.help = `Reassign UIDs in one mailbox or all mailboxes in an account and bump UID validity, causing IMAP clients to refetch messages.
2999Opens account database file directly. Ensure mox does not have the account
3000open, or is not running.
3003 if len(args) != 1 && len(args) != 2 {
3010 mailboxID, err = strconv.ParseInt(args[1], 10, 64)
3011 xcheckf(err, "parsing mailbox id")
3015 a, err := store.OpenAccount(c.log, args[0])
3016 xcheckf(err, "open account")
3018 if err := a.Close(); err != nil {
3019 log.Printf("closing account: %v", err)
3023 // Gather the last-assigned UIDs per mailbox.
3024 uidlasts := map[int64]store.UID{}
3026 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3027 // Reassign UIDs, going per mailbox. We assign starting at 1, only changing the
3028 // message if it isn't already at the intended UID. Doing it in this order ensures
3029 // we don't get into trouble with duplicate UIDs for a mailbox. We assign a new
3030 // modseq. Not strictly needed, for doesn't hurt.
3031 modseq, err := a.NextModSeq(tx)
3032 xcheckf(err, "assigning next modseq")
3034 q := bstore.QueryTx[store.Message](tx)
3036 q.FilterNonzero(store.Message{MailboxID: mailboxID})
3038 q.SortAsc("MailboxID", "UID")
3039 err = q.ForEach(func(m store.Message) error {
3040 uidlasts[m.MailboxID]++
3041 uid := uidlasts[m.MailboxID]
3045 if err := tx.Update(&m); err != nil {
3046 return fmt.Errorf("updating uid for message: %v", err)
3052 return fmt.Errorf("reading through messages: %v", err)
3055 // Now update the uidnext and uidvalidity for each mailbox.
3056 err = bstore.QueryTx[store.Mailbox](tx).ForEach(func(mb store.Mailbox) error {
3057 // Assign each mailbox a completely new uidvalidity.
3058 uidvalidity, err := a.NextUIDValidity(tx)
3060 return fmt.Errorf("assigning next uid validity: %v", err)
3063 if mb.UIDValidity >= uidvalidity {
3064 // This should not happen, but since we're fixing things up after a hypothetical
3065 // mishap, might as well account for inconsistent uidvalidity.
3066 next := store.NextUIDValidity{ID: 1, Next: mb.UIDValidity + 2}
3067 if err := tx.Update(&next); err != nil {
3068 log.Printf("updating nextuidvalidity: %v, continuing", err)
3072 mb.UIDValidity = uidvalidity
3074 mb.UIDNext = uidlasts[mb.ID] + 1
3075 if err := tx.Update(&mb); err != nil {
3076 return fmt.Errorf("updating uidvalidity and uidnext for mailbox: %v", err)
3081 return fmt.Errorf("updating mailboxes: %v", err)
3085 xcheckf(err, "updating database")
3088func cmdFixUIDMeta(c *cmd) {
3089 c.params = "account"
3090 c.help = `Fix inconsistent UIDVALIDITY and UIDNEXT in messages/mailboxes/account.
3092The next UID to use for a message in a mailbox should always be higher than any
3093existing message UID in the mailbox. If it is not, the mailbox UIDNEXT is
3096Each mailbox has a UIDVALIDITY sequence number, which should always be lower
3097than the per-account next UIDVALIDITY to use. If it is not, the account next
3098UIDVALIDITY is updated.
3100Opens account database file directly. Ensure mox does not have the account
3101open, or is not running.
3109 a, err := store.OpenAccount(c.log, args[0])
3110 xcheckf(err, "open account")
3112 if err := a.Close(); err != nil {
3113 log.Printf("closing account: %v", err)
3117 var maxUIDValidity uint32
3119 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3120 // We look at each mailbox, retrieve its max UID and compare against the mailbox
3122 err := bstore.QueryTx[store.Mailbox](tx).ForEach(func(mb store.Mailbox) error {
3123 if mb.UIDValidity > maxUIDValidity {
3124 maxUIDValidity = mb.UIDValidity
3126 m, err := bstore.QueryTx[store.Message](tx).FilterNonzero(store.Message{MailboxID: mb.ID}).SortDesc("UID").Limit(1).Get()
3127 if err == bstore.ErrAbsent || err == nil && m.UID < mb.UIDNext {
3129 } else if err != nil {
3130 return fmt.Errorf("finding message with max uid in mailbox: %w", err)
3132 olduidnext := mb.UIDNext
3133 mb.UIDNext = m.UID + 1
3134 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)
3135 if err := tx.Update(&mb); err != nil {
3136 return fmt.Errorf("updating mailbox uidnext: %v", err)
3141 return fmt.Errorf("processing mailboxes: %v", err)
3144 uidvalidity := store.NextUIDValidity{ID: 1}
3145 if err := tx.Get(&uidvalidity); err != nil {
3146 return fmt.Errorf("reading account next uidvalidity: %v", err)
3148 if maxUIDValidity >= uidvalidity.Next {
3149 log.Printf("account next uidvalidity %d <= highest uidvalidity %d found in mailbox, resetting account next uidvalidity to %d", uidvalidity.Next, maxUIDValidity, maxUIDValidity+1)
3150 uidvalidity.Next = maxUIDValidity + 1
3151 if err := tx.Update(&uidvalidity); err != nil {
3152 return fmt.Errorf("updating account next uidvalidity: %v", err)
3158 xcheckf(err, "updating database")
3161func cmdFixmsgsize(c *cmd) {
3162 c.params = "[account]"
3163 c.help = `Ensure message sizes in the database matching the sum of the message prefix length and on-disk file size.
3165Messages with an inconsistent size are also parsed again.
3167If an inconsistency is found, you should probably also run "mox
3168bumpuidvalidity" on the mailboxes or entire account to force IMAP clients to
3181 ctlcmdFixmsgsize(xctl(), account)
3184func ctlcmdFixmsgsize(ctl *ctl, account string) {
3185 ctl.xwrite("fixmsgsize")
3188 ctl.xstreamto(os.Stdout)
3191func cmdReparse(c *cmd) {
3192 c.params = "[account]"
3193 c.help = `Parse all messages in the account or all accounts again.
3195Can be useful after upgrading mox with improved message parsing. Messages are
3196parsed in batches, so other access to the mailboxes/messages are not blocked
3197while reparsing all messages.
3209 ctlcmdReparse(xctl(), account)
3212func ctlcmdReparse(ctl *ctl, account string) {
3213 ctl.xwrite("reparse")
3216 ctl.xstreamto(os.Stdout)
3219func cmdEnsureParsed(c *cmd) {
3220 c.params = "account"
3221 c.help = "Ensure messages in the database have a pre-parsed MIME form in the database."
3223 c.flag.BoolVar(&all, "all", false, "store new parsed message for all messages")
3230 a, err := store.OpenAccount(c.log, args[0])
3231 xcheckf(err, "open account")
3233 if err := a.Close(); err != nil {
3234 log.Printf("closing account: %v", err)
3239 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3240 q := bstore.QueryTx[store.Message](tx)
3241 q.FilterEqual("Expunged", false)
3242 q.FilterFn(func(m store.Message) bool {
3243 return all || m.ParsedBuf == nil
3247 return fmt.Errorf("list messages: %v", err)
3249 for _, m := range l {
3250 mr := a.MessageReader(m)
3251 p, err := message.EnsurePart(c.log.Logger, false, mr, m.Size)
3253 log.Printf("parsing message %d: %v (continuing)", m.ID, err)
3255 m.ParsedBuf, err = json.Marshal(p)
3257 return fmt.Errorf("marshal parsed message: %v", err)
3259 if err := tx.Update(&m); err != nil {
3260 return fmt.Errorf("update message: %v", err)
3266 xcheckf(err, "update messages with parsed mime structure")
3267 fmt.Printf("%d messages updated\n", n)
3270func cmdRecalculateMailboxCounts(c *cmd) {
3271 c.params = "account"
3272 c.help = `Recalculate message counts for all mailboxes in the account, and total message size for quota.
3274When a message is added to/removed from a mailbox, or when message flags change,
3275the total, unread, unseen and deleted messages are accounted, the total size of
3276the mailbox, and the total message size for the account. In case of a bug in
3277this accounting, the numbers could become incorrect. This command will find, fix
3286 ctlcmdRecalculateMailboxCounts(xctl(), args[0])
3289func ctlcmdRecalculateMailboxCounts(ctl *ctl, account string) {
3290 ctl.xwrite("recalculatemailboxcounts")
3293 ctl.xstreamto(os.Stdout)
3296func cmdMessageParse(c *cmd) {
3297 c.params = "message.eml"
3298 c.help = "Parse message, print JSON representation."
3301 c.flag.BoolVar(&smtputf8, "smtputf8", false, "check if message needs smtputf8")
3307 f, err := os.Open(args[0])
3308 xcheckf(err, "open")
3311 part, err := message.Parse(c.log.Logger, false, f)
3312 xcheckf(err, "parsing message")
3313 err = part.Walk(c.log.Logger, nil)
3314 xcheckf(err, "parsing nested parts")
3315 enc := json.NewEncoder(os.Stdout)
3316 enc.SetIndent("", "\t")
3317 enc.SetEscapeHTML(false)
3318 err = enc.Encode(part)
3319 xcheckf(err, "write")
3321 hasNonASCII := func(r io.Reader) bool {
3322 br := bufio.NewReader(r)
3324 b, err := br.ReadByte()
3328 xcheckf(err, "read header")
3336 var walk func(p *message.Part) bool
3337 walk = func(p *message.Part) bool {
3338 if hasNonASCII(p.HeaderReader()) {
3341 for _, pp := range p.Parts {
3349 fmt.Println("message needs smtputf8:", walk(&part))
3353func cmdOpenaccounts(c *cmd) {
3355 c.params = "datadir account ..."
3356 c.help = `Open and close accounts, for triggering data upgrades, for tests.
3358Opens database files directly, not going through a running mox instance.
3366 dataDir := filepath.Clean(args[0])
3367 for _, accName := range args[1:] {
3368 accDir := filepath.Join(dataDir, "accounts", accName)
3369 log.Printf("opening account %s...", accDir)
3370 a, err := store.OpenAccountDB(c.log, accDir, accName)
3371 xcheckf(err, "open account %s", accName)
3372 err = a.ThreadingWait(c.log)
3373 xcheckf(err, "wait for threading upgrade to complete for %s", accName)
3375 xcheckf(err, "close account %s", accName)
3379func cmdReassignthreads(c *cmd) {
3380 c.params = "[account]"
3381 c.help = `Reassign message threads.
3383For all accounts, or optionally only the specified account.
3385Threading for all messages in an account is first reset, and new base subject
3386and normalized message-id saved with the message. Then all messages are
3387evaluated and matched against their parents/ancestors.
3389Messages are matched based on the References header, with a fall-back to an
3390In-Reply-To header, and if neither is present/valid, based only on base
3393A References header typically points to multiple previous messages in a
3394hierarchy. From oldest ancestor to most recent parent. An In-Reply-To header
3395would have only a message-id of the parent message.
3397A message is only linked to a parent/ancestor if their base subject is the
3398same. This ensures unrelated replies, with a new subject, are placed in their
3401The base subject is lower cased, has whitespace collapsed to a single
3402space, and some components removed: leading "Re:", "Fwd:", "Fw:", or bracketed
3403tag (that mailing lists often add, e.g. "[listname]"), trailing "(fwd)", or
3404enclosing "[fwd: ...]".
3406Messages are linked to all their ancestors. If an intermediate parent/ancestor
3407message is deleted in the future, the message can still be linked to the earlier
3408ancestors. If the direct parent already wasn't available while matching, this is
3409stored as the message having a "missing link" to its stored ancestors.
3421 ctlcmdReassignthreads(xctl(), account)
3424func ctlcmdReassignthreads(ctl *ctl, account string) {
3425 ctl.xwrite("reassignthreads")
3428 ctl.xstreamto(os.Stdout)
3431func cmdReadmessages(c *cmd) {
3433 c.params = "datadir account ..."
3434 c.help = `Open account, parse several headers for all messages.
3436For performance testing.
3438Opens database files directly, not going through a running mox instance.
3441 gomaxprocs := runtime.GOMAXPROCS(0)
3442 var procs, workqueuesize, limit int
3443 c.flag.IntVar(&procs, "procs", gomaxprocs, "number of goroutines for reading messages")
3444 c.flag.IntVar(&workqueuesize, "workqueuesize", 2*gomaxprocs, "number of messages to keep in work queue")
3445 c.flag.IntVar(&limit, "limit", 0, "number of messages to process if greater than zero")
3451 type threadPrep struct {
3456 threadingFields := [][]byte{
3457 []byte("references"),
3458 []byte("in-reply-to"),
3461 dataDir := filepath.Clean(args[0])
3462 for _, accName := range args[1:] {
3463 accDir := filepath.Join(dataDir, "accounts", accName)
3464 log.Printf("opening account %s...", accDir)
3465 a, err := store.OpenAccountDB(c.log, accDir, accName)
3466 xcheckf(err, "open account %s", accName)
3468 prepareMessages := func(in, out chan moxio.Work[store.Message, threadPrep]) {
3469 headerbuf := make([]byte, 8*1024)
3470 scratch := make([]byte, 4*1024)
3478 var partialPart struct {
3482 if err := json.Unmarshal(m.ParsedBuf, &partialPart); err != nil {
3483 w.Err = fmt.Errorf("unmarshal part: %v", err)
3485 size := partialPart.BodyOffset - partialPart.HeaderOffset
3486 if int(size) > len(headerbuf) {
3487 headerbuf = make([]byte, size)
3490 buf := headerbuf[:int(size)]
3491 err := func() error {
3492 mr := a.MessageReader(m)
3495 // ReadAt returns whole buffer or error. Single read should be fast.
3496 n, err := mr.ReadAt(buf, partialPart.HeaderOffset)
3497 if err != nil || n != len(buf) {
3498 return fmt.Errorf("read header: %v", err)
3504 } else if h, err := message.ParseHeaderFields(buf, scratch, threadingFields); err != nil {
3507 w.Out.references = h["References"]
3508 w.Out.inReplyTo = h["In-Reply-To"]
3521 processMessage := func(m store.Message, prep threadPrep) error {
3523 log.Printf("%d messages (delta %s)", n, time.Since(t))
3530 wq := moxio.NewWorkQueue[store.Message, threadPrep](procs, workqueuesize, prepareMessages, processMessage)
3532 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3533 q := bstore.QueryTx[store.Message](tx)
3534 q.FilterEqual("Expunged", false)
3539 err = q.ForEach(wq.Add)
3547 xcheckf(err, "processing message")
3550 xcheckf(err, "close account %s", accName)
3551 log.Printf("account %s, total time %s", accName, time.Since(t0))
3555func cmdQueueFillRetired(c *cmd) {
3557 c.help = `Fill retired messag and webhooks queue with testdata.
3559For testing the pagination. Operates directly on queue database.
3562 c.flag.IntVar(&n, "n", 10000, "retired messages and retired webhooks to insert")
3570 xcheckf(err, "init queue")
3571 err = queue.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3574 // Cause autoincrement ID for queue.Msg to be forwarded, and use the reserved ID
3575 // space for inserting retired messages.
3577 err = tx.Insert(&fm)
3578 xcheckf(err, "temporarily insert message to get autoincrement sequence")
3579 err = tx.Delete(&fm)
3580 xcheckf(err, "removing temporary message for resetting autoincrement sequence")
3582 err = tx.Insert(&fm)
3583 xcheckf(err, "temporarily insert message to forward autoincrement sequence")
3584 err = tx.Delete(&fm)
3585 xcheckf(err, "removing temporary message after forwarding autoincrement sequence")
3588 // And likewise for webhooks.
3589 fh := queue.Hook{Account: "x", URL: "x", NextAttempt: time.Now()}
3590 err = tx.Insert(&fh)
3591 xcheckf(err, "temporarily insert webhook to get autoincrement sequence")
3592 err = tx.Delete(&fh)
3593 xcheckf(err, "removing temporary webhook for resetting autoincrement sequence")
3595 err = tx.Insert(&fh)
3596 xcheckf(err, "temporarily insert webhook to forward autoincrement sequence")
3597 err = tx.Delete(&fh)
3598 xcheckf(err, "removing temporary webhook after forwarding autoincrement sequence")
3601 for i := 0; i < n; i++ {
3602 t0 := now.Add(-time.Duration(i) * time.Second)
3603 last := now.Add(-time.Duration(i/10) * time.Second)
3604 mr := queue.MsgRetired{
3605 ID: fm.ID + int64(i),
3607 SenderAccount: "test",
3608 SenderLocalpart: "mox",
3609 SenderDomainStr: "localhost",
3610 FromID: fmt.Sprintf("%016d", i),
3611 RecipientLocalpart: "mox",
3612 RecipientDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "localhost"}},
3613 RecipientDomainStr: "localhost",
3616 Results: []queue.MsgResult{
3619 Duration: time.Millisecond,
3626 Size: int64(i * 100),
3627 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
3628 Subject: fmt.Sprintf("test message %d", i),
3629 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
3631 RecipientAddress: "mox@localhost",
3633 KeepUntil: now.Add(48 * time.Hour),
3635 err := tx.Insert(&mr)
3636 xcheckf(err, "inserting retired message")
3639 for i := 0; i < n; i++ {
3640 t0 := now.Add(-time.Duration(i) * time.Second)
3641 last := now.Add(-time.Duration(i/10) * time.Second)
3646 hr := queue.HookRetired{
3647 ID: fh.ID + int64(i),
3648 QueueMsgID: fm.ID + int64(i),
3649 FromID: fmt.Sprintf("%016d", i),
3650 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
3651 Subject: fmt.Sprintf("test message %d", i),
3652 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
3654 URL: "http://localhost/hook",
3655 IsIncoming: i%10 == 0,
3656 OutgoingEvent: event,
3661 Results: []queue.HookResult{
3664 Duration: time.Millisecond,
3665 URL: "http://localhost/hook",
3674 KeepUntil: now.Add(48 * time.Hour),
3676 err := tx.Insert(&hr)
3677 xcheckf(err, "inserting retired hook")
3682 xcheckf(err, "add to queue")
3683 log.Printf("added %d retired messages and %d retired webhooks", n, n)