maddy

Fork https://github.com/foxcpp/maddy

git clone git://git.lin.moe/go/maddy.git

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19package ctl
 20
 21import (
 22	"bytes"
 23	"errors"
 24	"fmt"
 25	"io"
 26	"os"
 27	"strings"
 28	"time"
 29
 30	"github.com/emersion/go-imap"
 31	imapsql "github.com/foxcpp/go-imap-sql"
 32	"github.com/foxcpp/maddy/framework/module"
 33	maddycli "github.com/foxcpp/maddy/internal/cli"
 34	clitools2 "github.com/foxcpp/maddy/internal/cli/clitools"
 35	"github.com/urfave/cli/v2"
 36)
 37
 38func init() {
 39	maddycli.AddSubcommand(
 40		&cli.Command{
 41			Name:  "imap-mboxes",
 42			Usage: "IMAP mailboxes (folders) management",
 43			Subcommands: []*cli.Command{
 44				{
 45					Name:      "list",
 46					Usage:     "Show mailboxes of user",
 47					ArgsUsage: "USERNAME",
 48					Flags: []cli.Flag{
 49						&cli.StringFlag{
 50							Name:    "cfg-block",
 51							Usage:   "Module configuration block to use",
 52							EnvVars: []string{"MADDY_CFGBLOCK"},
 53							Value:   "local_mailboxes",
 54						},
 55						&cli.BoolFlag{
 56							Name:    "subscribed",
 57							Aliases: []string{"s"},
 58							Usage:   "List only subscribed mailboxes",
 59						},
 60					},
 61					Action: func(ctx *cli.Context) error {
 62						be, err := openStorage(ctx)
 63						if err != nil {
 64							return err
 65						}
 66						defer closeIfNeeded(be)
 67						return mboxesList(be, ctx)
 68					},
 69				},
 70				{
 71					Name:      "create",
 72					Usage:     "Create mailbox",
 73					ArgsUsage: "USERNAME NAME",
 74					Flags: []cli.Flag{
 75						&cli.StringFlag{
 76							Name:    "cfg-block",
 77							Usage:   "Module configuration block to use",
 78							EnvVars: []string{"MADDY_CFGBLOCK"},
 79							Value:   "local_mailboxes",
 80						},
 81						&cli.StringFlag{
 82							Name:  "special",
 83							Usage: "Set SPECIAL-USE attribute on mailbox; valid values: archive, drafts, junk, sent, trash",
 84						},
 85					},
 86					Action: func(ctx *cli.Context) error {
 87						be, err := openStorage(ctx)
 88						if err != nil {
 89							return err
 90						}
 91						defer closeIfNeeded(be)
 92						return mboxesCreate(be, ctx)
 93					},
 94				},
 95				{
 96					Name:        "remove",
 97					Usage:       "Remove mailbox",
 98					Description: "WARNING: All contents of mailbox will be irrecoverably lost.",
 99					ArgsUsage:   "USERNAME MAILBOX",
100					Flags: []cli.Flag{
101						&cli.StringFlag{
102							Name:    "cfg-block",
103							Usage:   "Module configuration block to use",
104							EnvVars: []string{"MADDY_CFGBLOCK"},
105							Value:   "local_mailboxes",
106						},
107						&cli.BoolFlag{
108							Name:    "yes",
109							Aliases: []string{"y"},
110							Usage:   "Don't ask for confirmation",
111						},
112					},
113					Action: func(ctx *cli.Context) error {
114						be, err := openStorage(ctx)
115						if err != nil {
116							return err
117						}
118						defer closeIfNeeded(be)
119						return mboxesRemove(be, ctx)
120					},
121				},
122				{
123					Name:        "rename",
124					Usage:       "Rename mailbox",
125					Description: "Rename may cause unexpected failures on client-side so be careful.",
126					ArgsUsage:   "USERNAME OLDNAME NEWNAME",
127					Flags: []cli.Flag{
128						&cli.StringFlag{
129							Name:    "cfg-block",
130							Usage:   "Module configuration block to use",
131							EnvVars: []string{"MADDY_CFGBLOCK"},
132							Value:   "local_mailboxes",
133						},
134					},
135					Action: func(ctx *cli.Context) error {
136						be, err := openStorage(ctx)
137						if err != nil {
138							return err
139						}
140						defer closeIfNeeded(be)
141						return mboxesRename(be, ctx)
142					},
143				},
144			},
145		})
146	maddycli.AddSubcommand(&cli.Command{
147		Name:  "imap-msgs",
148		Usage: "IMAP messages management",
149		Subcommands: []*cli.Command{
150			{
151				Name:        "add",
152				Usage:       "Add message to mailbox",
153				ArgsUsage:   "USERNAME MAILBOX",
154				Description: "Reads message body (with headers) from stdin. Prints UID of created message on success.",
155				Flags: []cli.Flag{
156					&cli.StringFlag{
157						Name:    "cfg-block",
158						Usage:   "Module configuration block to use",
159						EnvVars: []string{"MADDY_CFGBLOCK"},
160						Value:   "local_mailboxes",
161					},
162					&cli.StringSliceFlag{
163						Name:    "flag",
164						Aliases: []string{"f"},
165						Usage:   "Add flag to message. Can be specified multiple times",
166					},
167					&cli.TimestampFlag{
168						Layout:  time.RFC3339,
169						Name:    "date",
170						Aliases: []string{"d"},
171						Usage:   "Set internal date value to specified one in ISO 8601 format (2006-01-02T15:04:05Z07:00)",
172					},
173				},
174				Action: func(ctx *cli.Context) error {
175					be, err := openStorage(ctx)
176					if err != nil {
177						return err
178					}
179					defer closeIfNeeded(be)
180					return msgsAdd(be, ctx)
181				},
182			},
183			{
184				Name:        "add-flags",
185				Usage:       "Add flags to messages",
186				ArgsUsage:   "USERNAME MAILBOX SEQ FLAGS...",
187				Description: "Add flags to all messages matched by SEQ.",
188				Flags: []cli.Flag{
189					&cli.StringFlag{
190						Name:    "cfg-block",
191						Usage:   "Module configuration block to use",
192						EnvVars: []string{"MADDY_CFGBLOCK"},
193						Value:   "local_mailboxes",
194					},
195					&cli.BoolFlag{
196						Name:    "uid",
197						Aliases: []string{"u"},
198						Usage:   "Use UIDs for SEQSET instead of sequence numbers",
199					},
200				},
201				Action: func(ctx *cli.Context) error {
202					be, err := openStorage(ctx)
203					if err != nil {
204						return err
205					}
206					defer closeIfNeeded(be)
207					return msgsFlags(be, ctx)
208				},
209			},
210			{
211				Name:        "rem-flags",
212				Usage:       "Remove flags from messages",
213				ArgsUsage:   "USERNAME MAILBOX SEQ FLAGS...",
214				Description: "Remove flags from all messages matched by SEQ.",
215				Flags: []cli.Flag{
216					&cli.StringFlag{
217						Name:    "cfg-block",
218						Usage:   "Module configuration block to use",
219						EnvVars: []string{"MADDY_CFGBLOCK"},
220						Value:   "local_mailboxes",
221					},
222					&cli.BoolFlag{
223						Name:    "uid",
224						Aliases: []string{"u"},
225						Usage:   "Use UIDs for SEQSET instead of sequence numbers",
226					},
227				},
228				Action: func(ctx *cli.Context) error {
229					be, err := openStorage(ctx)
230					if err != nil {
231						return err
232					}
233					defer closeIfNeeded(be)
234					return msgsFlags(be, ctx)
235				},
236			},
237			{
238				Name:        "set-flags",
239				Usage:       "Set flags on messages",
240				ArgsUsage:   "USERNAME MAILBOX SEQ FLAGS...",
241				Description: "Set flags on all messages matched by SEQ.",
242				Flags: []cli.Flag{
243					&cli.StringFlag{
244						Name:    "cfg-block",
245						Usage:   "Module configuration block to use",
246						EnvVars: []string{"MADDY_CFGBLOCK"},
247						Value:   "local_mailboxes",
248					},
249					&cli.BoolFlag{
250						Name:    "uid",
251						Aliases: []string{"u"},
252						Usage:   "Use UIDs for SEQSET instead of sequence numbers",
253					},
254				},
255				Action: func(ctx *cli.Context) error {
256					be, err := openStorage(ctx)
257					if err != nil {
258						return err
259					}
260					defer closeIfNeeded(be)
261					return msgsFlags(be, ctx)
262				},
263			},
264			{
265				Name:      "remove",
266				Usage:     "Remove messages from mailbox",
267				ArgsUsage: "USERNAME MAILBOX SEQSET",
268				Flags: []cli.Flag{
269					&cli.StringFlag{
270						Name:    "cfg-block",
271						Usage:   "Module configuration block to use",
272						EnvVars: []string{"MADDY_CFGBLOCK"},
273						Value:   "local_mailboxes",
274					},
275					&cli.BoolFlag{
276						Name:    "uid,u",
277						Aliases: []string{"u"},
278						Usage:   "Use UIDs for SEQSET instead of sequence numbers",
279					},
280					&cli.BoolFlag{
281						Name:    "yes",
282						Aliases: []string{"y"},
283						Usage:   "Don't ask for confirmation",
284					},
285				},
286				Action: func(ctx *cli.Context) error {
287					be, err := openStorage(ctx)
288					if err != nil {
289						return err
290					}
291					defer closeIfNeeded(be)
292					return msgsRemove(be, ctx)
293				},
294			},
295			{
296				Name:        "copy",
297				Usage:       "Copy messages between mailboxes",
298				Description: "Note: You can't copy between mailboxes of different users. APPENDLIMIT of target mailbox is not enforced.",
299				ArgsUsage:   "USERNAME SRCMAILBOX SEQSET TGTMAILBOX",
300				Flags: []cli.Flag{
301					&cli.StringFlag{
302						Name:    "cfg-block",
303						Usage:   "Module configuration block to use",
304						EnvVars: []string{"MADDY_CFGBLOCK"},
305						Value:   "local_mailboxes",
306					},
307					&cli.BoolFlag{
308						Name:    "uid",
309						Aliases: []string{"u"},
310						Usage:   "Use UIDs for SEQSET instead of sequence numbers",
311					},
312				},
313				Action: func(ctx *cli.Context) error {
314					be, err := openStorage(ctx)
315					if err != nil {
316						return err
317					}
318					defer closeIfNeeded(be)
319					return msgsCopy(be, ctx)
320				},
321			},
322			{
323				Name:        "move",
324				Usage:       "Move messages between mailboxes",
325				Description: "Note: You can't move between mailboxes of different users. APPENDLIMIT of target mailbox is not enforced.",
326				ArgsUsage:   "USERNAME SRCMAILBOX SEQSET TGTMAILBOX",
327				Flags: []cli.Flag{
328					&cli.StringFlag{
329						Name:    "cfg-block",
330						Usage:   "Module configuration block to use",
331						EnvVars: []string{"MADDY_CFGBLOCK"},
332						Value:   "local_mailboxes",
333					},
334					&cli.BoolFlag{
335						Name:    "uid",
336						Aliases: []string{"u"},
337						Usage:   "Use UIDs for SEQSET instead of sequence numbers",
338					},
339				},
340				Action: func(ctx *cli.Context) error {
341					be, err := openStorage(ctx)
342					if err != nil {
343						return err
344					}
345					defer closeIfNeeded(be)
346					return msgsMove(be, ctx)
347				},
348			},
349			{
350				Name:        "list",
351				Usage:       "List messages in mailbox",
352				Description: "If SEQSET is specified - only show messages that match it.",
353				ArgsUsage:   "USERNAME MAILBOX [SEQSET]",
354				Flags: []cli.Flag{
355					&cli.StringFlag{
356						Name:    "cfg-block",
357						Usage:   "Module configuration block to use",
358						EnvVars: []string{"MADDY_CFGBLOCK"},
359						Value:   "local_mailboxes",
360					},
361					&cli.BoolFlag{
362						Name:    "uid",
363						Aliases: []string{"u"},
364						Usage:   "Use UIDs for SEQSET instead of sequence numbers",
365					},
366					&cli.BoolFlag{
367						Name:    "full,f",
368						Aliases: []string{"f"},
369						Usage:   "Show entire envelope and all server meta-data",
370					},
371				},
372				Action: func(ctx *cli.Context) error {
373					be, err := openStorage(ctx)
374					if err != nil {
375						return err
376					}
377					defer closeIfNeeded(be)
378					return msgsList(be, ctx)
379				},
380			},
381			{
382				Name:        "dump",
383				Usage:       "Dump message body",
384				Description: "If passed SEQ matches multiple messages - they will be joined.",
385				ArgsUsage:   "USERNAME MAILBOX SEQ",
386				Flags: []cli.Flag{
387					&cli.StringFlag{
388						Name:    "cfg-block",
389						Usage:   "Module configuration block to use",
390						EnvVars: []string{"MADDY_CFGBLOCK"},
391						Value:   "local_mailboxes",
392					},
393					&cli.BoolFlag{
394						Name:    "uid",
395						Aliases: []string{"u"},
396						Usage:   "Use UIDs for SEQ instead of sequence numbers",
397					},
398				},
399				Action: func(ctx *cli.Context) error {
400					be, err := openStorage(ctx)
401					if err != nil {
402						return err
403					}
404					defer closeIfNeeded(be)
405					return msgsDump(be, ctx)
406				},
407			},
408		},
409	})
410}
411
412func FormatAddress(addr *imap.Address) string {
413	return fmt.Sprintf("%s <%s@%s>", addr.PersonalName, addr.MailboxName, addr.HostName)
414}
415
416func FormatAddressList(addrs []*imap.Address) string {
417	res := make([]string, 0, len(addrs))
418	for _, addr := range addrs {
419		res = append(res, FormatAddress(addr))
420	}
421	return strings.Join(res, ", ")
422}
423
424func mboxesList(be module.Storage, ctx *cli.Context) error {
425	username := ctx.Args().First()
426	if username == "" {
427		return cli.Exit("Error: USERNAME is required", 2)
428	}
429
430	u, err := be.GetIMAPAcct(username)
431	if err != nil {
432		return err
433	}
434
435	mboxes, err := u.ListMailboxes(ctx.Bool("subscribed,s"))
436	if err != nil {
437		return err
438	}
439
440	if len(mboxes) == 0 && !ctx.Bool("quiet") {
441		fmt.Fprintln(os.Stderr, "No mailboxes.")
442	}
443
444	for _, info := range mboxes {
445		if len(info.Attributes) != 0 {
446			fmt.Print(info.Name, "\t", info.Attributes, "\n")
447		} else {
448			fmt.Println(info.Name)
449		}
450	}
451
452	return nil
453}
454
455func mboxesCreate(be module.Storage, ctx *cli.Context) error {
456	username := ctx.Args().First()
457	if username == "" {
458		return cli.Exit("Error: USERNAME is required", 2)
459	}
460	name := ctx.Args().Get(1)
461	if name == "" {
462		return cli.Exit("Error: NAME is required", 2)
463	}
464
465	u, err := be.GetIMAPAcct(username)
466	if err != nil {
467		return err
468	}
469
470	if ctx.IsSet("special") {
471		attr := "\\" + strings.Title(ctx.String("special")) //nolint:staticcheck
472		// (nolint) strings.Title is perfectly fine there since special mailbox tags will never use Unicode.
473
474		suu, ok := u.(SpecialUseUser)
475		if !ok {
476			return cli.Exit("Error: storage backend does not support SPECIAL-USE IMAP extension", 2)
477		}
478
479		return suu.CreateMailboxSpecial(name, attr)
480	}
481
482	return u.CreateMailbox(name)
483}
484
485func mboxesRemove(be module.Storage, ctx *cli.Context) error {
486	username := ctx.Args().First()
487	if username == "" {
488		return cli.Exit("Error: USERNAME is required", 2)
489	}
490	name := ctx.Args().Get(1)
491	if name == "" {
492		return cli.Exit("Error: NAME is required", 2)
493	}
494
495	u, err := be.GetIMAPAcct(username)
496	if err != nil {
497		return err
498	}
499
500	if !ctx.Bool("yes") {
501		status, err := u.Status(name, []imap.StatusItem{imap.StatusMessages})
502		if err != nil {
503			return err
504		}
505
506		if status.Messages != 0 {
507			fmt.Fprintf(os.Stderr, "Mailbox %s contains %d messages.\n", name, status.Messages)
508		}
509
510		if !clitools2.Confirmation("Are you sure you want to delete that mailbox?", false) {
511			return errors.New("Cancelled")
512		}
513	}
514
515	return u.DeleteMailbox(name)
516}
517
518func mboxesRename(be module.Storage, ctx *cli.Context) error {
519	username := ctx.Args().First()
520	if username == "" {
521		return cli.Exit("Error: USERNAME is required", 2)
522	}
523	oldName := ctx.Args().Get(1)
524	if oldName == "" {
525		return cli.Exit("Error: OLDNAME is required", 2)
526	}
527	newName := ctx.Args().Get(2)
528	if newName == "" {
529		return cli.Exit("Error: NEWNAME is required", 2)
530	}
531
532	u, err := be.GetIMAPAcct(username)
533	if err != nil {
534		return err
535	}
536
537	return u.RenameMailbox(oldName, newName)
538}
539
540func msgsAdd(be module.Storage, ctx *cli.Context) error {
541	username := ctx.Args().First()
542	if username == "" {
543		return cli.Exit("Error: USERNAME is required", 2)
544	}
545	name := ctx.Args().Get(1)
546	if name == "" {
547		return cli.Exit("Error: MAILBOX is required", 2)
548	}
549
550	u, err := be.GetIMAPAcct(username)
551	if err != nil {
552		return err
553	}
554
555	flags := ctx.StringSlice("flag")
556	if flags == nil {
557		flags = []string{}
558	}
559
560	date := time.Now()
561	if ctx.IsSet("date") {
562		date = *ctx.Timestamp("date")
563	}
564
565	buf := bytes.Buffer{}
566	if _, err := io.Copy(&buf, os.Stdin); err != nil {
567		return err
568	}
569
570	if buf.Len() == 0 {
571		return cli.Exit("Error: Empty message, refusing to continue", 2)
572	}
573
574	status, err := u.Status(name, []imap.StatusItem{imap.StatusUidNext})
575	if err != nil {
576		return err
577	}
578
579	if err := u.CreateMessage(name, flags, date, &buf, nil); err != nil {
580		return err
581	}
582
583	// TODO: Use APPENDUID
584	fmt.Println(status.UidNext)
585
586	return nil
587}
588
589func msgsRemove(be module.Storage, ctx *cli.Context) error {
590	username := ctx.Args().First()
591	if username == "" {
592		return cli.Exit("Error: USERNAME is required", 2)
593	}
594	name := ctx.Args().Get(1)
595	if name == "" {
596		return cli.Exit("Error: MAILBOX is required", 2)
597	}
598	seqset := ctx.Args().Get(2)
599	if seqset == "" {
600		return cli.Exit("Error: SEQSET is required", 2)
601	}
602
603	if !ctx.Bool("uid") {
604		fmt.Fprintln(os.Stderr, "WARNING: --uid=true will be the default in 0.7")
605	}
606
607	seq, err := imap.ParseSeqSet(seqset)
608	if err != nil {
609		return err
610	}
611
612	u, err := be.GetIMAPAcct(username)
613	if err != nil {
614		return err
615	}
616
617	_, mbox, err := u.GetMailbox(name, true, nil)
618	if err != nil {
619		return err
620	}
621
622	if !ctx.Bool("yes") {
623		if !clitools2.Confirmation("Are you sure you want to delete these messages?", false) {
624			return errors.New("Cancelled")
625		}
626	}
627
628	mboxB := mbox.(*imapsql.Mailbox)
629	return mboxB.DelMessages(ctx.Bool("uid"), seq)
630}
631
632func msgsCopy(be module.Storage, ctx *cli.Context) error {
633	username := ctx.Args().First()
634	if username == "" {
635		return cli.Exit("Error: USERNAME is required", 2)
636	}
637	srcName := ctx.Args().Get(1)
638	if srcName == "" {
639		return cli.Exit("Error: SRCMAILBOX is required", 2)
640	}
641	seqset := ctx.Args().Get(2)
642	if seqset == "" {
643		return cli.Exit("Error: SEQSET is required", 2)
644	}
645	tgtName := ctx.Args().Get(3)
646	if tgtName == "" {
647		return cli.Exit("Error: TGTMAILBOX is required", 2)
648	}
649
650	if !ctx.Bool("uid") {
651		fmt.Fprintln(os.Stderr, "WARNING: --uid=true will be the default in 0.7")
652	}
653
654	seq, err := imap.ParseSeqSet(seqset)
655	if err != nil {
656		return err
657	}
658
659	u, err := be.GetIMAPAcct(username)
660	if err != nil {
661		return err
662	}
663
664	_, srcMbox, err := u.GetMailbox(srcName, true, nil)
665	if err != nil {
666		return err
667	}
668
669	return srcMbox.CopyMessages(ctx.Bool("uid"), seq, tgtName)
670}
671
672func msgsMove(be module.Storage, ctx *cli.Context) error {
673	username := ctx.Args().First()
674	if username == "" {
675		return cli.Exit("Error: USERNAME is required", 2)
676	}
677	srcName := ctx.Args().Get(1)
678	if srcName == "" {
679		return cli.Exit("Error: SRCMAILBOX is required", 2)
680	}
681	seqset := ctx.Args().Get(2)
682	if seqset == "" {
683		return cli.Exit("Error: SEQSET is required", 2)
684	}
685	tgtName := ctx.Args().Get(3)
686	if tgtName == "" {
687		return cli.Exit("Error: TGTMAILBOX is required", 2)
688	}
689
690	if !ctx.Bool("uid") {
691		fmt.Fprintln(os.Stderr, "WARNING: --uid=true will be the default in 0.7")
692	}
693
694	seq, err := imap.ParseSeqSet(seqset)
695	if err != nil {
696		return err
697	}
698
699	u, err := be.GetIMAPAcct(username)
700	if err != nil {
701		return err
702	}
703
704	_, srcMbox, err := u.GetMailbox(srcName, true, nil)
705	if err != nil {
706		return err
707	}
708
709	moveMbox := srcMbox.(*imapsql.Mailbox)
710
711	return moveMbox.MoveMessages(ctx.Bool("uid"), seq, tgtName)
712}
713
714func msgsList(be module.Storage, ctx *cli.Context) error {
715	username := ctx.Args().First()
716	if username == "" {
717		return cli.Exit("Error: USERNAME is required", 2)
718	}
719	mboxName := ctx.Args().Get(1)
720	if mboxName == "" {
721		return cli.Exit("Error: MAILBOX is required", 2)
722	}
723	seqset := ctx.Args().Get(2)
724	uid := ctx.Bool("uid")
725	if seqset == "" {
726		seqset = "1:*"
727		uid = true
728	} else if !uid {
729		fmt.Fprintln(os.Stderr, "WARNING: --uid=true will be the default in 0.7")
730	}
731
732	seq, err := imap.ParseSeqSet(seqset)
733	if err != nil {
734		return err
735	}
736
737	u, err := be.GetIMAPAcct(username)
738	if err != nil {
739		return err
740	}
741
742	_, mbox, err := u.GetMailbox(mboxName, true, nil)
743	if err != nil {
744		return err
745	}
746
747	ch := make(chan *imap.Message, 10)
748	go func() {
749		err = mbox.ListMessages(uid, seq, []imap.FetchItem{imap.FetchEnvelope, imap.FetchInternalDate, imap.FetchRFC822Size, imap.FetchFlags, imap.FetchUid}, ch)
750	}()
751
752	for msg := range ch {
753		if !ctx.Bool("full") {
754			fmt.Printf("UID %d: %s - %s\n  %v, %v\n\n", msg.Uid, FormatAddressList(msg.Envelope.From), msg.Envelope.Subject, msg.Flags, msg.Envelope.Date)
755			continue
756		}
757
758		fmt.Println("- Server meta-data:")
759		fmt.Println("UID:", msg.Uid)
760		fmt.Println("Sequence number:", msg.SeqNum)
761		fmt.Println("Flags:", msg.Flags)
762		fmt.Println("Body size:", msg.Size)
763		fmt.Println("Internal date:", msg.InternalDate.Unix(), msg.InternalDate)
764		fmt.Println("- Envelope:")
765		if len(msg.Envelope.From) != 0 {
766			fmt.Println("From:", FormatAddressList(msg.Envelope.From))
767		}
768		if len(msg.Envelope.To) != 0 {
769			fmt.Println("To:", FormatAddressList(msg.Envelope.To))
770		}
771		if len(msg.Envelope.Cc) != 0 {
772			fmt.Println("CC:", FormatAddressList(msg.Envelope.Cc))
773		}
774		if len(msg.Envelope.Bcc) != 0 {
775			fmt.Println("BCC:", FormatAddressList(msg.Envelope.Bcc))
776		}
777		if msg.Envelope.InReplyTo != "" {
778			fmt.Println("In-Reply-To:", msg.Envelope.InReplyTo)
779		}
780		if msg.Envelope.MessageId != "" {
781			fmt.Println("Message-Id:", msg.Envelope.MessageId)
782		}
783		if !msg.Envelope.Date.IsZero() {
784			fmt.Println("Date:", msg.Envelope.Date.Unix(), msg.Envelope.Date)
785		}
786		if msg.Envelope.Subject != "" {
787			fmt.Println("Subject:", msg.Envelope.Subject)
788		}
789		fmt.Println()
790	}
791	return err
792}
793
794func msgsDump(be module.Storage, ctx *cli.Context) error {
795	username := ctx.Args().First()
796	if username == "" {
797		return cli.Exit("Error: USERNAME is required", 2)
798	}
799	mboxName := ctx.Args().Get(1)
800	if mboxName == "" {
801		return cli.Exit("Error: MAILBOX is required", 2)
802	}
803	seqset := ctx.Args().Get(2)
804	uid := ctx.Bool("uid")
805	if seqset == "" {
806		seqset = "1:*"
807		uid = true
808	} else if !uid {
809		fmt.Fprintln(os.Stderr, "WARNING: --uid=true will be the default in 0.7")
810	}
811
812	seq, err := imap.ParseSeqSet(seqset)
813	if err != nil {
814		return err
815	}
816
817	u, err := be.GetIMAPAcct(username)
818	if err != nil {
819		return err
820	}
821
822	_, mbox, err := u.GetMailbox(mboxName, true, nil)
823	if err != nil {
824		return err
825	}
826
827	ch := make(chan *imap.Message, 10)
828	go func() {
829		err = mbox.ListMessages(uid, seq, []imap.FetchItem{imap.FetchRFC822}, ch)
830	}()
831
832	for msg := range ch {
833		for _, v := range msg.Body {
834			if _, err := io.Copy(os.Stdout, v); err != nil {
835				return err
836			}
837		}
838	}
839	return err
840}
841
842func msgsFlags(be module.Storage, ctx *cli.Context) error {
843	username := ctx.Args().First()
844	if username == "" {
845		return cli.Exit("Error: USERNAME is required", 2)
846	}
847	name := ctx.Args().Get(1)
848	if name == "" {
849		return cli.Exit("Error: MAILBOX is required", 2)
850	}
851	seqStr := ctx.Args().Get(2)
852	if seqStr == "" {
853		return cli.Exit("Error: SEQ is required", 2)
854	}
855
856	if !ctx.Bool("uid") {
857		fmt.Fprintln(os.Stderr, "WARNING: --uid=true will be the default in 0.7")
858	}
859
860	seq, err := imap.ParseSeqSet(seqStr)
861	if err != nil {
862		return err
863	}
864
865	u, err := be.GetIMAPAcct(username)
866	if err != nil {
867		return err
868	}
869
870	_, mbox, err := u.GetMailbox(name, false, nil)
871	if err != nil {
872		return err
873	}
874
875	flags := ctx.Args().Slice()[3:]
876	if len(flags) == 0 {
877		return cli.Exit("Error: at least once FLAG is required", 2)
878	}
879
880	var op imap.FlagsOp
881	switch ctx.Command.Name {
882	case "add-flags":
883		op = imap.AddFlags
884	case "rem-flags":
885		op = imap.RemoveFlags
886	case "set-flags":
887		op = imap.SetFlags
888	default:
889		panic("unknown command: " + ctx.Command.Name)
890	}
891
892	return mbox.UpdateMessagesFlags(ctx.Bool("uid"), seq, op, true, flags)
893}