imap.go

  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}