1package view
2
3import (
4 "fmt"
5 "io"
6 "mime/quotedprintable"
7 "regexp"
8 "strings"
9
10 "github.com/PuerkitoBio/goquery"
11)
12
13// hyperlink formats a string as a terminal-clickable hyperlink.
14func hyperlink(url, text string) string {
15 if text == "" {
16 text = url
17 }
18 return fmt.Sprintf("\x1b]8;;%s\x07%s\x1b]8;;\x07", url, text)
19}
20
21func decodeQuotedPrintable(s string) (string, error) {
22 reader := quotedprintable.NewReader(strings.NewReader(s))
23 body, err := io.ReadAll(reader)
24 if err != nil {
25 return "", err
26 }
27 return string(body), nil
28}
29
30// ProcessBody takes a raw email body, decodes it, and formats it as plain
31// text with terminal hyperlinks.
32func ProcessBody(rawBody string) (string, error) {
33 decodedBody, err := decodeQuotedPrintable(rawBody)
34 if err != nil {
35 // If decoding fails, fallback to the raw body
36 decodedBody = rawBody
37 }
38
39 doc, err := goquery.NewDocumentFromReader(strings.NewReader(decodedBody))
40 if err != nil {
41 return "", fmt.Errorf("could not parse email body: %w", err)
42 }
43
44 // Remove style and script tags to clean up the view
45 doc.Find("style, script").Remove()
46
47 // Add newlines after block elements for better spacing
48 doc.Find("p, div, h1, h2, h3, h4, h5, h6").Each(func(i int, s *goquery.Selection) {
49 s.After("\n\n")
50 })
51
52 // Replace <br> tags with newlines
53 doc.Find("br").Each(func(i int, s *goquery.Selection) {
54 s.ReplaceWithHtml("\n")
55 })
56
57 // Format links into terminal hyperlinks
58 doc.Find("a").Each(func(i int, s *goquery.Selection) {
59 href, exists := s.Attr("href")
60 if !exists {
61 return
62 }
63 s.ReplaceWithHtml(hyperlink(href, s.Text()))
64 })
65
66 // Get the text content, which now includes our formatting
67 text := doc.Text()
68
69 // Collapse more than 2 consecutive newlines into 2
70 re := regexp.MustCompile(`\n{3,}`)
71 text = re.ReplaceAllString(text, "\n\n")
72
73 return text, nil
74}