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	InReplyTo   string
 86	References  []string
 87	Attachments []Attachment
 88	AccountID   string
 89}
 90
 91// Attachment holds data for an email attachment.
 92type Attachment struct {
 93	Filename         string
 94	PartID           string
 95	Data             []byte
 96	Encoding         string
 97	MIMEType         string
 98	ContentID        string
 99	Inline           bool
100	IsSMIMESignature bool
101	SMIMEVerified    bool
102	IsSMIMEEncrypted bool
103	IsPGPSignature   bool
104	PGPVerified      bool
105	IsPGPEncrypted   bool
106}
107
108// SearchQuery is the parsed form of a user query string.
109type SearchQuery struct {
110	Raw        string
111	From       string
112	To         string
113	Subject    string
114	Body       string
115	Since      time.Time
116	Before     time.Time
117	LargerThan int
118	Limit      uint32
119}
120
121// ParseSearchQuery parses a compact search DSL into a SearchQuery.
122func ParseSearchQuery(s string) SearchQuery {
123	query := SearchQuery{Raw: s}
124	var bodyTerms []string
125
126	for _, term := range tokenizeSearchQuery(s) {
127		key, value, ok := strings.Cut(term, ":")
128		if !ok || value == "" {
129			bodyTerms = append(bodyTerms, term)
130			continue
131		}
132
133		switch strings.ToLower(key) {
134		case "from":
135			query.From = value
136		case "to":
137			query.To = value
138		case "subject":
139			query.Subject = value
140		case "body":
141			query.Body = value
142		case "since":
143			if t, ok := parseSearchDate(value); ok {
144				query.Since = t
145			}
146		case "before":
147			if t, ok := parseSearchDate(value); ok {
148				query.Before = t
149			}
150		case "larger":
151			if n, err := strconv.Atoi(value); err == nil && n > 0 {
152				query.LargerThan = n
153			}
154		default:
155			bodyTerms = append(bodyTerms, term)
156		}
157	}
158
159	if query.Body == "" && len(bodyTerms) > 0 {
160		query.Body = strings.Join(bodyTerms, " ")
161	}
162
163	return query
164}
165
166func tokenizeSearchQuery(s string) []string {
167	var tokens []string
168	var b strings.Builder
169	var quote rune
170
171	for _, r := range s {
172		if quote != 0 {
173			if r == quote {
174				quote = 0
175				continue
176			}
177			b.WriteRune(r)
178			continue
179		}
180		if r == '"' || r == '\'' {
181			quote = r
182			continue
183		}
184		if unicode.IsSpace(r) {
185			if b.Len() > 0 {
186				tokens = append(tokens, b.String())
187				b.Reset()
188			}
189			continue
190		}
191		b.WriteRune(r)
192	}
193
194	if b.Len() > 0 {
195		tokens = append(tokens, b.String())
196	}
197
198	return tokens
199}
200
201func parseSearchDate(value string) (time.Time, bool) {
202	for _, layout := range []string{"2006-01-02", time.RFC3339} {
203		if t, err := time.Parse(layout, value); err == nil {
204			return t, true
205		}
206	}
207	return time.Time{}, false
208}
209
210// Folder represents a mailbox/folder.
211type Folder struct {
212	Name       string
213	Delimiter  string
214	Attributes []string
215}
216
217// OutgoingEmail contains everything needed to send an email.
218type OutgoingEmail struct {
219	To           []string
220	Cc           []string
221	Bcc          []string
222	Subject      string
223	PlainBody    string
224	HTMLBody     string
225	Images       map[string][]byte
226	Attachments  map[string][]byte
227	InReplyTo    string
228	References   []string
229	SignSMIME    bool
230	EncryptSMIME bool
231	SignPGP      bool
232	EncryptPGP   bool
233}
234
235// NotifyType indicates the kind of notification event.
236type NotifyType int
237
238const (
239	NotifyNewEmail NotifyType = iota
240	NotifyExpunge
241	NotifyFlagChange
242)
243
244// NotifyEvent is emitted by Watch() when something changes in a mailbox.
245type NotifyEvent struct {
246	Type      NotifyType
247	Folder    string
248	AccountID string
249}
250
251// Capabilities describes what a backend supports.
252type Capabilities struct {
253	CanSend         bool
254	CanMove         bool
255	CanArchive      bool
256	CanPush         bool
257	CanSearchServer bool
258	CanFetchFolders bool
259	SupportsSMIME   bool
260}