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, string, []backend.Attachment, error) {
40 body, mimeType, atts, err := fetcher.FetchEmailBodyFromMailbox(p.account, folder, uid)
41 if err != nil {
42 return "", "", nil, err
43 }
44 return body, mimeType, 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) Search(_ context.Context, folder string, query backend.SearchQuery) ([]backend.Email, error) {
52 emails, err := fetcher.SearchMailbox(p.account, folder, query)
53 if err != nil {
54 return nil, err
55 }
56 return toBackendEmails(emails), nil
57}
58
59func (p *Provider) MarkAsRead(_ context.Context, folder string, uid uint32) error {
60 return fetcher.MarkEmailAsReadInMailbox(p.account, folder, uid)
61}
62
63func (p *Provider) MarkAsUnread(_ context.Context, folder string, uid uint32) error {
64 return fetcher.MarkEmailAsUnreadInMailbox(p.account, folder, uid)
65}
66
67func (p *Provider) DeleteEmail(_ context.Context, folder string, uid uint32) error {
68 return fetcher.DeleteEmailFromMailbox(p.account, folder, uid)
69}
70
71func (p *Provider) ArchiveEmail(_ context.Context, folder string, uid uint32) error {
72 return fetcher.ArchiveEmailFromMailbox(p.account, folder, uid)
73}
74
75func (p *Provider) MoveEmail(_ context.Context, uid uint32, srcFolder, dstFolder string) error {
76 return fetcher.MoveEmailToFolder(p.account, uid, srcFolder, dstFolder)
77}
78
79func (p *Provider) DeleteEmails(_ context.Context, folder string, uids []uint32) error {
80 return fetcher.DeleteEmailsFromMailbox(p.account, folder, uids)
81}
82
83func (p *Provider) ArchiveEmails(_ context.Context, folder string, uids []uint32) error {
84 return fetcher.ArchiveEmailsFromMailbox(p.account, folder, uids)
85}
86
87func (p *Provider) MoveEmails(_ context.Context, uids []uint32, srcFolder, dstFolder string) error {
88 return fetcher.MoveEmailsToFolder(p.account, uids, srcFolder, dstFolder)
89}
90
91func (p *Provider) SendEmail(_ context.Context, msg *backend.OutgoingEmail) error {
92 rawMsg, err := sender.SendEmail(
93 p.account, msg.To, msg.Cc, msg.Bcc,
94 msg.Subject, msg.PlainBody, msg.HTMLBody,
95 msg.Images, msg.Attachments,
96 msg.InReplyTo, msg.References,
97 msg.SignSMIME, msg.EncryptSMIME,
98 msg.SignPGP, msg.EncryptPGP,
99 )
100 if err != nil {
101 return err
102 }
103
104 // Gmail automatically saves sent messages server-side; skip APPEND to avoid duplicates.
105 if p.account.ServiceProvider == "gmail" {
106 return nil
107 }
108
109 if err := fetcher.AppendToSentMailbox(p.account, rawMsg); err != nil {
110 log.Printf("Failed to append sent message to Sent folder: %v", err)
111 }
112
113 return nil
114}
115
116func (p *Provider) FetchFolders(_ context.Context) ([]backend.Folder, error) {
117 folders, err := fetcher.FetchFolders(p.account)
118 if err != nil {
119 return nil, err
120 }
121 return toBackendFolders(folders), nil
122}
123
124func (p *Provider) Watch(_ context.Context, _ string) (<-chan backend.NotifyEvent, func(), error) {
125 // IMAP IDLE is handled by the existing IdleWatcher in main.go
126 return nil, nil, backend.ErrNotSupported
127}
128
129func (p *Provider) Close() error {
130 return nil
131}
132
133// Verify interface compliance at compile time.
134var _ backend.Provider = (*Provider)(nil)
135
136// Conversion helpers
137
138func toBackendEmails(emails []fetcher.Email) []backend.Email {
139 result := make([]backend.Email, len(emails))
140 for i, e := range emails {
141 result[i] = backend.Email{
142 UID: e.UID,
143 From: e.From,
144 To: e.To,
145 ReplyTo: e.ReplyTo,
146 Subject: e.Subject,
147 Body: e.Body,
148 Date: e.Date,
149 IsRead: e.IsRead,
150 MessageID: e.MessageID,
151 InReplyTo: e.InReplyTo,
152 References: e.References,
153 Attachments: toBackendAttachments(e.Attachments),
154 AccountID: e.AccountID,
155 }
156 }
157 return result
158}
159
160func toBackendAttachments(atts []fetcher.Attachment) []backend.Attachment {
161 result := make([]backend.Attachment, len(atts))
162 for i, a := range atts {
163 result[i] = backend.Attachment{
164 Filename: a.Filename,
165 PartID: a.PartID,
166 Data: a.Data,
167 Encoding: a.Encoding,
168 MIMEType: a.MIMEType,
169 ContentID: a.ContentID,
170 Inline: a.Inline,
171 IsSMIMESignature: a.IsSMIMESignature,
172 SMIMEVerified: a.SMIMEVerified,
173 IsSMIMEEncrypted: a.IsSMIMEEncrypted,
174 }
175 }
176 return result
177}
178
179func toBackendFolders(folders []fetcher.Folder) []backend.Folder {
180 result := make([]backend.Folder, len(folders))
181 for i, f := range folders {
182 result[i] = backend.Folder{
183 Name: f.Name,
184 Delimiter: f.Delimiter,
185 Attributes: f.Attributes,
186 Unread: f.Unread,
187 }
188 }
189 return result
190}