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 to := mbox.Messages - offset
183 from := uint32(1)
184 if to > limit {
185 from = to - limit + 1
186 }
187
188 if to < 1 {
189 return []Email{}, nil
190 }
191
192 seqset := new(imap.SeqSet)
193 seqset.AddRange(from, to)
194
195 messages := make(chan *imap.Message, limit)
196 done := make(chan error, 1)
197 fetchItems := []imap.FetchItem{imap.FetchEnvelope, imap.FetchUid}
198 go func() {
199 done <- c.Fetch(seqset, fetchItems, messages)
200 }()
201
202 var msgs []*imap.Message
203 for msg := range messages {
204 msgs = append(msgs, msg)
205 }
206
207 if err := <-done; err != nil {
208 return nil, err
209 }
210
211 var emails []Email
212 for _, msg := range msgs {
213 if msg == nil || msg.Envelope == nil {
214 continue
215 }
216
217 var fromAddr string
218 if len(msg.Envelope.From) > 0 {
219 fromAddr = msg.Envelope.From[0].Address()
220 }
221
222 var toAddrList []string
223 // Build recipient list from To and Cc for matching and display
224 for _, addr := range msg.Envelope.To {
225 toAddrList = append(toAddrList, addr.Address())
226 }
227 for _, addr := range msg.Envelope.Cc {
228 toAddrList = append(toAddrList, addr.Address())
229 }
230
231 // Determine which email to filter on: prefer Account.FetchEmail, fallback to Account.Email
232 fetchEmail := strings.ToLower(strings.TrimSpace(account.FetchEmail))
233 if fetchEmail == "" {
234 fetchEmail = strings.ToLower(strings.TrimSpace(account.Email))
235 }
236
237 // Determine if this is a sent mailbox
238 isSentMailbox := mailbox == getSentMailbox(account)
239
240 // Apply different filtering logic based on mailbox type
241 matched := false
242 if isSentMailbox {
243 // For sent mailbox, check if the sender matches the fetchEmail
244 if strings.EqualFold(strings.TrimSpace(fromAddr), fetchEmail) {
245 matched = true
246 }
247 } else {
248 // For inbox and other mailboxes, check if any recipient matches the fetchEmail
249 for _, r := range toAddrList {
250 if strings.EqualFold(strings.TrimSpace(r), fetchEmail) {
251 matched = true
252 break
253 }
254 }
255 }
256
257 if !matched {
258 // Skip messages not matching the filter criteria
259 continue
260 }
261
262 emails = append(emails, Email{
263 UID: msg.Uid,
264 From: fromAddr,
265 To: toAddrList,
266 Subject: decodeHeader(msg.Envelope.Subject),
267 Date: msg.Envelope.Date,
268 AccountID: account.ID,
269 })
270 }
271
272 for i, j := 0, len(emails)-1; i < j; i, j = i+1, j-1 {
273 emails[i], emails[j] = emails[j], emails[i]
274 }
275
276 return emails, nil
277}
278
279func FetchEmailBodyFromMailbox(account *config.Account, mailbox string, uid uint32) (string, []Attachment, error) {
280 c, err := connect(account)
281 if err != nil {
282 return "", nil, err
283 }
284 defer c.Logout()
285
286 if _, err := c.Select(mailbox, false); err != nil {
287 return "", nil, err
288 }
289
290 seqset := new(imap.SeqSet)
291 seqset.AddNum(uid)
292
293 fetchInlinePart := func(partID, encoding string) ([]byte, error) {
294 fetchItem := imap.FetchItem(fmt.Sprintf("BODY.PEEK[%s]", partID))
295 section, err := imap.ParseBodySectionName(fetchItem)
296 if err != nil {
297 return nil, err
298 }
299
300 partMessages := make(chan *imap.Message, 1)
301 partDone := make(chan error, 1)
302 go func() {
303 partDone <- c.UidFetch(seqset, []imap.FetchItem{fetchItem}, partMessages)
304 }()
305
306 if err := <-partDone; err != nil {
307 return nil, err
308 }
309
310 partMsg := <-partMessages
311 if partMsg == nil {
312 return nil, fmt.Errorf("could not fetch inline part %s", partID)
313 }
314
315 literal := partMsg.GetBody(section)
316 if literal == nil {
317 return nil, fmt.Errorf("could not get inline part body %s", partID)
318 }
319
320 rawBytes, err := ioutil.ReadAll(literal)
321 if err != nil {
322 return nil, err
323 }
324
325 return decodeAttachmentData(rawBytes, encoding)
326 }
327
328 messages := make(chan *imap.Message, 1)
329 done := make(chan error, 1)
330 fetchItems := []imap.FetchItem{imap.FetchBodyStructure}
331 go func() {
332 done <- c.UidFetch(seqset, fetchItems, messages)
333 }()
334
335 if err := <-done; err != nil {
336 return "", nil, err
337 }
338
339 msg := <-messages
340 if msg == nil || msg.BodyStructure == nil {
341 return "", nil, fmt.Errorf("no message or body structure found with UID %d", uid)
342 }
343
344 var plainPartID, plainPartEncoding string
345 var htmlPartID, htmlPartEncoding string
346 var attachments []Attachment
347 var checkPart func(part *imap.BodyStructure, partID string)
348 checkPart = func(part *imap.BodyStructure, partID string) {
349 // Check for text content (prefer html over plain)
350 if part.MIMEType == "text" {
351 sub := strings.ToLower(part.MIMESubType)
352 switch sub {
353 case "html":
354 if htmlPartID == "" {
355 htmlPartID = partID
356 htmlPartEncoding = part.Encoding
357 }
358 case "plain":
359 if plainPartID == "" {
360 plainPartID = partID
361 plainPartEncoding = part.Encoding
362 }
363 }
364 }
365
366 // Check for attachments using multiple methods
367 filename := ""
368 // First try the Filename() method which handles various cases
369 if fn, err := part.Filename(); err == nil && fn != "" {
370 filename = fn
371 }
372 // Fallback: check DispositionParams
373 if filename == "" {
374 if fn, ok := part.DispositionParams["filename"]; ok && fn != "" {
375 filename = fn
376 }
377 }
378 // Fallback: check Params (for name parameter)
379 if filename == "" {
380 if fn, ok := part.Params["name"]; ok && fn != "" {
381 filename = fn
382 }
383 }
384 // Fallback: check Params for filename
385 if filename == "" {
386 if fn, ok := part.Params["filename"]; ok && fn != "" {
387 filename = fn
388 }
389 }
390
391 // Add as attachment if it has a disposition or a filename (and not just plain text).
392 // Allow inline parts without filenames (common for cid images).
393 contentID := strings.Trim(part.Id, "<>")
394 mimeType := fmt.Sprintf("%s/%s", strings.ToLower(part.MIMEType), strings.ToLower(part.MIMESubType))
395 isCID := contentID != ""
396 isInline := part.Disposition == "inline" || isCID
397
398 if filename == "" && isInline && strings.HasPrefix(mimeType, "image/") {
399 filename = "inline"
400 }
401 if (filename != "" || isCID) && (part.Disposition == "attachment" || isInline || part.MIMEType != "text") {
402 att := Attachment{
403 Filename: filename,
404 PartID: partID,
405 Encoding: part.Encoding, // Store encoding for proper decoding
406 MIMEType: mimeType,
407 ContentID: contentID,
408 Inline: isInline,
409 }
410 if att.Inline && strings.HasPrefix(att.MIMEType, "image/") {
411 if data, err := fetchInlinePart(partID, part.Encoding); err == nil {
412 att.Data = data
413 }
414 }
415 attachments = append(attachments, att)
416 }
417 }
418
419 var findParts func(*imap.BodyStructure, string)
420 findParts = func(bs *imap.BodyStructure, prefix string) {
421 // If this is a non-multipart message, check the body structure itself
422 if len(bs.Parts) == 0 {
423 partID := prefix
424 if partID == "" {
425 partID = "1"
426 }
427 checkPart(bs, partID)
428 return
429 }
430
431 // Iterate through parts
432 for i, part := range bs.Parts {
433 partID := fmt.Sprintf("%d", i+1)
434 if prefix != "" {
435 partID = fmt.Sprintf("%s.%d", prefix, i+1)
436 }
437
438 checkPart(part, partID)
439
440 if len(part.Parts) > 0 {
441 findParts(part, partID)
442 }
443 }
444 }
445 findParts(msg.BodyStructure, "")
446
447 var body string
448 textPartID := ""
449 textPartEncoding := ""
450 if htmlPartID != "" {
451 textPartID = htmlPartID
452 textPartEncoding = htmlPartEncoding
453 } else if plainPartID != "" {
454 textPartID = plainPartID
455 textPartEncoding = plainPartEncoding
456 }
457 if os.Getenv("DEBUG_KITTY_IMAGES") != "" {
458 msg := fmt.Sprintf("[kitty-img] body selection html=%s plain=%s chosen=%s\n", htmlPartID, plainPartID, textPartID)
459 fmt.Print(msg)
460 if path := os.Getenv("DEBUG_KITTY_LOG"); path != "" {
461 if f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil {
462 _, _ = f.WriteString(msg)
463 _ = f.Close()
464 }
465 }
466 }
467 if textPartID != "" {
468 partMessages := make(chan *imap.Message, 1)
469 partDone := make(chan error, 1)
470
471 fetchItem := imap.FetchItem(fmt.Sprintf("BODY.PEEK[%s]", textPartID))
472 section, err := imap.ParseBodySectionName(fetchItem)
473 if err != nil {
474 return "", nil, err
475 }
476
477 go func() {
478 partDone <- c.UidFetch(seqset, []imap.FetchItem{fetchItem}, partMessages)
479 }()
480
481 if err := <-partDone; err != nil {
482 return "", nil, err
483 }
484
485 partMsg := <-partMessages
486 if partMsg != nil {
487 literal := partMsg.GetBody(section)
488 if literal != nil {
489 buf, _ := ioutil.ReadAll(literal)
490 // Use the encoding from BodyStructure to decode
491 if decoded, err := decodeAttachmentData(buf, textPartEncoding); err == nil {
492 body = string(decoded)
493 } else {
494 body = string(buf)
495 }
496 }
497 }
498 }
499
500 return body, attachments, nil
501}
502
503func FetchAttachmentFromMailbox(account *config.Account, mailbox string, uid uint32, partID string, encoding string) ([]byte, error) {
504 c, err := connect(account)
505 if err != nil {
506 return nil, err
507 }
508 defer c.Logout()
509
510 if _, err := c.Select(mailbox, false); err != nil {
511 return nil, err
512 }
513
514 seqset := new(imap.SeqSet)
515 seqset.AddNum(uid)
516
517 fetchItem := imap.FetchItem(fmt.Sprintf("BODY.PEEK[%s]", partID))
518 section, err := imap.ParseBodySectionName(fetchItem)
519 if err != nil {
520 return nil, err
521 }
522
523 messages := make(chan *imap.Message, 1)
524 done := make(chan error, 1)
525 go func() {
526 done <- c.UidFetch(seqset, []imap.FetchItem{fetchItem}, messages)
527 }()
528
529 if err := <-done; err != nil {
530 return nil, err
531 }
532
533 msg := <-messages
534 if msg == nil {
535 return nil, fmt.Errorf("could not fetch attachment")
536 }
537
538 literal := msg.GetBody(section)
539 if literal == nil {
540 return nil, fmt.Errorf("could not get attachment body")
541 }
542
543 rawBytes, err := ioutil.ReadAll(literal)
544 if err != nil {
545 return nil, err
546 }
547
548 decoded, err := decodeAttachmentData(rawBytes, encoding)
549 if err != nil {
550 return rawBytes, nil
551 }
552 return decoded, nil
553}
554
555func moveEmail(account *config.Account, uid uint32, sourceMailbox, destMailbox string) error {
556 c, err := connect(account)
557 if err != nil {
558 return err
559 }
560 defer c.Logout()
561
562 if _, err := c.Select(sourceMailbox, false); err != nil {
563 return err
564 }
565
566 seqSet := new(imap.SeqSet)
567 seqSet.AddNum(uid)
568
569 return c.UidMove(seqSet, destMailbox)
570}
571
572func DeleteEmailFromMailbox(account *config.Account, mailbox string, uid uint32) error {
573 c, err := connect(account)
574 if err != nil {
575 return err
576 }
577 defer c.Logout()
578
579 if _, err := c.Select(mailbox, false); err != nil {
580 return err
581 }
582
583 seqSet := new(imap.SeqSet)
584 seqSet.AddNum(uid)
585
586 item := imap.FormatFlagsOp(imap.AddFlags, true)
587 flags := []interface{}{imap.DeletedFlag}
588
589 if err := c.UidStore(seqSet, item, flags, nil); err != nil {
590 return err
591 }
592
593 return c.Expunge(nil)
594}
595
596func ArchiveEmailFromMailbox(account *config.Account, mailbox string, uid uint32) error {
597 c, err := connect(account)
598 if err != nil {
599 return err
600 }
601 defer c.Logout()
602
603 var archiveMailbox string
604 switch account.ServiceProvider {
605 case "gmail":
606 // For Gmail, find the mailbox with the \All attribute
607 archiveMailbox, err = getMailboxByAttr(c, imap.AllAttr)
608 if err != nil {
609 // Fallback to hardcoded path if attribute lookup fails
610 archiveMailbox = "[Gmail]/All Mail"
611 }
612 default:
613 archiveMailbox = "Archive"
614 }
615
616 if _, err := c.Select(mailbox, false); err != nil {
617 return err
618 }
619
620 seqSet := new(imap.SeqSet)
621 seqSet.AddNum(uid)
622
623 return c.UidMove(seqSet, archiveMailbox)
624}
625
626// Convenience wrappers defaulting to INBOX for existing call sites.
627
628func FetchEmails(account *config.Account, limit, offset uint32) ([]Email, error) {
629 return FetchMailboxEmails(account, "INBOX", limit, offset)
630}
631
632func FetchSentEmails(account *config.Account, limit, offset uint32) ([]Email, error) {
633 return FetchMailboxEmails(account, getSentMailbox(account), limit, offset)
634}
635
636func FetchEmailBody(account *config.Account, uid uint32) (string, []Attachment, error) {
637 return FetchEmailBodyFromMailbox(account, "INBOX", uid)
638}
639
640func FetchSentEmailBody(account *config.Account, uid uint32) (string, []Attachment, error) {
641 return FetchEmailBodyFromMailbox(account, getSentMailbox(account), uid)
642}
643
644func FetchAttachment(account *config.Account, uid uint32, partID string, encoding string) ([]byte, error) {
645 return FetchAttachmentFromMailbox(account, "INBOX", uid, partID, encoding)
646}
647
648func FetchSentAttachment(account *config.Account, uid uint32, partID string, encoding string) ([]byte, error) {
649 return FetchAttachmentFromMailbox(account, getSentMailbox(account), uid, partID, encoding)
650}
651
652func DeleteEmail(account *config.Account, uid uint32) error {
653 return DeleteEmailFromMailbox(account, "INBOX", uid)
654}
655
656func DeleteSentEmail(account *config.Account, uid uint32) error {
657 return DeleteEmailFromMailbox(account, getSentMailbox(account), uid)
658}
659
660func ArchiveEmail(account *config.Account, uid uint32) error {
661 return ArchiveEmailFromMailbox(account, "INBOX", uid)
662}
663
664func ArchiveSentEmail(account *config.Account, uid uint32) error {
665 return ArchiveEmailFromMailbox(account, getSentMailbox(account), uid)
666}
667
668// getTrashMailbox returns the trash mailbox name for the account
669func getTrashMailbox(account *config.Account) string {
670 switch account.ServiceProvider {
671 case "gmail":
672 return "[Gmail]/Trash"
673 case "icloud":
674 return "Deleted Messages"
675 default:
676 return "Trash"
677 }
678}
679
680// getArchiveMailbox returns the archive/all mail mailbox name for the account
681func getArchiveMailbox(account *config.Account) string {
682 switch account.ServiceProvider {
683 case "gmail":
684 return "[Gmail]/All Mail"
685 case "icloud":
686 return "Archive"
687 default:
688 return "Archive"
689 }
690}
691
692// FetchTrashEmails fetches emails from the trash folder
693func FetchTrashEmails(account *config.Account, limit, offset uint32) ([]Email, error) {
694 c, err := connect(account)
695 if err != nil {
696 return nil, err
697 }
698 defer c.Logout()
699
700 // Try to find trash by attribute first
701 trashMailbox, err := getMailboxByAttr(c, imap.TrashAttr)
702 if err != nil {
703 // Fallback to hardcoded path
704 trashMailbox = getTrashMailbox(account)
705 }
706
707 return FetchMailboxEmails(account, trashMailbox, limit, offset)
708}
709
710// FetchArchiveEmails fetches emails from the archive/all mail folder
711// Archive contains all emails, so we match where user is sender OR recipient
712func FetchArchiveEmails(account *config.Account, limit, offset uint32) ([]Email, error) {
713 c, err := connect(account)
714 if err != nil {
715 return nil, err
716 }
717 defer c.Logout()
718
719 // Try to find archive by attribute first (Gmail uses \All)
720 archiveMailbox, err := getMailboxByAttr(c, imap.AllAttr)
721 if err != nil {
722 // Fallback to hardcoded path
723 archiveMailbox = getArchiveMailbox(account)
724 }
725
726 mbox, err := c.Select(archiveMailbox, false)
727 if err != nil {
728 return nil, err
729 }
730
731 if mbox.Messages == 0 {
732 return []Email{}, nil
733 }
734
735 to := mbox.Messages - offset
736 from := uint32(1)
737 if to > limit {
738 from = to - limit + 1
739 }
740
741 if to < 1 {
742 return []Email{}, nil
743 }
744
745 seqset := new(imap.SeqSet)
746 seqset.AddRange(from, to)
747
748 messages := make(chan *imap.Message, limit)
749 done := make(chan error, 1)
750 fetchItems := []imap.FetchItem{imap.FetchEnvelope, imap.FetchUid}
751 go func() {
752 done <- c.Fetch(seqset, fetchItems, messages)
753 }()
754
755 var msgs []*imap.Message
756 for msg := range messages {
757 msgs = append(msgs, msg)
758 }
759
760 if err := <-done; err != nil {
761 return nil, err
762 }
763
764 // Determine which email to filter on: prefer Account.FetchEmail, fallback to Account.Email
765 fetchEmail := strings.ToLower(strings.TrimSpace(account.FetchEmail))
766 if fetchEmail == "" {
767 fetchEmail = strings.ToLower(strings.TrimSpace(account.Email))
768 }
769
770 var emails []Email
771 for _, msg := range msgs {
772 if msg == nil || msg.Envelope == nil {
773 continue
774 }
775
776 var fromAddr string
777 if len(msg.Envelope.From) > 0 {
778 fromAddr = msg.Envelope.From[0].Address()
779 }
780
781 var toAddrList []string
782 for _, addr := range msg.Envelope.To {
783 toAddrList = append(toAddrList, addr.Address())
784 }
785 for _, addr := range msg.Envelope.Cc {
786 toAddrList = append(toAddrList, addr.Address())
787 }
788
789 // For archive/All Mail, match emails where user is sender OR recipient
790 matched := false
791 // Check if user is the sender
792 if strings.EqualFold(strings.TrimSpace(fromAddr), fetchEmail) {
793 matched = true
794 }
795 // Check if user is a recipient
796 if !matched {
797 for _, r := range toAddrList {
798 if strings.EqualFold(strings.TrimSpace(r), fetchEmail) {
799 matched = true
800 break
801 }
802 }
803 }
804
805 if !matched {
806 continue
807 }
808
809 emails = append(emails, Email{
810 UID: msg.Uid,
811 From: fromAddr,
812 To: toAddrList,
813 Subject: decodeHeader(msg.Envelope.Subject),
814 Date: msg.Envelope.Date,
815 AccountID: account.ID,
816 })
817 }
818
819 // Reverse to get newest first
820 for i, j := 0, len(emails)-1; i < j; i, j = i+1, j-1 {
821 emails[i], emails[j] = emails[j], emails[i]
822 }
823
824 return emails, nil
825}
826
827// FetchTrashEmailBody fetches the body of an email from trash
828func FetchTrashEmailBody(account *config.Account, uid uint32) (string, []Attachment, error) {
829 c, err := connect(account)
830 if err != nil {
831 return "", nil, err
832 }
833 defer c.Logout()
834
835 trashMailbox, err := getMailboxByAttr(c, imap.TrashAttr)
836 if err != nil {
837 trashMailbox = getTrashMailbox(account)
838 }
839
840 return FetchEmailBodyFromMailbox(account, trashMailbox, uid)
841}
842
843// FetchArchiveEmailBody fetches the body of an email from archive
844func FetchArchiveEmailBody(account *config.Account, uid uint32) (string, []Attachment, error) {
845 c, err := connect(account)
846 if err != nil {
847 return "", nil, err
848 }
849 defer c.Logout()
850
851 archiveMailbox, err := getMailboxByAttr(c, imap.AllAttr)
852 if err != nil {
853 archiveMailbox = getArchiveMailbox(account)
854 }
855
856 return FetchEmailBodyFromMailbox(account, archiveMailbox, uid)
857}
858
859// FetchTrashAttachment fetches an attachment from trash
860func FetchTrashAttachment(account *config.Account, uid uint32, partID string, encoding string) ([]byte, error) {
861 c, err := connect(account)
862 if err != nil {
863 return nil, err
864 }
865 defer c.Logout()
866
867 trashMailbox, err := getMailboxByAttr(c, imap.TrashAttr)
868 if err != nil {
869 trashMailbox = getTrashMailbox(account)
870 }
871
872 return FetchAttachmentFromMailbox(account, trashMailbox, uid, partID, encoding)
873}
874
875// FetchArchiveAttachment fetches an attachment from archive
876func FetchArchiveAttachment(account *config.Account, uid uint32, partID string, encoding string) ([]byte, error) {
877 c, err := connect(account)
878 if err != nil {
879 return nil, err
880 }
881 defer c.Logout()
882
883 archiveMailbox, err := getMailboxByAttr(c, imap.AllAttr)
884 if err != nil {
885 archiveMailbox = getArchiveMailbox(account)
886 }
887
888 return FetchAttachmentFromMailbox(account, archiveMailbox, uid, partID, encoding)
889}
890
891// DeleteTrashEmail permanently deletes an email from trash
892func DeleteTrashEmail(account *config.Account, uid uint32) error {
893 c, err := connect(account)
894 if err != nil {
895 return err
896 }
897 defer c.Logout()
898
899 trashMailbox, err := getMailboxByAttr(c, imap.TrashAttr)
900 if err != nil {
901 trashMailbox = getTrashMailbox(account)
902 }
903
904 return DeleteEmailFromMailbox(account, trashMailbox, uid)
905}
906
907// DeleteArchiveEmail deletes an email from archive (moves to trash)
908func DeleteArchiveEmail(account *config.Account, uid uint32) error {
909 c, err := connect(account)
910 if err != nil {
911 return err
912 }
913 defer c.Logout()
914
915 archiveMailbox, err := getMailboxByAttr(c, imap.AllAttr)
916 if err != nil {
917 archiveMailbox = getArchiveMailbox(account)
918 }
919
920 return DeleteEmailFromMailbox(account, archiveMailbox, uid)
921}