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 References []string
86 Attachments []Attachment
87 AccountID string
88}
89
90// Attachment holds data for an email attachment.
91type Attachment struct {
92 Filename string
93 PartID string
94 Data []byte
95 Encoding string
96 MIMEType string
97 ContentID string
98 Inline bool
99 IsSMIMESignature bool
100 SMIMEVerified bool
101 IsSMIMEEncrypted bool
102 IsPGPSignature bool
103 PGPVerified bool
104 IsPGPEncrypted bool
105}
106
107// SearchQuery is the parsed form of a user query string.
108type SearchQuery struct {
109 Raw string
110 From string
111 To string
112 Subject string
113 Body string
114 Since time.Time
115 Before time.Time
116 LargerThan int
117 Limit uint32
118}
119
120// ParseSearchQuery parses a compact search DSL into a SearchQuery.
121func ParseSearchQuery(s string) SearchQuery {
122 query := SearchQuery{Raw: s}
123 var bodyTerms []string
124
125 for _, term := range tokenizeSearchQuery(s) {
126 key, value, ok := strings.Cut(term, ":")
127 if !ok || value == "" {
128 bodyTerms = append(bodyTerms, term)
129 continue
130 }
131
132 switch strings.ToLower(key) {
133 case "from":
134 query.From = value
135 case "to":
136 query.To = value
137 case "subject":
138 query.Subject = value
139 case "body":
140 query.Body = value
141 case "since":
142 if t, ok := parseSearchDate(value); ok {
143 query.Since = t
144 }
145 case "before":
146 if t, ok := parseSearchDate(value); ok {
147 query.Before = t
148 }
149 case "larger":
150 if n, err := strconv.Atoi(value); err == nil && n > 0 {
151 query.LargerThan = n
152 }
153 default:
154 bodyTerms = append(bodyTerms, term)
155 }
156 }
157
158 if query.Body == "" && len(bodyTerms) > 0 {
159 query.Body = strings.Join(bodyTerms, " ")
160 }
161
162 return query
163}
164
165func tokenizeSearchQuery(s string) []string {
166 var tokens []string
167 var b strings.Builder
168 var quote rune
169
170 for _, r := range s {
171 if quote != 0 {
172 if r == quote {
173 quote = 0
174 continue
175 }
176 b.WriteRune(r)
177 continue
178 }
179 if r == '"' || r == '\'' {
180 quote = r
181 continue
182 }
183 if unicode.IsSpace(r) {
184 if b.Len() > 0 {
185 tokens = append(tokens, b.String())
186 b.Reset()
187 }
188 continue
189 }
190 b.WriteRune(r)
191 }
192
193 if b.Len() > 0 {
194 tokens = append(tokens, b.String())
195 }
196
197 return tokens
198}
199
200func parseSearchDate(value string) (time.Time, bool) {
201 for _, layout := range []string{"2006-01-02", time.RFC3339} {
202 if t, err := time.Parse(layout, value); err == nil {
203 return t, true
204 }
205 }
206 return time.Time{}, false
207}
208
209// Folder represents a mailbox/folder.
210type Folder struct {
211 Name string
212 Delimiter string
213 Attributes []string
214}
215
216// OutgoingEmail contains everything needed to send an email.
217type OutgoingEmail struct {
218 To []string
219 Cc []string
220 Bcc []string
221 Subject string
222 PlainBody string
223 HTMLBody string
224 Images map[string][]byte
225 Attachments map[string][]byte
226 InReplyTo string
227 References []string
228 SignSMIME bool
229 EncryptSMIME bool
230 SignPGP bool
231 EncryptPGP bool
232}
233
234// NotifyType indicates the kind of notification event.
235type NotifyType int
236
237const (
238 NotifyNewEmail NotifyType = iota
239 NotifyExpunge
240 NotifyFlagChange
241)
242
243// NotifyEvent is emitted by Watch() when something changes in a mailbox.
244type NotifyEvent struct {
245 Type NotifyType
246 Folder string
247 AccountID string
248}
249
250// Capabilities describes what a backend supports.
251type Capabilities struct {
252 CanSend bool
253 CanMove bool
254 CanArchive bool
255 CanPush bool
256 CanSearchServer bool
257 CanFetchFolders bool
258 SupportsSMIME bool
259}