10 cryptorand "crypto/rand"
37 "golang.org/x/crypto/bcrypt"
38 "golang.org/x/text/secure/precis"
40 "github.com/mjl-/adns"
42 "github.com/mjl-/autocert"
43 "github.com/mjl-/bstore"
44 "github.com/mjl-/sconf"
45 "github.com/mjl-/sherpa"
47 "github.com/mjl-/mox/admin"
48 "github.com/mjl-/mox/config"
49 "github.com/mjl-/mox/dane"
50 "github.com/mjl-/mox/dkim"
51 "github.com/mjl-/mox/dmarc"
52 "github.com/mjl-/mox/dmarcdb"
53 "github.com/mjl-/mox/dmarcrpt"
54 "github.com/mjl-/mox/dns"
55 "github.com/mjl-/mox/dnsbl"
56 "github.com/mjl-/mox/message"
57 "github.com/mjl-/mox/mlog"
58 "github.com/mjl-/mox/mox-"
59 "github.com/mjl-/mox/moxio"
60 "github.com/mjl-/mox/moxvar"
61 "github.com/mjl-/mox/mtasts"
62 "github.com/mjl-/mox/publicsuffix"
63 "github.com/mjl-/mox/queue"
64 "github.com/mjl-/mox/smtp"
65 "github.com/mjl-/mox/smtpclient"
66 "github.com/mjl-/mox/spf"
67 "github.com/mjl-/mox/store"
68 "github.com/mjl-/mox/tlsrpt"
69 "github.com/mjl-/mox/tlsrptdb"
70 "github.com/mjl-/mox/updates"
71 "github.com/mjl-/mox/webadmin"
72 "github.com/mjl-/mox/webapi"
76 changelogDomain = "xmox.nl"
77 changelogURL = "https://updates.xmox.nl/changelog"
78 changelogPubKey = base64Decode("sPNiTDQzvb4FrytNEiebJhgyQzn57RwEjNbGWMM/bDY=")
81func base64Decode(s string) []byte {
82 buf, err := base64.StdEncoding.DecodeString(s)
89func envString(k, def string) string {
97var commands = []struct {
102 {"quickstart", cmdQuickstart},
104 {"setaccountpassword", cmdSetaccountpassword},
105 {"setadminpassword", cmdSetadminpassword},
106 {"loglevels", cmdLoglevels},
107 {"queue holdrules list", cmdQueueHoldrulesList},
108 {"queue holdrules add", cmdQueueHoldrulesAdd},
109 {"queue holdrules remove", cmdQueueHoldrulesRemove},
110 {"queue list", cmdQueueList},
111 {"queue hold", cmdQueueHold},
112 {"queue unhold", cmdQueueUnhold},
113 {"queue schedule", cmdQueueSchedule},
114 {"queue transport", cmdQueueTransport},
115 {"queue requiretls", cmdQueueRequireTLS},
116 {"queue fail", cmdQueueFail},
117 {"queue drop", cmdQueueDrop},
118 {"queue dump", cmdQueueDump},
119 {"queue retired list", cmdQueueRetiredList},
120 {"queue retired print", cmdQueueRetiredPrint},
121 {"queue suppress list", cmdQueueSuppressList},
122 {"queue suppress add", cmdQueueSuppressAdd},
123 {"queue suppress remove", cmdQueueSuppressRemove},
124 {"queue suppress lookup", cmdQueueSuppressLookup},
125 {"queue webhook list", cmdQueueHookList},
126 {"queue webhook schedule", cmdQueueHookSchedule},
127 {"queue webhook cancel", cmdQueueHookCancel},
128 {"queue webhook print", cmdQueueHookPrint},
129 {"queue webhook retired list", cmdQueueHookRetiredList},
130 {"queue webhook retired print", cmdQueueHookRetiredPrint},
131 {"import maildir", cmdImportMaildir},
132 {"import mbox", cmdImportMbox},
133 {"export maildir", cmdExportMaildir},
134 {"export mbox", cmdExportMbox},
135 {"localserve", cmdLocalserve},
137 {"backup", cmdBackup},
138 {"verifydata", cmdVerifydata},
139 {"licenses", cmdLicenses},
141 {"config test", cmdConfigTest},
142 {"config dnscheck", cmdConfigDNSCheck},
143 {"config dnsrecords", cmdConfigDNSRecords},
144 {"config describe-domains", cmdConfigDescribeDomains},
145 {"config describe-static", cmdConfigDescribeStatic},
146 {"config account add", cmdConfigAccountAdd},
147 {"config account rm", cmdConfigAccountRemove},
148 {"config address add", cmdConfigAddressAdd},
149 {"config address rm", cmdConfigAddressRemove},
150 {"config domain add", cmdConfigDomainAdd},
151 {"config domain rm", cmdConfigDomainRemove},
152 {"config tlspubkey list", cmdConfigTlspubkeyList},
153 {"config tlspubkey get", cmdConfigTlspubkeyGet},
154 {"config tlspubkey add", cmdConfigTlspubkeyAdd},
155 {"config tlspubkey rm", cmdConfigTlspubkeyRemove},
156 {"config tlspubkey gen", cmdConfigTlspubkeyGen},
157 {"config alias list", cmdConfigAliasList},
158 {"config alias print", cmdConfigAliasPrint},
159 {"config alias add", cmdConfigAliasAdd},
160 {"config alias update", cmdConfigAliasUpdate},
161 {"config alias rm", cmdConfigAliasRemove},
162 {"config alias addaddr", cmdConfigAliasAddaddr},
163 {"config alias rmaddr", cmdConfigAliasRemoveaddr},
165 {"config describe-sendmail", cmdConfigDescribeSendmail},
166 {"config printservice", cmdConfigPrintservice},
167 {"config ensureacmehostprivatekeys", cmdConfigEnsureACMEHostprivatekeys},
168 {"config example", cmdConfigExample},
170 {"checkupdate", cmdCheckupdate},
172 {"clientconfig", cmdClientConfig},
173 {"deliver", cmdDeliver},
174 // 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
175 {"dane dial", cmdDANEDial},
176 {"dane dialmx", cmdDANEDialmx},
177 {"dane makerecord", cmdDANEMakeRecord},
178 {"dns lookup", cmdDNSLookup},
179 {"dkim gened25519", cmdDKIMGened25519},
180 {"dkim genrsa", cmdDKIMGenrsa},
181 {"dkim lookup", cmdDKIMLookup},
182 {"dkim txt", cmdDKIMTXT},
183 {"dkim verify", cmdDKIMVerify},
184 {"dkim sign", cmdDKIMSign},
185 {"dmarc lookup", cmdDMARCLookup},
186 {"dmarc parsereportmsg", cmdDMARCParsereportmsg},
187 {"dmarc verify", cmdDMARCVerify},
188 {"dmarc checkreportaddrs", cmdDMARCCheckreportaddrs},
189 {"dnsbl check", cmdDNSBLCheck},
190 {"dnsbl checkhealth", cmdDNSBLCheckhealth},
191 {"mtasts lookup", cmdMTASTSLookup},
192 {"retrain", cmdRetrain},
193 {"sendmail", cmdSendmail},
194 {"spf check", cmdSPFCheck},
195 {"spf lookup", cmdSPFLookup},
196 {"spf parse", cmdSPFParse},
197 {"tlsrpt lookup", cmdTLSRPTLookup},
198 {"tlsrpt parsereportmsg", cmdTLSRPTParsereportmsg},
199 {"version", cmdVersion},
200 {"webapi", cmdWebapi},
202 {"example", cmdExample},
203 {"bumpuidvalidity", cmdBumpUIDValidity},
204 {"reassignuids", cmdReassignUIDs},
205 {"fixuidmeta", cmdFixUIDMeta},
206 {"fixmsgsize", cmdFixmsgsize},
207 {"reparse", cmdReparse},
208 {"ensureparsed", cmdEnsureParsed},
209 {"recalculatemailboxcounts", cmdRecalculateMailboxCounts},
210 {"message parse", cmdMessageParse},
211 {"reassignthreads", cmdReassignthreads},
214 {"helpall", cmdHelpall},
215 {"junk analyze", cmdJunkAnalyze},
216 {"junk check", cmdJunkCheck},
217 {"junk play", cmdJunkPlay},
218 {"junk test", cmdJunkTest},
219 {"junk train", cmdJunkTrain},
220 {"dmarcdb addreport", cmdDMARCDBAddReport},
221 {"tlsrptdb addreport", cmdTLSRPTDBAddReport},
222 {"updates addsigned", cmdUpdatesAddSigned},
223 {"updates genkey", cmdUpdatesGenkey},
224 {"updates pubkey", cmdUpdatesPubkey},
225 {"updates serve", cmdUpdatesServe},
226 {"updates verify", cmdUpdatesVerify},
227 {"gentestdata", cmdGentestdata},
228 {"ximport maildir", cmdXImportMaildir},
229 {"ximport mbox", cmdXImportMbox},
230 {"openaccounts", cmdOpenaccounts},
231 {"readmessages", cmdReadmessages},
232 {"queuefillretired", cmdQueueFillRetired},
238 for _, xc := range commands {
239 c := cmd{words: strings.Split(xc.cmd, " "), fn: xc.fn}
240 cmds = append(cmds, c)
248 // Set before calling command.
251 _gather bool // Set when using Parse to gather usage for a command.
253 // Set by invoked command or Parse.
254 unlisted bool // If set, command is not listed until at least some words are matched from command.
255 params string // Arguments to command. Multiple lines possible.
256 help string // Additional explanation. First line is synopsis, the rest is only printed for an explicit help/usage for that command.
262func (c *cmd) Parse() []string {
263 // To gather params and usage information, we just run the command but cause this
264 // panic after the command has registered its flags and set its params and help
265 // information. This is then caught and that info printed.
270 c.flag.Usage = c.Usage
271 c.flag.Parse(c.flagArgs)
272 c.args = c.flag.Args()
276func (c *cmd) gather() {
277 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
281 // panic generated by Parse.
289func (c *cmd) makeUsage() string {
290 var r strings.Builder
291 cs := "mox " + strings.Join(c.words, " ")
292 for i, line := range strings.Split(strings.TrimSpace(c.params), "\n") {
300 fmt.Fprintf(&r, "%6s %s%s\n", s, cs, line)
303 c.flag.PrintDefaults()
307func (c *cmd) printUsage() {
308 fmt.Fprint(os.Stderr, c.makeUsage())
310 fmt.Fprint(os.Stderr, "\n"+c.help+"\n")
314func (c *cmd) Usage() {
319func cmdHelp(c *cmd) {
320 c.params = "[command ...]"
321 c.help = `Prints help about matching commands.
323If multiple commands match, they are listed along with the first line of their help text.
324If a single command matches, its usage and full help text is printed.
331 prefix := func(l, pre []string) bool {
332 if len(pre) > len(l) {
335 return slices.Equal(pre, l[:len(pre)])
339 for _, c := range cmds {
340 if slices.Equal(c.words, args) {
342 fmt.Print(c.makeUsage())
344 fmt.Print("\n" + c.help + "\n")
347 } else if prefix(c.words, args) {
348 partial = append(partial, c)
351 if len(partial) == 0 {
352 fmt.Fprintf(os.Stderr, "%s: unknown command\n", strings.Join(args, " "))
355 for _, c := range partial {
357 line := "mox " + strings.Join(c.words, " ")
358 fmt.Printf("%s\n", line)
360 fmt.Printf("\t%s\n", strings.Split(c.help, "\n")[0])
365func cmdHelpall(c *cmd) {
367 c.help = `Print all detailed usage and help information for all listed commands.
369Used to generate documentation.
377 for _, c := range cmds {
383 fmt.Fprintf(os.Stderr, "\n")
387 fmt.Fprintf(os.Stderr, "# mox %s\n\n", strings.Join(c.words, " "))
389 fmt.Fprintln(os.Stderr, c.help+"\n")
392 s = "\t" + strings.ReplaceAll(s, "\n", "\n\t")
393 fmt.Fprintln(os.Stderr, s)
397func usage(l []cmd, unlisted bool) {
400 lines = append(lines, "mox [-config config/mox.conf] [-pedantic] ...")
402 for _, c := range l {
404 if c.unlisted && !unlisted {
407 for _, line := range strings.Split(c.params, "\n") {
408 x := append([]string{"mox"}, c.words...)
412 lines = append(lines, strings.Join(x, " "))
415 for i, line := range lines {
420 fmt.Fprintln(os.Stderr, pre+line)
425var loglevel string // Empty will be interpreted as info, except by localserve.
428// subcommands that are not "serve" should use this function to load the config, it
429// restores any loglevel specified on the command-line, instead of using the
430// loglevels from the config file and it does not load files like TLS keys/certs.
431func mustLoadConfig() {
432 mox.MustLoadConfig(false, false)
437 if level, ok := mlog.Levels[ll]; ok {
438 mox.Conf.Log[""] = level
439 mlog.SetConfig(mox.Conf.Log)
441 log.Fatal("unknown loglevel", slog.String("loglevel", loglevel))
444 mox.SetPedantic(true)
449 // CheckConsistencyOnClose is true by default, for all the test packages. A regular
450 // mox server should never use it. But integration tests enable it again with a
452 store.CheckConsistencyOnClose = false
454 ctxbg := context.Background()
460 // If invoked as sendmail, e.g. /usr/sbin/sendmail, we do enough so cron can get a
461 // message sent using smtp submission to a configured server.
462 if len(os.Args) > 0 && filepath.Base(os.Args[0]) == "sendmail" {
464 flag: flag.NewFlagSet("sendmail", flag.ExitOnError),
465 flagArgs: os.Args[1:],
466 log: mlog.New("sendmail", nil),
472 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")
473 flag.StringVar(&loglevel, "loglevel", "", "if non-empty, this log level is set early in startup")
474 flag.BoolVar(&pedantic, "pedantic", false, "protocol violations result in errors instead of accepting/working around them")
475 flag.BoolVar(&store.CheckConsistencyOnClose, "checkconsistency", false, "dangerous option for testing only, enables data checks that abort/panic when inconsistencies are found")
477 var cpuprofile, memprofile, tracefile string
478 flag.StringVar(&cpuprofile, "cpuprof", "", "store cpu profile to file")
479 flag.StringVar(&memprofile, "memprof", "", "store mem profile to file")
480 flag.StringVar(&tracefile, "trace", "", "store execution trace to file")
482 flag.Usage = func() { usage(cmds, false) }
490 defer traceExecution(tracefile)()
492 defer profile(cpuprofile, memprofile)()
495 mox.SetPedantic(true)
498 mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
503 if level, ok := mlog.Levels[ll]; ok {
504 mox.Conf.Log[""] = level
505 mlog.SetConfig(mox.Conf.Log)
506 // note: SetConfig may be called again when subcommands loads config.
508 log.Fatalf("unknown loglevel %q", loglevel)
513 for _, c := range cmds {
514 for i, w := range c.words {
515 if i >= len(args) || w != args[i] {
517 partial = append(partial, c)
522 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
523 c.flagArgs = args[len(c.words):]
524 c.log = mlog.New(strings.Join(c.words, ""), nil)
528 if len(partial) > 0 {
534func xcheckf(err error, format string, args ...any) {
538 msg := fmt.Sprintf(format, args...)
539 log.Fatalf("%s: %s", msg, err)
542func xparseIP(s, what string) net.IP {
545 log.Fatalf("invalid %s: %q", what, s)
550func xparseDomain(s, what string) dns.Domain {
551 d, err := dns.ParseDomain(s)
552 xcheckf(err, "parsing %s %q", what, s)
556func cmdClientConfig(c *cmd) {
558 c.help = `Print the configuration for email clients for a domain.
560Sending email is typically not done on the SMTP port 25, but on submission
561ports 465 (with TLS) and 587 (without initial TLS, but usually added to the
562connection with STARTTLS). For IMAP, the port with TLS is 993 and without is
565Without TLS/STARTTLS, passwords are sent in clear text, which should only be
566configured over otherwise secured connections, like a VPN.
572 d := xparseDomain(args[0], "domain")
577func printClientConfig(d dns.Domain) {
578 cc, err := admin.ClientConfigsDomain(d)
579 xcheckf(err, "getting client config")
580 fmt.Printf("%-20s %-30s %5s %-15s %s\n", "Protocol", "Host", "Port", "Listener", "Note")
581 for _, e := range cc.Entries {
582 fmt.Printf("%-20s %-30s %5d %-15s %s\n", e.Protocol, e.Host, e.Port, e.Listener, e.Note)
585To prevent authentication mechanism downgrade attempts that may result in
586clients sending plain text passwords to a MitM, clients should always be
587explicitly configured with the most secure authentication mechanism supported,
588the first of: SCRAM-SHA-256-PLUS, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, SCRAM-SHA-1,
593func cmdConfigTest(c *cmd) {
594 c.help = `Parses and validates the configuration files.
596If valid, the command exits with status 0. If not valid, all errors encountered
604 mox.FilesImmediate = true
606 _, errs := mox.ParseConfig(context.Background(), c.log, mox.ConfigStaticPath, true, true, false)
608 log.Printf("multiple errors:")
609 for _, err := range errs {
610 log.Printf("%s", err)
613 } else if len(errs) == 1 {
614 log.Fatalf("%s", errs[0])
617 fmt.Println("config OK")
620func cmdConfigDescribeStatic(c *cmd) {
621 c.params = ">mox.conf"
622 c.help = `Prints an annotated empty configuration for use as mox.conf.
624The static configuration file cannot be reloaded while mox is running. Mox has
625to be restarted for changes to the static configuration file to take effect.
627This configuration file needs modifications to make it valid. For example, it
628may contain unfinished list items.
630 if len(c.Parse()) != 0 {
635 err := sconf.Describe(os.Stdout, &sc)
636 xcheckf(err, "describing config")
639func cmdConfigDescribeDomains(c *cmd) {
640 c.params = ">domains.conf"
641 c.help = `Prints an annotated empty configuration for use as domains.conf.
643The domains configuration file contains the domains and their configuration,
644and accounts and their configuration. This includes the configured email
645addresses. The mox admin web interface, and the mox command line interface, can
646make changes to this file. Mox automatically reloads this file when it changes.
648Like the static configuration, the example domains.conf printed by this command
649needs modifications to make it valid.
651 if len(c.Parse()) != 0 {
655 var dc config.Dynamic
656 err := sconf.Describe(os.Stdout, &dc)
657 xcheckf(err, "describing config")
660func cmdConfigPrintservice(c *cmd) {
661 c.params = ">mox.service"
662 c.help = `Prints a systemd unit service file for mox.
664This is the same file as generated using quickstart. If the systemd service file
665has changed with a newer version of mox, use this command to generate an up to
668 if len(c.Parse()) != 0 {
672 pwd, err := os.Getwd()
674 log.Printf("current working directory: %v", err)
677 service := strings.ReplaceAll(moxService, "/home/mox", pwd)
681func cmdConfigDomainAdd(c *cmd) {
682 c.params = "domain account [localpart]"
683 c.help = `Adds a new domain to the configuration and reloads the configuration.
685The account is used for the postmaster mailboxes the domain, including as DMARC and
686TLS reporting. Localpart is the "username" at the domain for this account. If
687must be set if and only if account does not yet exist.
690 if len(args) != 2 && len(args) != 3 {
694 d := xparseDomain(args[0], "domain")
696 var localpart smtp.Localpart
699 localpart, err = smtp.ParseLocalpart(args[2])
700 xcheckf(err, "parsing localpart")
702 ctlcmdConfigDomainAdd(xctl(), d, args[1], localpart)
705func ctlcmdConfigDomainAdd(ctl *ctl, domain dns.Domain, account string, localpart smtp.Localpart) {
706 ctl.xwrite("domainadd")
707 ctl.xwrite(domain.Name())
709 ctl.xwrite(string(localpart))
711 fmt.Printf("domain added, remember to add dns records, see:\n\nmox config dnsrecords %s\nmox config dnscheck %s\n", domain.Name(), domain.Name())
714func cmdConfigDomainRemove(c *cmd) {
716 c.help = `Remove a domain from the configuration and reload the configuration.
718This is a dangerous operation. Incoming email delivery for this domain will be
726 d := xparseDomain(args[0], "domain")
728 ctlcmdConfigDomainRemove(xctl(), d)
731func ctlcmdConfigDomainRemove(ctl *ctl, d dns.Domain) {
732 ctl.xwrite("domainrm")
735 fmt.Printf("domain removed, remember to remove dns records for %s\n", d)
738func cmdConfigAliasList(c *cmd) {
740 c.help = `Show aliases (lists) for domain.`
747 ctlcmdConfigAliasList(xctl(), args[0])
750func ctlcmdConfigAliasList(ctl *ctl, address string) {
751 ctl.xwrite("aliaslist")
754 ctl.xstreamto(os.Stdout)
757func cmdConfigAliasPrint(c *cmd) {
759 c.help = `Print settings and members of alias (list).`
766 ctlcmdConfigAliasPrint(xctl(), args[0])
769func ctlcmdConfigAliasPrint(ctl *ctl, address string) {
770 ctl.xwrite("aliasprint")
773 ctl.xstreamto(os.Stdout)
776func cmdConfigAliasAdd(c *cmd) {
777 c.params = "alias@domain rcpt1@domain ..."
778 c.help = `Add new alias (list) with one or more addresses and public posting enabled.
780An alias is used for delivering incoming email to multiple recipients. If you
781want to add an address to an account, don't use an alias, just add the address
789 alias := config.Alias{PostPublic: true, Addresses: args[1:]}
792 ctlcmdConfigAliasAdd(xctl(), args[0], alias)
795func ctlcmdConfigAliasAdd(ctl *ctl, address string, alias config.Alias) {
796 ctl.xwrite("aliasadd")
798 xctlwriteJSON(ctl, alias)
802func cmdConfigAliasUpdate(c *cmd) {
803 c.params = "alias@domain [-postpublic false|true -listmembers false|true -allowmsgfrom false|true]"
804 c.help = `Update alias (list) configuration.`
805 var postpublic, listmembers, allowmsgfrom string
806 c.flag.StringVar(&postpublic, "postpublic", "", "whether anyone or only list members can post")
807 c.flag.StringVar(&listmembers, "listmembers", "", "whether list members can list members")
808 c.flag.StringVar(&allowmsgfrom, "allowmsgfrom", "", "whether alias address can be used in message from header")
816 ctlcmdConfigAliasUpdate(xctl(), alias, postpublic, listmembers, allowmsgfrom)
819func ctlcmdConfigAliasUpdate(ctl *ctl, alias, postpublic, listmembers, allowmsgfrom string) {
820 ctl.xwrite("aliasupdate")
822 ctl.xwrite(postpublic)
823 ctl.xwrite(listmembers)
824 ctl.xwrite(allowmsgfrom)
828func cmdConfigAliasRemove(c *cmd) {
829 c.params = "alias@domain"
830 c.help = "Remove alias (list)."
837 ctlcmdConfigAliasRemove(xctl(), args[0])
840func ctlcmdConfigAliasRemove(ctl *ctl, alias string) {
841 ctl.xwrite("aliasrm")
846func cmdConfigAliasAddaddr(c *cmd) {
847 c.params = "alias@domain rcpt1@domain ..."
848 c.help = `Add addresses to alias (list).`
855 ctlcmdConfigAliasAddaddr(xctl(), args[0], args[1:])
858func ctlcmdConfigAliasAddaddr(ctl *ctl, alias string, addresses []string) {
859 ctl.xwrite("aliasaddaddr")
861 xctlwriteJSON(ctl, addresses)
865func cmdConfigAliasRemoveaddr(c *cmd) {
866 c.params = "alias@domain rcpt1@domain ..."
867 c.help = `Remove addresses from alias (list).`
874 ctlcmdConfigAliasRmaddr(xctl(), args[0], args[1:])
877func ctlcmdConfigAliasRmaddr(ctl *ctl, alias string, addresses []string) {
878 ctl.xwrite("aliasrmaddr")
880 xctlwriteJSON(ctl, addresses)
884func cmdConfigAccountAdd(c *cmd) {
885 c.params = "account address"
886 c.help = `Add an account with an email address and reload the configuration.
888Email can be delivered to this address/account. A password has to be configured
889explicitly, see the setaccountpassword command.
897 ctlcmdConfigAccountAdd(xctl(), args[0], args[1])
900func ctlcmdConfigAccountAdd(ctl *ctl, account, address string) {
901 ctl.xwrite("accountadd")
905 fmt.Printf("account added, set a password with \"mox setaccountpassword %s\"\n", account)
908func cmdConfigAccountRemove(c *cmd) {
910 c.help = `Remove an account and reload the configuration.
912Email addresses for this account will also be removed, and incoming email for
913these addresses will be rejected.
915All data for the account will be removed.
923 ctlcmdConfigAccountRemove(xctl(), args[0])
926func ctlcmdConfigAccountRemove(ctl *ctl, account string) {
927 ctl.xwrite("accountrm")
930 fmt.Println("account removed")
933func cmdConfigTlspubkeyList(c *cmd) {
934 c.params = "[account]"
935 c.help = `List TLS public keys for TLS client certificate authentication.
937If account is absent, the TLS public keys for all accounts are listed.
940 var accountOpt string
943 } else if len(args) > 1 {
948 ctlcmdConfigTlspubkeyList(xctl(), accountOpt)
951func ctlcmdConfigTlspubkeyList(ctl *ctl, accountOpt string) {
952 ctl.xwrite("tlspubkeylist")
953 ctl.xwrite(accountOpt)
955 ctl.xstreamto(os.Stdout)
958func cmdConfigTlspubkeyGet(c *cmd) {
959 c.params = "fingerprint"
960 c.help = `Get a TLS public key for a fingerprint.
962Prints the type, name, account and address for the key, and the certificate in
971 ctlcmdConfigTlspubkeyGet(xctl(), args[0])
974func ctlcmdConfigTlspubkeyGet(ctl *ctl, fingerprint string) {
975 ctl.xwrite("tlspubkeyget")
976 ctl.xwrite(fingerprint)
980 account := ctl.xread()
981 address := ctl.xread()
982 noimappreauth := ctl.xread()
994 fmt.Printf("type: %s\nname: %s\naccount: %s\naddress: %s\nno imap preauth: %s\n", typ, name, account, address, noimappreauth)
996 fmt.Printf("certificate:\n\n")
997 pem.Encode(os.Stdout, block)
1001func cmdConfigTlspubkeyAdd(c *cmd) {
1002 c.params = "address [name] < cert.pem"
1003 c.help = `Add a TLS public key to the account of the given address.
1005The public key is read from the certificate.
1007The optional name is a human-readable descriptive name of the key. If absent,
1008the CommonName from the certificate is used.
1010 var noimappreauth bool
1011 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.")
1013 var address, name string
1016 } else if len(args) == 2 {
1017 address, name = args[0], args[1]
1022 buf, err := io.ReadAll(os.Stdin)
1023 xcheckf(err, "reading from stdin")
1024 block, _ := pem.Decode(buf)
1026 err = errors.New("no pem block found")
1027 } else if block.Type != "CERTIFICATE" {
1028 err = fmt.Errorf("unexpected type %q, expected CERTIFICATE", block.Type)
1030 xcheckf(err, "parsing pem")
1033 ctlcmdConfigTlspubkeyAdd(xctl(), address, name, noimappreauth, block.Bytes)
1036func ctlcmdConfigTlspubkeyAdd(ctl *ctl, address, name string, noimappreauth bool, certDER []byte) {
1037 ctl.xwrite("tlspubkeyadd")
1040 ctl.xwrite(fmt.Sprintf("%v", noimappreauth))
1041 ctl.xstreamfrom(bytes.NewReader(certDER))
1045func cmdConfigTlspubkeyRemove(c *cmd) {
1046 c.params = "fingerprint"
1047 c.help = `Remove TLS public key for fingerprint.`
1054 ctlcmdConfigTlspubkeyRemove(xctl(), args[0])
1057func ctlcmdConfigTlspubkeyRemove(ctl *ctl, fingerprint string) {
1058 ctl.xwrite("tlspubkeyrm")
1059 ctl.xwrite(fingerprint)
1063func cmdConfigTlspubkeyGen(c *cmd) {
1065 c.help = `Generate an ed25519 private key and minimal certificate for use a TLS public key and write to files starting with stem.
1067The private key is written to $stem.$timestamp.ed25519privatekey.pkcs8.pem.
1068The certificate is written to $stem.$timestamp.certificate.pem.
1069The private key and certificate are also written to
1070$stem.$timestamp.ed25519privatekey-certificate.pem.
1072The certificate can be added to an account with "mox config account tlspubkey add".
1074The combined file can be used with "mox sendmail".
1076The private key is also written to standard error in raw-url-base64-encoded
1077form, also for use with "mox sendmail". The fingerprint is written to standard
1078error too, for reference.
1086 timestamp := time.Now().Format("200601021504")
1087 prefix := stem + "." + timestamp
1089 seed := make([]byte, ed25519.SeedSize)
1090 if _, err := cryptorand.Read(seed); err != nil {
1093 privKey := ed25519.NewKeyFromSeed(seed)
1094 privKeyBuf, err := x509.MarshalPKCS8PrivateKey(privKey)
1095 xcheckf(err, "marshal private key as pkcs8")
1097 err = pem.Encode(&b, &pem.Block{Type: "PRIVATE KEY", Bytes: privKeyBuf})
1098 xcheckf(err, "marshal pkcs8 private key to pem")
1099 privKeyBufPEM := b.Bytes()
1101 certBuf, tlsCert := xminimalCert(privKey)
1103 err = pem.Encode(&b, &pem.Block{Type: "CERTIFICATE", Bytes: certBuf})
1104 xcheckf(err, "marshal certificate to pem")
1105 certBufPEM := b.Bytes()
1107 xwriteFile := func(p string, data []byte, what string) {
1108 log.Printf("writing %s", p)
1109 err = os.WriteFile(p, data, 0600)
1110 xcheckf(err, "writing %s file: %v", what, err)
1113 xwriteFile(prefix+".ed25519privatekey.pkcs8.pem", privKeyBufPEM, "private key")
1114 xwriteFile(prefix+".certificate.pem", certBufPEM, "certificate")
1115 combinedPEM := append(append([]byte{}, privKeyBufPEM...), certBufPEM...)
1116 xwriteFile(prefix+".ed25519privatekey-certificate.pem", combinedPEM, "combined private key and certificate")
1118 shabuf := sha256.Sum256(tlsCert.Leaf.RawSubjectPublicKeyInfo)
1120 _, err = fmt.Fprintf(os.Stderr, "ed25519 private key as raw-url-base64: %s\ned25519 public key fingerprint: %s\n",
1121 base64.RawURLEncoding.EncodeToString(seed),
1122 base64.RawURLEncoding.EncodeToString(shabuf[:]),
1124 xcheckf(err, "write private key and public key fingerprint")
1127func cmdConfigAddressAdd(c *cmd) {
1128 c.params = "address account"
1129 c.help = `Adds an address to an account and reloads the configuration.
1131If address starts with a @ (i.e. a missing localpart), this is a catchall
1132address for the domain.
1140 ctlcmdConfigAddressAdd(xctl(), args[0], args[1])
1143func ctlcmdConfigAddressAdd(ctl *ctl, address, account string) {
1144 ctl.xwrite("addressadd")
1148 fmt.Println("address added")
1151func cmdConfigAddressRemove(c *cmd) {
1152 c.params = "address"
1153 c.help = `Remove an address and reload the configuration.
1155Incoming email for this address will be rejected after removing an address.
1163 ctlcmdConfigAddressRemove(xctl(), args[0])
1166func ctlcmdConfigAddressRemove(ctl *ctl, address string) {
1167 ctl.xwrite("addressrm")
1170 fmt.Println("address removed")
1173func cmdConfigDNSRecords(c *cmd) {
1175 c.help = `Prints annotated DNS records as zone file that should be created for the domain.
1177The zone file can be imported into existing DNS software. You should review the
1178DNS records, especially if your domain previously/currently has email
1186 d := xparseDomain(args[0], "domain")
1188 domConf, ok := mox.Conf.Domain(d)
1190 log.Fatalf("unknown domain")
1193 resolver := dns.StrictResolver{Pkg: "main"}
1194 _, result, err := resolver.LookupTXT(context.Background(), d.ASCII+".")
1195 if !dns.IsNotFound(err) {
1196 xcheckf(err, "looking up record for dnssec-status")
1199 var certIssuerDomainName, acmeAccountURI string
1200 public := mox.Conf.Static.Listeners["public"]
1201 if public.TLS != nil && public.TLS.ACME != "" {
1202 acme, ok := mox.Conf.Static.ACME[public.TLS.ACME]
1203 if ok && acme.Manager.Manager.Client != nil {
1204 certIssuerDomainName = acme.IssuerDomainName
1205 acc, err := acme.Manager.Manager.Client.GetReg(context.Background(), "")
1206 c.log.Check(err, "get public acme account")
1208 acmeAccountURI = acc.URI
1213 records, err := admin.DomainRecords(domConf, d, result.Authentic, certIssuerDomainName, acmeAccountURI)
1214 xcheckf(err, "records")
1215 fmt.Print(strings.Join(records, "\n") + "\n")
1218func cmdConfigDNSCheck(c *cmd) {
1220 c.help = "Check the DNS records with the configuration for the domain, and print any errors/warnings."
1226 d := xparseDomain(args[0], "domain")
1228 _, ok := mox.Conf.Domain(d)
1230 log.Fatalf("unknown domain")
1233 // todo future: move http.Admin.CheckDomain to mox- and make it return a regular error.
1239 err, ok := x.(*sherpa.Error)
1243 log.Fatalf("%s", err)
1246 printResult := func(name string, r webadmin.Result) {
1247 if len(r.Errors) == 0 && len(r.Warnings) == 0 {
1250 fmt.Printf("# %s\n", name)
1251 for _, s := range r.Errors {
1252 fmt.Printf("error: %s\n", s)
1254 for _, s := range r.Warnings {
1255 fmt.Printf("warning: %s\n", s)
1259 result := webadmin.Admin{}.CheckDomain(context.Background(), args[0])
1260 printResult("DNSSEC", result.DNSSEC.Result)
1261 printResult("IPRev", result.IPRev.Result)
1262 printResult("MX", result.MX.Result)
1263 printResult("TLS", result.TLS.Result)
1264 printResult("DANE", result.DANE.Result)
1265 printResult("SPF", result.SPF.Result)
1266 printResult("DKIM", result.DKIM.Result)
1267 printResult("DMARC", result.DMARC.Result)
1268 printResult("Host TLSRPT", result.HostTLSRPT.Result)
1269 printResult("Domain TLSRPT", result.DomainTLSRPT.Result)
1270 printResult("MTASTS", result.MTASTS.Result)
1271 printResult("SRV conf", result.SRVConf.Result)
1272 printResult("Autoconf", result.Autoconf.Result)
1273 printResult("Autodiscover", result.Autodiscover.Result)
1276func cmdConfigEnsureACMEHostprivatekeys(c *cmd) {
1278 c.help = `Ensure host private keys exist for TLS listeners with ACME.
1280In mox.conf, each listener can have TLS configured. Long-lived private key files
1281can be specified, which will be used when requesting ACME certificates.
1282Configuring these private keys makes it feasible to publish DANE TLSA records
1283for the corresponding public keys in DNS, protected with DNSSEC, allowing TLS
1284certificate verification without depending on a list of Certificate Authorities
1285(CAs). Previous versions of mox did not pre-generate private keys for use with
1286ACME certificates, but would generate private keys on-demand. By explicitly
1287configuring private keys, they will not change automatedly with new
1288certificates, and the DNS TLSA records stay valid.
1290This command looks for listeners in mox.conf with TLS with ACME configured. For
1291each missing host private key (of type rsa-2048 and ecdsa-p256) a key is written
1292to config/hostkeys/. If a certificate exists in the ACME "cache", its private
1293key is copied. Otherwise a new private key is generated. Snippets for manually
1294updating/editing mox.conf are printed.
1296After running this command, and updating mox.conf, run "mox config dnsrecords"
1297for a domain and create the TLSA DNS records it suggests to enable DANE.
1304 // Load a private key from p, in various forms. We only look at the first PEM
1305 // block. Files with only a private key, or with multiple blocks but private key
1306 // first like autocert does, can be loaded.
1307 loadPrivateKey := func(f *os.File) (any, error) {
1308 buf, err := io.ReadAll(f)
1310 return nil, fmt.Errorf("reading private key file: %v", err)
1312 block, _ := pem.Decode(buf)
1314 return nil, fmt.Errorf("no pem block found in pem file")
1318 case "EC PRIVATE KEY":
1319 privKey, err = x509.ParseECPrivateKey(block.Bytes)
1320 case "RSA PRIVATE KEY":
1321 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
1323 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
1325 return nil, fmt.Errorf("unrecognized pem block type %q", block.Type)
1328 return nil, fmt.Errorf("parsing private key of type %q: %v", block.Type, err)
1333 // Either load a private key from file, or if it doesn't exist generate a new
1335 xtryLoadPrivateKey := func(kt autocert.KeyType, p string) any {
1336 f, err := os.Open(p)
1337 if err != nil && errors.Is(err, fs.ErrNotExist) {
1339 case autocert.KeyRSA2048:
1340 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1341 xcheckf(err, "generating new 2048-bit rsa private key")
1343 case autocert.KeyECDSAP256:
1344 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1345 xcheckf(err, "generating new ecdsa p-256 private key")
1348 log.Fatalf("unexpected keytype %v", kt)
1351 xcheckf(err, "%s: open acme key and certificate file", p)
1353 // Load private key from file. autocert stores a PEM file that starts with a
1354 // private key, followed by certificate(s). So we can just read it and should find
1355 // the private key we are looking for.
1356 privKey, err := loadPrivateKey(f)
1357 if xerr := f.Close(); xerr != nil {
1358 log.Printf("closing private key file: %v", xerr)
1360 xcheckf(err, "parsing private key from acme key and certificate file")
1362 switch k := privKey.(type) {
1363 case *rsa.PrivateKey:
1364 if k.N.BitLen() == 2048 {
1367 log.Printf("warning: rsa private key in %s has %d bits, skipping and generating new 2048-bit rsa private key", p, k.N.BitLen())
1368 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1369 xcheckf(err, "generating new 2048-bit rsa private key")
1371 case *ecdsa.PrivateKey:
1372 if k.Curve == elliptic.P256() {
1375 log.Printf("warning: ecdsa private key in %s has curve %v, skipping and generating new p-256 ecdsa key", p, k.Curve.Params().Name)
1376 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1377 xcheckf(err, "generating new ecdsa p-256 private key")
1380 log.Fatalf("%s: unexpected private key file of type %T", p, privKey)
1385 // Write privKey as PKCS#8 private key to p. Only if file does not yet exist.
1386 writeHostPrivateKey := func(privKey any, p string) error {
1387 os.MkdirAll(filepath.Dir(p), 0700)
1388 f, err := os.OpenFile(p, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
1390 return fmt.Errorf("create: %v", err)
1394 if err := f.Close(); err != nil {
1395 log.Printf("closing new hostkey file %s after error: %v", p, err)
1397 if err := os.Remove(p); err != nil {
1398 log.Printf("removing new hostkey file %s after error: %v", p, err)
1402 buf, err := x509.MarshalPKCS8PrivateKey(privKey)
1404 return fmt.Errorf("marshal private host key: %v", err)
1407 Type: "PRIVATE KEY",
1410 if err := pem.Encode(f, &block); err != nil {
1411 return fmt.Errorf("write as pem: %v", err)
1413 if err := f.Close(); err != nil {
1414 return fmt.Errorf("close: %v", err)
1421 timestamp := time.Now().Format("20060102T150405")
1423 for listenerName, l := range mox.Conf.Static.Listeners {
1424 if l.TLS == nil || l.TLS.ACME == "" {
1427 haveKeyTypes := map[autocert.KeyType]bool{}
1428 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
1429 p := mox.ConfigDirPath(privKeyFile)
1430 f, err := os.Open(p)
1431 xcheckf(err, "open host private key")
1432 privKey, err := loadPrivateKey(f)
1433 if err := f.Close(); err != nil {
1434 log.Printf("closing host private key file: %v", err)
1436 xcheckf(err, "loading host private key")
1437 switch k := privKey.(type) {
1438 case *rsa.PrivateKey:
1439 if k.N.BitLen() == 2048 {
1440 haveKeyTypes[autocert.KeyRSA2048] = true
1442 case *ecdsa.PrivateKey:
1443 if k.Curve == elliptic.P256() {
1444 haveKeyTypes[autocert.KeyECDSAP256] = true
1448 created := []string{}
1449 for _, kt := range []autocert.KeyType{autocert.KeyRSA2048, autocert.KeyECDSAP256} {
1450 if haveKeyTypes[kt] {
1453 // Lookup key in ACME cache.
1454 host := l.HostnameDomain
1455 if host.ASCII == "" {
1456 host = mox.Conf.Static.HostnameDomain
1458 filename := host.ASCII
1460 if kt == autocert.KeyRSA2048 {
1464 p := mox.DataDirPath(filepath.Join("acme", "keycerts", l.TLS.ACME, filename))
1465 privKey := xtryLoadPrivateKey(kt, p)
1467 relPath := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", host.Name(), timestamp, kind))
1468 destPath := mox.ConfigDirPath(relPath)
1469 err := writeHostPrivateKey(privKey, destPath)
1470 xcheckf(err, "writing host private key file to %s: %v", destPath, err)
1471 created = append(created, relPath)
1472 fmt.Printf("Wrote host private key: %s\n", destPath)
1474 didCreate = didCreate || len(created) > 0
1475 if len(created) > 0 {
1477 HostPrivateKeyFiles: append(l.TLS.HostPrivateKeyFiles, created...),
1479 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)
1480 err := sconf.Write(os.Stdout, tls)
1481 xcheckf(err, "writing new TLS.HostPrivateKeyFiles section")
1487After updating mox.conf and restarting, run "mox config dnsrecords" for a
1488domain and create the TLSA DNS records it suggests to enable DANE.
1493func cmdLoglevels(c *cmd) {
1494 c.params = "[level [pkg]]"
1495 c.help = `Print the log levels, or set a new default log level, or a level for the given package.
1497By default, a single log level applies to all logging in mox. But for each
1498"pkg", an overriding log level can be configured. Examples of packages:
1499smtpserver, smtpclient, queue, imapserver, spf, dkim, dmarc, junk, message,
1502Specify a pkg and an empty level to clear the configured level for a package.
1504Valid labels: error, info, debug, trace, traceauth, tracedata.
1513 ctlcmdLoglevels(xctl())
1519 ctlcmdSetLoglevels(xctl(), pkg, args[0])
1523func ctlcmdLoglevels(ctl *ctl) {
1524 ctl.xwrite("loglevels")
1526 ctl.xstreamto(os.Stdout)
1529func ctlcmdSetLoglevels(ctl *ctl, pkg, level string) {
1530 ctl.xwrite("setloglevels")
1536func cmdStop(c *cmd) {
1537 c.help = `Shut mox down, giving connections maximum 3 seconds to stop before closing them.
1539While shutting down, new IMAP and SMTP connections will get a status response
1540indicating temporary unavailability. Existing connections will get a 3 second
1541period to finish their transaction and shut down. Under normal circumstances,
1542only IMAP has long-living connections, with the IDLE command to get notified of
1545 if len(c.Parse()) != 0 {
1552 // Read will hang until remote has shut down.
1553 buf := make([]byte, 128)
1554 n, err := ctl.conn.Read(buf)
1556 log.Fatalf("expected eof after graceful shutdown, got data %q", buf[:n])
1557 } else if err != io.EOF {
1558 log.Fatalf("expected eof after graceful shutdown, got error %v", err)
1560 fmt.Println("mox stopped")
1563func cmdBackup(c *cmd) {
1564 c.params = "dest-dir"
1565 c.help = `Creates a backup of the data directory.
1567Backup creates consistent snapshots of the databases and message files and
1568copies other files in the data directory. Empty directories are not copied.
1569These files can then be stored elsewhere for long-term storage, or used to fall
1570back to should an upgrade fail. Simply copying files in the data directory
1571while mox is running can result in unusable database files.
1573Message files never change (they are read-only, though can be removed) and are
1574hard-linked so they don't consume additional space. If hardlinking fails, for
1575example when the backup destination directory is on a different file system, a
1576regular copy is made. Using a destination directory like "data/tmp/backup"
1577increases the odds hardlinking succeeds: the default systemd service file
1578specifically mounts the data directory, causing attempts to hardlink outside it
1579to fail with an error about cross-device linking.
1581All files in the data directory that aren't recognized (i.e. other than known
1582database files, message files, an acme directory, the "tmp" directory, etc),
1583are stored, but with a warning.
1585Remove files in the destination directory before doing another backup. The
1586backup command will not overwrite files, but print and return errors.
1588Exit code 0 indicates the backup was successful. A clean successful backup does
1589not print any output, but may print warnings. Use the -verbose flag for
1590details, including timing.
1592To restore a backup, first shut down mox, move away the old data directory and
1593move an earlier backed up directory in its place, run "mox verifydata",
1594possibly with the "-fix" option, and restart mox. After the restore, you may
1595also want to run "mox bumpuidvalidity" for each account for which messages in a
1596mailbox changed, to force IMAP clients to synchronize mailbox state.
1598Before upgrading, to check if the upgrade will likely succeed, first make a
1599backup, then use the new mox binary to run "mox verifydata" on the backup. This
1600can change the backup files (e.g. upgrade database files, move away
1601unrecognized message files), so you should make a new backup before actually
1606 c.flag.BoolVar(&verbose, "verbose", false, "print progress")
1613 dstDataDir, err := filepath.Abs(args[0])
1614 xcheckf(err, "making path absolute")
1616 ctlcmdBackup(xctl(), dstDataDir, verbose)
1619func ctlcmdBackup(ctl *ctl, dstDataDir string, verbose bool) {
1620 ctl.xwrite("backup")
1621 ctl.xwrite(dstDataDir)
1623 ctl.xwrite("verbose")
1627 ctl.xstreamto(os.Stdout)
1631func cmdSetadminpassword(c *cmd) {
1632 c.help = `Set a new admin password, for the web interface.
1634The password is read from stdin. Its bcrypt hash is stored in a file named
1635"adminpasswd" in the configuration directory.
1637 if len(c.Parse()) != 0 {
1642 path := mox.ConfigDirPath(mox.Conf.Static.AdminPasswordFile)
1644 log.Fatal("no admin password file configured")
1647 pw := xreadpassword()
1648 pw, err := precis.OpaqueString.String(pw)
1649 xcheckf(err, `checking password with "precis" requirements`)
1650 hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
1651 xcheckf(err, "generating hash for password")
1652 err = os.WriteFile(path, hash, 0660)
1653 xcheckf(err, "writing hash to admin password file")
1656func xreadpassword() string {
1658Type new password. Password WILL echo.
1660WARNING: Bots will try to bruteforce your password. Connections with failed
1661authentication attempts will be rate limited but attackers WILL find weak
1662passwords. If your account is compromised, spammers are likely to abuse your
1663system, spamming your address and the wider internet in your name. So please
1664pick a random, unguessable password, preferably at least 12 characters.
1667 fmt.Printf("password: ")
1668 buf := make([]byte, 64)
1669 n, err := os.Stdin.Read(buf)
1670 xcheckf(err, "reading stdin")
1671 pw := string(buf[:n])
1672 pw = strings.TrimSuffix(strings.TrimSuffix(pw, "\r\n"), "\n")
1674 log.Fatal("password must be at least 8 characters")
1679func cmdSetaccountpassword(c *cmd) {
1680 c.params = "account"
1681 c.help = `Set new password an account.
1683The password is read from stdin. Secrets derived from the password, but not the
1684password itself, are stored in the account database. The stored secrets are for
1685authentication with: scram-sha-256, scram-sha-1, cram-md5, plain text (bcrypt
1688The parameter is an account name, as configured under Accounts in domains.conf
1689and as present in the data/accounts/ directory, not a configured email address
1698 pw := xreadpassword()
1700 ctlcmdSetaccountpassword(xctl(), args[0], pw)
1703func ctlcmdSetaccountpassword(ctl *ctl, account, password string) {
1704 ctl.xwrite("setaccountpassword")
1706 ctl.xwrite(password)
1710func cmdDeliver(c *cmd) {
1712 c.params = "address < message"
1713 c.help = "Deliver message to address."
1719 ctlcmdDeliver(xctl(), args[0])
1722func ctlcmdDeliver(ctl *ctl, address string) {
1723 ctl.xwrite("deliver")
1726 ctl.xstreamfrom(os.Stdin)
1729 fmt.Println("message delivered")
1731 log.Fatalf("deliver: %s", line)
1735func cmdDKIMGenrsa(c *cmd) {
1736 c.params = ">$selector._domainkey.$domain.rsa2048.privatekey.pkcs8.pem"
1737 c.help = `Generate a new 2048 bit RSA private key for use with DKIM.
1739The generated file is in PEM format, and has a comment it is generated for use
1742 if len(c.Parse()) != 0 {
1746 buf, err := admin.MakeDKIMRSAKey(dns.Domain{}, dns.Domain{})
1747 xcheckf(err, "making rsa private key")
1748 _, err = os.Stdout.Write(buf)
1749 xcheckf(err, "writing rsa private key")
1752func cmdDANEDial(c *cmd) {
1753 c.params = "host:port"
1755 c.flag.StringVar(&usages, "usages", "pkix-ta,pkix-ee,dane-ta,dane-ee", "allowed usages for dane, comma-separated list")
1756 c.help = `Dial the address using TLS with certificate verification using DANE.
1758Data is copied between connection and stdin/stdout until either side closes the
1766 allowedUsages := []adns.TLSAUsage{}
1768 for _, s := range strings.Split(usages, ",") {
1769 var usage adns.TLSAUsage
1770 switch strings.ToLower(s) {
1771 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
1772 usage = adns.TLSAUsagePKIXTA
1773 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
1774 usage = adns.TLSAUsagePKIXEE
1775 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
1776 usage = adns.TLSAUsageDANETA
1777 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
1778 usage = adns.TLSAUsageDANEEE
1780 log.Fatalf("unknown dane usage %q", s)
1782 allowedUsages = append(allowedUsages, usage)
1786 pkixRoots, err := x509.SystemCertPool()
1787 xcheckf(err, "get system pkix certificate pool")
1789 resolver := dns.StrictResolver{Pkg: "danedial"}
1790 conn, record, err := dane.Dial(context.Background(), c.log.Logger, resolver, "tcp", args[0], allowedUsages, pkixRoots)
1791 xcheckf(err, "dial")
1792 log.Printf("(connected, verified with %s)", record)
1795 _, err := io.Copy(os.Stdout, conn)
1796 xcheckf(err, "copy from connection to stdout")
1799 _, err = io.Copy(conn, os.Stdin)
1800 xcheckf(err, "copy from stdin to connection")
1803func cmdDANEDialmx(c *cmd) {
1804 c.params = "domain [destination-host]"
1805 var ehloHostname string
1806 c.flag.StringVar(&ehloHostname, "ehlohostname", "localhost", "hostname to send in smtp ehlo command")
1807 c.help = `Connect to MX server for domain using STARTTLS verified with DANE.
1809If no destination host is specified, regular delivery logic is used to find the
1810hosts to attempt delivery too. This involves following CNAMEs for the domain,
1811looking up MX records, and possibly falling back to the domain name itself as
1814If a destination host is specified, that is the only candidate host considered
1817With a list of destinations gathered, each is dialed until a successful SMTP
1818session verified with DANE has been initialized, including EHLO and STARTTLS
1821Once connected, data is copied between connection and stdin/stdout, until
1822either side closes the connection.
1824This command follows the same logic as delivery attempts made from the queue,
1825sharing most of its code.
1828 if len(args) != 1 && len(args) != 2 {
1832 ehloDomain, err := dns.ParseDomain(ehloHostname)
1833 xcheckf(err, "parsing ehlo hostname")
1835 origNextHop, err := dns.ParseDomain(args[0])
1836 xcheckf(err, "parse domain")
1838 ctxbg := context.Background()
1840 resolver := dns.StrictResolver{}
1842 var origNextHopAuthentic, expandedNextHopAuthentic bool
1843 var expandedNextHop dns.Domain
1844 var hosts []dns.IPDomain
1847 haveMX, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hosts, permanent, err = smtpclient.GatherDestinations(ctxbg, c.log.Logger, resolver, dns.IPDomain{Domain: origNextHop})
1848 status := "temporary"
1850 status = "permanent"
1853 log.Fatalf("gathering destinations: %v (%s)", err, status)
1855 if expandedNextHop != origNextHop {
1856 log.Printf("followed cnames to %s", expandedNextHop)
1859 log.Printf("found mx record, trying mx hosts")
1861 log.Printf("no mx record found, will try to connect to domain directly")
1863 if !origNextHopAuthentic {
1864 log.Fatalf("error: initial domain not dnssec-secure")
1866 if !expandedNextHopAuthentic {
1867 log.Fatalf("error: expanded domain not dnssec-secure")
1871 for _, h := range hosts {
1872 l = append(l, h.String())
1874 log.Printf("destinations: %s", strings.Join(l, ", "))
1876 d, err := dns.ParseDomain(args[1])
1878 log.Fatalf("parsing destination host: %v", err)
1880 log.Printf("skipping domain mx/cname lookups, assuming domain is dnssec-protected")
1882 origNextHopAuthentic = true
1883 expandedNextHopAuthentic = true
1885 hosts = []dns.IPDomain{{Domain: d}}
1888 dialedIPs := map[string][]net.IP{}
1889 for _, host := range hosts {
1890 // It should not be possible for hosts to have IP addresses: They are not
1891 // allowed by dns.ParseDomain, and MX records cannot contain them.
1893 log.Fatalf("unexpected IP address for destination host")
1896 log.Printf("attempting to connect to %s", host)
1898 authentic, expandedAuthentic, expandedHost, ips, _, err := smtpclient.GatherIPs(ctxbg, c.log.Logger, resolver, "ip", host, dialedIPs)
1900 log.Printf("resolving ips for %s: %v, skipping", host, err)
1904 log.Printf("no dnssec for ips of %s, skipping", host)
1907 if !expandedAuthentic {
1908 log.Printf("no dnssec for cname-followed ips of %s, skipping", host)
1911 if expandedHost != host.Domain {
1912 log.Printf("host %s cname-expanded to %s", host, expandedHost)
1914 log.Printf("host %s resolved to ips %s, looking up tlsa records", host, ips)
1916 daneRequired, daneRecords, tlsaBaseDomain, err := smtpclient.GatherTLSA(ctxbg, c.log.Logger, resolver, host.Domain, expandedAuthentic, expandedHost)
1918 log.Printf("looking up tlsa records: %s, skipping", err)
1921 tlsMode := smtpclient.TLSRequiredStartTLS
1922 if len(daneRecords) == 0 {
1924 log.Printf("host %s has no tlsa records, skipping", expandedHost)
1927 log.Printf("warning: only unusable tlsa records found, continuing with required tls without certificate verification")
1931 for _, r := range daneRecords {
1932 l = append(l, r.String())
1934 log.Printf("tlsa records: %s", strings.Join(l, "; "))
1937 tlsHostnames := smtpclient.GatherTLSANames(haveMX, expandedNextHopAuthentic, expandedAuthentic, origNextHop, expandedNextHop, host.Domain, tlsaBaseDomain)
1939 for _, name := range tlsHostnames {
1940 l = append(l, name.String())
1942 log.Printf("gathered valid tls certificate names for potential verification with dane-ta: %s", strings.Join(l, ", "))
1944 dialer := &net.Dialer{Timeout: 5 * time.Second}
1945 conn, _, err := smtpclient.Dial(ctxbg, c.log.Logger, dialer, dns.IPDomain{Domain: expandedHost}, ips, 25, dialedIPs, nil)
1947 log.Printf("dial %s: %v, skipping", expandedHost, err)
1950 log.Printf("connected to %s, %s, starting smtp session with ehlo and starttls with dane verification", expandedHost, conn.RemoteAddr())
1952 var verifiedRecord adns.TLSA
1953 opts := smtpclient.Opts{
1954 DANERecords: daneRecords,
1955 DANEMoreHostnames: tlsHostnames[1:],
1956 DANEVerifiedRecord: &verifiedRecord,
1957 RootCAs: mox.Conf.Static.TLS.CertPool,
1960 sc, err := smtpclient.New(ctxbg, c.log.Logger, conn, tlsMode, tlsPKIX, ehloDomain, tlsHostnames[0], opts)
1962 log.Printf("setting up smtp session: %v, skipping", err)
1967 smtpConn, err := sc.Conn()
1969 log.Fatalf("error: taking over smtp connection: %s", err)
1971 log.Printf("tls verified with tlsa record: %s", verifiedRecord)
1972 log.Printf("smtp session initialized and connected to stdin/stdout")
1975 _, err := io.Copy(os.Stdout, smtpConn)
1976 xcheckf(err, "copy from connection to stdout")
1979 _, err = io.Copy(smtpConn, os.Stdin)
1980 xcheckf(err, "copy from stdin to connection")
1983 log.Fatalf("no remaining destinations")
1986func cmdDANEMakeRecord(c *cmd) {
1987 c.params = "usage selector matchtype [certificate.pem | publickey.pem | privatekey.pem]"
1988 c.help = `Print TLSA record for given certificate/key and parameters.
1991- usage: pkix-ta (0), pkix-ee (1), dane-ta (2), dane-ee (3)
1992- selector: cert (0), spki (1)
1993- matchtype: full (0), sha2-256 (1), sha2-512 (2)
1995Common DANE TLSA record parameters are: dane-ee spki sha2-256, or 3 1 1,
1996followed by a sha2-256 hash of the DER-encoded "SPKI" (subject public key info)
1997from the certificate. An example DNS zone file entry:
1999 _25._tcp.example.com. TLSA 3 1 1 133b919c9d65d8b1488157315327334ead8d83372db57465ecabf53ee5748aee
2001The first usable information from the pem file is used to compose the TLSA
2002record. In case of selector "cert", a certificate is required. Otherwise the
2003"subject public key info" (spki) of the first certificate or public or private
2004key (pkcs#8, pkcs#1 or ec private key) is used.
2012 var usage adns.TLSAUsage
2013 switch strings.ToLower(args[0]) {
2014 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
2015 usage = adns.TLSAUsagePKIXTA
2016 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
2017 usage = adns.TLSAUsagePKIXEE
2018 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
2019 usage = adns.TLSAUsageDANETA
2020 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
2021 usage = adns.TLSAUsageDANEEE
2023 if v, err := strconv.ParseUint(args[0], 10, 16); err != nil {
2024 log.Fatalf("bad usage %q", args[0])
2026 // Does not influence certificate association data, so we can accept other numbers.
2027 log.Printf("warning: continuing with unrecognized tlsa usage %d", v)
2028 usage = adns.TLSAUsage(v)
2032 var selector adns.TLSASelector
2033 switch strings.ToLower(args[1]) {
2034 case "cert", strconv.Itoa(int(adns.TLSASelectorCert)):
2035 selector = adns.TLSASelectorCert
2036 case "spki", strconv.Itoa(int(adns.TLSASelectorSPKI)):
2037 selector = adns.TLSASelectorSPKI
2039 log.Fatalf("bad selector %q", args[1])
2042 var matchType adns.TLSAMatchType
2043 switch strings.ToLower(args[2]) {
2044 case "full", strconv.Itoa(int(adns.TLSAMatchTypeFull)):
2045 matchType = adns.TLSAMatchTypeFull
2046 case "sha2-256", strconv.Itoa(int(adns.TLSAMatchTypeSHA256)):
2047 matchType = adns.TLSAMatchTypeSHA256
2048 case "sha2-512", strconv.Itoa(int(adns.TLSAMatchTypeSHA512)):
2049 matchType = adns.TLSAMatchTypeSHA512
2051 log.Fatalf("bad matchtype %q", args[2])
2054 buf, err := os.ReadFile(args[3])
2055 xcheckf(err, "reading certificate")
2057 var block *pem.Block
2058 block, buf = pem.Decode(buf)
2062 extra = " (with leftover data from pem file)"
2064 if selector == adns.TLSASelectorCert {
2065 log.Fatalf("no certificate found in pem file%s", extra)
2067 log.Fatalf("no certificate or public or private key found in pem file%s", extra)
2070 var cert *x509.Certificate
2072 if block.Type == "CERTIFICATE" {
2073 cert, err = x509.ParseCertificate(block.Bytes)
2074 xcheckf(err, "parse certificate")
2076 case adns.TLSASelectorCert:
2078 case adns.TLSASelectorSPKI:
2079 data = cert.RawSubjectPublicKeyInfo
2081 } else if selector == adns.TLSASelectorCert {
2082 // We need a certificate, just a public/private key won't do.
2083 log.Printf("skipping pem type %q, certificate is required", block.Type)
2086 var privKey, pubKey any
2090 _, err := x509.ParsePKIXPublicKey(block.Bytes)
2091 xcheckf(err, "parse pkix subject public key info (spki)")
2093 case "EC PRIVATE KEY":
2094 privKey, err = x509.ParseECPrivateKey(block.Bytes)
2095 xcheckf(err, "parse ec private key")
2096 case "RSA PRIVATE KEY":
2097 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
2098 xcheckf(err, "parse pkcs#1 rsa private key")
2099 case "RSA PUBLIC KEY":
2100 pubKey, err = x509.ParsePKCS1PublicKey(block.Bytes)
2101 xcheckf(err, "parse pkcs#1 rsa public key")
2103 // PKCS#8 private key
2104 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
2105 xcheckf(err, "parse pkcs#8 private key")
2107 log.Printf("skipping unrecognized pem type %q", block.Type)
2111 if pubKey == nil && privKey != nil {
2112 if signer, ok := privKey.(crypto.Signer); !ok {
2113 log.Fatalf("private key of type %T is not a signer, cannot get public key", privKey)
2115 pubKey = signer.Public()
2119 // Should not happen.
2120 log.Fatalf("internal error: did not find private or public key")
2122 data, err = x509.MarshalPKIXPublicKey(pubKey)
2123 xcheckf(err, "marshal pkix subject public key info (spki)")
2128 case adns.TLSAMatchTypeFull:
2129 case adns.TLSAMatchTypeSHA256:
2130 p := sha256.Sum256(data)
2132 case adns.TLSAMatchTypeSHA512:
2133 p := sha512.Sum512(data)
2136 fmt.Printf("%d %d %d %x\n", usage, selector, matchType, data)
2141func cmdDNSLookup(c *cmd) {
2142 c.params = "[ptr | mx | cname | ips | a | aaaa | ns | txt | srv | tlsa] name"
2143 c.help = `Lookup DNS name of given type.
2145Lookup always prints whether the response was DNSSEC-protected.
2149mox dns lookup ptr 1.1.1.1
2150mox dns lookup mx xmox.nl
2151mox dns lookup txt _dmarc.xmox.nl.
2152mox dns lookup tlsa _25._tcp.xmox.nl
2160 resolver := dns.StrictResolver{Pkg: "dns"}
2162 // like xparseDomain, but treat unparseable domain as an ASCII name so names with
2163 // underscores are still looked up, e,g <selector>._domainkey.<host>.
2164 xdomain := func(s string) dns.Domain {
2165 d, err := dns.ParseDomain(s)
2167 return dns.Domain{ASCII: strings.TrimSuffix(s, ".")}
2172 cmd, name := args[0], args[1]
2176 ip := xparseIP(name, "ip")
2177 ptrs, result, err := resolver.LookupAddr(context.Background(), ip.String())
2179 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2181 fmt.Printf("names (%d, %s):\n", len(ptrs), dnssecStatus(result.Authentic))
2182 for _, ptr := range ptrs {
2183 fmt.Printf("- %s\n", ptr)
2187 name := xdomain(name)
2188 mxl, result, err := resolver.LookupMX(context.Background(), name.ASCII+".")
2190 log.Printf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2191 // We can still have valid records...
2193 fmt.Printf("mx records (%d, %s):\n", len(mxl), dnssecStatus(result.Authentic))
2194 for _, mx := range mxl {
2195 fmt.Printf("- %s, preference %d\n", mx.Host, mx.Pref)
2199 name := xdomain(name)
2200 target, result, err := resolver.LookupCNAME(context.Background(), name.ASCII+".")
2202 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2204 fmt.Printf("%s (%s)\n", target, dnssecStatus(result.Authentic))
2206 case "ips", "a", "aaaa":
2210 } else if cmd == "aaaa" {
2213 name := xdomain(name)
2214 ips, result, err := resolver.LookupIP(context.Background(), network, name.ASCII+".")
2216 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2218 fmt.Printf("records (%d, %s):\n", len(ips), dnssecStatus(result.Authentic))
2219 for _, ip := range ips {
2220 fmt.Printf("- %s\n", ip)
2224 name := xdomain(name)
2225 nsl, result, err := resolver.LookupNS(context.Background(), name.ASCII+".")
2227 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2229 fmt.Printf("ns records (%d, %s):\n", len(nsl), dnssecStatus(result.Authentic))
2230 for _, ns := range nsl {
2231 fmt.Printf("- %s\n", ns)
2235 host := xdomain(name)
2236 l, result, err := resolver.LookupTXT(context.Background(), host.ASCII+".")
2238 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2240 fmt.Printf("txt records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2241 for _, txt := range l {
2242 fmt.Printf("- %s\n", txt)
2246 host := xdomain(name)
2247 _, l, result, err := resolver.LookupSRV(context.Background(), "", "", host.ASCII+".")
2249 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2251 fmt.Printf("srv records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2252 for _, srv := range l {
2253 fmt.Printf("- host %s, port %d, priority %d, weight %d\n", srv.Target, srv.Port, srv.Priority, srv.Weight)
2257 host := xdomain(name)
2258 l, result, err := resolver.LookupTLSA(context.Background(), 0, "", host.ASCII+".")
2260 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2262 fmt.Printf("tlsa records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2263 for _, tlsa := range l {
2264 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)
2267 log.Fatalf("unknown record type %q", args[0])
2271func cmdDKIMGened25519(c *cmd) {
2272 c.params = ">$selector._domainkey.$domain.ed25519.privatekey.pkcs8.pem"
2273 c.help = `Generate a new ed25519 key for use with DKIM.
2275Ed25519 keys are much smaller than RSA keys of comparable cryptographic
2276strength. This is convenient because of maximum DNS message sizes. At the time
2277of writing, not many mail servers appear to support ed25519 DKIM keys though,
2278so it is recommended to sign messages with both RSA and ed25519 keys.
2280 if len(c.Parse()) != 0 {
2284 buf, err := admin.MakeDKIMEd25519Key(dns.Domain{}, dns.Domain{})
2285 xcheckf(err, "making dkim ed25519 key")
2286 _, err = os.Stdout.Write(buf)
2287 xcheckf(err, "writing dkim ed25519 key")
2290func cmdDKIMTXT(c *cmd) {
2291 c.params = "<$selector._domainkey.$domain.key.pkcs8.pem"
2292 c.help = `Print a DKIM DNS TXT record with the public key derived from the private key read from stdin.
2294The DNS should be configured as a TXT record at $selector._domainkey.$domain.
2296 if len(c.Parse()) != 0 {
2300 privKey, err := parseDKIMKey(os.Stdin)
2301 xcheckf(err, "reading dkim private key from stdin")
2305 Hashes: []string{"sha256"},
2306 Flags: []string{"s"},
2309 switch key := privKey.(type) {
2310 case *rsa.PrivateKey:
2311 r.PublicKey = key.Public()
2312 case ed25519.PrivateKey:
2313 r.PublicKey = key.Public()
2316 log.Fatalf("unsupported private key type %T, must be rsa or ed25519", privKey)
2319 record, err := r.Record()
2320 xcheckf(err, "making record")
2321 fmt.Print("<selector>._domainkey.<your.domain.> TXT ")
2325 s, record = record[:100], record[100:]
2329 fmt.Printf(`"%s" `, s)
2334func parseDKIMKey(r io.Reader) (any, error) {
2335 buf, err := io.ReadAll(r)
2337 return nil, fmt.Errorf("reading pem from stdin: %v", err)
2339 b, _ := pem.Decode(buf)
2341 return nil, fmt.Errorf("decoding pem: %v", err)
2343 privKey, err := x509.ParsePKCS8PrivateKey(b.Bytes)
2345 return nil, fmt.Errorf("parsing private key: %v", err)
2350func cmdDKIMVerify(c *cmd) {
2351 c.params = "message"
2352 c.help = `Verify the DKIM signatures in a message and print the results.
2354The message is parsed, and the DKIM-Signature headers are validated. Validation
2355of older messages may fail because the DNS records have been removed or changed
2356by now, or because the signature header may have specified an expiration time
2364 msgf, err := os.Open(args[0])
2365 xcheckf(err, "open message")
2367 results, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, false, dkim.DefaultPolicy, msgf, true)
2368 xcheckf(err, "dkim verify")
2370 for _, result := range results {
2372 if result.Sig == nil {
2373 log.Printf("warning: could not parse signature")
2375 sigh, err = result.Sig.Header()
2377 log.Printf("warning: packing signature: %s", err)
2381 if result.Record == nil {
2382 log.Printf("warning: missing DNS record")
2384 txt, err = result.Record.Record()
2386 log.Printf("warning: packing record: %s", err)
2389 fmt.Printf("status %q, err %v\nrecord %q\nheader %s\n", result.Status, result.Err, txt, sigh)
2393func cmdDKIMSign(c *cmd) {
2394 c.params = "message"
2395 c.help = `Sign a message, adding DKIM-Signature headers based on the domain in the From header.
2397The message is parsed, the domain looked up in the configuration files, and
2398DKIM-Signature headers generated. The message is printed with the DKIM-Signature
2406 msgf, err := os.Open(args[0])
2407 xcheckf(err, "open message")
2410 p, err := message.Parse(c.log.Logger, true, msgf)
2411 xcheckf(err, "parsing message")
2413 if len(p.Envelope.From) != 1 {
2414 log.Fatalf("found %d from headers, need exactly 1", len(p.Envelope.From))
2416 localpart, err := smtp.ParseLocalpart(p.Envelope.From[0].User)
2417 xcheckf(err, "parsing localpart of address in from-header")
2418 dom, err := dns.ParseDomain(p.Envelope.From[0].Host)
2419 xcheckf(err, "parsing domain of address in from-header")
2423 domConf, ok := mox.Conf.Domain(dom)
2425 log.Fatalf("domain %s not configured", dom)
2428 selectors := mox.DKIMSelectors(domConf.DKIM)
2429 headers, err := dkim.Sign(context.Background(), c.log.Logger, localpart, dom, selectors, false, msgf)
2430 xcheckf(err, "signing message with dkim")
2432 log.Fatalf("no DKIM configured for domain %s", dom)
2434 _, err = fmt.Fprint(os.Stdout, headers)
2435 xcheckf(err, "write headers")
2436 _, err = io.Copy(os.Stdout, msgf)
2437 xcheckf(err, "write message")
2440func cmdDKIMLookup(c *cmd) {
2441 c.params = "selector domain"
2442 c.help = "Lookup and print the DKIM record for the selector at the domain."
2448 selector := xparseDomain(args[0], "selector")
2449 domain := xparseDomain(args[1], "domain")
2451 status, record, txt, authentic, err := dkim.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, selector, domain)
2453 fmt.Printf("error: %s\n", err)
2455 if status != dkim.StatusNeutral {
2456 fmt.Printf("status: %s\n", status)
2459 fmt.Printf("TXT record: %s\n", txt)
2462 fmt.Println("dnssec-signed: yes")
2464 fmt.Println("dnssec-signed: no")
2467 fmt.Printf("Record:\n")
2469 "version", record.Version,
2470 "hashes", record.Hashes,
2472 "notes", record.Notes,
2473 "services", record.Services,
2474 "flags", record.Flags,
2476 for i := 0; i < len(pairs); i += 2 {
2477 fmt.Printf("\t%s: %v\n", pairs[i], pairs[i+1])
2482func cmdDMARCLookup(c *cmd) {
2484 c.help = "Lookup dmarc policy for domain, a DNS TXT record at _dmarc.<domain>, validate and print it."
2490 fromdomain := xparseDomain(args[0], "domain")
2491 _, domain, _, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, fromdomain)
2492 xcheckf(err, "dmarc lookup domain %s", fromdomain)
2493 fmt.Printf("dmarc record at domain %s: %s\n", domain, txt)
2494 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2497func dnssecStatus(v bool) string {
2499 return "with dnssec"
2501 return "without dnssec"
2504func cmdDMARCVerify(c *cmd) {
2505 c.params = "remoteip mailfromaddress helodomain < message"
2506 c.help = `Parse an email message and evaluate it against the DMARC policy of the domain in the From-header.
2508mailfromaddress and helodomain are used for SPF validation. If both are empty,
2509SPF validation is skipped.
2511mailfromaddress should be the address used as MAIL FROM in the SMTP session.
2512For DSN messages, that address may be empty. The helo domain was specified at
2513the beginning of the SMTP transaction that delivered the message. These values
2514can be found in message headers.
2521 var heloDomain *dns.Domain
2523 remoteIP := xparseIP(args[0], "remoteip")
2525 var mailfrom *smtp.Address
2527 a, err := smtp.ParseAddress(args[1])
2528 xcheckf(err, "parsing mailfrom address")
2532 d := xparseDomain(args[2], "helo domain")
2535 var received *spf.Received
2536 spfStatus := spf.StatusNone
2537 var spfIdentity *dns.Domain
2538 if mailfrom != nil || heloDomain != nil {
2539 spfArgs := spf.Args{
2541 LocalIP: net.ParseIP("127.0.0.1"),
2542 LocalHostname: dns.Domain{ASCII: "localhost"},
2544 if mailfrom != nil {
2545 spfArgs.MailFromLocalpart = mailfrom.Localpart
2546 spfArgs.MailFromDomain = mailfrom.Domain
2548 if heloDomain != nil {
2549 spfArgs.HelloDomain = dns.IPDomain{Domain: *heloDomain}
2551 rspf, spfDomain, expl, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfArgs)
2553 log.Printf("spf verify: %v (explanation: %q, authentic %v)", err, expl, authentic)
2556 spfStatus = received.Result
2557 // todo: should probably potentially do two separate spf validations
2558 if mailfrom != nil {
2559 spfIdentity = &mailfrom.Domain
2561 spfIdentity = heloDomain
2563 fmt.Printf("spf result: %s: %s (%s)\n", spfDomain, spfStatus, dnssecStatus(authentic))
2567 data, err := io.ReadAll(os.Stdin)
2568 xcheckf(err, "read message")
2569 dmarcFrom, _, _, err := message.From(c.log.Logger, false, bytes.NewReader(data), nil)
2570 xcheckf(err, "extract dmarc from message")
2572 const ignoreTestMode = false
2573 dkimResults, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, true, func(*dkim.Sig) error { return nil }, bytes.NewReader(data), ignoreTestMode)
2574 xcheckf(err, "dkim verify")
2575 for _, r := range dkimResults {
2576 fmt.Printf("dkim result: %q (err %v)\n", r.Status, r.Err)
2579 _, result := dmarc.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, dmarcFrom.Domain, dkimResults, spfStatus, spfIdentity, false)
2580 xcheckf(result.Err, "dmarc verify")
2581 fmt.Printf("dmarc from: %s\ndmarc status: %q\ndmarc reject: %v\ncmarc record: %s\n", dmarcFrom, result.Status, result.Reject, result.Record)
2584func cmdDMARCCheckreportaddrs(c *cmd) {
2586 c.help = `For each reporting address in the domain's DMARC record, check if it has opted into receiving reports (if needed).
2588A DMARC record can request reports about DMARC evaluations to be sent to an
2589email/http address. If the organizational domains of that of the DMARC record
2590and that of the report destination address do not match, the destination
2591address must opt-in to receiving DMARC reports by creating a DMARC record at
2592<dmarcdomain>._report._dmarc.<reportdestdomain>.
2599 dom := xparseDomain(args[0], "domain")
2600 _, domain, record, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dom)
2601 xcheckf(err, "dmarc lookup domain %s", dom)
2602 fmt.Printf("dmarc record at domain %s: %q\n", domain, txt)
2603 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2605 check := func(kind, addr string) {
2608 printResult := func(format string, args ...any) {
2609 fmt.Printf("%s %s: %s (%s)\n", kind, addr, fmt.Sprintf(format, args...), dnssecStatus(authentic))
2612 u, err := url.Parse(addr)
2614 printResult("parsing uri: %v (skipping)", addr, err)
2617 var destdom dns.Domain
2620 a, err := smtp.ParseAddress(u.Opaque)
2622 printResult("parsing destination email address %s: %v (skipping)", u.Opaque, err)
2627 printResult("unrecognized scheme in reporting address %s (skipping)", u.Scheme)
2631 if publicsuffix.Lookup(context.Background(), c.log.Logger, dom) == publicsuffix.Lookup(context.Background(), c.log.Logger, destdom) {
2632 printResult("pass (same organizational domain)")
2636 accepts, status, _, txts, authentic, err := dmarc.LookupExternalReportsAccepted(context.Background(), c.log.Logger, dns.StrictResolver{}, domain, destdom)
2638 txtaddr := fmt.Sprintf("%s._report._dmarc.%s", domain.ASCII, destdom.ASCII)
2640 txtstr = fmt.Sprintf(" (no txt records %s)", txtaddr)
2642 txtstr = fmt.Sprintf(" (txt record %s: %q)", txtaddr, txts)
2644 if status != dmarc.StatusNone {
2645 printResult("fail: %s%s", err, txtstr)
2647 printResult("pass%s", txtstr)
2648 } else if err != nil {
2649 printResult("fail: %s%s", err, txtstr)
2651 printResult("fail%s", txtstr)
2655 for _, uri := range record.AggregateReportAddresses {
2656 check("aggregate reporting", uri.Address)
2658 for _, uri := range record.FailureReportAddresses {
2659 check("failure reporting", uri.Address)
2663func cmdDMARCParsereportmsg(c *cmd) {
2664 c.params = "message ..."
2665 c.help = `Parse a DMARC report from an email message, and print its extracted details.
2667DMARC reports are periodically mailed, if requested in the DMARC DNS record of
2668a domain. Reports are sent by mail servers that received messages with our
2669domain in a From header. This may or may not be legatimate email. DMARC reports
2670contain summaries of evaluations of DMARC and DKIM/SPF, which can help
2671understand email deliverability problems.
2678 for _, arg := range args {
2679 f, err := os.Open(arg)
2680 xcheckf(err, "open %q", arg)
2681 feedback, err := dmarcrpt.ParseMessageReport(c.log.Logger, f)
2682 xcheckf(err, "parse report in %q", arg)
2683 meta := feedback.ReportMetadata
2684 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)
2685 if len(meta.Errors) > 0 {
2686 fmt.Printf("Errors:\n")
2687 for _, s := range meta.Errors {
2688 fmt.Printf("\t- %s\n", s)
2691 pol := feedback.PolicyPublished
2692 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)
2693 for _, record := range feedback.Records {
2694 idents := record.Identifiers
2695 fmt.Printf("\theaderfrom %q, envelopes from %q, to %q\n", idents.HeaderFrom, idents.EnvelopeFrom, idents.EnvelopeTo)
2696 eval := record.Row.PolicyEvaluated
2698 for _, reason := range eval.Reasons {
2699 reasons += "; " + string(reason.Type)
2700 if reason.Comment != "" {
2701 reasons += fmt.Sprintf(": %q", reason.Comment)
2704 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)
2705 for _, dkim := range record.AuthResults.DKIM {
2707 if dkim.HumanResult != "" {
2708 result = fmt.Sprintf(": %q", dkim.HumanResult)
2710 fmt.Printf("\t\tdkim %s; domain %q selector %q%s\n", dkim.Result, dkim.Domain, dkim.Selector, result)
2712 for _, spf := range record.AuthResults.SPF {
2713 fmt.Printf("\t\tspf %s; domain %q scope %q\n", spf.Result, spf.Domain, spf.Scope)
2719func cmdDMARCDBAddReport(c *cmd) {
2721 c.params = "fromdomain < message"
2722 c.help = "Add a DMARC report to the database."
2730 fromdomain := xparseDomain(args[0], "domain")
2731 fmt.Fprintln(os.Stderr, "reading report message from stdin")
2732 report, err := dmarcrpt.ParseMessageReport(c.log.Logger, os.Stdin)
2733 xcheckf(err, "parse message")
2734 err = dmarcdb.AddReport(context.Background(), report, fromdomain)
2735 xcheckf(err, "add dmarc report")
2738func cmdTLSRPTLookup(c *cmd) {
2740 c.help = `Lookup the TLSRPT record for the domain.
2742A TLSRPT record typically contains an email address where reports about TLS
2743connectivity should be sent. Mail servers attempting delivery to our domain
2744should attempt to use TLS. TLSRPT lets them report how many connection
2745successfully used TLS, and how what kind of errors occurred otherwise.
2752 d := xparseDomain(args[0], "domain")
2753 _, txt, err := tlsrpt.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, d)
2754 xcheckf(err, "tlsrpt lookup for %s", d)
2758func cmdTLSRPTParsereportmsg(c *cmd) {
2759 c.params = "message ..."
2760 c.help = `Parse and print the TLSRPT in the message.
2762The report is printed in formatted JSON.
2769 for _, arg := range args {
2770 f, err := os.Open(arg)
2771 xcheckf(err, "open %q", arg)
2772 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, f)
2773 xcheckf(err, "parse report in %q", arg)
2774 // todo future: only print the highlights?
2775 enc := json.NewEncoder(os.Stdout)
2776 enc.SetIndent("", "\t")
2777 enc.SetEscapeHTML(false)
2778 err = enc.Encode(reportJSON)
2779 xcheckf(err, "write report")
2783func cmdSPFCheck(c *cmd) {
2784 c.params = "domain ip"
2785 c.help = `Check the status of IP for the policy published in DNS for the domain.
2787IPs may be allowed to send for a domain, or disallowed, and several shades in
2788between. If not allowed, an explanation may be provided by the policy. If so,
2789the explanation is printed. The SPF mechanism that matched (if any) is also
2797 domain := xparseDomain(args[0], "domain")
2799 ip := xparseIP(args[1], "ip")
2801 spfargs := spf.Args{
2803 MailFromLocalpart: "user",
2804 MailFromDomain: domain,
2805 HelloDomain: dns.IPDomain{Domain: domain},
2806 LocalIP: net.ParseIP("127.0.0.1"),
2807 LocalHostname: dns.Domain{ASCII: "localhost"},
2809 r, _, explanation, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfargs)
2811 fmt.Printf("error: %s\n", err)
2813 if explanation != "" {
2814 fmt.Printf("explanation: %s\n", explanation)
2816 fmt.Printf("status: %s (%s)\n", r.Result, dnssecStatus(authentic))
2817 if r.Mechanism != "" {
2818 fmt.Printf("mechanism: %s\n", r.Mechanism)
2822func cmdSPFParse(c *cmd) {
2823 c.params = "txtrecord"
2824 c.help = "Parse the record as SPF record. If valid, nothing is printed."
2830 _, _, err := spf.ParseRecord(args[0])
2831 xcheckf(err, "parsing record")
2834func cmdSPFLookup(c *cmd) {
2836 c.help = "Lookup the SPF record for the domain and print it."
2842 domain := xparseDomain(args[0], "domain")
2843 _, txt, _, authentic, err := spf.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
2844 xcheckf(err, "spf lookup for %s", domain)
2846 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2849func cmdMTASTSLookup(c *cmd) {
2851 c.help = `Lookup the MTASTS record and policy for the domain.
2853MTA-STS is a mechanism for a domain to specify if it requires TLS connections
2854for delivering email. If a domain has a valid MTA-STS DNS TXT record at
2855_mta-sts.<domain> it signals it implements MTA-STS. A policy can then be
2856fetched at https://mta-sts.<domain>/.well-known/mta-sts.txt. The policy
2857specifies the mode (enforce, testing, none), which MX servers support TLS and
2858should be used, and how long the policy can be cached.
2865 domain := xparseDomain(args[0], "domain")
2867 record, policy, _, err := mtasts.Get(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
2869 fmt.Printf("error: %s\n", err)
2872 fmt.Printf("DNS TXT record _mta-sts.%s: %s\n", domain.ASCII, record.String())
2876 fmt.Printf("policy at https://mta-sts.%s/.well-known/mta-sts.txt:\n", domain.ASCII)
2877 fmt.Printf("%s", policy.String())
2881func cmdRetrain(c *cmd) {
2882 c.params = "[accountname]"
2883 c.help = `Recreate and retrain the junk filter for the account or all accounts.
2885Useful after having made changes to the junk filter configuration, or if the
2886implementation has changed.
2898 ctlcmdRetrain(xctl(), account)
2901func ctlcmdRetrain(ctl *ctl, account string) {
2902 ctl.xwrite("retrain")
2907func cmdTLSRPTDBAddReport(c *cmd) {
2909 c.params = "< message"
2910 c.help = "Parse a TLS report from the message and add it to the database."
2912 c.flag.BoolVar(&hostReport, "hostreport", false, "report for a host instead of domain")
2920 // First read message, to get the From-header. Then parse it as TLSRPT.
2921 fmt.Fprintln(os.Stderr, "reading report message from stdin")
2922 buf, err := io.ReadAll(os.Stdin)
2923 xcheckf(err, "reading message")
2924 part, err := message.Parse(c.log.Logger, true, bytes.NewReader(buf))
2925 xcheckf(err, "parsing message")
2926 if part.Envelope == nil || len(part.Envelope.From) != 1 {
2927 log.Fatalf("message must have one From-header")
2929 from := part.Envelope.From[0]
2930 domain := xparseDomain(from.Host, "domain")
2932 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, bytes.NewReader(buf))
2933 xcheckf(err, "parsing tls report in message")
2935 mailfrom := from.User + "@" + from.Host // todo future: should escape and such
2936 report := reportJSON.Convert()
2937 err = tlsrptdb.AddReport(context.Background(), c.log, domain, mailfrom, hostReport, &report)
2938 xcheckf(err, "add tls report to database")
2941func cmdDNSBLCheck(c *cmd) {
2942 c.params = "zone ip"
2943 c.help = `Test if IP is in the DNS blocklist of the zone, e.g. bl.spamcop.net.
2945If the IP is in the blocklist, an explanation is printed. This is typically a
2946URL with more information.
2953 zone := xparseDomain(args[0], "zone")
2954 ip := xparseIP(args[1], "ip")
2956 status, explanation, err := dnsbl.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, zone, ip)
2957 fmt.Printf("status: %s\n", status)
2958 if status == dnsbl.StatusFail {
2959 fmt.Printf("explanation: %q\n", explanation)
2962 fmt.Printf("error: %s\n", err)
2966func cmdDNSBLCheckhealth(c *cmd) {
2968 c.help = `Check the health of the DNS blocklist represented by zone, e.g. bl.spamcop.net.
2970The health of a DNS blocklist can be checked by querying for 127.0.0.1 and
2971127.0.0.2. The second must and the first must not be present.
2978 zone := xparseDomain(args[0], "zone")
2979 err := dnsbl.CheckHealth(context.Background(), c.log.Logger, dns.StrictResolver{}, zone)
2980 xcheckf(err, "unhealthy")
2981 fmt.Println("healthy")
2984func cmdCheckupdate(c *cmd) {
2985 c.help = `Check if a newer version of mox is available.
2987A single DNS TXT lookup to _updates.xmox.nl tells if a new version is
2988available. If so, a changelog is fetched from https://updates.xmox.nl, and the
2989individual entries verified with a builtin public key. The changelog is
2992 if len(c.Parse()) != 0 {
2997 current, lastknown, _, err := store.LastKnown()
2999 log.Printf("getting last known version: %s", err)
3001 fmt.Printf("last known version: %s\n", lastknown)
3002 fmt.Printf("current version: %s\n", current)
3004 latest, _, err := updates.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dns.Domain{ASCII: changelogDomain})
3005 xcheckf(err, "lookup of latest version")
3006 fmt.Printf("latest version: %s\n", latest)
3008 if latest.After(current) {
3009 changelog, err := updates.FetchChangelog(context.Background(), c.log.Logger, changelogURL, current, changelogPubKey)
3010 xcheckf(err, "fetching changelog")
3011 if len(changelog.Changes) == 0 {
3012 log.Printf("no changes in changelog")
3015 fmt.Println("Changelog")
3016 for _, c := range changelog.Changes {
3017 fmt.Println("\n" + strings.TrimSpace(c.Text))
3022func cmdCid(c *cmd) {
3024 c.help = `Turn an ID from a Received header into a cid, for looking up in logs.
3026A cid is essentially a connection counter initialized when mox starts. Each log
3027line contains a cid. Received headers added by mox contain a unique ID that can
3028be decrypted to a cid by admin of a mox instance only.
3036 recvidpath := mox.DataDirPath("receivedid.key")
3037 recvidbuf, err := os.ReadFile(recvidpath)
3038 xcheckf(err, "reading %s", recvidpath)
3039 if len(recvidbuf) != 16+8 {
3040 log.Fatalf("bad data in %s: got %d bytes, expect 16+8=24", recvidpath, len(recvidbuf))
3042 err = mox.ReceivedIDInit(recvidbuf[:16], recvidbuf[16:])
3043 xcheckf(err, "init receivedid")
3045 cid, err := mox.ReceivedToCid(args[0])
3046 xcheckf(err, "received id to cid")
3047 fmt.Printf("%x\n", cid)
3050func cmdVersion(c *cmd) {
3051 c.help = "Prints this mox version."
3052 if len(c.Parse()) != 0 {
3055 fmt.Println(moxvar.Version)
3056 fmt.Printf("%s/%s\n", runtime.GOOS, runtime.GOARCH)
3059func cmdWebapi(c *cmd) {
3060 c.params = "[method [baseurl-with-credentials]"
3061 c.help = "Lists available methods, prints request/response parameters for method, or calls a method with a request read from standard input."
3067 t := reflect.TypeFor[webapi.Methods]()
3068 methods := map[string]reflect.Type{}
3070 for i := 0; i < t.NumMethod(); i++ {
3072 methods[mt.Name] = mt.Type
3073 ml = append(ml, mt.Name)
3077 fmt.Println(strings.Join(ml, "\n"))
3081 mt, ok := methods[args[0]]
3083 log.Fatalf("unknown method %q", args[0])
3085 resultNotJSON := mt.Out(0).Kind() == reflect.Interface
3088 fmt.Println("# Example request")
3090 printJSON("\t", mox.FillExample(nil, reflect.New(mt.In(1))).Interface())
3093 fmt.Println("Output is non-JSON data.")
3096 fmt.Println("# Example response")
3098 printJSON("\t", mox.FillExample(nil, reflect.New(mt.Out(0))).Interface())
3104 response = reflect.New(mt.Out(0))
3107 fmt.Fprintln(os.Stderr, "reading request from stdin...")
3108 request, err := io.ReadAll(os.Stdin)
3109 xcheckf(err, "read message")
3111 dec := json.NewDecoder(bytes.NewReader(request))
3112 dec.DisallowUnknownFields()
3113 err = dec.Decode(reflect.New(mt.In(1)).Interface())
3114 xcheckf(err, "parsing request")
3116 resp, err := http.PostForm(args[1]+args[0], url.Values{"request": []string{string(request)}})
3117 xcheckf(err, "http post")
3118 defer resp.Body.Close()
3119 if resp.StatusCode == http.StatusBadRequest {
3120 buf, err := io.ReadAll(&moxio.LimitReader{R: resp.Body, Limit: 10 * 1024})
3121 xcheckf(err, "reading response for 400 bad request error")
3122 err = json.Unmarshal(buf, &response)
3124 printJSON("", response)
3126 fmt.Fprintf(os.Stderr, "(not json)\n")
3127 os.Stderr.Write(buf)
3130 } else if resp.StatusCode != http.StatusOK {
3131 fmt.Fprintf(os.Stderr, "http response %s\n", resp.Status)
3132 _, err := io.Copy(os.Stderr, resp.Body)
3133 xcheckf(err, "copy body")
3135 err := json.NewDecoder(resp.Body).Decode(&resp)
3136 xcheckf(err, "unmarshal response")
3137 printJSON("", response)
3141func printJSON(indent string, v any) {
3142 fmt.Printf("%s", indent)
3143 enc := json.NewEncoder(os.Stdout)
3144 enc.SetIndent(indent, "\t")
3145 enc.SetEscapeHTML(false)
3146 err := enc.Encode(v)
3147 xcheckf(err, "encode json")
3150// 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.
3151func cmdBumpUIDValidity(c *cmd) {
3152 c.params = "account [mailbox]"
3153 c.help = `Change the IMAP UID validity of the mailbox, causing IMAP clients to refetch messages.
3155This can be useful after manually repairing metadata about the account/mailbox.
3157Opens account database file directly. Ensure mox does not have the account
3158open, or is not running.
3161 if len(args) != 1 && len(args) != 2 {
3166 a, err := store.OpenAccount(c.log, args[0])
3167 xcheckf(err, "open account")
3169 if err := a.Close(); err != nil {
3170 log.Printf("closing account: %v", err)
3174 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3175 uidvalidity, err := a.NextUIDValidity(tx)
3177 return fmt.Errorf("assigning next uid validity: %v", err)
3180 q := bstore.QueryTx[store.Mailbox](tx)
3182 q.FilterEqual("Name", args[1])
3184 mbl, err := q.SortAsc("Name").List()
3186 return fmt.Errorf("looking up mailbox: %v", err)
3188 if len(args) == 2 && len(mbl) != 1 {
3189 return fmt.Errorf("looking up mailbox %q, found %d mailboxes", args[1], len(mbl))
3191 for _, mb := range mbl {
3192 mb.UIDValidity = uidvalidity
3193 err = tx.Update(&mb)
3195 return fmt.Errorf("updating uid validity for mailbox: %v", err)
3197 fmt.Printf("uid validity for %q updated to %d\n", mb.Name, uidvalidity)
3201 xcheckf(err, "updating database")
3204func cmdReassignUIDs(c *cmd) {
3205 c.params = "account [mailboxid]"
3206 c.help = `Reassign UIDs in one mailbox or all mailboxes in an account and bump UID validity, causing IMAP clients to refetch messages.
3208Opens account database file directly. Ensure mox does not have the account
3209open, or is not running.
3212 if len(args) != 1 && len(args) != 2 {
3219 mailboxID, err = strconv.ParseInt(args[1], 10, 64)
3220 xcheckf(err, "parsing mailbox id")
3224 a, err := store.OpenAccount(c.log, args[0])
3225 xcheckf(err, "open account")
3227 if err := a.Close(); err != nil {
3228 log.Printf("closing account: %v", err)
3232 // Gather the last-assigned UIDs per mailbox.
3233 uidlasts := map[int64]store.UID{}
3235 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3236 // Reassign UIDs, going per mailbox. We assign starting at 1, only changing the
3237 // message if it isn't already at the intended UID. Doing it in this order ensures
3238 // we don't get into trouble with duplicate UIDs for a mailbox. We assign a new
3239 // modseq. Not strictly needed, for doesn't hurt.
3240 modseq, err := a.NextModSeq(tx)
3241 xcheckf(err, "assigning next modseq")
3243 q := bstore.QueryTx[store.Message](tx)
3245 q.FilterNonzero(store.Message{MailboxID: mailboxID})
3247 q.SortAsc("MailboxID", "UID")
3248 err = q.ForEach(func(m store.Message) error {
3249 uidlasts[m.MailboxID]++
3250 uid := uidlasts[m.MailboxID]
3254 if err := tx.Update(&m); err != nil {
3255 return fmt.Errorf("updating uid for message: %v", err)
3261 return fmt.Errorf("reading through messages: %v", err)
3264 // Now update the uidnext and uidvalidity for each mailbox.
3265 err = bstore.QueryTx[store.Mailbox](tx).ForEach(func(mb store.Mailbox) error {
3266 // Assign each mailbox a completely new uidvalidity.
3267 uidvalidity, err := a.NextUIDValidity(tx)
3269 return fmt.Errorf("assigning next uid validity: %v", err)
3272 if mb.UIDValidity >= uidvalidity {
3273 // This should not happen, but since we're fixing things up after a hypothetical
3274 // mishap, might as well account for inconsistent uidvalidity.
3275 next := store.NextUIDValidity{ID: 1, Next: mb.UIDValidity + 2}
3276 if err := tx.Update(&next); err != nil {
3277 log.Printf("updating nextuidvalidity: %v, continuing", err)
3281 mb.UIDValidity = uidvalidity
3283 mb.UIDNext = uidlasts[mb.ID] + 1
3284 if err := tx.Update(&mb); err != nil {
3285 return fmt.Errorf("updating uidvalidity and uidnext for mailbox: %v", err)
3290 return fmt.Errorf("updating mailboxes: %v", err)
3294 xcheckf(err, "updating database")
3297func cmdFixUIDMeta(c *cmd) {
3298 c.params = "account"
3299 c.help = `Fix inconsistent UIDVALIDITY and UIDNEXT in messages/mailboxes/account.
3301The next UID to use for a message in a mailbox should always be higher than any
3302existing message UID in the mailbox. If it is not, the mailbox UIDNEXT is
3305Each mailbox has a UIDVALIDITY sequence number, which should always be lower
3306than the per-account next UIDVALIDITY to use. If it is not, the account next
3307UIDVALIDITY is updated.
3309Opens account database file directly. Ensure mox does not have the account
3310open, or is not running.
3318 a, err := store.OpenAccount(c.log, args[0])
3319 xcheckf(err, "open account")
3321 if err := a.Close(); err != nil {
3322 log.Printf("closing account: %v", err)
3326 var maxUIDValidity uint32
3328 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3329 // We look at each mailbox, retrieve its max UID and compare against the mailbox
3331 err := bstore.QueryTx[store.Mailbox](tx).ForEach(func(mb store.Mailbox) error {
3332 if mb.UIDValidity > maxUIDValidity {
3333 maxUIDValidity = mb.UIDValidity
3335 m, err := bstore.QueryTx[store.Message](tx).FilterNonzero(store.Message{MailboxID: mb.ID}).SortDesc("UID").Limit(1).Get()
3336 if err == bstore.ErrAbsent || err == nil && m.UID < mb.UIDNext {
3338 } else if err != nil {
3339 return fmt.Errorf("finding message with max uid in mailbox: %w", err)
3341 olduidnext := mb.UIDNext
3342 mb.UIDNext = m.UID + 1
3343 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)
3344 if err := tx.Update(&mb); err != nil {
3345 return fmt.Errorf("updating mailbox uidnext: %v", err)
3350 return fmt.Errorf("processing mailboxes: %v", err)
3353 uidvalidity := store.NextUIDValidity{ID: 1}
3354 if err := tx.Get(&uidvalidity); err != nil {
3355 return fmt.Errorf("reading account next uidvalidity: %v", err)
3357 if maxUIDValidity >= uidvalidity.Next {
3358 log.Printf("account next uidvalidity %d <= highest uidvalidity %d found in mailbox, resetting account next uidvalidity to %d", uidvalidity.Next, maxUIDValidity, maxUIDValidity+1)
3359 uidvalidity.Next = maxUIDValidity + 1
3360 if err := tx.Update(&uidvalidity); err != nil {
3361 return fmt.Errorf("updating account next uidvalidity: %v", err)
3367 xcheckf(err, "updating database")
3370func cmdFixmsgsize(c *cmd) {
3371 c.params = "[account]"
3372 c.help = `Ensure message sizes in the database matching the sum of the message prefix length and on-disk file size.
3374Messages with an inconsistent size are also parsed again.
3376If an inconsistency is found, you should probably also run "mox
3377bumpuidvalidity" on the mailboxes or entire account to force IMAP clients to
3390 ctlcmdFixmsgsize(xctl(), account)
3393func ctlcmdFixmsgsize(ctl *ctl, account string) {
3394 ctl.xwrite("fixmsgsize")
3397 ctl.xstreamto(os.Stdout)
3400func cmdReparse(c *cmd) {
3401 c.params = "[account]"
3402 c.help = `Parse all messages in the account or all accounts again.
3404Can be useful after upgrading mox with improved message parsing. Messages are
3405parsed in batches, so other access to the mailboxes/messages are not blocked
3406while reparsing all messages.
3418 ctlcmdReparse(xctl(), account)
3421func ctlcmdReparse(ctl *ctl, account string) {
3422 ctl.xwrite("reparse")
3425 ctl.xstreamto(os.Stdout)
3428func cmdEnsureParsed(c *cmd) {
3429 c.params = "account"
3430 c.help = "Ensure messages in the database have a pre-parsed MIME form in the database."
3432 c.flag.BoolVar(&all, "all", false, "store new parsed message for all messages")
3439 a, err := store.OpenAccount(c.log, args[0])
3440 xcheckf(err, "open account")
3442 if err := a.Close(); err != nil {
3443 log.Printf("closing account: %v", err)
3448 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3449 q := bstore.QueryTx[store.Message](tx)
3450 q.FilterEqual("Expunged", false)
3451 q.FilterFn(func(m store.Message) bool {
3452 return all || m.ParsedBuf == nil
3456 return fmt.Errorf("list messages: %v", err)
3458 for _, m := range l {
3459 mr := a.MessageReader(m)
3460 p, err := message.EnsurePart(c.log.Logger, false, mr, m.Size)
3462 log.Printf("parsing message %d: %v (continuing)", m.ID, err)
3464 m.ParsedBuf, err = json.Marshal(p)
3466 return fmt.Errorf("marshal parsed message: %v", err)
3468 if err := tx.Update(&m); err != nil {
3469 return fmt.Errorf("update message: %v", err)
3475 xcheckf(err, "update messages with parsed mime structure")
3476 fmt.Printf("%d messages updated\n", n)
3479func cmdRecalculateMailboxCounts(c *cmd) {
3480 c.params = "account"
3481 c.help = `Recalculate message counts for all mailboxes in the account, and total message size for quota.
3483When a message is added to/removed from a mailbox, or when message flags change,
3484the total, unread, unseen and deleted messages are accounted, the total size of
3485the mailbox, and the total message size for the account. In case of a bug in
3486this accounting, the numbers could become incorrect. This command will find, fix
3495 ctlcmdRecalculateMailboxCounts(xctl(), args[0])
3498func ctlcmdRecalculateMailboxCounts(ctl *ctl, account string) {
3499 ctl.xwrite("recalculatemailboxcounts")
3502 ctl.xstreamto(os.Stdout)
3505func cmdMessageParse(c *cmd) {
3506 c.params = "message.eml"
3507 c.help = "Parse message, print JSON representation."
3510 c.flag.BoolVar(&smtputf8, "smtputf8", false, "check if message needs smtputf8")
3516 f, err := os.Open(args[0])
3517 xcheckf(err, "open")
3520 part, err := message.Parse(c.log.Logger, false, f)
3521 xcheckf(err, "parsing message")
3522 err = part.Walk(c.log.Logger, nil)
3523 xcheckf(err, "parsing nested parts")
3524 enc := json.NewEncoder(os.Stdout)
3525 enc.SetIndent("", "\t")
3526 enc.SetEscapeHTML(false)
3527 err = enc.Encode(part)
3528 xcheckf(err, "write")
3531 needs, err := part.NeedsSMTPUTF8()
3532 xcheckf(err, "checking if message needs smtputf8")
3533 fmt.Println("message needs smtputf8:", needs)
3537func cmdOpenaccounts(c *cmd) {
3539 c.params = "datadir account ..."
3540 c.help = `Open and close accounts, for triggering data upgrades, for tests.
3542Opens database files directly, not going through a running mox instance.
3550 dataDir := filepath.Clean(args[0])
3551 for _, accName := range args[1:] {
3552 accDir := filepath.Join(dataDir, "accounts", accName)
3553 log.Printf("opening account %s...", accDir)
3554 a, err := store.OpenAccountDB(c.log, accDir, accName)
3555 xcheckf(err, "open account %s", accName)
3556 err = a.ThreadingWait(c.log)
3557 xcheckf(err, "wait for threading upgrade to complete for %s", accName)
3559 xcheckf(err, "close account %s", accName)
3563func cmdReassignthreads(c *cmd) {
3564 c.params = "[account]"
3565 c.help = `Reassign message threads.
3567For all accounts, or optionally only the specified account.
3569Threading for all messages in an account is first reset, and new base subject
3570and normalized message-id saved with the message. Then all messages are
3571evaluated and matched against their parents/ancestors.
3573Messages are matched based on the References header, with a fall-back to an
3574In-Reply-To header, and if neither is present/valid, based only on base
3577A References header typically points to multiple previous messages in a
3578hierarchy. From oldest ancestor to most recent parent. An In-Reply-To header
3579would have only a message-id of the parent message.
3581A message is only linked to a parent/ancestor if their base subject is the
3582same. This ensures unrelated replies, with a new subject, are placed in their
3585The base subject is lower cased, has whitespace collapsed to a single
3586space, and some components removed: leading "Re:", "Fwd:", "Fw:", or bracketed
3587tag (that mailing lists often add, e.g. "[listname]"), trailing "(fwd)", or
3588enclosing "[fwd: ...]".
3590Messages are linked to all their ancestors. If an intermediate parent/ancestor
3591message is deleted in the future, the message can still be linked to the earlier
3592ancestors. If the direct parent already wasn't available while matching, this is
3593stored as the message having a "missing link" to its stored ancestors.
3605 ctlcmdReassignthreads(xctl(), account)
3608func ctlcmdReassignthreads(ctl *ctl, account string) {
3609 ctl.xwrite("reassignthreads")
3612 ctl.xstreamto(os.Stdout)
3615func cmdReadmessages(c *cmd) {
3617 c.params = "datadir account ..."
3618 c.help = `Open account, parse several headers for all messages.
3620For performance testing.
3622Opens database files directly, not going through a running mox instance.
3625 gomaxprocs := runtime.GOMAXPROCS(0)
3626 var procs, workqueuesize, limit int
3627 c.flag.IntVar(&procs, "procs", gomaxprocs, "number of goroutines for reading messages")
3628 c.flag.IntVar(&workqueuesize, "workqueuesize", 2*gomaxprocs, "number of messages to keep in work queue")
3629 c.flag.IntVar(&limit, "limit", 0, "number of messages to process if greater than zero")
3635 type threadPrep struct {
3640 threadingFields := [][]byte{
3641 []byte("references"),
3642 []byte("in-reply-to"),
3645 dataDir := filepath.Clean(args[0])
3646 for _, accName := range args[1:] {
3647 accDir := filepath.Join(dataDir, "accounts", accName)
3648 log.Printf("opening account %s...", accDir)
3649 a, err := store.OpenAccountDB(c.log, accDir, accName)
3650 xcheckf(err, "open account %s", accName)
3652 prepareMessages := func(in, out chan moxio.Work[store.Message, threadPrep]) {
3653 headerbuf := make([]byte, 8*1024)
3654 scratch := make([]byte, 4*1024)
3662 var partialPart struct {
3666 if err := json.Unmarshal(m.ParsedBuf, &partialPart); err != nil {
3667 w.Err = fmt.Errorf("unmarshal part: %v", err)
3669 size := partialPart.BodyOffset - partialPart.HeaderOffset
3670 if int(size) > len(headerbuf) {
3671 headerbuf = make([]byte, size)
3674 buf := headerbuf[:int(size)]
3675 err := func() error {
3676 mr := a.MessageReader(m)
3679 // ReadAt returns whole buffer or error. Single read should be fast.
3680 n, err := mr.ReadAt(buf, partialPart.HeaderOffset)
3681 if err != nil || n != len(buf) {
3682 return fmt.Errorf("read header: %v", err)
3688 } else if h, err := message.ParseHeaderFields(buf, scratch, threadingFields); err != nil {
3691 w.Out.references = h["References"]
3692 w.Out.inReplyTo = h["In-Reply-To"]
3705 processMessage := func(m store.Message, prep threadPrep) error {
3707 log.Printf("%d messages (delta %s)", n, time.Since(t))
3714 wq := moxio.NewWorkQueue[store.Message, threadPrep](procs, workqueuesize, prepareMessages, processMessage)
3716 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3717 q := bstore.QueryTx[store.Message](tx)
3718 q.FilterEqual("Expunged", false)
3723 err = q.ForEach(wq.Add)
3731 xcheckf(err, "processing message")
3734 xcheckf(err, "close account %s", accName)
3735 log.Printf("account %s, total time %s", accName, time.Since(t0))
3739func cmdQueueFillRetired(c *cmd) {
3741 c.help = `Fill retired messag and webhooks queue with testdata.
3743For testing the pagination. Operates directly on queue database.
3746 c.flag.IntVar(&n, "n", 10000, "retired messages and retired webhooks to insert")
3754 xcheckf(err, "init queue")
3755 err = queue.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3758 // Cause autoincrement ID for queue.Msg to be forwarded, and use the reserved ID
3759 // space for inserting retired messages.
3761 err = tx.Insert(&fm)
3762 xcheckf(err, "temporarily insert message to get autoincrement sequence")
3763 err = tx.Delete(&fm)
3764 xcheckf(err, "removing temporary message for resetting autoincrement sequence")
3766 err = tx.Insert(&fm)
3767 xcheckf(err, "temporarily insert message to forward autoincrement sequence")
3768 err = tx.Delete(&fm)
3769 xcheckf(err, "removing temporary message after forwarding autoincrement sequence")
3772 // And likewise for webhooks.
3773 fh := queue.Hook{Account: "x", URL: "x", NextAttempt: time.Now()}
3774 err = tx.Insert(&fh)
3775 xcheckf(err, "temporarily insert webhook to get autoincrement sequence")
3776 err = tx.Delete(&fh)
3777 xcheckf(err, "removing temporary webhook for resetting autoincrement sequence")
3779 err = tx.Insert(&fh)
3780 xcheckf(err, "temporarily insert webhook to forward autoincrement sequence")
3781 err = tx.Delete(&fh)
3782 xcheckf(err, "removing temporary webhook after forwarding autoincrement sequence")
3785 for i := 0; i < n; i++ {
3786 t0 := now.Add(-time.Duration(i) * time.Second)
3787 last := now.Add(-time.Duration(i/10) * time.Second)
3788 mr := queue.MsgRetired{
3789 ID: fm.ID + int64(i),
3791 SenderAccount: "test",
3792 SenderLocalpart: "mox",
3793 SenderDomainStr: "localhost",
3794 FromID: fmt.Sprintf("%016d", i),
3795 RecipientLocalpart: "mox",
3796 RecipientDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "localhost"}},
3797 RecipientDomainStr: "localhost",
3800 Results: []queue.MsgResult{
3803 Duration: time.Millisecond,
3810 Size: int64(i * 100),
3811 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
3812 Subject: fmt.Sprintf("test message %d", i),
3813 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
3815 RecipientAddress: "mox@localhost",
3817 KeepUntil: now.Add(48 * time.Hour),
3819 err := tx.Insert(&mr)
3820 xcheckf(err, "inserting retired message")
3823 for i := 0; i < n; i++ {
3824 t0 := now.Add(-time.Duration(i) * time.Second)
3825 last := now.Add(-time.Duration(i/10) * time.Second)
3830 hr := queue.HookRetired{
3831 ID: fh.ID + int64(i),
3832 QueueMsgID: fm.ID + int64(i),
3833 FromID: fmt.Sprintf("%016d", i),
3834 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
3835 Subject: fmt.Sprintf("test message %d", i),
3836 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
3838 URL: "http://localhost/hook",
3839 IsIncoming: i%10 == 0,
3840 OutgoingEvent: event,
3845 Results: []queue.HookResult{
3848 Duration: time.Millisecond,
3849 URL: "http://localhost/hook",
3858 KeepUntil: now.Add(48 * time.Hour),
3860 err := tx.Insert(&hr)
3861 xcheckf(err, "inserting retired hook")
3866 xcheckf(err, "add to queue")
3867 log.Printf("added %d retired messages and %d retired webhooks", n, n)