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