backend.go

  1// Package backend defines the Provider interface for multi-protocol email support.
  2package backend
  3
  4import (
  5	"context"
  6	"errors"
  7	"strconv"
  8	"strings"
  9	"time"
 10	"unicode"
 11)
 12
 13// ErrNotSupported is returned when a provider does not support an operation.
 14var ErrNotSupported = errors.New("operation not supported by this provider")
 15
 16// Provider is the unified interface that all email backends must implement.
 17type Provider interface {
 18	EmailReader
 19	EmailWriter
 20	EmailSender
 21	EmailSearcher
 22	FolderManager
 23	Notifier
 24	Close() error
 25}
 26
 27// EmailReader fetches emails and their content.
 28type EmailReader interface {
 29	FetchEmails(ctx context.Context, folder string, limit, offset uint32) ([]Email, error)
 30	// FetchEmailBody returns the chosen body, its MIME type ("text/html" or
 31	// "text/plain"; empty when unknown), parsed attachments, and any error.
 32	FetchEmailBody(ctx context.Context, folder string, uid uint32) (string, string, []Attachment, error)
 33	FetchAttachment(ctx context.Context, folder string, uid uint32, partID, encoding string) ([]byte, error)
 34}
 35
 36// EmailWriter modifies email state.
 37type EmailWriter interface {
 38	MarkAsRead(ctx context.Context, folder string, uid uint32) error
 39	DeleteEmail(ctx context.Context, folder string, uid uint32) error
 40	ArchiveEmail(ctx context.Context, folder string, uid uint32) error
 41	MoveEmail(ctx context.Context, uid uint32, srcFolder, dstFolder string) error
 42
 43	// Batch operations
 44	DeleteEmails(ctx context.Context, folder string, uids []uint32) error
 45	ArchiveEmails(ctx context.Context, folder string, uids []uint32) error
 46	MoveEmails(ctx context.Context, uids []uint32, srcFolder, dstFolder string) error
 47}
 48
 49// EmailSender sends outgoing email.
 50type EmailSender interface {
 51	SendEmail(ctx context.Context, msg *OutgoingEmail) error
 52}
 53
 54// EmailSearcher searches emails server-side.
 55type EmailSearcher interface {
 56	Search(ctx context.Context, folder string, query SearchQuery) ([]Email, error)
 57}
 58
 59// FolderManager lists folders/mailboxes.
 60type FolderManager interface {
 61	FetchFolders(ctx context.Context) ([]Folder, error)
 62}
 63
 64// Notifier provides real-time notifications for new email.
 65type Notifier interface {
 66	Watch(ctx context.Context, folder string) (<-chan NotifyEvent, func(), error)
 67}
 68
 69// CapabilityProvider optionally reports what a backend can do.
 70type CapabilityProvider interface {
 71	Capabilities() Capabilities
 72}
 73
 74// Email represents a single email message.
 75type Email struct {
 76	UID         uint32
 77	From        string
 78	To          []string
 79	ReplyTo     []string
 80	Subject     string
 81	Body        string
 82	Date        time.Time
 83	IsRead      bool
 84	MessageID   string
 85	References  []string
 86	Attachments []Attachment
 87	AccountID   string
 88}
 89
 90// Attachment holds data for an email attachment.
 91type Attachment struct {
 92	Filename         string
 93	PartID           string
 94	Data             []byte
 95	Encoding         string
 96	MIMEType         string
 97	ContentID        string
 98	Inline           bool
 99	IsSMIMESignature bool
100	SMIMEVerified    bool
101	IsSMIMEEncrypted bool
102	IsPGPSignature   bool
103	PGPVerified      bool
104	IsPGPEncrypted   bool
105}
106
107// SearchQuery is the parsed form of a user query string.
108type SearchQuery struct {
109	Raw        string
110	From       string
111	To         string
112	Subject    string
113	Body       string
114	Since      time.Time
115	Before     time.Time
116	LargerThan int
117	Limit      uint32
118}
119
120// ParseSearchQuery parses a compact search DSL into a SearchQuery.
121func ParseSearchQuery(s string) SearchQuery {
122	query := SearchQuery{Raw: s}
123	var bodyTerms []string
124
125	for _, term := range tokenizeSearchQuery(s) {
126		key, value, ok := strings.Cut(term, ":")
127		if !ok || value == "" {
128			bodyTerms = append(bodyTerms, term)
129			continue
130		}
131
132		switch strings.ToLower(key) {
133		case "from":
134			query.From = value
135		case "to":
136			query.To = value
137		case "subject":
138			query.Subject = value
139		case "body":
140			query.Body = value
141		case "since":
142			if t, ok := parseSearchDate(value); ok {
143				query.Since = t
144			}
145		case "before":
146			if t, ok := parseSearchDate(value); ok {
147				query.Before = t
148			}
149		case "larger":
150			if n, err := strconv.Atoi(value); err == nil && n > 0 {
151				query.LargerThan = n
152			}
153		default:
154			bodyTerms = append(bodyTerms, term)
155		}
156	}
157
158	if query.Body == "" && len(bodyTerms) > 0 {
159		query.Body = strings.Join(bodyTerms, " ")
160	}
161
162	return query
163}
164
165func tokenizeSearchQuery(s string) []string {
166	var tokens []string
167	var b strings.Builder
168	var quote rune
169
170	for _, r := range s {
171		if quote != 0 {
172			if r == quote {
173				quote = 0
174				continue
175			}
176			b.WriteRune(r)
177			continue
178		}
179		if r == '"' || r == '\'' {
180			quote = r
181			continue
182		}
183		if unicode.IsSpace(r) {
184			if b.Len() > 0 {
185				tokens = append(tokens, b.String())
186				b.Reset()
187			}
188			continue
189		}
190		b.WriteRune(r)
191	}
192
193	if b.Len() > 0 {
194		tokens = append(tokens, b.String())
195	}
196
197	return tokens
198}
199
200func parseSearchDate(value string) (time.Time, bool) {
201	for _, layout := range []string{"2006-01-02", time.RFC3339} {
202		if t, err := time.Parse(layout, value); err == nil {
203			return t, true
204		}
205	}
206	return time.Time{}, false
207}
208
209// Folder represents a mailbox/folder.
210type Folder struct {
211	Name       string
212	Delimiter  string
213	Attributes []string
214}
215
216// OutgoingEmail contains everything needed to send an email.
217type OutgoingEmail struct {
218	To           []string
219	Cc           []string
220	Bcc          []string
221	Subject      string
222	PlainBody    string
223	HTMLBody     string
224	Images       map[string][]byte
225	Attachments  map[string][]byte
226	InReplyTo    string
227	References   []string
228	SignSMIME    bool
229	EncryptSMIME bool
230	SignPGP      bool
231	EncryptPGP   bool
232}
233
234// NotifyType indicates the kind of notification event.
235type NotifyType int
236
237const (
238	NotifyNewEmail NotifyType = iota
239	NotifyExpunge
240	NotifyFlagChange
241)
242
243// NotifyEvent is emitted by Watch() when something changes in a mailbox.
244type NotifyEvent struct {
245	Type      NotifyType
246	Folder    string
247	AccountID string
248}
249
250// Capabilities describes what a backend supports.
251type Capabilities struct {
252	CanSend         bool
253	CanMove         bool
254	CanArchive      bool
255	CanPush         bool
256	CanSearchServer bool
257	CanFetchFolders bool
258	SupportsSMIME   bool
259}