1package view
2
3import (
4 "bytes"
5 "fmt"
6 "io"
7 "mime/quotedprintable"
8 "regexp"
9 "strings"
10
11 "github.com/PuerkitoBio/goquery"
12 "github.com/charmbracelet/lipgloss"
13 "github.com/yuin/goldmark"
14 "github.com/yuin/goldmark/renderer/html"
15)
16
17// hyperlink formats a string as a terminal-clickable hyperlink.
18func hyperlink(url, text string) string {
19 if text == "" {
20 text = url
21 }
22 return fmt.Sprintf("\x1b]8;;%s\x07%s\x1b]8;;\x07", url, text)
23}
24
25func decodeQuotedPrintable(s string) (string, error) {
26 reader := quotedprintable.NewReader(strings.NewReader(s))
27 body, err := io.ReadAll(reader)
28 if err != nil {
29 return "", err
30 }
31 return string(body), nil
32}
33
34// markdownToHTML converts a Markdown string to an HTML string.
35func markdownToHTML(md []byte) []byte {
36 var buf bytes.Buffer
37 p := goldmark.New(
38 goldmark.WithRendererOptions(
39 html.WithUnsafe(), // Allow raw HTML in email.
40 ),
41 )
42 if err := p.Convert(md, &buf); err != nil {
43 return md // Fallback to original markdown.
44 }
45 return buf.Bytes()
46}
47
48// ProcessBody takes a raw email body, decodes it, and formats it as plain
49// text with terminal hyperlinks.
50func ProcessBody(rawBody string, h1Style, h2Style, bodyStyle lipgloss.Style) (string, error) {
51 decodedBody, err := decodeQuotedPrintable(rawBody)
52 if err != nil {
53 decodedBody = rawBody
54 }
55
56 htmlBody := markdownToHTML([]byte(decodedBody))
57
58 doc, err := goquery.NewDocumentFromReader(bytes.NewReader(htmlBody))
59 if err != nil {
60 return "", fmt.Errorf("could not parse email body: %w", err)
61 }
62
63 doc.Find("style, script").Remove()
64
65 // Style headers
66 doc.Find("h1").Each(func(i int, s *goquery.Selection) {
67 s.SetText(h1Style.Render(s.Text()))
68 })
69
70 doc.Find("h2").Each(func(i int, s *goquery.Selection) {
71 s.SetText(h2Style.Render(s.Text()))
72 })
73
74 // Add newlines after block elements for better spacing
75 doc.Find("p, div").Each(func(i int, s *goquery.Selection) {
76 s.After("\n\n")
77 })
78
79 // Replace <br> tags with newlines
80 doc.Find("br").Each(func(i int, s *goquery.Selection) {
81 s.ReplaceWithHtml("\n")
82 })
83
84 // Format links and images
85 doc.Find("a").Each(func(i int, s *goquery.Selection) {
86 href, exists := s.Attr("href")
87 if !exists {
88 return
89 }
90 s.ReplaceWithHtml(hyperlink(href, s.Text()))
91 })
92
93 doc.Find("img").Each(func(i int, s *goquery.Selection) {
94 src, exists := s.Attr("src")
95 if !exists {
96 return
97 }
98 alt, _ := s.Attr("alt")
99 if alt == "" {
100 alt = "Does not contain alt text"
101 }
102 s.ReplaceWithHtml(hyperlink(src, fmt.Sprintf("\n [Click here to view image: %s] \n", alt)))
103 })
104
105 text := doc.Text()
106
107 re := regexp.MustCompile(`\n{3,}`)
108 text = re.ReplaceAllString(text, "\n\n")
109 return bodyStyle.Render(text), nil
110}