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}