package main import ( "errors" "flag" "fmt" "io" "io/ioutil" "math" "os" "strconv" "strings" "git.sr.ht/~blallo/papero/cli" "git.sr.ht/~blallo/papero/imaputils" "github.com/emersion/go-imap" ) const maxUInt32 = int(^uint32(0)) var infoVerbs cli.CommandMap func init() { infoVerbs = cli.CommandMap{ "list-mailboxes": LsMailboxCmd{}, // "list-subscribed": LsSubscribedCmd{}, "list-messages": LsMsgCmd{}, "display-message": CatMsgCmd{}, } } type InfoCmd struct{} func (i InfoCmd) Func(args []string) error { flagset := flag.NewFlagSet(args[0], flag.ExitOnError) flagset.Usage = func() { i.Help(os.Stdout, flagset) } flagset.Parse(args[1:]) subArgs := flagset.Args() if len(subArgs) == 0 { i.Help(os.Stderr, flagset) os.Exit(1) } cmd := subArgs[0] if Session.Info.Opts.Debug { Log.Debug("info verb:", cmd) } return infoVerbs[cmd].Func(subArgs) } func (i InfoCmd) Help(w io.Writer, set *flag.FlagSet) { fmt.Fprintf(w, "USAGE: %s info VERB [verb opts]\n", Session.Info.Name) fmt.Fprintf(w, "\nVERBS:\n") for verb := range infoVerbs { fmt.Fprintf(w, "\t%s\n", verb) } } // VERBS type LsMailboxCmd struct{} type mailboxInfo struct { name string attrs string } func (l LsMailboxCmd) Func(args []string) error { if Session.Info.Opts.Debug { Log.Debug("enter info list-mailboxes") } var withAttributes bool var output string var maxLen int var mailboxes []mailboxInfo flagset := flag.NewFlagSet(args[0], flag.ExitOnError) flagset.BoolVar(&withAttributes, "with-attrs", false, "toggle attributes display") flagset.Usage = func() { l.Help(os.Stdout, flagset) } flagset.Parse(args[1:]) mboxInfo, err := imaputils.ListMailboxes(Session.Config, Session.Info.Opts.Debug) if err != nil { return err } if Session.Info.Opts.Debug { Log.Debug(mboxInfo) } for _, box := range mboxInfo { if withAttributes { if nameLen := len(box.Name); nameLen > maxLen { maxLen = nameLen } mailboxes = append(mailboxes, mailboxInfo{name: box.Name, attrs: fmt.Sprint(box.Attributes)}) } else { output += fmt.Sprintln(box.Name) } } if maxLen == 0 { fmt.Print(output) } else { padFmtStr := fmt.Sprintf("%%-%dv\t", maxLen) for _, m := range mailboxes { line := fmt.Sprintf(padFmtStr, m.name) line += m.attrs fmt.Println(line) } } return nil } func (l LsMailboxCmd) Help(w io.Writer, set *flag.FlagSet) { fmt.Fprintf(w, "USAGE: %s info list-mailboxes [opts]\n", Session.Info.Name) fmt.Fprintf(w, "\nOPTS:\n") set.PrintDefaults() } type LsMsgCmd struct{} func (l LsMsgCmd) Func(args []string) error { if Session.Info.Opts.Debug { Log.Debug("enter info list-messages") } var limit uint var withSeq, withFrom, withDate, withFlags bool var sep string flagset := flag.NewFlagSet(args[0], flag.ExitOnError) flagset.StringVar(&sep, "sep", "\t", "separator between fields") flagset.UintVar(&limit, "limit", 0, "maximum number of messages to display (0 means no limit)") flagset.BoolVar(&withSeq, "seq", false, "show sequence number") flagset.BoolVar(&withFrom, "from", false, "show From address") flagset.BoolVar(&withDate, "date", false, "show message date") flagset.BoolVar(&withFlags, "flags", false, "show message flags") flagset.Usage = func() { l.Help(os.Stdout, flagset) } flagset.Parse(args[1:]) subArgs := flagset.Args() if len(subArgs) != 1 { l.Help(os.Stderr, flagset) os.Exit(1) } msgs, err := imaputils.ListMessages( Session.Config, &imaputils.ListMessagesOpts{ Mailbox: subArgs[0], Limit: uint32(limit), }, Session.Info.Opts.Debug, ) if err != nil { return err } printMsgs(msgs, withSeq, withFrom, withDate, withFlags, sep) return nil } func (l LsMsgCmd) Help(w io.Writer, set *flag.FlagSet) { fmt.Fprintf(w, "USAGE: %s info list-messages [opts] MAILBOX_NAME\n", Session.Info.Name) fmt.Fprintf(w, "\nOPTS:\n") set.PrintDefaults() } func numberSize(n uint32) int { return int(math.Floor(math.Log10(float64(n)))) } func printMsgs(msgs []*imap.Message, withSeq, withFrom, withDate, withFlags bool, sep string) { var maxSeq, maxDateLen, maxFromLen, maxFlagsLen int var out, seqFmtStr, dateFmtStr, fromFmtStr, flagsFmtStr string for _, msg := range msgs { if seqSize := numberSize(msg.SeqNum); withSeq && seqSize > maxSeq { maxSeq = seqSize } if withDate { date := fmt.Sprint(msg.Envelope.Date) if dateLen := len(date); dateLen > maxDateLen { maxDateLen = dateLen } } if withFrom { for _, from := range msg.Envelope.From { if fromLen := len(from.Address()); fromLen > maxFromLen { maxFromLen = fromLen } } } if withFlags { var flags string for _, f := range msg.Flags { flags += fmt.Sprintf("%s ", f) } flags = strings.TrimRight(flags, " ") if flagsLen := len(flags); flagsLen > maxFlagsLen { maxFlagsLen = flagsLen } } } if withSeq { seqFmtStr = fmt.Sprintf("%%%dv%s", maxSeq, sep) } if withDate { dateFmtStr = fmt.Sprintf("%%-%dv%s", maxDateLen, sep) } if withFrom { fromFmtStr = fmt.Sprintf("%%-%dv%s", maxFromLen, sep) } if withFlags { flagsFmtStr = fmt.Sprintf("%%-%dv%s", maxFlagsLen, sep) } for _, msg := range msgs { var line string if withSeq { line += fmt.Sprintf(seqFmtStr, msg.SeqNum) } if withDate { line += fmt.Sprintf(dateFmtStr, msg.Envelope.Date) } if withFrom { line += fmt.Sprintf(fromFmtStr, msg.Envelope.From[0].Address()) } if withFlags { var flags string for _, f := range msg.Flags { flags += fmt.Sprintf("%s ", f) } line += fmt.Sprintf(flagsFmtStr, strings.TrimRight(flags, " ")) } line += msg.Envelope.Subject out += fmt.Sprintln(line) } fmt.Println(out) } type CatMsgCmd struct{} func (c CatMsgCmd) Func(args []string) error { if Session.Info.Opts.Debug { Log.Debug("enter info display-message") } var idList []uint32 var withHeaders, withBody, withFlags, markRead bool flagset := flag.NewFlagSet(args[0], flag.ExitOnError) flagset.BoolVar(&withHeaders, "headers", false, "toggle headers display") flagset.BoolVar(&withBody, "no-body", false, "hide body") flagset.BoolVar(&withFlags, "flags", false, "show flags") flagset.BoolVar(&markRead, "seen", false, "mark as seen if not yet seen") flagset.Usage = func() { c.Help(os.Stdout, flagset) } flagset.Parse(args[1:]) subArgs := flagset.Args() lenSubArgs := len(subArgs) if lenSubArgs < 2 { c.Help(os.Stderr, flagset) os.Exit(1) } mailbox := subArgs[0] if lenSubArgs > 1 { for _, id := range subArgs[1:] { mailId, err := parseToUint32(id) if err != nil { return err } idList = append(idList, mailId) } } opts := &imaputils.FetchOpts{ Mailbox: mailbox, IdList: idList, WithHeaders: withHeaders, WithBody: !withBody, WithFlags: withFlags, Peek: !markRead, } if Session.Info.Opts.Debug { Log.Debug(opts) } messages, err := imaputils.FetchMessages(Session.Config, opts, Session.Info.Opts.Debug) if err != nil { return err } for _, m := range messages { err = printMessage(m, opts, withFlags) if err != nil { return err } } return nil } func (c CatMsgCmd) Help(w io.Writer, set *flag.FlagSet) { fmt.Fprintf(w, "USAGE: %s info display-message [opts] MAILBOX_NAME [MESSAGE_ID1 [MESSAGE_ID2 [...]]]\n", Session.Info.Name) fmt.Fprintf(w, "\nOPTS:\n") set.PrintDefaults() } var ErrOutOfBounds = errors.New("number is out of bounds") func parseToUint32(s string) (uint32, error) { out, err := strconv.Atoi(s) if err != nil { return 0, err } if out < 0 || out > maxUInt32 { return 0, ErrOutOfBounds } return uint32(out), nil } func printMessage(m *imap.Message, opts *imaputils.FetchOpts, withFlags bool) error { var out string if withFlags { if Session.Info.Opts.Debug { Log.Debug(m.Flags) } for _, f := range m.Flags { out += fmt.Sprintf("%s ", f) } out = strings.TrimRight(out, " ") out += "\n" } if opts.WithHeaders { out += fmt.Sprintln(formatAddresses(m.Envelope.From, "From")) out += fmt.Sprintln(formatAddresses(m.Envelope.Sender, "Sender")) out += fmt.Sprintln(formatAddresses(m.Envelope.Cc, "Cc")) out += fmt.Sprintln(formatAddresses(m.Envelope.Bcc, "Bcc")) out += fmt.Sprintln(formatAddresses(m.Envelope.ReplyTo, "ReplyTo")) out += fmt.Sprintf("InReplyTo: %s\n", m.Envelope.InReplyTo) out += fmt.Sprintf("Date: %v\n", m.Envelope.Date) out += fmt.Sprintf("Subject: %s\n", m.Envelope.Subject) headersSec := &imap.BodySectionName{Peek: opts.Peek, BodyPartName: imap.BodyPartName{Specifier: imap.HeaderSpecifier}} headersReader := m.GetBody(headersSec) headers, err := ioutil.ReadAll(headersReader) if err != nil { return err } out += fmt.Sprintln(string(headers)) } if opts.WithBody { bodySec := &imap.BodySectionName{Peek: opts.Peek, BodyPartName: imap.BodyPartName{Specifier: imap.TextSpecifier}} bodyReader := m.GetBody(bodySec) body, err := ioutil.ReadAll(bodyReader) if err != nil { return err } out += fmt.Sprintln(string(body)) } fmt.Println(out) return nil } func formatAddresses(addresses []*imap.Address, name string) string { out := fmt.Sprintf("%s: ", name) for _, address := range addresses { if address.PersonalName != "" { out += fmt.Sprintf("%s <%s>, ", address.PersonalName, address.Address()) } else { out += fmt.Sprintf("%s, ", address.Address()) } } return strings.TrimRight(out, ", ") }