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	Unread     uint32
217}
218
219// OutgoingEmail contains everything needed to send an email.
220type OutgoingEmail struct {
221	To           []string
222	Cc           []string
223	Bcc          []string
224	Subject      string
225	PlainBody    string
226	HTMLBody     string
227	Images       map[string][]byte
228	Attachments  map[string][]byte
229	InReplyTo    string
230	References   []string
231	SignSMIME    bool
232	EncryptSMIME bool
233	SignPGP      bool
234	EncryptPGP   bool
235}
236
237// NotifyType indicates the kind of notification event.
238type NotifyType int
239
240const (
241	NotifyNewEmail NotifyType = iota
242	NotifyExpunge
243	NotifyFlagChange
244)
245
246// NotifyEvent is emitted by Watch() when something changes in a mailbox.
247type NotifyEvent struct {
248	Type      NotifyType
249	Folder    string
250	AccountID string
251}
252
253// Capabilities describes what a backend supports.
254type Capabilities struct {
255	CanSend         bool
256	CanMove         bool
257	CanArchive      bool
258	CanPush         bool
259	CanSearchServer bool
260	CanFetchFolders bool
261	SupportsSMIME   bool
262}