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}