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, []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}