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}