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 addresses", cmdConfigAccountAddresses},
152 {"config account add", cmdConfigAccountAdd},
153 {"config account rm", cmdConfigAccountRemove},
154 {"config account disable", cmdConfigAccountDisable},
155 {"config account enable", cmdConfigAccountEnable},
156 {"config address add", cmdConfigAddressAdd},
157 {"config address rm", cmdConfigAddressRemove},
158 {"config address account", cmdConfigAddressAccount},
159 {"config domain add", cmdConfigDomainAdd},
160 {"config domain rm", cmdConfigDomainRemove},
161 {"config domain disable", cmdConfigDomainDisable},
162 {"config domain enable", cmdConfigDomainEnable},
163 {"config tlspubkey list", cmdConfigTlspubkeyList},
164 {"config tlspubkey get", cmdConfigTlspubkeyGet},
165 {"config tlspubkey add", cmdConfigTlspubkeyAdd},
166 {"config tlspubkey rm", cmdConfigTlspubkeyRemove},
167 {"config tlspubkey gen", cmdConfigTlspubkeyGen},
168 {"config alias list", cmdConfigAliasList},
169 {"config alias print", cmdConfigAliasPrint},
170 {"config alias add", cmdConfigAliasAdd},
171 {"config alias update", cmdConfigAliasUpdate},
172 {"config alias rm", cmdConfigAliasRemove},
173 {"config alias addaddr", cmdConfigAliasAddaddr},
174 {"config alias rmaddr", cmdConfigAliasRemoveaddr},
176 {"config describe-sendmail", cmdConfigDescribeSendmail},
177 {"config printservice", cmdConfigPrintservice},
178 {"config ensureacmehostprivatekeys", cmdConfigEnsureACMEHostprivatekeys},
179 {"config example", cmdConfigExample},
181 {"admin imapserve", cmdIMAPServe},
183 {"checkupdate", cmdCheckupdate},
185 {"clientconfig", cmdClientConfig},
186 {"deliver", cmdDeliver},
187 // 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
188 {"dane dial", cmdDANEDial},
189 {"dane dialmx", cmdDANEDialmx},
190 {"dane makerecord", cmdDANEMakeRecord},
191 {"dns lookup", cmdDNSLookup},
192 {"dkim gened25519", cmdDKIMGened25519},
193 {"dkim genrsa", cmdDKIMGenrsa},
194 {"dkim lookup", cmdDKIMLookup},
195 {"dkim txt", cmdDKIMTXT},
196 {"dkim verify", cmdDKIMVerify},
197 {"dkim sign", cmdDKIMSign},
198 {"dmarc lookup", cmdDMARCLookup},
199 {"dmarc parsereportmsg", cmdDMARCParsereportmsg},
200 {"dmarc verify", cmdDMARCVerify},
201 {"dmarc checkreportaddrs", cmdDMARCCheckreportaddrs},
202 {"dnsbl check", cmdDNSBLCheck},
203 {"dnsbl checkhealth", cmdDNSBLCheckhealth},
204 {"mtasts lookup", cmdMTASTSLookup},
205 {"rdap domainage", cmdRDAPDomainage},
206 {"retrain", cmdRetrain},
207 {"sendmail", cmdSendmail},
208 {"smtp dial", cmdSMTPDial},
209 {"spf check", cmdSPFCheck},
210 {"spf lookup", cmdSPFLookup},
211 {"spf parse", cmdSPFParse},
212 {"tlsrpt lookup", cmdTLSRPTLookup},
213 {"tlsrpt parsereportmsg", cmdTLSRPTParsereportmsg},
214 {"version", cmdVersion},
215 {"webapi", cmdWebapi},
217 {"example", cmdExample},
218 {"bumpuidvalidity", cmdBumpUIDValidity},
219 {"reassignuids", cmdReassignUIDs},
220 {"fixuidmeta", cmdFixUIDMeta},
221 {"fixmsgsize", cmdFixmsgsize},
222 {"reparse", cmdReparse},
223 {"ensureparsed", cmdEnsureParsed},
224 {"recalculatemailboxcounts", cmdRecalculateMailboxCounts},
225 {"message parse", cmdMessageParse},
226 {"reassignthreads", cmdReassignthreads},
229 {"helpall", cmdHelpall},
230 {"junk analyze", cmdJunkAnalyze},
231 {"junk check", cmdJunkCheck},
232 {"junk play", cmdJunkPlay},
233 {"junk test", cmdJunkTest},
234 {"junk train", cmdJunkTrain},
235 {"dmarcdb addreport", cmdDMARCDBAddReport},
236 {"tlsrptdb addreport", cmdTLSRPTDBAddReport},
237 {"updates addsigned", cmdUpdatesAddSigned},
238 {"updates genkey", cmdUpdatesGenkey},
239 {"updates pubkey", cmdUpdatesPubkey},
240 {"updates serve", cmdUpdatesServe},
241 {"updates verify", cmdUpdatesVerify},
242 {"gentestdata", cmdGentestdata},
243 {"ximport maildir", cmdXImportMaildir},
244 {"ximport mbox", cmdXImportMbox},
245 {"openaccounts", cmdOpenaccounts},
246 {"readmessages", cmdReadmessages},
247 {"queuefillretired", cmdQueueFillRetired},
253 for _, xc := range commands {
254 c := cmd{words: strings.Split(xc.cmd, " "), fn: xc.fn}
255 cmds = append(cmds, c)
263 // Set before calling command.
266 _gather bool // Set when using Parse to gather usage for a command.
268 // Set by invoked command or Parse.
269 unlisted bool // If set, command is not listed until at least some words are matched from command.
270 params string // Arguments to command. Multiple lines possible.
271 help string // Additional explanation. First line is synopsis, the rest is only printed for an explicit help/usage for that command.
277func (c *cmd) Parse() []string {
278 // To gather params and usage information, we just run the command but cause this
279 // panic after the command has registered its flags and set its params and help
280 // information. This is then caught and that info printed.
285 c.flag.Usage = c.Usage
286 c.flag.Parse(c.flagArgs)
287 c.args = c.flag.Args()
291func (c *cmd) gather() {
292 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
296 // panic generated by Parse.
304func (c *cmd) makeUsage() string {
305 var r strings.Builder
306 cs := "mox " + strings.Join(c.words, " ")
307 for i, line := range strings.Split(strings.TrimSpace(c.params), "\n") {
315 fmt.Fprintf(&r, "%6s %s%s\n", s, cs, line)
318 c.flag.PrintDefaults()
322func (c *cmd) printUsage() {
323 fmt.Fprint(os.Stderr, c.makeUsage())
325 fmt.Fprint(os.Stderr, "\n"+c.help+"\n")
329func (c *cmd) Usage() {
334func cmdHelp(c *cmd) {
335 c.params = "[$command ...]"
336 c.help = `Prints help about matching commands.
338If multiple commands match, they are listed along with the first line of their help text.
339If a single command matches, its usage and full help text is printed.
346 prefix := func(l, pre []string) bool {
347 if len(pre) > len(l) {
350 return slices.Equal(pre, l[:len(pre)])
354 for _, c := range cmds {
355 if slices.Equal(c.words, args) {
357 fmt.Print(c.makeUsage())
359 fmt.Print("\n" + c.help + "\n")
362 } else if prefix(c.words, args) {
363 partial = append(partial, c)
366 if len(partial) == 0 {
367 fmt.Fprintf(os.Stderr, "%s: unknown command\n", strings.Join(args, " "))
370 for _, c := range partial {
372 line := "mox " + strings.Join(c.words, " ")
373 fmt.Printf("%s\n", line)
375 fmt.Printf("\t%s\n", strings.Split(c.help, "\n")[0])
380func cmdHelpall(c *cmd) {
382 c.help = `Print all detailed usage and help information for all listed commands.
384Used to generate documentation.
392 for _, c := range cmds {
398 fmt.Fprintf(os.Stderr, "\n")
402 fmt.Fprintf(os.Stderr, "# mox %s\n\n", strings.Join(c.words, " "))
404 fmt.Fprintln(os.Stderr, c.help+"\n")
407 s = "\t" + strings.ReplaceAll(s, "\n", "\n\t")
408 fmt.Fprintln(os.Stderr, s)
412func usage(l []cmd, unlisted bool) {
415 lines = append(lines, "mox [-config config/mox.conf] [-pedantic] ...")
417 for _, c := range l {
419 if c.unlisted && !unlisted {
422 for line := range strings.SplitSeq(c.params, "\n") {
423 x := append([]string{"mox"}, c.words...)
427 lines = append(lines, strings.Join(x, " "))
430 for i, line := range lines {
435 fmt.Fprintln(os.Stderr, pre+line)
440var loglevel string // Empty will be interpreted as info, except by localserve.
443// subcommands that are not "serve" should use this function to load the config, it
444// restores any loglevel specified on the command-line, instead of using the
445// loglevels from the config file and it does not load files like TLS keys/certs.
446func mustLoadConfig() {
447 mox.MustLoadConfig(false, false)
452 if level, ok := mlog.Levels[ll]; ok {
453 mox.Conf.Log[""] = level
454 mlog.SetConfig(mox.Conf.Log)
456 log.Fatal("unknown loglevel", slog.String("loglevel", loglevel))
459 mox.SetPedantic(true)
464 // CheckConsistencyOnClose is true by default, for all the test packages. A regular
465 // mox server should never use it. But integration tests enable it again with a
467 store.CheckConsistencyOnClose = false
468 store.MsgFilesPerDirShiftSet(13) // For 1<<13 = 8k message files per directory.
470 ctxbg := context.Background()
476 // If invoked as sendmail, e.g. /usr/sbin/sendmail, we do enough so cron can get a
477 // message sent using smtp submission to a configured server.
478 if len(os.Args) > 0 && filepath.Base(os.Args[0]) == "sendmail" {
480 flag: flag.NewFlagSet("sendmail", flag.ExitOnError),
481 flagArgs: os.Args[1:],
482 log: mlog.New("sendmail", nil),
488 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")
489 flag.StringVar(&loglevel, "loglevel", "", "if non-empty, this log level is set early in startup")
490 flag.BoolVar(&pedantic, "pedantic", false, "protocol violations result in errors instead of accepting/working around them")
491 flag.BoolVar(&store.CheckConsistencyOnClose, "checkconsistency", false, "dangerous option for testing only, enables data checks that abort/panic when inconsistencies are found")
493 var cpuprofile, memprofile, tracefile string
494 flag.StringVar(&cpuprofile, "cpuprof", "", "store cpu profile to file")
495 flag.StringVar(&memprofile, "memprof", "", "store mem profile to file")
496 flag.StringVar(&tracefile, "trace", "", "store execution trace to file")
498 flag.Usage = func() { usage(cmds, false) }
506 defer traceExecution(tracefile)()
508 defer profile(cpuprofile, memprofile)()
511 mox.SetPedantic(true)
514 mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
519 if level, ok := mlog.Levels[ll]; ok {
520 mox.Conf.Log[""] = level
521 mlog.SetConfig(mox.Conf.Log)
522 // note: SetConfig may be called again when subcommands loads config.
524 log.Fatalf("unknown loglevel %q", loglevel)
529 for _, c := range cmds {
530 for i, w := range c.words {
531 if i >= len(args) || w != args[i] {
533 partial = append(partial, c)
538 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
539 c.flagArgs = args[len(c.words):]
540 c.log = mlog.New(strings.Join(c.words, ""), nil)
544 if len(partial) > 0 {
550func xcheckf(err error, format string, args ...any) {
554 msg := fmt.Sprintf(format, args...)
555 log.Fatalf("%s: %s", msg, err)
558func xparseIP(s, what string) net.IP {
561 log.Fatalf("invalid %s: %q", what, s)
566func xparseDomain(s, what string) dns.Domain {
567 d, err := dns.ParseDomain(s)
568 xcheckf(err, "parsing %s %q", what, s)
572func cmdClientConfig(c *cmd) {
574 c.help = `Print the configuration for email clients for a domain.
576Sending email is typically not done on the SMTP port 25, but on submission
577ports 465 (with TLS) and 587 (without initial TLS, but usually added to the
578connection with STARTTLS). For IMAP, the port with TLS is 993 and without is
581Without TLS/STARTTLS, passwords are sent in clear text, which should only be
582configured over otherwise secured connections, like a VPN.
588 d := xparseDomain(args[0], "domain")
593func printClientConfig(d dns.Domain) {
594 cc, err := admin.ClientConfigsDomain(d)
595 xcheckf(err, "getting client config")
596 fmt.Printf("%-20s %-30s %5s %-15s %s\n", "Protocol", "Host", "Port", "Listener", "Note")
597 for _, e := range cc.Entries {
598 fmt.Printf("%-20s %-30s %5d %-15s %s\n", e.Protocol, e.Host, e.Port, e.Listener, e.Note)
601To prevent authentication mechanism downgrade attempts that may result in
602clients sending plain text passwords to a MitM, clients should always be
603explicitly configured with the most secure authentication mechanism supported,
604the first of: SCRAM-SHA-256-PLUS, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, SCRAM-SHA-1,
609func cmdConfigTest(c *cmd) {
610 c.help = `Parses and validates the configuration files.
612If valid, the command exits with status 0. If not valid, all errors encountered
620 mox.FilesImmediate = true
622 _, errs := mox.ParseConfig(context.Background(), c.log, mox.ConfigStaticPath, true, true, false)
624 log.Printf("multiple errors:")
625 for _, err := range errs {
626 log.Printf("%s", err)
629 } else if len(errs) == 1 {
630 log.Fatalf("%s", errs[0])
633 fmt.Println("config OK")
636func cmdConfigDescribeStatic(c *cmd) {
637 c.params = ">mox.conf"
638 c.help = `Prints an annotated empty configuration for use as mox.conf.
640The static configuration file cannot be reloaded while mox is running. Mox has
641to be restarted for changes to the static configuration file to take effect.
643This configuration file needs modifications to make it valid. For example, it
644may contain unfinished list items.
646 if len(c.Parse()) != 0 {
651 err := sconf.Describe(os.Stdout, &sc)
652 xcheckf(err, "describing config")
655func cmdConfigDescribeDomains(c *cmd) {
656 c.params = ">domains.conf"
657 c.help = `Prints an annotated empty configuration for use as domains.conf.
659The domains configuration file contains the domains and their configuration,
660and accounts and their configuration. This includes the configured email
661addresses. The mox admin web interface, and the mox command line interface, can
662make changes to this file. Mox automatically reloads this file when it changes.
664Like the static configuration, the example domains.conf printed by this command
665needs modifications to make it valid.
667 if len(c.Parse()) != 0 {
671 var dc config.Dynamic
672 err := sconf.Describe(os.Stdout, &dc)
673 xcheckf(err, "describing config")
676func cmdConfigPrintservice(c *cmd) {
677 c.params = ">mox.service"
678 c.help = `Prints a systemd unit service file for mox.
680This is the same file as generated using quickstart. If the systemd service file
681has changed with a newer version of mox, use this command to generate an up to
684 if len(c.Parse()) != 0 {
688 pwd, err := os.Getwd()
690 log.Printf("current working directory: %v", err)
693 service := strings.ReplaceAll(moxService, "/home/mox", pwd)
697func cmdConfigDomainAdd(c *cmd) {
698 c.params = "[-disabled] $domain $account [$localpart]"
699 c.help = `Adds a new domain to the configuration and reloads the configuration.
701The account is used for the postmaster mailboxes the domain, including as DMARC and
702TLS reporting. Localpart is the "username" at the domain for this account. If
703must be set if and only if account does not yet exist.
705The domain can be created in disabled mode, preventing automatically requesting
706TLS certificates with ACME, and rejecting incoming/outgoing messages involving
707the domain, but allowing further configuration of the domain.
710 c.flag.BoolVar(&disabled, "disabled", false, "disable the new domain")
712 if len(args) != 2 && len(args) != 3 {
716 d := xparseDomain(args[0], "domain")
718 var localpart smtp.Localpart
721 localpart, err = smtp.ParseLocalpart(args[2])
722 xcheckf(err, "parsing localpart")
724 ctlcmdConfigDomainAdd(xctl(), disabled, d, args[1], localpart)
727func ctlcmdConfigDomainAdd(ctl *ctl, disabled bool, domain dns.Domain, account string, localpart smtp.Localpart) {
728 ctl.xwrite("domainadd")
734 ctl.xwrite(domain.Name())
736 ctl.xwrite(string(localpart))
738 fmt.Printf("domain added, remember to add dns records, see:\n\nmox config dnsrecords %s\nmox config dnscheck %s\n", domain.Name(), domain.Name())
741func cmdConfigDomainRemove(c *cmd) {
743 c.help = `Remove a domain from the configuration and reload the configuration.
745This is a dangerous operation. Incoming email delivery for this domain will be
753 d := xparseDomain(args[0], "domain")
755 ctlcmdConfigDomainRemove(xctl(), d)
758func ctlcmdConfigDomainRemove(ctl *ctl, d dns.Domain) {
759 ctl.xwrite("domainrm")
762 fmt.Printf("domain removed, remember to remove dns records for %s\n", d)
765func cmdConfigDomainDisable(c *cmd) {
767 c.help = `Disable a domain and reload the configuration.
769This is a dangerous operation. Incoming/outgoing messages involving this domain
777 d := xparseDomain(args[0], "domain")
779 ctlcmdConfigDomainDisabled(xctl(), d, true)
780 fmt.Printf("domain disabled")
783func cmdConfigDomainEnable(c *cmd) {
785 c.help = `Enable a domain and reload the configuration.
787Incoming/outgoing messages involving this domain will be accepted again.
794 d := xparseDomain(args[0], "domain")
796 ctlcmdConfigDomainDisabled(xctl(), d, false)
799func ctlcmdConfigDomainDisabled(ctl *ctl, d dns.Domain, disabled bool) {
800 ctl.xwrite("domaindisabled")
810func cmdConfigAliasList(c *cmd) {
812 c.help = `Show aliases (lists) for domain.`
819 ctlcmdConfigAliasList(xctl(), args[0])
822func ctlcmdConfigAliasList(ctl *ctl, address string) {
823 ctl.xwrite("aliaslist")
826 ctl.xstreamto(os.Stdout)
829func cmdConfigAliasPrint(c *cmd) {
831 c.help = `Print settings and members of alias (list).`
838 ctlcmdConfigAliasPrint(xctl(), args[0])
841func ctlcmdConfigAliasPrint(ctl *ctl, address string) {
842 ctl.xwrite("aliasprint")
845 ctl.xstreamto(os.Stdout)
848func cmdConfigAliasAdd(c *cmd) {
849 c.params = "$alias@domain $rcpt1@domain ..."
850 c.help = `Add new alias (list) with one or more addresses and public posting enabled.
852An alias is used for delivering incoming email to multiple recipients. If you
853want to add an address to an account, don't use an alias, just add the address
861 alias := config.Alias{PostPublic: true, Addresses: args[1:]}
864 ctlcmdConfigAliasAdd(xctl(), args[0], alias)
867func ctlcmdConfigAliasAdd(ctl *ctl, address string, alias config.Alias) {
868 ctl.xwrite("aliasadd")
870 xctlwriteJSON(ctl, alias)
874func cmdConfigAliasUpdate(c *cmd) {
875 c.params = "$alias@domain [-postpublic false|true -listmembers false|true -allowmsgfrom false|true]"
876 c.help = `Update alias (list) configuration.`
877 var postpublic, listmembers, allowmsgfrom string
878 c.flag.StringVar(&postpublic, "postpublic", "", "whether anyone or only list members can post")
879 c.flag.StringVar(&listmembers, "listmembers", "", "whether list members can list members")
880 c.flag.StringVar(&allowmsgfrom, "allowmsgfrom", "", "whether alias address can be used in message from header")
888 ctlcmdConfigAliasUpdate(xctl(), alias, postpublic, listmembers, allowmsgfrom)
891func ctlcmdConfigAliasUpdate(ctl *ctl, alias, postpublic, listmembers, allowmsgfrom string) {
892 ctl.xwrite("aliasupdate")
894 ctl.xwrite(postpublic)
895 ctl.xwrite(listmembers)
896 ctl.xwrite(allowmsgfrom)
900func cmdConfigAliasRemove(c *cmd) {
901 c.params = "$alias@domain"
902 c.help = "Remove alias (list)."
909 ctlcmdConfigAliasRemove(xctl(), args[0])
912func ctlcmdConfigAliasRemove(ctl *ctl, alias string) {
913 ctl.xwrite("aliasrm")
918func cmdConfigAliasAddaddr(c *cmd) {
919 c.params = "$alias@domain $rcpt1@domain ..."
920 c.help = `Add addresses to alias (list).`
927 ctlcmdConfigAliasAddaddr(xctl(), args[0], args[1:])
930func ctlcmdConfigAliasAddaddr(ctl *ctl, alias string, addresses []string) {
931 ctl.xwrite("aliasaddaddr")
933 xctlwriteJSON(ctl, addresses)
937func cmdConfigAliasRemoveaddr(c *cmd) {
938 c.params = "$alias@domain $rcpt1@domain ..."
939 c.help = `Remove addresses from alias (list).`
946 ctlcmdConfigAliasRmaddr(xctl(), args[0], args[1:])
949func ctlcmdConfigAliasRmaddr(ctl *ctl, alias string, addresses []string) {
950 ctl.xwrite("aliasrmaddr")
952 xctlwriteJSON(ctl, addresses)
956func cmdConfigAccountAdd(c *cmd) {
957 c.params = "$account $address"
958 c.help = `Add an account with an email address and reload the configuration.
960Email can be delivered to this address/account. A password has to be configured
961explicitly, see the setaccountpassword command.
969 ctlcmdConfigAccountAdd(xctl(), args[0], args[1])
972func ctlcmdConfigAccountAdd(ctl *ctl, account, address string) {
973 ctl.xwrite("accountadd")
977 fmt.Printf("account added, set a password with \"mox setaccountpassword %s\"\n", account)
980func cmdConfigAccountRemove(c *cmd) {
981 c.params = "$account"
982 c.help = `Remove an account and reload the configuration.
984Email addresses for this account will also be removed, and incoming email for
985these addresses will be rejected.
987All data for the account will be removed.
995 ctlcmdConfigAccountRemove(xctl(), args[0])
998func ctlcmdConfigAccountRemove(ctl *ctl, account string) {
999 ctl.xwrite("accountrm")
1002 fmt.Println("account removed")
1005func cmdConfigAccountList(c *cmd) {
1006 c.help = `List all accounts.
1008Each account is printed on a line, with optional additional tab-separated
1009information, such as "(disabled)".
1017 ctlcmdConfigAccountList(xctl())
1020func ctlcmdConfigAccountList(ctl *ctl) {
1021 ctl.xwrite("accountlist")
1023 ctl.xstreamto(os.Stdout)
1026func cmdConfigAccountAddresses(c *cmd) {
1027 c.help = `List all addresses for an account.
1029Each address is printed on a line.
1030An address starting with an "@" indicate it is a catchall address for the domain.
1032Does not check whether account is disabled.
1040 ctlcmdConfigAccountAddresses(xctl(), args[0])
1043func ctlcmdConfigAccountAddresses(ctl *ctl, account string) {
1044 ctl.xwrite("accountaddresses")
1047 ctl.xstreamto(os.Stdout)
1050func cmdConfigAccountDisable(c *cmd) {
1051 c.params = "$account $message"
1052 c.help = `Disable login for an account, showing message to users when they try to login.
1054Incoming email will still be accepted for the account, and queued email from the
1055account will still be delivered. No new login sessions are possible.
1057Message must be non-empty, ascii-only without control characters including
1058newline, and maximum 256 characters because it is used in SMTP/IMAP.
1065 log.Fatalf("message must be non-empty")
1069 ctlcmdConfigAccountDisabled(xctl(), args[0], args[1])
1070 fmt.Println("account disabled")
1073func cmdConfigAccountEnable(c *cmd) {
1074 c.params = "$account"
1075 c.help = `Enable login again for an account.
1077Login attempts by the user no long result in an error message.
1085 ctlcmdConfigAccountDisabled(xctl(), args[0], "")
1086 fmt.Println("account enabled")
1089func ctlcmdConfigAccountDisabled(ctl *ctl, account, loginDisabled string) {
1090 ctl.xwrite("accountdisabled")
1092 ctl.xwrite(loginDisabled)
1096func cmdConfigTlspubkeyList(c *cmd) {
1097 c.params = "[$account]"
1098 c.help = `List TLS public keys for TLS client certificate authentication.
1100If account is absent, the TLS public keys for all accounts are listed.
1103 var accountOpt string
1105 accountOpt = args[0]
1106 } else if len(args) > 1 {
1111 ctlcmdConfigTlspubkeyList(xctl(), accountOpt)
1114func ctlcmdConfigTlspubkeyList(ctl *ctl, accountOpt string) {
1115 ctl.xwrite("tlspubkeylist")
1116 ctl.xwrite(accountOpt)
1118 ctl.xstreamto(os.Stdout)
1121func cmdConfigTlspubkeyGet(c *cmd) {
1122 c.params = "$fingerprint"
1123 c.help = `Get a TLS public key for a fingerprint.
1125Prints the type, name, account and address for the key, and the certificate in
1134 ctlcmdConfigTlspubkeyGet(xctl(), args[0])
1137func ctlcmdConfigTlspubkeyGet(ctl *ctl, fingerprint string) {
1138 ctl.xwrite("tlspubkeyget")
1139 ctl.xwrite(fingerprint)
1143 account := ctl.xread()
1144 address := ctl.xread()
1145 noimappreauth := ctl.xread()
1149 var block *pem.Block
1152 Type: "CERTIFICATE",
1157 fmt.Printf("type: %s\nname: %s\naccount: %s\naddress: %s\nno imap preauth: %s\n", typ, name, account, address, noimappreauth)
1159 fmt.Printf("certificate:\n\n")
1160 if err := pem.Encode(os.Stdout, block); err != nil {
1161 log.Fatalf("pem encode: %v", err)
1166func cmdConfigTlspubkeyAdd(c *cmd) {
1167 c.params = "$address [$name] < cert.pem"
1168 c.help = `Add a TLS public key to the account of the given address.
1170The public key is read from the certificate.
1172The optional name is a human-readable descriptive name of the key. If absent,
1173the CommonName from the certificate is used.
1175 var noimappreauth bool
1176 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.")
1178 var address, name string
1181 } else if len(args) == 2 {
1182 address, name = args[0], args[1]
1187 buf, err := io.ReadAll(os.Stdin)
1188 xcheckf(err, "reading from stdin")
1189 block, _ := pem.Decode(buf)
1191 err = errors.New("no pem block found")
1192 } else if block.Type != "CERTIFICATE" {
1193 err = fmt.Errorf("unexpected type %q, expected CERTIFICATE", block.Type)
1195 xcheckf(err, "parsing pem")
1198 ctlcmdConfigTlspubkeyAdd(xctl(), address, name, noimappreauth, block.Bytes)
1201func ctlcmdConfigTlspubkeyAdd(ctl *ctl, address, name string, noimappreauth bool, certDER []byte) {
1202 ctl.xwrite("tlspubkeyadd")
1205 ctl.xwrite(fmt.Sprintf("%v", noimappreauth))
1206 ctl.xstreamfrom(bytes.NewReader(certDER))
1210func cmdConfigTlspubkeyRemove(c *cmd) {
1211 c.params = "$fingerprint"
1212 c.help = `Remove TLS public key for fingerprint.`
1219 ctlcmdConfigTlspubkeyRemove(xctl(), args[0])
1222func ctlcmdConfigTlspubkeyRemove(ctl *ctl, fingerprint string) {
1223 ctl.xwrite("tlspubkeyrm")
1224 ctl.xwrite(fingerprint)
1228func cmdConfigTlspubkeyGen(c *cmd) {
1230 c.help = `Generate an ed25519 private key and minimal certificate for use a TLS public key and write to files starting with stem.
1232The private key is written to $stem.$timestamp.ed25519privatekey.pkcs8.pem.
1233The certificate is written to $stem.$timestamp.certificate.pem.
1234The private key and certificate are also written to
1235$stem.$timestamp.ed25519privatekey-certificate.pem.
1237The certificate can be added to an account with "mox config account tlspubkey add".
1239The combined file can be used with "mox sendmail".
1241The private key is also written to standard error in raw-url-base64-encoded
1242form, also for use with "mox sendmail". The fingerprint is written to standard
1243error too, for reference.
1251 timestamp := time.Now().Format("200601021504")
1252 prefix := stem + "." + timestamp
1254 seed := make([]byte, ed25519.SeedSize)
1255 cryptorand.Read(seed)
1256 privKey := ed25519.NewKeyFromSeed(seed)
1257 privKeyBuf, err := x509.MarshalPKCS8PrivateKey(privKey)
1258 xcheckf(err, "marshal private key as pkcs8")
1260 err = pem.Encode(&b, &pem.Block{Type: "PRIVATE KEY", Bytes: privKeyBuf})
1261 xcheckf(err, "marshal pkcs8 private key to pem")
1262 privKeyBufPEM := b.Bytes()
1264 certBuf, tlsCert := xminimalCert(privKey)
1266 err = pem.Encode(&b, &pem.Block{Type: "CERTIFICATE", Bytes: certBuf})
1267 xcheckf(err, "marshal certificate to pem")
1268 certBufPEM := b.Bytes()
1270 xwriteFile := func(p string, data []byte, what string) {
1271 log.Printf("writing %s", p)
1272 err = os.WriteFile(p, data, 0600)
1273 xcheckf(err, "writing %s file: %v", what, err)
1276 xwriteFile(prefix+".ed25519privatekey.pkcs8.pem", privKeyBufPEM, "private key")
1277 xwriteFile(prefix+".certificate.pem", certBufPEM, "certificate")
1278 combinedPEM := slices.Concat(privKeyBufPEM, certBufPEM)
1279 xwriteFile(prefix+".ed25519privatekey-certificate.pem", combinedPEM, "combined private key and certificate")
1281 shabuf := sha256.Sum256(tlsCert.Leaf.RawSubjectPublicKeyInfo)
1283 _, err = fmt.Fprintf(os.Stderr, "ed25519 private key as raw-url-base64: %s\ned25519 public key fingerprint: %s\n",
1284 base64.RawURLEncoding.EncodeToString(seed),
1285 base64.RawURLEncoding.EncodeToString(shabuf[:]),
1287 xcheckf(err, "write private key and public key fingerprint")
1290func cmdConfigAddressAdd(c *cmd) {
1291 c.params = "$address $account"
1292 c.help = `Adds an address to an account and reloads the configuration.
1294If address starts with a @ (i.e. a missing localpart), this is a catchall
1295address for the domain.
1303 ctlcmdConfigAddressAdd(xctl(), args[0], args[1])
1306func ctlcmdConfigAddressAdd(ctl *ctl, address, account string) {
1307 ctl.xwrite("addressadd")
1311 fmt.Println("address added")
1314func cmdConfigAddressRemove(c *cmd) {
1315 c.params = "$address"
1316 c.help = `Remove an address and reload the configuration.
1318Incoming email for this address will be rejected after removing an address.
1326 ctlcmdConfigAddressRemove(xctl(), args[0])
1329func ctlcmdConfigAddressRemove(ctl *ctl, address string) {
1330 ctl.xwrite("addressrm")
1333 fmt.Println("address removed")
1336func cmdConfigAddressAccount(c *cmd) {
1337 c.params = "$address"
1338 c.help = `Print the account an address belongs to.
1340Catchall addresses and the account catch all separator are considered when
1341looking up the account.
1343Does not check whether account is disabled.
1351 ctlcmdConfigAddressAccount(xctl(), args[0])
1354func ctlcmdConfigAddressAccount(ctl *ctl, address string) {
1355 ctl.xwrite("addressaccount")
1358 account := ctl.xread()
1359 fmt.Println(account)
1362func cmdConfigDNSRecords(c *cmd) {
1363 c.params = "$domain"
1364 c.help = `Prints annotated DNS records as zone file that should be created for the domain.
1366The zone file can be imported into existing DNS software. You should review the
1367DNS records, especially if your domain previously/currently has email
1375 d := xparseDomain(args[0], "domain")
1377 domConf, ok := mox.Conf.Domain(d)
1379 log.Fatalf("unknown domain")
1382 resolver := dns.StrictResolver{Pkg: "main"}
1383 _, result, err := resolver.LookupTXT(context.Background(), d.ASCII+".")
1384 if !dns.IsNotFound(err) {
1385 xcheckf(err, "looking up record for dnssec-status")
1388 var certIssuerDomainName, acmeAccountURI string
1389 public := mox.Conf.Static.Listeners["public"]
1390 if public.TLS != nil && public.TLS.ACME != "" {
1391 acme, ok := mox.Conf.Static.ACME[public.TLS.ACME]
1392 if ok && acme.Manager.Manager.Client != nil {
1393 certIssuerDomainName = acme.IssuerDomainName
1394 acc, err := acme.Manager.Manager.Client.GetReg(context.Background(), "")
1395 c.log.Check(err, "get public acme account")
1397 acmeAccountURI = acc.URI
1402 records, err := admin.DomainRecords(domConf, d, result.Authentic, certIssuerDomainName, acmeAccountURI)
1403 xcheckf(err, "records")
1404 fmt.Print(strings.Join(records, "\n") + "\n")
1407func cmdConfigDNSCheck(c *cmd) {
1408 c.params = "$domain"
1409 c.help = "Check the DNS records with the configuration for the domain, and print any errors/warnings."
1415 d := xparseDomain(args[0], "domain")
1417 _, ok := mox.Conf.Domain(d)
1419 log.Fatalf("unknown domain")
1422 // todo future: move http.Admin.CheckDomain to mox- and make it return a regular error.
1428 err, ok := x.(*sherpa.Error)
1432 log.Fatalf("%s", err)
1435 printResult := func(name string, r webadmin.Result) {
1436 if len(r.Errors) == 0 && len(r.Warnings) == 0 {
1439 fmt.Printf("# %s\n", name)
1440 for _, s := range r.Errors {
1441 fmt.Printf("error: %s\n", s)
1443 for _, s := range r.Warnings {
1444 fmt.Printf("warning: %s\n", s)
1448 result := webadmin.Admin{}.CheckDomain(context.Background(), args[0])
1449 printResult("DNSSEC", result.DNSSEC.Result)
1450 printResult("IPRev", result.IPRev.Result)
1451 printResult("MX", result.MX.Result)
1452 printResult("TLS", result.TLS.Result)
1453 printResult("DANE", result.DANE.Result)
1454 printResult("SPF", result.SPF.Result)
1455 printResult("DKIM", result.DKIM.Result)
1456 printResult("DMARC", result.DMARC.Result)
1457 printResult("Host TLSRPT", result.HostTLSRPT.Result)
1458 printResult("Domain TLSRPT", result.DomainTLSRPT.Result)
1459 printResult("MTASTS", result.MTASTS.Result)
1460 printResult("SRV conf", result.SRVConf.Result)
1461 printResult("Autoconf", result.Autoconf.Result)
1462 printResult("Autodiscover", result.Autodiscover.Result)
1465func cmdConfigEnsureACMEHostprivatekeys(c *cmd) {
1467 c.help = `Ensure host private keys exist for TLS listeners with ACME.
1469In mox.conf, each listener can have TLS configured. Long-lived private key files
1470can be specified, which will be used when requesting ACME certificates.
1471Configuring these private keys makes it feasible to publish DANE TLSA records
1472for the corresponding public keys in DNS, protected with DNSSEC, allowing TLS
1473certificate verification without depending on a list of Certificate Authorities
1474(CAs). Previous versions of mox did not pre-generate private keys for use with
1475ACME certificates, but would generate private keys on-demand. By explicitly
1476configuring private keys, they will not change automatedly with new
1477certificates, and the DNS TLSA records stay valid.
1479This command looks for listeners in mox.conf with TLS with ACME configured. For
1480each missing host private key (of type rsa-2048 and ecdsa-p256) a key is written
1481to config/hostkeys/. If a certificate exists in the ACME "cache", its private
1482key is copied. Otherwise a new private key is generated. Snippets for manually
1483updating/editing mox.conf are printed.
1485After running this command, and updating mox.conf, run "mox config dnsrecords"
1486for a domain and create the TLSA DNS records it suggests to enable DANE.
1493 // Load a private key from p, in various forms. We only look at the first PEM
1494 // block. Files with only a private key, or with multiple blocks but private key
1495 // first like autocert does, can be loaded.
1496 loadPrivateKey := func(f *os.File) (any, error) {
1497 buf, err := io.ReadAll(f)
1499 return nil, fmt.Errorf("reading private key file: %v", err)
1501 block, _ := pem.Decode(buf)
1503 return nil, fmt.Errorf("no pem block found in pem file")
1507 case "EC PRIVATE KEY":
1508 privKey, err = x509.ParseECPrivateKey(block.Bytes)
1509 case "RSA PRIVATE KEY":
1510 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
1512 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
1514 return nil, fmt.Errorf("unrecognized pem block type %q", block.Type)
1517 return nil, fmt.Errorf("parsing private key of type %q: %v", block.Type, err)
1522 // Either load a private key from file, or if it doesn't exist generate a new
1524 xtryLoadPrivateKey := func(kt autocert.KeyType, p string) any {
1525 f, err := os.Open(p)
1526 if err != nil && errors.Is(err, fs.ErrNotExist) {
1528 case autocert.KeyRSA2048:
1529 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1530 xcheckf(err, "generating new 2048-bit rsa private key")
1532 case autocert.KeyECDSAP256:
1533 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1534 xcheckf(err, "generating new ecdsa p-256 private key")
1537 log.Fatalf("unexpected keytype %v", kt)
1540 xcheckf(err, "%s: open acme key and certificate file", p)
1542 // Load private key from file. autocert stores a PEM file that starts with a
1543 // private key, followed by certificate(s). So we can just read it and should find
1544 // the private key we are looking for.
1545 privKey, err := loadPrivateKey(f)
1546 if xerr := f.Close(); xerr != nil {
1547 log.Printf("closing private key file: %v", xerr)
1549 xcheckf(err, "parsing private key from acme key and certificate file")
1551 switch k := privKey.(type) {
1552 case *rsa.PrivateKey:
1553 if k.N.BitLen() == 2048 {
1556 log.Printf("warning: rsa private key in %s has %d bits, skipping and generating new 2048-bit rsa private key", p, k.N.BitLen())
1557 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1558 xcheckf(err, "generating new 2048-bit rsa private key")
1560 case *ecdsa.PrivateKey:
1561 if k.Curve == elliptic.P256() {
1564 log.Printf("warning: ecdsa private key in %s has curve %v, skipping and generating new p-256 ecdsa key", p, k.Curve.Params().Name)
1565 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1566 xcheckf(err, "generating new ecdsa p-256 private key")
1569 log.Fatalf("%s: unexpected private key file of type %T", p, privKey)
1574 // Write privKey as PKCS#8 private key to p. Only if file does not yet exist.
1575 writeHostPrivateKey := func(privKey any, p string) error {
1576 os.MkdirAll(filepath.Dir(p), 0700)
1577 f, err := os.OpenFile(p, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
1579 return fmt.Errorf("create: %v", err)
1583 if err := f.Close(); err != nil {
1584 log.Printf("closing new hostkey file %s after error: %v", p, err)
1586 if err := os.Remove(p); err != nil {
1587 log.Printf("removing new hostkey file %s after error: %v", p, err)
1591 buf, err := x509.MarshalPKCS8PrivateKey(privKey)
1593 return fmt.Errorf("marshal private host key: %v", err)
1596 Type: "PRIVATE KEY",
1599 if err := pem.Encode(f, &block); err != nil {
1600 return fmt.Errorf("write as pem: %v", err)
1602 if err := f.Close(); err != nil {
1603 return fmt.Errorf("close: %v", err)
1610 timestamp := time.Now().Format("20060102T150405")
1612 for listenerName, l := range mox.Conf.Static.Listeners {
1613 if l.TLS == nil || l.TLS.ACME == "" {
1616 haveKeyTypes := map[autocert.KeyType]bool{}
1617 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
1618 p := mox.ConfigDirPath(privKeyFile)
1619 f, err := os.Open(p)
1620 xcheckf(err, "open host private key")
1621 privKey, err := loadPrivateKey(f)
1622 if err := f.Close(); err != nil {
1623 log.Printf("closing host private key file: %v", err)
1625 xcheckf(err, "loading host private key")
1626 switch k := privKey.(type) {
1627 case *rsa.PrivateKey:
1628 if k.N.BitLen() == 2048 {
1629 haveKeyTypes[autocert.KeyRSA2048] = true
1631 case *ecdsa.PrivateKey:
1632 if k.Curve == elliptic.P256() {
1633 haveKeyTypes[autocert.KeyECDSAP256] = true
1637 created := []string{}
1638 for _, kt := range []autocert.KeyType{autocert.KeyRSA2048, autocert.KeyECDSAP256} {
1639 if haveKeyTypes[kt] {
1642 // Lookup key in ACME cache.
1643 host := l.HostnameDomain
1644 if host.ASCII == "" {
1645 host = mox.Conf.Static.HostnameDomain
1647 filename := host.ASCII
1649 if kt == autocert.KeyRSA2048 {
1653 p := mox.DataDirPath(filepath.Join("acme", "keycerts", l.TLS.ACME, filename))
1654 privKey := xtryLoadPrivateKey(kt, p)
1656 relPath := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", host.Name(), timestamp, kind))
1657 destPath := mox.ConfigDirPath(relPath)
1658 err := writeHostPrivateKey(privKey, destPath)
1659 xcheckf(err, "writing host private key file to %s: %v", destPath, err)
1660 created = append(created, relPath)
1661 fmt.Printf("Wrote host private key: %s\n", destPath)
1663 didCreate = didCreate || len(created) > 0
1664 if len(created) > 0 {
1666 HostPrivateKeyFiles: append(l.TLS.HostPrivateKeyFiles, created...),
1668 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)
1669 err := sconf.Write(os.Stdout, tls)
1670 xcheckf(err, "writing new TLS.HostPrivateKeyFiles section")
1676After updating mox.conf and restarting, run "mox config dnsrecords" for a
1677domain and create the TLSA DNS records it suggests to enable DANE.
1682func cmdLoglevels(c *cmd) {
1683 c.params = "[$level [$pkg]]"
1684 c.help = `Print the log levels, or set a new default log level, or a level for the given package.
1686By default, a single log level applies to all logging in mox. But for each
1687"pkg", an overriding log level can be configured. Examples of packages:
1688smtpserver, smtpclient, queue, imapserver, spf, dkim, dmarc, junk, message,
1691Specify a pkg and an empty level to clear the configured level for a package.
1693Valid labels: error, info, debug, trace, traceauth, tracedata.
1702 ctlcmdLoglevels(xctl())
1708 ctlcmdSetLoglevels(xctl(), pkg, args[0])
1712func ctlcmdLoglevels(ctl *ctl) {
1713 ctl.xwrite("loglevels")
1715 ctl.xstreamto(os.Stdout)
1718func ctlcmdSetLoglevels(ctl *ctl, pkg, level string) {
1719 ctl.xwrite("setloglevels")
1725func cmdStop(c *cmd) {
1726 c.help = `Shut mox down, giving connections maximum 3 seconds to stop before closing them.
1728While shutting down, new IMAP and SMTP connections will get a status response
1729indicating temporary unavailability. Existing connections will get a 3 second
1730period to finish their transaction and shut down. Under normal circumstances,
1731only IMAP has long-living connections, with the IDLE command to get notified of
1734 if len(c.Parse()) != 0 {
1741 // Read will hang until remote has shut down.
1742 buf := make([]byte, 128)
1743 n, err := xctl.conn.Read(buf)
1745 log.Fatalf("expected eof after graceful shutdown, got data %q", buf[:n])
1746 } else if err != io.EOF {
1747 log.Fatalf("expected eof after graceful shutdown, got error %v", err)
1749 fmt.Println("mox stopped")
1752func cmdBackup(c *cmd) {
1753 c.params = "$destdir"
1754 c.help = `Creates a backup of the config and data directory.
1756Backup copies the config directory to <destdir>/config, and creates
1757<destdir>/data with a consistent snapshot of the databases and message files
1758and copies other files from the data directory. Empty directories are not
1759copied. The backup can then be stored elsewhere for long-term storage, or used
1760to fall back to should an upgrade fail. Simply copying files in the data
1761directory while mox is running can result in unusable database files.
1763Message files never change (they are read-only, though can be removed) and are
1764hard-linked so they don't consume additional space. If hardlinking fails, for
1765example when the backup destination directory is on a different file system, a
1766regular copy is made. Using a destination directory like "data/tmp/backup"
1767increases the odds hardlinking succeeds: the default systemd service file
1768specifically mounts the data directory, causing attempts to hardlink outside it
1769to fail with an error about cross-device linking.
1771All files in the data directory that aren't recognized (i.e. other than known
1772database files, message files, an acme directory, the "tmp" directory, etc),
1773are stored, but with a warning.
1775Remove files in the destination directory before doing another backup. The
1776backup command will not overwrite files, but print and return errors.
1778Exit code 0 indicates the backup was successful. A clean successful backup does
1779not print any output, but may print warnings. Use the -verbose flag for
1780details, including timing.
1782To restore a backup, first shut down mox, move away the old data directory and
1783move an earlier backed up directory in its place, run "mox verifydata
1784<datadir>", possibly with the "-fix" option, and restart mox. After the
1785restore, you may also want to run "mox bumpuidvalidity" for each account for
1786which messages in a mailbox changed, to force IMAP clients to synchronize
1789Before upgrading, to check if the upgrade will likely succeed, first make a
1790backup, then use the new mox binary to run "mox verifydata <backupdir>/data".
1791This can change the backup files (e.g. upgrade database files, move away
1792unrecognized message files), so you should make a new backup before actually
1797 c.flag.BoolVar(&verbose, "verbose", false, "print progress")
1804 dstDataDir, err := filepath.Abs(args[0])
1805 xcheckf(err, "making path absolute")
1807 ctlcmdBackup(xctl(), dstDataDir, verbose)
1810func ctlcmdBackup(ctl *ctl, dstDataDir string, verbose bool) {
1811 ctl.xwrite("backup")
1812 ctl.xwrite(dstDataDir)
1814 ctl.xwrite("verbose")
1818 ctl.xstreamto(os.Stdout)
1822func cmdSetadminpassword(c *cmd) {
1823 c.help = `Set a new admin password, for the web interface.
1825The password is read from stdin. Its bcrypt hash is stored in a file named
1826"adminpasswd" in the configuration directory.
1828 if len(c.Parse()) != 0 {
1833 path := mox.ConfigDirPath(mox.Conf.Static.AdminPasswordFile)
1835 log.Fatal("no admin password file configured")
1838 pw := xreadpassword()
1839 pw, err := precis.OpaqueString.String(pw)
1840 xcheckf(err, `checking password with "precis" requirements`)
1841 hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
1842 xcheckf(err, "generating hash for password")
1843 err = os.WriteFile(path, hash, 0660)
1844 xcheckf(err, "writing hash to admin password file")
1847func xreadpassword() string {
1849Type new password. Password WILL echo.
1851WARNING: Bots will try to bruteforce your password. Connections with failed
1852authentication attempts will be rate limited but attackers WILL find passwords
1853reused at other services and weak passwords. If your account is compromised,
1854spammers are likely to abuse your system, spamming your address and the wider
1855internet in your name. So please pick a random, unguessable password, preferably
1856at least 12 characters.
1859 fmt.Printf("password: ")
1860 scanner := bufio.NewScanner(os.Stdin)
1861 // The default splitter for scanners is one that splits by lines, so we
1862 // don't have to set up another one here.
1864 // We discard the return value of Scan() since failing to tokenize could
1865 // either mean reaching EOF but no newline (which can be legitimate if the
1866 // CLI was programatically called to set the password, but with no trailing
1867 // newline), or an actual error. We can distinguish between the two by
1868 // calling Err() since it will return nil if it were EOF, but the actual
1871 xcheckf(scanner.Err(), "reading stdin")
1872 // No need to trim, the scanner does not return the token in the output.
1873 pw := scanner.Text()
1875 log.Fatal("password must be at least 8 characters")
1880func cmdSetaccountpassword(c *cmd) {
1881 c.params = "$account"
1882 c.help = `Set new password an account.
1884The password is read from stdin. Secrets derived from the password, but not the
1885password itself, are stored in the account database. The stored secrets are for
1886authentication with: scram-sha-256, scram-sha-1, cram-md5, plain text (bcrypt
1889The parameter is an account name, as configured under Accounts in domains.conf
1890and as present in the data/accounts/ directory, not a configured email address
1899 pw := xreadpassword()
1901 ctlcmdSetaccountpassword(xctl(), args[0], pw)
1904func ctlcmdSetaccountpassword(ctl *ctl, account, password string) {
1905 ctl.xwrite("setaccountpassword")
1907 ctl.xwrite(password)
1911func cmdDeliver(c *cmd) {
1913 c.params = "$address < message"
1914 c.help = "Deliver message to address."
1920 ctlcmdDeliver(xctl(), args[0])
1923func ctlcmdDeliver(ctl *ctl, address string) {
1924 ctl.xwrite("deliver")
1927 ctl.xstreamfrom(os.Stdin)
1930 fmt.Println("message delivered")
1932 log.Fatalf("deliver: %s", line)
1936func cmdDKIMGenrsa(c *cmd) {
1937 c.params = ">$selector._domainkey.$domain.rsa2048.privatekey.pkcs8.pem"
1938 c.help = `Generate a new 2048 bit RSA private key for use with DKIM.
1940The generated file is in PEM format, and has a comment it is generated for use
1943 if len(c.Parse()) != 0 {
1947 buf, err := admin.MakeDKIMRSAKey(dns.Domain{}, dns.Domain{})
1948 xcheckf(err, "making rsa private key")
1949 _, err = os.Stdout.Write(buf)
1950 xcheckf(err, "writing rsa private key")
1953// todo: options for specifying the domain this is the mx host of, and enabling dane and/or mta-sts verification
1954func cmdSMTPDial(c *cmd) {
1955 c.params = "$host[:$port]"
1957 var tlsCerts, tlsCiphersuites, tlsCurves, tlsVersionMin, tlsVersionMax, tlsRenegotiation string
1958 var tlsVerify, noTLS, forceTLS, tlsNoSessionTickets, tlsNoDynamicRecordSizing bool
1959 var ehloHostnameStr, remoteHostnameStr string
1961 ciphersuites := map[string]*tls.CipherSuite{}
1962 ciphersuitesInsecure := map[string]*tls.CipherSuite{}
1963 for _, v := range tls.CipherSuites() {
1964 if slices.Contains(v.SupportedVersions, tls.VersionTLS10) || slices.Contains(v.SupportedVersions, tls.VersionTLS11) || slices.Contains(v.SupportedVersions, tls.VersionTLS12) {
1965 ciphersuites[strings.ToLower(v.Name)] = v
1968 for _, v := range tls.InsecureCipherSuites() {
1969 if slices.Contains(v.SupportedVersions, tls.VersionTLS10) || slices.Contains(v.SupportedVersions, tls.VersionTLS11) || slices.Contains(v.SupportedVersions, tls.VersionTLS12) {
1970 ciphersuitesInsecure[strings.ToLower(v.Name)] = v
1974 curves := map[string]tls.CurveID{}
1975 for _, a := range curvesList {
1976 curves[strings.ToLower(a.String())] = a
1979 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)), ", "))
1980 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")
1981 c.flag.StringVar(&tlsCerts, "tlscerts", "", "path to root ca certificates in pem form, for verification")
1982 c.flag.StringVar(&tlsVersionMin, "tlsversionmin", "", "minimum TLS version, empty value uses TLS stack default; values: tls1.2, etc.")
1983 c.flag.StringVar(&tlsVersionMax, "tlsversionmax", "", "maximum TLS version, empty value uses TLS stack default; values: tls1.2, etc.")
1984 c.flag.BoolVar(&tlsVerify, "tlsverify", false, "verify remote hostname during TLS")
1985 c.flag.BoolVar(&tlsNoSessionTickets, "tlsnosessiontickets", false, "disable TLS session tickets")
1986 c.flag.BoolVar(&tlsNoDynamicRecordSizing, "tlsnodynamicrecordsizing", false, "disable TLS dynamic record sizing")
1987 c.flag.BoolVar(&noTLS, "notls", false, "do not use TLS")
1988 c.flag.BoolVar(&forceTLS, "forcetls", false, "use TLS, even if remote SMTP server does not announce STARTTLS extension")
1989 c.flag.StringVar(&tlsRenegotiation, "tlsrenegotiation", "never", "when to allow renegotiation; only applies to tls1.2 and earlier, not tls1.3; values: never, once, always")
1990 c.flag.StringVar(&ehloHostnameStr, "ehlohostname", "", "our hostname to use during the SMTP EHLO command")
1991 c.flag.StringVar(&remoteHostnameStr, "remotehostname", "", "remote hostname to use for TLS verification, if enabled; the hostname from the parameter is used by default")
1993 c.help = `Dial the address, initialize the SMTP session, including using STARTTLS to enable TLS if the server supports it.
1995If no port is specified, SMTP port 25 is used.
1997Data is copied between connection and stdin/stdout until either side closes the
2000The flags influence the TLS configuration, useful for debugging interoperability
2003No MTA-STS or DANE verification is done.
2005Hint: Use "mox -loglevel trace smtp dial ..." to see the protocol messages
2006exchanged during connection set up.
2013 if noTLS && forceTLS {
2014 log.Fatalf("cannot have both -notls and -forcetls")
2017 parseTLSVersion := func(s string) uint16 {
2020 return tls.VersionTLS10
2022 return tls.VersionTLS11
2024 return tls.VersionTLS12
2026 return tls.VersionTLS13
2030 log.Fatalf("invalid tls version %q", s)
2031 panic("not reached")
2034 tlsConfig := tls.Config{
2035 MinVersion: parseTLSVersion(tlsVersionMin),
2036 MaxVersion: parseTLSVersion(tlsVersionMax),
2037 InsecureSkipVerify: !tlsVerify,
2038 SessionTicketsDisabled: tlsNoSessionTickets,
2039 DynamicRecordSizingDisabled: tlsNoDynamicRecordSizing,
2042 switch tlsRenegotiation {
2044 tlsConfig.Renegotiation = tls.RenegotiateNever
2046 tlsConfig.Renegotiation = tls.RenegotiateOnceAsClient
2048 tlsConfig.Renegotiation = tls.RenegotiateFreelyAsClient
2050 log.Fatalf("invalid value %q for -tlsrenegotation", tlsRenegotiation)
2053 pool := x509.NewCertPool()
2054 pembuf, err := os.ReadFile(tlsCerts)
2055 xcheckf(err, "reading tls certificates")
2056 ok := pool.AppendCertsFromPEM(pembuf)
2058 c.log.Warn("no tls certificates found", slog.String("path", tlsCerts))
2060 tlsConfig.RootCAs = pool
2062 if tlsCiphersuites != "" {
2063 for s := range strings.SplitSeq(tlsCiphersuites, ",") {
2064 s = strings.TrimSpace(s)
2065 c, ok := ciphersuites[s]
2067 c, ok = ciphersuitesInsecure[s]
2070 log.Fatalf("unknown ciphersuite %q", s)
2072 tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, c.ID)
2075 if tlsCurves != "" {
2076 for s := range strings.SplitSeq(tlsCurves, ",") {
2077 s = strings.TrimSpace(s)
2078 if c, ok := curves[s]; !ok {
2079 log.Fatalf("unknown ecc key exchange algorithm %q", s)
2081 tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, c)
2086 var host, portStr string
2088 host, portStr, err = net.SplitHostPort(args[0])
2093 port, err := strconv.ParseInt(portStr, 10, 64)
2094 xcheckf(err, "parsing port %q", portStr)
2096 if remoteHostnameStr == "" {
2097 remoteHostnameStr = host
2099 remoteHostname, err := dns.ParseDomain(remoteHostnameStr)
2100 xcheckf(err, "parsing remote host")
2101 tlsConfig.ServerName = remoteHostname.Name()
2103 resolver := dns.StrictResolver{Pkg: "smtpdial"}
2104 _, _, _, ips, _, err := smtpclient.GatherIPs(context.Background(), c.log.Logger, resolver, "ip", dns.IPDomain{Domain: remoteHostname}, nil)
2105 xcheckf(err, "resolve host")
2106 c.log.Info("resolved remote address", slog.Any("ips", ips))
2108 dialer := &net.Dialer{Timeout: 5 * time.Second}
2109 dialedIPs := map[string][]net.IP{}
2110 conn, ip, err := smtpclient.Dial(context.Background(), c.log.Logger, dialer, dns.IPDomain{Domain: remoteHostname}, ips, int(port), dialedIPs, nil)
2111 xcheckf(err, "dial")
2112 c.log.Info("connected to remote host", slog.Any("ip", ip))
2114 tlsMode := smtpclient.TLSOpportunistic
2116 tlsMode = smtpclient.TLSRequiredStartTLS
2118 tlsMode = smtpclient.TLSSkip
2120 var ehloHostname dns.Domain
2121 if ehloHostnameStr == "" {
2122 name, err := os.Hostname()
2123 xcheckf(err, "get hostname")
2124 ehloHostnameStr = name
2126 ehloHostname, err = dns.ParseDomain(ehloHostnameStr)
2127 xcheckf(err, "parse hostname")
2129 opts := smtpclient.Opts{
2130 TLSConfig: &tlsConfig,
2132 client, err := smtpclient.New(context.Background(), c.log.Logger, conn, tlsMode, false, ehloHostname, dns.Domain{}, opts)
2133 xcheckf(err, "new smtp client")
2135 cs := client.TLSConnectionState()
2137 c.log.Info("smtp initialized without tls")
2139 c.log.Info("smtp initialized with tls",
2140 slog.String("version", tls.VersionName(cs.Version)),
2141 slog.String("ciphersuite", strings.ToLower(tls.CipherSuiteName(cs.CipherSuite))),
2142 slog.String("sni", cs.ServerName),
2144 for _, chain := range cs.VerifiedChains {
2146 for _, cert := range chain {
2147 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)
2150 c.log.Info("tls certificate verification chain", slog.String("chain", strings.Join(l, "; ")))
2154 conn, err = client.Conn()
2155 xcheckf(err, "get smtp session connection")
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 cmdDANEDial(c *cmd) {
2168 c.params = "$host:$port"
2170 c.flag.StringVar(&usages, "usages", "pkix-ta,pkix-ee,dane-ta,dane-ee", "allowed usages for dane, comma-separated list")
2171 c.help = `Dial the address using TLS with certificate verification using DANE.
2173Data is copied between connection and stdin/stdout until either side closes the
2181 allowedUsages := []adns.TLSAUsage{}
2183 for s := range strings.SplitSeq(usages, ",") {
2184 var usage adns.TLSAUsage
2185 switch strings.ToLower(s) {
2186 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
2187 usage = adns.TLSAUsagePKIXTA
2188 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
2189 usage = adns.TLSAUsagePKIXEE
2190 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
2191 usage = adns.TLSAUsageDANETA
2192 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
2193 usage = adns.TLSAUsageDANEEE
2195 log.Fatalf("unknown dane usage %q", s)
2197 allowedUsages = append(allowedUsages, usage)
2201 pkixRoots, err := x509.SystemCertPool()
2202 xcheckf(err, "get system pkix certificate pool")
2204 resolver := dns.StrictResolver{Pkg: "danedial"}
2205 conn, record, err := dane.Dial(context.Background(), c.log.Logger, resolver, "tcp", args[0], allowedUsages, pkixRoots)
2206 xcheckf(err, "dial")
2207 log.Printf("(connected, verified with %s)", record)
2210 _, err := io.Copy(os.Stdout, conn)
2211 xcheckf(err, "copy from connection to stdout")
2213 c.log.Check(err, "closing connection")
2215 _, err = io.Copy(conn, os.Stdin)
2216 xcheckf(err, "copy from stdin to connection")
2219func cmdDANEDialmx(c *cmd) {
2220 c.params = "$domain [$desthost]"
2221 var ehloHostname string
2222 c.flag.StringVar(&ehloHostname, "ehlohostname", "localhost", "hostname to send in smtp ehlo command")
2223 c.help = `Connect to MX server for domain using STARTTLS verified with DANE.
2225If no destination host is specified, regular delivery logic is used to find the
2226hosts to attempt delivery too. This involves following CNAMEs for the domain,
2227looking up MX records, and possibly falling back to the domain name itself as
2230If a destination host is specified, that is the only candidate host considered
2233With a list of destinations gathered, each is dialed until a successful SMTP
2234session verified with DANE has been initialized, including EHLO and STARTTLS
2237Once connected, data is copied between connection and stdin/stdout, until
2238either side closes the connection.
2240This command follows the same logic as delivery attempts made from the queue,
2241sharing most of its code.
2244 if len(args) != 1 && len(args) != 2 {
2248 ehloDomain := xparseDomain(ehloHostname, "ehlo host name")
2249 origNextHop := xparseDomain(args[0], "domain")
2251 ctxbg := context.Background()
2253 resolver := dns.StrictResolver{}
2255 var expandedNextHopAuthentic bool
2256 var expandedNextHop dns.Domain
2257 var hostPrefs []smtpclient.HostPref
2260 var origNextHopAuthentic bool
2262 haveMX, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hostPrefs, permanent, err = smtpclient.GatherDestinations(ctxbg, c.log.Logger, resolver, dns.IPDomain{Domain: origNextHop})
2263 status := "temporary"
2265 status = "permanent"
2268 log.Fatalf("gathering destinations: %v (%s)", err, status)
2270 if expandedNextHop != origNextHop {
2271 log.Printf("followed cnames to %s", expandedNextHop)
2274 log.Printf("found mx record, trying mx hosts")
2276 log.Printf("no mx record found, will try to connect to domain directly")
2278 if !origNextHopAuthentic {
2279 log.Fatalf("error: initial domain not dnssec-secure")
2281 if !expandedNextHopAuthentic {
2282 log.Fatalf("error: expanded domain not dnssec-secure")
2286 for _, hp := range hostPrefs {
2287 s := hp.Host.String()
2289 s += fmt.Sprintf(" (pref %d)", hp.Pref)
2293 log.Printf("destinations: %s", strings.Join(l, ", "))
2295 d := xparseDomain(args[1], "destination host")
2296 log.Printf("skipping domain mx/cname lookups, assuming domain is dnssec-protected")
2298 expandedNextHopAuthentic = true
2300 hostPrefs = []smtpclient.HostPref{{Host: dns.IPDomain{Domain: d}, Pref: -1}}
2303 dialedIPs := map[string][]net.IP{}
2304 for _, hp := range hostPrefs {
2307 log.Printf("attempting to connect to %s (pref %d)", host, hp.Pref)
2309 authentic, expandedAuthentic, expandedHost, ips, _, err := smtpclient.GatherIPs(ctxbg, c.log.Logger, resolver, "ip", host, dialedIPs)
2311 log.Printf("resolving ips for %s: %v, skipping", host, err)
2315 log.Printf("no dnssec for ips of %s, skipping", host)
2318 if !expandedAuthentic {
2319 log.Printf("no dnssec for cname-followed ips of %s, skipping", host)
2322 if expandedHost != host.Domain {
2323 log.Printf("host %s cname-expanded to %s", host, expandedHost)
2325 log.Printf("host %s resolved to ips %s, looking up tlsa records", host, ips)
2327 daneRequired, daneRecords, tlsaBaseDomain, err := smtpclient.GatherTLSA(ctxbg, c.log.Logger, resolver, host.Domain, expandedAuthentic, expandedHost)
2329 log.Printf("looking up tlsa records: %s, skipping", err)
2332 tlsMode := smtpclient.TLSRequiredStartTLS
2333 if len(daneRecords) == 0 {
2335 log.Printf("host %s has no tlsa records, skipping", expandedHost)
2338 log.Printf("warning: only unusable tlsa records found, continuing with required tls without certificate verification")
2342 for _, r := range daneRecords {
2343 l = append(l, r.String())
2345 log.Printf("tlsa records: %s", strings.Join(l, "; "))
2348 tlsHostnames := smtpclient.GatherTLSANames(haveMX, expandedNextHopAuthentic, expandedAuthentic, origNextHop, expandedNextHop, host.Domain, tlsaBaseDomain)
2350 for _, name := range tlsHostnames {
2351 l = append(l, name.String())
2353 log.Printf("gathered valid tls certificate names for potential verification with dane-ta: %s", strings.Join(l, ", "))
2355 dialer := &net.Dialer{Timeout: 5 * time.Second}
2356 conn, _, err := smtpclient.Dial(ctxbg, c.log.Logger, dialer, dns.IPDomain{Domain: expandedHost}, ips, 25, dialedIPs, nil)
2358 log.Printf("dial %s: %v, skipping", expandedHost, err)
2361 log.Printf("connected to %s, %s, starting smtp session with ehlo and starttls with dane verification", expandedHost, conn.RemoteAddr())
2363 var verifiedRecord adns.TLSA
2364 opts := smtpclient.Opts{
2365 DANERecords: daneRecords,
2366 DANEMoreHostnames: tlsHostnames[1:],
2367 DANEVerifiedRecord: &verifiedRecord,
2368 RootCAs: mox.Conf.Static.TLS.CertPool,
2371 sc, err := smtpclient.New(ctxbg, c.log.Logger, conn, tlsMode, tlsPKIX, ehloDomain, tlsHostnames[0], opts)
2373 log.Printf("setting up smtp session: %v, skipping", err)
2374 if xerr := conn.Close(); xerr != nil {
2375 log.Printf("closing connection: %v", xerr)
2380 smtpConn, err := sc.Conn()
2382 log.Fatalf("error: taking over smtp connection: %s", err)
2384 log.Printf("tls verified with tlsa record: %s", verifiedRecord)
2385 log.Printf("smtp session initialized and connected to stdin/stdout")
2388 _, err := io.Copy(os.Stdout, smtpConn)
2389 xcheckf(err, "copy from connection to stdout")
2390 if err := smtpConn.Close(); err != nil {
2391 log.Printf("closing smtp connection: %v", err)
2394 _, err = io.Copy(smtpConn, os.Stdin)
2395 xcheckf(err, "copy from stdin to connection")
2398 log.Fatalf("no remaining destinations")
2401func cmdDANEMakeRecord(c *cmd) {
2402 c.params = "$usage $selector $matchtype [certificate.pem | publickey.pem | privatekey.pem]"
2403 c.help = `Print TLSA record for given certificate/key and parameters.
2406- usage: pkix-ta (0), pkix-ee (1), dane-ta (2), dane-ee (3)
2407- selector: cert (0), spki (1)
2408- matchtype: full (0), sha2-256 (1), sha2-512 (2)
2410Common DANE TLSA record parameters are: dane-ee spki sha2-256, or 3 1 1,
2411followed by a sha2-256 hash of the DER-encoded "SPKI" (subject public key info)
2412from the certificate. An example DNS zone file entry:
2414 _25._tcp.example.com. TLSA 3 1 1 133b919c9d65d8b1488157315327334ead8d83372db57465ecabf53ee5748aee
2416The first usable information from the pem file is used to compose the TLSA
2417record. In case of selector "cert", a certificate is required. Otherwise the
2418"subject public key info" (spki) of the first certificate or public or private
2419key (pkcs#8, pkcs#1 or ec private key) is used.
2427 var usage adns.TLSAUsage
2428 switch strings.ToLower(args[0]) {
2429 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
2430 usage = adns.TLSAUsagePKIXTA
2431 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
2432 usage = adns.TLSAUsagePKIXEE
2433 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
2434 usage = adns.TLSAUsageDANETA
2435 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
2436 usage = adns.TLSAUsageDANEEE
2438 if v, err := strconv.ParseUint(args[0], 10, 16); err != nil {
2439 log.Fatalf("bad usage %q", args[0])
2441 // Does not influence certificate association data, so we can accept other numbers.
2442 log.Printf("warning: continuing with unrecognized tlsa usage %d", v)
2443 usage = adns.TLSAUsage(v)
2447 var selector adns.TLSASelector
2448 switch strings.ToLower(args[1]) {
2449 case "cert", strconv.Itoa(int(adns.TLSASelectorCert)):
2450 selector = adns.TLSASelectorCert
2451 case "spki", strconv.Itoa(int(adns.TLSASelectorSPKI)):
2452 selector = adns.TLSASelectorSPKI
2454 log.Fatalf("bad selector %q", args[1])
2457 var matchType adns.TLSAMatchType
2458 switch strings.ToLower(args[2]) {
2459 case "full", strconv.Itoa(int(adns.TLSAMatchTypeFull)):
2460 matchType = adns.TLSAMatchTypeFull
2461 case "sha2-256", strconv.Itoa(int(adns.TLSAMatchTypeSHA256)):
2462 matchType = adns.TLSAMatchTypeSHA256
2463 case "sha2-512", strconv.Itoa(int(adns.TLSAMatchTypeSHA512)):
2464 matchType = adns.TLSAMatchTypeSHA512
2466 log.Fatalf("bad matchtype %q", args[2])
2469 buf, err := os.ReadFile(args[3])
2470 xcheckf(err, "reading certificate")
2472 var block *pem.Block
2473 block, buf = pem.Decode(buf)
2477 extra = " (with leftover data from pem file)"
2479 if selector == adns.TLSASelectorCert {
2480 log.Fatalf("no certificate found in pem file%s", extra)
2482 log.Fatalf("no certificate or public or private key found in pem file%s", extra)
2485 var cert *x509.Certificate
2487 if block.Type == "CERTIFICATE" {
2488 cert, err = x509.ParseCertificate(block.Bytes)
2489 xcheckf(err, "parse certificate")
2491 case adns.TLSASelectorCert:
2493 case adns.TLSASelectorSPKI:
2494 data = cert.RawSubjectPublicKeyInfo
2496 } else if selector == adns.TLSASelectorCert {
2497 // We need a certificate, just a public/private key won't do.
2498 log.Printf("skipping pem type %q, certificate is required", block.Type)
2501 var privKey, pubKey any
2505 _, err := x509.ParsePKIXPublicKey(block.Bytes)
2506 xcheckf(err, "parse pkix subject public key info (spki)")
2508 case "EC PRIVATE KEY":
2509 privKey, err = x509.ParseECPrivateKey(block.Bytes)
2510 xcheckf(err, "parse ec private key")
2511 case "RSA PRIVATE KEY":
2512 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
2513 xcheckf(err, "parse pkcs#1 rsa private key")
2514 case "RSA PUBLIC KEY":
2515 pubKey, err = x509.ParsePKCS1PublicKey(block.Bytes)
2516 xcheckf(err, "parse pkcs#1 rsa public key")
2518 // PKCS#8 private key
2519 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
2520 xcheckf(err, "parse pkcs#8 private key")
2522 log.Printf("skipping unrecognized pem type %q", block.Type)
2526 if pubKey == nil && privKey != nil {
2527 if signer, ok := privKey.(crypto.Signer); !ok {
2528 log.Fatalf("private key of type %T is not a signer, cannot get public key", privKey)
2530 pubKey = signer.Public()
2534 // Should not happen.
2535 log.Fatalf("internal error: did not find private or public key")
2537 data, err = x509.MarshalPKIXPublicKey(pubKey)
2538 xcheckf(err, "marshal pkix subject public key info (spki)")
2543 case adns.TLSAMatchTypeFull:
2544 case adns.TLSAMatchTypeSHA256:
2545 p := sha256.Sum256(data)
2547 case adns.TLSAMatchTypeSHA512:
2548 p := sha512.Sum512(data)
2551 fmt.Printf("%d %d %d %x\n", usage, selector, matchType, data)
2556func cmdDNSLookup(c *cmd) {
2557 c.params = "[ptr | mx | cname | ips | a | aaaa | ns | txt | srv | tlsa] $name"
2558 c.help = `Lookup DNS name of given type.
2560Lookup always prints whether the response was DNSSEC-protected.
2564mox dns lookup ptr 1.1.1.1
2565mox dns lookup mx xmox.nl
2566mox dns lookup txt _dmarc.xmox.nl.
2567mox dns lookup tlsa _25._tcp.xmox.nl
2575 resolver := dns.StrictResolver{Pkg: "dns"}
2577 // like xparseDomain, but treat unparseable domain as an ASCII name so names with
2578 // underscores are still looked up, e,g <selector>._domainkey.<host>.
2579 xdomain := func(s string) dns.Domain {
2580 d, err := dns.ParseDomain(s)
2582 return dns.Domain{ASCII: strings.TrimSuffix(s, ".")}
2587 cmd, name := args[0], args[1]
2591 ip := xparseIP(name, "ip")
2592 ptrs, result, err := resolver.LookupAddr(context.Background(), ip.String())
2594 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2596 fmt.Printf("names (%d, %s):\n", len(ptrs), dnssecStatus(result.Authentic))
2597 for _, ptr := range ptrs {
2598 fmt.Printf("- %s\n", ptr)
2602 name := xdomain(name)
2603 mxl, result, err := resolver.LookupMX(context.Background(), name.ASCII+".")
2605 log.Printf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2606 // We can still have valid records...
2608 fmt.Printf("mx records (%d, %s):\n", len(mxl), dnssecStatus(result.Authentic))
2609 for _, mx := range mxl {
2610 fmt.Printf("- %s, preference %d\n", mx.Host, mx.Pref)
2614 name := xdomain(name)
2615 target, result, err := resolver.LookupCNAME(context.Background(), name.ASCII+".")
2617 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2619 fmt.Printf("%s (%s)\n", target, dnssecStatus(result.Authentic))
2621 case "ips", "a", "aaaa":
2625 } else if cmd == "aaaa" {
2628 name := xdomain(name)
2629 ips, result, err := resolver.LookupIP(context.Background(), network, name.ASCII+".")
2631 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2633 fmt.Printf("records (%d, %s):\n", len(ips), dnssecStatus(result.Authentic))
2634 for _, ip := range ips {
2635 fmt.Printf("- %s\n", ip)
2639 name := xdomain(name)
2640 nsl, result, err := resolver.LookupNS(context.Background(), name.ASCII+".")
2642 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2644 fmt.Printf("ns records (%d, %s):\n", len(nsl), dnssecStatus(result.Authentic))
2645 for _, ns := range nsl {
2646 fmt.Printf("- %s\n", ns)
2650 host := xdomain(name)
2651 l, result, err := resolver.LookupTXT(context.Background(), host.ASCII+".")
2653 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2655 fmt.Printf("txt records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2656 for _, txt := range l {
2657 fmt.Printf("- %s\n", txt)
2661 host := xdomain(name)
2662 _, l, result, err := resolver.LookupSRV(context.Background(), "", "", host.ASCII+".")
2664 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2666 fmt.Printf("srv records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2667 for _, srv := range l {
2668 fmt.Printf("- host %s, port %d, priority %d, weight %d\n", srv.Target, srv.Port, srv.Priority, srv.Weight)
2672 host := xdomain(name)
2673 l, result, err := resolver.LookupTLSA(context.Background(), 0, "", host.ASCII+".")
2675 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2677 fmt.Printf("tlsa records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2678 for _, tlsa := range l {
2679 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)
2682 log.Fatalf("unknown record type %q", args[0])
2686func cmdDKIMGened25519(c *cmd) {
2687 c.params = ">$selector._domainkey.$domain.ed25519.privatekey.pkcs8.pem"
2688 c.help = `Generate a new ed25519 key for use with DKIM.
2690Ed25519 keys are much smaller than RSA keys of comparable cryptographic
2691strength. This is convenient because of maximum DNS message sizes. At the time
2692of writing, not many mail servers appear to support ed25519 DKIM keys though,
2693so it is recommended to sign messages with both RSA and ed25519 keys.
2695 if len(c.Parse()) != 0 {
2699 buf, err := admin.MakeDKIMEd25519Key(dns.Domain{}, dns.Domain{})
2700 xcheckf(err, "making dkim ed25519 key")
2701 _, err = os.Stdout.Write(buf)
2702 xcheckf(err, "writing dkim ed25519 key")
2705func cmdDKIMTXT(c *cmd) {
2706 c.params = "<$selector._domainkey.$domain.key.pkcs8.pem"
2707 c.help = `Print a DKIM DNS TXT record with the public key derived from the private key read from stdin.
2709The DNS should be configured as a TXT record at $selector._domainkey.$domain.
2711 if len(c.Parse()) != 0 {
2715 privKey, err := parseDKIMKey(os.Stdin)
2716 xcheckf(err, "reading dkim private key from stdin")
2720 Hashes: []string{"sha256"},
2721 Flags: []string{"s"},
2724 switch key := privKey.(type) {
2725 case *rsa.PrivateKey:
2726 r.PublicKey = key.Public()
2727 case ed25519.PrivateKey:
2728 r.PublicKey = key.Public()
2731 log.Fatalf("unsupported private key type %T, must be rsa or ed25519", privKey)
2734 record, err := r.Record()
2735 xcheckf(err, "making record")
2736 fmt.Print("<selector>._domainkey.<your.domain.> TXT ")
2740 s, record = record[:100], record[100:]
2744 fmt.Printf(`"%s" `, s)
2749func parseDKIMKey(r io.Reader) (any, error) {
2750 buf, err := io.ReadAll(r)
2752 return nil, fmt.Errorf("reading pem from stdin: %v", err)
2754 b, _ := pem.Decode(buf)
2756 return nil, fmt.Errorf("decoding pem: %v", err)
2758 privKey, err := x509.ParsePKCS8PrivateKey(b.Bytes)
2760 return nil, fmt.Errorf("parsing private key: %v", err)
2765func cmdDKIMVerify(c *cmd) {
2766 c.params = "$messagefile"
2767 c.help = `Verify the DKIM signatures in a message and print the results.
2769The message is parsed, and the DKIM-Signature headers are validated. Validation
2770of older messages may fail because the DNS records have been removed or changed
2771by now, or because the signature header may have specified an expiration time
2779 msgf, err := os.Open(args[0])
2780 xcheckf(err, "open message")
2782 results, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, false, dkim.DefaultPolicy, msgf, true)
2783 xcheckf(err, "dkim verify")
2785 for _, result := range results {
2787 if result.Sig == nil {
2788 log.Printf("warning: could not parse signature")
2790 sigh, err = result.Sig.Header()
2792 log.Printf("warning: packing signature: %s", err)
2796 if result.Record == nil {
2797 log.Printf("warning: missing DNS record")
2799 txt, err = result.Record.Record()
2801 log.Printf("warning: packing record: %s", err)
2804 fmt.Printf("status %q, err %v\nrecord %q\nheader %s\n", result.Status, result.Err, txt, sigh)
2808func cmdDKIMSign(c *cmd) {
2809 c.params = "$messagefile"
2810 c.help = `Sign a message, adding DKIM-Signature headers based on the domain in the From header.
2812The message is parsed, the domain looked up in the configuration files, and
2813DKIM-Signature headers generated. The message is printed with the DKIM-Signature
2821 msgf, err := os.Open(args[0])
2822 xcheckf(err, "open message")
2824 if err := msgf.Close(); err != nil {
2825 log.Printf("closing message file: %v", err)
2829 p, err := message.Parse(c.log.Logger, true, msgf)
2830 xcheckf(err, "parsing message")
2832 if len(p.Envelope.From) != 1 {
2833 log.Fatalf("found %d from headers, need exactly 1", len(p.Envelope.From))
2835 localpart, err := smtp.ParseLocalpart(p.Envelope.From[0].User)
2836 xcheckf(err, "parsing localpart of address in from-header")
2837 dom := xparseDomain(p.Envelope.From[0].Host, "domain of address in from-header")
2841 domConf, ok := mox.Conf.Domain(dom)
2843 log.Fatalf("domain %s not configured", dom)
2846 selectors := mox.DKIMSelectors(domConf.DKIM)
2847 headers, err := dkim.Sign(context.Background(), c.log.Logger, localpart, dom, selectors, false, msgf)
2848 xcheckf(err, "signing message with dkim")
2850 log.Fatalf("no DKIM configured for domain %s", dom)
2852 _, err = fmt.Fprint(os.Stdout, headers)
2853 xcheckf(err, "write headers")
2854 _, err = io.Copy(os.Stdout, msgf)
2855 xcheckf(err, "write message")
2858func cmdDKIMLookup(c *cmd) {
2859 c.params = "$selector $domain"
2860 c.help = "Lookup and print the DKIM record for the selector at the domain."
2866 selector := xparseDomain(args[0], "selector")
2867 domain := xparseDomain(args[1], "domain")
2869 status, record, txt, authentic, err := dkim.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, selector, domain)
2871 fmt.Printf("error: %s\n", err)
2873 if status != dkim.StatusNeutral {
2874 fmt.Printf("status: %s\n", status)
2877 fmt.Printf("TXT record: %s\n", txt)
2880 fmt.Println("dnssec-signed: yes")
2882 fmt.Println("dnssec-signed: no")
2885 fmt.Printf("Record:\n")
2887 "version", record.Version,
2888 "hashes", record.Hashes,
2890 "notes", record.Notes,
2891 "services", record.Services,
2892 "flags", record.Flags,
2894 for i := 0; i < len(pairs); i += 2 {
2895 fmt.Printf("\t%s: %v\n", pairs[i], pairs[i+1])
2900func cmdDMARCLookup(c *cmd) {
2901 c.params = "$domain"
2902 c.help = "Lookup dmarc policy for domain, a DNS TXT record at _dmarc.<domain>, validate and print it."
2908 fromdomain := xparseDomain(args[0], "domain")
2909 _, domain, _, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, fromdomain)
2910 xcheckf(err, "dmarc lookup domain %s", fromdomain)
2911 fmt.Printf("dmarc record at domain %s: %s\n", domain, txt)
2912 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2915func dnssecStatus(v bool) string {
2917 return "with dnssec"
2919 return "without dnssec"
2922func cmdDMARCVerify(c *cmd) {
2923 c.params = "$remoteip $mailfromaddress $helodomain < messagefile"
2924 c.help = `Parse an email message and evaluate it against the DMARC policy of the domain in the From-header.
2926mailfromaddress and helodomain are used for SPF validation. If both are empty,
2927SPF validation is skipped.
2929mailfromaddress should be the address used as MAIL FROM in the SMTP session.
2930For DSN messages, that address may be empty. The helo domain was specified at
2931the beginning of the SMTP transaction that delivered the message. These values
2932can be found in message headers.
2939 var heloDomain *dns.Domain
2941 remoteIP := xparseIP(args[0], "remoteip")
2943 var mailfrom *smtp.Address
2945 a, err := smtp.ParseAddress(args[1])
2946 xcheckf(err, "parsing mailfrom address")
2950 d := xparseDomain(args[2], "helo domain")
2953 var received *spf.Received
2954 spfStatus := spf.StatusNone
2955 var spfIdentity *dns.Domain
2956 if mailfrom != nil || heloDomain != nil {
2957 spfArgs := spf.Args{
2959 LocalIP: net.ParseIP("127.0.0.1"),
2960 LocalHostname: dns.Domain{ASCII: "localhost"},
2962 if mailfrom != nil {
2963 spfArgs.MailFromLocalpart = mailfrom.Localpart
2964 spfArgs.MailFromDomain = mailfrom.Domain
2966 if heloDomain != nil {
2967 spfArgs.HelloDomain = dns.IPDomain{Domain: *heloDomain}
2969 rspf, spfDomain, expl, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfArgs)
2971 log.Printf("spf verify: %v (explanation: %q, authentic %v)", err, expl, authentic)
2974 spfStatus = received.Result
2975 // todo: should probably potentially do two separate spf validations
2976 if mailfrom != nil {
2977 spfIdentity = &mailfrom.Domain
2979 spfIdentity = heloDomain
2981 fmt.Printf("spf result: %s: %s (%s)\n", spfDomain, spfStatus, dnssecStatus(authentic))
2985 data, err := io.ReadAll(os.Stdin)
2986 xcheckf(err, "read message")
2987 dmarcFrom, _, _, err := message.From(c.log.Logger, false, bytes.NewReader(data), nil)
2988 xcheckf(err, "extract dmarc from message")
2990 const ignoreTestMode = false
2991 dkimResults, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, true, func(*dkim.Sig) error { return nil }, bytes.NewReader(data), ignoreTestMode)
2992 xcheckf(err, "dkim verify")
2993 for _, r := range dkimResults {
2994 fmt.Printf("dkim result: %q (err %v)\n", r.Status, r.Err)
2997 _, result := dmarc.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, dmarcFrom.Domain, dkimResults, spfStatus, spfIdentity, false)
2998 xcheckf(result.Err, "dmarc verify")
2999 fmt.Printf("dmarc from: %s\ndmarc status: %q\ndmarc reject: %v\ncmarc record: %s\n", dmarcFrom, result.Status, result.Reject, result.Record)
3002func cmdDMARCCheckreportaddrs(c *cmd) {
3003 c.params = "$domain"
3004 c.help = `For each reporting address in the domain's DMARC record, check if it has opted into receiving reports (if needed).
3006A DMARC record can request reports about DMARC evaluations to be sent to an
3007email/http address. If the organizational domains of that of the DMARC record
3008and that of the report destination address do not match, the destination
3009address must opt-in to receiving DMARC reports by creating a DMARC record at
3010<dmarcdomain>._report._dmarc.<reportdestdomain>.
3017 dom := xparseDomain(args[0], "domain")
3018 _, domain, record, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dom)
3019 xcheckf(err, "dmarc lookup domain %s", dom)
3020 fmt.Printf("dmarc record at domain %s: %q\n", domain, txt)
3021 fmt.Printf("(%s)\n", dnssecStatus(authentic))
3023 check := func(kind, addr string) {
3026 printResult := func(format string, args ...any) {
3027 fmt.Printf("%s %s: %s (%s)\n", kind, addr, fmt.Sprintf(format, args...), dnssecStatus(authentic))
3030 u, err := url.Parse(addr)
3032 printResult("parsing uri %s: %v (skipping)", addr, err)
3035 var destdom dns.Domain
3038 a, err := smtp.ParseAddress(u.Opaque)
3040 printResult("parsing destination email address %s: %v (skipping)", u.Opaque, err)
3045 printResult("unrecognized scheme in reporting address %s (skipping)", u.Scheme)
3049 if publicsuffix.Lookup(context.Background(), c.log.Logger, dom) == publicsuffix.Lookup(context.Background(), c.log.Logger, destdom) {
3050 printResult("pass (same organizational domain)")
3054 accepts, status, _, txts, authentic, err := dmarc.LookupExternalReportsAccepted(context.Background(), c.log.Logger, dns.StrictResolver{}, domain, destdom)
3056 txtaddr := fmt.Sprintf("%s._report._dmarc.%s", domain.ASCII, destdom.ASCII)
3058 txtstr = fmt.Sprintf(" (no txt records %s)", txtaddr)
3060 txtstr = fmt.Sprintf(" (txt record %s: %q)", txtaddr, txts)
3062 if status != dmarc.StatusNone {
3063 printResult("fail: %s%s", err, txtstr)
3065 printResult("pass%s", txtstr)
3066 } else if err != nil {
3067 printResult("fail: %s%s", err, txtstr)
3069 printResult("fail%s", txtstr)
3073 for _, uri := range record.AggregateReportAddresses {
3074 check("aggregate reporting", uri.Address)
3076 for _, uri := range record.FailureReportAddresses {
3077 check("failure reporting", uri.Address)
3081func cmdDMARCParsereportmsg(c *cmd) {
3082 c.params = "$messagefile ..."
3083 c.help = `Parse a DMARC report from an email message, and print its extracted details.
3085DMARC reports are periodically mailed, if requested in the DMARC DNS record of
3086a domain. Reports are sent by mail servers that received messages with our
3087domain in a From header. This may or may not be legatimate email. DMARC reports
3088contain summaries of evaluations of DMARC and DKIM/SPF, which can help
3089understand email deliverability problems.
3096 for _, arg := range args {
3097 f, err := os.Open(arg)
3098 xcheckf(err, "open %q", arg)
3099 feedback, err := dmarcrpt.ParseMessageReport(c.log.Logger, f)
3100 xcheckf(err, "parse report in %q", arg)
3101 meta := feedback.ReportMetadata
3102 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)
3103 if len(meta.Errors) > 0 {
3104 fmt.Printf("Errors:\n")
3105 for _, s := range meta.Errors {
3106 fmt.Printf("\t- %s\n", s)
3109 pol := feedback.PolicyPublished
3110 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)
3111 for _, record := range feedback.Records {
3112 idents := record.Identifiers
3113 fmt.Printf("\theaderfrom %q, envelopes from %q, to %q\n", idents.HeaderFrom, idents.EnvelopeFrom, idents.EnvelopeTo)
3114 eval := record.Row.PolicyEvaluated
3115 var reasons strings.Builder
3116 for _, reason := range eval.Reasons {
3117 reasons.WriteString("; " + string(reason.Type))
3118 if reason.Comment != "" {
3119 reasons.WriteString(fmt.Sprintf(": %q", reason.Comment))
3122 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.String())
3123 for _, dkim := range record.AuthResults.DKIM {
3125 if dkim.HumanResult != "" {
3126 result = fmt.Sprintf(": %q", dkim.HumanResult)
3128 fmt.Printf("\t\tdkim %s; domain %q selector %q%s\n", dkim.Result, dkim.Domain, dkim.Selector, result)
3130 for _, spf := range record.AuthResults.SPF {
3131 fmt.Printf("\t\tspf %s; domain %q scope %q\n", spf.Result, spf.Domain, spf.Scope)
3137func cmdDMARCDBAddReport(c *cmd) {
3139 c.params = "$fromdomain < messagefile"
3140 c.help = "Add a DMARC report to the database."
3148 fromdomain := xparseDomain(args[0], "domain")
3149 fmt.Fprintln(os.Stderr, "reading report message from stdin")
3150 report, err := dmarcrpt.ParseMessageReport(c.log.Logger, os.Stdin)
3151 xcheckf(err, "parse message")
3152 err = dmarcdb.AddReport(context.Background(), report, fromdomain)
3153 xcheckf(err, "add dmarc report")
3156func cmdTLSRPTLookup(c *cmd) {
3157 c.params = "$domain"
3158 c.help = `Lookup the TLSRPT record for the domain.
3160A TLSRPT record typically contains an email address where reports about TLS
3161connectivity should be sent. Mail servers attempting delivery to our domain
3162should attempt to use TLS. TLSRPT lets them report how many connection
3163successfully used TLS, and how what kind of errors occurred otherwise.
3170 d := xparseDomain(args[0], "domain")
3171 _, txt, err := tlsrpt.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, d)
3172 xcheckf(err, "tlsrpt lookup for %s", d)
3176func cmdTLSRPTParsereportmsg(c *cmd) {
3177 c.params = "$messagefile ..."
3178 c.help = `Parse and print the TLSRPT in the message.
3180The report is printed in formatted JSON.
3187 for _, arg := range args {
3188 f, err := os.Open(arg)
3189 xcheckf(err, "open %q", arg)
3190 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, f)
3191 xcheckf(err, "parse report in %q", arg)
3192 // todo future: only print the highlights?
3193 enc := json.NewEncoder(os.Stdout)
3194 enc.SetIndent("", "\t")
3195 enc.SetEscapeHTML(false)
3196 err = enc.Encode(reportJSON)
3197 xcheckf(err, "write report")
3201func cmdSPFCheck(c *cmd) {
3202 c.params = "$domain $ip"
3203 c.help = `Check the status of IP for the policy published in DNS for the domain.
3205IPs may be allowed to send for a domain, or disallowed, and several shades in
3206between. If not allowed, an explanation may be provided by the policy. If so,
3207the explanation is printed. The SPF mechanism that matched (if any) is also
3215 domain := xparseDomain(args[0], "domain")
3217 ip := xparseIP(args[1], "ip")
3219 spfargs := spf.Args{
3221 MailFromLocalpart: "user",
3222 MailFromDomain: domain,
3223 HelloDomain: dns.IPDomain{Domain: domain},
3224 LocalIP: net.ParseIP("127.0.0.1"),
3225 LocalHostname: dns.Domain{ASCII: "localhost"},
3227 r, _, explanation, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfargs)
3229 fmt.Printf("error: %s\n", err)
3231 if explanation != "" {
3232 fmt.Printf("explanation: %s\n", explanation)
3234 fmt.Printf("status: %s (%s)\n", r.Result, dnssecStatus(authentic))
3235 if r.Mechanism != "" {
3236 fmt.Printf("mechanism: %s\n", r.Mechanism)
3240func cmdSPFParse(c *cmd) {
3241 c.params = "$txtrecord"
3242 c.help = "Parse the record as SPF record. If valid, nothing is printed."
3248 _, _, err := spf.ParseRecord(args[0])
3249 xcheckf(err, "parsing record")
3252func cmdSPFLookup(c *cmd) {
3253 c.params = "$domain"
3254 c.help = "Lookup the SPF record for the domain and print it."
3260 domain := xparseDomain(args[0], "domain")
3261 _, txt, _, authentic, err := spf.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
3262 xcheckf(err, "spf lookup for %s", domain)
3264 fmt.Printf("(%s)\n", dnssecStatus(authentic))
3267func cmdMTASTSLookup(c *cmd) {
3268 c.params = "$domain"
3269 c.help = `Lookup the MTASTS record and policy for the domain.
3271MTA-STS is a mechanism for a domain to specify if it requires TLS connections
3272for delivering email. If a domain has a valid MTA-STS DNS TXT record at
3273_mta-sts.<domain> it signals it implements MTA-STS. A policy can then be
3274fetched at https://mta-sts.<domain>/.well-known/mta-sts.txt. The policy
3275specifies the mode (enforce, testing, none), which MX servers support TLS and
3276should be used, and how long the policy can be cached.
3283 domain := xparseDomain(args[0], "domain")
3285 record, policy, _, err := mtasts.Get(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
3287 fmt.Printf("error: %s\n", err)
3290 fmt.Printf("DNS TXT record _mta-sts.%s: %s\n", domain.ASCII, record.String())
3294 fmt.Printf("policy at https://mta-sts.%s/.well-known/mta-sts.txt:\n", domain.ASCII)
3295 fmt.Printf("%s", policy.String())
3299func cmdRDAPDomainage(c *cmd) {
3300 c.params = "$domain"
3301 c.help = `Lookup the age of domain in RDAP based on latest registration.
3303RDAP is the registration data access protocol. Registries run RDAP services for
3304their top level domains, providing information such as the registration date of
3305domains. This command looks up the "age" of a domain by looking at the most
3306recent "registration", "reregistration" or "reinstantiation" event.
3308Email messages from recently registered domains are often treated with
3309suspicion, and some mail systems are more likely to classify them as junk.
3311On each invocation, a bootstrap file with a list of registries (of top-level
3312domains) is retrieved, without caching. Do not run this command too often with
3320 domain := xparseDomain(args[0], "domain")
3322 registration, err := rdap.LookupLastDomainRegistration(context.Background(), c.log, domain)
3323 xcheckf(err, "looking up domain in rdap")
3325 age := time.Since(registration)
3326 const day = 24 * time.Hour
3327 const year = 365 * day
3329 days := (age - years*year) / day
3333 } else if years > 0 {
3334 s = fmt.Sprintf("%d years, ", years)
3339 s += fmt.Sprintf("%d days", days)
3344func cmdRetrain(c *cmd) {
3345 c.params = "[$accountname]"
3346 c.help = `Recreate and retrain the junk filter for the account or all accounts.
3348Useful after having made changes to the junk filter configuration, or if the
3349implementation has changed.
3361 ctlcmdRetrain(xctl(), account)
3364func ctlcmdRetrain(ctl *ctl, account string) {
3365 ctl.xwrite("retrain")
3370func cmdTLSRPTDBAddReport(c *cmd) {
3372 c.params = "< messagefile"
3373 c.help = "Parse a TLS report from the message and add it to the database."
3375 c.flag.BoolVar(&hostReport, "hostreport", false, "report for a host instead of domain")
3383 // First read message, to get the From-header. Then parse it as TLSRPT.
3384 fmt.Fprintln(os.Stderr, "reading report message from stdin")
3385 buf, err := io.ReadAll(os.Stdin)
3386 xcheckf(err, "reading message")
3387 part, err := message.Parse(c.log.Logger, true, bytes.NewReader(buf))
3388 xcheckf(err, "parsing message")
3389 if part.Envelope == nil || len(part.Envelope.From) != 1 {
3390 log.Fatalf("message must have one From-header")
3392 from := part.Envelope.From[0]
3393 domain := xparseDomain(from.Host, "domain")
3395 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, bytes.NewReader(buf))
3396 xcheckf(err, "parsing tls report in message")
3398 mailfrom := from.User + "@" + from.Host // todo future: should escape and such
3399 report := reportJSON.Convert()
3400 err = tlsrptdb.AddReport(context.Background(), c.log, domain, mailfrom, hostReport, &report)
3401 xcheckf(err, "add tls report to database")
3404func cmdDNSBLCheck(c *cmd) {
3405 c.params = "$zone $ip"
3406 c.help = `Test if IP is in the DNS blocklist of the zone, e.g. bl.spamcop.net.
3408If the IP is in the blocklist, an explanation is printed. This is typically a
3409URL with more information.
3416 zone := xparseDomain(args[0], "zone")
3417 ip := xparseIP(args[1], "ip")
3419 status, explanation, err := dnsbl.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, zone, ip)
3420 fmt.Printf("status: %s\n", status)
3421 if status == dnsbl.StatusFail {
3422 fmt.Printf("explanation: %q\n", explanation)
3425 fmt.Printf("error: %s\n", err)
3429func cmdDNSBLCheckhealth(c *cmd) {
3431 c.help = `Check the health of the DNS blocklist represented by zone, e.g. bl.spamcop.net.
3433The health of a DNS blocklist can be checked by querying for 127.0.0.1 and
3434127.0.0.2. The second must and the first must not be present.
3441 zone := xparseDomain(args[0], "zone")
3442 err := dnsbl.CheckHealth(context.Background(), c.log.Logger, dns.StrictResolver{}, zone)
3443 xcheckf(err, "unhealthy")
3444 fmt.Println("healthy")
3447func cmdCheckupdate(c *cmd) {
3448 c.help = `Check if a newer version of mox is available.
3450A single DNS TXT lookup to _updates.xmox.nl tells if a new version is
3451available. If so, a changelog is fetched from https://updates.xmox.nl, and the
3452individual entries verified with a builtin public key. The changelog is
3455 if len(c.Parse()) != 0 {
3460 current, lastknown, _, err := store.LastKnown()
3462 log.Printf("getting last known version: %s", err)
3464 fmt.Printf("last known version: %s\n", lastknown)
3465 fmt.Printf("current version: %s\n", current)
3467 latest, _, err := updates.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dns.Domain{ASCII: changelogDomain})
3468 xcheckf(err, "lookup of latest version")
3469 fmt.Printf("latest version: %s\n", latest)
3471 if latest.After(current) {
3472 changelog, err := updates.FetchChangelog(context.Background(), c.log.Logger, changelogURL, current, changelogPubKey)
3473 xcheckf(err, "fetching changelog")
3474 if len(changelog.Changes) == 0 {
3475 log.Printf("no changes in changelog")
3478 fmt.Println("Changelog")
3479 for _, c := range changelog.Changes {
3480 fmt.Println("\n" + strings.TrimSpace(c.Text))
3485func cmdCid(c *cmd) {
3487 c.help = `Turn an ID from a Received header into a cid, for looking up in logs.
3489A cid is essentially a connection counter initialized when mox starts. Each log
3490line contains a cid. Received headers added by mox contain a unique ID that can
3491be decrypted to a cid by admin of a mox instance only.
3499 recvidpath := mox.DataDirPath("receivedid.key")
3500 recvidbuf, err := os.ReadFile(recvidpath)
3501 xcheckf(err, "reading %s", recvidpath)
3502 if len(recvidbuf) != 16+8 {
3503 log.Fatalf("bad data in %s: got %d bytes, expect 16+8=24", recvidpath, len(recvidbuf))
3505 err = mox.ReceivedIDInit(recvidbuf[:16], recvidbuf[16:])
3506 xcheckf(err, "init receivedid")
3508 cid, err := mox.ReceivedToCid(args[0])
3509 xcheckf(err, "received id to cid")
3510 fmt.Printf("%x\n", cid)
3513func cmdVersion(c *cmd) {
3514 c.help = "Prints this mox version."
3515 if len(c.Parse()) != 0 {
3518 fmt.Println(moxvar.Version)
3519 fmt.Printf("%s/%s\n", runtime.GOOS, runtime.GOARCH)
3522func cmdWebapi(c *cmd) {
3523 c.params = "[$method [$baseurl-with-credentials]"
3524 c.help = "Lists available methods, prints request/response parameters for method, or calls a method with a request read from standard input."
3530 t := reflect.TypeFor[webapi.Methods]()
3531 methods := map[string]reflect.Type{}
3533 for i := range t.NumMethod() {
3535 methods[mt.Name] = mt.Type
3536 ml = append(ml, mt.Name)
3540 fmt.Println(strings.Join(ml, "\n"))
3544 mt, ok := methods[args[0]]
3546 log.Fatalf("unknown method %q", args[0])
3548 resultNotJSON := mt.Out(0).Kind() == reflect.Interface
3551 fmt.Println("# Example request")
3553 printJSON("\t", mox.FillExample(nil, reflect.New(mt.In(1))).Interface())
3556 fmt.Println("Output is non-JSON data.")
3559 fmt.Println("# Example response")
3561 printJSON("\t", mox.FillExample(nil, reflect.New(mt.Out(0))).Interface())
3567 response = reflect.New(mt.Out(0))
3570 fmt.Fprintln(os.Stderr, "reading request from stdin...")
3571 request, err := io.ReadAll(os.Stdin)
3572 xcheckf(err, "read message")
3574 dec := json.NewDecoder(bytes.NewReader(request))
3575 dec.DisallowUnknownFields()
3576 err = dec.Decode(reflect.New(mt.In(1)).Interface())
3577 xcheckf(err, "parsing request")
3579 resp, err := http.PostForm(args[1]+args[0], url.Values{"request": []string{string(request)}})
3580 xcheckf(err, "http post")
3582 if err := resp.Body.Close(); err != nil {
3583 log.Printf("closing http response body: %v", err)
3586 if resp.StatusCode == http.StatusBadRequest {
3587 buf, err := io.ReadAll(&moxio.LimitReader{R: resp.Body, Limit: 10 * 1024})
3588 xcheckf(err, "reading response for 400 bad request error")
3589 err = json.Unmarshal(buf, &response)
3591 printJSON("", response)
3593 fmt.Fprintf(os.Stderr, "(not json)\n")
3594 os.Stderr.Write(buf)
3597 } else if resp.StatusCode != http.StatusOK {
3598 fmt.Fprintf(os.Stderr, "http response %s\n", resp.Status)
3599 _, err := io.Copy(os.Stderr, resp.Body)
3600 xcheckf(err, "copy body")
3602 err := json.NewDecoder(resp.Body).Decode(&resp)
3603 xcheckf(err, "unmarshal response")
3604 printJSON("", response)
3608func printJSON(indent string, v any) {
3609 fmt.Printf("%s", indent)
3610 enc := json.NewEncoder(os.Stdout)
3611 enc.SetIndent(indent, "\t")
3612 enc.SetEscapeHTML(false)
3613 err := enc.Encode(v)
3614 xcheckf(err, "encode json")
3617// 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.
3618func cmdBumpUIDValidity(c *cmd) {
3619 c.params = "$account [$mailbox]"
3620 c.help = `Change the IMAP UID validity of the mailbox, causing IMAP clients to refetch messages.
3622This can be useful after manually repairing metadata about the account/mailbox.
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 {
3633 a, err := store.OpenAccount(c.log, args[0], false)
3634 xcheckf(err, "open account")
3636 if err := a.Close(); err != nil {
3637 log.Printf("closing account: %v", err)
3641 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3642 uidvalidity, err := a.NextUIDValidity(tx)
3644 return fmt.Errorf("assigning next uid validity: %v", err)
3647 q := bstore.QueryTx[store.Mailbox](tx)
3648 q.FilterEqual("Expunged", false)
3650 q.FilterEqual("Name", args[1])
3652 mbl, err := q.SortAsc("Name").List()
3654 return fmt.Errorf("looking up mailbox: %v", err)
3656 if len(args) == 2 && len(mbl) != 1 {
3657 return fmt.Errorf("looking up mailbox %q, found %d mailboxes", args[1], len(mbl))
3659 for _, mb := range mbl {
3660 mb.UIDValidity = uidvalidity
3661 err = tx.Update(&mb)
3663 return fmt.Errorf("updating uid validity for mailbox: %v", err)
3665 fmt.Printf("uid validity for %q updated to %d\n", mb.Name, uidvalidity)
3669 xcheckf(err, "updating database")
3672func cmdReassignUIDs(c *cmd) {
3673 c.params = "$account [$mailboxid]"
3674 c.help = `Reassign UIDs in one mailbox or all mailboxes in an account and bump UID validity, causing IMAP clients to refetch messages.
3676Opens account database file directly. Ensure mox does not have the account
3677open, or is not running.
3680 if len(args) != 1 && len(args) != 2 {
3687 mailboxID, err = strconv.ParseInt(args[1], 10, 64)
3688 xcheckf(err, "parsing mailbox id")
3692 a, err := store.OpenAccount(c.log, args[0], false)
3693 xcheckf(err, "open account")
3695 if err := a.Close(); err != nil {
3696 log.Printf("closing account: %v", err)
3700 // Gather the last-assigned UIDs per mailbox.
3701 uidlasts := map[int64]store.UID{}
3703 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3704 // Reassign UIDs, going per mailbox. We assign starting at 1, only changing the
3705 // message if it isn't already at the intended UID. Doing it in this order ensures
3706 // we don't get into trouble with duplicate UIDs for a mailbox. We assign a new
3707 // modseq. Not strictly needed, but doesn't hurt. It's also why we assign a UID to
3708 // expunged messages.
3709 modseq, err := a.NextModSeq(tx)
3710 xcheckf(err, "assigning next modseq")
3712 q := bstore.QueryTx[store.Message](tx)
3714 q.FilterNonzero(store.Message{MailboxID: mailboxID})
3716 q.SortAsc("MailboxID", "UID")
3717 err = q.ForEach(func(m store.Message) error {
3718 uidlasts[m.MailboxID]++
3719 uid := uidlasts[m.MailboxID]
3723 if err := tx.Update(&m); err != nil {
3724 return fmt.Errorf("updating uid for message: %v", err)
3730 return fmt.Errorf("reading through messages: %v", err)
3733 // Now update the uidnext, uidvalidity and modseq for each mailbox.
3734 err = bstore.QueryTx[store.Mailbox](tx).FilterEqual("Expunged", false).ForEach(func(mb store.Mailbox) error {
3735 // Assign each mailbox a completely new uidvalidity.
3736 uidvalidity, err := a.NextUIDValidity(tx)
3738 return fmt.Errorf("assigning next uid validity: %v", err)
3741 if mb.UIDValidity >= uidvalidity {
3742 // This should not happen, but since we're fixing things up after a hypothetical
3743 // mishap, might as well account for inconsistent uidvalidity.
3744 next := store.NextUIDValidity{ID: 1, Next: mb.UIDValidity + 2}
3745 if err := tx.Update(&next); err != nil {
3746 log.Printf("updating nextuidvalidity: %v, continuing", err)
3750 mb.UIDValidity = uidvalidity
3752 mb.UIDNext = uidlasts[mb.ID] + 1
3754 if err := tx.Update(&mb); err != nil {
3755 return fmt.Errorf("updating uidvalidity and uidnext for mailbox: %v", err)
3760 return fmt.Errorf("updating mailboxes: %v", err)
3764 xcheckf(err, "updating database")
3767func cmdFixUIDMeta(c *cmd) {
3768 c.params = "$account"
3769 c.help = `Fix inconsistent UIDVALIDITY and UIDNEXT in messages/mailboxes/account.
3771The next UID to use for a message in a mailbox should always be higher than any
3772existing message UID in the mailbox. If it is not, the mailbox UIDNEXT is
3775Each mailbox has a UIDVALIDITY sequence number, which should always be lower
3776than the per-account next UIDVALIDITY to use. If it is not, the account next
3777UIDVALIDITY is updated.
3779Opens account database file directly. Ensure mox does not have the account
3780open, or is not running.
3788 a, err := store.OpenAccount(c.log, args[0], false)
3789 xcheckf(err, "open account")
3791 if err := a.Close(); err != nil {
3792 log.Printf("closing account: %v", err)
3796 var maxUIDValidity uint32
3798 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3799 // We look at each mailbox, retrieve its max UID and compare against the mailbox
3801 err := bstore.QueryTx[store.Mailbox](tx).FilterEqual("Expunged", false).ForEach(func(mb store.Mailbox) error {
3802 if mb.UIDValidity > maxUIDValidity {
3803 maxUIDValidity = mb.UIDValidity
3805 m, err := bstore.QueryTx[store.Message](tx).FilterNonzero(store.Message{MailboxID: mb.ID}).SortDesc("UID").Limit(1).Get()
3806 if err == bstore.ErrAbsent || err == nil && m.UID < mb.UIDNext {
3808 } else if err != nil {
3809 return fmt.Errorf("finding message with max uid in mailbox: %w", err)
3811 olduidnext := mb.UIDNext
3812 mb.UIDNext = m.UID + 1
3813 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)
3814 if err := tx.Update(&mb); err != nil {
3815 return fmt.Errorf("updating mailbox uidnext: %v", err)
3820 return fmt.Errorf("processing mailboxes: %v", err)
3823 uidvalidity := store.NextUIDValidity{ID: 1}
3824 if err := tx.Get(&uidvalidity); err != nil {
3825 return fmt.Errorf("reading account next uidvalidity: %v", err)
3827 if maxUIDValidity >= uidvalidity.Next {
3828 log.Printf("account next uidvalidity %d <= highest uidvalidity %d found in mailbox, resetting account next uidvalidity to %d", uidvalidity.Next, maxUIDValidity, maxUIDValidity+1)
3829 uidvalidity.Next = maxUIDValidity + 1
3830 if err := tx.Update(&uidvalidity); err != nil {
3831 return fmt.Errorf("updating account next uidvalidity: %v", err)
3837 xcheckf(err, "updating database")
3840func cmdFixmsgsize(c *cmd) {
3841 c.params = "[$account]"
3842 c.help = `Ensure message sizes in the database matching the sum of the message prefix length and on-disk file size.
3844Messages with an inconsistent size are also parsed again.
3846If an inconsistency is found, you should probably also run "mox
3847bumpuidvalidity" on the mailboxes or entire account to force IMAP clients to
3860 ctlcmdFixmsgsize(xctl(), account)
3863func ctlcmdFixmsgsize(ctl *ctl, account string) {
3864 ctl.xwrite("fixmsgsize")
3867 ctl.xstreamto(os.Stdout)
3870func cmdReparse(c *cmd) {
3871 c.params = "[$account]"
3872 c.help = `Parse all messages in the account or all accounts again.
3874Can be useful after upgrading mox with improved message parsing. Messages are
3875parsed in batches, so other access to the mailboxes/messages are not blocked
3876while reparsing all messages.
3888 ctlcmdReparse(xctl(), account)
3891func ctlcmdReparse(ctl *ctl, account string) {
3892 ctl.xwrite("reparse")
3895 ctl.xstreamto(os.Stdout)
3898func cmdEnsureParsed(c *cmd) {
3899 c.params = "$account"
3900 c.help = "Ensure messages in the database have a pre-parsed MIME form in the database."
3902 c.flag.BoolVar(&all, "all", false, "store new parsed message for all messages")
3909 a, err := store.OpenAccount(c.log, args[0], false)
3910 xcheckf(err, "open account")
3912 if err := a.Close(); err != nil {
3913 log.Printf("closing account: %v", err)
3918 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3919 q := bstore.QueryTx[store.Message](tx)
3920 q.FilterEqual("Expunged", false)
3921 q.FilterFn(func(m store.Message) bool {
3922 return all || m.ParsedBuf == nil
3926 return fmt.Errorf("list messages: %v", err)
3928 for _, m := range l {
3929 mr := a.MessageReader(m)
3930 p, err := message.EnsurePart(c.log.Logger, false, mr, m.Size)
3932 log.Printf("parsing message %d: %v (continuing)", m.ID, err)
3934 m.ParsedBuf, err = json.Marshal(p)
3936 return fmt.Errorf("marshal parsed message: %v", err)
3938 if err := tx.Update(&m); err != nil {
3939 return fmt.Errorf("update message: %v", err)
3945 xcheckf(err, "update messages with parsed mime structure")
3946 fmt.Printf("%d messages updated\n", n)
3949func cmdRecalculateMailboxCounts(c *cmd) {
3950 c.params = "$account"
3951 c.help = `Recalculate message counts for all mailboxes in the account, and total message size for quota.
3953When a message is added to/removed from a mailbox, or when message flags change,
3954the total, unread, unseen and deleted messages are accounted, the total size of
3955the mailbox, and the total message size for the account. In case of a bug in
3956this accounting, the numbers could become incorrect. This command will find, fix
3965 ctlcmdRecalculateMailboxCounts(xctl(), args[0])
3968func ctlcmdRecalculateMailboxCounts(ctl *ctl, account string) {
3969 ctl.xwrite("recalculatemailboxcounts")
3972 ctl.xstreamto(os.Stdout)
3975func cmdMessageParse(c *cmd) {
3976 c.params = "$messagefile"
3977 c.help = "Parse message, print JSON representation."
3980 c.flag.BoolVar(&smtputf8, "smtputf8", false, "check if message needs smtputf8")
3986 f, err := os.Open(args[0])
3987 xcheckf(err, "open")
3989 if err := f.Close(); err != nil {
3990 log.Printf("closing message file: %v", err)
3994 part, err := message.Parse(c.log.Logger, false, f)
3995 xcheckf(err, "parsing message")
3996 err = part.Walk(c.log.Logger, nil)
3997 xcheckf(err, "parsing nested parts")
3998 enc := json.NewEncoder(os.Stdout)
3999 enc.SetIndent("", "\t")
4000 enc.SetEscapeHTML(false)
4001 err = enc.Encode(part)
4002 xcheckf(err, "write")
4005 needs, err := part.NeedsSMTPUTF8()
4006 xcheckf(err, "checking if message needs smtputf8")
4007 fmt.Println("message needs smtputf8:", needs)
4011func cmdOpenaccounts(c *cmd) {
4013 c.params = "$datadir $account ..."
4014 c.help = `Open and close accounts, for triggering data upgrades, for tests.
4016Opens database files directly, not going through a running mox instance.
4024 dataDir := filepath.Clean(args[0])
4025 for _, accName := range args[1:] {
4026 accDir := filepath.Join(dataDir, "accounts", accName)
4027 log.Printf("opening account %s...", accDir)
4028 a, err := store.OpenAccountDB(c.log, accDir, accName)
4029 xcheckf(err, "open account %s", accName)
4030 err = a.ThreadingWait(c.log)
4031 xcheckf(err, "wait for threading upgrade to complete for %s", accName)
4033 xcheckf(err, "close account %s", accName)
4037func cmdReassignthreads(c *cmd) {
4038 c.params = "[$account]"
4039 c.help = `Reassign message threads.
4041For all accounts, or optionally only the specified account.
4043Threading for all messages in an account is first reset, and new base subject
4044and normalized message-id saved with the message. Then all messages are
4045evaluated and matched against their parents/ancestors.
4047Messages are matched based on the References header, with a fall-back to an
4048In-Reply-To header, and if neither is present/valid, based only on base
4051A References header typically points to multiple previous messages in a
4052hierarchy. From oldest ancestor to most recent parent. An In-Reply-To header
4053would have only a message-id of the parent message.
4055A message is only linked to a parent/ancestor if their base subject is the
4056same. This ensures unrelated replies, with a new subject, are placed in their
4059The base subject is lower cased, has whitespace collapsed to a single
4060space, and some components removed: leading "Re:", "Fwd:", "Fw:", or bracketed
4061tag (that mailing lists often add, e.g. "[listname]"), trailing "(fwd)", or
4062enclosing "[fwd: ...]".
4064Messages are linked to all their ancestors. If an intermediate parent/ancestor
4065message is deleted in the future, the message can still be linked to the earlier
4066ancestors. If the direct parent already wasn't available while matching, this is
4067stored as the message having a "missing link" to its stored ancestors.
4079 ctlcmdReassignthreads(xctl(), account)
4082func ctlcmdReassignthreads(ctl *ctl, account string) {
4083 ctl.xwrite("reassignthreads")
4086 ctl.xstreamto(os.Stdout)
4089func cmdIMAPServe(c *cmd) {
4090 c.params = "$preauthaddress"
4091 c.help = `Initiate a preauthenticated IMAP connection on file descriptor 0.
4093For use with tools that can do IMAP over tunneled connections, e.g. with SSH
4094during migrations. TLS is not possible on the connection, and authentication
4095does not require TLS.
4098 c.flag.BoolVar(&fd0, "fd0", false, "write IMAP to file descriptor 0 instead of stdout")
4109 ctlcmdIMAPServe(xctl(), address, os.Stdin, output)
4112func ctlcmdIMAPServe(ctl *ctl, address string, input io.ReadCloser, output io.WriteCloser) {
4113 ctl.xwrite("imapserve")
4117 done := make(chan struct{}, 1)
4122 _, err := io.Copy(output, ctl.conn)
4126 log.Printf("reading from imap: %v", err)
4132 _, err := io.Copy(ctl.conn, input)
4136 log.Printf("writing to imap: %v", err)
4141func cmdReadmessages(c *cmd) {
4143 c.params = "$datadir $account ..."
4144 c.help = `Open account, parse several headers for all messages.
4146For performance testing.
4148Opens database files directly, not going through a running mox instance.
4151 gomaxprocs := runtime.GOMAXPROCS(0)
4152 var procs, workqueuesize, limit int
4153 c.flag.IntVar(&procs, "procs", gomaxprocs, "number of goroutines for reading messages")
4154 c.flag.IntVar(&workqueuesize, "workqueuesize", 2*gomaxprocs, "number of messages to keep in work queue")
4155 c.flag.IntVar(&limit, "limit", 0, "number of messages to process if greater than zero")
4161 type threadPrep struct {
4166 threadingFields := [][]byte{
4167 []byte("references"),
4168 []byte("in-reply-to"),
4171 dataDir := filepath.Clean(args[0])
4172 for _, accName := range args[1:] {
4173 accDir := filepath.Join(dataDir, "accounts", accName)
4174 log.Printf("opening account %s...", accDir)
4175 a, err := store.OpenAccountDB(c.log, accDir, accName)
4176 xcheckf(err, "open account %s", accName)
4178 prepareMessages := func(in, out chan moxio.Work[store.Message, threadPrep]) {
4179 headerbuf := make([]byte, 8*1024)
4180 scratch := make([]byte, 4*1024)
4188 var partialPart struct {
4192 if err := json.Unmarshal(m.ParsedBuf, &partialPart); err != nil {
4193 w.Err = fmt.Errorf("unmarshal part: %v", err)
4195 size := partialPart.BodyOffset - partialPart.HeaderOffset
4196 if int(size) > len(headerbuf) {
4197 headerbuf = make([]byte, size)
4200 buf := headerbuf[:int(size)]
4201 err := func() error {
4202 mr := a.MessageReader(m)
4204 if err := mr.Close(); err != nil {
4205 log.Printf("closing message reader: %v", err)
4209 // ReadAt returns whole buffer or error. Single read should be fast.
4210 n, err := mr.ReadAt(buf, partialPart.HeaderOffset)
4211 if err != nil || n != len(buf) {
4212 return fmt.Errorf("read header: %v", err)
4218 } else if h, err := message.ParseHeaderFields(buf, scratch, threadingFields); err != nil {
4221 w.Out.references = h["References"]
4222 w.Out.inReplyTo = h["In-Reply-To"]
4235 processMessage := func(m store.Message, prep threadPrep) error {
4237 log.Printf("%d messages (delta %s)", n, time.Since(t))
4244 wq := moxio.NewWorkQueue(procs, workqueuesize, prepareMessages, processMessage)
4246 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
4247 q := bstore.QueryTx[store.Message](tx)
4248 q.FilterEqual("Expunged", false)
4253 err = q.ForEach(wq.Add)
4261 xcheckf(err, "processing message")
4264 xcheckf(err, "close account %s", accName)
4265 log.Printf("account %s, total time %s", accName, time.Since(t0))
4269func cmdQueueFillRetired(c *cmd) {
4271 c.help = `Fill retired messag and webhooks queue with testdata.
4273For testing the pagination. Operates directly on queue database.
4276 c.flag.IntVar(&n, "n", 10000, "retired messages and retired webhooks to insert")
4284 xcheckf(err, "init queue")
4285 err = queue.DB.Write(context.Background(), func(tx *bstore.Tx) error {
4288 // Cause autoincrement ID for queue.Msg to be forwarded, and use the reserved ID
4289 // space for inserting retired messages.
4291 err = tx.Insert(&fm)
4292 xcheckf(err, "temporarily insert message to get autoincrement sequence")
4293 err = tx.Delete(&fm)
4294 xcheckf(err, "removing temporary message for resetting autoincrement sequence")
4296 err = tx.Insert(&fm)
4297 xcheckf(err, "temporarily insert message to forward autoincrement sequence")
4298 err = tx.Delete(&fm)
4299 xcheckf(err, "removing temporary message after forwarding autoincrement sequence")
4302 // And likewise for webhooks.
4303 fh := queue.Hook{Account: "x", URL: "x", NextAttempt: time.Now()}
4304 err = tx.Insert(&fh)
4305 xcheckf(err, "temporarily insert webhook to get autoincrement sequence")
4306 err = tx.Delete(&fh)
4307 xcheckf(err, "removing temporary webhook for resetting autoincrement sequence")
4309 err = tx.Insert(&fh)
4310 xcheckf(err, "temporarily insert webhook to forward autoincrement sequence")
4311 err = tx.Delete(&fh)
4312 xcheckf(err, "removing temporary webhook after forwarding autoincrement sequence")
4316 t0 := now.Add(-time.Duration(i) * time.Second)
4317 last := now.Add(-time.Duration(i/10) * time.Second)
4318 mr := queue.MsgRetired{
4319 ID: fm.ID + int64(i),
4321 SenderAccount: "test",
4322 SenderLocalpart: "mox",
4323 SenderDomainStr: "localhost",
4324 FromID: fmt.Sprintf("%016d", i),
4325 RecipientLocalpart: "mox",
4326 RecipientDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "localhost"}},
4327 RecipientDomainStr: "localhost",
4330 Results: []queue.MsgResult{
4333 Duration: time.Millisecond,
4340 Size: int64(i * 100),
4341 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
4342 Subject: fmt.Sprintf("test message %d", i),
4343 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
4345 RecipientAddress: "mox@localhost",
4347 KeepUntil: now.Add(48 * time.Hour),
4349 err := tx.Insert(&mr)
4350 xcheckf(err, "inserting retired message")
4354 t0 := now.Add(-time.Duration(i) * time.Second)
4355 last := now.Add(-time.Duration(i/10) * time.Second)
4360 hr := queue.HookRetired{
4361 ID: fh.ID + int64(i),
4362 QueueMsgID: fm.ID + int64(i),
4363 FromID: fmt.Sprintf("%016d", i),
4364 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
4365 Subject: fmt.Sprintf("test message %d", i),
4366 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
4368 URL: "http://localhost/hook",
4369 IsIncoming: i%10 == 0,
4370 OutgoingEvent: event,
4375 Results: []queue.HookResult{
4378 Duration: time.Millisecond,
4379 URL: "http://localhost/hook",
4388 KeepUntil: now.Add(48 * time.Hour),
4390 err := tx.Insert(&hr)
4391 xcheckf(err, "inserting retired hook")
4396 xcheckf(err, "add to queue")
4397 log.Printf("added %d retired messages and %d retired webhooks", n, n)