11 cryptorand "crypto/rand"
38 "golang.org/x/crypto/bcrypt"
39 "golang.org/x/text/secure/precis"
41 "github.com/mjl-/adns"
43 "github.com/mjl-/autocert"
44 "github.com/mjl-/bstore"
45 "github.com/mjl-/sconf"
46 "github.com/mjl-/sherpa"
48 "github.com/mjl-/mox/config"
49 "github.com/mjl-/mox/dane"
50 "github.com/mjl-/mox/dkim"
51 "github.com/mjl-/mox/dmarc"
52 "github.com/mjl-/mox/dmarcdb"
53 "github.com/mjl-/mox/dmarcrpt"
54 "github.com/mjl-/mox/dns"
55 "github.com/mjl-/mox/dnsbl"
56 "github.com/mjl-/mox/message"
57 "github.com/mjl-/mox/mlog"
58 "github.com/mjl-/mox/mox-"
59 "github.com/mjl-/mox/moxio"
60 "github.com/mjl-/mox/moxvar"
61 "github.com/mjl-/mox/mtasts"
62 "github.com/mjl-/mox/publicsuffix"
63 "github.com/mjl-/mox/queue"
64 "github.com/mjl-/mox/smtp"
65 "github.com/mjl-/mox/smtpclient"
66 "github.com/mjl-/mox/spf"
67 "github.com/mjl-/mox/store"
68 "github.com/mjl-/mox/tlsrpt"
69 "github.com/mjl-/mox/tlsrptdb"
70 "github.com/mjl-/mox/updates"
71 "github.com/mjl-/mox/webadmin"
72 "github.com/mjl-/mox/webapi"
76 changelogDomain = "xmox.nl"
77 changelogURL = "https://updates.xmox.nl/changelog"
78 changelogPubKey = base64Decode("sPNiTDQzvb4FrytNEiebJhgyQzn57RwEjNbGWMM/bDY=")
81func base64Decode(s string) []byte {
82 buf, err := base64.StdEncoding.DecodeString(s)
89func envString(k, def string) string {
97var commands = []struct {
102 {"quickstart", cmdQuickstart},
104 {"setaccountpassword", cmdSetaccountpassword},
105 {"setadminpassword", cmdSetadminpassword},
106 {"loglevels", cmdLoglevels},
107 {"queue holdrules list", cmdQueueHoldrulesList},
108 {"queue holdrules add", cmdQueueHoldrulesAdd},
109 {"queue holdrules remove", cmdQueueHoldrulesRemove},
110 {"queue list", cmdQueueList},
111 {"queue hold", cmdQueueHold},
112 {"queue unhold", cmdQueueUnhold},
113 {"queue schedule", cmdQueueSchedule},
114 {"queue transport", cmdQueueTransport},
115 {"queue requiretls", cmdQueueRequireTLS},
116 {"queue fail", cmdQueueFail},
117 {"queue drop", cmdQueueDrop},
118 {"queue dump", cmdQueueDump},
119 {"queue retired list", cmdQueueRetiredList},
120 {"queue retired print", cmdQueueRetiredPrint},
121 {"queue suppress list", cmdQueueSuppressList},
122 {"queue suppress add", cmdQueueSuppressAdd},
123 {"queue suppress remove", cmdQueueSuppressRemove},
124 {"queue suppress lookup", cmdQueueSuppressLookup},
125 {"queue webhook list", cmdQueueHookList},
126 {"queue webhook schedule", cmdQueueHookSchedule},
127 {"queue webhook cancel", cmdQueueHookCancel},
128 {"queue webhook print", cmdQueueHookPrint},
129 {"queue webhook retired list", cmdQueueHookRetiredList},
130 {"queue webhook retired print", cmdQueueHookRetiredPrint},
131 {"import maildir", cmdImportMaildir},
132 {"import mbox", cmdImportMbox},
133 {"export maildir", cmdExportMaildir},
134 {"export mbox", cmdExportMbox},
135 {"localserve", cmdLocalserve},
137 {"backup", cmdBackup},
138 {"verifydata", cmdVerifydata},
140 {"config test", cmdConfigTest},
141 {"config dnscheck", cmdConfigDNSCheck},
142 {"config dnsrecords", cmdConfigDNSRecords},
143 {"config describe-domains", cmdConfigDescribeDomains},
144 {"config describe-static", cmdConfigDescribeStatic},
145 {"config account add", cmdConfigAccountAdd},
146 {"config account rm", cmdConfigAccountRemove},
147 {"config address add", cmdConfigAddressAdd},
148 {"config address rm", cmdConfigAddressRemove},
149 {"config domain add", cmdConfigDomainAdd},
150 {"config domain rm", cmdConfigDomainRemove},
151 {"config alias list", cmdConfigAliasList},
152 {"config alias print", cmdConfigAliasPrint},
153 {"config alias add", cmdConfigAliasAdd},
154 {"config alias update", cmdConfigAliasUpdate},
155 {"config alias rm", cmdConfigAliasRemove},
156 {"config alias addaddr", cmdConfigAliasAddaddr},
157 {"config alias rmaddr", cmdConfigAliasRemoveaddr},
159 {"config describe-sendmail", cmdConfigDescribeSendmail},
160 {"config printservice", cmdConfigPrintservice},
161 {"config ensureacmehostprivatekeys", cmdConfigEnsureACMEHostprivatekeys},
162 {"config example", cmdConfigExample},
164 {"checkupdate", cmdCheckupdate},
166 {"clientconfig", cmdClientConfig},
167 {"deliver", cmdDeliver},
168 // todo: turn cmdDANEDialmx into a regular "dialmx" command that follows mta-sts policy, with options to require dane, mta-sts or requiretls. the code will be similar to queue/direct.go
169 {"dane dial", cmdDANEDial},
170 {"dane dialmx", cmdDANEDialmx},
171 {"dane makerecord", cmdDANEMakeRecord},
172 {"dns lookup", cmdDNSLookup},
173 {"dkim gened25519", cmdDKIMGened25519},
174 {"dkim genrsa", cmdDKIMGenrsa},
175 {"dkim lookup", cmdDKIMLookup},
176 {"dkim txt", cmdDKIMTXT},
177 {"dkim verify", cmdDKIMVerify},
178 {"dkim sign", cmdDKIMSign},
179 {"dmarc lookup", cmdDMARCLookup},
180 {"dmarc parsereportmsg", cmdDMARCParsereportmsg},
181 {"dmarc verify", cmdDMARCVerify},
182 {"dmarc checkreportaddrs", cmdDMARCCheckreportaddrs},
183 {"dnsbl check", cmdDNSBLCheck},
184 {"dnsbl checkhealth", cmdDNSBLCheckhealth},
185 {"mtasts lookup", cmdMTASTSLookup},
186 {"retrain", cmdRetrain},
187 {"sendmail", cmdSendmail},
188 {"spf check", cmdSPFCheck},
189 {"spf lookup", cmdSPFLookup},
190 {"spf parse", cmdSPFParse},
191 {"tlsrpt lookup", cmdTLSRPTLookup},
192 {"tlsrpt parsereportmsg", cmdTLSRPTParsereportmsg},
193 {"version", cmdVersion},
194 {"webapi", cmdWebapi},
196 {"example", cmdExample},
197 {"bumpuidvalidity", cmdBumpUIDValidity},
198 {"reassignuids", cmdReassignUIDs},
199 {"fixuidmeta", cmdFixUIDMeta},
200 {"fixmsgsize", cmdFixmsgsize},
201 {"reparse", cmdReparse},
202 {"ensureparsed", cmdEnsureParsed},
203 {"recalculatemailboxcounts", cmdRecalculateMailboxCounts},
204 {"message parse", cmdMessageParse},
205 {"reassignthreads", cmdReassignthreads},
208 {"helpall", cmdHelpall},
209 {"junk analyze", cmdJunkAnalyze},
210 {"junk check", cmdJunkCheck},
211 {"junk play", cmdJunkPlay},
212 {"junk test", cmdJunkTest},
213 {"junk train", cmdJunkTrain},
214 {"dmarcdb addreport", cmdDMARCDBAddReport},
215 {"tlsrptdb addreport", cmdTLSRPTDBAddReport},
216 {"updates addsigned", cmdUpdatesAddSigned},
217 {"updates genkey", cmdUpdatesGenkey},
218 {"updates pubkey", cmdUpdatesPubkey},
219 {"updates serve", cmdUpdatesServe},
220 {"updates verify", cmdUpdatesVerify},
221 {"gentestdata", cmdGentestdata},
222 {"ximport maildir", cmdXImportMaildir},
223 {"ximport mbox", cmdXImportMbox},
224 {"openaccounts", cmdOpenaccounts},
225 {"readmessages", cmdReadmessages},
226 {"queuefillretired", cmdQueueFillRetired},
232 for _, xc := range commands {
233 c := cmd{words: strings.Split(xc.cmd, " "), fn: xc.fn}
234 cmds = append(cmds, c)
242 // Set before calling command.
245 _gather bool // Set when using Parse to gather usage for a command.
247 // Set by invoked command or Parse.
248 unlisted bool // If set, command is not listed until at least some words are matched from command.
249 params string // Arguments to command. Multiple lines possible.
250 help string // Additional explanation. First line is synopsis, the rest is only printed for an explicit help/usage for that command.
256func (c *cmd) Parse() []string {
257 // To gather params and usage information, we just run the command but cause this
258 // panic after the command has registered its flags and set its params and help
259 // information. This is then caught and that info printed.
264 c.flag.Usage = c.Usage
265 c.flag.Parse(c.flagArgs)
266 c.args = c.flag.Args()
270func (c *cmd) gather() {
271 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
275 // panic generated by Parse.
283func (c *cmd) makeUsage() string {
284 var r strings.Builder
285 cs := "mox " + strings.Join(c.words, " ")
286 for i, line := range strings.Split(strings.TrimSpace(c.params), "\n") {
294 fmt.Fprintf(&r, "%6s %s%s\n", s, cs, line)
297 c.flag.PrintDefaults()
301func (c *cmd) printUsage() {
302 fmt.Fprint(os.Stderr, c.makeUsage())
304 fmt.Fprint(os.Stderr, "\n"+c.help+"\n")
308func (c *cmd) Usage() {
313func cmdHelp(c *cmd) {
314 c.params = "[command ...]"
315 c.help = `Prints help about matching commands.
317If multiple commands match, they are listed along with the first line of their help text.
318If a single command matches, its usage and full help text is printed.
325 prefix := func(l, pre []string) bool {
326 if len(pre) > len(l) {
329 return slices.Equal(pre, l[:len(pre)])
333 for _, c := range cmds {
334 if slices.Equal(c.words, args) {
336 fmt.Print(c.makeUsage())
338 fmt.Print("\n" + c.help + "\n")
341 } else if prefix(c.words, args) {
342 partial = append(partial, c)
345 if len(partial) == 0 {
346 fmt.Fprintf(os.Stderr, "%s: unknown command\n", strings.Join(args, " "))
349 for _, c := range partial {
351 line := "mox " + strings.Join(c.words, " ")
352 fmt.Printf("%s\n", line)
354 fmt.Printf("\t%s\n", strings.Split(c.help, "\n")[0])
359func cmdHelpall(c *cmd) {
361 c.help = `Print all detailed usage and help information for all listed commands.
363Used to generate documentation.
371 for _, c := range cmds {
377 fmt.Fprintf(os.Stderr, "\n")
381 fmt.Fprintf(os.Stderr, "# mox %s\n\n", strings.Join(c.words, " "))
383 fmt.Fprintln(os.Stderr, c.help+"\n")
386 s = "\t" + strings.ReplaceAll(s, "\n", "\n\t")
387 fmt.Fprintln(os.Stderr, s)
391func usage(l []cmd, unlisted bool) {
394 lines = append(lines, "mox [-config config/mox.conf] [-pedantic] ...")
396 for _, c := range l {
398 if c.unlisted && !unlisted {
401 for _, line := range strings.Split(c.params, "\n") {
402 x := append([]string{"mox"}, c.words...)
406 lines = append(lines, strings.Join(x, " "))
409 for i, line := range lines {
414 fmt.Fprintln(os.Stderr, pre+line)
422// subcommands that are not "serve" should use this function to load the config, it
423// restores any loglevel specified on the command-line, instead of using the
424// loglevels from the config file and it does not load files like TLS keys/certs.
425func mustLoadConfig() {
426 mox.MustLoadConfig(false, false)
427 if level, ok := mlog.Levels[loglevel]; loglevel != "" && ok {
428 mox.Conf.Log[""] = level
429 mlog.SetConfig(mox.Conf.Log)
430 } else if loglevel != "" && !ok {
431 log.Fatal("unknown loglevel", slog.String("loglevel", loglevel))
434 mox.SetPedantic(true)
439 // CheckConsistencyOnClose is true by default, for all the test packages. A regular
440 // mox server should never use it. But integration tests enable it again with a
442 store.CheckConsistencyOnClose = false
444 ctxbg := context.Background()
450 // If invoked as sendmail, e.g. /usr/sbin/sendmail, we do enough so cron can get a
451 // message sent using smtp submission to a configured server.
452 if len(os.Args) > 0 && filepath.Base(os.Args[0]) == "sendmail" {
454 flag: flag.NewFlagSet("sendmail", flag.ExitOnError),
455 flagArgs: os.Args[1:],
456 log: mlog.New("sendmail", nil),
462 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")
463 flag.StringVar(&loglevel, "loglevel", "", "if non-empty, this log level is set early in startup")
464 flag.BoolVar(&pedantic, "pedantic", false, "protocol violations result in errors instead of accepting/working around them")
465 flag.BoolVar(&store.CheckConsistencyOnClose, "checkconsistency", false, "dangerous option for testing only, enables data checks that abort/panic when inconsistencies are found")
467 var cpuprofile, memprofile, tracefile string
468 flag.StringVar(&cpuprofile, "cpuprof", "", "store cpu profile to file")
469 flag.StringVar(&memprofile, "memprof", "", "store mem profile to file")
470 flag.StringVar(&tracefile, "trace", "", "store execution trace to file")
472 flag.Usage = func() { usage(cmds, false) }
480 defer traceExecution(tracefile)()
482 defer profile(cpuprofile, memprofile)()
485 mox.SetPedantic(true)
488 mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
489 if level, ok := mlog.Levels[loglevel]; ok && loglevel != "" {
490 mox.Conf.Log[""] = level
491 mlog.SetConfig(mox.Conf.Log)
492 // note: SetConfig may be called again when subcommands loads config.
497 for _, c := range cmds {
498 for i, w := range c.words {
499 if i >= len(args) || w != args[i] {
501 partial = append(partial, c)
506 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
507 c.flagArgs = args[len(c.words):]
508 c.log = mlog.New(strings.Join(c.words, ""), nil)
512 if len(partial) > 0 {
518func xcheckf(err error, format string, args ...any) {
522 msg := fmt.Sprintf(format, args...)
523 log.Fatalf("%s: %s", msg, err)
526func xparseIP(s, what string) net.IP {
529 log.Fatalf("invalid %s: %q", what, s)
534func xparseDomain(s, what string) dns.Domain {
535 d, err := dns.ParseDomain(s)
536 xcheckf(err, "parsing %s %q", what, s)
540func cmdClientConfig(c *cmd) {
542 c.help = `Print the configuration for email clients for a domain.
544Sending email is typically not done on the SMTP port 25, but on submission
545ports 465 (with TLS) and 587 (without initial TLS, but usually added to the
546connection with STARTTLS). For IMAP, the port with TLS is 993 and without is
549Without TLS/STARTTLS, passwords are sent in clear text, which should only be
550configured over otherwise secured connections, like a VPN.
556 d := xparseDomain(args[0], "domain")
561func printClientConfig(d dns.Domain) {
562 cc, err := mox.ClientConfigsDomain(d)
563 xcheckf(err, "getting client config")
564 fmt.Printf("%-20s %-30s %5s %-15s %s\n", "Protocol", "Host", "Port", "Listener", "Note")
565 for _, e := range cc.Entries {
566 fmt.Printf("%-20s %-30s %5d %-15s %s\n", e.Protocol, e.Host, e.Port, e.Listener, e.Note)
569To prevent authentication mechanism downgrade attempts that may result in
570clients sending plain text passwords to a MitM, clients should always be
571explicitly configured with the most secure authentication mechanism supported,
572the first of: SCRAM-SHA-256-PLUS, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, SCRAM-SHA-1,
577func cmdConfigTest(c *cmd) {
578 c.help = `Parses and validates the configuration files.
580If valid, the command exits with status 0. If not valid, all errors encountered
588 mox.FilesImmediate = true
590 _, errs := mox.ParseConfig(context.Background(), c.log, mox.ConfigStaticPath, true, true, false)
592 log.Printf("multiple errors:")
593 for _, err := range errs {
594 log.Printf("%s", err)
597 } else if len(errs) == 1 {
598 log.Fatalf("%s", errs[0])
601 fmt.Println("config OK")
604func cmdConfigDescribeStatic(c *cmd) {
605 c.params = ">mox.conf"
606 c.help = `Prints an annotated empty configuration for use as mox.conf.
608The static configuration file cannot be reloaded while mox is running. Mox has
609to be restarted for changes to the static configuration file to take effect.
611This configuration file needs modifications to make it valid. For example, it
612may contain unfinished list items.
614 if len(c.Parse()) != 0 {
619 err := sconf.Describe(os.Stdout, &sc)
620 xcheckf(err, "describing config")
623func cmdConfigDescribeDomains(c *cmd) {
624 c.params = ">domains.conf"
625 c.help = `Prints an annotated empty configuration for use as domains.conf.
627The domains configuration file contains the domains and their configuration,
628and accounts and their configuration. This includes the configured email
629addresses. The mox admin web interface, and the mox command line interface, can
630make changes to this file. Mox automatically reloads this file when it changes.
632Like the static configuration, the example domains.conf printed by this command
633needs modifications to make it valid.
635 if len(c.Parse()) != 0 {
639 var dc config.Dynamic
640 err := sconf.Describe(os.Stdout, &dc)
641 xcheckf(err, "describing config")
644func cmdConfigPrintservice(c *cmd) {
645 c.params = ">mox.service"
646 c.help = `Prints a systemd unit service file for mox.
648This is the same file as generated using quickstart. If the systemd service file
649has changed with a newer version of mox, use this command to generate an up to
652 if len(c.Parse()) != 0 {
656 pwd, err := os.Getwd()
658 log.Printf("current working directory: %v", err)
661 service := strings.ReplaceAll(moxService, "/home/mox", pwd)
665func cmdConfigDomainAdd(c *cmd) {
666 c.params = "domain account [localpart]"
667 c.help = `Adds a new domain to the configuration and reloads the configuration.
669The account is used for the postmaster mailboxes the domain, including as DMARC and
670TLS reporting. Localpart is the "username" at the domain for this account. If
671must be set if and only if account does not yet exist.
674 if len(args) != 2 && len(args) != 3 {
678 d := xparseDomain(args[0], "domain")
680 var localpart smtp.Localpart
683 localpart, err = smtp.ParseLocalpart(args[2])
684 xcheckf(err, "parsing localpart")
686 ctlcmdConfigDomainAdd(xctl(), d, args[1], localpart)
689func ctlcmdConfigDomainAdd(ctl *ctl, domain dns.Domain, account string, localpart smtp.Localpart) {
690 ctl.xwrite("domainadd")
691 ctl.xwrite(domain.Name())
693 ctl.xwrite(string(localpart))
695 fmt.Printf("domain added, remember to add dns records, see:\n\nmox config dnsrecords %s\nmox config dnscheck %s\n", domain.Name(), domain.Name())
698func cmdConfigDomainRemove(c *cmd) {
700 c.help = `Remove a domain from the configuration and reload the configuration.
702This is a dangerous operation. Incoming email delivery for this domain will be
710 d := xparseDomain(args[0], "domain")
712 ctlcmdConfigDomainRemove(xctl(), d)
715func ctlcmdConfigDomainRemove(ctl *ctl, d dns.Domain) {
716 ctl.xwrite("domainrm")
719 fmt.Printf("domain removed, remember to remove dns records for %s\n", d)
722func cmdConfigAliasList(c *cmd) {
724 c.help = `List aliases for domain.`
731 ctlcmdConfigAliasList(xctl(), args[0])
734func ctlcmdConfigAliasList(ctl *ctl, address string) {
735 ctl.xwrite("aliaslist")
738 ctl.xstreamto(os.Stdout)
741func cmdConfigAliasPrint(c *cmd) {
743 c.help = `Print settings and members of alias.`
750 ctlcmdConfigAliasPrint(xctl(), args[0])
753func ctlcmdConfigAliasPrint(ctl *ctl, address string) {
754 ctl.xwrite("aliasprint")
757 ctl.xstreamto(os.Stdout)
760func cmdConfigAliasAdd(c *cmd) {
761 c.params = "alias@domain rcpt1@domain ..."
762 c.help = `Add new alias with one or more addresses.`
768 alias := config.Alias{Addresses: args[1:]}
771 ctlcmdConfigAliasAdd(xctl(), args[0], alias)
774func ctlcmdConfigAliasAdd(ctl *ctl, address string, alias config.Alias) {
775 ctl.xwrite("aliasadd")
777 xctlwriteJSON(ctl, alias)
781func cmdConfigAliasUpdate(c *cmd) {
782 c.params = "alias@domain [-postpublic false|true -listmembers false|true -allowmsgfrom false|true]"
783 c.help = `Update alias configuration.`
784 var postpublic, listmembers, allowmsgfrom string
785 c.flag.StringVar(&postpublic, "postpublic", "", "whether anyone or only list members can post")
786 c.flag.StringVar(&listmembers, "listmembers", "", "whether list members can list members")
787 c.flag.StringVar(&allowmsgfrom, "allowmsgfrom", "", "whether alias address can be used in message from header")
795 ctlcmdConfigAliasUpdate(xctl(), alias, postpublic, listmembers, allowmsgfrom)
798func ctlcmdConfigAliasUpdate(ctl *ctl, alias, postpublic, listmembers, allowmsgfrom string) {
799 ctl.xwrite("aliasupdate")
801 ctl.xwrite(postpublic)
802 ctl.xwrite(listmembers)
803 ctl.xwrite(allowmsgfrom)
807func cmdConfigAliasRemove(c *cmd) {
808 c.params = "alias@domain"
809 c.help = "Remove alias."
816 ctlcmdConfigAliasRemove(xctl(), args[0])
819func ctlcmdConfigAliasRemove(ctl *ctl, alias string) {
820 ctl.xwrite("aliasrm")
825func cmdConfigAliasAddaddr(c *cmd) {
826 c.params = "alias@domain rcpt1@domain ..."
827 c.help = `Add addresses to alias.`
834 ctlcmdConfigAliasAddaddr(xctl(), args[0], args[1:])
837func ctlcmdConfigAliasAddaddr(ctl *ctl, alias string, addresses []string) {
838 ctl.xwrite("aliasaddaddr")
840 xctlwriteJSON(ctl, addresses)
844func cmdConfigAliasRemoveaddr(c *cmd) {
845 c.params = "alias@domain rcpt1@domain ..."
846 c.help = `Remove addresses from alias.`
853 ctlcmdConfigAliasRmaddr(xctl(), args[0], args[1:])
856func ctlcmdConfigAliasRmaddr(ctl *ctl, alias string, addresses []string) {
857 ctl.xwrite("aliasrmaddr")
859 xctlwriteJSON(ctl, addresses)
863func cmdConfigAccountAdd(c *cmd) {
864 c.params = "account address"
865 c.help = `Add an account with an email address and reload the configuration.
867Email can be delivered to this address/account. A password has to be configured
868explicitly, see the setaccountpassword command.
876 ctlcmdConfigAccountAdd(xctl(), args[0], args[1])
879func ctlcmdConfigAccountAdd(ctl *ctl, account, address string) {
880 ctl.xwrite("accountadd")
884 fmt.Printf("account added, set a password with \"mox setaccountpassword %s\"\n", account)
887func cmdConfigAccountRemove(c *cmd) {
889 c.help = `Remove an account and reload the configuration.
891Email addresses for this account will also be removed, and incoming email for
892these addresses will be rejected.
900 ctlcmdConfigAccountRemove(xctl(), args[0])
903func ctlcmdConfigAccountRemove(ctl *ctl, account string) {
904 ctl.xwrite("accountrm")
907 fmt.Println("account removed")
910func cmdConfigAddressAdd(c *cmd) {
911 c.params = "address account"
912 c.help = `Adds an address to an account and reloads the configuration.
914If address starts with a @ (i.e. a missing localpart), this is a catchall
915address for the domain.
923 ctlcmdConfigAddressAdd(xctl(), args[0], args[1])
926func ctlcmdConfigAddressAdd(ctl *ctl, address, account string) {
927 ctl.xwrite("addressadd")
931 fmt.Println("address added")
934func cmdConfigAddressRemove(c *cmd) {
936 c.help = `Remove an address and reload the configuration.
938Incoming email for this address will be rejected after removing an address.
946 ctlcmdConfigAddressRemove(xctl(), args[0])
949func ctlcmdConfigAddressRemove(ctl *ctl, address string) {
950 ctl.xwrite("addressrm")
953 fmt.Println("address removed")
956func cmdConfigDNSRecords(c *cmd) {
958 c.help = `Prints annotated DNS records as zone file that should be created for the domain.
960The zone file can be imported into existing DNS software. You should review the
961DNS records, especially if your domain previously/currently has email
969 d := xparseDomain(args[0], "domain")
971 domConf, ok := mox.Conf.Domain(d)
973 log.Fatalf("unknown domain")
976 resolver := dns.StrictResolver{Pkg: "main"}
977 _, result, err := resolver.LookupTXT(context.Background(), d.ASCII+".")
978 if !dns.IsNotFound(err) {
979 xcheckf(err, "looking up record for dnssec-status")
982 var certIssuerDomainName, acmeAccountURI string
983 public := mox.Conf.Static.Listeners["public"]
984 if public.TLS != nil && public.TLS.ACME != "" {
985 acme, ok := mox.Conf.Static.ACME[public.TLS.ACME]
986 if ok && acme.Manager.Manager.Client != nil {
987 certIssuerDomainName = acme.IssuerDomainName
988 acc, err := acme.Manager.Manager.Client.GetReg(context.Background(), "")
989 c.log.Check(err, "get public acme account")
991 acmeAccountURI = acc.URI
996 records, err := mox.DomainRecords(domConf, d, result.Authentic, certIssuerDomainName, acmeAccountURI)
997 xcheckf(err, "records")
998 fmt.Print(strings.Join(records, "\n") + "\n")
1001func cmdConfigDNSCheck(c *cmd) {
1003 c.help = "Check the DNS records with the configuration for the domain, and print any errors/warnings."
1009 d := xparseDomain(args[0], "domain")
1011 _, ok := mox.Conf.Domain(d)
1013 log.Fatalf("unknown domain")
1016 // todo future: move http.Admin.CheckDomain to mox- and make it return a regular error.
1022 err, ok := x.(*sherpa.Error)
1026 log.Fatalf("%s", err)
1029 printResult := func(name string, r webadmin.Result) {
1030 if len(r.Errors) == 0 && len(r.Warnings) == 0 {
1033 fmt.Printf("# %s\n", name)
1034 for _, s := range r.Errors {
1035 fmt.Printf("error: %s\n", s)
1037 for _, s := range r.Warnings {
1038 fmt.Printf("warning: %s\n", s)
1042 result := webadmin.Admin{}.CheckDomain(context.Background(), args[0])
1043 printResult("DNSSEC", result.DNSSEC.Result)
1044 printResult("IPRev", result.IPRev.Result)
1045 printResult("MX", result.MX.Result)
1046 printResult("TLS", result.TLS.Result)
1047 printResult("DANE", result.DANE.Result)
1048 printResult("SPF", result.SPF.Result)
1049 printResult("DKIM", result.DKIM.Result)
1050 printResult("DMARC", result.DMARC.Result)
1051 printResult("Host TLSRPT", result.HostTLSRPT.Result)
1052 printResult("Domain TLSRPT", result.DomainTLSRPT.Result)
1053 printResult("MTASTS", result.MTASTS.Result)
1054 printResult("SRV conf", result.SRVConf.Result)
1055 printResult("Autoconf", result.Autoconf.Result)
1056 printResult("Autodiscover", result.Autodiscover.Result)
1059func cmdConfigEnsureACMEHostprivatekeys(c *cmd) {
1061 c.help = `Ensure host private keys exist for TLS listeners with ACME.
1063In mox.conf, each listener can have TLS configured. Long-lived private key files
1064can be specified, which will be used when requesting ACME certificates.
1065Configuring these private keys makes it feasible to publish DANE TLSA records
1066for the corresponding public keys in DNS, protected with DNSSEC, allowing TLS
1067certificate verification without depending on a list of Certificate Authorities
1068(CAs). Previous versions of mox did not pre-generate private keys for use with
1069ACME certificates, but would generate private keys on-demand. By explicitly
1070configuring private keys, they will not change automatedly with new
1071certificates, and the DNS TLSA records stay valid.
1073This command looks for listeners in mox.conf with TLS with ACME configured. For
1074each missing host private key (of type rsa-2048 and ecdsa-p256) a key is written
1075to config/hostkeys/. If a certificate exists in the ACME "cache", its private
1076key is copied. Otherwise a new private key is generated. Snippets for manually
1077updating/editing mox.conf are printed.
1079After running this command, and updating mox.conf, run "mox config dnsrecords"
1080for a domain and create the TLSA DNS records it suggests to enable DANE.
1087 // Load a private key from p, in various forms. We only look at the first PEM
1088 // block. Files with only a private key, or with multiple blocks but private key
1089 // first like autocert does, can be loaded.
1090 loadPrivateKey := func(f *os.File) (any, error) {
1091 buf, err := io.ReadAll(f)
1093 return nil, fmt.Errorf("reading private key file: %v", err)
1095 block, _ := pem.Decode(buf)
1097 return nil, fmt.Errorf("no pem block found in pem file")
1101 case "EC PRIVATE KEY":
1102 privKey, err = x509.ParseECPrivateKey(block.Bytes)
1103 case "RSA PRIVATE KEY":
1104 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
1106 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
1108 return nil, fmt.Errorf("unrecognized pem block type %q", block.Type)
1111 return nil, fmt.Errorf("parsing private key of type %q: %v", block.Type, err)
1116 // Either load a private key from file, or if it doesn't exist generate a new
1118 xtryLoadPrivateKey := func(kt autocert.KeyType, p string) any {
1119 f, err := os.Open(p)
1120 if err != nil && errors.Is(err, fs.ErrNotExist) {
1122 case autocert.KeyRSA2048:
1123 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1124 xcheckf(err, "generating new 2048-bit rsa private key")
1126 case autocert.KeyECDSAP256:
1127 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1128 xcheckf(err, "generating new ecdsa p-256 private key")
1131 log.Fatalf("unexpected keytype %v", kt)
1134 xcheckf(err, "%s: open acme key and certificate file", p)
1136 // Load private key from file. autocert stores a PEM file that starts with a
1137 // private key, followed by certificate(s). So we can just read it and should find
1138 // the private key we are looking for.
1139 privKey, err := loadPrivateKey(f)
1140 if xerr := f.Close(); xerr != nil {
1141 log.Printf("closing private key file: %v", xerr)
1143 xcheckf(err, "parsing private key from acme key and certificate file")
1145 switch k := privKey.(type) {
1146 case *rsa.PrivateKey:
1147 if k.N.BitLen() == 2048 {
1150 log.Printf("warning: rsa private key in %s has %d bits, skipping and generating new 2048-bit rsa private key", p, k.N.BitLen())
1151 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1152 xcheckf(err, "generating new 2048-bit rsa private key")
1154 case *ecdsa.PrivateKey:
1155 if k.Curve == elliptic.P256() {
1158 log.Printf("warning: ecdsa private key in %s has curve %v, skipping and generating new p-256 ecdsa key", p, k.Curve.Params().Name)
1159 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1160 xcheckf(err, "generating new ecdsa p-256 private key")
1163 log.Fatalf("%s: unexpected private key file of type %T", p, privKey)
1168 // Write privKey as PKCS#8 private key to p. Only if file does not yet exist.
1169 writeHostPrivateKey := func(privKey any, p string) error {
1170 os.MkdirAll(filepath.Dir(p), 0700)
1171 f, err := os.OpenFile(p, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
1173 return fmt.Errorf("create: %v", err)
1177 if err := f.Close(); err != nil {
1178 log.Printf("closing new hostkey file %s after error: %v", p, err)
1180 if err := os.Remove(p); err != nil {
1181 log.Printf("removing new hostkey file %s after error: %v", p, err)
1185 buf, err := x509.MarshalPKCS8PrivateKey(privKey)
1187 return fmt.Errorf("marshal private host key: %v", err)
1190 Type: "PRIVATE KEY",
1193 if err := pem.Encode(f, &block); err != nil {
1194 return fmt.Errorf("write as pem: %v", err)
1196 if err := f.Close(); err != nil {
1197 return fmt.Errorf("close: %v", err)
1204 timestamp := time.Now().Format("20060102T150405")
1206 for listenerName, l := range mox.Conf.Static.Listeners {
1207 if l.TLS == nil || l.TLS.ACME == "" {
1210 haveKeyTypes := map[autocert.KeyType]bool{}
1211 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
1212 p := mox.ConfigDirPath(privKeyFile)
1213 f, err := os.Open(p)
1214 xcheckf(err, "open host private key")
1215 privKey, err := loadPrivateKey(f)
1216 if err := f.Close(); err != nil {
1217 log.Printf("closing host private key file: %v", err)
1219 xcheckf(err, "loading host private key")
1220 switch k := privKey.(type) {
1221 case *rsa.PrivateKey:
1222 if k.N.BitLen() == 2048 {
1223 haveKeyTypes[autocert.KeyRSA2048] = true
1225 case *ecdsa.PrivateKey:
1226 if k.Curve == elliptic.P256() {
1227 haveKeyTypes[autocert.KeyECDSAP256] = true
1231 created := []string{}
1232 for _, kt := range []autocert.KeyType{autocert.KeyRSA2048, autocert.KeyECDSAP256} {
1233 if haveKeyTypes[kt] {
1236 // Lookup key in ACME cache.
1237 host := l.HostnameDomain
1238 if host.ASCII == "" {
1239 host = mox.Conf.Static.HostnameDomain
1241 filename := host.ASCII
1243 if kt == autocert.KeyRSA2048 {
1247 p := mox.DataDirPath(filepath.Join("acme", "keycerts", l.TLS.ACME, filename))
1248 privKey := xtryLoadPrivateKey(kt, p)
1250 relPath := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", host.Name(), timestamp, kind))
1251 destPath := mox.ConfigDirPath(relPath)
1252 err := writeHostPrivateKey(privKey, destPath)
1253 xcheckf(err, "writing host private key file to %s: %v", destPath, err)
1254 created = append(created, relPath)
1255 fmt.Printf("Wrote host private key: %s\n", destPath)
1257 didCreate = didCreate || len(created) > 0
1258 if len(created) > 0 {
1260 HostPrivateKeyFiles: append(l.TLS.HostPrivateKeyFiles, created...),
1262 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)
1263 err := sconf.Write(os.Stdout, tls)
1264 xcheckf(err, "writing new TLS.HostPrivateKeyFiles section")
1270After updating mox.conf and restarting, run "mox config dnsrecords" for a
1271domain and create the TLSA DNS records it suggests to enable DANE.
1276func cmdLoglevels(c *cmd) {
1277 c.params = "[level [pkg]]"
1278 c.help = `Print the log levels, or set a new default log level, or a level for the given package.
1280By default, a single log level applies to all logging in mox. But for each
1281"pkg", an overriding log level can be configured. Examples of packages:
1282smtpserver, smtpclient, queue, imapserver, spf, dkim, dmarc, junk, message,
1285Specify a pkg and an empty level to clear the configured level for a package.
1287Valid labels: error, info, debug, trace, traceauth, tracedata.
1296 ctlcmdLoglevels(xctl())
1302 ctlcmdSetLoglevels(xctl(), pkg, args[0])
1306func ctlcmdLoglevels(ctl *ctl) {
1307 ctl.xwrite("loglevels")
1309 ctl.xstreamto(os.Stdout)
1312func ctlcmdSetLoglevels(ctl *ctl, pkg, level string) {
1313 ctl.xwrite("setloglevels")
1319func cmdStop(c *cmd) {
1320 c.help = `Shut mox down, giving connections maximum 3 seconds to stop before closing them.
1322While shutting down, new IMAP and SMTP connections will get a status response
1323indicating temporary unavailability. Existing connections will get a 3 second
1324period to finish their transaction and shut down. Under normal circumstances,
1325only IMAP has long-living connections, with the IDLE command to get notified of
1328 if len(c.Parse()) != 0 {
1335 // Read will hang until remote has shut down.
1336 buf := make([]byte, 128)
1337 n, err := ctl.conn.Read(buf)
1339 log.Fatalf("expected eof after graceful shutdown, got data %q", buf[:n])
1340 } else if err != io.EOF {
1341 log.Fatalf("expected eof after graceful shutdown, got error %v", err)
1343 fmt.Println("mox stopped")
1346func cmdBackup(c *cmd) {
1347 c.params = "dest-dir"
1348 c.help = `Creates a backup of the data directory.
1350Backup creates consistent snapshots of the databases and message files and
1351copies other files in the data directory. Empty directories are not copied.
1352These files can then be stored elsewhere for long-term storage, or used to fall
1353back to should an upgrade fail. Simply copying files in the data directory
1354while mox is running can result in unusable database files.
1356Message files never change (they are read-only, though can be removed) and are
1357hard-linked so they don't consume additional space. If hardlinking fails, for
1358example when the backup destination directory is on a different file system, a
1359regular copy is made. Using a destination directory like "data/tmp/backup"
1360increases the odds hardlinking succeeds: the default systemd service file
1361specifically mounts the data directory, causing attempts to hardlink outside it
1362to fail with an error about cross-device linking.
1364All files in the data directory that aren't recognized (i.e. other than known
1365database files, message files, an acme directory, the "tmp" directory, etc),
1366are stored, but with a warning.
1368A clean successful backup does not print any output by default. Use the
1369-verbose flag for details, including timing.
1371To restore a backup, first shut down mox, move away the old data directory and
1372move an earlier backed up directory in its place, run "mox verifydata",
1373possibly with the "-fix" option, and restart mox. After the restore, you may
1374also want to run "mox bumpuidvalidity" for each account for which messages in a
1375mailbox changed, to force IMAP clients to synchronize mailbox state.
1377Before upgrading, to check if the upgrade will likely succeed, first make a
1378backup, then use the new mox binary to run "mox verifydata" on the backup. This
1379can change the backup files (e.g. upgrade database files, move away
1380unrecognized message files), so you should make a new backup before actually
1385 c.flag.BoolVar(&verbose, "verbose", false, "print progress")
1392 dstDataDir, err := filepath.Abs(args[0])
1393 xcheckf(err, "making path absolute")
1395 ctlcmdBackup(xctl(), dstDataDir, verbose)
1398func ctlcmdBackup(ctl *ctl, dstDataDir string, verbose bool) {
1399 ctl.xwrite("backup")
1400 ctl.xwrite(dstDataDir)
1402 ctl.xwrite("verbose")
1406 ctl.xstreamto(os.Stdout)
1410func cmdSetadminpassword(c *cmd) {
1411 c.help = `Set a new admin password, for the web interface.
1413The password is read from stdin. Its bcrypt hash is stored in a file named
1414"adminpasswd" in the configuration directory.
1416 if len(c.Parse()) != 0 {
1421 path := mox.ConfigDirPath(mox.Conf.Static.AdminPasswordFile)
1423 log.Fatal("no admin password file configured")
1426 pw := xreadpassword()
1427 pw, err := precis.OpaqueString.String(pw)
1428 xcheckf(err, `checking password with "precis" requirements`)
1429 hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
1430 xcheckf(err, "generating hash for password")
1431 err = os.WriteFile(path, hash, 0660)
1432 xcheckf(err, "writing hash to admin password file")
1435func xreadpassword() string {
1437Type new password. Password WILL echo.
1439WARNING: Bots will try to bruteforce your password. Connections with failed
1440authentication attempts will be rate limited but attackers WILL find weak
1441passwords. If your account is compromised, spammers are likely to abuse your
1442system, spamming your address and the wider internet in your name. So please
1443pick a random, unguessable password, preferably at least 12 characters.
1446 fmt.Printf("password: ")
1447 buf := make([]byte, 64)
1448 n, err := os.Stdin.Read(buf)
1449 xcheckf(err, "reading stdin")
1450 pw := string(buf[:n])
1451 pw = strings.TrimSuffix(strings.TrimSuffix(pw, "\r\n"), "\n")
1453 log.Fatal("password must be at least 8 characters")
1458func cmdSetaccountpassword(c *cmd) {
1459 c.params = "account"
1460 c.help = `Set new password an account.
1462The password is read from stdin. Secrets derived from the password, but not the
1463password itself, are stored in the account database. The stored secrets are for
1464authentication with: scram-sha-256, scram-sha-1, cram-md5, plain text (bcrypt
1467The parameter is an account name, as configured under Accounts in domains.conf
1468and as present in the data/accounts/ directory, not a configured email address
1477 pw := xreadpassword()
1479 ctlcmdSetaccountpassword(xctl(), args[0], pw)
1482func ctlcmdSetaccountpassword(ctl *ctl, account, password string) {
1483 ctl.xwrite("setaccountpassword")
1485 ctl.xwrite(password)
1489func cmdDeliver(c *cmd) {
1491 c.params = "address < message"
1492 c.help = "Deliver message to address."
1498 ctlcmdDeliver(xctl(), args[0])
1501func ctlcmdDeliver(ctl *ctl, address string) {
1502 ctl.xwrite("deliver")
1505 ctl.xstreamfrom(os.Stdin)
1508 fmt.Println("message delivered")
1510 log.Fatalf("deliver: %s", line)
1514func cmdDKIMGenrsa(c *cmd) {
1515 c.params = ">$selector._domainkey.$domain.rsa2048.privatekey.pkcs8.pem"
1516 c.help = `Generate a new 2048 bit RSA private key for use with DKIM.
1518The generated file is in PEM format, and has a comment it is generated for use
1521 if len(c.Parse()) != 0 {
1525 buf, err := mox.MakeDKIMRSAKey(dns.Domain{}, dns.Domain{})
1526 xcheckf(err, "making rsa private key")
1527 _, err = os.Stdout.Write(buf)
1528 xcheckf(err, "writing rsa private key")
1531func cmdDANEDial(c *cmd) {
1532 c.params = "host:port"
1534 c.flag.StringVar(&usages, "usages", "pkix-ta,pkix-ee,dane-ta,dane-ee", "allowed usages for dane, comma-separated list")
1535 c.help = `Dial the address using TLS with certificate verification using DANE.
1537Data is copied between connection and stdin/stdout until either side closes the
1545 allowedUsages := []adns.TLSAUsage{}
1547 for _, s := range strings.Split(usages, ",") {
1548 var usage adns.TLSAUsage
1549 switch strings.ToLower(s) {
1550 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
1551 usage = adns.TLSAUsagePKIXTA
1552 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
1553 usage = adns.TLSAUsagePKIXEE
1554 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
1555 usage = adns.TLSAUsageDANETA
1556 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
1557 usage = adns.TLSAUsageDANEEE
1559 log.Fatalf("unknown dane usage %q", s)
1561 allowedUsages = append(allowedUsages, usage)
1565 pkixRoots, err := x509.SystemCertPool()
1566 xcheckf(err, "get system pkix certificate pool")
1568 resolver := dns.StrictResolver{Pkg: "danedial"}
1569 conn, record, err := dane.Dial(context.Background(), c.log.Logger, resolver, "tcp", args[0], allowedUsages, pkixRoots)
1570 xcheckf(err, "dial")
1571 log.Printf("(connected, verified with %s)", record)
1574 _, err := io.Copy(os.Stdout, conn)
1575 xcheckf(err, "copy from connection to stdout")
1578 _, err = io.Copy(conn, os.Stdin)
1579 xcheckf(err, "copy from stdin to connection")
1582func cmdDANEDialmx(c *cmd) {
1583 c.params = "domain [destination-host]"
1584 var ehloHostname string
1585 c.flag.StringVar(&ehloHostname, "ehlohostname", "localhost", "hostname to send in smtp ehlo command")
1586 c.help = `Connect to MX server for domain using STARTTLS verified with DANE.
1588If no destination host is specified, regular delivery logic is used to find the
1589hosts to attempt delivery too. This involves following CNAMEs for the domain,
1590looking up MX records, and possibly falling back to the domain name itself as
1593If a destination host is specified, that is the only candidate host considered
1596With a list of destinations gathered, each is dialed until a successful SMTP
1597session verified with DANE has been initialized, including EHLO and STARTTLS
1600Once connected, data is copied between connection and stdin/stdout, until
1601either side closes the connection.
1603This command follows the same logic as delivery attempts made from the queue,
1604sharing most of its code.
1607 if len(args) != 1 && len(args) != 2 {
1611 ehloDomain, err := dns.ParseDomain(ehloHostname)
1612 xcheckf(err, "parsing ehlo hostname")
1614 origNextHop, err := dns.ParseDomain(args[0])
1615 xcheckf(err, "parse domain")
1617 ctxbg := context.Background()
1619 resolver := dns.StrictResolver{}
1621 var origNextHopAuthentic, expandedNextHopAuthentic bool
1622 var expandedNextHop dns.Domain
1623 var hosts []dns.IPDomain
1626 haveMX, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hosts, permanent, err = smtpclient.GatherDestinations(ctxbg, c.log.Logger, resolver, dns.IPDomain{Domain: origNextHop})
1627 status := "temporary"
1629 status = "permanent"
1632 log.Fatalf("gathering destinations: %v (%s)", err, status)
1634 if expandedNextHop != origNextHop {
1635 log.Printf("followed cnames to %s", expandedNextHop)
1638 log.Printf("found mx record, trying mx hosts")
1640 log.Printf("no mx record found, will try to connect to domain directly")
1642 if !origNextHopAuthentic {
1643 log.Fatalf("error: initial domain not dnssec-secure")
1645 if !expandedNextHopAuthentic {
1646 log.Fatalf("error: expanded domain not dnssec-secure")
1650 for _, h := range hosts {
1651 l = append(l, h.String())
1653 log.Printf("destinations: %s", strings.Join(l, ", "))
1655 d, err := dns.ParseDomain(args[1])
1657 log.Fatalf("parsing destination host: %v", err)
1659 log.Printf("skipping domain mx/cname lookups, assuming domain is dnssec-protected")
1661 origNextHopAuthentic = true
1662 expandedNextHopAuthentic = true
1664 hosts = []dns.IPDomain{{Domain: d}}
1667 dialedIPs := map[string][]net.IP{}
1668 for _, host := range hosts {
1669 // It should not be possible for hosts to have IP addresses: They are not
1670 // allowed by dns.ParseDomain, and MX records cannot contain them.
1672 log.Fatalf("unexpected IP address for destination host")
1675 log.Printf("attempting to connect to %s", host)
1677 authentic, expandedAuthentic, expandedHost, ips, _, err := smtpclient.GatherIPs(ctxbg, c.log.Logger, resolver, "ip", host, dialedIPs)
1679 log.Printf("resolving ips for %s: %v, skipping", host, err)
1683 log.Printf("no dnssec for ips of %s, skipping", host)
1686 if !expandedAuthentic {
1687 log.Printf("no dnssec for cname-followed ips of %s, skipping", host)
1690 if expandedHost != host.Domain {
1691 log.Printf("host %s cname-expanded to %s", host, expandedHost)
1693 log.Printf("host %s resolved to ips %s, looking up tlsa records", host, ips)
1695 daneRequired, daneRecords, tlsaBaseDomain, err := smtpclient.GatherTLSA(ctxbg, c.log.Logger, resolver, host.Domain, expandedAuthentic, expandedHost)
1697 log.Printf("looking up tlsa records: %s, skipping", err)
1700 tlsMode := smtpclient.TLSRequiredStartTLS
1701 if len(daneRecords) == 0 {
1703 log.Printf("host %s has no tlsa records, skipping", expandedHost)
1706 log.Printf("warning: only unusable tlsa records found, continuing with required tls without certificate verification")
1710 for _, r := range daneRecords {
1711 l = append(l, r.String())
1713 log.Printf("tlsa records: %s", strings.Join(l, "; "))
1716 tlsHostnames := smtpclient.GatherTLSANames(haveMX, expandedNextHopAuthentic, expandedAuthentic, origNextHop, expandedNextHop, host.Domain, tlsaBaseDomain)
1718 for _, name := range tlsHostnames {
1719 l = append(l, name.String())
1721 log.Printf("gathered valid tls certificate names for potential verification with dane-ta: %s", strings.Join(l, ", "))
1723 dialer := &net.Dialer{Timeout: 5 * time.Second}
1724 conn, _, err := smtpclient.Dial(ctxbg, c.log.Logger, dialer, dns.IPDomain{Domain: expandedHost}, ips, 25, dialedIPs, nil)
1726 log.Printf("dial %s: %v, skipping", expandedHost, err)
1729 log.Printf("connected to %s, %s, starting smtp session with ehlo and starttls with dane verification", expandedHost, conn.RemoteAddr())
1731 var verifiedRecord adns.TLSA
1732 opts := smtpclient.Opts{
1733 DANERecords: daneRecords,
1734 DANEMoreHostnames: tlsHostnames[1:],
1735 DANEVerifiedRecord: &verifiedRecord,
1736 RootCAs: mox.Conf.Static.TLS.CertPool,
1739 sc, err := smtpclient.New(ctxbg, c.log.Logger, conn, tlsMode, tlsPKIX, ehloDomain, tlsHostnames[0], opts)
1741 log.Printf("setting up smtp session: %v, skipping", err)
1746 smtpConn, err := sc.Conn()
1748 log.Fatalf("error: taking over smtp connection: %s", err)
1750 log.Printf("tls verified with tlsa record: %s", verifiedRecord)
1751 log.Printf("smtp session initialized and connected to stdin/stdout")
1754 _, err := io.Copy(os.Stdout, smtpConn)
1755 xcheckf(err, "copy from connection to stdout")
1758 _, err = io.Copy(smtpConn, os.Stdin)
1759 xcheckf(err, "copy from stdin to connection")
1762 log.Fatalf("no remaining destinations")
1765func cmdDANEMakeRecord(c *cmd) {
1766 c.params = "usage selector matchtype [certificate.pem | publickey.pem | privatekey.pem]"
1767 c.help = `Print TLSA record for given certificate/key and parameters.
1770- usage: pkix-ta (0), pkix-ee (1), dane-ta (2), dane-ee (3)
1771- selector: cert (0), spki (1)
1772- matchtype: full (0), sha2-256 (1), sha2-512 (2)
1774Common DANE TLSA record parameters are: dane-ee spki sha2-256, or 3 1 1,
1775followed by a sha2-256 hash of the DER-encoded "SPKI" (subject public key info)
1776from the certificate. An example DNS zone file entry:
1778 _25._tcp.example.com. TLSA 3 1 1 133b919c9d65d8b1488157315327334ead8d83372db57465ecabf53ee5748aee
1780The first usable information from the pem file is used to compose the TLSA
1781record. In case of selector "cert", a certificate is required. Otherwise the
1782"subject public key info" (spki) of the first certificate or public or private
1783key (pkcs#8, pkcs#1 or ec private key) is used.
1791 var usage adns.TLSAUsage
1792 switch strings.ToLower(args[0]) {
1793 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
1794 usage = adns.TLSAUsagePKIXTA
1795 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
1796 usage = adns.TLSAUsagePKIXEE
1797 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
1798 usage = adns.TLSAUsageDANETA
1799 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
1800 usage = adns.TLSAUsageDANEEE
1802 if v, err := strconv.ParseUint(args[0], 10, 16); err != nil {
1803 log.Fatalf("bad usage %q", args[0])
1805 // Does not influence certificate association data, so we can accept other numbers.
1806 log.Printf("warning: continuing with unrecognized tlsa usage %d", v)
1807 usage = adns.TLSAUsage(v)
1811 var selector adns.TLSASelector
1812 switch strings.ToLower(args[1]) {
1813 case "cert", strconv.Itoa(int(adns.TLSASelectorCert)):
1814 selector = adns.TLSASelectorCert
1815 case "spki", strconv.Itoa(int(adns.TLSASelectorSPKI)):
1816 selector = adns.TLSASelectorSPKI
1818 log.Fatalf("bad selector %q", args[1])
1821 var matchType adns.TLSAMatchType
1822 switch strings.ToLower(args[2]) {
1823 case "full", strconv.Itoa(int(adns.TLSAMatchTypeFull)):
1824 matchType = adns.TLSAMatchTypeFull
1825 case "sha2-256", strconv.Itoa(int(adns.TLSAMatchTypeSHA256)):
1826 matchType = adns.TLSAMatchTypeSHA256
1827 case "sha2-512", strconv.Itoa(int(adns.TLSAMatchTypeSHA512)):
1828 matchType = adns.TLSAMatchTypeSHA512
1830 log.Fatalf("bad matchtype %q", args[2])
1833 buf, err := os.ReadFile(args[3])
1834 xcheckf(err, "reading certificate")
1836 var block *pem.Block
1837 block, buf = pem.Decode(buf)
1841 extra = " (with leftover data from pem file)"
1843 if selector == adns.TLSASelectorCert {
1844 log.Fatalf("no certificate found in pem file%s", extra)
1846 log.Fatalf("no certificate or public or private key found in pem file%s", extra)
1849 var cert *x509.Certificate
1851 if block.Type == "CERTIFICATE" {
1852 cert, err = x509.ParseCertificate(block.Bytes)
1853 xcheckf(err, "parse certificate")
1855 case adns.TLSASelectorCert:
1857 case adns.TLSASelectorSPKI:
1858 data = cert.RawSubjectPublicKeyInfo
1860 } else if selector == adns.TLSASelectorCert {
1861 // We need a certificate, just a public/private key won't do.
1862 log.Printf("skipping pem type %q, certificate is required", block.Type)
1865 var privKey, pubKey any
1869 _, err := x509.ParsePKIXPublicKey(block.Bytes)
1870 xcheckf(err, "parse pkix subject public key info (spki)")
1872 case "EC PRIVATE KEY":
1873 privKey, err = x509.ParseECPrivateKey(block.Bytes)
1874 xcheckf(err, "parse ec private key")
1875 case "RSA PRIVATE KEY":
1876 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
1877 xcheckf(err, "parse pkcs#1 rsa private key")
1878 case "RSA PUBLIC KEY":
1879 pubKey, err = x509.ParsePKCS1PublicKey(block.Bytes)
1880 xcheckf(err, "parse pkcs#1 rsa public key")
1882 // PKCS#8 private key
1883 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
1884 xcheckf(err, "parse pkcs#8 private key")
1886 log.Printf("skipping unrecognized pem type %q", block.Type)
1890 if pubKey == nil && privKey != nil {
1891 if signer, ok := privKey.(crypto.Signer); !ok {
1892 log.Fatalf("private key of type %T is not a signer, cannot get public key", privKey)
1894 pubKey = signer.Public()
1898 // Should not happen.
1899 log.Fatalf("internal error: did not find private or public key")
1901 data, err = x509.MarshalPKIXPublicKey(pubKey)
1902 xcheckf(err, "marshal pkix subject public key info (spki)")
1907 case adns.TLSAMatchTypeFull:
1908 case adns.TLSAMatchTypeSHA256:
1909 p := sha256.Sum256(data)
1911 case adns.TLSAMatchTypeSHA512:
1912 p := sha512.Sum512(data)
1915 fmt.Printf("%d %d %d %x\n", usage, selector, matchType, data)
1920func cmdDNSLookup(c *cmd) {
1921 c.params = "[ptr | mx | cname | ips | a | aaaa | ns | txt | srv | tlsa] name"
1922 c.help = `Lookup DNS name of given type.
1924Lookup always prints whether the response was DNSSEC-protected.
1928mox dns lookup ptr 1.1.1.1
1929mox dns lookup mx xmox.nl
1930mox dns lookup txt _dmarc.xmox.nl.
1931mox dns lookup tlsa _25._tcp.xmox.nl
1939 resolver := dns.StrictResolver{Pkg: "dns"}
1941 // like xparseDomain, but treat unparseable domain as an ASCII name so names with
1942 // underscores are still looked up, e,g <selector>._domainkey.<host>.
1943 xdomain := func(s string) dns.Domain {
1944 d, err := dns.ParseDomain(s)
1946 return dns.Domain{ASCII: strings.TrimSuffix(s, ".")}
1951 cmd, name := args[0], args[1]
1955 ip := xparseIP(name, "ip")
1956 ptrs, result, err := resolver.LookupAddr(context.Background(), ip.String())
1958 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
1960 fmt.Printf("names (%d, %s):\n", len(ptrs), dnssecStatus(result.Authentic))
1961 for _, ptr := range ptrs {
1962 fmt.Printf("- %s\n", ptr)
1966 name := xdomain(name)
1967 mxl, result, err := resolver.LookupMX(context.Background(), name.ASCII+".")
1969 log.Printf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
1970 // We can still have valid records...
1972 fmt.Printf("mx records (%d, %s):\n", len(mxl), dnssecStatus(result.Authentic))
1973 for _, mx := range mxl {
1974 fmt.Printf("- %s, preference %d\n", mx.Host, mx.Pref)
1978 name := xdomain(name)
1979 target, result, err := resolver.LookupCNAME(context.Background(), name.ASCII+".")
1981 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
1983 fmt.Printf("%s (%s)\n", target, dnssecStatus(result.Authentic))
1985 case "ips", "a", "aaaa":
1989 } else if cmd == "aaaa" {
1992 name := xdomain(name)
1993 ips, result, err := resolver.LookupIP(context.Background(), network, name.ASCII+".")
1995 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
1997 fmt.Printf("records (%d, %s):\n", len(ips), dnssecStatus(result.Authentic))
1998 for _, ip := range ips {
1999 fmt.Printf("- %s\n", ip)
2003 name := xdomain(name)
2004 nsl, result, err := resolver.LookupNS(context.Background(), name.ASCII+".")
2006 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2008 fmt.Printf("ns records (%d, %s):\n", len(nsl), dnssecStatus(result.Authentic))
2009 for _, ns := range nsl {
2010 fmt.Printf("- %s\n", ns)
2014 host := xdomain(name)
2015 l, result, err := resolver.LookupTXT(context.Background(), host.ASCII+".")
2017 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2019 fmt.Printf("txt records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2020 for _, txt := range l {
2021 fmt.Printf("- %s\n", txt)
2025 host := xdomain(name)
2026 _, l, result, err := resolver.LookupSRV(context.Background(), "", "", host.ASCII+".")
2028 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2030 fmt.Printf("srv records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2031 for _, srv := range l {
2032 fmt.Printf("- host %s, port %d, priority %d, weight %d\n", srv.Target, srv.Port, srv.Priority, srv.Weight)
2036 host := xdomain(name)
2037 l, result, err := resolver.LookupTLSA(context.Background(), 0, "", host.ASCII+".")
2039 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2041 fmt.Printf("tlsa records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2042 for _, tlsa := range l {
2043 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)
2046 log.Fatalf("unknown record type %q", args[0])
2050func cmdDKIMGened25519(c *cmd) {
2051 c.params = ">$selector._domainkey.$domain.ed25519.privatekey.pkcs8.pem"
2052 c.help = `Generate a new ed25519 key for use with DKIM.
2054Ed25519 keys are much smaller than RSA keys of comparable cryptographic
2055strength. This is convenient because of maximum DNS message sizes. At the time
2056of writing, not many mail servers appear to support ed25519 DKIM keys though,
2057so it is recommended to sign messages with both RSA and ed25519 keys.
2059 if len(c.Parse()) != 0 {
2063 buf, err := mox.MakeDKIMEd25519Key(dns.Domain{}, dns.Domain{})
2064 xcheckf(err, "making dkim ed25519 key")
2065 _, err = os.Stdout.Write(buf)
2066 xcheckf(err, "writing dkim ed25519 key")
2069func cmdDKIMTXT(c *cmd) {
2070 c.params = "<$selector._domainkey.$domain.key.pkcs8.pem"
2071 c.help = `Print a DKIM DNS TXT record with the public key derived from the private key read from stdin.
2073The DNS should be configured as a TXT record at $selector._domainkey.$domain.
2075 if len(c.Parse()) != 0 {
2079 privKey, err := parseDKIMKey(os.Stdin)
2080 xcheckf(err, "reading dkim private key from stdin")
2084 Hashes: []string{"sha256"},
2085 Flags: []string{"s"},
2088 switch key := privKey.(type) {
2089 case *rsa.PrivateKey:
2090 r.PublicKey = key.Public()
2091 case ed25519.PrivateKey:
2092 r.PublicKey = key.Public()
2095 log.Fatalf("unsupported private key type %T, must be rsa or ed25519", privKey)
2098 record, err := r.Record()
2099 xcheckf(err, "making record")
2100 fmt.Print("<selector>._domainkey.<your.domain.> TXT ")
2104 s, record = record[:100], record[100:]
2108 fmt.Printf(`"%s" `, s)
2113func parseDKIMKey(r io.Reader) (any, error) {
2114 buf, err := io.ReadAll(r)
2116 return nil, fmt.Errorf("reading pem from stdin: %v", err)
2118 b, _ := pem.Decode(buf)
2120 return nil, fmt.Errorf("decoding pem: %v", err)
2122 privKey, err := x509.ParsePKCS8PrivateKey(b.Bytes)
2124 return nil, fmt.Errorf("parsing private key: %v", err)
2129func cmdDKIMVerify(c *cmd) {
2130 c.params = "message"
2131 c.help = `Verify the DKIM signatures in a message and print the results.
2133The message is parsed, and the DKIM-Signature headers are validated. Validation
2134of older messages may fail because the DNS records have been removed or changed
2135by now, or because the signature header may have specified an expiration time
2143 msgf, err := os.Open(args[0])
2144 xcheckf(err, "open message")
2146 results, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, false, dkim.DefaultPolicy, msgf, true)
2147 xcheckf(err, "dkim verify")
2149 for _, result := range results {
2151 if result.Sig == nil {
2152 log.Printf("warning: could not parse signature")
2154 sigh, err = result.Sig.Header()
2156 log.Printf("warning: packing signature: %s", err)
2160 if result.Record == nil {
2161 log.Printf("warning: missing DNS record")
2163 txt, err = result.Record.Record()
2165 log.Printf("warning: packing record: %s", err)
2168 fmt.Printf("status %q, err %v\nrecord %q\nheader %s\n", result.Status, result.Err, txt, sigh)
2172func cmdDKIMSign(c *cmd) {
2173 c.params = "message"
2174 c.help = `Sign a message, adding DKIM-Signature headers based on the domain in the From header.
2176The message is parsed, the domain looked up in the configuration files, and
2177DKIM-Signature headers generated. The message is printed with the DKIM-Signature
2185 msgf, err := os.Open(args[0])
2186 xcheckf(err, "open message")
2189 p, err := message.Parse(c.log.Logger, true, msgf)
2190 xcheckf(err, "parsing message")
2192 if len(p.Envelope.From) != 1 {
2193 log.Fatalf("found %d from headers, need exactly 1", len(p.Envelope.From))
2195 localpart, err := smtp.ParseLocalpart(p.Envelope.From[0].User)
2196 xcheckf(err, "parsing localpart of address in from-header")
2197 dom, err := dns.ParseDomain(p.Envelope.From[0].Host)
2198 xcheckf(err, "parsing domain of address in from-header")
2202 domConf, ok := mox.Conf.Domain(dom)
2204 log.Fatalf("domain %s not configured", dom)
2207 selectors := mox.DKIMSelectors(domConf.DKIM)
2208 headers, err := dkim.Sign(context.Background(), c.log.Logger, localpart, dom, selectors, false, msgf)
2209 xcheckf(err, "signing message with dkim")
2211 log.Fatalf("no DKIM configured for domain %s", dom)
2213 _, err = fmt.Fprint(os.Stdout, headers)
2214 xcheckf(err, "write headers")
2215 _, err = io.Copy(os.Stdout, msgf)
2216 xcheckf(err, "write message")
2219func cmdDKIMLookup(c *cmd) {
2220 c.params = "selector domain"
2221 c.help = "Lookup and print the DKIM record for the selector at the domain."
2227 selector := xparseDomain(args[0], "selector")
2228 domain := xparseDomain(args[1], "domain")
2230 status, record, txt, authentic, err := dkim.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, selector, domain)
2232 fmt.Printf("error: %s\n", err)
2234 if status != dkim.StatusNeutral {
2235 fmt.Printf("status: %s\n", status)
2238 fmt.Printf("TXT record: %s\n", txt)
2241 fmt.Println("dnssec-signed: yes")
2243 fmt.Println("dnssec-signed: no")
2246 fmt.Printf("Record:\n")
2248 "version", record.Version,
2249 "hashes", record.Hashes,
2251 "notes", record.Notes,
2252 "services", record.Services,
2253 "flags", record.Flags,
2255 for i := 0; i < len(pairs); i += 2 {
2256 fmt.Printf("\t%s: %v\n", pairs[i], pairs[i+1])
2261func cmdDMARCLookup(c *cmd) {
2263 c.help = "Lookup dmarc policy for domain, a DNS TXT record at _dmarc.<domain>, validate and print it."
2269 fromdomain := xparseDomain(args[0], "domain")
2270 _, domain, _, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, fromdomain)
2271 xcheckf(err, "dmarc lookup domain %s", fromdomain)
2272 fmt.Printf("dmarc record at domain %s: %s\n", domain, txt)
2273 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2276func dnssecStatus(v bool) string {
2278 return "with dnssec"
2280 return "without dnssec"
2283func cmdDMARCVerify(c *cmd) {
2284 c.params = "remoteip mailfromaddress helodomain < message"
2285 c.help = `Parse an email message and evaluate it against the DMARC policy of the domain in the From-header.
2287mailfromaddress and helodomain are used for SPF validation. If both are empty,
2288SPF validation is skipped.
2290mailfromaddress should be the address used as MAIL FROM in the SMTP session.
2291For DSN messages, that address may be empty. The helo domain was specified at
2292the beginning of the SMTP transaction that delivered the message. These values
2293can be found in message headers.
2300 var heloDomain *dns.Domain
2302 remoteIP := xparseIP(args[0], "remoteip")
2304 var mailfrom *smtp.Address
2306 a, err := smtp.ParseAddress(args[1])
2307 xcheckf(err, "parsing mailfrom address")
2311 d := xparseDomain(args[2], "helo domain")
2314 var received *spf.Received
2315 spfStatus := spf.StatusNone
2316 var spfIdentity *dns.Domain
2317 if mailfrom != nil || heloDomain != nil {
2318 spfArgs := spf.Args{
2320 LocalIP: net.ParseIP("127.0.0.1"),
2321 LocalHostname: dns.Domain{ASCII: "localhost"},
2323 if mailfrom != nil {
2324 spfArgs.MailFromLocalpart = mailfrom.Localpart
2325 spfArgs.MailFromDomain = mailfrom.Domain
2327 if heloDomain != nil {
2328 spfArgs.HelloDomain = dns.IPDomain{Domain: *heloDomain}
2330 rspf, spfDomain, expl, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfArgs)
2332 log.Printf("spf verify: %v (explanation: %q, authentic %v)", err, expl, authentic)
2335 spfStatus = received.Result
2336 // todo: should probably potentially do two separate spf validations
2337 if mailfrom != nil {
2338 spfIdentity = &mailfrom.Domain
2340 spfIdentity = heloDomain
2342 fmt.Printf("spf result: %s: %s (%s)\n", spfDomain, spfStatus, dnssecStatus(authentic))
2346 data, err := io.ReadAll(os.Stdin)
2347 xcheckf(err, "read message")
2348 dmarcFrom, _, _, err := message.From(c.log.Logger, false, bytes.NewReader(data), nil)
2349 xcheckf(err, "extract dmarc from message")
2351 const ignoreTestMode = false
2352 dkimResults, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, true, func(*dkim.Sig) error { return nil }, bytes.NewReader(data), ignoreTestMode)
2353 xcheckf(err, "dkim verify")
2354 for _, r := range dkimResults {
2355 fmt.Printf("dkim result: %q (err %v)\n", r.Status, r.Err)
2358 _, result := dmarc.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, dmarcFrom.Domain, dkimResults, spfStatus, spfIdentity, false)
2359 xcheckf(result.Err, "dmarc verify")
2360 fmt.Printf("dmarc from: %s\ndmarc status: %q\ndmarc reject: %v\ncmarc record: %s\n", dmarcFrom, result.Status, result.Reject, result.Record)
2363func cmdDMARCCheckreportaddrs(c *cmd) {
2365 c.help = `For each reporting address in the domain's DMARC record, check if it has opted into receiving reports (if needed).
2367A DMARC record can request reports about DMARC evaluations to be sent to an
2368email/http address. If the organizational domains of that of the DMARC record
2369and that of the report destination address do not match, the destination
2370address must opt-in to receiving DMARC reports by creating a DMARC record at
2371<dmarcdomain>._report._dmarc.<reportdestdomain>.
2378 dom := xparseDomain(args[0], "domain")
2379 _, domain, record, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dom)
2380 xcheckf(err, "dmarc lookup domain %s", dom)
2381 fmt.Printf("dmarc record at domain %s: %q\n", domain, txt)
2382 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2384 check := func(kind, addr string) {
2387 printResult := func(format string, args ...any) {
2388 fmt.Printf("%s %s: %s (%s)\n", kind, addr, fmt.Sprintf(format, args...), dnssecStatus(authentic))
2391 u, err := url.Parse(addr)
2393 printResult("parsing uri: %v (skipping)", addr, err)
2396 var destdom dns.Domain
2399 a, err := smtp.ParseAddress(u.Opaque)
2401 printResult("parsing destination email address %s: %v (skipping)", u.Opaque, err)
2406 printResult("unrecognized scheme in reporting address %s (skipping)", u.Scheme)
2410 if publicsuffix.Lookup(context.Background(), c.log.Logger, dom) == publicsuffix.Lookup(context.Background(), c.log.Logger, destdom) {
2411 printResult("pass (same organizational domain)")
2415 accepts, status, _, txts, authentic, err := dmarc.LookupExternalReportsAccepted(context.Background(), c.log.Logger, dns.StrictResolver{}, domain, destdom)
2417 txtaddr := fmt.Sprintf("%s._report._dmarc.%s", domain.ASCII, destdom.ASCII)
2419 txtstr = fmt.Sprintf(" (no txt records %s)", txtaddr)
2421 txtstr = fmt.Sprintf(" (txt record %s: %q)", txtaddr, txts)
2423 if status != dmarc.StatusNone {
2424 printResult("fail: %s%s", err, txtstr)
2426 printResult("pass%s", txtstr)
2427 } else if err != nil {
2428 printResult("fail: %s%s", err, txtstr)
2430 printResult("fail%s", txtstr)
2434 for _, uri := range record.AggregateReportAddresses {
2435 check("aggregate reporting", uri.Address)
2437 for _, uri := range record.FailureReportAddresses {
2438 check("failure reporting", uri.Address)
2442func cmdDMARCParsereportmsg(c *cmd) {
2443 c.params = "message ..."
2444 c.help = `Parse a DMARC report from an email message, and print its extracted details.
2446DMARC reports are periodically mailed, if requested in the DMARC DNS record of
2447a domain. Reports are sent by mail servers that received messages with our
2448domain in a From header. This may or may not be legatimate email. DMARC reports
2449contain summaries of evaluations of DMARC and DKIM/SPF, which can help
2450understand email deliverability problems.
2457 for _, arg := range args {
2458 f, err := os.Open(arg)
2459 xcheckf(err, "open %q", arg)
2460 feedback, err := dmarcrpt.ParseMessageReport(c.log.Logger, f)
2461 xcheckf(err, "parse report in %q", arg)
2462 meta := feedback.ReportMetadata
2463 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)
2464 if len(meta.Errors) > 0 {
2465 fmt.Printf("Errors:\n")
2466 for _, s := range meta.Errors {
2467 fmt.Printf("\t- %s\n", s)
2470 pol := feedback.PolicyPublished
2471 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)
2472 for _, record := range feedback.Records {
2473 idents := record.Identifiers
2474 fmt.Printf("\theaderfrom %q, envelopes from %q, to %q\n", idents.HeaderFrom, idents.EnvelopeFrom, idents.EnvelopeTo)
2475 eval := record.Row.PolicyEvaluated
2477 for _, reason := range eval.Reasons {
2478 reasons += "; " + string(reason.Type)
2479 if reason.Comment != "" {
2480 reasons += fmt.Sprintf(": %q", reason.Comment)
2483 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)
2484 for _, dkim := range record.AuthResults.DKIM {
2486 if dkim.HumanResult != "" {
2487 result = fmt.Sprintf(": %q", dkim.HumanResult)
2489 fmt.Printf("\t\tdkim %s; domain %q selector %q%s\n", dkim.Result, dkim.Domain, dkim.Selector, result)
2491 for _, spf := range record.AuthResults.SPF {
2492 fmt.Printf("\t\tspf %s; domain %q scope %q\n", spf.Result, spf.Domain, spf.Scope)
2498func cmdDMARCDBAddReport(c *cmd) {
2500 c.params = "fromdomain < message"
2501 c.help = "Add a DMARC report to the database."
2509 fromdomain := xparseDomain(args[0], "domain")
2510 fmt.Fprintln(os.Stderr, "reading report message from stdin")
2511 report, err := dmarcrpt.ParseMessageReport(c.log.Logger, os.Stdin)
2512 xcheckf(err, "parse message")
2513 err = dmarcdb.AddReport(context.Background(), report, fromdomain)
2514 xcheckf(err, "add dmarc report")
2517func cmdTLSRPTLookup(c *cmd) {
2519 c.help = `Lookup the TLSRPT record for the domain.
2521A TLSRPT record typically contains an email address where reports about TLS
2522connectivity should be sent. Mail servers attempting delivery to our domain
2523should attempt to use TLS. TLSRPT lets them report how many connection
2524successfully used TLS, and how what kind of errors occurred otherwise.
2531 d := xparseDomain(args[0], "domain")
2532 _, txt, err := tlsrpt.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, d)
2533 xcheckf(err, "tlsrpt lookup for %s", d)
2537func cmdTLSRPTParsereportmsg(c *cmd) {
2538 c.params = "message ..."
2539 c.help = `Parse and print the TLSRPT in the message.
2541The report is printed in formatted JSON.
2548 for _, arg := range args {
2549 f, err := os.Open(arg)
2550 xcheckf(err, "open %q", arg)
2551 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, f)
2552 xcheckf(err, "parse report in %q", arg)
2553 // todo future: only print the highlights?
2554 enc := json.NewEncoder(os.Stdout)
2555 enc.SetIndent("", "\t")
2556 enc.SetEscapeHTML(false)
2557 err = enc.Encode(reportJSON)
2558 xcheckf(err, "write report")
2562func cmdSPFCheck(c *cmd) {
2563 c.params = "domain ip"
2564 c.help = `Check the status of IP for the policy published in DNS for the domain.
2566IPs may be allowed to send for a domain, or disallowed, and several shades in
2567between. If not allowed, an explanation may be provided by the policy. If so,
2568the explanation is printed. The SPF mechanism that matched (if any) is also
2576 domain := xparseDomain(args[0], "domain")
2578 ip := xparseIP(args[1], "ip")
2580 spfargs := spf.Args{
2582 MailFromLocalpart: "user",
2583 MailFromDomain: domain,
2584 HelloDomain: dns.IPDomain{Domain: domain},
2585 LocalIP: net.ParseIP("127.0.0.1"),
2586 LocalHostname: dns.Domain{ASCII: "localhost"},
2588 r, _, explanation, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfargs)
2590 fmt.Printf("error: %s\n", err)
2592 if explanation != "" {
2593 fmt.Printf("explanation: %s\n", explanation)
2595 fmt.Printf("status: %s (%s)\n", r.Result, dnssecStatus(authentic))
2596 if r.Mechanism != "" {
2597 fmt.Printf("mechanism: %s\n", r.Mechanism)
2601func cmdSPFParse(c *cmd) {
2602 c.params = "txtrecord"
2603 c.help = "Parse the record as SPF record. If valid, nothing is printed."
2609 _, _, err := spf.ParseRecord(args[0])
2610 xcheckf(err, "parsing record")
2613func cmdSPFLookup(c *cmd) {
2615 c.help = "Lookup the SPF record for the domain and print it."
2621 domain := xparseDomain(args[0], "domain")
2622 _, txt, _, authentic, err := spf.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
2623 xcheckf(err, "spf lookup for %s", domain)
2625 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2628func cmdMTASTSLookup(c *cmd) {
2630 c.help = `Lookup the MTASTS record and policy for the domain.
2632MTA-STS is a mechanism for a domain to specify if it requires TLS connections
2633for delivering email. If a domain has a valid MTA-STS DNS TXT record at
2634_mta-sts.<domain> it signals it implements MTA-STS. A policy can then be
2635fetched at https://mta-sts.<domain>/.well-known/mta-sts.txt. The policy
2636specifies the mode (enforce, testing, none), which MX servers support TLS and
2637should be used, and how long the policy can be cached.
2644 domain := xparseDomain(args[0], "domain")
2646 record, policy, _, err := mtasts.Get(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
2648 fmt.Printf("error: %s\n", err)
2651 fmt.Printf("DNS TXT record _mta-sts.%s: %s\n", domain.ASCII, record.String())
2655 fmt.Printf("policy at https://mta-sts.%s/.well-known/mta-sts.txt:\n", domain.ASCII)
2656 fmt.Printf("%s", policy.String())
2660func cmdRetrain(c *cmd) {
2661 c.params = "accountname"
2662 c.help = `Recreate and retrain the junk filter for the account.
2664Useful after having made changes to the junk filter configuration, or if the
2665implementation has changed.
2673 ctlcmdRetrain(xctl(), args[0])
2676func ctlcmdRetrain(ctl *ctl, account string) {
2677 ctl.xwrite("retrain")
2682func cmdTLSRPTDBAddReport(c *cmd) {
2684 c.params = "< message"
2685 c.help = "Parse a TLS report from the message and add it to the database."
2687 c.flag.BoolVar(&hostReport, "hostreport", false, "report for a host instead of domain")
2695 // First read message, to get the From-header. Then parse it as TLSRPT.
2696 fmt.Fprintln(os.Stderr, "reading report message from stdin")
2697 buf, err := io.ReadAll(os.Stdin)
2698 xcheckf(err, "reading message")
2699 part, err := message.Parse(c.log.Logger, true, bytes.NewReader(buf))
2700 xcheckf(err, "parsing message")
2701 if part.Envelope == nil || len(part.Envelope.From) != 1 {
2702 log.Fatalf("message must have one From-header")
2704 from := part.Envelope.From[0]
2705 domain := xparseDomain(from.Host, "domain")
2707 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, bytes.NewReader(buf))
2708 xcheckf(err, "parsing tls report in message")
2710 mailfrom := from.User + "@" + from.Host // todo future: should escape and such
2711 report := reportJSON.Convert()
2712 err = tlsrptdb.AddReport(context.Background(), c.log, domain, mailfrom, hostReport, &report)
2713 xcheckf(err, "add tls report to database")
2716func cmdDNSBLCheck(c *cmd) {
2717 c.params = "zone ip"
2718 c.help = `Test if IP is in the DNS blocklist of the zone, e.g. bl.spamcop.net.
2720If the IP is in the blocklist, an explanation is printed. This is typically a
2721URL with more information.
2728 zone := xparseDomain(args[0], "zone")
2729 ip := xparseIP(args[1], "ip")
2731 status, explanation, err := dnsbl.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, zone, ip)
2732 fmt.Printf("status: %s\n", status)
2733 if status == dnsbl.StatusFail {
2734 fmt.Printf("explanation: %q\n", explanation)
2737 fmt.Printf("error: %s\n", err)
2741func cmdDNSBLCheckhealth(c *cmd) {
2743 c.help = `Check the health of the DNS blocklist represented by zone, e.g. bl.spamcop.net.
2745The health of a DNS blocklist can be checked by querying for 127.0.0.1 and
2746127.0.0.2. The second must and the first must not be present.
2753 zone := xparseDomain(args[0], "zone")
2754 err := dnsbl.CheckHealth(context.Background(), c.log.Logger, dns.StrictResolver{}, zone)
2755 xcheckf(err, "unhealthy")
2756 fmt.Println("healthy")
2759func cmdCheckupdate(c *cmd) {
2760 c.help = `Check if a newer version of mox is available.
2762A single DNS TXT lookup to _updates.xmox.nl tells if a new version is
2763available. If so, a changelog is fetched from https://updates.xmox.nl, and the
2764individual entries verified with a builtin public key. The changelog is
2767 if len(c.Parse()) != 0 {
2772 current, lastknown, _, err := mox.LastKnown()
2774 log.Printf("getting last known version: %s", err)
2776 fmt.Printf("last known version: %s\n", lastknown)
2777 fmt.Printf("current version: %s\n", current)
2779 latest, _, err := updates.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dns.Domain{ASCII: changelogDomain})
2780 xcheckf(err, "lookup of latest version")
2781 fmt.Printf("latest version: %s\n", latest)
2783 if latest.After(current) {
2784 changelog, err := updates.FetchChangelog(context.Background(), c.log.Logger, changelogURL, current, changelogPubKey)
2785 xcheckf(err, "fetching changelog")
2786 if len(changelog.Changes) == 0 {
2787 log.Printf("no changes in changelog")
2790 fmt.Println("Changelog")
2791 for _, c := range changelog.Changes {
2792 fmt.Println("\n" + strings.TrimSpace(c.Text))
2797func cmdCid(c *cmd) {
2799 c.help = `Turn an ID from a Received header into a cid, for looking up in logs.
2801A cid is essentially a connection counter initialized when mox starts. Each log
2802line contains a cid. Received headers added by mox contain a unique ID that can
2803be decrypted to a cid by admin of a mox instance only.
2811 recvidpath := mox.DataDirPath("receivedid.key")
2812 recvidbuf, err := os.ReadFile(recvidpath)
2813 xcheckf(err, "reading %s", recvidpath)
2814 if len(recvidbuf) != 16+8 {
2815 log.Fatalf("bad data in %s: got %d bytes, expect 16+8=24", recvidpath, len(recvidbuf))
2817 err = mox.ReceivedIDInit(recvidbuf[:16], recvidbuf[16:])
2818 xcheckf(err, "init receivedid")
2820 cid, err := mox.ReceivedToCid(args[0])
2821 xcheckf(err, "received id to cid")
2822 fmt.Printf("%x\n", cid)
2825func cmdVersion(c *cmd) {
2826 c.help = "Prints this mox version."
2827 if len(c.Parse()) != 0 {
2830 fmt.Println(moxvar.Version)
2831 fmt.Printf("%s %s/%s\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
2834func cmdWebapi(c *cmd) {
2835 c.params = "[method [baseurl-with-credentials]"
2836 c.help = "Lists available methods, prints request/response parameters for method, or calls a method with a request read from standard input."
2842 t := reflect.TypeOf((*webapi.Methods)(nil)).Elem()
2843 methods := map[string]reflect.Type{}
2845 for i := 0; i < t.NumMethod(); i++ {
2847 methods[mt.Name] = mt.Type
2848 ml = append(ml, mt.Name)
2852 fmt.Println(strings.Join(ml, "\n"))
2856 mt, ok := methods[args[0]]
2858 log.Fatalf("unknown method %q", args[0])
2860 resultNotJSON := mt.Out(0).Kind() == reflect.Interface
2863 fmt.Println("# Example request")
2865 printJSON("\t", mox.FillExample(nil, reflect.New(mt.In(1))).Interface())
2868 fmt.Println("Output is non-JSON data.")
2871 fmt.Println("# Example response")
2873 printJSON("\t", mox.FillExample(nil, reflect.New(mt.Out(0))).Interface())
2879 response = reflect.New(mt.Out(0))
2882 fmt.Fprintln(os.Stderr, "reading request from stdin...")
2883 request, err := io.ReadAll(os.Stdin)
2884 xcheckf(err, "read message")
2886 dec := json.NewDecoder(bytes.NewReader(request))
2887 dec.DisallowUnknownFields()
2888 err = dec.Decode(reflect.New(mt.In(1)).Interface())
2889 xcheckf(err, "parsing request")
2891 resp, err := http.PostForm(args[1]+args[0], url.Values{"request": []string{string(request)}})
2892 xcheckf(err, "http post")
2893 defer resp.Body.Close()
2894 if resp.StatusCode == http.StatusBadRequest {
2895 buf, err := io.ReadAll(&moxio.LimitReader{R: resp.Body, Limit: 10 * 1024})
2896 xcheckf(err, "reading response for 400 bad request error")
2897 err = json.Unmarshal(buf, &response)
2899 printJSON("", response)
2901 fmt.Fprintf(os.Stderr, "(not json)\n")
2902 os.Stderr.Write(buf)
2905 } else if resp.StatusCode != http.StatusOK {
2906 fmt.Fprintf(os.Stderr, "http response %s\n", resp.Status)
2907 _, err := io.Copy(os.Stderr, resp.Body)
2908 xcheckf(err, "copy body")
2910 err := json.NewDecoder(resp.Body).Decode(&resp)
2911 xcheckf(err, "unmarshal response")
2912 printJSON("", response)
2916func printJSON(indent string, v any) {
2917 fmt.Printf("%s", indent)
2918 enc := json.NewEncoder(os.Stdout)
2919 enc.SetIndent(indent, "\t")
2920 enc.SetEscapeHTML(false)
2921 err := enc.Encode(v)
2922 xcheckf(err, "encode json")
2925// 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.
2926func cmdBumpUIDValidity(c *cmd) {
2927 c.params = "account [mailbox]"
2928 c.help = `Change the IMAP UID validity of the mailbox, causing IMAP clients to refetch messages.
2930This can be useful after manually repairing metadata about the account/mailbox.
2932Opens account database file directly. Ensure mox does not have the account
2933open, or is not running.
2936 if len(args) != 1 && len(args) != 2 {
2941 a, err := store.OpenAccount(c.log, args[0])
2942 xcheckf(err, "open account")
2944 if err := a.Close(); err != nil {
2945 log.Printf("closing account: %v", err)
2949 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
2950 uidvalidity, err := a.NextUIDValidity(tx)
2952 return fmt.Errorf("assigning next uid validity: %v", err)
2955 q := bstore.QueryTx[store.Mailbox](tx)
2957 q.FilterEqual("Name", args[1])
2959 mbl, err := q.SortAsc("Name").List()
2961 return fmt.Errorf("looking up mailbox: %v", err)
2963 if len(args) == 2 && len(mbl) != 1 {
2964 return fmt.Errorf("looking up mailbox %q, found %d mailboxes", args[1], len(mbl))
2966 for _, mb := range mbl {
2967 mb.UIDValidity = uidvalidity
2968 err = tx.Update(&mb)
2970 return fmt.Errorf("updating uid validity for mailbox: %v", err)
2972 fmt.Printf("uid validity for %q updated to %d\n", mb.Name, uidvalidity)
2976 xcheckf(err, "updating database")
2979func cmdReassignUIDs(c *cmd) {
2980 c.params = "account [mailboxid]"
2981 c.help = `Reassign UIDs in one mailbox or all mailboxes in an account and bump UID validity, causing IMAP clients to refetch messages.
2983Opens account database file directly. Ensure mox does not have the account
2984open, or is not running.
2987 if len(args) != 1 && len(args) != 2 {
2994 mailboxID, err = strconv.ParseInt(args[1], 10, 64)
2995 xcheckf(err, "parsing mailbox id")
2999 a, err := store.OpenAccount(c.log, args[0])
3000 xcheckf(err, "open account")
3002 if err := a.Close(); err != nil {
3003 log.Printf("closing account: %v", err)
3007 // Gather the last-assigned UIDs per mailbox.
3008 uidlasts := map[int64]store.UID{}
3010 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3011 // Reassign UIDs, going per mailbox. We assign starting at 1, only changing the
3012 // message if it isn't already at the intended UID. Doing it in this order ensures
3013 // we don't get into trouble with duplicate UIDs for a mailbox. We assign a new
3014 // modseq. Not strictly needed, for doesn't hurt.
3015 modseq, err := a.NextModSeq(tx)
3016 xcheckf(err, "assigning next modseq")
3018 q := bstore.QueryTx[store.Message](tx)
3020 q.FilterNonzero(store.Message{MailboxID: mailboxID})
3022 q.SortAsc("MailboxID", "UID")
3023 err = q.ForEach(func(m store.Message) error {
3024 uidlasts[m.MailboxID]++
3025 uid := uidlasts[m.MailboxID]
3029 if err := tx.Update(&m); err != nil {
3030 return fmt.Errorf("updating uid for message: %v", err)
3036 return fmt.Errorf("reading through messages: %v", err)
3039 // Now update the uidnext and uidvalidity for each mailbox.
3040 err = bstore.QueryTx[store.Mailbox](tx).ForEach(func(mb store.Mailbox) error {
3041 // Assign each mailbox a completely new uidvalidity.
3042 uidvalidity, err := a.NextUIDValidity(tx)
3044 return fmt.Errorf("assigning next uid validity: %v", err)
3047 if mb.UIDValidity >= uidvalidity {
3048 // This should not happen, but since we're fixing things up after a hypothetical
3049 // mishap, might as well account for inconsistent uidvalidity.
3050 next := store.NextUIDValidity{ID: 1, Next: mb.UIDValidity + 2}
3051 if err := tx.Update(&next); err != nil {
3052 log.Printf("updating nextuidvalidity: %v, continuing", err)
3056 mb.UIDValidity = uidvalidity
3058 mb.UIDNext = uidlasts[mb.ID] + 1
3059 if err := tx.Update(&mb); err != nil {
3060 return fmt.Errorf("updating uidvalidity and uidnext for mailbox: %v", err)
3065 return fmt.Errorf("updating mailboxes: %v", err)
3069 xcheckf(err, "updating database")
3072func cmdFixUIDMeta(c *cmd) {
3073 c.params = "account"
3074 c.help = `Fix inconsistent UIDVALIDITY and UIDNEXT in messages/mailboxes/account.
3076The next UID to use for a message in a mailbox should always be higher than any
3077existing message UID in the mailbox. If it is not, the mailbox UIDNEXT is
3080Each mailbox has a UIDVALIDITY sequence number, which should always be lower
3081than the per-account next UIDVALIDITY to use. If it is not, the account next
3082UIDVALIDITY is updated.
3084Opens account database file directly. Ensure mox does not have the account
3085open, or is not running.
3093 a, err := store.OpenAccount(c.log, args[0])
3094 xcheckf(err, "open account")
3096 if err := a.Close(); err != nil {
3097 log.Printf("closing account: %v", err)
3101 var maxUIDValidity uint32
3103 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3104 // We look at each mailbox, retrieve its max UID and compare against the mailbox
3106 err := bstore.QueryTx[store.Mailbox](tx).ForEach(func(mb store.Mailbox) error {
3107 if mb.UIDValidity > maxUIDValidity {
3108 maxUIDValidity = mb.UIDValidity
3110 m, err := bstore.QueryTx[store.Message](tx).FilterNonzero(store.Message{MailboxID: mb.ID}).SortDesc("UID").Limit(1).Get()
3111 if err == bstore.ErrAbsent || err == nil && m.UID < mb.UIDNext {
3113 } else if err != nil {
3114 return fmt.Errorf("finding message with max uid in mailbox: %w", err)
3116 olduidnext := mb.UIDNext
3117 mb.UIDNext = m.UID + 1
3118 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)
3119 if err := tx.Update(&mb); err != nil {
3120 return fmt.Errorf("updating mailbox uidnext: %v", err)
3125 return fmt.Errorf("processing mailboxes: %v", err)
3128 uidvalidity := store.NextUIDValidity{ID: 1}
3129 if err := tx.Get(&uidvalidity); err != nil {
3130 return fmt.Errorf("reading account next uidvalidity: %v", err)
3132 if maxUIDValidity >= uidvalidity.Next {
3133 log.Printf("account next uidvalidity %d <= highest uidvalidity %d found in mailbox, resetting account next uidvalidity to %d", uidvalidity.Next, maxUIDValidity, maxUIDValidity+1)
3134 uidvalidity.Next = maxUIDValidity + 1
3135 if err := tx.Update(&uidvalidity); err != nil {
3136 return fmt.Errorf("updating account next uidvalidity: %v", err)
3142 xcheckf(err, "updating database")
3145func cmdFixmsgsize(c *cmd) {
3146 c.params = "[account]"
3147 c.help = `Ensure message sizes in the database matching the sum of the message prefix length and on-disk file size.
3149Messages with an inconsistent size are also parsed again.
3151If an inconsistency is found, you should probably also run "mox
3152bumpuidvalidity" on the mailboxes or entire account to force IMAP clients to
3165 ctlcmdFixmsgsize(xctl(), account)
3168func ctlcmdFixmsgsize(ctl *ctl, account string) {
3169 ctl.xwrite("fixmsgsize")
3172 ctl.xstreamto(os.Stdout)
3175func cmdReparse(c *cmd) {
3176 c.params = "[account]"
3177 c.help = `Parse all messages in the account or all accounts again.
3179Can be useful after upgrading mox with improved message parsing. Messages are
3180parsed in batches, so other access to the mailboxes/messages are not blocked
3181while reparsing all messages.
3193 ctlcmdReparse(xctl(), account)
3196func ctlcmdReparse(ctl *ctl, account string) {
3197 ctl.xwrite("reparse")
3200 ctl.xstreamto(os.Stdout)
3203func cmdEnsureParsed(c *cmd) {
3204 c.params = "account"
3205 c.help = "Ensure messages in the database have a pre-parsed MIME form in the database."
3207 c.flag.BoolVar(&all, "all", false, "store new parsed message for all messages")
3214 a, err := store.OpenAccount(c.log, args[0])
3215 xcheckf(err, "open account")
3217 if err := a.Close(); err != nil {
3218 log.Printf("closing account: %v", err)
3223 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3224 q := bstore.QueryTx[store.Message](tx)
3225 q.FilterEqual("Expunged", false)
3226 q.FilterFn(func(m store.Message) bool {
3227 return all || m.ParsedBuf == nil
3231 return fmt.Errorf("list messages: %v", err)
3233 for _, m := range l {
3234 mr := a.MessageReader(m)
3235 p, err := message.EnsurePart(c.log.Logger, false, mr, m.Size)
3237 log.Printf("parsing message %d: %v (continuing)", m.ID, err)
3239 m.ParsedBuf, err = json.Marshal(p)
3241 return fmt.Errorf("marshal parsed message: %v", err)
3243 if err := tx.Update(&m); err != nil {
3244 return fmt.Errorf("update message: %v", err)
3250 xcheckf(err, "update messages with parsed mime structure")
3251 fmt.Printf("%d messages updated\n", n)
3254func cmdRecalculateMailboxCounts(c *cmd) {
3255 c.params = "account"
3256 c.help = `Recalculate message counts for all mailboxes in the account, and total message size for quota.
3258When a message is added to/removed from a mailbox, or when message flags change,
3259the total, unread, unseen and deleted messages are accounted, the total size of
3260the mailbox, and the total message size for the account. In case of a bug in
3261this accounting, the numbers could become incorrect. This command will find, fix
3270 ctlcmdRecalculateMailboxCounts(xctl(), args[0])
3273func ctlcmdRecalculateMailboxCounts(ctl *ctl, account string) {
3274 ctl.xwrite("recalculatemailboxcounts")
3277 ctl.xstreamto(os.Stdout)
3280func cmdMessageParse(c *cmd) {
3281 c.params = "message.eml"
3282 c.help = "Parse message, print JSON representation."
3285 c.flag.BoolVar(&smtputf8, "smtputf8", false, "check if message needs smtputf8")
3291 f, err := os.Open(args[0])
3292 xcheckf(err, "open")
3295 part, err := message.Parse(c.log.Logger, false, f)
3296 xcheckf(err, "parsing message")
3297 err = part.Walk(c.log.Logger, nil)
3298 xcheckf(err, "parsing nested parts")
3299 enc := json.NewEncoder(os.Stdout)
3300 enc.SetIndent("", "\t")
3301 enc.SetEscapeHTML(false)
3302 err = enc.Encode(part)
3303 xcheckf(err, "write")
3305 hasNonASCII := func(r io.Reader) bool {
3306 br := bufio.NewReader(r)
3308 b, err := br.ReadByte()
3312 xcheckf(err, "read header")
3320 var walk func(p *message.Part) bool
3321 walk = func(p *message.Part) bool {
3322 if hasNonASCII(p.HeaderReader()) {
3325 for _, pp := range p.Parts {
3333 fmt.Println("message needs smtputf8:", walk(&part))
3337func cmdOpenaccounts(c *cmd) {
3339 c.params = "datadir account ..."
3340 c.help = `Open and close accounts, for triggering data upgrades, for tests.
3342Opens database files directly, not going through a running mox instance.
3350 dataDir := filepath.Clean(args[0])
3351 for _, accName := range args[1:] {
3352 accDir := filepath.Join(dataDir, "accounts", accName)
3353 log.Printf("opening account %s...", accDir)
3354 a, err := store.OpenAccountDB(c.log, accDir, accName)
3355 xcheckf(err, "open account %s", accName)
3356 err = a.ThreadingWait(c.log)
3357 xcheckf(err, "wait for threading upgrade to complete for %s", accName)
3359 xcheckf(err, "close account %s", accName)
3363func cmdReassignthreads(c *cmd) {
3364 c.params = "[account]"
3365 c.help = `Reassign message threads.
3367For all accounts, or optionally only the specified account.
3369Threading for all messages in an account is first reset, and new base subject
3370and normalized message-id saved with the message. Then all messages are
3371evaluated and matched against their parents/ancestors.
3373Messages are matched based on the References header, with a fall-back to an
3374In-Reply-To header, and if neither is present/valid, based only on base
3377A References header typically points to multiple previous messages in a
3378hierarchy. From oldest ancestor to most recent parent. An In-Reply-To header
3379would have only a message-id of the parent message.
3381A message is only linked to a parent/ancestor if their base subject is the
3382same. This ensures unrelated replies, with a new subject, are placed in their
3385The base subject is lower cased, has whitespace collapsed to a single
3386space, and some components removed: leading "Re:", "Fwd:", "Fw:", or bracketed
3387tag (that mailing lists often add, e.g. "[listname]"), trailing "(fwd)", or
3388enclosing "[fwd: ...]".
3390Messages are linked to all their ancestors. If an intermediate parent/ancestor
3391message is deleted in the future, the message can still be linked to the earlier
3392ancestors. If the direct parent already wasn't available while matching, this is
3393stored as the message having a "missing link" to its stored ancestors.
3405 ctlcmdReassignthreads(xctl(), account)
3408func ctlcmdReassignthreads(ctl *ctl, account string) {
3409 ctl.xwrite("reassignthreads")
3412 ctl.xstreamto(os.Stdout)
3415func cmdReadmessages(c *cmd) {
3417 c.params = "datadir account ..."
3418 c.help = `Open account, parse several headers for all messages.
3420For performance testing.
3422Opens database files directly, not going through a running mox instance.
3425 gomaxprocs := runtime.GOMAXPROCS(0)
3426 var procs, workqueuesize, limit int
3427 c.flag.IntVar(&procs, "procs", gomaxprocs, "number of goroutines for reading messages")
3428 c.flag.IntVar(&workqueuesize, "workqueuesize", 2*gomaxprocs, "number of messages to keep in work queue")
3429 c.flag.IntVar(&limit, "limit", 0, "number of messages to process if greater than zero")
3435 type threadPrep struct {
3440 threadingFields := [][]byte{
3441 []byte("references"),
3442 []byte("in-reply-to"),
3445 dataDir := filepath.Clean(args[0])
3446 for _, accName := range args[1:] {
3447 accDir := filepath.Join(dataDir, "accounts", accName)
3448 log.Printf("opening account %s...", accDir)
3449 a, err := store.OpenAccountDB(c.log, accDir, accName)
3450 xcheckf(err, "open account %s", accName)
3452 prepareMessages := func(in, out chan moxio.Work[store.Message, threadPrep]) {
3453 headerbuf := make([]byte, 8*1024)
3454 scratch := make([]byte, 4*1024)
3462 var partialPart struct {
3466 if err := json.Unmarshal(m.ParsedBuf, &partialPart); err != nil {
3467 w.Err = fmt.Errorf("unmarshal part: %v", err)
3469 size := partialPart.BodyOffset - partialPart.HeaderOffset
3470 if int(size) > len(headerbuf) {
3471 headerbuf = make([]byte, size)
3474 buf := headerbuf[:int(size)]
3475 err := func() error {
3476 mr := a.MessageReader(m)
3479 // ReadAt returns whole buffer or error. Single read should be fast.
3480 n, err := mr.ReadAt(buf, partialPart.HeaderOffset)
3481 if err != nil || n != len(buf) {
3482 return fmt.Errorf("read header: %v", err)
3488 } else if h, err := message.ParseHeaderFields(buf, scratch, threadingFields); err != nil {
3491 w.Out.references = h["References"]
3492 w.Out.inReplyTo = h["In-Reply-To"]
3505 processMessage := func(m store.Message, prep threadPrep) error {
3507 log.Printf("%d messages (delta %s)", n, time.Since(t))
3514 wq := moxio.NewWorkQueue[store.Message, threadPrep](procs, workqueuesize, prepareMessages, processMessage)
3516 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3517 q := bstore.QueryTx[store.Message](tx)
3518 q.FilterEqual("Expunged", false)
3523 err = q.ForEach(wq.Add)
3531 xcheckf(err, "processing message")
3534 xcheckf(err, "close account %s", accName)
3535 log.Printf("account %s, total time %s", accName, time.Since(t0))
3539func cmdQueueFillRetired(c *cmd) {
3541 c.help = `Fill retired messag and webhooks queue with testdata.
3543For testing the pagination. Operates directly on queue database.
3546 c.flag.IntVar(&n, "n", 10000, "retired messages and retired webhooks to insert")
3554 xcheckf(err, "init queue")
3555 err = queue.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3558 // Cause autoincrement ID for queue.Msg to be forwarded, and use the reserved ID
3559 // space for inserting retired messages.
3561 err = tx.Insert(&fm)
3562 xcheckf(err, "temporarily insert message to get autoincrement sequence")
3563 err = tx.Delete(&fm)
3564 xcheckf(err, "removing temporary message for resetting autoincrement sequence")
3566 err = tx.Insert(&fm)
3567 xcheckf(err, "temporarily insert message to forward autoincrement sequence")
3568 err = tx.Delete(&fm)
3569 xcheckf(err, "removing temporary message after forwarding autoincrement sequence")
3572 // And likewise for webhooks.
3573 fh := queue.Hook{Account: "x", URL: "x", NextAttempt: time.Now()}
3574 err = tx.Insert(&fh)
3575 xcheckf(err, "temporarily insert webhook to get autoincrement sequence")
3576 err = tx.Delete(&fh)
3577 xcheckf(err, "removing temporary webhook for resetting autoincrement sequence")
3579 err = tx.Insert(&fh)
3580 xcheckf(err, "temporarily insert webhook to forward autoincrement sequence")
3581 err = tx.Delete(&fh)
3582 xcheckf(err, "removing temporary webhook after forwarding autoincrement sequence")
3585 for i := 0; i < n; i++ {
3586 t0 := now.Add(-time.Duration(i) * time.Second)
3587 last := now.Add(-time.Duration(i/10) * time.Second)
3588 mr := queue.MsgRetired{
3589 ID: fm.ID + int64(i),
3591 SenderAccount: "test",
3592 SenderLocalpart: "mox",
3593 SenderDomainStr: "localhost",
3594 FromID: fmt.Sprintf("%016d", i),
3595 RecipientLocalpart: "mox",
3596 RecipientDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "localhost"}},
3597 RecipientDomainStr: "localhost",
3600 Results: []queue.MsgResult{
3603 Duration: time.Millisecond,
3610 Size: int64(i * 100),
3611 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
3612 Subject: fmt.Sprintf("test message %d", i),
3613 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
3615 RecipientAddress: "mox@localhost",
3617 KeepUntil: now.Add(48 * time.Hour),
3619 err := tx.Insert(&mr)
3620 xcheckf(err, "inserting retired message")
3623 for i := 0; i < n; i++ {
3624 t0 := now.Add(-time.Duration(i) * time.Second)
3625 last := now.Add(-time.Duration(i/10) * time.Second)
3630 hr := queue.HookRetired{
3631 ID: fh.ID + int64(i),
3632 QueueMsgID: fm.ID + int64(i),
3633 FromID: fmt.Sprintf("%016d", i),
3634 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
3635 Subject: fmt.Sprintf("test message %d", i),
3636 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
3638 URL: "http://localhost/hook",
3639 IsIncoming: i%10 == 0,
3640 OutgoingEvent: event,
3645 Results: []queue.HookResult{
3648 Duration: time.Millisecond,
3649 URL: "http://localhost/hook",
3658 KeepUntil: now.Add(48 * time.Hour),
3660 err := tx.Insert(&hr)
3661 xcheckf(err, "inserting retired hook")
3666 xcheckf(err, "add to queue")
3667 log.Printf("added %d retired messages and %d retired webhooks", n, n)