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