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
462 store.MsgFilesPerDirShiftSet(13) // For 1<<13 = 8k message files per directory.
464 ctxbg := context.Background()
470 // If invoked as sendmail, e.g. /usr/sbin/sendmail, we do enough so cron can get a
471 // message sent using smtp submission to a configured server.
472 if len(os.Args) > 0 && filepath.Base(os.Args[0]) == "sendmail" {
474 flag: flag.NewFlagSet("sendmail", flag.ExitOnError),
475 flagArgs: os.Args[1:],
476 log: mlog.New("sendmail", nil),
482 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")
483 flag.StringVar(&loglevel, "loglevel", "", "if non-empty, this log level is set early in startup")
484 flag.BoolVar(&pedantic, "pedantic", false, "protocol violations result in errors instead of accepting/working around them")
485 flag.BoolVar(&store.CheckConsistencyOnClose, "checkconsistency", false, "dangerous option for testing only, enables data checks that abort/panic when inconsistencies are found")
487 var cpuprofile, memprofile, tracefile string
488 flag.StringVar(&cpuprofile, "cpuprof", "", "store cpu profile to file")
489 flag.StringVar(&memprofile, "memprof", "", "store mem profile to file")
490 flag.StringVar(&tracefile, "trace", "", "store execution trace to file")
492 flag.Usage = func() { usage(cmds, false) }
500 defer traceExecution(tracefile)()
502 defer profile(cpuprofile, memprofile)()
505 mox.SetPedantic(true)
508 mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
513 if level, ok := mlog.Levels[ll]; ok {
514 mox.Conf.Log[""] = level
515 mlog.SetConfig(mox.Conf.Log)
516 // note: SetConfig may be called again when subcommands loads config.
518 log.Fatalf("unknown loglevel %q", loglevel)
523 for _, c := range cmds {
524 for i, w := range c.words {
525 if i >= len(args) || w != args[i] {
527 partial = append(partial, c)
532 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
533 c.flagArgs = args[len(c.words):]
534 c.log = mlog.New(strings.Join(c.words, ""), nil)
538 if len(partial) > 0 {
544func xcheckf(err error, format string, args ...any) {
548 msg := fmt.Sprintf(format, args...)
549 log.Fatalf("%s: %s", msg, err)
552func xparseIP(s, what string) net.IP {
555 log.Fatalf("invalid %s: %q", what, s)
560func xparseDomain(s, what string) dns.Domain {
561 d, err := dns.ParseDomain(s)
562 xcheckf(err, "parsing %s %q", what, s)
566func cmdClientConfig(c *cmd) {
568 c.help = `Print the configuration for email clients for a domain.
570Sending email is typically not done on the SMTP port 25, but on submission
571ports 465 (with TLS) and 587 (without initial TLS, but usually added to the
572connection with STARTTLS). For IMAP, the port with TLS is 993 and without is
575Without TLS/STARTTLS, passwords are sent in clear text, which should only be
576configured over otherwise secured connections, like a VPN.
582 d := xparseDomain(args[0], "domain")
587func printClientConfig(d dns.Domain) {
588 cc, err := admin.ClientConfigsDomain(d)
589 xcheckf(err, "getting client config")
590 fmt.Printf("%-20s %-30s %5s %-15s %s\n", "Protocol", "Host", "Port", "Listener", "Note")
591 for _, e := range cc.Entries {
592 fmt.Printf("%-20s %-30s %5d %-15s %s\n", e.Protocol, e.Host, e.Port, e.Listener, e.Note)
595To prevent authentication mechanism downgrade attempts that may result in
596clients sending plain text passwords to a MitM, clients should always be
597explicitly configured with the most secure authentication mechanism supported,
598the first of: SCRAM-SHA-256-PLUS, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, SCRAM-SHA-1,
603func cmdConfigTest(c *cmd) {
604 c.help = `Parses and validates the configuration files.
606If valid, the command exits with status 0. If not valid, all errors encountered
614 mox.FilesImmediate = true
616 _, errs := mox.ParseConfig(context.Background(), c.log, mox.ConfigStaticPath, true, true, false)
618 log.Printf("multiple errors:")
619 for _, err := range errs {
620 log.Printf("%s", err)
623 } else if len(errs) == 1 {
624 log.Fatalf("%s", errs[0])
627 fmt.Println("config OK")
630func cmdConfigDescribeStatic(c *cmd) {
631 c.params = ">mox.conf"
632 c.help = `Prints an annotated empty configuration for use as mox.conf.
634The static configuration file cannot be reloaded while mox is running. Mox has
635to be restarted for changes to the static configuration file to take effect.
637This configuration file needs modifications to make it valid. For example, it
638may contain unfinished list items.
640 if len(c.Parse()) != 0 {
645 err := sconf.Describe(os.Stdout, &sc)
646 xcheckf(err, "describing config")
649func cmdConfigDescribeDomains(c *cmd) {
650 c.params = ">domains.conf"
651 c.help = `Prints an annotated empty configuration for use as domains.conf.
653The domains configuration file contains the domains and their configuration,
654and accounts and their configuration. This includes the configured email
655addresses. The mox admin web interface, and the mox command line interface, can
656make changes to this file. Mox automatically reloads this file when it changes.
658Like the static configuration, the example domains.conf printed by this command
659needs modifications to make it valid.
661 if len(c.Parse()) != 0 {
665 var dc config.Dynamic
666 err := sconf.Describe(os.Stdout, &dc)
667 xcheckf(err, "describing config")
670func cmdConfigPrintservice(c *cmd) {
671 c.params = ">mox.service"
672 c.help = `Prints a systemd unit service file for mox.
674This is the same file as generated using quickstart. If the systemd service file
675has changed with a newer version of mox, use this command to generate an up to
678 if len(c.Parse()) != 0 {
682 pwd, err := os.Getwd()
684 log.Printf("current working directory: %v", err)
687 service := strings.ReplaceAll(moxService, "/home/mox", pwd)
691func cmdConfigDomainAdd(c *cmd) {
692 c.params = "[-disabled] domain account [localpart]"
693 c.help = `Adds a new domain to the configuration and reloads the configuration.
695The account is used for the postmaster mailboxes the domain, including as DMARC and
696TLS reporting. Localpart is the "username" at the domain for this account. If
697must be set if and only if account does not yet exist.
699The domain can be created in disabled mode, preventing automatically requesting
700TLS certificates with ACME, and rejecting incoming/outgoing messages involving
701the domain, but allowing further configuration of the domain.
704 c.flag.BoolVar(&disabled, "disabled", false, "disable the new domain")
706 if len(args) != 2 && len(args) != 3 {
710 d := xparseDomain(args[0], "domain")
712 var localpart smtp.Localpart
715 localpart, err = smtp.ParseLocalpart(args[2])
716 xcheckf(err, "parsing localpart")
718 ctlcmdConfigDomainAdd(xctl(), disabled, d, args[1], localpart)
721func ctlcmdConfigDomainAdd(ctl *ctl, disabled bool, domain dns.Domain, account string, localpart smtp.Localpart) {
722 ctl.xwrite("domainadd")
728 ctl.xwrite(domain.Name())
730 ctl.xwrite(string(localpart))
732 fmt.Printf("domain added, remember to add dns records, see:\n\nmox config dnsrecords %s\nmox config dnscheck %s\n", domain.Name(), domain.Name())
735func cmdConfigDomainRemove(c *cmd) {
737 c.help = `Remove a domain from the configuration and reload the configuration.
739This is a dangerous operation. Incoming email delivery for this domain will be
747 d := xparseDomain(args[0], "domain")
749 ctlcmdConfigDomainRemove(xctl(), d)
752func ctlcmdConfigDomainRemove(ctl *ctl, d dns.Domain) {
753 ctl.xwrite("domainrm")
756 fmt.Printf("domain removed, remember to remove dns records for %s\n", d)
759func cmdConfigDomainDisable(c *cmd) {
761 c.help = `Disable a domain and reload the configuration.
763This is a dangerous operation. Incoming/outgoing messages involving this domain
771 d := xparseDomain(args[0], "domain")
773 ctlcmdConfigDomainDisabled(xctl(), d, true)
774 fmt.Printf("domain disabled")
777func cmdConfigDomainEnable(c *cmd) {
779 c.help = `Enable a domain and reload the configuration.
781Incoming/outgoing messages involving this domain will be accepted again.
788 d := xparseDomain(args[0], "domain")
790 ctlcmdConfigDomainDisabled(xctl(), d, false)
793func ctlcmdConfigDomainDisabled(ctl *ctl, d dns.Domain, disabled bool) {
794 ctl.xwrite("domaindisabled")
804func cmdConfigAliasList(c *cmd) {
806 c.help = `Show aliases (lists) for domain.`
813 ctlcmdConfigAliasList(xctl(), args[0])
816func ctlcmdConfigAliasList(ctl *ctl, address string) {
817 ctl.xwrite("aliaslist")
820 ctl.xstreamto(os.Stdout)
823func cmdConfigAliasPrint(c *cmd) {
825 c.help = `Print settings and members of alias (list).`
832 ctlcmdConfigAliasPrint(xctl(), args[0])
835func ctlcmdConfigAliasPrint(ctl *ctl, address string) {
836 ctl.xwrite("aliasprint")
839 ctl.xstreamto(os.Stdout)
842func cmdConfigAliasAdd(c *cmd) {
843 c.params = "alias@domain rcpt1@domain ..."
844 c.help = `Add new alias (list) with one or more addresses and public posting enabled.
846An alias is used for delivering incoming email to multiple recipients. If you
847want to add an address to an account, don't use an alias, just add the address
855 alias := config.Alias{PostPublic: true, Addresses: args[1:]}
858 ctlcmdConfigAliasAdd(xctl(), args[0], alias)
861func ctlcmdConfigAliasAdd(ctl *ctl, address string, alias config.Alias) {
862 ctl.xwrite("aliasadd")
864 xctlwriteJSON(ctl, alias)
868func cmdConfigAliasUpdate(c *cmd) {
869 c.params = "alias@domain [-postpublic false|true -listmembers false|true -allowmsgfrom false|true]"
870 c.help = `Update alias (list) configuration.`
871 var postpublic, listmembers, allowmsgfrom string
872 c.flag.StringVar(&postpublic, "postpublic", "", "whether anyone or only list members can post")
873 c.flag.StringVar(&listmembers, "listmembers", "", "whether list members can list members")
874 c.flag.StringVar(&allowmsgfrom, "allowmsgfrom", "", "whether alias address can be used in message from header")
882 ctlcmdConfigAliasUpdate(xctl(), alias, postpublic, listmembers, allowmsgfrom)
885func ctlcmdConfigAliasUpdate(ctl *ctl, alias, postpublic, listmembers, allowmsgfrom string) {
886 ctl.xwrite("aliasupdate")
888 ctl.xwrite(postpublic)
889 ctl.xwrite(listmembers)
890 ctl.xwrite(allowmsgfrom)
894func cmdConfigAliasRemove(c *cmd) {
895 c.params = "alias@domain"
896 c.help = "Remove alias (list)."
903 ctlcmdConfigAliasRemove(xctl(), args[0])
906func ctlcmdConfigAliasRemove(ctl *ctl, alias string) {
907 ctl.xwrite("aliasrm")
912func cmdConfigAliasAddaddr(c *cmd) {
913 c.params = "alias@domain rcpt1@domain ..."
914 c.help = `Add addresses to alias (list).`
921 ctlcmdConfigAliasAddaddr(xctl(), args[0], args[1:])
924func ctlcmdConfigAliasAddaddr(ctl *ctl, alias string, addresses []string) {
925 ctl.xwrite("aliasaddaddr")
927 xctlwriteJSON(ctl, addresses)
931func cmdConfigAliasRemoveaddr(c *cmd) {
932 c.params = "alias@domain rcpt1@domain ..."
933 c.help = `Remove addresses from alias (list).`
940 ctlcmdConfigAliasRmaddr(xctl(), args[0], args[1:])
943func ctlcmdConfigAliasRmaddr(ctl *ctl, alias string, addresses []string) {
944 ctl.xwrite("aliasrmaddr")
946 xctlwriteJSON(ctl, addresses)
950func cmdConfigAccountAdd(c *cmd) {
951 c.params = "account address"
952 c.help = `Add an account with an email address and reload the configuration.
954Email can be delivered to this address/account. A password has to be configured
955explicitly, see the setaccountpassword command.
963 ctlcmdConfigAccountAdd(xctl(), args[0], args[1])
966func ctlcmdConfigAccountAdd(ctl *ctl, account, address string) {
967 ctl.xwrite("accountadd")
971 fmt.Printf("account added, set a password with \"mox setaccountpassword %s\"\n", account)
974func cmdConfigAccountRemove(c *cmd) {
976 c.help = `Remove an account and reload the configuration.
978Email addresses for this account will also be removed, and incoming email for
979these addresses will be rejected.
981All data for the account will be removed.
989 ctlcmdConfigAccountRemove(xctl(), args[0])
992func ctlcmdConfigAccountRemove(ctl *ctl, account string) {
993 ctl.xwrite("accountrm")
996 fmt.Println("account removed")
999func cmdConfigAccountDisable(c *cmd) {
1000 c.params = "account message"
1001 c.help = `Disable login for an account, showing message to users when they try to login.
1003Incoming email will still be accepted for the account, and queued email from the
1004account will still be delivered. No new login sessions are possible.
1006Message must be non-empty, ascii-only without control characters including
1007newline, and maximum 256 characters because it is used in SMTP/IMAP.
1014 log.Fatalf("message must be non-empty")
1018 ctlcmdConfigAccountDisabled(xctl(), args[0], args[1])
1019 fmt.Println("account disabled")
1022func cmdConfigAccountEnable(c *cmd) {
1023 c.params = "account"
1024 c.help = `Enable login again for an account.
1026Login attempts by the user no long result in an error message.
1034 ctlcmdConfigAccountDisabled(xctl(), args[0], "")
1035 fmt.Println("account enabled")
1038func ctlcmdConfigAccountDisabled(ctl *ctl, account, loginDisabled string) {
1039 ctl.xwrite("accountdisabled")
1041 ctl.xwrite(loginDisabled)
1045func cmdConfigTlspubkeyList(c *cmd) {
1046 c.params = "[account]"
1047 c.help = `List TLS public keys for TLS client certificate authentication.
1049If account is absent, the TLS public keys for all accounts are listed.
1052 var accountOpt string
1054 accountOpt = args[0]
1055 } else if len(args) > 1 {
1060 ctlcmdConfigTlspubkeyList(xctl(), accountOpt)
1063func ctlcmdConfigTlspubkeyList(ctl *ctl, accountOpt string) {
1064 ctl.xwrite("tlspubkeylist")
1065 ctl.xwrite(accountOpt)
1067 ctl.xstreamto(os.Stdout)
1070func cmdConfigTlspubkeyGet(c *cmd) {
1071 c.params = "fingerprint"
1072 c.help = `Get a TLS public key for a fingerprint.
1074Prints the type, name, account and address for the key, and the certificate in
1083 ctlcmdConfigTlspubkeyGet(xctl(), args[0])
1086func ctlcmdConfigTlspubkeyGet(ctl *ctl, fingerprint string) {
1087 ctl.xwrite("tlspubkeyget")
1088 ctl.xwrite(fingerprint)
1092 account := ctl.xread()
1093 address := ctl.xread()
1094 noimappreauth := ctl.xread()
1098 var block *pem.Block
1101 Type: "CERTIFICATE",
1106 fmt.Printf("type: %s\nname: %s\naccount: %s\naddress: %s\nno imap preauth: %s\n", typ, name, account, address, noimappreauth)
1108 fmt.Printf("certificate:\n\n")
1109 if err := pem.Encode(os.Stdout, block); err != nil {
1110 log.Fatalf("pem encode: %v", err)
1115func cmdConfigTlspubkeyAdd(c *cmd) {
1116 c.params = "address [name] < cert.pem"
1117 c.help = `Add a TLS public key to the account of the given address.
1119The public key is read from the certificate.
1121The optional name is a human-readable descriptive name of the key. If absent,
1122the CommonName from the certificate is used.
1124 var noimappreauth bool
1125 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.")
1127 var address, name string
1130 } else if len(args) == 2 {
1131 address, name = args[0], args[1]
1136 buf, err := io.ReadAll(os.Stdin)
1137 xcheckf(err, "reading from stdin")
1138 block, _ := pem.Decode(buf)
1140 err = errors.New("no pem block found")
1141 } else if block.Type != "CERTIFICATE" {
1142 err = fmt.Errorf("unexpected type %q, expected CERTIFICATE", block.Type)
1144 xcheckf(err, "parsing pem")
1147 ctlcmdConfigTlspubkeyAdd(xctl(), address, name, noimappreauth, block.Bytes)
1150func ctlcmdConfigTlspubkeyAdd(ctl *ctl, address, name string, noimappreauth bool, certDER []byte) {
1151 ctl.xwrite("tlspubkeyadd")
1154 ctl.xwrite(fmt.Sprintf("%v", noimappreauth))
1155 ctl.xstreamfrom(bytes.NewReader(certDER))
1159func cmdConfigTlspubkeyRemove(c *cmd) {
1160 c.params = "fingerprint"
1161 c.help = `Remove TLS public key for fingerprint.`
1168 ctlcmdConfigTlspubkeyRemove(xctl(), args[0])
1171func ctlcmdConfigTlspubkeyRemove(ctl *ctl, fingerprint string) {
1172 ctl.xwrite("tlspubkeyrm")
1173 ctl.xwrite(fingerprint)
1177func cmdConfigTlspubkeyGen(c *cmd) {
1179 c.help = `Generate an ed25519 private key and minimal certificate for use a TLS public key and write to files starting with stem.
1181The private key is written to $stem.$timestamp.ed25519privatekey.pkcs8.pem.
1182The certificate is written to $stem.$timestamp.certificate.pem.
1183The private key and certificate are also written to
1184$stem.$timestamp.ed25519privatekey-certificate.pem.
1186The certificate can be added to an account with "mox config account tlspubkey add".
1188The combined file can be used with "mox sendmail".
1190The private key is also written to standard error in raw-url-base64-encoded
1191form, also for use with "mox sendmail". The fingerprint is written to standard
1192error too, for reference.
1200 timestamp := time.Now().Format("200601021504")
1201 prefix := stem + "." + timestamp
1203 seed := make([]byte, ed25519.SeedSize)
1204 if _, err := cryptorand.Read(seed); err != nil {
1207 privKey := ed25519.NewKeyFromSeed(seed)
1208 privKeyBuf, err := x509.MarshalPKCS8PrivateKey(privKey)
1209 xcheckf(err, "marshal private key as pkcs8")
1211 err = pem.Encode(&b, &pem.Block{Type: "PRIVATE KEY", Bytes: privKeyBuf})
1212 xcheckf(err, "marshal pkcs8 private key to pem")
1213 privKeyBufPEM := b.Bytes()
1215 certBuf, tlsCert := xminimalCert(privKey)
1217 err = pem.Encode(&b, &pem.Block{Type: "CERTIFICATE", Bytes: certBuf})
1218 xcheckf(err, "marshal certificate to pem")
1219 certBufPEM := b.Bytes()
1221 xwriteFile := func(p string, data []byte, what string) {
1222 log.Printf("writing %s", p)
1223 err = os.WriteFile(p, data, 0600)
1224 xcheckf(err, "writing %s file: %v", what, err)
1227 xwriteFile(prefix+".ed25519privatekey.pkcs8.pem", privKeyBufPEM, "private key")
1228 xwriteFile(prefix+".certificate.pem", certBufPEM, "certificate")
1229 combinedPEM := slices.Concat(privKeyBufPEM, certBufPEM)
1230 xwriteFile(prefix+".ed25519privatekey-certificate.pem", combinedPEM, "combined private key and certificate")
1232 shabuf := sha256.Sum256(tlsCert.Leaf.RawSubjectPublicKeyInfo)
1234 _, err = fmt.Fprintf(os.Stderr, "ed25519 private key as raw-url-base64: %s\ned25519 public key fingerprint: %s\n",
1235 base64.RawURLEncoding.EncodeToString(seed),
1236 base64.RawURLEncoding.EncodeToString(shabuf[:]),
1238 xcheckf(err, "write private key and public key fingerprint")
1241func cmdConfigAddressAdd(c *cmd) {
1242 c.params = "address account"
1243 c.help = `Adds an address to an account and reloads the configuration.
1245If address starts with a @ (i.e. a missing localpart), this is a catchall
1246address for the domain.
1254 ctlcmdConfigAddressAdd(xctl(), args[0], args[1])
1257func ctlcmdConfigAddressAdd(ctl *ctl, address, account string) {
1258 ctl.xwrite("addressadd")
1262 fmt.Println("address added")
1265func cmdConfigAddressRemove(c *cmd) {
1266 c.params = "address"
1267 c.help = `Remove an address and reload the configuration.
1269Incoming email for this address will be rejected after removing an address.
1277 ctlcmdConfigAddressRemove(xctl(), args[0])
1280func ctlcmdConfigAddressRemove(ctl *ctl, address string) {
1281 ctl.xwrite("addressrm")
1284 fmt.Println("address removed")
1287func cmdConfigDNSRecords(c *cmd) {
1289 c.help = `Prints annotated DNS records as zone file that should be created for the domain.
1291The zone file can be imported into existing DNS software. You should review the
1292DNS records, especially if your domain previously/currently has email
1300 d := xparseDomain(args[0], "domain")
1302 domConf, ok := mox.Conf.Domain(d)
1304 log.Fatalf("unknown domain")
1307 resolver := dns.StrictResolver{Pkg: "main"}
1308 _, result, err := resolver.LookupTXT(context.Background(), d.ASCII+".")
1309 if !dns.IsNotFound(err) {
1310 xcheckf(err, "looking up record for dnssec-status")
1313 var certIssuerDomainName, acmeAccountURI string
1314 public := mox.Conf.Static.Listeners["public"]
1315 if public.TLS != nil && public.TLS.ACME != "" {
1316 acme, ok := mox.Conf.Static.ACME[public.TLS.ACME]
1317 if ok && acme.Manager.Manager.Client != nil {
1318 certIssuerDomainName = acme.IssuerDomainName
1319 acc, err := acme.Manager.Manager.Client.GetReg(context.Background(), "")
1320 c.log.Check(err, "get public acme account")
1322 acmeAccountURI = acc.URI
1327 records, err := admin.DomainRecords(domConf, d, result.Authentic, certIssuerDomainName, acmeAccountURI)
1328 xcheckf(err, "records")
1329 fmt.Print(strings.Join(records, "\n") + "\n")
1332func cmdConfigDNSCheck(c *cmd) {
1334 c.help = "Check the DNS records with the configuration for the domain, and print any errors/warnings."
1340 d := xparseDomain(args[0], "domain")
1342 _, ok := mox.Conf.Domain(d)
1344 log.Fatalf("unknown domain")
1347 // todo future: move http.Admin.CheckDomain to mox- and make it return a regular error.
1353 err, ok := x.(*sherpa.Error)
1357 log.Fatalf("%s", err)
1360 printResult := func(name string, r webadmin.Result) {
1361 if len(r.Errors) == 0 && len(r.Warnings) == 0 {
1364 fmt.Printf("# %s\n", name)
1365 for _, s := range r.Errors {
1366 fmt.Printf("error: %s\n", s)
1368 for _, s := range r.Warnings {
1369 fmt.Printf("warning: %s\n", s)
1373 result := webadmin.Admin{}.CheckDomain(context.Background(), args[0])
1374 printResult("DNSSEC", result.DNSSEC.Result)
1375 printResult("IPRev", result.IPRev.Result)
1376 printResult("MX", result.MX.Result)
1377 printResult("TLS", result.TLS.Result)
1378 printResult("DANE", result.DANE.Result)
1379 printResult("SPF", result.SPF.Result)
1380 printResult("DKIM", result.DKIM.Result)
1381 printResult("DMARC", result.DMARC.Result)
1382 printResult("Host TLSRPT", result.HostTLSRPT.Result)
1383 printResult("Domain TLSRPT", result.DomainTLSRPT.Result)
1384 printResult("MTASTS", result.MTASTS.Result)
1385 printResult("SRV conf", result.SRVConf.Result)
1386 printResult("Autoconf", result.Autoconf.Result)
1387 printResult("Autodiscover", result.Autodiscover.Result)
1390func cmdConfigEnsureACMEHostprivatekeys(c *cmd) {
1392 c.help = `Ensure host private keys exist for TLS listeners with ACME.
1394In mox.conf, each listener can have TLS configured. Long-lived private key files
1395can be specified, which will be used when requesting ACME certificates.
1396Configuring these private keys makes it feasible to publish DANE TLSA records
1397for the corresponding public keys in DNS, protected with DNSSEC, allowing TLS
1398certificate verification without depending on a list of Certificate Authorities
1399(CAs). Previous versions of mox did not pre-generate private keys for use with
1400ACME certificates, but would generate private keys on-demand. By explicitly
1401configuring private keys, they will not change automatedly with new
1402certificates, and the DNS TLSA records stay valid.
1404This command looks for listeners in mox.conf with TLS with ACME configured. For
1405each missing host private key (of type rsa-2048 and ecdsa-p256) a key is written
1406to config/hostkeys/. If a certificate exists in the ACME "cache", its private
1407key is copied. Otherwise a new private key is generated. Snippets for manually
1408updating/editing mox.conf are printed.
1410After running this command, and updating mox.conf, run "mox config dnsrecords"
1411for a domain and create the TLSA DNS records it suggests to enable DANE.
1418 // Load a private key from p, in various forms. We only look at the first PEM
1419 // block. Files with only a private key, or with multiple blocks but private key
1420 // first like autocert does, can be loaded.
1421 loadPrivateKey := func(f *os.File) (any, error) {
1422 buf, err := io.ReadAll(f)
1424 return nil, fmt.Errorf("reading private key file: %v", err)
1426 block, _ := pem.Decode(buf)
1428 return nil, fmt.Errorf("no pem block found in pem file")
1432 case "EC PRIVATE KEY":
1433 privKey, err = x509.ParseECPrivateKey(block.Bytes)
1434 case "RSA PRIVATE KEY":
1435 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
1437 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
1439 return nil, fmt.Errorf("unrecognized pem block type %q", block.Type)
1442 return nil, fmt.Errorf("parsing private key of type %q: %v", block.Type, err)
1447 // Either load a private key from file, or if it doesn't exist generate a new
1449 xtryLoadPrivateKey := func(kt autocert.KeyType, p string) any {
1450 f, err := os.Open(p)
1451 if err != nil && errors.Is(err, fs.ErrNotExist) {
1453 case autocert.KeyRSA2048:
1454 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1455 xcheckf(err, "generating new 2048-bit rsa private key")
1457 case autocert.KeyECDSAP256:
1458 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1459 xcheckf(err, "generating new ecdsa p-256 private key")
1462 log.Fatalf("unexpected keytype %v", kt)
1465 xcheckf(err, "%s: open acme key and certificate file", p)
1467 // Load private key from file. autocert stores a PEM file that starts with a
1468 // private key, followed by certificate(s). So we can just read it and should find
1469 // the private key we are looking for.
1470 privKey, err := loadPrivateKey(f)
1471 if xerr := f.Close(); xerr != nil {
1472 log.Printf("closing private key file: %v", xerr)
1474 xcheckf(err, "parsing private key from acme key and certificate file")
1476 switch k := privKey.(type) {
1477 case *rsa.PrivateKey:
1478 if k.N.BitLen() == 2048 {
1481 log.Printf("warning: rsa private key in %s has %d bits, skipping and generating new 2048-bit rsa private key", p, k.N.BitLen())
1482 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1483 xcheckf(err, "generating new 2048-bit rsa private key")
1485 case *ecdsa.PrivateKey:
1486 if k.Curve == elliptic.P256() {
1489 log.Printf("warning: ecdsa private key in %s has curve %v, skipping and generating new p-256 ecdsa key", p, k.Curve.Params().Name)
1490 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1491 xcheckf(err, "generating new ecdsa p-256 private key")
1494 log.Fatalf("%s: unexpected private key file of type %T", p, privKey)
1499 // Write privKey as PKCS#8 private key to p. Only if file does not yet exist.
1500 writeHostPrivateKey := func(privKey any, p string) error {
1501 os.MkdirAll(filepath.Dir(p), 0700)
1502 f, err := os.OpenFile(p, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
1504 return fmt.Errorf("create: %v", err)
1508 if err := f.Close(); err != nil {
1509 log.Printf("closing new hostkey file %s after error: %v", p, err)
1511 if err := os.Remove(p); err != nil {
1512 log.Printf("removing new hostkey file %s after error: %v", p, err)
1516 buf, err := x509.MarshalPKCS8PrivateKey(privKey)
1518 return fmt.Errorf("marshal private host key: %v", err)
1521 Type: "PRIVATE KEY",
1524 if err := pem.Encode(f, &block); err != nil {
1525 return fmt.Errorf("write as pem: %v", err)
1527 if err := f.Close(); err != nil {
1528 return fmt.Errorf("close: %v", err)
1535 timestamp := time.Now().Format("20060102T150405")
1537 for listenerName, l := range mox.Conf.Static.Listeners {
1538 if l.TLS == nil || l.TLS.ACME == "" {
1541 haveKeyTypes := map[autocert.KeyType]bool{}
1542 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
1543 p := mox.ConfigDirPath(privKeyFile)
1544 f, err := os.Open(p)
1545 xcheckf(err, "open host private key")
1546 privKey, err := loadPrivateKey(f)
1547 if err := f.Close(); err != nil {
1548 log.Printf("closing host private key file: %v", err)
1550 xcheckf(err, "loading host private key")
1551 switch k := privKey.(type) {
1552 case *rsa.PrivateKey:
1553 if k.N.BitLen() == 2048 {
1554 haveKeyTypes[autocert.KeyRSA2048] = true
1556 case *ecdsa.PrivateKey:
1557 if k.Curve == elliptic.P256() {
1558 haveKeyTypes[autocert.KeyECDSAP256] = true
1562 created := []string{}
1563 for _, kt := range []autocert.KeyType{autocert.KeyRSA2048, autocert.KeyECDSAP256} {
1564 if haveKeyTypes[kt] {
1567 // Lookup key in ACME cache.
1568 host := l.HostnameDomain
1569 if host.ASCII == "" {
1570 host = mox.Conf.Static.HostnameDomain
1572 filename := host.ASCII
1574 if kt == autocert.KeyRSA2048 {
1578 p := mox.DataDirPath(filepath.Join("acme", "keycerts", l.TLS.ACME, filename))
1579 privKey := xtryLoadPrivateKey(kt, p)
1581 relPath := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", host.Name(), timestamp, kind))
1582 destPath := mox.ConfigDirPath(relPath)
1583 err := writeHostPrivateKey(privKey, destPath)
1584 xcheckf(err, "writing host private key file to %s: %v", destPath, err)
1585 created = append(created, relPath)
1586 fmt.Printf("Wrote host private key: %s\n", destPath)
1588 didCreate = didCreate || len(created) > 0
1589 if len(created) > 0 {
1591 HostPrivateKeyFiles: append(l.TLS.HostPrivateKeyFiles, created...),
1593 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)
1594 err := sconf.Write(os.Stdout, tls)
1595 xcheckf(err, "writing new TLS.HostPrivateKeyFiles section")
1601After updating mox.conf and restarting, run "mox config dnsrecords" for a
1602domain and create the TLSA DNS records it suggests to enable DANE.
1607func cmdLoglevels(c *cmd) {
1608 c.params = "[level [pkg]]"
1609 c.help = `Print the log levels, or set a new default log level, or a level for the given package.
1611By default, a single log level applies to all logging in mox. But for each
1612"pkg", an overriding log level can be configured. Examples of packages:
1613smtpserver, smtpclient, queue, imapserver, spf, dkim, dmarc, junk, message,
1616Specify a pkg and an empty level to clear the configured level for a package.
1618Valid labels: error, info, debug, trace, traceauth, tracedata.
1627 ctlcmdLoglevels(xctl())
1633 ctlcmdSetLoglevels(xctl(), pkg, args[0])
1637func ctlcmdLoglevels(ctl *ctl) {
1638 ctl.xwrite("loglevels")
1640 ctl.xstreamto(os.Stdout)
1643func ctlcmdSetLoglevels(ctl *ctl, pkg, level string) {
1644 ctl.xwrite("setloglevels")
1650func cmdStop(c *cmd) {
1651 c.help = `Shut mox down, giving connections maximum 3 seconds to stop before closing them.
1653While shutting down, new IMAP and SMTP connections will get a status response
1654indicating temporary unavailability. Existing connections will get a 3 second
1655period to finish their transaction and shut down. Under normal circumstances,
1656only IMAP has long-living connections, with the IDLE command to get notified of
1659 if len(c.Parse()) != 0 {
1666 // Read will hang until remote has shut down.
1667 buf := make([]byte, 128)
1668 n, err := xctl.conn.Read(buf)
1670 log.Fatalf("expected eof after graceful shutdown, got data %q", buf[:n])
1671 } else if err != io.EOF {
1672 log.Fatalf("expected eof after graceful shutdown, got error %v", err)
1674 fmt.Println("mox stopped")
1677func cmdBackup(c *cmd) {
1678 c.params = "destdir"
1679 c.help = `Creates a backup of the config and data directory.
1681Backup copies the config directory to <destdir>/config, and creates
1682<destdir>/data with a consistent snapshot of the databases and message files
1683and copies other files from the data directory. Empty directories are not
1684copied. The backup can then be stored elsewhere for long-term storage, or used
1685to fall back to should an upgrade fail. Simply copying files in the data
1686directory while mox is running can result in unusable database files.
1688Message files never change (they are read-only, though can be removed) and are
1689hard-linked so they don't consume additional space. If hardlinking fails, for
1690example when the backup destination directory is on a different file system, a
1691regular copy is made. Using a destination directory like "data/tmp/backup"
1692increases the odds hardlinking succeeds: the default systemd service file
1693specifically mounts the data directory, causing attempts to hardlink outside it
1694to fail with an error about cross-device linking.
1696All files in the data directory that aren't recognized (i.e. other than known
1697database files, message files, an acme directory, the "tmp" directory, etc),
1698are stored, but with a warning.
1700Remove files in the destination directory before doing another backup. The
1701backup command will not overwrite files, but print and return errors.
1703Exit code 0 indicates the backup was successful. A clean successful backup does
1704not print any output, but may print warnings. Use the -verbose flag for
1705details, including timing.
1707To restore a backup, first shut down mox, move away the old data directory and
1708move an earlier backed up directory in its place, run "mox verifydata
1709<datadir>", possibly with the "-fix" option, and restart mox. After the
1710restore, you may also want to run "mox bumpuidvalidity" for each account for
1711which messages in a mailbox changed, to force IMAP clients to synchronize
1714Before upgrading, to check if the upgrade will likely succeed, first make a
1715backup, then use the new mox binary to run "mox verifydata <backupdir>/data".
1716This can change the backup files (e.g. upgrade database files, move away
1717unrecognized message files), so you should make a new backup before actually
1722 c.flag.BoolVar(&verbose, "verbose", false, "print progress")
1729 dstDataDir, err := filepath.Abs(args[0])
1730 xcheckf(err, "making path absolute")
1732 ctlcmdBackup(xctl(), dstDataDir, verbose)
1735func ctlcmdBackup(ctl *ctl, dstDataDir string, verbose bool) {
1736 ctl.xwrite("backup")
1737 ctl.xwrite(dstDataDir)
1739 ctl.xwrite("verbose")
1743 ctl.xstreamto(os.Stdout)
1747func cmdSetadminpassword(c *cmd) {
1748 c.help = `Set a new admin password, for the web interface.
1750The password is read from stdin. Its bcrypt hash is stored in a file named
1751"adminpasswd" in the configuration directory.
1753 if len(c.Parse()) != 0 {
1758 path := mox.ConfigDirPath(mox.Conf.Static.AdminPasswordFile)
1760 log.Fatal("no admin password file configured")
1763 pw := xreadpassword()
1764 pw, err := precis.OpaqueString.String(pw)
1765 xcheckf(err, `checking password with "precis" requirements`)
1766 hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
1767 xcheckf(err, "generating hash for password")
1768 err = os.WriteFile(path, hash, 0660)
1769 xcheckf(err, "writing hash to admin password file")
1772func xreadpassword() string {
1774Type new password. Password WILL echo.
1776WARNING: Bots will try to bruteforce your password. Connections with failed
1777authentication attempts will be rate limited but attackers WILL find passwords
1778reused at other services and weak passwords. If your account is compromised,
1779spammers are likely to abuse your system, spamming your address and the wider
1780internet in your name. So please pick a random, unguessable password, preferably
1781at least 12 characters.
1784 fmt.Printf("password: ")
1785 scanner := bufio.NewScanner(os.Stdin)
1786 // The default splitter for scanners is one that splits by lines, so we
1787 // don't have to set up another one here.
1789 // We discard the return value of Scan() since failing to tokenize could
1790 // either mean reaching EOF but no newline (which can be legitimate if the
1791 // CLI was programatically called to set the password, but with no trailing
1792 // newline), or an actual error. We can distinguish between the two by
1793 // calling Err() since it will return nil if it were EOF, but the actual
1796 xcheckf(scanner.Err(), "reading stdin")
1797 // No need to trim, the scanner does not return the token in the output.
1798 pw := scanner.Text()
1800 log.Fatal("password must be at least 8 characters")
1805func cmdSetaccountpassword(c *cmd) {
1806 c.params = "account"
1807 c.help = `Set new password an account.
1809The password is read from stdin. Secrets derived from the password, but not the
1810password itself, are stored in the account database. The stored secrets are for
1811authentication with: scram-sha-256, scram-sha-1, cram-md5, plain text (bcrypt
1814The parameter is an account name, as configured under Accounts in domains.conf
1815and as present in the data/accounts/ directory, not a configured email address
1824 pw := xreadpassword()
1826 ctlcmdSetaccountpassword(xctl(), args[0], pw)
1829func ctlcmdSetaccountpassword(ctl *ctl, account, password string) {
1830 ctl.xwrite("setaccountpassword")
1832 ctl.xwrite(password)
1836func cmdDeliver(c *cmd) {
1838 c.params = "address < message"
1839 c.help = "Deliver message to address."
1845 ctlcmdDeliver(xctl(), args[0])
1848func ctlcmdDeliver(ctl *ctl, address string) {
1849 ctl.xwrite("deliver")
1852 ctl.xstreamfrom(os.Stdin)
1855 fmt.Println("message delivered")
1857 log.Fatalf("deliver: %s", line)
1861func cmdDKIMGenrsa(c *cmd) {
1862 c.params = ">$selector._domainkey.$domain.rsa2048.privatekey.pkcs8.pem"
1863 c.help = `Generate a new 2048 bit RSA private key for use with DKIM.
1865The generated file is in PEM format, and has a comment it is generated for use
1868 if len(c.Parse()) != 0 {
1872 buf, err := admin.MakeDKIMRSAKey(dns.Domain{}, dns.Domain{})
1873 xcheckf(err, "making rsa private key")
1874 _, err = os.Stdout.Write(buf)
1875 xcheckf(err, "writing rsa private key")
1878func cmdDANEDial(c *cmd) {
1879 c.params = "host:port"
1881 c.flag.StringVar(&usages, "usages", "pkix-ta,pkix-ee,dane-ta,dane-ee", "allowed usages for dane, comma-separated list")
1882 c.help = `Dial the address using TLS with certificate verification using DANE.
1884Data is copied between connection and stdin/stdout until either side closes the
1892 allowedUsages := []adns.TLSAUsage{}
1894 for _, s := range strings.Split(usages, ",") {
1895 var usage adns.TLSAUsage
1896 switch strings.ToLower(s) {
1897 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
1898 usage = adns.TLSAUsagePKIXTA
1899 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
1900 usage = adns.TLSAUsagePKIXEE
1901 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
1902 usage = adns.TLSAUsageDANETA
1903 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
1904 usage = adns.TLSAUsageDANEEE
1906 log.Fatalf("unknown dane usage %q", s)
1908 allowedUsages = append(allowedUsages, usage)
1912 pkixRoots, err := x509.SystemCertPool()
1913 xcheckf(err, "get system pkix certificate pool")
1915 resolver := dns.StrictResolver{Pkg: "danedial"}
1916 conn, record, err := dane.Dial(context.Background(), c.log.Logger, resolver, "tcp", args[0], allowedUsages, pkixRoots)
1917 xcheckf(err, "dial")
1918 log.Printf("(connected, verified with %s)", record)
1921 _, err := io.Copy(os.Stdout, conn)
1922 xcheckf(err, "copy from connection to stdout")
1924 c.log.Check(err, "closing connection")
1926 _, err = io.Copy(conn, os.Stdin)
1927 xcheckf(err, "copy from stdin to connection")
1930func cmdDANEDialmx(c *cmd) {
1931 c.params = "domain [destination-host]"
1932 var ehloHostname string
1933 c.flag.StringVar(&ehloHostname, "ehlohostname", "localhost", "hostname to send in smtp ehlo command")
1934 c.help = `Connect to MX server for domain using STARTTLS verified with DANE.
1936If no destination host is specified, regular delivery logic is used to find the
1937hosts to attempt delivery too. This involves following CNAMEs for the domain,
1938looking up MX records, and possibly falling back to the domain name itself as
1941If a destination host is specified, that is the only candidate host considered
1944With a list of destinations gathered, each is dialed until a successful SMTP
1945session verified with DANE has been initialized, including EHLO and STARTTLS
1948Once connected, data is copied between connection and stdin/stdout, until
1949either side closes the connection.
1951This command follows the same logic as delivery attempts made from the queue,
1952sharing most of its code.
1955 if len(args) != 1 && len(args) != 2 {
1959 ehloDomain := xparseDomain(ehloHostname, "ehlo host name")
1960 origNextHop := xparseDomain(args[0], "domain")
1962 ctxbg := context.Background()
1964 resolver := dns.StrictResolver{}
1966 var expandedNextHopAuthentic bool
1967 var expandedNextHop dns.Domain
1968 var hosts []dns.IPDomain
1971 var origNextHopAuthentic bool
1973 haveMX, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hosts, permanent, err = smtpclient.GatherDestinations(ctxbg, c.log.Logger, resolver, dns.IPDomain{Domain: origNextHop})
1974 status := "temporary"
1976 status = "permanent"
1979 log.Fatalf("gathering destinations: %v (%s)", err, status)
1981 if expandedNextHop != origNextHop {
1982 log.Printf("followed cnames to %s", expandedNextHop)
1985 log.Printf("found mx record, trying mx hosts")
1987 log.Printf("no mx record found, will try to connect to domain directly")
1989 if !origNextHopAuthentic {
1990 log.Fatalf("error: initial domain not dnssec-secure")
1992 if !expandedNextHopAuthentic {
1993 log.Fatalf("error: expanded domain not dnssec-secure")
1997 for _, h := range hosts {
1998 l = append(l, h.String())
2000 log.Printf("destinations: %s", strings.Join(l, ", "))
2002 d := xparseDomain(args[1], "destination host")
2003 log.Printf("skipping domain mx/cname lookups, assuming domain is dnssec-protected")
2005 expandedNextHopAuthentic = true
2007 hosts = []dns.IPDomain{{Domain: d}}
2010 dialedIPs := map[string][]net.IP{}
2011 for _, host := range hosts {
2012 // It should not be possible for hosts to have IP addresses: They are not
2013 // allowed by dns.ParseDomain, and MX records cannot contain them.
2015 log.Fatalf("unexpected IP address for destination host")
2018 log.Printf("attempting to connect to %s", host)
2020 authentic, expandedAuthentic, expandedHost, ips, _, err := smtpclient.GatherIPs(ctxbg, c.log.Logger, resolver, "ip", host, dialedIPs)
2022 log.Printf("resolving ips for %s: %v, skipping", host, err)
2026 log.Printf("no dnssec for ips of %s, skipping", host)
2029 if !expandedAuthentic {
2030 log.Printf("no dnssec for cname-followed ips of %s, skipping", host)
2033 if expandedHost != host.Domain {
2034 log.Printf("host %s cname-expanded to %s", host, expandedHost)
2036 log.Printf("host %s resolved to ips %s, looking up tlsa records", host, ips)
2038 daneRequired, daneRecords, tlsaBaseDomain, err := smtpclient.GatherTLSA(ctxbg, c.log.Logger, resolver, host.Domain, expandedAuthentic, expandedHost)
2040 log.Printf("looking up tlsa records: %s, skipping", err)
2043 tlsMode := smtpclient.TLSRequiredStartTLS
2044 if len(daneRecords) == 0 {
2046 log.Printf("host %s has no tlsa records, skipping", expandedHost)
2049 log.Printf("warning: only unusable tlsa records found, continuing with required tls without certificate verification")
2053 for _, r := range daneRecords {
2054 l = append(l, r.String())
2056 log.Printf("tlsa records: %s", strings.Join(l, "; "))
2059 tlsHostnames := smtpclient.GatherTLSANames(haveMX, expandedNextHopAuthentic, expandedAuthentic, origNextHop, expandedNextHop, host.Domain, tlsaBaseDomain)
2061 for _, name := range tlsHostnames {
2062 l = append(l, name.String())
2064 log.Printf("gathered valid tls certificate names for potential verification with dane-ta: %s", strings.Join(l, ", "))
2066 dialer := &net.Dialer{Timeout: 5 * time.Second}
2067 conn, _, err := smtpclient.Dial(ctxbg, c.log.Logger, dialer, dns.IPDomain{Domain: expandedHost}, ips, 25, dialedIPs, nil)
2069 log.Printf("dial %s: %v, skipping", expandedHost, err)
2072 log.Printf("connected to %s, %s, starting smtp session with ehlo and starttls with dane verification", expandedHost, conn.RemoteAddr())
2074 var verifiedRecord adns.TLSA
2075 opts := smtpclient.Opts{
2076 DANERecords: daneRecords,
2077 DANEMoreHostnames: tlsHostnames[1:],
2078 DANEVerifiedRecord: &verifiedRecord,
2079 RootCAs: mox.Conf.Static.TLS.CertPool,
2082 sc, err := smtpclient.New(ctxbg, c.log.Logger, conn, tlsMode, tlsPKIX, ehloDomain, tlsHostnames[0], opts)
2084 log.Printf("setting up smtp session: %v, skipping", err)
2085 if xerr := conn.Close(); xerr != nil {
2086 log.Printf("closing connection: %v", xerr)
2091 smtpConn, err := sc.Conn()
2093 log.Fatalf("error: taking over smtp connection: %s", err)
2095 log.Printf("tls verified with tlsa record: %s", verifiedRecord)
2096 log.Printf("smtp session initialized and connected to stdin/stdout")
2099 _, err := io.Copy(os.Stdout, smtpConn)
2100 xcheckf(err, "copy from connection to stdout")
2101 if err := smtpConn.Close(); err != nil {
2102 log.Printf("closing smtp connection: %v", err)
2105 _, err = io.Copy(smtpConn, os.Stdin)
2106 xcheckf(err, "copy from stdin to connection")
2109 log.Fatalf("no remaining destinations")
2112func cmdDANEMakeRecord(c *cmd) {
2113 c.params = "usage selector matchtype [certificate.pem | publickey.pem | privatekey.pem]"
2114 c.help = `Print TLSA record for given certificate/key and parameters.
2117- usage: pkix-ta (0), pkix-ee (1), dane-ta (2), dane-ee (3)
2118- selector: cert (0), spki (1)
2119- matchtype: full (0), sha2-256 (1), sha2-512 (2)
2121Common DANE TLSA record parameters are: dane-ee spki sha2-256, or 3 1 1,
2122followed by a sha2-256 hash of the DER-encoded "SPKI" (subject public key info)
2123from the certificate. An example DNS zone file entry:
2125 _25._tcp.example.com. TLSA 3 1 1 133b919c9d65d8b1488157315327334ead8d83372db57465ecabf53ee5748aee
2127The first usable information from the pem file is used to compose the TLSA
2128record. In case of selector "cert", a certificate is required. Otherwise the
2129"subject public key info" (spki) of the first certificate or public or private
2130key (pkcs#8, pkcs#1 or ec private key) is used.
2138 var usage adns.TLSAUsage
2139 switch strings.ToLower(args[0]) {
2140 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
2141 usage = adns.TLSAUsagePKIXTA
2142 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
2143 usage = adns.TLSAUsagePKIXEE
2144 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
2145 usage = adns.TLSAUsageDANETA
2146 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
2147 usage = adns.TLSAUsageDANEEE
2149 if v, err := strconv.ParseUint(args[0], 10, 16); err != nil {
2150 log.Fatalf("bad usage %q", args[0])
2152 // Does not influence certificate association data, so we can accept other numbers.
2153 log.Printf("warning: continuing with unrecognized tlsa usage %d", v)
2154 usage = adns.TLSAUsage(v)
2158 var selector adns.TLSASelector
2159 switch strings.ToLower(args[1]) {
2160 case "cert", strconv.Itoa(int(adns.TLSASelectorCert)):
2161 selector = adns.TLSASelectorCert
2162 case "spki", strconv.Itoa(int(adns.TLSASelectorSPKI)):
2163 selector = adns.TLSASelectorSPKI
2165 log.Fatalf("bad selector %q", args[1])
2168 var matchType adns.TLSAMatchType
2169 switch strings.ToLower(args[2]) {
2170 case "full", strconv.Itoa(int(adns.TLSAMatchTypeFull)):
2171 matchType = adns.TLSAMatchTypeFull
2172 case "sha2-256", strconv.Itoa(int(adns.TLSAMatchTypeSHA256)):
2173 matchType = adns.TLSAMatchTypeSHA256
2174 case "sha2-512", strconv.Itoa(int(adns.TLSAMatchTypeSHA512)):
2175 matchType = adns.TLSAMatchTypeSHA512
2177 log.Fatalf("bad matchtype %q", args[2])
2180 buf, err := os.ReadFile(args[3])
2181 xcheckf(err, "reading certificate")
2183 var block *pem.Block
2184 block, buf = pem.Decode(buf)
2188 extra = " (with leftover data from pem file)"
2190 if selector == adns.TLSASelectorCert {
2191 log.Fatalf("no certificate found in pem file%s", extra)
2193 log.Fatalf("no certificate or public or private key found in pem file%s", extra)
2196 var cert *x509.Certificate
2198 if block.Type == "CERTIFICATE" {
2199 cert, err = x509.ParseCertificate(block.Bytes)
2200 xcheckf(err, "parse certificate")
2202 case adns.TLSASelectorCert:
2204 case adns.TLSASelectorSPKI:
2205 data = cert.RawSubjectPublicKeyInfo
2207 } else if selector == adns.TLSASelectorCert {
2208 // We need a certificate, just a public/private key won't do.
2209 log.Printf("skipping pem type %q, certificate is required", block.Type)
2212 var privKey, pubKey any
2216 _, err := x509.ParsePKIXPublicKey(block.Bytes)
2217 xcheckf(err, "parse pkix subject public key info (spki)")
2219 case "EC PRIVATE KEY":
2220 privKey, err = x509.ParseECPrivateKey(block.Bytes)
2221 xcheckf(err, "parse ec private key")
2222 case "RSA PRIVATE KEY":
2223 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
2224 xcheckf(err, "parse pkcs#1 rsa private key")
2225 case "RSA PUBLIC KEY":
2226 pubKey, err = x509.ParsePKCS1PublicKey(block.Bytes)
2227 xcheckf(err, "parse pkcs#1 rsa public key")
2229 // PKCS#8 private key
2230 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
2231 xcheckf(err, "parse pkcs#8 private key")
2233 log.Printf("skipping unrecognized pem type %q", block.Type)
2237 if pubKey == nil && privKey != nil {
2238 if signer, ok := privKey.(crypto.Signer); !ok {
2239 log.Fatalf("private key of type %T is not a signer, cannot get public key", privKey)
2241 pubKey = signer.Public()
2245 // Should not happen.
2246 log.Fatalf("internal error: did not find private or public key")
2248 data, err = x509.MarshalPKIXPublicKey(pubKey)
2249 xcheckf(err, "marshal pkix subject public key info (spki)")
2254 case adns.TLSAMatchTypeFull:
2255 case adns.TLSAMatchTypeSHA256:
2256 p := sha256.Sum256(data)
2258 case adns.TLSAMatchTypeSHA512:
2259 p := sha512.Sum512(data)
2262 fmt.Printf("%d %d %d %x\n", usage, selector, matchType, data)
2267func cmdDNSLookup(c *cmd) {
2268 c.params = "[ptr | mx | cname | ips | a | aaaa | ns | txt | srv | tlsa] name"
2269 c.help = `Lookup DNS name of given type.
2271Lookup always prints whether the response was DNSSEC-protected.
2275mox dns lookup ptr 1.1.1.1
2276mox dns lookup mx xmox.nl
2277mox dns lookup txt _dmarc.xmox.nl.
2278mox dns lookup tlsa _25._tcp.xmox.nl
2286 resolver := dns.StrictResolver{Pkg: "dns"}
2288 // like xparseDomain, but treat unparseable domain as an ASCII name so names with
2289 // underscores are still looked up, e,g <selector>._domainkey.<host>.
2290 xdomain := func(s string) dns.Domain {
2291 d, err := dns.ParseDomain(s)
2293 return dns.Domain{ASCII: strings.TrimSuffix(s, ".")}
2298 cmd, name := args[0], args[1]
2302 ip := xparseIP(name, "ip")
2303 ptrs, result, err := resolver.LookupAddr(context.Background(), ip.String())
2305 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2307 fmt.Printf("names (%d, %s):\n", len(ptrs), dnssecStatus(result.Authentic))
2308 for _, ptr := range ptrs {
2309 fmt.Printf("- %s\n", ptr)
2313 name := xdomain(name)
2314 mxl, result, err := resolver.LookupMX(context.Background(), name.ASCII+".")
2316 log.Printf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2317 // We can still have valid records...
2319 fmt.Printf("mx records (%d, %s):\n", len(mxl), dnssecStatus(result.Authentic))
2320 for _, mx := range mxl {
2321 fmt.Printf("- %s, preference %d\n", mx.Host, mx.Pref)
2325 name := xdomain(name)
2326 target, result, err := resolver.LookupCNAME(context.Background(), name.ASCII+".")
2328 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2330 fmt.Printf("%s (%s)\n", target, dnssecStatus(result.Authentic))
2332 case "ips", "a", "aaaa":
2336 } else if cmd == "aaaa" {
2339 name := xdomain(name)
2340 ips, result, err := resolver.LookupIP(context.Background(), network, name.ASCII+".")
2342 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2344 fmt.Printf("records (%d, %s):\n", len(ips), dnssecStatus(result.Authentic))
2345 for _, ip := range ips {
2346 fmt.Printf("- %s\n", ip)
2350 name := xdomain(name)
2351 nsl, result, err := resolver.LookupNS(context.Background(), name.ASCII+".")
2353 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2355 fmt.Printf("ns records (%d, %s):\n", len(nsl), dnssecStatus(result.Authentic))
2356 for _, ns := range nsl {
2357 fmt.Printf("- %s\n", ns)
2361 host := xdomain(name)
2362 l, result, err := resolver.LookupTXT(context.Background(), host.ASCII+".")
2364 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2366 fmt.Printf("txt records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2367 for _, txt := range l {
2368 fmt.Printf("- %s\n", txt)
2372 host := xdomain(name)
2373 _, l, result, err := resolver.LookupSRV(context.Background(), "", "", host.ASCII+".")
2375 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2377 fmt.Printf("srv records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2378 for _, srv := range l {
2379 fmt.Printf("- host %s, port %d, priority %d, weight %d\n", srv.Target, srv.Port, srv.Priority, srv.Weight)
2383 host := xdomain(name)
2384 l, result, err := resolver.LookupTLSA(context.Background(), 0, "", host.ASCII+".")
2386 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2388 fmt.Printf("tlsa records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2389 for _, tlsa := range l {
2390 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)
2393 log.Fatalf("unknown record type %q", args[0])
2397func cmdDKIMGened25519(c *cmd) {
2398 c.params = ">$selector._domainkey.$domain.ed25519.privatekey.pkcs8.pem"
2399 c.help = `Generate a new ed25519 key for use with DKIM.
2401Ed25519 keys are much smaller than RSA keys of comparable cryptographic
2402strength. This is convenient because of maximum DNS message sizes. At the time
2403of writing, not many mail servers appear to support ed25519 DKIM keys though,
2404so it is recommended to sign messages with both RSA and ed25519 keys.
2406 if len(c.Parse()) != 0 {
2410 buf, err := admin.MakeDKIMEd25519Key(dns.Domain{}, dns.Domain{})
2411 xcheckf(err, "making dkim ed25519 key")
2412 _, err = os.Stdout.Write(buf)
2413 xcheckf(err, "writing dkim ed25519 key")
2416func cmdDKIMTXT(c *cmd) {
2417 c.params = "<$selector._domainkey.$domain.key.pkcs8.pem"
2418 c.help = `Print a DKIM DNS TXT record with the public key derived from the private key read from stdin.
2420The DNS should be configured as a TXT record at $selector._domainkey.$domain.
2422 if len(c.Parse()) != 0 {
2426 privKey, err := parseDKIMKey(os.Stdin)
2427 xcheckf(err, "reading dkim private key from stdin")
2431 Hashes: []string{"sha256"},
2432 Flags: []string{"s"},
2435 switch key := privKey.(type) {
2436 case *rsa.PrivateKey:
2437 r.PublicKey = key.Public()
2438 case ed25519.PrivateKey:
2439 r.PublicKey = key.Public()
2442 log.Fatalf("unsupported private key type %T, must be rsa or ed25519", privKey)
2445 record, err := r.Record()
2446 xcheckf(err, "making record")
2447 fmt.Print("<selector>._domainkey.<your.domain.> TXT ")
2451 s, record = record[:100], record[100:]
2455 fmt.Printf(`"%s" `, s)
2460func parseDKIMKey(r io.Reader) (any, error) {
2461 buf, err := io.ReadAll(r)
2463 return nil, fmt.Errorf("reading pem from stdin: %v", err)
2465 b, _ := pem.Decode(buf)
2467 return nil, fmt.Errorf("decoding pem: %v", err)
2469 privKey, err := x509.ParsePKCS8PrivateKey(b.Bytes)
2471 return nil, fmt.Errorf("parsing private key: %v", err)
2476func cmdDKIMVerify(c *cmd) {
2477 c.params = "message"
2478 c.help = `Verify the DKIM signatures in a message and print the results.
2480The message is parsed, and the DKIM-Signature headers are validated. Validation
2481of older messages may fail because the DNS records have been removed or changed
2482by now, or because the signature header may have specified an expiration time
2490 msgf, err := os.Open(args[0])
2491 xcheckf(err, "open message")
2493 results, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, false, dkim.DefaultPolicy, msgf, true)
2494 xcheckf(err, "dkim verify")
2496 for _, result := range results {
2498 if result.Sig == nil {
2499 log.Printf("warning: could not parse signature")
2501 sigh, err = result.Sig.Header()
2503 log.Printf("warning: packing signature: %s", err)
2507 if result.Record == nil {
2508 log.Printf("warning: missing DNS record")
2510 txt, err = result.Record.Record()
2512 log.Printf("warning: packing record: %s", err)
2515 fmt.Printf("status %q, err %v\nrecord %q\nheader %s\n", result.Status, result.Err, txt, sigh)
2519func cmdDKIMSign(c *cmd) {
2520 c.params = "message"
2521 c.help = `Sign a message, adding DKIM-Signature headers based on the domain in the From header.
2523The message is parsed, the domain looked up in the configuration files, and
2524DKIM-Signature headers generated. The message is printed with the DKIM-Signature
2532 msgf, err := os.Open(args[0])
2533 xcheckf(err, "open message")
2535 if err := msgf.Close(); err != nil {
2536 log.Printf("closing message file: %v", err)
2540 p, err := message.Parse(c.log.Logger, true, msgf)
2541 xcheckf(err, "parsing message")
2543 if len(p.Envelope.From) != 1 {
2544 log.Fatalf("found %d from headers, need exactly 1", len(p.Envelope.From))
2546 localpart, err := smtp.ParseLocalpart(p.Envelope.From[0].User)
2547 xcheckf(err, "parsing localpart of address in from-header")
2548 dom := xparseDomain(p.Envelope.From[0].Host, "domain of address in from-header")
2552 domConf, ok := mox.Conf.Domain(dom)
2554 log.Fatalf("domain %s not configured", dom)
2557 selectors := mox.DKIMSelectors(domConf.DKIM)
2558 headers, err := dkim.Sign(context.Background(), c.log.Logger, localpart, dom, selectors, false, msgf)
2559 xcheckf(err, "signing message with dkim")
2561 log.Fatalf("no DKIM configured for domain %s", dom)
2563 _, err = fmt.Fprint(os.Stdout, headers)
2564 xcheckf(err, "write headers")
2565 _, err = io.Copy(os.Stdout, msgf)
2566 xcheckf(err, "write message")
2569func cmdDKIMLookup(c *cmd) {
2570 c.params = "selector domain"
2571 c.help = "Lookup and print the DKIM record for the selector at the domain."
2577 selector := xparseDomain(args[0], "selector")
2578 domain := xparseDomain(args[1], "domain")
2580 status, record, txt, authentic, err := dkim.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, selector, domain)
2582 fmt.Printf("error: %s\n", err)
2584 if status != dkim.StatusNeutral {
2585 fmt.Printf("status: %s\n", status)
2588 fmt.Printf("TXT record: %s\n", txt)
2591 fmt.Println("dnssec-signed: yes")
2593 fmt.Println("dnssec-signed: no")
2596 fmt.Printf("Record:\n")
2598 "version", record.Version,
2599 "hashes", record.Hashes,
2601 "notes", record.Notes,
2602 "services", record.Services,
2603 "flags", record.Flags,
2605 for i := 0; i < len(pairs); i += 2 {
2606 fmt.Printf("\t%s: %v\n", pairs[i], pairs[i+1])
2611func cmdDMARCLookup(c *cmd) {
2613 c.help = "Lookup dmarc policy for domain, a DNS TXT record at _dmarc.<domain>, validate and print it."
2619 fromdomain := xparseDomain(args[0], "domain")
2620 _, domain, _, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, fromdomain)
2621 xcheckf(err, "dmarc lookup domain %s", fromdomain)
2622 fmt.Printf("dmarc record at domain %s: %s\n", domain, txt)
2623 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2626func dnssecStatus(v bool) string {
2628 return "with dnssec"
2630 return "without dnssec"
2633func cmdDMARCVerify(c *cmd) {
2634 c.params = "remoteip mailfromaddress helodomain < message"
2635 c.help = `Parse an email message and evaluate it against the DMARC policy of the domain in the From-header.
2637mailfromaddress and helodomain are used for SPF validation. If both are empty,
2638SPF validation is skipped.
2640mailfromaddress should be the address used as MAIL FROM in the SMTP session.
2641For DSN messages, that address may be empty. The helo domain was specified at
2642the beginning of the SMTP transaction that delivered the message. These values
2643can be found in message headers.
2650 var heloDomain *dns.Domain
2652 remoteIP := xparseIP(args[0], "remoteip")
2654 var mailfrom *smtp.Address
2656 a, err := smtp.ParseAddress(args[1])
2657 xcheckf(err, "parsing mailfrom address")
2661 d := xparseDomain(args[2], "helo domain")
2664 var received *spf.Received
2665 spfStatus := spf.StatusNone
2666 var spfIdentity *dns.Domain
2667 if mailfrom != nil || heloDomain != nil {
2668 spfArgs := spf.Args{
2670 LocalIP: net.ParseIP("127.0.0.1"),
2671 LocalHostname: dns.Domain{ASCII: "localhost"},
2673 if mailfrom != nil {
2674 spfArgs.MailFromLocalpart = mailfrom.Localpart
2675 spfArgs.MailFromDomain = mailfrom.Domain
2677 if heloDomain != nil {
2678 spfArgs.HelloDomain = dns.IPDomain{Domain: *heloDomain}
2680 rspf, spfDomain, expl, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfArgs)
2682 log.Printf("spf verify: %v (explanation: %q, authentic %v)", err, expl, authentic)
2685 spfStatus = received.Result
2686 // todo: should probably potentially do two separate spf validations
2687 if mailfrom != nil {
2688 spfIdentity = &mailfrom.Domain
2690 spfIdentity = heloDomain
2692 fmt.Printf("spf result: %s: %s (%s)\n", spfDomain, spfStatus, dnssecStatus(authentic))
2696 data, err := io.ReadAll(os.Stdin)
2697 xcheckf(err, "read message")
2698 dmarcFrom, _, _, err := message.From(c.log.Logger, false, bytes.NewReader(data), nil)
2699 xcheckf(err, "extract dmarc from message")
2701 const ignoreTestMode = false
2702 dkimResults, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, true, func(*dkim.Sig) error { return nil }, bytes.NewReader(data), ignoreTestMode)
2703 xcheckf(err, "dkim verify")
2704 for _, r := range dkimResults {
2705 fmt.Printf("dkim result: %q (err %v)\n", r.Status, r.Err)
2708 _, result := dmarc.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, dmarcFrom.Domain, dkimResults, spfStatus, spfIdentity, false)
2709 xcheckf(result.Err, "dmarc verify")
2710 fmt.Printf("dmarc from: %s\ndmarc status: %q\ndmarc reject: %v\ncmarc record: %s\n", dmarcFrom, result.Status, result.Reject, result.Record)
2713func cmdDMARCCheckreportaddrs(c *cmd) {
2715 c.help = `For each reporting address in the domain's DMARC record, check if it has opted into receiving reports (if needed).
2717A DMARC record can request reports about DMARC evaluations to be sent to an
2718email/http address. If the organizational domains of that of the DMARC record
2719and that of the report destination address do not match, the destination
2720address must opt-in to receiving DMARC reports by creating a DMARC record at
2721<dmarcdomain>._report._dmarc.<reportdestdomain>.
2728 dom := xparseDomain(args[0], "domain")
2729 _, domain, record, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dom)
2730 xcheckf(err, "dmarc lookup domain %s", dom)
2731 fmt.Printf("dmarc record at domain %s: %q\n", domain, txt)
2732 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2734 check := func(kind, addr string) {
2737 printResult := func(format string, args ...any) {
2738 fmt.Printf("%s %s: %s (%s)\n", kind, addr, fmt.Sprintf(format, args...), dnssecStatus(authentic))
2741 u, err := url.Parse(addr)
2743 printResult("parsing uri: %v (skipping)", addr, err)
2746 var destdom dns.Domain
2749 a, err := smtp.ParseAddress(u.Opaque)
2751 printResult("parsing destination email address %s: %v (skipping)", u.Opaque, err)
2756 printResult("unrecognized scheme in reporting address %s (skipping)", u.Scheme)
2760 if publicsuffix.Lookup(context.Background(), c.log.Logger, dom) == publicsuffix.Lookup(context.Background(), c.log.Logger, destdom) {
2761 printResult("pass (same organizational domain)")
2765 accepts, status, _, txts, authentic, err := dmarc.LookupExternalReportsAccepted(context.Background(), c.log.Logger, dns.StrictResolver{}, domain, destdom)
2767 txtaddr := fmt.Sprintf("%s._report._dmarc.%s", domain.ASCII, destdom.ASCII)
2769 txtstr = fmt.Sprintf(" (no txt records %s)", txtaddr)
2771 txtstr = fmt.Sprintf(" (txt record %s: %q)", txtaddr, txts)
2773 if status != dmarc.StatusNone {
2774 printResult("fail: %s%s", err, txtstr)
2776 printResult("pass%s", txtstr)
2777 } else if err != nil {
2778 printResult("fail: %s%s", err, txtstr)
2780 printResult("fail%s", txtstr)
2784 for _, uri := range record.AggregateReportAddresses {
2785 check("aggregate reporting", uri.Address)
2787 for _, uri := range record.FailureReportAddresses {
2788 check("failure reporting", uri.Address)
2792func cmdDMARCParsereportmsg(c *cmd) {
2793 c.params = "message ..."
2794 c.help = `Parse a DMARC report from an email message, and print its extracted details.
2796DMARC reports are periodically mailed, if requested in the DMARC DNS record of
2797a domain. Reports are sent by mail servers that received messages with our
2798domain in a From header. This may or may not be legatimate email. DMARC reports
2799contain summaries of evaluations of DMARC and DKIM/SPF, which can help
2800understand email deliverability problems.
2807 for _, arg := range args {
2808 f, err := os.Open(arg)
2809 xcheckf(err, "open %q", arg)
2810 feedback, err := dmarcrpt.ParseMessageReport(c.log.Logger, f)
2811 xcheckf(err, "parse report in %q", arg)
2812 meta := feedback.ReportMetadata
2813 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)
2814 if len(meta.Errors) > 0 {
2815 fmt.Printf("Errors:\n")
2816 for _, s := range meta.Errors {
2817 fmt.Printf("\t- %s\n", s)
2820 pol := feedback.PolicyPublished
2821 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)
2822 for _, record := range feedback.Records {
2823 idents := record.Identifiers
2824 fmt.Printf("\theaderfrom %q, envelopes from %q, to %q\n", idents.HeaderFrom, idents.EnvelopeFrom, idents.EnvelopeTo)
2825 eval := record.Row.PolicyEvaluated
2827 for _, reason := range eval.Reasons {
2828 reasons += "; " + string(reason.Type)
2829 if reason.Comment != "" {
2830 reasons += fmt.Sprintf(": %q", reason.Comment)
2833 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)
2834 for _, dkim := range record.AuthResults.DKIM {
2836 if dkim.HumanResult != "" {
2837 result = fmt.Sprintf(": %q", dkim.HumanResult)
2839 fmt.Printf("\t\tdkim %s; domain %q selector %q%s\n", dkim.Result, dkim.Domain, dkim.Selector, result)
2841 for _, spf := range record.AuthResults.SPF {
2842 fmt.Printf("\t\tspf %s; domain %q scope %q\n", spf.Result, spf.Domain, spf.Scope)
2848func cmdDMARCDBAddReport(c *cmd) {
2850 c.params = "fromdomain < message"
2851 c.help = "Add a DMARC report to the database."
2859 fromdomain := xparseDomain(args[0], "domain")
2860 fmt.Fprintln(os.Stderr, "reading report message from stdin")
2861 report, err := dmarcrpt.ParseMessageReport(c.log.Logger, os.Stdin)
2862 xcheckf(err, "parse message")
2863 err = dmarcdb.AddReport(context.Background(), report, fromdomain)
2864 xcheckf(err, "add dmarc report")
2867func cmdTLSRPTLookup(c *cmd) {
2869 c.help = `Lookup the TLSRPT record for the domain.
2871A TLSRPT record typically contains an email address where reports about TLS
2872connectivity should be sent. Mail servers attempting delivery to our domain
2873should attempt to use TLS. TLSRPT lets them report how many connection
2874successfully used TLS, and how what kind of errors occurred otherwise.
2881 d := xparseDomain(args[0], "domain")
2882 _, txt, err := tlsrpt.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, d)
2883 xcheckf(err, "tlsrpt lookup for %s", d)
2887func cmdTLSRPTParsereportmsg(c *cmd) {
2888 c.params = "message ..."
2889 c.help = `Parse and print the TLSRPT in the message.
2891The report is printed in formatted JSON.
2898 for _, arg := range args {
2899 f, err := os.Open(arg)
2900 xcheckf(err, "open %q", arg)
2901 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, f)
2902 xcheckf(err, "parse report in %q", arg)
2903 // todo future: only print the highlights?
2904 enc := json.NewEncoder(os.Stdout)
2905 enc.SetIndent("", "\t")
2906 enc.SetEscapeHTML(false)
2907 err = enc.Encode(reportJSON)
2908 xcheckf(err, "write report")
2912func cmdSPFCheck(c *cmd) {
2913 c.params = "domain ip"
2914 c.help = `Check the status of IP for the policy published in DNS for the domain.
2916IPs may be allowed to send for a domain, or disallowed, and several shades in
2917between. If not allowed, an explanation may be provided by the policy. If so,
2918the explanation is printed. The SPF mechanism that matched (if any) is also
2926 domain := xparseDomain(args[0], "domain")
2928 ip := xparseIP(args[1], "ip")
2930 spfargs := spf.Args{
2932 MailFromLocalpart: "user",
2933 MailFromDomain: domain,
2934 HelloDomain: dns.IPDomain{Domain: domain},
2935 LocalIP: net.ParseIP("127.0.0.1"),
2936 LocalHostname: dns.Domain{ASCII: "localhost"},
2938 r, _, explanation, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfargs)
2940 fmt.Printf("error: %s\n", err)
2942 if explanation != "" {
2943 fmt.Printf("explanation: %s\n", explanation)
2945 fmt.Printf("status: %s (%s)\n", r.Result, dnssecStatus(authentic))
2946 if r.Mechanism != "" {
2947 fmt.Printf("mechanism: %s\n", r.Mechanism)
2951func cmdSPFParse(c *cmd) {
2952 c.params = "txtrecord"
2953 c.help = "Parse the record as SPF record. If valid, nothing is printed."
2959 _, _, err := spf.ParseRecord(args[0])
2960 xcheckf(err, "parsing record")
2963func cmdSPFLookup(c *cmd) {
2965 c.help = "Lookup the SPF record for the domain and print it."
2971 domain := xparseDomain(args[0], "domain")
2972 _, txt, _, authentic, err := spf.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
2973 xcheckf(err, "spf lookup for %s", domain)
2975 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2978func cmdMTASTSLookup(c *cmd) {
2980 c.help = `Lookup the MTASTS record and policy for the domain.
2982MTA-STS is a mechanism for a domain to specify if it requires TLS connections
2983for delivering email. If a domain has a valid MTA-STS DNS TXT record at
2984_mta-sts.<domain> it signals it implements MTA-STS. A policy can then be
2985fetched at https://mta-sts.<domain>/.well-known/mta-sts.txt. The policy
2986specifies the mode (enforce, testing, none), which MX servers support TLS and
2987should be used, and how long the policy can be cached.
2994 domain := xparseDomain(args[0], "domain")
2996 record, policy, _, err := mtasts.Get(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
2998 fmt.Printf("error: %s\n", err)
3001 fmt.Printf("DNS TXT record _mta-sts.%s: %s\n", domain.ASCII, record.String())
3005 fmt.Printf("policy at https://mta-sts.%s/.well-known/mta-sts.txt:\n", domain.ASCII)
3006 fmt.Printf("%s", policy.String())
3010func cmdRDAPDomainage(c *cmd) {
3012 c.help = `Lookup the age of domain in RDAP based on latest registration.
3014RDAP is the registration data access protocol. Registries run RDAP services for
3015their top level domains, providing information such as the registration date of
3016domains. This command looks up the "age" of a domain by looking at the most
3017recent "registration", "reregistration" or "reinstantiation" event.
3019Email messages from recently registered domains are often treated with
3020suspicion, and some mail systems are more likely to classify them as junk.
3022On each invocation, a bootstrap file with a list of registries (of top-level
3023domains) is retrieved, without caching. Do not run this command too often with
3031 domain := xparseDomain(args[0], "domain")
3033 registration, err := rdap.LookupLastDomainRegistration(context.Background(), c.log, domain)
3034 xcheckf(err, "looking up domain in rdap")
3036 age := time.Since(registration)
3037 const day = 24 * time.Hour
3038 const year = 365 * day
3040 days := (age - years*year) / day
3044 } else if years > 0 {
3045 s = fmt.Sprintf("%d years, ", years)
3050 s += fmt.Sprintf("%d days", days)
3055func cmdRetrain(c *cmd) {
3056 c.params = "[accountname]"
3057 c.help = `Recreate and retrain the junk filter for the account or all accounts.
3059Useful after having made changes to the junk filter configuration, or if the
3060implementation has changed.
3072 ctlcmdRetrain(xctl(), account)
3075func ctlcmdRetrain(ctl *ctl, account string) {
3076 ctl.xwrite("retrain")
3081func cmdTLSRPTDBAddReport(c *cmd) {
3083 c.params = "< message"
3084 c.help = "Parse a TLS report from the message and add it to the database."
3086 c.flag.BoolVar(&hostReport, "hostreport", false, "report for a host instead of domain")
3094 // First read message, to get the From-header. Then parse it as TLSRPT.
3095 fmt.Fprintln(os.Stderr, "reading report message from stdin")
3096 buf, err := io.ReadAll(os.Stdin)
3097 xcheckf(err, "reading message")
3098 part, err := message.Parse(c.log.Logger, true, bytes.NewReader(buf))
3099 xcheckf(err, "parsing message")
3100 if part.Envelope == nil || len(part.Envelope.From) != 1 {
3101 log.Fatalf("message must have one From-header")
3103 from := part.Envelope.From[0]
3104 domain := xparseDomain(from.Host, "domain")
3106 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, bytes.NewReader(buf))
3107 xcheckf(err, "parsing tls report in message")
3109 mailfrom := from.User + "@" + from.Host // todo future: should escape and such
3110 report := reportJSON.Convert()
3111 err = tlsrptdb.AddReport(context.Background(), c.log, domain, mailfrom, hostReport, &report)
3112 xcheckf(err, "add tls report to database")
3115func cmdDNSBLCheck(c *cmd) {
3116 c.params = "zone ip"
3117 c.help = `Test if IP is in the DNS blocklist of the zone, e.g. bl.spamcop.net.
3119If the IP is in the blocklist, an explanation is printed. This is typically a
3120URL with more information.
3127 zone := xparseDomain(args[0], "zone")
3128 ip := xparseIP(args[1], "ip")
3130 status, explanation, err := dnsbl.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, zone, ip)
3131 fmt.Printf("status: %s\n", status)
3132 if status == dnsbl.StatusFail {
3133 fmt.Printf("explanation: %q\n", explanation)
3136 fmt.Printf("error: %s\n", err)
3140func cmdDNSBLCheckhealth(c *cmd) {
3142 c.help = `Check the health of the DNS blocklist represented by zone, e.g. bl.spamcop.net.
3144The health of a DNS blocklist can be checked by querying for 127.0.0.1 and
3145127.0.0.2. The second must and the first must not be present.
3152 zone := xparseDomain(args[0], "zone")
3153 err := dnsbl.CheckHealth(context.Background(), c.log.Logger, dns.StrictResolver{}, zone)
3154 xcheckf(err, "unhealthy")
3155 fmt.Println("healthy")
3158func cmdCheckupdate(c *cmd) {
3159 c.help = `Check if a newer version of mox is available.
3161A single DNS TXT lookup to _updates.xmox.nl tells if a new version is
3162available. If so, a changelog is fetched from https://updates.xmox.nl, and the
3163individual entries verified with a builtin public key. The changelog is
3166 if len(c.Parse()) != 0 {
3171 current, lastknown, _, err := store.LastKnown()
3173 log.Printf("getting last known version: %s", err)
3175 fmt.Printf("last known version: %s\n", lastknown)
3176 fmt.Printf("current version: %s\n", current)
3178 latest, _, err := updates.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dns.Domain{ASCII: changelogDomain})
3179 xcheckf(err, "lookup of latest version")
3180 fmt.Printf("latest version: %s\n", latest)
3182 if latest.After(current) {
3183 changelog, err := updates.FetchChangelog(context.Background(), c.log.Logger, changelogURL, current, changelogPubKey)
3184 xcheckf(err, "fetching changelog")
3185 if len(changelog.Changes) == 0 {
3186 log.Printf("no changes in changelog")
3189 fmt.Println("Changelog")
3190 for _, c := range changelog.Changes {
3191 fmt.Println("\n" + strings.TrimSpace(c.Text))
3196func cmdCid(c *cmd) {
3198 c.help = `Turn an ID from a Received header into a cid, for looking up in logs.
3200A cid is essentially a connection counter initialized when mox starts. Each log
3201line contains a cid. Received headers added by mox contain a unique ID that can
3202be decrypted to a cid by admin of a mox instance only.
3210 recvidpath := mox.DataDirPath("receivedid.key")
3211 recvidbuf, err := os.ReadFile(recvidpath)
3212 xcheckf(err, "reading %s", recvidpath)
3213 if len(recvidbuf) != 16+8 {
3214 log.Fatalf("bad data in %s: got %d bytes, expect 16+8=24", recvidpath, len(recvidbuf))
3216 err = mox.ReceivedIDInit(recvidbuf[:16], recvidbuf[16:])
3217 xcheckf(err, "init receivedid")
3219 cid, err := mox.ReceivedToCid(args[0])
3220 xcheckf(err, "received id to cid")
3221 fmt.Printf("%x\n", cid)
3224func cmdVersion(c *cmd) {
3225 c.help = "Prints this mox version."
3226 if len(c.Parse()) != 0 {
3229 fmt.Println(moxvar.Version)
3230 fmt.Printf("%s/%s\n", runtime.GOOS, runtime.GOARCH)
3233func cmdWebapi(c *cmd) {
3234 c.params = "[method [baseurl-with-credentials]"
3235 c.help = "Lists available methods, prints request/response parameters for method, or calls a method with a request read from standard input."
3241 t := reflect.TypeFor[webapi.Methods]()
3242 methods := map[string]reflect.Type{}
3244 for i := range t.NumMethod() {
3246 methods[mt.Name] = mt.Type
3247 ml = append(ml, mt.Name)
3251 fmt.Println(strings.Join(ml, "\n"))
3255 mt, ok := methods[args[0]]
3257 log.Fatalf("unknown method %q", args[0])
3259 resultNotJSON := mt.Out(0).Kind() == reflect.Interface
3262 fmt.Println("# Example request")
3264 printJSON("\t", mox.FillExample(nil, reflect.New(mt.In(1))).Interface())
3267 fmt.Println("Output is non-JSON data.")
3270 fmt.Println("# Example response")
3272 printJSON("\t", mox.FillExample(nil, reflect.New(mt.Out(0))).Interface())
3278 response = reflect.New(mt.Out(0))
3281 fmt.Fprintln(os.Stderr, "reading request from stdin...")
3282 request, err := io.ReadAll(os.Stdin)
3283 xcheckf(err, "read message")
3285 dec := json.NewDecoder(bytes.NewReader(request))
3286 dec.DisallowUnknownFields()
3287 err = dec.Decode(reflect.New(mt.In(1)).Interface())
3288 xcheckf(err, "parsing request")
3290 resp, err := http.PostForm(args[1]+args[0], url.Values{"request": []string{string(request)}})
3291 xcheckf(err, "http post")
3293 if err := resp.Body.Close(); err != nil {
3294 log.Printf("closing http response body: %v", err)
3297 if resp.StatusCode == http.StatusBadRequest {
3298 buf, err := io.ReadAll(&moxio.LimitReader{R: resp.Body, Limit: 10 * 1024})
3299 xcheckf(err, "reading response for 400 bad request error")
3300 err = json.Unmarshal(buf, &response)
3302 printJSON("", response)
3304 fmt.Fprintf(os.Stderr, "(not json)\n")
3305 os.Stderr.Write(buf)
3308 } else if resp.StatusCode != http.StatusOK {
3309 fmt.Fprintf(os.Stderr, "http response %s\n", resp.Status)
3310 _, err := io.Copy(os.Stderr, resp.Body)
3311 xcheckf(err, "copy body")
3313 err := json.NewDecoder(resp.Body).Decode(&resp)
3314 xcheckf(err, "unmarshal response")
3315 printJSON("", response)
3319func printJSON(indent string, v any) {
3320 fmt.Printf("%s", indent)
3321 enc := json.NewEncoder(os.Stdout)
3322 enc.SetIndent(indent, "\t")
3323 enc.SetEscapeHTML(false)
3324 err := enc.Encode(v)
3325 xcheckf(err, "encode json")
3328// 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.
3329func cmdBumpUIDValidity(c *cmd) {
3330 c.params = "account [mailbox]"
3331 c.help = `Change the IMAP UID validity of the mailbox, causing IMAP clients to refetch messages.
3333This can be useful after manually repairing metadata about the account/mailbox.
3335Opens account database file directly. Ensure mox does not have the account
3336open, or is not running.
3339 if len(args) != 1 && len(args) != 2 {
3344 a, err := store.OpenAccount(c.log, args[0], false)
3345 xcheckf(err, "open account")
3347 if err := a.Close(); err != nil {
3348 log.Printf("closing account: %v", err)
3352 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3353 uidvalidity, err := a.NextUIDValidity(tx)
3355 return fmt.Errorf("assigning next uid validity: %v", err)
3358 q := bstore.QueryTx[store.Mailbox](tx)
3359 q.FilterEqual("Expunged", false)
3361 q.FilterEqual("Name", args[1])
3363 mbl, err := q.SortAsc("Name").List()
3365 return fmt.Errorf("looking up mailbox: %v", err)
3367 if len(args) == 2 && len(mbl) != 1 {
3368 return fmt.Errorf("looking up mailbox %q, found %d mailboxes", args[1], len(mbl))
3370 for _, mb := range mbl {
3371 mb.UIDValidity = uidvalidity
3372 err = tx.Update(&mb)
3374 return fmt.Errorf("updating uid validity for mailbox: %v", err)
3376 fmt.Printf("uid validity for %q updated to %d\n", mb.Name, uidvalidity)
3380 xcheckf(err, "updating database")
3383func cmdReassignUIDs(c *cmd) {
3384 c.params = "account [mailboxid]"
3385 c.help = `Reassign UIDs in one mailbox or all mailboxes in an account and bump UID validity, causing IMAP clients to refetch messages.
3387Opens account database file directly. Ensure mox does not have the account
3388open, or is not running.
3391 if len(args) != 1 && len(args) != 2 {
3398 mailboxID, err = strconv.ParseInt(args[1], 10, 64)
3399 xcheckf(err, "parsing mailbox id")
3403 a, err := store.OpenAccount(c.log, args[0], false)
3404 xcheckf(err, "open account")
3406 if err := a.Close(); err != nil {
3407 log.Printf("closing account: %v", err)
3411 // Gather the last-assigned UIDs per mailbox.
3412 uidlasts := map[int64]store.UID{}
3414 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3415 // Reassign UIDs, going per mailbox. We assign starting at 1, only changing the
3416 // message if it isn't already at the intended UID. Doing it in this order ensures
3417 // we don't get into trouble with duplicate UIDs for a mailbox. We assign a new
3418 // modseq. Not strictly needed, but doesn't hurt. It's also why we assign a UID to
3419 // expunged messages.
3420 modseq, err := a.NextModSeq(tx)
3421 xcheckf(err, "assigning next modseq")
3423 q := bstore.QueryTx[store.Message](tx)
3425 q.FilterNonzero(store.Message{MailboxID: mailboxID})
3427 q.SortAsc("MailboxID", "UID")
3428 err = q.ForEach(func(m store.Message) error {
3429 uidlasts[m.MailboxID]++
3430 uid := uidlasts[m.MailboxID]
3434 if err := tx.Update(&m); err != nil {
3435 return fmt.Errorf("updating uid for message: %v", err)
3441 return fmt.Errorf("reading through messages: %v", err)
3444 // Now update the uidnext, uidvalidity and modseq for each mailbox.
3445 err = bstore.QueryTx[store.Mailbox](tx).FilterEqual("Expunged", false).ForEach(func(mb store.Mailbox) error {
3446 // Assign each mailbox a completely new uidvalidity.
3447 uidvalidity, err := a.NextUIDValidity(tx)
3449 return fmt.Errorf("assigning next uid validity: %v", err)
3452 if mb.UIDValidity >= uidvalidity {
3453 // This should not happen, but since we're fixing things up after a hypothetical
3454 // mishap, might as well account for inconsistent uidvalidity.
3455 next := store.NextUIDValidity{ID: 1, Next: mb.UIDValidity + 2}
3456 if err := tx.Update(&next); err != nil {
3457 log.Printf("updating nextuidvalidity: %v, continuing", err)
3461 mb.UIDValidity = uidvalidity
3463 mb.UIDNext = uidlasts[mb.ID] + 1
3465 if err := tx.Update(&mb); err != nil {
3466 return fmt.Errorf("updating uidvalidity and uidnext for mailbox: %v", err)
3471 return fmt.Errorf("updating mailboxes: %v", err)
3475 xcheckf(err, "updating database")
3478func cmdFixUIDMeta(c *cmd) {
3479 c.params = "account"
3480 c.help = `Fix inconsistent UIDVALIDITY and UIDNEXT in messages/mailboxes/account.
3482The next UID to use for a message in a mailbox should always be higher than any
3483existing message UID in the mailbox. If it is not, the mailbox UIDNEXT is
3486Each mailbox has a UIDVALIDITY sequence number, which should always be lower
3487than the per-account next UIDVALIDITY to use. If it is not, the account next
3488UIDVALIDITY is updated.
3490Opens account database file directly. Ensure mox does not have the account
3491open, or is not running.
3499 a, err := store.OpenAccount(c.log, args[0], false)
3500 xcheckf(err, "open account")
3502 if err := a.Close(); err != nil {
3503 log.Printf("closing account: %v", err)
3507 var maxUIDValidity uint32
3509 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3510 // We look at each mailbox, retrieve its max UID and compare against the mailbox
3512 err := bstore.QueryTx[store.Mailbox](tx).FilterEqual("Expunged", false).ForEach(func(mb store.Mailbox) error {
3513 if mb.UIDValidity > maxUIDValidity {
3514 maxUIDValidity = mb.UIDValidity
3516 m, err := bstore.QueryTx[store.Message](tx).FilterNonzero(store.Message{MailboxID: mb.ID}).SortDesc("UID").Limit(1).Get()
3517 if err == bstore.ErrAbsent || err == nil && m.UID < mb.UIDNext {
3519 } else if err != nil {
3520 return fmt.Errorf("finding message with max uid in mailbox: %w", err)
3522 olduidnext := mb.UIDNext
3523 mb.UIDNext = m.UID + 1
3524 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)
3525 if err := tx.Update(&mb); err != nil {
3526 return fmt.Errorf("updating mailbox uidnext: %v", err)
3531 return fmt.Errorf("processing mailboxes: %v", err)
3534 uidvalidity := store.NextUIDValidity{ID: 1}
3535 if err := tx.Get(&uidvalidity); err != nil {
3536 return fmt.Errorf("reading account next uidvalidity: %v", err)
3538 if maxUIDValidity >= uidvalidity.Next {
3539 log.Printf("account next uidvalidity %d <= highest uidvalidity %d found in mailbox, resetting account next uidvalidity to %d", uidvalidity.Next, maxUIDValidity, maxUIDValidity+1)
3540 uidvalidity.Next = maxUIDValidity + 1
3541 if err := tx.Update(&uidvalidity); err != nil {
3542 return fmt.Errorf("updating account next uidvalidity: %v", err)
3548 xcheckf(err, "updating database")
3551func cmdFixmsgsize(c *cmd) {
3552 c.params = "[account]"
3553 c.help = `Ensure message sizes in the database matching the sum of the message prefix length and on-disk file size.
3555Messages with an inconsistent size are also parsed again.
3557If an inconsistency is found, you should probably also run "mox
3558bumpuidvalidity" on the mailboxes or entire account to force IMAP clients to
3571 ctlcmdFixmsgsize(xctl(), account)
3574func ctlcmdFixmsgsize(ctl *ctl, account string) {
3575 ctl.xwrite("fixmsgsize")
3578 ctl.xstreamto(os.Stdout)
3581func cmdReparse(c *cmd) {
3582 c.params = "[account]"
3583 c.help = `Parse all messages in the account or all accounts again.
3585Can be useful after upgrading mox with improved message parsing. Messages are
3586parsed in batches, so other access to the mailboxes/messages are not blocked
3587while reparsing all messages.
3599 ctlcmdReparse(xctl(), account)
3602func ctlcmdReparse(ctl *ctl, account string) {
3603 ctl.xwrite("reparse")
3606 ctl.xstreamto(os.Stdout)
3609func cmdEnsureParsed(c *cmd) {
3610 c.params = "account"
3611 c.help = "Ensure messages in the database have a pre-parsed MIME form in the database."
3613 c.flag.BoolVar(&all, "all", false, "store new parsed message for all messages")
3620 a, err := store.OpenAccount(c.log, args[0], false)
3621 xcheckf(err, "open account")
3623 if err := a.Close(); err != nil {
3624 log.Printf("closing account: %v", err)
3629 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3630 q := bstore.QueryTx[store.Message](tx)
3631 q.FilterEqual("Expunged", false)
3632 q.FilterFn(func(m store.Message) bool {
3633 return all || m.ParsedBuf == nil
3637 return fmt.Errorf("list messages: %v", err)
3639 for _, m := range l {
3640 mr := a.MessageReader(m)
3641 p, err := message.EnsurePart(c.log.Logger, false, mr, m.Size)
3643 log.Printf("parsing message %d: %v (continuing)", m.ID, err)
3645 m.ParsedBuf, err = json.Marshal(p)
3647 return fmt.Errorf("marshal parsed message: %v", err)
3649 if err := tx.Update(&m); err != nil {
3650 return fmt.Errorf("update message: %v", err)
3656 xcheckf(err, "update messages with parsed mime structure")
3657 fmt.Printf("%d messages updated\n", n)
3660func cmdRecalculateMailboxCounts(c *cmd) {
3661 c.params = "account"
3662 c.help = `Recalculate message counts for all mailboxes in the account, and total message size for quota.
3664When a message is added to/removed from a mailbox, or when message flags change,
3665the total, unread, unseen and deleted messages are accounted, the total size of
3666the mailbox, and the total message size for the account. In case of a bug in
3667this accounting, the numbers could become incorrect. This command will find, fix
3676 ctlcmdRecalculateMailboxCounts(xctl(), args[0])
3679func ctlcmdRecalculateMailboxCounts(ctl *ctl, account string) {
3680 ctl.xwrite("recalculatemailboxcounts")
3683 ctl.xstreamto(os.Stdout)
3686func cmdMessageParse(c *cmd) {
3687 c.params = "message.eml"
3688 c.help = "Parse message, print JSON representation."
3691 c.flag.BoolVar(&smtputf8, "smtputf8", false, "check if message needs smtputf8")
3697 f, err := os.Open(args[0])
3698 xcheckf(err, "open")
3700 if err := f.Close(); err != nil {
3701 log.Printf("closing message file: %v", err)
3705 part, err := message.Parse(c.log.Logger, false, f)
3706 xcheckf(err, "parsing message")
3707 err = part.Walk(c.log.Logger, nil)
3708 xcheckf(err, "parsing nested parts")
3709 enc := json.NewEncoder(os.Stdout)
3710 enc.SetIndent("", "\t")
3711 enc.SetEscapeHTML(false)
3712 err = enc.Encode(part)
3713 xcheckf(err, "write")
3716 needs, err := part.NeedsSMTPUTF8()
3717 xcheckf(err, "checking if message needs smtputf8")
3718 fmt.Println("message needs smtputf8:", needs)
3722func cmdOpenaccounts(c *cmd) {
3724 c.params = "datadir account ..."
3725 c.help = `Open and close accounts, for triggering data upgrades, for tests.
3727Opens database files directly, not going through a running mox instance.
3735 dataDir := filepath.Clean(args[0])
3736 for _, accName := range args[1:] {
3737 accDir := filepath.Join(dataDir, "accounts", accName)
3738 log.Printf("opening account %s...", accDir)
3739 a, err := store.OpenAccountDB(c.log, accDir, accName)
3740 xcheckf(err, "open account %s", accName)
3741 err = a.ThreadingWait(c.log)
3742 xcheckf(err, "wait for threading upgrade to complete for %s", accName)
3744 xcheckf(err, "close account %s", accName)
3748func cmdReassignthreads(c *cmd) {
3749 c.params = "[account]"
3750 c.help = `Reassign message threads.
3752For all accounts, or optionally only the specified account.
3754Threading for all messages in an account is first reset, and new base subject
3755and normalized message-id saved with the message. Then all messages are
3756evaluated and matched against their parents/ancestors.
3758Messages are matched based on the References header, with a fall-back to an
3759In-Reply-To header, and if neither is present/valid, based only on base
3762A References header typically points to multiple previous messages in a
3763hierarchy. From oldest ancestor to most recent parent. An In-Reply-To header
3764would have only a message-id of the parent message.
3766A message is only linked to a parent/ancestor if their base subject is the
3767same. This ensures unrelated replies, with a new subject, are placed in their
3770The base subject is lower cased, has whitespace collapsed to a single
3771space, and some components removed: leading "Re:", "Fwd:", "Fw:", or bracketed
3772tag (that mailing lists often add, e.g. "[listname]"), trailing "(fwd)", or
3773enclosing "[fwd: ...]".
3775Messages are linked to all their ancestors. If an intermediate parent/ancestor
3776message is deleted in the future, the message can still be linked to the earlier
3777ancestors. If the direct parent already wasn't available while matching, this is
3778stored as the message having a "missing link" to its stored ancestors.
3790 ctlcmdReassignthreads(xctl(), account)
3793func ctlcmdReassignthreads(ctl *ctl, account string) {
3794 ctl.xwrite("reassignthreads")
3797 ctl.xstreamto(os.Stdout)
3800func cmdIMAPServe(c *cmd) {
3801 c.params = "preauth-address"
3802 c.help = `Initiate a preauthenticated IMAP connection on file descriptor 0.
3804For use with tools that can do IMAP over tunneled connections, e.g. with SSH
3805during migrations. TLS is not possible on the connection, and authentication
3806does not require TLS.
3809 c.flag.BoolVar(&fd0, "fd0", false, "write IMAP to file descriptor 0 instead of stdout")
3820 ctlcmdIMAPServe(xctl(), address, os.Stdin, output)
3823func ctlcmdIMAPServe(ctl *ctl, address string, input io.ReadCloser, output io.WriteCloser) {
3824 ctl.xwrite("imapserve")
3828 done := make(chan struct{}, 1)
3833 _, err := io.Copy(output, ctl.conn)
3837 log.Printf("reading from imap: %v", err)
3843 _, err := io.Copy(ctl.conn, input)
3847 log.Printf("writing to imap: %v", err)
3852func cmdReadmessages(c *cmd) {
3854 c.params = "datadir account ..."
3855 c.help = `Open account, parse several headers for all messages.
3857For performance testing.
3859Opens database files directly, not going through a running mox instance.
3862 gomaxprocs := runtime.GOMAXPROCS(0)
3863 var procs, workqueuesize, limit int
3864 c.flag.IntVar(&procs, "procs", gomaxprocs, "number of goroutines for reading messages")
3865 c.flag.IntVar(&workqueuesize, "workqueuesize", 2*gomaxprocs, "number of messages to keep in work queue")
3866 c.flag.IntVar(&limit, "limit", 0, "number of messages to process if greater than zero")
3872 type threadPrep struct {
3877 threadingFields := [][]byte{
3878 []byte("references"),
3879 []byte("in-reply-to"),
3882 dataDir := filepath.Clean(args[0])
3883 for _, accName := range args[1:] {
3884 accDir := filepath.Join(dataDir, "accounts", accName)
3885 log.Printf("opening account %s...", accDir)
3886 a, err := store.OpenAccountDB(c.log, accDir, accName)
3887 xcheckf(err, "open account %s", accName)
3889 prepareMessages := func(in, out chan moxio.Work[store.Message, threadPrep]) {
3890 headerbuf := make([]byte, 8*1024)
3891 scratch := make([]byte, 4*1024)
3899 var partialPart struct {
3903 if err := json.Unmarshal(m.ParsedBuf, &partialPart); err != nil {
3904 w.Err = fmt.Errorf("unmarshal part: %v", err)
3906 size := partialPart.BodyOffset - partialPart.HeaderOffset
3907 if int(size) > len(headerbuf) {
3908 headerbuf = make([]byte, size)
3911 buf := headerbuf[:int(size)]
3912 err := func() error {
3913 mr := a.MessageReader(m)
3915 if err := mr.Close(); err != nil {
3916 log.Printf("closing message reader: %v", err)
3920 // ReadAt returns whole buffer or error. Single read should be fast.
3921 n, err := mr.ReadAt(buf, partialPart.HeaderOffset)
3922 if err != nil || n != len(buf) {
3923 return fmt.Errorf("read header: %v", err)
3929 } else if h, err := message.ParseHeaderFields(buf, scratch, threadingFields); err != nil {
3932 w.Out.references = h["References"]
3933 w.Out.inReplyTo = h["In-Reply-To"]
3946 processMessage := func(m store.Message, prep threadPrep) error {
3948 log.Printf("%d messages (delta %s)", n, time.Since(t))
3955 wq := moxio.NewWorkQueue[store.Message, threadPrep](procs, workqueuesize, prepareMessages, processMessage)
3957 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3958 q := bstore.QueryTx[store.Message](tx)
3959 q.FilterEqual("Expunged", false)
3964 err = q.ForEach(wq.Add)
3972 xcheckf(err, "processing message")
3975 xcheckf(err, "close account %s", accName)
3976 log.Printf("account %s, total time %s", accName, time.Since(t0))
3980func cmdQueueFillRetired(c *cmd) {
3982 c.help = `Fill retired messag and webhooks queue with testdata.
3984For testing the pagination. Operates directly on queue database.
3987 c.flag.IntVar(&n, "n", 10000, "retired messages and retired webhooks to insert")
3995 xcheckf(err, "init queue")
3996 err = queue.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3999 // Cause autoincrement ID for queue.Msg to be forwarded, and use the reserved ID
4000 // space for inserting retired messages.
4002 err = tx.Insert(&fm)
4003 xcheckf(err, "temporarily insert message to get autoincrement sequence")
4004 err = tx.Delete(&fm)
4005 xcheckf(err, "removing temporary message for resetting autoincrement sequence")
4007 err = tx.Insert(&fm)
4008 xcheckf(err, "temporarily insert message to forward autoincrement sequence")
4009 err = tx.Delete(&fm)
4010 xcheckf(err, "removing temporary message after forwarding autoincrement sequence")
4013 // And likewise for webhooks.
4014 fh := queue.Hook{Account: "x", URL: "x", NextAttempt: time.Now()}
4015 err = tx.Insert(&fh)
4016 xcheckf(err, "temporarily insert webhook to get autoincrement sequence")
4017 err = tx.Delete(&fh)
4018 xcheckf(err, "removing temporary webhook for resetting autoincrement sequence")
4020 err = tx.Insert(&fh)
4021 xcheckf(err, "temporarily insert webhook to forward autoincrement sequence")
4022 err = tx.Delete(&fh)
4023 xcheckf(err, "removing temporary webhook after forwarding autoincrement sequence")
4027 t0 := now.Add(-time.Duration(i) * time.Second)
4028 last := now.Add(-time.Duration(i/10) * time.Second)
4029 mr := queue.MsgRetired{
4030 ID: fm.ID + int64(i),
4032 SenderAccount: "test",
4033 SenderLocalpart: "mox",
4034 SenderDomainStr: "localhost",
4035 FromID: fmt.Sprintf("%016d", i),
4036 RecipientLocalpart: "mox",
4037 RecipientDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "localhost"}},
4038 RecipientDomainStr: "localhost",
4041 Results: []queue.MsgResult{
4044 Duration: time.Millisecond,
4051 Size: int64(i * 100),
4052 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
4053 Subject: fmt.Sprintf("test message %d", i),
4054 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
4056 RecipientAddress: "mox@localhost",
4058 KeepUntil: now.Add(48 * time.Hour),
4060 err := tx.Insert(&mr)
4061 xcheckf(err, "inserting retired message")
4065 t0 := now.Add(-time.Duration(i) * time.Second)
4066 last := now.Add(-time.Duration(i/10) * time.Second)
4071 hr := queue.HookRetired{
4072 ID: fh.ID + int64(i),
4073 QueueMsgID: fm.ID + int64(i),
4074 FromID: fmt.Sprintf("%016d", i),
4075 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
4076 Subject: fmt.Sprintf("test message %d", i),
4077 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
4079 URL: "http://localhost/hook",
4080 IsIncoming: i%10 == 0,
4081 OutgoingEvent: event,
4086 Results: []queue.HookResult{
4089 Duration: time.Millisecond,
4090 URL: "http://localhost/hook",
4099 KeepUntil: now.Add(48 * time.Hour),
4101 err := tx.Insert(&hr)
4102 xcheckf(err, "inserting retired hook")
4107 xcheckf(err, "add to queue")
4108 log.Printf("added %d retired messages and %d retired webhooks", n, n)