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/yuin/goldmark"
13 "github.com/yuin/goldmark/renderer/html"
14)
15
16// hyperlink formats a string as a terminal-clickable hyperlink.
17func hyperlink(url, text string) string {
18 if text == "" {
19 text = url
20 }
21 return fmt.Sprintf("\x1b]8;;%s\x07%s\x1b]8;;\x07", url, text)
22}
23
24func decodeQuotedPrintable(s string) (string, error) {
25 reader := quotedprintable.NewReader(strings.NewReader(s))
26 body, err := io.ReadAll(reader)
27 if err != nil {
28 return "", err
29 }
30 return string(body), nil
31}
32
33// markdownToHTML converts a Markdown string to an HTML string.
34func markdownToHTML(md []byte) []byte {
35 var buf bytes.Buffer
36 p := goldmark.New(
37 goldmark.WithRendererOptions(
38 html.WithUnsafe(), // Allow raw HTML in email.
39 ),
40 )
41 if err := p.Convert(md, &buf); err != nil {
42 return md // Fallback to original markdown.
43 }
44 return buf.Bytes()
45}
46
47// ProcessBody takes a raw email body, decodes it, and formats it as plain
48// text with terminal hyperlinks.
49func ProcessBody(rawBody string) (string, error) {
50 decodedBody, err := decodeQuotedPrintable(rawBody)
51 if err != nil {
52 // If decoding fails, fallback to the raw body
53 decodedBody = rawBody
54 }
55
56 // Convert markdown to HTML before processing
57 htmlBody := markdownToHTML([]byte(decodedBody))
58
59 doc, err := goquery.NewDocumentFromReader(bytes.NewReader(htmlBody))
60 if err != nil {
61 return "", fmt.Errorf("could not parse email body: %w", err)
62 }
63
64 // Remove style and script tags to clean up the view
65 doc.Find("style, script").Remove()
66
67 // Add newlines after block elements for better spacing
68 doc.Find("p, div, h1, h2, h3, h4, h5, h6").Each(func(i int, s *goquery.Selection) {
69 s.After("\n\n")
70 })
71
72 // Replace <br> tags with newlines
73 doc.Find("br").Each(func(i int, s *goquery.Selection) {
74 s.ReplaceWithHtml("\n")
75 })
76
77 // Format links into terminal hyperlinks
78 doc.Find("a").Each(func(i int, s *goquery.Selection) {
79 href, exists := s.Attr("href")
80 if !exists {
81 return
82 }
83 s.ReplaceWithHtml(hyperlink(href, s.Text()))
84 })
85
86 // Get the document's text content, which now includes our formatting
87 text := doc.Text()
88
89 // Collapse more than 2 consecutive newlines into 2
90 re := regexp.MustCompile(`\n{3,}`)
91 text = re.ReplaceAllString(text, "\n\n")
92
93 return text, nil
94}