From 19e275025538a92bae92b152c4e39bdd22ad7e7f Mon Sep 17 00:00:00 2001
From: Koni Marti <koni.marti@gmail.com>
Date: Thu, 20 Oct 2022 23:55:28 +0200
Subject: [PATCH] envelope: display message envelope info

Display entire message envelope in a user-friendly dialog popup with the
:envelope command. All header fields can be displayed with the -h flag.

Fixes: https://todo.sr.ht/~rjarry/aerc/85
Signed-off-by: Koni Marti <koni.marti@gmail.com>
Acked-by: Robin Jarry <robin@jarry.cc>
---
 CHANGELOG.md             |   4 ++
 commands/msg/envelope.go | 152 +++++++++++++++++++++++++++++++++++++++
 doc/aerc.1.scd           |   9 +++
 3 files changed, 165 insertions(+)
 create mode 100644 commands/msg/envelope.go

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9eb86e6..30ed0d8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
 ## [Unreleased](https://git.sr.ht/~rjarry/aerc/log/master)
 
+### Added
+
+- View common email envelope headers with `:envelope`.
+
 ### Fixed
 
 - `:pipe -m git am -3` on patch series when `Message-Id` headers have not been
diff --git a/commands/msg/envelope.go b/commands/msg/envelope.go
new file mode 100644
index 0000000..616798d
--- /dev/null
+++ b/commands/msg/envelope.go
@@ -0,0 +1,152 @@
+package msg
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+
+	"git.sr.ht/~rjarry/aerc/lib/format"
+	"git.sr.ht/~rjarry/aerc/logging"
+	"git.sr.ht/~rjarry/aerc/models"
+	"git.sr.ht/~rjarry/aerc/widgets"
+	"git.sr.ht/~sircmpwn/getopt"
+	"github.com/emersion/go-message/mail"
+)
+
+type Envelope struct{}
+
+func init() {
+	register(Envelope{})
+}
+
+func (Envelope) Aliases() []string {
+	return []string{"envelope"}
+}
+
+func (Envelope) Complete(aerc *widgets.Aerc, args []string) []string {
+	return nil
+}
+
+func (Envelope) Execute(aerc *widgets.Aerc, args []string) error {
+	header := false
+	fmtStr := "%-20.20s: %s"
+	opts, _, err := getopt.Getopts(args, "hs:")
+	if err != nil {
+		return err
+	}
+	for _, opt := range opts {
+		switch opt.Option {
+		case 's':
+			fmtStr = opt.Value
+		case 'h':
+			header = true
+		}
+	}
+
+	acct := aerc.SelectedAccount()
+	if acct == nil {
+		return errors.New("No account selected")
+	}
+
+	var list []string
+	if msg, err := acct.SelectedMessage(); err != nil {
+		return err
+	} else {
+		if msg != nil {
+			if header {
+				list = parseHeader(msg, fmtStr)
+			} else {
+				list = parseEnvelope(msg, fmtStr,
+					acct.UiConfig().TimestampFormat)
+			}
+		} else {
+			return fmt.Errorf("Selected message is empty.")
+		}
+	}
+
+	n := len(list)
+	aerc.AddDialog(widgets.NewDialog(
+		widgets.NewListBox(
+			"Message Envelope. Press <Esc> or <Enter> to close. "+
+				"Start typing to filter.",
+			list,
+			aerc.SelectedAccountUiConfig(),
+			func(_ string) {
+				aerc.CloseDialog()
+			},
+		),
+		// start pos on screen
+		func(h int) int {
+			if n < h/8*6 {
+				return h/2 - n/2 - 1
+			}
+			return h / 8
+		},
+		// dialog height
+		func(h int) int {
+			if n < h/8*6 {
+				return n + 2
+			}
+			return h / 8 * 6
+		},
+	))
+
+	return nil
+}
+
+func parseEnvelope(msg *models.MessageInfo, fmtStr, fmtTime string,
+) (result []string) {
+	if envlp := msg.Envelope; envlp != nil {
+		addStr := func(key, text string) {
+			result = append(result, fmt.Sprintf(fmtStr, key, text))
+		}
+		addAddr := func(key string, ls []*mail.Address) {
+			for _, l := range ls {
+				result = append(result,
+					fmt.Sprintf(fmtStr, key,
+						format.AddressForHumans(l)))
+			}
+		}
+
+		addStr("Date", envlp.Date.Format(fmtTime))
+		addStr("Subject", envlp.Subject)
+		addStr("Message-Id", envlp.MessageId)
+
+		addAddr("From", envlp.From)
+		addAddr("To", envlp.To)
+		addAddr("ReplyTo", envlp.ReplyTo)
+		addAddr("Cc", envlp.Cc)
+		addAddr("Bcc", envlp.Bcc)
+	}
+	return
+}
+
+func parseHeader(msg *models.MessageInfo, fmtStr string) (result []string) {
+	if h := msg.RFC822Headers; h != nil {
+		hf := h.Fields()
+		for hf.Next() {
+			text, err := hf.Text()
+			if err != nil {
+				logging.Errorf(err.Error())
+				text = hf.Value()
+			}
+			result = append(result,
+				headerExpand(fmtStr, hf.Key(), text)...)
+		}
+	}
+	return
+}
+
+func headerExpand(fmtStr, key, text string) []string {
+	var result []string
+	switch strings.ToLower(key) {
+	case "to", "from", "bcc", "cc":
+		for _, item := range strings.Split(text, ",") {
+			result = append(result, fmt.Sprintf(fmtStr, key,
+				strings.TrimSpace(item)))
+		}
+	default:
+		result = append(result, fmt.Sprintf(fmtStr, key, text))
+	}
+	return result
+}
diff --git a/doc/aerc.1.scd b/doc/aerc.1.scd
index ea4be17..f14d467 100644
--- a/doc/aerc.1.scd
+++ b/doc/aerc.1.scd
@@ -162,6 +162,15 @@ message list, the message in the message viewer, etc).
 *delete-message*
 	Deletes the selected message.
 
+*envelope [-h] [-s <format-specifier>]*
+	Opens the message envelope in a dialog popup.
+
+	*-h*: Show all header fields
+
+	*-s* <format-specifier>
+		User-defined format specifier requiring two %s for the key and
+		value strings. Default format: '%-20.20s: %s'
+
 *recall* [-f]
 	Opens the selected message for re-editing. Messages can only be
 	recalled from the postpone directory. The original message is deleted.