11 cryptorand "crypto/rand"
40 "golang.org/x/crypto/bcrypt"
41 "golang.org/x/text/secure/precis"
43 "github.com/mjl-/adns"
45 "github.com/mjl-/autocert"
46 "github.com/mjl-/bstore"
47 "github.com/mjl-/sconf"
48 "github.com/mjl-/sherpa"
50 "github.com/mjl-/mox/admin"
51 "github.com/mjl-/mox/config"
52 "github.com/mjl-/mox/dane"
53 "github.com/mjl-/mox/dkim"
54 "github.com/mjl-/mox/dmarc"
55 "github.com/mjl-/mox/dmarcdb"
56 "github.com/mjl-/mox/dmarcrpt"
57 "github.com/mjl-/mox/dns"
58 "github.com/mjl-/mox/dnsbl"
59 "github.com/mjl-/mox/message"
60 "github.com/mjl-/mox/mlog"
61 "github.com/mjl-/mox/mox-"
62 "github.com/mjl-/mox/moxio"
63 "github.com/mjl-/mox/moxvar"
64 "github.com/mjl-/mox/mtasts"
65 "github.com/mjl-/mox/publicsuffix"
66 "github.com/mjl-/mox/queue"
67 "github.com/mjl-/mox/rdap"
68 "github.com/mjl-/mox/smtp"
69 "github.com/mjl-/mox/smtpclient"
70 "github.com/mjl-/mox/spf"
71 "github.com/mjl-/mox/store"
72 "github.com/mjl-/mox/tlsrpt"
73 "github.com/mjl-/mox/tlsrptdb"
74 "github.com/mjl-/mox/updates"
75 "github.com/mjl-/mox/webadmin"
76 "github.com/mjl-/mox/webapi"
80 changelogDomain = "xmox.nl"
81 changelogURL = "https://updates.xmox.nl/changelog"
82 changelogPubKey = base64Decode("sPNiTDQzvb4FrytNEiebJhgyQzn57RwEjNbGWMM/bDY=")
85func base64Decode(s string) []byte {
86 buf, err := base64.StdEncoding.DecodeString(s)
93func envString(k, def string) string {
101var commands = []struct {
106 {"quickstart", cmdQuickstart},
108 {"setaccountpassword", cmdSetaccountpassword},
109 {"setadminpassword", cmdSetadminpassword},
110 {"loglevels", cmdLoglevels},
111 {"queue holdrules list", cmdQueueHoldrulesList},
112 {"queue holdrules add", cmdQueueHoldrulesAdd},
113 {"queue holdrules remove", cmdQueueHoldrulesRemove},
114 {"queue list", cmdQueueList},
115 {"queue hold", cmdQueueHold},
116 {"queue unhold", cmdQueueUnhold},
117 {"queue schedule", cmdQueueSchedule},
118 {"queue transport", cmdQueueTransport},
119 {"queue requiretls", cmdQueueRequireTLS},
120 {"queue fail", cmdQueueFail},
121 {"queue drop", cmdQueueDrop},
122 {"queue dump", cmdQueueDump},
123 {"queue retired list", cmdQueueRetiredList},
124 {"queue retired print", cmdQueueRetiredPrint},
125 {"queue suppress list", cmdQueueSuppressList},
126 {"queue suppress add", cmdQueueSuppressAdd},
127 {"queue suppress remove", cmdQueueSuppressRemove},
128 {"queue suppress lookup", cmdQueueSuppressLookup},
129 {"queue webhook list", cmdQueueHookList},
130 {"queue webhook schedule", cmdQueueHookSchedule},
131 {"queue webhook cancel", cmdQueueHookCancel},
132 {"queue webhook print", cmdQueueHookPrint},
133 {"queue webhook retired list", cmdQueueHookRetiredList},
134 {"queue webhook retired print", cmdQueueHookRetiredPrint},
135 {"import maildir", cmdImportMaildir},
136 {"import mbox", cmdImportMbox},
137 {"export maildir", cmdExportMaildir},
138 {"export mbox", cmdExportMbox},
139 {"localserve", cmdLocalserve},
141 {"backup", cmdBackup},
142 {"verifydata", cmdVerifydata},
143 {"licenses", cmdLicenses},
145 {"config test", cmdConfigTest},
146 {"config dnscheck", cmdConfigDNSCheck},
147 {"config dnsrecords", cmdConfigDNSRecords},
148 {"config describe-domains", cmdConfigDescribeDomains},
149 {"config describe-static", cmdConfigDescribeStatic},
150 {"config account list", cmdConfigAccountList},
151 {"config account add", cmdConfigAccountAdd},
152 {"config account rm", cmdConfigAccountRemove},
153 {"config account disable", cmdConfigAccountDisable},
154 {"config account enable", cmdConfigAccountEnable},
155 {"config address add", cmdConfigAddressAdd},
156 {"config address rm", cmdConfigAddressRemove},
157 {"config domain add", cmdConfigDomainAdd},
158 {"config domain rm", cmdConfigDomainRemove},
159 {"config domain disable", cmdConfigDomainDisable},
160 {"config domain enable", cmdConfigDomainEnable},
161 {"config tlspubkey list", cmdConfigTlspubkeyList},
162 {"config tlspubkey get", cmdConfigTlspubkeyGet},
163 {"config tlspubkey add", cmdConfigTlspubkeyAdd},
164 {"config tlspubkey rm", cmdConfigTlspubkeyRemove},
165 {"config tlspubkey gen", cmdConfigTlspubkeyGen},
166 {"config alias list", cmdConfigAliasList},
167 {"config alias print", cmdConfigAliasPrint},
168 {"config alias add", cmdConfigAliasAdd},
169 {"config alias update", cmdConfigAliasUpdate},
170 {"config alias rm", cmdConfigAliasRemove},
171 {"config alias addaddr", cmdConfigAliasAddaddr},
172 {"config alias rmaddr", cmdConfigAliasRemoveaddr},
174 {"config describe-sendmail", cmdConfigDescribeSendmail},
175 {"config printservice", cmdConfigPrintservice},
176 {"config ensureacmehostprivatekeys", cmdConfigEnsureACMEHostprivatekeys},
177 {"config example", cmdConfigExample},
179 {"admin imapserve", cmdIMAPServe},
181 {"checkupdate", cmdCheckupdate},
183 {"clientconfig", cmdClientConfig},
184 {"deliver", cmdDeliver},
185 // 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
186 {"dane dial", cmdDANEDial},
187 {"dane dialmx", cmdDANEDialmx},
188 {"dane makerecord", cmdDANEMakeRecord},
189 {"dns lookup", cmdDNSLookup},
190 {"dkim gened25519", cmdDKIMGened25519},
191 {"dkim genrsa", cmdDKIMGenrsa},
192 {"dkim lookup", cmdDKIMLookup},
193 {"dkim txt", cmdDKIMTXT},
194 {"dkim verify", cmdDKIMVerify},
195 {"dkim sign", cmdDKIMSign},
196 {"dmarc lookup", cmdDMARCLookup},
197 {"dmarc parsereportmsg", cmdDMARCParsereportmsg},
198 {"dmarc verify", cmdDMARCVerify},
199 {"dmarc checkreportaddrs", cmdDMARCCheckreportaddrs},
200 {"dnsbl check", cmdDNSBLCheck},
201 {"dnsbl checkhealth", cmdDNSBLCheckhealth},
202 {"mtasts lookup", cmdMTASTSLookup},
203 {"rdap domainage", cmdRDAPDomainage},
204 {"retrain", cmdRetrain},
205 {"sendmail", cmdSendmail},
206 {"smtp dial", cmdSMTPDial},
207 {"spf check", cmdSPFCheck},
208 {"spf lookup", cmdSPFLookup},
209 {"spf parse", cmdSPFParse},
210 {"tlsrpt lookup", cmdTLSRPTLookup},
211 {"tlsrpt parsereportmsg", cmdTLSRPTParsereportmsg},
212 {"version", cmdVersion},
213 {"webapi", cmdWebapi},
215 {"example", cmdExample},
216 {"bumpuidvalidity", cmdBumpUIDValidity},
217 {"reassignuids", cmdReassignUIDs},
218 {"fixuidmeta", cmdFixUIDMeta},
219 {"fixmsgsize", cmdFixmsgsize},
220 {"reparse", cmdReparse},
221 {"ensureparsed", cmdEnsureParsed},
222 {"recalculatemailboxcounts", cmdRecalculateMailboxCounts},
223 {"message parse", cmdMessageParse},
224 {"reassignthreads", cmdReassignthreads},
227 {"helpall", cmdHelpall},
228 {"junk analyze", cmdJunkAnalyze},
229 {"junk check", cmdJunkCheck},
230 {"junk play", cmdJunkPlay},
231 {"junk test", cmdJunkTest},
232 {"junk train", cmdJunkTrain},
233 {"dmarcdb addreport", cmdDMARCDBAddReport},
234 {"tlsrptdb addreport", cmdTLSRPTDBAddReport},
235 {"updates addsigned", cmdUpdatesAddSigned},
236 {"updates genkey", cmdUpdatesGenkey},
237 {"updates pubkey", cmdUpdatesPubkey},
238 {"updates serve", cmdUpdatesServe},
239 {"updates verify", cmdUpdatesVerify},
240 {"gentestdata", cmdGentestdata},
241 {"ximport maildir", cmdXImportMaildir},
242 {"ximport mbox", cmdXImportMbox},
243 {"openaccounts", cmdOpenaccounts},
244 {"readmessages", cmdReadmessages},
245 {"queuefillretired", cmdQueueFillRetired},
251 for _, xc := range commands {
252 c := cmd{words: strings.Split(xc.cmd, " "), fn: xc.fn}
253 cmds = append(cmds, c)
261 // Set before calling command.
264 _gather bool // Set when using Parse to gather usage for a command.
266 // Set by invoked command or Parse.
267 unlisted bool // If set, command is not listed until at least some words are matched from command.
268 params string // Arguments to command. Multiple lines possible.
269 help string // Additional explanation. First line is synopsis, the rest is only printed for an explicit help/usage for that command.
275func (c *cmd) Parse() []string {
276 // To gather params and usage information, we just run the command but cause this
277 // panic after the command has registered its flags and set its params and help
278 // information. This is then caught and that info printed.
283 c.flag.Usage = c.Usage
284 c.flag.Parse(c.flagArgs)
285 c.args = c.flag.Args()
289func (c *cmd) gather() {
290 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
294 // panic generated by Parse.
302func (c *cmd) makeUsage() string {
303 var r strings.Builder
304 cs := "mox " + strings.Join(c.words, " ")
305 for i, line := range strings.Split(strings.TrimSpace(c.params), "\n") {
313 fmt.Fprintf(&r, "%6s %s%s\n", s, cs, line)
316 c.flag.PrintDefaults()
320func (c *cmd) printUsage() {
321 fmt.Fprint(os.Stderr, c.makeUsage())
323 fmt.Fprint(os.Stderr, "\n"+c.help+"\n")
327func (c *cmd) Usage() {
332func cmdHelp(c *cmd) {
333 c.params = "[command ...]"
334 c.help = `Prints help about matching commands.
336If multiple commands match, they are listed along with the first line of their help text.
337If a single command matches, its usage and full help text is printed.
344 prefix := func(l, pre []string) bool {
345 if len(pre) > len(l) {
348 return slices.Equal(pre, l[:len(pre)])
352 for _, c := range cmds {
353 if slices.Equal(c.words, args) {
355 fmt.Print(c.makeUsage())
357 fmt.Print("\n" + c.help + "\n")
360 } else if prefix(c.words, args) {
361 partial = append(partial, c)
364 if len(partial) == 0 {
365 fmt.Fprintf(os.Stderr, "%s: unknown command\n", strings.Join(args, " "))
368 for _, c := range partial {
370 line := "mox " + strings.Join(c.words, " ")
371 fmt.Printf("%s\n", line)
373 fmt.Printf("\t%s\n", strings.Split(c.help, "\n")[0])
378func cmdHelpall(c *cmd) {
380 c.help = `Print all detailed usage and help information for all listed commands.
382Used to generate documentation.
390 for _, c := range cmds {
396 fmt.Fprintf(os.Stderr, "\n")
400 fmt.Fprintf(os.Stderr, "# mox %s\n\n", strings.Join(c.words, " "))
402 fmt.Fprintln(os.Stderr, c.help+"\n")
405 s = "\t" + strings.ReplaceAll(s, "\n", "\n\t")
406 fmt.Fprintln(os.Stderr, s)
410func usage(l []cmd, unlisted bool) {
413 lines = append(lines, "mox [-config config/mox.conf] [-pedantic] ...")
415 for _, c := range l {
417 if c.unlisted && !unlisted {
420 for _, line := range strings.Split(c.params, "\n") {
421 x := append([]string{"mox"}, c.words...)
425 lines = append(lines, strings.Join(x, " "))
428 for i, line := range lines {
433 fmt.Fprintln(os.Stderr, pre+line)
438var loglevel string // Empty will be interpreted as info, except by localserve.
441// subcommands that are not "serve" should use this function to load the config, it
442// restores any loglevel specified on the command-line, instead of using the
443// loglevels from the config file and it does not load files like TLS keys/certs.
444func mustLoadConfig() {
445 mox.MustLoadConfig(false, false)
450 if level, ok := mlog.Levels[ll]; ok {
451 mox.Conf.Log[""] = level
452 mlog.SetConfig(mox.Conf.Log)
454 log.Fatal("unknown loglevel", slog.String("loglevel", loglevel))
457 mox.SetPedantic(true)
462 // CheckConsistencyOnClose is true by default, for all the test packages. A regular
463 // mox server should never use it. But integration tests enable it again with a
465 store.CheckConsistencyOnClose = false
466 store.MsgFilesPerDirShiftSet(13) // For 1<<13 = 8k message files per directory.
468 ctxbg := context.Background()
474 // If invoked as sendmail, e.g. /usr/sbin/sendmail, we do enough so cron can get a
475 // message sent using smtp submission to a configured server.
476 if len(os.Args) > 0 && filepath.Base(os.Args[0]) == "sendmail" {
478 flag: flag.NewFlagSet("sendmail", flag.ExitOnError),
479 flagArgs: os.Args[1:],
480 log: mlog.New("sendmail", nil),
486 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")
487 flag.StringVar(&loglevel, "loglevel", "", "if non-empty, this log level is set early in startup")
488 flag.BoolVar(&pedantic, "pedantic", false, "protocol violations result in errors instead of accepting/working around them")
489 flag.BoolVar(&store.CheckConsistencyOnClose, "checkconsistency", false, "dangerous option for testing only, enables data checks that abort/panic when inconsistencies are found")
491 var cpuprofile, memprofile, tracefile string
492 flag.StringVar(&cpuprofile, "cpuprof", "", "store cpu profile to file")
493 flag.StringVar(&memprofile, "memprof", "", "store mem profile to file")
494 flag.StringVar(&tracefile, "trace", "", "store execution trace to file")
496 flag.Usage = func() { usage(cmds, false) }
504 defer traceExecution(tracefile)()
506 defer profile(cpuprofile, memprofile)()
509 mox.SetPedantic(true)
512 mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
517 if level, ok := mlog.Levels[ll]; ok {
518 mox.Conf.Log[""] = level
519 mlog.SetConfig(mox.Conf.Log)
520 // note: SetConfig may be called again when subcommands loads config.
522 log.Fatalf("unknown loglevel %q", loglevel)
527 for _, c := range cmds {
528 for i, w := range c.words {
529 if i >= len(args) || w != args[i] {
531 partial = append(partial, c)
536 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
537 c.flagArgs = args[len(c.words):]
538 c.log = mlog.New(strings.Join(c.words, ""), nil)
542 if len(partial) > 0 {
548func xcheckf(err error, format string, args ...any) {
552 msg := fmt.Sprintf(format, args...)
553 log.Fatalf("%s: %s", msg, err)
556func xparseIP(s, what string) net.IP {
559 log.Fatalf("invalid %s: %q", what, s)
564func xparseDomain(s, what string) dns.Domain {
565 d, err := dns.ParseDomain(s)
566 xcheckf(err, "parsing %s %q", what, s)
570func cmdClientConfig(c *cmd) {
572 c.help = `Print the configuration for email clients for a domain.
574Sending email is typically not done on the SMTP port 25, but on submission
575ports 465 (with TLS) and 587 (without initial TLS, but usually added to the
576connection with STARTTLS). For IMAP, the port with TLS is 993 and without is
579Without TLS/STARTTLS, passwords are sent in clear text, which should only be
580configured over otherwise secured connections, like a VPN.
586 d := xparseDomain(args[0], "domain")
591func printClientConfig(d dns.Domain) {
592 cc, err := admin.ClientConfigsDomain(d)
593 xcheckf(err, "getting client config")
594 fmt.Printf("%-20s %-30s %5s %-15s %s\n", "Protocol", "Host", "Port", "Listener", "Note")
595 for _, e := range cc.Entries {
596 fmt.Printf("%-20s %-30s %5d %-15s %s\n", e.Protocol, e.Host, e.Port, e.Listener, e.Note)
599To prevent authentication mechanism downgrade attempts that may result in
600clients sending plain text passwords to a MitM, clients should always be
601explicitly configured with the most secure authentication mechanism supported,
602the first of: SCRAM-SHA-256-PLUS, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, SCRAM-SHA-1,
607func cmdConfigTest(c *cmd) {
608 c.help = `Parses and validates the configuration files.
610If valid, the command exits with status 0. If not valid, all errors encountered
618 mox.FilesImmediate = true
620 _, errs := mox.ParseConfig(context.Background(), c.log, mox.ConfigStaticPath, true, true, false)
622 log.Printf("multiple errors:")
623 for _, err := range errs {
624 log.Printf("%s", err)
627 } else if len(errs) == 1 {
628 log.Fatalf("%s", errs[0])
631 fmt.Println("config OK")
634func cmdConfigDescribeStatic(c *cmd) {
635 c.params = ">mox.conf"
636 c.help = `Prints an annotated empty configuration for use as mox.conf.
638The static configuration file cannot be reloaded while mox is running. Mox has
639to be restarted for changes to the static configuration file to take effect.
641This configuration file needs modifications to make it valid. For example, it
642may contain unfinished list items.
644 if len(c.Parse()) != 0 {
649 err := sconf.Describe(os.Stdout, &sc)
650 xcheckf(err, "describing config")
653func cmdConfigDescribeDomains(c *cmd) {
654 c.params = ">domains.conf"
655 c.help = `Prints an annotated empty configuration for use as domains.conf.
657The domains configuration file contains the domains and their configuration,
658and accounts and their configuration. This includes the configured email
659addresses. The mox admin web interface, and the mox command line interface, can
660make changes to this file. Mox automatically reloads this file when it changes.
662Like the static configuration, the example domains.conf printed by this command
663needs modifications to make it valid.
665 if len(c.Parse()) != 0 {
669 var dc config.Dynamic
670 err := sconf.Describe(os.Stdout, &dc)
671 xcheckf(err, "describing config")
674func cmdConfigPrintservice(c *cmd) {
675 c.params = ">mox.service"
676 c.help = `Prints a systemd unit service file for mox.
678This is the same file as generated using quickstart. If the systemd service file
679has changed with a newer version of mox, use this command to generate an up to
682 if len(c.Parse()) != 0 {
686 pwd, err := os.Getwd()
688 log.Printf("current working directory: %v", err)
691 service := strings.ReplaceAll(moxService, "/home/mox", pwd)
695func cmdConfigDomainAdd(c *cmd) {
696 c.params = "[-disabled] domain account [localpart]"
697 c.help = `Adds a new domain to the configuration and reloads the configuration.
699The account is used for the postmaster mailboxes the domain, including as DMARC and
700TLS reporting. Localpart is the "username" at the domain for this account. If
701must be set if and only if account does not yet exist.
703The domain can be created in disabled mode, preventing automatically requesting
704TLS certificates with ACME, and rejecting incoming/outgoing messages involving
705the domain, but allowing further configuration of the domain.
708 c.flag.BoolVar(&disabled, "disabled", false, "disable the new domain")
710 if len(args) != 2 && len(args) != 3 {
714 d := xparseDomain(args[0], "domain")
716 var localpart smtp.Localpart
719 localpart, err = smtp.ParseLocalpart(args[2])
720 xcheckf(err, "parsing localpart")
722 ctlcmdConfigDomainAdd(xctl(), disabled, d, args[1], localpart)
725func ctlcmdConfigDomainAdd(ctl *ctl, disabled bool, domain dns.Domain, account string, localpart smtp.Localpart) {
726 ctl.xwrite("domainadd")
732 ctl.xwrite(domain.Name())
734 ctl.xwrite(string(localpart))
736 fmt.Printf("domain added, remember to add dns records, see:\n\nmox config dnsrecords %s\nmox config dnscheck %s\n", domain.Name(), domain.Name())
739func cmdConfigDomainRemove(c *cmd) {
741 c.help = `Remove a domain from the configuration and reload the configuration.
743This is a dangerous operation. Incoming email delivery for this domain will be
751 d := xparseDomain(args[0], "domain")
753 ctlcmdConfigDomainRemove(xctl(), d)
756func ctlcmdConfigDomainRemove(ctl *ctl, d dns.Domain) {
757 ctl.xwrite("domainrm")
760 fmt.Printf("domain removed, remember to remove dns records for %s\n", d)
763func cmdConfigDomainDisable(c *cmd) {
765 c.help = `Disable a domain and reload the configuration.
767This is a dangerous operation. Incoming/outgoing messages involving this domain
775 d := xparseDomain(args[0], "domain")
777 ctlcmdConfigDomainDisabled(xctl(), d, true)
778 fmt.Printf("domain disabled")
781func cmdConfigDomainEnable(c *cmd) {
783 c.help = `Enable a domain and reload the configuration.
785Incoming/outgoing messages involving this domain will be accepted again.
792 d := xparseDomain(args[0], "domain")
794 ctlcmdConfigDomainDisabled(xctl(), d, false)
797func ctlcmdConfigDomainDisabled(ctl *ctl, d dns.Domain, disabled bool) {
798 ctl.xwrite("domaindisabled")
808func cmdConfigAliasList(c *cmd) {
810 c.help = `Show aliases (lists) for domain.`
817 ctlcmdConfigAliasList(xctl(), args[0])
820func ctlcmdConfigAliasList(ctl *ctl, address string) {
821 ctl.xwrite("aliaslist")
824 ctl.xstreamto(os.Stdout)
827func cmdConfigAliasPrint(c *cmd) {
829 c.help = `Print settings and members of alias (list).`
836 ctlcmdConfigAliasPrint(xctl(), args[0])
839func ctlcmdConfigAliasPrint(ctl *ctl, address string) {
840 ctl.xwrite("aliasprint")
843 ctl.xstreamto(os.Stdout)
846func cmdConfigAliasAdd(c *cmd) {
847 c.params = "alias@domain rcpt1@domain ..."
848 c.help = `Add new alias (list) with one or more addresses and public posting enabled.
850An alias is used for delivering incoming email to multiple recipients. If you
851want to add an address to an account, don't use an alias, just add the address
859 alias := config.Alias{PostPublic: true, Addresses: args[1:]}
862 ctlcmdConfigAliasAdd(xctl(), args[0], alias)
865func ctlcmdConfigAliasAdd(ctl *ctl, address string, alias config.Alias) {
866 ctl.xwrite("aliasadd")
868 xctlwriteJSON(ctl, alias)
872func cmdConfigAliasUpdate(c *cmd) {
873 c.params = "alias@domain [-postpublic false|true -listmembers false|true -allowmsgfrom false|true]"
874 c.help = `Update alias (list) configuration.`
875 var postpublic, listmembers, allowmsgfrom string
876 c.flag.StringVar(&postpublic, "postpublic", "", "whether anyone or only list members can post")
877 c.flag.StringVar(&listmembers, "listmembers", "", "whether list members can list members")
878 c.flag.StringVar(&allowmsgfrom, "allowmsgfrom", "", "whether alias address can be used in message from header")
886 ctlcmdConfigAliasUpdate(xctl(), alias, postpublic, listmembers, allowmsgfrom)
889func ctlcmdConfigAliasUpdate(ctl *ctl, alias, postpublic, listmembers, allowmsgfrom string) {
890 ctl.xwrite("aliasupdate")
892 ctl.xwrite(postpublic)
893 ctl.xwrite(listmembers)
894 ctl.xwrite(allowmsgfrom)
898func cmdConfigAliasRemove(c *cmd) {
899 c.params = "alias@domain"
900 c.help = "Remove alias (list)."
907 ctlcmdConfigAliasRemove(xctl(), args[0])
910func ctlcmdConfigAliasRemove(ctl *ctl, alias string) {
911 ctl.xwrite("aliasrm")
916func cmdConfigAliasAddaddr(c *cmd) {
917 c.params = "alias@domain rcpt1@domain ..."
918 c.help = `Add addresses to alias (list).`
925 ctlcmdConfigAliasAddaddr(xctl(), args[0], args[1:])
928func ctlcmdConfigAliasAddaddr(ctl *ctl, alias string, addresses []string) {
929 ctl.xwrite("aliasaddaddr")
931 xctlwriteJSON(ctl, addresses)
935func cmdConfigAliasRemoveaddr(c *cmd) {
936 c.params = "alias@domain rcpt1@domain ..."
937 c.help = `Remove addresses from alias (list).`
944 ctlcmdConfigAliasRmaddr(xctl(), args[0], args[1:])
947func ctlcmdConfigAliasRmaddr(ctl *ctl, alias string, addresses []string) {
948 ctl.xwrite("aliasrmaddr")
950 xctlwriteJSON(ctl, addresses)
954func cmdConfigAccountAdd(c *cmd) {
955 c.params = "account address"
956 c.help = `Add an account with an email address and reload the configuration.
958Email can be delivered to this address/account. A password has to be configured
959explicitly, see the setaccountpassword command.
967 ctlcmdConfigAccountAdd(xctl(), args[0], args[1])
970func ctlcmdConfigAccountAdd(ctl *ctl, account, address string) {
971 ctl.xwrite("accountadd")
975 fmt.Printf("account added, set a password with \"mox setaccountpassword %s\"\n", account)
978func cmdConfigAccountRemove(c *cmd) {
980 c.help = `Remove an account and reload the configuration.
982Email addresses for this account will also be removed, and incoming email for
983these addresses will be rejected.
985All data for the account will be removed.
993 ctlcmdConfigAccountRemove(xctl(), args[0])
996func ctlcmdConfigAccountRemove(ctl *ctl, account string) {
997 ctl.xwrite("accountrm")
1000 fmt.Println("account removed")
1003func cmdConfigAccountList(c *cmd) {
1004 c.help = `List all accounts.
1006Each account is printed on a line, with optional additional tab-separated
1007information, such as "(disabled)".
1015 ctlcmdConfigAccountList(xctl())
1018func ctlcmdConfigAccountList(ctl *ctl) {
1019 ctl.xwrite("accountlist")
1021 ctl.xstreamto(os.Stdout)
1024func cmdConfigAccountDisable(c *cmd) {
1025 c.params = "account message"
1026 c.help = `Disable login for an account, showing message to users when they try to login.
1028Incoming email will still be accepted for the account, and queued email from the
1029account will still be delivered. No new login sessions are possible.
1031Message must be non-empty, ascii-only without control characters including
1032newline, and maximum 256 characters because it is used in SMTP/IMAP.
1039 log.Fatalf("message must be non-empty")
1043 ctlcmdConfigAccountDisabled(xctl(), args[0], args[1])
1044 fmt.Println("account disabled")
1047func cmdConfigAccountEnable(c *cmd) {
1048 c.params = "account"
1049 c.help = `Enable login again for an account.
1051Login attempts by the user no long result in an error message.
1059 ctlcmdConfigAccountDisabled(xctl(), args[0], "")
1060 fmt.Println("account enabled")
1063func ctlcmdConfigAccountDisabled(ctl *ctl, account, loginDisabled string) {
1064 ctl.xwrite("accountdisabled")
1066 ctl.xwrite(loginDisabled)
1070func cmdConfigTlspubkeyList(c *cmd) {
1071 c.params = "[account]"
1072 c.help = `List TLS public keys for TLS client certificate authentication.
1074If account is absent, the TLS public keys for all accounts are listed.
1077 var accountOpt string
1079 accountOpt = args[0]
1080 } else if len(args) > 1 {
1085 ctlcmdConfigTlspubkeyList(xctl(), accountOpt)
1088func ctlcmdConfigTlspubkeyList(ctl *ctl, accountOpt string) {
1089 ctl.xwrite("tlspubkeylist")
1090 ctl.xwrite(accountOpt)
1092 ctl.xstreamto(os.Stdout)
1095func cmdConfigTlspubkeyGet(c *cmd) {
1096 c.params = "fingerprint"
1097 c.help = `Get a TLS public key for a fingerprint.
1099Prints the type, name, account and address for the key, and the certificate in
1108 ctlcmdConfigTlspubkeyGet(xctl(), args[0])
1111func ctlcmdConfigTlspubkeyGet(ctl *ctl, fingerprint string) {
1112 ctl.xwrite("tlspubkeyget")
1113 ctl.xwrite(fingerprint)
1117 account := ctl.xread()
1118 address := ctl.xread()
1119 noimappreauth := ctl.xread()
1123 var block *pem.Block
1126 Type: "CERTIFICATE",
1131 fmt.Printf("type: %s\nname: %s\naccount: %s\naddress: %s\nno imap preauth: %s\n", typ, name, account, address, noimappreauth)
1133 fmt.Printf("certificate:\n\n")
1134 if err := pem.Encode(os.Stdout, block); err != nil {
1135 log.Fatalf("pem encode: %v", err)
1140func cmdConfigTlspubkeyAdd(c *cmd) {
1141 c.params = "address [name] < cert.pem"
1142 c.help = `Add a TLS public key to the account of the given address.
1144The public key is read from the certificate.
1146The optional name is a human-readable descriptive name of the key. If absent,
1147the CommonName from the certificate is used.
1149 var noimappreauth bool
1150 c.flag.BoolVar(&noimappreauth, "no-imap-preauth", false, "Don't automatically switch new IMAP connections authenticated with this key to \"authenticated\" state after the TLS handshake. For working around clients that ignore the untagged IMAP PREAUTH response and try to authenticate while already authenticated.")
1152 var address, name string
1155 } else if len(args) == 2 {
1156 address, name = args[0], args[1]
1161 buf, err := io.ReadAll(os.Stdin)
1162 xcheckf(err, "reading from stdin")
1163 block, _ := pem.Decode(buf)
1165 err = errors.New("no pem block found")
1166 } else if block.Type != "CERTIFICATE" {
1167 err = fmt.Errorf("unexpected type %q, expected CERTIFICATE", block.Type)
1169 xcheckf(err, "parsing pem")
1172 ctlcmdConfigTlspubkeyAdd(xctl(), address, name, noimappreauth, block.Bytes)
1175func ctlcmdConfigTlspubkeyAdd(ctl *ctl, address, name string, noimappreauth bool, certDER []byte) {
1176 ctl.xwrite("tlspubkeyadd")
1179 ctl.xwrite(fmt.Sprintf("%v", noimappreauth))
1180 ctl.xstreamfrom(bytes.NewReader(certDER))
1184func cmdConfigTlspubkeyRemove(c *cmd) {
1185 c.params = "fingerprint"
1186 c.help = `Remove TLS public key for fingerprint.`
1193 ctlcmdConfigTlspubkeyRemove(xctl(), args[0])
1196func ctlcmdConfigTlspubkeyRemove(ctl *ctl, fingerprint string) {
1197 ctl.xwrite("tlspubkeyrm")
1198 ctl.xwrite(fingerprint)
1202func cmdConfigTlspubkeyGen(c *cmd) {
1204 c.help = `Generate an ed25519 private key and minimal certificate for use a TLS public key and write to files starting with stem.
1206The private key is written to $stem.$timestamp.ed25519privatekey.pkcs8.pem.
1207The certificate is written to $stem.$timestamp.certificate.pem.
1208The private key and certificate are also written to
1209$stem.$timestamp.ed25519privatekey-certificate.pem.
1211The certificate can be added to an account with "mox config account tlspubkey add".
1213The combined file can be used with "mox sendmail".
1215The private key is also written to standard error in raw-url-base64-encoded
1216form, also for use with "mox sendmail". The fingerprint is written to standard
1217error too, for reference.
1225 timestamp := time.Now().Format("200601021504")
1226 prefix := stem + "." + timestamp
1228 seed := make([]byte, ed25519.SeedSize)
1229 if _, err := cryptorand.Read(seed); err != nil {
1232 privKey := ed25519.NewKeyFromSeed(seed)
1233 privKeyBuf, err := x509.MarshalPKCS8PrivateKey(privKey)
1234 xcheckf(err, "marshal private key as pkcs8")
1236 err = pem.Encode(&b, &pem.Block{Type: "PRIVATE KEY", Bytes: privKeyBuf})
1237 xcheckf(err, "marshal pkcs8 private key to pem")
1238 privKeyBufPEM := b.Bytes()
1240 certBuf, tlsCert := xminimalCert(privKey)
1242 err = pem.Encode(&b, &pem.Block{Type: "CERTIFICATE", Bytes: certBuf})
1243 xcheckf(err, "marshal certificate to pem")
1244 certBufPEM := b.Bytes()
1246 xwriteFile := func(p string, data []byte, what string) {
1247 log.Printf("writing %s", p)
1248 err = os.WriteFile(p, data, 0600)
1249 xcheckf(err, "writing %s file: %v", what, err)
1252 xwriteFile(prefix+".ed25519privatekey.pkcs8.pem", privKeyBufPEM, "private key")
1253 xwriteFile(prefix+".certificate.pem", certBufPEM, "certificate")
1254 combinedPEM := slices.Concat(privKeyBufPEM, certBufPEM)
1255 xwriteFile(prefix+".ed25519privatekey-certificate.pem", combinedPEM, "combined private key and certificate")
1257 shabuf := sha256.Sum256(tlsCert.Leaf.RawSubjectPublicKeyInfo)
1259 _, err = fmt.Fprintf(os.Stderr, "ed25519 private key as raw-url-base64: %s\ned25519 public key fingerprint: %s\n",
1260 base64.RawURLEncoding.EncodeToString(seed),
1261 base64.RawURLEncoding.EncodeToString(shabuf[:]),
1263 xcheckf(err, "write private key and public key fingerprint")
1266func cmdConfigAddressAdd(c *cmd) {
1267 c.params = "address account"
1268 c.help = `Adds an address to an account and reloads the configuration.
1270If address starts with a @ (i.e. a missing localpart), this is a catchall
1271address for the domain.
1279 ctlcmdConfigAddressAdd(xctl(), args[0], args[1])
1282func ctlcmdConfigAddressAdd(ctl *ctl, address, account string) {
1283 ctl.xwrite("addressadd")
1287 fmt.Println("address added")
1290func cmdConfigAddressRemove(c *cmd) {
1291 c.params = "address"
1292 c.help = `Remove an address and reload the configuration.
1294Incoming email for this address will be rejected after removing an address.
1302 ctlcmdConfigAddressRemove(xctl(), args[0])
1305func ctlcmdConfigAddressRemove(ctl *ctl, address string) {
1306 ctl.xwrite("addressrm")
1309 fmt.Println("address removed")
1312func cmdConfigDNSRecords(c *cmd) {
1314 c.help = `Prints annotated DNS records as zone file that should be created for the domain.
1316The zone file can be imported into existing DNS software. You should review the
1317DNS records, especially if your domain previously/currently has email
1325 d := xparseDomain(args[0], "domain")
1327 domConf, ok := mox.Conf.Domain(d)
1329 log.Fatalf("unknown domain")
1332 resolver := dns.StrictResolver{Pkg: "main"}
1333 _, result, err := resolver.LookupTXT(context.Background(), d.ASCII+".")
1334 if !dns.IsNotFound(err) {
1335 xcheckf(err, "looking up record for dnssec-status")
1338 var certIssuerDomainName, acmeAccountURI string
1339 public := mox.Conf.Static.Listeners["public"]
1340 if public.TLS != nil && public.TLS.ACME != "" {
1341 acme, ok := mox.Conf.Static.ACME[public.TLS.ACME]
1342 if ok && acme.Manager.Manager.Client != nil {
1343 certIssuerDomainName = acme.IssuerDomainName
1344 acc, err := acme.Manager.Manager.Client.GetReg(context.Background(), "")
1345 c.log.Check(err, "get public acme account")
1347 acmeAccountURI = acc.URI
1352 records, err := admin.DomainRecords(domConf, d, result.Authentic, certIssuerDomainName, acmeAccountURI)
1353 xcheckf(err, "records")
1354 fmt.Print(strings.Join(records, "\n") + "\n")
1357func cmdConfigDNSCheck(c *cmd) {
1359 c.help = "Check the DNS records with the configuration for the domain, and print any errors/warnings."
1365 d := xparseDomain(args[0], "domain")
1367 _, ok := mox.Conf.Domain(d)
1369 log.Fatalf("unknown domain")
1372 // todo future: move http.Admin.CheckDomain to mox- and make it return a regular error.
1378 err, ok := x.(*sherpa.Error)
1382 log.Fatalf("%s", err)
1385 printResult := func(name string, r webadmin.Result) {
1386 if len(r.Errors) == 0 && len(r.Warnings) == 0 {
1389 fmt.Printf("# %s\n", name)
1390 for _, s := range r.Errors {
1391 fmt.Printf("error: %s\n", s)
1393 for _, s := range r.Warnings {
1394 fmt.Printf("warning: %s\n", s)
1398 result := webadmin.Admin{}.CheckDomain(context.Background(), args[0])
1399 printResult("DNSSEC", result.DNSSEC.Result)
1400 printResult("IPRev", result.IPRev.Result)
1401 printResult("MX", result.MX.Result)
1402 printResult("TLS", result.TLS.Result)
1403 printResult("DANE", result.DANE.Result)
1404 printResult("SPF", result.SPF.Result)
1405 printResult("DKIM", result.DKIM.Result)
1406 printResult("DMARC", result.DMARC.Result)
1407 printResult("Host TLSRPT", result.HostTLSRPT.Result)
1408 printResult("Domain TLSRPT", result.DomainTLSRPT.Result)
1409 printResult("MTASTS", result.MTASTS.Result)
1410 printResult("SRV conf", result.SRVConf.Result)
1411 printResult("Autoconf", result.Autoconf.Result)
1412 printResult("Autodiscover", result.Autodiscover.Result)
1415func cmdConfigEnsureACMEHostprivatekeys(c *cmd) {
1417 c.help = `Ensure host private keys exist for TLS listeners with ACME.
1419In mox.conf, each listener can have TLS configured. Long-lived private key files
1420can be specified, which will be used when requesting ACME certificates.
1421Configuring these private keys makes it feasible to publish DANE TLSA records
1422for the corresponding public keys in DNS, protected with DNSSEC, allowing TLS
1423certificate verification without depending on a list of Certificate Authorities
1424(CAs). Previous versions of mox did not pre-generate private keys for use with
1425ACME certificates, but would generate private keys on-demand. By explicitly
1426configuring private keys, they will not change automatedly with new
1427certificates, and the DNS TLSA records stay valid.
1429This command looks for listeners in mox.conf with TLS with ACME configured. For
1430each missing host private key (of type rsa-2048 and ecdsa-p256) a key is written
1431to config/hostkeys/. If a certificate exists in the ACME "cache", its private
1432key is copied. Otherwise a new private key is generated. Snippets for manually
1433updating/editing mox.conf are printed.
1435After running this command, and updating mox.conf, run "mox config dnsrecords"
1436for a domain and create the TLSA DNS records it suggests to enable DANE.
1443 // Load a private key from p, in various forms. We only look at the first PEM
1444 // block. Files with only a private key, or with multiple blocks but private key
1445 // first like autocert does, can be loaded.
1446 loadPrivateKey := func(f *os.File) (any, error) {
1447 buf, err := io.ReadAll(f)
1449 return nil, fmt.Errorf("reading private key file: %v", err)
1451 block, _ := pem.Decode(buf)
1453 return nil, fmt.Errorf("no pem block found in pem file")
1457 case "EC PRIVATE KEY":
1458 privKey, err = x509.ParseECPrivateKey(block.Bytes)
1459 case "RSA PRIVATE KEY":
1460 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
1462 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
1464 return nil, fmt.Errorf("unrecognized pem block type %q", block.Type)
1467 return nil, fmt.Errorf("parsing private key of type %q: %v", block.Type, err)
1472 // Either load a private key from file, or if it doesn't exist generate a new
1474 xtryLoadPrivateKey := func(kt autocert.KeyType, p string) any {
1475 f, err := os.Open(p)
1476 if err != nil && errors.Is(err, fs.ErrNotExist) {
1478 case autocert.KeyRSA2048:
1479 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1480 xcheckf(err, "generating new 2048-bit rsa private key")
1482 case autocert.KeyECDSAP256:
1483 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1484 xcheckf(err, "generating new ecdsa p-256 private key")
1487 log.Fatalf("unexpected keytype %v", kt)
1490 xcheckf(err, "%s: open acme key and certificate file", p)
1492 // Load private key from file. autocert stores a PEM file that starts with a
1493 // private key, followed by certificate(s). So we can just read it and should find
1494 // the private key we are looking for.
1495 privKey, err := loadPrivateKey(f)
1496 if xerr := f.Close(); xerr != nil {
1497 log.Printf("closing private key file: %v", xerr)
1499 xcheckf(err, "parsing private key from acme key and certificate file")
1501 switch k := privKey.(type) {
1502 case *rsa.PrivateKey:
1503 if k.N.BitLen() == 2048 {
1506 log.Printf("warning: rsa private key in %s has %d bits, skipping and generating new 2048-bit rsa private key", p, k.N.BitLen())
1507 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1508 xcheckf(err, "generating new 2048-bit rsa private key")
1510 case *ecdsa.PrivateKey:
1511 if k.Curve == elliptic.P256() {
1514 log.Printf("warning: ecdsa private key in %s has curve %v, skipping and generating new p-256 ecdsa key", p, k.Curve.Params().Name)
1515 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1516 xcheckf(err, "generating new ecdsa p-256 private key")
1519 log.Fatalf("%s: unexpected private key file of type %T", p, privKey)
1524 // Write privKey as PKCS#8 private key to p. Only if file does not yet exist.
1525 writeHostPrivateKey := func(privKey any, p string) error {
1526 os.MkdirAll(filepath.Dir(p), 0700)
1527 f, err := os.OpenFile(p, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
1529 return fmt.Errorf("create: %v", err)
1533 if err := f.Close(); err != nil {
1534 log.Printf("closing new hostkey file %s after error: %v", p, err)
1536 if err := os.Remove(p); err != nil {
1537 log.Printf("removing new hostkey file %s after error: %v", p, err)
1541 buf, err := x509.MarshalPKCS8PrivateKey(privKey)
1543 return fmt.Errorf("marshal private host key: %v", err)
1546 Type: "PRIVATE KEY",
1549 if err := pem.Encode(f, &block); err != nil {
1550 return fmt.Errorf("write as pem: %v", err)
1552 if err := f.Close(); err != nil {
1553 return fmt.Errorf("close: %v", err)
1560 timestamp := time.Now().Format("20060102T150405")
1562 for listenerName, l := range mox.Conf.Static.Listeners {
1563 if l.TLS == nil || l.TLS.ACME == "" {
1566 haveKeyTypes := map[autocert.KeyType]bool{}
1567 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
1568 p := mox.ConfigDirPath(privKeyFile)
1569 f, err := os.Open(p)
1570 xcheckf(err, "open host private key")
1571 privKey, err := loadPrivateKey(f)
1572 if err := f.Close(); err != nil {
1573 log.Printf("closing host private key file: %v", err)
1575 xcheckf(err, "loading host private key")
1576 switch k := privKey.(type) {
1577 case *rsa.PrivateKey:
1578 if k.N.BitLen() == 2048 {
1579 haveKeyTypes[autocert.KeyRSA2048] = true
1581 case *ecdsa.PrivateKey:
1582 if k.Curve == elliptic.P256() {
1583 haveKeyTypes[autocert.KeyECDSAP256] = true
1587 created := []string{}
1588 for _, kt := range []autocert.KeyType{autocert.KeyRSA2048, autocert.KeyECDSAP256} {
1589 if haveKeyTypes[kt] {
1592 // Lookup key in ACME cache.
1593 host := l.HostnameDomain
1594 if host.ASCII == "" {
1595 host = mox.Conf.Static.HostnameDomain
1597 filename := host.ASCII
1599 if kt == autocert.KeyRSA2048 {
1603 p := mox.DataDirPath(filepath.Join("acme", "keycerts", l.TLS.ACME, filename))
1604 privKey := xtryLoadPrivateKey(kt, p)
1606 relPath := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", host.Name(), timestamp, kind))
1607 destPath := mox.ConfigDirPath(relPath)
1608 err := writeHostPrivateKey(privKey, destPath)
1609 xcheckf(err, "writing host private key file to %s: %v", destPath, err)
1610 created = append(created, relPath)
1611 fmt.Printf("Wrote host private key: %s\n", destPath)
1613 didCreate = didCreate || len(created) > 0
1614 if len(created) > 0 {
1616 HostPrivateKeyFiles: append(l.TLS.HostPrivateKeyFiles, created...),
1618 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)
1619 err := sconf.Write(os.Stdout, tls)
1620 xcheckf(err, "writing new TLS.HostPrivateKeyFiles section")
1626After updating mox.conf and restarting, run "mox config dnsrecords" for a
1627domain and create the TLSA DNS records it suggests to enable DANE.
1632func cmdLoglevels(c *cmd) {
1633 c.params = "[level [pkg]]"
1634 c.help = `Print the log levels, or set a new default log level, or a level for the given package.
1636By default, a single log level applies to all logging in mox. But for each
1637"pkg", an overriding log level can be configured. Examples of packages:
1638smtpserver, smtpclient, queue, imapserver, spf, dkim, dmarc, junk, message,
1641Specify a pkg and an empty level to clear the configured level for a package.
1643Valid labels: error, info, debug, trace, traceauth, tracedata.
1652 ctlcmdLoglevels(xctl())
1658 ctlcmdSetLoglevels(xctl(), pkg, args[0])
1662func ctlcmdLoglevels(ctl *ctl) {
1663 ctl.xwrite("loglevels")
1665 ctl.xstreamto(os.Stdout)
1668func ctlcmdSetLoglevels(ctl *ctl, pkg, level string) {
1669 ctl.xwrite("setloglevels")
1675func cmdStop(c *cmd) {
1676 c.help = `Shut mox down, giving connections maximum 3 seconds to stop before closing them.
1678While shutting down, new IMAP and SMTP connections will get a status response
1679indicating temporary unavailability. Existing connections will get a 3 second
1680period to finish their transaction and shut down. Under normal circumstances,
1681only IMAP has long-living connections, with the IDLE command to get notified of
1684 if len(c.Parse()) != 0 {
1691 // Read will hang until remote has shut down.
1692 buf := make([]byte, 128)
1693 n, err := xctl.conn.Read(buf)
1695 log.Fatalf("expected eof after graceful shutdown, got data %q", buf[:n])
1696 } else if err != io.EOF {
1697 log.Fatalf("expected eof after graceful shutdown, got error %v", err)
1699 fmt.Println("mox stopped")
1702func cmdBackup(c *cmd) {
1703 c.params = "destdir"
1704 c.help = `Creates a backup of the config and data directory.
1706Backup copies the config directory to <destdir>/config, and creates
1707<destdir>/data with a consistent snapshot of the databases and message files
1708and copies other files from the data directory. Empty directories are not
1709copied. The backup can then be stored elsewhere for long-term storage, or used
1710to fall back to should an upgrade fail. Simply copying files in the data
1711directory while mox is running can result in unusable database files.
1713Message files never change (they are read-only, though can be removed) and are
1714hard-linked so they don't consume additional space. If hardlinking fails, for
1715example when the backup destination directory is on a different file system, a
1716regular copy is made. Using a destination directory like "data/tmp/backup"
1717increases the odds hardlinking succeeds: the default systemd service file
1718specifically mounts the data directory, causing attempts to hardlink outside it
1719to fail with an error about cross-device linking.
1721All files in the data directory that aren't recognized (i.e. other than known
1722database files, message files, an acme directory, the "tmp" directory, etc),
1723are stored, but with a warning.
1725Remove files in the destination directory before doing another backup. The
1726backup command will not overwrite files, but print and return errors.
1728Exit code 0 indicates the backup was successful. A clean successful backup does
1729not print any output, but may print warnings. Use the -verbose flag for
1730details, including timing.
1732To restore a backup, first shut down mox, move away the old data directory and
1733move an earlier backed up directory in its place, run "mox verifydata
1734<datadir>", possibly with the "-fix" option, and restart mox. After the
1735restore, you may also want to run "mox bumpuidvalidity" for each account for
1736which messages in a mailbox changed, to force IMAP clients to synchronize
1739Before upgrading, to check if the upgrade will likely succeed, first make a
1740backup, then use the new mox binary to run "mox verifydata <backupdir>/data".
1741This can change the backup files (e.g. upgrade database files, move away
1742unrecognized message files), so you should make a new backup before actually
1747 c.flag.BoolVar(&verbose, "verbose", false, "print progress")
1754 dstDataDir, err := filepath.Abs(args[0])
1755 xcheckf(err, "making path absolute")
1757 ctlcmdBackup(xctl(), dstDataDir, verbose)
1760func ctlcmdBackup(ctl *ctl, dstDataDir string, verbose bool) {
1761 ctl.xwrite("backup")
1762 ctl.xwrite(dstDataDir)
1764 ctl.xwrite("verbose")
1768 ctl.xstreamto(os.Stdout)
1772func cmdSetadminpassword(c *cmd) {
1773 c.help = `Set a new admin password, for the web interface.
1775The password is read from stdin. Its bcrypt hash is stored in a file named
1776"adminpasswd" in the configuration directory.
1778 if len(c.Parse()) != 0 {
1783 path := mox.ConfigDirPath(mox.Conf.Static.AdminPasswordFile)
1785 log.Fatal("no admin password file configured")
1788 pw := xreadpassword()
1789 pw, err := precis.OpaqueString.String(pw)
1790 xcheckf(err, `checking password with "precis" requirements`)
1791 hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
1792 xcheckf(err, "generating hash for password")
1793 err = os.WriteFile(path, hash, 0660)
1794 xcheckf(err, "writing hash to admin password file")
1797func xreadpassword() string {
1799Type new password. Password WILL echo.
1801WARNING: Bots will try to bruteforce your password. Connections with failed
1802authentication attempts will be rate limited but attackers WILL find passwords
1803reused at other services and weak passwords. If your account is compromised,
1804spammers are likely to abuse your system, spamming your address and the wider
1805internet in your name. So please pick a random, unguessable password, preferably
1806at least 12 characters.
1809 fmt.Printf("password: ")
1810 scanner := bufio.NewScanner(os.Stdin)
1811 // The default splitter for scanners is one that splits by lines, so we
1812 // don't have to set up another one here.
1814 // We discard the return value of Scan() since failing to tokenize could
1815 // either mean reaching EOF but no newline (which can be legitimate if the
1816 // CLI was programatically called to set the password, but with no trailing
1817 // newline), or an actual error. We can distinguish between the two by
1818 // calling Err() since it will return nil if it were EOF, but the actual
1821 xcheckf(scanner.Err(), "reading stdin")
1822 // No need to trim, the scanner does not return the token in the output.
1823 pw := scanner.Text()
1825 log.Fatal("password must be at least 8 characters")
1830func cmdSetaccountpassword(c *cmd) {
1831 c.params = "account"
1832 c.help = `Set new password an account.
1834The password is read from stdin. Secrets derived from the password, but not the
1835password itself, are stored in the account database. The stored secrets are for
1836authentication with: scram-sha-256, scram-sha-1, cram-md5, plain text (bcrypt
1839The parameter is an account name, as configured under Accounts in domains.conf
1840and as present in the data/accounts/ directory, not a configured email address
1849 pw := xreadpassword()
1851 ctlcmdSetaccountpassword(xctl(), args[0], pw)
1854func ctlcmdSetaccountpassword(ctl *ctl, account, password string) {
1855 ctl.xwrite("setaccountpassword")
1857 ctl.xwrite(password)
1861func cmdDeliver(c *cmd) {
1863 c.params = "address < message"
1864 c.help = "Deliver message to address."
1870 ctlcmdDeliver(xctl(), args[0])
1873func ctlcmdDeliver(ctl *ctl, address string) {
1874 ctl.xwrite("deliver")
1877 ctl.xstreamfrom(os.Stdin)
1880 fmt.Println("message delivered")
1882 log.Fatalf("deliver: %s", line)
1886func cmdDKIMGenrsa(c *cmd) {
1887 c.params = ">$selector._domainkey.$domain.rsa2048.privatekey.pkcs8.pem"
1888 c.help = `Generate a new 2048 bit RSA private key for use with DKIM.
1890The generated file is in PEM format, and has a comment it is generated for use
1893 if len(c.Parse()) != 0 {
1897 buf, err := admin.MakeDKIMRSAKey(dns.Domain{}, dns.Domain{})
1898 xcheckf(err, "making rsa private key")
1899 _, err = os.Stdout.Write(buf)
1900 xcheckf(err, "writing rsa private key")
1903// todo: options for specifying the domain this is the mx host of, and enabling dane and/or mta-sts verification
1904func cmdSMTPDial(c *cmd) {
1905 c.params = "host[:port]"
1907 var tlsCerts, tlsCiphersuites, tlsCurves, tlsVersionMin, tlsVersionMax, tlsRenegotiation string
1908 var tlsVerify, noTLS, forceTLS, tlsNoSessionTickets, tlsNoDynamicRecordSizing bool
1909 var ehloHostnameStr, remoteHostnameStr string
1911 ciphersuites := map[string]*tls.CipherSuite{}
1912 ciphersuitesInsecure := map[string]*tls.CipherSuite{}
1913 for _, v := range tls.CipherSuites() {
1914 if slices.Contains(v.SupportedVersions, tls.VersionTLS10) || slices.Contains(v.SupportedVersions, tls.VersionTLS11) || slices.Contains(v.SupportedVersions, tls.VersionTLS12) {
1915 ciphersuites[strings.ToLower(v.Name)] = v
1918 for _, v := range tls.InsecureCipherSuites() {
1919 if slices.Contains(v.SupportedVersions, tls.VersionTLS10) || slices.Contains(v.SupportedVersions, tls.VersionTLS11) || slices.Contains(v.SupportedVersions, tls.VersionTLS12) {
1920 ciphersuitesInsecure[strings.ToLower(v.Name)] = v
1924 curves := map[string]tls.CurveID{}
1925 for _, a := range curvesList {
1926 curves[strings.ToLower(a.String())] = a
1929 c.flag.StringVar(&tlsCiphersuites, "tlsciphersuites", "", "ciphersuites to allow, comma-separated, order is ignored, only for TLS 1.2 and earlier, empty value uses TLS stack defaults; values: "+strings.Join(slices.Sorted(maps.Keys(ciphersuites)), ", ")+", and insecure: "+strings.Join(slices.Sorted(maps.Keys(ciphersuitesInsecure)), ", "))
1930 c.flag.StringVar(&tlsCurves, "tlscurves", "", "tls ecc key exchange mechanisms to allow, comma-separated, order is ignored, empty value uses TLS stack defaults; values: curvep256, curvep384, curvep521, x25519, x25519mlkem768")
1931 c.flag.StringVar(&tlsCerts, "tlscerts", "", "path to root ca certificates in pem form, for verification")
1932 c.flag.StringVar(&tlsVersionMin, "tlsversionmin", "", "minimum TLS version, empty value uses TLS stack default; values: tls1.2, etc.")
1933 c.flag.StringVar(&tlsVersionMax, "tlsversionmax", "", "maximum TLS version, empty value uses TLS stack default; values: tls1.2, etc.")
1934 c.flag.BoolVar(&tlsVerify, "tlsverify", false, "verify remote hostname during TLS")
1935 c.flag.BoolVar(&tlsNoSessionTickets, "tlsnosessiontickets", false, "disable TLS session tickets")
1936 c.flag.BoolVar(&tlsNoDynamicRecordSizing, "tlsnodynamicrecordsizing", false, "disable TLS dynamic record sizing")
1937 c.flag.BoolVar(&noTLS, "notls", false, "do not use TLS")
1938 c.flag.BoolVar(&forceTLS, "forcetls", false, "use TLS, even if remote SMTP server does not announce STARTTLS extension")
1939 c.flag.StringVar(&tlsRenegotiation, "tlsrenegotiation", "never", "when to allow renegotiation; only applies to tls1.2 and earlier, not tls1.3; values: never, once, always")
1940 c.flag.StringVar(&ehloHostnameStr, "ehlohostname", "", "our hostname to use during the SMTP EHLO command")
1941 c.flag.StringVar(&remoteHostnameStr, "remotehostname", "", "remote hostname to use for TLS verification, if enabled; the hostname from the parameter is used by default")
1943 c.help = `Dial the address, initialize the SMTP session, including using STARTTLS to enable TLS if the server supports it.
1945If no port is specified, SMTP port 25 is used.
1947Data is copied between connection and stdin/stdout until either side closes the
1950The flags influence the TLS configuration, useful for debugging interoperability
1953No MTA-STS or DANE verification is done.
1955Hint: Use "mox -loglevel trace smtp dial ..." to see the protocol messages
1956exchanged during connection set up.
1963 if noTLS && forceTLS {
1964 log.Fatalf("cannot have both -notls and -forcetls")
1967 parseTLSVersion := func(s string) uint16 {
1970 return tls.VersionTLS10
1972 return tls.VersionTLS11
1974 return tls.VersionTLS12
1976 return tls.VersionTLS13
1980 log.Fatalf("invalid tls version %q", s)
1981 panic("not reached")
1984 tlsConfig := tls.Config{
1985 MinVersion: parseTLSVersion(tlsVersionMin),
1986 MaxVersion: parseTLSVersion(tlsVersionMax),
1987 InsecureSkipVerify: !tlsVerify,
1988 SessionTicketsDisabled: tlsNoSessionTickets,
1989 DynamicRecordSizingDisabled: tlsNoDynamicRecordSizing,
1992 switch tlsRenegotiation {
1994 tlsConfig.Renegotiation = tls.RenegotiateNever
1996 tlsConfig.Renegotiation = tls.RenegotiateOnceAsClient
1998 tlsConfig.Renegotiation = tls.RenegotiateFreelyAsClient
2000 log.Fatalf("invalid value %q for -tlsrenegotation", tlsRenegotiation)
2003 pool := x509.NewCertPool()
2004 pembuf, err := os.ReadFile(tlsCerts)
2005 xcheckf(err, "reading tls certificates")
2006 ok := pool.AppendCertsFromPEM(pembuf)
2008 c.log.Warn("no tls certificates found", slog.String("path", tlsCerts))
2010 tlsConfig.RootCAs = pool
2012 if tlsCiphersuites != "" {
2013 for _, s := range strings.Split(tlsCiphersuites, ",") {
2014 s = strings.TrimSpace(s)
2015 c, ok := ciphersuites[s]
2017 c, ok = ciphersuitesInsecure[s]
2020 log.Fatalf("unknown ciphersuite %q", s)
2022 tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, c.ID)
2025 if tlsCurves != "" {
2026 for _, s := range strings.Split(tlsCurves, ",") {
2027 s = strings.TrimSpace(s)
2028 if c, ok := curves[s]; !ok {
2029 log.Fatalf("unknown ecc key exchange algorithm %q", s)
2031 tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, c)
2036 var host, portStr string
2038 host, portStr, err = net.SplitHostPort(args[0])
2043 port, err := strconv.ParseInt(portStr, 10, 64)
2044 xcheckf(err, "parsing port %q", portStr)
2046 if remoteHostnameStr == "" {
2047 remoteHostnameStr = host
2049 remoteHostname, err := dns.ParseDomain(remoteHostnameStr)
2050 xcheckf(err, "parsing remote host")
2051 tlsConfig.ServerName = remoteHostname.Name()
2053 resolver := dns.StrictResolver{Pkg: "smtpdial"}
2054 _, _, _, ips, _, err := smtpclient.GatherIPs(context.Background(), c.log.Logger, resolver, "ip", dns.IPDomain{Domain: remoteHostname}, nil)
2055 xcheckf(err, "resolve host")
2056 c.log.Info("resolved remote address", slog.Any("ips", ips))
2058 dialer := &net.Dialer{Timeout: 5 * time.Second}
2059 dialedIPs := map[string][]net.IP{}
2060 conn, ip, err := smtpclient.Dial(context.Background(), c.log.Logger, dialer, dns.IPDomain{Domain: remoteHostname}, ips, int(port), dialedIPs, nil)
2061 xcheckf(err, "dial")
2062 c.log.Info("connected to remote host", slog.Any("ip", ip))
2064 tlsMode := smtpclient.TLSOpportunistic
2066 tlsMode = smtpclient.TLSRequiredStartTLS
2068 tlsMode = smtpclient.TLSSkip
2070 var ehloHostname dns.Domain
2071 if ehloHostnameStr == "" {
2072 name, err := os.Hostname()
2073 xcheckf(err, "get hostname")
2074 ehloHostnameStr = name
2076 ehloHostname, err = dns.ParseDomain(ehloHostnameStr)
2077 xcheckf(err, "parse hostname")
2079 opts := smtpclient.Opts{
2080 TLSConfig: &tlsConfig,
2082 client, err := smtpclient.New(context.Background(), c.log.Logger, conn, tlsMode, false, ehloHostname, dns.Domain{}, opts)
2083 xcheckf(err, "new smtp client")
2085 cs := client.TLSConnectionState()
2087 c.log.Info("smtp initialized without tls")
2089 c.log.Info("smtp initialized with tls",
2090 slog.String("version", tls.VersionName(cs.Version)),
2091 slog.String("ciphersuite", strings.ToLower(tls.CipherSuiteName(cs.CipherSuite))),
2092 slog.String("sni", cs.ServerName),
2094 for _, chain := range cs.VerifiedChains {
2096 for _, cert := range chain {
2097 s := fmt.Sprintf("dns names %q, common name %q, %s - %s, issuer %q)", strings.Join(cert.DNSNames, ","), cert.Subject.CommonName, cert.NotBefore.Format("2006-01-02T15:04:05"), cert.NotAfter.Format("2006-01-02T15:04:05"), cert.Issuer.CommonName)
2100 c.log.Info("tls certificate verification chain", slog.String("chain", strings.Join(l, "; ")))
2104 conn, err = client.Conn()
2105 xcheckf(err, "get smtp session connection")
2108 _, err := io.Copy(os.Stdout, conn)
2109 xcheckf(err, "copy from connection to stdout")
2111 c.log.Check(err, "closing connection")
2113 _, err = io.Copy(conn, os.Stdin)
2114 xcheckf(err, "copy from stdin to connection")
2117func cmdDANEDial(c *cmd) {
2118 c.params = "host:port"
2120 c.flag.StringVar(&usages, "usages", "pkix-ta,pkix-ee,dane-ta,dane-ee", "allowed usages for dane, comma-separated list")
2121 c.help = `Dial the address using TLS with certificate verification using DANE.
2123Data is copied between connection and stdin/stdout until either side closes the
2131 allowedUsages := []adns.TLSAUsage{}
2133 for _, s := range strings.Split(usages, ",") {
2134 var usage adns.TLSAUsage
2135 switch strings.ToLower(s) {
2136 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
2137 usage = adns.TLSAUsagePKIXTA
2138 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
2139 usage = adns.TLSAUsagePKIXEE
2140 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
2141 usage = adns.TLSAUsageDANETA
2142 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
2143 usage = adns.TLSAUsageDANEEE
2145 log.Fatalf("unknown dane usage %q", s)
2147 allowedUsages = append(allowedUsages, usage)
2151 pkixRoots, err := x509.SystemCertPool()
2152 xcheckf(err, "get system pkix certificate pool")
2154 resolver := dns.StrictResolver{Pkg: "danedial"}
2155 conn, record, err := dane.Dial(context.Background(), c.log.Logger, resolver, "tcp", args[0], allowedUsages, pkixRoots)
2156 xcheckf(err, "dial")
2157 log.Printf("(connected, verified with %s)", record)
2160 _, err := io.Copy(os.Stdout, conn)
2161 xcheckf(err, "copy from connection to stdout")
2163 c.log.Check(err, "closing connection")
2165 _, err = io.Copy(conn, os.Stdin)
2166 xcheckf(err, "copy from stdin to connection")
2169func cmdDANEDialmx(c *cmd) {
2170 c.params = "domain [destination-host]"
2171 var ehloHostname string
2172 c.flag.StringVar(&ehloHostname, "ehlohostname", "localhost", "hostname to send in smtp ehlo command")
2173 c.help = `Connect to MX server for domain using STARTTLS verified with DANE.
2175If no destination host is specified, regular delivery logic is used to find the
2176hosts to attempt delivery too. This involves following CNAMEs for the domain,
2177looking up MX records, and possibly falling back to the domain name itself as
2180If a destination host is specified, that is the only candidate host considered
2183With a list of destinations gathered, each is dialed until a successful SMTP
2184session verified with DANE has been initialized, including EHLO and STARTTLS
2187Once connected, data is copied between connection and stdin/stdout, until
2188either side closes the connection.
2190This command follows the same logic as delivery attempts made from the queue,
2191sharing most of its code.
2194 if len(args) != 1 && len(args) != 2 {
2198 ehloDomain := xparseDomain(ehloHostname, "ehlo host name")
2199 origNextHop := xparseDomain(args[0], "domain")
2201 ctxbg := context.Background()
2203 resolver := dns.StrictResolver{}
2205 var expandedNextHopAuthentic bool
2206 var expandedNextHop dns.Domain
2207 var hostPrefs []smtpclient.HostPref
2210 var origNextHopAuthentic bool
2212 haveMX, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hostPrefs, permanent, err = smtpclient.GatherDestinations(ctxbg, c.log.Logger, resolver, dns.IPDomain{Domain: origNextHop})
2213 status := "temporary"
2215 status = "permanent"
2218 log.Fatalf("gathering destinations: %v (%s)", err, status)
2220 if expandedNextHop != origNextHop {
2221 log.Printf("followed cnames to %s", expandedNextHop)
2224 log.Printf("found mx record, trying mx hosts")
2226 log.Printf("no mx record found, will try to connect to domain directly")
2228 if !origNextHopAuthentic {
2229 log.Fatalf("error: initial domain not dnssec-secure")
2231 if !expandedNextHopAuthentic {
2232 log.Fatalf("error: expanded domain not dnssec-secure")
2236 for _, hp := range hostPrefs {
2237 s := hp.Host.String()
2239 s += fmt.Sprintf(" (pref %d)", hp.Pref)
2243 log.Printf("destinations: %s", strings.Join(l, ", "))
2245 d := xparseDomain(args[1], "destination host")
2246 log.Printf("skipping domain mx/cname lookups, assuming domain is dnssec-protected")
2248 expandedNextHopAuthentic = true
2250 hostPrefs = []smtpclient.HostPref{{Host: dns.IPDomain{Domain: d}, Pref: -1}}
2253 dialedIPs := map[string][]net.IP{}
2254 for _, hp := range hostPrefs {
2257 log.Printf("attempting to connect to %s (pref %d)", host, hp.Pref)
2259 authentic, expandedAuthentic, expandedHost, ips, _, err := smtpclient.GatherIPs(ctxbg, c.log.Logger, resolver, "ip", host, dialedIPs)
2261 log.Printf("resolving ips for %s: %v, skipping", host, err)
2265 log.Printf("no dnssec for ips of %s, skipping", host)
2268 if !expandedAuthentic {
2269 log.Printf("no dnssec for cname-followed ips of %s, skipping", host)
2272 if expandedHost != host.Domain {
2273 log.Printf("host %s cname-expanded to %s", host, expandedHost)
2275 log.Printf("host %s resolved to ips %s, looking up tlsa records", host, ips)
2277 daneRequired, daneRecords, tlsaBaseDomain, err := smtpclient.GatherTLSA(ctxbg, c.log.Logger, resolver, host.Domain, expandedAuthentic, expandedHost)
2279 log.Printf("looking up tlsa records: %s, skipping", err)
2282 tlsMode := smtpclient.TLSRequiredStartTLS
2283 if len(daneRecords) == 0 {
2285 log.Printf("host %s has no tlsa records, skipping", expandedHost)
2288 log.Printf("warning: only unusable tlsa records found, continuing with required tls without certificate verification")
2292 for _, r := range daneRecords {
2293 l = append(l, r.String())
2295 log.Printf("tlsa records: %s", strings.Join(l, "; "))
2298 tlsHostnames := smtpclient.GatherTLSANames(haveMX, expandedNextHopAuthentic, expandedAuthentic, origNextHop, expandedNextHop, host.Domain, tlsaBaseDomain)
2300 for _, name := range tlsHostnames {
2301 l = append(l, name.String())
2303 log.Printf("gathered valid tls certificate names for potential verification with dane-ta: %s", strings.Join(l, ", "))
2305 dialer := &net.Dialer{Timeout: 5 * time.Second}
2306 conn, _, err := smtpclient.Dial(ctxbg, c.log.Logger, dialer, dns.IPDomain{Domain: expandedHost}, ips, 25, dialedIPs, nil)
2308 log.Printf("dial %s: %v, skipping", expandedHost, err)
2311 log.Printf("connected to %s, %s, starting smtp session with ehlo and starttls with dane verification", expandedHost, conn.RemoteAddr())
2313 var verifiedRecord adns.TLSA
2314 opts := smtpclient.Opts{
2315 DANERecords: daneRecords,
2316 DANEMoreHostnames: tlsHostnames[1:],
2317 DANEVerifiedRecord: &verifiedRecord,
2318 RootCAs: mox.Conf.Static.TLS.CertPool,
2321 sc, err := smtpclient.New(ctxbg, c.log.Logger, conn, tlsMode, tlsPKIX, ehloDomain, tlsHostnames[0], opts)
2323 log.Printf("setting up smtp session: %v, skipping", err)
2324 if xerr := conn.Close(); xerr != nil {
2325 log.Printf("closing connection: %v", xerr)
2330 smtpConn, err := sc.Conn()
2332 log.Fatalf("error: taking over smtp connection: %s", err)
2334 log.Printf("tls verified with tlsa record: %s", verifiedRecord)
2335 log.Printf("smtp session initialized and connected to stdin/stdout")
2338 _, err := io.Copy(os.Stdout, smtpConn)
2339 xcheckf(err, "copy from connection to stdout")
2340 if err := smtpConn.Close(); err != nil {
2341 log.Printf("closing smtp connection: %v", err)
2344 _, err = io.Copy(smtpConn, os.Stdin)
2345 xcheckf(err, "copy from stdin to connection")
2348 log.Fatalf("no remaining destinations")
2351func cmdDANEMakeRecord(c *cmd) {
2352 c.params = "usage selector matchtype [certificate.pem | publickey.pem | privatekey.pem]"
2353 c.help = `Print TLSA record for given certificate/key and parameters.
2356- usage: pkix-ta (0), pkix-ee (1), dane-ta (2), dane-ee (3)
2357- selector: cert (0), spki (1)
2358- matchtype: full (0), sha2-256 (1), sha2-512 (2)
2360Common DANE TLSA record parameters are: dane-ee spki sha2-256, or 3 1 1,
2361followed by a sha2-256 hash of the DER-encoded "SPKI" (subject public key info)
2362from the certificate. An example DNS zone file entry:
2364 _25._tcp.example.com. TLSA 3 1 1 133b919c9d65d8b1488157315327334ead8d83372db57465ecabf53ee5748aee
2366The first usable information from the pem file is used to compose the TLSA
2367record. In case of selector "cert", a certificate is required. Otherwise the
2368"subject public key info" (spki) of the first certificate or public or private
2369key (pkcs#8, pkcs#1 or ec private key) is used.
2377 var usage adns.TLSAUsage
2378 switch strings.ToLower(args[0]) {
2379 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
2380 usage = adns.TLSAUsagePKIXTA
2381 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
2382 usage = adns.TLSAUsagePKIXEE
2383 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
2384 usage = adns.TLSAUsageDANETA
2385 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
2386 usage = adns.TLSAUsageDANEEE
2388 if v, err := strconv.ParseUint(args[0], 10, 16); err != nil {
2389 log.Fatalf("bad usage %q", args[0])
2391 // Does not influence certificate association data, so we can accept other numbers.
2392 log.Printf("warning: continuing with unrecognized tlsa usage %d", v)
2393 usage = adns.TLSAUsage(v)
2397 var selector adns.TLSASelector
2398 switch strings.ToLower(args[1]) {
2399 case "cert", strconv.Itoa(int(adns.TLSASelectorCert)):
2400 selector = adns.TLSASelectorCert
2401 case "spki", strconv.Itoa(int(adns.TLSASelectorSPKI)):
2402 selector = adns.TLSASelectorSPKI
2404 log.Fatalf("bad selector %q", args[1])
2407 var matchType adns.TLSAMatchType
2408 switch strings.ToLower(args[2]) {
2409 case "full", strconv.Itoa(int(adns.TLSAMatchTypeFull)):
2410 matchType = adns.TLSAMatchTypeFull
2411 case "sha2-256", strconv.Itoa(int(adns.TLSAMatchTypeSHA256)):
2412 matchType = adns.TLSAMatchTypeSHA256
2413 case "sha2-512", strconv.Itoa(int(adns.TLSAMatchTypeSHA512)):
2414 matchType = adns.TLSAMatchTypeSHA512
2416 log.Fatalf("bad matchtype %q", args[2])
2419 buf, err := os.ReadFile(args[3])
2420 xcheckf(err, "reading certificate")
2422 var block *pem.Block
2423 block, buf = pem.Decode(buf)
2427 extra = " (with leftover data from pem file)"
2429 if selector == adns.TLSASelectorCert {
2430 log.Fatalf("no certificate found in pem file%s", extra)
2432 log.Fatalf("no certificate or public or private key found in pem file%s", extra)
2435 var cert *x509.Certificate
2437 if block.Type == "CERTIFICATE" {
2438 cert, err = x509.ParseCertificate(block.Bytes)
2439 xcheckf(err, "parse certificate")
2441 case adns.TLSASelectorCert:
2443 case adns.TLSASelectorSPKI:
2444 data = cert.RawSubjectPublicKeyInfo
2446 } else if selector == adns.TLSASelectorCert {
2447 // We need a certificate, just a public/private key won't do.
2448 log.Printf("skipping pem type %q, certificate is required", block.Type)
2451 var privKey, pubKey any
2455 _, err := x509.ParsePKIXPublicKey(block.Bytes)
2456 xcheckf(err, "parse pkix subject public key info (spki)")
2458 case "EC PRIVATE KEY":
2459 privKey, err = x509.ParseECPrivateKey(block.Bytes)
2460 xcheckf(err, "parse ec private key")
2461 case "RSA PRIVATE KEY":
2462 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
2463 xcheckf(err, "parse pkcs#1 rsa private key")
2464 case "RSA PUBLIC KEY":
2465 pubKey, err = x509.ParsePKCS1PublicKey(block.Bytes)
2466 xcheckf(err, "parse pkcs#1 rsa public key")
2468 // PKCS#8 private key
2469 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
2470 xcheckf(err, "parse pkcs#8 private key")
2472 log.Printf("skipping unrecognized pem type %q", block.Type)
2476 if pubKey == nil && privKey != nil {
2477 if signer, ok := privKey.(crypto.Signer); !ok {
2478 log.Fatalf("private key of type %T is not a signer, cannot get public key", privKey)
2480 pubKey = signer.Public()
2484 // Should not happen.
2485 log.Fatalf("internal error: did not find private or public key")
2487 data, err = x509.MarshalPKIXPublicKey(pubKey)
2488 xcheckf(err, "marshal pkix subject public key info (spki)")
2493 case adns.TLSAMatchTypeFull:
2494 case adns.TLSAMatchTypeSHA256:
2495 p := sha256.Sum256(data)
2497 case adns.TLSAMatchTypeSHA512:
2498 p := sha512.Sum512(data)
2501 fmt.Printf("%d %d %d %x\n", usage, selector, matchType, data)
2506func cmdDNSLookup(c *cmd) {
2507 c.params = "[ptr | mx | cname | ips | a | aaaa | ns | txt | srv | tlsa] name"
2508 c.help = `Lookup DNS name of given type.
2510Lookup always prints whether the response was DNSSEC-protected.
2514mox dns lookup ptr 1.1.1.1
2515mox dns lookup mx xmox.nl
2516mox dns lookup txt _dmarc.xmox.nl.
2517mox dns lookup tlsa _25._tcp.xmox.nl
2525 resolver := dns.StrictResolver{Pkg: "dns"}
2527 // like xparseDomain, but treat unparseable domain as an ASCII name so names with
2528 // underscores are still looked up, e,g <selector>._domainkey.<host>.
2529 xdomain := func(s string) dns.Domain {
2530 d, err := dns.ParseDomain(s)
2532 return dns.Domain{ASCII: strings.TrimSuffix(s, ".")}
2537 cmd, name := args[0], args[1]
2541 ip := xparseIP(name, "ip")
2542 ptrs, result, err := resolver.LookupAddr(context.Background(), ip.String())
2544 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2546 fmt.Printf("names (%d, %s):\n", len(ptrs), dnssecStatus(result.Authentic))
2547 for _, ptr := range ptrs {
2548 fmt.Printf("- %s\n", ptr)
2552 name := xdomain(name)
2553 mxl, result, err := resolver.LookupMX(context.Background(), name.ASCII+".")
2555 log.Printf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2556 // We can still have valid records...
2558 fmt.Printf("mx records (%d, %s):\n", len(mxl), dnssecStatus(result.Authentic))
2559 for _, mx := range mxl {
2560 fmt.Printf("- %s, preference %d\n", mx.Host, mx.Pref)
2564 name := xdomain(name)
2565 target, result, err := resolver.LookupCNAME(context.Background(), name.ASCII+".")
2567 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2569 fmt.Printf("%s (%s)\n", target, dnssecStatus(result.Authentic))
2571 case "ips", "a", "aaaa":
2575 } else if cmd == "aaaa" {
2578 name := xdomain(name)
2579 ips, result, err := resolver.LookupIP(context.Background(), network, name.ASCII+".")
2581 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2583 fmt.Printf("records (%d, %s):\n", len(ips), dnssecStatus(result.Authentic))
2584 for _, ip := range ips {
2585 fmt.Printf("- %s\n", ip)
2589 name := xdomain(name)
2590 nsl, result, err := resolver.LookupNS(context.Background(), name.ASCII+".")
2592 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2594 fmt.Printf("ns records (%d, %s):\n", len(nsl), dnssecStatus(result.Authentic))
2595 for _, ns := range nsl {
2596 fmt.Printf("- %s\n", ns)
2600 host := xdomain(name)
2601 l, result, err := resolver.LookupTXT(context.Background(), host.ASCII+".")
2603 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2605 fmt.Printf("txt records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2606 for _, txt := range l {
2607 fmt.Printf("- %s\n", txt)
2611 host := xdomain(name)
2612 _, l, result, err := resolver.LookupSRV(context.Background(), "", "", host.ASCII+".")
2614 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2616 fmt.Printf("srv records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2617 for _, srv := range l {
2618 fmt.Printf("- host %s, port %d, priority %d, weight %d\n", srv.Target, srv.Port, srv.Priority, srv.Weight)
2622 host := xdomain(name)
2623 l, result, err := resolver.LookupTLSA(context.Background(), 0, "", host.ASCII+".")
2625 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2627 fmt.Printf("tlsa records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2628 for _, tlsa := range l {
2629 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)
2632 log.Fatalf("unknown record type %q", args[0])
2636func cmdDKIMGened25519(c *cmd) {
2637 c.params = ">$selector._domainkey.$domain.ed25519.privatekey.pkcs8.pem"
2638 c.help = `Generate a new ed25519 key for use with DKIM.
2640Ed25519 keys are much smaller than RSA keys of comparable cryptographic
2641strength. This is convenient because of maximum DNS message sizes. At the time
2642of writing, not many mail servers appear to support ed25519 DKIM keys though,
2643so it is recommended to sign messages with both RSA and ed25519 keys.
2645 if len(c.Parse()) != 0 {
2649 buf, err := admin.MakeDKIMEd25519Key(dns.Domain{}, dns.Domain{})
2650 xcheckf(err, "making dkim ed25519 key")
2651 _, err = os.Stdout.Write(buf)
2652 xcheckf(err, "writing dkim ed25519 key")
2655func cmdDKIMTXT(c *cmd) {
2656 c.params = "<$selector._domainkey.$domain.key.pkcs8.pem"
2657 c.help = `Print a DKIM DNS TXT record with the public key derived from the private key read from stdin.
2659The DNS should be configured as a TXT record at $selector._domainkey.$domain.
2661 if len(c.Parse()) != 0 {
2665 privKey, err := parseDKIMKey(os.Stdin)
2666 xcheckf(err, "reading dkim private key from stdin")
2670 Hashes: []string{"sha256"},
2671 Flags: []string{"s"},
2674 switch key := privKey.(type) {
2675 case *rsa.PrivateKey:
2676 r.PublicKey = key.Public()
2677 case ed25519.PrivateKey:
2678 r.PublicKey = key.Public()
2681 log.Fatalf("unsupported private key type %T, must be rsa or ed25519", privKey)
2684 record, err := r.Record()
2685 xcheckf(err, "making record")
2686 fmt.Print("<selector>._domainkey.<your.domain.> TXT ")
2690 s, record = record[:100], record[100:]
2694 fmt.Printf(`"%s" `, s)
2699func parseDKIMKey(r io.Reader) (any, error) {
2700 buf, err := io.ReadAll(r)
2702 return nil, fmt.Errorf("reading pem from stdin: %v", err)
2704 b, _ := pem.Decode(buf)
2706 return nil, fmt.Errorf("decoding pem: %v", err)
2708 privKey, err := x509.ParsePKCS8PrivateKey(b.Bytes)
2710 return nil, fmt.Errorf("parsing private key: %v", err)
2715func cmdDKIMVerify(c *cmd) {
2716 c.params = "message"
2717 c.help = `Verify the DKIM signatures in a message and print the results.
2719The message is parsed, and the DKIM-Signature headers are validated. Validation
2720of older messages may fail because the DNS records have been removed or changed
2721by now, or because the signature header may have specified an expiration time
2729 msgf, err := os.Open(args[0])
2730 xcheckf(err, "open message")
2732 results, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, false, dkim.DefaultPolicy, msgf, true)
2733 xcheckf(err, "dkim verify")
2735 for _, result := range results {
2737 if result.Sig == nil {
2738 log.Printf("warning: could not parse signature")
2740 sigh, err = result.Sig.Header()
2742 log.Printf("warning: packing signature: %s", err)
2746 if result.Record == nil {
2747 log.Printf("warning: missing DNS record")
2749 txt, err = result.Record.Record()
2751 log.Printf("warning: packing record: %s", err)
2754 fmt.Printf("status %q, err %v\nrecord %q\nheader %s\n", result.Status, result.Err, txt, sigh)
2758func cmdDKIMSign(c *cmd) {
2759 c.params = "message"
2760 c.help = `Sign a message, adding DKIM-Signature headers based on the domain in the From header.
2762The message is parsed, the domain looked up in the configuration files, and
2763DKIM-Signature headers generated. The message is printed with the DKIM-Signature
2771 msgf, err := os.Open(args[0])
2772 xcheckf(err, "open message")
2774 if err := msgf.Close(); err != nil {
2775 log.Printf("closing message file: %v", err)
2779 p, err := message.Parse(c.log.Logger, true, msgf)
2780 xcheckf(err, "parsing message")
2782 if len(p.Envelope.From) != 1 {
2783 log.Fatalf("found %d from headers, need exactly 1", len(p.Envelope.From))
2785 localpart, err := smtp.ParseLocalpart(p.Envelope.From[0].User)
2786 xcheckf(err, "parsing localpart of address in from-header")
2787 dom := xparseDomain(p.Envelope.From[0].Host, "domain of address in from-header")
2791 domConf, ok := mox.Conf.Domain(dom)
2793 log.Fatalf("domain %s not configured", dom)
2796 selectors := mox.DKIMSelectors(domConf.DKIM)
2797 headers, err := dkim.Sign(context.Background(), c.log.Logger, localpart, dom, selectors, false, msgf)
2798 xcheckf(err, "signing message with dkim")
2800 log.Fatalf("no DKIM configured for domain %s", dom)
2802 _, err = fmt.Fprint(os.Stdout, headers)
2803 xcheckf(err, "write headers")
2804 _, err = io.Copy(os.Stdout, msgf)
2805 xcheckf(err, "write message")
2808func cmdDKIMLookup(c *cmd) {
2809 c.params = "selector domain"
2810 c.help = "Lookup and print the DKIM record for the selector at the domain."
2816 selector := xparseDomain(args[0], "selector")
2817 domain := xparseDomain(args[1], "domain")
2819 status, record, txt, authentic, err := dkim.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, selector, domain)
2821 fmt.Printf("error: %s\n", err)
2823 if status != dkim.StatusNeutral {
2824 fmt.Printf("status: %s\n", status)
2827 fmt.Printf("TXT record: %s\n", txt)
2830 fmt.Println("dnssec-signed: yes")
2832 fmt.Println("dnssec-signed: no")
2835 fmt.Printf("Record:\n")
2837 "version", record.Version,
2838 "hashes", record.Hashes,
2840 "notes", record.Notes,
2841 "services", record.Services,
2842 "flags", record.Flags,
2844 for i := 0; i < len(pairs); i += 2 {
2845 fmt.Printf("\t%s: %v\n", pairs[i], pairs[i+1])
2850func cmdDMARCLookup(c *cmd) {
2852 c.help = "Lookup dmarc policy for domain, a DNS TXT record at _dmarc.<domain>, validate and print it."
2858 fromdomain := xparseDomain(args[0], "domain")
2859 _, domain, _, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, fromdomain)
2860 xcheckf(err, "dmarc lookup domain %s", fromdomain)
2861 fmt.Printf("dmarc record at domain %s: %s\n", domain, txt)
2862 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2865func dnssecStatus(v bool) string {
2867 return "with dnssec"
2869 return "without dnssec"
2872func cmdDMARCVerify(c *cmd) {
2873 c.params = "remoteip mailfromaddress helodomain < message"
2874 c.help = `Parse an email message and evaluate it against the DMARC policy of the domain in the From-header.
2876mailfromaddress and helodomain are used for SPF validation. If both are empty,
2877SPF validation is skipped.
2879mailfromaddress should be the address used as MAIL FROM in the SMTP session.
2880For DSN messages, that address may be empty. The helo domain was specified at
2881the beginning of the SMTP transaction that delivered the message. These values
2882can be found in message headers.
2889 var heloDomain *dns.Domain
2891 remoteIP := xparseIP(args[0], "remoteip")
2893 var mailfrom *smtp.Address
2895 a, err := smtp.ParseAddress(args[1])
2896 xcheckf(err, "parsing mailfrom address")
2900 d := xparseDomain(args[2], "helo domain")
2903 var received *spf.Received
2904 spfStatus := spf.StatusNone
2905 var spfIdentity *dns.Domain
2906 if mailfrom != nil || heloDomain != nil {
2907 spfArgs := spf.Args{
2909 LocalIP: net.ParseIP("127.0.0.1"),
2910 LocalHostname: dns.Domain{ASCII: "localhost"},
2912 if mailfrom != nil {
2913 spfArgs.MailFromLocalpart = mailfrom.Localpart
2914 spfArgs.MailFromDomain = mailfrom.Domain
2916 if heloDomain != nil {
2917 spfArgs.HelloDomain = dns.IPDomain{Domain: *heloDomain}
2919 rspf, spfDomain, expl, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfArgs)
2921 log.Printf("spf verify: %v (explanation: %q, authentic %v)", err, expl, authentic)
2924 spfStatus = received.Result
2925 // todo: should probably potentially do two separate spf validations
2926 if mailfrom != nil {
2927 spfIdentity = &mailfrom.Domain
2929 spfIdentity = heloDomain
2931 fmt.Printf("spf result: %s: %s (%s)\n", spfDomain, spfStatus, dnssecStatus(authentic))
2935 data, err := io.ReadAll(os.Stdin)
2936 xcheckf(err, "read message")
2937 dmarcFrom, _, _, err := message.From(c.log.Logger, false, bytes.NewReader(data), nil)
2938 xcheckf(err, "extract dmarc from message")
2940 const ignoreTestMode = false
2941 dkimResults, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, true, func(*dkim.Sig) error { return nil }, bytes.NewReader(data), ignoreTestMode)
2942 xcheckf(err, "dkim verify")
2943 for _, r := range dkimResults {
2944 fmt.Printf("dkim result: %q (err %v)\n", r.Status, r.Err)
2947 _, result := dmarc.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, dmarcFrom.Domain, dkimResults, spfStatus, spfIdentity, false)
2948 xcheckf(result.Err, "dmarc verify")
2949 fmt.Printf("dmarc from: %s\ndmarc status: %q\ndmarc reject: %v\ncmarc record: %s\n", dmarcFrom, result.Status, result.Reject, result.Record)
2952func cmdDMARCCheckreportaddrs(c *cmd) {
2954 c.help = `For each reporting address in the domain's DMARC record, check if it has opted into receiving reports (if needed).
2956A DMARC record can request reports about DMARC evaluations to be sent to an
2957email/http address. If the organizational domains of that of the DMARC record
2958and that of the report destination address do not match, the destination
2959address must opt-in to receiving DMARC reports by creating a DMARC record at
2960<dmarcdomain>._report._dmarc.<reportdestdomain>.
2967 dom := xparseDomain(args[0], "domain")
2968 _, domain, record, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dom)
2969 xcheckf(err, "dmarc lookup domain %s", dom)
2970 fmt.Printf("dmarc record at domain %s: %q\n", domain, txt)
2971 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2973 check := func(kind, addr string) {
2976 printResult := func(format string, args ...any) {
2977 fmt.Printf("%s %s: %s (%s)\n", kind, addr, fmt.Sprintf(format, args...), dnssecStatus(authentic))
2980 u, err := url.Parse(addr)
2982 printResult("parsing uri: %v (skipping)", addr, err)
2985 var destdom dns.Domain
2988 a, err := smtp.ParseAddress(u.Opaque)
2990 printResult("parsing destination email address %s: %v (skipping)", u.Opaque, err)
2995 printResult("unrecognized scheme in reporting address %s (skipping)", u.Scheme)
2999 if publicsuffix.Lookup(context.Background(), c.log.Logger, dom) == publicsuffix.Lookup(context.Background(), c.log.Logger, destdom) {
3000 printResult("pass (same organizational domain)")
3004 accepts, status, _, txts, authentic, err := dmarc.LookupExternalReportsAccepted(context.Background(), c.log.Logger, dns.StrictResolver{}, domain, destdom)
3006 txtaddr := fmt.Sprintf("%s._report._dmarc.%s", domain.ASCII, destdom.ASCII)
3008 txtstr = fmt.Sprintf(" (no txt records %s)", txtaddr)
3010 txtstr = fmt.Sprintf(" (txt record %s: %q)", txtaddr, txts)
3012 if status != dmarc.StatusNone {
3013 printResult("fail: %s%s", err, txtstr)
3015 printResult("pass%s", txtstr)
3016 } else if err != nil {
3017 printResult("fail: %s%s", err, txtstr)
3019 printResult("fail%s", txtstr)
3023 for _, uri := range record.AggregateReportAddresses {
3024 check("aggregate reporting", uri.Address)
3026 for _, uri := range record.FailureReportAddresses {
3027 check("failure reporting", uri.Address)
3031func cmdDMARCParsereportmsg(c *cmd) {
3032 c.params = "message ..."
3033 c.help = `Parse a DMARC report from an email message, and print its extracted details.
3035DMARC reports are periodically mailed, if requested in the DMARC DNS record of
3036a domain. Reports are sent by mail servers that received messages with our
3037domain in a From header. This may or may not be legatimate email. DMARC reports
3038contain summaries of evaluations of DMARC and DKIM/SPF, which can help
3039understand email deliverability problems.
3046 for _, arg := range args {
3047 f, err := os.Open(arg)
3048 xcheckf(err, "open %q", arg)
3049 feedback, err := dmarcrpt.ParseMessageReport(c.log.Logger, f)
3050 xcheckf(err, "parse report in %q", arg)
3051 meta := feedback.ReportMetadata
3052 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)
3053 if len(meta.Errors) > 0 {
3054 fmt.Printf("Errors:\n")
3055 for _, s := range meta.Errors {
3056 fmt.Printf("\t- %s\n", s)
3059 pol := feedback.PolicyPublished
3060 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)
3061 for _, record := range feedback.Records {
3062 idents := record.Identifiers
3063 fmt.Printf("\theaderfrom %q, envelopes from %q, to %q\n", idents.HeaderFrom, idents.EnvelopeFrom, idents.EnvelopeTo)
3064 eval := record.Row.PolicyEvaluated
3066 for _, reason := range eval.Reasons {
3067 reasons += "; " + string(reason.Type)
3068 if reason.Comment != "" {
3069 reasons += fmt.Sprintf(": %q", reason.Comment)
3072 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)
3073 for _, dkim := range record.AuthResults.DKIM {
3075 if dkim.HumanResult != "" {
3076 result = fmt.Sprintf(": %q", dkim.HumanResult)
3078 fmt.Printf("\t\tdkim %s; domain %q selector %q%s\n", dkim.Result, dkim.Domain, dkim.Selector, result)
3080 for _, spf := range record.AuthResults.SPF {
3081 fmt.Printf("\t\tspf %s; domain %q scope %q\n", spf.Result, spf.Domain, spf.Scope)
3087func cmdDMARCDBAddReport(c *cmd) {
3089 c.params = "fromdomain < message"
3090 c.help = "Add a DMARC report to the database."
3098 fromdomain := xparseDomain(args[0], "domain")
3099 fmt.Fprintln(os.Stderr, "reading report message from stdin")
3100 report, err := dmarcrpt.ParseMessageReport(c.log.Logger, os.Stdin)
3101 xcheckf(err, "parse message")
3102 err = dmarcdb.AddReport(context.Background(), report, fromdomain)
3103 xcheckf(err, "add dmarc report")
3106func cmdTLSRPTLookup(c *cmd) {
3108 c.help = `Lookup the TLSRPT record for the domain.
3110A TLSRPT record typically contains an email address where reports about TLS
3111connectivity should be sent. Mail servers attempting delivery to our domain
3112should attempt to use TLS. TLSRPT lets them report how many connection
3113successfully used TLS, and how what kind of errors occurred otherwise.
3120 d := xparseDomain(args[0], "domain")
3121 _, txt, err := tlsrpt.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, d)
3122 xcheckf(err, "tlsrpt lookup for %s", d)
3126func cmdTLSRPTParsereportmsg(c *cmd) {
3127 c.params = "message ..."
3128 c.help = `Parse and print the TLSRPT in the message.
3130The report is printed in formatted JSON.
3137 for _, arg := range args {
3138 f, err := os.Open(arg)
3139 xcheckf(err, "open %q", arg)
3140 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, f)
3141 xcheckf(err, "parse report in %q", arg)
3142 // todo future: only print the highlights?
3143 enc := json.NewEncoder(os.Stdout)
3144 enc.SetIndent("", "\t")
3145 enc.SetEscapeHTML(false)
3146 err = enc.Encode(reportJSON)
3147 xcheckf(err, "write report")
3151func cmdSPFCheck(c *cmd) {
3152 c.params = "domain ip"
3153 c.help = `Check the status of IP for the policy published in DNS for the domain.
3155IPs may be allowed to send for a domain, or disallowed, and several shades in
3156between. If not allowed, an explanation may be provided by the policy. If so,
3157the explanation is printed. The SPF mechanism that matched (if any) is also
3165 domain := xparseDomain(args[0], "domain")
3167 ip := xparseIP(args[1], "ip")
3169 spfargs := spf.Args{
3171 MailFromLocalpart: "user",
3172 MailFromDomain: domain,
3173 HelloDomain: dns.IPDomain{Domain: domain},
3174 LocalIP: net.ParseIP("127.0.0.1"),
3175 LocalHostname: dns.Domain{ASCII: "localhost"},
3177 r, _, explanation, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfargs)
3179 fmt.Printf("error: %s\n", err)
3181 if explanation != "" {
3182 fmt.Printf("explanation: %s\n", explanation)
3184 fmt.Printf("status: %s (%s)\n", r.Result, dnssecStatus(authentic))
3185 if r.Mechanism != "" {
3186 fmt.Printf("mechanism: %s\n", r.Mechanism)
3190func cmdSPFParse(c *cmd) {
3191 c.params = "txtrecord"
3192 c.help = "Parse the record as SPF record. If valid, nothing is printed."
3198 _, _, err := spf.ParseRecord(args[0])
3199 xcheckf(err, "parsing record")
3202func cmdSPFLookup(c *cmd) {
3204 c.help = "Lookup the SPF record for the domain and print it."
3210 domain := xparseDomain(args[0], "domain")
3211 _, txt, _, authentic, err := spf.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
3212 xcheckf(err, "spf lookup for %s", domain)
3214 fmt.Printf("(%s)\n", dnssecStatus(authentic))
3217func cmdMTASTSLookup(c *cmd) {
3219 c.help = `Lookup the MTASTS record and policy for the domain.
3221MTA-STS is a mechanism for a domain to specify if it requires TLS connections
3222for delivering email. If a domain has a valid MTA-STS DNS TXT record at
3223_mta-sts.<domain> it signals it implements MTA-STS. A policy can then be
3224fetched at https://mta-sts.<domain>/.well-known/mta-sts.txt. The policy
3225specifies the mode (enforce, testing, none), which MX servers support TLS and
3226should be used, and how long the policy can be cached.
3233 domain := xparseDomain(args[0], "domain")
3235 record, policy, _, err := mtasts.Get(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
3237 fmt.Printf("error: %s\n", err)
3240 fmt.Printf("DNS TXT record _mta-sts.%s: %s\n", domain.ASCII, record.String())
3244 fmt.Printf("policy at https://mta-sts.%s/.well-known/mta-sts.txt:\n", domain.ASCII)
3245 fmt.Printf("%s", policy.String())
3249func cmdRDAPDomainage(c *cmd) {
3251 c.help = `Lookup the age of domain in RDAP based on latest registration.
3253RDAP is the registration data access protocol. Registries run RDAP services for
3254their top level domains, providing information such as the registration date of
3255domains. This command looks up the "age" of a domain by looking at the most
3256recent "registration", "reregistration" or "reinstantiation" event.
3258Email messages from recently registered domains are often treated with
3259suspicion, and some mail systems are more likely to classify them as junk.
3261On each invocation, a bootstrap file with a list of registries (of top-level
3262domains) is retrieved, without caching. Do not run this command too often with
3270 domain := xparseDomain(args[0], "domain")
3272 registration, err := rdap.LookupLastDomainRegistration(context.Background(), c.log, domain)
3273 xcheckf(err, "looking up domain in rdap")
3275 age := time.Since(registration)
3276 const day = 24 * time.Hour
3277 const year = 365 * day
3279 days := (age - years*year) / day
3283 } else if years > 0 {
3284 s = fmt.Sprintf("%d years, ", years)
3289 s += fmt.Sprintf("%d days", days)
3294func cmdRetrain(c *cmd) {
3295 c.params = "[accountname]"
3296 c.help = `Recreate and retrain the junk filter for the account or all accounts.
3298Useful after having made changes to the junk filter configuration, or if the
3299implementation has changed.
3311 ctlcmdRetrain(xctl(), account)
3314func ctlcmdRetrain(ctl *ctl, account string) {
3315 ctl.xwrite("retrain")
3320func cmdTLSRPTDBAddReport(c *cmd) {
3322 c.params = "< message"
3323 c.help = "Parse a TLS report from the message and add it to the database."
3325 c.flag.BoolVar(&hostReport, "hostreport", false, "report for a host instead of domain")
3333 // First read message, to get the From-header. Then parse it as TLSRPT.
3334 fmt.Fprintln(os.Stderr, "reading report message from stdin")
3335 buf, err := io.ReadAll(os.Stdin)
3336 xcheckf(err, "reading message")
3337 part, err := message.Parse(c.log.Logger, true, bytes.NewReader(buf))
3338 xcheckf(err, "parsing message")
3339 if part.Envelope == nil || len(part.Envelope.From) != 1 {
3340 log.Fatalf("message must have one From-header")
3342 from := part.Envelope.From[0]
3343 domain := xparseDomain(from.Host, "domain")
3345 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, bytes.NewReader(buf))
3346 xcheckf(err, "parsing tls report in message")
3348 mailfrom := from.User + "@" + from.Host // todo future: should escape and such
3349 report := reportJSON.Convert()
3350 err = tlsrptdb.AddReport(context.Background(), c.log, domain, mailfrom, hostReport, &report)
3351 xcheckf(err, "add tls report to database")
3354func cmdDNSBLCheck(c *cmd) {
3355 c.params = "zone ip"
3356 c.help = `Test if IP is in the DNS blocklist of the zone, e.g. bl.spamcop.net.
3358If the IP is in the blocklist, an explanation is printed. This is typically a
3359URL with more information.
3366 zone := xparseDomain(args[0], "zone")
3367 ip := xparseIP(args[1], "ip")
3369 status, explanation, err := dnsbl.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, zone, ip)
3370 fmt.Printf("status: %s\n", status)
3371 if status == dnsbl.StatusFail {
3372 fmt.Printf("explanation: %q\n", explanation)
3375 fmt.Printf("error: %s\n", err)
3379func cmdDNSBLCheckhealth(c *cmd) {
3381 c.help = `Check the health of the DNS blocklist represented by zone, e.g. bl.spamcop.net.
3383The health of a DNS blocklist can be checked by querying for 127.0.0.1 and
3384127.0.0.2. The second must and the first must not be present.
3391 zone := xparseDomain(args[0], "zone")
3392 err := dnsbl.CheckHealth(context.Background(), c.log.Logger, dns.StrictResolver{}, zone)
3393 xcheckf(err, "unhealthy")
3394 fmt.Println("healthy")
3397func cmdCheckupdate(c *cmd) {
3398 c.help = `Check if a newer version of mox is available.
3400A single DNS TXT lookup to _updates.xmox.nl tells if a new version is
3401available. If so, a changelog is fetched from https://updates.xmox.nl, and the
3402individual entries verified with a builtin public key. The changelog is
3405 if len(c.Parse()) != 0 {
3410 current, lastknown, _, err := store.LastKnown()
3412 log.Printf("getting last known version: %s", err)
3414 fmt.Printf("last known version: %s\n", lastknown)
3415 fmt.Printf("current version: %s\n", current)
3417 latest, _, err := updates.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dns.Domain{ASCII: changelogDomain})
3418 xcheckf(err, "lookup of latest version")
3419 fmt.Printf("latest version: %s\n", latest)
3421 if latest.After(current) {
3422 changelog, err := updates.FetchChangelog(context.Background(), c.log.Logger, changelogURL, current, changelogPubKey)
3423 xcheckf(err, "fetching changelog")
3424 if len(changelog.Changes) == 0 {
3425 log.Printf("no changes in changelog")
3428 fmt.Println("Changelog")
3429 for _, c := range changelog.Changes {
3430 fmt.Println("\n" + strings.TrimSpace(c.Text))
3435func cmdCid(c *cmd) {
3437 c.help = `Turn an ID from a Received header into a cid, for looking up in logs.
3439A cid is essentially a connection counter initialized when mox starts. Each log
3440line contains a cid. Received headers added by mox contain a unique ID that can
3441be decrypted to a cid by admin of a mox instance only.
3449 recvidpath := mox.DataDirPath("receivedid.key")
3450 recvidbuf, err := os.ReadFile(recvidpath)
3451 xcheckf(err, "reading %s", recvidpath)
3452 if len(recvidbuf) != 16+8 {
3453 log.Fatalf("bad data in %s: got %d bytes, expect 16+8=24", recvidpath, len(recvidbuf))
3455 err = mox.ReceivedIDInit(recvidbuf[:16], recvidbuf[16:])
3456 xcheckf(err, "init receivedid")
3458 cid, err := mox.ReceivedToCid(args[0])
3459 xcheckf(err, "received id to cid")
3460 fmt.Printf("%x\n", cid)
3463func cmdVersion(c *cmd) {
3464 c.help = "Prints this mox version."
3465 if len(c.Parse()) != 0 {
3468 fmt.Println(moxvar.Version)
3469 fmt.Printf("%s/%s\n", runtime.GOOS, runtime.GOARCH)
3472func cmdWebapi(c *cmd) {
3473 c.params = "[method [baseurl-with-credentials]"
3474 c.help = "Lists available methods, prints request/response parameters for method, or calls a method with a request read from standard input."
3480 t := reflect.TypeFor[webapi.Methods]()
3481 methods := map[string]reflect.Type{}
3483 for i := range t.NumMethod() {
3485 methods[mt.Name] = mt.Type
3486 ml = append(ml, mt.Name)
3490 fmt.Println(strings.Join(ml, "\n"))
3494 mt, ok := methods[args[0]]
3496 log.Fatalf("unknown method %q", args[0])
3498 resultNotJSON := mt.Out(0).Kind() == reflect.Interface
3501 fmt.Println("# Example request")
3503 printJSON("\t", mox.FillExample(nil, reflect.New(mt.In(1))).Interface())
3506 fmt.Println("Output is non-JSON data.")
3509 fmt.Println("# Example response")
3511 printJSON("\t", mox.FillExample(nil, reflect.New(mt.Out(0))).Interface())
3517 response = reflect.New(mt.Out(0))
3520 fmt.Fprintln(os.Stderr, "reading request from stdin...")
3521 request, err := io.ReadAll(os.Stdin)
3522 xcheckf(err, "read message")
3524 dec := json.NewDecoder(bytes.NewReader(request))
3525 dec.DisallowUnknownFields()
3526 err = dec.Decode(reflect.New(mt.In(1)).Interface())
3527 xcheckf(err, "parsing request")
3529 resp, err := http.PostForm(args[1]+args[0], url.Values{"request": []string{string(request)}})
3530 xcheckf(err, "http post")
3532 if err := resp.Body.Close(); err != nil {
3533 log.Printf("closing http response body: %v", err)
3536 if resp.StatusCode == http.StatusBadRequest {
3537 buf, err := io.ReadAll(&moxio.LimitReader{R: resp.Body, Limit: 10 * 1024})
3538 xcheckf(err, "reading response for 400 bad request error")
3539 err = json.Unmarshal(buf, &response)
3541 printJSON("", response)
3543 fmt.Fprintf(os.Stderr, "(not json)\n")
3544 os.Stderr.Write(buf)
3547 } else if resp.StatusCode != http.StatusOK {
3548 fmt.Fprintf(os.Stderr, "http response %s\n", resp.Status)
3549 _, err := io.Copy(os.Stderr, resp.Body)
3550 xcheckf(err, "copy body")
3552 err := json.NewDecoder(resp.Body).Decode(&resp)
3553 xcheckf(err, "unmarshal response")
3554 printJSON("", response)
3558func printJSON(indent string, v any) {
3559 fmt.Printf("%s", indent)
3560 enc := json.NewEncoder(os.Stdout)
3561 enc.SetIndent(indent, "\t")
3562 enc.SetEscapeHTML(false)
3563 err := enc.Encode(v)
3564 xcheckf(err, "encode json")
3567// 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.
3568func cmdBumpUIDValidity(c *cmd) {
3569 c.params = "account [mailbox]"
3570 c.help = `Change the IMAP UID validity of the mailbox, causing IMAP clients to refetch messages.
3572This can be useful after manually repairing metadata about the account/mailbox.
3574Opens account database file directly. Ensure mox does not have the account
3575open, or is not running.
3578 if len(args) != 1 && len(args) != 2 {
3583 a, err := store.OpenAccount(c.log, args[0], false)
3584 xcheckf(err, "open account")
3586 if err := a.Close(); err != nil {
3587 log.Printf("closing account: %v", err)
3591 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3592 uidvalidity, err := a.NextUIDValidity(tx)
3594 return fmt.Errorf("assigning next uid validity: %v", err)
3597 q := bstore.QueryTx[store.Mailbox](tx)
3598 q.FilterEqual("Expunged", false)
3600 q.FilterEqual("Name", args[1])
3602 mbl, err := q.SortAsc("Name").List()
3604 return fmt.Errorf("looking up mailbox: %v", err)
3606 if len(args) == 2 && len(mbl) != 1 {
3607 return fmt.Errorf("looking up mailbox %q, found %d mailboxes", args[1], len(mbl))
3609 for _, mb := range mbl {
3610 mb.UIDValidity = uidvalidity
3611 err = tx.Update(&mb)
3613 return fmt.Errorf("updating uid validity for mailbox: %v", err)
3615 fmt.Printf("uid validity for %q updated to %d\n", mb.Name, uidvalidity)
3619 xcheckf(err, "updating database")
3622func cmdReassignUIDs(c *cmd) {
3623 c.params = "account [mailboxid]"
3624 c.help = `Reassign UIDs in one mailbox or all mailboxes in an account and bump UID validity, causing IMAP clients to refetch messages.
3626Opens account database file directly. Ensure mox does not have the account
3627open, or is not running.
3630 if len(args) != 1 && len(args) != 2 {
3637 mailboxID, err = strconv.ParseInt(args[1], 10, 64)
3638 xcheckf(err, "parsing mailbox id")
3642 a, err := store.OpenAccount(c.log, args[0], false)
3643 xcheckf(err, "open account")
3645 if err := a.Close(); err != nil {
3646 log.Printf("closing account: %v", err)
3650 // Gather the last-assigned UIDs per mailbox.
3651 uidlasts := map[int64]store.UID{}
3653 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3654 // Reassign UIDs, going per mailbox. We assign starting at 1, only changing the
3655 // message if it isn't already at the intended UID. Doing it in this order ensures
3656 // we don't get into trouble with duplicate UIDs for a mailbox. We assign a new
3657 // modseq. Not strictly needed, but doesn't hurt. It's also why we assign a UID to
3658 // expunged messages.
3659 modseq, err := a.NextModSeq(tx)
3660 xcheckf(err, "assigning next modseq")
3662 q := bstore.QueryTx[store.Message](tx)
3664 q.FilterNonzero(store.Message{MailboxID: mailboxID})
3666 q.SortAsc("MailboxID", "UID")
3667 err = q.ForEach(func(m store.Message) error {
3668 uidlasts[m.MailboxID]++
3669 uid := uidlasts[m.MailboxID]
3673 if err := tx.Update(&m); err != nil {
3674 return fmt.Errorf("updating uid for message: %v", err)
3680 return fmt.Errorf("reading through messages: %v", err)
3683 // Now update the uidnext, uidvalidity and modseq for each mailbox.
3684 err = bstore.QueryTx[store.Mailbox](tx).FilterEqual("Expunged", false).ForEach(func(mb store.Mailbox) error {
3685 // Assign each mailbox a completely new uidvalidity.
3686 uidvalidity, err := a.NextUIDValidity(tx)
3688 return fmt.Errorf("assigning next uid validity: %v", err)
3691 if mb.UIDValidity >= uidvalidity {
3692 // This should not happen, but since we're fixing things up after a hypothetical
3693 // mishap, might as well account for inconsistent uidvalidity.
3694 next := store.NextUIDValidity{ID: 1, Next: mb.UIDValidity + 2}
3695 if err := tx.Update(&next); err != nil {
3696 log.Printf("updating nextuidvalidity: %v, continuing", err)
3700 mb.UIDValidity = uidvalidity
3702 mb.UIDNext = uidlasts[mb.ID] + 1
3704 if err := tx.Update(&mb); err != nil {
3705 return fmt.Errorf("updating uidvalidity and uidnext for mailbox: %v", err)
3710 return fmt.Errorf("updating mailboxes: %v", err)
3714 xcheckf(err, "updating database")
3717func cmdFixUIDMeta(c *cmd) {
3718 c.params = "account"
3719 c.help = `Fix inconsistent UIDVALIDITY and UIDNEXT in messages/mailboxes/account.
3721The next UID to use for a message in a mailbox should always be higher than any
3722existing message UID in the mailbox. If it is not, the mailbox UIDNEXT is
3725Each mailbox has a UIDVALIDITY sequence number, which should always be lower
3726than the per-account next UIDVALIDITY to use. If it is not, the account next
3727UIDVALIDITY is updated.
3729Opens account database file directly. Ensure mox does not have the account
3730open, or is not running.
3738 a, err := store.OpenAccount(c.log, args[0], false)
3739 xcheckf(err, "open account")
3741 if err := a.Close(); err != nil {
3742 log.Printf("closing account: %v", err)
3746 var maxUIDValidity uint32
3748 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3749 // We look at each mailbox, retrieve its max UID and compare against the mailbox
3751 err := bstore.QueryTx[store.Mailbox](tx).FilterEqual("Expunged", false).ForEach(func(mb store.Mailbox) error {
3752 if mb.UIDValidity > maxUIDValidity {
3753 maxUIDValidity = mb.UIDValidity
3755 m, err := bstore.QueryTx[store.Message](tx).FilterNonzero(store.Message{MailboxID: mb.ID}).SortDesc("UID").Limit(1).Get()
3756 if err == bstore.ErrAbsent || err == nil && m.UID < mb.UIDNext {
3758 } else if err != nil {
3759 return fmt.Errorf("finding message with max uid in mailbox: %w", err)
3761 olduidnext := mb.UIDNext
3762 mb.UIDNext = m.UID + 1
3763 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)
3764 if err := tx.Update(&mb); err != nil {
3765 return fmt.Errorf("updating mailbox uidnext: %v", err)
3770 return fmt.Errorf("processing mailboxes: %v", err)
3773 uidvalidity := store.NextUIDValidity{ID: 1}
3774 if err := tx.Get(&uidvalidity); err != nil {
3775 return fmt.Errorf("reading account next uidvalidity: %v", err)
3777 if maxUIDValidity >= uidvalidity.Next {
3778 log.Printf("account next uidvalidity %d <= highest uidvalidity %d found in mailbox, resetting account next uidvalidity to %d", uidvalidity.Next, maxUIDValidity, maxUIDValidity+1)
3779 uidvalidity.Next = maxUIDValidity + 1
3780 if err := tx.Update(&uidvalidity); err != nil {
3781 return fmt.Errorf("updating account next uidvalidity: %v", err)
3787 xcheckf(err, "updating database")
3790func cmdFixmsgsize(c *cmd) {
3791 c.params = "[account]"
3792 c.help = `Ensure message sizes in the database matching the sum of the message prefix length and on-disk file size.
3794Messages with an inconsistent size are also parsed again.
3796If an inconsistency is found, you should probably also run "mox
3797bumpuidvalidity" on the mailboxes or entire account to force IMAP clients to
3810 ctlcmdFixmsgsize(xctl(), account)
3813func ctlcmdFixmsgsize(ctl *ctl, account string) {
3814 ctl.xwrite("fixmsgsize")
3817 ctl.xstreamto(os.Stdout)
3820func cmdReparse(c *cmd) {
3821 c.params = "[account]"
3822 c.help = `Parse all messages in the account or all accounts again.
3824Can be useful after upgrading mox with improved message parsing. Messages are
3825parsed in batches, so other access to the mailboxes/messages are not blocked
3826while reparsing all messages.
3838 ctlcmdReparse(xctl(), account)
3841func ctlcmdReparse(ctl *ctl, account string) {
3842 ctl.xwrite("reparse")
3845 ctl.xstreamto(os.Stdout)
3848func cmdEnsureParsed(c *cmd) {
3849 c.params = "account"
3850 c.help = "Ensure messages in the database have a pre-parsed MIME form in the database."
3852 c.flag.BoolVar(&all, "all", false, "store new parsed message for all messages")
3859 a, err := store.OpenAccount(c.log, args[0], false)
3860 xcheckf(err, "open account")
3862 if err := a.Close(); err != nil {
3863 log.Printf("closing account: %v", err)
3868 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3869 q := bstore.QueryTx[store.Message](tx)
3870 q.FilterEqual("Expunged", false)
3871 q.FilterFn(func(m store.Message) bool {
3872 return all || m.ParsedBuf == nil
3876 return fmt.Errorf("list messages: %v", err)
3878 for _, m := range l {
3879 mr := a.MessageReader(m)
3880 p, err := message.EnsurePart(c.log.Logger, false, mr, m.Size)
3882 log.Printf("parsing message %d: %v (continuing)", m.ID, err)
3884 m.ParsedBuf, err = json.Marshal(p)
3886 return fmt.Errorf("marshal parsed message: %v", err)
3888 if err := tx.Update(&m); err != nil {
3889 return fmt.Errorf("update message: %v", err)
3895 xcheckf(err, "update messages with parsed mime structure")
3896 fmt.Printf("%d messages updated\n", n)
3899func cmdRecalculateMailboxCounts(c *cmd) {
3900 c.params = "account"
3901 c.help = `Recalculate message counts for all mailboxes in the account, and total message size for quota.
3903When a message is added to/removed from a mailbox, or when message flags change,
3904the total, unread, unseen and deleted messages are accounted, the total size of
3905the mailbox, and the total message size for the account. In case of a bug in
3906this accounting, the numbers could become incorrect. This command will find, fix
3915 ctlcmdRecalculateMailboxCounts(xctl(), args[0])
3918func ctlcmdRecalculateMailboxCounts(ctl *ctl, account string) {
3919 ctl.xwrite("recalculatemailboxcounts")
3922 ctl.xstreamto(os.Stdout)
3925func cmdMessageParse(c *cmd) {
3926 c.params = "message.eml"
3927 c.help = "Parse message, print JSON representation."
3930 c.flag.BoolVar(&smtputf8, "smtputf8", false, "check if message needs smtputf8")
3936 f, err := os.Open(args[0])
3937 xcheckf(err, "open")
3939 if err := f.Close(); err != nil {
3940 log.Printf("closing message file: %v", err)
3944 part, err := message.Parse(c.log.Logger, false, f)
3945 xcheckf(err, "parsing message")
3946 err = part.Walk(c.log.Logger, nil)
3947 xcheckf(err, "parsing nested parts")
3948 enc := json.NewEncoder(os.Stdout)
3949 enc.SetIndent("", "\t")
3950 enc.SetEscapeHTML(false)
3951 err = enc.Encode(part)
3952 xcheckf(err, "write")
3955 needs, err := part.NeedsSMTPUTF8()
3956 xcheckf(err, "checking if message needs smtputf8")
3957 fmt.Println("message needs smtputf8:", needs)
3961func cmdOpenaccounts(c *cmd) {
3963 c.params = "datadir account ..."
3964 c.help = `Open and close accounts, for triggering data upgrades, for tests.
3966Opens database files directly, not going through a running mox instance.
3974 dataDir := filepath.Clean(args[0])
3975 for _, accName := range args[1:] {
3976 accDir := filepath.Join(dataDir, "accounts", accName)
3977 log.Printf("opening account %s...", accDir)
3978 a, err := store.OpenAccountDB(c.log, accDir, accName)
3979 xcheckf(err, "open account %s", accName)
3980 err = a.ThreadingWait(c.log)
3981 xcheckf(err, "wait for threading upgrade to complete for %s", accName)
3983 xcheckf(err, "close account %s", accName)
3987func cmdReassignthreads(c *cmd) {
3988 c.params = "[account]"
3989 c.help = `Reassign message threads.
3991For all accounts, or optionally only the specified account.
3993Threading for all messages in an account is first reset, and new base subject
3994and normalized message-id saved with the message. Then all messages are
3995evaluated and matched against their parents/ancestors.
3997Messages are matched based on the References header, with a fall-back to an
3998In-Reply-To header, and if neither is present/valid, based only on base
4001A References header typically points to multiple previous messages in a
4002hierarchy. From oldest ancestor to most recent parent. An In-Reply-To header
4003would have only a message-id of the parent message.
4005A message is only linked to a parent/ancestor if their base subject is the
4006same. This ensures unrelated replies, with a new subject, are placed in their
4009The base subject is lower cased, has whitespace collapsed to a single
4010space, and some components removed: leading "Re:", "Fwd:", "Fw:", or bracketed
4011tag (that mailing lists often add, e.g. "[listname]"), trailing "(fwd)", or
4012enclosing "[fwd: ...]".
4014Messages are linked to all their ancestors. If an intermediate parent/ancestor
4015message is deleted in the future, the message can still be linked to the earlier
4016ancestors. If the direct parent already wasn't available while matching, this is
4017stored as the message having a "missing link" to its stored ancestors.
4029 ctlcmdReassignthreads(xctl(), account)
4032func ctlcmdReassignthreads(ctl *ctl, account string) {
4033 ctl.xwrite("reassignthreads")
4036 ctl.xstreamto(os.Stdout)
4039func cmdIMAPServe(c *cmd) {
4040 c.params = "preauth-address"
4041 c.help = `Initiate a preauthenticated IMAP connection on file descriptor 0.
4043For use with tools that can do IMAP over tunneled connections, e.g. with SSH
4044during migrations. TLS is not possible on the connection, and authentication
4045does not require TLS.
4048 c.flag.BoolVar(&fd0, "fd0", false, "write IMAP to file descriptor 0 instead of stdout")
4059 ctlcmdIMAPServe(xctl(), address, os.Stdin, output)
4062func ctlcmdIMAPServe(ctl *ctl, address string, input io.ReadCloser, output io.WriteCloser) {
4063 ctl.xwrite("imapserve")
4067 done := make(chan struct{}, 1)
4072 _, err := io.Copy(output, ctl.conn)
4076 log.Printf("reading from imap: %v", err)
4082 _, err := io.Copy(ctl.conn, input)
4086 log.Printf("writing to imap: %v", err)
4091func cmdReadmessages(c *cmd) {
4093 c.params = "datadir account ..."
4094 c.help = `Open account, parse several headers for all messages.
4096For performance testing.
4098Opens database files directly, not going through a running mox instance.
4101 gomaxprocs := runtime.GOMAXPROCS(0)
4102 var procs, workqueuesize, limit int
4103 c.flag.IntVar(&procs, "procs", gomaxprocs, "number of goroutines for reading messages")
4104 c.flag.IntVar(&workqueuesize, "workqueuesize", 2*gomaxprocs, "number of messages to keep in work queue")
4105 c.flag.IntVar(&limit, "limit", 0, "number of messages to process if greater than zero")
4111 type threadPrep struct {
4116 threadingFields := [][]byte{
4117 []byte("references"),
4118 []byte("in-reply-to"),
4121 dataDir := filepath.Clean(args[0])
4122 for _, accName := range args[1:] {
4123 accDir := filepath.Join(dataDir, "accounts", accName)
4124 log.Printf("opening account %s...", accDir)
4125 a, err := store.OpenAccountDB(c.log, accDir, accName)
4126 xcheckf(err, "open account %s", accName)
4128 prepareMessages := func(in, out chan moxio.Work[store.Message, threadPrep]) {
4129 headerbuf := make([]byte, 8*1024)
4130 scratch := make([]byte, 4*1024)
4138 var partialPart struct {
4142 if err := json.Unmarshal(m.ParsedBuf, &partialPart); err != nil {
4143 w.Err = fmt.Errorf("unmarshal part: %v", err)
4145 size := partialPart.BodyOffset - partialPart.HeaderOffset
4146 if int(size) > len(headerbuf) {
4147 headerbuf = make([]byte, size)
4150 buf := headerbuf[:int(size)]
4151 err := func() error {
4152 mr := a.MessageReader(m)
4154 if err := mr.Close(); err != nil {
4155 log.Printf("closing message reader: %v", err)
4159 // ReadAt returns whole buffer or error. Single read should be fast.
4160 n, err := mr.ReadAt(buf, partialPart.HeaderOffset)
4161 if err != nil || n != len(buf) {
4162 return fmt.Errorf("read header: %v", err)
4168 } else if h, err := message.ParseHeaderFields(buf, scratch, threadingFields); err != nil {
4171 w.Out.references = h["References"]
4172 w.Out.inReplyTo = h["In-Reply-To"]
4185 processMessage := func(m store.Message, prep threadPrep) error {
4187 log.Printf("%d messages (delta %s)", n, time.Since(t))
4194 wq := moxio.NewWorkQueue(procs, workqueuesize, prepareMessages, processMessage)
4196 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
4197 q := bstore.QueryTx[store.Message](tx)
4198 q.FilterEqual("Expunged", false)
4203 err = q.ForEach(wq.Add)
4211 xcheckf(err, "processing message")
4214 xcheckf(err, "close account %s", accName)
4215 log.Printf("account %s, total time %s", accName, time.Since(t0))
4219func cmdQueueFillRetired(c *cmd) {
4221 c.help = `Fill retired messag and webhooks queue with testdata.
4223For testing the pagination. Operates directly on queue database.
4226 c.flag.IntVar(&n, "n", 10000, "retired messages and retired webhooks to insert")
4234 xcheckf(err, "init queue")
4235 err = queue.DB.Write(context.Background(), func(tx *bstore.Tx) error {
4238 // Cause autoincrement ID for queue.Msg to be forwarded, and use the reserved ID
4239 // space for inserting retired messages.
4241 err = tx.Insert(&fm)
4242 xcheckf(err, "temporarily insert message to get autoincrement sequence")
4243 err = tx.Delete(&fm)
4244 xcheckf(err, "removing temporary message for resetting autoincrement sequence")
4246 err = tx.Insert(&fm)
4247 xcheckf(err, "temporarily insert message to forward autoincrement sequence")
4248 err = tx.Delete(&fm)
4249 xcheckf(err, "removing temporary message after forwarding autoincrement sequence")
4252 // And likewise for webhooks.
4253 fh := queue.Hook{Account: "x", URL: "x", NextAttempt: time.Now()}
4254 err = tx.Insert(&fh)
4255 xcheckf(err, "temporarily insert webhook to get autoincrement sequence")
4256 err = tx.Delete(&fh)
4257 xcheckf(err, "removing temporary webhook for resetting autoincrement sequence")
4259 err = tx.Insert(&fh)
4260 xcheckf(err, "temporarily insert webhook to forward autoincrement sequence")
4261 err = tx.Delete(&fh)
4262 xcheckf(err, "removing temporary webhook after forwarding autoincrement sequence")
4266 t0 := now.Add(-time.Duration(i) * time.Second)
4267 last := now.Add(-time.Duration(i/10) * time.Second)
4268 mr := queue.MsgRetired{
4269 ID: fm.ID + int64(i),
4271 SenderAccount: "test",
4272 SenderLocalpart: "mox",
4273 SenderDomainStr: "localhost",
4274 FromID: fmt.Sprintf("%016d", i),
4275 RecipientLocalpart: "mox",
4276 RecipientDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "localhost"}},
4277 RecipientDomainStr: "localhost",
4280 Results: []queue.MsgResult{
4283 Duration: time.Millisecond,
4290 Size: int64(i * 100),
4291 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
4292 Subject: fmt.Sprintf("test message %d", i),
4293 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
4295 RecipientAddress: "mox@localhost",
4297 KeepUntil: now.Add(48 * time.Hour),
4299 err := tx.Insert(&mr)
4300 xcheckf(err, "inserting retired message")
4304 t0 := now.Add(-time.Duration(i) * time.Second)
4305 last := now.Add(-time.Duration(i/10) * time.Second)
4310 hr := queue.HookRetired{
4311 ID: fh.ID + int64(i),
4312 QueueMsgID: fm.ID + int64(i),
4313 FromID: fmt.Sprintf("%016d", i),
4314 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
4315 Subject: fmt.Sprintf("test message %d", i),
4316 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
4318 URL: "http://localhost/hook",
4319 IsIncoming: i%10 == 0,
4320 OutgoingEvent: event,
4325 Results: []queue.HookResult{
4328 Duration: time.Millisecond,
4329 URL: "http://localhost/hook",
4338 KeepUntil: now.Add(48 * time.Hour),
4340 err := tx.Insert(&hr)
4341 xcheckf(err, "inserting retired hook")
4346 xcheckf(err, "add to queue")
4347 log.Printf("added %d retired messages and %d retired webhooks", n, n)