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