Browse Source

Added info subcommands to examine mailbox(es)

Blallo 3 years ago
parent
commit
28e0b28a71
2 changed files with 272 additions and 6 deletions
  1. 176 6
      cli/papero/info.go
  2. 96 0
      imaputils/info.go

+ 176 - 6
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, ", ")
+}

+ 96 - 0
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
+}