1// Package imap implements the backend.Provider interface by delegating
2// to the existing fetcher and sender packages.
3package imap
4
5import (
6 "context"
7 "log"
8
9 "github.com/floatpane/matcha/backend"
10 "github.com/floatpane/matcha/config"
11 "github.com/floatpane/matcha/fetcher"
12 "github.com/floatpane/matcha/sender"
13)
14
15func init() {
16 backend.RegisterBackend("imap", func(account *config.Account) (backend.Provider, error) {
17 return New(account)
18 })
19}
20
21// Provider wraps the existing fetcher/sender packages behind the backend.Provider interface.
22type Provider struct {
23 account *config.Account
24}
25
26// New creates a new IMAP provider.
27func New(account *config.Account) (*Provider, error) {
28 return &Provider{account: account}, nil
29}
30
31func (p *Provider) FetchEmails(_ context.Context, folder string, limit, offset uint32) ([]backend.Email, error) {
32 emails, err := fetcher.FetchMailboxEmails(p.account, folder, limit, offset)
33 if err != nil {
34 return nil, err
35 }
36 return toBackendEmails(emails), nil
37}
38
39func (p *Provider) FetchEmailBody(_ context.Context, folder string, uid uint32) (string, []backend.Attachment, error) {
40 body, atts, err := fetcher.FetchEmailBodyFromMailbox(p.account, folder, uid)
41 if err != nil {
42 return "", nil, err
43 }
44 return body, toBackendAttachments(atts), nil
45}
46
47func (p *Provider) FetchAttachment(_ context.Context, folder string, uid uint32, partID, encoding string) ([]byte, error) {
48 return fetcher.FetchAttachmentFromMailbox(p.account, folder, uid, partID, encoding)
49}
50
51func (p *Provider) MarkAsRead(_ context.Context, folder string, uid uint32) error {
52 return fetcher.MarkEmailAsReadInMailbox(p.account, folder, uid)
53}
54
55func (p *Provider) DeleteEmail(_ context.Context, folder string, uid uint32) error {
56 return fetcher.DeleteEmailFromMailbox(p.account, folder, uid)
57}
58
59func (p *Provider) ArchiveEmail(_ context.Context, folder string, uid uint32) error {
60 return fetcher.ArchiveEmailFromMailbox(p.account, folder, uid)
61}
62
63func (p *Provider) MoveEmail(_ context.Context, uid uint32, srcFolder, dstFolder string) error {
64 return fetcher.MoveEmailToFolder(p.account, uid, srcFolder, dstFolder)
65}
66
67func (p *Provider) DeleteEmails(_ context.Context, folder string, uids []uint32) error {
68 return fetcher.DeleteEmailsFromMailbox(p.account, folder, uids)
69}
70
71func (p *Provider) ArchiveEmails(_ context.Context, folder string, uids []uint32) error {
72 return fetcher.ArchiveEmailsFromMailbox(p.account, folder, uids)
73}
74
75func (p *Provider) MoveEmails(_ context.Context, uids []uint32, srcFolder, dstFolder string) error {
76 return fetcher.MoveEmailsToFolder(p.account, uids, srcFolder, dstFolder)
77}
78
79func (p *Provider) SendEmail(_ context.Context, msg *backend.OutgoingEmail) error {
80 rawMsg, err := sender.SendEmail(
81 p.account, msg.To, msg.Cc, msg.Bcc,
82 msg.Subject, msg.PlainBody, msg.HTMLBody,
83 msg.Images, msg.Attachments,
84 msg.InReplyTo, msg.References,
85 msg.SignSMIME, msg.EncryptSMIME,
86 msg.SignPGP, msg.EncryptPGP,
87 )
88 if err != nil {
89 return err
90 }
91
92 // Gmail automatically saves sent messages server-side; skip APPEND to avoid duplicates.
93 if p.account.ServiceProvider == "gmail" {
94 return nil
95 }
96
97 if err := fetcher.AppendToSentMailbox(p.account, rawMsg); err != nil {
98 log.Printf("Failed to append sent message to Sent folder: %v", err)
99 }
100
101 return nil
102}
103
104func (p *Provider) FetchFolders(_ context.Context) ([]backend.Folder, error) {
105 folders, err := fetcher.FetchFolders(p.account)
106 if err != nil {
107 return nil, err
108 }
109 return toBackendFolders(folders), nil
110}
111
112func (p *Provider) Watch(_ context.Context, _ string) (<-chan backend.NotifyEvent, func(), error) {
113 // IMAP IDLE is handled by the existing IdleWatcher in main.go
114 return nil, nil, backend.ErrNotSupported
115}
116
117func (p *Provider) Close() error {
118 return nil
119}
120
121// Verify interface compliance at compile time.
122var _ backend.Provider = (*Provider)(nil)
123
124// Conversion helpers
125
126func toBackendEmails(emails []fetcher.Email) []backend.Email {
127 result := make([]backend.Email, len(emails))
128 for i, e := range emails {
129 result[i] = backend.Email{
130 UID: e.UID,
131 From: e.From,
132 To: e.To,
133 ReplyTo: e.ReplyTo,
134 Subject: e.Subject,
135 Body: e.Body,
136 Date: e.Date,
137 IsRead: e.IsRead,
138 MessageID: e.MessageID,
139 References: e.References,
140 Attachments: toBackendAttachments(e.Attachments),
141 AccountID: e.AccountID,
142 }
143 }
144 return result
145}
146
147func toBackendAttachments(atts []fetcher.Attachment) []backend.Attachment {
148 result := make([]backend.Attachment, len(atts))
149 for i, a := range atts {
150 result[i] = backend.Attachment{
151 Filename: a.Filename,
152 PartID: a.PartID,
153 Data: a.Data,
154 Encoding: a.Encoding,
155 MIMEType: a.MIMEType,
156 ContentID: a.ContentID,
157 Inline: a.Inline,
158 IsSMIMESignature: a.IsSMIMESignature,
159 SMIMEVerified: a.SMIMEVerified,
160 IsSMIMEEncrypted: a.IsSMIMEEncrypted,
161 }
162 }
163 return result
164}
165
166func toBackendFolders(folders []fetcher.Folder) []backend.Folder {
167 result := make([]backend.Folder, len(folders))
168 for i, f := range folders {
169 result[i] = backend.Folder{
170 Name: f.Name,
171 Delimiter: f.Delimiter,
172 Attributes: f.Attributes,
173 }
174 }
175 return result
176}