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