Skip to content

Commit f16fa6c

Browse files
authored
#46 from andrinoff/next
2 parents a46ef55 + 5b3eb17 commit f16fa6c

6 files changed

Lines changed: 253 additions & 90 deletions

File tree

fetcher/fetcher.go

Lines changed: 174 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
package fetcher
22

33
import (
4+
"bytes"
45
"encoding/base64"
56
"fmt"
67
"io"
78
"io/ioutil"
8-
"log"
99
"mime"
10+
"mime/quotedprintable"
1011
"strings"
1112
"time"
1213

@@ -21,6 +22,7 @@ import (
2122
// Attachment holds data for an email attachment.
2223
type Attachment struct {
2324
Filename string
25+
PartID string // Keep PartID to fetch on demand
2426
Data []byte
2527
}
2628

@@ -141,111 +143,211 @@ func FetchEmails(cfg *config.Config, limit, offset uint32) ([]Email, error) {
141143

142144
messages := make(chan *imap.Message, limit)
143145
done := make(chan error, 1)
144-
fetchItems := []imap.FetchItem{imap.FetchEnvelope, imap.FetchUid, imap.FetchItem("BODY[]")}
146+
fetchItems := []imap.FetchItem{imap.FetchEnvelope, imap.FetchUid}
145147
go func() {
146148
done <- c.Fetch(seqset, fetchItems, messages)
147149
}()
148150

149-
var emails []Email
151+
var msgs []*imap.Message
150152
for msg := range messages {
151-
if msg == nil {
152-
continue
153-
}
153+
msgs = append(msgs, msg)
154+
}
154155

155-
bodyLiteral := msg.GetBody(&imap.BodySectionName{})
156-
if bodyLiteral == nil {
157-
log.Println("Could not get message body")
158-
continue
159-
}
156+
if err := <-done; err != nil {
157+
return nil, err
158+
}
160159

161-
mr, err := mail.CreateReader(bodyLiteral)
162-
if err != nil {
163-
log.Printf("Error creating mail reader: %v", err)
160+
var emails []Email
161+
for _, msg := range msgs {
162+
if msg == nil || msg.Envelope == nil {
164163
continue
165164
}
166165

167-
header := mr.Header
168-
fromAddrs, _ := header.AddressList("From")
169-
toAddrs, _ := header.AddressList("To")
170-
subject := decodeHeader(header.Get("Subject"))
171-
date, _ := header.Date()
172-
messageID := header.Get("Message-ID")
173-
references := header.Get("References")
174-
175166
var fromAddr string
176-
if len(fromAddrs) > 0 {
177-
fromAddr = fromAddrs[0].Address
167+
if len(msg.Envelope.From) > 0 {
168+
fromAddr = msg.Envelope.From[0].Address()
178169
}
179170

180171
var toAddrList []string
181-
for _, addr := range toAddrs {
182-
toAddrList = append(toAddrList, addr.Address)
172+
for _, addr := range msg.Envelope.To {
173+
toAddrList = append(toAddrList, addr.Address())
183174
}
184175

185-
var body string
186-
var attachments []Attachment
187-
for {
188-
p, err := mr.NextPart()
189-
if err == io.EOF {
190-
break
191-
} else if err != nil {
192-
log.Printf("Error getting next part: %v", err)
193-
break
176+
emails = append(emails, Email{
177+
UID: msg.Uid,
178+
From: fromAddr,
179+
To: toAddrList,
180+
Subject: decodeHeader(msg.Envelope.Subject),
181+
Date: msg.Envelope.Date,
182+
})
183+
}
184+
185+
for i, j := 0, len(emails)-1; i < j; i, j = i+1, j-1 {
186+
emails[i], emails[j] = emails[j], emails[i]
187+
}
188+
189+
return emails, nil
190+
}
191+
192+
func FetchEmailBody(cfg *config.Config, uid uint32) (string, []Attachment, error) {
193+
c, err := connect(cfg)
194+
if err != nil {
195+
return "", nil, err
196+
}
197+
defer c.Logout()
198+
199+
if _, err := c.Select("INBOX", false); err != nil {
200+
return "", nil, err
201+
}
202+
203+
seqset := new(imap.SeqSet)
204+
seqset.AddNum(uid)
205+
206+
messages := make(chan *imap.Message, 1)
207+
done := make(chan error, 1)
208+
fetchItems := []imap.FetchItem{imap.FetchBodyStructure}
209+
go func() {
210+
done <- c.UidFetch(seqset, fetchItems, messages)
211+
}()
212+
213+
if err := <-done; err != nil {
214+
return "", nil, err
215+
}
216+
217+
msg := <-messages
218+
if msg == nil || msg.BodyStructure == nil {
219+
return "", nil, fmt.Errorf("no message or body structure found with UID %d", uid)
220+
}
221+
222+
var textPartID string
223+
var attachments []Attachment
224+
var findParts func(*imap.BodyStructure, string)
225+
findParts = func(bs *imap.BodyStructure, prefix string) {
226+
for i, part := range bs.Parts {
227+
partID := fmt.Sprintf("%d", i+1)
228+
if prefix != "" {
229+
partID = fmt.Sprintf("%s.%d", prefix, i+1)
230+
}
231+
232+
if part.MIMEType == "text" && (part.MIMESubType == "plain" || part.MIMESubType == "html") && textPartID == "" {
233+
textPartID = partID
194234
}
235+
if part.Disposition == "attachment" || part.Disposition == "inline" {
236+
if filename, ok := part.Params["filename"]; ok {
237+
attachments = append(attachments, Attachment{Filename: filename, PartID: partID})
238+
}
239+
}
240+
if len(part.Parts) > 0 {
241+
findParts(part, partID)
242+
}
243+
}
244+
}
245+
findParts(msg.BodyStructure, "")
246+
247+
var body string
248+
if textPartID != "" {
249+
partMessages := make(chan *imap.Message, 1)
250+
partDone := make(chan error, 1)
251+
252+
fetchItem := imap.FetchItem(fmt.Sprintf("BODY.PEEK[%s]", textPartID))
253+
section, err := imap.ParseBodySectionName(fetchItem)
254+
if err != nil {
255+
return "", nil, err
256+
}
257+
258+
go func() {
259+
partDone <- c.UidFetch(seqset, []imap.FetchItem{fetchItem}, partMessages)
260+
}()
261+
262+
if err := <-partDone; err != nil {
263+
return "", nil, err
264+
}
195265

196-
// Correctly parse Content-Disposition
197-
cdHeader := p.Header.Get("Content-Disposition")
198-
if cdHeader != "" {
199-
disposition, params, err := mime.ParseMediaType(cdHeader)
200-
if err == nil && (disposition == "attachment" || disposition == "inline") {
201-
filename := params["filename"]
202-
if filename != "" {
203-
partBody, _ := ioutil.ReadAll(p.Body)
266+
partMsg := <-partMessages
267+
if partMsg != nil {
268+
literal := partMsg.GetBody(section)
269+
if literal != nil {
270+
// The new decoding logic starts here
271+
buf, _ := ioutil.ReadAll(literal)
272+
mr, err := mail.CreateReader(bytes.NewReader(buf))
273+
if err != nil {
274+
body = string(buf)
275+
} else {
276+
p, err := mr.NextPart()
277+
if err != nil {
278+
body = string(buf)
279+
} else {
204280
encoding := p.Header.Get("Content-Transfer-Encoding")
205-
if strings.ToLower(encoding) == "base64" {
206-
decoded, decodeErr := base64.StdEncoding.DecodeString(string(partBody))
207-
if decodeErr == nil {
208-
partBody = decoded
281+
bodyBytes, _ := ioutil.ReadAll(p.Body)
282+
283+
switch strings.ToLower(encoding) {
284+
case "base64":
285+
decoded, err := base64.StdEncoding.DecodeString(string(bodyBytes))
286+
if err == nil {
287+
body = string(decoded)
288+
} else {
289+
body = string(bodyBytes)
290+
}
291+
case "quoted-printable":
292+
decoded, err := ioutil.ReadAll(quotedprintable.NewReader(strings.NewReader(string(bodyBytes))))
293+
if err == nil {
294+
body = string(decoded)
295+
} else {
296+
body = string(bodyBytes)
209297
}
298+
default:
299+
body = string(bodyBytes)
210300
}
211-
attachments = append(attachments, Attachment{Filename: filename, Data: partBody})
212-
continue // Skip to next part
213301
}
214302
}
215303
}
216-
217-
// Process body part if not an attachment
218-
mediaType, _, _ := mime.ParseMediaType(p.Header.Get("Content-Type"))
219-
if (mediaType == "text/plain" || mediaType == "text/html") && body == "" {
220-
decodedPart, decodeErr := decodePart(p.Body, p.Header)
221-
if decodeErr == nil {
222-
body = decodedPart
223-
}
224-
}
225304
}
305+
}
226306

227-
emails = append(emails, Email{
228-
UID: msg.Uid,
229-
From: fromAddr,
230-
To: toAddrList,
231-
Subject: subject,
232-
Body: body,
233-
Date: date,
234-
MessageID: messageID,
235-
References: strings.Fields(references),
236-
Attachments: attachments,
237-
})
307+
return body, attachments, nil
308+
}
309+
310+
func FetchAttachment(cfg *config.Config, uid uint32, partID string) ([]byte, error) {
311+
c, err := connect(cfg)
312+
if err != nil {
313+
return nil, err
314+
}
315+
defer c.Logout()
316+
317+
if _, err := c.Select("INBOX", false); err != nil {
318+
return nil, err
319+
}
320+
321+
seqset := new(imap.SeqSet)
322+
seqset.AddNum(uid)
323+
324+
fetchItem := imap.FetchItem(fmt.Sprintf("BODY.PEEK[%s]", partID))
325+
section, err := imap.ParseBodySectionName(fetchItem)
326+
if err != nil {
327+
return nil, err
238328
}
239329

330+
messages := make(chan *imap.Message, 1)
331+
done := make(chan error, 1)
332+
go func() {
333+
done <- c.UidFetch(seqset, []imap.FetchItem{fetchItem}, messages)
334+
}()
335+
240336
if err := <-done; err != nil {
241337
return nil, err
242338
}
243339

244-
for i, j := 0, len(emails)-1; i < j; i, j = i+1, j-1 {
245-
emails[i], emails[j] = emails[j], emails[i]
340+
msg := <-messages
341+
if msg == nil {
342+
return nil, fmt.Errorf("could not fetch attachment")
246343
}
247344

248-
return emails, nil
345+
literal := msg.GetBody(section)
346+
if literal == nil {
347+
return nil, fmt.Errorf("could not get attachment body")
348+
}
349+
350+
return ioutil.ReadAll(literal)
249351
}
250352

251353
func moveEmail(cfg *config.Config, uid uint32, destMailbox string) error {

main.go

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,21 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
152152
return m, m.current.Init()
153153

154154
case tui.ViewEmailMsg:
155+
// Show a status message while fetching the email body
156+
m.current = tui.NewStatus("Fetching email content...")
157+
// Pass the index directly to the command
158+
return m, tea.Batch(m.current.Init(), fetchEmailBodyCmd(m.config, m.emails[msg.Index], msg.Index))
159+
160+
case tui.EmailBodyFetchedMsg:
161+
if msg.Err != nil {
162+
log.Printf("could not fetch email body: %v", msg.Err)
163+
m.current = m.inbox
164+
return m, nil
165+
}
166+
// Use the index from the message to update the correct email
167+
m.emails[msg.Index].Body = msg.Body
168+
m.emails[msg.Index].Attachments = msg.Attachments
169+
155170
emailView := tui.NewEmailView(m.emails[msg.Index], m.width, m.height)
156171
m.current = emailView
157172
return m, m.current.Init()
@@ -219,7 +234,8 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
219234
case tui.DownloadAttachmentMsg:
220235
m.previousModel = m.current
221236
m.current = tui.NewStatus(fmt.Sprintf("Downloading %s...", msg.Filename))
222-
return m, tea.Batch(m.current.Init(), downloadAttachmentCmd(msg))
237+
// Use the new FetchAttachment function
238+
return m, tea.Batch(m.current.Init(), downloadAttachmentCmd(m.config, m.emails[msg.Index].UID, msg))
223239

224240
case tui.AttachmentDownloadedMsg:
225241
var statusMsg string
@@ -248,6 +264,22 @@ func (m *mainModel) View() string {
248264
return m.current.View()
249265
}
250266

267+
func fetchEmailBodyCmd(cfg *config.Config, email fetcher.Email, index int) tea.Cmd {
268+
return func() tea.Msg {
269+
body, attachments, err := fetcher.FetchEmailBody(cfg, email.UID)
270+
if err != nil {
271+
return tui.EmailBodyFetchedMsg{Index: index, Err: err}
272+
}
273+
274+
// Return the fetched data along with the original index
275+
return tui.EmailBodyFetchedMsg{
276+
Index: index,
277+
Body: body,
278+
Attachments: attachments,
279+
}
280+
}
281+
}
282+
251283
func markdownToHTML(md []byte) []byte {
252284
var buf bytes.Buffer
253285
p := goldmark.New(goldmark.WithRendererOptions(html.WithUnsafe()))
@@ -327,8 +359,13 @@ func archiveEmailCmd(cfg *config.Config, uid uint32) tea.Cmd {
327359
}
328360
}
329361

330-
func downloadAttachmentCmd(msg tui.DownloadAttachmentMsg) tea.Cmd {
362+
func downloadAttachmentCmd(cfg *config.Config, uid uint32, msg tui.DownloadAttachmentMsg) tea.Cmd {
331363
return func() tea.Msg {
364+
data, err := fetcher.FetchAttachment(cfg, uid, msg.PartID)
365+
if err != nil {
366+
return tui.AttachmentDownloadedMsg{Err: err}
367+
}
368+
332369
homeDir, err := os.UserHomeDir()
333370
if err != nil {
334371
return tui.AttachmentDownloadedMsg{Err: err}
@@ -340,7 +377,7 @@ func downloadAttachmentCmd(msg tui.DownloadAttachmentMsg) tea.Cmd {
340377
}
341378
}
342379
filePath := filepath.Join(downloadsPath, msg.Filename)
343-
err = os.WriteFile(filePath, msg.Data, 0644)
380+
err = os.WriteFile(filePath, data, 0644)
344381
return tui.AttachmentDownloadedMsg{Path: filePath, Err: err}
345382
}
346383
}

0 commit comments

Comments
 (0)