feat: forwarding emails (#144)(#145)

Drew Smirnoff created

* feat: forwarding emails

* fix: weird tags in the forward body

Change summary

fetcher/fetcher.go | 43 +++++++++++--------------------------------
main.go            | 31 +++++++++++++++++++++++++++++++
tui/email_view.go  |  6 +++++-
tui/messages.go    |  4 ++++
4 files changed, 51 insertions(+), 33 deletions(-)

Detailed changes

fetcher/fetcher.go 🔗

@@ -341,8 +341,8 @@ func FetchEmailBodyFromMailbox(account *config.Account, mailbox string, uid uint
 		return "", nil, fmt.Errorf("no message or body structure found with UID %d", uid)
 	}
 
-	var plainPartID string
-	var htmlPartID string
+	var plainPartID, plainPartEncoding string
+	var htmlPartID, htmlPartEncoding string
 	var attachments []Attachment
 	var checkPart func(part *imap.BodyStructure, partID string)
 	checkPart = func(part *imap.BodyStructure, partID string) {
@@ -353,10 +353,12 @@ func FetchEmailBodyFromMailbox(account *config.Account, mailbox string, uid uint
 			case "html":
 				if htmlPartID == "" {
 					htmlPartID = partID
+					htmlPartEncoding = part.Encoding
 				}
 			case "plain":
 				if plainPartID == "" {
 					plainPartID = partID
+					plainPartEncoding = part.Encoding
 				}
 			}
 		}
@@ -444,10 +446,13 @@ func FetchEmailBodyFromMailbox(account *config.Account, mailbox string, uid uint
 
 	var body string
 	textPartID := ""
+	textPartEncoding := ""
 	if htmlPartID != "" {
 		textPartID = htmlPartID
+		textPartEncoding = htmlPartEncoding
 	} else if plainPartID != "" {
 		textPartID = plainPartID
+		textPartEncoding = plainPartEncoding
 	}
 	if os.Getenv("DEBUG_KITTY_IMAGES") != "" {
 		msg := fmt.Sprintf("[kitty-img] body selection html=%s plain=%s chosen=%s\n", htmlPartID, plainPartID, textPartID)
@@ -481,38 +486,12 @@ func FetchEmailBodyFromMailbox(account *config.Account, mailbox string, uid uint
 		if partMsg != nil {
 			literal := partMsg.GetBody(section)
 			if literal != nil {
-				// The new decoding logic starts here
 				buf, _ := ioutil.ReadAll(literal)
-				mr, err := mail.CreateReader(bytes.NewReader(buf))
-				if err != nil {
-					body = string(buf)
+				// Use the encoding from BodyStructure to decode
+				if decoded, err := decodeAttachmentData(buf, textPartEncoding); err == nil {
+					body = string(decoded)
 				} else {
-					p, err := mr.NextPart()
-					if err != nil {
-						body = string(buf)
-					} else {
-						encoding := p.Header.Get("Content-Transfer-Encoding")
-						bodyBytes, _ := ioutil.ReadAll(p.Body)
-
-						switch strings.ToLower(encoding) {
-						case "base64":
-							decoded, err := base64.StdEncoding.DecodeString(string(bodyBytes))
-							if err == nil {
-								body = string(decoded)
-							} else {
-								body = string(bodyBytes)
-							}
-						case "quoted-printable":
-							decoded, err := ioutil.ReadAll(quotedprintable.NewReader(strings.NewReader(string(bodyBytes))))
-							if err == nil {
-								body = string(decoded)
-							} else {
-								body = string(bodyBytes)
-							}
-						default:
-							body = string(bodyBytes)
-						}
-					}
+					body = string(buf)
 				}
 			}
 		}

main.go 🔗

@@ -599,6 +599,37 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
 		return m, m.current.Init()
 
+	case tui.ForwardEmailMsg:
+		subject := msg.Email.Subject
+		if !strings.HasPrefix(strings.ToLower(subject), "fwd:") {
+			subject = "Fwd: " + subject
+		}
+
+		forwardHeader := fmt.Sprintf("\n\n---------- Forwarded message ----------\nFrom: %s\nDate: %s\nSubject: %s\nTo: %s\n\n",
+			msg.Email.From,
+			msg.Email.Date.Format("Mon, Jan 2, 2006 at 3:04 PM"),
+			msg.Email.Subject,
+			msg.Email.To,
+		)
+
+		body := forwardHeader + msg.Email.Body
+
+		var composer *tui.Composer
+		if m.config != nil && len(m.config.Accounts) > 0 {
+			// Use the account that received the email
+			accountID := msg.Email.AccountID
+			if accountID == "" {
+				accountID = m.config.GetFirstAccount().ID
+			}
+			composer = tui.NewComposerWithAccounts(m.config.Accounts, accountID, "", subject, body)
+		} else {
+			composer = tui.NewComposer("", "", subject, body)
+		}
+
+		m.current = composer
+		m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
+		return m, m.current.Init()
+
 	case tui.GoToFilePickerMsg:
 		m.previousModel = m.current
 		wd, _ := os.Getwd()

tui/email_view.go 🔗

@@ -122,6 +122,10 @@ func (m *EmailView) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				// Clear Kitty graphics before opening composer
 				clearKittyGraphics()
 				return m, func() tea.Msg { return ReplyToEmailMsg{Email: m.email} }
+			case "f":
+				// Clear Kitty graphics before opening composer
+				clearKittyGraphics()
+				return m, func() tea.Msg { return ForwardEmailMsg{Email: m.email} }
 			case "d":
 				accountID := m.accountID
 				uid := m.email.UID
@@ -184,7 +188,7 @@ func (m *EmailView) View() string {
 	if m.focusOnAttachments {
 		help = helpStyle.Render("↑/↓: navigate • enter: download • esc/tab: back to email body")
 	} else {
-		help = helpStyle.Render("\uf112 r: reply • \uea81 d: delete • \uea98 a: archive • \uf435 tab: focus attachments • \ueb06 esc: back to inbox")
+		help = helpStyle.Render("\uf112 r: reply • \uf064 f: forward • \uea81 d: delete • \uea98 a: archive • \uf435 tab: focus attachments • \ueb06 esc: back to inbox")
 	}
 
 	var attachmentView string

tui/messages.go 🔗

@@ -97,6 +97,10 @@ type ReplyToEmailMsg struct {
 	Email fetcher.Email
 }
 
+type ForwardEmailMsg struct {
+	Email fetcher.Email
+}
+
 type SetComposerCursorToStartMsg struct{}
 
 type GoToFilePickerMsg struct{}