11 cryptorand "crypto/rand"
40 "golang.org/x/crypto/bcrypt"
41 "golang.org/x/text/secure/precis"
43 "github.com/mjl-/adns"
45 "github.com/mjl-/autocert"
46 "github.com/mjl-/bstore"
47 "github.com/mjl-/sconf"
48 "github.com/mjl-/sherpa"
50 "github.com/mjl-/mox/admin"
51 "github.com/mjl-/mox/config"
52 "github.com/mjl-/mox/dane"
53 "github.com/mjl-/mox/dkim"
54 "github.com/mjl-/mox/dmarc"
55 "github.com/mjl-/mox/dmarcdb"
56 "github.com/mjl-/mox/dmarcrpt"
57 "github.com/mjl-/mox/dns"
58 "github.com/mjl-/mox/dnsbl"
59 "github.com/mjl-/mox/message"
60 "github.com/mjl-/mox/mlog"
61 "github.com/mjl-/mox/mox-"
62 "github.com/mjl-/mox/moxio"
63 "github.com/mjl-/mox/moxvar"
64 "github.com/mjl-/mox/mtasts"
65 "github.com/mjl-/mox/publicsuffix"
66 "github.com/mjl-/mox/queue"
67 "github.com/mjl-/mox/rdap"
68 "github.com/mjl-/mox/smtp"
69 "github.com/mjl-/mox/smtpclient"
70 "github.com/mjl-/mox/spf"
71 "github.com/mjl-/mox/store"
72 "github.com/mjl-/mox/tlsrpt"
73 "github.com/mjl-/mox/tlsrptdb"
74 "github.com/mjl-/mox/updates"
75 "github.com/mjl-/mox/webadmin"
76 "github.com/mjl-/mox/webapi"
80 changelogDomain = "xmox.nl"
81 changelogURL = "https://updates.xmox.nl/changelog"
82 changelogPubKey = base64Decode("sPNiTDQzvb4FrytNEiebJhgyQzn57RwEjNbGWMM/bDY=")
85func base64Decode(s string) []byte {
86 buf, err := base64.StdEncoding.DecodeString(s)
93func envString(k, def string) string {
101var commands = []struct {
106 {"quickstart", cmdQuickstart},
108 {"setaccountpassword", cmdSetaccountpassword},
109 {"setadminpassword", cmdSetadminpassword},
110 {"loglevels", cmdLoglevels},
111 {"queue holdrules list", cmdQueueHoldrulesList},
112 {"queue holdrules add", cmdQueueHoldrulesAdd},
113 {"queue holdrules remove", cmdQueueHoldrulesRemove},
114 {"queue list", cmdQueueList},
115 {"queue hold", cmdQueueHold},
116 {"queue unhold", cmdQueueUnhold},
117 {"queue schedule", cmdQueueSchedule},
118 {"queue transport", cmdQueueTransport},
119 {"queue requiretls", cmdQueueRequireTLS},
120 {"queue fail", cmdQueueFail},
121 {"queue drop", cmdQueueDrop},
122 {"queue dump", cmdQueueDump},
123 {"queue retired list", cmdQueueRetiredList},
124 {"queue retired print", cmdQueueRetiredPrint},
125 {"queue suppress list", cmdQueueSuppressList},
126 {"queue suppress add", cmdQueueSuppressAdd},
127 {"queue suppress remove", cmdQueueSuppressRemove},
128 {"queue suppress lookup", cmdQueueSuppressLookup},
129 {"queue webhook list", cmdQueueHookList},
130 {"queue webhook schedule", cmdQueueHookSchedule},
131 {"queue webhook cancel", cmdQueueHookCancel},
132 {"queue webhook print", cmdQueueHookPrint},
133 {"queue webhook retired list", cmdQueueHookRetiredList},
134 {"queue webhook retired print", cmdQueueHookRetiredPrint},
135 {"import maildir", cmdImportMaildir},
136 {"import mbox", cmdImportMbox},
137 {"export maildir", cmdExportMaildir},
138 {"export mbox", cmdExportMbox},
139 {"localserve", cmdLocalserve},
141 {"backup", cmdBackup},
142 {"verifydata", cmdVerifydata},
143 {"licenses", cmdLicenses},
145 {"config test", cmdConfigTest},
146 {"config dnscheck", cmdConfigDNSCheck},
147 {"config dnsrecords", cmdConfigDNSRecords},
148 {"config describe-domains", cmdConfigDescribeDomains},
149 {"config describe-static", cmdConfigDescribeStatic},
150 {"config account list", cmdConfigAccountList},
151 {"config account addresses", cmdConfigAccountAddresses},
152 {"config account add", cmdConfigAccountAdd},
153 {"config account rm", cmdConfigAccountRemove},
154 {"config account disable", cmdConfigAccountDisable},
155 {"config account enable", cmdConfigAccountEnable},
156 {"config address add", cmdConfigAddressAdd},
157 {"config address rm", cmdConfigAddressRemove},
158 {"config address account", cmdConfigAddressAccount},
159 {"config domain add", cmdConfigDomainAdd},
160 {"config domain rm", cmdConfigDomainRemove},
161 {"config domain disable", cmdConfigDomainDisable},
162 {"config domain enable", cmdConfigDomainEnable},
163 {"config tlspubkey list", cmdConfigTlspubkeyList},
164 {"config tlspubkey get", cmdConfigTlspubkeyGet},
165 {"config tlspubkey add", cmdConfigTlspubkeyAdd},
166 {"config tlspubkey rm", cmdConfigTlspubkeyRemove},
167 {"config tlspubkey gen", cmdConfigTlspubkeyGen},
168 {"config alias list", cmdConfigAliasList},
169 {"config alias print", cmdConfigAliasPrint},
170 {"config alias add", cmdConfigAliasAdd},
171 {"config alias update", cmdConfigAliasUpdate},
172 {"config alias rm", cmdConfigAliasRemove},
173 {"config alias addaddr", cmdConfigAliasAddaddr},
174 {"config alias rmaddr", cmdConfigAliasRemoveaddr},
176 {"config describe-sendmail", cmdConfigDescribeSendmail},
177 {"config printservice", cmdConfigPrintservice},
178 {"config ensureacmehostprivatekeys", cmdConfigEnsureACMEHostprivatekeys},
179 {"config example", cmdConfigExample},
181 {"admin imapserve", cmdIMAPServe},
183 {"checkupdate", cmdCheckupdate},
185 {"clientconfig", cmdClientConfig},
186 {"deliver", cmdDeliver},
187 // todo: turn cmdDANEDialmx into a regular "dialmx" command that follows mta-sts policy, with options to require dane, mta-sts or requiretls. the code will be similar to queue/direct.go
188 {"dane dial", cmdDANEDial},
189 {"dane dialmx", cmdDANEDialmx},
190 {"dane makerecord", cmdDANEMakeRecord},
191 {"dns lookup", cmdDNSLookup},
192 {"dkim gened25519", cmdDKIMGened25519},
193 {"dkim genrsa", cmdDKIMGenrsa},
194 {"dkim lookup", cmdDKIMLookup},
195 {"dkim txt", cmdDKIMTXT},
196 {"dkim verify", cmdDKIMVerify},
197 {"dkim sign", cmdDKIMSign},
198 {"dmarc lookup", cmdDMARCLookup},
199 {"dmarc parsereportmsg", cmdDMARCParsereportmsg},
200 {"dmarc verify", cmdDMARCVerify},
201 {"dmarc checkreportaddrs", cmdDMARCCheckreportaddrs},
202 {"dnsbl check", cmdDNSBLCheck},
203 {"dnsbl checkhealth", cmdDNSBLCheckhealth},
204 {"mtasts lookup", cmdMTASTSLookup},
205 {"rdap domainage", cmdRDAPDomainage},
206 {"retrain", cmdRetrain},
207 {"sendmail", cmdSendmail},
208 {"smtp dial", cmdSMTPDial},
209 {"spf check", cmdSPFCheck},
210 {"spf lookup", cmdSPFLookup},
211 {"spf parse", cmdSPFParse},
212 {"tlsrpt lookup", cmdTLSRPTLookup},
213 {"tlsrpt parsereportmsg", cmdTLSRPTParsereportmsg},
214 {"version", cmdVersion},
215 {"webapi", cmdWebapi},
217 {"example", cmdExample},
218 {"bumpuidvalidity", cmdBumpUIDValidity},
219 {"reassignuids", cmdReassignUIDs},
220 {"fixuidmeta", cmdFixUIDMeta},
221 {"fixmsgsize", cmdFixmsgsize},
222 {"reparse", cmdReparse},
223 {"ensureparsed", cmdEnsureParsed},
224 {"recalculatemailboxcounts", cmdRecalculateMailboxCounts},
225 {"message parse", cmdMessageParse},
226 {"reassignthreads", cmdReassignthreads},
229 {"helpall", cmdHelpall},
230 {"junk analyze", cmdJunkAnalyze},
231 {"junk check", cmdJunkCheck},
232 {"junk play", cmdJunkPlay},
233 {"junk test", cmdJunkTest},
234 {"junk train", cmdJunkTrain},
235 {"dmarcdb addreport", cmdDMARCDBAddReport},
236 {"tlsrptdb addreport", cmdTLSRPTDBAddReport},
237 {"updates addsigned", cmdUpdatesAddSigned},
238 {"updates genkey", cmdUpdatesGenkey},
239 {"updates pubkey", cmdUpdatesPubkey},
240 {"updates serve", cmdUpdatesServe},
241 {"updates verify", cmdUpdatesVerify},
242 {"gentestdata", cmdGentestdata},
243 {"ximport maildir", cmdXImportMaildir},
244 {"ximport mbox", cmdXImportMbox},
245 {"openaccounts", cmdOpenaccounts},
246 {"readmessages", cmdReadmessages},
247 {"queuefillretired", cmdQueueFillRetired},
253 for _, xc := range commands {
254 c := cmd{words: strings.Split(xc.cmd, " "), fn: xc.fn}
255 cmds = append(cmds, c)
263 // Set before calling command.
266 _gather bool // Set when using Parse to gather usage for a command.
268 // Set by invoked command or Parse.
269 unlisted bool // If set, command is not listed until at least some words are matched from command.
270 params string // Arguments to command. Multiple lines possible.
271 help string // Additional explanation. First line is synopsis, the rest is only printed for an explicit help/usage for that command.
277func (c *cmd) Parse() []string {
278 // To gather params and usage information, we just run the command but cause this
279 // panic after the command has registered its flags and set its params and help
280 // information. This is then caught and that info printed.
285 c.flag.Usage = c.Usage
286 c.flag.Parse(c.flagArgs)
287 c.args = c.flag.Args()
291func (c *cmd) gather() {
292 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
296 // panic generated by Parse.
304func (c *cmd) makeUsage() string {
305 var r strings.Builder
306 cs := "mox " + strings.Join(c.words, " ")
307 for i, line := range strings.Split(strings.TrimSpace(c.params), "\n") {
315 fmt.Fprintf(&r, "%6s %s%s\n", s, cs, line)
318 c.flag.PrintDefaults()
322func (c *cmd) printUsage() {
323 fmt.Fprint(os.Stderr, c.makeUsage())
325 fmt.Fprint(os.Stderr, "\n"+c.help+"\n")
329func (c *cmd) Usage() {
334func cmdHelp(c *cmd) {
335 c.params = "[$command ...]"
336 c.help = `Prints help about matching commands.
338If multiple commands match, they are listed along with the first line of their help text.
339If a single command matches, its usage and full help text is printed.
346 prefix := func(l, pre []string) bool {
347 if len(pre) > len(l) {
350 return slices.Equal(pre, l[:len(pre)])
354 for _, c := range cmds {
355 if slices.Equal(c.words, args) {
357 fmt.Print(c.makeUsage())
359 fmt.Print("\n" + c.help + "\n")
362 } else if prefix(c.words, args) {
363 partial = append(partial, c)
366 if len(partial) == 0 {
367 fmt.Fprintf(os.Stderr, "%s: unknown command\n", strings.Join(args, " "))
370 for _, c := range partial {
372 line := "mox " + strings.Join(c.words, " ")
373 fmt.Printf("%s\n", line)
375 fmt.Printf("\t%s\n", strings.Split(c.help, "\n")[0])
380func cmdHelpall(c *cmd) {
382 c.help = `Print all detailed usage and help information for all listed commands.
384Used to generate documentation.
392 for _, c := range cmds {
398 fmt.Fprintf(os.Stderr, "\n")
402 fmt.Fprintf(os.Stderr, "# mox %s\n\n", strings.Join(c.words, " "))
404 fmt.Fprintln(os.Stderr, c.help+"\n")
407 s = "\t" + strings.ReplaceAll(s, "\n", "\n\t")
408 fmt.Fprintln(os.Stderr, s)
412func usage(l []cmd, unlisted bool) {
415 lines = append(lines, "mox [-config config/mox.conf] [-pedantic] ...")
417 for _, c := range l {
419 if c.unlisted && !unlisted {
422 for line := range strings.SplitSeq(c.params, "\n") {
423 x := append([]string{"mox"}, c.words...)
427 lines = append(lines, strings.Join(x, " "))
430 for i, line := range lines {
435 fmt.Fprintln(os.Stderr, pre+line)
440var loglevel string // Empty will be interpreted as info, except by localserve.
443// subcommands that are not "serve" should use this function to load the config, it
444// restores any loglevel specified on the command-line, instead of using the
445// loglevels from the config file and it does not load files like TLS keys/certs.
446func mustLoadConfig() {
447 mox.MustLoadConfig(false, false)
452 if level, ok := mlog.Levels[ll]; ok {
453 mox.Conf.Log[""] = level
454 mlog.SetConfig(mox.Conf.Log)
456 log.Fatal("unknown loglevel", slog.String("loglevel", loglevel))
459 mox.SetPedantic(true)
464 // CheckConsistencyOnClose is true by default, for all the test packages. A regular
465 // mox server should never use it. But integration tests enable it again with a
467 store.CheckConsistencyOnClose = false
468 store.MsgFilesPerDirShiftSet(13) // For 1<<13 = 8k message files per directory.
470 ctxbg := context.Background()
476 // If invoked as sendmail, e.g. /usr/sbin/sendmail, we do enough so cron can get a
477 // message sent using smtp submission to a configured server.
478 if len(os.Args) > 0 && filepath.Base(os.Args[0]) == "sendmail" {
480 flag: flag.NewFlagSet("sendmail", flag.ExitOnError),
481 flagArgs: os.Args[1:],
482 log: mlog.New("sendmail", nil),
487 // Instructions explain to install mox as /usr/sbin/sendmail as setgid "moxsubmit".
488 // Users can invoke that binary as regular "mox" command too. We don't want to run
489 // our regular code, users could use one of the commands to read
490 // /etc/moxsubmit.conf, or enable traceauth logging and read a password.
491 if os.Getgid() != os.Getegid() {
492 log.Fatalf("can only execute as sendmail when effective gid is different from gid")
495 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")
496 flag.StringVar(&loglevel, "loglevel", "", "if non-empty, this log level is set early in startup")
497 flag.BoolVar(&pedantic, "pedantic", false, "protocol violations result in errors instead of accepting/working around them")
498 flag.BoolVar(&store.CheckConsistencyOnClose, "checkconsistency", false, "dangerous option for testing only, enables data checks that abort/panic when inconsistencies are found")
500 var cpuprofile, memprofile, tracefile string
501 flag.StringVar(&cpuprofile, "cpuprof", "", "store cpu profile to file")
502 flag.StringVar(&memprofile, "memprof", "", "store mem profile to file")
503 flag.StringVar(&tracefile, "trace", "", "store execution trace to file")
505 flag.Usage = func() { usage(cmds, false) }
513 defer traceExecution(tracefile)()
515 defer profile(cpuprofile, memprofile)()
518 mox.SetPedantic(true)
521 mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
526 if level, ok := mlog.Levels[ll]; ok {
527 mox.Conf.Log[""] = level
528 mlog.SetConfig(mox.Conf.Log)
529 // note: SetConfig may be called again when subcommands loads config.
531 log.Fatalf("unknown loglevel %q", loglevel)
536 for _, c := range cmds {
537 for i, w := range c.words {
538 if i >= len(args) || w != args[i] {
540 partial = append(partial, c)
545 c.flag = flag.NewFlagSet("mox "+strings.Join(c.words, " "), flag.ExitOnError)
546 c.flagArgs = args[len(c.words):]
547 c.log = mlog.New(strings.Join(c.words, ""), nil)
551 if len(partial) > 0 {
557func xcheckf(err error, format string, args ...any) {
561 msg := fmt.Sprintf(format, args...)
562 log.Fatalf("%s: %s", msg, err)
565func xparseIP(s, what string) net.IP {
568 log.Fatalf("invalid %s: %q", what, s)
573func xparseDomain(s, what string) dns.Domain {
574 d, err := dns.ParseDomain(s)
575 xcheckf(err, "parsing %s %q", what, s)
579func cmdClientConfig(c *cmd) {
581 c.help = `Print the configuration for email clients for a domain.
583Sending email is typically not done on the SMTP port 25, but on submission
584ports 465 (with TLS) and 587 (without initial TLS, but usually added to the
585connection with STARTTLS). For IMAP, the port with TLS is 993 and without is
588Without TLS/STARTTLS, passwords are sent in clear text, which should only be
589configured over otherwise secured connections, like a VPN.
595 d := xparseDomain(args[0], "domain")
600func printClientConfig(d dns.Domain) {
601 cc, err := admin.ClientConfigsDomain(d)
602 xcheckf(err, "getting client config")
603 fmt.Printf("%-20s %-30s %5s %-15s %s\n", "Protocol", "Host", "Port", "Listener", "Note")
604 for _, e := range cc.Entries {
605 fmt.Printf("%-20s %-30s %5d %-15s %s\n", e.Protocol, e.Host, e.Port, e.Listener, e.Note)
608To prevent authentication mechanism downgrade attempts that may result in
609clients sending plain text passwords to a MitM, clients should always be
610explicitly configured with the most secure authentication mechanism supported,
611the first of: SCRAM-SHA-256-PLUS, SCRAM-SHA-1-PLUS, SCRAM-SHA-256, SCRAM-SHA-1,
616func cmdConfigTest(c *cmd) {
617 c.help = `Parses and validates the configuration files.
619If valid, the command exits with status 0. If not valid, all errors encountered
627 mox.FilesImmediate = true
629 _, errs := mox.ParseConfig(context.Background(), c.log, mox.ConfigStaticPath, true, true, false)
631 log.Printf("multiple errors:")
632 for _, err := range errs {
633 log.Printf("%s", err)
636 } else if len(errs) == 1 {
637 log.Fatalf("%s", errs[0])
640 fmt.Println("config OK")
643func cmdConfigDescribeStatic(c *cmd) {
644 c.params = ">mox.conf"
645 c.help = `Prints an annotated empty configuration for use as mox.conf.
647The static configuration file cannot be reloaded while mox is running. Mox has
648to be restarted for changes to the static configuration file to take effect.
650This configuration file needs modifications to make it valid. For example, it
651may contain unfinished list items.
653 if len(c.Parse()) != 0 {
658 err := sconf.Describe(os.Stdout, &sc)
659 xcheckf(err, "describing config")
662func cmdConfigDescribeDomains(c *cmd) {
663 c.params = ">domains.conf"
664 c.help = `Prints an annotated empty configuration for use as domains.conf.
666The domains configuration file contains the domains and their configuration,
667and accounts and their configuration. This includes the configured email
668addresses. The mox admin web interface, and the mox command line interface, can
669make changes to this file. Mox automatically reloads this file when it changes.
671Like the static configuration, the example domains.conf printed by this command
672needs modifications to make it valid.
674 if len(c.Parse()) != 0 {
678 var dc config.Dynamic
679 err := sconf.Describe(os.Stdout, &dc)
680 xcheckf(err, "describing config")
683func cmdConfigPrintservice(c *cmd) {
684 c.params = ">mox.service"
685 c.help = `Prints a systemd unit service file for mox.
687This is the same file as generated using quickstart. If the systemd service file
688has changed with a newer version of mox, use this command to generate an up to
691 if len(c.Parse()) != 0 {
695 pwd, err := os.Getwd()
697 log.Printf("current working directory: %v", err)
700 service := strings.ReplaceAll(moxService, "/home/mox", pwd)
704func cmdConfigDomainAdd(c *cmd) {
705 c.params = "[-disabled] $domain $account [$localpart]"
706 c.help = `Adds a new domain to the configuration and reloads the configuration.
708The account is used for the postmaster mailboxes the domain, including as DMARC and
709TLS reporting. Localpart is the "username" at the domain for this account. If
710must be set if and only if account does not yet exist.
712The domain can be created in disabled mode, preventing automatically requesting
713TLS certificates with ACME, and rejecting incoming/outgoing messages involving
714the domain, but allowing further configuration of the domain.
717 c.flag.BoolVar(&disabled, "disabled", false, "disable the new domain")
719 if len(args) != 2 && len(args) != 3 {
723 d := xparseDomain(args[0], "domain")
725 var localpart smtp.Localpart
728 localpart, err = smtp.ParseLocalpart(args[2])
729 xcheckf(err, "parsing localpart")
731 ctlcmdConfigDomainAdd(xctl(), disabled, d, args[1], localpart)
734func ctlcmdConfigDomainAdd(ctl *ctl, disabled bool, domain dns.Domain, account string, localpart smtp.Localpart) {
735 ctl.xwrite("domainadd")
741 ctl.xwrite(domain.Name())
743 ctl.xwrite(string(localpart))
745 fmt.Printf("domain added, remember to add dns records, see:\n\nmox config dnsrecords %s\nmox config dnscheck %s\n", domain.Name(), domain.Name())
748func cmdConfigDomainRemove(c *cmd) {
750 c.help = `Remove a domain from the configuration and reload the configuration.
752This is a dangerous operation. Incoming email delivery for this domain will be
760 d := xparseDomain(args[0], "domain")
762 ctlcmdConfigDomainRemove(xctl(), d)
765func ctlcmdConfigDomainRemove(ctl *ctl, d dns.Domain) {
766 ctl.xwrite("domainrm")
769 fmt.Printf("domain removed, remember to remove dns records for %s\n", d)
772func cmdConfigDomainDisable(c *cmd) {
774 c.help = `Disable a domain and reload the configuration.
776This is a dangerous operation. Incoming/outgoing messages involving this domain
784 d := xparseDomain(args[0], "domain")
786 ctlcmdConfigDomainDisabled(xctl(), d, true)
787 fmt.Printf("domain disabled")
790func cmdConfigDomainEnable(c *cmd) {
792 c.help = `Enable a domain and reload the configuration.
794Incoming/outgoing messages involving this domain will be accepted again.
801 d := xparseDomain(args[0], "domain")
803 ctlcmdConfigDomainDisabled(xctl(), d, false)
806func ctlcmdConfigDomainDisabled(ctl *ctl, d dns.Domain, disabled bool) {
807 ctl.xwrite("domaindisabled")
817func cmdConfigAliasList(c *cmd) {
819 c.help = `Show aliases (lists) for domain.`
826 ctlcmdConfigAliasList(xctl(), args[0])
829func ctlcmdConfigAliasList(ctl *ctl, address string) {
830 ctl.xwrite("aliaslist")
833 ctl.xstreamto(os.Stdout)
836func cmdConfigAliasPrint(c *cmd) {
838 c.help = `Print settings and members of alias (list).`
845 ctlcmdConfigAliasPrint(xctl(), args[0])
848func ctlcmdConfigAliasPrint(ctl *ctl, address string) {
849 ctl.xwrite("aliasprint")
852 ctl.xstreamto(os.Stdout)
855func cmdConfigAliasAdd(c *cmd) {
856 c.params = "$alias@domain $rcpt1@domain ..."
857 c.help = `Add new alias (list) with one or more addresses and public posting enabled.
859An alias is used for delivering incoming email to multiple recipients. If you
860want to add an address to an account, don't use an alias, just add the address
868 alias := config.Alias{PostPublic: true, Addresses: args[1:]}
871 ctlcmdConfigAliasAdd(xctl(), args[0], alias)
874func ctlcmdConfigAliasAdd(ctl *ctl, address string, alias config.Alias) {
875 ctl.xwrite("aliasadd")
877 xctlwriteJSON(ctl, alias)
881func cmdConfigAliasUpdate(c *cmd) {
882 c.params = "$alias@domain [-postpublic false|true -listmembers false|true -allowmsgfrom false|true]"
883 c.help = `Update alias (list) configuration.`
884 var postpublic, listmembers, allowmsgfrom string
885 c.flag.StringVar(&postpublic, "postpublic", "", "whether anyone or only list members can post")
886 c.flag.StringVar(&listmembers, "listmembers", "", "whether list members can list members")
887 c.flag.StringVar(&allowmsgfrom, "allowmsgfrom", "", "whether alias address can be used in message from header")
895 ctlcmdConfigAliasUpdate(xctl(), alias, postpublic, listmembers, allowmsgfrom)
898func ctlcmdConfigAliasUpdate(ctl *ctl, alias, postpublic, listmembers, allowmsgfrom string) {
899 ctl.xwrite("aliasupdate")
901 ctl.xwrite(postpublic)
902 ctl.xwrite(listmembers)
903 ctl.xwrite(allowmsgfrom)
907func cmdConfigAliasRemove(c *cmd) {
908 c.params = "$alias@domain"
909 c.help = "Remove alias (list)."
916 ctlcmdConfigAliasRemove(xctl(), args[0])
919func ctlcmdConfigAliasRemove(ctl *ctl, alias string) {
920 ctl.xwrite("aliasrm")
925func cmdConfigAliasAddaddr(c *cmd) {
926 c.params = "$alias@domain $rcpt1@domain ..."
927 c.help = `Add addresses to alias (list).`
934 ctlcmdConfigAliasAddaddr(xctl(), args[0], args[1:])
937func ctlcmdConfigAliasAddaddr(ctl *ctl, alias string, addresses []string) {
938 ctl.xwrite("aliasaddaddr")
940 xctlwriteJSON(ctl, addresses)
944func cmdConfigAliasRemoveaddr(c *cmd) {
945 c.params = "$alias@domain $rcpt1@domain ..."
946 c.help = `Remove addresses from alias (list).`
953 ctlcmdConfigAliasRmaddr(xctl(), args[0], args[1:])
956func ctlcmdConfigAliasRmaddr(ctl *ctl, alias string, addresses []string) {
957 ctl.xwrite("aliasrmaddr")
959 xctlwriteJSON(ctl, addresses)
963func cmdConfigAccountAdd(c *cmd) {
964 c.params = "$account $address"
965 c.help = `Add an account with an email address and reload the configuration.
967Email can be delivered to this address/account. A password has to be configured
968explicitly, see the setaccountpassword command.
976 ctlcmdConfigAccountAdd(xctl(), args[0], args[1])
979func ctlcmdConfigAccountAdd(ctl *ctl, account, address string) {
980 ctl.xwrite("accountadd")
984 fmt.Printf("account added, set a password with \"mox setaccountpassword %s\"\n", account)
987func cmdConfigAccountRemove(c *cmd) {
988 c.params = "$account"
989 c.help = `Remove an account and reload the configuration.
991Email addresses for this account will also be removed, and incoming email for
992these addresses will be rejected.
994All data for the account will be removed.
1002 ctlcmdConfigAccountRemove(xctl(), args[0])
1005func ctlcmdConfigAccountRemove(ctl *ctl, account string) {
1006 ctl.xwrite("accountrm")
1009 fmt.Println("account removed")
1012func cmdConfigAccountList(c *cmd) {
1013 c.help = `List all accounts.
1015Each account is printed on a line, with optional additional tab-separated
1016information, such as "(disabled)".
1024 ctlcmdConfigAccountList(xctl())
1027func ctlcmdConfigAccountList(ctl *ctl) {
1028 ctl.xwrite("accountlist")
1030 ctl.xstreamto(os.Stdout)
1033func cmdConfigAccountAddresses(c *cmd) {
1034 c.params = "$account"
1035 c.help = `List all addresses for an account.
1037Each address is printed on a line.
1038An address starting with an "@" indicate it is a catchall address for the domain.
1040Does not check whether account is disabled.
1048 ctlcmdConfigAccountAddresses(xctl(), args[0])
1051func ctlcmdConfigAccountAddresses(ctl *ctl, account string) {
1052 ctl.xwrite("accountaddresses")
1055 ctl.xstreamto(os.Stdout)
1058func cmdConfigAccountDisable(c *cmd) {
1059 c.params = "$account $message"
1060 c.help = `Disable login for an account, showing message to users when they try to login.
1062Incoming email will still be accepted for the account, and queued email from the
1063account will still be delivered. No new login sessions are possible.
1065Message must be non-empty, ascii-only without control characters including
1066newline, and maximum 256 characters because it is used in SMTP/IMAP.
1073 log.Fatalf("message must be non-empty")
1077 ctlcmdConfigAccountDisabled(xctl(), args[0], args[1])
1078 fmt.Println("account disabled")
1081func cmdConfigAccountEnable(c *cmd) {
1082 c.params = "$account"
1083 c.help = `Enable login again for an account.
1085Login attempts by the user no long result in an error message.
1093 ctlcmdConfigAccountDisabled(xctl(), args[0], "")
1094 fmt.Println("account enabled")
1097func ctlcmdConfigAccountDisabled(ctl *ctl, account, loginDisabled string) {
1098 ctl.xwrite("accountdisabled")
1100 ctl.xwrite(loginDisabled)
1104func cmdConfigTlspubkeyList(c *cmd) {
1105 c.params = "[$account]"
1106 c.help = `List TLS public keys for TLS client certificate authentication.
1108If account is absent, the TLS public keys for all accounts are listed.
1111 var accountOpt string
1113 accountOpt = args[0]
1114 } else if len(args) > 1 {
1119 ctlcmdConfigTlspubkeyList(xctl(), accountOpt)
1122func ctlcmdConfigTlspubkeyList(ctl *ctl, accountOpt string) {
1123 ctl.xwrite("tlspubkeylist")
1124 ctl.xwrite(accountOpt)
1126 ctl.xstreamto(os.Stdout)
1129func cmdConfigTlspubkeyGet(c *cmd) {
1130 c.params = "$fingerprint"
1131 c.help = `Get a TLS public key for a fingerprint.
1133Prints the type, name, account and address for the key, and the certificate in
1142 ctlcmdConfigTlspubkeyGet(xctl(), args[0])
1145func ctlcmdConfigTlspubkeyGet(ctl *ctl, fingerprint string) {
1146 ctl.xwrite("tlspubkeyget")
1147 ctl.xwrite(fingerprint)
1151 account := ctl.xread()
1152 address := ctl.xread()
1153 noimappreauth := ctl.xread()
1157 var block *pem.Block
1160 Type: "CERTIFICATE",
1165 fmt.Printf("type: %s\nname: %s\naccount: %s\naddress: %s\nno imap preauth: %s\n", typ, name, account, address, noimappreauth)
1167 fmt.Printf("certificate:\n\n")
1168 if err := pem.Encode(os.Stdout, block); err != nil {
1169 log.Fatalf("pem encode: %v", err)
1174func cmdConfigTlspubkeyAdd(c *cmd) {
1175 c.params = "$address [$name] < cert.pem"
1176 c.help = `Add a TLS public key to the account of the given address.
1178The public key is read from the certificate.
1180The optional name is a human-readable descriptive name of the key. If absent,
1181the CommonName from the certificate is used.
1183 var noimappreauth bool
1184 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.")
1186 var address, name string
1189 } else if len(args) == 2 {
1190 address, name = args[0], args[1]
1195 buf, err := io.ReadAll(os.Stdin)
1196 xcheckf(err, "reading from stdin")
1197 block, _ := pem.Decode(buf)
1199 err = errors.New("no pem block found")
1200 } else if block.Type != "CERTIFICATE" {
1201 err = fmt.Errorf("unexpected type %q, expected CERTIFICATE", block.Type)
1203 xcheckf(err, "parsing pem")
1206 ctlcmdConfigTlspubkeyAdd(xctl(), address, name, noimappreauth, block.Bytes)
1209func ctlcmdConfigTlspubkeyAdd(ctl *ctl, address, name string, noimappreauth bool, certDER []byte) {
1210 ctl.xwrite("tlspubkeyadd")
1213 ctl.xwrite(fmt.Sprintf("%v", noimappreauth))
1214 ctl.xstreamfrom(bytes.NewReader(certDER))
1218func cmdConfigTlspubkeyRemove(c *cmd) {
1219 c.params = "$fingerprint"
1220 c.help = `Remove TLS public key for fingerprint.`
1227 ctlcmdConfigTlspubkeyRemove(xctl(), args[0])
1230func ctlcmdConfigTlspubkeyRemove(ctl *ctl, fingerprint string) {
1231 ctl.xwrite("tlspubkeyrm")
1232 ctl.xwrite(fingerprint)
1236func cmdConfigTlspubkeyGen(c *cmd) {
1238 c.help = `Generate an ed25519 private key and minimal certificate for use a TLS public key and write to files starting with stem.
1240The private key is written to $stem.$timestamp.ed25519privatekey.pkcs8.pem.
1241The certificate is written to $stem.$timestamp.certificate.pem.
1242The private key and certificate are also written to
1243$stem.$timestamp.ed25519privatekey-certificate.pem.
1245The certificate can be added to an account with "mox config account tlspubkey add".
1247The combined file can be used with "mox sendmail".
1249The private key is also written to standard error in raw-url-base64-encoded
1250form, also for use with "mox sendmail". The fingerprint is written to standard
1251error too, for reference.
1259 timestamp := time.Now().Format("200601021504")
1260 prefix := stem + "." + timestamp
1262 seed := make([]byte, ed25519.SeedSize)
1263 cryptorand.Read(seed)
1264 privKey := ed25519.NewKeyFromSeed(seed)
1265 privKeyBuf, err := x509.MarshalPKCS8PrivateKey(privKey)
1266 xcheckf(err, "marshal private key as pkcs8")
1268 err = pem.Encode(&b, &pem.Block{Type: "PRIVATE KEY", Bytes: privKeyBuf})
1269 xcheckf(err, "marshal pkcs8 private key to pem")
1270 privKeyBufPEM := b.Bytes()
1272 certBuf, tlsCert := xminimalCert(privKey)
1274 err = pem.Encode(&b, &pem.Block{Type: "CERTIFICATE", Bytes: certBuf})
1275 xcheckf(err, "marshal certificate to pem")
1276 certBufPEM := b.Bytes()
1278 xwriteFile := func(p string, data []byte, what string) {
1279 log.Printf("writing %s", p)
1280 err = os.WriteFile(p, data, 0600)
1281 xcheckf(err, "writing %s file: %v", what, err)
1284 xwriteFile(prefix+".ed25519privatekey.pkcs8.pem", privKeyBufPEM, "private key")
1285 xwriteFile(prefix+".certificate.pem", certBufPEM, "certificate")
1286 combinedPEM := slices.Concat(privKeyBufPEM, certBufPEM)
1287 xwriteFile(prefix+".ed25519privatekey-certificate.pem", combinedPEM, "combined private key and certificate")
1289 shabuf := sha256.Sum256(tlsCert.Leaf.RawSubjectPublicKeyInfo)
1291 _, err = fmt.Fprintf(os.Stderr, "ed25519 private key as raw-url-base64: %s\ned25519 public key fingerprint: %s\n",
1292 base64.RawURLEncoding.EncodeToString(seed),
1293 base64.RawURLEncoding.EncodeToString(shabuf[:]),
1295 xcheckf(err, "write private key and public key fingerprint")
1298func cmdConfigAddressAdd(c *cmd) {
1299 c.params = "$address $account"
1300 c.help = `Adds an address to an account and reloads the configuration.
1302If address starts with a @ (i.e. a missing localpart), this is a catchall
1303address for the domain.
1311 ctlcmdConfigAddressAdd(xctl(), args[0], args[1])
1314func ctlcmdConfigAddressAdd(ctl *ctl, address, account string) {
1315 ctl.xwrite("addressadd")
1319 fmt.Println("address added")
1322func cmdConfigAddressRemove(c *cmd) {
1323 c.params = "$address"
1324 c.help = `Remove an address and reload the configuration.
1326Incoming email for this address will be rejected after removing an address.
1334 ctlcmdConfigAddressRemove(xctl(), args[0])
1337func ctlcmdConfigAddressRemove(ctl *ctl, address string) {
1338 ctl.xwrite("addressrm")
1341 fmt.Println("address removed")
1344func cmdConfigAddressAccount(c *cmd) {
1345 c.params = "$address"
1346 c.help = `Print the account an address belongs to.
1348Catchall addresses and the account catch all separator are considered when
1349looking up the account.
1351Does not check whether account is disabled.
1359 ctlcmdConfigAddressAccount(xctl(), args[0])
1362func ctlcmdConfigAddressAccount(ctl *ctl, address string) {
1363 ctl.xwrite("addressaccount")
1366 account := ctl.xread()
1367 fmt.Println(account)
1370func cmdConfigDNSRecords(c *cmd) {
1371 c.params = "$domain"
1372 c.help = `Prints annotated DNS records as zone file that should be created for the domain.
1374The zone file can be imported into existing DNS software. You should review the
1375DNS records, especially if your domain previously/currently has email
1383 d := xparseDomain(args[0], "domain")
1385 domConf, ok := mox.Conf.Domain(d)
1387 log.Fatalf("unknown domain")
1390 resolver := dns.StrictResolver{Pkg: "main"}
1391 _, result, err := resolver.LookupTXT(context.Background(), d.ASCII+".")
1392 if !dns.IsNotFound(err) {
1393 xcheckf(err, "looking up record for dnssec-status")
1396 var certIssuerDomainName, acmeAccountURI string
1397 public := mox.Conf.Static.Listeners["public"]
1398 if public.TLS != nil && public.TLS.ACME != "" {
1399 acme, ok := mox.Conf.Static.ACME[public.TLS.ACME]
1400 if ok && acme.Manager.Manager.Client != nil {
1401 certIssuerDomainName = acme.IssuerDomainName
1402 acc, err := acme.Manager.Manager.Client.GetReg(context.Background(), "")
1403 c.log.Check(err, "get public acme account")
1405 acmeAccountURI = acc.URI
1410 records, err := admin.DomainRecords(domConf, d, result.Authentic, certIssuerDomainName, acmeAccountURI)
1411 xcheckf(err, "records")
1412 fmt.Print(strings.Join(records, "\n") + "\n")
1415func cmdConfigDNSCheck(c *cmd) {
1416 c.params = "$domain"
1417 c.help = "Check the DNS records with the configuration for the domain, and print any errors/warnings."
1423 d := xparseDomain(args[0], "domain")
1425 _, ok := mox.Conf.Domain(d)
1427 log.Fatalf("unknown domain")
1430 // todo future: move http.Admin.CheckDomain to mox- and make it return a regular error.
1436 err, ok := x.(*sherpa.Error)
1440 log.Fatalf("%s", err)
1443 printResult := func(name string, r webadmin.Result) {
1444 if len(r.Errors) == 0 && len(r.Warnings) == 0 {
1447 fmt.Printf("# %s\n", name)
1448 for _, s := range r.Errors {
1449 fmt.Printf("error: %s\n", s)
1451 for _, s := range r.Warnings {
1452 fmt.Printf("warning: %s\n", s)
1456 result := webadmin.Admin{}.CheckDomain(context.Background(), args[0])
1457 printResult("DNSSEC", result.DNSSEC.Result)
1458 printResult("IPRev", result.IPRev.Result)
1459 printResult("MX", result.MX.Result)
1460 printResult("TLS", result.TLS.Result)
1461 printResult("DANE", result.DANE.Result)
1462 printResult("SPF", result.SPF.Result)
1463 printResult("DKIM", result.DKIM.Result)
1464 printResult("DMARC", result.DMARC.Result)
1465 printResult("Host TLSRPT", result.HostTLSRPT.Result)
1466 printResult("Domain TLSRPT", result.DomainTLSRPT.Result)
1467 printResult("MTASTS", result.MTASTS.Result)
1468 printResult("SRV conf", result.SRVConf.Result)
1469 printResult("Autoconf", result.Autoconf.Result)
1470 printResult("Autodiscover", result.Autodiscover.Result)
1473func cmdConfigEnsureACMEHostprivatekeys(c *cmd) {
1475 c.help = `Ensure host private keys exist for TLS listeners with ACME.
1477In mox.conf, each listener can have TLS configured. Long-lived private key files
1478can be specified, which will be used when requesting ACME certificates.
1479Configuring these private keys makes it feasible to publish DANE TLSA records
1480for the corresponding public keys in DNS, protected with DNSSEC, allowing TLS
1481certificate verification without depending on a list of Certificate Authorities
1482(CAs). Previous versions of mox did not pre-generate private keys for use with
1483ACME certificates, but would generate private keys on-demand. By explicitly
1484configuring private keys, they will not change automatedly with new
1485certificates, and the DNS TLSA records stay valid.
1487This command looks for listeners in mox.conf with TLS with ACME configured. For
1488each missing host private key (of type rsa-2048 and ecdsa-p256) a key is written
1489to config/hostkeys/. If a certificate exists in the ACME "cache", its private
1490key is copied. Otherwise a new private key is generated. Snippets for manually
1491updating/editing mox.conf are printed.
1493After running this command, and updating mox.conf, run "mox config dnsrecords"
1494for a domain and create the TLSA DNS records it suggests to enable DANE.
1501 // Load a private key from p, in various forms. We only look at the first PEM
1502 // block. Files with only a private key, or with multiple blocks but private key
1503 // first like autocert does, can be loaded.
1504 loadPrivateKey := func(f *os.File) (any, error) {
1505 buf, err := io.ReadAll(f)
1507 return nil, fmt.Errorf("reading private key file: %v", err)
1509 block, _ := pem.Decode(buf)
1511 return nil, fmt.Errorf("no pem block found in pem file")
1515 case "EC PRIVATE KEY":
1516 privKey, err = x509.ParseECPrivateKey(block.Bytes)
1517 case "RSA PRIVATE KEY":
1518 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
1520 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
1522 return nil, fmt.Errorf("unrecognized pem block type %q", block.Type)
1525 return nil, fmt.Errorf("parsing private key of type %q: %v", block.Type, err)
1530 // Either load a private key from file, or if it doesn't exist generate a new
1532 xtryLoadPrivateKey := func(kt autocert.KeyType, p string) any {
1533 f, err := os.Open(p)
1534 if err != nil && errors.Is(err, fs.ErrNotExist) {
1536 case autocert.KeyRSA2048:
1537 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1538 xcheckf(err, "generating new 2048-bit rsa private key")
1540 case autocert.KeyECDSAP256:
1541 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1542 xcheckf(err, "generating new ecdsa p-256 private key")
1545 log.Fatalf("unexpected keytype %v", kt)
1548 xcheckf(err, "%s: open acme key and certificate file", p)
1550 // Load private key from file. autocert stores a PEM file that starts with a
1551 // private key, followed by certificate(s). So we can just read it and should find
1552 // the private key we are looking for.
1553 privKey, err := loadPrivateKey(f)
1554 if xerr := f.Close(); xerr != nil {
1555 log.Printf("closing private key file: %v", xerr)
1557 xcheckf(err, "parsing private key from acme key and certificate file")
1559 switch k := privKey.(type) {
1560 case *rsa.PrivateKey:
1561 if k.N.BitLen() == 2048 {
1564 log.Printf("warning: rsa private key in %s has %d bits, skipping and generating new 2048-bit rsa private key", p, k.N.BitLen())
1565 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
1566 xcheckf(err, "generating new 2048-bit rsa private key")
1568 case *ecdsa.PrivateKey:
1569 if k.Curve == elliptic.P256() {
1572 log.Printf("warning: ecdsa private key in %s has curve %v, skipping and generating new p-256 ecdsa key", p, k.Curve.Params().Name)
1573 privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
1574 xcheckf(err, "generating new ecdsa p-256 private key")
1577 log.Fatalf("%s: unexpected private key file of type %T", p, privKey)
1582 // Write privKey as PKCS#8 private key to p. Only if file does not yet exist.
1583 writeHostPrivateKey := func(privKey any, p string) error {
1584 os.MkdirAll(filepath.Dir(p), 0700)
1585 f, err := os.OpenFile(p, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
1587 return fmt.Errorf("create: %v", err)
1591 if err := f.Close(); err != nil {
1592 log.Printf("closing new hostkey file %s after error: %v", p, err)
1594 if err := os.Remove(p); err != nil {
1595 log.Printf("removing new hostkey file %s after error: %v", p, err)
1599 buf, err := x509.MarshalPKCS8PrivateKey(privKey)
1601 return fmt.Errorf("marshal private host key: %v", err)
1604 Type: "PRIVATE KEY",
1607 if err := pem.Encode(f, &block); err != nil {
1608 return fmt.Errorf("write as pem: %v", err)
1610 if err := f.Close(); err != nil {
1611 return fmt.Errorf("close: %v", err)
1618 timestamp := time.Now().Format("20060102T150405")
1620 for listenerName, l := range mox.Conf.Static.Listeners {
1621 if l.TLS == nil || l.TLS.ACME == "" {
1624 haveKeyTypes := map[autocert.KeyType]bool{}
1625 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
1626 p := mox.ConfigDirPath(privKeyFile)
1627 f, err := os.Open(p)
1628 xcheckf(err, "open host private key")
1629 privKey, err := loadPrivateKey(f)
1630 if err := f.Close(); err != nil {
1631 log.Printf("closing host private key file: %v", err)
1633 xcheckf(err, "loading host private key")
1634 switch k := privKey.(type) {
1635 case *rsa.PrivateKey:
1636 if k.N.BitLen() == 2048 {
1637 haveKeyTypes[autocert.KeyRSA2048] = true
1639 case *ecdsa.PrivateKey:
1640 if k.Curve == elliptic.P256() {
1641 haveKeyTypes[autocert.KeyECDSAP256] = true
1645 created := []string{}
1646 for _, kt := range []autocert.KeyType{autocert.KeyRSA2048, autocert.KeyECDSAP256} {
1647 if haveKeyTypes[kt] {
1650 // Lookup key in ACME cache.
1651 host := l.HostnameDomain
1652 if host.ASCII == "" {
1653 host = mox.Conf.Static.HostnameDomain
1655 filename := host.ASCII
1657 if kt == autocert.KeyRSA2048 {
1661 p := mox.DataDirPath(filepath.Join("acme", "keycerts", l.TLS.ACME, filename))
1662 privKey := xtryLoadPrivateKey(kt, p)
1664 relPath := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", host.Name(), timestamp, kind))
1665 destPath := mox.ConfigDirPath(relPath)
1666 err := writeHostPrivateKey(privKey, destPath)
1667 xcheckf(err, "writing host private key file to %s: %v", destPath, err)
1668 created = append(created, relPath)
1669 fmt.Printf("Wrote host private key: %s\n", destPath)
1671 didCreate = didCreate || len(created) > 0
1672 if len(created) > 0 {
1674 HostPrivateKeyFiles: append(l.TLS.HostPrivateKeyFiles, created...),
1676 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)
1677 err := sconf.Write(os.Stdout, tls)
1678 xcheckf(err, "writing new TLS.HostPrivateKeyFiles section")
1684After updating mox.conf and restarting, run "mox config dnsrecords" for a
1685domain and create the TLSA DNS records it suggests to enable DANE.
1690func cmdLoglevels(c *cmd) {
1691 c.params = "[$level [$pkg]]"
1692 c.help = `Print the log levels, or set a new default log level, or a level for the given package.
1694By default, a single log level applies to all logging in mox. But for each
1695"pkg", an overriding log level can be configured. Examples of packages:
1696smtpserver, smtpclient, queue, imapserver, spf, dkim, dmarc, junk, message,
1699Specify a pkg and an empty level to clear the configured level for a package.
1701Valid labels: error, info, debug, trace, traceauth, tracedata.
1710 ctlcmdLoglevels(xctl())
1716 ctlcmdSetLoglevels(xctl(), pkg, args[0])
1720func ctlcmdLoglevels(ctl *ctl) {
1721 ctl.xwrite("loglevels")
1723 ctl.xstreamto(os.Stdout)
1726func ctlcmdSetLoglevels(ctl *ctl, pkg, level string) {
1727 ctl.xwrite("setloglevels")
1733func cmdStop(c *cmd) {
1734 c.help = `Shut mox down, giving connections maximum 3 seconds to stop before closing them.
1736While shutting down, new IMAP and SMTP connections will get a status response
1737indicating temporary unavailability. Existing connections will get a 3 second
1738period to finish their transaction and shut down. Under normal circumstances,
1739only IMAP has long-living connections, with the IDLE command to get notified of
1742 if len(c.Parse()) != 0 {
1749 // Read will hang until remote has shut down.
1750 buf := make([]byte, 128)
1751 n, err := xctl.conn.Read(buf)
1753 log.Fatalf("expected eof after graceful shutdown, got data %q", buf[:n])
1754 } else if err != io.EOF {
1755 log.Fatalf("expected eof after graceful shutdown, got error %v", err)
1757 fmt.Println("mox stopped")
1760func cmdBackup(c *cmd) {
1761 c.params = "$destdir"
1762 c.help = `Creates a backup of the config and data directory.
1764Backup copies the config directory to <destdir>/config, and creates
1765<destdir>/data with a consistent snapshot of the databases and message files
1766and copies other files from the data directory. Empty directories are not
1767copied. The backup can then be stored elsewhere for long-term storage, or used
1768to fall back to should an upgrade fail. Simply copying files in the data
1769directory while mox is running can result in unusable database files.
1771Message files never change (they are read-only, though can be removed) and are
1772hard-linked so they don't consume additional space. If hardlinking fails, for
1773example when the backup destination directory is on a different file system, a
1774regular copy is made. Using a destination directory like "data/tmp/backup"
1775increases the odds hardlinking succeeds: the default systemd service file
1776specifically mounts the data directory, causing attempts to hardlink outside it
1777to fail with an error about cross-device linking.
1779All files in the data directory that aren't recognized (i.e. other than known
1780database files, message files, an acme directory, the "tmp" directory, etc),
1781are stored, but with a warning.
1783Remove files in the destination directory before doing another backup. The
1784backup command will not overwrite files, but print and return errors.
1786Exit code 0 indicates the backup was successful. A clean successful backup does
1787not print any output, but may print warnings. Use the -verbose flag for
1788details, including timing.
1790To restore a backup, first shut down mox, move away the old data directory and
1791move an earlier backed up directory in its place, run "mox verifydata
1792<datadir>", possibly with the "-fix" option, and restart mox. After the
1793restore, you may also want to run "mox bumpuidvalidity" for each account for
1794which messages in a mailbox changed, to force IMAP clients to synchronize
1797Before upgrading, to check if the upgrade will likely succeed, first make a
1798backup, then use the new mox binary to run "mox verifydata <backupdir>/data".
1799This can change the backup files (e.g. upgrade database files, move away
1800unrecognized message files), so you should make a new backup before actually
1805 c.flag.BoolVar(&verbose, "verbose", false, "print progress")
1812 dstDataDir, err := filepath.Abs(args[0])
1813 xcheckf(err, "making path absolute")
1815 ctlcmdBackup(xctl(), dstDataDir, verbose)
1818func ctlcmdBackup(ctl *ctl, dstDataDir string, verbose bool) {
1819 ctl.xwrite("backup")
1820 ctl.xwrite(dstDataDir)
1822 ctl.xwrite("verbose")
1826 ctl.xstreamto(os.Stdout)
1830func cmdSetadminpassword(c *cmd) {
1831 c.help = `Set a new admin password, for the web interface.
1833The password is read from stdin. Its bcrypt hash is stored in a file named
1834"adminpasswd" in the configuration directory.
1836 if len(c.Parse()) != 0 {
1841 path := mox.ConfigDirPath(mox.Conf.Static.AdminPasswordFile)
1843 log.Fatal("no admin password file configured")
1846 pw := xreadpassword()
1847 pw, err := precis.OpaqueString.String(pw)
1848 xcheckf(err, `checking password with "precis" requirements`)
1849 hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
1850 xcheckf(err, "generating hash for password")
1851 err = os.WriteFile(path, hash, 0660)
1852 xcheckf(err, "writing hash to admin password file")
1855func xreadpassword() string {
1857Type new password. Password WILL echo.
1859WARNING: Bots will try to bruteforce your password. Connections with failed
1860authentication attempts will be rate limited but attackers WILL find passwords
1861reused at other services and weak passwords. If your account is compromised,
1862spammers are likely to abuse your system, spamming your address and the wider
1863internet in your name. So please pick a random, unguessable password, preferably
1864at least 12 characters.
1867 fmt.Printf("password: ")
1868 scanner := bufio.NewScanner(os.Stdin)
1869 // The default splitter for scanners is one that splits by lines, so we
1870 // don't have to set up another one here.
1872 // We discard the return value of Scan() since failing to tokenize could
1873 // either mean reaching EOF but no newline (which can be legitimate if the
1874 // CLI was programatically called to set the password, but with no trailing
1875 // newline), or an actual error. We can distinguish between the two by
1876 // calling Err() since it will return nil if it were EOF, but the actual
1879 xcheckf(scanner.Err(), "reading stdin")
1880 // No need to trim, the scanner does not return the token in the output.
1881 pw := scanner.Text()
1883 log.Fatal("password must be at least 8 characters")
1888func cmdSetaccountpassword(c *cmd) {
1889 c.params = "$account"
1890 c.help = `Set new password an account.
1892The password is read from stdin. Secrets derived from the password, but not the
1893password itself, are stored in the account database. The stored secrets are for
1894authentication with: scram-sha-256, scram-sha-1, cram-md5, plain text (bcrypt
1897The parameter is an account name, as configured under Accounts in domains.conf
1898and as present in the data/accounts/ directory, not a configured email address
1907 pw := xreadpassword()
1909 ctlcmdSetaccountpassword(xctl(), args[0], pw)
1912func ctlcmdSetaccountpassword(ctl *ctl, account, password string) {
1913 ctl.xwrite("setaccountpassword")
1915 ctl.xwrite(password)
1919func cmdDeliver(c *cmd) {
1921 c.params = "$address < message"
1922 c.help = "Deliver message to address."
1928 ctlcmdDeliver(xctl(), args[0])
1931func ctlcmdDeliver(ctl *ctl, address string) {
1932 ctl.xwrite("deliver")
1935 ctl.xstreamfrom(os.Stdin)
1938 fmt.Println("message delivered")
1940 log.Fatalf("deliver: %s", line)
1944func cmdDKIMGenrsa(c *cmd) {
1945 c.params = ">$selector._domainkey.$domain.rsa2048.privatekey.pkcs8.pem"
1946 c.help = `Generate a new 2048 bit RSA private key for use with DKIM.
1948The generated file is in PEM format, and has a comment it is generated for use
1951 if len(c.Parse()) != 0 {
1955 buf, err := admin.MakeDKIMRSAKey(dns.Domain{}, dns.Domain{})
1956 xcheckf(err, "making rsa private key")
1957 _, err = os.Stdout.Write(buf)
1958 xcheckf(err, "writing rsa private key")
1961// todo: options for specifying the domain this is the mx host of, and enabling dane and/or mta-sts verification
1962func cmdSMTPDial(c *cmd) {
1963 c.params = "$host[:$port]"
1965 var tlsCerts, tlsCiphersuites, tlsCurves, tlsVersionMin, tlsVersionMax, tlsRenegotiation string
1966 var tlsVerify, noTLS, forceTLS, tlsNoSessionTickets, tlsNoDynamicRecordSizing bool
1967 var ehloHostnameStr, remoteHostnameStr string
1969 ciphersuites := map[string]*tls.CipherSuite{}
1970 ciphersuitesInsecure := map[string]*tls.CipherSuite{}
1971 for _, v := range tls.CipherSuites() {
1972 if slices.Contains(v.SupportedVersions, tls.VersionTLS10) || slices.Contains(v.SupportedVersions, tls.VersionTLS11) || slices.Contains(v.SupportedVersions, tls.VersionTLS12) {
1973 ciphersuites[strings.ToLower(v.Name)] = v
1976 for _, v := range tls.InsecureCipherSuites() {
1977 if slices.Contains(v.SupportedVersions, tls.VersionTLS10) || slices.Contains(v.SupportedVersions, tls.VersionTLS11) || slices.Contains(v.SupportedVersions, tls.VersionTLS12) {
1978 ciphersuitesInsecure[strings.ToLower(v.Name)] = v
1982 curves := map[string]tls.CurveID{}
1983 for _, a := range curvesList {
1984 curves[strings.ToLower(a.String())] = a
1987 c.flag.StringVar(&tlsCiphersuites, "tlsciphersuites", "", "ciphersuites to allow, comma-separated, order is ignored, only for TLS 1.2 and earlier, empty value uses TLS stack defaults; values: "+strings.Join(slices.Sorted(maps.Keys(ciphersuites)), ", ")+", and insecure: "+strings.Join(slices.Sorted(maps.Keys(ciphersuitesInsecure)), ", "))
1988 c.flag.StringVar(&tlsCurves, "tlscurves", "", "tls ecc key exchange mechanisms to allow, comma-separated, order is ignored, empty value uses TLS stack defaults; values: curvep256, curvep384, curvep521, x25519, x25519mlkem768")
1989 c.flag.StringVar(&tlsCerts, "tlscerts", "", "path to root ca certificates in pem form, for verification")
1990 c.flag.StringVar(&tlsVersionMin, "tlsversionmin", "", "minimum TLS version, empty value uses TLS stack default; values: tls1.2, etc.")
1991 c.flag.StringVar(&tlsVersionMax, "tlsversionmax", "", "maximum TLS version, empty value uses TLS stack default; values: tls1.2, etc.")
1992 c.flag.BoolVar(&tlsVerify, "tlsverify", false, "verify remote hostname during TLS")
1993 c.flag.BoolVar(&tlsNoSessionTickets, "tlsnosessiontickets", false, "disable TLS session tickets")
1994 c.flag.BoolVar(&tlsNoDynamicRecordSizing, "tlsnodynamicrecordsizing", false, "disable TLS dynamic record sizing")
1995 c.flag.BoolVar(&noTLS, "notls", false, "do not use TLS")
1996 c.flag.BoolVar(&forceTLS, "forcetls", false, "use TLS, even if remote SMTP server does not announce STARTTLS extension")
1997 c.flag.StringVar(&tlsRenegotiation, "tlsrenegotiation", "never", "when to allow renegotiation; only applies to tls1.2 and earlier, not tls1.3; values: never, once, always")
1998 c.flag.StringVar(&ehloHostnameStr, "ehlohostname", "", "our hostname to use during the SMTP EHLO command")
1999 c.flag.StringVar(&remoteHostnameStr, "remotehostname", "", "remote hostname to use for TLS verification, if enabled; the hostname from the parameter is used by default")
2001 c.help = `Dial the address, initialize the SMTP session, including using STARTTLS to enable TLS if the server supports it.
2003If no port is specified, SMTP port 25 is used.
2005Data is copied between connection and stdin/stdout until either side closes the
2008The flags influence the TLS configuration, useful for debugging interoperability
2011No MTA-STS or DANE verification is done.
2013Hint: Use "mox -loglevel trace smtp dial ..." to see the protocol messages
2014exchanged during connection set up.
2021 if noTLS && forceTLS {
2022 log.Fatalf("cannot have both -notls and -forcetls")
2025 parseTLSVersion := func(s string) uint16 {
2028 return tls.VersionTLS10
2030 return tls.VersionTLS11
2032 return tls.VersionTLS12
2034 return tls.VersionTLS13
2038 log.Fatalf("invalid tls version %q", s)
2039 panic("not reached")
2042 tlsConfig := tls.Config{
2043 MinVersion: parseTLSVersion(tlsVersionMin),
2044 MaxVersion: parseTLSVersion(tlsVersionMax),
2045 InsecureSkipVerify: !tlsVerify,
2046 SessionTicketsDisabled: tlsNoSessionTickets,
2047 DynamicRecordSizingDisabled: tlsNoDynamicRecordSizing,
2050 switch tlsRenegotiation {
2052 tlsConfig.Renegotiation = tls.RenegotiateNever
2054 tlsConfig.Renegotiation = tls.RenegotiateOnceAsClient
2056 tlsConfig.Renegotiation = tls.RenegotiateFreelyAsClient
2058 log.Fatalf("invalid value %q for -tlsrenegotation", tlsRenegotiation)
2061 pool := x509.NewCertPool()
2062 pembuf, err := os.ReadFile(tlsCerts)
2063 xcheckf(err, "reading tls certificates")
2064 ok := pool.AppendCertsFromPEM(pembuf)
2066 c.log.Warn("no tls certificates found", slog.String("path", tlsCerts))
2068 tlsConfig.RootCAs = pool
2070 if tlsCiphersuites != "" {
2071 for s := range strings.SplitSeq(tlsCiphersuites, ",") {
2072 s = strings.TrimSpace(s)
2073 c, ok := ciphersuites[s]
2075 c, ok = ciphersuitesInsecure[s]
2078 log.Fatalf("unknown ciphersuite %q", s)
2080 tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, c.ID)
2083 if tlsCurves != "" {
2084 for s := range strings.SplitSeq(tlsCurves, ",") {
2085 s = strings.TrimSpace(s)
2086 if c, ok := curves[s]; !ok {
2087 log.Fatalf("unknown ecc key exchange algorithm %q", s)
2089 tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, c)
2094 var host, portStr string
2096 host, portStr, err = net.SplitHostPort(args[0])
2101 port, err := strconv.ParseInt(portStr, 10, 64)
2102 xcheckf(err, "parsing port %q", portStr)
2104 if remoteHostnameStr == "" {
2105 remoteHostnameStr = host
2107 remoteHostname, err := dns.ParseDomain(remoteHostnameStr)
2108 xcheckf(err, "parsing remote host")
2109 tlsConfig.ServerName = remoteHostname.Name()
2111 resolver := dns.StrictResolver{Pkg: "smtpdial"}
2112 _, _, _, ips, _, err := smtpclient.GatherIPs(context.Background(), c.log.Logger, resolver, "ip", dns.IPDomain{Domain: remoteHostname}, nil)
2113 xcheckf(err, "resolve host")
2114 c.log.Info("resolved remote address", slog.Any("ips", ips))
2116 dialer := &net.Dialer{Timeout: 5 * time.Second}
2117 dialedIPs := map[string][]net.IP{}
2118 conn, ip, err := smtpclient.Dial(context.Background(), c.log.Logger, dialer, dns.IPDomain{Domain: remoteHostname}, ips, int(port), dialedIPs, nil)
2119 xcheckf(err, "dial")
2120 c.log.Info("connected to remote host", slog.Any("ip", ip))
2122 tlsMode := smtpclient.TLSOpportunistic
2124 tlsMode = smtpclient.TLSRequiredStartTLS
2126 tlsMode = smtpclient.TLSSkip
2128 var ehloHostname dns.Domain
2129 if ehloHostnameStr == "" {
2130 name, err := os.Hostname()
2131 xcheckf(err, "get hostname")
2132 ehloHostnameStr = name
2134 ehloHostname, err = dns.ParseDomain(ehloHostnameStr)
2135 xcheckf(err, "parse hostname")
2137 opts := smtpclient.Opts{
2138 TLSConfig: &tlsConfig,
2140 client, err := smtpclient.New(context.Background(), c.log.Logger, conn, tlsMode, false, ehloHostname, dns.Domain{}, opts)
2141 xcheckf(err, "new smtp client")
2143 cs := client.TLSConnectionState()
2145 c.log.Info("smtp initialized without tls")
2147 c.log.Info("smtp initialized with tls",
2148 slog.String("version", tls.VersionName(cs.Version)),
2149 slog.String("ciphersuite", strings.ToLower(tls.CipherSuiteName(cs.CipherSuite))),
2150 slog.String("sni", cs.ServerName),
2152 for _, chain := range cs.VerifiedChains {
2154 for _, cert := range chain {
2155 s := fmt.Sprintf("dns names %q, common name %q, %s - %s, issuer %q)", strings.Join(cert.DNSNames, ","), cert.Subject.CommonName, cert.NotBefore.Format("2006-01-02T15:04:05"), cert.NotAfter.Format("2006-01-02T15:04:05"), cert.Issuer.CommonName)
2158 c.log.Info("tls certificate verification chain", slog.String("chain", strings.Join(l, "; ")))
2162 conn, err = client.Conn()
2163 xcheckf(err, "get smtp session connection")
2166 _, err := io.Copy(os.Stdout, conn)
2167 xcheckf(err, "copy from connection to stdout")
2169 c.log.Check(err, "closing connection")
2171 _, err = io.Copy(conn, os.Stdin)
2172 xcheckf(err, "copy from stdin to connection")
2175func cmdDANEDial(c *cmd) {
2176 c.params = "$host:$port"
2178 c.flag.StringVar(&usages, "usages", "pkix-ta,pkix-ee,dane-ta,dane-ee", "allowed usages for dane, comma-separated list")
2179 c.help = `Dial the address using TLS with certificate verification using DANE.
2181Data is copied between connection and stdin/stdout until either side closes the
2189 allowedUsages := []adns.TLSAUsage{}
2191 for s := range strings.SplitSeq(usages, ",") {
2192 var usage adns.TLSAUsage
2193 switch strings.ToLower(s) {
2194 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
2195 usage = adns.TLSAUsagePKIXTA
2196 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
2197 usage = adns.TLSAUsagePKIXEE
2198 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
2199 usage = adns.TLSAUsageDANETA
2200 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
2201 usage = adns.TLSAUsageDANEEE
2203 log.Fatalf("unknown dane usage %q", s)
2205 allowedUsages = append(allowedUsages, usage)
2209 pkixRoots, err := x509.SystemCertPool()
2210 xcheckf(err, "get system pkix certificate pool")
2212 resolver := dns.StrictResolver{Pkg: "danedial"}
2213 conn, record, err := dane.Dial(context.Background(), c.log.Logger, resolver, "tcp", args[0], allowedUsages, pkixRoots)
2214 xcheckf(err, "dial")
2215 log.Printf("(connected, verified with %s)", record)
2218 _, err := io.Copy(os.Stdout, conn)
2219 xcheckf(err, "copy from connection to stdout")
2221 c.log.Check(err, "closing connection")
2223 _, err = io.Copy(conn, os.Stdin)
2224 xcheckf(err, "copy from stdin to connection")
2227func cmdDANEDialmx(c *cmd) {
2228 c.params = "$domain [$desthost]"
2229 var ehloHostname string
2230 c.flag.StringVar(&ehloHostname, "ehlohostname", "localhost", "hostname to send in smtp ehlo command")
2231 c.help = `Connect to MX server for domain using STARTTLS verified with DANE.
2233If no destination host is specified, regular delivery logic is used to find the
2234hosts to attempt delivery too. This involves following CNAMEs for the domain,
2235looking up MX records, and possibly falling back to the domain name itself as
2238If a destination host is specified, that is the only candidate host considered
2241With a list of destinations gathered, each is dialed until a successful SMTP
2242session verified with DANE has been initialized, including EHLO and STARTTLS
2245Once connected, data is copied between connection and stdin/stdout, until
2246either side closes the connection.
2248This command follows the same logic as delivery attempts made from the queue,
2249sharing most of its code.
2252 if len(args) != 1 && len(args) != 2 {
2256 ehloDomain := xparseDomain(ehloHostname, "ehlo host name")
2257 origNextHop := xparseDomain(args[0], "domain")
2259 ctxbg := context.Background()
2261 resolver := dns.StrictResolver{}
2263 var expandedNextHopAuthentic bool
2264 var expandedNextHop dns.Domain
2265 var hostPrefs []smtpclient.HostPref
2268 var origNextHopAuthentic bool
2270 haveMX, origNextHopAuthentic, expandedNextHopAuthentic, expandedNextHop, hostPrefs, permanent, err = smtpclient.GatherDestinations(ctxbg, c.log.Logger, resolver, dns.IPDomain{Domain: origNextHop})
2271 status := "temporary"
2273 status = "permanent"
2276 log.Fatalf("gathering destinations: %v (%s)", err, status)
2278 if expandedNextHop != origNextHop {
2279 log.Printf("followed cnames to %s", expandedNextHop)
2282 log.Printf("found mx record, trying mx hosts")
2284 log.Printf("no mx record found, will try to connect to domain directly")
2286 if !origNextHopAuthentic {
2287 log.Fatalf("error: initial domain not dnssec-secure")
2289 if !expandedNextHopAuthentic {
2290 log.Fatalf("error: expanded domain not dnssec-secure")
2294 for _, hp := range hostPrefs {
2295 s := hp.Host.String()
2297 s += fmt.Sprintf(" (pref %d)", hp.Pref)
2301 log.Printf("destinations: %s", strings.Join(l, ", "))
2303 d := xparseDomain(args[1], "destination host")
2304 log.Printf("skipping domain mx/cname lookups, assuming domain is dnssec-protected")
2306 expandedNextHopAuthentic = true
2308 hostPrefs = []smtpclient.HostPref{{Host: dns.IPDomain{Domain: d}, Pref: -1}}
2311 dialedIPs := map[string][]net.IP{}
2312 for _, hp := range hostPrefs {
2315 log.Printf("attempting to connect to %s (pref %d)", host, hp.Pref)
2317 authentic, expandedAuthentic, expandedHost, ips, _, err := smtpclient.GatherIPs(ctxbg, c.log.Logger, resolver, "ip", host, dialedIPs)
2319 log.Printf("resolving ips for %s: %v, skipping", host, err)
2323 log.Printf("no dnssec for ips of %s, skipping", host)
2326 if !expandedAuthentic {
2327 log.Printf("no dnssec for cname-followed ips of %s, skipping", host)
2330 if expandedHost != host.Domain {
2331 log.Printf("host %s cname-expanded to %s", host, expandedHost)
2333 log.Printf("host %s resolved to ips %s, looking up tlsa records", host, ips)
2335 daneRequired, daneRecords, tlsaBaseDomain, err := smtpclient.GatherTLSA(ctxbg, c.log.Logger, resolver, host.Domain, expandedAuthentic, expandedHost)
2337 log.Printf("looking up tlsa records: %s, skipping", err)
2340 tlsMode := smtpclient.TLSRequiredStartTLS
2341 if len(daneRecords) == 0 {
2343 log.Printf("host %s has no tlsa records, skipping", expandedHost)
2346 log.Printf("warning: only unusable tlsa records found, continuing with required tls without certificate verification")
2350 for _, r := range daneRecords {
2351 l = append(l, r.String())
2353 log.Printf("tlsa records: %s", strings.Join(l, "; "))
2356 tlsHostnames := smtpclient.GatherTLSANames(haveMX, expandedNextHopAuthentic, expandedAuthentic, origNextHop, expandedNextHop, host.Domain, tlsaBaseDomain)
2358 for _, name := range tlsHostnames {
2359 l = append(l, name.String())
2361 log.Printf("gathered valid tls certificate names for potential verification with dane-ta: %s", strings.Join(l, ", "))
2363 dialer := &net.Dialer{Timeout: 5 * time.Second}
2364 conn, _, err := smtpclient.Dial(ctxbg, c.log.Logger, dialer, dns.IPDomain{Domain: expandedHost}, ips, 25, dialedIPs, nil)
2366 log.Printf("dial %s: %v, skipping", expandedHost, err)
2369 log.Printf("connected to %s, %s, starting smtp session with ehlo and starttls with dane verification", expandedHost, conn.RemoteAddr())
2371 var verifiedRecord adns.TLSA
2372 opts := smtpclient.Opts{
2373 DANERecords: daneRecords,
2374 DANEMoreHostnames: tlsHostnames[1:],
2375 DANEVerifiedRecord: &verifiedRecord,
2376 RootCAs: mox.Conf.Static.TLS.CertPool,
2379 sc, err := smtpclient.New(ctxbg, c.log.Logger, conn, tlsMode, tlsPKIX, ehloDomain, tlsHostnames[0], opts)
2381 log.Printf("setting up smtp session: %v, skipping", err)
2382 if xerr := conn.Close(); xerr != nil {
2383 log.Printf("closing connection: %v", xerr)
2388 smtpConn, err := sc.Conn()
2390 log.Fatalf("error: taking over smtp connection: %s", err)
2392 log.Printf("tls verified with tlsa record: %s", verifiedRecord)
2393 log.Printf("smtp session initialized and connected to stdin/stdout")
2396 _, err := io.Copy(os.Stdout, smtpConn)
2397 xcheckf(err, "copy from connection to stdout")
2398 if err := smtpConn.Close(); err != nil {
2399 log.Printf("closing smtp connection: %v", err)
2402 _, err = io.Copy(smtpConn, os.Stdin)
2403 xcheckf(err, "copy from stdin to connection")
2406 log.Fatalf("no remaining destinations")
2409func cmdDANEMakeRecord(c *cmd) {
2410 c.params = "$usage $selector $matchtype [certificate.pem | publickey.pem | privatekey.pem]"
2411 c.help = `Print TLSA record for given certificate/key and parameters.
2414- usage: pkix-ta (0), pkix-ee (1), dane-ta (2), dane-ee (3)
2415- selector: cert (0), spki (1)
2416- matchtype: full (0), sha2-256 (1), sha2-512 (2)
2418Common DANE TLSA record parameters are: dane-ee spki sha2-256, or 3 1 1,
2419followed by a sha2-256 hash of the DER-encoded "SPKI" (subject public key info)
2420from the certificate. An example DNS zone file entry:
2422 _25._tcp.example.com. TLSA 3 1 1 133b919c9d65d8b1488157315327334ead8d83372db57465ecabf53ee5748aee
2424The first usable information from the pem file is used to compose the TLSA
2425record. In case of selector "cert", a certificate is required. Otherwise the
2426"subject public key info" (spki) of the first certificate or public or private
2427key (pkcs#8, pkcs#1 or ec private key) is used.
2435 var usage adns.TLSAUsage
2436 switch strings.ToLower(args[0]) {
2437 case "pkix-ta", strconv.Itoa(int(adns.TLSAUsagePKIXTA)):
2438 usage = adns.TLSAUsagePKIXTA
2439 case "pkix-ee", strconv.Itoa(int(adns.TLSAUsagePKIXEE)):
2440 usage = adns.TLSAUsagePKIXEE
2441 case "dane-ta", strconv.Itoa(int(adns.TLSAUsageDANETA)):
2442 usage = adns.TLSAUsageDANETA
2443 case "dane-ee", strconv.Itoa(int(adns.TLSAUsageDANEEE)):
2444 usage = adns.TLSAUsageDANEEE
2446 if v, err := strconv.ParseUint(args[0], 10, 16); err != nil {
2447 log.Fatalf("bad usage %q", args[0])
2449 // Does not influence certificate association data, so we can accept other numbers.
2450 log.Printf("warning: continuing with unrecognized tlsa usage %d", v)
2451 usage = adns.TLSAUsage(v)
2455 var selector adns.TLSASelector
2456 switch strings.ToLower(args[1]) {
2457 case "cert", strconv.Itoa(int(adns.TLSASelectorCert)):
2458 selector = adns.TLSASelectorCert
2459 case "spki", strconv.Itoa(int(adns.TLSASelectorSPKI)):
2460 selector = adns.TLSASelectorSPKI
2462 log.Fatalf("bad selector %q", args[1])
2465 var matchType adns.TLSAMatchType
2466 switch strings.ToLower(args[2]) {
2467 case "full", strconv.Itoa(int(adns.TLSAMatchTypeFull)):
2468 matchType = adns.TLSAMatchTypeFull
2469 case "sha2-256", strconv.Itoa(int(adns.TLSAMatchTypeSHA256)):
2470 matchType = adns.TLSAMatchTypeSHA256
2471 case "sha2-512", strconv.Itoa(int(adns.TLSAMatchTypeSHA512)):
2472 matchType = adns.TLSAMatchTypeSHA512
2474 log.Fatalf("bad matchtype %q", args[2])
2477 buf, err := os.ReadFile(args[3])
2478 xcheckf(err, "reading certificate")
2480 var block *pem.Block
2481 block, buf = pem.Decode(buf)
2485 extra = " (with leftover data from pem file)"
2487 if selector == adns.TLSASelectorCert {
2488 log.Fatalf("no certificate found in pem file%s", extra)
2490 log.Fatalf("no certificate or public or private key found in pem file%s", extra)
2493 var cert *x509.Certificate
2495 if block.Type == "CERTIFICATE" {
2496 cert, err = x509.ParseCertificate(block.Bytes)
2497 xcheckf(err, "parse certificate")
2499 case adns.TLSASelectorCert:
2501 case adns.TLSASelectorSPKI:
2502 data = cert.RawSubjectPublicKeyInfo
2504 } else if selector == adns.TLSASelectorCert {
2505 // We need a certificate, just a public/private key won't do.
2506 log.Printf("skipping pem type %q, certificate is required", block.Type)
2509 var privKey, pubKey any
2513 _, err := x509.ParsePKIXPublicKey(block.Bytes)
2514 xcheckf(err, "parse pkix subject public key info (spki)")
2516 case "EC PRIVATE KEY":
2517 privKey, err = x509.ParseECPrivateKey(block.Bytes)
2518 xcheckf(err, "parse ec private key")
2519 case "RSA PRIVATE KEY":
2520 privKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
2521 xcheckf(err, "parse pkcs#1 rsa private key")
2522 case "RSA PUBLIC KEY":
2523 pubKey, err = x509.ParsePKCS1PublicKey(block.Bytes)
2524 xcheckf(err, "parse pkcs#1 rsa public key")
2526 // PKCS#8 private key
2527 privKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
2528 xcheckf(err, "parse pkcs#8 private key")
2530 log.Printf("skipping unrecognized pem type %q", block.Type)
2534 if pubKey == nil && privKey != nil {
2535 if signer, ok := privKey.(crypto.Signer); !ok {
2536 log.Fatalf("private key of type %T is not a signer, cannot get public key", privKey)
2538 pubKey = signer.Public()
2542 // Should not happen.
2543 log.Fatalf("internal error: did not find private or public key")
2545 data, err = x509.MarshalPKIXPublicKey(pubKey)
2546 xcheckf(err, "marshal pkix subject public key info (spki)")
2551 case adns.TLSAMatchTypeFull:
2552 case adns.TLSAMatchTypeSHA256:
2553 p := sha256.Sum256(data)
2555 case adns.TLSAMatchTypeSHA512:
2556 p := sha512.Sum512(data)
2559 fmt.Printf("%d %d %d %x\n", usage, selector, matchType, data)
2564func cmdDNSLookup(c *cmd) {
2565 c.params = "[ptr | mx | cname | ips | a | aaaa | ns | txt | srv | tlsa] $name"
2566 c.help = `Lookup DNS name of given type.
2568Lookup always prints whether the response was DNSSEC-protected.
2572mox dns lookup ptr 1.1.1.1
2573mox dns lookup mx xmox.nl
2574mox dns lookup txt _dmarc.xmox.nl.
2575mox dns lookup tlsa _25._tcp.xmox.nl
2583 resolver := dns.StrictResolver{Pkg: "dns"}
2585 // like xparseDomain, but treat unparseable domain as an ASCII name so names with
2586 // underscores are still looked up, e,g <selector>._domainkey.<host>.
2587 xdomain := func(s string) dns.Domain {
2588 d, err := dns.ParseDomain(s)
2590 return dns.Domain{ASCII: strings.TrimSuffix(s, ".")}
2595 cmd, name := args[0], args[1]
2599 ip := xparseIP(name, "ip")
2600 ptrs, result, err := resolver.LookupAddr(context.Background(), ip.String())
2602 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2604 fmt.Printf("names (%d, %s):\n", len(ptrs), dnssecStatus(result.Authentic))
2605 for _, ptr := range ptrs {
2606 fmt.Printf("- %s\n", ptr)
2610 name := xdomain(name)
2611 mxl, result, err := resolver.LookupMX(context.Background(), name.ASCII+".")
2613 log.Printf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2614 // We can still have valid records...
2616 fmt.Printf("mx records (%d, %s):\n", len(mxl), dnssecStatus(result.Authentic))
2617 for _, mx := range mxl {
2618 fmt.Printf("- %s, preference %d\n", mx.Host, mx.Pref)
2622 name := xdomain(name)
2623 target, result, err := resolver.LookupCNAME(context.Background(), name.ASCII+".")
2625 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2627 fmt.Printf("%s (%s)\n", target, dnssecStatus(result.Authentic))
2629 case "ips", "a", "aaaa":
2633 } else if cmd == "aaaa" {
2636 name := xdomain(name)
2637 ips, result, err := resolver.LookupIP(context.Background(), network, name.ASCII+".")
2639 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2641 fmt.Printf("records (%d, %s):\n", len(ips), dnssecStatus(result.Authentic))
2642 for _, ip := range ips {
2643 fmt.Printf("- %s\n", ip)
2647 name := xdomain(name)
2648 nsl, result, err := resolver.LookupNS(context.Background(), name.ASCII+".")
2650 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2652 fmt.Printf("ns records (%d, %s):\n", len(nsl), dnssecStatus(result.Authentic))
2653 for _, ns := range nsl {
2654 fmt.Printf("- %s\n", ns)
2658 host := xdomain(name)
2659 l, result, err := resolver.LookupTXT(context.Background(), host.ASCII+".")
2661 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2663 fmt.Printf("txt records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2664 for _, txt := range l {
2665 fmt.Printf("- %s\n", txt)
2669 host := xdomain(name)
2670 _, l, result, err := resolver.LookupSRV(context.Background(), "", "", host.ASCII+".")
2672 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2674 fmt.Printf("srv records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2675 for _, srv := range l {
2676 fmt.Printf("- host %s, port %d, priority %d, weight %d\n", srv.Target, srv.Port, srv.Priority, srv.Weight)
2680 host := xdomain(name)
2681 l, result, err := resolver.LookupTLSA(context.Background(), 0, "", host.ASCII+".")
2683 log.Fatalf("dns lookup: %v (%s)", err, dnssecStatus(result.Authentic))
2685 fmt.Printf("tlsa records (%d, %s):\n", len(l), dnssecStatus(result.Authentic))
2686 for _, tlsa := range l {
2687 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)
2690 log.Fatalf("unknown record type %q", args[0])
2694func cmdDKIMGened25519(c *cmd) {
2695 c.params = ">$selector._domainkey.$domain.ed25519.privatekey.pkcs8.pem"
2696 c.help = `Generate a new ed25519 key for use with DKIM.
2698Ed25519 keys are much smaller than RSA keys of comparable cryptographic
2699strength. This is convenient because of maximum DNS message sizes. At the time
2700of writing, not many mail servers appear to support ed25519 DKIM keys though,
2701so it is recommended to sign messages with both RSA and ed25519 keys.
2703 if len(c.Parse()) != 0 {
2707 buf, err := admin.MakeDKIMEd25519Key(dns.Domain{}, dns.Domain{})
2708 xcheckf(err, "making dkim ed25519 key")
2709 _, err = os.Stdout.Write(buf)
2710 xcheckf(err, "writing dkim ed25519 key")
2713func cmdDKIMTXT(c *cmd) {
2714 c.params = "<$selector._domainkey.$domain.key.pkcs8.pem"
2715 c.help = `Print a DKIM DNS TXT record with the public key derived from the private key read from stdin.
2717The DNS should be configured as a TXT record at $selector._domainkey.$domain.
2719 if len(c.Parse()) != 0 {
2723 privKey, err := parseDKIMKey(os.Stdin)
2724 xcheckf(err, "reading dkim private key from stdin")
2728 Hashes: []string{"sha256"},
2729 Flags: []string{"s"},
2732 switch key := privKey.(type) {
2733 case *rsa.PrivateKey:
2734 r.PublicKey = key.Public()
2735 case ed25519.PrivateKey:
2736 r.PublicKey = key.Public()
2739 log.Fatalf("unsupported private key type %T, must be rsa or ed25519", privKey)
2742 record, err := r.Record()
2743 xcheckf(err, "making record")
2744 fmt.Print("<selector>._domainkey.<your.domain.> TXT ")
2748 s, record = record[:100], record[100:]
2752 fmt.Printf(`"%s" `, s)
2757func parseDKIMKey(r io.Reader) (any, error) {
2758 buf, err := io.ReadAll(r)
2760 return nil, fmt.Errorf("reading pem from stdin: %v", err)
2762 b, _ := pem.Decode(buf)
2764 return nil, fmt.Errorf("decoding pem: %v", err)
2766 privKey, err := x509.ParsePKCS8PrivateKey(b.Bytes)
2768 return nil, fmt.Errorf("parsing private key: %v", err)
2773func cmdDKIMVerify(c *cmd) {
2774 c.params = "$messagefile"
2775 c.help = `Verify the DKIM signatures in a message and print the results.
2777The message is parsed, and the DKIM-Signature headers are validated. Validation
2778of older messages may fail because the DNS records have been removed or changed
2779by now, or because the signature header may have specified an expiration time
2787 msgf, err := os.Open(args[0])
2788 xcheckf(err, "open message")
2790 results, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, false, dkim.DefaultPolicy, msgf, true)
2791 xcheckf(err, "dkim verify")
2793 for _, result := range results {
2795 if result.Sig == nil {
2796 log.Printf("warning: could not parse signature")
2798 sigh, err = result.Sig.Header()
2800 log.Printf("warning: packing signature: %s", err)
2804 if result.Record == nil {
2805 log.Printf("warning: missing DNS record")
2807 txt, err = result.Record.Record()
2809 log.Printf("warning: packing record: %s", err)
2812 fmt.Printf("status %q, err %v\nrecord %q\nheader %s\n", result.Status, result.Err, txt, sigh)
2816func cmdDKIMSign(c *cmd) {
2817 c.params = "$messagefile"
2818 c.help = `Sign a message, adding DKIM-Signature headers based on the domain in the From header.
2820The message is parsed, the domain looked up in the configuration files, and
2821DKIM-Signature headers generated. The message is printed with the DKIM-Signature
2829 msgf, err := os.Open(args[0])
2830 xcheckf(err, "open message")
2832 if err := msgf.Close(); err != nil {
2833 log.Printf("closing message file: %v", err)
2837 p, err := message.Parse(c.log.Logger, true, msgf)
2838 xcheckf(err, "parsing message")
2840 if len(p.Envelope.From) != 1 {
2841 log.Fatalf("found %d from headers, need exactly 1", len(p.Envelope.From))
2843 localpart, err := smtp.ParseLocalpart(p.Envelope.From[0].User)
2844 xcheckf(err, "parsing localpart of address in from-header")
2845 dom := xparseDomain(p.Envelope.From[0].Host, "domain of address in from-header")
2849 domConf, ok := mox.Conf.Domain(dom)
2851 log.Fatalf("domain %s not configured", dom)
2854 selectors := mox.DKIMSelectors(domConf.DKIM)
2855 headers, err := dkim.Sign(context.Background(), c.log.Logger, localpart, dom, selectors, false, msgf)
2856 xcheckf(err, "signing message with dkim")
2858 log.Fatalf("no DKIM configured for domain %s", dom)
2860 _, err = fmt.Fprint(os.Stdout, headers)
2861 xcheckf(err, "write headers")
2862 _, err = io.Copy(os.Stdout, msgf)
2863 xcheckf(err, "write message")
2866func cmdDKIMLookup(c *cmd) {
2867 c.params = "$selector $domain"
2868 c.help = "Lookup and print the DKIM record for the selector at the domain."
2874 selector := xparseDomain(args[0], "selector")
2875 domain := xparseDomain(args[1], "domain")
2877 status, record, txt, authentic, err := dkim.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, selector, domain)
2879 fmt.Printf("error: %s\n", err)
2881 if status != dkim.StatusNeutral {
2882 fmt.Printf("status: %s\n", status)
2885 fmt.Printf("TXT record: %s\n", txt)
2888 fmt.Println("dnssec-signed: yes")
2890 fmt.Println("dnssec-signed: no")
2893 fmt.Printf("Record:\n")
2895 "version", record.Version,
2896 "hashes", record.Hashes,
2898 "notes", record.Notes,
2899 "services", record.Services,
2900 "flags", record.Flags,
2902 for i := 0; i < len(pairs); i += 2 {
2903 fmt.Printf("\t%s: %v\n", pairs[i], pairs[i+1])
2908func cmdDMARCLookup(c *cmd) {
2909 c.params = "$domain"
2910 c.help = "Lookup dmarc policy for domain, a DNS TXT record at _dmarc.<domain>, validate and print it."
2916 fromdomain := xparseDomain(args[0], "domain")
2917 _, domain, _, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, fromdomain)
2918 xcheckf(err, "dmarc lookup domain %s", fromdomain)
2919 fmt.Printf("dmarc record at domain %s: %s\n", domain, txt)
2920 fmt.Printf("(%s)\n", dnssecStatus(authentic))
2923func dnssecStatus(v bool) string {
2925 return "with dnssec"
2927 return "without dnssec"
2930func cmdDMARCVerify(c *cmd) {
2931 c.params = "$remoteip $mailfromaddress $helodomain < messagefile"
2932 c.help = `Parse an email message and evaluate it against the DMARC policy of the domain in the From-header.
2934mailfromaddress and helodomain are used for SPF validation. If both are empty,
2935SPF validation is skipped.
2937mailfromaddress should be the address used as MAIL FROM in the SMTP session.
2938For DSN messages, that address may be empty. The helo domain was specified at
2939the beginning of the SMTP transaction that delivered the message. These values
2940can be found in message headers.
2947 var heloDomain *dns.Domain
2949 remoteIP := xparseIP(args[0], "remoteip")
2951 var mailfrom *smtp.Address
2953 a, err := smtp.ParseAddress(args[1])
2954 xcheckf(err, "parsing mailfrom address")
2958 d := xparseDomain(args[2], "helo domain")
2961 var received *spf.Received
2962 spfStatus := spf.StatusNone
2963 var spfIdentity *dns.Domain
2964 if mailfrom != nil || heloDomain != nil {
2965 spfArgs := spf.Args{
2967 LocalIP: net.ParseIP("127.0.0.1"),
2968 LocalHostname: dns.Domain{ASCII: "localhost"},
2970 if mailfrom != nil {
2971 spfArgs.MailFromLocalpart = mailfrom.Localpart
2972 spfArgs.MailFromDomain = mailfrom.Domain
2974 if heloDomain != nil {
2975 spfArgs.HelloDomain = dns.IPDomain{Domain: *heloDomain}
2977 rspf, spfDomain, expl, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfArgs)
2979 log.Printf("spf verify: %v (explanation: %q, authentic %v)", err, expl, authentic)
2982 spfStatus = received.Result
2983 // todo: should probably potentially do two separate spf validations
2984 if mailfrom != nil {
2985 spfIdentity = &mailfrom.Domain
2987 spfIdentity = heloDomain
2989 fmt.Printf("spf result: %s: %s (%s)\n", spfDomain, spfStatus, dnssecStatus(authentic))
2993 data, err := io.ReadAll(os.Stdin)
2994 xcheckf(err, "read message")
2995 dmarcFrom, _, _, err := message.From(c.log.Logger, false, bytes.NewReader(data), nil)
2996 xcheckf(err, "extract dmarc from message")
2998 const ignoreTestMode = false
2999 dkimResults, err := dkim.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, true, func(*dkim.Sig) error { return nil }, bytes.NewReader(data), ignoreTestMode)
3000 xcheckf(err, "dkim verify")
3001 for _, r := range dkimResults {
3002 fmt.Printf("dkim result: %q (err %v)\n", r.Status, r.Err)
3005 _, result := dmarc.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, dmarcFrom.Domain, dkimResults, spfStatus, spfIdentity, false)
3006 xcheckf(result.Err, "dmarc verify")
3007 fmt.Printf("dmarc from: %s\ndmarc status: %q\ndmarc reject: %v\ncmarc record: %s\n", dmarcFrom, result.Status, result.Reject, result.Record)
3010func cmdDMARCCheckreportaddrs(c *cmd) {
3011 c.params = "$domain"
3012 c.help = `For each reporting address in the domain's DMARC record, check if it has opted into receiving reports (if needed).
3014A DMARC record can request reports about DMARC evaluations to be sent to an
3015email/http address. If the organizational domains of that of the DMARC record
3016and that of the report destination address do not match, the destination
3017address must opt-in to receiving DMARC reports by creating a DMARC record at
3018<dmarcdomain>._report._dmarc.<reportdestdomain>.
3025 dom := xparseDomain(args[0], "domain")
3026 _, domain, record, txt, authentic, err := dmarc.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dom)
3027 xcheckf(err, "dmarc lookup domain %s", dom)
3028 fmt.Printf("dmarc record at domain %s: %q\n", domain, txt)
3029 fmt.Printf("(%s)\n", dnssecStatus(authentic))
3031 check := func(kind, addr string) {
3034 printResult := func(format string, args ...any) {
3035 fmt.Printf("%s %s: %s (%s)\n", kind, addr, fmt.Sprintf(format, args...), dnssecStatus(authentic))
3038 u, err := url.Parse(addr)
3040 printResult("parsing uri %s: %v (skipping)", addr, err)
3043 var destdom dns.Domain
3046 a, err := smtp.ParseAddress(u.Opaque)
3048 printResult("parsing destination email address %s: %v (skipping)", u.Opaque, err)
3053 printResult("unrecognized scheme in reporting address %s (skipping)", u.Scheme)
3057 if publicsuffix.Lookup(context.Background(), c.log.Logger, dom) == publicsuffix.Lookup(context.Background(), c.log.Logger, destdom) {
3058 printResult("pass (same organizational domain)")
3062 accepts, status, _, txts, authentic, err := dmarc.LookupExternalReportsAccepted(context.Background(), c.log.Logger, dns.StrictResolver{}, domain, destdom)
3064 txtaddr := fmt.Sprintf("%s._report._dmarc.%s", domain.ASCII, destdom.ASCII)
3066 txtstr = fmt.Sprintf(" (no txt records %s)", txtaddr)
3068 txtstr = fmt.Sprintf(" (txt record %s: %q)", txtaddr, txts)
3070 if status != dmarc.StatusNone {
3071 printResult("fail: %s%s", err, txtstr)
3073 printResult("pass%s", txtstr)
3074 } else if err != nil {
3075 printResult("fail: %s%s", err, txtstr)
3077 printResult("fail%s", txtstr)
3081 for _, uri := range record.AggregateReportAddresses {
3082 check("aggregate reporting", uri.Address)
3084 for _, uri := range record.FailureReportAddresses {
3085 check("failure reporting", uri.Address)
3089func cmdDMARCParsereportmsg(c *cmd) {
3090 c.params = "$messagefile ..."
3091 c.help = `Parse a DMARC report from an email message, and print its extracted details.
3093DMARC reports are periodically mailed, if requested in the DMARC DNS record of
3094a domain. Reports are sent by mail servers that received messages with our
3095domain in a From header. This may or may not be legatimate email. DMARC reports
3096contain summaries of evaluations of DMARC and DKIM/SPF, which can help
3097understand email deliverability problems.
3104 for _, arg := range args {
3105 f, err := os.Open(arg)
3106 xcheckf(err, "open %q", arg)
3107 feedback, err := dmarcrpt.ParseMessageReport(c.log.Logger, f)
3108 xcheckf(err, "parse report in %q", arg)
3109 meta := feedback.ReportMetadata
3110 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)
3111 if len(meta.Errors) > 0 {
3112 fmt.Printf("Errors:\n")
3113 for _, s := range meta.Errors {
3114 fmt.Printf("\t- %s\n", s)
3117 pol := feedback.PolicyPublished
3118 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)
3119 for _, record := range feedback.Records {
3120 idents := record.Identifiers
3121 fmt.Printf("\theaderfrom %q, envelopes from %q, to %q\n", idents.HeaderFrom, idents.EnvelopeFrom, idents.EnvelopeTo)
3122 eval := record.Row.PolicyEvaluated
3123 var reasons strings.Builder
3124 for _, reason := range eval.Reasons {
3125 reasons.WriteString("; " + string(reason.Type))
3126 if reason.Comment != "" {
3127 reasons.WriteString(fmt.Sprintf(": %q", reason.Comment))
3130 fmt.Printf("\tresult %s: dkim %s, spf %s; sourceIP %s, count %d%s\n", eval.Disposition, eval.DKIM, eval.SPF, record.Row.SourceIP, record.Row.Count, reasons.String())
3131 for _, dkim := range record.AuthResults.DKIM {
3133 if dkim.HumanResult != "" {
3134 result = fmt.Sprintf(": %q", dkim.HumanResult)
3136 fmt.Printf("\t\tdkim %s; domain %q selector %q%s\n", dkim.Result, dkim.Domain, dkim.Selector, result)
3138 for _, spf := range record.AuthResults.SPF {
3139 fmt.Printf("\t\tspf %s; domain %q scope %q\n", spf.Result, spf.Domain, spf.Scope)
3145func cmdDMARCDBAddReport(c *cmd) {
3147 c.params = "$fromdomain < messagefile"
3148 c.help = "Add a DMARC report to the database."
3156 fromdomain := xparseDomain(args[0], "domain")
3157 fmt.Fprintln(os.Stderr, "reading report message from stdin")
3158 report, err := dmarcrpt.ParseMessageReport(c.log.Logger, os.Stdin)
3159 xcheckf(err, "parse message")
3160 err = dmarcdb.AddReport(context.Background(), report, fromdomain)
3161 xcheckf(err, "add dmarc report")
3164func cmdTLSRPTLookup(c *cmd) {
3165 c.params = "$domain"
3166 c.help = `Lookup the TLSRPT record for the domain.
3168A TLSRPT record typically contains an email address where reports about TLS
3169connectivity should be sent. Mail servers attempting delivery to our domain
3170should attempt to use TLS. TLSRPT lets them report how many connection
3171successfully used TLS, and how what kind of errors occurred otherwise.
3178 d := xparseDomain(args[0], "domain")
3179 _, txt, err := tlsrpt.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, d)
3180 xcheckf(err, "tlsrpt lookup for %s", d)
3184func cmdTLSRPTParsereportmsg(c *cmd) {
3185 c.params = "$messagefile ..."
3186 c.help = `Parse and print the TLSRPT in the message.
3188The report is printed in formatted JSON.
3195 for _, arg := range args {
3196 f, err := os.Open(arg)
3197 xcheckf(err, "open %q", arg)
3198 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, f)
3199 xcheckf(err, "parse report in %q", arg)
3200 // todo future: only print the highlights?
3201 enc := json.NewEncoder(os.Stdout)
3202 enc.SetIndent("", "\t")
3203 enc.SetEscapeHTML(false)
3204 err = enc.Encode(reportJSON)
3205 xcheckf(err, "write report")
3209func cmdSPFCheck(c *cmd) {
3210 c.params = "$domain $ip"
3211 c.help = `Check the status of IP for the policy published in DNS for the domain.
3213IPs may be allowed to send for a domain, or disallowed, and several shades in
3214between. If not allowed, an explanation may be provided by the policy. If so,
3215the explanation is printed. The SPF mechanism that matched (if any) is also
3223 domain := xparseDomain(args[0], "domain")
3225 ip := xparseIP(args[1], "ip")
3227 spfargs := spf.Args{
3229 MailFromLocalpart: "user",
3230 MailFromDomain: domain,
3231 HelloDomain: dns.IPDomain{Domain: domain},
3232 LocalIP: net.ParseIP("127.0.0.1"),
3233 LocalHostname: dns.Domain{ASCII: "localhost"},
3235 r, _, explanation, authentic, err := spf.Verify(context.Background(), c.log.Logger, dns.StrictResolver{}, spfargs)
3237 fmt.Printf("error: %s\n", err)
3239 if explanation != "" {
3240 fmt.Printf("explanation: %s\n", explanation)
3242 fmt.Printf("status: %s (%s)\n", r.Result, dnssecStatus(authentic))
3243 if r.Mechanism != "" {
3244 fmt.Printf("mechanism: %s\n", r.Mechanism)
3248func cmdSPFParse(c *cmd) {
3249 c.params = "$txtrecord"
3250 c.help = "Parse the record as SPF record. If valid, nothing is printed."
3256 _, _, err := spf.ParseRecord(args[0])
3257 xcheckf(err, "parsing record")
3260func cmdSPFLookup(c *cmd) {
3261 c.params = "$domain"
3262 c.help = "Lookup the SPF record for the domain and print it."
3268 domain := xparseDomain(args[0], "domain")
3269 _, txt, _, authentic, err := spf.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
3270 xcheckf(err, "spf lookup for %s", domain)
3272 fmt.Printf("(%s)\n", dnssecStatus(authentic))
3275func cmdMTASTSLookup(c *cmd) {
3276 c.params = "$domain"
3277 c.help = `Lookup the MTASTS record and policy for the domain.
3279MTA-STS is a mechanism for a domain to specify if it requires TLS connections
3280for delivering email. If a domain has a valid MTA-STS DNS TXT record at
3281_mta-sts.<domain> it signals it implements MTA-STS. A policy can then be
3282fetched at https://mta-sts.<domain>/.well-known/mta-sts.txt. The policy
3283specifies the mode (enforce, testing, none), which MX servers support TLS and
3284should be used, and how long the policy can be cached.
3291 domain := xparseDomain(args[0], "domain")
3293 record, policy, _, err := mtasts.Get(context.Background(), c.log.Logger, dns.StrictResolver{}, domain)
3295 fmt.Printf("error: %s\n", err)
3298 fmt.Printf("DNS TXT record _mta-sts.%s: %s\n", domain.ASCII, record.String())
3302 fmt.Printf("policy at https://mta-sts.%s/.well-known/mta-sts.txt:\n", domain.ASCII)
3303 fmt.Printf("%s", policy.String())
3307func cmdRDAPDomainage(c *cmd) {
3308 c.params = "$domain"
3309 c.help = `Lookup the age of domain in RDAP based on latest registration.
3311RDAP is the registration data access protocol. Registries run RDAP services for
3312their top level domains, providing information such as the registration date of
3313domains. This command looks up the "age" of a domain by looking at the most
3314recent "registration", "reregistration" or "reinstantiation" event.
3316Email messages from recently registered domains are often treated with
3317suspicion, and some mail systems are more likely to classify them as junk.
3319On each invocation, a bootstrap file with a list of registries (of top-level
3320domains) is retrieved, without caching. Do not run this command too often with
3328 domain := xparseDomain(args[0], "domain")
3330 registration, err := rdap.LookupLastDomainRegistration(context.Background(), c.log, domain)
3331 xcheckf(err, "looking up domain in rdap")
3333 age := time.Since(registration)
3334 const day = 24 * time.Hour
3335 const year = 365 * day
3337 days := (age - years*year) / day
3341 } else if years > 0 {
3342 s = fmt.Sprintf("%d years, ", years)
3347 s += fmt.Sprintf("%d days", days)
3352func cmdRetrain(c *cmd) {
3353 c.params = "[$accountname]"
3354 c.help = `Recreate and retrain the junk filter for the account or all accounts.
3356Useful after having made changes to the junk filter configuration, or if the
3357implementation has changed.
3369 ctlcmdRetrain(xctl(), account)
3372func ctlcmdRetrain(ctl *ctl, account string) {
3373 ctl.xwrite("retrain")
3378func cmdTLSRPTDBAddReport(c *cmd) {
3380 c.params = "< messagefile"
3381 c.help = "Parse a TLS report from the message and add it to the database."
3383 c.flag.BoolVar(&hostReport, "hostreport", false, "report for a host instead of domain")
3391 // First read message, to get the From-header. Then parse it as TLSRPT.
3392 fmt.Fprintln(os.Stderr, "reading report message from stdin")
3393 buf, err := io.ReadAll(os.Stdin)
3394 xcheckf(err, "reading message")
3395 part, err := message.Parse(c.log.Logger, true, bytes.NewReader(buf))
3396 xcheckf(err, "parsing message")
3397 if part.Envelope == nil || len(part.Envelope.From) != 1 {
3398 log.Fatalf("message must have one From-header")
3400 from := part.Envelope.From[0]
3401 domain := xparseDomain(from.Host, "domain")
3403 reportJSON, err := tlsrpt.ParseMessage(c.log.Logger, bytes.NewReader(buf))
3404 xcheckf(err, "parsing tls report in message")
3406 mailfrom := from.User + "@" + from.Host // todo future: should escape and such
3407 report := reportJSON.Convert()
3408 err = tlsrptdb.AddReport(context.Background(), c.log, domain, mailfrom, hostReport, &report)
3409 xcheckf(err, "add tls report to database")
3412func cmdDNSBLCheck(c *cmd) {
3413 c.params = "$zone $ip"
3414 c.help = `Test if IP is in the DNS blocklist of the zone, e.g. bl.spamcop.net.
3416If the IP is in the blocklist, an explanation is printed. This is typically a
3417URL with more information.
3424 zone := xparseDomain(args[0], "zone")
3425 ip := xparseIP(args[1], "ip")
3427 status, explanation, err := dnsbl.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, zone, ip)
3428 fmt.Printf("status: %s\n", status)
3429 if status == dnsbl.StatusFail {
3430 fmt.Printf("explanation: %q\n", explanation)
3433 fmt.Printf("error: %s\n", err)
3437func cmdDNSBLCheckhealth(c *cmd) {
3439 c.help = `Check the health of the DNS blocklist represented by zone, e.g. bl.spamcop.net.
3441The health of a DNS blocklist can be checked by querying for 127.0.0.1 and
3442127.0.0.2. The second must and the first must not be present.
3449 zone := xparseDomain(args[0], "zone")
3450 err := dnsbl.CheckHealth(context.Background(), c.log.Logger, dns.StrictResolver{}, zone)
3451 xcheckf(err, "unhealthy")
3452 fmt.Println("healthy")
3455func cmdCheckupdate(c *cmd) {
3456 c.help = `Check if a newer version of mox is available.
3458A single DNS TXT lookup to _updates.xmox.nl tells if a new version is
3459available. If so, a changelog is fetched from https://updates.xmox.nl, and the
3460individual entries verified with a builtin public key. The changelog is
3463 if len(c.Parse()) != 0 {
3468 current, lastknown, _, err := store.LastKnown()
3470 log.Printf("getting last known version: %s", err)
3472 fmt.Printf("last known version: %s\n", lastknown)
3473 fmt.Printf("current version: %s\n", current)
3475 latest, _, err := updates.Lookup(context.Background(), c.log.Logger, dns.StrictResolver{}, dns.Domain{ASCII: changelogDomain})
3476 xcheckf(err, "lookup of latest version")
3477 fmt.Printf("latest version: %s\n", latest)
3479 if latest.After(current) {
3480 changelog, err := updates.FetchChangelog(context.Background(), c.log.Logger, changelogURL, current, changelogPubKey)
3481 xcheckf(err, "fetching changelog")
3482 if len(changelog.Changes) == 0 {
3483 log.Printf("no changes in changelog")
3486 fmt.Println("Changelog")
3487 for _, c := range changelog.Changes {
3488 fmt.Println("\n" + strings.TrimSpace(c.Text))
3493func cmdCid(c *cmd) {
3495 c.help = `Turn an ID from a Received header into a cid, for looking up in logs.
3497A cid is essentially a connection counter initialized when mox starts. Each log
3498line contains a cid. Received headers added by mox contain a unique ID that can
3499be decrypted to a cid by admin of a mox instance only.
3507 recvidpath := mox.DataDirPath("receivedid.key")
3508 recvidbuf, err := os.ReadFile(recvidpath)
3509 xcheckf(err, "reading %s", recvidpath)
3510 if len(recvidbuf) != 16+8 {
3511 log.Fatalf("bad data in %s: got %d bytes, expect 16+8=24", recvidpath, len(recvidbuf))
3513 err = mox.ReceivedIDInit(recvidbuf[:16], recvidbuf[16:])
3514 xcheckf(err, "init receivedid")
3516 cid, err := mox.ReceivedToCid(args[0])
3517 xcheckf(err, "received id to cid")
3518 fmt.Printf("%x\n", cid)
3521func cmdVersion(c *cmd) {
3522 c.help = "Prints this mox version."
3523 if len(c.Parse()) != 0 {
3526 fmt.Println(moxvar.Version)
3527 fmt.Printf("%s/%s\n", runtime.GOOS, runtime.GOARCH)
3530func cmdWebapi(c *cmd) {
3531 c.params = "[$method [$baseurl-with-credentials]"
3532 c.help = "Lists available methods, prints request/response parameters for method, or calls a method with a request read from standard input."
3538 t := reflect.TypeFor[webapi.Methods]()
3539 methods := map[string]reflect.Type{}
3541 for i := range t.NumMethod() {
3543 methods[mt.Name] = mt.Type
3544 ml = append(ml, mt.Name)
3548 fmt.Println(strings.Join(ml, "\n"))
3552 mt, ok := methods[args[0]]
3554 log.Fatalf("unknown method %q", args[0])
3556 resultNotJSON := mt.Out(0).Kind() == reflect.Interface
3559 fmt.Println("# Example request")
3561 printJSON("\t", mox.FillExample(nil, reflect.New(mt.In(1))).Interface())
3564 fmt.Println("Output is non-JSON data.")
3567 fmt.Println("# Example response")
3569 printJSON("\t", mox.FillExample(nil, reflect.New(mt.Out(0))).Interface())
3575 response = reflect.New(mt.Out(0))
3578 fmt.Fprintln(os.Stderr, "reading request from stdin...")
3579 request, err := io.ReadAll(os.Stdin)
3580 xcheckf(err, "read message")
3582 dec := json.NewDecoder(bytes.NewReader(request))
3583 dec.DisallowUnknownFields()
3584 err = dec.Decode(reflect.New(mt.In(1)).Interface())
3585 xcheckf(err, "parsing request")
3587 resp, err := http.PostForm(args[1]+args[0], url.Values{"request": []string{string(request)}})
3588 xcheckf(err, "http post")
3590 if err := resp.Body.Close(); err != nil {
3591 log.Printf("closing http response body: %v", err)
3594 if resp.StatusCode == http.StatusBadRequest {
3595 buf, err := io.ReadAll(&moxio.LimitReader{R: resp.Body, Limit: 10 * 1024})
3596 xcheckf(err, "reading response for 400 bad request error")
3597 err = json.Unmarshal(buf, &response)
3599 printJSON("", response)
3601 fmt.Fprintf(os.Stderr, "(not json)\n")
3602 os.Stderr.Write(buf)
3605 } else if resp.StatusCode != http.StatusOK {
3606 fmt.Fprintf(os.Stderr, "http response %s\n", resp.Status)
3607 _, err := io.Copy(os.Stderr, resp.Body)
3608 xcheckf(err, "copy body")
3610 err := json.NewDecoder(resp.Body).Decode(&resp)
3611 xcheckf(err, "unmarshal response")
3612 printJSON("", response)
3616func printJSON(indent string, v any) {
3617 fmt.Printf("%s", indent)
3618 enc := json.NewEncoder(os.Stdout)
3619 enc.SetIndent(indent, "\t")
3620 enc.SetEscapeHTML(false)
3621 err := enc.Encode(v)
3622 xcheckf(err, "encode json")
3625// 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.
3626func cmdBumpUIDValidity(c *cmd) {
3627 c.params = "$account [$mailbox]"
3628 c.help = `Change the IMAP UID validity of the mailbox, causing IMAP clients to refetch messages.
3630This can be useful after manually repairing metadata about the account/mailbox.
3632Opens account database file directly. Ensure mox does not have the account
3633open, or is not running.
3636 if len(args) != 1 && len(args) != 2 {
3641 a, err := store.OpenAccount(c.log, args[0], false)
3642 xcheckf(err, "open account")
3644 if err := a.Close(); err != nil {
3645 log.Printf("closing account: %v", err)
3649 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3650 uidvalidity, err := a.NextUIDValidity(tx)
3652 return fmt.Errorf("assigning next uid validity: %v", err)
3655 q := bstore.QueryTx[store.Mailbox](tx)
3656 q.FilterEqual("Expunged", false)
3658 q.FilterEqual("Name", args[1])
3660 mbl, err := q.SortAsc("Name").List()
3662 return fmt.Errorf("looking up mailbox: %v", err)
3664 if len(args) == 2 && len(mbl) != 1 {
3665 return fmt.Errorf("looking up mailbox %q, found %d mailboxes", args[1], len(mbl))
3667 for _, mb := range mbl {
3668 mb.UIDValidity = uidvalidity
3669 err = tx.Update(&mb)
3671 return fmt.Errorf("updating uid validity for mailbox: %v", err)
3673 fmt.Printf("uid validity for %q updated to %d\n", mb.Name, uidvalidity)
3677 xcheckf(err, "updating database")
3680func cmdReassignUIDs(c *cmd) {
3681 c.params = "$account [$mailboxid]"
3682 c.help = `Reassign UIDs in one mailbox or all mailboxes in an account and bump UID validity, causing IMAP clients to refetch messages.
3684Opens account database file directly. Ensure mox does not have the account
3685open, or is not running.
3688 if len(args) != 1 && len(args) != 2 {
3695 mailboxID, err = strconv.ParseInt(args[1], 10, 64)
3696 xcheckf(err, "parsing mailbox id")
3700 a, err := store.OpenAccount(c.log, args[0], false)
3701 xcheckf(err, "open account")
3703 if err := a.Close(); err != nil {
3704 log.Printf("closing account: %v", err)
3708 // Gather the last-assigned UIDs per mailbox.
3709 uidlasts := map[int64]store.UID{}
3711 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3712 // Reassign UIDs, going per mailbox. We assign starting at 1, only changing the
3713 // message if it isn't already at the intended UID. Doing it in this order ensures
3714 // we don't get into trouble with duplicate UIDs for a mailbox. We assign a new
3715 // modseq. Not strictly needed, but doesn't hurt. It's also why we assign a UID to
3716 // expunged messages.
3717 modseq, err := a.NextModSeq(tx)
3718 xcheckf(err, "assigning next modseq")
3720 q := bstore.QueryTx[store.Message](tx)
3722 q.FilterNonzero(store.Message{MailboxID: mailboxID})
3724 q.SortAsc("MailboxID", "UID")
3725 err = q.ForEach(func(m store.Message) error {
3726 uidlasts[m.MailboxID]++
3727 uid := uidlasts[m.MailboxID]
3731 if err := tx.Update(&m); err != nil {
3732 return fmt.Errorf("updating uid for message: %v", err)
3738 return fmt.Errorf("reading through messages: %v", err)
3741 // Now update the uidnext, uidvalidity and modseq for each mailbox.
3742 err = bstore.QueryTx[store.Mailbox](tx).FilterEqual("Expunged", false).ForEach(func(mb store.Mailbox) error {
3743 // Assign each mailbox a completely new uidvalidity.
3744 uidvalidity, err := a.NextUIDValidity(tx)
3746 return fmt.Errorf("assigning next uid validity: %v", err)
3749 if mb.UIDValidity >= uidvalidity {
3750 // This should not happen, but since we're fixing things up after a hypothetical
3751 // mishap, might as well account for inconsistent uidvalidity.
3752 next := store.NextUIDValidity{ID: 1, Next: mb.UIDValidity + 2}
3753 if err := tx.Update(&next); err != nil {
3754 log.Printf("updating nextuidvalidity: %v, continuing", err)
3758 mb.UIDValidity = uidvalidity
3760 mb.UIDNext = uidlasts[mb.ID] + 1
3762 if err := tx.Update(&mb); err != nil {
3763 return fmt.Errorf("updating uidvalidity and uidnext for mailbox: %v", err)
3768 return fmt.Errorf("updating mailboxes: %v", err)
3772 xcheckf(err, "updating database")
3775func cmdFixUIDMeta(c *cmd) {
3776 c.params = "$account"
3777 c.help = `Fix inconsistent UIDVALIDITY and UIDNEXT in messages/mailboxes/account.
3779The next UID to use for a message in a mailbox should always be higher than any
3780existing message UID in the mailbox. If it is not, the mailbox UIDNEXT is
3783Each mailbox has a UIDVALIDITY sequence number, which should always be lower
3784than the per-account next UIDVALIDITY to use. If it is not, the account next
3785UIDVALIDITY is updated.
3787Opens account database file directly. Ensure mox does not have the account
3788open, or is not running.
3796 a, err := store.OpenAccount(c.log, args[0], false)
3797 xcheckf(err, "open account")
3799 if err := a.Close(); err != nil {
3800 log.Printf("closing account: %v", err)
3804 var maxUIDValidity uint32
3806 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3807 // We look at each mailbox, retrieve its max UID and compare against the mailbox
3809 err := bstore.QueryTx[store.Mailbox](tx).FilterEqual("Expunged", false).ForEach(func(mb store.Mailbox) error {
3810 if mb.UIDValidity > maxUIDValidity {
3811 maxUIDValidity = mb.UIDValidity
3813 m, err := bstore.QueryTx[store.Message](tx).FilterNonzero(store.Message{MailboxID: mb.ID}).SortDesc("UID").Limit(1).Get()
3814 if err == bstore.ErrAbsent || err == nil && m.UID < mb.UIDNext {
3816 } else if err != nil {
3817 return fmt.Errorf("finding message with max uid in mailbox: %w", err)
3819 olduidnext := mb.UIDNext
3820 mb.UIDNext = m.UID + 1
3821 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)
3822 if err := tx.Update(&mb); err != nil {
3823 return fmt.Errorf("updating mailbox uidnext: %v", err)
3828 return fmt.Errorf("processing mailboxes: %v", err)
3831 uidvalidity := store.NextUIDValidity{ID: 1}
3832 if err := tx.Get(&uidvalidity); err != nil {
3833 return fmt.Errorf("reading account next uidvalidity: %v", err)
3835 if maxUIDValidity >= uidvalidity.Next {
3836 log.Printf("account next uidvalidity %d <= highest uidvalidity %d found in mailbox, resetting account next uidvalidity to %d", uidvalidity.Next, maxUIDValidity, maxUIDValidity+1)
3837 uidvalidity.Next = maxUIDValidity + 1
3838 if err := tx.Update(&uidvalidity); err != nil {
3839 return fmt.Errorf("updating account next uidvalidity: %v", err)
3845 xcheckf(err, "updating database")
3848func cmdFixmsgsize(c *cmd) {
3849 c.params = "[$account]"
3850 c.help = `Ensure message sizes in the database matching the sum of the message prefix length and on-disk file size.
3852Messages with an inconsistent size are also parsed again.
3854If an inconsistency is found, you should probably also run "mox
3855bumpuidvalidity" on the mailboxes or entire account to force IMAP clients to
3868 ctlcmdFixmsgsize(xctl(), account)
3871func ctlcmdFixmsgsize(ctl *ctl, account string) {
3872 ctl.xwrite("fixmsgsize")
3875 ctl.xstreamto(os.Stdout)
3878func cmdReparse(c *cmd) {
3879 c.params = "[$account]"
3880 c.help = `Parse all messages in the account or all accounts again.
3882Can be useful after upgrading mox with improved message parsing. Messages are
3883parsed in batches, so other access to the mailboxes/messages are not blocked
3884while reparsing all messages.
3896 ctlcmdReparse(xctl(), account)
3899func ctlcmdReparse(ctl *ctl, account string) {
3900 ctl.xwrite("reparse")
3903 ctl.xstreamto(os.Stdout)
3906func cmdEnsureParsed(c *cmd) {
3907 c.params = "$account"
3908 c.help = "Ensure messages in the database have a pre-parsed MIME form in the database."
3910 c.flag.BoolVar(&all, "all", false, "store new parsed message for all messages")
3917 a, err := store.OpenAccount(c.log, args[0], false)
3918 xcheckf(err, "open account")
3920 if err := a.Close(); err != nil {
3921 log.Printf("closing account: %v", err)
3926 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
3927 q := bstore.QueryTx[store.Message](tx)
3928 q.FilterEqual("Expunged", false)
3929 q.FilterFn(func(m store.Message) bool {
3930 return all || m.ParsedBuf == nil
3934 return fmt.Errorf("list messages: %v", err)
3936 for _, m := range l {
3937 mr := a.MessageReader(m)
3938 p, err := message.EnsurePart(c.log.Logger, false, mr, m.Size)
3940 log.Printf("parsing message %d: %v (continuing)", m.ID, err)
3942 m.ParsedBuf, err = json.Marshal(p)
3944 return fmt.Errorf("marshal parsed message: %v", err)
3946 if err := tx.Update(&m); err != nil {
3947 return fmt.Errorf("update message: %v", err)
3953 xcheckf(err, "update messages with parsed mime structure")
3954 fmt.Printf("%d messages updated\n", n)
3957func cmdRecalculateMailboxCounts(c *cmd) {
3958 c.params = "$account"
3959 c.help = `Recalculate message counts for all mailboxes in the account, and total message size for quota.
3961When a message is added to/removed from a mailbox, or when message flags change,
3962the total, unread, unseen and deleted messages are accounted, the total size of
3963the mailbox, and the total message size for the account. In case of a bug in
3964this accounting, the numbers could become incorrect. This command will find, fix
3973 ctlcmdRecalculateMailboxCounts(xctl(), args[0])
3976func ctlcmdRecalculateMailboxCounts(ctl *ctl, account string) {
3977 ctl.xwrite("recalculatemailboxcounts")
3980 ctl.xstreamto(os.Stdout)
3983func cmdMessageParse(c *cmd) {
3984 c.params = "$messagefile"
3985 c.help = "Parse message, print JSON representation."
3988 c.flag.BoolVar(&smtputf8, "smtputf8", false, "check if message needs smtputf8")
3994 f, err := os.Open(args[0])
3995 xcheckf(err, "open")
3997 if err := f.Close(); err != nil {
3998 log.Printf("closing message file: %v", err)
4002 part, err := message.Parse(c.log.Logger, false, f)
4003 xcheckf(err, "parsing message")
4004 err = part.Walk(c.log.Logger, nil)
4005 xcheckf(err, "parsing nested parts")
4006 enc := json.NewEncoder(os.Stdout)
4007 enc.SetIndent("", "\t")
4008 enc.SetEscapeHTML(false)
4009 err = enc.Encode(part)
4010 xcheckf(err, "write")
4013 needs, err := part.NeedsSMTPUTF8()
4014 xcheckf(err, "checking if message needs smtputf8")
4015 fmt.Println("message needs smtputf8:", needs)
4019func cmdOpenaccounts(c *cmd) {
4021 c.params = "$datadir $account ..."
4022 c.help = `Open and close accounts, for triggering data upgrades, for tests.
4024Opens database files directly, not going through a running mox instance.
4032 dataDir := filepath.Clean(args[0])
4033 for _, accName := range args[1:] {
4034 accDir := filepath.Join(dataDir, "accounts", accName)
4035 log.Printf("opening account %s...", accDir)
4036 a, err := store.OpenAccountDB(c.log, accDir, accName)
4037 xcheckf(err, "open account %s", accName)
4038 err = a.ThreadingWait(c.log)
4039 xcheckf(err, "wait for threading upgrade to complete for %s", accName)
4041 xcheckf(err, "close account %s", accName)
4045func cmdReassignthreads(c *cmd) {
4046 c.params = "[$account]"
4047 c.help = `Reassign message threads.
4049For all accounts, or optionally only the specified account.
4051Threading for all messages in an account is first reset, and new base subject
4052and normalized message-id saved with the message. Then all messages are
4053evaluated and matched against their parents/ancestors.
4055Messages are matched based on the References header, with a fall-back to an
4056In-Reply-To header, and if neither is present/valid, based only on base
4059A References header typically points to multiple previous messages in a
4060hierarchy. From oldest ancestor to most recent parent. An In-Reply-To header
4061would have only a message-id of the parent message.
4063A message is only linked to a parent/ancestor if their base subject is the
4064same. This ensures unrelated replies, with a new subject, are placed in their
4067The base subject is lower cased, has whitespace collapsed to a single
4068space, and some components removed: leading "Re:", "Fwd:", "Fw:", or bracketed
4069tag (that mailing lists often add, e.g. "[listname]"), trailing "(fwd)", or
4070enclosing "[fwd: ...]".
4072Messages are linked to all their ancestors. If an intermediate parent/ancestor
4073message is deleted in the future, the message can still be linked to the earlier
4074ancestors. If the direct parent already wasn't available while matching, this is
4075stored as the message having a "missing link" to its stored ancestors.
4087 ctlcmdReassignthreads(xctl(), account)
4090func ctlcmdReassignthreads(ctl *ctl, account string) {
4091 ctl.xwrite("reassignthreads")
4094 ctl.xstreamto(os.Stdout)
4097func cmdIMAPServe(c *cmd) {
4098 c.params = "$preauthaddress"
4099 c.help = `Initiate a preauthenticated IMAP connection on file descriptor 0.
4101For use with tools that can do IMAP over tunneled connections, e.g. with SSH
4102during migrations. TLS is not possible on the connection, and authentication
4103does not require TLS.
4106 c.flag.BoolVar(&fd0, "fd0", false, "write IMAP to file descriptor 0 instead of stdout")
4117 ctlcmdIMAPServe(xctl(), address, os.Stdin, output)
4120func ctlcmdIMAPServe(ctl *ctl, address string, input io.ReadCloser, output io.WriteCloser) {
4121 ctl.xwrite("imapserve")
4125 done := make(chan struct{}, 1)
4130 _, err := io.Copy(output, ctl.conn)
4134 log.Printf("reading from imap: %v", err)
4140 _, err := io.Copy(ctl.conn, input)
4144 log.Printf("writing to imap: %v", err)
4149func cmdReadmessages(c *cmd) {
4151 c.params = "$datadir $account ..."
4152 c.help = `Open account, parse several headers for all messages.
4154For performance testing.
4156Opens database files directly, not going through a running mox instance.
4159 gomaxprocs := runtime.GOMAXPROCS(0)
4160 var procs, workqueuesize, limit int
4161 c.flag.IntVar(&procs, "procs", gomaxprocs, "number of goroutines for reading messages")
4162 c.flag.IntVar(&workqueuesize, "workqueuesize", 2*gomaxprocs, "number of messages to keep in work queue")
4163 c.flag.IntVar(&limit, "limit", 0, "number of messages to process if greater than zero")
4169 type threadPrep struct {
4174 threadingFields := [][]byte{
4175 []byte("references"),
4176 []byte("in-reply-to"),
4179 dataDir := filepath.Clean(args[0])
4180 for _, accName := range args[1:] {
4181 accDir := filepath.Join(dataDir, "accounts", accName)
4182 log.Printf("opening account %s...", accDir)
4183 a, err := store.OpenAccountDB(c.log, accDir, accName)
4184 xcheckf(err, "open account %s", accName)
4186 prepareMessages := func(in, out chan moxio.Work[store.Message, threadPrep]) {
4187 headerbuf := make([]byte, 8*1024)
4188 scratch := make([]byte, 4*1024)
4196 var partialPart struct {
4200 if err := json.Unmarshal(m.ParsedBuf, &partialPart); err != nil {
4201 w.Err = fmt.Errorf("unmarshal part: %v", err)
4203 size := partialPart.BodyOffset - partialPart.HeaderOffset
4204 if int(size) > len(headerbuf) {
4205 headerbuf = make([]byte, size)
4208 buf := headerbuf[:int(size)]
4209 err := func() error {
4210 mr := a.MessageReader(m)
4212 if err := mr.Close(); err != nil {
4213 log.Printf("closing message reader: %v", err)
4217 // ReadAt returns whole buffer or error. Single read should be fast.
4218 n, err := mr.ReadAt(buf, partialPart.HeaderOffset)
4219 if err != nil || n != len(buf) {
4220 return fmt.Errorf("read header: %v", err)
4226 } else if h, err := message.ParseHeaderFields(buf, scratch, threadingFields); err != nil {
4229 w.Out.references = h["References"]
4230 w.Out.inReplyTo = h["In-Reply-To"]
4243 processMessage := func(m store.Message, prep threadPrep) error {
4245 log.Printf("%d messages (delta %s)", n, time.Since(t))
4252 wq := moxio.NewWorkQueue(procs, workqueuesize, prepareMessages, processMessage)
4254 err = a.DB.Write(context.Background(), func(tx *bstore.Tx) error {
4255 q := bstore.QueryTx[store.Message](tx)
4256 q.FilterEqual("Expunged", false)
4261 err = q.ForEach(wq.Add)
4269 xcheckf(err, "processing message")
4272 xcheckf(err, "close account %s", accName)
4273 log.Printf("account %s, total time %s", accName, time.Since(t0))
4277func cmdQueueFillRetired(c *cmd) {
4279 c.help = `Fill retired messag and webhooks queue with testdata.
4281For testing the pagination. Operates directly on queue database.
4284 c.flag.IntVar(&n, "n", 10000, "retired messages and retired webhooks to insert")
4292 xcheckf(err, "init queue")
4293 err = queue.DB.Write(context.Background(), func(tx *bstore.Tx) error {
4296 // Cause autoincrement ID for queue.Msg to be forwarded, and use the reserved ID
4297 // space for inserting retired messages.
4299 err = tx.Insert(&fm)
4300 xcheckf(err, "temporarily insert message to get autoincrement sequence")
4301 err = tx.Delete(&fm)
4302 xcheckf(err, "removing temporary message for resetting autoincrement sequence")
4304 err = tx.Insert(&fm)
4305 xcheckf(err, "temporarily insert message to forward autoincrement sequence")
4306 err = tx.Delete(&fm)
4307 xcheckf(err, "removing temporary message after forwarding autoincrement sequence")
4310 // And likewise for webhooks.
4311 fh := queue.Hook{Account: "x", URL: "x", NextAttempt: time.Now()}
4312 err = tx.Insert(&fh)
4313 xcheckf(err, "temporarily insert webhook to get autoincrement sequence")
4314 err = tx.Delete(&fh)
4315 xcheckf(err, "removing temporary webhook for resetting autoincrement sequence")
4317 err = tx.Insert(&fh)
4318 xcheckf(err, "temporarily insert webhook to forward autoincrement sequence")
4319 err = tx.Delete(&fh)
4320 xcheckf(err, "removing temporary webhook after forwarding autoincrement sequence")
4324 t0 := now.Add(-time.Duration(i) * time.Second)
4325 last := now.Add(-time.Duration(i/10) * time.Second)
4326 mr := queue.MsgRetired{
4327 ID: fm.ID + int64(i),
4329 SenderAccount: "test",
4330 SenderLocalpart: "mox",
4331 SenderDomainStr: "localhost",
4332 FromID: fmt.Sprintf("%016d", i),
4333 RecipientLocalpart: "mox",
4334 RecipientDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "localhost"}},
4335 RecipientDomainStr: "localhost",
4338 Results: []queue.MsgResult{
4341 Duration: time.Millisecond,
4348 Size: int64(i * 100),
4349 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
4350 Subject: fmt.Sprintf("test message %d", i),
4351 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
4353 RecipientAddress: "mox@localhost",
4355 KeepUntil: now.Add(48 * time.Hour),
4357 err := tx.Insert(&mr)
4358 xcheckf(err, "inserting retired message")
4362 t0 := now.Add(-time.Duration(i) * time.Second)
4363 last := now.Add(-time.Duration(i/10) * time.Second)
4368 hr := queue.HookRetired{
4369 ID: fh.ID + int64(i),
4370 QueueMsgID: fm.ID + int64(i),
4371 FromID: fmt.Sprintf("%016d", i),
4372 MessageID: fmt.Sprintf("<msg%d@localhost>", i),
4373 Subject: fmt.Sprintf("test message %d", i),
4374 Extra: map[string]string{"i": fmt.Sprintf("%d", i)},
4376 URL: "http://localhost/hook",
4377 IsIncoming: i%10 == 0,
4378 OutgoingEvent: event,
4383 Results: []queue.HookResult{
4386 Duration: time.Millisecond,
4387 URL: "http://localhost/hook",
4396 KeepUntil: now.Add(48 * time.Hour),
4398 err := tx.Insert(&hr)
4399 xcheckf(err, "inserting retired hook")
4404 xcheckf(err, "add to queue")
4405 log.Printf("added %d retired messages and %d retired webhooks", n, n)