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)