diff --git a/cli/papero/info.go b/cli/papero/info.go index 725a213..4958f79 100644 --- a/cli/papero/info.go +++ b/cli/papero/info.go @@ -1,15 +1,23 @@ package main import ( + "errors" "flag" "fmt" "io" + "io/ioutil" + "math" "os" + "strconv" + "strings" "git.lattuga.net/blallo/papero/cli" "git.lattuga.net/blallo/papero/imaputils" + "github.com/emersion/go-imap" ) +const maxUInt32 = int(^uint32(0)) + var infoVerbs cli.CommandMap func init() { @@ -54,11 +62,19 @@ func (i InfoCmd) Help(w io.Writer, set *flag.FlagSet) { 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) } @@ -73,17 +89,30 @@ func (l LsMailboxCmd) Func(args []string) error { } for _, box := range mboxInfo { if withAttributes { - attrs := fmt.Sprint(box.Attributes) - fmt.Printf("%s (attrs: %s)", box.Name, attrs) + if nameLen := len(box.Name); nameLen > maxLen { + maxLen = nameLen + } + mailboxes = append(mailboxes, mailboxInfo{name: box.Name, attrs: fmt.Sprint(box.Attributes)}) } else { - fmt.Println(box.Name) + 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\n", Session.Info.Name) + fmt.Fprintf(w, "USAGE: %s info list-mailboxes [opts]\n", Session.Info.Name) + fmt.Fprintf(w, "\nOPTS:\n") set.PrintDefaults() } @@ -94,8 +123,14 @@ func (l LsMsgCmd) Func(args []string) error { Log.Debug("enter info list-messages") } var limit uint + var withSeq, withFrom, withDate 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.Usage = func() { l.Help(os.Stdout, flagset) } flagset.Parse(args[1:]) @@ -104,6 +139,12 @@ func (l LsMsgCmd) Func(args []string) error { l.Help(os.Stderr, flagset) os.Exit(1) } + + msgs, err := imaputils.ListMessages(Session.Config, uint32(limit), Session.Info.Opts.Debug, subArgs[0]) + if err != nil { + return err + } + printMsgs(msgs, withSeq, withFrom, withDate, sep) return nil } @@ -113,25 +154,154 @@ func (l LsMsgCmd) Help(w io.Writer, set *flag.FlagSet) { set.PrintDefaults() } +func numberSize(n uint32) int { + return int(math.Floor(math.Log10(float64(n)))) +} + +func printMsgs(msgs []*imap.Message, withSeq, withFrom, withDate bool, sep string) { + var maxSeq, maxDateLen, maxFromLen int + var out, seqFmtStr, dateFmtStr, fromFmtStr 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 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) + } + 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()) + } + 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 flagset := flag.NewFlagSet(args[0], flag.ExitOnError) flagset.Usage = func() { c.Help(os.Stdout, flagset) } flagset.Parse(args[1:]) subArgs := flagset.Args() - if len(subArgs) != 1 { + 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) + } + } + + messages, err := imaputils.FetchMessages(Session.Config, mailbox, idList, Session.Info.Opts.Debug) + if err != nil { + return err + } + + for _, m := range messages { + err = printMessage(m) + 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 MAILBOX_NAME MESSAGE_ID\n", Session.Info.Name) + 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) error { + var out string + + 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) + + bodyReader := m.GetBody(&imap.BodySectionName{BodyPartName: imap.BodyPartName{}, Peek: true}) + 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, ", ") +} diff --git a/imaputils/info.go b/imaputils/info.go index 4189667..1071a84 100644 --- a/imaputils/info.go +++ b/imaputils/info.go @@ -31,3 +31,99 @@ func ListMailboxes(conf *config.AccountData, debug bool) ([]*imap.MailboxInfo, e return mailboxes, nil } + +func ListMessages(conf *config.AccountData, limit uint32, debug bool, mailbox string) ([]*imap.Message, error) { + var messages []*imap.Message + conn := NewConnection(conf) + + err := conn.Start(debug) + if err != nil { + return messages, err + } + defer conn.Close() + + mbox, err := conn.client.Select(mailbox, true) + if err != nil { + return messages, err + } + + return getMessages(mbox, conn, limit, 10) +} + +func getMessages(mbox *imap.MailboxStatus, conn *IMAPConnection, limit, size uint32) ([]*imap.Message, error) { + var start uint32 + var msgs []*imap.Message + seqset := new(imap.SeqSet) + messages := make(chan *imap.Message, size) + done := make(chan error, 1) + + switch { + case limit == 0: + start = 1 + case mbox.Messages > limit: + start = mbox.Messages - limit + 1 + default: + start = 1 + } + + seqset.AddRange(start, mbox.Messages) + go func() { + done <- conn.client.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope}, messages) + }() + + for msg := range messages { + msgs = append(msgs, msg) + } + + if err := <-done; err != nil { + return []*imap.Message{}, err + } + return msgs, nil +} + +func FetchMessages(conf *config.AccountData, mailbox string, idList []uint32, debug bool) ([]*imap.Message, error) { + var messages []*imap.Message + conn := NewConnection(conf) + + err := conn.Start(debug) + if err != nil { + return messages, err + } + defer conn.Close() + + mbox, err := conn.client.Select(mailbox, true) + if err != nil { + return messages, err + } + + for _, id := range idList { + m, err := fetchMessage(mbox, conn, id) + if err != nil { + return messages, err + } + messages = append(messages, m) + } + + return messages, nil +} + +func fetchMessage(mbox *imap.MailboxStatus, conn *IMAPConnection, id uint32) (*imap.Message, error) { + var message *imap.Message + seqset := new(imap.SeqSet) + messages := make(chan *imap.Message, 1) + done := make(chan error, 1) + section := &imap.BodySectionName{} + section.Peek = true + items := []imap.FetchItem{imap.FetchEnvelope, section.FetchItem()} + + seqset.AddNum(id) + go func() { + done <- conn.client.Fetch(seqset, items, messages) + }() + + msg := <-messages + if err := <-done; err != nil { + return message, err + } + return msg, nil +}