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) 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}