1package fetcher
2
3import (
4 "bytes"
5 "encoding/base64"
6 "fmt"
7 "io"
8 "io/ioutil"
9 "mime"
10 "mime/quotedprintable"
11 "os"
12 "strings"
13 "time"
14
15 "github.com/emersion/go-imap"
16 "github.com/emersion/go-imap/client"
17 "github.com/emersion/go-message/mail"
18 "github.com/floatpane/matcha/config"
19 "golang.org/x/text/encoding/ianaindex"
20 "golang.org/x/text/transform"
21)
22
23// Attachment holds data for an email attachment.
24type Attachment struct {
25 Filename string
26 PartID string // Keep PartID to fetch on demand
27 Data []byte
28 Encoding string // Store encoding for proper decoding
29 MIMEType string // Full MIME type (e.g., image/png)
30 ContentID string // Content-ID for inline assets (e.g., cid: references)
31 Inline bool // True when the part is meant to be displayed inline
32}
33
34type Email struct {
35 UID uint32
36 From string
37 To []string
38 Subject string
39 Body string
40 Date time.Time
41 MessageID string
42 References []string
43 Attachments []Attachment
44 AccountID string // ID of the account this email belongs to
45}
46
47func decodePart(reader io.Reader, header mail.PartHeader) (string, error) {
48 mediaType, params, err := mime.ParseMediaType(header.Get("Content-Type"))
49 if err != nil {
50 body, _ := ioutil.ReadAll(reader)
51 return string(body), nil
52 }
53
54 charset := "utf-8"
55 if params["charset"] != "" {
56 charset = strings.ToLower(params["charset"])
57 }
58
59 encoding, err := ianaindex.IANA.Encoding(charset)
60 if err != nil || encoding == nil {
61 encoding, _ = ianaindex.IANA.Encoding("utf-8")
62 }
63
64 transformReader := transform.NewReader(reader, encoding.NewDecoder())
65 decodedBody, err := ioutil.ReadAll(transformReader)
66 if err != nil {
67 return "", err
68 }
69
70 if strings.HasPrefix(mediaType, "multipart/") {
71 return "[This is a multipart message]", nil
72 }
73
74 return string(decodedBody), nil
75}
76
77func decodeHeader(header string) string {
78 dec := new(mime.WordDecoder)
79 dec.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) {
80 encoding, err := ianaindex.IANA.Encoding(charset)
81 if err != nil {
82 return nil, err
83 }
84 return transform.NewReader(input, encoding.NewDecoder()), nil
85 }
86 decoded, err := dec.DecodeHeader(header)
87 if err != nil {
88 return header
89 }
90 return decoded
91}
92
93func decodeAttachmentData(rawBytes []byte, encoding string) ([]byte, error) {
94 switch strings.ToLower(encoding) {
95 case "base64":
96 decoder := base64.NewDecoder(base64.StdEncoding, bytes.NewReader(rawBytes))
97 return ioutil.ReadAll(decoder)
98 case "quoted-printable":
99 return ioutil.ReadAll(quotedprintable.NewReader(bytes.NewReader(rawBytes)))
100 default:
101 return rawBytes, nil
102 }
103}
104
105func connect(account *config.Account) (*client.Client, error) {
106 imapServer := account.GetIMAPServer()
107 imapPort := account.GetIMAPPort()
108
109 if imapServer == "" {
110 return nil, fmt.Errorf("unsupported service_provider: %s", account.ServiceProvider)
111 }
112
113 addr := fmt.Sprintf("%s:%d", imapServer, imapPort)
114 c, err := client.DialTLS(addr, nil)
115 if err != nil {
116 return nil, err
117 }
118
119 if err := c.Login(account.Email, account.Password); err != nil {
120 return nil, err
121 }
122
123 return c, nil
124}
125
126func getSentMailbox(account *config.Account) string {
127 switch account.ServiceProvider {
128 case "gmail":
129 return "[Gmail]/Sent Mail"
130 case "icloud":
131 return "Sent Messages"
132 default:
133 return "Sent"
134 }
135}
136
137// getMailboxByAttr finds a mailbox with the given IMAP attribute (e.g., \All, \Sent, \Trash).
138func getMailboxByAttr(c *client.Client, attr string) (string, error) {
139 mailboxes := make(chan *imap.MailboxInfo, 10)
140 done := make(chan error, 1)
141 go func() {
142 done <- c.List("", "*", mailboxes)
143 }()
144
145 var foundMailbox string
146 for m := range mailboxes {
147 for _, a := range m.Attributes {
148 if a == attr {
149 foundMailbox = m.Name
150 break
151 }
152 }
153 }
154
155 if err := <-done; err != nil {
156 return "", err
157 }
158
159 if foundMailbox == "" {
160 return "", fmt.Errorf("no mailbox found with attribute %s", attr)
161 }
162
163 return foundMailbox, nil
164}
165
166func FetchMailboxEmails(account *config.Account, mailbox string, limit, offset uint32) ([]Email, error) {
167 c, err := connect(account)
168 if err != nil {
169 return nil, err
170 }
171 defer c.Logout()
172
173 mbox, err := c.Select(mailbox, false)
174 if err != nil {
175 return nil, err
176 }
177
178 if mbox.Messages == 0 {
179 return []Email{}, nil
180 }
181
182 var allEmails []Email
183
184 // Start from the top minus offset
185 cursor := uint32(0)
186 if mbox.Messages > offset {
187 cursor = mbox.Messages - offset
188 } else {
189 return []Email{}, nil
190 }
191
192 // Determine if we should filter
193 fetchEmail := strings.ToLower(strings.TrimSpace(account.FetchEmail))
194 if fetchEmail == "" {
195 fetchEmail = strings.ToLower(strings.TrimSpace(account.Email))
196 }
197 isSentMailbox := mailbox == getSentMailbox(account)
198
199 // Loop until we have enough emails or run out of messages
200 for len(allEmails) < int(limit) && cursor > 0 {
201 // Determine chunk size
202 // Fetch at least 'limit' or 50 messages to reduce round trips
203 chunkSize := limit
204 if chunkSize < 50 {
205 chunkSize = 50
206 }
207
208 from := uint32(1)
209 if cursor > uint32(chunkSize) {
210 from = cursor - uint32(chunkSize) + 1
211 }
212
213 seqset := new(imap.SeqSet)
214 seqset.AddRange(from, cursor)
215
216 messages := make(chan *imap.Message, chunkSize)
217 done := make(chan error, 1)
218 fetchItems := []imap.FetchItem{imap.FetchEnvelope, imap.FetchUid}
219
220 go func() {
221 done <- c.Fetch(seqset, fetchItems, messages)
222 }()
223
224 var batchMsgs []*imap.Message
225 for msg := range messages {
226 batchMsgs = append(batchMsgs, msg)
227 }
228
229 if err := <-done; err != nil {
230 return nil, err
231 }
232
233 // Filter messages in this batch
234 var batchEmails []Email
235 for _, msg := range batchMsgs {
236 if msg == nil || msg.Envelope == nil {
237 continue
238 }
239
240 var fromAddr string
241 if len(msg.Envelope.From) > 0 {
242 fromAddr = msg.Envelope.From[0].Address()
243 }
244
245 var toAddrList []string
246 for _, addr := range msg.Envelope.To {
247 toAddrList = append(toAddrList, addr.Address())
248 }
249 for _, addr := range msg.Envelope.Cc {
250 toAddrList = append(toAddrList, addr.Address())
251 }
252
253 matched := false
254 if isSentMailbox {
255 if strings.EqualFold(strings.TrimSpace(fromAddr), fetchEmail) {
256 matched = true
257 }
258 } else {
259 for _, r := range toAddrList {
260 if strings.EqualFold(strings.TrimSpace(r), fetchEmail) {
261 matched = true
262 break
263 }
264 }
265 }
266
267 if !matched {
268 continue
269 }
270
271 batchEmails = append(batchEmails, Email{
272 UID: msg.Uid,
273 From: fromAddr,
274 To: toAddrList,
275 Subject: decodeHeader(msg.Envelope.Subject),
276 Date: msg.Envelope.Date,
277 AccountID: account.ID,
278 })
279 }
280
281 // Sort batch Newest -> Oldest (since IMAP usually returns Oldest->Newest or arbitrary)
282 // Assuming seqset order or standard behavior, we want to ensure we append Newest emails first
283 // so that the final list is correct.
284 // Actually, let's just sort the batch by UID desc (Newest first)
285 // Simple bubble sort for small batch
286 for i := 0; i < len(batchEmails); i++ {
287 for j := i + 1; j < len(batchEmails); j++ {
288 if batchEmails[j].UID > batchEmails[i].UID {
289 batchEmails[i], batchEmails[j] = batchEmails[j], batchEmails[i]
290 }
291 }
292 }
293
294 // Append to allEmails
295 allEmails = append(allEmails, batchEmails...)
296
297 // Update cursor for next iteration
298 cursor = from - 1
299 }
300
301 // Trim if we have too many
302 if len(allEmails) > int(limit) {
303 allEmails = allEmails[:limit]
304 }
305
306 return allEmails, nil
307}
308
309func FetchEmailBodyFromMailbox(account *config.Account, mailbox string, uid uint32) (string, []Attachment, error) {
310 c, err := connect(account)
311 if err != nil {
312 return "", nil, err
313 }
314 defer c.Logout()
315
316 if _, err := c.Select(mailbox, false); err != nil {
317 return "", nil, err
318 }
319
320 seqset := new(imap.SeqSet)
321 seqset.AddNum(uid)
322
323 fetchInlinePart := func(partID, encoding string) ([]byte, error) {
324 fetchItem := imap.FetchItem(fmt.Sprintf("BODY.PEEK[%s]", partID))
325 section, err := imap.ParseBodySectionName(fetchItem)
326 if err != nil {
327 return nil, err
328 }
329
330 partMessages := make(chan *imap.Message, 1)
331 partDone := make(chan error, 1)
332 go func() {
333 partDone <- c.UidFetch(seqset, []imap.FetchItem{fetchItem}, partMessages)
334 }()
335
336 if err := <-partDone; err != nil {
337 return nil, err
338 }
339
340 partMsg := <-partMessages
341 if partMsg == nil {
342 return nil, fmt.Errorf("could not fetch inline part %s", partID)
343 }
344
345 literal := partMsg.GetBody(section)
346 if literal == nil {
347 return nil, fmt.Errorf("could not get inline part body %s", partID)
348 }
349
350 rawBytes, err := ioutil.ReadAll(literal)
351 if err != nil {
352 return nil, err
353 }
354
355 return decodeAttachmentData(rawBytes, encoding)
356 }
357
358 messages := make(chan *imap.Message, 1)
359 done := make(chan error, 1)
360 fetchItems := []imap.FetchItem{imap.FetchBodyStructure}
361 go func() {
362 done <- c.UidFetch(seqset, fetchItems, messages)
363 }()
364
365 if err := <-done; err != nil {
366 return "", nil, err
367 }
368
369 msg := <-messages
370 if msg == nil || msg.BodyStructure == nil {
371 return "", nil, fmt.Errorf("no message or body structure found with UID %d", uid)
372 }
373
374 var plainPartID, plainPartEncoding string
375 var htmlPartID, htmlPartEncoding string
376 var attachments []Attachment
377 var checkPart func(part *imap.BodyStructure, partID string)
378 checkPart = func(part *imap.BodyStructure, partID string) {
379 // Check for text content (prefer html over plain)
380 if part.MIMEType == "text" {
381 sub := strings.ToLower(part.MIMESubType)
382 switch sub {
383 case "html":
384 if htmlPartID == "" {
385 htmlPartID = partID
386 htmlPartEncoding = part.Encoding
387 }
388 case "plain":
389 if plainPartID == "" {
390 plainPartID = partID
391 plainPartEncoding = part.Encoding
392 }
393 }
394 }
395
396 // Check for attachments using multiple methods
397 filename := ""
398 // First try the Filename() method which handles various cases
399 if fn, err := part.Filename(); err == nil && fn != "" {
400 filename = fn
401 }
402 // Fallback: check DispositionParams
403 if filename == "" {
404 if fn, ok := part.DispositionParams["filename"]; ok && fn != "" {
405 filename = fn
406 }
407 }
408 // Fallback: check Params (for name parameter)
409 if filename == "" {
410 if fn, ok := part.Params["name"]; ok && fn != "" {
411 filename = fn
412 }
413 }
414 // Fallback: check Params for filename
415 if filename == "" {
416 if fn, ok := part.Params["filename"]; ok && fn != "" {
417 filename = fn
418 }
419 }
420
421 // Add as attachment if it has a disposition or a filename (and not just plain text).
422 // Allow inline parts without filenames (common for cid images).
423 contentID := strings.Trim(part.Id, "<>")
424 mimeType := fmt.Sprintf("%s/%s", strings.ToLower(part.MIMEType), strings.ToLower(part.MIMESubType))
425 isCID := contentID != ""
426 isInline := part.Disposition == "inline" || isCID
427
428 if filename == "" && isInline && strings.HasPrefix(mimeType, "image/") {
429 filename = "inline"
430 }
431 if (filename != "" || isCID) && (part.Disposition == "attachment" || isInline || part.MIMEType != "text") {
432 att := Attachment{
433 Filename: filename,
434 PartID: partID,
435 Encoding: part.Encoding, // Store encoding for proper decoding
436 MIMEType: mimeType,
437 ContentID: contentID,
438 Inline: isInline,
439 }
440 if att.Inline && strings.HasPrefix(att.MIMEType, "image/") {
441 if data, err := fetchInlinePart(partID, part.Encoding); err == nil {
442 att.Data = data
443 }
444 }
445 attachments = append(attachments, att)
446 }
447 }
448
449 var findParts func(*imap.BodyStructure, string)
450 findParts = func(bs *imap.BodyStructure, prefix string) {
451 // If this is a non-multipart message, check the body structure itself
452 if len(bs.Parts) == 0 {
453 partID := prefix
454 if partID == "" {
455 partID = "1"
456 }
457 checkPart(bs, partID)
458 return
459 }
460
461 // Iterate through parts
462 for i, part := range bs.Parts {
463 partID := fmt.Sprintf("%d", i+1)
464 if prefix != "" {
465 partID = fmt.Sprintf("%s.%d", prefix, i+1)
466 }
467
468 checkPart(part, partID)
469
470 if len(part.Parts) > 0 {
471 findParts(part, partID)
472 }
473 }
474 }
475 findParts(msg.BodyStructure, "")
476
477 var body string
478 textPartID := ""
479 textPartEncoding := ""
480 if htmlPartID != "" {
481 textPartID = htmlPartID
482 textPartEncoding = htmlPartEncoding
483 } else if plainPartID != "" {
484 textPartID = plainPartID
485 textPartEncoding = plainPartEncoding
486 }
487 if os.Getenv("DEBUG_KITTY_IMAGES") != "" {
488 msg := fmt.Sprintf("[kitty-img] body selection html=%s plain=%s chosen=%s\n", htmlPartID, plainPartID, textPartID)
489 fmt.Print(msg)
490 if path := os.Getenv("DEBUG_KITTY_LOG"); path != "" {
491 if f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil {
492 _, _ = f.WriteString(msg)
493 _ = f.Close()
494 }
495 }
496 }
497 if textPartID != "" {
498 partMessages := make(chan *imap.Message, 1)
499 partDone := make(chan error, 1)
500
501 fetchItem := imap.FetchItem(fmt.Sprintf("BODY.PEEK[%s]", textPartID))
502 section, err := imap.ParseBodySectionName(fetchItem)
503 if err != nil {
504 return "", nil, err
505 }
506
507 go func() {
508 partDone <- c.UidFetch(seqset, []imap.FetchItem{fetchItem}, partMessages)
509 }()
510
511 if err := <-partDone; err != nil {
512 return "", nil, err
513 }
514
515 partMsg := <-partMessages
516 if partMsg != nil {
517 literal := partMsg.GetBody(section)
518 if literal != nil {
519 buf, _ := ioutil.ReadAll(literal)
520 // Use the encoding from BodyStructure to decode
521 if decoded, err := decodeAttachmentData(buf, textPartEncoding); err == nil {
522 body = string(decoded)
523 } else {
524 body = string(buf)
525 }
526 }
527 }
528 }
529
530 return body, attachments, nil
531}
532
533func FetchAttachmentFromMailbox(account *config.Account, mailbox string, uid uint32, partID string, encoding string) ([]byte, error) {
534 c, err := connect(account)
535 if err != nil {
536 return nil, err
537 }
538 defer c.Logout()
539
540 if _, err := c.Select(mailbox, false); err != nil {
541 return nil, err
542 }
543
544 seqset := new(imap.SeqSet)
545 seqset.AddNum(uid)
546
547 fetchItem := imap.FetchItem(fmt.Sprintf("BODY.PEEK[%s]", partID))
548 section, err := imap.ParseBodySectionName(fetchItem)
549 if err != nil {
550 return nil, err
551 }
552
553 messages := make(chan *imap.Message, 1)
554 done := make(chan error, 1)
555 go func() {
556 done <- c.UidFetch(seqset, []imap.FetchItem{fetchItem}, messages)
557 }()
558
559 if err := <-done; err != nil {
560 return nil, err
561 }
562
563 msg := <-messages
564 if msg == nil {
565 return nil, fmt.Errorf("could not fetch attachment")
566 }
567
568 literal := msg.GetBody(section)
569 if literal == nil {
570 return nil, fmt.Errorf("could not get attachment body")
571 }
572
573 rawBytes, err := ioutil.ReadAll(literal)
574 if err != nil {
575 return nil, err
576 }
577
578 decoded, err := decodeAttachmentData(rawBytes, encoding)
579 if err != nil {
580 return rawBytes, nil
581 }
582 return decoded, nil
583}
584
585func moveEmail(account *config.Account, uid uint32, sourceMailbox, destMailbox string) error {
586 c, err := connect(account)
587 if err != nil {
588 return err
589 }
590 defer c.Logout()
591
592 if _, err := c.Select(sourceMailbox, false); err != nil {
593 return err
594 }
595
596 seqSet := new(imap.SeqSet)
597 seqSet.AddNum(uid)
598
599 return c.UidMove(seqSet, destMailbox)
600}
601
602func DeleteEmailFromMailbox(account *config.Account, mailbox string, uid uint32) error {
603 c, err := connect(account)
604 if err != nil {
605 return err
606 }
607 defer c.Logout()
608
609 if _, err := c.Select(mailbox, false); err != nil {
610 return err
611 }
612
613 seqSet := new(imap.SeqSet)
614 seqSet.AddNum(uid)
615
616 item := imap.FormatFlagsOp(imap.AddFlags, true)
617 flags := []interface{}{imap.DeletedFlag}
618
619 if err := c.UidStore(seqSet, item, flags, nil); err != nil {
620 return err
621 }
622
623 return c.Expunge(nil)
624}
625
626func ArchiveEmailFromMailbox(account *config.Account, mailbox string, uid uint32) error {
627 c, err := connect(account)
628 if err != nil {
629 return err
630 }
631 defer c.Logout()
632
633 var archiveMailbox string
634 switch account.ServiceProvider {
635 case "gmail":
636 // For Gmail, find the mailbox with the \All attribute
637 archiveMailbox, err = getMailboxByAttr(c, imap.AllAttr)
638 if err != nil {
639 // Fallback to hardcoded path if attribute lookup fails
640 archiveMailbox = "[Gmail]/All Mail"
641 }
642 default:
643 archiveMailbox = "Archive"
644 }
645
646 if _, err := c.Select(mailbox, false); err != nil {
647 return err
648 }
649
650 seqSet := new(imap.SeqSet)
651 seqSet.AddNum(uid)
652
653 return c.UidMove(seqSet, archiveMailbox)
654}
655
656// Convenience wrappers defaulting to INBOX for existing call sites.
657
658func FetchEmails(account *config.Account, limit, offset uint32) ([]Email, error) {
659 return FetchMailboxEmails(account, "INBOX", limit, offset)
660}
661
662func FetchSentEmails(account *config.Account, limit, offset uint32) ([]Email, error) {
663 return FetchMailboxEmails(account, getSentMailbox(account), limit, offset)
664}
665
666func FetchEmailBody(account *config.Account, uid uint32) (string, []Attachment, error) {
667 return FetchEmailBodyFromMailbox(account, "INBOX", uid)
668}
669
670func FetchSentEmailBody(account *config.Account, uid uint32) (string, []Attachment, error) {
671 return FetchEmailBodyFromMailbox(account, getSentMailbox(account), uid)
672}
673
674func FetchAttachment(account *config.Account, uid uint32, partID string, encoding string) ([]byte, error) {
675 return FetchAttachmentFromMailbox(account, "INBOX", uid, partID, encoding)
676}
677
678func FetchSentAttachment(account *config.Account, uid uint32, partID string, encoding string) ([]byte, error) {
679 return FetchAttachmentFromMailbox(account, getSentMailbox(account), uid, partID, encoding)
680}
681
682func DeleteEmail(account *config.Account, uid uint32) error {
683 return DeleteEmailFromMailbox(account, "INBOX", uid)
684}
685
686func DeleteSentEmail(account *config.Account, uid uint32) error {
687 return DeleteEmailFromMailbox(account, getSentMailbox(account), uid)
688}
689
690func ArchiveEmail(account *config.Account, uid uint32) error {
691 return ArchiveEmailFromMailbox(account, "INBOX", uid)
692}
693
694func ArchiveSentEmail(account *config.Account, uid uint32) error {
695 return ArchiveEmailFromMailbox(account, getSentMailbox(account), uid)
696}
697
698// getTrashMailbox returns the trash mailbox name for the account
699func getTrashMailbox(account *config.Account) string {
700 switch account.ServiceProvider {
701 case "gmail":
702 return "[Gmail]/Trash"
703 case "icloud":
704 return "Deleted Messages"
705 default:
706 return "Trash"
707 }
708}
709
710// getArchiveMailbox returns the archive/all mail mailbox name for the account
711func getArchiveMailbox(account *config.Account) string {
712 switch account.ServiceProvider {
713 case "gmail":
714 return "[Gmail]/All Mail"
715 case "icloud":
716 return "Archive"
717 default:
718 return "Archive"
719 }
720}
721
722// FetchTrashEmails fetches emails from the trash folder
723func FetchTrashEmails(account *config.Account, limit, offset uint32) ([]Email, error) {
724 c, err := connect(account)
725 if err != nil {
726 return nil, err
727 }
728 defer c.Logout()
729
730 // Try to find trash by attribute first
731 trashMailbox, err := getMailboxByAttr(c, imap.TrashAttr)
732 if err != nil {
733 // Fallback to hardcoded path
734 trashMailbox = getTrashMailbox(account)
735 }
736
737 return FetchMailboxEmails(account, trashMailbox, limit, offset)
738}
739
740// FetchArchiveEmails fetches emails from the archive/all mail folder
741// Archive contains all emails, so we match where user is sender OR recipient
742func FetchArchiveEmails(account *config.Account, limit, offset uint32) ([]Email, error) {
743 c, err := connect(account)
744 if err != nil {
745 return nil, err
746 }
747 defer c.Logout()
748
749 // Try to find archive by attribute first (Gmail uses \All)
750 archiveMailbox, err := getMailboxByAttr(c, imap.AllAttr)
751 if err != nil {
752 // Fallback to hardcoded path
753 archiveMailbox = getArchiveMailbox(account)
754 }
755
756 mbox, err := c.Select(archiveMailbox, false)
757 if err != nil {
758 return nil, err
759 }
760
761 if mbox.Messages == 0 {
762 return []Email{}, nil
763 }
764
765 to := mbox.Messages - offset
766 from := uint32(1)
767 if to > limit {
768 from = to - limit + 1
769 }
770
771 if to < 1 {
772 return []Email{}, nil
773 }
774
775 seqset := new(imap.SeqSet)
776 seqset.AddRange(from, to)
777
778 messages := make(chan *imap.Message, limit)
779 done := make(chan error, 1)
780 fetchItems := []imap.FetchItem{imap.FetchEnvelope, imap.FetchUid}
781 go func() {
782 done <- c.Fetch(seqset, fetchItems, messages)
783 }()
784
785 var msgs []*imap.Message
786 for msg := range messages {
787 msgs = append(msgs, msg)
788 }
789
790 if err := <-done; err != nil {
791 return nil, err
792 }
793
794 // Determine which email to filter on: prefer Account.FetchEmail, fallback to Account.Email
795 fetchEmail := strings.ToLower(strings.TrimSpace(account.FetchEmail))
796 if fetchEmail == "" {
797 fetchEmail = strings.ToLower(strings.TrimSpace(account.Email))
798 }
799
800 var emails []Email
801 for _, msg := range msgs {
802 if msg == nil || msg.Envelope == nil {
803 continue
804 }
805
806 var fromAddr string
807 if len(msg.Envelope.From) > 0 {
808 fromAddr = msg.Envelope.From[0].Address()
809 }
810
811 var toAddrList []string
812 for _, addr := range msg.Envelope.To {
813 toAddrList = append(toAddrList, addr.Address())
814 }
815 for _, addr := range msg.Envelope.Cc {
816 toAddrList = append(toAddrList, addr.Address())
817 }
818
819 // For archive/All Mail, match emails where user is sender OR recipient
820 matched := false
821 // Check if user is the sender
822 if strings.EqualFold(strings.TrimSpace(fromAddr), fetchEmail) {
823 matched = true
824 }
825 // Check if user is a recipient
826 if !matched {
827 for _, r := range toAddrList {
828 if strings.EqualFold(strings.TrimSpace(r), fetchEmail) {
829 matched = true
830 break
831 }
832 }
833 }
834
835 if !matched {
836 continue
837 }
838
839 emails = append(emails, Email{
840 UID: msg.Uid,
841 From: fromAddr,
842 To: toAddrList,
843 Subject: decodeHeader(msg.Envelope.Subject),
844 Date: msg.Envelope.Date,
845 AccountID: account.ID,
846 })
847 }
848
849 // Reverse to get newest first
850 for i, j := 0, len(emails)-1; i < j; i, j = i+1, j-1 {
851 emails[i], emails[j] = emails[j], emails[i]
852 }
853
854 return emails, nil
855}
856
857// FetchTrashEmailBody fetches the body of an email from trash
858func FetchTrashEmailBody(account *config.Account, uid uint32) (string, []Attachment, error) {
859 c, err := connect(account)
860 if err != nil {
861 return "", nil, err
862 }
863 defer c.Logout()
864
865 trashMailbox, err := getMailboxByAttr(c, imap.TrashAttr)
866 if err != nil {
867 trashMailbox = getTrashMailbox(account)
868 }
869
870 return FetchEmailBodyFromMailbox(account, trashMailbox, uid)
871}
872
873// FetchArchiveEmailBody fetches the body of an email from archive
874func FetchArchiveEmailBody(account *config.Account, uid uint32) (string, []Attachment, error) {
875 c, err := connect(account)
876 if err != nil {
877 return "", nil, err
878 }
879 defer c.Logout()
880
881 archiveMailbox, err := getMailboxByAttr(c, imap.AllAttr)
882 if err != nil {
883 archiveMailbox = getArchiveMailbox(account)
884 }
885
886 return FetchEmailBodyFromMailbox(account, archiveMailbox, uid)
887}
888
889// FetchTrashAttachment fetches an attachment from trash
890func FetchTrashAttachment(account *config.Account, uid uint32, partID string, encoding string) ([]byte, error) {
891 c, err := connect(account)
892 if err != nil {
893 return nil, err
894 }
895 defer c.Logout()
896
897 trashMailbox, err := getMailboxByAttr(c, imap.TrashAttr)
898 if err != nil {
899 trashMailbox = getTrashMailbox(account)
900 }
901
902 return FetchAttachmentFromMailbox(account, trashMailbox, uid, partID, encoding)
903}
904
905// FetchArchiveAttachment fetches an attachment from archive
906func FetchArchiveAttachment(account *config.Account, uid uint32, partID string, encoding string) ([]byte, error) {
907 c, err := connect(account)
908 if err != nil {
909 return nil, err
910 }
911 defer c.Logout()
912
913 archiveMailbox, err := getMailboxByAttr(c, imap.AllAttr)
914 if err != nil {
915 archiveMailbox = getArchiveMailbox(account)
916 }
917
918 return FetchAttachmentFromMailbox(account, archiveMailbox, uid, partID, encoding)
919}
920
921// DeleteTrashEmail permanently deletes an email from trash
922func DeleteTrashEmail(account *config.Account, uid uint32) error {
923 c, err := connect(account)
924 if err != nil {
925 return err
926 }
927 defer c.Logout()
928
929 trashMailbox, err := getMailboxByAttr(c, imap.TrashAttr)
930 if err != nil {
931 trashMailbox = getTrashMailbox(account)
932 }
933
934 return DeleteEmailFromMailbox(account, trashMailbox, uid)
935}
936
937// DeleteArchiveEmail deletes an email from archive (moves to trash)
938func DeleteArchiveEmail(account *config.Account, uid uint32) error {
939 c, err := connect(account)
940 if err != nil {
941 return err
942 }
943 defer c.Logout()
944
945 archiveMailbox, err := getMailboxByAttr(c, imap.AllAttr)
946 if err != nil {
947 archiveMailbox = getArchiveMailbox(account)
948 }
949
950 return DeleteEmailFromMailbox(account, archiveMailbox, uid)
951}