11 cryptorand "crypto/rand"
38 "golang.org/x/crypto/bcrypt"
39 "golang.org/x/text/secure/precis"
41 "github.com/mjl-/adns"
43 "github.com/mjl-/autocert"
44 "github.com/mjl-/bstore"
45 "github.com/mjl-/sconf"
46 "github.com/mjl-/sherpa"
48 "github.com/mjl-/mox/admin"
49 "github.com/mjl-/mox/config"
50 "github.com/mjl-/mox/dane"
51 "github.com/mjl-/mox/dkim"
52 "github.com/mjl-/mox/dmarc"
53 "github.com/mjl-/mox/dmarcdb"
54 "github.com/mjl-/mox/dmarcrpt"
55 "github.com/mjl-/mox/dns"
56 "github.com/mjl-/mox/dnsbl"
57 "github.com/mjl-/mox/message"
58 "github.com/mjl-/mox/mlog"
59 "github.com/mjl-/mox/mox-"
60 "github.com/mjl-/mox/moxio"
61 "github.com/mjl-/mox/moxvar"
62 "github.com/mjl-/mox/mtasts"
63 "github.com/mjl-/mox/publicsuffix"
64 "github.com/mjl-/mox/queue"
65 "github.com/mjl-/mox/rdap"
66 "github.com/mjl-/mox/smtp"
67 "github.com/mjl-/mox/smtpclient"
68 "github.com/mjl-/mox/spf"
69 "github.com/mjl-/mox/store"
70 "github.com/mjl-/mox/tlsrpt"
71 "github.com/mjl-/mox/tlsrptdb"
72 "github.com/mjl-/mox/updates"
73 "github.com/mjl-/mox/webadmin"
74 "github.com/mjl-/mox/webapi"
78 changelogDomain = "xmox.nl"
79 changelogURL = "https://updates.xmox.nl/changelog"
80 changelogPubKey = base64Decode("sPNiTDQzvb4FrytNEiebJhgyQzn57RwEjNbGWMM/bDY=")
83func base64Decode(s string) []byte {
84 buf, err := base64.StdEncoding.DecodeString(s)
91func envString(k, def string) string {
99var commands = []struct {
104 {"quickstart", cmdQuickstart},
106 {"setaccountpassword", cmdSetaccountpassword},
107 {"setadminpassword", cmdSetadminpassword},
108 {"loglevels", cmdLoglevels},
109 {"queue holdrules list", cmdQueueHoldrulesList},
110 {"queue holdrules add", cmdQueueHoldrulesAdd},
111 {"queue holdrules remove", cmdQueueHoldrulesRemove},
112 {"queue list", cmdQueueList},
113 {"queue hold", cmdQueueHold},
114 {"queue unhold", cmdQueueUnhold},
115 {"queue schedule", cmdQueueSchedule},
116 {"queue transport", cmdQueueTransport},
117 {"queue requiretls", cmdQueueRequireTLS},
118 {"queue fail", cmdQueueFail},
119 {"queue drop", cmdQueueDrop},
120 {"queue dump", cmdQueueDump},
121 {"queue retired list", cmdQueueRetiredList},
122 {"queue retired print", cmdQueueRetiredPrint},
123 {"queue suppress list", cmdQueueSuppressList},
124 {"queue suppress add", cmdQueueSuppressAdd},
125 {"queue suppress remove", cmdQueueSuppressRemove},
126 {"queue suppress lookup", cmdQueueSuppressLookup},
127 {"queue webhook list", cmdQueueHookList},
128 {"queue webhook schedule", cmdQueueHookSchedule},
129 {"queue webhook cancel", cmdQueueHookCancel},
130 {"queue webhook print", cmdQueueHookPrint},
131 {"queue webhook retired list", cmdQueueHookRetiredList},
132 {"queue webhook retired print", cmdQueueHookRetiredPrint},
133 {"import maildir", cmdImportMaildir},
134 {"import mbox", cmdImportMbox},
135 {"export maildir", cmdExportMaildir},
136 {"export mbox", cmdExportMbox},
137 {"localserve", cmdLocalserve},
139 {"backup", cmdBackup},
140 {"verifydata", cmdVerifydata},
141 {"licenses", cmdLicenses},
143 {"config test", cmdConfigTest},
144 {"config dnscheck", cmdConfigDNSCheck},
145 {"config dnsrecords", cmdConfigDNSRecords},
146 {"config describe-domains", cmdConfigDescribeDomains},
147 {"config describe-static", cmdConfigDescribeStatic},
148 {"config account add", cmdConfigAccountAdd},
149 {"config account rm", cmdConfigAccountRemove},
150 {"config account disable", cmdConfigAccountDisable},
151 {"config account enable", cmdConfigAccountEnable},
152 {"config address add", cmdConfigAddressAdd},
153 {"config address rm", cmdConfigAddressRemove},
154 {"config domain add", cmdConfigDomainAdd},
155 {"config domain rm", cmdConfigDomainRemove},
156 {"config domain disable", cmdConfigDomainDisable},
157 {"config domain enable", cmdConfigDomainEnable},
158 {"config tlspubkey list", cmdConfigTlspubkeyList},
159 {"config tlspubkey get", cmdConfigTlspubkeyGet},
160 {"config tlspubkey add", cmdConfigTlspubkeyAdd},
161 {"config tlspubkey rm", cmdConfigTlspubkeyRemove},
162 {"config tlspubkey gen", cmdConfigTlspubkeyGen},
163 {"config alias list", cmdConfigAliasList},
164 {"config alias print", cmdConfigAliasPrint},
165 {"config alias add", cmdConfigAliasAdd},
166 {"config alias update", cmdConfigAliasUpdate},
167 {"config alias rm", cmdConfigAliasRemove},
168 {"config alias addaddr", cmdConfigAliasAddaddr},
169 {"config alias rmaddr", cmdConfigAliasRemoveaddr},
171 {"config describe-sendmail", cmdConfigDescribeSendmail},
172 {"config printservice", cmdConfigPrintservice},
173 {"config ensureacmehostprivatekeys", cmdConfigEnsureACMEHostprivatekeys},
174 {"config example", cmdConfigExample},
176 {"admin imapserve", cmdIMAPServe},
178 {"checkupdate", cmdCheckupdate},
180 {"clientconfig", cmdClientConfig},
181 {"deliver", cmdDeliver},
182 // 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
183 {"dane dial", cmdDANEDial},
184 {"dane dialmx", cmdDANEDialmx},
185 {"dane makerecord", cmdDANEMakeRecord},
186 {"dns lookup", cmdDNSLookup},
187 {"dkim gened25519", cmdDKIMGened25519},
188 {"dkim genrsa", cmdDKIMGenrsa},
189 {"dkim lookup", cmdDKIMLookup},
190 {"dkim txt", cmdDKIMTXT},
191 {"dkim verify", cmdDKIMVerify},
192 {"dkim sign", cmdDKIMSign},
193 {"dmarc lookup", cmdDMARCLookup},
194 {"dmarc parsereportmsg", cmdDMARCParsereportmsg},
195 {"dmarc verify", cmdDMARCVerify},
196 {"dmarc checkreportaddrs", cmdDMARCCheckreportaddrs},
197 {"dnsbl check", cmdDNSBLCheck},
198 {"dnsbl checkhealth", cmdDNSBLCheckhealth},
199 {"mtasts lookup", cmdMTASTSLookup},
200 {"rdap domainage", cmdRDAPDomainage},
201 {"retrain", cmdRetrain},
202 {"sendmail", cmdSendmail},
203 {"spf check", cmdSPFCheck},
204 {"spf lookup", cmdSPFLookup},
205 {"spf parse", cmdSPFParse},
206 {"tlsrpt lookup", cmdTLSRPTLookup},
207 {"tlsrpt parsereportmsg", cmdTLSRPTParsereportmsg},
208 {"version", cmdVersion},
209 {"webapi", cmdWebapi},
211 {"example", cmdExample},
212 {"bumpuidvalidity", cmdBumpUIDValidity},
213 {"reassignuids", cmdReassignUIDs},
214 {"fixuidmeta", cmdFixUIDMeta},
215 {"fixmsgsize", cmdFixmsgsize},
216 {"reparse", cmdReparse},
217 {"ensureparsed", cmdEnsureParsed},
218 {"recalculatemailboxcounts", cmdRecalculateMailboxCounts},
219 {"message parse", cmdMessageParse},
220 {"reassignthreads", cmdReassignthreads},
223 {"helpall", cmdHelpall},
224 {"junk analyze", cmdJunkAnalyze},
225 {"junk check", cmdJunkCheck},
226 {"junk play", cmdJunkPlay},
227 {"junk test", cmdJunkTest},
228 {"junk train", cmdJunkTrain},
229 {"dmarcdb addreport", cmdDMARCDBAddReport},
230 {"tlsrptdb addreport", cmdTLSRPTDBAddReport},
231 {"updates addsigned", cmdUpdatesAddSigned},
232 {"updates genkey", cmdUpdatesGenkey},
233 {"updates pubkey", cmdUpdatesPubkey},
234 {"updates serve", cmdUpdatesServe},
235 {"updates verify", cmdUpdatesVerify},
236 {"gentestdata", cmdGentestdata},
237 {"ximport maildir", cmdXImportMaildir},
238 {"ximport mbox", cmdXImportMbox},
239 {"openaccounts", cmdOpenaccounts},
240 {"readmessages", cmdReadmessages},
241 {"queuefillretired", cmdQueueFillRetired},
247 for _, xc := range commands {
248 c := cmd{words: strings.Split(xc.cmd, " "), fn: xc.fn}
249 cmds = append(cmds, c)
257 // Set before calling command.
260 _gather bool // Set when using Parse to gather usage for a command.
262 // Set by invoked command or Parse.
263 unlisted bool // If set, command is not listed until at least some words are matched from command.
264 params string // Arguments to command. Multiple lines possible.
265 help string // Additional explanation. First line is synopsis, the rest is only printed for an explicit help/usage for that command.
271func (c *cmd) Parse() []string {
272 // To gather params and usage information, we just run the command but cause this
273 // panic after the command has registered its flags and set its params and help
274 // information. This is then caught and that info printed.
279 c.flag.Usage = c.Usage
280 c.flag.Parse(c.flagArgs)
281 c.args = c.flag.Args()
285func (c *cmd) gather() {
286 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
290 // panic generated by Parse.
298func (c *cmd) makeUsage() string {
299 var r strings.Builder
300 cs := "mox " + strings.Join(c.words, " ")
301 for i, line := range strings.Split(strings.TrimSpace(c.params), "\n") {
309 fmt.Fprintf(&r, "%6s %s%s\n", s, cs, line)
312 c.flag.PrintDefaults()
316func (c *cmd) printUsage() {
317 fmt.Fprint(os.Stderr, c.makeUsage())
319 fmt.Fprint(os.Stderr, "\n"+c.help+"\n")
323func (c *cmd) Usage() {
328func cmdHelp(c *cmd) {
329 c.params = "[command ...]"
330 c.help = `Prints help about matching commands.
332If multiple commands match, they are listed along with the first line of their help text.
333If a single command matches, its usage and full help text is printed.
340 prefix := func(l, pre []string) bool {
341 if len(pre) > len(l) {
344 return slices.Equal(pre, l[:len(pre)])
348 for _, c := range cmds {
349 if slices.Equal(c.words, args) {
351 fmt.Print(c.makeUsage())
353 fmt.Print("\n" + c.help + "\n")
356 } else if prefix(c.words, args) {
357 partial = append(partial, c)
360 if len(partial) == 0 {
361 fmt.Fprintf(os.Stderr, "%s: unknown command\n", strings.Join(args, " "))
364 for _, c := range partial {
366 line := "mox " + strings.Join(c.words, " ")
367 fmt.Printf("%s\n", line)
369 fmt.Printf("\t%s\n", strings.Split(c.help, "\n")[0])
374func cmdHelpall(c *cmd) {
376 c.help = `Print all detailed usage and help information for all listed commands.
378Used to generate documentation.
386 for _, c := range cmds {
392 fmt.Fprintf(os.Stderr, "\n")
396 fmt.Fprintf(os.Stderr, "# mox %s\n\n", strings.Join(c.words, " "))
398 fmt.Fprintln(os.Stderr, c.help+"\n")
401 s = "\t" + strings.ReplaceAll(s, "\n", "\n\t")
402 fmt.Fprintln(os.Stderr, s)
406func usage(l []cmd, unlisted bool) {
409 lines = append(lines, "mox [-config config/mox.conf] [-pedantic] ...")
411 for _, c := range l {
413 if c.unlisted && !unlisted {
416 for _, line := range strings.Split(c.params, "\n") {
417 x := append([]string{"mox"}, c.words...)
421 lines = append(lines, strings.Join(x, " "))
424 for i, line := range lines {
429 fmt.Fprintln(os.Stderr, pre+line)
434var loglevel string // Empty will be interpreted as info, except by localserve.
437// subcommands that are not "serve" should use this function to load the config, it
438// restores any loglevel specified on the command-line, instead of using the
439// loglevels from the config file and it does not load files like TLS keys/certs.
440func mustLoadConfig() {
441 mox.MustLoadConfig(false, false)
446 if level, ok := mlog.Levels[ll]; ok {
447 mox.Conf.Log[""] = level
448 mlog.SetConfig(mox.Conf.Log)
450 log.Fatal("unknown loglevel", slog.String("loglevel", loglevel))
453 mox.SetPedantic(true)
458 // CheckConsistencyOnClose is true by default, for all the test packages. A regular
459 // mox server should never use it. But integration tests enable it again with a
461 store.CheckConsistencyOnClose = false
463 ctxbg := context.Background()
469 // If invoked as sendmail, e.g. /usr/sbin/sendmail, we do enough so cron can get a
470 // message sent using smtp submission to a configured server.
471 if len(os.Args) > 0 && filepath.Base(os.Args[0]) == "sendmail" {
473 flag: flag.NewFlagSet("sendmail", flag.ExitOnError),
474 flagArgs: os.Args[1:],
475 log: mlog.New("sendmail", nil),
481 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")
482 flag.StringVar(&loglevel, "loglevel", "", "if non-empty, this log level is set early in startup")
483 flag.BoolVar(&pedantic, "pedantic", false, "protocol violations result in errors instead of accepting/working around them")
484 flag.BoolVar(&store.CheckConsistencyOnClose, "checkconsistency", false, "dangerous option for testing only, enables data checks that abort/panic when inconsistencies are found")
486 var cpuprofile, memprofile, tracefile string
487 flag.StringVar(&cpuprofile, "cpuprof", "", "store cpu profile to file")
488 flag.StringVar(&memprofile, "memprof", "", "store mem profile to file")
489 flag.StringVar(&tracefile, "trace", "", "store execution trace to file")
491 flag.Usage = func() { usage(cmds, false) }
499 defer traceExecution(tracefile)()
501 defer profile(cpuprofile, memprofile)()
504 mox.SetPedantic(true)
507 mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
512 if level, ok := mlog.Levels[ll]; ok {
513 mox.Conf.Log[""] = level
514 mlog.SetConfig(mox.Conf.Log)
515 // note: SetConfig may be called again when subcommands loads config.
517 log.Fatalf("unknown loglevel %q", loglevel)
522 for _, c := range cmds {
523 for i, w := range c.words {
524 if i >= len(args) || w != args[i] {
526 partial = append(partial, c)
531 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
532 c.flagArgs = args[len(c.words):]
533 c.log = mlog.New(strings.Join(c.words, ""), nil)
537 if len(partial) > 0 {
543func xcheckf(err error, format string, args ...any) {
547 msg := fmt.Sprintf(format, args...)
548 log.Fatalf("%s: %s", msg, err)
551func xparseIP(s, what string) net.IP {
554 log.Fatalf("invalid %s: %q", what, s)
559func xparseDomain(s, what string) dns.Domain {
560 d, err := dns.ParseDomain(s)
561 xcheckf(err, "parsing %s %q", what, s)
565func cmdClientConfig(c *cmd) {
567 c.help = `Print the configuration for email clients for a domain.
569Sending email is typically not done on the SMTP port 25, but on submission
570ports 465 (with TLS) and 587 (without initial TLS, but usually added to the
571connection with STARTTLS). For IMAP, the port with TLS is 993 and without is
574Without TLS/STARTTLS, passwords are sent in clear text, which should only be
575configured over otherwise secured connections, like a VPN.
581 d := xparseDomain(args[0], "domain")
586func printClientConfig(d dns.Domain) {
587 cc, err := admin.ClientConfigsDomain(d)
588 xcheckf(err, "getting client config")
589 fmt.Printf("%-20s %-30s %5s %-15s %s\n", "Protocol", "Host", "Port", "Listener", "Note")
590 for _, e := range cc.Entries {
591 fmt.Printf("%-20s %-30s %5d %-15s %s\n", e.Protocol, e.Host, e.Port, e.Listener, e.Note)
594To prevent authentication mechanism downgrade attempts that may result in
595clients sending plain text passwords to a MitM, clients should always be
596explicitly configured with the most secure authentication mechanism supported,
597the first of: SCRAM-SHA-256-PLUS, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, SCRAM-SHA-1,
602func cmdConfigTest(c *cmd) {
603 c.help = `Parses and validates the configuration files.
605If valid, the command exits with status 0. If not valid, all errors encountered
613 mox.FilesImmediate = true
615 _, errs := mox.ParseConfig(context.Background(), c.log, mox.ConfigStaticPath, true, true, false)
617 log.Printf("multiple errors:")
618 for _, err := range errs {
619 log.Printf("%s", err)
622 } else if len(errs) == 1 {
623 log.Fatalf("%s", errs[0])
626 fmt.Println("config OK")
629func cmdConfigDescribeStatic(c *cmd) {
630 c.params = ">mox.conf"
631 c.help = `Prints an annotated empty configuration for use as mox.conf.
633The static configuration file cannot be reloaded while mox is running. Mox has
634to be restarted for changes to the static configuration file to take effect.
636This configuration file needs modifications to make it valid. For example, it
637may contain unfinished list items.
639 if len(c.Parse()) != 0 {
644 err := sconf.Describe(os.Stdout, &sc)
645 xcheckf(err, "describing config")
648func cmdConfigDescribeDomains(c *cmd) {
649 c.params = ">domains.conf"
650 c.help = `Prints an annotated empty configuration for use as domains.conf.
652The domains configuration file contains the domains and their configuration,
653and accounts and their configuration. This includes the configured email
654addresses. The mox admin web interface, and the mox command line interface, can
655make changes to this file. Mox automatically reloads this file when it changes.
657Like the static configuration, the example domains.conf printed by this command
658needs modifications to make it valid.
660 if len(c.Parse()) != 0 {
664 var dc config.Dynamic
665 err := sconf.Describe(os.Stdout, &dc)
666 xcheckf(err, "describing config")
669func cmdConfigPrintservice(c *cmd) {
670 c.params = ">mox.service"
671 c.help = `Prints a systemd unit service file for mox.
673This is the same file as generated using quickstart. If the systemd service file
674has changed with a newer version of mox, use this command to generate an up to
677 if len(c.Parse()) != 0 {
681 pwd, err := os.Getwd()
683 log.Printf("current working directory: %v", err)
686 service := strings.ReplaceAll(moxService, "/home/mox", pwd)
690func cmdConfigDomainAdd(c *cmd) {
691 c.params = "[-disabled] domain account [localpart]"
692 c.help = `Adds a new domain to the configuration and reloads the configuration.
694The account is used for the postmaster mailboxes the domain, including as DMARC and
695TLS reporting. Localpart is the "username" at the domain for this account. If
696must be set if and only if account does not yet exist.
698The domain can be created in disabled mode, preventing automatically requesting
699TLS certificates with ACME, and rejecting incoming/outgoing messages involving
700the domain, but allowing further configuration of the domain.
703 c.flag.BoolVar(&disabled, "disabled", false, "disable the new domain")
705 if len(args) != 2 && len(args) != 3 {
709 d := xparseDomain(args[0], "domain")
711 var localpart smtp.Localpart
714 localpart, err = smtp.ParseLocalpart(args[2])
715 xcheckf(err, "parsing localpart")
717 ctlcmdConfigDomainAdd(xctl(), disabled, d, args[1], localpart)
720func ctlcmdConfigDomainAdd(ctl *ctl, disabled bool, domain dns.Domain, account string, localpart smtp.Localpart) {
721 ctl.xwrite("domainadd")
727 ctl.xwrite(domain.Name())
729 ctl.xwrite(string(localpart))
731 fmt.Printf("domain added, remember to add dns records, see:\n\nmox config dnsrecords %s\nmox config dnscheck %s\n", domain.Name(), domain.Name())
734func cmdConfigDomainRemove(c *cmd) {
736 c.help = `Remove a domain from the configuration and reload the configuration.
738This is a dangerous operation. Incoming email delivery for this domain will be
746 d := xparseDomain(args[0], "domain")
748 ctlcmdConfigDomainRemove(xctl(), d)
751func ctlcmdConfigDomainRemove(ctl *ctl, d dns.Domain) {
752 ctl.xwrite("domainrm")
755 fmt.Printf("domain removed, remember to remove dns records for %s\n", d)
758func cmdConfigDomainDisable(c *cmd) {
760 c.help = `Disable a domain and reload the configuration.
762This is a dangerous operation. Incoming/outgoing messages involving this domain
770 d := xparseDomain(args[0], "domain")
772 ctlcmdConfigDomainDisabled(xctl(), d, true)
773 fmt.Printf("domain disabled")
776func cmdConfigDomainEnable(c *cmd) {
778 c.help = `Enable a domain and reload the configuration.
780Incoming/outgoing messages involving this domain will be accepted again.
787 d := xparseDomain(args[0], "domain")
789 ctlcmdConfigDomainDisabled(xctl(), d, false)
792func ctlcmdConfigDomainDisabled(ctl *ctl, d dns.Domain, disabled bool) {
793 ctl.xwrite("domaindisabled")
803func cmdConfigAliasList(c *cmd) {
805 c.help = `Show aliases (lists) for domain.`
812 ctlcmdConfigAliasList(xctl(), args[0])
815func ctlcmdConfigAliasList(ctl *ctl, address string) {
816 ctl.xwrite("aliaslist")
819 ctl.xstreamto(os.Stdout)
822func cmdConfigAliasPrint(c *cmd) {
824 c.help = `Print settings and members of alias (list).`
831 ctlcmdConfigAliasPrint(xctl(), args[0])
834func ctlcmdConfigAliasPrint(ctl *ctl, address string) {
835 ctl.xwrite("aliasprint")
838 ctl.xstreamto(os.Stdout)
841func cmdConfigAliasAdd(c *cmd) {
842 c.params = "alias@domain rcpt1@domain ..."
843 c.help = `Add new alias (list) with one or more addresses and public posting enabled.
845An alias is used for delivering incoming email to multiple recipients. If you
846want to add an address to an account, don't use an alias, just add the address
854 alias := config.Alias{PostPublic: true, Addresses: args[1:]}
857 ctlcmdConfigAliasAdd(xctl(), args[0], alias)
860func ctlcmdConfigAliasAdd(ctl *ctl, address string, alias config.Alias) {
861 ctl.xwrite("aliasadd")
863 xctlwriteJSON(ctl, alias)
867func cmdConfigAliasUpdate(c *cmd) {
868 c.params = "alias@domain [-postpublic false|true -listmembers false|true -allowmsgfrom false|true]"
869 c.help = `Update alias (list) configuration.`
870 var postpublic, listmembers, allowmsgfrom string
871 c.flag.StringVar(&postpublic, "postpublic", "", "whether anyone or only list members can post")
872 c.flag.StringVar(&listmembers, "listmembers", "", "whether list members can list members")
873 c.flag.StringVar(&allowmsgfrom, "allowmsgfrom", "", "whether alias address can be used in message from header")
881 ctlcmdConfigAliasUpdate(xctl(), alias, postpublic, listmembers, allowmsgfrom)
884func ctlcmdConfigAliasUpdate(ctl *ctl, alias, postpublic, listmembers, allowmsgfrom string) {
885 ctl.xwrite("aliasupdate")
887 ctl.xwrite(postpublic)
888 ctl.xwrite(listmembers)
889 ctl.xwrite(allowmsgfrom)
893func cmdConfigAliasRemove(c *cmd) {
894 c.params = "alias@domain"
895 c.help = "Remove alias (list)."
902 ctlcmdConfigAliasRemove(xctl(), args[0])
905func ctlcmdConfigAliasRemove(ctl *ctl, alias string) {
906 ctl.xwrite("aliasrm")
911func cmdConfigAliasAddaddr(c *cmd) {
912 c.params = "alias@domain rcpt1@domain ..."
913 c.help = `Add addresses to alias (list).`
920 ctlcmdConfigAliasAddaddr(xctl(), args[0], args[1:])
923func ctlcmdConfigAliasAddaddr(ctl *ctl, alias string, addresses []string) {
924 ctl.xwrite("aliasaddaddr")
926 xctlwriteJSON(ctl, addresses)
930func cmdConfigAliasRemoveaddr(c *cmd) {
931 c.params = "alias@domain rcpt1@domain ..."
932 c.help = `Remove addresses from alias (list).`
939 ctlcmdConfigAliasRmaddr(xctl(), args[0], args[1:])
942func ctlcmdConfigAliasRmaddr(ctl *ctl, alias string, addresses []string) {
943 ctl.xwrite("aliasrmaddr")
945 xctlwriteJSON(ctl, addresses)
949func cmdConfigAccountAdd(c *cmd) {
950 c.params = "account address"
951 c.help = `Add an account with an email address and reload the configuration.
953Email can be delivered to this address/account. A password has to be configured
954explicitly, see the setaccountpassword command.
962 ctlcmdConfigAccountAdd(xctl(), args[0], args[1])
965func ctlcmdConfigAccountAdd(ctl *ctl, account, address string) {
966 ctl.xwrite("accountadd")
970 fmt.Printf("account added, set a password with \"mox setaccountpassword %s\"\n", account)
973func cmdConfigAccountRemove(c *cmd) {
975 c.help = `Remove an account and reload the configuration.
977Email addresses for this account will also be removed, and incoming email for
978these addresses will be rejected.
980All data for the account will be removed.
988 ctlcmdConfigAccountRemove(xctl(), args[0])
991func ctlcmdConfigAccountRemove(ctl *ctl, account string) {
992 ctl.xwrite("accountrm")
995 fmt.Println("account removed")
998func cmdConfigAccountDisable(c *cmd) {
999 c.params = "account message"
1000 c.help = `Disable login for an account, showing message to users when they try to login.
1002Incoming email will still be accepted for the account, and queued email from the
1003account will still be delivered. No new login sessions are possible.
1005Message must be non-empty, ascii-only without control characters including
1006newline, and maximum 256 characters because it is used in SMTP/IMAP.
1013 log.Fatalf("message must be non-empty")
1017 ctlcmdConfigAccountDisabled(xctl(), args[0], args[1])
1018 fmt.Println("account disabled")
1021func cmdConfigAccountEnable(c *cmd) {
1022 c.params = "account"
1023 c.help = `Enable login again for an account.
1025Login attempts by the user no long result in an error message.
1033 ctlcmdConfigAccountDisabled(xctl(), args[0], "")
1034 fmt.Println("account enabled")
1037func ctlcmdConfigAccountDisabled(ctl *ctl, account, loginDisabled string) {
1038 ctl.xwrite("accountdisabled")
1040 ctl.xwrite(loginDisabled)
1044func cmdConfigTlspubkeyList(c *cmd) {
1045 c.params = "[account]"
1046 c.help = `List TLS public keys for TLS client certificate authentication.
1048If account is absent, the TLS public keys for all accounts are listed.
1051 var accountOpt string
1053 accountOpt = args[0]
1054 } else if len(args) > 1 {
1059 ctlcmdConfigTlspubkeyList(xctl(), accountOpt)
1062func ctlcmdConfigTlspubkeyList(ctl *ctl, accountOpt string) {
1063 ctl.xwrite("tlspubkeylist")
1064 ctl.xwrite(accountOpt)
1066 ctl.xstreamto(os.Stdout)
1069func cmdConfigTlspubkeyGet(c *cmd) {
1070 c.params = "fingerprint"
1071 c.help = `Get a TLS public key for a fingerprint.
1073Prints the type, name, account and address for the key, and the certificate in
1082 ctlcmdConfigTlspubkeyGet(xctl(), args[0])
1085func ctlcmdConfigTlspubkeyGet(ctl *ctl, fingerprint string) {
1086 ctl.xwrite("tlspubkeyget")
1087 ctl.xwrite(fingerprint)
1091 account := ctl.xread()
1092 address := ctl.xread()
1093 noimappreauth := ctl.xread()
1097 var block *pem.Block
1100 Type: "CERTIFICATE",
1105 fmt.Printf("type: %s\nname: %s\naccount: %s\naddress: %s\nno imap preauth: %s\n", typ, name, account, address, noimappreauth)
1107 fmt.Printf("certificate:\n\n")
1108 pem.Encode(os.Stdout, block)
1112func cmdConfigTlspubkeyAdd(c *cmd) {
1113 c.params = "address [name] < cert.pem"
1114 c.help = `Add a TLS public key to the account of the given address.
1116The public key is read from the certificate.
1118The optional name is a human-readable descriptive name of the key. If absent,
1119the CommonName from the certificate is used.
1121 var noimappreauth bool
1122 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.")
1124 var address, name string
1127 } else if len(args) == 2 {
1128 address, name = args[0], args[1]
1133 buf, err := io.ReadAll(os.Stdin)
1134 xcheckf(err, "reading from stdin")
1135 block, _ := pem.Decode(buf)
1137 err = errors.New("no pem block found")
1138 } else if block.Type != "CERTIFICATE" {
1139 err = fmt.Errorf("unexpected type %q, expected CERTIFICATE", block.Type)
1141 xcheckf(err, "parsing pem")
1144 ctlcmdConfigTlspubkeyAdd(xctl(), address, name, noimappreauth, block.Bytes)
1147func ctlcmdConfigTlspubkeyAdd(ctl *ctl, address, name string, noimappreauth bool, certDER []byte) {
1148 ctl.xwrite("tlspubkeyadd")
1151 ctl.xwrite(fmt.Sprintf("%v", noimappreauth))
1152 ctl.xstreamfrom(bytes.NewReader(certDER))
1156func cmdConfigTlspubkeyRemove(c *cmd) {
1157 c.params = "fingerprint"
1158 c.help = `Remove TLS public key for fingerprint.`
1165 ctlcmdConfigTlspubkeyRemove(xctl(), args[0])
1168func ctlcmdConfigTlspubkeyRemove(ctl *ctl, fingerprint string) {
1169 ctl.xwrite("tlspubkeyrm")
1170 ctl.xwrite(fingerprint)
1174func cmdConfigTlspubkeyGen(c *cmd) {
1176 c.help = `Generate an ed25519 private key and minimal certificate for use a TLS public key and write to files starting with stem.
1178The private key is written to $stem.$timestamp.ed25519privatekey.pkcs8.pem.
1179The certificate is written to $stem.$timestamp.certificate.pem.
1180The private key and certificate are also written to
1181$stem.$timestamp.ed25519privatekey-certificate.pem.
1183The certificate can be added to an account with "mox config account tlspubkey add".
1185The combined file can be used with "mox sendmail".
1187The private key is also written to standard error in raw-url-base64-encoded
1188form, also for use with "mox sendmail". The fingerprint is written to standard
1189error too, for reference.
1197 timestamp := time.Now().Format("200601021504")
1198 prefix := stem + "." + timestamp
1200 seed := make([]byte, ed25519.SeedSize)
1201 if _, err := cryptorand.Read(seed); err != nil {
1204 privKey := ed25519.NewKeyFromSeed(seed)
1205 privKeyBuf, err := x509.MarshalPKCS8PrivateKey(privKey)
1206 xcheckf(err, "marshal private key as pkcs8")
1208 err = pem.Encode(&b, &pem.Block{Type: "PRIVATE KEY", Bytes: privKeyBuf})
1209 xcheckf(err, "marshal pkcs8 private key to pem")
1210 privKeyBufPEM := b.Bytes()
1212 certBuf, tlsCert := xminimalCert(privKey)
1214 err = pem.Encode(&b, &pem.Block{Type: "CERTIFICATE", Bytes: certBuf})
1215 xcheckf(err, "marshal certificate to pem")
1216 certBufPEM := b.Bytes()
1218 xwriteFile := func(p string, data []byte, what string) {
1219 log.Printf("writing %s", p)
1220 err = os.WriteFile(p, data, 0600)
1221 xcheckf(err, "writing %s file: %v", what, err)
1224 xwriteFile(prefix+".ed25519privatekey.pkcs8.pem", privKeyBufPEM, "private key")
1225 xwriteFile(prefix+".certificate.pem", certBufPEM, "certificate")
1226 combinedPEM := append(append([]byte{}, privKeyBufPEM...), certBufPEM...)
1227 xwriteFile(prefix+".ed25519privatekey-certificate.pem", combinedPEM, "combined private key and certificate")
1229 shabuf := sha256.Sum256(tlsCert.Leaf.RawSubjectPublicKeyInfo)
1231 _, err = fmt.Fprintf(os.Stderr, "ed25519 private key as raw-url-base64: %s\ned25519 public key fingerprint: %s\n",
1232 base64.RawURLEncoding.EncodeToString(seed),
1233 base64.RawURLEncoding.EncodeToString(shabuf[:]),
1235 xcheckf(err, "write private key and public key fingerprint")
1238func cmdConfigAddressAdd(c *cmd) {
1239 c.params = "address account"
1240 c.help = `Adds an address to an account and reloads the configuration.
1242If address starts with a @ (i.e. a missing localpart), this is a catchall
1243address for the domain.
1251 ctlcmdConfigAddressAdd(xctl(), args[0], args[1])
1254func ctlcmdConfigAddressAdd(ctl *ctl, address, account string) {
1255 ctl.xwrite("addressadd")
1259 fmt.Println("address added")
1262func cmdConfigAddressRemove(c *cmd) {
1263 c.params = "address"
1264 c.help = `Remove an address and reload the configuration.
1266Incoming email for this address will be rejected after removing an address.
1274 ctlcmdConfigAddressRemove(xctl(), args[0])
1277func ctlcmdConfigAddressRemove(ctl *ctl, address string) {
1278 ctl.xwrite("addressrm")
1281 fmt.Println("address removed")
1284func cmdConfigDNSRecords(c *cmd) {
1286 c.help = `Prints annotated DNS records as zone file that should be created for the domain.
1288The zone file can be imported into existing DNS software. You should review the
1289DNS records, especially if your domain previously/currently has email
1297 d := xparseDomain(args[0], "domain")
1299 domConf, ok := mox.Conf.Domain(d)
1301 log.Fatalf("unknown domain")
1304 resolver := dns.StrictResolver{Pkg: "main"}
1305 _, result, err := resolver.LookupTXT(context.Background(), d.ASCII+".")
1306 if !dns.IsNotFound(err) {
1307 xcheckf(err, "looking up record for dnssec-status")
1310 var certIssuerDomainName, acmeAccountURI string
1311 public := mox.Conf.Static.Listeners["public"]
1312 if public.TLS != nil && public.TLS.ACME != "" {
1313 acme, ok := mox.Conf.Static.ACME[public.TLS.ACME]
1314 if ok && acme.Manager.Manager.Client != nil {
1315 certIssuerDomainName = acme.IssuerDomainName
1316 acc, err := acme.Manager.Manager.Client.GetReg(context.Background(), "")
1317 c.log.Check(err, "get public acme account")
1319 acmeAccountURI = acc.URI
1324 records, err := admin.DomainRecords(domConf, d, result.Authentic, certIssuerDomainName, acmeAccountURI)
1325 xcheckf(err, "records")
1326 fmt.Print(strings.Join(records, "\n") + "\n")
1329func cmdConfigDNSCheck(c *cmd) {
1331 c.help = "Check the DNS records with the configuration for the domain, and print any errors/warnings."
1337 d := xparseDomain(args[0], "domain")
1339 _, ok := mox.Conf.Domain(d)
1341 log.Fatalf("unknown domain")
1344 // todo future: move http.Admin.CheckDomain to mox- and make it return a regular error.
1350 err, ok := x.(*sherpa.Error)
1354 log.Fatalf("%s", err)
1357 printResult := func(name string, r webadmin.Result) {
1358 if len(r.Errors) == 0 && len(r.Warnings) == 0 {
1361 fmt.Printf("# %s\n", name)
1362 for _, s := range r.Errors {
1363 fmt.Printf("error: %s\n", s)
1365 for _, s := range r.Warnings {
1366 fmt.Printf("warning: %s\n", s)
1370 result := webadmin.Admin{}.CheckDomain(context.Background(), args[0])
1371 printResult("DNSSEC", result.DNSSEC.Result)
1372 printResult("IPRev", result.IPRev.Result)
1373 printResult("MX", result.MX.Result)
1374 printResult("TLS", result.TLS.Result)
1375 printResult("DANE", result.DANE.Result)
1376 printResult("SPF", result.SPF.Result)
1377 printResult("DKIM", result.DKIM.Result)
1378 printResult("DMARC", result.DMARC.Result)
1379 printResult("Host TLSRPT", result.HostTLSRPT.Result)
1380 printResult("Domain TLSRPT", result.DomainTLSRPT.Result)
1381 printResult("MTASTS", result.MTASTS.Result)
1382 printResult("SRV conf", result.SRVConf.Result)
1383 printResult("Autoconf", result.Autoconf.Result)
1384 printResult("Autodiscover", result.Autodiscover.Result)
1387func cmdConfigEnsureACMEHostprivatekeys(c *cmd) {
1389 c.help = `Ensure host private keys exist for TLS listeners with ACME.
1391In mox.conf, each listener can have TLS configured. Long-lived private key files
1392can be specified, which will be used when requesting ACME certificates.
1393Configuring these private keys makes it feasible to publish DANE TLSA records
1394for the corresponding public keys in DNS, protected with DNSSEC, allowing TLS
1395certificate verification without depending on a list of Certificate Authorities
1396(CAs). Previous versions of mox did not pre-generate private keys for use with
1397ACME certificates, but would generate private keys on-demand. By explicitly
1398configuring private keys, they will not change automatedly with new
1399certificates, and the DNS TLSA records stay valid.
1401This command looks for listeners in mox.conf with TLS with ACME configured. For
1402each missing host private key (of type rsa-2048 and ecdsa-p256) a key is written
1403to config/hostkeys/. If a certificate exists in the ACME "cache", its private
1404key is copied. Otherwise a new private key is generated. Snippets for manually
1405updating/editing mox.conf are printed.
1407After running this command, and updating mox.conf, run "mox config dnsrecords"
1408for a domain and create the TLSA DNS records it suggests to enable DANE.
1415 // Load a private key from p, in various forms. We only look at the first PEM
1416 // block. Files with only a private key, or with multiple blocks but private key
1417 // first like autocert does, can be loaded.
1418 loadPrivateKey := func(f *os.File) (any, error) {
1419 buf, err := io.ReadAll(f)
1421 return nil, fmt.Errorf("reading private key file: %v", err)
1423 block, _ := pem.Decode(buf)
1425 return nil, fmt.Errorf("no pem block found in pem file")
1429 case "EC PRIVATE KEY":
1430 privKey, err = x509.ParseECPrivateKey(block.Bytes)
1431 case "RSA PRIVATE KEY":
1432 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
1434 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
1436 return nil, fmt.Errorf("unrecognized pem block type %q", block.Type)
1439 return nil, fmt.Errorf("parsing private key of type %q: %v", block.Type, err)
1444 // Either load a private key from file, or if it doesn't exist generate a new
1446 xtryLoadPrivateKey := func(kt autocert.KeyType, p string) any {
1447 f, err := os.Open(p)
1448 if err != nil && errors.Is(err, fs.ErrNotExist) {
1450 case autocert.KeyRSA2048:
1451 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1452 xcheckf(err, "generating new 2048-bit rsa private key")
1454 case autocert.KeyECDSAP256:
1455 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1456 xcheckf(err, "generating new ecdsa p-256 private key")
1459 log.Fatalf("unexpected keytype %v", kt)
1462 xcheckf(err, "%s: open acme key and certificate file", p)
1464 // Load private key from file. autocert stores a PEM file that starts with a
1465 // private key, followed by certificate(s). So we can just read it and should find
1466 // the private key we are looking for.
1467 privKey, err := loadPrivateKey(f)
1468 if xerr := f.Close(); xerr != nil {
1469 log.Printf("closing private key file: %v", xerr)
1471 xcheckf(err, "parsing private key from acme key and certificate file")
1473 switch k := privKey.(type) {
1474 case *rsa.PrivateKey:
1475 if k.N.BitLen() == 2048 {
1478 log.Printf("warning: rsa private key in %s has %d bits, skipping and generating new 2048-bit rsa private key", p, k.N.BitLen())
1479 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1480 xcheckf(err, "generating new 2048-bit rsa private key")
1482 case *ecdsa.PrivateKey:
1483 if k.Curve == elliptic.P256() {
1486 log.Printf("warning: ecdsa private key in %s has curve %v, skipping and generating new p-256 ecdsa key", p, k.Curve.Params().Name)
1487 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1488 xcheckf(err, "generating new ecdsa p-256 private key")
1491 log.Fatalf("%s: unexpected private key file of type %T", p, privKey)
1496 // Write privKey as PKCS#8 private key to p. Only if file does not yet exist.
1497 writeHostPrivateKey := func(privKey any, p string) error {
1498 os.MkdirAll(filepath.Dir(p), 0700)
1499 f, err := os.OpenFile(p, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
1501 return fmt.Errorf("create: %v", err)
1505 if err := f.Close(); err != nil {
1506 log.Printf("closing new hostkey file %s after error: %v", p, err)
1508 if err := os.Remove(p); err != nil {
1509 log.Printf("removing new hostkey file %s after error: %v", p, err)
1513 buf, err := x509.MarshalPKCS8PrivateKey(privKey)
1515 return fmt.Errorf("marshal private host key: %v", err)
1518 Type: "PRIVATE KEY",
1521 if err := pem.Encode(f, &block); err != nil {
1522 return fmt.Errorf("write as pem: %v", err)
1524 if err := f.Close(); err != nil {
1525 return fmt.Errorf("close: %v", err)
1532 timestamp := time.Now().Format("20060102T150405")
1534 for listenerName, l := range mox.Conf.Static.Listeners {
1535 if l.TLS == nil || l.TLS.ACME == "" {
1538 haveKeyTypes := map[autocert.KeyType]bool{}
1539 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
1540 p := mox.ConfigDirPath(privKeyFile)
1541 f, err := os.Open(p)
1542 xcheckf(err, "open host private key")
1543 privKey, err := loadPrivateKey(f)
1544 if err := f.Close(); err != nil {
1545 log.Printf("closing host private key file: %v", err)
1547 xcheckf(err, "loading host private key")
1548 switch k := privKey.(type) {
1549 case *rsa.PrivateKey:
1550 if k.N.BitLen() == 2048 {
1551 haveKeyTypes[autocert.KeyRSA2048] = true
1553 case *ecdsa.PrivateKey:
1554 if k.Curve == elliptic.P256() {
1555 haveKeyTypes[autocert.KeyECDSAP256] = true
1559 created := []string{}
1560 for _, kt := range []autocert.KeyType{autocert.KeyRSA2048, autocert.KeyECDSAP256} {
1561 if haveKeyTypes[kt] {
1564 // Lookup key in ACME cache.
1565 host := l.HostnameDomain
1566 if host.ASCII == "" {
1567 host = mox.Conf.Static.HostnameDomain
1569 filename := host.ASCII
1571 if kt == autocert.KeyRSA2048 {
1575 p := mox.DataDirPath(filepath.Join("acme", "keycerts", l.TLS.ACME, filename))
1576 privKey := xtryLoadPrivateKey(kt, p)
1578 relPath := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", host.Name(), timestamp, kind))
1579 destPath := mox.ConfigDirPath(relPath)
1580 err := writeHostPrivateKey(privKey, destPath)
1581 xcheckf(err, "writing host private key file to %s: %v", destPath, err)
1582 created = append(created, relPath)
1583 fmt.Printf("Wrote host private key: %s\n", destPath)
1585 didCreate = didCreate || len(created) > 0
1586 if len(created) > 0 {
1588 HostPrivateKeyFiles: append(l.TLS.HostPrivateKeyFiles, created...),
1590 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)
1591 err := sconf.Write(os.Stdout, tls)
1592 xcheckf(err, "writing new TLS.HostPrivateKeyFiles section")
1598After updating mox.conf and restarting, run "mox config dnsrecords" for a
1599domain and create the TLSA DNS records it suggests to enable DANE.
1604func cmdLoglevels(c *cmd) {
1605 c.params = "[level [pkg]]"
1606 c.help = `Print the log levels, or set a new default log level, or a level for the given package.
1608By default, a single log level applies to all logging in mox. But for each
1609"pkg", an overriding log level can be configured. Examples of packages:
1610smtpserver, smtpclient, queue, imapserver, spf, dkim, dmarc, junk, message,
1613Specify a pkg and an empty level to clear the configured level for a package.
1615Valid labels: error, info, debug, trace, traceauth, tracedata.
1624 ctlcmdLoglevels(xctl())
1630 ctlcmdSetLoglevels(xctl(), pkg, args[0])
1634func ctlcmdLoglevels(ctl *ctl) {
1635 ctl.xwrite("loglevels")
1637 ctl.xstreamto(os.Stdout)
1640func ctlcmdSetLoglevels(ctl *ctl, pkg, level string) {
1641 ctl.xwrite("setloglevels")
1647func cmdStop(c *cmd) {
1648 c.help = `Shut mox down, giving connections maximum 3 seconds to stop before closing them.
1650While shutting down, new IMAP and SMTP connections will get a status response
1651indicating temporary unavailability. Existing connections will get a 3 second
1652period to finish their transaction and shut down. Under normal circumstances,
1653only IMAP has long-living connections, with the IDLE command to get notified of
1656 if len(c.Parse()) != 0 {
1663 // Read will hang until remote has shut down.
1664 buf := make([]byte, 128)
1665 n, err := ctl.conn.Read(buf)
1667 log.Fatalf("expected eof after graceful shutdown, got data %q", buf[:n])
1668 } else if err != io.EOF {
1669 log.Fatalf("expected eof after graceful shutdown, got error %v", err)
1671 fmt.Println("mox stopped")
1674func cmdBackup(c *cmd) {
1675 c.params = "destdir"
1676 c.help = `Creates a backup of the config and data directory.
1678Backup copies the config directory to <destdir>/config, and creates
1679<destdir>/data with a consistent snapshot of the databases and message files
1680and copies other files from the data directory. Empty directories are not
1681copied. The backup can then be stored elsewhere for long-term storage, or used
1682to fall back to should an upgrade fail. Simply copying files in the data
1683directory while mox is running can result in unusable database files.
1685Message files never change (they are read-only, though can be removed) and are
1686hard-linked so they don't consume additional space. If hardlinking fails, for
1687example when the backup destination directory is on a different file system, a
1688regular copy is made. Using a destination directory like "data/tmp/backup"
1689increases the odds hardlinking succeeds: the default systemd service file
1690specifically mounts the data directory, causing attempts to hardlink outside it
1691to fail with an error about cross-device linking.
1693All files in the data directory that aren't recognized (i.e. other than known
1694database files, message files, an acme directory, the "tmp" directory, etc),
1695are stored, but with a warning.
1697Remove files in the destination directory before doing another backup. The
1698backup command will not overwrite files, but print and return errors.
1700Exit code 0 indicates the backup was successful. A clean successful backup does
1701not print any output, but may print warnings. Use the -verbose flag for
1702details, including timing.
1704To restore a backup, first shut down mox, move away the old data directory and
1705move an earlier backed up directory in its place, run "mox verifydata
1706<datadir>", possibly with the "-fix" option, and restart mox. After the
1707restore, you may also want to run "mox bumpuidvalidity" for each account for
1708which messages in a mailbox changed, to force IMAP clients to synchronize
1711Before upgrading, to check if the upgrade will likely succeed, first make a
1712backup, then use the new mox binary to run "mox verifydata <backupdir>/data".
1713This can change the backup files (e.g. upgrade database files, move away
1714unrecognized message files), so you should make a new backup before actually
1719 c.flag.BoolVar(&verbose, "verbose", false, "print progress")
1726 dstDataDir, err := filepath.Abs(args[0])
1727 xcheckf(err, "making path absolute")
1729 ctlcmdBackup(xctl(), dstDataDir, verbose)
1732func ctlcmdBackup(ctl *ctl, dstDataDir string, verbose bool) {
1733 ctl.xwrite("backup")
1734 ctl.xwrite(dstDataDir)
1736 ctl.xwrite("verbose")
1740 ctl.xstreamto(os.Stdout)
1744func cmdSetadminpassword(c *cmd) {
1745 c.help = `Set a new admin password, for the web interface.
1747The password is read from stdin. Its bcrypt hash is stored in a file named
1748"adminpasswd" in the configuration directory.
1750 if len(c.Parse()) != 0 {
1755 path := mox.ConfigDirPath(mox.Conf.Static.AdminPasswordFile)
1757 log.Fatal("no admin password file configured")
1760 pw := xreadpassword()
1761 pw, err := precis.OpaqueString.String(pw)
1762 xcheckf(err, `checking password with "precis" requirements`)
1763 hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
1764 xcheckf(err, "generating hash for password")
1765 err = os.WriteFile(path, hash, 0660)
1766 xcheckf(err, "writing hash to admin password file")
1769func xreadpassword() string {
1771Type new password. Password WILL echo.
1773WARNING: Bots will try to bruteforce your password. Connections with failed
1774authentication attempts will be rate limited but attackers WILL find passwords
1775reused at other services and weak passwords. If your account is compromised,
1776spammers are likely to abuse your system, spamming your address and the wider
1777internet in your name. So please pick a random, unguessable password, preferably
1778at least 12 characters.
1781 fmt.Printf("password: ")
1782 scanner := bufio.NewScanner(os.Stdin)
1783 // The default splitter for scanners is one that splits by lines, so we
1784 // don't have to set up another one here.
1786 // We discard the return value of Scan() since failing to tokenize could
1787 // either mean reaching EOF but no newline (which can be legitimate if the
1788 // CLI was programatically called to set the password, but with no trailing
1789 // newline), or an actual error. We can distinguish between the two by
1790 // calling Err() since it will return nil if it were EOF, but the actual
1793 xcheckf(scanner.Err(), "reading stdin")
1794 // No need to trim, the scanner does not return the token in the output.
1795 pw := scanner.Text()
1797 log.Fatal("password must be at least 8 characters")
1802func cmdSetaccountpassword(c *cmd) {
1803 c.params = "account"
1804 c.help = `Set new password an account.
1806The password is read from stdin. Secrets derived from the password, but not the
1807password itself, are stored in the account database. The stored secrets are for
1808authentication with: scram-sha-256, scram-sha-1, cram-md5, plain text (bcrypt
1811The parameter is an account name, as configured under Accounts in domains.conf
1812and as present in the data/accounts/ directory, not a configured email address
1821 pw := xreadpassword()
1823 ctlcmdSetaccountpassword(xctl(), args[0], pw)
1826func ctlcmdSetaccountpassword(ctl *ctl, account, password string) {
1827 ctl.xwrite("setaccountpassword")
1829 ctl.xwrite(password)
1833func cmdDeliver(c *cmd) {
1835 c.params = "address < message"
1836 c.help = "Deliver message to address."
1842 ctlcmdDeliver(xctl(), args[0])
1845func ctlcmdDeliver(ctl *ctl, address string) {
1846 ctl.xwrite("deliver")
1849 ctl.xstreamfrom(os.Stdin)
1852 fmt.Println("message delivered")
1854 log.Fatalf("deliver: %s", line)
1858func cmdDKIMGenrsa(c *cmd) {
1859 c.params = ">$selector._domainkey.$domain.rsa2048.privatekey.pkcs8.pem"
1860 c.help = `Generate a new 2048 bit RSA private key for use with DKIM.
1862The generated file is in PEM format, and has a comment it is generated for use
1865 if len(c.Parse()) != 0 {
1869 buf, err := admin.MakeDKIMRSAKey(dns.Domain{}, dns.Domain{})
1870 xcheckf(err, "making rsa private key")
1871 _, err = os.Stdout.Write(buf)
1872 xcheckf(err, "writing rsa private key")
1875func cmdDANEDial(c *cmd) {
1876 c.params = "host:port"
1878 c.flag.StringVar(&usages, "usages", "pkix-ta,pkix-ee,dane-ta,dane-ee", "allowed usages for dane, comma-separated list")
1879 c.help = `Dial the address using TLS with certificate verification using DANE.
1881Data is copied between connection and stdin/stdout until either side closes the
1889 allowedUsages := []adns.TLSAUsage{}
1891 for _, s := range strings.Split(usages, ",") {
1892 var usage adns.TLSAUsage
1893 switch strings.ToLower(s) {
1894 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
1895 usage = adns.TLSAUsagePKIXTA
1896 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
1897 usage = adns.TLSAUsagePKIXEE
1898 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
1899 usage = adns.TLSAUsageDANETA
1900 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
1901 usage = adns.TLSAUsageDANEEE
1903 log.Fatalf("unknown dane usage %q", s)
1905 allowedUsages = append(allowedUsages, usage)
1909 pkixRoots, err := x509.SystemCertPool()
1910 xcheckf(err, "get system pkix certificate pool")
1912 resolver := dns.StrictResolver{Pkg: "danedial"}
1913 conn, record, err := dane.Dial(context.Background(), c.log.Logger, resolver, "tcp", args[0], allowedUsages, pkixRoots)
1914 xcheckf(err, "dial")
1915 log.Printf("(connected, verified with %s)", record)
1918 _, err := io.Copy(os.Stdout, conn)
1919 xcheckf(err, "copy from connection to stdout")
1922 _, err = io.Copy(conn, os.Stdin)
1923 xcheckf(err, "copy from stdin to connection")
1926func cmdDANEDialmx(c *cmd) {
1927 c.params = "domain [destination-host]"
1928 var ehloHostname string
1929 c.flag.StringVar(&ehloHostname, "ehlohostname", "localhost", "hostname to send in smtp ehlo command")
1930 c.help = `Connect to MX server for domain using STARTTLS verified with DANE.
1932If no destination host is specified, regular delivery logic is used to find the
1933hosts to attempt delivery too. This involves following CNAMEs for the domain,
1934looking up MX records, and possibly falling back to the domain name itself as
1937If a destination host is specified, that is the only candidate host considered
1940With a list of destinations gathered, each is dialed until a successful SMTP
1941session verified with DANE has been initialized, including EHLO and STARTTLS
1944Once connected, data is copied between connection and stdin/stdout, until
1945either side closes the connection.
1947This command follows the same logic as delivery attempts made from the queue,
1948sharing most of its code.
1951 if len(args) != 1 && len(args) != 2 {
1955 ehloDomain, err := dns.ParseDomain(ehloHostname)
1956 xcheckf(err, "parsing ehlo hostname")
1958 origNextHop, err := dns.ParseDomain(args[0])
1959 xcheckf(err, "parse domain")
1961 ctxbg := context.Background()
1963 resolver := dns.StrictResolver{}
1965 var origNextHopAuthentic, expandedNextHopAuthentic bool
1966 var expandedNextHop dns.Domain
1967 var hosts []dns.IPDomain
1970 haveMX, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hosts, permanent, err = smtpclient.GatherDestinations(ctxbg, c.log.Logger, resolver, dns.IPDomain{Domain: origNextHop})
1971 status := "temporary"
1973 status = "permanent"
1976 log.Fatalf("gathering destinations: %v (%s)", err, status)
1978 if expandedNextHop != origNextHop {
1979 log.Printf("followed cnames to %s", expandedNextHop)
1982 log.Printf("found mx record, trying mx hosts")
1984 log.Printf("no mx record found, will try to connect to domain directly")
1986 if !origNextHopAuthentic {
1987 log.Fatalf("error: initial domain not dnssec-secure")
1989 if !expandedNextHopAuthentic {
1990 log.Fatalf("error: expanded domain not dnssec-secure")
1994 for _, h := range hosts {
1995 l = append(l, h.String())
1997 log.Printf("destinations: %s", strings.Join(l, ", "))
1999 d, err := dns.ParseDomain(args[1])
2001 log.Fatalf("parsing destination host: %v", err)
2003 log.Printf("skipping domain mx/cname lookups, assuming domain is dnssec-protected")
2005 origNextHopAuthentic = true
2006 expandedNextHopAuthentic = true
2008 hosts = []dns.IPDomain{{Domain: d}}
2011 dialedIPs := map[string][]net.IP{}
2012 for _, host := range hosts {
2013 // It should not be possible for hosts to have IP addresses: They are not
2014 // allowed by dns.ParseDomain, and MX records cannot contain them.
2016 log.Fatalf("unexpected IP address for destination host")
2019 log.Printf("attempting to connect to %s", host)
2021 authentic, expandedAuthentic, expandedHost, ips, _, err := smtpclient.GatherIPs(ctxbg, c.log.Logger, resolver, "ip", host, dialedIPs)
2023 log.Printf("resolving ips for %s: %v, skipping", host, err)
2027 log.Printf("no dnssec for ips of %s, skipping", host)
2030 if !expandedAuthentic {
2031 log.Printf("no dnssec for cname-followed ips of %s, skipping", host)
2034 if expandedHost != host.Domain {
2035 log.Printf("host %s cname-expanded to %s", host, expandedHost)
2037 log.Printf("host %s resolved to ips %s, looking up tlsa records", host, ips)
2039 daneRequired, daneRecords, tlsaBaseDomain, err := smtpclient.GatherTLSA(ctxbg, c.log.Logger, resolver, host.Domain, expandedAuthentic, expandedHost)
2041 log.Printf("looking up tlsa records: %s, skipping", err)
2044 tlsMode := smtpclient.TLSRequiredStartTLS
2045 if len(daneRecords) == 0 {
2047 log.Printf("host %s has no tlsa records, skipping", expandedHost)
2050 log.Printf("warning: only unusable tlsa records found, continuing with required tls without certificate verification")
2054 for _, r := range daneRecords {
2055 l = append(l, r.String())
2057 log.Printf("tlsa records: %s", strings.Join(l, "; "))
2060 tlsHostnames := smtpclient.GatherTLSANames(haveMX, expandedNextHopAuthentic, expandedAuthentic, origNextHop, expandedNextHop, host.Domain, tlsaBaseDomain)
2062 for _, name := range tlsHostnames {
2063 l = append(l, name.String())
2065 log.Printf("gathered valid tls certificate names for potential verification with dane-ta: %s", strings.Join(l, ", "))
2067 dialer := &net.Dialer{Timeout: 5 * time.Second}
2068 conn, _, err := smtpclient.Dial(ctxbg, c.log.Logger, dialer, dns.IPDomain{Domain: expandedHost}, ips, 25, dialedIPs, nil)
2070 log.Printf("dial %s: %v, skipping", expandedHost, err)
2073 log.Printf("connected to %s, %s, starting smtp session with ehlo and starttls with dane verification", expandedHost, conn.RemoteAddr())
2075 var verifiedRecord adns.TLSA
2076 opts := smtpclient.Opts{
2077 DANERecords: daneRecords,
2078 DANEMoreHostnames: tlsHostnames[1:],
2079 DANEVerifiedRecord: &verifiedRecord,
2080 RootCAs: mox.Conf.Static.TLS.CertPool,
2083 sc, err := smtpclient.New(ctxbg, c.log.Logger, conn, tlsMode, tlsPKIX, ehloDomain, tlsHostnames[0], opts)
2085 log.Printf("setting up smtp session: %v, skipping", err)
2090 smtpConn, err := sc.Conn()
2092 log.Fatalf("error: taking over smtp connection: %s", err)
2094 log.Printf("tls verified with tlsa record: %s", verifiedRecord)
2095 log.Printf("smtp session initialized and connected to stdin/stdout")
2098 _, err := io.Copy(os.Stdout, smtpConn)
2099 xcheckf(err, "copy from connection to stdout")
2102 _, err = io.Copy(smtpConn, os.Stdin)
2103 xcheckf(err, "copy from stdin to connection")
2106 log.Fatalf("no remaining destinations")
2109func cmdDANEMakeRecord(c *cmd) {
2110 c.params = "usage selector matchtype [certificate.pem | publickey.pem | privatekey.pem]"
2111 c.help = `Print TLSA record for given certificate/key and parameters.
2114- usage: pkix-ta (0), pkix-ee (1), dane-ta (2), dane-ee (3)
2115- selector: cert (0), spki (1)
2116- matchtype: full (0), sha2-256 (1), sha2-512 (2)
2118Common DANE TLSA record parameters are: dane-ee spki sha2-256, or 3 1 1,
2119followed by a sha2-256 hash of the DER-encoded "SPKI" (subject public key info)
2120from the certificate. An example DNS zone file entry:
2122 _25._tcp.example.com. TLSA 3 1 1 133b919c9d65d8b1488157315327334ead8d83372db57465ecabf53ee5748aee
2124The first usable information from the pem file is used to compose the TLSA
2125record. In case of selector "cert", a certificate is required. Otherwise the
2126"subject public key info" (spki) of the first certificate or public or private
2127key (pkcs#8, pkcs#1 or ec private key) is used.
2135 var usage adns.TLSAUsage
2136 switch strings.ToLower(args[0]) {
2137 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
2138 usage = adns.TLSAUsagePKIXTA
2139 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
2140 usage = adns.TLSAUsagePKIXEE
2141 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
2142 usage = adns.TLSAUsageDANETA
2143 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
2144 usage = adns.TLSAUsageDANEEE
2146 if v, err := strconv.ParseUint(args[0], 10, 16); err != nil {
2147 log.Fatalf("bad usage %q", args[0])
2149 // Does not influence certificate association data, so we can accept other numbers.
2150 log.Printf("warning: continuing with unrecognized tlsa usage %d", v)
2151 usage = adns.TLSAUsage(v)
2155 var selector adns.TLSASelector
2156 switch strings.ToLower(args[1]) {
2157 case "cert", strconv.Itoa(int(adns.TLSASelectorCert)):
2158 selector = adns.TLSASelectorCert
2159 case "spki", strconv.Itoa(int(adns.TLSASelectorSPKI)):
2160 selector = adns.TLSASelectorSPKI
2162 log.Fatalf("bad selector %q", args[1])
2165 var matchType adns.TLSAMatchType
2166 switch strings.ToLower(args[2]) {
2167 case "full", strconv.Itoa(int(adns.TLSAMatchTypeFull)):
2168 matchType = adns.TLSAMatchTypeFull
2169 case "sha2-256", strconv.Itoa(int(adns.TLSAMatchTypeSHA256)):
2170 matchType = adns.TLSAMatchTypeSHA256
2171 case "sha2-512", strconv.Itoa(int(adns.TLSAMatchTypeSHA512)):
2172 matchType = adns.TLSAMatchTypeSHA512
2174 log.Fatalf("bad matchtype %q", args[2])
2177 buf, err := os.ReadFile(args[3])
2178 xcheckf(err, "reading certificate")
2180 var block *pem.Block
2181 block, buf = pem.Decode(buf)
2185 extra = " (with leftover data from pem file)"
2187 if selector == adns.TLSASelectorCert {
2188 log.Fatalf("no certificate found in pem file%s", extra)
2190 log.Fatalf("no certificate or public or private key found in pem file%s", extra)
2193 var cert *x509.Certificate
2195 if block.Type == "CERTIFICATE" {
2196 cert, err = x509.ParseCertificate(block.Bytes)
2197 xcheckf(err, "parse certificate")
2199 case adns.TLSASelectorCert:
2201 case adns.TLSASelectorSPKI:
2202 data = cert.RawSubjectPublicKeyInfo
2204 } else if selector == adns.TLSASelectorCert {
2205 // We need a certificate, just a public/private key won't do.
2206 log.Printf("skipping pem type %q, certificate is required", block.Type)
2209 var privKey, pubKey any
2213 _, err := x509.ParsePKIXPublicKey(block.Bytes)
2214 xcheckf(err, "parse pkix subject public key info (spki)")
2216 case "EC PRIVATE KEY":
2217 privKey, err = x509.ParseECPrivateKey(block.Bytes)
2218 xcheckf(err, "parse ec private key")
2219 case "RSA PRIVATE KEY":
2220 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
2221 xcheckf(err, "parse pkcs#1 rsa private key")
2222 case "RSA PUBLIC KEY":
2223 pubKey, err = x509.ParsePKCS1PublicKey(block.Bytes)
2224 xcheckf(err, "parse pkcs#1 rsa public key")
2226 // PKCS#8 private key
2227 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
2228 xcheckf(err, "parse pkcs#8 private key")
2230 log.Printf("skipping unrecognized pem type %q", block.Type)
2234 if pubKey == nil && privKey != nil {
2235 if signer, ok := privKey.(crypto.Signer); !ok {
2236 log.Fatalf("private key of type %T is not a signer, cannot get public key", privKey)
2238 pubKey = signer.Public()
2242 // Should not happen.
2243 log.Fatalf("internal error: did not find private or public key")
2245 data, err = x509.MarshalPKIXPublicKey(pubKey)
2246 xcheckf(err, "marshal pkix subject public key info (spki)")
2251 case adns.TLSAMatchTypeFull:
2252 case adns.TLSAMatchTypeSHA256:
2253 p := sha256.Sum256(data)
2255 case adns.TLSAMatchTypeSHA512:
2256 p := sha512.Sum512(data)
2259 fmt.Printf("%d %d %d %x\n", usage, selector, matchType, data)
2264func cmdDNSLookup(c *cmd) {
2265 c.params = "[ptr | mx | cname | ips | a | aaaa | ns | txt | srv | tlsa] name"
2266 c.help = `Lookup DNS name of given type.
2268Lookup always prints whether the response was DNSSEC-protected.
2272mox dns lookup ptr 1.1.1.1
2273mox dns lookup mx xmox.nl
2274mox dns lookup txt _dmarc.xmox.nl.
2275mox dns lookup tlsa _25._tcp.xmox.nl
2283 resolver := dns.StrictResolver{Pkg: "dns"}
2285 // like xparseDomain, but treat unparseable domain as an ASCII name so names with
2286 // underscores are still looked up, e,g <selector>._domainkey.<host>.
2287 xdomain := func(s string) dns.Domain {
2288 d, err := dns.ParseDomain(s)
2290 return dns.Domain{ASCII: strings.TrimSuffix(s, ".")}
2295 cmd, name := args[0], args[1]
2299 ip := xparseIP(name, "ip")
2300 ptrs, result, err := resolver.LookupAddr(context.Background(), ip.String())
2302 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2304 fmt.Printf("names (%d, %s):\n", len(ptrs), dnssecStatus(result.Authentic))
2305 for _, ptr := range ptrs {
2306 fmt.Printf("- %s\n", ptr)
2310 name := xdomain(name)
2311 mxl, result, err := resolver.LookupMX(context.Background(), name.ASCII+".")
2313 log.Printf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2314 // We can still have valid records...
2316 fmt.Printf("mx records (%d, %s):\n", len(mxl), dnssecStatus(result.Authentic))
2317 for _, mx := range mxl {
2318 fmt.Printf("- %s, preference %d\n", mx.Host, mx.Pref)
2322 name := xdomain(name)
2323 target, result, err := resolver.LookupCNAME(context.Background(), name.ASCII+".")
2325 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2327 fmt.Printf("%s (%s)\n", target, dnssecStatus(result.Authentic))
2329 case "ips", "a", "aaaa":
2333 } else if cmd == "aaaa" {
2336 name := xdomain(name)
2337 ips, result, err := resolver.LookupIP(context.Background(), network, name.ASCII+".")
2339 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2341 fmt.Printf("records (%d, %s):\n", len(ips), dnssecStatus(result.Authentic))
2342 for _, ip := range ips {
2343 fmt.Printf("- %s\n", ip)
2347 name := xdomain(name)
2348 nsl, result, err := resolver.LookupNS(context.Background(), name.ASCII+".")
2350 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2352 fmt.Printf("ns records (%d, %s):\n", len(nsl), dnssecStatus(result.Authentic))
2353 for _, ns := range nsl {
2354 fmt.Printf("- %s\n", ns)
2358 host := xdomain(name)
2359 l, result, err := resolver.LookupTXT(context.Background(), host.ASCII+".")
2361 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2363 fmt.Printf("txt records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2364 for _, txt := range l {
2365 fmt.Printf("- %s\n", txt)
2369 host := xdomain(name)
2370 _, l, result, err := resolver.LookupSRV(context.Background(), "", "", host.ASCII+".")
2372 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2374 fmt.Printf("srv records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2375 for _, srv := range l {
2376 fmt.Printf("- host %s, port %d, priority %d, weight %d\n", srv.Target, srv.Port, srv.Priority, srv.Weight)
2380 host := xdomain(name)
2381 l, result, err := resolver.LookupTLSA(context.Background(), 0, "", host.ASCII+".")
2383 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2385 fmt.Printf("tlsa records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2386 for _, tlsa := range l {
2387 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)
2390 log.Fatalf("unknown record type %q", args[0])
2394func cmdDKIMGened25519(c *cmd) {
2395 c.params = ">$selector._domainkey.$domain.ed25519.privatekey.pkcs8.pem"
2396 c.help = `Generate a new ed25519 key for use with DKIM.
2398Ed25519 keys are much smaller than RSA keys of comparable cryptographic
2399strength. This is convenient because of maximum DNS message sizes. At the time
2400of writing, not many mail servers appear to support ed25519 DKIM keys though,
2401so it is recommended to sign messages with both RSA and ed25519 keys.
2403 if len(c.Parse()) != 0 {
2407 buf, err := admin.MakeDKIMEd25519Key(dns.Domain{}, dns.Domain{})
2408 xcheckf(err, "making dkim ed25519 key")
2409 _, err = os.Stdout.Write(buf)
2410 xcheckf(err, "writing dkim ed25519 key")
2413func cmdDKIMTXT(c *cmd) {
2414 c.params = "<$selector._domainkey.$domain.key.pkcs8.pem"
2415 c.help = `Print a DKIM DNS TXT record with the public key derived from the private key read from stdin.
2417The DNS should be configured as a TXT record at $selector._domainkey.$domain.
2419 if len(c.Parse()) != 0 {
2423 privKey, err := parseDKIMKey(os.Stdin)
2424 xcheckf(err, "reading dkim private key from stdin")
2428 Hashes: []string{"sha256"},
2429 Flags: []string{"s"},
2432 switch key := privKey.(type) {
2433 case *rsa.PrivateKey:
2434 r.PublicKey = key.Public()
2435 case ed25519.PrivateKey:
2436 r.PublicKey = key.Public()
2439 log.Fatalf("unsupported private key type %T, must be rsa or ed25519", privKey)
2442 record, err := r.Record()
2443 xcheckf(err, "making record")
2444 fmt.Print("<selector>._domainkey.<your.domain.> TXT ")
2448 s, record = record[:100], record[100:]
2452 fmt.Printf(`"%s" `, s)
2457func parseDKIMKey(r io.Reader) (any, error) {
2458 buf, err := io.ReadAll(r)
2460 return nil, fmt.Errorf("reading pem from stdin: %v", err)
2462 b, _ := pem.Decode(buf)
2464 return nil, fmt.Errorf("decoding pem: %v", err)
2466 privKey, err := x509.ParsePKCS8PrivateKey(b.Bytes)
2468 return nil, fmt.Errorf("parsing private key: %v", err)
2473func cmdDKIMVerify(c *cmd) {
2474 c.params = "message"
2475 c.help = `Verify the DKIM signatures in a message and print the results.
2477The message is parsed, and the DKIM-Signature headers are validated. Validation
2478of older messages may fail because the DNS records have been removed or changed
2479by now, or because the signature header may have specified an expiration time
2487 msgf, err := os.Open(args[0])
2488 xcheckf(err, "open message")
2490 results, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, false, dkim.DefaultPolicy, msgf, true)
2491 xcheckf(err, "dkim verify")
2493 for _, result := range results {
2495 if result.Sig == nil {
2496 log.Printf("warning: could not parse signature")
2498 sigh, err = result.Sig.Header()
2500 log.Printf("warning: packing signature: %s", err)
2504 if result.Record == nil {
2505 log.Printf("warning: missing DNS record")
2507 txt, err = result.Record.Record()
2509 log.Printf("warning: packing record: %s", err)
2512 fmt.Printf("status %q, err %v\nrecord %q\nheader %s\n", result.Status, result.Err, txt, sigh)
2516func cmdDKIMSign(c *cmd) {
2517 c.params = "message"
2518 c.help = `Sign a message, adding DKIM-Signature headers based on the domain in the From header.
2520The message is parsed, the domain looked up in the configuration files, and
2521DKIM-Signature headers generated. The message is printed with the DKIM-Signature
2529 msgf, err := os.Open(args[0])
2530 xcheckf(err, "open message")
2533 p, err := message.Parse(c.log.Logger, true, msgf)
2534 xcheckf(err, "parsing message")
2536 if len(p.Envelope.From) != 1 {
2537 log.Fatalf("found %d from headers, need exactly 1", len(p.Envelope.From))
2539 localpart, err := smtp.ParseLocalpart(p.Envelope.From[0].User)
2540 xcheckf(err, "parsing localpart of address in from-header")
2541 dom, err := dns.ParseDomain(p.Envelope.From[0].Host)
2542 xcheckf(err, "parsing domain of address in from-header")
2546 domConf, ok := mox.Conf.Domain(dom)
2548 log.Fatalf("domain %s not configured", dom)
2551 selectors := mox.DKIMSelectors(domConf.DKIM)
2552 headers, err := dkim.Sign(context.Background(), c.log.Logger, localpart, dom, selectors, false, msgf)
2553 xcheckf(err, "signing message with dkim")
2555 log.Fatalf("no DKIM configured for domain %s", dom)
2557 _, err = fmt.Fprint(os.Stdout, headers)
2558 xcheckf(err, "write headers")
2559 _, err = io.Copy(os.Stdout, msgf)
2560 xcheckf(err, "write message")
2563func cmdDKIMLookup(c *cmd) {
2564 c.params = "selector domain"
2565 c.help = "Lookup and print the DKIM record for the selector at the domain."
2571 selector := xparseDomain(args[0], "selector")
2572 domain := xparseDomain(args[1], "domain")
2574 status, record, txt, authentic, err := dkim.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, selector, domain)
2576 fmt.Printf("error: %s\n", err)
2578 if status != dkim.StatusNeutral {
2579 fmt.Printf("status: %s\n", status)
2582 fmt.Printf("TXT record: %s\n", txt)
2585 fmt.Println("dnssec-signed: yes")
2587 fmt.Println("dnssec-signed: no")
2590 fmt.Printf("Record:\n")
2592 "version", record.Version,
2593 "hashes", record.Hashes,
2595 "notes", record.Notes,
2596 "services", record.Services,
2597 "flags", record.Flags,
2599 for i := 0; i < len(pairs); i += 2 {
2600 fmt.Printf("\t%s: %v\n", pairs[i], pairs[i+1])
2605func cmdDMARCLookup(c *cmd) {
2607 c.help = "Lookup dmarc policy for domain, a DNS TXT record at _dmarc.<domain>, validate and print it."
2613 fromdomain := xparseDomain(args[0], "domain")
2614 _, domain, _, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, fromdomain)
2615 xcheckf(err, "dmarc lookup domain %s", fromdomain)
2616 fmt.Printf("dmarc record at domain %s: %s\n", domain, txt)
2617 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2620func dnssecStatus(v bool) string {
2622 return "with dnssec"
2624 return "without dnssec"
2627func cmdDMARCVerify(c *cmd) {
2628 c.params = "remoteip mailfromaddress helodomain < message"
2629 c.help = `Parse an email message and evaluate it against the DMARC policy of the domain in the From-header.
2631mailfromaddress and helodomain are used for SPF validation. If both are empty,
2632SPF validation is skipped.
2634mailfromaddress should be the address used as MAIL FROM in the SMTP session.
2635For DSN messages, that address may be empty. The helo domain was specified at
2636the beginning of the SMTP transaction that delivered the message. These values
2637can be found in message headers.
2644 var heloDomain *dns.Domain
2646 remoteIP := xparseIP(args[0], "remoteip")
2648 var mailfrom *smtp.Address
2650 a, err := smtp.ParseAddress(args[1])
2651 xcheckf(err, "parsing mailfrom address")
2655 d := xparseDomain(args[2], "helo domain")
2658 var received *spf.Received
2659 spfStatus := spf.StatusNone
2660 var spfIdentity *dns.Domain
2661 if mailfrom != nil || heloDomain != nil {
2662 spfArgs := spf.Args{
2664 LocalIP: net.ParseIP("127.0.0.1"),
2665 LocalHostname: dns.Domain{ASCII: "localhost"},
2667 if mailfrom != nil {
2668 spfArgs.MailFromLocalpart = mailfrom.Localpart
2669 spfArgs.MailFromDomain = mailfrom.Domain
2671 if heloDomain != nil {
2672 spfArgs.HelloDomain = dns.IPDomain{Domain: *heloDomain}
2674 rspf, spfDomain, expl, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfArgs)
2676 log.Printf("spf verify: %v (explanation: %q, authentic %v)", err, expl, authentic)
2679 spfStatus = received.Result
2680 // todo: should probably potentially do two separate spf validations
2681 if mailfrom != nil {
2682 spfIdentity = &mailfrom.Domain
2684 spfIdentity = heloDomain
2686 fmt.Printf("spf result: %s: %s (%s)\n", spfDomain, spfStatus, dnssecStatus(authentic))
2690 data, err := io.ReadAll(os.Stdin)
2691 xcheckf(err, "read message")
2692 dmarcFrom, _, _, err := message.From(c.log.Logger, false, bytes.NewReader(data), nil)
2693 xcheckf(err, "extract dmarc from message")
2695 const ignoreTestMode = false
2696 dkimResults, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, true, func(*dkim.Sig) error { return nil }, bytes.NewReader(data), ignoreTestMode)
2697 xcheckf(err, "dkim verify")
2698 for _, r := range dkimResults {
2699 fmt.Printf("dkim result: %q (err %v)\n", r.Status, r.Err)
2702 _, result := dmarc.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, dmarcFrom.Domain, dkimResults, spfStatus, spfIdentity, false)
2703 xcheckf(result.Err, "dmarc verify")
2704 fmt.Printf("dmarc from: %s\ndmarc status: %q\ndmarc reject: %v\ncmarc record: %s\n", dmarcFrom, result.Status, result.Reject, result.Record)
2707func cmdDMARCCheckreportaddrs(c *cmd) {
2709 c.help = `For each reporting address in the domain's DMARC record, check if it has opted into receiving reports (if needed).
2711A DMARC record can request reports about DMARC evaluations to be sent to an
2712email/http address. If the organizational domains of that of the DMARC record
2713and that of the report destination address do not match, the destination
2714address must opt-in to receiving DMARC reports by creating a DMARC record at
2715<dmarcdomain>._report._dmarc.<reportdestdomain>.
2722 dom := xparseDomain(args[0], "domain")
2723 _, domain, record, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dom)
2724 xcheckf(err, "dmarc lookup domain %s", dom)
2725 fmt.Printf("dmarc record at domain %s: %q\n", domain, txt)
2726 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2728 check := func(kind, addr string) {
2731 printResult := func(format string, args ...any) {
2732 fmt.Printf("%s %s: %s (%s)\n", kind, addr, fmt.Sprintf(format, args...), dnssecStatus(authentic))
2735 u, err := url.Parse(addr)
2737 printResult("parsing uri: %v (skipping)", addr, err)
2740 var destdom dns.Domain
2743 a, err := smtp.ParseAddress(u.Opaque)
2745 printResult("parsing destination email address %s: %v (skipping)", u.Opaque, err)
2750 printResult("unrecognized scheme in reporting address %s (skipping)", u.Scheme)
2754 if publicsuffix.Lookup(context.Background(), c.log.Logger, dom) == publicsuffix.Lookup(context.Background(), c.log.Logger, destdom) {
2755 printResult("pass (same organizational domain)")
2759 accepts, status, _, txts, authentic, err := dmarc.LookupExternalReportsAccepted(context.Background(), c.log.Logger, dns.StrictResolver{}, domain, destdom)
2761 txtaddr := fmt.Sprintf("%s._report._dmarc.%s", domain.ASCII, destdom.ASCII)
2763 txtstr = fmt.Sprintf(" (no txt records %s)", txtaddr)
2765 txtstr = fmt.Sprintf(" (txt record %s: %q)", txtaddr, txts)
2767 if status != dmarc.StatusNone {
2768 printResult("fail: %s%s", err, txtstr)
2770 printResult("pass%s", txtstr)
2771 } else if err != nil {
2772 printResult("fail: %s%s", err, txtstr)
2774 printResult("fail%s", txtstr)
2778 for _, uri := range record.AggregateReportAddresses {
2779 check("aggregate reporting", uri.Address)
2781 for _, uri := range record.FailureReportAddresses {
2782 check("failure reporting", uri.Address)
2786func cmdDMARCParsereportmsg(c *cmd) {
2787 c.params = "message ..."
2788 c.help = `Parse a DMARC report from an email message, and print its extracted details.
2790DMARC reports are periodically mailed, if requested in the DMARC DNS record of
2791a domain. Reports are sent by mail servers that received messages with our
2792domain in a From header. This may or may not be legatimate email. DMARC reports
2793contain summaries of evaluations of DMARC and DKIM/SPF, which can help
2794understand email deliverability problems.
2801 for _, arg := range args {
2802 f, err := os.Open(arg)
2803 xcheckf(err, "open %q", arg)
2804 feedback, err := dmarcrpt.ParseMessageReport(c.log.Logger, f)
2805 xcheckf(err, "parse report in %q", arg)
2806 meta := feedback.ReportMetadata
2807 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)
2808 if len(meta.Errors) > 0 {
2809 fmt.Printf("Errors:\n")
2810 for _, s := range meta.Errors {
2811 fmt.Printf("\t- %s\n", s)
2814 pol := feedback.PolicyPublished
2815 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)
2816 for _, record := range feedback.Records {
2817 idents := record.Identifiers
2818 fmt.Printf("\theaderfrom %q, envelopes from %q, to %q\n", idents.HeaderFrom, idents.EnvelopeFrom, idents.EnvelopeTo)
2819 eval := record.Row.PolicyEvaluated
2821 for _, reason := range eval.Reasons {
2822 reasons += "; " + string(reason.Type)
2823 if reason.Comment != "" {
2824 reasons += fmt.Sprintf(": %q", reason.Comment)
2827 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)
2828 for _, dkim := range record.AuthResults.DKIM {
2830 if dkim.HumanResult != "" {
2831 result = fmt.Sprintf(": %q", dkim.HumanResult)
2833 fmt.Printf("\t\tdkim %s; domain %q selector %q%s\n", dkim.Result, dkim.Domain, dkim.Selector, result)
2835 for _, spf := range record.AuthResults.SPF {
2836 fmt.Printf("\t\tspf %s; domain %q scope %q\n", spf.Result, spf.Domain, spf.Scope)
2842func cmdDMARCDBAddReport(c *cmd) {
2844 c.params = "fromdomain < message"
2845 c.help = "Add a DMARC report to the database."
2853 fromdomain := xparseDomain(args[0], "domain")
2854 fmt.Fprintln(os.Stderr, "reading report message from stdin")
2855 report, err := dmarcrpt.ParseMessageReport(c.log.Logger, os.Stdin)
2856 xcheckf(err, "parse message")
2857 err = dmarcdb.AddReport(context.Background(), report, fromdomain)
2858 xcheckf(err, "add dmarc report")
2861func cmdTLSRPTLookup(c *cmd) {
2863 c.help = `Lookup the TLSRPT record for the domain.
2865A TLSRPT record typically contains an email address where reports about TLS
2866connectivity should be sent. Mail servers attempting delivery to our domain
2867should attempt to use TLS. TLSRPT lets them report how many connection
2868successfully used TLS, and how what kind of errors occurred otherwise.
2875 d := xparseDomain(args[0], "domain")
2876 _, txt, err := tlsrpt.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, d)
2877 xcheckf(err, "tlsrpt lookup for %s", d)
2881func cmdTLSRPTParsereportmsg(c *cmd) {
2882 c.params = "message ..."
2883 c.help = `Parse and print the TLSRPT in the message.
2885The report is printed in formatted JSON.
2892 for _, arg := range args {
2893 f, err := os.Open(arg)
2894 xcheckf(err, "open %q", arg)
2895 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, f)
2896 xcheckf(err, "parse report in %q", arg)
2897 // todo future: only print the highlights?
2898 enc := json.NewEncoder(os.Stdout)
2899 enc.SetIndent("", "\t")
2900 enc.SetEscapeHTML(false)
2901 err = enc.Encode(reportJSON)
2902 xcheckf(err, "write report")
2906func cmdSPFCheck(c *cmd) {
2907 c.params = "domain ip"
2908 c.help = `Check the status of IP for the policy published in DNS for the domain.
2910IPs may be allowed to send for a domain, or disallowed, and several shades in
2911between. If not allowed, an explanation may be provided by the policy. If so,
2912the explanation is printed. The SPF mechanism that matched (if any) is also
2920 domain := xparseDomain(args[0], "domain")
2922 ip := xparseIP(args[1], "ip")
2924 spfargs := spf.Args{
2926 MailFromLocalpart: "user",
2927 MailFromDomain: domain,
2928 HelloDomain: dns.IPDomain{Domain: domain},
2929 LocalIP: net.ParseIP("127.0.0.1"),
2930 LocalHostname: dns.Domain{ASCII: "localhost"},
2932 r, _, explanation, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfargs)
2934 fmt.Printf("error: %s\n", err)
2936 if explanation != "" {
2937 fmt.Printf("explanation: %s\n", explanation)
2939 fmt.Printf("status: %s (%s)\n", r.Result, dnssecStatus(authentic))
2940 if r.Mechanism != "" {
2941 fmt.Printf("mechanism: %s\n", r.Mechanism)
2945func cmdSPFParse(c *cmd) {
2946 c.params = "txtrecord"
2947 c.help = "Parse the record as SPF record. If valid, nothing is printed."
2953 _, _, err := spf.ParseRecord(args[0])
2954 xcheckf(err, "parsing record")
2957func cmdSPFLookup(c *cmd) {
2959 c.help = "Lookup the SPF record for the domain and print it."
2965 domain := xparseDomain(args[0], "domain")
2966 _, txt, _, authentic, err := spf.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
2967 xcheckf(err, "spf lookup for %s", domain)
2969 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2972func cmdMTASTSLookup(c *cmd) {
2974 c.help = `Lookup the MTASTS record and policy for the domain.
2976MTA-STS is a mechanism for a domain to specify if it requires TLS connections
2977for delivering email. If a domain has a valid MTA-STS DNS TXT record at
2978_mta-sts.<domain> it signals it implements MTA-STS. A policy can then be
2979fetched at https://mta-sts.<domain>/.well-known/mta-sts.txt. The policy
2980specifies the mode (enforce, testing, none), which MX servers support TLS and
2981should be used, and how long the policy can be cached.
2988 domain := xparseDomain(args[0], "domain")
2990 record, policy, _, err := mtasts.Get(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
2992 fmt.Printf("error: %s\n", err)
2995 fmt.Printf("DNS TXT record _mta-sts.%s: %s\n", domain.ASCII, record.String())
2999 fmt.Printf("policy at https://mta-sts.%s/.well-known/mta-sts.txt:\n", domain.ASCII)
3000 fmt.Printf("%s", policy.String())
3004func cmdRDAPDomainage(c *cmd) {
3006 c.help = `Lookup the age of domain in RDAP based on latest registration.
3008RDAP is the registration data access protocol. Registries run RDAP services for
3009their top level domains, providing information such as the registration date of
3010domains. This command looks up the "age" of a domain by looking at the most
3011recent "registration", "reregistration" or "reinstantiation" event.
3013Email messages from recently registered domains are often treated with
3014suspicion, and some mail systems are more likely to classify them as junk.
3016On each invocation, a bootstrap file with a list of registries (of top-level
3017domains) is retrieved, without caching. Do not run this command too often with
3025 domain := xparseDomain(args[0], "domain")
3027 registration, err := rdap.LookupLastDomainRegistration(context.Background(), domain)
3028 xcheckf(err, "looking up domain in rdap")
3030 age := time.Since(registration)
3031 const day = 24 * time.Hour
3032 const year = 365 * day
3034 days := (age - years*year) / day
3038 } else if years > 0 {
3039 s = fmt.Sprintf("%d years, ", years)
3044 s += fmt.Sprintf("%d days", days)
3049func cmdRetrain(c *cmd) {
3050 c.params = "[accountname]"
3051 c.help = `Recreate and retrain the junk filter for the account or all accounts.
3053Useful after having made changes to the junk filter configuration, or if the
3054implementation has changed.
3066 ctlcmdRetrain(xctl(), account)
3069func ctlcmdRetrain(ctl *ctl, account string) {
3070 ctl.xwrite("retrain")
3075func cmdTLSRPTDBAddReport(c *cmd) {
3077 c.params = "< message"
3078 c.help = "Parse a TLS report from the message and add it to the database."
3080 c.flag.BoolVar(&hostReport, "hostreport", false, "report for a host instead of domain")
3088 // First read message, to get the From-header. Then parse it as TLSRPT.
3089 fmt.Fprintln(os.Stderr, "reading report message from stdin")
3090 buf, err := io.ReadAll(os.Stdin)
3091 xcheckf(err, "reading message")
3092 part, err := message.Parse(c.log.Logger, true, bytes.NewReader(buf))
3093 xcheckf(err, "parsing message")
3094 if part.Envelope == nil || len(part.Envelope.From) != 1 {
3095 log.Fatalf("message must have one From-header")
3097 from := part.Envelope.From[0]
3098 domain := xparseDomain(from.Host, "domain")
3100 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, bytes.NewReader(buf))
3101 xcheckf(err, "parsing tls report in message")
3103 mailfrom := from.User + "@" + from.Host // todo future: should escape and such
3104 report := reportJSON.Convert()
3105 err = tlsrptdb.AddReport(context.Background(), c.log, domain, mailfrom, hostReport, &report)
3106 xcheckf(err, "add tls report to database")
3109func cmdDNSBLCheck(c *cmd) {
3110 c.params = "zone ip"
3111 c.help = `Test if IP is in the DNS blocklist of the zone, e.g. bl.spamcop.net.
3113If the IP is in the blocklist, an explanation is printed. This is typically a
3114URL with more information.
3121 zone := xparseDomain(args[0], "zone")
3122 ip := xparseIP(args[1], "ip")
3124 status, explanation, err := dnsbl.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, zone, ip)
3125 fmt.Printf("status: %s\n", status)
3126 if status == dnsbl.StatusFail {
3127 fmt.Printf("explanation: %q\n", explanation)
3130 fmt.Printf("error: %s\n", err)
3134func cmdDNSBLCheckhealth(c *cmd) {
3136 c.help = `Check the health of the DNS blocklist represented by zone, e.g. bl.spamcop.net.
3138The health of a DNS blocklist can be checked by querying for 127.0.0.1 and
3139127.0.0.2. The second must and the first must not be present.
3146 zone := xparseDomain(args[0], "zone")
3147 err := dnsbl.CheckHealth(context.Background(), c.log.Logger, dns.StrictResolver{}, zone)
3148 xcheckf(err, "unhealthy")
3149 fmt.Println("healthy")
3152func cmdCheckupdate(c *cmd) {
3153 c.help = `Check if a newer version of mox is available.
3155A single DNS TXT lookup to _updates.xmox.nl tells if a new version is
3156available. If so, a changelog is fetched from https://updates.xmox.nl, and the
3157individual entries verified with a builtin public key. The changelog is
3160 if len(c.Parse()) != 0 {
3165 current, lastknown, _, err := store.LastKnown()
3167 log.Printf("getting last known version: %s", err)
3169 fmt.Printf("last known version: %s\n", lastknown)
3170 fmt.Printf("current version: %s\n", current)
3172 latest, _, err := updates.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dns.Domain{ASCII: changelogDomain})
3173 xcheckf(err, "lookup of latest version")
3174 fmt.Printf("latest version: %s\n", latest)
3176 if latest.After(current) {
3177 changelog, err := updates.FetchChangelog(context.Background(), c.log.Logger, changelogURL, current, changelogPubKey)
3178 xcheckf(err, "fetching changelog")
3179 if len(changelog.Changes) == 0 {
3180 log.Printf("no changes in changelog")
3183 fmt.Println("Changelog")
3184 for _, c := range changelog.Changes {
3185 fmt.Println("\n" + strings.TrimSpace(c.Text))
3190func cmdCid(c *cmd) {
3192 c.help = `Turn an ID from a Received header into a cid, for looking up in logs.
3194A cid is essentially a connection counter initialized when mox starts. Each log
3195line contains a cid. Received headers added by mox contain a unique ID that can
3196be decrypted to a cid by admin of a mox instance only.
3204 recvidpath := mox.DataDirPath("receivedid.key")
3205 recvidbuf, err := os.ReadFile(recvidpath)
3206 xcheckf(err, "reading %s", recvidpath)
3207 if len(recvidbuf) != 16+8 {
3208 log.Fatalf("bad data in %s: got %d bytes, expect 16+8=24", recvidpath, len(recvidbuf))
3210 err = mox.ReceivedIDInit(recvidbuf[:16], recvidbuf[16:])
3211 xcheckf(err, "init receivedid")
3213 cid, err := mox.ReceivedToCid(args[0])
3214 xcheckf(err, "received id to cid")
3215 fmt.Printf("%x\n", cid)
3218func cmdVersion(c *cmd) {
3219 c.help = "Prints this mox version."
3220 if len(c.Parse()) != 0 {
3223 fmt.Println(moxvar.Version)
3224 fmt.Printf("%s/%s\n", runtime.GOOS, runtime.GOARCH)
3227func cmdWebapi(c *cmd) {
3228 c.params = "[method [baseurl-with-credentials]"
3229 c.help = "Lists available methods, prints request/response parameters for method, or calls a method with a request read from standard input."
3235 t := reflect.TypeFor[webapi.Methods]()
3236 methods := map[string]reflect.Type{}
3238 for i := 0; i < t.NumMethod(); i++ {
3240 methods[mt.Name] = mt.Type
3241 ml = append(ml, mt.Name)
3245 fmt.Println(strings.Join(ml, "\n"))
3249 mt, ok := methods[args[0]]
3251 log.Fatalf("unknown method %q", args[0])
3253 resultNotJSON := mt.Out(0).Kind() == reflect.Interface
3256 fmt.Println("# Example request")
3258 printJSON("\t", mox.FillExample(nil, reflect.New(mt.In(1))).Interface())
3261 fmt.Println("Output is non-JSON data.")
3264 fmt.Println("# Example response")
3266 printJSON("\t", mox.FillExample(nil, reflect.New(mt.Out(0))).Interface())
3272 response = reflect.New(mt.Out(0))
3275 fmt.Fprintln(os.Stderr, "reading request from stdin...")
3276 request, err := io.ReadAll(os.Stdin)
3277 xcheckf(err, "read message")
3279 dec := json.NewDecoder(bytes.NewReader(request))
3280 dec.DisallowUnknownFields()
3281 err = dec.Decode(reflect.New(mt.In(1)).Interface())
3282 xcheckf(err, "parsing request")
3284 resp, err := http.PostForm(args[1]+args[0], url.Values{"request": []string{string(request)}})
3285 xcheckf(err, "http post")
3286 defer resp.Body.Close()
3287 if resp.StatusCode == http.StatusBadRequest {
3288 buf, err := io.ReadAll(&moxio.LimitReader{R: resp.Body, Limit: 10 * 1024})
3289 xcheckf(err, "reading response for 400 bad request error")
3290 err = json.Unmarshal(buf, &response)
3292 printJSON("", response)
3294 fmt.Fprintf(os.Stderr, "(not json)\n")
3295 os.Stderr.Write(buf)
3298 } else if resp.StatusCode != http.StatusOK {
3299 fmt.Fprintf(os.Stderr, "http response %s\n", resp.Status)
3300 _, err := io.Copy(os.Stderr, resp.Body)
3301 xcheckf(err, "copy body")
3303 err := json.NewDecoder(resp.Body).Decode(&resp)
3304 xcheckf(err, "unmarshal response")
3305 printJSON("", response)
3309func printJSON(indent string, v any) {
3310 fmt.Printf("%s", indent)
3311 enc := json.NewEncoder(os.Stdout)
3312 enc.SetIndent(indent, "\t")
3313 enc.SetEscapeHTML(false)
3314 err := enc.Encode(v)
3315 xcheckf(err, "encode json")
3318// 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.
3319func cmdBumpUIDValidity(c *cmd) {
3320 c.params = "account [mailbox]"
3321 c.help = `Change the IMAP UID validity of the mailbox, causing IMAP clients to refetch messages.
3323This can be useful after manually repairing metadata about the account/mailbox.
3325Opens account database file directly. Ensure mox does not have the account
3326open, or is not running.
3329 if len(args) != 1 && len(args) != 2 {
3334 a, err := store.OpenAccount(c.log, args[0], false)
3335 xcheckf(err, "open account")
3337 if err := a.Close(); err != nil {
3338 log.Printf("closing account: %v", err)
3342 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3343 uidvalidity, err := a.NextUIDValidity(tx)
3345 return fmt.Errorf("assigning next uid validity: %v", err)
3348 q := bstore.QueryTx[store.Mailbox](tx)
3350 q.FilterEqual("Name", args[1])
3352 mbl, err := q.SortAsc("Name").List()
3354 return fmt.Errorf("looking up mailbox: %v", err)
3356 if len(args) == 2 && len(mbl) != 1 {
3357 return fmt.Errorf("looking up mailbox %q, found %d mailboxes", args[1], len(mbl))
3359 for _, mb := range mbl {
3360 mb.UIDValidity = uidvalidity
3361 err = tx.Update(&mb)
3363 return fmt.Errorf("updating uid validity for mailbox: %v", err)
3365 fmt.Printf("uid validity for %q updated to %d\n", mb.Name, uidvalidity)
3369 xcheckf(err, "updating database")
3372func cmdReassignUIDs(c *cmd) {
3373 c.params = "account [mailboxid]"
3374 c.help = `Reassign UIDs in one mailbox or all mailboxes in an account and bump UID validity, causing IMAP clients to refetch messages.
3376Opens account database file directly. Ensure mox does not have the account
3377open, or is not running.
3380 if len(args) != 1 && len(args) != 2 {
3387 mailboxID, err = strconv.ParseInt(args[1], 10, 64)
3388 xcheckf(err, "parsing mailbox id")
3392 a, err := store.OpenAccount(c.log, args[0], false)
3393 xcheckf(err, "open account")
3395 if err := a.Close(); err != nil {
3396 log.Printf("closing account: %v", err)
3400 // Gather the last-assigned UIDs per mailbox.
3401 uidlasts := map[int64]store.UID{}
3403 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3404 // Reassign UIDs, going per mailbox. We assign starting at 1, only changing the
3405 // message if it isn't already at the intended UID. Doing it in this order ensures
3406 // we don't get into trouble with duplicate UIDs for a mailbox. We assign a new
3407 // modseq. Not strictly needed, for doesn't hurt.
3408 modseq, err := a.NextModSeq(tx)
3409 xcheckf(err, "assigning next modseq")
3411 q := bstore.QueryTx[store.Message](tx)
3413 q.FilterNonzero(store.Message{MailboxID: mailboxID})
3415 q.SortAsc("MailboxID", "UID")
3416 err = q.ForEach(func(m store.Message) error {
3417 uidlasts[m.MailboxID]++
3418 uid := uidlasts[m.MailboxID]
3422 if err := tx.Update(&m); err != nil {
3423 return fmt.Errorf("updating uid for message: %v", err)
3429 return fmt.Errorf("reading through messages: %v", err)
3432 // Now update the uidnext and uidvalidity for each mailbox.
3433 err = bstore.QueryTx[store.Mailbox](tx).ForEach(func(mb store.Mailbox) error {
3434 // Assign each mailbox a completely new uidvalidity.
3435 uidvalidity, err := a.NextUIDValidity(tx)
3437 return fmt.Errorf("assigning next uid validity: %v", err)
3440 if mb.UIDValidity >= uidvalidity {
3441 // This should not happen, but since we're fixing things up after a hypothetical
3442 // mishap, might as well account for inconsistent uidvalidity.
3443 next := store.NextUIDValidity{ID: 1, Next: mb.UIDValidity + 2}
3444 if err := tx.Update(&next); err != nil {
3445 log.Printf("updating nextuidvalidity: %v, continuing", err)
3449 mb.UIDValidity = uidvalidity
3451 mb.UIDNext = uidlasts[mb.ID] + 1
3452 if err := tx.Update(&mb); err != nil {
3453 return fmt.Errorf("updating uidvalidity and uidnext for mailbox: %v", err)
3458 return fmt.Errorf("updating mailboxes: %v", err)
3462 xcheckf(err, "updating database")
3465func cmdFixUIDMeta(c *cmd) {
3466 c.params = "account"
3467 c.help = `Fix inconsistent UIDVALIDITY and UIDNEXT in messages/mailboxes/account.
3469The next UID to use for a message in a mailbox should always be higher than any
3470existing message UID in the mailbox. If it is not, the mailbox UIDNEXT is
3473Each mailbox has a UIDVALIDITY sequence number, which should always be lower
3474than the per-account next UIDVALIDITY to use. If it is not, the account next
3475UIDVALIDITY is updated.
3477Opens account database file directly. Ensure mox does not have the account
3478open, or is not running.
3486 a, err := store.OpenAccount(c.log, args[0], false)
3487 xcheckf(err, "open account")
3489 if err := a.Close(); err != nil {
3490 log.Printf("closing account: %v", err)
3494 var maxUIDValidity uint32
3496 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3497 // We look at each mailbox, retrieve its max UID and compare against the mailbox
3499 err := bstore.QueryTx[store.Mailbox](tx).ForEach(func(mb store.Mailbox) error {
3500 if mb.UIDValidity > maxUIDValidity {
3501 maxUIDValidity = mb.UIDValidity
3503 m, err := bstore.QueryTx[store.Message](tx).FilterNonzero(store.Message{MailboxID: mb.ID}).SortDesc("UID").Limit(1).Get()
3504 if err == bstore.ErrAbsent || err == nil && m.UID < mb.UIDNext {
3506 } else if err != nil {
3507 return fmt.Errorf("finding message with max uid in mailbox: %w", err)
3509 olduidnext := mb.UIDNext
3510 mb.UIDNext = m.UID + 1
3511 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)
3512 if err := tx.Update(&mb); err != nil {
3513 return fmt.Errorf("updating mailbox uidnext: %v", err)
3518 return fmt.Errorf("processing mailboxes: %v", err)
3521 uidvalidity := store.NextUIDValidity{ID: 1}
3522 if err := tx.Get(&uidvalidity); err != nil {
3523 return fmt.Errorf("reading account next uidvalidity: %v", err)
3525 if maxUIDValidity >= uidvalidity.Next {
3526 log.Printf("account next uidvalidity %d <= highest uidvalidity %d found in mailbox, resetting account next uidvalidity to %d", uidvalidity.Next, maxUIDValidity, maxUIDValidity+1)
3527 uidvalidity.Next = maxUIDValidity + 1
3528 if err := tx.Update(&uidvalidity); err != nil {
3529 return fmt.Errorf("updating account next uidvalidity: %v", err)
3535 xcheckf(err, "updating database")
3538func cmdFixmsgsize(c *cmd) {
3539 c.params = "[account]"
3540 c.help = `Ensure message sizes in the database matching the sum of the message prefix length and on-disk file size.
3542Messages with an inconsistent size are also parsed again.
3544If an inconsistency is found, you should probably also run "mox
3545bumpuidvalidity" on the mailboxes or entire account to force IMAP clients to
3558 ctlcmdFixmsgsize(xctl(), account)
3561func ctlcmdFixmsgsize(ctl *ctl, account string) {
3562 ctl.xwrite("fixmsgsize")
3565 ctl.xstreamto(os.Stdout)
3568func cmdReparse(c *cmd) {
3569 c.params = "[account]"
3570 c.help = `Parse all messages in the account or all accounts again.
3572Can be useful after upgrading mox with improved message parsing. Messages are
3573parsed in batches, so other access to the mailboxes/messages are not blocked
3574while reparsing all messages.
3586 ctlcmdReparse(xctl(), account)
3589func ctlcmdReparse(ctl *ctl, account string) {
3590 ctl.xwrite("reparse")
3593 ctl.xstreamto(os.Stdout)
3596func cmdEnsureParsed(c *cmd) {
3597 c.params = "account"
3598 c.help = "Ensure messages in the database have a pre-parsed MIME form in the database."
3600 c.flag.BoolVar(&all, "all", false, "store new parsed message for all messages")
3607 a, err := store.OpenAccount(c.log, args[0], false)
3608 xcheckf(err, "open account")
3610 if err := a.Close(); err != nil {
3611 log.Printf("closing account: %v", err)
3616 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3617 q := bstore.QueryTx[store.Message](tx)
3618 q.FilterEqual("Expunged", false)
3619 q.FilterFn(func(m store.Message) bool {
3620 return all || m.ParsedBuf == nil
3624 return fmt.Errorf("list messages: %v", err)
3626 for _, m := range l {
3627 mr := a.MessageReader(m)
3628 p, err := message.EnsurePart(c.log.Logger, false, mr, m.Size)
3630 log.Printf("parsing message %d: %v (continuing)", m.ID, err)
3632 m.ParsedBuf, err = json.Marshal(p)
3634 return fmt.Errorf("marshal parsed message: %v", err)
3636 if err := tx.Update(&m); err != nil {
3637 return fmt.Errorf("update message: %v", err)
3643 xcheckf(err, "update messages with parsed mime structure")
3644 fmt.Printf("%d messages updated\n", n)
3647func cmdRecalculateMailboxCounts(c *cmd) {
3648 c.params = "account"
3649 c.help = `Recalculate message counts for all mailboxes in the account, and total message size for quota.
3651When a message is added to/removed from a mailbox, or when message flags change,
3652the total, unread, unseen and deleted messages are accounted, the total size of
3653the mailbox, and the total message size for the account. In case of a bug in
3654this accounting, the numbers could become incorrect. This command will find, fix
3663 ctlcmdRecalculateMailboxCounts(xctl(), args[0])
3666func ctlcmdRecalculateMailboxCounts(ctl *ctl, account string) {
3667 ctl.xwrite("recalculatemailboxcounts")
3670 ctl.xstreamto(os.Stdout)
3673func cmdMessageParse(c *cmd) {
3674 c.params = "message.eml"
3675 c.help = "Parse message, print JSON representation."
3678 c.flag.BoolVar(&smtputf8, "smtputf8", false, "check if message needs smtputf8")
3684 f, err := os.Open(args[0])
3685 xcheckf(err, "open")
3688 part, err := message.Parse(c.log.Logger, false, f)
3689 xcheckf(err, "parsing message")
3690 err = part.Walk(c.log.Logger, nil)
3691 xcheckf(err, "parsing nested parts")
3692 enc := json.NewEncoder(os.Stdout)
3693 enc.SetIndent("", "\t")
3694 enc.SetEscapeHTML(false)
3695 err = enc.Encode(part)
3696 xcheckf(err, "write")
3699 needs, err := part.NeedsSMTPUTF8()
3700 xcheckf(err, "checking if message needs smtputf8")
3701 fmt.Println("message needs smtputf8:", needs)
3705func cmdOpenaccounts(c *cmd) {
3707 c.params = "datadir account ..."
3708 c.help = `Open and close accounts, for triggering data upgrades, for tests.
3710Opens database files directly, not going through a running mox instance.
3718 dataDir := filepath.Clean(args[0])
3719 for _, accName := range args[1:] {
3720 accDir := filepath.Join(dataDir, "accounts", accName)
3721 log.Printf("opening account %s...", accDir)
3722 a, err := store.OpenAccountDB(c.log, accDir, accName)
3723 xcheckf(err, "open account %s", accName)
3724 err = a.ThreadingWait(c.log)
3725 xcheckf(err, "wait for threading upgrade to complete for %s", accName)
3727 xcheckf(err, "close account %s", accName)
3731func cmdReassignthreads(c *cmd) {
3732 c.params = "[account]"
3733 c.help = `Reassign message threads.
3735For all accounts, or optionally only the specified account.
3737Threading for all messages in an account is first reset, and new base subject
3738and normalized message-id saved with the message. Then all messages are
3739evaluated and matched against their parents/ancestors.
3741Messages are matched based on the References header, with a fall-back to an
3742In-Reply-To header, and if neither is present/valid, based only on base
3745A References header typically points to multiple previous messages in a
3746hierarchy. From oldest ancestor to most recent parent. An In-Reply-To header
3747would have only a message-id of the parent message.
3749A message is only linked to a parent/ancestor if their base subject is the
3750same. This ensures unrelated replies, with a new subject, are placed in their
3753The base subject is lower cased, has whitespace collapsed to a single
3754space, and some components removed: leading "Re:", "Fwd:", "Fw:", or bracketed
3755tag (that mailing lists often add, e.g. "[listname]"), trailing "(fwd)", or
3756enclosing "[fwd: ...]".
3758Messages are linked to all their ancestors. If an intermediate parent/ancestor
3759message is deleted in the future, the message can still be linked to the earlier
3760ancestors. If the direct parent already wasn't available while matching, this is
3761stored as the message having a "missing link" to its stored ancestors.
3773 ctlcmdReassignthreads(xctl(), account)
3776func ctlcmdReassignthreads(ctl *ctl, account string) {
3777 ctl.xwrite("reassignthreads")
3780 ctl.xstreamto(os.Stdout)
3783func cmdIMAPServe(c *cmd) {
3784 c.params = "preauth-address"
3785 c.help = `Initiate a preauthenticated IMAP connection on file descriptor 0.
3787For use with tools that can do IMAP over tunneled connections, e.g. with SSH
3788during migrations. TLS is not possible on the connection, and authentication
3789does not require TLS.
3792 c.flag.BoolVar(&fd0, "fd0", false, "write IMAP to file descriptor 0 instead of stdout")
3803 ctlcmdIMAPServe(xctl(), address, os.Stdin, output)
3806func ctlcmdIMAPServe(ctl *ctl, address string, input io.ReadCloser, output io.WriteCloser) {
3807 ctl.xwrite("imapserve")
3811 done := make(chan struct{}, 1)
3816 _, err := io.Copy(output, ctl.conn)
3820 log.Printf("reading from imap: %v", err)
3826 _, err := io.Copy(ctl.conn, input)
3830 log.Printf("writing to imap: %v", err)
3835func cmdReadmessages(c *cmd) {
3837 c.params = "datadir account ..."
3838 c.help = `Open account, parse several headers for all messages.
3840For performance testing.
3842Opens database files directly, not going through a running mox instance.
3845 gomaxprocs := runtime.GOMAXPROCS(0)
3846 var procs, workqueuesize, limit int
3847 c.flag.IntVar(&procs, "procs", gomaxprocs, "number of goroutines for reading messages")
3848 c.flag.IntVar(&workqueuesize, "workqueuesize", 2*gomaxprocs, "number of messages to keep in work queue")
3849 c.flag.IntVar(&limit, "limit", 0, "number of messages to process if greater than zero")
3855 type threadPrep struct {
3860 threadingFields := [][]byte{
3861 []byte("references"),
3862 []byte("in-reply-to"),
3865 dataDir := filepath.Clean(args[0])
3866 for _, accName := range args[1:] {
3867 accDir := filepath.Join(dataDir, "accounts", accName)
3868 log.Printf("opening account %s...", accDir)
3869 a, err := store.OpenAccountDB(c.log, accDir, accName)
3870 xcheckf(err, "open account %s", accName)
3872 prepareMessages := func(in, out chan moxio.Work[store.Message, threadPrep]) {
3873 headerbuf := make([]byte, 8*1024)
3874 scratch := make([]byte, 4*1024)
3882 var partialPart struct {
3886 if err := json.Unmarshal(m.ParsedBuf, &partialPart); err != nil {
3887 w.Err = fmt.Errorf("unmarshal part: %v", err)
3889 size := partialPart.BodyOffset - partialPart.HeaderOffset
3890 if int(size) > len(headerbuf) {
3891 headerbuf = make([]byte, size)
3894 buf := headerbuf[:int(size)]
3895 err := func() error {
3896 mr := a.MessageReader(m)
3899 // ReadAt returns whole buffer or error. Single read should be fast.
3900 n, err := mr.ReadAt(buf, partialPart.HeaderOffset)
3901 if err != nil || n != len(buf) {
3902 return fmt.Errorf("read header: %v", err)
3908 } else if h, err := message.ParseHeaderFields(buf, scratch, threadingFields); err != nil {
3911 w.Out.references = h["References"]
3912 w.Out.inReplyTo = h["In-Reply-To"]
3925 processMessage := func(m store.Message, prep threadPrep) error {
3927 log.Printf("%d messages (delta %s)", n, time.Since(t))
3934 wq := moxio.NewWorkQueue[store.Message, threadPrep](procs, workqueuesize, prepareMessages, processMessage)
3936 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3937 q := bstore.QueryTx[store.Message](tx)
3938 q.FilterEqual("Expunged", false)
3943 err = q.ForEach(wq.Add)
3951 xcheckf(err, "processing message")
3954 xcheckf(err, "close account %s", accName)
3955 log.Printf("account %s, total time %s", accName, time.Since(t0))
3959func cmdQueueFillRetired(c *cmd) {
3961 c.help = `Fill retired messag and webhooks queue with testdata.
3963For testing the pagination. Operates directly on queue database.
3966 c.flag.IntVar(&n, "n", 10000, "retired messages and retired webhooks to insert")
3974 xcheckf(err, "init queue")
3975 err = queue.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3978 // Cause autoincrement ID for queue.Msg to be forwarded, and use the reserved ID
3979 // space for inserting retired messages.
3981 err = tx.Insert(&fm)
3982 xcheckf(err, "temporarily insert message to get autoincrement sequence")
3983 err = tx.Delete(&fm)
3984 xcheckf(err, "removing temporary message for resetting autoincrement sequence")
3986 err = tx.Insert(&fm)
3987 xcheckf(err, "temporarily insert message to forward autoincrement sequence")
3988 err = tx.Delete(&fm)
3989 xcheckf(err, "removing temporary message after forwarding autoincrement sequence")
3992 // And likewise for webhooks.
3993 fh := queue.Hook{Account: "x", URL: "x", NextAttempt: time.Now()}
3994 err = tx.Insert(&fh)
3995 xcheckf(err, "temporarily insert webhook to get autoincrement sequence")
3996 err = tx.Delete(&fh)
3997 xcheckf(err, "removing temporary webhook for resetting autoincrement sequence")
3999 err = tx.Insert(&fh)
4000 xcheckf(err, "temporarily insert webhook to forward autoincrement sequence")
4001 err = tx.Delete(&fh)
4002 xcheckf(err, "removing temporary webhook after forwarding autoincrement sequence")
4005 for i := 0; i < n; i++ {
4006 t0 := now.Add(-time.Duration(i) * time.Second)
4007 last := now.Add(-time.Duration(i/10) * time.Second)
4008 mr := queue.MsgRetired{
4009 ID: fm.ID + int64(i),
4011 SenderAccount: "test",
4012 SenderLocalpart: "mox",
4013 SenderDomainStr: "localhost",
4014 FromID: fmt.Sprintf("%016d", i),
4015 RecipientLocalpart: "mox",
4016 RecipientDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "localhost"}},
4017 RecipientDomainStr: "localhost",
4020 Results: []queue.MsgResult{
4023 Duration: time.Millisecond,
4030 Size: int64(i * 100),
4031 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
4032 Subject: fmt.Sprintf("test message %d", i),
4033 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
4035 RecipientAddress: "mox@localhost",
4037 KeepUntil: now.Add(48 * time.Hour),
4039 err := tx.Insert(&mr)
4040 xcheckf(err, "inserting retired message")
4043 for i := 0; i < n; i++ {
4044 t0 := now.Add(-time.Duration(i) * time.Second)
4045 last := now.Add(-time.Duration(i/10) * time.Second)
4050 hr := queue.HookRetired{
4051 ID: fh.ID + int64(i),
4052 QueueMsgID: fm.ID + int64(i),
4053 FromID: fmt.Sprintf("%016d", i),
4054 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
4055 Subject: fmt.Sprintf("test message %d", i),
4056 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
4058 URL: "http://localhost/hook",
4059 IsIncoming: i%10 == 0,
4060 OutgoingEvent: event,
4065 Results: []queue.HookResult{
4068 Duration: time.Millisecond,
4069 URL: "http://localhost/hook",
4078 KeepUntil: now.Add(48 * time.Hour),
4080 err := tx.Insert(&hr)
4081 xcheckf(err, "inserting retired hook")
4086 xcheckf(err, "add to queue")
4087 log.Printf("added %d retired messages and %d retired webhooks", n, n)