11 cryptorand "crypto/rand"
40 "golang.org/x/crypto/bcrypt"
41 "golang.org/x/text/secure/precis"
43 "github.com/mjl-/adns"
45 "github.com/mjl-/autocert"
46 "github.com/mjl-/bstore"
47 "github.com/mjl-/sconf"
48 "github.com/mjl-/sherpa"
50 "github.com/mjl-/mox/admin"
51 "github.com/mjl-/mox/config"
52 "github.com/mjl-/mox/dane"
53 "github.com/mjl-/mox/dkim"
54 "github.com/mjl-/mox/dmarc"
55 "github.com/mjl-/mox/dmarcdb"
56 "github.com/mjl-/mox/dmarcrpt"
57 "github.com/mjl-/mox/dns"
58 "github.com/mjl-/mox/dnsbl"
59 "github.com/mjl-/mox/message"
60 "github.com/mjl-/mox/mlog"
61 "github.com/mjl-/mox/mox-"
62 "github.com/mjl-/mox/moxio"
63 "github.com/mjl-/mox/moxvar"
64 "github.com/mjl-/mox/mtasts"
65 "github.com/mjl-/mox/publicsuffix"
66 "github.com/mjl-/mox/queue"
67 "github.com/mjl-/mox/rdap"
68 "github.com/mjl-/mox/smtp"
69 "github.com/mjl-/mox/smtpclient"
70 "github.com/mjl-/mox/spf"
71 "github.com/mjl-/mox/store"
72 "github.com/mjl-/mox/tlsrpt"
73 "github.com/mjl-/mox/tlsrptdb"
74 "github.com/mjl-/mox/updates"
75 "github.com/mjl-/mox/webadmin"
76 "github.com/mjl-/mox/webapi"
80 changelogDomain = "xmox.nl"
81 changelogURL = "https://updates.xmox.nl/changelog"
82 changelogPubKey = base64Decode("sPNiTDQzvb4FrytNEiebJhgyQzn57RwEjNbGWMM/bDY=")
85func base64Decode(s string) []byte {
86 buf, err := base64.StdEncoding.DecodeString(s)
93func envString(k, def string) string {
101var commands = []struct {
106 {"quickstart", cmdQuickstart},
108 {"setaccountpassword", cmdSetaccountpassword},
109 {"setadminpassword", cmdSetadminpassword},
110 {"loglevels", cmdLoglevels},
111 {"queue holdrules list", cmdQueueHoldrulesList},
112 {"queue holdrules add", cmdQueueHoldrulesAdd},
113 {"queue holdrules remove", cmdQueueHoldrulesRemove},
114 {"queue list", cmdQueueList},
115 {"queue hold", cmdQueueHold},
116 {"queue unhold", cmdQueueUnhold},
117 {"queue schedule", cmdQueueSchedule},
118 {"queue transport", cmdQueueTransport},
119 {"queue requiretls", cmdQueueRequireTLS},
120 {"queue fail", cmdQueueFail},
121 {"queue drop", cmdQueueDrop},
122 {"queue dump", cmdQueueDump},
123 {"queue retired list", cmdQueueRetiredList},
124 {"queue retired print", cmdQueueRetiredPrint},
125 {"queue suppress list", cmdQueueSuppressList},
126 {"queue suppress add", cmdQueueSuppressAdd},
127 {"queue suppress remove", cmdQueueSuppressRemove},
128 {"queue suppress lookup", cmdQueueSuppressLookup},
129 {"queue webhook list", cmdQueueHookList},
130 {"queue webhook schedule", cmdQueueHookSchedule},
131 {"queue webhook cancel", cmdQueueHookCancel},
132 {"queue webhook print", cmdQueueHookPrint},
133 {"queue webhook retired list", cmdQueueHookRetiredList},
134 {"queue webhook retired print", cmdQueueHookRetiredPrint},
135 {"import maildir", cmdImportMaildir},
136 {"import mbox", cmdImportMbox},
137 {"export maildir", cmdExportMaildir},
138 {"export mbox", cmdExportMbox},
139 {"localserve", cmdLocalserve},
141 {"backup", cmdBackup},
142 {"verifydata", cmdVerifydata},
143 {"licenses", cmdLicenses},
145 {"config test", cmdConfigTest},
146 {"config dnscheck", cmdConfigDNSCheck},
147 {"config dnsrecords", cmdConfigDNSRecords},
148 {"config describe-domains", cmdConfigDescribeDomains},
149 {"config describe-static", cmdConfigDescribeStatic},
150 {"config account list", cmdConfigAccountList},
151 {"config account add", cmdConfigAccountAdd},
152 {"config account rm", cmdConfigAccountRemove},
153 {"config account disable", cmdConfigAccountDisable},
154 {"config account enable", cmdConfigAccountEnable},
155 {"config address add", cmdConfigAddressAdd},
156 {"config address rm", cmdConfigAddressRemove},
157 {"config domain add", cmdConfigDomainAdd},
158 {"config domain rm", cmdConfigDomainRemove},
159 {"config domain disable", cmdConfigDomainDisable},
160 {"config domain enable", cmdConfigDomainEnable},
161 {"config tlspubkey list", cmdConfigTlspubkeyList},
162 {"config tlspubkey get", cmdConfigTlspubkeyGet},
163 {"config tlspubkey add", cmdConfigTlspubkeyAdd},
164 {"config tlspubkey rm", cmdConfigTlspubkeyRemove},
165 {"config tlspubkey gen", cmdConfigTlspubkeyGen},
166 {"config alias list", cmdConfigAliasList},
167 {"config alias print", cmdConfigAliasPrint},
168 {"config alias add", cmdConfigAliasAdd},
169 {"config alias update", cmdConfigAliasUpdate},
170 {"config alias rm", cmdConfigAliasRemove},
171 {"config alias addaddr", cmdConfigAliasAddaddr},
172 {"config alias rmaddr", cmdConfigAliasRemoveaddr},
174 {"config describe-sendmail", cmdConfigDescribeSendmail},
175 {"config printservice", cmdConfigPrintservice},
176 {"config ensureacmehostprivatekeys", cmdConfigEnsureACMEHostprivatekeys},
177 {"config example", cmdConfigExample},
179 {"admin imapserve", cmdIMAPServe},
181 {"checkupdate", cmdCheckupdate},
183 {"clientconfig", cmdClientConfig},
184 {"deliver", cmdDeliver},
185 // todo: turn cmdDANEDialmx into a regular "dialmx" command that follows mta-sts policy, with options to require dane, mta-sts or requiretls. the code will be similar to queue/direct.go
186 {"dane dial", cmdDANEDial},
187 {"dane dialmx", cmdDANEDialmx},
188 {"dane makerecord", cmdDANEMakeRecord},
189 {"dns lookup", cmdDNSLookup},
190 {"dkim gened25519", cmdDKIMGened25519},
191 {"dkim genrsa", cmdDKIMGenrsa},
192 {"dkim lookup", cmdDKIMLookup},
193 {"dkim txt", cmdDKIMTXT},
194 {"dkim verify", cmdDKIMVerify},
195 {"dkim sign", cmdDKIMSign},
196 {"dmarc lookup", cmdDMARCLookup},
197 {"dmarc parsereportmsg", cmdDMARCParsereportmsg},
198 {"dmarc verify", cmdDMARCVerify},
199 {"dmarc checkreportaddrs", cmdDMARCCheckreportaddrs},
200 {"dnsbl check", cmdDNSBLCheck},
201 {"dnsbl checkhealth", cmdDNSBLCheckhealth},
202 {"mtasts lookup", cmdMTASTSLookup},
203 {"rdap domainage", cmdRDAPDomainage},
204 {"retrain", cmdRetrain},
205 {"sendmail", cmdSendmail},
206 {"smtp dial", cmdSMTPDial},
207 {"spf check", cmdSPFCheck},
208 {"spf lookup", cmdSPFLookup},
209 {"spf parse", cmdSPFParse},
210 {"tlsrpt lookup", cmdTLSRPTLookup},
211 {"tlsrpt parsereportmsg", cmdTLSRPTParsereportmsg},
212 {"version", cmdVersion},
213 {"webapi", cmdWebapi},
215 {"example", cmdExample},
216 {"bumpuidvalidity", cmdBumpUIDValidity},
217 {"reassignuids", cmdReassignUIDs},
218 {"fixuidmeta", cmdFixUIDMeta},
219 {"fixmsgsize", cmdFixmsgsize},
220 {"reparse", cmdReparse},
221 {"ensureparsed", cmdEnsureParsed},
222 {"recalculatemailboxcounts", cmdRecalculateMailboxCounts},
223 {"message parse", cmdMessageParse},
224 {"reassignthreads", cmdReassignthreads},
227 {"helpall", cmdHelpall},
228 {"junk analyze", cmdJunkAnalyze},
229 {"junk check", cmdJunkCheck},
230 {"junk play", cmdJunkPlay},
231 {"junk test", cmdJunkTest},
232 {"junk train", cmdJunkTrain},
233 {"dmarcdb addreport", cmdDMARCDBAddReport},
234 {"tlsrptdb addreport", cmdTLSRPTDBAddReport},
235 {"updates addsigned", cmdUpdatesAddSigned},
236 {"updates genkey", cmdUpdatesGenkey},
237 {"updates pubkey", cmdUpdatesPubkey},
238 {"updates serve", cmdUpdatesServe},
239 {"updates verify", cmdUpdatesVerify},
240 {"gentestdata", cmdGentestdata},
241 {"ximport maildir", cmdXImportMaildir},
242 {"ximport mbox", cmdXImportMbox},
243 {"openaccounts", cmdOpenaccounts},
244 {"readmessages", cmdReadmessages},
245 {"queuefillretired", cmdQueueFillRetired},
251 for _, xc := range commands {
252 c := cmd{words: strings.Split(xc.cmd, " "), fn: xc.fn}
253 cmds = append(cmds, c)
261 // Set before calling command.
264 _gather bool // Set when using Parse to gather usage for a command.
266 // Set by invoked command or Parse.
267 unlisted bool // If set, command is not listed until at least some words are matched from command.
268 params string // Arguments to command. Multiple lines possible.
269 help string // Additional explanation. First line is synopsis, the rest is only printed for an explicit help/usage for that command.
275func (c *cmd) Parse() []string {
276 // To gather params and usage information, we just run the command but cause this
277 // panic after the command has registered its flags and set its params and help
278 // information. This is then caught and that info printed.
283 c.flag.Usage = c.Usage
284 c.flag.Parse(c.flagArgs)
285 c.args = c.flag.Args()
289func (c *cmd) gather() {
290 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
294 // panic generated by Parse.
302func (c *cmd) makeUsage() string {
303 var r strings.Builder
304 cs := "mox " + strings.Join(c.words, " ")
305 for i, line := range strings.Split(strings.TrimSpace(c.params), "\n") {
313 fmt.Fprintf(&r, "%6s %s%s\n", s, cs, line)
316 c.flag.PrintDefaults()
320func (c *cmd) printUsage() {
321 fmt.Fprint(os.Stderr, c.makeUsage())
323 fmt.Fprint(os.Stderr, "\n"+c.help+"\n")
327func (c *cmd) Usage() {
332func cmdHelp(c *cmd) {
333 c.params = "[command ...]"
334 c.help = `Prints help about matching commands.
336If multiple commands match, they are listed along with the first line of their help text.
337If a single command matches, its usage and full help text is printed.
344 prefix := func(l, pre []string) bool {
345 if len(pre) > len(l) {
348 return slices.Equal(pre, l[:len(pre)])
352 for _, c := range cmds {
353 if slices.Equal(c.words, args) {
355 fmt.Print(c.makeUsage())
357 fmt.Print("\n" + c.help + "\n")
360 } else if prefix(c.words, args) {
361 partial = append(partial, c)
364 if len(partial) == 0 {
365 fmt.Fprintf(os.Stderr, "%s: unknown command\n", strings.Join(args, " "))
368 for _, c := range partial {
370 line := "mox " + strings.Join(c.words, " ")
371 fmt.Printf("%s\n", line)
373 fmt.Printf("\t%s\n", strings.Split(c.help, "\n")[0])
378func cmdHelpall(c *cmd) {
380 c.help = `Print all detailed usage and help information for all listed commands.
382Used to generate documentation.
390 for _, c := range cmds {
396 fmt.Fprintf(os.Stderr, "\n")
400 fmt.Fprintf(os.Stderr, "# mox %s\n\n", strings.Join(c.words, " "))
402 fmt.Fprintln(os.Stderr, c.help+"\n")
405 s = "\t" + strings.ReplaceAll(s, "\n", "\n\t")
406 fmt.Fprintln(os.Stderr, s)
410func usage(l []cmd, unlisted bool) {
413 lines = append(lines, "mox [-config config/mox.conf] [-pedantic] ...")
415 for _, c := range l {
417 if c.unlisted && !unlisted {
420 for line := range strings.SplitSeq(c.params, "\n") {
421 x := append([]string{"mox"}, c.words...)
425 lines = append(lines, strings.Join(x, " "))
428 for i, line := range lines {
433 fmt.Fprintln(os.Stderr, pre+line)
438var loglevel string // Empty will be interpreted as info, except by localserve.
441// subcommands that are not "serve" should use this function to load the config, it
442// restores any loglevel specified on the command-line, instead of using the
443// loglevels from the config file and it does not load files like TLS keys/certs.
444func mustLoadConfig() {
445 mox.MustLoadConfig(false, false)
450 if level, ok := mlog.Levels[ll]; ok {
451 mox.Conf.Log[""] = level
452 mlog.SetConfig(mox.Conf.Log)
454 log.Fatal("unknown loglevel", slog.String("loglevel", loglevel))
457 mox.SetPedantic(true)
462 // CheckConsistencyOnClose is true by default, for all the test packages. A regular
463 // mox server should never use it. But integration tests enable it again with a
465 store.CheckConsistencyOnClose = false
466 store.MsgFilesPerDirShiftSet(13) // For 1<<13 = 8k message files per directory.
468 ctxbg := context.Background()
474 // If invoked as sendmail, e.g. /usr/sbin/sendmail, we do enough so cron can get a
475 // message sent using smtp submission to a configured server.
476 if len(os.Args) > 0 && filepath.Base(os.Args[0]) == "sendmail" {
478 flag: flag.NewFlagSet("sendmail", flag.ExitOnError),
479 flagArgs: os.Args[1:],
480 log: mlog.New("sendmail", nil),
486 flag.StringVar(&mox.ConfigStaticPath, "config", envString("MOXCONF", filepath.FromSlash("config/mox.conf")), "configuration file, other config files are looked up in the same directory, defaults to $MOXCONF with a fallback to mox.conf")
487 flag.StringVar(&loglevel, "loglevel", "", "if non-empty, this log level is set early in startup")
488 flag.BoolVar(&pedantic, "pedantic", false, "protocol violations result in errors instead of accepting/working around them")
489 flag.BoolVar(&store.CheckConsistencyOnClose, "checkconsistency", false, "dangerous option for testing only, enables data checks that abort/panic when inconsistencies are found")
491 var cpuprofile, memprofile, tracefile string
492 flag.StringVar(&cpuprofile, "cpuprof", "", "store cpu profile to file")
493 flag.StringVar(&memprofile, "memprof", "", "store mem profile to file")
494 flag.StringVar(&tracefile, "trace", "", "store execution trace to file")
496 flag.Usage = func() { usage(cmds, false) }
504 defer traceExecution(tracefile)()
506 defer profile(cpuprofile, memprofile)()
509 mox.SetPedantic(true)
512 mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
517 if level, ok := mlog.Levels[ll]; ok {
518 mox.Conf.Log[""] = level
519 mlog.SetConfig(mox.Conf.Log)
520 // note: SetConfig may be called again when subcommands loads config.
522 log.Fatalf("unknown loglevel %q", loglevel)
527 for _, c := range cmds {
528 for i, w := range c.words {
529 if i >= len(args) || w != args[i] {
531 partial = append(partial, c)
536 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
537 c.flagArgs = args[len(c.words):]
538 c.log = mlog.New(strings.Join(c.words, ""), nil)
542 if len(partial) > 0 {
548func xcheckf(err error, format string, args ...any) {
552 msg := fmt.Sprintf(format, args...)
553 log.Fatalf("%s: %s", msg, err)
556func xparseIP(s, what string) net.IP {
559 log.Fatalf("invalid %s: %q", what, s)
564func xparseDomain(s, what string) dns.Domain {
565 d, err := dns.ParseDomain(s)
566 xcheckf(err, "parsing %s %q", what, s)
570func cmdClientConfig(c *cmd) {
572 c.help = `Print the configuration for email clients for a domain.
574Sending email is typically not done on the SMTP port 25, but on submission
575ports 465 (with TLS) and 587 (without initial TLS, but usually added to the
576connection with STARTTLS). For IMAP, the port with TLS is 993 and without is
579Without TLS/STARTTLS, passwords are sent in clear text, which should only be
580configured over otherwise secured connections, like a VPN.
586 d := xparseDomain(args[0], "domain")
591func printClientConfig(d dns.Domain) {
592 cc, err := admin.ClientConfigsDomain(d)
593 xcheckf(err, "getting client config")
594 fmt.Printf("%-20s %-30s %5s %-15s %s\n", "Protocol", "Host", "Port", "Listener", "Note")
595 for _, e := range cc.Entries {
596 fmt.Printf("%-20s %-30s %5d %-15s %s\n", e.Protocol, e.Host, e.Port, e.Listener, e.Note)
599To prevent authentication mechanism downgrade attempts that may result in
600clients sending plain text passwords to a MitM, clients should always be
601explicitly configured with the most secure authentication mechanism supported,
602the first of: SCRAM-SHA-256-PLUS, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, SCRAM-SHA-1,
607func cmdConfigTest(c *cmd) {
608 c.help = `Parses and validates the configuration files.
610If valid, the command exits with status 0. If not valid, all errors encountered
618 mox.FilesImmediate = true
620 _, errs := mox.ParseConfig(context.Background(), c.log, mox.ConfigStaticPath, true, true, false)
622 log.Printf("multiple errors:")
623 for _, err := range errs {
624 log.Printf("%s", err)
627 } else if len(errs) == 1 {
628 log.Fatalf("%s", errs[0])
631 fmt.Println("config OK")
634func cmdConfigDescribeStatic(c *cmd) {
635 c.params = ">mox.conf"
636 c.help = `Prints an annotated empty configuration for use as mox.conf.
638The static configuration file cannot be reloaded while mox is running. Mox has
639to be restarted for changes to the static configuration file to take effect.
641This configuration file needs modifications to make it valid. For example, it
642may contain unfinished list items.
644 if len(c.Parse()) != 0 {
649 err := sconf.Describe(os.Stdout, &sc)
650 xcheckf(err, "describing config")
653func cmdConfigDescribeDomains(c *cmd) {
654 c.params = ">domains.conf"
655 c.help = `Prints an annotated empty configuration for use as domains.conf.
657The domains configuration file contains the domains and their configuration,
658and accounts and their configuration. This includes the configured email
659addresses. The mox admin web interface, and the mox command line interface, can
660make changes to this file. Mox automatically reloads this file when it changes.
662Like the static configuration, the example domains.conf printed by this command
663needs modifications to make it valid.
665 if len(c.Parse()) != 0 {
669 var dc config.Dynamic
670 err := sconf.Describe(os.Stdout, &dc)
671 xcheckf(err, "describing config")
674func cmdConfigPrintservice(c *cmd) {
675 c.params = ">mox.service"
676 c.help = `Prints a systemd unit service file for mox.
678This is the same file as generated using quickstart. If the systemd service file
679has changed with a newer version of mox, use this command to generate an up to
682 if len(c.Parse()) != 0 {
686 pwd, err := os.Getwd()
688 log.Printf("current working directory: %v", err)
691 service := strings.ReplaceAll(moxService, "/home/mox", pwd)
695func cmdConfigDomainAdd(c *cmd) {
696 c.params = "[-disabled] domain account [localpart]"
697 c.help = `Adds a new domain to the configuration and reloads the configuration.
699The account is used for the postmaster mailboxes the domain, including as DMARC and
700TLS reporting. Localpart is the "username" at the domain for this account. If
701must be set if and only if account does not yet exist.
703The domain can be created in disabled mode, preventing automatically requesting
704TLS certificates with ACME, and rejecting incoming/outgoing messages involving
705the domain, but allowing further configuration of the domain.
708 c.flag.BoolVar(&disabled, "disabled", false, "disable the new domain")
710 if len(args) != 2 && len(args) != 3 {
714 d := xparseDomain(args[0], "domain")
716 var localpart smtp.Localpart
719 localpart, err = smtp.ParseLocalpart(args[2])
720 xcheckf(err, "parsing localpart")
722 ctlcmdConfigDomainAdd(xctl(), disabled, d, args[1], localpart)
725func ctlcmdConfigDomainAdd(ctl *ctl, disabled bool, domain dns.Domain, account string, localpart smtp.Localpart) {
726 ctl.xwrite("domainadd")
732 ctl.xwrite(domain.Name())
734 ctl.xwrite(string(localpart))
736 fmt.Printf("domain added, remember to add dns records, see:\n\nmox config dnsrecords %s\nmox config dnscheck %s\n", domain.Name(), domain.Name())
739func cmdConfigDomainRemove(c *cmd) {
741 c.help = `Remove a domain from the configuration and reload the configuration.
743This is a dangerous operation. Incoming email delivery for this domain will be
751 d := xparseDomain(args[0], "domain")
753 ctlcmdConfigDomainRemove(xctl(), d)
756func ctlcmdConfigDomainRemove(ctl *ctl, d dns.Domain) {
757 ctl.xwrite("domainrm")
760 fmt.Printf("domain removed, remember to remove dns records for %s\n", d)
763func cmdConfigDomainDisable(c *cmd) {
765 c.help = `Disable a domain and reload the configuration.
767This is a dangerous operation. Incoming/outgoing messages involving this domain
775 d := xparseDomain(args[0], "domain")
777 ctlcmdConfigDomainDisabled(xctl(), d, true)
778 fmt.Printf("domain disabled")
781func cmdConfigDomainEnable(c *cmd) {
783 c.help = `Enable a domain and reload the configuration.
785Incoming/outgoing messages involving this domain will be accepted again.
792 d := xparseDomain(args[0], "domain")
794 ctlcmdConfigDomainDisabled(xctl(), d, false)
797func ctlcmdConfigDomainDisabled(ctl *ctl, d dns.Domain, disabled bool) {
798 ctl.xwrite("domaindisabled")
808func cmdConfigAliasList(c *cmd) {
810 c.help = `Show aliases (lists) for domain.`
817 ctlcmdConfigAliasList(xctl(), args[0])
820func ctlcmdConfigAliasList(ctl *ctl, address string) {
821 ctl.xwrite("aliaslist")
824 ctl.xstreamto(os.Stdout)
827func cmdConfigAliasPrint(c *cmd) {
829 c.help = `Print settings and members of alias (list).`
836 ctlcmdConfigAliasPrint(xctl(), args[0])
839func ctlcmdConfigAliasPrint(ctl *ctl, address string) {
840 ctl.xwrite("aliasprint")
843 ctl.xstreamto(os.Stdout)
846func cmdConfigAliasAdd(c *cmd) {
847 c.params = "alias@domain rcpt1@domain ..."
848 c.help = `Add new alias (list) with one or more addresses and public posting enabled.
850An alias is used for delivering incoming email to multiple recipients. If you
851want to add an address to an account, don't use an alias, just add the address
859 alias := config.Alias{PostPublic: true, Addresses: args[1:]}
862 ctlcmdConfigAliasAdd(xctl(), args[0], alias)
865func ctlcmdConfigAliasAdd(ctl *ctl, address string, alias config.Alias) {
866 ctl.xwrite("aliasadd")
868 xctlwriteJSON(ctl, alias)
872func cmdConfigAliasUpdate(c *cmd) {
873 c.params = "alias@domain [-postpublic false|true -listmembers false|true -allowmsgfrom false|true]"
874 c.help = `Update alias (list) configuration.`
875 var postpublic, listmembers, allowmsgfrom string
876 c.flag.StringVar(&postpublic, "postpublic", "", "whether anyone or only list members can post")
877 c.flag.StringVar(&listmembers, "listmembers", "", "whether list members can list members")
878 c.flag.StringVar(&allowmsgfrom, "allowmsgfrom", "", "whether alias address can be used in message from header")
886 ctlcmdConfigAliasUpdate(xctl(), alias, postpublic, listmembers, allowmsgfrom)
889func ctlcmdConfigAliasUpdate(ctl *ctl, alias, postpublic, listmembers, allowmsgfrom string) {
890 ctl.xwrite("aliasupdate")
892 ctl.xwrite(postpublic)
893 ctl.xwrite(listmembers)
894 ctl.xwrite(allowmsgfrom)
898func cmdConfigAliasRemove(c *cmd) {
899 c.params = "alias@domain"
900 c.help = "Remove alias (list)."
907 ctlcmdConfigAliasRemove(xctl(), args[0])
910func ctlcmdConfigAliasRemove(ctl *ctl, alias string) {
911 ctl.xwrite("aliasrm")
916func cmdConfigAliasAddaddr(c *cmd) {
917 c.params = "alias@domain rcpt1@domain ..."
918 c.help = `Add addresses to alias (list).`
925 ctlcmdConfigAliasAddaddr(xctl(), args[0], args[1:])
928func ctlcmdConfigAliasAddaddr(ctl *ctl, alias string, addresses []string) {
929 ctl.xwrite("aliasaddaddr")
931 xctlwriteJSON(ctl, addresses)
935func cmdConfigAliasRemoveaddr(c *cmd) {
936 c.params = "alias@domain rcpt1@domain ..."
937 c.help = `Remove addresses from alias (list).`
944 ctlcmdConfigAliasRmaddr(xctl(), args[0], args[1:])
947func ctlcmdConfigAliasRmaddr(ctl *ctl, alias string, addresses []string) {
948 ctl.xwrite("aliasrmaddr")
950 xctlwriteJSON(ctl, addresses)
954func cmdConfigAccountAdd(c *cmd) {
955 c.params = "account address"
956 c.help = `Add an account with an email address and reload the configuration.
958Email can be delivered to this address/account. A password has to be configured
959explicitly, see the setaccountpassword command.
967 ctlcmdConfigAccountAdd(xctl(), args[0], args[1])
970func ctlcmdConfigAccountAdd(ctl *ctl, account, address string) {
971 ctl.xwrite("accountadd")
975 fmt.Printf("account added, set a password with \"mox setaccountpassword %s\"\n", account)
978func cmdConfigAccountRemove(c *cmd) {
980 c.help = `Remove an account and reload the configuration.
982Email addresses for this account will also be removed, and incoming email for
983these addresses will be rejected.
985All data for the account will be removed.
993 ctlcmdConfigAccountRemove(xctl(), args[0])
996func ctlcmdConfigAccountRemove(ctl *ctl, account string) {
997 ctl.xwrite("accountrm")
1000 fmt.Println("account removed")
1003func cmdConfigAccountList(c *cmd) {
1004 c.help = `List all accounts.
1006Each account is printed on a line, with optional additional tab-separated
1007information, such as "(disabled)".
1015 ctlcmdConfigAccountList(xctl())
1018func ctlcmdConfigAccountList(ctl *ctl) {
1019 ctl.xwrite("accountlist")
1021 ctl.xstreamto(os.Stdout)
1024func cmdConfigAccountDisable(c *cmd) {
1025 c.params = "account message"
1026 c.help = `Disable login for an account, showing message to users when they try to login.
1028Incoming email will still be accepted for the account, and queued email from the
1029account will still be delivered. No new login sessions are possible.
1031Message must be non-empty, ascii-only without control characters including
1032newline, and maximum 256 characters because it is used in SMTP/IMAP.
1039 log.Fatalf("message must be non-empty")
1043 ctlcmdConfigAccountDisabled(xctl(), args[0], args[1])
1044 fmt.Println("account disabled")
1047func cmdConfigAccountEnable(c *cmd) {
1048 c.params = "account"
1049 c.help = `Enable login again for an account.
1051Login attempts by the user no long result in an error message.
1059 ctlcmdConfigAccountDisabled(xctl(), args[0], "")
1060 fmt.Println("account enabled")
1063func ctlcmdConfigAccountDisabled(ctl *ctl, account, loginDisabled string) {
1064 ctl.xwrite("accountdisabled")
1066 ctl.xwrite(loginDisabled)
1070func cmdConfigTlspubkeyList(c *cmd) {
1071 c.params = "[account]"
1072 c.help = `List TLS public keys for TLS client certificate authentication.
1074If account is absent, the TLS public keys for all accounts are listed.
1077 var accountOpt string
1079 accountOpt = args[0]
1080 } else if len(args) > 1 {
1085 ctlcmdConfigTlspubkeyList(xctl(), accountOpt)
1088func ctlcmdConfigTlspubkeyList(ctl *ctl, accountOpt string) {
1089 ctl.xwrite("tlspubkeylist")
1090 ctl.xwrite(accountOpt)
1092 ctl.xstreamto(os.Stdout)
1095func cmdConfigTlspubkeyGet(c *cmd) {
1096 c.params = "fingerprint"
1097 c.help = `Get a TLS public key for a fingerprint.
1099Prints the type, name, account and address for the key, and the certificate in
1108 ctlcmdConfigTlspubkeyGet(xctl(), args[0])
1111func ctlcmdConfigTlspubkeyGet(ctl *ctl, fingerprint string) {
1112 ctl.xwrite("tlspubkeyget")
1113 ctl.xwrite(fingerprint)
1117 account := ctl.xread()
1118 address := ctl.xread()
1119 noimappreauth := ctl.xread()
1123 var block *pem.Block
1126 Type: "CERTIFICATE",
1131 fmt.Printf("type: %s\nname: %s\naccount: %s\naddress: %s\nno imap preauth: %s\n", typ, name, account, address, noimappreauth)
1133 fmt.Printf("certificate:\n\n")
1134 if err := pem.Encode(os.Stdout, block); err != nil {
1135 log.Fatalf("pem encode: %v", err)
1140func cmdConfigTlspubkeyAdd(c *cmd) {
1141 c.params = "address [name] < cert.pem"
1142 c.help = `Add a TLS public key to the account of the given address.
1144The public key is read from the certificate.
1146The optional name is a human-readable descriptive name of the key. If absent,
1147the CommonName from the certificate is used.
1149 var noimappreauth bool
1150 c.flag.BoolVar(&noimappreauth, "no-imap-preauth", false, "Don't automatically switch new IMAP connections authenticated with this key to \"authenticated\" state after the TLS handshake. For working around clients that ignore the untagged IMAP PREAUTH response and try to authenticate while already authenticated.")
1152 var address, name string
1155 } else if len(args) == 2 {
1156 address, name = args[0], args[1]
1161 buf, err := io.ReadAll(os.Stdin)
1162 xcheckf(err, "reading from stdin")
1163 block, _ := pem.Decode(buf)
1165 err = errors.New("no pem block found")
1166 } else if block.Type != "CERTIFICATE" {
1167 err = fmt.Errorf("unexpected type %q, expected CERTIFICATE", block.Type)
1169 xcheckf(err, "parsing pem")
1172 ctlcmdConfigTlspubkeyAdd(xctl(), address, name, noimappreauth, block.Bytes)
1175func ctlcmdConfigTlspubkeyAdd(ctl *ctl, address, name string, noimappreauth bool, certDER []byte) {
1176 ctl.xwrite("tlspubkeyadd")
1179 ctl.xwrite(fmt.Sprintf("%v", noimappreauth))
1180 ctl.xstreamfrom(bytes.NewReader(certDER))
1184func cmdConfigTlspubkeyRemove(c *cmd) {
1185 c.params = "fingerprint"
1186 c.help = `Remove TLS public key for fingerprint.`
1193 ctlcmdConfigTlspubkeyRemove(xctl(), args[0])
1196func ctlcmdConfigTlspubkeyRemove(ctl *ctl, fingerprint string) {
1197 ctl.xwrite("tlspubkeyrm")
1198 ctl.xwrite(fingerprint)
1202func cmdConfigTlspubkeyGen(c *cmd) {
1204 c.help = `Generate an ed25519 private key and minimal certificate for use a TLS public key and write to files starting with stem.
1206The private key is written to $stem.$timestamp.ed25519privatekey.pkcs8.pem.
1207The certificate is written to $stem.$timestamp.certificate.pem.
1208The private key and certificate are also written to
1209$stem.$timestamp.ed25519privatekey-certificate.pem.
1211The certificate can be added to an account with "mox config account tlspubkey add".
1213The combined file can be used with "mox sendmail".
1215The private key is also written to standard error in raw-url-base64-encoded
1216form, also for use with "mox sendmail". The fingerprint is written to standard
1217error too, for reference.
1225 timestamp := time.Now().Format("200601021504")
1226 prefix := stem + "." + timestamp
1228 seed := make([]byte, ed25519.SeedSize)
1229 cryptorand.Read(seed)
1230 privKey := ed25519.NewKeyFromSeed(seed)
1231 privKeyBuf, err := x509.MarshalPKCS8PrivateKey(privKey)
1232 xcheckf(err, "marshal private key as pkcs8")
1234 err = pem.Encode(&b, &pem.Block{Type: "PRIVATE KEY", Bytes: privKeyBuf})
1235 xcheckf(err, "marshal pkcs8 private key to pem")
1236 privKeyBufPEM := b.Bytes()
1238 certBuf, tlsCert := xminimalCert(privKey)
1240 err = pem.Encode(&b, &pem.Block{Type: "CERTIFICATE", Bytes: certBuf})
1241 xcheckf(err, "marshal certificate to pem")
1242 certBufPEM := b.Bytes()
1244 xwriteFile := func(p string, data []byte, what string) {
1245 log.Printf("writing %s", p)
1246 err = os.WriteFile(p, data, 0600)
1247 xcheckf(err, "writing %s file: %v", what, err)
1250 xwriteFile(prefix+".ed25519privatekey.pkcs8.pem", privKeyBufPEM, "private key")
1251 xwriteFile(prefix+".certificate.pem", certBufPEM, "certificate")
1252 combinedPEM := slices.Concat(privKeyBufPEM, certBufPEM)
1253 xwriteFile(prefix+".ed25519privatekey-certificate.pem", combinedPEM, "combined private key and certificate")
1255 shabuf := sha256.Sum256(tlsCert.Leaf.RawSubjectPublicKeyInfo)
1257 _, err = fmt.Fprintf(os.Stderr, "ed25519 private key as raw-url-base64: %s\ned25519 public key fingerprint: %s\n",
1258 base64.RawURLEncoding.EncodeToString(seed),
1259 base64.RawURLEncoding.EncodeToString(shabuf[:]),
1261 xcheckf(err, "write private key and public key fingerprint")
1264func cmdConfigAddressAdd(c *cmd) {
1265 c.params = "address account"
1266 c.help = `Adds an address to an account and reloads the configuration.
1268If address starts with a @ (i.e. a missing localpart), this is a catchall
1269address for the domain.
1277 ctlcmdConfigAddressAdd(xctl(), args[0], args[1])
1280func ctlcmdConfigAddressAdd(ctl *ctl, address, account string) {
1281 ctl.xwrite("addressadd")
1285 fmt.Println("address added")
1288func cmdConfigAddressRemove(c *cmd) {
1289 c.params = "address"
1290 c.help = `Remove an address and reload the configuration.
1292Incoming email for this address will be rejected after removing an address.
1300 ctlcmdConfigAddressRemove(xctl(), args[0])
1303func ctlcmdConfigAddressRemove(ctl *ctl, address string) {
1304 ctl.xwrite("addressrm")
1307 fmt.Println("address removed")
1310func cmdConfigDNSRecords(c *cmd) {
1312 c.help = `Prints annotated DNS records as zone file that should be created for the domain.
1314The zone file can be imported into existing DNS software. You should review the
1315DNS records, especially if your domain previously/currently has email
1323 d := xparseDomain(args[0], "domain")
1325 domConf, ok := mox.Conf.Domain(d)
1327 log.Fatalf("unknown domain")
1330 resolver := dns.StrictResolver{Pkg: "main"}
1331 _, result, err := resolver.LookupTXT(context.Background(), d.ASCII+".")
1332 if !dns.IsNotFound(err) {
1333 xcheckf(err, "looking up record for dnssec-status")
1336 var certIssuerDomainName, acmeAccountURI string
1337 public := mox.Conf.Static.Listeners["public"]
1338 if public.TLS != nil && public.TLS.ACME != "" {
1339 acme, ok := mox.Conf.Static.ACME[public.TLS.ACME]
1340 if ok && acme.Manager.Manager.Client != nil {
1341 certIssuerDomainName = acme.IssuerDomainName
1342 acc, err := acme.Manager.Manager.Client.GetReg(context.Background(), "")
1343 c.log.Check(err, "get public acme account")
1345 acmeAccountURI = acc.URI
1350 records, err := admin.DomainRecords(domConf, d, result.Authentic, certIssuerDomainName, acmeAccountURI)
1351 xcheckf(err, "records")
1352 fmt.Print(strings.Join(records, "\n") + "\n")
1355func cmdConfigDNSCheck(c *cmd) {
1357 c.help = "Check the DNS records with the configuration for the domain, and print any errors/warnings."
1363 d := xparseDomain(args[0], "domain")
1365 _, ok := mox.Conf.Domain(d)
1367 log.Fatalf("unknown domain")
1370 // todo future: move http.Admin.CheckDomain to mox- and make it return a regular error.
1376 err, ok := x.(*sherpa.Error)
1380 log.Fatalf("%s", err)
1383 printResult := func(name string, r webadmin.Result) {
1384 if len(r.Errors) == 0 && len(r.Warnings) == 0 {
1387 fmt.Printf("# %s\n", name)
1388 for _, s := range r.Errors {
1389 fmt.Printf("error: %s\n", s)
1391 for _, s := range r.Warnings {
1392 fmt.Printf("warning: %s\n", s)
1396 result := webadmin.Admin{}.CheckDomain(context.Background(), args[0])
1397 printResult("DNSSEC", result.DNSSEC.Result)
1398 printResult("IPRev", result.IPRev.Result)
1399 printResult("MX", result.MX.Result)
1400 printResult("TLS", result.TLS.Result)
1401 printResult("DANE", result.DANE.Result)
1402 printResult("SPF", result.SPF.Result)
1403 printResult("DKIM", result.DKIM.Result)
1404 printResult("DMARC", result.DMARC.Result)
1405 printResult("Host TLSRPT", result.HostTLSRPT.Result)
1406 printResult("Domain TLSRPT", result.DomainTLSRPT.Result)
1407 printResult("MTASTS", result.MTASTS.Result)
1408 printResult("SRV conf", result.SRVConf.Result)
1409 printResult("Autoconf", result.Autoconf.Result)
1410 printResult("Autodiscover", result.Autodiscover.Result)
1413func cmdConfigEnsureACMEHostprivatekeys(c *cmd) {
1415 c.help = `Ensure host private keys exist for TLS listeners with ACME.
1417In mox.conf, each listener can have TLS configured. Long-lived private key files
1418can be specified, which will be used when requesting ACME certificates.
1419Configuring these private keys makes it feasible to publish DANE TLSA records
1420for the corresponding public keys in DNS, protected with DNSSEC, allowing TLS
1421certificate verification without depending on a list of Certificate Authorities
1422(CAs). Previous versions of mox did not pre-generate private keys for use with
1423ACME certificates, but would generate private keys on-demand. By explicitly
1424configuring private keys, they will not change automatedly with new
1425certificates, and the DNS TLSA records stay valid.
1427This command looks for listeners in mox.conf with TLS with ACME configured. For
1428each missing host private key (of type rsa-2048 and ecdsa-p256) a key is written
1429to config/hostkeys/. If a certificate exists in the ACME "cache", its private
1430key is copied. Otherwise a new private key is generated. Snippets for manually
1431updating/editing mox.conf are printed.
1433After running this command, and updating mox.conf, run "mox config dnsrecords"
1434for a domain and create the TLSA DNS records it suggests to enable DANE.
1441 // Load a private key from p, in various forms. We only look at the first PEM
1442 // block. Files with only a private key, or with multiple blocks but private key
1443 // first like autocert does, can be loaded.
1444 loadPrivateKey := func(f *os.File) (any, error) {
1445 buf, err := io.ReadAll(f)
1447 return nil, fmt.Errorf("reading private key file: %v", err)
1449 block, _ := pem.Decode(buf)
1451 return nil, fmt.Errorf("no pem block found in pem file")
1455 case "EC PRIVATE KEY":
1456 privKey, err = x509.ParseECPrivateKey(block.Bytes)
1457 case "RSA PRIVATE KEY":
1458 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
1460 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
1462 return nil, fmt.Errorf("unrecognized pem block type %q", block.Type)
1465 return nil, fmt.Errorf("parsing private key of type %q: %v", block.Type, err)
1470 // Either load a private key from file, or if it doesn't exist generate a new
1472 xtryLoadPrivateKey := func(kt autocert.KeyType, p string) any {
1473 f, err := os.Open(p)
1474 if err != nil && errors.Is(err, fs.ErrNotExist) {
1476 case autocert.KeyRSA2048:
1477 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1478 xcheckf(err, "generating new 2048-bit rsa private key")
1480 case autocert.KeyECDSAP256:
1481 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1482 xcheckf(err, "generating new ecdsa p-256 private key")
1485 log.Fatalf("unexpected keytype %v", kt)
1488 xcheckf(err, "%s: open acme key and certificate file", p)
1490 // Load private key from file. autocert stores a PEM file that starts with a
1491 // private key, followed by certificate(s). So we can just read it and should find
1492 // the private key we are looking for.
1493 privKey, err := loadPrivateKey(f)
1494 if xerr := f.Close(); xerr != nil {
1495 log.Printf("closing private key file: %v", xerr)
1497 xcheckf(err, "parsing private key from acme key and certificate file")
1499 switch k := privKey.(type) {
1500 case *rsa.PrivateKey:
1501 if k.N.BitLen() == 2048 {
1504 log.Printf("warning: rsa private key in %s has %d bits, skipping and generating new 2048-bit rsa private key", p, k.N.BitLen())
1505 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1506 xcheckf(err, "generating new 2048-bit rsa private key")
1508 case *ecdsa.PrivateKey:
1509 if k.Curve == elliptic.P256() {
1512 log.Printf("warning: ecdsa private key in %s has curve %v, skipping and generating new p-256 ecdsa key", p, k.Curve.Params().Name)
1513 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1514 xcheckf(err, "generating new ecdsa p-256 private key")
1517 log.Fatalf("%s: unexpected private key file of type %T", p, privKey)
1522 // Write privKey as PKCS#8 private key to p. Only if file does not yet exist.
1523 writeHostPrivateKey := func(privKey any, p string) error {
1524 os.MkdirAll(filepath.Dir(p), 0700)
1525 f, err := os.OpenFile(p, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
1527 return fmt.Errorf("create: %v", err)
1531 if err := f.Close(); err != nil {
1532 log.Printf("closing new hostkey file %s after error: %v", p, err)
1534 if err := os.Remove(p); err != nil {
1535 log.Printf("removing new hostkey file %s after error: %v", p, err)
1539 buf, err := x509.MarshalPKCS8PrivateKey(privKey)
1541 return fmt.Errorf("marshal private host key: %v", err)
1544 Type: "PRIVATE KEY",
1547 if err := pem.Encode(f, &block); err != nil {
1548 return fmt.Errorf("write as pem: %v", err)
1550 if err := f.Close(); err != nil {
1551 return fmt.Errorf("close: %v", err)
1558 timestamp := time.Now().Format("20060102T150405")
1560 for listenerName, l := range mox.Conf.Static.Listeners {
1561 if l.TLS == nil || l.TLS.ACME == "" {
1564 haveKeyTypes := map[autocert.KeyType]bool{}
1565 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
1566 p := mox.ConfigDirPath(privKeyFile)
1567 f, err := os.Open(p)
1568 xcheckf(err, "open host private key")
1569 privKey, err := loadPrivateKey(f)
1570 if err := f.Close(); err != nil {
1571 log.Printf("closing host private key file: %v", err)
1573 xcheckf(err, "loading host private key")
1574 switch k := privKey.(type) {
1575 case *rsa.PrivateKey:
1576 if k.N.BitLen() == 2048 {
1577 haveKeyTypes[autocert.KeyRSA2048] = true
1579 case *ecdsa.PrivateKey:
1580 if k.Curve == elliptic.P256() {
1581 haveKeyTypes[autocert.KeyECDSAP256] = true
1585 created := []string{}
1586 for _, kt := range []autocert.KeyType{autocert.KeyRSA2048, autocert.KeyECDSAP256} {
1587 if haveKeyTypes[kt] {
1590 // Lookup key in ACME cache.
1591 host := l.HostnameDomain
1592 if host.ASCII == "" {
1593 host = mox.Conf.Static.HostnameDomain
1595 filename := host.ASCII
1597 if kt == autocert.KeyRSA2048 {
1601 p := mox.DataDirPath(filepath.Join("acme", "keycerts", l.TLS.ACME, filename))
1602 privKey := xtryLoadPrivateKey(kt, p)
1604 relPath := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", host.Name(), timestamp, kind))
1605 destPath := mox.ConfigDirPath(relPath)
1606 err := writeHostPrivateKey(privKey, destPath)
1607 xcheckf(err, "writing host private key file to %s: %v", destPath, err)
1608 created = append(created, relPath)
1609 fmt.Printf("Wrote host private key: %s\n", destPath)
1611 didCreate = didCreate || len(created) > 0
1612 if len(created) > 0 {
1614 HostPrivateKeyFiles: append(l.TLS.HostPrivateKeyFiles, created...),
1616 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)
1617 err := sconf.Write(os.Stdout, tls)
1618 xcheckf(err, "writing new TLS.HostPrivateKeyFiles section")
1624After updating mox.conf and restarting, run "mox config dnsrecords" for a
1625domain and create the TLSA DNS records it suggests to enable DANE.
1630func cmdLoglevels(c *cmd) {
1631 c.params = "[level [pkg]]"
1632 c.help = `Print the log levels, or set a new default log level, or a level for the given package.
1634By default, a single log level applies to all logging in mox. But for each
1635"pkg", an overriding log level can be configured. Examples of packages:
1636smtpserver, smtpclient, queue, imapserver, spf, dkim, dmarc, junk, message,
1639Specify a pkg and an empty level to clear the configured level for a package.
1641Valid labels: error, info, debug, trace, traceauth, tracedata.
1650 ctlcmdLoglevels(xctl())
1656 ctlcmdSetLoglevels(xctl(), pkg, args[0])
1660func ctlcmdLoglevels(ctl *ctl) {
1661 ctl.xwrite("loglevels")
1663 ctl.xstreamto(os.Stdout)
1666func ctlcmdSetLoglevels(ctl *ctl, pkg, level string) {
1667 ctl.xwrite("setloglevels")
1673func cmdStop(c *cmd) {
1674 c.help = `Shut mox down, giving connections maximum 3 seconds to stop before closing them.
1676While shutting down, new IMAP and SMTP connections will get a status response
1677indicating temporary unavailability. Existing connections will get a 3 second
1678period to finish their transaction and shut down. Under normal circumstances,
1679only IMAP has long-living connections, with the IDLE command to get notified of
1682 if len(c.Parse()) != 0 {
1689 // Read will hang until remote has shut down.
1690 buf := make([]byte, 128)
1691 n, err := xctl.conn.Read(buf)
1693 log.Fatalf("expected eof after graceful shutdown, got data %q", buf[:n])
1694 } else if err != io.EOF {
1695 log.Fatalf("expected eof after graceful shutdown, got error %v", err)
1697 fmt.Println("mox stopped")
1700func cmdBackup(c *cmd) {
1701 c.params = "destdir"
1702 c.help = `Creates a backup of the config and data directory.
1704Backup copies the config directory to <destdir>/config, and creates
1705<destdir>/data with a consistent snapshot of the databases and message files
1706and copies other files from the data directory. Empty directories are not
1707copied. The backup can then be stored elsewhere for long-term storage, or used
1708to fall back to should an upgrade fail. Simply copying files in the data
1709directory while mox is running can result in unusable database files.
1711Message files never change (they are read-only, though can be removed) and are
1712hard-linked so they don't consume additional space. If hardlinking fails, for
1713example when the backup destination directory is on a different file system, a
1714regular copy is made. Using a destination directory like "data/tmp/backup"
1715increases the odds hardlinking succeeds: the default systemd service file
1716specifically mounts the data directory, causing attempts to hardlink outside it
1717to fail with an error about cross-device linking.
1719All files in the data directory that aren't recognized (i.e. other than known
1720database files, message files, an acme directory, the "tmp" directory, etc),
1721are stored, but with a warning.
1723Remove files in the destination directory before doing another backup. The
1724backup command will not overwrite files, but print and return errors.
1726Exit code 0 indicates the backup was successful. A clean successful backup does
1727not print any output, but may print warnings. Use the -verbose flag for
1728details, including timing.
1730To restore a backup, first shut down mox, move away the old data directory and
1731move an earlier backed up directory in its place, run "mox verifydata
1732<datadir>", possibly with the "-fix" option, and restart mox. After the
1733restore, you may also want to run "mox bumpuidvalidity" for each account for
1734which messages in a mailbox changed, to force IMAP clients to synchronize
1737Before upgrading, to check if the upgrade will likely succeed, first make a
1738backup, then use the new mox binary to run "mox verifydata <backupdir>/data".
1739This can change the backup files (e.g. upgrade database files, move away
1740unrecognized message files), so you should make a new backup before actually
1745 c.flag.BoolVar(&verbose, "verbose", false, "print progress")
1752 dstDataDir, err := filepath.Abs(args[0])
1753 xcheckf(err, "making path absolute")
1755 ctlcmdBackup(xctl(), dstDataDir, verbose)
1758func ctlcmdBackup(ctl *ctl, dstDataDir string, verbose bool) {
1759 ctl.xwrite("backup")
1760 ctl.xwrite(dstDataDir)
1762 ctl.xwrite("verbose")
1766 ctl.xstreamto(os.Stdout)
1770func cmdSetadminpassword(c *cmd) {
1771 c.help = `Set a new admin password, for the web interface.
1773The password is read from stdin. Its bcrypt hash is stored in a file named
1774"adminpasswd" in the configuration directory.
1776 if len(c.Parse()) != 0 {
1781 path := mox.ConfigDirPath(mox.Conf.Static.AdminPasswordFile)
1783 log.Fatal("no admin password file configured")
1786 pw := xreadpassword()
1787 pw, err := precis.OpaqueString.String(pw)
1788 xcheckf(err, `checking password with "precis" requirements`)
1789 hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
1790 xcheckf(err, "generating hash for password")
1791 err = os.WriteFile(path, hash, 0660)
1792 xcheckf(err, "writing hash to admin password file")
1795func xreadpassword() string {
1797Type new password. Password WILL echo.
1799WARNING: Bots will try to bruteforce your password. Connections with failed
1800authentication attempts will be rate limited but attackers WILL find passwords
1801reused at other services and weak passwords. If your account is compromised,
1802spammers are likely to abuse your system, spamming your address and the wider
1803internet in your name. So please pick a random, unguessable password, preferably
1804at least 12 characters.
1807 fmt.Printf("password: ")
1808 scanner := bufio.NewScanner(os.Stdin)
1809 // The default splitter for scanners is one that splits by lines, so we
1810 // don't have to set up another one here.
1812 // We discard the return value of Scan() since failing to tokenize could
1813 // either mean reaching EOF but no newline (which can be legitimate if the
1814 // CLI was programatically called to set the password, but with no trailing
1815 // newline), or an actual error. We can distinguish between the two by
1816 // calling Err() since it will return nil if it were EOF, but the actual
1819 xcheckf(scanner.Err(), "reading stdin")
1820 // No need to trim, the scanner does not return the token in the output.
1821 pw := scanner.Text()
1823 log.Fatal("password must be at least 8 characters")
1828func cmdSetaccountpassword(c *cmd) {
1829 c.params = "account"
1830 c.help = `Set new password an account.
1832The password is read from stdin. Secrets derived from the password, but not the
1833password itself, are stored in the account database. The stored secrets are for
1834authentication with: scram-sha-256, scram-sha-1, cram-md5, plain text (bcrypt
1837The parameter is an account name, as configured under Accounts in domains.conf
1838and as present in the data/accounts/ directory, not a configured email address
1847 pw := xreadpassword()
1849 ctlcmdSetaccountpassword(xctl(), args[0], pw)
1852func ctlcmdSetaccountpassword(ctl *ctl, account, password string) {
1853 ctl.xwrite("setaccountpassword")
1855 ctl.xwrite(password)
1859func cmdDeliver(c *cmd) {
1861 c.params = "address < message"
1862 c.help = "Deliver message to address."
1868 ctlcmdDeliver(xctl(), args[0])
1871func ctlcmdDeliver(ctl *ctl, address string) {
1872 ctl.xwrite("deliver")
1875 ctl.xstreamfrom(os.Stdin)
1878 fmt.Println("message delivered")
1880 log.Fatalf("deliver: %s", line)
1884func cmdDKIMGenrsa(c *cmd) {
1885 c.params = ">$selector._domainkey.$domain.rsa2048.privatekey.pkcs8.pem"
1886 c.help = `Generate a new 2048 bit RSA private key for use with DKIM.
1888The generated file is in PEM format, and has a comment it is generated for use
1891 if len(c.Parse()) != 0 {
1895 buf, err := admin.MakeDKIMRSAKey(dns.Domain{}, dns.Domain{})
1896 xcheckf(err, "making rsa private key")
1897 _, err = os.Stdout.Write(buf)
1898 xcheckf(err, "writing rsa private key")
1901// todo: options for specifying the domain this is the mx host of, and enabling dane and/or mta-sts verification
1902func cmdSMTPDial(c *cmd) {
1903 c.params = "host[:port]"
1905 var tlsCerts, tlsCiphersuites, tlsCurves, tlsVersionMin, tlsVersionMax, tlsRenegotiation string
1906 var tlsVerify, noTLS, forceTLS, tlsNoSessionTickets, tlsNoDynamicRecordSizing bool
1907 var ehloHostnameStr, remoteHostnameStr string
1909 ciphersuites := map[string]*tls.CipherSuite{}
1910 ciphersuitesInsecure := map[string]*tls.CipherSuite{}
1911 for _, v := range tls.CipherSuites() {
1912 if slices.Contains(v.SupportedVersions, tls.VersionTLS10) || slices.Contains(v.SupportedVersions, tls.VersionTLS11) || slices.Contains(v.SupportedVersions, tls.VersionTLS12) {
1913 ciphersuites[strings.ToLower(v.Name)] = v
1916 for _, v := range tls.InsecureCipherSuites() {
1917 if slices.Contains(v.SupportedVersions, tls.VersionTLS10) || slices.Contains(v.SupportedVersions, tls.VersionTLS11) || slices.Contains(v.SupportedVersions, tls.VersionTLS12) {
1918 ciphersuitesInsecure[strings.ToLower(v.Name)] = v
1922 curves := map[string]tls.CurveID{}
1923 for _, a := range curvesList {
1924 curves[strings.ToLower(a.String())] = a
1927 c.flag.StringVar(&tlsCiphersuites, "tlsciphersuites", "", "ciphersuites to allow, comma-separated, order is ignored, only for TLS 1.2 and earlier, empty value uses TLS stack defaults; values: "+strings.Join(slices.Sorted(maps.Keys(ciphersuites)), ", ")+", and insecure: "+strings.Join(slices.Sorted(maps.Keys(ciphersuitesInsecure)), ", "))
1928 c.flag.StringVar(&tlsCurves, "tlscurves", "", "tls ecc key exchange mechanisms to allow, comma-separated, order is ignored, empty value uses TLS stack defaults; values: curvep256, curvep384, curvep521, x25519, x25519mlkem768")
1929 c.flag.StringVar(&tlsCerts, "tlscerts", "", "path to root ca certificates in pem form, for verification")
1930 c.flag.StringVar(&tlsVersionMin, "tlsversionmin", "", "minimum TLS version, empty value uses TLS stack default; values: tls1.2, etc.")
1931 c.flag.StringVar(&tlsVersionMax, "tlsversionmax", "", "maximum TLS version, empty value uses TLS stack default; values: tls1.2, etc.")
1932 c.flag.BoolVar(&tlsVerify, "tlsverify", false, "verify remote hostname during TLS")
1933 c.flag.BoolVar(&tlsNoSessionTickets, "tlsnosessiontickets", false, "disable TLS session tickets")
1934 c.flag.BoolVar(&tlsNoDynamicRecordSizing, "tlsnodynamicrecordsizing", false, "disable TLS dynamic record sizing")
1935 c.flag.BoolVar(&noTLS, "notls", false, "do not use TLS")
1936 c.flag.BoolVar(&forceTLS, "forcetls", false, "use TLS, even if remote SMTP server does not announce STARTTLS extension")
1937 c.flag.StringVar(&tlsRenegotiation, "tlsrenegotiation", "never", "when to allow renegotiation; only applies to tls1.2 and earlier, not tls1.3; values: never, once, always")
1938 c.flag.StringVar(&ehloHostnameStr, "ehlohostname", "", "our hostname to use during the SMTP EHLO command")
1939 c.flag.StringVar(&remoteHostnameStr, "remotehostname", "", "remote hostname to use for TLS verification, if enabled; the hostname from the parameter is used by default")
1941 c.help = `Dial the address, initialize the SMTP session, including using STARTTLS to enable TLS if the server supports it.
1943If no port is specified, SMTP port 25 is used.
1945Data is copied between connection and stdin/stdout until either side closes the
1948The flags influence the TLS configuration, useful for debugging interoperability
1951No MTA-STS or DANE verification is done.
1953Hint: Use "mox -loglevel trace smtp dial ..." to see the protocol messages
1954exchanged during connection set up.
1961 if noTLS && forceTLS {
1962 log.Fatalf("cannot have both -notls and -forcetls")
1965 parseTLSVersion := func(s string) uint16 {
1968 return tls.VersionTLS10
1970 return tls.VersionTLS11
1972 return tls.VersionTLS12
1974 return tls.VersionTLS13
1978 log.Fatalf("invalid tls version %q", s)
1979 panic("not reached")
1982 tlsConfig := tls.Config{
1983 MinVersion: parseTLSVersion(tlsVersionMin),
1984 MaxVersion: parseTLSVersion(tlsVersionMax),
1985 InsecureSkipVerify: !tlsVerify,
1986 SessionTicketsDisabled: tlsNoSessionTickets,
1987 DynamicRecordSizingDisabled: tlsNoDynamicRecordSizing,
1990 switch tlsRenegotiation {
1992 tlsConfig.Renegotiation = tls.RenegotiateNever
1994 tlsConfig.Renegotiation = tls.RenegotiateOnceAsClient
1996 tlsConfig.Renegotiation = tls.RenegotiateFreelyAsClient
1998 log.Fatalf("invalid value %q for -tlsrenegotation", tlsRenegotiation)
2001 pool := x509.NewCertPool()
2002 pembuf, err := os.ReadFile(tlsCerts)
2003 xcheckf(err, "reading tls certificates")
2004 ok := pool.AppendCertsFromPEM(pembuf)
2006 c.log.Warn("no tls certificates found", slog.String("path", tlsCerts))
2008 tlsConfig.RootCAs = pool
2010 if tlsCiphersuites != "" {
2011 for s := range strings.SplitSeq(tlsCiphersuites, ",") {
2012 s = strings.TrimSpace(s)
2013 c, ok := ciphersuites[s]
2015 c, ok = ciphersuitesInsecure[s]
2018 log.Fatalf("unknown ciphersuite %q", s)
2020 tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, c.ID)
2023 if tlsCurves != "" {
2024 for s := range strings.SplitSeq(tlsCurves, ",") {
2025 s = strings.TrimSpace(s)
2026 if c, ok := curves[s]; !ok {
2027 log.Fatalf("unknown ecc key exchange algorithm %q", s)
2029 tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, c)
2034 var host, portStr string
2036 host, portStr, err = net.SplitHostPort(args[0])
2041 port, err := strconv.ParseInt(portStr, 10, 64)
2042 xcheckf(err, "parsing port %q", portStr)
2044 if remoteHostnameStr == "" {
2045 remoteHostnameStr = host
2047 remoteHostname, err := dns.ParseDomain(remoteHostnameStr)
2048 xcheckf(err, "parsing remote host")
2049 tlsConfig.ServerName = remoteHostname.Name()
2051 resolver := dns.StrictResolver{Pkg: "smtpdial"}
2052 _, _, _, ips, _, err := smtpclient.GatherIPs(context.Background(), c.log.Logger, resolver, "ip", dns.IPDomain{Domain: remoteHostname}, nil)
2053 xcheckf(err, "resolve host")
2054 c.log.Info("resolved remote address", slog.Any("ips", ips))
2056 dialer := &net.Dialer{Timeout: 5 * time.Second}
2057 dialedIPs := map[string][]net.IP{}
2058 conn, ip, err := smtpclient.Dial(context.Background(), c.log.Logger, dialer, dns.IPDomain{Domain: remoteHostname}, ips, int(port), dialedIPs, nil)
2059 xcheckf(err, "dial")
2060 c.log.Info("connected to remote host", slog.Any("ip", ip))
2062 tlsMode := smtpclient.TLSOpportunistic
2064 tlsMode = smtpclient.TLSRequiredStartTLS
2066 tlsMode = smtpclient.TLSSkip
2068 var ehloHostname dns.Domain
2069 if ehloHostnameStr == "" {
2070 name, err := os.Hostname()
2071 xcheckf(err, "get hostname")
2072 ehloHostnameStr = name
2074 ehloHostname, err = dns.ParseDomain(ehloHostnameStr)
2075 xcheckf(err, "parse hostname")
2077 opts := smtpclient.Opts{
2078 TLSConfig: &tlsConfig,
2080 client, err := smtpclient.New(context.Background(), c.log.Logger, conn, tlsMode, false, ehloHostname, dns.Domain{}, opts)
2081 xcheckf(err, "new smtp client")
2083 cs := client.TLSConnectionState()
2085 c.log.Info("smtp initialized without tls")
2087 c.log.Info("smtp initialized with tls",
2088 slog.String("version", tls.VersionName(cs.Version)),
2089 slog.String("ciphersuite", strings.ToLower(tls.CipherSuiteName(cs.CipherSuite))),
2090 slog.String("sni", cs.ServerName),
2092 for _, chain := range cs.VerifiedChains {
2094 for _, cert := range chain {
2095 s := fmt.Sprintf("dns names %q, common name %q, %s - %s, issuer %q)", strings.Join(cert.DNSNames, ","), cert.Subject.CommonName, cert.NotBefore.Format("2006-01-02T15:04:05"), cert.NotAfter.Format("2006-01-02T15:04:05"), cert.Issuer.CommonName)
2098 c.log.Info("tls certificate verification chain", slog.String("chain", strings.Join(l, "; ")))
2102 conn, err = client.Conn()
2103 xcheckf(err, "get smtp session connection")
2106 _, err := io.Copy(os.Stdout, conn)
2107 xcheckf(err, "copy from connection to stdout")
2109 c.log.Check(err, "closing connection")
2111 _, err = io.Copy(conn, os.Stdin)
2112 xcheckf(err, "copy from stdin to connection")
2115func cmdDANEDial(c *cmd) {
2116 c.params = "host:port"
2118 c.flag.StringVar(&usages, "usages", "pkix-ta,pkix-ee,dane-ta,dane-ee", "allowed usages for dane, comma-separated list")
2119 c.help = `Dial the address using TLS with certificate verification using DANE.
2121Data is copied between connection and stdin/stdout until either side closes the
2129 allowedUsages := []adns.TLSAUsage{}
2131 for s := range strings.SplitSeq(usages, ",") {
2132 var usage adns.TLSAUsage
2133 switch strings.ToLower(s) {
2134 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
2135 usage = adns.TLSAUsagePKIXTA
2136 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
2137 usage = adns.TLSAUsagePKIXEE
2138 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
2139 usage = adns.TLSAUsageDANETA
2140 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
2141 usage = adns.TLSAUsageDANEEE
2143 log.Fatalf("unknown dane usage %q", s)
2145 allowedUsages = append(allowedUsages, usage)
2149 pkixRoots, err := x509.SystemCertPool()
2150 xcheckf(err, "get system pkix certificate pool")
2152 resolver := dns.StrictResolver{Pkg: "danedial"}
2153 conn, record, err := dane.Dial(context.Background(), c.log.Logger, resolver, "tcp", args[0], allowedUsages, pkixRoots)
2154 xcheckf(err, "dial")
2155 log.Printf("(connected, verified with %s)", record)
2158 _, err := io.Copy(os.Stdout, conn)
2159 xcheckf(err, "copy from connection to stdout")
2161 c.log.Check(err, "closing connection")
2163 _, err = io.Copy(conn, os.Stdin)
2164 xcheckf(err, "copy from stdin to connection")
2167func cmdDANEDialmx(c *cmd) {
2168 c.params = "domain [destination-host]"
2169 var ehloHostname string
2170 c.flag.StringVar(&ehloHostname, "ehlohostname", "localhost", "hostname to send in smtp ehlo command")
2171 c.help = `Connect to MX server for domain using STARTTLS verified with DANE.
2173If no destination host is specified, regular delivery logic is used to find the
2174hosts to attempt delivery too. This involves following CNAMEs for the domain,
2175looking up MX records, and possibly falling back to the domain name itself as
2178If a destination host is specified, that is the only candidate host considered
2181With a list of destinations gathered, each is dialed until a successful SMTP
2182session verified with DANE has been initialized, including EHLO and STARTTLS
2185Once connected, data is copied between connection and stdin/stdout, until
2186either side closes the connection.
2188This command follows the same logic as delivery attempts made from the queue,
2189sharing most of its code.
2192 if len(args) != 1 && len(args) != 2 {
2196 ehloDomain := xparseDomain(ehloHostname, "ehlo host name")
2197 origNextHop := xparseDomain(args[0], "domain")
2199 ctxbg := context.Background()
2201 resolver := dns.StrictResolver{}
2203 var expandedNextHopAuthentic bool
2204 var expandedNextHop dns.Domain
2205 var hostPrefs []smtpclient.HostPref
2208 var origNextHopAuthentic bool
2210 haveMX, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hostPrefs, permanent, err = smtpclient.GatherDestinations(ctxbg, c.log.Logger, resolver, dns.IPDomain{Domain: origNextHop})
2211 status := "temporary"
2213 status = "permanent"
2216 log.Fatalf("gathering destinations: %v (%s)", err, status)
2218 if expandedNextHop != origNextHop {
2219 log.Printf("followed cnames to %s", expandedNextHop)
2222 log.Printf("found mx record, trying mx hosts")
2224 log.Printf("no mx record found, will try to connect to domain directly")
2226 if !origNextHopAuthentic {
2227 log.Fatalf("error: initial domain not dnssec-secure")
2229 if !expandedNextHopAuthentic {
2230 log.Fatalf("error: expanded domain not dnssec-secure")
2234 for _, hp := range hostPrefs {
2235 s := hp.Host.String()
2237 s += fmt.Sprintf(" (pref %d)", hp.Pref)
2241 log.Printf("destinations: %s", strings.Join(l, ", "))
2243 d := xparseDomain(args[1], "destination host")
2244 log.Printf("skipping domain mx/cname lookups, assuming domain is dnssec-protected")
2246 expandedNextHopAuthentic = true
2248 hostPrefs = []smtpclient.HostPref{{Host: dns.IPDomain{Domain: d}, Pref: -1}}
2251 dialedIPs := map[string][]net.IP{}
2252 for _, hp := range hostPrefs {
2255 log.Printf("attempting to connect to %s (pref %d)", host, hp.Pref)
2257 authentic, expandedAuthentic, expandedHost, ips, _, err := smtpclient.GatherIPs(ctxbg, c.log.Logger, resolver, "ip", host, dialedIPs)
2259 log.Printf("resolving ips for %s: %v, skipping", host, err)
2263 log.Printf("no dnssec for ips of %s, skipping", host)
2266 if !expandedAuthentic {
2267 log.Printf("no dnssec for cname-followed ips of %s, skipping", host)
2270 if expandedHost != host.Domain {
2271 log.Printf("host %s cname-expanded to %s", host, expandedHost)
2273 log.Printf("host %s resolved to ips %s, looking up tlsa records", host, ips)
2275 daneRequired, daneRecords, tlsaBaseDomain, err := smtpclient.GatherTLSA(ctxbg, c.log.Logger, resolver, host.Domain, expandedAuthentic, expandedHost)
2277 log.Printf("looking up tlsa records: %s, skipping", err)
2280 tlsMode := smtpclient.TLSRequiredStartTLS
2281 if len(daneRecords) == 0 {
2283 log.Printf("host %s has no tlsa records, skipping", expandedHost)
2286 log.Printf("warning: only unusable tlsa records found, continuing with required tls without certificate verification")
2290 for _, r := range daneRecords {
2291 l = append(l, r.String())
2293 log.Printf("tlsa records: %s", strings.Join(l, "; "))
2296 tlsHostnames := smtpclient.GatherTLSANames(haveMX, expandedNextHopAuthentic, expandedAuthentic, origNextHop, expandedNextHop, host.Domain, tlsaBaseDomain)
2298 for _, name := range tlsHostnames {
2299 l = append(l, name.String())
2301 log.Printf("gathered valid tls certificate names for potential verification with dane-ta: %s", strings.Join(l, ", "))
2303 dialer := &net.Dialer{Timeout: 5 * time.Second}
2304 conn, _, err := smtpclient.Dial(ctxbg, c.log.Logger, dialer, dns.IPDomain{Domain: expandedHost}, ips, 25, dialedIPs, nil)
2306 log.Printf("dial %s: %v, skipping", expandedHost, err)
2309 log.Printf("connected to %s, %s, starting smtp session with ehlo and starttls with dane verification", expandedHost, conn.RemoteAddr())
2311 var verifiedRecord adns.TLSA
2312 opts := smtpclient.Opts{
2313 DANERecords: daneRecords,
2314 DANEMoreHostnames: tlsHostnames[1:],
2315 DANEVerifiedRecord: &verifiedRecord,
2316 RootCAs: mox.Conf.Static.TLS.CertPool,
2319 sc, err := smtpclient.New(ctxbg, c.log.Logger, conn, tlsMode, tlsPKIX, ehloDomain, tlsHostnames[0], opts)
2321 log.Printf("setting up smtp session: %v, skipping", err)
2322 if xerr := conn.Close(); xerr != nil {
2323 log.Printf("closing connection: %v", xerr)
2328 smtpConn, err := sc.Conn()
2330 log.Fatalf("error: taking over smtp connection: %s", err)
2332 log.Printf("tls verified with tlsa record: %s", verifiedRecord)
2333 log.Printf("smtp session initialized and connected to stdin/stdout")
2336 _, err := io.Copy(os.Stdout, smtpConn)
2337 xcheckf(err, "copy from connection to stdout")
2338 if err := smtpConn.Close(); err != nil {
2339 log.Printf("closing smtp connection: %v", err)
2342 _, err = io.Copy(smtpConn, os.Stdin)
2343 xcheckf(err, "copy from stdin to connection")
2346 log.Fatalf("no remaining destinations")
2349func cmdDANEMakeRecord(c *cmd) {
2350 c.params = "usage selector matchtype [certificate.pem | publickey.pem | privatekey.pem]"
2351 c.help = `Print TLSA record for given certificate/key and parameters.
2354- usage: pkix-ta (0), pkix-ee (1), dane-ta (2), dane-ee (3)
2355- selector: cert (0), spki (1)
2356- matchtype: full (0), sha2-256 (1), sha2-512 (2)
2358Common DANE TLSA record parameters are: dane-ee spki sha2-256, or 3 1 1,
2359followed by a sha2-256 hash of the DER-encoded "SPKI" (subject public key info)
2360from the certificate. An example DNS zone file entry:
2362 _25._tcp.example.com. TLSA 3 1 1 133b919c9d65d8b1488157315327334ead8d83372db57465ecabf53ee5748aee
2364The first usable information from the pem file is used to compose the TLSA
2365record. In case of selector "cert", a certificate is required. Otherwise the
2366"subject public key info" (spki) of the first certificate or public or private
2367key (pkcs#8, pkcs#1 or ec private key) is used.
2375 var usage adns.TLSAUsage
2376 switch strings.ToLower(args[0]) {
2377 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
2378 usage = adns.TLSAUsagePKIXTA
2379 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
2380 usage = adns.TLSAUsagePKIXEE
2381 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
2382 usage = adns.TLSAUsageDANETA
2383 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
2384 usage = adns.TLSAUsageDANEEE
2386 if v, err := strconv.ParseUint(args[0], 10, 16); err != nil {
2387 log.Fatalf("bad usage %q", args[0])
2389 // Does not influence certificate association data, so we can accept other numbers.
2390 log.Printf("warning: continuing with unrecognized tlsa usage %d", v)
2391 usage = adns.TLSAUsage(v)
2395 var selector adns.TLSASelector
2396 switch strings.ToLower(args[1]) {
2397 case "cert", strconv.Itoa(int(adns.TLSASelectorCert)):
2398 selector = adns.TLSASelectorCert
2399 case "spki", strconv.Itoa(int(adns.TLSASelectorSPKI)):
2400 selector = adns.TLSASelectorSPKI
2402 log.Fatalf("bad selector %q", args[1])
2405 var matchType adns.TLSAMatchType
2406 switch strings.ToLower(args[2]) {
2407 case "full", strconv.Itoa(int(adns.TLSAMatchTypeFull)):
2408 matchType = adns.TLSAMatchTypeFull
2409 case "sha2-256", strconv.Itoa(int(adns.TLSAMatchTypeSHA256)):
2410 matchType = adns.TLSAMatchTypeSHA256
2411 case "sha2-512", strconv.Itoa(int(adns.TLSAMatchTypeSHA512)):
2412 matchType = adns.TLSAMatchTypeSHA512
2414 log.Fatalf("bad matchtype %q", args[2])
2417 buf, err := os.ReadFile(args[3])
2418 xcheckf(err, "reading certificate")
2420 var block *pem.Block
2421 block, buf = pem.Decode(buf)
2425 extra = " (with leftover data from pem file)"
2427 if selector == adns.TLSASelectorCert {
2428 log.Fatalf("no certificate found in pem file%s", extra)
2430 log.Fatalf("no certificate or public or private key found in pem file%s", extra)
2433 var cert *x509.Certificate
2435 if block.Type == "CERTIFICATE" {
2436 cert, err = x509.ParseCertificate(block.Bytes)
2437 xcheckf(err, "parse certificate")
2439 case adns.TLSASelectorCert:
2441 case adns.TLSASelectorSPKI:
2442 data = cert.RawSubjectPublicKeyInfo
2444 } else if selector == adns.TLSASelectorCert {
2445 // We need a certificate, just a public/private key won't do.
2446 log.Printf("skipping pem type %q, certificate is required", block.Type)
2449 var privKey, pubKey any
2453 _, err := x509.ParsePKIXPublicKey(block.Bytes)
2454 xcheckf(err, "parse pkix subject public key info (spki)")
2456 case "EC PRIVATE KEY":
2457 privKey, err = x509.ParseECPrivateKey(block.Bytes)
2458 xcheckf(err, "parse ec private key")
2459 case "RSA PRIVATE KEY":
2460 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
2461 xcheckf(err, "parse pkcs#1 rsa private key")
2462 case "RSA PUBLIC KEY":
2463 pubKey, err = x509.ParsePKCS1PublicKey(block.Bytes)
2464 xcheckf(err, "parse pkcs#1 rsa public key")
2466 // PKCS#8 private key
2467 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
2468 xcheckf(err, "parse pkcs#8 private key")
2470 log.Printf("skipping unrecognized pem type %q", block.Type)
2474 if pubKey == nil && privKey != nil {
2475 if signer, ok := privKey.(crypto.Signer); !ok {
2476 log.Fatalf("private key of type %T is not a signer, cannot get public key", privKey)
2478 pubKey = signer.Public()
2482 // Should not happen.
2483 log.Fatalf("internal error: did not find private or public key")
2485 data, err = x509.MarshalPKIXPublicKey(pubKey)
2486 xcheckf(err, "marshal pkix subject public key info (spki)")
2491 case adns.TLSAMatchTypeFull:
2492 case adns.TLSAMatchTypeSHA256:
2493 p := sha256.Sum256(data)
2495 case adns.TLSAMatchTypeSHA512:
2496 p := sha512.Sum512(data)
2499 fmt.Printf("%d %d %d %x\n", usage, selector, matchType, data)
2504func cmdDNSLookup(c *cmd) {
2505 c.params = "[ptr | mx | cname | ips | a | aaaa | ns | txt | srv | tlsa] name"
2506 c.help = `Lookup DNS name of given type.
2508Lookup always prints whether the response was DNSSEC-protected.
2512mox dns lookup ptr 1.1.1.1
2513mox dns lookup mx xmox.nl
2514mox dns lookup txt _dmarc.xmox.nl.
2515mox dns lookup tlsa _25._tcp.xmox.nl
2523 resolver := dns.StrictResolver{Pkg: "dns"}
2525 // like xparseDomain, but treat unparseable domain as an ASCII name so names with
2526 // underscores are still looked up, e,g <selector>._domainkey.<host>.
2527 xdomain := func(s string) dns.Domain {
2528 d, err := dns.ParseDomain(s)
2530 return dns.Domain{ASCII: strings.TrimSuffix(s, ".")}
2535 cmd, name := args[0], args[1]
2539 ip := xparseIP(name, "ip")
2540 ptrs, result, err := resolver.LookupAddr(context.Background(), ip.String())
2542 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2544 fmt.Printf("names (%d, %s):\n", len(ptrs), dnssecStatus(result.Authentic))
2545 for _, ptr := range ptrs {
2546 fmt.Printf("- %s\n", ptr)
2550 name := xdomain(name)
2551 mxl, result, err := resolver.LookupMX(context.Background(), name.ASCII+".")
2553 log.Printf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2554 // We can still have valid records...
2556 fmt.Printf("mx records (%d, %s):\n", len(mxl), dnssecStatus(result.Authentic))
2557 for _, mx := range mxl {
2558 fmt.Printf("- %s, preference %d\n", mx.Host, mx.Pref)
2562 name := xdomain(name)
2563 target, result, err := resolver.LookupCNAME(context.Background(), name.ASCII+".")
2565 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2567 fmt.Printf("%s (%s)\n", target, dnssecStatus(result.Authentic))
2569 case "ips", "a", "aaaa":
2573 } else if cmd == "aaaa" {
2576 name := xdomain(name)
2577 ips, result, err := resolver.LookupIP(context.Background(), network, name.ASCII+".")
2579 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2581 fmt.Printf("records (%d, %s):\n", len(ips), dnssecStatus(result.Authentic))
2582 for _, ip := range ips {
2583 fmt.Printf("- %s\n", ip)
2587 name := xdomain(name)
2588 nsl, result, err := resolver.LookupNS(context.Background(), name.ASCII+".")
2590 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2592 fmt.Printf("ns records (%d, %s):\n", len(nsl), dnssecStatus(result.Authentic))
2593 for _, ns := range nsl {
2594 fmt.Printf("- %s\n", ns)
2598 host := xdomain(name)
2599 l, result, err := resolver.LookupTXT(context.Background(), host.ASCII+".")
2601 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2603 fmt.Printf("txt records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2604 for _, txt := range l {
2605 fmt.Printf("- %s\n", txt)
2609 host := xdomain(name)
2610 _, l, result, err := resolver.LookupSRV(context.Background(), "", "", host.ASCII+".")
2612 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2614 fmt.Printf("srv records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2615 for _, srv := range l {
2616 fmt.Printf("- host %s, port %d, priority %d, weight %d\n", srv.Target, srv.Port, srv.Priority, srv.Weight)
2620 host := xdomain(name)
2621 l, result, err := resolver.LookupTLSA(context.Background(), 0, "", host.ASCII+".")
2623 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2625 fmt.Printf("tlsa records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2626 for _, tlsa := range l {
2627 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)
2630 log.Fatalf("unknown record type %q", args[0])
2634func cmdDKIMGened25519(c *cmd) {
2635 c.params = ">$selector._domainkey.$domain.ed25519.privatekey.pkcs8.pem"
2636 c.help = `Generate a new ed25519 key for use with DKIM.
2638Ed25519 keys are much smaller than RSA keys of comparable cryptographic
2639strength. This is convenient because of maximum DNS message sizes. At the time
2640of writing, not many mail servers appear to support ed25519 DKIM keys though,
2641so it is recommended to sign messages with both RSA and ed25519 keys.
2643 if len(c.Parse()) != 0 {
2647 buf, err := admin.MakeDKIMEd25519Key(dns.Domain{}, dns.Domain{})
2648 xcheckf(err, "making dkim ed25519 key")
2649 _, err = os.Stdout.Write(buf)
2650 xcheckf(err, "writing dkim ed25519 key")
2653func cmdDKIMTXT(c *cmd) {
2654 c.params = "<$selector._domainkey.$domain.key.pkcs8.pem"
2655 c.help = `Print a DKIM DNS TXT record with the public key derived from the private key read from stdin.
2657The DNS should be configured as a TXT record at $selector._domainkey.$domain.
2659 if len(c.Parse()) != 0 {
2663 privKey, err := parseDKIMKey(os.Stdin)
2664 xcheckf(err, "reading dkim private key from stdin")
2668 Hashes: []string{"sha256"},
2669 Flags: []string{"s"},
2672 switch key := privKey.(type) {
2673 case *rsa.PrivateKey:
2674 r.PublicKey = key.Public()
2675 case ed25519.PrivateKey:
2676 r.PublicKey = key.Public()
2679 log.Fatalf("unsupported private key type %T, must be rsa or ed25519", privKey)
2682 record, err := r.Record()
2683 xcheckf(err, "making record")
2684 fmt.Print("<selector>._domainkey.<your.domain.> TXT ")
2688 s, record = record[:100], record[100:]
2692 fmt.Printf(`"%s" `, s)
2697func parseDKIMKey(r io.Reader) (any, error) {
2698 buf, err := io.ReadAll(r)
2700 return nil, fmt.Errorf("reading pem from stdin: %v", err)
2702 b, _ := pem.Decode(buf)
2704 return nil, fmt.Errorf("decoding pem: %v", err)
2706 privKey, err := x509.ParsePKCS8PrivateKey(b.Bytes)
2708 return nil, fmt.Errorf("parsing private key: %v", err)
2713func cmdDKIMVerify(c *cmd) {
2714 c.params = "message"
2715 c.help = `Verify the DKIM signatures in a message and print the results.
2717The message is parsed, and the DKIM-Signature headers are validated. Validation
2718of older messages may fail because the DNS records have been removed or changed
2719by now, or because the signature header may have specified an expiration time
2727 msgf, err := os.Open(args[0])
2728 xcheckf(err, "open message")
2730 results, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, false, dkim.DefaultPolicy, msgf, true)
2731 xcheckf(err, "dkim verify")
2733 for _, result := range results {
2735 if result.Sig == nil {
2736 log.Printf("warning: could not parse signature")
2738 sigh, err = result.Sig.Header()
2740 log.Printf("warning: packing signature: %s", err)
2744 if result.Record == nil {
2745 log.Printf("warning: missing DNS record")
2747 txt, err = result.Record.Record()
2749 log.Printf("warning: packing record: %s", err)
2752 fmt.Printf("status %q, err %v\nrecord %q\nheader %s\n", result.Status, result.Err, txt, sigh)
2756func cmdDKIMSign(c *cmd) {
2757 c.params = "message"
2758 c.help = `Sign a message, adding DKIM-Signature headers based on the domain in the From header.
2760The message is parsed, the domain looked up in the configuration files, and
2761DKIM-Signature headers generated. The message is printed with the DKIM-Signature
2769 msgf, err := os.Open(args[0])
2770 xcheckf(err, "open message")
2772 if err := msgf.Close(); err != nil {
2773 log.Printf("closing message file: %v", err)
2777 p, err := message.Parse(c.log.Logger, true, msgf)
2778 xcheckf(err, "parsing message")
2780 if len(p.Envelope.From) != 1 {
2781 log.Fatalf("found %d from headers, need exactly 1", len(p.Envelope.From))
2783 localpart, err := smtp.ParseLocalpart(p.Envelope.From[0].User)
2784 xcheckf(err, "parsing localpart of address in from-header")
2785 dom := xparseDomain(p.Envelope.From[0].Host, "domain of address in from-header")
2789 domConf, ok := mox.Conf.Domain(dom)
2791 log.Fatalf("domain %s not configured", dom)
2794 selectors := mox.DKIMSelectors(domConf.DKIM)
2795 headers, err := dkim.Sign(context.Background(), c.log.Logger, localpart, dom, selectors, false, msgf)
2796 xcheckf(err, "signing message with dkim")
2798 log.Fatalf("no DKIM configured for domain %s", dom)
2800 _, err = fmt.Fprint(os.Stdout, headers)
2801 xcheckf(err, "write headers")
2802 _, err = io.Copy(os.Stdout, msgf)
2803 xcheckf(err, "write message")
2806func cmdDKIMLookup(c *cmd) {
2807 c.params = "selector domain"
2808 c.help = "Lookup and print the DKIM record for the selector at the domain."
2814 selector := xparseDomain(args[0], "selector")
2815 domain := xparseDomain(args[1], "domain")
2817 status, record, txt, authentic, err := dkim.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, selector, domain)
2819 fmt.Printf("error: %s\n", err)
2821 if status != dkim.StatusNeutral {
2822 fmt.Printf("status: %s\n", status)
2825 fmt.Printf("TXT record: %s\n", txt)
2828 fmt.Println("dnssec-signed: yes")
2830 fmt.Println("dnssec-signed: no")
2833 fmt.Printf("Record:\n")
2835 "version", record.Version,
2836 "hashes", record.Hashes,
2838 "notes", record.Notes,
2839 "services", record.Services,
2840 "flags", record.Flags,
2842 for i := 0; i < len(pairs); i += 2 {
2843 fmt.Printf("\t%s: %v\n", pairs[i], pairs[i+1])
2848func cmdDMARCLookup(c *cmd) {
2850 c.help = "Lookup dmarc policy for domain, a DNS TXT record at _dmarc.<domain>, validate and print it."
2856 fromdomain := xparseDomain(args[0], "domain")
2857 _, domain, _, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, fromdomain)
2858 xcheckf(err, "dmarc lookup domain %s", fromdomain)
2859 fmt.Printf("dmarc record at domain %s: %s\n", domain, txt)
2860 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2863func dnssecStatus(v bool) string {
2865 return "with dnssec"
2867 return "without dnssec"
2870func cmdDMARCVerify(c *cmd) {
2871 c.params = "remoteip mailfromaddress helodomain < message"
2872 c.help = `Parse an email message and evaluate it against the DMARC policy of the domain in the From-header.
2874mailfromaddress and helodomain are used for SPF validation. If both are empty,
2875SPF validation is skipped.
2877mailfromaddress should be the address used as MAIL FROM in the SMTP session.
2878For DSN messages, that address may be empty. The helo domain was specified at
2879the beginning of the SMTP transaction that delivered the message. These values
2880can be found in message headers.
2887 var heloDomain *dns.Domain
2889 remoteIP := xparseIP(args[0], "remoteip")
2891 var mailfrom *smtp.Address
2893 a, err := smtp.ParseAddress(args[1])
2894 xcheckf(err, "parsing mailfrom address")
2898 d := xparseDomain(args[2], "helo domain")
2901 var received *spf.Received
2902 spfStatus := spf.StatusNone
2903 var spfIdentity *dns.Domain
2904 if mailfrom != nil || heloDomain != nil {
2905 spfArgs := spf.Args{
2907 LocalIP: net.ParseIP("127.0.0.1"),
2908 LocalHostname: dns.Domain{ASCII: "localhost"},
2910 if mailfrom != nil {
2911 spfArgs.MailFromLocalpart = mailfrom.Localpart
2912 spfArgs.MailFromDomain = mailfrom.Domain
2914 if heloDomain != nil {
2915 spfArgs.HelloDomain = dns.IPDomain{Domain: *heloDomain}
2917 rspf, spfDomain, expl, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfArgs)
2919 log.Printf("spf verify: %v (explanation: %q, authentic %v)", err, expl, authentic)
2922 spfStatus = received.Result
2923 // todo: should probably potentially do two separate spf validations
2924 if mailfrom != nil {
2925 spfIdentity = &mailfrom.Domain
2927 spfIdentity = heloDomain
2929 fmt.Printf("spf result: %s: %s (%s)\n", spfDomain, spfStatus, dnssecStatus(authentic))
2933 data, err := io.ReadAll(os.Stdin)
2934 xcheckf(err, "read message")
2935 dmarcFrom, _, _, err := message.From(c.log.Logger, false, bytes.NewReader(data), nil)
2936 xcheckf(err, "extract dmarc from message")
2938 const ignoreTestMode = false
2939 dkimResults, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, true, func(*dkim.Sig) error { return nil }, bytes.NewReader(data), ignoreTestMode)
2940 xcheckf(err, "dkim verify")
2941 for _, r := range dkimResults {
2942 fmt.Printf("dkim result: %q (err %v)\n", r.Status, r.Err)
2945 _, result := dmarc.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, dmarcFrom.Domain, dkimResults, spfStatus, spfIdentity, false)
2946 xcheckf(result.Err, "dmarc verify")
2947 fmt.Printf("dmarc from: %s\ndmarc status: %q\ndmarc reject: %v\ncmarc record: %s\n", dmarcFrom, result.Status, result.Reject, result.Record)
2950func cmdDMARCCheckreportaddrs(c *cmd) {
2952 c.help = `For each reporting address in the domain's DMARC record, check if it has opted into receiving reports (if needed).
2954A DMARC record can request reports about DMARC evaluations to be sent to an
2955email/http address. If the organizational domains of that of the DMARC record
2956and that of the report destination address do not match, the destination
2957address must opt-in to receiving DMARC reports by creating a DMARC record at
2958<dmarcdomain>._report._dmarc.<reportdestdomain>.
2965 dom := xparseDomain(args[0], "domain")
2966 _, domain, record, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dom)
2967 xcheckf(err, "dmarc lookup domain %s", dom)
2968 fmt.Printf("dmarc record at domain %s: %q\n", domain, txt)
2969 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2971 check := func(kind, addr string) {
2974 printResult := func(format string, args ...any) {
2975 fmt.Printf("%s %s: %s (%s)\n", kind, addr, fmt.Sprintf(format, args...), dnssecStatus(authentic))
2978 u, err := url.Parse(addr)
2980 printResult("parsing uri: %v (skipping)", addr, err)
2983 var destdom dns.Domain
2986 a, err := smtp.ParseAddress(u.Opaque)
2988 printResult("parsing destination email address %s: %v (skipping)", u.Opaque, err)
2993 printResult("unrecognized scheme in reporting address %s (skipping)", u.Scheme)
2997 if publicsuffix.Lookup(context.Background(), c.log.Logger, dom) == publicsuffix.Lookup(context.Background(), c.log.Logger, destdom) {
2998 printResult("pass (same organizational domain)")
3002 accepts, status, _, txts, authentic, err := dmarc.LookupExternalReportsAccepted(context.Background(), c.log.Logger, dns.StrictResolver{}, domain, destdom)
3004 txtaddr := fmt.Sprintf("%s._report._dmarc.%s", domain.ASCII, destdom.ASCII)
3006 txtstr = fmt.Sprintf(" (no txt records %s)", txtaddr)
3008 txtstr = fmt.Sprintf(" (txt record %s: %q)", txtaddr, txts)
3010 if status != dmarc.StatusNone {
3011 printResult("fail: %s%s", err, txtstr)
3013 printResult("pass%s", txtstr)
3014 } else if err != nil {
3015 printResult("fail: %s%s", err, txtstr)
3017 printResult("fail%s", txtstr)
3021 for _, uri := range record.AggregateReportAddresses {
3022 check("aggregate reporting", uri.Address)
3024 for _, uri := range record.FailureReportAddresses {
3025 check("failure reporting", uri.Address)
3029func cmdDMARCParsereportmsg(c *cmd) {
3030 c.params = "message ..."
3031 c.help = `Parse a DMARC report from an email message, and print its extracted details.
3033DMARC reports are periodically mailed, if requested in the DMARC DNS record of
3034a domain. Reports are sent by mail servers that received messages with our
3035domain in a From header. This may or may not be legatimate email. DMARC reports
3036contain summaries of evaluations of DMARC and DKIM/SPF, which can help
3037understand email deliverability problems.
3044 for _, arg := range args {
3045 f, err := os.Open(arg)
3046 xcheckf(err, "open %q", arg)
3047 feedback, err := dmarcrpt.ParseMessageReport(c.log.Logger, f)
3048 xcheckf(err, "parse report in %q", arg)
3049 meta := feedback.ReportMetadata
3050 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)
3051 if len(meta.Errors) > 0 {
3052 fmt.Printf("Errors:\n")
3053 for _, s := range meta.Errors {
3054 fmt.Printf("\t- %s\n", s)
3057 pol := feedback.PolicyPublished
3058 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)
3059 for _, record := range feedback.Records {
3060 idents := record.Identifiers
3061 fmt.Printf("\theaderfrom %q, envelopes from %q, to %q\n", idents.HeaderFrom, idents.EnvelopeFrom, idents.EnvelopeTo)
3062 eval := record.Row.PolicyEvaluated
3064 for _, reason := range eval.Reasons {
3065 reasons += "; " + string(reason.Type)
3066 if reason.Comment != "" {
3067 reasons += fmt.Sprintf(": %q", reason.Comment)
3070 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)
3071 for _, dkim := range record.AuthResults.DKIM {
3073 if dkim.HumanResult != "" {
3074 result = fmt.Sprintf(": %q", dkim.HumanResult)
3076 fmt.Printf("\t\tdkim %s; domain %q selector %q%s\n", dkim.Result, dkim.Domain, dkim.Selector, result)
3078 for _, spf := range record.AuthResults.SPF {
3079 fmt.Printf("\t\tspf %s; domain %q scope %q\n", spf.Result, spf.Domain, spf.Scope)
3085func cmdDMARCDBAddReport(c *cmd) {
3087 c.params = "fromdomain < message"
3088 c.help = "Add a DMARC report to the database."
3096 fromdomain := xparseDomain(args[0], "domain")
3097 fmt.Fprintln(os.Stderr, "reading report message from stdin")
3098 report, err := dmarcrpt.ParseMessageReport(c.log.Logger, os.Stdin)
3099 xcheckf(err, "parse message")
3100 err = dmarcdb.AddReport(context.Background(), report, fromdomain)
3101 xcheckf(err, "add dmarc report")
3104func cmdTLSRPTLookup(c *cmd) {
3106 c.help = `Lookup the TLSRPT record for the domain.
3108A TLSRPT record typically contains an email address where reports about TLS
3109connectivity should be sent. Mail servers attempting delivery to our domain
3110should attempt to use TLS. TLSRPT lets them report how many connection
3111successfully used TLS, and how what kind of errors occurred otherwise.
3118 d := xparseDomain(args[0], "domain")
3119 _, txt, err := tlsrpt.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, d)
3120 xcheckf(err, "tlsrpt lookup for %s", d)
3124func cmdTLSRPTParsereportmsg(c *cmd) {
3125 c.params = "message ..."
3126 c.help = `Parse and print the TLSRPT in the message.
3128The report is printed in formatted JSON.
3135 for _, arg := range args {
3136 f, err := os.Open(arg)
3137 xcheckf(err, "open %q", arg)
3138 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, f)
3139 xcheckf(err, "parse report in %q", arg)
3140 // todo future: only print the highlights?
3141 enc := json.NewEncoder(os.Stdout)
3142 enc.SetIndent("", "\t")
3143 enc.SetEscapeHTML(false)
3144 err = enc.Encode(reportJSON)
3145 xcheckf(err, "write report")
3149func cmdSPFCheck(c *cmd) {
3150 c.params = "domain ip"
3151 c.help = `Check the status of IP for the policy published in DNS for the domain.
3153IPs may be allowed to send for a domain, or disallowed, and several shades in
3154between. If not allowed, an explanation may be provided by the policy. If so,
3155the explanation is printed. The SPF mechanism that matched (if any) is also
3163 domain := xparseDomain(args[0], "domain")
3165 ip := xparseIP(args[1], "ip")
3167 spfargs := spf.Args{
3169 MailFromLocalpart: "user",
3170 MailFromDomain: domain,
3171 HelloDomain: dns.IPDomain{Domain: domain},
3172 LocalIP: net.ParseIP("127.0.0.1"),
3173 LocalHostname: dns.Domain{ASCII: "localhost"},
3175 r, _, explanation, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfargs)
3177 fmt.Printf("error: %s\n", err)
3179 if explanation != "" {
3180 fmt.Printf("explanation: %s\n", explanation)
3182 fmt.Printf("status: %s (%s)\n", r.Result, dnssecStatus(authentic))
3183 if r.Mechanism != "" {
3184 fmt.Printf("mechanism: %s\n", r.Mechanism)
3188func cmdSPFParse(c *cmd) {
3189 c.params = "txtrecord"
3190 c.help = "Parse the record as SPF record. If valid, nothing is printed."
3196 _, _, err := spf.ParseRecord(args[0])
3197 xcheckf(err, "parsing record")
3200func cmdSPFLookup(c *cmd) {
3202 c.help = "Lookup the SPF record for the domain and print it."
3208 domain := xparseDomain(args[0], "domain")
3209 _, txt, _, authentic, err := spf.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
3210 xcheckf(err, "spf lookup for %s", domain)
3212 fmt.Printf("(%s)\n", dnssecStatus(authentic))
3215func cmdMTASTSLookup(c *cmd) {
3217 c.help = `Lookup the MTASTS record and policy for the domain.
3219MTA-STS is a mechanism for a domain to specify if it requires TLS connections
3220for delivering email. If a domain has a valid MTA-STS DNS TXT record at
3221_mta-sts.<domain> it signals it implements MTA-STS. A policy can then be
3222fetched at https://mta-sts.<domain>/.well-known/mta-sts.txt. The policy
3223specifies the mode (enforce, testing, none), which MX servers support TLS and
3224should be used, and how long the policy can be cached.
3231 domain := xparseDomain(args[0], "domain")
3233 record, policy, _, err := mtasts.Get(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
3235 fmt.Printf("error: %s\n", err)
3238 fmt.Printf("DNS TXT record _mta-sts.%s: %s\n", domain.ASCII, record.String())
3242 fmt.Printf("policy at https://mta-sts.%s/.well-known/mta-sts.txt:\n", domain.ASCII)
3243 fmt.Printf("%s", policy.String())
3247func cmdRDAPDomainage(c *cmd) {
3249 c.help = `Lookup the age of domain in RDAP based on latest registration.
3251RDAP is the registration data access protocol. Registries run RDAP services for
3252their top level domains, providing information such as the registration date of
3253domains. This command looks up the "age" of a domain by looking at the most
3254recent "registration", "reregistration" or "reinstantiation" event.
3256Email messages from recently registered domains are often treated with
3257suspicion, and some mail systems are more likely to classify them as junk.
3259On each invocation, a bootstrap file with a list of registries (of top-level
3260domains) is retrieved, without caching. Do not run this command too often with
3268 domain := xparseDomain(args[0], "domain")
3270 registration, err := rdap.LookupLastDomainRegistration(context.Background(), c.log, domain)
3271 xcheckf(err, "looking up domain in rdap")
3273 age := time.Since(registration)
3274 const day = 24 * time.Hour
3275 const year = 365 * day
3277 days := (age - years*year) / day
3281 } else if years > 0 {
3282 s = fmt.Sprintf("%d years, ", years)
3287 s += fmt.Sprintf("%d days", days)
3292func cmdRetrain(c *cmd) {
3293 c.params = "[accountname]"
3294 c.help = `Recreate and retrain the junk filter for the account or all accounts.
3296Useful after having made changes to the junk filter configuration, or if the
3297implementation has changed.
3309 ctlcmdRetrain(xctl(), account)
3312func ctlcmdRetrain(ctl *ctl, account string) {
3313 ctl.xwrite("retrain")
3318func cmdTLSRPTDBAddReport(c *cmd) {
3320 c.params = "< message"
3321 c.help = "Parse a TLS report from the message and add it to the database."
3323 c.flag.BoolVar(&hostReport, "hostreport", false, "report for a host instead of domain")
3331 // First read message, to get the From-header. Then parse it as TLSRPT.
3332 fmt.Fprintln(os.Stderr, "reading report message from stdin")
3333 buf, err := io.ReadAll(os.Stdin)
3334 xcheckf(err, "reading message")
3335 part, err := message.Parse(c.log.Logger, true, bytes.NewReader(buf))
3336 xcheckf(err, "parsing message")
3337 if part.Envelope == nil || len(part.Envelope.From) != 1 {
3338 log.Fatalf("message must have one From-header")
3340 from := part.Envelope.From[0]
3341 domain := xparseDomain(from.Host, "domain")
3343 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, bytes.NewReader(buf))
3344 xcheckf(err, "parsing tls report in message")
3346 mailfrom := from.User + "@" + from.Host // todo future: should escape and such
3347 report := reportJSON.Convert()
3348 err = tlsrptdb.AddReport(context.Background(), c.log, domain, mailfrom, hostReport, &report)
3349 xcheckf(err, "add tls report to database")
3352func cmdDNSBLCheck(c *cmd) {
3353 c.params = "zone ip"
3354 c.help = `Test if IP is in the DNS blocklist of the zone, e.g. bl.spamcop.net.
3356If the IP is in the blocklist, an explanation is printed. This is typically a
3357URL with more information.
3364 zone := xparseDomain(args[0], "zone")
3365 ip := xparseIP(args[1], "ip")
3367 status, explanation, err := dnsbl.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, zone, ip)
3368 fmt.Printf("status: %s\n", status)
3369 if status == dnsbl.StatusFail {
3370 fmt.Printf("explanation: %q\n", explanation)
3373 fmt.Printf("error: %s\n", err)
3377func cmdDNSBLCheckhealth(c *cmd) {
3379 c.help = `Check the health of the DNS blocklist represented by zone, e.g. bl.spamcop.net.
3381The health of a DNS blocklist can be checked by querying for 127.0.0.1 and
3382127.0.0.2. The second must and the first must not be present.
3389 zone := xparseDomain(args[0], "zone")
3390 err := dnsbl.CheckHealth(context.Background(), c.log.Logger, dns.StrictResolver{}, zone)
3391 xcheckf(err, "unhealthy")
3392 fmt.Println("healthy")
3395func cmdCheckupdate(c *cmd) {
3396 c.help = `Check if a newer version of mox is available.
3398A single DNS TXT lookup to _updates.xmox.nl tells if a new version is
3399available. If so, a changelog is fetched from https://updates.xmox.nl, and the
3400individual entries verified with a builtin public key. The changelog is
3403 if len(c.Parse()) != 0 {
3408 current, lastknown, _, err := store.LastKnown()
3410 log.Printf("getting last known version: %s", err)
3412 fmt.Printf("last known version: %s\n", lastknown)
3413 fmt.Printf("current version: %s\n", current)
3415 latest, _, err := updates.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dns.Domain{ASCII: changelogDomain})
3416 xcheckf(err, "lookup of latest version")
3417 fmt.Printf("latest version: %s\n", latest)
3419 if latest.After(current) {
3420 changelog, err := updates.FetchChangelog(context.Background(), c.log.Logger, changelogURL, current, changelogPubKey)
3421 xcheckf(err, "fetching changelog")
3422 if len(changelog.Changes) == 0 {
3423 log.Printf("no changes in changelog")
3426 fmt.Println("Changelog")
3427 for _, c := range changelog.Changes {
3428 fmt.Println("\n" + strings.TrimSpace(c.Text))
3433func cmdCid(c *cmd) {
3435 c.help = `Turn an ID from a Received header into a cid, for looking up in logs.
3437A cid is essentially a connection counter initialized when mox starts. Each log
3438line contains a cid. Received headers added by mox contain a unique ID that can
3439be decrypted to a cid by admin of a mox instance only.
3447 recvidpath := mox.DataDirPath("receivedid.key")
3448 recvidbuf, err := os.ReadFile(recvidpath)
3449 xcheckf(err, "reading %s", recvidpath)
3450 if len(recvidbuf) != 16+8 {
3451 log.Fatalf("bad data in %s: got %d bytes, expect 16+8=24", recvidpath, len(recvidbuf))
3453 err = mox.ReceivedIDInit(recvidbuf[:16], recvidbuf[16:])
3454 xcheckf(err, "init receivedid")
3456 cid, err := mox.ReceivedToCid(args[0])
3457 xcheckf(err, "received id to cid")
3458 fmt.Printf("%x\n", cid)
3461func cmdVersion(c *cmd) {
3462 c.help = "Prints this mox version."
3463 if len(c.Parse()) != 0 {
3466 fmt.Println(moxvar.Version)
3467 fmt.Printf("%s/%s\n", runtime.GOOS, runtime.GOARCH)
3470func cmdWebapi(c *cmd) {
3471 c.params = "[method [baseurl-with-credentials]"
3472 c.help = "Lists available methods, prints request/response parameters for method, or calls a method with a request read from standard input."
3478 t := reflect.TypeFor[webapi.Methods]()
3479 methods := map[string]reflect.Type{}
3481 for i := range t.NumMethod() {
3483 methods[mt.Name] = mt.Type
3484 ml = append(ml, mt.Name)
3488 fmt.Println(strings.Join(ml, "\n"))
3492 mt, ok := methods[args[0]]
3494 log.Fatalf("unknown method %q", args[0])
3496 resultNotJSON := mt.Out(0).Kind() == reflect.Interface
3499 fmt.Println("# Example request")
3501 printJSON("\t", mox.FillExample(nil, reflect.New(mt.In(1))).Interface())
3504 fmt.Println("Output is non-JSON data.")
3507 fmt.Println("# Example response")
3509 printJSON("\t", mox.FillExample(nil, reflect.New(mt.Out(0))).Interface())
3515 response = reflect.New(mt.Out(0))
3518 fmt.Fprintln(os.Stderr, "reading request from stdin...")
3519 request, err := io.ReadAll(os.Stdin)
3520 xcheckf(err, "read message")
3522 dec := json.NewDecoder(bytes.NewReader(request))
3523 dec.DisallowUnknownFields()
3524 err = dec.Decode(reflect.New(mt.In(1)).Interface())
3525 xcheckf(err, "parsing request")
3527 resp, err := http.PostForm(args[1]+args[0], url.Values{"request": []string{string(request)}})
3528 xcheckf(err, "http post")
3530 if err := resp.Body.Close(); err != nil {
3531 log.Printf("closing http response body: %v", err)
3534 if resp.StatusCode == http.StatusBadRequest {
3535 buf, err := io.ReadAll(&moxio.LimitReader{R: resp.Body, Limit: 10 * 1024})
3536 xcheckf(err, "reading response for 400 bad request error")
3537 err = json.Unmarshal(buf, &response)
3539 printJSON("", response)
3541 fmt.Fprintf(os.Stderr, "(not json)\n")
3542 os.Stderr.Write(buf)
3545 } else if resp.StatusCode != http.StatusOK {
3546 fmt.Fprintf(os.Stderr, "http response %s\n", resp.Status)
3547 _, err := io.Copy(os.Stderr, resp.Body)
3548 xcheckf(err, "copy body")
3550 err := json.NewDecoder(resp.Body).Decode(&resp)
3551 xcheckf(err, "unmarshal response")
3552 printJSON("", response)
3556func printJSON(indent string, v any) {
3557 fmt.Printf("%s", indent)
3558 enc := json.NewEncoder(os.Stdout)
3559 enc.SetIndent(indent, "\t")
3560 enc.SetEscapeHTML(false)
3561 err := enc.Encode(v)
3562 xcheckf(err, "encode json")
3565// 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.
3566func cmdBumpUIDValidity(c *cmd) {
3567 c.params = "account [mailbox]"
3568 c.help = `Change the IMAP UID validity of the mailbox, causing IMAP clients to refetch messages.
3570This can be useful after manually repairing metadata about the account/mailbox.
3572Opens account database file directly. Ensure mox does not have the account
3573open, or is not running.
3576 if len(args) != 1 && len(args) != 2 {
3581 a, err := store.OpenAccount(c.log, args[0], false)
3582 xcheckf(err, "open account")
3584 if err := a.Close(); err != nil {
3585 log.Printf("closing account: %v", err)
3589 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3590 uidvalidity, err := a.NextUIDValidity(tx)
3592 return fmt.Errorf("assigning next uid validity: %v", err)
3595 q := bstore.QueryTx[store.Mailbox](tx)
3596 q.FilterEqual("Expunged", false)
3598 q.FilterEqual("Name", args[1])
3600 mbl, err := q.SortAsc("Name").List()
3602 return fmt.Errorf("looking up mailbox: %v", err)
3604 if len(args) == 2 && len(mbl) != 1 {
3605 return fmt.Errorf("looking up mailbox %q, found %d mailboxes", args[1], len(mbl))
3607 for _, mb := range mbl {
3608 mb.UIDValidity = uidvalidity
3609 err = tx.Update(&mb)
3611 return fmt.Errorf("updating uid validity for mailbox: %v", err)
3613 fmt.Printf("uid validity for %q updated to %d\n", mb.Name, uidvalidity)
3617 xcheckf(err, "updating database")
3620func cmdReassignUIDs(c *cmd) {
3621 c.params = "account [mailboxid]"
3622 c.help = `Reassign UIDs in one mailbox or all mailboxes in an account and bump UID validity, causing IMAP clients to refetch messages.
3624Opens account database file directly. Ensure mox does not have the account
3625open, or is not running.
3628 if len(args) != 1 && len(args) != 2 {
3635 mailboxID, err = strconv.ParseInt(args[1], 10, 64)
3636 xcheckf(err, "parsing mailbox id")
3640 a, err := store.OpenAccount(c.log, args[0], false)
3641 xcheckf(err, "open account")
3643 if err := a.Close(); err != nil {
3644 log.Printf("closing account: %v", err)
3648 // Gather the last-assigned UIDs per mailbox.
3649 uidlasts := map[int64]store.UID{}
3651 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3652 // Reassign UIDs, going per mailbox. We assign starting at 1, only changing the
3653 // message if it isn't already at the intended UID. Doing it in this order ensures
3654 // we don't get into trouble with duplicate UIDs for a mailbox. We assign a new
3655 // modseq. Not strictly needed, but doesn't hurt. It's also why we assign a UID to
3656 // expunged messages.
3657 modseq, err := a.NextModSeq(tx)
3658 xcheckf(err, "assigning next modseq")
3660 q := bstore.QueryTx[store.Message](tx)
3662 q.FilterNonzero(store.Message{MailboxID: mailboxID})
3664 q.SortAsc("MailboxID", "UID")
3665 err = q.ForEach(func(m store.Message) error {
3666 uidlasts[m.MailboxID]++
3667 uid := uidlasts[m.MailboxID]
3671 if err := tx.Update(&m); err != nil {
3672 return fmt.Errorf("updating uid for message: %v", err)
3678 return fmt.Errorf("reading through messages: %v", err)
3681 // Now update the uidnext, uidvalidity and modseq for each mailbox.
3682 err = bstore.QueryTx[store.Mailbox](tx).FilterEqual("Expunged", false).ForEach(func(mb store.Mailbox) error {
3683 // Assign each mailbox a completely new uidvalidity.
3684 uidvalidity, err := a.NextUIDValidity(tx)
3686 return fmt.Errorf("assigning next uid validity: %v", err)
3689 if mb.UIDValidity >= uidvalidity {
3690 // This should not happen, but since we're fixing things up after a hypothetical
3691 // mishap, might as well account for inconsistent uidvalidity.
3692 next := store.NextUIDValidity{ID: 1, Next: mb.UIDValidity + 2}
3693 if err := tx.Update(&next); err != nil {
3694 log.Printf("updating nextuidvalidity: %v, continuing", err)
3698 mb.UIDValidity = uidvalidity
3700 mb.UIDNext = uidlasts[mb.ID] + 1
3702 if err := tx.Update(&mb); err != nil {
3703 return fmt.Errorf("updating uidvalidity and uidnext for mailbox: %v", err)
3708 return fmt.Errorf("updating mailboxes: %v", err)
3712 xcheckf(err, "updating database")
3715func cmdFixUIDMeta(c *cmd) {
3716 c.params = "account"
3717 c.help = `Fix inconsistent UIDVALIDITY and UIDNEXT in messages/mailboxes/account.
3719The next UID to use for a message in a mailbox should always be higher than any
3720existing message UID in the mailbox. If it is not, the mailbox UIDNEXT is
3723Each mailbox has a UIDVALIDITY sequence number, which should always be lower
3724than the per-account next UIDVALIDITY to use. If it is not, the account next
3725UIDVALIDITY is updated.
3727Opens account database file directly. Ensure mox does not have the account
3728open, or is not running.
3736 a, err := store.OpenAccount(c.log, args[0], false)
3737 xcheckf(err, "open account")
3739 if err := a.Close(); err != nil {
3740 log.Printf("closing account: %v", err)
3744 var maxUIDValidity uint32
3746 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3747 // We look at each mailbox, retrieve its max UID and compare against the mailbox
3749 err := bstore.QueryTx[store.Mailbox](tx).FilterEqual("Expunged", false).ForEach(func(mb store.Mailbox) error {
3750 if mb.UIDValidity > maxUIDValidity {
3751 maxUIDValidity = mb.UIDValidity
3753 m, err := bstore.QueryTx[store.Message](tx).FilterNonzero(store.Message{MailboxID: mb.ID}).SortDesc("UID").Limit(1).Get()
3754 if err == bstore.ErrAbsent || err == nil && m.UID < mb.UIDNext {
3756 } else if err != nil {
3757 return fmt.Errorf("finding message with max uid in mailbox: %w", err)
3759 olduidnext := mb.UIDNext
3760 mb.UIDNext = m.UID + 1
3761 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)
3762 if err := tx.Update(&mb); err != nil {
3763 return fmt.Errorf("updating mailbox uidnext: %v", err)
3768 return fmt.Errorf("processing mailboxes: %v", err)
3771 uidvalidity := store.NextUIDValidity{ID: 1}
3772 if err := tx.Get(&uidvalidity); err != nil {
3773 return fmt.Errorf("reading account next uidvalidity: %v", err)
3775 if maxUIDValidity >= uidvalidity.Next {
3776 log.Printf("account next uidvalidity %d <= highest uidvalidity %d found in mailbox, resetting account next uidvalidity to %d", uidvalidity.Next, maxUIDValidity, maxUIDValidity+1)
3777 uidvalidity.Next = maxUIDValidity + 1
3778 if err := tx.Update(&uidvalidity); err != nil {
3779 return fmt.Errorf("updating account next uidvalidity: %v", err)
3785 xcheckf(err, "updating database")
3788func cmdFixmsgsize(c *cmd) {
3789 c.params = "[account]"
3790 c.help = `Ensure message sizes in the database matching the sum of the message prefix length and on-disk file size.
3792Messages with an inconsistent size are also parsed again.
3794If an inconsistency is found, you should probably also run "mox
3795bumpuidvalidity" on the mailboxes or entire account to force IMAP clients to
3808 ctlcmdFixmsgsize(xctl(), account)
3811func ctlcmdFixmsgsize(ctl *ctl, account string) {
3812 ctl.xwrite("fixmsgsize")
3815 ctl.xstreamto(os.Stdout)
3818func cmdReparse(c *cmd) {
3819 c.params = "[account]"
3820 c.help = `Parse all messages in the account or all accounts again.
3822Can be useful after upgrading mox with improved message parsing. Messages are
3823parsed in batches, so other access to the mailboxes/messages are not blocked
3824while reparsing all messages.
3836 ctlcmdReparse(xctl(), account)
3839func ctlcmdReparse(ctl *ctl, account string) {
3840 ctl.xwrite("reparse")
3843 ctl.xstreamto(os.Stdout)
3846func cmdEnsureParsed(c *cmd) {
3847 c.params = "account"
3848 c.help = "Ensure messages in the database have a pre-parsed MIME form in the database."
3850 c.flag.BoolVar(&all, "all", false, "store new parsed message for all messages")
3857 a, err := store.OpenAccount(c.log, args[0], false)
3858 xcheckf(err, "open account")
3860 if err := a.Close(); err != nil {
3861 log.Printf("closing account: %v", err)
3866 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3867 q := bstore.QueryTx[store.Message](tx)
3868 q.FilterEqual("Expunged", false)
3869 q.FilterFn(func(m store.Message) bool {
3870 return all || m.ParsedBuf == nil
3874 return fmt.Errorf("list messages: %v", err)
3876 for _, m := range l {
3877 mr := a.MessageReader(m)
3878 p, err := message.EnsurePart(c.log.Logger, false, mr, m.Size)
3880 log.Printf("parsing message %d: %v (continuing)", m.ID, err)
3882 m.ParsedBuf, err = json.Marshal(p)
3884 return fmt.Errorf("marshal parsed message: %v", err)
3886 if err := tx.Update(&m); err != nil {
3887 return fmt.Errorf("update message: %v", err)
3893 xcheckf(err, "update messages with parsed mime structure")
3894 fmt.Printf("%d messages updated\n", n)
3897func cmdRecalculateMailboxCounts(c *cmd) {
3898 c.params = "account"
3899 c.help = `Recalculate message counts for all mailboxes in the account, and total message size for quota.
3901When a message is added to/removed from a mailbox, or when message flags change,
3902the total, unread, unseen and deleted messages are accounted, the total size of
3903the mailbox, and the total message size for the account. In case of a bug in
3904this accounting, the numbers could become incorrect. This command will find, fix
3913 ctlcmdRecalculateMailboxCounts(xctl(), args[0])
3916func ctlcmdRecalculateMailboxCounts(ctl *ctl, account string) {
3917 ctl.xwrite("recalculatemailboxcounts")
3920 ctl.xstreamto(os.Stdout)
3923func cmdMessageParse(c *cmd) {
3924 c.params = "message.eml"
3925 c.help = "Parse message, print JSON representation."
3928 c.flag.BoolVar(&smtputf8, "smtputf8", false, "check if message needs smtputf8")
3934 f, err := os.Open(args[0])
3935 xcheckf(err, "open")
3937 if err := f.Close(); err != nil {
3938 log.Printf("closing message file: %v", err)
3942 part, err := message.Parse(c.log.Logger, false, f)
3943 xcheckf(err, "parsing message")
3944 err = part.Walk(c.log.Logger, nil)
3945 xcheckf(err, "parsing nested parts")
3946 enc := json.NewEncoder(os.Stdout)
3947 enc.SetIndent("", "\t")
3948 enc.SetEscapeHTML(false)
3949 err = enc.Encode(part)
3950 xcheckf(err, "write")
3953 needs, err := part.NeedsSMTPUTF8()
3954 xcheckf(err, "checking if message needs smtputf8")
3955 fmt.Println("message needs smtputf8:", needs)
3959func cmdOpenaccounts(c *cmd) {
3961 c.params = "datadir account ..."
3962 c.help = `Open and close accounts, for triggering data upgrades, for tests.
3964Opens database files directly, not going through a running mox instance.
3972 dataDir := filepath.Clean(args[0])
3973 for _, accName := range args[1:] {
3974 accDir := filepath.Join(dataDir, "accounts", accName)
3975 log.Printf("opening account %s...", accDir)
3976 a, err := store.OpenAccountDB(c.log, accDir, accName)
3977 xcheckf(err, "open account %s", accName)
3978 err = a.ThreadingWait(c.log)
3979 xcheckf(err, "wait for threading upgrade to complete for %s", accName)
3981 xcheckf(err, "close account %s", accName)
3985func cmdReassignthreads(c *cmd) {
3986 c.params = "[account]"
3987 c.help = `Reassign message threads.
3989For all accounts, or optionally only the specified account.
3991Threading for all messages in an account is first reset, and new base subject
3992and normalized message-id saved with the message. Then all messages are
3993evaluated and matched against their parents/ancestors.
3995Messages are matched based on the References header, with a fall-back to an
3996In-Reply-To header, and if neither is present/valid, based only on base
3999A References header typically points to multiple previous messages in a
4000hierarchy. From oldest ancestor to most recent parent. An In-Reply-To header
4001would have only a message-id of the parent message.
4003A message is only linked to a parent/ancestor if their base subject is the
4004same. This ensures unrelated replies, with a new subject, are placed in their
4007The base subject is lower cased, has whitespace collapsed to a single
4008space, and some components removed: leading "Re:", "Fwd:", "Fw:", or bracketed
4009tag (that mailing lists often add, e.g. "[listname]"), trailing "(fwd)", or
4010enclosing "[fwd: ...]".
4012Messages are linked to all their ancestors. If an intermediate parent/ancestor
4013message is deleted in the future, the message can still be linked to the earlier
4014ancestors. If the direct parent already wasn't available while matching, this is
4015stored as the message having a "missing link" to its stored ancestors.
4027 ctlcmdReassignthreads(xctl(), account)
4030func ctlcmdReassignthreads(ctl *ctl, account string) {
4031 ctl.xwrite("reassignthreads")
4034 ctl.xstreamto(os.Stdout)
4037func cmdIMAPServe(c *cmd) {
4038 c.params = "preauth-address"
4039 c.help = `Initiate a preauthenticated IMAP connection on file descriptor 0.
4041For use with tools that can do IMAP over tunneled connections, e.g. with SSH
4042during migrations. TLS is not possible on the connection, and authentication
4043does not require TLS.
4046 c.flag.BoolVar(&fd0, "fd0", false, "write IMAP to file descriptor 0 instead of stdout")
4057 ctlcmdIMAPServe(xctl(), address, os.Stdin, output)
4060func ctlcmdIMAPServe(ctl *ctl, address string, input io.ReadCloser, output io.WriteCloser) {
4061 ctl.xwrite("imapserve")
4065 done := make(chan struct{}, 1)
4070 _, err := io.Copy(output, ctl.conn)
4074 log.Printf("reading from imap: %v", err)
4080 _, err := io.Copy(ctl.conn, input)
4084 log.Printf("writing to imap: %v", err)
4089func cmdReadmessages(c *cmd) {
4091 c.params = "datadir account ..."
4092 c.help = `Open account, parse several headers for all messages.
4094For performance testing.
4096Opens database files directly, not going through a running mox instance.
4099 gomaxprocs := runtime.GOMAXPROCS(0)
4100 var procs, workqueuesize, limit int
4101 c.flag.IntVar(&procs, "procs", gomaxprocs, "number of goroutines for reading messages")
4102 c.flag.IntVar(&workqueuesize, "workqueuesize", 2*gomaxprocs, "number of messages to keep in work queue")
4103 c.flag.IntVar(&limit, "limit", 0, "number of messages to process if greater than zero")
4109 type threadPrep struct {
4114 threadingFields := [][]byte{
4115 []byte("references"),
4116 []byte("in-reply-to"),
4119 dataDir := filepath.Clean(args[0])
4120 for _, accName := range args[1:] {
4121 accDir := filepath.Join(dataDir, "accounts", accName)
4122 log.Printf("opening account %s...", accDir)
4123 a, err := store.OpenAccountDB(c.log, accDir, accName)
4124 xcheckf(err, "open account %s", accName)
4126 prepareMessages := func(in, out chan moxio.Work[store.Message, threadPrep]) {
4127 headerbuf := make([]byte, 8*1024)
4128 scratch := make([]byte, 4*1024)
4136 var partialPart struct {
4140 if err := json.Unmarshal(m.ParsedBuf, &partialPart); err != nil {
4141 w.Err = fmt.Errorf("unmarshal part: %v", err)
4143 size := partialPart.BodyOffset - partialPart.HeaderOffset
4144 if int(size) > len(headerbuf) {
4145 headerbuf = make([]byte, size)
4148 buf := headerbuf[:int(size)]
4149 err := func() error {
4150 mr := a.MessageReader(m)
4152 if err := mr.Close(); err != nil {
4153 log.Printf("closing message reader: %v", err)
4157 // ReadAt returns whole buffer or error. Single read should be fast.
4158 n, err := mr.ReadAt(buf, partialPart.HeaderOffset)
4159 if err != nil || n != len(buf) {
4160 return fmt.Errorf("read header: %v", err)
4166 } else if h, err := message.ParseHeaderFields(buf, scratch, threadingFields); err != nil {
4169 w.Out.references = h["References"]
4170 w.Out.inReplyTo = h["In-Reply-To"]
4183 processMessage := func(m store.Message, prep threadPrep) error {
4185 log.Printf("%d messages (delta %s)", n, time.Since(t))
4192 wq := moxio.NewWorkQueue(procs, workqueuesize, prepareMessages, processMessage)
4194 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
4195 q := bstore.QueryTx[store.Message](tx)
4196 q.FilterEqual("Expunged", false)
4201 err = q.ForEach(wq.Add)
4209 xcheckf(err, "processing message")
4212 xcheckf(err, "close account %s", accName)
4213 log.Printf("account %s, total time %s", accName, time.Since(t0))
4217func cmdQueueFillRetired(c *cmd) {
4219 c.help = `Fill retired messag and webhooks queue with testdata.
4221For testing the pagination. Operates directly on queue database.
4224 c.flag.IntVar(&n, "n", 10000, "retired messages and retired webhooks to insert")
4232 xcheckf(err, "init queue")
4233 err = queue.DB.Write(context.Background(), func(tx *bstore.Tx) error {
4236 // Cause autoincrement ID for queue.Msg to be forwarded, and use the reserved ID
4237 // space for inserting retired messages.
4239 err = tx.Insert(&fm)
4240 xcheckf(err, "temporarily insert message to get autoincrement sequence")
4241 err = tx.Delete(&fm)
4242 xcheckf(err, "removing temporary message for resetting autoincrement sequence")
4244 err = tx.Insert(&fm)
4245 xcheckf(err, "temporarily insert message to forward autoincrement sequence")
4246 err = tx.Delete(&fm)
4247 xcheckf(err, "removing temporary message after forwarding autoincrement sequence")
4250 // And likewise for webhooks.
4251 fh := queue.Hook{Account: "x", URL: "x", NextAttempt: time.Now()}
4252 err = tx.Insert(&fh)
4253 xcheckf(err, "temporarily insert webhook to get autoincrement sequence")
4254 err = tx.Delete(&fh)
4255 xcheckf(err, "removing temporary webhook for resetting autoincrement sequence")
4257 err = tx.Insert(&fh)
4258 xcheckf(err, "temporarily insert webhook to forward autoincrement sequence")
4259 err = tx.Delete(&fh)
4260 xcheckf(err, "removing temporary webhook after forwarding autoincrement sequence")
4264 t0 := now.Add(-time.Duration(i) * time.Second)
4265 last := now.Add(-time.Duration(i/10) * time.Second)
4266 mr := queue.MsgRetired{
4267 ID: fm.ID + int64(i),
4269 SenderAccount: "test",
4270 SenderLocalpart: "mox",
4271 SenderDomainStr: "localhost",
4272 FromID: fmt.Sprintf("%016d", i),
4273 RecipientLocalpart: "mox",
4274 RecipientDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "localhost"}},
4275 RecipientDomainStr: "localhost",
4278 Results: []queue.MsgResult{
4281 Duration: time.Millisecond,
4288 Size: int64(i * 100),
4289 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
4290 Subject: fmt.Sprintf("test message %d", i),
4291 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
4293 RecipientAddress: "mox@localhost",
4295 KeepUntil: now.Add(48 * time.Hour),
4297 err := tx.Insert(&mr)
4298 xcheckf(err, "inserting retired message")
4302 t0 := now.Add(-time.Duration(i) * time.Second)
4303 last := now.Add(-time.Duration(i/10) * time.Second)
4308 hr := queue.HookRetired{
4309 ID: fh.ID + int64(i),
4310 QueueMsgID: fm.ID + int64(i),
4311 FromID: fmt.Sprintf("%016d", i),
4312 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
4313 Subject: fmt.Sprintf("test message %d", i),
4314 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
4316 URL: "http://localhost/hook",
4317 IsIncoming: i%10 == 0,
4318 OutgoingEvent: event,
4323 Results: []queue.HookResult{
4326 Duration: time.Millisecond,
4327 URL: "http://localhost/hook",
4336 KeepUntil: now.Add(48 * time.Hour),
4338 err := tx.Insert(&hr)
4339 xcheckf(err, "inserting retired hook")
4344 xcheckf(err, "add to queue")
4345 log.Printf("added %d retired messages and %d retired webhooks", n, n)