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},
139 {"licenses", cmdLicenses},
141 {"config test", cmdConfigTest},
142 {"config dnscheck", cmdConfigDNSCheck},
143 {"config dnsrecords", cmdConfigDNSRecords},
144 {"config describe-domains", cmdConfigDescribeDomains},
145 {"config describe-static", cmdConfigDescribeStatic},
146 {"config account add", cmdConfigAccountAdd},
147 {"config account rm", cmdConfigAccountRemove},
148 {"config address add", cmdConfigAddressAdd},
149 {"config address rm", cmdConfigAddressRemove},
150 {"config domain add", cmdConfigDomainAdd},
151 {"config domain rm", cmdConfigDomainRemove},
152 {"config alias list", cmdConfigAliasList},
153 {"config alias print", cmdConfigAliasPrint},
154 {"config alias add", cmdConfigAliasAdd},
155 {"config alias update", cmdConfigAliasUpdate},
156 {"config alias rm", cmdConfigAliasRemove},
157 {"config alias addaddr", cmdConfigAliasAddaddr},
158 {"config alias rmaddr", cmdConfigAliasRemoveaddr},
160 {"config describe-sendmail", cmdConfigDescribeSendmail},
161 {"config printservice", cmdConfigPrintservice},
162 {"config ensureacmehostprivatekeys", cmdConfigEnsureACMEHostprivatekeys},
163 {"config example", cmdConfigExample},
165 {"checkupdate", cmdCheckupdate},
167 {"clientconfig", cmdClientConfig},
168 {"deliver", cmdDeliver},
169 // 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
170 {"dane dial", cmdDANEDial},
171 {"dane dialmx", cmdDANEDialmx},
172 {"dane makerecord", cmdDANEMakeRecord},
173 {"dns lookup", cmdDNSLookup},
174 {"dkim gened25519", cmdDKIMGened25519},
175 {"dkim genrsa", cmdDKIMGenrsa},
176 {"dkim lookup", cmdDKIMLookup},
177 {"dkim txt", cmdDKIMTXT},
178 {"dkim verify", cmdDKIMVerify},
179 {"dkim sign", cmdDKIMSign},
180 {"dmarc lookup", cmdDMARCLookup},
181 {"dmarc parsereportmsg", cmdDMARCParsereportmsg},
182 {"dmarc verify", cmdDMARCVerify},
183 {"dmarc checkreportaddrs", cmdDMARCCheckreportaddrs},
184 {"dnsbl check", cmdDNSBLCheck},
185 {"dnsbl checkhealth", cmdDNSBLCheckhealth},
186 {"mtasts lookup", cmdMTASTSLookup},
187 {"retrain", cmdRetrain},
188 {"sendmail", cmdSendmail},
189 {"spf check", cmdSPFCheck},
190 {"spf lookup", cmdSPFLookup},
191 {"spf parse", cmdSPFParse},
192 {"tlsrpt lookup", cmdTLSRPTLookup},
193 {"tlsrpt parsereportmsg", cmdTLSRPTParsereportmsg},
194 {"version", cmdVersion},
195 {"webapi", cmdWebapi},
197 {"example", cmdExample},
198 {"bumpuidvalidity", cmdBumpUIDValidity},
199 {"reassignuids", cmdReassignUIDs},
200 {"fixuidmeta", cmdFixUIDMeta},
201 {"fixmsgsize", cmdFixmsgsize},
202 {"reparse", cmdReparse},
203 {"ensureparsed", cmdEnsureParsed},
204 {"recalculatemailboxcounts", cmdRecalculateMailboxCounts},
205 {"message parse", cmdMessageParse},
206 {"reassignthreads", cmdReassignthreads},
209 {"helpall", cmdHelpall},
210 {"junk analyze", cmdJunkAnalyze},
211 {"junk check", cmdJunkCheck},
212 {"junk play", cmdJunkPlay},
213 {"junk test", cmdJunkTest},
214 {"junk train", cmdJunkTrain},
215 {"dmarcdb addreport", cmdDMARCDBAddReport},
216 {"tlsrptdb addreport", cmdTLSRPTDBAddReport},
217 {"updates addsigned", cmdUpdatesAddSigned},
218 {"updates genkey", cmdUpdatesGenkey},
219 {"updates pubkey", cmdUpdatesPubkey},
220 {"updates serve", cmdUpdatesServe},
221 {"updates verify", cmdUpdatesVerify},
222 {"gentestdata", cmdGentestdata},
223 {"ximport maildir", cmdXImportMaildir},
224 {"ximport mbox", cmdXImportMbox},
225 {"openaccounts", cmdOpenaccounts},
226 {"readmessages", cmdReadmessages},
227 {"queuefillretired", cmdQueueFillRetired},
233 for _, xc := range commands {
234 c := cmd{words: strings.Split(xc.cmd, " "), fn: xc.fn}
235 cmds = append(cmds, c)
243 // Set before calling command.
246 _gather bool // Set when using Parse to gather usage for a command.
248 // Set by invoked command or Parse.
249 unlisted bool // If set, command is not listed until at least some words are matched from command.
250 params string // Arguments to command. Multiple lines possible.
251 help string // Additional explanation. First line is synopsis, the rest is only printed for an explicit help/usage for that command.
257func (c *cmd) Parse() []string {
258 // To gather params and usage information, we just run the command but cause this
259 // panic after the command has registered its flags and set its params and help
260 // information. This is then caught and that info printed.
265 c.flag.Usage = c.Usage
266 c.flag.Parse(c.flagArgs)
267 c.args = c.flag.Args()
271func (c *cmd) gather() {
272 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
276 // panic generated by Parse.
284func (c *cmd) makeUsage() string {
285 var r strings.Builder
286 cs := "mox " + strings.Join(c.words, " ")
287 for i, line := range strings.Split(strings.TrimSpace(c.params), "\n") {
295 fmt.Fprintf(&r, "%6s %s%s\n", s, cs, line)
298 c.flag.PrintDefaults()
302func (c *cmd) printUsage() {
303 fmt.Fprint(os.Stderr, c.makeUsage())
305 fmt.Fprint(os.Stderr, "\n"+c.help+"\n")
309func (c *cmd) Usage() {
314func cmdHelp(c *cmd) {
315 c.params = "[command ...]"
316 c.help = `Prints help about matching commands.
318If multiple commands match, they are listed along with the first line of their help text.
319If a single command matches, its usage and full help text is printed.
326 prefix := func(l, pre []string) bool {
327 if len(pre) > len(l) {
330 return slices.Equal(pre, l[:len(pre)])
334 for _, c := range cmds {
335 if slices.Equal(c.words, args) {
337 fmt.Print(c.makeUsage())
339 fmt.Print("\n" + c.help + "\n")
342 } else if prefix(c.words, args) {
343 partial = append(partial, c)
346 if len(partial) == 0 {
347 fmt.Fprintf(os.Stderr, "%s: unknown command\n", strings.Join(args, " "))
350 for _, c := range partial {
352 line := "mox " + strings.Join(c.words, " ")
353 fmt.Printf("%s\n", line)
355 fmt.Printf("\t%s\n", strings.Split(c.help, "\n")[0])
360func cmdHelpall(c *cmd) {
362 c.help = `Print all detailed usage and help information for all listed commands.
364Used to generate documentation.
372 for _, c := range cmds {
378 fmt.Fprintf(os.Stderr, "\n")
382 fmt.Fprintf(os.Stderr, "# mox %s\n\n", strings.Join(c.words, " "))
384 fmt.Fprintln(os.Stderr, c.help+"\n")
387 s = "\t" + strings.ReplaceAll(s, "\n", "\n\t")
388 fmt.Fprintln(os.Stderr, s)
392func usage(l []cmd, unlisted bool) {
395 lines = append(lines, "mox [-config config/mox.conf] [-pedantic] ...")
397 for _, c := range l {
399 if c.unlisted && !unlisted {
402 for _, line := range strings.Split(c.params, "\n") {
403 x := append([]string{"mox"}, c.words...)
407 lines = append(lines, strings.Join(x, " "))
410 for i, line := range lines {
415 fmt.Fprintln(os.Stderr, pre+line)
420var loglevel string // Empty will be interpreted as info, except by localserve.
423// subcommands that are not "serve" should use this function to load the config, it
424// restores any loglevel specified on the command-line, instead of using the
425// loglevels from the config file and it does not load files like TLS keys/certs.
426func mustLoadConfig() {
427 mox.MustLoadConfig(false, false)
432 if level, ok := mlog.Levels[ll]; ok {
433 mox.Conf.Log[""] = level
434 mlog.SetConfig(mox.Conf.Log)
436 log.Fatal("unknown loglevel", slog.String("loglevel", loglevel))
439 mox.SetPedantic(true)
444 // CheckConsistencyOnClose is true by default, for all the test packages. A regular
445 // mox server should never use it. But integration tests enable it again with a
447 store.CheckConsistencyOnClose = false
449 ctxbg := context.Background()
455 // If invoked as sendmail, e.g. /usr/sbin/sendmail, we do enough so cron can get a
456 // message sent using smtp submission to a configured server.
457 if len(os.Args) > 0 && filepath.Base(os.Args[0]) == "sendmail" {
459 flag: flag.NewFlagSet("sendmail", flag.ExitOnError),
460 flagArgs: os.Args[1:],
461 log: mlog.New("sendmail", nil),
467 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")
468 flag.StringVar(&loglevel, "loglevel", "", "if non-empty, this log level is set early in startup")
469 flag.BoolVar(&pedantic, "pedantic", false, "protocol violations result in errors instead of accepting/working around them")
470 flag.BoolVar(&store.CheckConsistencyOnClose, "checkconsistency", false, "dangerous option for testing only, enables data checks that abort/panic when inconsistencies are found")
472 var cpuprofile, memprofile, tracefile string
473 flag.StringVar(&cpuprofile, "cpuprof", "", "store cpu profile to file")
474 flag.StringVar(&memprofile, "memprof", "", "store mem profile to file")
475 flag.StringVar(&tracefile, "trace", "", "store execution trace to file")
477 flag.Usage = func() { usage(cmds, false) }
485 defer traceExecution(tracefile)()
487 defer profile(cpuprofile, memprofile)()
490 mox.SetPedantic(true)
493 mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
498 if level, ok := mlog.Levels[ll]; ok {
499 mox.Conf.Log[""] = level
500 mlog.SetConfig(mox.Conf.Log)
501 // note: SetConfig may be called again when subcommands loads config.
503 log.Fatalf("unknown loglevel %q", loglevel)
508 for _, c := range cmds {
509 for i, w := range c.words {
510 if i >= len(args) || w != args[i] {
512 partial = append(partial, c)
517 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
518 c.flagArgs = args[len(c.words):]
519 c.log = mlog.New(strings.Join(c.words, ""), nil)
523 if len(partial) > 0 {
529func xcheckf(err error, format string, args ...any) {
533 msg := fmt.Sprintf(format, args...)
534 log.Fatalf("%s: %s", msg, err)
537func xparseIP(s, what string) net.IP {
540 log.Fatalf("invalid %s: %q", what, s)
545func xparseDomain(s, what string) dns.Domain {
546 d, err := dns.ParseDomain(s)
547 xcheckf(err, "parsing %s %q", what, s)
551func cmdClientConfig(c *cmd) {
553 c.help = `Print the configuration for email clients for a domain.
555Sending email is typically not done on the SMTP port 25, but on submission
556ports 465 (with TLS) and 587 (without initial TLS, but usually added to the
557connection with STARTTLS). For IMAP, the port with TLS is 993 and without is
560Without TLS/STARTTLS, passwords are sent in clear text, which should only be
561configured over otherwise secured connections, like a VPN.
567 d := xparseDomain(args[0], "domain")
572func printClientConfig(d dns.Domain) {
573 cc, err := mox.ClientConfigsDomain(d)
574 xcheckf(err, "getting client config")
575 fmt.Printf("%-20s %-30s %5s %-15s %s\n", "Protocol", "Host", "Port", "Listener", "Note")
576 for _, e := range cc.Entries {
577 fmt.Printf("%-20s %-30s %5d %-15s %s\n", e.Protocol, e.Host, e.Port, e.Listener, e.Note)
580To prevent authentication mechanism downgrade attempts that may result in
581clients sending plain text passwords to a MitM, clients should always be
582explicitly configured with the most secure authentication mechanism supported,
583the first of: SCRAM-SHA-256-PLUS, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, SCRAM-SHA-1,
588func cmdConfigTest(c *cmd) {
589 c.help = `Parses and validates the configuration files.
591If valid, the command exits with status 0. If not valid, all errors encountered
599 mox.FilesImmediate = true
601 _, errs := mox.ParseConfig(context.Background(), c.log, mox.ConfigStaticPath, true, true, false)
603 log.Printf("multiple errors:")
604 for _, err := range errs {
605 log.Printf("%s", err)
608 } else if len(errs) == 1 {
609 log.Fatalf("%s", errs[0])
612 fmt.Println("config OK")
615func cmdConfigDescribeStatic(c *cmd) {
616 c.params = ">mox.conf"
617 c.help = `Prints an annotated empty configuration for use as mox.conf.
619The static configuration file cannot be reloaded while mox is running. Mox has
620to be restarted for changes to the static configuration file to take effect.
622This configuration file needs modifications to make it valid. For example, it
623may contain unfinished list items.
625 if len(c.Parse()) != 0 {
630 err := sconf.Describe(os.Stdout, &sc)
631 xcheckf(err, "describing config")
634func cmdConfigDescribeDomains(c *cmd) {
635 c.params = ">domains.conf"
636 c.help = `Prints an annotated empty configuration for use as domains.conf.
638The domains configuration file contains the domains and their configuration,
639and accounts and their configuration. This includes the configured email
640addresses. The mox admin web interface, and the mox command line interface, can
641make changes to this file. Mox automatically reloads this file when it changes.
643Like the static configuration, the example domains.conf printed by this command
644needs modifications to make it valid.
646 if len(c.Parse()) != 0 {
650 var dc config.Dynamic
651 err := sconf.Describe(os.Stdout, &dc)
652 xcheckf(err, "describing config")
655func cmdConfigPrintservice(c *cmd) {
656 c.params = ">mox.service"
657 c.help = `Prints a systemd unit service file for mox.
659This is the same file as generated using quickstart. If the systemd service file
660has changed with a newer version of mox, use this command to generate an up to
663 if len(c.Parse()) != 0 {
667 pwd, err := os.Getwd()
669 log.Printf("current working directory: %v", err)
672 service := strings.ReplaceAll(moxService, "/home/mox", pwd)
676func cmdConfigDomainAdd(c *cmd) {
677 c.params = "domain account [localpart]"
678 c.help = `Adds a new domain to the configuration and reloads the configuration.
680The account is used for the postmaster mailboxes the domain, including as DMARC and
681TLS reporting. Localpart is the "username" at the domain for this account. If
682must be set if and only if account does not yet exist.
685 if len(args) != 2 && len(args) != 3 {
689 d := xparseDomain(args[0], "domain")
691 var localpart smtp.Localpart
694 localpart, err = smtp.ParseLocalpart(args[2])
695 xcheckf(err, "parsing localpart")
697 ctlcmdConfigDomainAdd(xctl(), d, args[1], localpart)
700func ctlcmdConfigDomainAdd(ctl *ctl, domain dns.Domain, account string, localpart smtp.Localpart) {
701 ctl.xwrite("domainadd")
702 ctl.xwrite(domain.Name())
704 ctl.xwrite(string(localpart))
706 fmt.Printf("domain added, remember to add dns records, see:\n\nmox config dnsrecords %s\nmox config dnscheck %s\n", domain.Name(), domain.Name())
709func cmdConfigDomainRemove(c *cmd) {
711 c.help = `Remove a domain from the configuration and reload the configuration.
713This is a dangerous operation. Incoming email delivery for this domain will be
721 d := xparseDomain(args[0], "domain")
723 ctlcmdConfigDomainRemove(xctl(), d)
726func ctlcmdConfigDomainRemove(ctl *ctl, d dns.Domain) {
727 ctl.xwrite("domainrm")
730 fmt.Printf("domain removed, remember to remove dns records for %s\n", d)
733func cmdConfigAliasList(c *cmd) {
735 c.help = `List aliases for domain.`
742 ctlcmdConfigAliasList(xctl(), args[0])
745func ctlcmdConfigAliasList(ctl *ctl, address string) {
746 ctl.xwrite("aliaslist")
749 ctl.xstreamto(os.Stdout)
752func cmdConfigAliasPrint(c *cmd) {
754 c.help = `Print settings and members of alias.`
761 ctlcmdConfigAliasPrint(xctl(), args[0])
764func ctlcmdConfigAliasPrint(ctl *ctl, address string) {
765 ctl.xwrite("aliasprint")
768 ctl.xstreamto(os.Stdout)
771func cmdConfigAliasAdd(c *cmd) {
772 c.params = "alias@domain rcpt1@domain ..."
773 c.help = `Add new alias with one or more addresses and public posting enabled.`
779 alias := config.Alias{PostPublic: true, Addresses: args[1:]}
782 ctlcmdConfigAliasAdd(xctl(), args[0], alias)
785func ctlcmdConfigAliasAdd(ctl *ctl, address string, alias config.Alias) {
786 ctl.xwrite("aliasadd")
788 xctlwriteJSON(ctl, alias)
792func cmdConfigAliasUpdate(c *cmd) {
793 c.params = "alias@domain [-postpublic false|true -listmembers false|true -allowmsgfrom false|true]"
794 c.help = `Update alias configuration.`
795 var postpublic, listmembers, allowmsgfrom string
796 c.flag.StringVar(&postpublic, "postpublic", "", "whether anyone or only list members can post")
797 c.flag.StringVar(&listmembers, "listmembers", "", "whether list members can list members")
798 c.flag.StringVar(&allowmsgfrom, "allowmsgfrom", "", "whether alias address can be used in message from header")
806 ctlcmdConfigAliasUpdate(xctl(), alias, postpublic, listmembers, allowmsgfrom)
809func ctlcmdConfigAliasUpdate(ctl *ctl, alias, postpublic, listmembers, allowmsgfrom string) {
810 ctl.xwrite("aliasupdate")
812 ctl.xwrite(postpublic)
813 ctl.xwrite(listmembers)
814 ctl.xwrite(allowmsgfrom)
818func cmdConfigAliasRemove(c *cmd) {
819 c.params = "alias@domain"
820 c.help = "Remove alias."
827 ctlcmdConfigAliasRemove(xctl(), args[0])
830func ctlcmdConfigAliasRemove(ctl *ctl, alias string) {
831 ctl.xwrite("aliasrm")
836func cmdConfigAliasAddaddr(c *cmd) {
837 c.params = "alias@domain rcpt1@domain ..."
838 c.help = `Add addresses to alias.`
845 ctlcmdConfigAliasAddaddr(xctl(), args[0], args[1:])
848func ctlcmdConfigAliasAddaddr(ctl *ctl, alias string, addresses []string) {
849 ctl.xwrite("aliasaddaddr")
851 xctlwriteJSON(ctl, addresses)
855func cmdConfigAliasRemoveaddr(c *cmd) {
856 c.params = "alias@domain rcpt1@domain ..."
857 c.help = `Remove addresses from alias.`
864 ctlcmdConfigAliasRmaddr(xctl(), args[0], args[1:])
867func ctlcmdConfigAliasRmaddr(ctl *ctl, alias string, addresses []string) {
868 ctl.xwrite("aliasrmaddr")
870 xctlwriteJSON(ctl, addresses)
874func cmdConfigAccountAdd(c *cmd) {
875 c.params = "account address"
876 c.help = `Add an account with an email address and reload the configuration.
878Email can be delivered to this address/account. A password has to be configured
879explicitly, see the setaccountpassword command.
887 ctlcmdConfigAccountAdd(xctl(), args[0], args[1])
890func ctlcmdConfigAccountAdd(ctl *ctl, account, address string) {
891 ctl.xwrite("accountadd")
895 fmt.Printf("account added, set a password with \"mox setaccountpassword %s\"\n", account)
898func cmdConfigAccountRemove(c *cmd) {
900 c.help = `Remove an account and reload the configuration.
902Email addresses for this account will also be removed, and incoming email for
903these addresses will be rejected.
905All data for the account will be removed.
913 ctlcmdConfigAccountRemove(xctl(), args[0])
916func ctlcmdConfigAccountRemove(ctl *ctl, account string) {
917 ctl.xwrite("accountrm")
920 fmt.Println("account removed")
923func cmdConfigAddressAdd(c *cmd) {
924 c.params = "address account"
925 c.help = `Adds an address to an account and reloads the configuration.
927If address starts with a @ (i.e. a missing localpart), this is a catchall
928address for the domain.
936 ctlcmdConfigAddressAdd(xctl(), args[0], args[1])
939func ctlcmdConfigAddressAdd(ctl *ctl, address, account string) {
940 ctl.xwrite("addressadd")
944 fmt.Println("address added")
947func cmdConfigAddressRemove(c *cmd) {
949 c.help = `Remove an address and reload the configuration.
951Incoming email for this address will be rejected after removing an address.
959 ctlcmdConfigAddressRemove(xctl(), args[0])
962func ctlcmdConfigAddressRemove(ctl *ctl, address string) {
963 ctl.xwrite("addressrm")
966 fmt.Println("address removed")
969func cmdConfigDNSRecords(c *cmd) {
971 c.help = `Prints annotated DNS records as zone file that should be created for the domain.
973The zone file can be imported into existing DNS software. You should review the
974DNS records, especially if your domain previously/currently has email
982 d := xparseDomain(args[0], "domain")
984 domConf, ok := mox.Conf.Domain(d)
986 log.Fatalf("unknown domain")
989 resolver := dns.StrictResolver{Pkg: "main"}
990 _, result, err := resolver.LookupTXT(context.Background(), d.ASCII+".")
991 if !dns.IsNotFound(err) {
992 xcheckf(err, "looking up record for dnssec-status")
995 var certIssuerDomainName, acmeAccountURI string
996 public := mox.Conf.Static.Listeners["public"]
997 if public.TLS != nil && public.TLS.ACME != "" {
998 acme, ok := mox.Conf.Static.ACME[public.TLS.ACME]
999 if ok && acme.Manager.Manager.Client != nil {
1000 certIssuerDomainName = acme.IssuerDomainName
1001 acc, err := acme.Manager.Manager.Client.GetReg(context.Background(), "")
1002 c.log.Check(err, "get public acme account")
1004 acmeAccountURI = acc.URI
1009 records, err := mox.DomainRecords(domConf, d, result.Authentic, certIssuerDomainName, acmeAccountURI)
1010 xcheckf(err, "records")
1011 fmt.Print(strings.Join(records, "\n") + "\n")
1014func cmdConfigDNSCheck(c *cmd) {
1016 c.help = "Check the DNS records with the configuration for the domain, and print any errors/warnings."
1022 d := xparseDomain(args[0], "domain")
1024 _, ok := mox.Conf.Domain(d)
1026 log.Fatalf("unknown domain")
1029 // todo future: move http.Admin.CheckDomain to mox- and make it return a regular error.
1035 err, ok := x.(*sherpa.Error)
1039 log.Fatalf("%s", err)
1042 printResult := func(name string, r webadmin.Result) {
1043 if len(r.Errors) == 0 && len(r.Warnings) == 0 {
1046 fmt.Printf("# %s\n", name)
1047 for _, s := range r.Errors {
1048 fmt.Printf("error: %s\n", s)
1050 for _, s := range r.Warnings {
1051 fmt.Printf("warning: %s\n", s)
1055 result := webadmin.Admin{}.CheckDomain(context.Background(), args[0])
1056 printResult("DNSSEC", result.DNSSEC.Result)
1057 printResult("IPRev", result.IPRev.Result)
1058 printResult("MX", result.MX.Result)
1059 printResult("TLS", result.TLS.Result)
1060 printResult("DANE", result.DANE.Result)
1061 printResult("SPF", result.SPF.Result)
1062 printResult("DKIM", result.DKIM.Result)
1063 printResult("DMARC", result.DMARC.Result)
1064 printResult("Host TLSRPT", result.HostTLSRPT.Result)
1065 printResult("Domain TLSRPT", result.DomainTLSRPT.Result)
1066 printResult("MTASTS", result.MTASTS.Result)
1067 printResult("SRV conf", result.SRVConf.Result)
1068 printResult("Autoconf", result.Autoconf.Result)
1069 printResult("Autodiscover", result.Autodiscover.Result)
1072func cmdConfigEnsureACMEHostprivatekeys(c *cmd) {
1074 c.help = `Ensure host private keys exist for TLS listeners with ACME.
1076In mox.conf, each listener can have TLS configured. Long-lived private key files
1077can be specified, which will be used when requesting ACME certificates.
1078Configuring these private keys makes it feasible to publish DANE TLSA records
1079for the corresponding public keys in DNS, protected with DNSSEC, allowing TLS
1080certificate verification without depending on a list of Certificate Authorities
1081(CAs). Previous versions of mox did not pre-generate private keys for use with
1082ACME certificates, but would generate private keys on-demand. By explicitly
1083configuring private keys, they will not change automatedly with new
1084certificates, and the DNS TLSA records stay valid.
1086This command looks for listeners in mox.conf with TLS with ACME configured. For
1087each missing host private key (of type rsa-2048 and ecdsa-p256) a key is written
1088to config/hostkeys/. If a certificate exists in the ACME "cache", its private
1089key is copied. Otherwise a new private key is generated. Snippets for manually
1090updating/editing mox.conf are printed.
1092After running this command, and updating mox.conf, run "mox config dnsrecords"
1093for a domain and create the TLSA DNS records it suggests to enable DANE.
1100 // Load a private key from p, in various forms. We only look at the first PEM
1101 // block. Files with only a private key, or with multiple blocks but private key
1102 // first like autocert does, can be loaded.
1103 loadPrivateKey := func(f *os.File) (any, error) {
1104 buf, err := io.ReadAll(f)
1106 return nil, fmt.Errorf("reading private key file: %v", err)
1108 block, _ := pem.Decode(buf)
1110 return nil, fmt.Errorf("no pem block found in pem file")
1114 case "EC PRIVATE KEY":
1115 privKey, err = x509.ParseECPrivateKey(block.Bytes)
1116 case "RSA PRIVATE KEY":
1117 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
1119 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
1121 return nil, fmt.Errorf("unrecognized pem block type %q", block.Type)
1124 return nil, fmt.Errorf("parsing private key of type %q: %v", block.Type, err)
1129 // Either load a private key from file, or if it doesn't exist generate a new
1131 xtryLoadPrivateKey := func(kt autocert.KeyType, p string) any {
1132 f, err := os.Open(p)
1133 if err != nil && errors.Is(err, fs.ErrNotExist) {
1135 case autocert.KeyRSA2048:
1136 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1137 xcheckf(err, "generating new 2048-bit rsa private key")
1139 case autocert.KeyECDSAP256:
1140 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1141 xcheckf(err, "generating new ecdsa p-256 private key")
1144 log.Fatalf("unexpected keytype %v", kt)
1147 xcheckf(err, "%s: open acme key and certificate file", p)
1149 // Load private key from file. autocert stores a PEM file that starts with a
1150 // private key, followed by certificate(s). So we can just read it and should find
1151 // the private key we are looking for.
1152 privKey, err := loadPrivateKey(f)
1153 if xerr := f.Close(); xerr != nil {
1154 log.Printf("closing private key file: %v", xerr)
1156 xcheckf(err, "parsing private key from acme key and certificate file")
1158 switch k := privKey.(type) {
1159 case *rsa.PrivateKey:
1160 if k.N.BitLen() == 2048 {
1163 log.Printf("warning: rsa private key in %s has %d bits, skipping and generating new 2048-bit rsa private key", p, k.N.BitLen())
1164 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1165 xcheckf(err, "generating new 2048-bit rsa private key")
1167 case *ecdsa.PrivateKey:
1168 if k.Curve == elliptic.P256() {
1171 log.Printf("warning: ecdsa private key in %s has curve %v, skipping and generating new p-256 ecdsa key", p, k.Curve.Params().Name)
1172 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1173 xcheckf(err, "generating new ecdsa p-256 private key")
1176 log.Fatalf("%s: unexpected private key file of type %T", p, privKey)
1181 // Write privKey as PKCS#8 private key to p. Only if file does not yet exist.
1182 writeHostPrivateKey := func(privKey any, p string) error {
1183 os.MkdirAll(filepath.Dir(p), 0700)
1184 f, err := os.OpenFile(p, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
1186 return fmt.Errorf("create: %v", err)
1190 if err := f.Close(); err != nil {
1191 log.Printf("closing new hostkey file %s after error: %v", p, err)
1193 if err := os.Remove(p); err != nil {
1194 log.Printf("removing new hostkey file %s after error: %v", p, err)
1198 buf, err := x509.MarshalPKCS8PrivateKey(privKey)
1200 return fmt.Errorf("marshal private host key: %v", err)
1203 Type: "PRIVATE KEY",
1206 if err := pem.Encode(f, &block); err != nil {
1207 return fmt.Errorf("write as pem: %v", err)
1209 if err := f.Close(); err != nil {
1210 return fmt.Errorf("close: %v", err)
1217 timestamp := time.Now().Format("20060102T150405")
1219 for listenerName, l := range mox.Conf.Static.Listeners {
1220 if l.TLS == nil || l.TLS.ACME == "" {
1223 haveKeyTypes := map[autocert.KeyType]bool{}
1224 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
1225 p := mox.ConfigDirPath(privKeyFile)
1226 f, err := os.Open(p)
1227 xcheckf(err, "open host private key")
1228 privKey, err := loadPrivateKey(f)
1229 if err := f.Close(); err != nil {
1230 log.Printf("closing host private key file: %v", err)
1232 xcheckf(err, "loading host private key")
1233 switch k := privKey.(type) {
1234 case *rsa.PrivateKey:
1235 if k.N.BitLen() == 2048 {
1236 haveKeyTypes[autocert.KeyRSA2048] = true
1238 case *ecdsa.PrivateKey:
1239 if k.Curve == elliptic.P256() {
1240 haveKeyTypes[autocert.KeyECDSAP256] = true
1244 created := []string{}
1245 for _, kt := range []autocert.KeyType{autocert.KeyRSA2048, autocert.KeyECDSAP256} {
1246 if haveKeyTypes[kt] {
1249 // Lookup key in ACME cache.
1250 host := l.HostnameDomain
1251 if host.ASCII == "" {
1252 host = mox.Conf.Static.HostnameDomain
1254 filename := host.ASCII
1256 if kt == autocert.KeyRSA2048 {
1260 p := mox.DataDirPath(filepath.Join("acme", "keycerts", l.TLS.ACME, filename))
1261 privKey := xtryLoadPrivateKey(kt, p)
1263 relPath := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", host.Name(), timestamp, kind))
1264 destPath := mox.ConfigDirPath(relPath)
1265 err := writeHostPrivateKey(privKey, destPath)
1266 xcheckf(err, "writing host private key file to %s: %v", destPath, err)
1267 created = append(created, relPath)
1268 fmt.Printf("Wrote host private key: %s\n", destPath)
1270 didCreate = didCreate || len(created) > 0
1271 if len(created) > 0 {
1273 HostPrivateKeyFiles: append(l.TLS.HostPrivateKeyFiles, created...),
1275 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)
1276 err := sconf.Write(os.Stdout, tls)
1277 xcheckf(err, "writing new TLS.HostPrivateKeyFiles section")
1283After updating mox.conf and restarting, run "mox config dnsrecords" for a
1284domain and create the TLSA DNS records it suggests to enable DANE.
1289func cmdLoglevels(c *cmd) {
1290 c.params = "[level [pkg]]"
1291 c.help = `Print the log levels, or set a new default log level, or a level for the given package.
1293By default, a single log level applies to all logging in mox. But for each
1294"pkg", an overriding log level can be configured. Examples of packages:
1295smtpserver, smtpclient, queue, imapserver, spf, dkim, dmarc, junk, message,
1298Specify a pkg and an empty level to clear the configured level for a package.
1300Valid labels: error, info, debug, trace, traceauth, tracedata.
1309 ctlcmdLoglevels(xctl())
1315 ctlcmdSetLoglevels(xctl(), pkg, args[0])
1319func ctlcmdLoglevels(ctl *ctl) {
1320 ctl.xwrite("loglevels")
1322 ctl.xstreamto(os.Stdout)
1325func ctlcmdSetLoglevels(ctl *ctl, pkg, level string) {
1326 ctl.xwrite("setloglevels")
1332func cmdStop(c *cmd) {
1333 c.help = `Shut mox down, giving connections maximum 3 seconds to stop before closing them.
1335While shutting down, new IMAP and SMTP connections will get a status response
1336indicating temporary unavailability. Existing connections will get a 3 second
1337period to finish their transaction and shut down. Under normal circumstances,
1338only IMAP has long-living connections, with the IDLE command to get notified of
1341 if len(c.Parse()) != 0 {
1348 // Read will hang until remote has shut down.
1349 buf := make([]byte, 128)
1350 n, err := ctl.conn.Read(buf)
1352 log.Fatalf("expected eof after graceful shutdown, got data %q", buf[:n])
1353 } else if err != io.EOF {
1354 log.Fatalf("expected eof after graceful shutdown, got error %v", err)
1356 fmt.Println("mox stopped")
1359func cmdBackup(c *cmd) {
1360 c.params = "dest-dir"
1361 c.help = `Creates a backup of the data directory.
1363Backup creates consistent snapshots of the databases and message files and
1364copies other files in the data directory. Empty directories are not copied.
1365These files can then be stored elsewhere for long-term storage, or used to fall
1366back to should an upgrade fail. Simply copying files in the data directory
1367while mox is running can result in unusable database files.
1369Message files never change (they are read-only, though can be removed) and are
1370hard-linked so they don't consume additional space. If hardlinking fails, for
1371example when the backup destination directory is on a different file system, a
1372regular copy is made. Using a destination directory like "data/tmp/backup"
1373increases the odds hardlinking succeeds: the default systemd service file
1374specifically mounts the data directory, causing attempts to hardlink outside it
1375to fail with an error about cross-device linking.
1377All files in the data directory that aren't recognized (i.e. other than known
1378database files, message files, an acme directory, the "tmp" directory, etc),
1379are stored, but with a warning.
1381Remove files in the destination directory before doing another backup. The
1382backup command will not overwrite files, but print and return errors.
1384Exit code 0 indicates the backup was successful. A clean successful backup does
1385not print any output, but may print warnings. Use the -verbose flag for
1386details, including timing.
1388To restore a backup, first shut down mox, move away the old data directory and
1389move an earlier backed up directory in its place, run "mox verifydata",
1390possibly with the "-fix" option, and restart mox. After the restore, you may
1391also want to run "mox bumpuidvalidity" for each account for which messages in a
1392mailbox changed, to force IMAP clients to synchronize mailbox state.
1394Before upgrading, to check if the upgrade will likely succeed, first make a
1395backup, then use the new mox binary to run "mox verifydata" on the backup. This
1396can change the backup files (e.g. upgrade database files, move away
1397unrecognized message files), so you should make a new backup before actually
1402 c.flag.BoolVar(&verbose, "verbose", false, "print progress")
1409 dstDataDir, err := filepath.Abs(args[0])
1410 xcheckf(err, "making path absolute")
1412 ctlcmdBackup(xctl(), dstDataDir, verbose)
1415func ctlcmdBackup(ctl *ctl, dstDataDir string, verbose bool) {
1416 ctl.xwrite("backup")
1417 ctl.xwrite(dstDataDir)
1419 ctl.xwrite("verbose")
1423 ctl.xstreamto(os.Stdout)
1427func cmdSetadminpassword(c *cmd) {
1428 c.help = `Set a new admin password, for the web interface.
1430The password is read from stdin. Its bcrypt hash is stored in a file named
1431"adminpasswd" in the configuration directory.
1433 if len(c.Parse()) != 0 {
1438 path := mox.ConfigDirPath(mox.Conf.Static.AdminPasswordFile)
1440 log.Fatal("no admin password file configured")
1443 pw := xreadpassword()
1444 pw, err := precis.OpaqueString.String(pw)
1445 xcheckf(err, `checking password with "precis" requirements`)
1446 hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
1447 xcheckf(err, "generating hash for password")
1448 err = os.WriteFile(path, hash, 0660)
1449 xcheckf(err, "writing hash to admin password file")
1452func xreadpassword() string {
1454Type new password. Password WILL echo.
1456WARNING: Bots will try to bruteforce your password. Connections with failed
1457authentication attempts will be rate limited but attackers WILL find weak
1458passwords. If your account is compromised, spammers are likely to abuse your
1459system, spamming your address and the wider internet in your name. So please
1460pick a random, unguessable password, preferably at least 12 characters.
1463 fmt.Printf("password: ")
1464 buf := make([]byte, 64)
1465 n, err := os.Stdin.Read(buf)
1466 xcheckf(err, "reading stdin")
1467 pw := string(buf[:n])
1468 pw = strings.TrimSuffix(strings.TrimSuffix(pw, "\r\n"), "\n")
1470 log.Fatal("password must be at least 8 characters")
1475func cmdSetaccountpassword(c *cmd) {
1476 c.params = "account"
1477 c.help = `Set new password an account.
1479The password is read from stdin. Secrets derived from the password, but not the
1480password itself, are stored in the account database. The stored secrets are for
1481authentication with: scram-sha-256, scram-sha-1, cram-md5, plain text (bcrypt
1484The parameter is an account name, as configured under Accounts in domains.conf
1485and as present in the data/accounts/ directory, not a configured email address
1494 pw := xreadpassword()
1496 ctlcmdSetaccountpassword(xctl(), args[0], pw)
1499func ctlcmdSetaccountpassword(ctl *ctl, account, password string) {
1500 ctl.xwrite("setaccountpassword")
1502 ctl.xwrite(password)
1506func cmdDeliver(c *cmd) {
1508 c.params = "address < message"
1509 c.help = "Deliver message to address."
1515 ctlcmdDeliver(xctl(), args[0])
1518func ctlcmdDeliver(ctl *ctl, address string) {
1519 ctl.xwrite("deliver")
1522 ctl.xstreamfrom(os.Stdin)
1525 fmt.Println("message delivered")
1527 log.Fatalf("deliver: %s", line)
1531func cmdDKIMGenrsa(c *cmd) {
1532 c.params = ">$selector._domainkey.$domain.rsa2048.privatekey.pkcs8.pem"
1533 c.help = `Generate a new 2048 bit RSA private key for use with DKIM.
1535The generated file is in PEM format, and has a comment it is generated for use
1538 if len(c.Parse()) != 0 {
1542 buf, err := mox.MakeDKIMRSAKey(dns.Domain{}, dns.Domain{})
1543 xcheckf(err, "making rsa private key")
1544 _, err = os.Stdout.Write(buf)
1545 xcheckf(err, "writing rsa private key")
1548func cmdDANEDial(c *cmd) {
1549 c.params = "host:port"
1551 c.flag.StringVar(&usages, "usages", "pkix-ta,pkix-ee,dane-ta,dane-ee", "allowed usages for dane, comma-separated list")
1552 c.help = `Dial the address using TLS with certificate verification using DANE.
1554Data is copied between connection and stdin/stdout until either side closes the
1562 allowedUsages := []adns.TLSAUsage{}
1564 for _, s := range strings.Split(usages, ",") {
1565 var usage adns.TLSAUsage
1566 switch strings.ToLower(s) {
1567 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
1568 usage = adns.TLSAUsagePKIXTA
1569 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
1570 usage = adns.TLSAUsagePKIXEE
1571 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
1572 usage = adns.TLSAUsageDANETA
1573 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
1574 usage = adns.TLSAUsageDANEEE
1576 log.Fatalf("unknown dane usage %q", s)
1578 allowedUsages = append(allowedUsages, usage)
1582 pkixRoots, err := x509.SystemCertPool()
1583 xcheckf(err, "get system pkix certificate pool")
1585 resolver := dns.StrictResolver{Pkg: "danedial"}
1586 conn, record, err := dane.Dial(context.Background(), c.log.Logger, resolver, "tcp", args[0], allowedUsages, pkixRoots)
1587 xcheckf(err, "dial")
1588 log.Printf("(connected, verified with %s)", record)
1591 _, err := io.Copy(os.Stdout, conn)
1592 xcheckf(err, "copy from connection to stdout")
1595 _, err = io.Copy(conn, os.Stdin)
1596 xcheckf(err, "copy from stdin to connection")
1599func cmdDANEDialmx(c *cmd) {
1600 c.params = "domain [destination-host]"
1601 var ehloHostname string
1602 c.flag.StringVar(&ehloHostname, "ehlohostname", "localhost", "hostname to send in smtp ehlo command")
1603 c.help = `Connect to MX server for domain using STARTTLS verified with DANE.
1605If no destination host is specified, regular delivery logic is used to find the
1606hosts to attempt delivery too. This involves following CNAMEs for the domain,
1607looking up MX records, and possibly falling back to the domain name itself as
1610If a destination host is specified, that is the only candidate host considered
1613With a list of destinations gathered, each is dialed until a successful SMTP
1614session verified with DANE has been initialized, including EHLO and STARTTLS
1617Once connected, data is copied between connection and stdin/stdout, until
1618either side closes the connection.
1620This command follows the same logic as delivery attempts made from the queue,
1621sharing most of its code.
1624 if len(args) != 1 && len(args) != 2 {
1628 ehloDomain, err := dns.ParseDomain(ehloHostname)
1629 xcheckf(err, "parsing ehlo hostname")
1631 origNextHop, err := dns.ParseDomain(args[0])
1632 xcheckf(err, "parse domain")
1634 ctxbg := context.Background()
1636 resolver := dns.StrictResolver{}
1638 var origNextHopAuthentic, expandedNextHopAuthentic bool
1639 var expandedNextHop dns.Domain
1640 var hosts []dns.IPDomain
1643 haveMX, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hosts, permanent, err = smtpclient.GatherDestinations(ctxbg, c.log.Logger, resolver, dns.IPDomain{Domain: origNextHop})
1644 status := "temporary"
1646 status = "permanent"
1649 log.Fatalf("gathering destinations: %v (%s)", err, status)
1651 if expandedNextHop != origNextHop {
1652 log.Printf("followed cnames to %s", expandedNextHop)
1655 log.Printf("found mx record, trying mx hosts")
1657 log.Printf("no mx record found, will try to connect to domain directly")
1659 if !origNextHopAuthentic {
1660 log.Fatalf("error: initial domain not dnssec-secure")
1662 if !expandedNextHopAuthentic {
1663 log.Fatalf("error: expanded domain not dnssec-secure")
1667 for _, h := range hosts {
1668 l = append(l, h.String())
1670 log.Printf("destinations: %s", strings.Join(l, ", "))
1672 d, err := dns.ParseDomain(args[1])
1674 log.Fatalf("parsing destination host: %v", err)
1676 log.Printf("skipping domain mx/cname lookups, assuming domain is dnssec-protected")
1678 origNextHopAuthentic = true
1679 expandedNextHopAuthentic = true
1681 hosts = []dns.IPDomain{{Domain: d}}
1684 dialedIPs := map[string][]net.IP{}
1685 for _, host := range hosts {
1686 // It should not be possible for hosts to have IP addresses: They are not
1687 // allowed by dns.ParseDomain, and MX records cannot contain them.
1689 log.Fatalf("unexpected IP address for destination host")
1692 log.Printf("attempting to connect to %s", host)
1694 authentic, expandedAuthentic, expandedHost, ips, _, err := smtpclient.GatherIPs(ctxbg, c.log.Logger, resolver, "ip", host, dialedIPs)
1696 log.Printf("resolving ips for %s: %v, skipping", host, err)
1700 log.Printf("no dnssec for ips of %s, skipping", host)
1703 if !expandedAuthentic {
1704 log.Printf("no dnssec for cname-followed ips of %s, skipping", host)
1707 if expandedHost != host.Domain {
1708 log.Printf("host %s cname-expanded to %s", host, expandedHost)
1710 log.Printf("host %s resolved to ips %s, looking up tlsa records", host, ips)
1712 daneRequired, daneRecords, tlsaBaseDomain, err := smtpclient.GatherTLSA(ctxbg, c.log.Logger, resolver, host.Domain, expandedAuthentic, expandedHost)
1714 log.Printf("looking up tlsa records: %s, skipping", err)
1717 tlsMode := smtpclient.TLSRequiredStartTLS
1718 if len(daneRecords) == 0 {
1720 log.Printf("host %s has no tlsa records, skipping", expandedHost)
1723 log.Printf("warning: only unusable tlsa records found, continuing with required tls without certificate verification")
1727 for _, r := range daneRecords {
1728 l = append(l, r.String())
1730 log.Printf("tlsa records: %s", strings.Join(l, "; "))
1733 tlsHostnames := smtpclient.GatherTLSANames(haveMX, expandedNextHopAuthentic, expandedAuthentic, origNextHop, expandedNextHop, host.Domain, tlsaBaseDomain)
1735 for _, name := range tlsHostnames {
1736 l = append(l, name.String())
1738 log.Printf("gathered valid tls certificate names for potential verification with dane-ta: %s", strings.Join(l, ", "))
1740 dialer := &net.Dialer{Timeout: 5 * time.Second}
1741 conn, _, err := smtpclient.Dial(ctxbg, c.log.Logger, dialer, dns.IPDomain{Domain: expandedHost}, ips, 25, dialedIPs, nil)
1743 log.Printf("dial %s: %v, skipping", expandedHost, err)
1746 log.Printf("connected to %s, %s, starting smtp session with ehlo and starttls with dane verification", expandedHost, conn.RemoteAddr())
1748 var verifiedRecord adns.TLSA
1749 opts := smtpclient.Opts{
1750 DANERecords: daneRecords,
1751 DANEMoreHostnames: tlsHostnames[1:],
1752 DANEVerifiedRecord: &verifiedRecord,
1753 RootCAs: mox.Conf.Static.TLS.CertPool,
1756 sc, err := smtpclient.New(ctxbg, c.log.Logger, conn, tlsMode, tlsPKIX, ehloDomain, tlsHostnames[0], opts)
1758 log.Printf("setting up smtp session: %v, skipping", err)
1763 smtpConn, err := sc.Conn()
1765 log.Fatalf("error: taking over smtp connection: %s", err)
1767 log.Printf("tls verified with tlsa record: %s", verifiedRecord)
1768 log.Printf("smtp session initialized and connected to stdin/stdout")
1771 _, err := io.Copy(os.Stdout, smtpConn)
1772 xcheckf(err, "copy from connection to stdout")
1775 _, err = io.Copy(smtpConn, os.Stdin)
1776 xcheckf(err, "copy from stdin to connection")
1779 log.Fatalf("no remaining destinations")
1782func cmdDANEMakeRecord(c *cmd) {
1783 c.params = "usage selector matchtype [certificate.pem | publickey.pem | privatekey.pem]"
1784 c.help = `Print TLSA record for given certificate/key and parameters.
1787- usage: pkix-ta (0), pkix-ee (1), dane-ta (2), dane-ee (3)
1788- selector: cert (0), spki (1)
1789- matchtype: full (0), sha2-256 (1), sha2-512 (2)
1791Common DANE TLSA record parameters are: dane-ee spki sha2-256, or 3 1 1,
1792followed by a sha2-256 hash of the DER-encoded "SPKI" (subject public key info)
1793from the certificate. An example DNS zone file entry:
1795 _25._tcp.example.com. TLSA 3 1 1 133b919c9d65d8b1488157315327334ead8d83372db57465ecabf53ee5748aee
1797The first usable information from the pem file is used to compose the TLSA
1798record. In case of selector "cert", a certificate is required. Otherwise the
1799"subject public key info" (spki) of the first certificate or public or private
1800key (pkcs#8, pkcs#1 or ec private key) is used.
1808 var usage adns.TLSAUsage
1809 switch strings.ToLower(args[0]) {
1810 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
1811 usage = adns.TLSAUsagePKIXTA
1812 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
1813 usage = adns.TLSAUsagePKIXEE
1814 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
1815 usage = adns.TLSAUsageDANETA
1816 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
1817 usage = adns.TLSAUsageDANEEE
1819 if v, err := strconv.ParseUint(args[0], 10, 16); err != nil {
1820 log.Fatalf("bad usage %q", args[0])
1822 // Does not influence certificate association data, so we can accept other numbers.
1823 log.Printf("warning: continuing with unrecognized tlsa usage %d", v)
1824 usage = adns.TLSAUsage(v)
1828 var selector adns.TLSASelector
1829 switch strings.ToLower(args[1]) {
1830 case "cert", strconv.Itoa(int(adns.TLSASelectorCert)):
1831 selector = adns.TLSASelectorCert
1832 case "spki", strconv.Itoa(int(adns.TLSASelectorSPKI)):
1833 selector = adns.TLSASelectorSPKI
1835 log.Fatalf("bad selector %q", args[1])
1838 var matchType adns.TLSAMatchType
1839 switch strings.ToLower(args[2]) {
1840 case "full", strconv.Itoa(int(adns.TLSAMatchTypeFull)):
1841 matchType = adns.TLSAMatchTypeFull
1842 case "sha2-256", strconv.Itoa(int(adns.TLSAMatchTypeSHA256)):
1843 matchType = adns.TLSAMatchTypeSHA256
1844 case "sha2-512", strconv.Itoa(int(adns.TLSAMatchTypeSHA512)):
1845 matchType = adns.TLSAMatchTypeSHA512
1847 log.Fatalf("bad matchtype %q", args[2])
1850 buf, err := os.ReadFile(args[3])
1851 xcheckf(err, "reading certificate")
1853 var block *pem.Block
1854 block, buf = pem.Decode(buf)
1858 extra = " (with leftover data from pem file)"
1860 if selector == adns.TLSASelectorCert {
1861 log.Fatalf("no certificate found in pem file%s", extra)
1863 log.Fatalf("no certificate or public or private key found in pem file%s", extra)
1866 var cert *x509.Certificate
1868 if block.Type == "CERTIFICATE" {
1869 cert, err = x509.ParseCertificate(block.Bytes)
1870 xcheckf(err, "parse certificate")
1872 case adns.TLSASelectorCert:
1874 case adns.TLSASelectorSPKI:
1875 data = cert.RawSubjectPublicKeyInfo
1877 } else if selector == adns.TLSASelectorCert {
1878 // We need a certificate, just a public/private key won't do.
1879 log.Printf("skipping pem type %q, certificate is required", block.Type)
1882 var privKey, pubKey any
1886 _, err := x509.ParsePKIXPublicKey(block.Bytes)
1887 xcheckf(err, "parse pkix subject public key info (spki)")
1889 case "EC PRIVATE KEY":
1890 privKey, err = x509.ParseECPrivateKey(block.Bytes)
1891 xcheckf(err, "parse ec private key")
1892 case "RSA PRIVATE KEY":
1893 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
1894 xcheckf(err, "parse pkcs#1 rsa private key")
1895 case "RSA PUBLIC KEY":
1896 pubKey, err = x509.ParsePKCS1PublicKey(block.Bytes)
1897 xcheckf(err, "parse pkcs#1 rsa public key")
1899 // PKCS#8 private key
1900 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
1901 xcheckf(err, "parse pkcs#8 private key")
1903 log.Printf("skipping unrecognized pem type %q", block.Type)
1907 if pubKey == nil && privKey != nil {
1908 if signer, ok := privKey.(crypto.Signer); !ok {
1909 log.Fatalf("private key of type %T is not a signer, cannot get public key", privKey)
1911 pubKey = signer.Public()
1915 // Should not happen.
1916 log.Fatalf("internal error: did not find private or public key")
1918 data, err = x509.MarshalPKIXPublicKey(pubKey)
1919 xcheckf(err, "marshal pkix subject public key info (spki)")
1924 case adns.TLSAMatchTypeFull:
1925 case adns.TLSAMatchTypeSHA256:
1926 p := sha256.Sum256(data)
1928 case adns.TLSAMatchTypeSHA512:
1929 p := sha512.Sum512(data)
1932 fmt.Printf("%d %d %d %x\n", usage, selector, matchType, data)
1937func cmdDNSLookup(c *cmd) {
1938 c.params = "[ptr | mx | cname | ips | a | aaaa | ns | txt | srv | tlsa] name"
1939 c.help = `Lookup DNS name of given type.
1941Lookup always prints whether the response was DNSSEC-protected.
1945mox dns lookup ptr 1.1.1.1
1946mox dns lookup mx xmox.nl
1947mox dns lookup txt _dmarc.xmox.nl.
1948mox dns lookup tlsa _25._tcp.xmox.nl
1956 resolver := dns.StrictResolver{Pkg: "dns"}
1958 // like xparseDomain, but treat unparseable domain as an ASCII name so names with
1959 // underscores are still looked up, e,g <selector>._domainkey.<host>.
1960 xdomain := func(s string) dns.Domain {
1961 d, err := dns.ParseDomain(s)
1963 return dns.Domain{ASCII: strings.TrimSuffix(s, ".")}
1968 cmd, name := args[0], args[1]
1972 ip := xparseIP(name, "ip")
1973 ptrs, result, err := resolver.LookupAddr(context.Background(), ip.String())
1975 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
1977 fmt.Printf("names (%d, %s):\n", len(ptrs), dnssecStatus(result.Authentic))
1978 for _, ptr := range ptrs {
1979 fmt.Printf("- %s\n", ptr)
1983 name := xdomain(name)
1984 mxl, result, err := resolver.LookupMX(context.Background(), name.ASCII+".")
1986 log.Printf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
1987 // We can still have valid records...
1989 fmt.Printf("mx records (%d, %s):\n", len(mxl), dnssecStatus(result.Authentic))
1990 for _, mx := range mxl {
1991 fmt.Printf("- %s, preference %d\n", mx.Host, mx.Pref)
1995 name := xdomain(name)
1996 target, result, err := resolver.LookupCNAME(context.Background(), name.ASCII+".")
1998 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2000 fmt.Printf("%s (%s)\n", target, dnssecStatus(result.Authentic))
2002 case "ips", "a", "aaaa":
2006 } else if cmd == "aaaa" {
2009 name := xdomain(name)
2010 ips, result, err := resolver.LookupIP(context.Background(), network, name.ASCII+".")
2012 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2014 fmt.Printf("records (%d, %s):\n", len(ips), dnssecStatus(result.Authentic))
2015 for _, ip := range ips {
2016 fmt.Printf("- %s\n", ip)
2020 name := xdomain(name)
2021 nsl, result, err := resolver.LookupNS(context.Background(), name.ASCII+".")
2023 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2025 fmt.Printf("ns records (%d, %s):\n", len(nsl), dnssecStatus(result.Authentic))
2026 for _, ns := range nsl {
2027 fmt.Printf("- %s\n", ns)
2031 host := xdomain(name)
2032 l, result, err := resolver.LookupTXT(context.Background(), host.ASCII+".")
2034 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2036 fmt.Printf("txt records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2037 for _, txt := range l {
2038 fmt.Printf("- %s\n", txt)
2042 host := xdomain(name)
2043 _, l, result, err := resolver.LookupSRV(context.Background(), "", "", host.ASCII+".")
2045 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2047 fmt.Printf("srv records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2048 for _, srv := range l {
2049 fmt.Printf("- host %s, port %d, priority %d, weight %d\n", srv.Target, srv.Port, srv.Priority, srv.Weight)
2053 host := xdomain(name)
2054 l, result, err := resolver.LookupTLSA(context.Background(), 0, "", host.ASCII+".")
2056 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2058 fmt.Printf("tlsa records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2059 for _, tlsa := range l {
2060 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)
2063 log.Fatalf("unknown record type %q", args[0])
2067func cmdDKIMGened25519(c *cmd) {
2068 c.params = ">$selector._domainkey.$domain.ed25519.privatekey.pkcs8.pem"
2069 c.help = `Generate a new ed25519 key for use with DKIM.
2071Ed25519 keys are much smaller than RSA keys of comparable cryptographic
2072strength. This is convenient because of maximum DNS message sizes. At the time
2073of writing, not many mail servers appear to support ed25519 DKIM keys though,
2074so it is recommended to sign messages with both RSA and ed25519 keys.
2076 if len(c.Parse()) != 0 {
2080 buf, err := mox.MakeDKIMEd25519Key(dns.Domain{}, dns.Domain{})
2081 xcheckf(err, "making dkim ed25519 key")
2082 _, err = os.Stdout.Write(buf)
2083 xcheckf(err, "writing dkim ed25519 key")
2086func cmdDKIMTXT(c *cmd) {
2087 c.params = "<$selector._domainkey.$domain.key.pkcs8.pem"
2088 c.help = `Print a DKIM DNS TXT record with the public key derived from the private key read from stdin.
2090The DNS should be configured as a TXT record at $selector._domainkey.$domain.
2092 if len(c.Parse()) != 0 {
2096 privKey, err := parseDKIMKey(os.Stdin)
2097 xcheckf(err, "reading dkim private key from stdin")
2101 Hashes: []string{"sha256"},
2102 Flags: []string{"s"},
2105 switch key := privKey.(type) {
2106 case *rsa.PrivateKey:
2107 r.PublicKey = key.Public()
2108 case ed25519.PrivateKey:
2109 r.PublicKey = key.Public()
2112 log.Fatalf("unsupported private key type %T, must be rsa or ed25519", privKey)
2115 record, err := r.Record()
2116 xcheckf(err, "making record")
2117 fmt.Print("<selector>._domainkey.<your.domain.> TXT ")
2121 s, record = record[:100], record[100:]
2125 fmt.Printf(`"%s" `, s)
2130func parseDKIMKey(r io.Reader) (any, error) {
2131 buf, err := io.ReadAll(r)
2133 return nil, fmt.Errorf("reading pem from stdin: %v", err)
2135 b, _ := pem.Decode(buf)
2137 return nil, fmt.Errorf("decoding pem: %v", err)
2139 privKey, err := x509.ParsePKCS8PrivateKey(b.Bytes)
2141 return nil, fmt.Errorf("parsing private key: %v", err)
2146func cmdDKIMVerify(c *cmd) {
2147 c.params = "message"
2148 c.help = `Verify the DKIM signatures in a message and print the results.
2150The message is parsed, and the DKIM-Signature headers are validated. Validation
2151of older messages may fail because the DNS records have been removed or changed
2152by now, or because the signature header may have specified an expiration time
2160 msgf, err := os.Open(args[0])
2161 xcheckf(err, "open message")
2163 results, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, false, dkim.DefaultPolicy, msgf, true)
2164 xcheckf(err, "dkim verify")
2166 for _, result := range results {
2168 if result.Sig == nil {
2169 log.Printf("warning: could not parse signature")
2171 sigh, err = result.Sig.Header()
2173 log.Printf("warning: packing signature: %s", err)
2177 if result.Record == nil {
2178 log.Printf("warning: missing DNS record")
2180 txt, err = result.Record.Record()
2182 log.Printf("warning: packing record: %s", err)
2185 fmt.Printf("status %q, err %v\nrecord %q\nheader %s\n", result.Status, result.Err, txt, sigh)
2189func cmdDKIMSign(c *cmd) {
2190 c.params = "message"
2191 c.help = `Sign a message, adding DKIM-Signature headers based on the domain in the From header.
2193The message is parsed, the domain looked up in the configuration files, and
2194DKIM-Signature headers generated. The message is printed with the DKIM-Signature
2202 msgf, err := os.Open(args[0])
2203 xcheckf(err, "open message")
2206 p, err := message.Parse(c.log.Logger, true, msgf)
2207 xcheckf(err, "parsing message")
2209 if len(p.Envelope.From) != 1 {
2210 log.Fatalf("found %d from headers, need exactly 1", len(p.Envelope.From))
2212 localpart, err := smtp.ParseLocalpart(p.Envelope.From[0].User)
2213 xcheckf(err, "parsing localpart of address in from-header")
2214 dom, err := dns.ParseDomain(p.Envelope.From[0].Host)
2215 xcheckf(err, "parsing domain of address in from-header")
2219 domConf, ok := mox.Conf.Domain(dom)
2221 log.Fatalf("domain %s not configured", dom)
2224 selectors := mox.DKIMSelectors(domConf.DKIM)
2225 headers, err := dkim.Sign(context.Background(), c.log.Logger, localpart, dom, selectors, false, msgf)
2226 xcheckf(err, "signing message with dkim")
2228 log.Fatalf("no DKIM configured for domain %s", dom)
2230 _, err = fmt.Fprint(os.Stdout, headers)
2231 xcheckf(err, "write headers")
2232 _, err = io.Copy(os.Stdout, msgf)
2233 xcheckf(err, "write message")
2236func cmdDKIMLookup(c *cmd) {
2237 c.params = "selector domain"
2238 c.help = "Lookup and print the DKIM record for the selector at the domain."
2244 selector := xparseDomain(args[0], "selector")
2245 domain := xparseDomain(args[1], "domain")
2247 status, record, txt, authentic, err := dkim.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, selector, domain)
2249 fmt.Printf("error: %s\n", err)
2251 if status != dkim.StatusNeutral {
2252 fmt.Printf("status: %s\n", status)
2255 fmt.Printf("TXT record: %s\n", txt)
2258 fmt.Println("dnssec-signed: yes")
2260 fmt.Println("dnssec-signed: no")
2263 fmt.Printf("Record:\n")
2265 "version", record.Version,
2266 "hashes", record.Hashes,
2268 "notes", record.Notes,
2269 "services", record.Services,
2270 "flags", record.Flags,
2272 for i := 0; i < len(pairs); i += 2 {
2273 fmt.Printf("\t%s: %v\n", pairs[i], pairs[i+1])
2278func cmdDMARCLookup(c *cmd) {
2280 c.help = "Lookup dmarc policy for domain, a DNS TXT record at _dmarc.<domain>, validate and print it."
2286 fromdomain := xparseDomain(args[0], "domain")
2287 _, domain, _, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, fromdomain)
2288 xcheckf(err, "dmarc lookup domain %s", fromdomain)
2289 fmt.Printf("dmarc record at domain %s: %s\n", domain, txt)
2290 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2293func dnssecStatus(v bool) string {
2295 return "with dnssec"
2297 return "without dnssec"
2300func cmdDMARCVerify(c *cmd) {
2301 c.params = "remoteip mailfromaddress helodomain < message"
2302 c.help = `Parse an email message and evaluate it against the DMARC policy of the domain in the From-header.
2304mailfromaddress and helodomain are used for SPF validation. If both are empty,
2305SPF validation is skipped.
2307mailfromaddress should be the address used as MAIL FROM in the SMTP session.
2308For DSN messages, that address may be empty. The helo domain was specified at
2309the beginning of the SMTP transaction that delivered the message. These values
2310can be found in message headers.
2317 var heloDomain *dns.Domain
2319 remoteIP := xparseIP(args[0], "remoteip")
2321 var mailfrom *smtp.Address
2323 a, err := smtp.ParseAddress(args[1])
2324 xcheckf(err, "parsing mailfrom address")
2328 d := xparseDomain(args[2], "helo domain")
2331 var received *spf.Received
2332 spfStatus := spf.StatusNone
2333 var spfIdentity *dns.Domain
2334 if mailfrom != nil || heloDomain != nil {
2335 spfArgs := spf.Args{
2337 LocalIP: net.ParseIP("127.0.0.1"),
2338 LocalHostname: dns.Domain{ASCII: "localhost"},
2340 if mailfrom != nil {
2341 spfArgs.MailFromLocalpart = mailfrom.Localpart
2342 spfArgs.MailFromDomain = mailfrom.Domain
2344 if heloDomain != nil {
2345 spfArgs.HelloDomain = dns.IPDomain{Domain: *heloDomain}
2347 rspf, spfDomain, expl, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfArgs)
2349 log.Printf("spf verify: %v (explanation: %q, authentic %v)", err, expl, authentic)
2352 spfStatus = received.Result
2353 // todo: should probably potentially do two separate spf validations
2354 if mailfrom != nil {
2355 spfIdentity = &mailfrom.Domain
2357 spfIdentity = heloDomain
2359 fmt.Printf("spf result: %s: %s (%s)\n", spfDomain, spfStatus, dnssecStatus(authentic))
2363 data, err := io.ReadAll(os.Stdin)
2364 xcheckf(err, "read message")
2365 dmarcFrom, _, _, err := message.From(c.log.Logger, false, bytes.NewReader(data), nil)
2366 xcheckf(err, "extract dmarc from message")
2368 const ignoreTestMode = false
2369 dkimResults, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, true, func(*dkim.Sig) error { return nil }, bytes.NewReader(data), ignoreTestMode)
2370 xcheckf(err, "dkim verify")
2371 for _, r := range dkimResults {
2372 fmt.Printf("dkim result: %q (err %v)\n", r.Status, r.Err)
2375 _, result := dmarc.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, dmarcFrom.Domain, dkimResults, spfStatus, spfIdentity, false)
2376 xcheckf(result.Err, "dmarc verify")
2377 fmt.Printf("dmarc from: %s\ndmarc status: %q\ndmarc reject: %v\ncmarc record: %s\n", dmarcFrom, result.Status, result.Reject, result.Record)
2380func cmdDMARCCheckreportaddrs(c *cmd) {
2382 c.help = `For each reporting address in the domain's DMARC record, check if it has opted into receiving reports (if needed).
2384A DMARC record can request reports about DMARC evaluations to be sent to an
2385email/http address. If the organizational domains of that of the DMARC record
2386and that of the report destination address do not match, the destination
2387address must opt-in to receiving DMARC reports by creating a DMARC record at
2388<dmarcdomain>._report._dmarc.<reportdestdomain>.
2395 dom := xparseDomain(args[0], "domain")
2396 _, domain, record, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dom)
2397 xcheckf(err, "dmarc lookup domain %s", dom)
2398 fmt.Printf("dmarc record at domain %s: %q\n", domain, txt)
2399 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2401 check := func(kind, addr string) {
2404 printResult := func(format string, args ...any) {
2405 fmt.Printf("%s %s: %s (%s)\n", kind, addr, fmt.Sprintf(format, args...), dnssecStatus(authentic))
2408 u, err := url.Parse(addr)
2410 printResult("parsing uri: %v (skipping)", addr, err)
2413 var destdom dns.Domain
2416 a, err := smtp.ParseAddress(u.Opaque)
2418 printResult("parsing destination email address %s: %v (skipping)", u.Opaque, err)
2423 printResult("unrecognized scheme in reporting address %s (skipping)", u.Scheme)
2427 if publicsuffix.Lookup(context.Background(), c.log.Logger, dom) == publicsuffix.Lookup(context.Background(), c.log.Logger, destdom) {
2428 printResult("pass (same organizational domain)")
2432 accepts, status, _, txts, authentic, err := dmarc.LookupExternalReportsAccepted(context.Background(), c.log.Logger, dns.StrictResolver{}, domain, destdom)
2434 txtaddr := fmt.Sprintf("%s._report._dmarc.%s", domain.ASCII, destdom.ASCII)
2436 txtstr = fmt.Sprintf(" (no txt records %s)", txtaddr)
2438 txtstr = fmt.Sprintf(" (txt record %s: %q)", txtaddr, txts)
2440 if status != dmarc.StatusNone {
2441 printResult("fail: %s%s", err, txtstr)
2443 printResult("pass%s", txtstr)
2444 } else if err != nil {
2445 printResult("fail: %s%s", err, txtstr)
2447 printResult("fail%s", txtstr)
2451 for _, uri := range record.AggregateReportAddresses {
2452 check("aggregate reporting", uri.Address)
2454 for _, uri := range record.FailureReportAddresses {
2455 check("failure reporting", uri.Address)
2459func cmdDMARCParsereportmsg(c *cmd) {
2460 c.params = "message ..."
2461 c.help = `Parse a DMARC report from an email message, and print its extracted details.
2463DMARC reports are periodically mailed, if requested in the DMARC DNS record of
2464a domain. Reports are sent by mail servers that received messages with our
2465domain in a From header. This may or may not be legatimate email. DMARC reports
2466contain summaries of evaluations of DMARC and DKIM/SPF, which can help
2467understand email deliverability problems.
2474 for _, arg := range args {
2475 f, err := os.Open(arg)
2476 xcheckf(err, "open %q", arg)
2477 feedback, err := dmarcrpt.ParseMessageReport(c.log.Logger, f)
2478 xcheckf(err, "parse report in %q", arg)
2479 meta := feedback.ReportMetadata
2480 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)
2481 if len(meta.Errors) > 0 {
2482 fmt.Printf("Errors:\n")
2483 for _, s := range meta.Errors {
2484 fmt.Printf("\t- %s\n", s)
2487 pol := feedback.PolicyPublished
2488 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)
2489 for _, record := range feedback.Records {
2490 idents := record.Identifiers
2491 fmt.Printf("\theaderfrom %q, envelopes from %q, to %q\n", idents.HeaderFrom, idents.EnvelopeFrom, idents.EnvelopeTo)
2492 eval := record.Row.PolicyEvaluated
2494 for _, reason := range eval.Reasons {
2495 reasons += "; " + string(reason.Type)
2496 if reason.Comment != "" {
2497 reasons += fmt.Sprintf(": %q", reason.Comment)
2500 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)
2501 for _, dkim := range record.AuthResults.DKIM {
2503 if dkim.HumanResult != "" {
2504 result = fmt.Sprintf(": %q", dkim.HumanResult)
2506 fmt.Printf("\t\tdkim %s; domain %q selector %q%s\n", dkim.Result, dkim.Domain, dkim.Selector, result)
2508 for _, spf := range record.AuthResults.SPF {
2509 fmt.Printf("\t\tspf %s; domain %q scope %q\n", spf.Result, spf.Domain, spf.Scope)
2515func cmdDMARCDBAddReport(c *cmd) {
2517 c.params = "fromdomain < message"
2518 c.help = "Add a DMARC report to the database."
2526 fromdomain := xparseDomain(args[0], "domain")
2527 fmt.Fprintln(os.Stderr, "reading report message from stdin")
2528 report, err := dmarcrpt.ParseMessageReport(c.log.Logger, os.Stdin)
2529 xcheckf(err, "parse message")
2530 err = dmarcdb.AddReport(context.Background(), report, fromdomain)
2531 xcheckf(err, "add dmarc report")
2534func cmdTLSRPTLookup(c *cmd) {
2536 c.help = `Lookup the TLSRPT record for the domain.
2538A TLSRPT record typically contains an email address where reports about TLS
2539connectivity should be sent. Mail servers attempting delivery to our domain
2540should attempt to use TLS. TLSRPT lets them report how many connection
2541successfully used TLS, and how what kind of errors occurred otherwise.
2548 d := xparseDomain(args[0], "domain")
2549 _, txt, err := tlsrpt.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, d)
2550 xcheckf(err, "tlsrpt lookup for %s", d)
2554func cmdTLSRPTParsereportmsg(c *cmd) {
2555 c.params = "message ..."
2556 c.help = `Parse and print the TLSRPT in the message.
2558The report is printed in formatted JSON.
2565 for _, arg := range args {
2566 f, err := os.Open(arg)
2567 xcheckf(err, "open %q", arg)
2568 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, f)
2569 xcheckf(err, "parse report in %q", arg)
2570 // todo future: only print the highlights?
2571 enc := json.NewEncoder(os.Stdout)
2572 enc.SetIndent("", "\t")
2573 enc.SetEscapeHTML(false)
2574 err = enc.Encode(reportJSON)
2575 xcheckf(err, "write report")
2579func cmdSPFCheck(c *cmd) {
2580 c.params = "domain ip"
2581 c.help = `Check the status of IP for the policy published in DNS for the domain.
2583IPs may be allowed to send for a domain, or disallowed, and several shades in
2584between. If not allowed, an explanation may be provided by the policy. If so,
2585the explanation is printed. The SPF mechanism that matched (if any) is also
2593 domain := xparseDomain(args[0], "domain")
2595 ip := xparseIP(args[1], "ip")
2597 spfargs := spf.Args{
2599 MailFromLocalpart: "user",
2600 MailFromDomain: domain,
2601 HelloDomain: dns.IPDomain{Domain: domain},
2602 LocalIP: net.ParseIP("127.0.0.1"),
2603 LocalHostname: dns.Domain{ASCII: "localhost"},
2605 r, _, explanation, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfargs)
2607 fmt.Printf("error: %s\n", err)
2609 if explanation != "" {
2610 fmt.Printf("explanation: %s\n", explanation)
2612 fmt.Printf("status: %s (%s)\n", r.Result, dnssecStatus(authentic))
2613 if r.Mechanism != "" {
2614 fmt.Printf("mechanism: %s\n", r.Mechanism)
2618func cmdSPFParse(c *cmd) {
2619 c.params = "txtrecord"
2620 c.help = "Parse the record as SPF record. If valid, nothing is printed."
2626 _, _, err := spf.ParseRecord(args[0])
2627 xcheckf(err, "parsing record")
2630func cmdSPFLookup(c *cmd) {
2632 c.help = "Lookup the SPF record for the domain and print it."
2638 domain := xparseDomain(args[0], "domain")
2639 _, txt, _, authentic, err := spf.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
2640 xcheckf(err, "spf lookup for %s", domain)
2642 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2645func cmdMTASTSLookup(c *cmd) {
2647 c.help = `Lookup the MTASTS record and policy for the domain.
2649MTA-STS is a mechanism for a domain to specify if it requires TLS connections
2650for delivering email. If a domain has a valid MTA-STS DNS TXT record at
2651_mta-sts.<domain> it signals it implements MTA-STS. A policy can then be
2652fetched at https://mta-sts.<domain>/.well-known/mta-sts.txt. The policy
2653specifies the mode (enforce, testing, none), which MX servers support TLS and
2654should be used, and how long the policy can be cached.
2661 domain := xparseDomain(args[0], "domain")
2663 record, policy, _, err := mtasts.Get(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
2665 fmt.Printf("error: %s\n", err)
2668 fmt.Printf("DNS TXT record _mta-sts.%s: %s\n", domain.ASCII, record.String())
2672 fmt.Printf("policy at https://mta-sts.%s/.well-known/mta-sts.txt:\n", domain.ASCII)
2673 fmt.Printf("%s", policy.String())
2677func cmdRetrain(c *cmd) {
2678 c.params = "accountname"
2679 c.help = `Recreate and retrain the junk filter for the account.
2681Useful after having made changes to the junk filter configuration, or if the
2682implementation has changed.
2690 ctlcmdRetrain(xctl(), args[0])
2693func ctlcmdRetrain(ctl *ctl, account string) {
2694 ctl.xwrite("retrain")
2699func cmdTLSRPTDBAddReport(c *cmd) {
2701 c.params = "< message"
2702 c.help = "Parse a TLS report from the message and add it to the database."
2704 c.flag.BoolVar(&hostReport, "hostreport", false, "report for a host instead of domain")
2712 // First read message, to get the From-header. Then parse it as TLSRPT.
2713 fmt.Fprintln(os.Stderr, "reading report message from stdin")
2714 buf, err := io.ReadAll(os.Stdin)
2715 xcheckf(err, "reading message")
2716 part, err := message.Parse(c.log.Logger, true, bytes.NewReader(buf))
2717 xcheckf(err, "parsing message")
2718 if part.Envelope == nil || len(part.Envelope.From) != 1 {
2719 log.Fatalf("message must have one From-header")
2721 from := part.Envelope.From[0]
2722 domain := xparseDomain(from.Host, "domain")
2724 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, bytes.NewReader(buf))
2725 xcheckf(err, "parsing tls report in message")
2727 mailfrom := from.User + "@" + from.Host // todo future: should escape and such
2728 report := reportJSON.Convert()
2729 err = tlsrptdb.AddReport(context.Background(), c.log, domain, mailfrom, hostReport, &report)
2730 xcheckf(err, "add tls report to database")
2733func cmdDNSBLCheck(c *cmd) {
2734 c.params = "zone ip"
2735 c.help = `Test if IP is in the DNS blocklist of the zone, e.g. bl.spamcop.net.
2737If the IP is in the blocklist, an explanation is printed. This is typically a
2738URL with more information.
2745 zone := xparseDomain(args[0], "zone")
2746 ip := xparseIP(args[1], "ip")
2748 status, explanation, err := dnsbl.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, zone, ip)
2749 fmt.Printf("status: %s\n", status)
2750 if status == dnsbl.StatusFail {
2751 fmt.Printf("explanation: %q\n", explanation)
2754 fmt.Printf("error: %s\n", err)
2758func cmdDNSBLCheckhealth(c *cmd) {
2760 c.help = `Check the health of the DNS blocklist represented by zone, e.g. bl.spamcop.net.
2762The health of a DNS blocklist can be checked by querying for 127.0.0.1 and
2763127.0.0.2. The second must and the first must not be present.
2770 zone := xparseDomain(args[0], "zone")
2771 err := dnsbl.CheckHealth(context.Background(), c.log.Logger, dns.StrictResolver{}, zone)
2772 xcheckf(err, "unhealthy")
2773 fmt.Println("healthy")
2776func cmdCheckupdate(c *cmd) {
2777 c.help = `Check if a newer version of mox is available.
2779A single DNS TXT lookup to _updates.xmox.nl tells if a new version is
2780available. If so, a changelog is fetched from https://updates.xmox.nl, and the
2781individual entries verified with a builtin public key. The changelog is
2784 if len(c.Parse()) != 0 {
2789 current, lastknown, _, err := mox.LastKnown()
2791 log.Printf("getting last known version: %s", err)
2793 fmt.Printf("last known version: %s\n", lastknown)
2794 fmt.Printf("current version: %s\n", current)
2796 latest, _, err := updates.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dns.Domain{ASCII: changelogDomain})
2797 xcheckf(err, "lookup of latest version")
2798 fmt.Printf("latest version: %s\n", latest)
2800 if latest.After(current) {
2801 changelog, err := updates.FetchChangelog(context.Background(), c.log.Logger, changelogURL, current, changelogPubKey)
2802 xcheckf(err, "fetching changelog")
2803 if len(changelog.Changes) == 0 {
2804 log.Printf("no changes in changelog")
2807 fmt.Println("Changelog")
2808 for _, c := range changelog.Changes {
2809 fmt.Println("\n" + strings.TrimSpace(c.Text))
2814func cmdCid(c *cmd) {
2816 c.help = `Turn an ID from a Received header into a cid, for looking up in logs.
2818A cid is essentially a connection counter initialized when mox starts. Each log
2819line contains a cid. Received headers added by mox contain a unique ID that can
2820be decrypted to a cid by admin of a mox instance only.
2828 recvidpath := mox.DataDirPath("receivedid.key")
2829 recvidbuf, err := os.ReadFile(recvidpath)
2830 xcheckf(err, "reading %s", recvidpath)
2831 if len(recvidbuf) != 16+8 {
2832 log.Fatalf("bad data in %s: got %d bytes, expect 16+8=24", recvidpath, len(recvidbuf))
2834 err = mox.ReceivedIDInit(recvidbuf[:16], recvidbuf[16:])
2835 xcheckf(err, "init receivedid")
2837 cid, err := mox.ReceivedToCid(args[0])
2838 xcheckf(err, "received id to cid")
2839 fmt.Printf("%x\n", cid)
2842func cmdVersion(c *cmd) {
2843 c.help = "Prints this mox version."
2844 if len(c.Parse()) != 0 {
2847 fmt.Println(moxvar.Version)
2848 fmt.Printf("%s %s/%s\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
2851func cmdWebapi(c *cmd) {
2852 c.params = "[method [baseurl-with-credentials]"
2853 c.help = "Lists available methods, prints request/response parameters for method, or calls a method with a request read from standard input."
2859 t := reflect.TypeOf((*webapi.Methods)(nil)).Elem()
2860 methods := map[string]reflect.Type{}
2862 for i := 0; i < t.NumMethod(); i++ {
2864 methods[mt.Name] = mt.Type
2865 ml = append(ml, mt.Name)
2869 fmt.Println(strings.Join(ml, "\n"))
2873 mt, ok := methods[args[0]]
2875 log.Fatalf("unknown method %q", args[0])
2877 resultNotJSON := mt.Out(0).Kind() == reflect.Interface
2880 fmt.Println("# Example request")
2882 printJSON("\t", mox.FillExample(nil, reflect.New(mt.In(1))).Interface())
2885 fmt.Println("Output is non-JSON data.")
2888 fmt.Println("# Example response")
2890 printJSON("\t", mox.FillExample(nil, reflect.New(mt.Out(0))).Interface())
2896 response = reflect.New(mt.Out(0))
2899 fmt.Fprintln(os.Stderr, "reading request from stdin...")
2900 request, err := io.ReadAll(os.Stdin)
2901 xcheckf(err, "read message")
2903 dec := json.NewDecoder(bytes.NewReader(request))
2904 dec.DisallowUnknownFields()
2905 err = dec.Decode(reflect.New(mt.In(1)).Interface())
2906 xcheckf(err, "parsing request")
2908 resp, err := http.PostForm(args[1]+args[0], url.Values{"request": []string{string(request)}})
2909 xcheckf(err, "http post")
2910 defer resp.Body.Close()
2911 if resp.StatusCode == http.StatusBadRequest {
2912 buf, err := io.ReadAll(&moxio.LimitReader{R: resp.Body, Limit: 10 * 1024})
2913 xcheckf(err, "reading response for 400 bad request error")
2914 err = json.Unmarshal(buf, &response)
2916 printJSON("", response)
2918 fmt.Fprintf(os.Stderr, "(not json)\n")
2919 os.Stderr.Write(buf)
2922 } else if resp.StatusCode != http.StatusOK {
2923 fmt.Fprintf(os.Stderr, "http response %s\n", resp.Status)
2924 _, err := io.Copy(os.Stderr, resp.Body)
2925 xcheckf(err, "copy body")
2927 err := json.NewDecoder(resp.Body).Decode(&resp)
2928 xcheckf(err, "unmarshal response")
2929 printJSON("", response)
2933func printJSON(indent string, v any) {
2934 fmt.Printf("%s", indent)
2935 enc := json.NewEncoder(os.Stdout)
2936 enc.SetIndent(indent, "\t")
2937 enc.SetEscapeHTML(false)
2938 err := enc.Encode(v)
2939 xcheckf(err, "encode json")
2942// 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.
2943func cmdBumpUIDValidity(c *cmd) {
2944 c.params = "account [mailbox]"
2945 c.help = `Change the IMAP UID validity of the mailbox, causing IMAP clients to refetch messages.
2947This can be useful after manually repairing metadata about the account/mailbox.
2949Opens account database file directly. Ensure mox does not have the account
2950open, or is not running.
2953 if len(args) != 1 && len(args) != 2 {
2958 a, err := store.OpenAccount(c.log, args[0])
2959 xcheckf(err, "open account")
2961 if err := a.Close(); err != nil {
2962 log.Printf("closing account: %v", err)
2966 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
2967 uidvalidity, err := a.NextUIDValidity(tx)
2969 return fmt.Errorf("assigning next uid validity: %v", err)
2972 q := bstore.QueryTx[store.Mailbox](tx)
2974 q.FilterEqual("Name", args[1])
2976 mbl, err := q.SortAsc("Name").List()
2978 return fmt.Errorf("looking up mailbox: %v", err)
2980 if len(args) == 2 && len(mbl) != 1 {
2981 return fmt.Errorf("looking up mailbox %q, found %d mailboxes", args[1], len(mbl))
2983 for _, mb := range mbl {
2984 mb.UIDValidity = uidvalidity
2985 err = tx.Update(&mb)
2987 return fmt.Errorf("updating uid validity for mailbox: %v", err)
2989 fmt.Printf("uid validity for %q updated to %d\n", mb.Name, uidvalidity)
2993 xcheckf(err, "updating database")
2996func cmdReassignUIDs(c *cmd) {
2997 c.params = "account [mailboxid]"
2998 c.help = `Reassign UIDs in one mailbox or all mailboxes in an account and bump UID validity, causing IMAP clients to refetch messages.
3000Opens account database file directly. Ensure mox does not have the account
3001open, or is not running.
3004 if len(args) != 1 && len(args) != 2 {
3011 mailboxID, err = strconv.ParseInt(args[1], 10, 64)
3012 xcheckf(err, "parsing mailbox id")
3016 a, err := store.OpenAccount(c.log, args[0])
3017 xcheckf(err, "open account")
3019 if err := a.Close(); err != nil {
3020 log.Printf("closing account: %v", err)
3024 // Gather the last-assigned UIDs per mailbox.
3025 uidlasts := map[int64]store.UID{}
3027 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3028 // Reassign UIDs, going per mailbox. We assign starting at 1, only changing the
3029 // message if it isn't already at the intended UID. Doing it in this order ensures
3030 // we don't get into trouble with duplicate UIDs for a mailbox. We assign a new
3031 // modseq. Not strictly needed, for doesn't hurt.
3032 modseq, err := a.NextModSeq(tx)
3033 xcheckf(err, "assigning next modseq")
3035 q := bstore.QueryTx[store.Message](tx)
3037 q.FilterNonzero(store.Message{MailboxID: mailboxID})
3039 q.SortAsc("MailboxID", "UID")
3040 err = q.ForEach(func(m store.Message) error {
3041 uidlasts[m.MailboxID]++
3042 uid := uidlasts[m.MailboxID]
3046 if err := tx.Update(&m); err != nil {
3047 return fmt.Errorf("updating uid for message: %v", err)
3053 return fmt.Errorf("reading through messages: %v", err)
3056 // Now update the uidnext and uidvalidity for each mailbox.
3057 err = bstore.QueryTx[store.Mailbox](tx).ForEach(func(mb store.Mailbox) error {
3058 // Assign each mailbox a completely new uidvalidity.
3059 uidvalidity, err := a.NextUIDValidity(tx)
3061 return fmt.Errorf("assigning next uid validity: %v", err)
3064 if mb.UIDValidity >= uidvalidity {
3065 // This should not happen, but since we're fixing things up after a hypothetical
3066 // mishap, might as well account for inconsistent uidvalidity.
3067 next := store.NextUIDValidity{ID: 1, Next: mb.UIDValidity + 2}
3068 if err := tx.Update(&next); err != nil {
3069 log.Printf("updating nextuidvalidity: %v, continuing", err)
3073 mb.UIDValidity = uidvalidity
3075 mb.UIDNext = uidlasts[mb.ID] + 1
3076 if err := tx.Update(&mb); err != nil {
3077 return fmt.Errorf("updating uidvalidity and uidnext for mailbox: %v", err)
3082 return fmt.Errorf("updating mailboxes: %v", err)
3086 xcheckf(err, "updating database")
3089func cmdFixUIDMeta(c *cmd) {
3090 c.params = "account"
3091 c.help = `Fix inconsistent UIDVALIDITY and UIDNEXT in messages/mailboxes/account.
3093The next UID to use for a message in a mailbox should always be higher than any
3094existing message UID in the mailbox. If it is not, the mailbox UIDNEXT is
3097Each mailbox has a UIDVALIDITY sequence number, which should always be lower
3098than the per-account next UIDVALIDITY to use. If it is not, the account next
3099UIDVALIDITY is updated.
3101Opens account database file directly. Ensure mox does not have the account
3102open, or is not running.
3110 a, err := store.OpenAccount(c.log, args[0])
3111 xcheckf(err, "open account")
3113 if err := a.Close(); err != nil {
3114 log.Printf("closing account: %v", err)
3118 var maxUIDValidity uint32
3120 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3121 // We look at each mailbox, retrieve its max UID and compare against the mailbox
3123 err := bstore.QueryTx[store.Mailbox](tx).ForEach(func(mb store.Mailbox) error {
3124 if mb.UIDValidity > maxUIDValidity {
3125 maxUIDValidity = mb.UIDValidity
3127 m, err := bstore.QueryTx[store.Message](tx).FilterNonzero(store.Message{MailboxID: mb.ID}).SortDesc("UID").Limit(1).Get()
3128 if err == bstore.ErrAbsent || err == nil && m.UID < mb.UIDNext {
3130 } else if err != nil {
3131 return fmt.Errorf("finding message with max uid in mailbox: %w", err)
3133 olduidnext := mb.UIDNext
3134 mb.UIDNext = m.UID + 1
3135 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)
3136 if err := tx.Update(&mb); err != nil {
3137 return fmt.Errorf("updating mailbox uidnext: %v", err)
3142 return fmt.Errorf("processing mailboxes: %v", err)
3145 uidvalidity := store.NextUIDValidity{ID: 1}
3146 if err := tx.Get(&uidvalidity); err != nil {
3147 return fmt.Errorf("reading account next uidvalidity: %v", err)
3149 if maxUIDValidity >= uidvalidity.Next {
3150 log.Printf("account next uidvalidity %d <= highest uidvalidity %d found in mailbox, resetting account next uidvalidity to %d", uidvalidity.Next, maxUIDValidity, maxUIDValidity+1)
3151 uidvalidity.Next = maxUIDValidity + 1
3152 if err := tx.Update(&uidvalidity); err != nil {
3153 return fmt.Errorf("updating account next uidvalidity: %v", err)
3159 xcheckf(err, "updating database")
3162func cmdFixmsgsize(c *cmd) {
3163 c.params = "[account]"
3164 c.help = `Ensure message sizes in the database matching the sum of the message prefix length and on-disk file size.
3166Messages with an inconsistent size are also parsed again.
3168If an inconsistency is found, you should probably also run "mox
3169bumpuidvalidity" on the mailboxes or entire account to force IMAP clients to
3182 ctlcmdFixmsgsize(xctl(), account)
3185func ctlcmdFixmsgsize(ctl *ctl, account string) {
3186 ctl.xwrite("fixmsgsize")
3189 ctl.xstreamto(os.Stdout)
3192func cmdReparse(c *cmd) {
3193 c.params = "[account]"
3194 c.help = `Parse all messages in the account or all accounts again.
3196Can be useful after upgrading mox with improved message parsing. Messages are
3197parsed in batches, so other access to the mailboxes/messages are not blocked
3198while reparsing all messages.
3210 ctlcmdReparse(xctl(), account)
3213func ctlcmdReparse(ctl *ctl, account string) {
3214 ctl.xwrite("reparse")
3217 ctl.xstreamto(os.Stdout)
3220func cmdEnsureParsed(c *cmd) {
3221 c.params = "account"
3222 c.help = "Ensure messages in the database have a pre-parsed MIME form in the database."
3224 c.flag.BoolVar(&all, "all", false, "store new parsed message for all messages")
3231 a, err := store.OpenAccount(c.log, args[0])
3232 xcheckf(err, "open account")
3234 if err := a.Close(); err != nil {
3235 log.Printf("closing account: %v", err)
3240 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3241 q := bstore.QueryTx[store.Message](tx)
3242 q.FilterEqual("Expunged", false)
3243 q.FilterFn(func(m store.Message) bool {
3244 return all || m.ParsedBuf == nil
3248 return fmt.Errorf("list messages: %v", err)
3250 for _, m := range l {
3251 mr := a.MessageReader(m)
3252 p, err := message.EnsurePart(c.log.Logger, false, mr, m.Size)
3254 log.Printf("parsing message %d: %v (continuing)", m.ID, err)
3256 m.ParsedBuf, err = json.Marshal(p)
3258 return fmt.Errorf("marshal parsed message: %v", err)
3260 if err := tx.Update(&m); err != nil {
3261 return fmt.Errorf("update message: %v", err)
3267 xcheckf(err, "update messages with parsed mime structure")
3268 fmt.Printf("%d messages updated\n", n)
3271func cmdRecalculateMailboxCounts(c *cmd) {
3272 c.params = "account"
3273 c.help = `Recalculate message counts for all mailboxes in the account, and total message size for quota.
3275When a message is added to/removed from a mailbox, or when message flags change,
3276the total, unread, unseen and deleted messages are accounted, the total size of
3277the mailbox, and the total message size for the account. In case of a bug in
3278this accounting, the numbers could become incorrect. This command will find, fix
3287 ctlcmdRecalculateMailboxCounts(xctl(), args[0])
3290func ctlcmdRecalculateMailboxCounts(ctl *ctl, account string) {
3291 ctl.xwrite("recalculatemailboxcounts")
3294 ctl.xstreamto(os.Stdout)
3297func cmdMessageParse(c *cmd) {
3298 c.params = "message.eml"
3299 c.help = "Parse message, print JSON representation."
3302 c.flag.BoolVar(&smtputf8, "smtputf8", false, "check if message needs smtputf8")
3308 f, err := os.Open(args[0])
3309 xcheckf(err, "open")
3312 part, err := message.Parse(c.log.Logger, false, f)
3313 xcheckf(err, "parsing message")
3314 err = part.Walk(c.log.Logger, nil)
3315 xcheckf(err, "parsing nested parts")
3316 enc := json.NewEncoder(os.Stdout)
3317 enc.SetIndent("", "\t")
3318 enc.SetEscapeHTML(false)
3319 err = enc.Encode(part)
3320 xcheckf(err, "write")
3322 hasNonASCII := func(r io.Reader) bool {
3323 br := bufio.NewReader(r)
3325 b, err := br.ReadByte()
3329 xcheckf(err, "read header")
3337 var walk func(p *message.Part) bool
3338 walk = func(p *message.Part) bool {
3339 if hasNonASCII(p.HeaderReader()) {
3342 for _, pp := range p.Parts {
3350 fmt.Println("message needs smtputf8:", walk(&part))
3354func cmdOpenaccounts(c *cmd) {
3356 c.params = "datadir account ..."
3357 c.help = `Open and close accounts, for triggering data upgrades, for tests.
3359Opens database files directly, not going through a running mox instance.
3367 dataDir := filepath.Clean(args[0])
3368 for _, accName := range args[1:] {
3369 accDir := filepath.Join(dataDir, "accounts", accName)
3370 log.Printf("opening account %s...", accDir)
3371 a, err := store.OpenAccountDB(c.log, accDir, accName)
3372 xcheckf(err, "open account %s", accName)
3373 err = a.ThreadingWait(c.log)
3374 xcheckf(err, "wait for threading upgrade to complete for %s", accName)
3376 xcheckf(err, "close account %s", accName)
3380func cmdReassignthreads(c *cmd) {
3381 c.params = "[account]"
3382 c.help = `Reassign message threads.
3384For all accounts, or optionally only the specified account.
3386Threading for all messages in an account is first reset, and new base subject
3387and normalized message-id saved with the message. Then all messages are
3388evaluated and matched against their parents/ancestors.
3390Messages are matched based on the References header, with a fall-back to an
3391In-Reply-To header, and if neither is present/valid, based only on base
3394A References header typically points to multiple previous messages in a
3395hierarchy. From oldest ancestor to most recent parent. An In-Reply-To header
3396would have only a message-id of the parent message.
3398A message is only linked to a parent/ancestor if their base subject is the
3399same. This ensures unrelated replies, with a new subject, are placed in their
3402The base subject is lower cased, has whitespace collapsed to a single
3403space, and some components removed: leading "Re:", "Fwd:", "Fw:", or bracketed
3404tag (that mailing lists often add, e.g. "[listname]"), trailing "(fwd)", or
3405enclosing "[fwd: ...]".
3407Messages are linked to all their ancestors. If an intermediate parent/ancestor
3408message is deleted in the future, the message can still be linked to the earlier
3409ancestors. If the direct parent already wasn't available while matching, this is
3410stored as the message having a "missing link" to its stored ancestors.
3422 ctlcmdReassignthreads(xctl(), account)
3425func ctlcmdReassignthreads(ctl *ctl, account string) {
3426 ctl.xwrite("reassignthreads")
3429 ctl.xstreamto(os.Stdout)
3432func cmdReadmessages(c *cmd) {
3434 c.params = "datadir account ..."
3435 c.help = `Open account, parse several headers for all messages.
3437For performance testing.
3439Opens database files directly, not going through a running mox instance.
3442 gomaxprocs := runtime.GOMAXPROCS(0)
3443 var procs, workqueuesize, limit int
3444 c.flag.IntVar(&procs, "procs", gomaxprocs, "number of goroutines for reading messages")
3445 c.flag.IntVar(&workqueuesize, "workqueuesize", 2*gomaxprocs, "number of messages to keep in work queue")
3446 c.flag.IntVar(&limit, "limit", 0, "number of messages to process if greater than zero")
3452 type threadPrep struct {
3457 threadingFields := [][]byte{
3458 []byte("references"),
3459 []byte("in-reply-to"),
3462 dataDir := filepath.Clean(args[0])
3463 for _, accName := range args[1:] {
3464 accDir := filepath.Join(dataDir, "accounts", accName)
3465 log.Printf("opening account %s...", accDir)
3466 a, err := store.OpenAccountDB(c.log, accDir, accName)
3467 xcheckf(err, "open account %s", accName)
3469 prepareMessages := func(in, out chan moxio.Work[store.Message, threadPrep]) {
3470 headerbuf := make([]byte, 8*1024)
3471 scratch := make([]byte, 4*1024)
3479 var partialPart struct {
3483 if err := json.Unmarshal(m.ParsedBuf, &partialPart); err != nil {
3484 w.Err = fmt.Errorf("unmarshal part: %v", err)
3486 size := partialPart.BodyOffset - partialPart.HeaderOffset
3487 if int(size) > len(headerbuf) {
3488 headerbuf = make([]byte, size)
3491 buf := headerbuf[:int(size)]
3492 err := func() error {
3493 mr := a.MessageReader(m)
3496 // ReadAt returns whole buffer or error. Single read should be fast.
3497 n, err := mr.ReadAt(buf, partialPart.HeaderOffset)
3498 if err != nil || n != len(buf) {
3499 return fmt.Errorf("read header: %v", err)
3505 } else if h, err := message.ParseHeaderFields(buf, scratch, threadingFields); err != nil {
3508 w.Out.references = h["References"]
3509 w.Out.inReplyTo = h["In-Reply-To"]
3522 processMessage := func(m store.Message, prep threadPrep) error {
3524 log.Printf("%d messages (delta %s)", n, time.Since(t))
3531 wq := moxio.NewWorkQueue[store.Message, threadPrep](procs, workqueuesize, prepareMessages, processMessage)
3533 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3534 q := bstore.QueryTx[store.Message](tx)
3535 q.FilterEqual("Expunged", false)
3540 err = q.ForEach(wq.Add)
3548 xcheckf(err, "processing message")
3551 xcheckf(err, "close account %s", accName)
3552 log.Printf("account %s, total time %s", accName, time.Since(t0))
3556func cmdQueueFillRetired(c *cmd) {
3558 c.help = `Fill retired messag and webhooks queue with testdata.
3560For testing the pagination. Operates directly on queue database.
3563 c.flag.IntVar(&n, "n", 10000, "retired messages and retired webhooks to insert")
3571 xcheckf(err, "init queue")
3572 err = queue.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3575 // Cause autoincrement ID for queue.Msg to be forwarded, and use the reserved ID
3576 // space for inserting retired messages.
3578 err = tx.Insert(&fm)
3579 xcheckf(err, "temporarily insert message to get autoincrement sequence")
3580 err = tx.Delete(&fm)
3581 xcheckf(err, "removing temporary message for resetting autoincrement sequence")
3583 err = tx.Insert(&fm)
3584 xcheckf(err, "temporarily insert message to forward autoincrement sequence")
3585 err = tx.Delete(&fm)
3586 xcheckf(err, "removing temporary message after forwarding autoincrement sequence")
3589 // And likewise for webhooks.
3590 fh := queue.Hook{Account: "x", URL: "x", NextAttempt: time.Now()}
3591 err = tx.Insert(&fh)
3592 xcheckf(err, "temporarily insert webhook to get autoincrement sequence")
3593 err = tx.Delete(&fh)
3594 xcheckf(err, "removing temporary webhook for resetting autoincrement sequence")
3596 err = tx.Insert(&fh)
3597 xcheckf(err, "temporarily insert webhook to forward autoincrement sequence")
3598 err = tx.Delete(&fh)
3599 xcheckf(err, "removing temporary webhook after forwarding autoincrement sequence")
3602 for i := 0; i < n; i++ {
3603 t0 := now.Add(-time.Duration(i) * time.Second)
3604 last := now.Add(-time.Duration(i/10) * time.Second)
3605 mr := queue.MsgRetired{
3606 ID: fm.ID + int64(i),
3608 SenderAccount: "test",
3609 SenderLocalpart: "mox",
3610 SenderDomainStr: "localhost",
3611 FromID: fmt.Sprintf("%016d", i),
3612 RecipientLocalpart: "mox",
3613 RecipientDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "localhost"}},
3614 RecipientDomainStr: "localhost",
3617 Results: []queue.MsgResult{
3620 Duration: time.Millisecond,
3627 Size: int64(i * 100),
3628 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
3629 Subject: fmt.Sprintf("test message %d", i),
3630 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
3632 RecipientAddress: "mox@localhost",
3634 KeepUntil: now.Add(48 * time.Hour),
3636 err := tx.Insert(&mr)
3637 xcheckf(err, "inserting retired message")
3640 for i := 0; i < n; i++ {
3641 t0 := now.Add(-time.Duration(i) * time.Second)
3642 last := now.Add(-time.Duration(i/10) * time.Second)
3647 hr := queue.HookRetired{
3648 ID: fh.ID + int64(i),
3649 QueueMsgID: fm.ID + int64(i),
3650 FromID: fmt.Sprintf("%016d", i),
3651 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
3652 Subject: fmt.Sprintf("test message %d", i),
3653 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
3655 URL: "http://localhost/hook",
3656 IsIncoming: i%10 == 0,
3657 OutgoingEvent: event,
3662 Results: []queue.HookResult{
3665 Duration: time.Millisecond,
3666 URL: "http://localhost/hook",
3675 KeepUntil: now.Add(48 * time.Hour),
3677 err := tx.Insert(&hr)
3678 xcheckf(err, "inserting retired hook")
3683 xcheckf(err, "add to queue")
3684 log.Printf("added %d retired messages and %d retired webhooks", n, n)