@@ -5,7 +5,6 @@ import (
"fmt"
"io"
"io/ioutil"
- "log"
"mime"
"strings"
"time"
@@ -141,7 +140,8 @@ func FetchEmails(cfg *config.Config, limit, offset uint32) ([]Email, error) {
messages := make(chan *imap.Message, limit)
done := make(chan error, 1)
- fetchItems := []imap.FetchItem{imap.FetchEnvelope, imap.FetchUid, imap.FetchItem("BODY[]")}
+ // Only fetch Envelope and UID for the inbox view
+ fetchItems := []imap.FetchItem{imap.FetchEnvelope, imap.FetchUid}
go func() {
done <- c.Fetch(seqset, fetchItems, messages)
}()
@@ -152,102 +152,118 @@ func FetchEmails(cfg *config.Config, limit, offset uint32) ([]Email, error) {
continue
}
- bodyLiteral := msg.GetBody(&imap.BodySectionName{})
- if bodyLiteral == nil {
- log.Println("Could not get message body")
- continue
+ fromAddrs := msg.Envelope.From[0].Address()
+ var toAddrList []string
+ for _, addr := range msg.Envelope.To {
+ toAddrList = append(toAddrList, addr.Address())
}
- mr, err := mail.CreateReader(bodyLiteral)
- if err != nil {
- log.Printf("Error creating mail reader: %v", err)
- continue
- }
+ emails = append(emails, Email{
+ UID: msg.Uid,
+ From: fromAddrs,
+ To: toAddrList,
+ Subject: decodeHeader(msg.Envelope.Subject),
+ Date: msg.Envelope.Date,
+ })
+ }
- header := mr.Header
- fromAddrs, _ := header.AddressList("From")
- toAddrs, _ := header.AddressList("To")
- subject := decodeHeader(header.Get("Subject"))
- date, _ := header.Date()
- messageID := header.Get("Message-ID")
- references := header.Get("References")
-
- var fromAddr string
- if len(fromAddrs) > 0 {
- fromAddr = fromAddrs[0].Address
- }
+ if err := <-done; err != nil {
+ return nil, err
+ }
- var toAddrList []string
- for _, addr := range toAddrs {
- toAddrList = append(toAddrList, addr.Address)
- }
+ // Reverse the order to show newest first
+ for i, j := 0, len(emails)-1; i < j; i, j = i+1, j-1 {
+ emails[i], emails[j] = emails[j], emails[i]
+ }
- var body string
- var attachments []Attachment
- for {
- p, err := mr.NextPart()
- if err == io.EOF {
- break
- } else if err != nil {
- log.Printf("Error getting next part: %v", err)
- break
- }
+ return emails, nil
+}
+
+
+// New function to fetch the body for a single email
+func FetchEmailBody(cfg *config.Config, uid uint32) (string, []Attachment, error) {
+ c, err := connect(cfg)
+ if err != nil {
+ return "", nil, err
+ }
+ defer c.Logout()
+
+ if _, err := c.Select("INBOX", false); err != nil {
+ return "", nil, err
+ }
- // Correctly parse Content-Disposition
- cdHeader := p.Header.Get("Content-Disposition")
- if cdHeader != "" {
- disposition, params, err := mime.ParseMediaType(cdHeader)
- if err == nil && (disposition == "attachment" || disposition == "inline") {
- filename := params["filename"]
- if filename != "" {
- partBody, _ := ioutil.ReadAll(p.Body)
- encoding := p.Header.Get("Content-Transfer-Encoding")
- if strings.ToLower(encoding) == "base64" {
- decoded, decodeErr := base64.StdEncoding.DecodeString(string(partBody))
- if decodeErr == nil {
- partBody = decoded
- }
+ seqset := new(imap.SeqSet)
+ seqset.AddNum(uid)
+
+ messages := make(chan *imap.Message, 1)
+ done := make(chan error, 1)
+ fetchItems := []imap.FetchItem{imap.FetchItem("BODY[]")}
+ go func() {
+ done <- c.Fetch(seqset, fetchItems, messages)
+ }()
+
+ msg := <-messages
+ if msg == nil {
+ return "", nil, fmt.Errorf("could not fetch email body")
+ }
+
+ bodyLiteral := msg.GetBody(&imap.BodySectionName{})
+ if bodyLiteral == nil {
+ return "", nil, fmt.Errorf("could not get message body")
+ }
+
+ mr, err := mail.CreateReader(bodyLiteral)
+ if err != nil {
+ return "", nil, fmt.Errorf("error creating mail reader: %v", err)
+ }
+
+ var body string
+ var attachments []Attachment
+ for {
+ p, err := mr.NextPart()
+ if err == io.EOF {
+ break
+ } else if err != nil {
+ return "", nil, fmt.Errorf("error getting next part: %v", err)
+ }
+
+ cdHeader := p.Header.Get("Content-Disposition")
+ if cdHeader != "" {
+ disposition, params, err := mime.ParseMediaType(cdHeader)
+ if err == nil && (disposition == "attachment" || disposition == "inline") {
+ filename := params["filename"]
+ if filename != "" {
+ partBody, _ := ioutil.ReadAll(p.Body)
+ encoding := p.Header.Get("Content-Transfer-Encoding")
+ if strings.ToLower(encoding) == "base64" {
+ decoded, decodeErr := base64.StdEncoding.DecodeString(string(partBody))
+ if decodeErr == nil {
+ partBody = decoded
}
- attachments = append(attachments, Attachment{Filename: filename, Data: partBody})
- continue // Skip to next part
}
+ attachments = append(attachments, Attachment{Filename: filename, Data: partBody})
+ continue
}
}
+ }
- // Process body part if not an attachment
- mediaType, _, _ := mime.ParseMediaType(p.Header.Get("Content-Type"))
- if (mediaType == "text/plain" || mediaType == "text/html") && body == "" {
- decodedPart, decodeErr := decodePart(p.Body, p.Header)
- if decodeErr == nil {
- body = decodedPart
- }
+ mediaType, _, _ := mime.ParseMediaType(p.Header.Get("Content-Type"))
+ if (mediaType == "text/plain" || mediaType == "text/html") && body == "" {
+ decodedPart, decodeErr := decodePart(p.Body, p.Header)
+ if decodeErr == nil {
+ body = decodedPart
}
}
-
- emails = append(emails, Email{
- UID: msg.Uid,
- From: fromAddr,
- To: toAddrList,
- Subject: subject,
- Body: body,
- Date: date,
- MessageID: messageID,
- References: strings.Fields(references),
- Attachments: attachments,
- })
}
if err := <-done; err != nil {
- return nil, err
+ return "", nil, err
}
- for i, j := 0, len(emails)-1; i < j; i, j = i+1, j-1 {
- emails[i], emails[j] = emails[j], emails[i]
- }
-
- return emails, nil
+ return body, attachments, nil
}
+
func moveEmail(cfg *config.Config, uid uint32, destMailbox string) error {
c, err := connect(cfg)
if err != nil {
@@ -24,7 +24,8 @@ type EmailView struct {
}
func NewEmailView(email fetcher.Email, width, height int) *EmailView {
- body, err := view.ProcessBody(email.Body)
+ // Pass the styles from the tui package to the view package
+ body, err := view.ProcessBody(email.Body, H1Style, H2Style, BodyStyle)
if err != nil {
body = fmt.Sprintf("Error rendering body: %v", err)
}
@@ -32,10 +33,9 @@ func NewEmailView(email fetcher.Email, width, height int) *EmailView {
header := fmt.Sprintf("From: %s\nSubject: %s", email.From, email.Subject)
headerHeight := lipgloss.Height(header) + 2
- // Calculate height for attachments if they exist
attachmentHeight := 0
if len(email.Attachments) > 0 {
- attachmentHeight = len(email.Attachments) + 2 // +2 for title and border
+ attachmentHeight = len(email.Attachments) + 2
}
vp := viewport.New(width, height-headerHeight-attachmentHeight)
@@ -9,6 +9,7 @@ import (
"strings"
"github.com/PuerkitoBio/goquery"
+ "github.com/charmbracelet/lipgloss"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/renderer/html"
)
@@ -46,14 +47,12 @@ func markdownToHTML(md []byte) []byte {
// ProcessBody takes a raw email body, decodes it, and formats it as plain
// text with terminal hyperlinks.
-func ProcessBody(rawBody string) (string, error) {
+func ProcessBody(rawBody string, h1Style, h2Style, bodyStyle lipgloss.Style) (string, error) {
decodedBody, err := decodeQuotedPrintable(rawBody)
if err != nil {
- // If decoding fails, fallback to the raw body
decodedBody = rawBody
}
- // Convert markdown to HTML before processing
htmlBody := markdownToHTML([]byte(decodedBody))
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(htmlBody))
@@ -61,11 +60,19 @@ func ProcessBody(rawBody string) (string, error) {
return "", fmt.Errorf("could not parse email body: %w", err)
}
- // Remove style and script tags to clean up the view
doc.Find("style, script").Remove()
+ // Style headers
+ doc.Find("h1").Each(func(i int, s *goquery.Selection) {
+ s.SetText(h1Style.Render(s.Text()))
+ })
+
+ doc.Find("h2").Each(func(i int, s *goquery.Selection) {
+ s.SetText(h2Style.Render(s.Text()))
+ })
+
// Add newlines after block elements for better spacing
- doc.Find("p, div, h1, h2, h3, h4, h5, h6").Each(func(i int, s *goquery.Selection) {
+ doc.Find("p, div").Each(func(i int, s *goquery.Selection) {
s.After("\n\n")
})
@@ -74,7 +81,7 @@ func ProcessBody(rawBody string) (string, error) {
s.ReplaceWithHtml("\n")
})
- // Format links into terminal hyperlinks
+ // Format links and images
doc.Find("a").Each(func(i int, s *goquery.Selection) {
href, exists := s.Attr("href")
if !exists {
@@ -83,26 +90,21 @@ func ProcessBody(rawBody string) (string, error) {
s.ReplaceWithHtml(hyperlink(href, s.Text()))
})
- // Format images into terminal hyperlinks
doc.Find("img").Each(func(i int, s *goquery.Selection) {
src, exists := s.Attr("src")
if !exists {
return
}
alt, _ := s.Attr("alt")
-
if alt == "" {
alt = "Does not contain alt text"
}
s.ReplaceWithHtml(hyperlink(src, fmt.Sprintf("\n [Click here to view image: %s] \n", alt)))
})
- // Get the document's text content, which now includes our formatting
text := doc.Text()
- // Collapse more than 2 consecutive newlines into 2
re := regexp.MustCompile(`\n{3,}`)
text = re.ReplaceAllString(text, "\n\n")
-
- return text, nil
-}
+ return bodyStyle.Render(text), nil
+}