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 list", cmdConfigAccountList},
149 {"config account add", cmdConfigAccountAdd},
150 {"config account rm", cmdConfigAccountRemove},
151 {"config account disable", cmdConfigAccountDisable},
152 {"config account enable", cmdConfigAccountEnable},
153 {"config address add", cmdConfigAddressAdd},
154 {"config address rm", cmdConfigAddressRemove},
155 {"config domain add", cmdConfigDomainAdd},
156 {"config domain rm", cmdConfigDomainRemove},
157 {"config domain disable", cmdConfigDomainDisable},
158 {"config domain enable", cmdConfigDomainEnable},
159 {"config tlspubkey list", cmdConfigTlspubkeyList},
160 {"config tlspubkey get", cmdConfigTlspubkeyGet},
161 {"config tlspubkey add", cmdConfigTlspubkeyAdd},
162 {"config tlspubkey rm", cmdConfigTlspubkeyRemove},
163 {"config tlspubkey gen", cmdConfigTlspubkeyGen},
164 {"config alias list", cmdConfigAliasList},
165 {"config alias print", cmdConfigAliasPrint},
166 {"config alias add", cmdConfigAliasAdd},
167 {"config alias update", cmdConfigAliasUpdate},
168 {"config alias rm", cmdConfigAliasRemove},
169 {"config alias addaddr", cmdConfigAliasAddaddr},
170 {"config alias rmaddr", cmdConfigAliasRemoveaddr},
172 {"config describe-sendmail", cmdConfigDescribeSendmail},
173 {"config printservice", cmdConfigPrintservice},
174 {"config ensureacmehostprivatekeys", cmdConfigEnsureACMEHostprivatekeys},
175 {"config example", cmdConfigExample},
177 {"admin imapserve", cmdIMAPServe},
179 {"checkupdate", cmdCheckupdate},
181 {"clientconfig", cmdClientConfig},
182 {"deliver", cmdDeliver},
183 // 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
184 {"dane dial", cmdDANEDial},
185 {"dane dialmx", cmdDANEDialmx},
186 {"dane makerecord", cmdDANEMakeRecord},
187 {"dns lookup", cmdDNSLookup},
188 {"dkim gened25519", cmdDKIMGened25519},
189 {"dkim genrsa", cmdDKIMGenrsa},
190 {"dkim lookup", cmdDKIMLookup},
191 {"dkim txt", cmdDKIMTXT},
192 {"dkim verify", cmdDKIMVerify},
193 {"dkim sign", cmdDKIMSign},
194 {"dmarc lookup", cmdDMARCLookup},
195 {"dmarc parsereportmsg", cmdDMARCParsereportmsg},
196 {"dmarc verify", cmdDMARCVerify},
197 {"dmarc checkreportaddrs", cmdDMARCCheckreportaddrs},
198 {"dnsbl check", cmdDNSBLCheck},
199 {"dnsbl checkhealth", cmdDNSBLCheckhealth},
200 {"mtasts lookup", cmdMTASTSLookup},
201 {"rdap domainage", cmdRDAPDomainage},
202 {"retrain", cmdRetrain},
203 {"sendmail", cmdSendmail},
204 {"spf check", cmdSPFCheck},
205 {"spf lookup", cmdSPFLookup},
206 {"spf parse", cmdSPFParse},
207 {"tlsrpt lookup", cmdTLSRPTLookup},
208 {"tlsrpt parsereportmsg", cmdTLSRPTParsereportmsg},
209 {"version", cmdVersion},
210 {"webapi", cmdWebapi},
212 {"example", cmdExample},
213 {"bumpuidvalidity", cmdBumpUIDValidity},
214 {"reassignuids", cmdReassignUIDs},
215 {"fixuidmeta", cmdFixUIDMeta},
216 {"fixmsgsize", cmdFixmsgsize},
217 {"reparse", cmdReparse},
218 {"ensureparsed", cmdEnsureParsed},
219 {"recalculatemailboxcounts", cmdRecalculateMailboxCounts},
220 {"message parse", cmdMessageParse},
221 {"reassignthreads", cmdReassignthreads},
224 {"helpall", cmdHelpall},
225 {"junk analyze", cmdJunkAnalyze},
226 {"junk check", cmdJunkCheck},
227 {"junk play", cmdJunkPlay},
228 {"junk test", cmdJunkTest},
229 {"junk train", cmdJunkTrain},
230 {"dmarcdb addreport", cmdDMARCDBAddReport},
231 {"tlsrptdb addreport", cmdTLSRPTDBAddReport},
232 {"updates addsigned", cmdUpdatesAddSigned},
233 {"updates genkey", cmdUpdatesGenkey},
234 {"updates pubkey", cmdUpdatesPubkey},
235 {"updates serve", cmdUpdatesServe},
236 {"updates verify", cmdUpdatesVerify},
237 {"gentestdata", cmdGentestdata},
238 {"ximport maildir", cmdXImportMaildir},
239 {"ximport mbox", cmdXImportMbox},
240 {"openaccounts", cmdOpenaccounts},
241 {"readmessages", cmdReadmessages},
242 {"queuefillretired", cmdQueueFillRetired},
248 for _, xc := range commands {
249 c := cmd{words: strings.Split(xc.cmd, " "), fn: xc.fn}
250 cmds = append(cmds, c)
258 // Set before calling command.
261 _gather bool // Set when using Parse to gather usage for a command.
263 // Set by invoked command or Parse.
264 unlisted bool // If set, command is not listed until at least some words are matched from command.
265 params string // Arguments to command. Multiple lines possible.
266 help string // Additional explanation. First line is synopsis, the rest is only printed for an explicit help/usage for that command.
272func (c *cmd) Parse() []string {
273 // To gather params and usage information, we just run the command but cause this
274 // panic after the command has registered its flags and set its params and help
275 // information. This is then caught and that info printed.
280 c.flag.Usage = c.Usage
281 c.flag.Parse(c.flagArgs)
282 c.args = c.flag.Args()
286func (c *cmd) gather() {
287 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
291 // panic generated by Parse.
299func (c *cmd) makeUsage() string {
300 var r strings.Builder
301 cs := "mox " + strings.Join(c.words, " ")
302 for i, line := range strings.Split(strings.TrimSpace(c.params), "\n") {
310 fmt.Fprintf(&r, "%6s %s%s\n", s, cs, line)
313 c.flag.PrintDefaults()
317func (c *cmd) printUsage() {
318 fmt.Fprint(os.Stderr, c.makeUsage())
320 fmt.Fprint(os.Stderr, "\n"+c.help+"\n")
324func (c *cmd) Usage() {
329func cmdHelp(c *cmd) {
330 c.params = "[command ...]"
331 c.help = `Prints help about matching commands.
333If multiple commands match, they are listed along with the first line of their help text.
334If a single command matches, its usage and full help text is printed.
341 prefix := func(l, pre []string) bool {
342 if len(pre) > len(l) {
345 return slices.Equal(pre, l[:len(pre)])
349 for _, c := range cmds {
350 if slices.Equal(c.words, args) {
352 fmt.Print(c.makeUsage())
354 fmt.Print("\n" + c.help + "\n")
357 } else if prefix(c.words, args) {
358 partial = append(partial, c)
361 if len(partial) == 0 {
362 fmt.Fprintf(os.Stderr, "%s: unknown command\n", strings.Join(args, " "))
365 for _, c := range partial {
367 line := "mox " + strings.Join(c.words, " ")
368 fmt.Printf("%s\n", line)
370 fmt.Printf("\t%s\n", strings.Split(c.help, "\n")[0])
375func cmdHelpall(c *cmd) {
377 c.help = `Print all detailed usage and help information for all listed commands.
379Used to generate documentation.
387 for _, c := range cmds {
393 fmt.Fprintf(os.Stderr, "\n")
397 fmt.Fprintf(os.Stderr, "# mox %s\n\n", strings.Join(c.words, " "))
399 fmt.Fprintln(os.Stderr, c.help+"\n")
402 s = "\t" + strings.ReplaceAll(s, "\n", "\n\t")
403 fmt.Fprintln(os.Stderr, s)
407func usage(l []cmd, unlisted bool) {
410 lines = append(lines, "mox [-config config/mox.conf] [-pedantic] ...")
412 for _, c := range l {
414 if c.unlisted && !unlisted {
417 for _, line := range strings.Split(c.params, "\n") {
418 x := append([]string{"mox"}, c.words...)
422 lines = append(lines, strings.Join(x, " "))
425 for i, line := range lines {
430 fmt.Fprintln(os.Stderr, pre+line)
435var loglevel string // Empty will be interpreted as info, except by localserve.
438// subcommands that are not "serve" should use this function to load the config, it
439// restores any loglevel specified on the command-line, instead of using the
440// loglevels from the config file and it does not load files like TLS keys/certs.
441func mustLoadConfig() {
442 mox.MustLoadConfig(false, false)
447 if level, ok := mlog.Levels[ll]; ok {
448 mox.Conf.Log[""] = level
449 mlog.SetConfig(mox.Conf.Log)
451 log.Fatal("unknown loglevel", slog.String("loglevel", loglevel))
454 mox.SetPedantic(true)
459 // CheckConsistencyOnClose is true by default, for all the test packages. A regular
460 // mox server should never use it. But integration tests enable it again with a
462 store.CheckConsistencyOnClose = false
463 store.MsgFilesPerDirShiftSet(13) // For 1<<13 = 8k message files per directory.
465 ctxbg := context.Background()
471 // If invoked as sendmail, e.g. /usr/sbin/sendmail, we do enough so cron can get a
472 // message sent using smtp submission to a configured server.
473 if len(os.Args) > 0 && filepath.Base(os.Args[0]) == "sendmail" {
475 flag: flag.NewFlagSet("sendmail", flag.ExitOnError),
476 flagArgs: os.Args[1:],
477 log: mlog.New("sendmail", nil),
483 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")
484 flag.StringVar(&loglevel, "loglevel", "", "if non-empty, this log level is set early in startup")
485 flag.BoolVar(&pedantic, "pedantic", false, "protocol violations result in errors instead of accepting/working around them")
486 flag.BoolVar(&store.CheckConsistencyOnClose, "checkconsistency", false, "dangerous option for testing only, enables data checks that abort/panic when inconsistencies are found")
488 var cpuprofile, memprofile, tracefile string
489 flag.StringVar(&cpuprofile, "cpuprof", "", "store cpu profile to file")
490 flag.StringVar(&memprofile, "memprof", "", "store mem profile to file")
491 flag.StringVar(&tracefile, "trace", "", "store execution trace to file")
493 flag.Usage = func() { usage(cmds, false) }
501 defer traceExecution(tracefile)()
503 defer profile(cpuprofile, memprofile)()
506 mox.SetPedantic(true)
509 mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
514 if level, ok := mlog.Levels[ll]; ok {
515 mox.Conf.Log[""] = level
516 mlog.SetConfig(mox.Conf.Log)
517 // note: SetConfig may be called again when subcommands loads config.
519 log.Fatalf("unknown loglevel %q", loglevel)
524 for _, c := range cmds {
525 for i, w := range c.words {
526 if i >= len(args) || w != args[i] {
528 partial = append(partial, c)
533 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
534 c.flagArgs = args[len(c.words):]
535 c.log = mlog.New(strings.Join(c.words, ""), nil)
539 if len(partial) > 0 {
545func xcheckf(err error, format string, args ...any) {
549 msg := fmt.Sprintf(format, args...)
550 log.Fatalf("%s: %s", msg, err)
553func xparseIP(s, what string) net.IP {
556 log.Fatalf("invalid %s: %q", what, s)
561func xparseDomain(s, what string) dns.Domain {
562 d, err := dns.ParseDomain(s)
563 xcheckf(err, "parsing %s %q", what, s)
567func cmdClientConfig(c *cmd) {
569 c.help = `Print the configuration for email clients for a domain.
571Sending email is typically not done on the SMTP port 25, but on submission
572ports 465 (with TLS) and 587 (without initial TLS, but usually added to the
573connection with STARTTLS). For IMAP, the port with TLS is 993 and without is
576Without TLS/STARTTLS, passwords are sent in clear text, which should only be
577configured over otherwise secured connections, like a VPN.
583 d := xparseDomain(args[0], "domain")
588func printClientConfig(d dns.Domain) {
589 cc, err := admin.ClientConfigsDomain(d)
590 xcheckf(err, "getting client config")
591 fmt.Printf("%-20s %-30s %5s %-15s %s\n", "Protocol", "Host", "Port", "Listener", "Note")
592 for _, e := range cc.Entries {
593 fmt.Printf("%-20s %-30s %5d %-15s %s\n", e.Protocol, e.Host, e.Port, e.Listener, e.Note)
596To prevent authentication mechanism downgrade attempts that may result in
597clients sending plain text passwords to a MitM, clients should always be
598explicitly configured with the most secure authentication mechanism supported,
599the first of: SCRAM-SHA-256-PLUS, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, SCRAM-SHA-1,
604func cmdConfigTest(c *cmd) {
605 c.help = `Parses and validates the configuration files.
607If valid, the command exits with status 0. If not valid, all errors encountered
615 mox.FilesImmediate = true
617 _, errs := mox.ParseConfig(context.Background(), c.log, mox.ConfigStaticPath, true, true, false)
619 log.Printf("multiple errors:")
620 for _, err := range errs {
621 log.Printf("%s", err)
624 } else if len(errs) == 1 {
625 log.Fatalf("%s", errs[0])
628 fmt.Println("config OK")
631func cmdConfigDescribeStatic(c *cmd) {
632 c.params = ">mox.conf"
633 c.help = `Prints an annotated empty configuration for use as mox.conf.
635The static configuration file cannot be reloaded while mox is running. Mox has
636to be restarted for changes to the static configuration file to take effect.
638This configuration file needs modifications to make it valid. For example, it
639may contain unfinished list items.
641 if len(c.Parse()) != 0 {
646 err := sconf.Describe(os.Stdout, &sc)
647 xcheckf(err, "describing config")
650func cmdConfigDescribeDomains(c *cmd) {
651 c.params = ">domains.conf"
652 c.help = `Prints an annotated empty configuration for use as domains.conf.
654The domains configuration file contains the domains and their configuration,
655and accounts and their configuration. This includes the configured email
656addresses. The mox admin web interface, and the mox command line interface, can
657make changes to this file. Mox automatically reloads this file when it changes.
659Like the static configuration, the example domains.conf printed by this command
660needs modifications to make it valid.
662 if len(c.Parse()) != 0 {
666 var dc config.Dynamic
667 err := sconf.Describe(os.Stdout, &dc)
668 xcheckf(err, "describing config")
671func cmdConfigPrintservice(c *cmd) {
672 c.params = ">mox.service"
673 c.help = `Prints a systemd unit service file for mox.
675This is the same file as generated using quickstart. If the systemd service file
676has changed with a newer version of mox, use this command to generate an up to
679 if len(c.Parse()) != 0 {
683 pwd, err := os.Getwd()
685 log.Printf("current working directory: %v", err)
688 service := strings.ReplaceAll(moxService, "/home/mox", pwd)
692func cmdConfigDomainAdd(c *cmd) {
693 c.params = "[-disabled] domain account [localpart]"
694 c.help = `Adds a new domain to the configuration and reloads the configuration.
696The account is used for the postmaster mailboxes the domain, including as DMARC and
697TLS reporting. Localpart is the "username" at the domain for this account. If
698must be set if and only if account does not yet exist.
700The domain can be created in disabled mode, preventing automatically requesting
701TLS certificates with ACME, and rejecting incoming/outgoing messages involving
702the domain, but allowing further configuration of the domain.
705 c.flag.BoolVar(&disabled, "disabled", false, "disable the new domain")
707 if len(args) != 2 && len(args) != 3 {
711 d := xparseDomain(args[0], "domain")
713 var localpart smtp.Localpart
716 localpart, err = smtp.ParseLocalpart(args[2])
717 xcheckf(err, "parsing localpart")
719 ctlcmdConfigDomainAdd(xctl(), disabled, d, args[1], localpart)
722func ctlcmdConfigDomainAdd(ctl *ctl, disabled bool, domain dns.Domain, account string, localpart smtp.Localpart) {
723 ctl.xwrite("domainadd")
729 ctl.xwrite(domain.Name())
731 ctl.xwrite(string(localpart))
733 fmt.Printf("domain added, remember to add dns records, see:\n\nmox config dnsrecords %s\nmox config dnscheck %s\n", domain.Name(), domain.Name())
736func cmdConfigDomainRemove(c *cmd) {
738 c.help = `Remove a domain from the configuration and reload the configuration.
740This is a dangerous operation. Incoming email delivery for this domain will be
748 d := xparseDomain(args[0], "domain")
750 ctlcmdConfigDomainRemove(xctl(), d)
753func ctlcmdConfigDomainRemove(ctl *ctl, d dns.Domain) {
754 ctl.xwrite("domainrm")
757 fmt.Printf("domain removed, remember to remove dns records for %s\n", d)
760func cmdConfigDomainDisable(c *cmd) {
762 c.help = `Disable a domain and reload the configuration.
764This is a dangerous operation. Incoming/outgoing messages involving this domain
772 d := xparseDomain(args[0], "domain")
774 ctlcmdConfigDomainDisabled(xctl(), d, true)
775 fmt.Printf("domain disabled")
778func cmdConfigDomainEnable(c *cmd) {
780 c.help = `Enable a domain and reload the configuration.
782Incoming/outgoing messages involving this domain will be accepted again.
789 d := xparseDomain(args[0], "domain")
791 ctlcmdConfigDomainDisabled(xctl(), d, false)
794func ctlcmdConfigDomainDisabled(ctl *ctl, d dns.Domain, disabled bool) {
795 ctl.xwrite("domaindisabled")
805func cmdConfigAliasList(c *cmd) {
807 c.help = `Show aliases (lists) for domain.`
814 ctlcmdConfigAliasList(xctl(), args[0])
817func ctlcmdConfigAliasList(ctl *ctl, address string) {
818 ctl.xwrite("aliaslist")
821 ctl.xstreamto(os.Stdout)
824func cmdConfigAliasPrint(c *cmd) {
826 c.help = `Print settings and members of alias (list).`
833 ctlcmdConfigAliasPrint(xctl(), args[0])
836func ctlcmdConfigAliasPrint(ctl *ctl, address string) {
837 ctl.xwrite("aliasprint")
840 ctl.xstreamto(os.Stdout)
843func cmdConfigAliasAdd(c *cmd) {
844 c.params = "alias@domain rcpt1@domain ..."
845 c.help = `Add new alias (list) with one or more addresses and public posting enabled.
847An alias is used for delivering incoming email to multiple recipients. If you
848want to add an address to an account, don't use an alias, just add the address
856 alias := config.Alias{PostPublic: true, Addresses: args[1:]}
859 ctlcmdConfigAliasAdd(xctl(), args[0], alias)
862func ctlcmdConfigAliasAdd(ctl *ctl, address string, alias config.Alias) {
863 ctl.xwrite("aliasadd")
865 xctlwriteJSON(ctl, alias)
869func cmdConfigAliasUpdate(c *cmd) {
870 c.params = "alias@domain [-postpublic false|true -listmembers false|true -allowmsgfrom false|true]"
871 c.help = `Update alias (list) configuration.`
872 var postpublic, listmembers, allowmsgfrom string
873 c.flag.StringVar(&postpublic, "postpublic", "", "whether anyone or only list members can post")
874 c.flag.StringVar(&listmembers, "listmembers", "", "whether list members can list members")
875 c.flag.StringVar(&allowmsgfrom, "allowmsgfrom", "", "whether alias address can be used in message from header")
883 ctlcmdConfigAliasUpdate(xctl(), alias, postpublic, listmembers, allowmsgfrom)
886func ctlcmdConfigAliasUpdate(ctl *ctl, alias, postpublic, listmembers, allowmsgfrom string) {
887 ctl.xwrite("aliasupdate")
889 ctl.xwrite(postpublic)
890 ctl.xwrite(listmembers)
891 ctl.xwrite(allowmsgfrom)
895func cmdConfigAliasRemove(c *cmd) {
896 c.params = "alias@domain"
897 c.help = "Remove alias (list)."
904 ctlcmdConfigAliasRemove(xctl(), args[0])
907func ctlcmdConfigAliasRemove(ctl *ctl, alias string) {
908 ctl.xwrite("aliasrm")
913func cmdConfigAliasAddaddr(c *cmd) {
914 c.params = "alias@domain rcpt1@domain ..."
915 c.help = `Add addresses to alias (list).`
922 ctlcmdConfigAliasAddaddr(xctl(), args[0], args[1:])
925func ctlcmdConfigAliasAddaddr(ctl *ctl, alias string, addresses []string) {
926 ctl.xwrite("aliasaddaddr")
928 xctlwriteJSON(ctl, addresses)
932func cmdConfigAliasRemoveaddr(c *cmd) {
933 c.params = "alias@domain rcpt1@domain ..."
934 c.help = `Remove addresses from alias (list).`
941 ctlcmdConfigAliasRmaddr(xctl(), args[0], args[1:])
944func ctlcmdConfigAliasRmaddr(ctl *ctl, alias string, addresses []string) {
945 ctl.xwrite("aliasrmaddr")
947 xctlwriteJSON(ctl, addresses)
951func cmdConfigAccountAdd(c *cmd) {
952 c.params = "account address"
953 c.help = `Add an account with an email address and reload the configuration.
955Email can be delivered to this address/account. A password has to be configured
956explicitly, see the setaccountpassword command.
964 ctlcmdConfigAccountAdd(xctl(), args[0], args[1])
967func ctlcmdConfigAccountAdd(ctl *ctl, account, address string) {
968 ctl.xwrite("accountadd")
972 fmt.Printf("account added, set a password with \"mox setaccountpassword %s\"\n", account)
975func cmdConfigAccountRemove(c *cmd) {
977 c.help = `Remove an account and reload the configuration.
979Email addresses for this account will also be removed, and incoming email for
980these addresses will be rejected.
982All data for the account will be removed.
990 ctlcmdConfigAccountRemove(xctl(), args[0])
993func ctlcmdConfigAccountRemove(ctl *ctl, account string) {
994 ctl.xwrite("accountrm")
997 fmt.Println("account removed")
1000func cmdConfigAccountList(c *cmd) {
1001 c.help = `List all accounts.
1003Each account is printed on a line, with optional additional tab-separated
1004information, such as "(disabled)".
1012 ctlcmdConfigAccountList(xctl())
1015func ctlcmdConfigAccountList(ctl *ctl) {
1016 ctl.xwrite("accountlist")
1018 ctl.xstreamto(os.Stdout)
1021func cmdConfigAccountDisable(c *cmd) {
1022 c.params = "account message"
1023 c.help = `Disable login for an account, showing message to users when they try to login.
1025Incoming email will still be accepted for the account, and queued email from the
1026account will still be delivered. No new login sessions are possible.
1028Message must be non-empty, ascii-only without control characters including
1029newline, and maximum 256 characters because it is used in SMTP/IMAP.
1036 log.Fatalf("message must be non-empty")
1040 ctlcmdConfigAccountDisabled(xctl(), args[0], args[1])
1041 fmt.Println("account disabled")
1044func cmdConfigAccountEnable(c *cmd) {
1045 c.params = "account"
1046 c.help = `Enable login again for an account.
1048Login attempts by the user no long result in an error message.
1056 ctlcmdConfigAccountDisabled(xctl(), args[0], "")
1057 fmt.Println("account enabled")
1060func ctlcmdConfigAccountDisabled(ctl *ctl, account, loginDisabled string) {
1061 ctl.xwrite("accountdisabled")
1063 ctl.xwrite(loginDisabled)
1067func cmdConfigTlspubkeyList(c *cmd) {
1068 c.params = "[account]"
1069 c.help = `List TLS public keys for TLS client certificate authentication.
1071If account is absent, the TLS public keys for all accounts are listed.
1074 var accountOpt string
1076 accountOpt = args[0]
1077 } else if len(args) > 1 {
1082 ctlcmdConfigTlspubkeyList(xctl(), accountOpt)
1085func ctlcmdConfigTlspubkeyList(ctl *ctl, accountOpt string) {
1086 ctl.xwrite("tlspubkeylist")
1087 ctl.xwrite(accountOpt)
1089 ctl.xstreamto(os.Stdout)
1092func cmdConfigTlspubkeyGet(c *cmd) {
1093 c.params = "fingerprint"
1094 c.help = `Get a TLS public key for a fingerprint.
1096Prints the type, name, account and address for the key, and the certificate in
1105 ctlcmdConfigTlspubkeyGet(xctl(), args[0])
1108func ctlcmdConfigTlspubkeyGet(ctl *ctl, fingerprint string) {
1109 ctl.xwrite("tlspubkeyget")
1110 ctl.xwrite(fingerprint)
1114 account := ctl.xread()
1115 address := ctl.xread()
1116 noimappreauth := ctl.xread()
1120 var block *pem.Block
1123 Type: "CERTIFICATE",
1128 fmt.Printf("type: %s\nname: %s\naccount: %s\naddress: %s\nno imap preauth: %s\n", typ, name, account, address, noimappreauth)
1130 fmt.Printf("certificate:\n\n")
1131 if err := pem.Encode(os.Stdout, block); err != nil {
1132 log.Fatalf("pem encode: %v", err)
1137func cmdConfigTlspubkeyAdd(c *cmd) {
1138 c.params = "address [name] < cert.pem"
1139 c.help = `Add a TLS public key to the account of the given address.
1141The public key is read from the certificate.
1143The optional name is a human-readable descriptive name of the key. If absent,
1144the CommonName from the certificate is used.
1146 var noimappreauth bool
1147 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.")
1149 var address, name string
1152 } else if len(args) == 2 {
1153 address, name = args[0], args[1]
1158 buf, err := io.ReadAll(os.Stdin)
1159 xcheckf(err, "reading from stdin")
1160 block, _ := pem.Decode(buf)
1162 err = errors.New("no pem block found")
1163 } else if block.Type != "CERTIFICATE" {
1164 err = fmt.Errorf("unexpected type %q, expected CERTIFICATE", block.Type)
1166 xcheckf(err, "parsing pem")
1169 ctlcmdConfigTlspubkeyAdd(xctl(), address, name, noimappreauth, block.Bytes)
1172func ctlcmdConfigTlspubkeyAdd(ctl *ctl, address, name string, noimappreauth bool, certDER []byte) {
1173 ctl.xwrite("tlspubkeyadd")
1176 ctl.xwrite(fmt.Sprintf("%v", noimappreauth))
1177 ctl.xstreamfrom(bytes.NewReader(certDER))
1181func cmdConfigTlspubkeyRemove(c *cmd) {
1182 c.params = "fingerprint"
1183 c.help = `Remove TLS public key for fingerprint.`
1190 ctlcmdConfigTlspubkeyRemove(xctl(), args[0])
1193func ctlcmdConfigTlspubkeyRemove(ctl *ctl, fingerprint string) {
1194 ctl.xwrite("tlspubkeyrm")
1195 ctl.xwrite(fingerprint)
1199func cmdConfigTlspubkeyGen(c *cmd) {
1201 c.help = `Generate an ed25519 private key and minimal certificate for use a TLS public key and write to files starting with stem.
1203The private key is written to $stem.$timestamp.ed25519privatekey.pkcs8.pem.
1204The certificate is written to $stem.$timestamp.certificate.pem.
1205The private key and certificate are also written to
1206$stem.$timestamp.ed25519privatekey-certificate.pem.
1208The certificate can be added to an account with "mox config account tlspubkey add".
1210The combined file can be used with "mox sendmail".
1212The private key is also written to standard error in raw-url-base64-encoded
1213form, also for use with "mox sendmail". The fingerprint is written to standard
1214error too, for reference.
1222 timestamp := time.Now().Format("200601021504")
1223 prefix := stem + "." + timestamp
1225 seed := make([]byte, ed25519.SeedSize)
1226 if _, err := cryptorand.Read(seed); err != nil {
1229 privKey := ed25519.NewKeyFromSeed(seed)
1230 privKeyBuf, err := x509.MarshalPKCS8PrivateKey(privKey)
1231 xcheckf(err, "marshal private key as pkcs8")
1233 err = pem.Encode(&b, &pem.Block{Type: "PRIVATE KEY", Bytes: privKeyBuf})
1234 xcheckf(err, "marshal pkcs8 private key to pem")
1235 privKeyBufPEM := b.Bytes()
1237 certBuf, tlsCert := xminimalCert(privKey)
1239 err = pem.Encode(&b, &pem.Block{Type: "CERTIFICATE", Bytes: certBuf})
1240 xcheckf(err, "marshal certificate to pem")
1241 certBufPEM := b.Bytes()
1243 xwriteFile := func(p string, data []byte, what string) {
1244 log.Printf("writing %s", p)
1245 err = os.WriteFile(p, data, 0600)
1246 xcheckf(err, "writing %s file: %v", what, err)
1249 xwriteFile(prefix+".ed25519privatekey.pkcs8.pem", privKeyBufPEM, "private key")
1250 xwriteFile(prefix+".certificate.pem", certBufPEM, "certificate")
1251 combinedPEM := slices.Concat(privKeyBufPEM, certBufPEM)
1252 xwriteFile(prefix+".ed25519privatekey-certificate.pem", combinedPEM, "combined private key and certificate")
1254 shabuf := sha256.Sum256(tlsCert.Leaf.RawSubjectPublicKeyInfo)
1256 _, err = fmt.Fprintf(os.Stderr, "ed25519 private key as raw-url-base64: %s\ned25519 public key fingerprint: %s\n",
1257 base64.RawURLEncoding.EncodeToString(seed),
1258 base64.RawURLEncoding.EncodeToString(shabuf[:]),
1260 xcheckf(err, "write private key and public key fingerprint")
1263func cmdConfigAddressAdd(c *cmd) {
1264 c.params = "address account"
1265 c.help = `Adds an address to an account and reloads the configuration.
1267If address starts with a @ (i.e. a missing localpart), this is a catchall
1268address for the domain.
1276 ctlcmdConfigAddressAdd(xctl(), args[0], args[1])
1279func ctlcmdConfigAddressAdd(ctl *ctl, address, account string) {
1280 ctl.xwrite("addressadd")
1284 fmt.Println("address added")
1287func cmdConfigAddressRemove(c *cmd) {
1288 c.params = "address"
1289 c.help = `Remove an address and reload the configuration.
1291Incoming email for this address will be rejected after removing an address.
1299 ctlcmdConfigAddressRemove(xctl(), args[0])
1302func ctlcmdConfigAddressRemove(ctl *ctl, address string) {
1303 ctl.xwrite("addressrm")
1306 fmt.Println("address removed")
1309func cmdConfigDNSRecords(c *cmd) {
1311 c.help = `Prints annotated DNS records as zone file that should be created for the domain.
1313The zone file can be imported into existing DNS software. You should review the
1314DNS records, especially if your domain previously/currently has email
1322 d := xparseDomain(args[0], "domain")
1324 domConf, ok := mox.Conf.Domain(d)
1326 log.Fatalf("unknown domain")
1329 resolver := dns.StrictResolver{Pkg: "main"}
1330 _, result, err := resolver.LookupTXT(context.Background(), d.ASCII+".")
1331 if !dns.IsNotFound(err) {
1332 xcheckf(err, "looking up record for dnssec-status")
1335 var certIssuerDomainName, acmeAccountURI string
1336 public := mox.Conf.Static.Listeners["public"]
1337 if public.TLS != nil && public.TLS.ACME != "" {
1338 acme, ok := mox.Conf.Static.ACME[public.TLS.ACME]
1339 if ok && acme.Manager.Manager.Client != nil {
1340 certIssuerDomainName = acme.IssuerDomainName
1341 acc, err := acme.Manager.Manager.Client.GetReg(context.Background(), "")
1342 c.log.Check(err, "get public acme account")
1344 acmeAccountURI = acc.URI
1349 records, err := admin.DomainRecords(domConf, d, result.Authentic, certIssuerDomainName, acmeAccountURI)
1350 xcheckf(err, "records")
1351 fmt.Print(strings.Join(records, "\n") + "\n")
1354func cmdConfigDNSCheck(c *cmd) {
1356 c.help = "Check the DNS records with the configuration for the domain, and print any errors/warnings."
1362 d := xparseDomain(args[0], "domain")
1364 _, ok := mox.Conf.Domain(d)
1366 log.Fatalf("unknown domain")
1369 // todo future: move http.Admin.CheckDomain to mox- and make it return a regular error.
1375 err, ok := x.(*sherpa.Error)
1379 log.Fatalf("%s", err)
1382 printResult := func(name string, r webadmin.Result) {
1383 if len(r.Errors) == 0 && len(r.Warnings) == 0 {
1386 fmt.Printf("# %s\n", name)
1387 for _, s := range r.Errors {
1388 fmt.Printf("error: %s\n", s)
1390 for _, s := range r.Warnings {
1391 fmt.Printf("warning: %s\n", s)
1395 result := webadmin.Admin{}.CheckDomain(context.Background(), args[0])
1396 printResult("DNSSEC", result.DNSSEC.Result)
1397 printResult("IPRev", result.IPRev.Result)
1398 printResult("MX", result.MX.Result)
1399 printResult("TLS", result.TLS.Result)
1400 printResult("DANE", result.DANE.Result)
1401 printResult("SPF", result.SPF.Result)
1402 printResult("DKIM", result.DKIM.Result)
1403 printResult("DMARC", result.DMARC.Result)
1404 printResult("Host TLSRPT", result.HostTLSRPT.Result)
1405 printResult("Domain TLSRPT", result.DomainTLSRPT.Result)
1406 printResult("MTASTS", result.MTASTS.Result)
1407 printResult("SRV conf", result.SRVConf.Result)
1408 printResult("Autoconf", result.Autoconf.Result)
1409 printResult("Autodiscover", result.Autodiscover.Result)
1412func cmdConfigEnsureACMEHostprivatekeys(c *cmd) {
1414 c.help = `Ensure host private keys exist for TLS listeners with ACME.
1416In mox.conf, each listener can have TLS configured. Long-lived private key files
1417can be specified, which will be used when requesting ACME certificates.
1418Configuring these private keys makes it feasible to publish DANE TLSA records
1419for the corresponding public keys in DNS, protected with DNSSEC, allowing TLS
1420certificate verification without depending on a list of Certificate Authorities
1421(CAs). Previous versions of mox did not pre-generate private keys for use with
1422ACME certificates, but would generate private keys on-demand. By explicitly
1423configuring private keys, they will not change automatedly with new
1424certificates, and the DNS TLSA records stay valid.
1426This command looks for listeners in mox.conf with TLS with ACME configured. For
1427each missing host private key (of type rsa-2048 and ecdsa-p256) a key is written
1428to config/hostkeys/. If a certificate exists in the ACME "cache", its private
1429key is copied. Otherwise a new private key is generated. Snippets for manually
1430updating/editing mox.conf are printed.
1432After running this command, and updating mox.conf, run "mox config dnsrecords"
1433for a domain and create the TLSA DNS records it suggests to enable DANE.
1440 // Load a private key from p, in various forms. We only look at the first PEM
1441 // block. Files with only a private key, or with multiple blocks but private key
1442 // first like autocert does, can be loaded.
1443 loadPrivateKey := func(f *os.File) (any, error) {
1444 buf, err := io.ReadAll(f)
1446 return nil, fmt.Errorf("reading private key file: %v", err)
1448 block, _ := pem.Decode(buf)
1450 return nil, fmt.Errorf("no pem block found in pem file")
1454 case "EC PRIVATE KEY":
1455 privKey, err = x509.ParseECPrivateKey(block.Bytes)
1456 case "RSA PRIVATE KEY":
1457 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
1459 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
1461 return nil, fmt.Errorf("unrecognized pem block type %q", block.Type)
1464 return nil, fmt.Errorf("parsing private key of type %q: %v", block.Type, err)
1469 // Either load a private key from file, or if it doesn't exist generate a new
1471 xtryLoadPrivateKey := func(kt autocert.KeyType, p string) any {
1472 f, err := os.Open(p)
1473 if err != nil && errors.Is(err, fs.ErrNotExist) {
1475 case autocert.KeyRSA2048:
1476 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1477 xcheckf(err, "generating new 2048-bit rsa private key")
1479 case autocert.KeyECDSAP256:
1480 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1481 xcheckf(err, "generating new ecdsa p-256 private key")
1484 log.Fatalf("unexpected keytype %v", kt)
1487 xcheckf(err, "%s: open acme key and certificate file", p)
1489 // Load private key from file. autocert stores a PEM file that starts with a
1490 // private key, followed by certificate(s). So we can just read it and should find
1491 // the private key we are looking for.
1492 privKey, err := loadPrivateKey(f)
1493 if xerr := f.Close(); xerr != nil {
1494 log.Printf("closing private key file: %v", xerr)
1496 xcheckf(err, "parsing private key from acme key and certificate file")
1498 switch k := privKey.(type) {
1499 case *rsa.PrivateKey:
1500 if k.N.BitLen() == 2048 {
1503 log.Printf("warning: rsa private key in %s has %d bits, skipping and generating new 2048-bit rsa private key", p, k.N.BitLen())
1504 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1505 xcheckf(err, "generating new 2048-bit rsa private key")
1507 case *ecdsa.PrivateKey:
1508 if k.Curve == elliptic.P256() {
1511 log.Printf("warning: ecdsa private key in %s has curve %v, skipping and generating new p-256 ecdsa key", p, k.Curve.Params().Name)
1512 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1513 xcheckf(err, "generating new ecdsa p-256 private key")
1516 log.Fatalf("%s: unexpected private key file of type %T", p, privKey)
1521 // Write privKey as PKCS#8 private key to p. Only if file does not yet exist.
1522 writeHostPrivateKey := func(privKey any, p string) error {
1523 os.MkdirAll(filepath.Dir(p), 0700)
1524 f, err := os.OpenFile(p, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
1526 return fmt.Errorf("create: %v", err)
1530 if err := f.Close(); err != nil {
1531 log.Printf("closing new hostkey file %s after error: %v", p, err)
1533 if err := os.Remove(p); err != nil {
1534 log.Printf("removing new hostkey file %s after error: %v", p, err)
1538 buf, err := x509.MarshalPKCS8PrivateKey(privKey)
1540 return fmt.Errorf("marshal private host key: %v", err)
1543 Type: "PRIVATE KEY",
1546 if err := pem.Encode(f, &block); err != nil {
1547 return fmt.Errorf("write as pem: %v", err)
1549 if err := f.Close(); err != nil {
1550 return fmt.Errorf("close: %v", err)
1557 timestamp := time.Now().Format("20060102T150405")
1559 for listenerName, l := range mox.Conf.Static.Listeners {
1560 if l.TLS == nil || l.TLS.ACME == "" {
1563 haveKeyTypes := map[autocert.KeyType]bool{}
1564 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
1565 p := mox.ConfigDirPath(privKeyFile)
1566 f, err := os.Open(p)
1567 xcheckf(err, "open host private key")
1568 privKey, err := loadPrivateKey(f)
1569 if err := f.Close(); err != nil {
1570 log.Printf("closing host private key file: %v", err)
1572 xcheckf(err, "loading host private key")
1573 switch k := privKey.(type) {
1574 case *rsa.PrivateKey:
1575 if k.N.BitLen() == 2048 {
1576 haveKeyTypes[autocert.KeyRSA2048] = true
1578 case *ecdsa.PrivateKey:
1579 if k.Curve == elliptic.P256() {
1580 haveKeyTypes[autocert.KeyECDSAP256] = true
1584 created := []string{}
1585 for _, kt := range []autocert.KeyType{autocert.KeyRSA2048, autocert.KeyECDSAP256} {
1586 if haveKeyTypes[kt] {
1589 // Lookup key in ACME cache.
1590 host := l.HostnameDomain
1591 if host.ASCII == "" {
1592 host = mox.Conf.Static.HostnameDomain
1594 filename := host.ASCII
1596 if kt == autocert.KeyRSA2048 {
1600 p := mox.DataDirPath(filepath.Join("acme", "keycerts", l.TLS.ACME, filename))
1601 privKey := xtryLoadPrivateKey(kt, p)
1603 relPath := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", host.Name(), timestamp, kind))
1604 destPath := mox.ConfigDirPath(relPath)
1605 err := writeHostPrivateKey(privKey, destPath)
1606 xcheckf(err, "writing host private key file to %s: %v", destPath, err)
1607 created = append(created, relPath)
1608 fmt.Printf("Wrote host private key: %s\n", destPath)
1610 didCreate = didCreate || len(created) > 0
1611 if len(created) > 0 {
1613 HostPrivateKeyFiles: append(l.TLS.HostPrivateKeyFiles, created...),
1615 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)
1616 err := sconf.Write(os.Stdout, tls)
1617 xcheckf(err, "writing new TLS.HostPrivateKeyFiles section")
1623After updating mox.conf and restarting, run "mox config dnsrecords" for a
1624domain and create the TLSA DNS records it suggests to enable DANE.
1629func cmdLoglevels(c *cmd) {
1630 c.params = "[level [pkg]]"
1631 c.help = `Print the log levels, or set a new default log level, or a level for the given package.
1633By default, a single log level applies to all logging in mox. But for each
1634"pkg", an overriding log level can be configured. Examples of packages:
1635smtpserver, smtpclient, queue, imapserver, spf, dkim, dmarc, junk, message,
1638Specify a pkg and an empty level to clear the configured level for a package.
1640Valid labels: error, info, debug, trace, traceauth, tracedata.
1649 ctlcmdLoglevels(xctl())
1655 ctlcmdSetLoglevels(xctl(), pkg, args[0])
1659func ctlcmdLoglevels(ctl *ctl) {
1660 ctl.xwrite("loglevels")
1662 ctl.xstreamto(os.Stdout)
1665func ctlcmdSetLoglevels(ctl *ctl, pkg, level string) {
1666 ctl.xwrite("setloglevels")
1672func cmdStop(c *cmd) {
1673 c.help = `Shut mox down, giving connections maximum 3 seconds to stop before closing them.
1675While shutting down, new IMAP and SMTP connections will get a status response
1676indicating temporary unavailability. Existing connections will get a 3 second
1677period to finish their transaction and shut down. Under normal circumstances,
1678only IMAP has long-living connections, with the IDLE command to get notified of
1681 if len(c.Parse()) != 0 {
1688 // Read will hang until remote has shut down.
1689 buf := make([]byte, 128)
1690 n, err := xctl.conn.Read(buf)
1692 log.Fatalf("expected eof after graceful shutdown, got data %q", buf[:n])
1693 } else if err != io.EOF {
1694 log.Fatalf("expected eof after graceful shutdown, got error %v", err)
1696 fmt.Println("mox stopped")
1699func cmdBackup(c *cmd) {
1700 c.params = "destdir"
1701 c.help = `Creates a backup of the config and data directory.
1703Backup copies the config directory to <destdir>/config, and creates
1704<destdir>/data with a consistent snapshot of the databases and message files
1705and copies other files from the data directory. Empty directories are not
1706copied. The backup can then be stored elsewhere for long-term storage, or used
1707to fall back to should an upgrade fail. Simply copying files in the data
1708directory while mox is running can result in unusable database files.
1710Message files never change (they are read-only, though can be removed) and are
1711hard-linked so they don't consume additional space. If hardlinking fails, for
1712example when the backup destination directory is on a different file system, a
1713regular copy is made. Using a destination directory like "data/tmp/backup"
1714increases the odds hardlinking succeeds: the default systemd service file
1715specifically mounts the data directory, causing attempts to hardlink outside it
1716to fail with an error about cross-device linking.
1718All files in the data directory that aren't recognized (i.e. other than known
1719database files, message files, an acme directory, the "tmp" directory, etc),
1720are stored, but with a warning.
1722Remove files in the destination directory before doing another backup. The
1723backup command will not overwrite files, but print and return errors.
1725Exit code 0 indicates the backup was successful. A clean successful backup does
1726not print any output, but may print warnings. Use the -verbose flag for
1727details, including timing.
1729To restore a backup, first shut down mox, move away the old data directory and
1730move an earlier backed up directory in its place, run "mox verifydata
1731<datadir>", possibly with the "-fix" option, and restart mox. After the
1732restore, you may also want to run "mox bumpuidvalidity" for each account for
1733which messages in a mailbox changed, to force IMAP clients to synchronize
1736Before upgrading, to check if the upgrade will likely succeed, first make a
1737backup, then use the new mox binary to run "mox verifydata <backupdir>/data".
1738This can change the backup files (e.g. upgrade database files, move away
1739unrecognized message files), so you should make a new backup before actually
1744 c.flag.BoolVar(&verbose, "verbose", false, "print progress")
1751 dstDataDir, err := filepath.Abs(args[0])
1752 xcheckf(err, "making path absolute")
1754 ctlcmdBackup(xctl(), dstDataDir, verbose)
1757func ctlcmdBackup(ctl *ctl, dstDataDir string, verbose bool) {
1758 ctl.xwrite("backup")
1759 ctl.xwrite(dstDataDir)
1761 ctl.xwrite("verbose")
1765 ctl.xstreamto(os.Stdout)
1769func cmdSetadminpassword(c *cmd) {
1770 c.help = `Set a new admin password, for the web interface.
1772The password is read from stdin. Its bcrypt hash is stored in a file named
1773"adminpasswd" in the configuration directory.
1775 if len(c.Parse()) != 0 {
1780 path := mox.ConfigDirPath(mox.Conf.Static.AdminPasswordFile)
1782 log.Fatal("no admin password file configured")
1785 pw := xreadpassword()
1786 pw, err := precis.OpaqueString.String(pw)
1787 xcheckf(err, `checking password with "precis" requirements`)
1788 hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
1789 xcheckf(err, "generating hash for password")
1790 err = os.WriteFile(path, hash, 0660)
1791 xcheckf(err, "writing hash to admin password file")
1794func xreadpassword() string {
1796Type new password. Password WILL echo.
1798WARNING: Bots will try to bruteforce your password. Connections with failed
1799authentication attempts will be rate limited but attackers WILL find passwords
1800reused at other services and weak passwords. If your account is compromised,
1801spammers are likely to abuse your system, spamming your address and the wider
1802internet in your name. So please pick a random, unguessable password, preferably
1803at least 12 characters.
1806 fmt.Printf("password: ")
1807 scanner := bufio.NewScanner(os.Stdin)
1808 // The default splitter for scanners is one that splits by lines, so we
1809 // don't have to set up another one here.
1811 // We discard the return value of Scan() since failing to tokenize could
1812 // either mean reaching EOF but no newline (which can be legitimate if the
1813 // CLI was programatically called to set the password, but with no trailing
1814 // newline), or an actual error. We can distinguish between the two by
1815 // calling Err() since it will return nil if it were EOF, but the actual
1818 xcheckf(scanner.Err(), "reading stdin")
1819 // No need to trim, the scanner does not return the token in the output.
1820 pw := scanner.Text()
1822 log.Fatal("password must be at least 8 characters")
1827func cmdSetaccountpassword(c *cmd) {
1828 c.params = "account"
1829 c.help = `Set new password an account.
1831The password is read from stdin. Secrets derived from the password, but not the
1832password itself, are stored in the account database. The stored secrets are for
1833authentication with: scram-sha-256, scram-sha-1, cram-md5, plain text (bcrypt
1836The parameter is an account name, as configured under Accounts in domains.conf
1837and as present in the data/accounts/ directory, not a configured email address
1846 pw := xreadpassword()
1848 ctlcmdSetaccountpassword(xctl(), args[0], pw)
1851func ctlcmdSetaccountpassword(ctl *ctl, account, password string) {
1852 ctl.xwrite("setaccountpassword")
1854 ctl.xwrite(password)
1858func cmdDeliver(c *cmd) {
1860 c.params = "address < message"
1861 c.help = "Deliver message to address."
1867 ctlcmdDeliver(xctl(), args[0])
1870func ctlcmdDeliver(ctl *ctl, address string) {
1871 ctl.xwrite("deliver")
1874 ctl.xstreamfrom(os.Stdin)
1877 fmt.Println("message delivered")
1879 log.Fatalf("deliver: %s", line)
1883func cmdDKIMGenrsa(c *cmd) {
1884 c.params = ">$selector._domainkey.$domain.rsa2048.privatekey.pkcs8.pem"
1885 c.help = `Generate a new 2048 bit RSA private key for use with DKIM.
1887The generated file is in PEM format, and has a comment it is generated for use
1890 if len(c.Parse()) != 0 {
1894 buf, err := admin.MakeDKIMRSAKey(dns.Domain{}, dns.Domain{})
1895 xcheckf(err, "making rsa private key")
1896 _, err = os.Stdout.Write(buf)
1897 xcheckf(err, "writing rsa private key")
1900func cmdDANEDial(c *cmd) {
1901 c.params = "host:port"
1903 c.flag.StringVar(&usages, "usages", "pkix-ta,pkix-ee,dane-ta,dane-ee", "allowed usages for dane, comma-separated list")
1904 c.help = `Dial the address using TLS with certificate verification using DANE.
1906Data is copied between connection and stdin/stdout until either side closes the
1914 allowedUsages := []adns.TLSAUsage{}
1916 for _, s := range strings.Split(usages, ",") {
1917 var usage adns.TLSAUsage
1918 switch strings.ToLower(s) {
1919 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
1920 usage = adns.TLSAUsagePKIXTA
1921 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
1922 usage = adns.TLSAUsagePKIXEE
1923 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
1924 usage = adns.TLSAUsageDANETA
1925 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
1926 usage = adns.TLSAUsageDANEEE
1928 log.Fatalf("unknown dane usage %q", s)
1930 allowedUsages = append(allowedUsages, usage)
1934 pkixRoots, err := x509.SystemCertPool()
1935 xcheckf(err, "get system pkix certificate pool")
1937 resolver := dns.StrictResolver{Pkg: "danedial"}
1938 conn, record, err := dane.Dial(context.Background(), c.log.Logger, resolver, "tcp", args[0], allowedUsages, pkixRoots)
1939 xcheckf(err, "dial")
1940 log.Printf("(connected, verified with %s)", record)
1943 _, err := io.Copy(os.Stdout, conn)
1944 xcheckf(err, "copy from connection to stdout")
1946 c.log.Check(err, "closing connection")
1948 _, err = io.Copy(conn, os.Stdin)
1949 xcheckf(err, "copy from stdin to connection")
1952func cmdDANEDialmx(c *cmd) {
1953 c.params = "domain [destination-host]"
1954 var ehloHostname string
1955 c.flag.StringVar(&ehloHostname, "ehlohostname", "localhost", "hostname to send in smtp ehlo command")
1956 c.help = `Connect to MX server for domain using STARTTLS verified with DANE.
1958If no destination host is specified, regular delivery logic is used to find the
1959hosts to attempt delivery too. This involves following CNAMEs for the domain,
1960looking up MX records, and possibly falling back to the domain name itself as
1963If a destination host is specified, that is the only candidate host considered
1966With a list of destinations gathered, each is dialed until a successful SMTP
1967session verified with DANE has been initialized, including EHLO and STARTTLS
1970Once connected, data is copied between connection and stdin/stdout, until
1971either side closes the connection.
1973This command follows the same logic as delivery attempts made from the queue,
1974sharing most of its code.
1977 if len(args) != 1 && len(args) != 2 {
1981 ehloDomain := xparseDomain(ehloHostname, "ehlo host name")
1982 origNextHop := xparseDomain(args[0], "domain")
1984 ctxbg := context.Background()
1986 resolver := dns.StrictResolver{}
1988 var expandedNextHopAuthentic bool
1989 var expandedNextHop dns.Domain
1990 var hosts []dns.IPDomain
1993 var origNextHopAuthentic bool
1995 haveMX, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hosts, permanent, err = smtpclient.GatherDestinations(ctxbg, c.log.Logger, resolver, dns.IPDomain{Domain: origNextHop})
1996 status := "temporary"
1998 status = "permanent"
2001 log.Fatalf("gathering destinations: %v (%s)", err, status)
2003 if expandedNextHop != origNextHop {
2004 log.Printf("followed cnames to %s", expandedNextHop)
2007 log.Printf("found mx record, trying mx hosts")
2009 log.Printf("no mx record found, will try to connect to domain directly")
2011 if !origNextHopAuthentic {
2012 log.Fatalf("error: initial domain not dnssec-secure")
2014 if !expandedNextHopAuthentic {
2015 log.Fatalf("error: expanded domain not dnssec-secure")
2019 for _, h := range hosts {
2020 l = append(l, h.String())
2022 log.Printf("destinations: %s", strings.Join(l, ", "))
2024 d := xparseDomain(args[1], "destination host")
2025 log.Printf("skipping domain mx/cname lookups, assuming domain is dnssec-protected")
2027 expandedNextHopAuthentic = true
2029 hosts = []dns.IPDomain{{Domain: d}}
2032 dialedIPs := map[string][]net.IP{}
2033 for _, host := range hosts {
2034 // It should not be possible for hosts to have IP addresses: They are not
2035 // allowed by dns.ParseDomain, and MX records cannot contain them.
2037 log.Fatalf("unexpected IP address for destination host")
2040 log.Printf("attempting to connect to %s", host)
2042 authentic, expandedAuthentic, expandedHost, ips, _, err := smtpclient.GatherIPs(ctxbg, c.log.Logger, resolver, "ip", host, dialedIPs)
2044 log.Printf("resolving ips for %s: %v, skipping", host, err)
2048 log.Printf("no dnssec for ips of %s, skipping", host)
2051 if !expandedAuthentic {
2052 log.Printf("no dnssec for cname-followed ips of %s, skipping", host)
2055 if expandedHost != host.Domain {
2056 log.Printf("host %s cname-expanded to %s", host, expandedHost)
2058 log.Printf("host %s resolved to ips %s, looking up tlsa records", host, ips)
2060 daneRequired, daneRecords, tlsaBaseDomain, err := smtpclient.GatherTLSA(ctxbg, c.log.Logger, resolver, host.Domain, expandedAuthentic, expandedHost)
2062 log.Printf("looking up tlsa records: %s, skipping", err)
2065 tlsMode := smtpclient.TLSRequiredStartTLS
2066 if len(daneRecords) == 0 {
2068 log.Printf("host %s has no tlsa records, skipping", expandedHost)
2071 log.Printf("warning: only unusable tlsa records found, continuing with required tls without certificate verification")
2075 for _, r := range daneRecords {
2076 l = append(l, r.String())
2078 log.Printf("tlsa records: %s", strings.Join(l, "; "))
2081 tlsHostnames := smtpclient.GatherTLSANames(haveMX, expandedNextHopAuthentic, expandedAuthentic, origNextHop, expandedNextHop, host.Domain, tlsaBaseDomain)
2083 for _, name := range tlsHostnames {
2084 l = append(l, name.String())
2086 log.Printf("gathered valid tls certificate names for potential verification with dane-ta: %s", strings.Join(l, ", "))
2088 dialer := &net.Dialer{Timeout: 5 * time.Second}
2089 conn, _, err := smtpclient.Dial(ctxbg, c.log.Logger, dialer, dns.IPDomain{Domain: expandedHost}, ips, 25, dialedIPs, nil)
2091 log.Printf("dial %s: %v, skipping", expandedHost, err)
2094 log.Printf("connected to %s, %s, starting smtp session with ehlo and starttls with dane verification", expandedHost, conn.RemoteAddr())
2096 var verifiedRecord adns.TLSA
2097 opts := smtpclient.Opts{
2098 DANERecords: daneRecords,
2099 DANEMoreHostnames: tlsHostnames[1:],
2100 DANEVerifiedRecord: &verifiedRecord,
2101 RootCAs: mox.Conf.Static.TLS.CertPool,
2104 sc, err := smtpclient.New(ctxbg, c.log.Logger, conn, tlsMode, tlsPKIX, ehloDomain, tlsHostnames[0], opts)
2106 log.Printf("setting up smtp session: %v, skipping", err)
2107 if xerr := conn.Close(); xerr != nil {
2108 log.Printf("closing connection: %v", xerr)
2113 smtpConn, err := sc.Conn()
2115 log.Fatalf("error: taking over smtp connection: %s", err)
2117 log.Printf("tls verified with tlsa record: %s", verifiedRecord)
2118 log.Printf("smtp session initialized and connected to stdin/stdout")
2121 _, err := io.Copy(os.Stdout, smtpConn)
2122 xcheckf(err, "copy from connection to stdout")
2123 if err := smtpConn.Close(); err != nil {
2124 log.Printf("closing smtp connection: %v", err)
2127 _, err = io.Copy(smtpConn, os.Stdin)
2128 xcheckf(err, "copy from stdin to connection")
2131 log.Fatalf("no remaining destinations")
2134func cmdDANEMakeRecord(c *cmd) {
2135 c.params = "usage selector matchtype [certificate.pem | publickey.pem | privatekey.pem]"
2136 c.help = `Print TLSA record for given certificate/key and parameters.
2139- usage: pkix-ta (0), pkix-ee (1), dane-ta (2), dane-ee (3)
2140- selector: cert (0), spki (1)
2141- matchtype: full (0), sha2-256 (1), sha2-512 (2)
2143Common DANE TLSA record parameters are: dane-ee spki sha2-256, or 3 1 1,
2144followed by a sha2-256 hash of the DER-encoded "SPKI" (subject public key info)
2145from the certificate. An example DNS zone file entry:
2147 _25._tcp.example.com. TLSA 3 1 1 133b919c9d65d8b1488157315327334ead8d83372db57465ecabf53ee5748aee
2149The first usable information from the pem file is used to compose the TLSA
2150record. In case of selector "cert", a certificate is required. Otherwise the
2151"subject public key info" (spki) of the first certificate or public or private
2152key (pkcs#8, pkcs#1 or ec private key) is used.
2160 var usage adns.TLSAUsage
2161 switch strings.ToLower(args[0]) {
2162 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
2163 usage = adns.TLSAUsagePKIXTA
2164 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
2165 usage = adns.TLSAUsagePKIXEE
2166 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
2167 usage = adns.TLSAUsageDANETA
2168 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
2169 usage = adns.TLSAUsageDANEEE
2171 if v, err := strconv.ParseUint(args[0], 10, 16); err != nil {
2172 log.Fatalf("bad usage %q", args[0])
2174 // Does not influence certificate association data, so we can accept other numbers.
2175 log.Printf("warning: continuing with unrecognized tlsa usage %d", v)
2176 usage = adns.TLSAUsage(v)
2180 var selector adns.TLSASelector
2181 switch strings.ToLower(args[1]) {
2182 case "cert", strconv.Itoa(int(adns.TLSASelectorCert)):
2183 selector = adns.TLSASelectorCert
2184 case "spki", strconv.Itoa(int(adns.TLSASelectorSPKI)):
2185 selector = adns.TLSASelectorSPKI
2187 log.Fatalf("bad selector %q", args[1])
2190 var matchType adns.TLSAMatchType
2191 switch strings.ToLower(args[2]) {
2192 case "full", strconv.Itoa(int(adns.TLSAMatchTypeFull)):
2193 matchType = adns.TLSAMatchTypeFull
2194 case "sha2-256", strconv.Itoa(int(adns.TLSAMatchTypeSHA256)):
2195 matchType = adns.TLSAMatchTypeSHA256
2196 case "sha2-512", strconv.Itoa(int(adns.TLSAMatchTypeSHA512)):
2197 matchType = adns.TLSAMatchTypeSHA512
2199 log.Fatalf("bad matchtype %q", args[2])
2202 buf, err := os.ReadFile(args[3])
2203 xcheckf(err, "reading certificate")
2205 var block *pem.Block
2206 block, buf = pem.Decode(buf)
2210 extra = " (with leftover data from pem file)"
2212 if selector == adns.TLSASelectorCert {
2213 log.Fatalf("no certificate found in pem file%s", extra)
2215 log.Fatalf("no certificate or public or private key found in pem file%s", extra)
2218 var cert *x509.Certificate
2220 if block.Type == "CERTIFICATE" {
2221 cert, err = x509.ParseCertificate(block.Bytes)
2222 xcheckf(err, "parse certificate")
2224 case adns.TLSASelectorCert:
2226 case adns.TLSASelectorSPKI:
2227 data = cert.RawSubjectPublicKeyInfo
2229 } else if selector == adns.TLSASelectorCert {
2230 // We need a certificate, just a public/private key won't do.
2231 log.Printf("skipping pem type %q, certificate is required", block.Type)
2234 var privKey, pubKey any
2238 _, err := x509.ParsePKIXPublicKey(block.Bytes)
2239 xcheckf(err, "parse pkix subject public key info (spki)")
2241 case "EC PRIVATE KEY":
2242 privKey, err = x509.ParseECPrivateKey(block.Bytes)
2243 xcheckf(err, "parse ec private key")
2244 case "RSA PRIVATE KEY":
2245 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
2246 xcheckf(err, "parse pkcs#1 rsa private key")
2247 case "RSA PUBLIC KEY":
2248 pubKey, err = x509.ParsePKCS1PublicKey(block.Bytes)
2249 xcheckf(err, "parse pkcs#1 rsa public key")
2251 // PKCS#8 private key
2252 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
2253 xcheckf(err, "parse pkcs#8 private key")
2255 log.Printf("skipping unrecognized pem type %q", block.Type)
2259 if pubKey == nil && privKey != nil {
2260 if signer, ok := privKey.(crypto.Signer); !ok {
2261 log.Fatalf("private key of type %T is not a signer, cannot get public key", privKey)
2263 pubKey = signer.Public()
2267 // Should not happen.
2268 log.Fatalf("internal error: did not find private or public key")
2270 data, err = x509.MarshalPKIXPublicKey(pubKey)
2271 xcheckf(err, "marshal pkix subject public key info (spki)")
2276 case adns.TLSAMatchTypeFull:
2277 case adns.TLSAMatchTypeSHA256:
2278 p := sha256.Sum256(data)
2280 case adns.TLSAMatchTypeSHA512:
2281 p := sha512.Sum512(data)
2284 fmt.Printf("%d %d %d %x\n", usage, selector, matchType, data)
2289func cmdDNSLookup(c *cmd) {
2290 c.params = "[ptr | mx | cname | ips | a | aaaa | ns | txt | srv | tlsa] name"
2291 c.help = `Lookup DNS name of given type.
2293Lookup always prints whether the response was DNSSEC-protected.
2297mox dns lookup ptr 1.1.1.1
2298mox dns lookup mx xmox.nl
2299mox dns lookup txt _dmarc.xmox.nl.
2300mox dns lookup tlsa _25._tcp.xmox.nl
2308 resolver := dns.StrictResolver{Pkg: "dns"}
2310 // like xparseDomain, but treat unparseable domain as an ASCII name so names with
2311 // underscores are still looked up, e,g <selector>._domainkey.<host>.
2312 xdomain := func(s string) dns.Domain {
2313 d, err := dns.ParseDomain(s)
2315 return dns.Domain{ASCII: strings.TrimSuffix(s, ".")}
2320 cmd, name := args[0], args[1]
2324 ip := xparseIP(name, "ip")
2325 ptrs, result, err := resolver.LookupAddr(context.Background(), ip.String())
2327 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2329 fmt.Printf("names (%d, %s):\n", len(ptrs), dnssecStatus(result.Authentic))
2330 for _, ptr := range ptrs {
2331 fmt.Printf("- %s\n", ptr)
2335 name := xdomain(name)
2336 mxl, result, err := resolver.LookupMX(context.Background(), name.ASCII+".")
2338 log.Printf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2339 // We can still have valid records...
2341 fmt.Printf("mx records (%d, %s):\n", len(mxl), dnssecStatus(result.Authentic))
2342 for _, mx := range mxl {
2343 fmt.Printf("- %s, preference %d\n", mx.Host, mx.Pref)
2347 name := xdomain(name)
2348 target, result, err := resolver.LookupCNAME(context.Background(), name.ASCII+".")
2350 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2352 fmt.Printf("%s (%s)\n", target, dnssecStatus(result.Authentic))
2354 case "ips", "a", "aaaa":
2358 } else if cmd == "aaaa" {
2361 name := xdomain(name)
2362 ips, result, err := resolver.LookupIP(context.Background(), network, name.ASCII+".")
2364 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2366 fmt.Printf("records (%d, %s):\n", len(ips), dnssecStatus(result.Authentic))
2367 for _, ip := range ips {
2368 fmt.Printf("- %s\n", ip)
2372 name := xdomain(name)
2373 nsl, result, err := resolver.LookupNS(context.Background(), name.ASCII+".")
2375 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2377 fmt.Printf("ns records (%d, %s):\n", len(nsl), dnssecStatus(result.Authentic))
2378 for _, ns := range nsl {
2379 fmt.Printf("- %s\n", ns)
2383 host := xdomain(name)
2384 l, result, err := resolver.LookupTXT(context.Background(), host.ASCII+".")
2386 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2388 fmt.Printf("txt records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2389 for _, txt := range l {
2390 fmt.Printf("- %s\n", txt)
2394 host := xdomain(name)
2395 _, l, result, err := resolver.LookupSRV(context.Background(), "", "", host.ASCII+".")
2397 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2399 fmt.Printf("srv records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2400 for _, srv := range l {
2401 fmt.Printf("- host %s, port %d, priority %d, weight %d\n", srv.Target, srv.Port, srv.Priority, srv.Weight)
2405 host := xdomain(name)
2406 l, result, err := resolver.LookupTLSA(context.Background(), 0, "", host.ASCII+".")
2408 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2410 fmt.Printf("tlsa records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2411 for _, tlsa := range l {
2412 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)
2415 log.Fatalf("unknown record type %q", args[0])
2419func cmdDKIMGened25519(c *cmd) {
2420 c.params = ">$selector._domainkey.$domain.ed25519.privatekey.pkcs8.pem"
2421 c.help = `Generate a new ed25519 key for use with DKIM.
2423Ed25519 keys are much smaller than RSA keys of comparable cryptographic
2424strength. This is convenient because of maximum DNS message sizes. At the time
2425of writing, not many mail servers appear to support ed25519 DKIM keys though,
2426so it is recommended to sign messages with both RSA and ed25519 keys.
2428 if len(c.Parse()) != 0 {
2432 buf, err := admin.MakeDKIMEd25519Key(dns.Domain{}, dns.Domain{})
2433 xcheckf(err, "making dkim ed25519 key")
2434 _, err = os.Stdout.Write(buf)
2435 xcheckf(err, "writing dkim ed25519 key")
2438func cmdDKIMTXT(c *cmd) {
2439 c.params = "<$selector._domainkey.$domain.key.pkcs8.pem"
2440 c.help = `Print a DKIM DNS TXT record with the public key derived from the private key read from stdin.
2442The DNS should be configured as a TXT record at $selector._domainkey.$domain.
2444 if len(c.Parse()) != 0 {
2448 privKey, err := parseDKIMKey(os.Stdin)
2449 xcheckf(err, "reading dkim private key from stdin")
2453 Hashes: []string{"sha256"},
2454 Flags: []string{"s"},
2457 switch key := privKey.(type) {
2458 case *rsa.PrivateKey:
2459 r.PublicKey = key.Public()
2460 case ed25519.PrivateKey:
2461 r.PublicKey = key.Public()
2464 log.Fatalf("unsupported private key type %T, must be rsa or ed25519", privKey)
2467 record, err := r.Record()
2468 xcheckf(err, "making record")
2469 fmt.Print("<selector>._domainkey.<your.domain.> TXT ")
2473 s, record = record[:100], record[100:]
2477 fmt.Printf(`"%s" `, s)
2482func parseDKIMKey(r io.Reader) (any, error) {
2483 buf, err := io.ReadAll(r)
2485 return nil, fmt.Errorf("reading pem from stdin: %v", err)
2487 b, _ := pem.Decode(buf)
2489 return nil, fmt.Errorf("decoding pem: %v", err)
2491 privKey, err := x509.ParsePKCS8PrivateKey(b.Bytes)
2493 return nil, fmt.Errorf("parsing private key: %v", err)
2498func cmdDKIMVerify(c *cmd) {
2499 c.params = "message"
2500 c.help = `Verify the DKIM signatures in a message and print the results.
2502The message is parsed, and the DKIM-Signature headers are validated. Validation
2503of older messages may fail because the DNS records have been removed or changed
2504by now, or because the signature header may have specified an expiration time
2512 msgf, err := os.Open(args[0])
2513 xcheckf(err, "open message")
2515 results, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, false, dkim.DefaultPolicy, msgf, true)
2516 xcheckf(err, "dkim verify")
2518 for _, result := range results {
2520 if result.Sig == nil {
2521 log.Printf("warning: could not parse signature")
2523 sigh, err = result.Sig.Header()
2525 log.Printf("warning: packing signature: %s", err)
2529 if result.Record == nil {
2530 log.Printf("warning: missing DNS record")
2532 txt, err = result.Record.Record()
2534 log.Printf("warning: packing record: %s", err)
2537 fmt.Printf("status %q, err %v\nrecord %q\nheader %s\n", result.Status, result.Err, txt, sigh)
2541func cmdDKIMSign(c *cmd) {
2542 c.params = "message"
2543 c.help = `Sign a message, adding DKIM-Signature headers based on the domain in the From header.
2545The message is parsed, the domain looked up in the configuration files, and
2546DKIM-Signature headers generated. The message is printed with the DKIM-Signature
2554 msgf, err := os.Open(args[0])
2555 xcheckf(err, "open message")
2557 if err := msgf.Close(); err != nil {
2558 log.Printf("closing message file: %v", err)
2562 p, err := message.Parse(c.log.Logger, true, msgf)
2563 xcheckf(err, "parsing message")
2565 if len(p.Envelope.From) != 1 {
2566 log.Fatalf("found %d from headers, need exactly 1", len(p.Envelope.From))
2568 localpart, err := smtp.ParseLocalpart(p.Envelope.From[0].User)
2569 xcheckf(err, "parsing localpart of address in from-header")
2570 dom := xparseDomain(p.Envelope.From[0].Host, "domain of address in from-header")
2574 domConf, ok := mox.Conf.Domain(dom)
2576 log.Fatalf("domain %s not configured", dom)
2579 selectors := mox.DKIMSelectors(domConf.DKIM)
2580 headers, err := dkim.Sign(context.Background(), c.log.Logger, localpart, dom, selectors, false, msgf)
2581 xcheckf(err, "signing message with dkim")
2583 log.Fatalf("no DKIM configured for domain %s", dom)
2585 _, err = fmt.Fprint(os.Stdout, headers)
2586 xcheckf(err, "write headers")
2587 _, err = io.Copy(os.Stdout, msgf)
2588 xcheckf(err, "write message")
2591func cmdDKIMLookup(c *cmd) {
2592 c.params = "selector domain"
2593 c.help = "Lookup and print the DKIM record for the selector at the domain."
2599 selector := xparseDomain(args[0], "selector")
2600 domain := xparseDomain(args[1], "domain")
2602 status, record, txt, authentic, err := dkim.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, selector, domain)
2604 fmt.Printf("error: %s\n", err)
2606 if status != dkim.StatusNeutral {
2607 fmt.Printf("status: %s\n", status)
2610 fmt.Printf("TXT record: %s\n", txt)
2613 fmt.Println("dnssec-signed: yes")
2615 fmt.Println("dnssec-signed: no")
2618 fmt.Printf("Record:\n")
2620 "version", record.Version,
2621 "hashes", record.Hashes,
2623 "notes", record.Notes,
2624 "services", record.Services,
2625 "flags", record.Flags,
2627 for i := 0; i < len(pairs); i += 2 {
2628 fmt.Printf("\t%s: %v\n", pairs[i], pairs[i+1])
2633func cmdDMARCLookup(c *cmd) {
2635 c.help = "Lookup dmarc policy for domain, a DNS TXT record at _dmarc.<domain>, validate and print it."
2641 fromdomain := xparseDomain(args[0], "domain")
2642 _, domain, _, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, fromdomain)
2643 xcheckf(err, "dmarc lookup domain %s", fromdomain)
2644 fmt.Printf("dmarc record at domain %s: %s\n", domain, txt)
2645 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2648func dnssecStatus(v bool) string {
2650 return "with dnssec"
2652 return "without dnssec"
2655func cmdDMARCVerify(c *cmd) {
2656 c.params = "remoteip mailfromaddress helodomain < message"
2657 c.help = `Parse an email message and evaluate it against the DMARC policy of the domain in the From-header.
2659mailfromaddress and helodomain are used for SPF validation. If both are empty,
2660SPF validation is skipped.
2662mailfromaddress should be the address used as MAIL FROM in the SMTP session.
2663For DSN messages, that address may be empty. The helo domain was specified at
2664the beginning of the SMTP transaction that delivered the message. These values
2665can be found in message headers.
2672 var heloDomain *dns.Domain
2674 remoteIP := xparseIP(args[0], "remoteip")
2676 var mailfrom *smtp.Address
2678 a, err := smtp.ParseAddress(args[1])
2679 xcheckf(err, "parsing mailfrom address")
2683 d := xparseDomain(args[2], "helo domain")
2686 var received *spf.Received
2687 spfStatus := spf.StatusNone
2688 var spfIdentity *dns.Domain
2689 if mailfrom != nil || heloDomain != nil {
2690 spfArgs := spf.Args{
2692 LocalIP: net.ParseIP("127.0.0.1"),
2693 LocalHostname: dns.Domain{ASCII: "localhost"},
2695 if mailfrom != nil {
2696 spfArgs.MailFromLocalpart = mailfrom.Localpart
2697 spfArgs.MailFromDomain = mailfrom.Domain
2699 if heloDomain != nil {
2700 spfArgs.HelloDomain = dns.IPDomain{Domain: *heloDomain}
2702 rspf, spfDomain, expl, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfArgs)
2704 log.Printf("spf verify: %v (explanation: %q, authentic %v)", err, expl, authentic)
2707 spfStatus = received.Result
2708 // todo: should probably potentially do two separate spf validations
2709 if mailfrom != nil {
2710 spfIdentity = &mailfrom.Domain
2712 spfIdentity = heloDomain
2714 fmt.Printf("spf result: %s: %s (%s)\n", spfDomain, spfStatus, dnssecStatus(authentic))
2718 data, err := io.ReadAll(os.Stdin)
2719 xcheckf(err, "read message")
2720 dmarcFrom, _, _, err := message.From(c.log.Logger, false, bytes.NewReader(data), nil)
2721 xcheckf(err, "extract dmarc from message")
2723 const ignoreTestMode = false
2724 dkimResults, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, true, func(*dkim.Sig) error { return nil }, bytes.NewReader(data), ignoreTestMode)
2725 xcheckf(err, "dkim verify")
2726 for _, r := range dkimResults {
2727 fmt.Printf("dkim result: %q (err %v)\n", r.Status, r.Err)
2730 _, result := dmarc.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, dmarcFrom.Domain, dkimResults, spfStatus, spfIdentity, false)
2731 xcheckf(result.Err, "dmarc verify")
2732 fmt.Printf("dmarc from: %s\ndmarc status: %q\ndmarc reject: %v\ncmarc record: %s\n", dmarcFrom, result.Status, result.Reject, result.Record)
2735func cmdDMARCCheckreportaddrs(c *cmd) {
2737 c.help = `For each reporting address in the domain's DMARC record, check if it has opted into receiving reports (if needed).
2739A DMARC record can request reports about DMARC evaluations to be sent to an
2740email/http address. If the organizational domains of that of the DMARC record
2741and that of the report destination address do not match, the destination
2742address must opt-in to receiving DMARC reports by creating a DMARC record at
2743<dmarcdomain>._report._dmarc.<reportdestdomain>.
2750 dom := xparseDomain(args[0], "domain")
2751 _, domain, record, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dom)
2752 xcheckf(err, "dmarc lookup domain %s", dom)
2753 fmt.Printf("dmarc record at domain %s: %q\n", domain, txt)
2754 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2756 check := func(kind, addr string) {
2759 printResult := func(format string, args ...any) {
2760 fmt.Printf("%s %s: %s (%s)\n", kind, addr, fmt.Sprintf(format, args...), dnssecStatus(authentic))
2763 u, err := url.Parse(addr)
2765 printResult("parsing uri: %v (skipping)", addr, err)
2768 var destdom dns.Domain
2771 a, err := smtp.ParseAddress(u.Opaque)
2773 printResult("parsing destination email address %s: %v (skipping)", u.Opaque, err)
2778 printResult("unrecognized scheme in reporting address %s (skipping)", u.Scheme)
2782 if publicsuffix.Lookup(context.Background(), c.log.Logger, dom) == publicsuffix.Lookup(context.Background(), c.log.Logger, destdom) {
2783 printResult("pass (same organizational domain)")
2787 accepts, status, _, txts, authentic, err := dmarc.LookupExternalReportsAccepted(context.Background(), c.log.Logger, dns.StrictResolver{}, domain, destdom)
2789 txtaddr := fmt.Sprintf("%s._report._dmarc.%s", domain.ASCII, destdom.ASCII)
2791 txtstr = fmt.Sprintf(" (no txt records %s)", txtaddr)
2793 txtstr = fmt.Sprintf(" (txt record %s: %q)", txtaddr, txts)
2795 if status != dmarc.StatusNone {
2796 printResult("fail: %s%s", err, txtstr)
2798 printResult("pass%s", txtstr)
2799 } else if err != nil {
2800 printResult("fail: %s%s", err, txtstr)
2802 printResult("fail%s", txtstr)
2806 for _, uri := range record.AggregateReportAddresses {
2807 check("aggregate reporting", uri.Address)
2809 for _, uri := range record.FailureReportAddresses {
2810 check("failure reporting", uri.Address)
2814func cmdDMARCParsereportmsg(c *cmd) {
2815 c.params = "message ..."
2816 c.help = `Parse a DMARC report from an email message, and print its extracted details.
2818DMARC reports are periodically mailed, if requested in the DMARC DNS record of
2819a domain. Reports are sent by mail servers that received messages with our
2820domain in a From header. This may or may not be legatimate email. DMARC reports
2821contain summaries of evaluations of DMARC and DKIM/SPF, which can help
2822understand email deliverability problems.
2829 for _, arg := range args {
2830 f, err := os.Open(arg)
2831 xcheckf(err, "open %q", arg)
2832 feedback, err := dmarcrpt.ParseMessageReport(c.log.Logger, f)
2833 xcheckf(err, "parse report in %q", arg)
2834 meta := feedback.ReportMetadata
2835 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)
2836 if len(meta.Errors) > 0 {
2837 fmt.Printf("Errors:\n")
2838 for _, s := range meta.Errors {
2839 fmt.Printf("\t- %s\n", s)
2842 pol := feedback.PolicyPublished
2843 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)
2844 for _, record := range feedback.Records {
2845 idents := record.Identifiers
2846 fmt.Printf("\theaderfrom %q, envelopes from %q, to %q\n", idents.HeaderFrom, idents.EnvelopeFrom, idents.EnvelopeTo)
2847 eval := record.Row.PolicyEvaluated
2849 for _, reason := range eval.Reasons {
2850 reasons += "; " + string(reason.Type)
2851 if reason.Comment != "" {
2852 reasons += fmt.Sprintf(": %q", reason.Comment)
2855 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)
2856 for _, dkim := range record.AuthResults.DKIM {
2858 if dkim.HumanResult != "" {
2859 result = fmt.Sprintf(": %q", dkim.HumanResult)
2861 fmt.Printf("\t\tdkim %s; domain %q selector %q%s\n", dkim.Result, dkim.Domain, dkim.Selector, result)
2863 for _, spf := range record.AuthResults.SPF {
2864 fmt.Printf("\t\tspf %s; domain %q scope %q\n", spf.Result, spf.Domain, spf.Scope)
2870func cmdDMARCDBAddReport(c *cmd) {
2872 c.params = "fromdomain < message"
2873 c.help = "Add a DMARC report to the database."
2881 fromdomain := xparseDomain(args[0], "domain")
2882 fmt.Fprintln(os.Stderr, "reading report message from stdin")
2883 report, err := dmarcrpt.ParseMessageReport(c.log.Logger, os.Stdin)
2884 xcheckf(err, "parse message")
2885 err = dmarcdb.AddReport(context.Background(), report, fromdomain)
2886 xcheckf(err, "add dmarc report")
2889func cmdTLSRPTLookup(c *cmd) {
2891 c.help = `Lookup the TLSRPT record for the domain.
2893A TLSRPT record typically contains an email address where reports about TLS
2894connectivity should be sent. Mail servers attempting delivery to our domain
2895should attempt to use TLS. TLSRPT lets them report how many connection
2896successfully used TLS, and how what kind of errors occurred otherwise.
2903 d := xparseDomain(args[0], "domain")
2904 _, txt, err := tlsrpt.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, d)
2905 xcheckf(err, "tlsrpt lookup for %s", d)
2909func cmdTLSRPTParsereportmsg(c *cmd) {
2910 c.params = "message ..."
2911 c.help = `Parse and print the TLSRPT in the message.
2913The report is printed in formatted JSON.
2920 for _, arg := range args {
2921 f, err := os.Open(arg)
2922 xcheckf(err, "open %q", arg)
2923 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, f)
2924 xcheckf(err, "parse report in %q", arg)
2925 // todo future: only print the highlights?
2926 enc := json.NewEncoder(os.Stdout)
2927 enc.SetIndent("", "\t")
2928 enc.SetEscapeHTML(false)
2929 err = enc.Encode(reportJSON)
2930 xcheckf(err, "write report")
2934func cmdSPFCheck(c *cmd) {
2935 c.params = "domain ip"
2936 c.help = `Check the status of IP for the policy published in DNS for the domain.
2938IPs may be allowed to send for a domain, or disallowed, and several shades in
2939between. If not allowed, an explanation may be provided by the policy. If so,
2940the explanation is printed. The SPF mechanism that matched (if any) is also
2948 domain := xparseDomain(args[0], "domain")
2950 ip := xparseIP(args[1], "ip")
2952 spfargs := spf.Args{
2954 MailFromLocalpart: "user",
2955 MailFromDomain: domain,
2956 HelloDomain: dns.IPDomain{Domain: domain},
2957 LocalIP: net.ParseIP("127.0.0.1"),
2958 LocalHostname: dns.Domain{ASCII: "localhost"},
2960 r, _, explanation, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfargs)
2962 fmt.Printf("error: %s\n", err)
2964 if explanation != "" {
2965 fmt.Printf("explanation: %s\n", explanation)
2967 fmt.Printf("status: %s (%s)\n", r.Result, dnssecStatus(authentic))
2968 if r.Mechanism != "" {
2969 fmt.Printf("mechanism: %s\n", r.Mechanism)
2973func cmdSPFParse(c *cmd) {
2974 c.params = "txtrecord"
2975 c.help = "Parse the record as SPF record. If valid, nothing is printed."
2981 _, _, err := spf.ParseRecord(args[0])
2982 xcheckf(err, "parsing record")
2985func cmdSPFLookup(c *cmd) {
2987 c.help = "Lookup the SPF record for the domain and print it."
2993 domain := xparseDomain(args[0], "domain")
2994 _, txt, _, authentic, err := spf.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
2995 xcheckf(err, "spf lookup for %s", domain)
2997 fmt.Printf("(%s)\n", dnssecStatus(authentic))
3000func cmdMTASTSLookup(c *cmd) {
3002 c.help = `Lookup the MTASTS record and policy for the domain.
3004MTA-STS is a mechanism for a domain to specify if it requires TLS connections
3005for delivering email. If a domain has a valid MTA-STS DNS TXT record at
3006_mta-sts.<domain> it signals it implements MTA-STS. A policy can then be
3007fetched at https://mta-sts.<domain>/.well-known/mta-sts.txt. The policy
3008specifies the mode (enforce, testing, none), which MX servers support TLS and
3009should be used, and how long the policy can be cached.
3016 domain := xparseDomain(args[0], "domain")
3018 record, policy, _, err := mtasts.Get(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
3020 fmt.Printf("error: %s\n", err)
3023 fmt.Printf("DNS TXT record _mta-sts.%s: %s\n", domain.ASCII, record.String())
3027 fmt.Printf("policy at https://mta-sts.%s/.well-known/mta-sts.txt:\n", domain.ASCII)
3028 fmt.Printf("%s", policy.String())
3032func cmdRDAPDomainage(c *cmd) {
3034 c.help = `Lookup the age of domain in RDAP based on latest registration.
3036RDAP is the registration data access protocol. Registries run RDAP services for
3037their top level domains, providing information such as the registration date of
3038domains. This command looks up the "age" of a domain by looking at the most
3039recent "registration", "reregistration" or "reinstantiation" event.
3041Email messages from recently registered domains are often treated with
3042suspicion, and some mail systems are more likely to classify them as junk.
3044On each invocation, a bootstrap file with a list of registries (of top-level
3045domains) is retrieved, without caching. Do not run this command too often with
3053 domain := xparseDomain(args[0], "domain")
3055 registration, err := rdap.LookupLastDomainRegistration(context.Background(), c.log, domain)
3056 xcheckf(err, "looking up domain in rdap")
3058 age := time.Since(registration)
3059 const day = 24 * time.Hour
3060 const year = 365 * day
3062 days := (age - years*year) / day
3066 } else if years > 0 {
3067 s = fmt.Sprintf("%d years, ", years)
3072 s += fmt.Sprintf("%d days", days)
3077func cmdRetrain(c *cmd) {
3078 c.params = "[accountname]"
3079 c.help = `Recreate and retrain the junk filter for the account or all accounts.
3081Useful after having made changes to the junk filter configuration, or if the
3082implementation has changed.
3094 ctlcmdRetrain(xctl(), account)
3097func ctlcmdRetrain(ctl *ctl, account string) {
3098 ctl.xwrite("retrain")
3103func cmdTLSRPTDBAddReport(c *cmd) {
3105 c.params = "< message"
3106 c.help = "Parse a TLS report from the message and add it to the database."
3108 c.flag.BoolVar(&hostReport, "hostreport", false, "report for a host instead of domain")
3116 // First read message, to get the From-header. Then parse it as TLSRPT.
3117 fmt.Fprintln(os.Stderr, "reading report message from stdin")
3118 buf, err := io.ReadAll(os.Stdin)
3119 xcheckf(err, "reading message")
3120 part, err := message.Parse(c.log.Logger, true, bytes.NewReader(buf))
3121 xcheckf(err, "parsing message")
3122 if part.Envelope == nil || len(part.Envelope.From) != 1 {
3123 log.Fatalf("message must have one From-header")
3125 from := part.Envelope.From[0]
3126 domain := xparseDomain(from.Host, "domain")
3128 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, bytes.NewReader(buf))
3129 xcheckf(err, "parsing tls report in message")
3131 mailfrom := from.User + "@" + from.Host // todo future: should escape and such
3132 report := reportJSON.Convert()
3133 err = tlsrptdb.AddReport(context.Background(), c.log, domain, mailfrom, hostReport, &report)
3134 xcheckf(err, "add tls report to database")
3137func cmdDNSBLCheck(c *cmd) {
3138 c.params = "zone ip"
3139 c.help = `Test if IP is in the DNS blocklist of the zone, e.g. bl.spamcop.net.
3141If the IP is in the blocklist, an explanation is printed. This is typically a
3142URL with more information.
3149 zone := xparseDomain(args[0], "zone")
3150 ip := xparseIP(args[1], "ip")
3152 status, explanation, err := dnsbl.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, zone, ip)
3153 fmt.Printf("status: %s\n", status)
3154 if status == dnsbl.StatusFail {
3155 fmt.Printf("explanation: %q\n", explanation)
3158 fmt.Printf("error: %s\n", err)
3162func cmdDNSBLCheckhealth(c *cmd) {
3164 c.help = `Check the health of the DNS blocklist represented by zone, e.g. bl.spamcop.net.
3166The health of a DNS blocklist can be checked by querying for 127.0.0.1 and
3167127.0.0.2. The second must and the first must not be present.
3174 zone := xparseDomain(args[0], "zone")
3175 err := dnsbl.CheckHealth(context.Background(), c.log.Logger, dns.StrictResolver{}, zone)
3176 xcheckf(err, "unhealthy")
3177 fmt.Println("healthy")
3180func cmdCheckupdate(c *cmd) {
3181 c.help = `Check if a newer version of mox is available.
3183A single DNS TXT lookup to _updates.xmox.nl tells if a new version is
3184available. If so, a changelog is fetched from https://updates.xmox.nl, and the
3185individual entries verified with a builtin public key. The changelog is
3188 if len(c.Parse()) != 0 {
3193 current, lastknown, _, err := store.LastKnown()
3195 log.Printf("getting last known version: %s", err)
3197 fmt.Printf("last known version: %s\n", lastknown)
3198 fmt.Printf("current version: %s\n", current)
3200 latest, _, err := updates.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dns.Domain{ASCII: changelogDomain})
3201 xcheckf(err, "lookup of latest version")
3202 fmt.Printf("latest version: %s\n", latest)
3204 if latest.After(current) {
3205 changelog, err := updates.FetchChangelog(context.Background(), c.log.Logger, changelogURL, current, changelogPubKey)
3206 xcheckf(err, "fetching changelog")
3207 if len(changelog.Changes) == 0 {
3208 log.Printf("no changes in changelog")
3211 fmt.Println("Changelog")
3212 for _, c := range changelog.Changes {
3213 fmt.Println("\n" + strings.TrimSpace(c.Text))
3218func cmdCid(c *cmd) {
3220 c.help = `Turn an ID from a Received header into a cid, for looking up in logs.
3222A cid is essentially a connection counter initialized when mox starts. Each log
3223line contains a cid. Received headers added by mox contain a unique ID that can
3224be decrypted to a cid by admin of a mox instance only.
3232 recvidpath := mox.DataDirPath("receivedid.key")
3233 recvidbuf, err := os.ReadFile(recvidpath)
3234 xcheckf(err, "reading %s", recvidpath)
3235 if len(recvidbuf) != 16+8 {
3236 log.Fatalf("bad data in %s: got %d bytes, expect 16+8=24", recvidpath, len(recvidbuf))
3238 err = mox.ReceivedIDInit(recvidbuf[:16], recvidbuf[16:])
3239 xcheckf(err, "init receivedid")
3241 cid, err := mox.ReceivedToCid(args[0])
3242 xcheckf(err, "received id to cid")
3243 fmt.Printf("%x\n", cid)
3246func cmdVersion(c *cmd) {
3247 c.help = "Prints this mox version."
3248 if len(c.Parse()) != 0 {
3251 fmt.Println(moxvar.Version)
3252 fmt.Printf("%s/%s\n", runtime.GOOS, runtime.GOARCH)
3255func cmdWebapi(c *cmd) {
3256 c.params = "[method [baseurl-with-credentials]"
3257 c.help = "Lists available methods, prints request/response parameters for method, or calls a method with a request read from standard input."
3263 t := reflect.TypeFor[webapi.Methods]()
3264 methods := map[string]reflect.Type{}
3266 for i := range t.NumMethod() {
3268 methods[mt.Name] = mt.Type
3269 ml = append(ml, mt.Name)
3273 fmt.Println(strings.Join(ml, "\n"))
3277 mt, ok := methods[args[0]]
3279 log.Fatalf("unknown method %q", args[0])
3281 resultNotJSON := mt.Out(0).Kind() == reflect.Interface
3284 fmt.Println("# Example request")
3286 printJSON("\t", mox.FillExample(nil, reflect.New(mt.In(1))).Interface())
3289 fmt.Println("Output is non-JSON data.")
3292 fmt.Println("# Example response")
3294 printJSON("\t", mox.FillExample(nil, reflect.New(mt.Out(0))).Interface())
3300 response = reflect.New(mt.Out(0))
3303 fmt.Fprintln(os.Stderr, "reading request from stdin...")
3304 request, err := io.ReadAll(os.Stdin)
3305 xcheckf(err, "read message")
3307 dec := json.NewDecoder(bytes.NewReader(request))
3308 dec.DisallowUnknownFields()
3309 err = dec.Decode(reflect.New(mt.In(1)).Interface())
3310 xcheckf(err, "parsing request")
3312 resp, err := http.PostForm(args[1]+args[0], url.Values{"request": []string{string(request)}})
3313 xcheckf(err, "http post")
3315 if err := resp.Body.Close(); err != nil {
3316 log.Printf("closing http response body: %v", err)
3319 if resp.StatusCode == http.StatusBadRequest {
3320 buf, err := io.ReadAll(&moxio.LimitReader{R: resp.Body, Limit: 10 * 1024})
3321 xcheckf(err, "reading response for 400 bad request error")
3322 err = json.Unmarshal(buf, &response)
3324 printJSON("", response)
3326 fmt.Fprintf(os.Stderr, "(not json)\n")
3327 os.Stderr.Write(buf)
3330 } else if resp.StatusCode != http.StatusOK {
3331 fmt.Fprintf(os.Stderr, "http response %s\n", resp.Status)
3332 _, err := io.Copy(os.Stderr, resp.Body)
3333 xcheckf(err, "copy body")
3335 err := json.NewDecoder(resp.Body).Decode(&resp)
3336 xcheckf(err, "unmarshal response")
3337 printJSON("", response)
3341func printJSON(indent string, v any) {
3342 fmt.Printf("%s", indent)
3343 enc := json.NewEncoder(os.Stdout)
3344 enc.SetIndent(indent, "\t")
3345 enc.SetEscapeHTML(false)
3346 err := enc.Encode(v)
3347 xcheckf(err, "encode json")
3350// 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.
3351func cmdBumpUIDValidity(c *cmd) {
3352 c.params = "account [mailbox]"
3353 c.help = `Change the IMAP UID validity of the mailbox, causing IMAP clients to refetch messages.
3355This can be useful after manually repairing metadata about the account/mailbox.
3357Opens account database file directly. Ensure mox does not have the account
3358open, or is not running.
3361 if len(args) != 1 && len(args) != 2 {
3366 a, err := store.OpenAccount(c.log, args[0], false)
3367 xcheckf(err, "open account")
3369 if err := a.Close(); err != nil {
3370 log.Printf("closing account: %v", err)
3374 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3375 uidvalidity, err := a.NextUIDValidity(tx)
3377 return fmt.Errorf("assigning next uid validity: %v", err)
3380 q := bstore.QueryTx[store.Mailbox](tx)
3381 q.FilterEqual("Expunged", false)
3383 q.FilterEqual("Name", args[1])
3385 mbl, err := q.SortAsc("Name").List()
3387 return fmt.Errorf("looking up mailbox: %v", err)
3389 if len(args) == 2 && len(mbl) != 1 {
3390 return fmt.Errorf("looking up mailbox %q, found %d mailboxes", args[1], len(mbl))
3392 for _, mb := range mbl {
3393 mb.UIDValidity = uidvalidity
3394 err = tx.Update(&mb)
3396 return fmt.Errorf("updating uid validity for mailbox: %v", err)
3398 fmt.Printf("uid validity for %q updated to %d\n", mb.Name, uidvalidity)
3402 xcheckf(err, "updating database")
3405func cmdReassignUIDs(c *cmd) {
3406 c.params = "account [mailboxid]"
3407 c.help = `Reassign UIDs in one mailbox or all mailboxes in an account and bump UID validity, causing IMAP clients to refetch messages.
3409Opens account database file directly. Ensure mox does not have the account
3410open, or is not running.
3413 if len(args) != 1 && len(args) != 2 {
3420 mailboxID, err = strconv.ParseInt(args[1], 10, 64)
3421 xcheckf(err, "parsing mailbox id")
3425 a, err := store.OpenAccount(c.log, args[0], false)
3426 xcheckf(err, "open account")
3428 if err := a.Close(); err != nil {
3429 log.Printf("closing account: %v", err)
3433 // Gather the last-assigned UIDs per mailbox.
3434 uidlasts := map[int64]store.UID{}
3436 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3437 // Reassign UIDs, going per mailbox. We assign starting at 1, only changing the
3438 // message if it isn't already at the intended UID. Doing it in this order ensures
3439 // we don't get into trouble with duplicate UIDs for a mailbox. We assign a new
3440 // modseq. Not strictly needed, but doesn't hurt. It's also why we assign a UID to
3441 // expunged messages.
3442 modseq, err := a.NextModSeq(tx)
3443 xcheckf(err, "assigning next modseq")
3445 q := bstore.QueryTx[store.Message](tx)
3447 q.FilterNonzero(store.Message{MailboxID: mailboxID})
3449 q.SortAsc("MailboxID", "UID")
3450 err = q.ForEach(func(m store.Message) error {
3451 uidlasts[m.MailboxID]++
3452 uid := uidlasts[m.MailboxID]
3456 if err := tx.Update(&m); err != nil {
3457 return fmt.Errorf("updating uid for message: %v", err)
3463 return fmt.Errorf("reading through messages: %v", err)
3466 // Now update the uidnext, uidvalidity and modseq for each mailbox.
3467 err = bstore.QueryTx[store.Mailbox](tx).FilterEqual("Expunged", false).ForEach(func(mb store.Mailbox) error {
3468 // Assign each mailbox a completely new uidvalidity.
3469 uidvalidity, err := a.NextUIDValidity(tx)
3471 return fmt.Errorf("assigning next uid validity: %v", err)
3474 if mb.UIDValidity >= uidvalidity {
3475 // This should not happen, but since we're fixing things up after a hypothetical
3476 // mishap, might as well account for inconsistent uidvalidity.
3477 next := store.NextUIDValidity{ID: 1, Next: mb.UIDValidity + 2}
3478 if err := tx.Update(&next); err != nil {
3479 log.Printf("updating nextuidvalidity: %v, continuing", err)
3483 mb.UIDValidity = uidvalidity
3485 mb.UIDNext = uidlasts[mb.ID] + 1
3487 if err := tx.Update(&mb); err != nil {
3488 return fmt.Errorf("updating uidvalidity and uidnext for mailbox: %v", err)
3493 return fmt.Errorf("updating mailboxes: %v", err)
3497 xcheckf(err, "updating database")
3500func cmdFixUIDMeta(c *cmd) {
3501 c.params = "account"
3502 c.help = `Fix inconsistent UIDVALIDITY and UIDNEXT in messages/mailboxes/account.
3504The next UID to use for a message in a mailbox should always be higher than any
3505existing message UID in the mailbox. If it is not, the mailbox UIDNEXT is
3508Each mailbox has a UIDVALIDITY sequence number, which should always be lower
3509than the per-account next UIDVALIDITY to use. If it is not, the account next
3510UIDVALIDITY is updated.
3512Opens account database file directly. Ensure mox does not have the account
3513open, or is not running.
3521 a, err := store.OpenAccount(c.log, args[0], false)
3522 xcheckf(err, "open account")
3524 if err := a.Close(); err != nil {
3525 log.Printf("closing account: %v", err)
3529 var maxUIDValidity uint32
3531 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3532 // We look at each mailbox, retrieve its max UID and compare against the mailbox
3534 err := bstore.QueryTx[store.Mailbox](tx).FilterEqual("Expunged", false).ForEach(func(mb store.Mailbox) error {
3535 if mb.UIDValidity > maxUIDValidity {
3536 maxUIDValidity = mb.UIDValidity
3538 m, err := bstore.QueryTx[store.Message](tx).FilterNonzero(store.Message{MailboxID: mb.ID}).SortDesc("UID").Limit(1).Get()
3539 if err == bstore.ErrAbsent || err == nil && m.UID < mb.UIDNext {
3541 } else if err != nil {
3542 return fmt.Errorf("finding message with max uid in mailbox: %w", err)
3544 olduidnext := mb.UIDNext
3545 mb.UIDNext = m.UID + 1
3546 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)
3547 if err := tx.Update(&mb); err != nil {
3548 return fmt.Errorf("updating mailbox uidnext: %v", err)
3553 return fmt.Errorf("processing mailboxes: %v", err)
3556 uidvalidity := store.NextUIDValidity{ID: 1}
3557 if err := tx.Get(&uidvalidity); err != nil {
3558 return fmt.Errorf("reading account next uidvalidity: %v", err)
3560 if maxUIDValidity >= uidvalidity.Next {
3561 log.Printf("account next uidvalidity %d <= highest uidvalidity %d found in mailbox, resetting account next uidvalidity to %d", uidvalidity.Next, maxUIDValidity, maxUIDValidity+1)
3562 uidvalidity.Next = maxUIDValidity + 1
3563 if err := tx.Update(&uidvalidity); err != nil {
3564 return fmt.Errorf("updating account next uidvalidity: %v", err)
3570 xcheckf(err, "updating database")
3573func cmdFixmsgsize(c *cmd) {
3574 c.params = "[account]"
3575 c.help = `Ensure message sizes in the database matching the sum of the message prefix length and on-disk file size.
3577Messages with an inconsistent size are also parsed again.
3579If an inconsistency is found, you should probably also run "mox
3580bumpuidvalidity" on the mailboxes or entire account to force IMAP clients to
3593 ctlcmdFixmsgsize(xctl(), account)
3596func ctlcmdFixmsgsize(ctl *ctl, account string) {
3597 ctl.xwrite("fixmsgsize")
3600 ctl.xstreamto(os.Stdout)
3603func cmdReparse(c *cmd) {
3604 c.params = "[account]"
3605 c.help = `Parse all messages in the account or all accounts again.
3607Can be useful after upgrading mox with improved message parsing. Messages are
3608parsed in batches, so other access to the mailboxes/messages are not blocked
3609while reparsing all messages.
3621 ctlcmdReparse(xctl(), account)
3624func ctlcmdReparse(ctl *ctl, account string) {
3625 ctl.xwrite("reparse")
3628 ctl.xstreamto(os.Stdout)
3631func cmdEnsureParsed(c *cmd) {
3632 c.params = "account"
3633 c.help = "Ensure messages in the database have a pre-parsed MIME form in the database."
3635 c.flag.BoolVar(&all, "all", false, "store new parsed message for all messages")
3642 a, err := store.OpenAccount(c.log, args[0], false)
3643 xcheckf(err, "open account")
3645 if err := a.Close(); err != nil {
3646 log.Printf("closing account: %v", err)
3651 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3652 q := bstore.QueryTx[store.Message](tx)
3653 q.FilterEqual("Expunged", false)
3654 q.FilterFn(func(m store.Message) bool {
3655 return all || m.ParsedBuf == nil
3659 return fmt.Errorf("list messages: %v", err)
3661 for _, m := range l {
3662 mr := a.MessageReader(m)
3663 p, err := message.EnsurePart(c.log.Logger, false, mr, m.Size)
3665 log.Printf("parsing message %d: %v (continuing)", m.ID, err)
3667 m.ParsedBuf, err = json.Marshal(p)
3669 return fmt.Errorf("marshal parsed message: %v", err)
3671 if err := tx.Update(&m); err != nil {
3672 return fmt.Errorf("update message: %v", err)
3678 xcheckf(err, "update messages with parsed mime structure")
3679 fmt.Printf("%d messages updated\n", n)
3682func cmdRecalculateMailboxCounts(c *cmd) {
3683 c.params = "account"
3684 c.help = `Recalculate message counts for all mailboxes in the account, and total message size for quota.
3686When a message is added to/removed from a mailbox, or when message flags change,
3687the total, unread, unseen and deleted messages are accounted, the total size of
3688the mailbox, and the total message size for the account. In case of a bug in
3689this accounting, the numbers could become incorrect. This command will find, fix
3698 ctlcmdRecalculateMailboxCounts(xctl(), args[0])
3701func ctlcmdRecalculateMailboxCounts(ctl *ctl, account string) {
3702 ctl.xwrite("recalculatemailboxcounts")
3705 ctl.xstreamto(os.Stdout)
3708func cmdMessageParse(c *cmd) {
3709 c.params = "message.eml"
3710 c.help = "Parse message, print JSON representation."
3713 c.flag.BoolVar(&smtputf8, "smtputf8", false, "check if message needs smtputf8")
3719 f, err := os.Open(args[0])
3720 xcheckf(err, "open")
3722 if err := f.Close(); err != nil {
3723 log.Printf("closing message file: %v", err)
3727 part, err := message.Parse(c.log.Logger, false, f)
3728 xcheckf(err, "parsing message")
3729 err = part.Walk(c.log.Logger, nil)
3730 xcheckf(err, "parsing nested parts")
3731 enc := json.NewEncoder(os.Stdout)
3732 enc.SetIndent("", "\t")
3733 enc.SetEscapeHTML(false)
3734 err = enc.Encode(part)
3735 xcheckf(err, "write")
3738 needs, err := part.NeedsSMTPUTF8()
3739 xcheckf(err, "checking if message needs smtputf8")
3740 fmt.Println("message needs smtputf8:", needs)
3744func cmdOpenaccounts(c *cmd) {
3746 c.params = "datadir account ..."
3747 c.help = `Open and close accounts, for triggering data upgrades, for tests.
3749Opens database files directly, not going through a running mox instance.
3757 dataDir := filepath.Clean(args[0])
3758 for _, accName := range args[1:] {
3759 accDir := filepath.Join(dataDir, "accounts", accName)
3760 log.Printf("opening account %s...", accDir)
3761 a, err := store.OpenAccountDB(c.log, accDir, accName)
3762 xcheckf(err, "open account %s", accName)
3763 err = a.ThreadingWait(c.log)
3764 xcheckf(err, "wait for threading upgrade to complete for %s", accName)
3766 xcheckf(err, "close account %s", accName)
3770func cmdReassignthreads(c *cmd) {
3771 c.params = "[account]"
3772 c.help = `Reassign message threads.
3774For all accounts, or optionally only the specified account.
3776Threading for all messages in an account is first reset, and new base subject
3777and normalized message-id saved with the message. Then all messages are
3778evaluated and matched against their parents/ancestors.
3780Messages are matched based on the References header, with a fall-back to an
3781In-Reply-To header, and if neither is present/valid, based only on base
3784A References header typically points to multiple previous messages in a
3785hierarchy. From oldest ancestor to most recent parent. An In-Reply-To header
3786would have only a message-id of the parent message.
3788A message is only linked to a parent/ancestor if their base subject is the
3789same. This ensures unrelated replies, with a new subject, are placed in their
3792The base subject is lower cased, has whitespace collapsed to a single
3793space, and some components removed: leading "Re:", "Fwd:", "Fw:", or bracketed
3794tag (that mailing lists often add, e.g. "[listname]"), trailing "(fwd)", or
3795enclosing "[fwd: ...]".
3797Messages are linked to all their ancestors. If an intermediate parent/ancestor
3798message is deleted in the future, the message can still be linked to the earlier
3799ancestors. If the direct parent already wasn't available while matching, this is
3800stored as the message having a "missing link" to its stored ancestors.
3812 ctlcmdReassignthreads(xctl(), account)
3815func ctlcmdReassignthreads(ctl *ctl, account string) {
3816 ctl.xwrite("reassignthreads")
3819 ctl.xstreamto(os.Stdout)
3822func cmdIMAPServe(c *cmd) {
3823 c.params = "preauth-address"
3824 c.help = `Initiate a preauthenticated IMAP connection on file descriptor 0.
3826For use with tools that can do IMAP over tunneled connections, e.g. with SSH
3827during migrations. TLS is not possible on the connection, and authentication
3828does not require TLS.
3831 c.flag.BoolVar(&fd0, "fd0", false, "write IMAP to file descriptor 0 instead of stdout")
3842 ctlcmdIMAPServe(xctl(), address, os.Stdin, output)
3845func ctlcmdIMAPServe(ctl *ctl, address string, input io.ReadCloser, output io.WriteCloser) {
3846 ctl.xwrite("imapserve")
3850 done := make(chan struct{}, 1)
3855 _, err := io.Copy(output, ctl.conn)
3859 log.Printf("reading from imap: %v", err)
3865 _, err := io.Copy(ctl.conn, input)
3869 log.Printf("writing to imap: %v", err)
3874func cmdReadmessages(c *cmd) {
3876 c.params = "datadir account ..."
3877 c.help = `Open account, parse several headers for all messages.
3879For performance testing.
3881Opens database files directly, not going through a running mox instance.
3884 gomaxprocs := runtime.GOMAXPROCS(0)
3885 var procs, workqueuesize, limit int
3886 c.flag.IntVar(&procs, "procs", gomaxprocs, "number of goroutines for reading messages")
3887 c.flag.IntVar(&workqueuesize, "workqueuesize", 2*gomaxprocs, "number of messages to keep in work queue")
3888 c.flag.IntVar(&limit, "limit", 0, "number of messages to process if greater than zero")
3894 type threadPrep struct {
3899 threadingFields := [][]byte{
3900 []byte("references"),
3901 []byte("in-reply-to"),
3904 dataDir := filepath.Clean(args[0])
3905 for _, accName := range args[1:] {
3906 accDir := filepath.Join(dataDir, "accounts", accName)
3907 log.Printf("opening account %s...", accDir)
3908 a, err := store.OpenAccountDB(c.log, accDir, accName)
3909 xcheckf(err, "open account %s", accName)
3911 prepareMessages := func(in, out chan moxio.Work[store.Message, threadPrep]) {
3912 headerbuf := make([]byte, 8*1024)
3913 scratch := make([]byte, 4*1024)
3921 var partialPart struct {
3925 if err := json.Unmarshal(m.ParsedBuf, &partialPart); err != nil {
3926 w.Err = fmt.Errorf("unmarshal part: %v", err)
3928 size := partialPart.BodyOffset - partialPart.HeaderOffset
3929 if int(size) > len(headerbuf) {
3930 headerbuf = make([]byte, size)
3933 buf := headerbuf[:int(size)]
3934 err := func() error {
3935 mr := a.MessageReader(m)
3937 if err := mr.Close(); err != nil {
3938 log.Printf("closing message reader: %v", err)
3942 // ReadAt returns whole buffer or error. Single read should be fast.
3943 n, err := mr.ReadAt(buf, partialPart.HeaderOffset)
3944 if err != nil || n != len(buf) {
3945 return fmt.Errorf("read header: %v", err)
3951 } else if h, err := message.ParseHeaderFields(buf, scratch, threadingFields); err != nil {
3954 w.Out.references = h["References"]
3955 w.Out.inReplyTo = h["In-Reply-To"]
3968 processMessage := func(m store.Message, prep threadPrep) error {
3970 log.Printf("%d messages (delta %s)", n, time.Since(t))
3977 wq := moxio.NewWorkQueue[store.Message, threadPrep](procs, workqueuesize, prepareMessages, processMessage)
3979 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3980 q := bstore.QueryTx[store.Message](tx)
3981 q.FilterEqual("Expunged", false)
3986 err = q.ForEach(wq.Add)
3994 xcheckf(err, "processing message")
3997 xcheckf(err, "close account %s", accName)
3998 log.Printf("account %s, total time %s", accName, time.Since(t0))
4002func cmdQueueFillRetired(c *cmd) {
4004 c.help = `Fill retired messag and webhooks queue with testdata.
4006For testing the pagination. Operates directly on queue database.
4009 c.flag.IntVar(&n, "n", 10000, "retired messages and retired webhooks to insert")
4017 xcheckf(err, "init queue")
4018 err = queue.DB.Write(context.Background(), func(tx *bstore.Tx) error {
4021 // Cause autoincrement ID for queue.Msg to be forwarded, and use the reserved ID
4022 // space for inserting retired messages.
4024 err = tx.Insert(&fm)
4025 xcheckf(err, "temporarily insert message to get autoincrement sequence")
4026 err = tx.Delete(&fm)
4027 xcheckf(err, "removing temporary message for resetting autoincrement sequence")
4029 err = tx.Insert(&fm)
4030 xcheckf(err, "temporarily insert message to forward autoincrement sequence")
4031 err = tx.Delete(&fm)
4032 xcheckf(err, "removing temporary message after forwarding autoincrement sequence")
4035 // And likewise for webhooks.
4036 fh := queue.Hook{Account: "x", URL: "x", NextAttempt: time.Now()}
4037 err = tx.Insert(&fh)
4038 xcheckf(err, "temporarily insert webhook to get autoincrement sequence")
4039 err = tx.Delete(&fh)
4040 xcheckf(err, "removing temporary webhook for resetting autoincrement sequence")
4042 err = tx.Insert(&fh)
4043 xcheckf(err, "temporarily insert webhook to forward autoincrement sequence")
4044 err = tx.Delete(&fh)
4045 xcheckf(err, "removing temporary webhook after forwarding autoincrement sequence")
4049 t0 := now.Add(-time.Duration(i) * time.Second)
4050 last := now.Add(-time.Duration(i/10) * time.Second)
4051 mr := queue.MsgRetired{
4052 ID: fm.ID + int64(i),
4054 SenderAccount: "test",
4055 SenderLocalpart: "mox",
4056 SenderDomainStr: "localhost",
4057 FromID: fmt.Sprintf("%016d", i),
4058 RecipientLocalpart: "mox",
4059 RecipientDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "localhost"}},
4060 RecipientDomainStr: "localhost",
4063 Results: []queue.MsgResult{
4066 Duration: time.Millisecond,
4073 Size: int64(i * 100),
4074 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
4075 Subject: fmt.Sprintf("test message %d", i),
4076 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
4078 RecipientAddress: "mox@localhost",
4080 KeepUntil: now.Add(48 * time.Hour),
4082 err := tx.Insert(&mr)
4083 xcheckf(err, "inserting retired message")
4087 t0 := now.Add(-time.Duration(i) * time.Second)
4088 last := now.Add(-time.Duration(i/10) * time.Second)
4093 hr := queue.HookRetired{
4094 ID: fh.ID + int64(i),
4095 QueueMsgID: fm.ID + int64(i),
4096 FromID: fmt.Sprintf("%016d", i),
4097 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
4098 Subject: fmt.Sprintf("test message %d", i),
4099 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
4101 URL: "http://localhost/hook",
4102 IsIncoming: i%10 == 0,
4103 OutgoingEvent: event,
4108 Results: []queue.HookResult{
4111 Duration: time.Millisecond,
4112 URL: "http://localhost/hook",
4121 KeepUntil: now.Add(48 * time.Hour),
4123 err := tx.Insert(&hr)
4124 xcheckf(err, "inserting retired hook")
4129 xcheckf(err, "add to queue")
4130 log.Printf("added %d retired messages and %d retired webhooks", n, n)