14 "github.com/mjl-/mox/queue"
17func xctlwriteJSON(ctl *ctl, v any) {
18 fbuf, err := json.Marshal(v)
19 xcheckf(err, "marshal as json to ctl")
20 ctl.xwrite(string(fbuf))
23func cmdQueueHoldrulesList(c *cmd) {
24 c.help = `List hold rules for the delivery queue.
26Messages submitted to the queue that match a hold rule will be marked as on hold
27and not scheduled for delivery.
29 if len(c.Parse()) != 0 {
33 ctlcmdQueueHoldrulesList(xctl())
36func ctlcmdQueueHoldrulesList(ctl *ctl) {
37 ctl.xwrite("queueholdruleslist")
39 if _, err := io.Copy(os.Stdout, ctl.reader()); err != nil {
44func cmdQueueHoldrulesAdd(c *cmd) {
45 c.params = "[ruleflags]"
46 c.help = `Add hold rule for the delivery queue.
48Add a hold rule to mark matching newly submitted messages as on hold. Set the
49matching rules with the flags. Don't specify any flags to match all submitted
52 var account, senderDomain, recipientDomain string
53 c.flag.StringVar(&account, "account", "", "account submitting the message")
54 c.flag.StringVar(&senderDomain, "senderdom", "", "sender domain")
55 c.flag.StringVar(&recipientDomain, "recipientdom", "", "recipient domain")
56 if len(c.Parse()) != 0 {
60 ctlcmdQueueHoldrulesAdd(xctl(), account, senderDomain, recipientDomain)
63func ctlcmdQueueHoldrulesAdd(ctl *ctl, account, senderDomain, recipientDomain string) {
64 ctl.xwrite("queueholdrulesadd")
66 ctl.xwrite(senderDomain)
67 ctl.xwrite(recipientDomain)
71func cmdQueueHoldrulesRemove(c *cmd) {
73 c.help = `Remove hold rule for the delivery queue.
75Remove a hold rule by its id.
81 id, err := strconv.ParseInt(args[0], 10, 64)
82 xcheckf(err, "parsing id")
84 ctlcmdQueueHoldrulesRemove(xctl(), id)
87func ctlcmdQueueHoldrulesRemove(ctl *ctl, id int64) {
88 ctl.xwrite("queueholdrulesremove")
89 ctl.xwrite(fmt.Sprintf("%d", id))
93// flagFilterSort is used by many of the queue commands to accept flags for
94// filtering the messages the operation applies to.
95func flagFilterSort(fs *flag.FlagSet, f *queue.Filter, s *queue.Sort) {
96 fs.Func("ids", "comma-separated list of message IDs", func(v string) error {
97 for _, s := range strings.Split(v, ",") {
98 id, err := strconv.ParseInt(s, 10, 64)
102 f.IDs = append(f.IDs, id)
106 fs.IntVar(&f.Max, "n", 0, "number of messages to return")
107 fs.StringVar(&f.Account, "account", "", "account that queued the message")
108 fs.StringVar(&f.From, "from", "", `from address of message, use "@example.com" to match all messages for a domain`)
109 fs.StringVar(&f.To, "to", "", `recipient address of message, use "@example.com" to match all messages for a domain`)
110 fs.StringVar(&f.Submitted, "submitted", "", `filter by time of submission relative to now, value must start with "<" (before now) or ">" (after now)`)
111 fs.StringVar(&f.NextAttempt, "nextattempt", "", `filter by time of next delivery attempt relative to now, value must start with "<" (before now) or ">" (after now)`)
112 fs.Func("transport", "transport to use for messages, empty string sets the default behaviour", func(v string) error {
116 fs.Func("hold", "true or false, whether to match only messages that are (not) on hold", func(v string) error {
120 } else if v == "false" {
123 return fmt.Errorf("bad value %q", v)
129 fs.Func("sort", `field to sort by, "nextattempt" (default) or "queued"`, func(v string) error {
132 s.Field = "NextAttempt"
136 return fmt.Errorf("unknown value %q", v)
140 fs.BoolVar(&s.Asc, "asc", false, "sort ascending instead of descending (default)")
144// flagRetiredFilterSort has filters for retired messages.
145func flagRetiredFilterSort(fs *flag.FlagSet, f *queue.RetiredFilter, s *queue.RetiredSort) {
146 fs.Func("ids", "comma-separated list of retired message IDs", func(v string) error {
147 for _, s := range strings.Split(v, ",") {
148 id, err := strconv.ParseInt(s, 10, 64)
152 f.IDs = append(f.IDs, id)
156 fs.IntVar(&f.Max, "n", 0, "number of messages to return")
157 fs.StringVar(&f.Account, "account", "", "account that queued the message")
158 fs.StringVar(&f.From, "from", "", `from address of message, use "@example.com" to match all messages for a domain`)
159 fs.StringVar(&f.To, "to", "", `recipient address of message, use "@example.com" to match all messages for a domain`)
160 fs.StringVar(&f.Submitted, "submitted", "", `filter by time of submission relative to now, value must start with "<" (before now) or ">" (after now)`)
161 fs.StringVar(&f.LastActivity, "lastactivity", "", `filter by time of last activity relative to now, value must start with "<" (before now) or ">" (after now)`)
162 fs.Func("transport", "transport to use for messages, empty string sets the default behaviour", func(v string) error {
166 fs.Func("result", `"success" or "failure" as result of delivery`, func(v string) error {
175 return fmt.Errorf("bad argument %q, need success or failure", v)
180 fs.Func("sort", `field to sort by, "lastactivity" (default) or "queued"`, func(v string) error {
183 s.Field = "LastActivity"
187 return fmt.Errorf("unknown value %q", v)
191 fs.BoolVar(&s.Asc, "asc", false, "sort ascending instead of descending (default)")
195func cmdQueueList(c *cmd) {
196 c.params = "[filtersortflags]"
197 c.help = `List matching messages in the delivery queue.
199Prints the message with its ID, last and next delivery attempts, last error.
203 flagFilterSort(c.flag, &f, &s)
204 if len(c.Parse()) != 0 {
208 ctlcmdQueueList(xctl(), f, s)
211func ctlcmdQueueList(ctl *ctl, f queue.Filter, s queue.Sort) {
212 ctl.xwrite("queuelist")
213 xctlwriteJSON(ctl, f)
214 xctlwriteJSON(ctl, s)
216 if _, err := io.Copy(os.Stdout, ctl.reader()); err != nil {
217 log.Fatalf("%s", err)
221func cmdQueueHold(c *cmd) {
222 c.params = "[filterflags]"
223 c.help = `Mark matching messages on hold.
225Messages that are on hold are not delivered until marked as off hold again, or
226otherwise handled by the admin.
229 flagFilterSort(c.flag, &f, nil)
230 if len(c.Parse()) != 0 {
234 ctlcmdQueueHoldSet(xctl(), f, true)
237func cmdQueueUnhold(c *cmd) {
238 c.params = "[filterflags]"
239 c.help = `Mark matching messages off hold.
241Once off hold, messages can be delivered according to their current next
242delivery attempt. See the "queue schedule" command.
245 flagFilterSort(c.flag, &f, nil)
246 if len(c.Parse()) != 0 {
250 ctlcmdQueueHoldSet(xctl(), f, false)
253func ctlcmdQueueHoldSet(ctl *ctl, f queue.Filter, hold bool) {
254 ctl.xwrite("queueholdset")
255 xctlwriteJSON(ctl, f)
263 fmt.Printf("%s messages changed\n", ctl.xread())
265 log.Fatalf("%s", line)
269func cmdQueueSchedule(c *cmd) {
270 c.params = "[filterflags] [-now] duration"
271 c.help = `Change next delivery attempt for matching messages.
273The next delivery attempt is adjusted by the duration parameter. If the -now
274flag is set, the new delivery attempt is set to the duration added to the
275current time, instead of added to the current scheduled time.
277Schedule immediate delivery with "mox queue schedule -now 0".
280 c.flag.BoolVar(&fromNow, "now", false, "schedule for duration relative to current time instead of relative to current next delivery attempt for messages")
282 flagFilterSort(c.flag, &f, nil)
287 d, err := time.ParseDuration(args[0])
288 xcheckf(err, "parsing duration %q", args[0])
290 ctlcmdQueueSchedule(xctl(), f, fromNow, d)
293func ctlcmdQueueSchedule(ctl *ctl, f queue.Filter, fromNow bool, d time.Duration) {
294 ctl.xwrite("queueschedule")
295 xctlwriteJSON(ctl, f)
301 ctl.xwrite(d.String())
304 fmt.Printf("%s message(s) rescheduled\n", ctl.xread())
306 log.Fatalf("%s", line)
310func cmdQueueTransport(c *cmd) {
311 c.params = "[filterflags] transport"
312 c.help = `Set transport for matching messages.
314By default, the routing rules determine how a message is delivered. The default
315and common case is direct delivery with SMTP. Messages can get a previously
316configured transport assigned to use for delivery, e.g. using submission to
317another mail server or with connections over a SOCKS proxy.
320 flagFilterSort(c.flag, &f, nil)
326 ctlcmdQueueTransport(xctl(), f, args[0])
329func ctlcmdQueueTransport(ctl *ctl, f queue.Filter, transport string) {
330 ctl.xwrite("queuetransport")
331 xctlwriteJSON(ctl, f)
332 ctl.xwrite(transport)
335 fmt.Printf("%s message(s) changed\n", ctl.xread())
337 log.Fatalf("%s", line)
341func cmdQueueRequireTLS(c *cmd) {
342 c.params = "[filterflags] {yes | no | default}"
343 c.help = `Set TLS requirements for delivery of matching messages.
345Value "yes" is handled as if the RequireTLS extension was specified during
348Value "no" is handled as if the message has a header "TLS-Required: No". This
349header is not added by the queue. If messages without this header are relayed
350through other mail servers they will apply their own default TLS policy.
352Value "default" is the default behaviour, currently for unverified opportunistic
356 flagFilterSort(c.flag, &f, nil)
374 ctlcmdQueueRequireTLS(xctl(), f, tlsreq)
377func ctlcmdQueueRequireTLS(ctl *ctl, f queue.Filter, tlsreq *bool) {
378 ctl.xwrite("queuerequiretls")
379 xctlwriteJSON(ctl, f)
391 fmt.Printf("%s message(s) changed\n", ctl.xread())
393 log.Fatalf("%s", line)
397func cmdQueueFail(c *cmd) {
398 c.params = "[filterflags]"
399 c.help = `Fail delivery of matching messages, delivering DSNs.
401Failing a message is handled similar to how delivery is given up after all
402delivery attempts failed. The DSN (delivery status notification) message
403contains a line saying the message was canceled by the admin.
406 flagFilterSort(c.flag, &f, nil)
407 if len(c.Parse()) != 0 {
411 ctlcmdQueueFail(xctl(), f)
414func ctlcmdQueueFail(ctl *ctl, f queue.Filter) {
415 ctl.xwrite("queuefail")
416 xctlwriteJSON(ctl, f)
419 fmt.Printf("%s message(s) marked as failed\n", ctl.xread())
421 log.Fatalf("%s", line)
425func cmdQueueDrop(c *cmd) {
426 c.params = "[filterflags]"
427 c.help = `Remove matching messages from the queue.
429Dangerous operation, this completely removes the message. If you want to store
430the message, use "queue dump" before removing.
433 flagFilterSort(c.flag, &f, nil)
434 if len(c.Parse()) != 0 {
438 ctlcmdQueueDrop(xctl(), f)
441func ctlcmdQueueDrop(ctl *ctl, f queue.Filter) {
442 ctl.xwrite("queuedrop")
443 xctlwriteJSON(ctl, f)
446 fmt.Printf("%s message(s) dropped\n", ctl.xread())
448 log.Fatalf("%s", line)
452func cmdQueueDump(c *cmd) {
454 c.help = `Dump a message from the queue.
456The message is printed to stdout and is in standard internet mail format.
463 ctlcmdQueueDump(xctl(), args[0])
466func ctlcmdQueueDump(ctl *ctl, id string) {
467 ctl.xwrite("queuedump")
470 if _, err := io.Copy(os.Stdout, ctl.reader()); err != nil {
471 log.Fatalf("%s", err)
475func cmdQueueSuppressList(c *cmd) {
476 c.params = "[-account account]"
477 c.help = `Print addresses in suppression list.`
479 c.flag.StringVar(&account, "account", "", "only show suppression list for this account")
485 ctlcmdQueueSuppressList(xctl(), account)
488func ctlcmdQueueSuppressList(ctl *ctl, account string) {
489 ctl.xwrite("queuesuppresslist")
492 if _, err := io.Copy(os.Stdout, ctl.reader()); err != nil {
493 log.Fatalf("%s", err)
497func cmdQueueSuppressAdd(c *cmd) {
498 c.params = "account address"
499 c.help = `Add address to suppression list for account.`
505 ctlcmdQueueSuppressAdd(xctl(), args[0], args[1])
508func ctlcmdQueueSuppressAdd(ctl *ctl, account, address string) {
509 ctl.xwrite("queuesuppressadd")
515func cmdQueueSuppressRemove(c *cmd) {
516 c.params = "account address"
517 c.help = `Remove address from suppression list for account.`
523 ctlcmdQueueSuppressRemove(xctl(), args[0], args[1])
526func ctlcmdQueueSuppressRemove(ctl *ctl, account, address string) {
527 ctl.xwrite("queuesuppressremove")
533func cmdQueueSuppressLookup(c *cmd) {
534 c.params = "[-account account] address"
535 c.help = `Check if address is present in suppression list, for any or specific account.`
537 c.flag.StringVar(&account, "account", "", "only check address in specified account")
543 ctlcmdQueueSuppressLookup(xctl(), account, args[0])
546func ctlcmdQueueSuppressLookup(ctl *ctl, account, address string) {
547 ctl.xwrite("queuesuppresslookup")
551 if _, err := io.Copy(os.Stdout, ctl.reader()); err != nil {
552 log.Fatalf("%s", err)
556func cmdQueueRetiredList(c *cmd) {
557 c.params = "[filtersortflags]"
558 c.help = `List matching messages in the retired queue.
560Prints messages with their ID and results.
562 var f queue.RetiredFilter
563 var s queue.RetiredSort
564 flagRetiredFilterSort(c.flag, &f, &s)
565 if len(c.Parse()) != 0 {
569 ctlcmdQueueRetiredList(xctl(), f, s)
572func ctlcmdQueueRetiredList(ctl *ctl, f queue.RetiredFilter, s queue.RetiredSort) {
573 ctl.xwrite("queueretiredlist")
574 xctlwriteJSON(ctl, f)
575 xctlwriteJSON(ctl, s)
577 if _, err := io.Copy(os.Stdout, ctl.reader()); err != nil {
578 log.Fatalf("%s", err)
582func cmdQueueRetiredPrint(c *cmd) {
584 c.help = `Print a message from the retired queue.
586Prints a JSON representation of the information from the retired queue.
593 ctlcmdQueueRetiredPrint(xctl(), args[0])
596func ctlcmdQueueRetiredPrint(ctl *ctl, id string) {
597 ctl.xwrite("queueretiredprint")
600 if _, err := io.Copy(os.Stdout, ctl.reader()); err != nil {
601 log.Fatalf("%s", err)
605// note: outgoing hook events are in queue/hooks.go, mox-/config.go, queue.go and webapi/gendoc.sh. keep in sync.
607// flagHookFilterSort is used by many of the queue commands to accept flags for
608// filtering the webhooks the operation applies to.
609func flagHookFilterSort(fs *flag.FlagSet, f *queue.HookFilter, s *queue.HookSort) {
610 fs.Func("ids", "comma-separated list of webhook IDs", func(v string) error {
611 for _, s := range strings.Split(v, ",") {
612 id, err := strconv.ParseInt(s, 10, 64)
616 f.IDs = append(f.IDs, id)
620 fs.IntVar(&f.Max, "n", 0, "number of webhooks to return")
621 fs.StringVar(&f.Account, "account", "", "account that queued the message/webhook")
622 fs.StringVar(&f.Submitted, "submitted", "", `filter by time of submission relative to now, value must start with "<" (before now) or ">" (after now)`)
623 fs.StringVar(&f.NextAttempt, "nextattempt", "", `filter by time of next delivery attempt relative to now, value must start with "<" (before now) or ">" (after now)`)
624 fs.Func("event", `event this webhook is about: incoming, delivered, suppressed, delayed, failed, relayed, expanded, canceled, unrecognized`, func(v string) error {
626 case "incoming", "delivered", "suppressed", "delayed", "failed", "relayed", "expanded", "canceled", "unrecognized":
629 return fmt.Errorf("invalid parameter %q", v)
634 fs.Func("sort", `field to sort by, "nextattempt" (default) or "queued"`, func(v string) error {
637 s.Field = "NextAttempt"
641 return fmt.Errorf("unknown value %q", v)
645 fs.BoolVar(&s.Asc, "asc", false, "sort ascending instead of descending (default)")
649// flagHookRetiredFilterSort is used by many of the queue commands to accept flags
650// for filtering the webhooks the operation applies to.
651func flagHookRetiredFilterSort(fs *flag.FlagSet, f *queue.HookRetiredFilter, s *queue.HookRetiredSort) {
652 fs.Func("ids", "comma-separated list of retired webhook IDs", func(v string) error {
653 for _, s := range strings.Split(v, ",") {
654 id, err := strconv.ParseInt(s, 10, 64)
658 f.IDs = append(f.IDs, id)
662 fs.IntVar(&f.Max, "n", 0, "number of webhooks to return")
663 fs.StringVar(&f.Account, "account", "", "account that queued the message/webhook")
664 fs.StringVar(&f.Submitted, "submitted", "", `filter by time of submission relative to now, value must start with "<" (before now) or ">" (after now)`)
665 fs.StringVar(&f.LastActivity, "lastactivity", "", `filter by time of last activity relative to now, value must start with "<" (before now) or ">" (after now)`)
666 fs.Func("event", `event this webhook is about: incoming, delivered, suppressed, delayed, failed, relayed, expanded, canceled, unrecognized`, func(v string) error {
668 case "incoming", "delivered", "suppressed", "delayed", "failed", "relayed", "expanded", "canceled", "unrecognized":
671 return fmt.Errorf("invalid parameter %q", v)
676 fs.Func("sort", `field to sort by, "lastactivity" (default) or "queued"`, func(v string) error {
679 s.Field = "LastActivity"
683 return fmt.Errorf("unknown value %q", v)
687 fs.BoolVar(&s.Asc, "asc", false, "sort ascending instead of descending (default)")
691func cmdQueueHookList(c *cmd) {
692 c.params = "[filtersortflags]"
693 c.help = `List matching webhooks in the queue.
695Prints list of webhooks, their IDs and basic information.
697 var f queue.HookFilter
699 flagHookFilterSort(c.flag, &f, &s)
700 if len(c.Parse()) != 0 {
704 ctlcmdQueueHookList(xctl(), f, s)
707func ctlcmdQueueHookList(ctl *ctl, f queue.HookFilter, s queue.HookSort) {
708 ctl.xwrite("queuehooklist")
709 xctlwriteJSON(ctl, f)
710 xctlwriteJSON(ctl, s)
712 if _, err := io.Copy(os.Stdout, ctl.reader()); err != nil {
713 log.Fatalf("%s", err)
717func cmdQueueHookSchedule(c *cmd) {
718 c.params = "[filterflags] duration"
719 c.help = `Change next delivery attempt for matching webhooks.
721The next delivery attempt is adjusted by the duration parameter. If the -now
722flag is set, the new delivery attempt is set to the duration added to the
723current time, instead of added to the current scheduled time.
725Schedule immediate delivery with "mox queue schedule -now 0".
728 c.flag.BoolVar(&fromNow, "now", false, "schedule for duration relative to current time instead of relative to current next delivery attempt for webhooks")
729 var f queue.HookFilter
730 flagHookFilterSort(c.flag, &f, nil)
735 d, err := time.ParseDuration(args[0])
736 xcheckf(err, "parsing duration %q", args[0])
738 ctlcmdQueueHookSchedule(xctl(), f, fromNow, d)
741func ctlcmdQueueHookSchedule(ctl *ctl, f queue.HookFilter, fromNow bool, d time.Duration) {
742 ctl.xwrite("queuehookschedule")
743 xctlwriteJSON(ctl, f)
749 ctl.xwrite(d.String())
752 fmt.Printf("%s webhook(s) rescheduled\n", ctl.xread())
754 log.Fatalf("%s", line)
758func cmdQueueHookCancel(c *cmd) {
759 c.params = "[filterflags]"
760 c.help = `Fail delivery of matching webhooks.`
761 var f queue.HookFilter
762 flagHookFilterSort(c.flag, &f, nil)
763 if len(c.Parse()) != 0 {
767 ctlcmdQueueHookCancel(xctl(), f)
770func ctlcmdQueueHookCancel(ctl *ctl, f queue.HookFilter) {
771 ctl.xwrite("queuehookcancel")
772 xctlwriteJSON(ctl, f)
775 fmt.Printf("%s webhook(s)s marked as canceled\n", ctl.xread())
777 log.Fatalf("%s", line)
781func cmdQueueHookPrint(c *cmd) {
783 c.help = `Print details of a webhook from the queue.
785The webhook is printed to stdout as JSON.
792 ctlcmdQueueHookPrint(xctl(), args[0])
795func ctlcmdQueueHookPrint(ctl *ctl, id string) {
796 ctl.xwrite("queuehookprint")
799 if _, err := io.Copy(os.Stdout, ctl.reader()); err != nil {
800 log.Fatalf("%s", err)
804func cmdQueueHookRetiredList(c *cmd) {
805 c.params = "[filtersortflags]"
806 c.help = `List matching webhooks in the retired queue.
808Prints list of retired webhooks, their IDs and basic information.
810 var f queue.HookRetiredFilter
811 var s queue.HookRetiredSort
812 flagHookRetiredFilterSort(c.flag, &f, &s)
813 if len(c.Parse()) != 0 {
817 ctlcmdQueueHookRetiredList(xctl(), f, s)
820func ctlcmdQueueHookRetiredList(ctl *ctl, f queue.HookRetiredFilter, s queue.HookRetiredSort) {
821 ctl.xwrite("queuehookretiredlist")
822 xctlwriteJSON(ctl, f)
823 xctlwriteJSON(ctl, s)
825 if _, err := io.Copy(os.Stdout, ctl.reader()); err != nil {
826 log.Fatalf("%s", err)
830func cmdQueueHookRetiredPrint(c *cmd) {
832 c.help = `Print details of a webhook from the retired queue.
834The retired webhook is printed to stdout as JSON.
841 ctlcmdQueueHookRetiredPrint(xctl(), args[0])
844func ctlcmdQueueHookRetiredPrint(ctl *ctl, id string) {
845 ctl.xwrite("queuehookretiredprint")
848 if _, err := io.Copy(os.Stdout, ctl.reader()); err != nil {
849 log.Fatalf("%s", err)