1package imapclient
2
3import (
4 "bufio"
5 "crypto/tls"
6 "encoding/base64"
7 "fmt"
8 "hash"
9 "io"
10 "strings"
11 "time"
12
13 "github.com/mjl-/flate"
14
15 "github.com/mjl-/mox/mlog"
16 "github.com/mjl-/mox/moxio"
17 "github.com/mjl-/mox/scram"
18)
19
20// Capability requests a list of capabilities from the server. They are returned in
21// an UntaggedCapability response. The server also sends capabilities in initial
22// server greeting, in the response code.
23func (c *Conn) Capability() (untagged []Untagged, result Result, rerr error) {
24 defer c.recover(&rerr)
25 return c.Transactf("capability")
26}
27
28// Noop does nothing on its own, but a server will return any pending untagged
29// responses for new message delivery and changes to mailboxes.
30func (c *Conn) Noop() (untagged []Untagged, result Result, rerr error) {
31 defer c.recover(&rerr)
32 return c.Transactf("noop")
33}
34
35// Logout ends the IMAP session by writing a LOGOUT command. Close must still be
36// called on this client to close the socket.
37func (c *Conn) Logout() (untagged []Untagged, result Result, rerr error) {
38 defer c.recover(&rerr)
39 return c.Transactf("logout")
40}
41
42// Starttls enables TLS on the connection with the STARTTLS command.
43func (c *Conn) Starttls(config *tls.Config) (untagged []Untagged, result Result, rerr error) {
44 defer c.recover(&rerr)
45 untagged, result, rerr = c.Transactf("starttls")
46 c.xcheckf(rerr, "starttls command")
47
48 conn := c.xprefixConn()
49 tlsConn := tls.Client(conn, config)
50 err := tlsConn.Handshake()
51 c.xcheckf(err, "tls handshake")
52 c.conn = tlsConn
53 return untagged, result, nil
54}
55
56// Login authenticates with username and password
57func (c *Conn) Login(username, password string) (untagged []Untagged, result Result, rerr error) {
58 defer c.recover(&rerr)
59
60 c.LastTag = c.nextTag()
61 fmt.Fprintf(c.xbw, "%s login %s ", c.LastTag, astring(username))
62 defer c.xtrace(mlog.LevelTraceauth)()
63 fmt.Fprintf(c.xbw, "%s\r\n", astring(password))
64 c.xtrace(mlog.LevelTrace) // Restore.
65 return c.Response()
66}
67
68// Authenticate with plaintext password using AUTHENTICATE PLAIN.
69func (c *Conn) AuthenticatePlain(username, password string) (untagged []Untagged, result Result, rerr error) {
70 defer c.recover(&rerr)
71
72 err := c.Commandf("", "authenticate plain")
73 c.xcheckf(err, "writing authenticate command")
74 _, untagged, result, rerr = c.ReadContinuation()
75 c.xcheckf(rerr, "reading continuation")
76 if result.Status != "" {
77 c.xerrorf("got result status %q, expected continuation", result.Status)
78 }
79 defer c.xtrace(mlog.LevelTraceauth)()
80 xw := base64.NewEncoder(base64.StdEncoding, c.xbw)
81 fmt.Fprintf(xw, "\u0000%s\u0000%s", username, password)
82 xw.Close()
83 c.xtrace(mlog.LevelTrace) // Restore.
84 fmt.Fprintf(c.xbw, "\r\n")
85 c.xflush()
86 return c.Response()
87}
88
89// todo: implement cram-md5, write its credentials as traceauth.
90
91// Authenticate with SCRAM-SHA-256(-PLUS) or SCRAM-SHA-1(-PLUS). With SCRAM, the
92// password is not exchanged in plaintext form, but only derived hashes are
93// exchanged by both parties as proof of knowledge of password.
94//
95// The PLUS variants bind the authentication exchange to the TLS connection,
96// detecting MitM attacks.
97func (c *Conn) AuthenticateSCRAM(method string, h func() hash.Hash, username, password string) (untagged []Untagged, result Result, rerr error) {
98 defer c.recover(&rerr)
99
100 var cs *tls.ConnectionState
101 lmethod := strings.ToLower(method)
102 if strings.HasSuffix(lmethod, "-plus") {
103 tlsConn, ok := c.conn.(*tls.Conn)
104 if !ok {
105 c.xerrorf("cannot use scram plus without tls")
106 }
107 xcs := tlsConn.ConnectionState()
108 cs = &xcs
109 }
110 sc := scram.NewClient(h, username, "", false, cs)
111 clientFirst, err := sc.ClientFirst()
112 c.xcheckf(err, "scram clientFirst")
113 c.LastTag = c.nextTag()
114 err = c.Writelinef("%s authenticate %s %s", c.LastTag, method, base64.StdEncoding.EncodeToString([]byte(clientFirst)))
115 c.xcheckf(err, "writing command line")
116
117 xreadContinuation := func() []byte {
118 var line string
119 line, untagged, result, rerr = c.ReadContinuation()
120 c.xcheckf(err, "read continuation")
121 if result.Status != "" {
122 c.xerrorf("got result status %q, expected continuation", result.Status)
123 }
124 buf, err := base64.StdEncoding.DecodeString(line)
125 c.xcheckf(err, "parsing base64 from remote")
126 return buf
127 }
128
129 serverFirst := xreadContinuation()
130 clientFinal, err := sc.ServerFirst(serverFirst, password)
131 c.xcheckf(err, "scram clientFinal")
132 err = c.Writelinef("%s", base64.StdEncoding.EncodeToString([]byte(clientFinal)))
133 c.xcheckf(err, "write scram clientFinal")
134
135 serverFinal := xreadContinuation()
136 err = sc.ServerFinal(serverFinal)
137 c.xcheckf(err, "scram serverFinal")
138
139 // We must send a response to the server continuation line, but we have nothing to say. ../rfc/9051:6221
140 err = c.Writelinef("%s", base64.StdEncoding.EncodeToString(nil))
141 c.xcheckf(err, "scram client end")
142
143 return c.ResponseOK()
144}
145
146// CompressDeflate enables compression with deflate on the connection.
147//
148// Only possible when server has announced the COMPRESS=DEFLATE capability.
149//
150// State: Authenticated or selected.
151func (c *Conn) CompressDeflate() (untagged []Untagged, result Result, rerr error) {
152 defer c.recover(&rerr)
153
154 untagged, result, rerr = c.Transactf("compress deflate")
155 c.xcheck(rerr)
156
157 c.xflateBW = bufio.NewWriter(c)
158 fw0, err := flate.NewWriter(c.xflateBW, flate.DefaultCompression)
159 c.xcheckf(err, "deflate") // Cannot happen.
160 fw := moxio.NewFlateWriter(fw0)
161
162 c.compress = true
163 c.xflateWriter = fw
164 c.xtw = moxio.NewTraceWriter(mlog.New("imapclient", nil), "CW: ", fw)
165 c.xbw = bufio.NewWriter(c.xtw)
166
167 rc := c.xprefixConn()
168 fr := flate.NewReaderPartial(rc)
169 c.tr = moxio.NewTraceReader(mlog.New("imapclient", nil), "CR: ", fr)
170 c.br = bufio.NewReader(c.tr)
171
172 return
173}
174
175// Enable enables capabilities for use with the connection, verifying the server has indeed enabled them.
176func (c *Conn) Enable(capabilities ...string) (untagged []Untagged, result Result, rerr error) {
177 defer c.recover(&rerr)
178
179 untagged, result, rerr = c.Transactf("enable %s", strings.Join(capabilities, " "))
180 c.xcheck(rerr)
181 var enabled UntaggedEnabled
182 c.xgetUntagged(untagged, &enabled)
183 got := map[string]struct{}{}
184 for _, cap := range enabled {
185 got[cap] = struct{}{}
186 }
187 for _, cap := range capabilities {
188 if _, ok := got[cap]; !ok {
189 c.xerrorf("capability %q not enabled by server", cap)
190 }
191 }
192 return
193}
194
195// Select opens mailbox as active mailbox.
196func (c *Conn) Select(mailbox string) (untagged []Untagged, result Result, rerr error) {
197 defer c.recover(&rerr)
198 return c.Transactf("select %s", astring(mailbox))
199}
200
201// Examine opens mailbox as active mailbox read-only.
202func (c *Conn) Examine(mailbox string) (untagged []Untagged, result Result, rerr error) {
203 defer c.recover(&rerr)
204 return c.Transactf("examine %s", astring(mailbox))
205}
206
207// Create makes a new mailbox on the server.
208// SpecialUse can only be used on servers that announced the CREATE-SPECIAL-USE
209// capability. Specify flags like \Archive, \Drafts, \Junk, \Sent, \Trash, \All.
210func (c *Conn) Create(mailbox string, specialUse []string) (untagged []Untagged, result Result, rerr error) {
211 defer c.recover(&rerr)
212 if _, ok := c.CapAvailable[CapCreateSpecialUse]; !ok && len(specialUse) > 0 {
213 c.xerrorf("server does not implement create-special-use extension")
214 }
215 var useStr string
216 if len(specialUse) > 0 {
217 useStr = fmt.Sprintf(" USE (%s)", strings.Join(specialUse, " "))
218 }
219 return c.Transactf("create %s%s", astring(mailbox), useStr)
220}
221
222// Delete removes an entire mailbox and its messages.
223func (c *Conn) Delete(mailbox string) (untagged []Untagged, result Result, rerr error) {
224 defer c.recover(&rerr)
225 return c.Transactf("delete %s", astring(mailbox))
226}
227
228// Rename changes the name of a mailbox and all its child mailboxes.
229func (c *Conn) Rename(omailbox, nmailbox string) (untagged []Untagged, result Result, rerr error) {
230 defer c.recover(&rerr)
231 return c.Transactf("rename %s %s", astring(omailbox), astring(nmailbox))
232}
233
234// Subscribe marks a mailbox as subscribed. The mailbox does not have to exist. It
235// is not an error if the mailbox is already subscribed.
236func (c *Conn) Subscribe(mailbox string) (untagged []Untagged, result Result, rerr error) {
237 defer c.recover(&rerr)
238 return c.Transactf("subscribe %s", astring(mailbox))
239}
240
241// Unsubscribe marks a mailbox as unsubscribed.
242func (c *Conn) Unsubscribe(mailbox string) (untagged []Untagged, result Result, rerr error) {
243 defer c.recover(&rerr)
244 return c.Transactf("unsubscribe %s", astring(mailbox))
245}
246
247// List lists mailboxes with the basic LIST syntax.
248// Pattern can contain * (match any) or % (match any except hierarchy delimiter).
249func (c *Conn) List(pattern string) (untagged []Untagged, result Result, rerr error) {
250 defer c.recover(&rerr)
251 return c.Transactf(`list "" %s`, astring(pattern))
252}
253
254// ListFull lists mailboxes with the extended LIST syntax requesting all supported data.
255// Pattern can contain * (match any) or % (match any except hierarchy delimiter).
256func (c *Conn) ListFull(subscribedOnly bool, patterns ...string) (untagged []Untagged, result Result, rerr error) {
257 defer c.recover(&rerr)
258 var subscribedStr string
259 if subscribedOnly {
260 subscribedStr = "subscribed recursivematch"
261 }
262 for i, s := range patterns {
263 patterns[i] = astring(s)
264 }
265 return c.Transactf(`list (%s) "" (%s) return (subscribed children special-use status (messages uidnext uidvalidity unseen deleted size recent appendlimit))`, subscribedStr, strings.Join(patterns, " "))
266}
267
268// Namespace returns the hiearchy separator in an UntaggedNamespace response with personal/shared/other namespaces if present.
269func (c *Conn) Namespace() (untagged []Untagged, result Result, rerr error) {
270 defer c.recover(&rerr)
271 return c.Transactf("namespace")
272}
273
274// Status requests information about a mailbox, such as number of messages, size,
275// etc. At least one attribute required.
276func (c *Conn) Status(mailbox string, attrs ...StatusAttr) (untagged []Untagged, result Result, rerr error) {
277 defer c.recover(&rerr)
278 l := make([]string, len(attrs))
279 for i, a := range attrs {
280 l[i] = string(a)
281 }
282 return c.Transactf("status %s (%s)", astring(mailbox), strings.Join(l, " "))
283}
284
285// Append represents a parameter to the APPEND or REPLACE commands.
286type Append struct {
287 Flags []string
288 Received *time.Time
289 Size int64
290 Data io.Reader // Must return Size bytes.
291}
292
293// Append adds message to mailbox with flags and optional receive time.
294//
295// Multiple messages are only possible when the server has announced the
296// MULTIAPPEND capability.
297func (c *Conn) Append(mailbox string, message Append, more ...Append) (untagged []Untagged, result Result, rerr error) {
298 defer c.recover(&rerr)
299
300 if _, ok := c.CapAvailable[CapMultiAppend]; !ok && len(more) > 0 {
301 c.xerrorf("can only append multiple messages when server has announced MULTIAPPEND capability")
302 }
303
304 tag := c.nextTag()
305 c.LastTag = tag
306
307 fmt.Fprintf(c.xbw, "%s append %s", tag, astring(mailbox))
308
309 msgs := append([]Append{message}, more...)
310 for _, m := range msgs {
311 var date string
312 if m.Received != nil {
313 date = ` "` + m.Received.Format("_2-Jan-2006 15:04:05 -0700") + `"`
314 }
315
316 // todo: use literal8 if needed, with "UTF8()" if required.
317 // todo: for larger messages, use a synchronizing literal.
318
319 fmt.Fprintf(c.xbw, " (%s)%s {%d+}\r\n", strings.Join(m.Flags, " "), date, m.Size)
320 defer c.xtrace(mlog.LevelTracedata)()
321 _, err := io.Copy(c.xbw, m.Data)
322 c.xcheckf(err, "write message data")
323 c.xtrace(mlog.LevelTrace) // Restore
324 }
325
326 fmt.Fprintf(c.xbw, "\r\n")
327 c.xflush()
328 return c.Response()
329}
330
331// note: No idle command. Idle is better implemented by writing the request and reading and handling the responses as they come in.
332
333// CloseMailbox closes the currently selected/active mailbox, permanently removing
334// any messages marked with \Deleted.
335func (c *Conn) CloseMailbox() (untagged []Untagged, result Result, rerr error) {
336 return c.Transactf("close")
337}
338
339// Unselect closes the currently selected/active mailbox, but unlike CloseMailbox
340// does not permanently remove any messages marked with \Deleted.
341func (c *Conn) Unselect() (untagged []Untagged, result Result, rerr error) {
342 return c.Transactf("unselect")
343}
344
345// Expunge removes messages marked as deleted for the selected mailbox.
346func (c *Conn) Expunge() (untagged []Untagged, result Result, rerr error) {
347 defer c.recover(&rerr)
348 return c.Transactf("expunge")
349}
350
351// UIDExpunge is like expunge, but only removes messages matching uidSet.
352func (c *Conn) UIDExpunge(uidSet NumSet) (untagged []Untagged, result Result, rerr error) {
353 defer c.recover(&rerr)
354 return c.Transactf("uid expunge %s", uidSet.String())
355}
356
357// Note: No search, fetch command yet due to its large syntax.
358
359// StoreFlagsSet stores a new set of flags for messages from seqset with the STORE command.
360// If silent, no untagged responses with the updated flags will be sent by the server.
361func (c *Conn) StoreFlagsSet(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) {
362 defer c.recover(&rerr)
363 item := "flags"
364 if silent {
365 item += ".silent"
366 }
367 return c.Transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " "))
368}
369
370// StoreFlagsAdd is like StoreFlagsSet, but only adds flags, leaving current flags on the message intact.
371func (c *Conn) StoreFlagsAdd(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) {
372 defer c.recover(&rerr)
373 item := "+flags"
374 if silent {
375 item += ".silent"
376 }
377 return c.Transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " "))
378}
379
380// StoreFlagsClear is like StoreFlagsSet, but only removes flags, leaving other flags on the message intact.
381func (c *Conn) StoreFlagsClear(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) {
382 defer c.recover(&rerr)
383 item := "-flags"
384 if silent {
385 item += ".silent"
386 }
387 return c.Transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " "))
388}
389
390// Copy adds messages from the sequences in seqSet in the currently selected/active mailbox to dstMailbox.
391func (c *Conn) Copy(seqSet NumSet, dstMailbox string) (untagged []Untagged, result Result, rerr error) {
392 defer c.recover(&rerr)
393 return c.Transactf("copy %s %s", seqSet.String(), astring(dstMailbox))
394}
395
396// UIDCopy is like copy, but operates on UIDs.
397func (c *Conn) UIDCopy(uidSet NumSet, dstMailbox string) (untagged []Untagged, result Result, rerr error) {
398 defer c.recover(&rerr)
399 return c.Transactf("uid copy %s %s", uidSet.String(), astring(dstMailbox))
400}
401
402// Move moves messages from the sequences in seqSet in the currently selected/active mailbox to dstMailbox.
403func (c *Conn) Move(seqSet NumSet, dstMailbox string) (untagged []Untagged, result Result, rerr error) {
404 defer c.recover(&rerr)
405 return c.Transactf("move %s %s", seqSet.String(), astring(dstMailbox))
406}
407
408// UIDMove is like move, but operates on UIDs.
409func (c *Conn) UIDMove(uidSet NumSet, dstMailbox string) (untagged []Untagged, result Result, rerr error) {
410 defer c.recover(&rerr)
411 return c.Transactf("uid move %s %s", uidSet.String(), astring(dstMailbox))
412}
413
414// Replace replaces a message from the currently selected mailbox with a
415// new/different version of the message in the named mailbox, which may be the
416// same or different than the currently selected mailbox.
417//
418// Num is a message sequence number. "*" references the last message.
419//
420// Servers must have announced the REPLACE capability.
421func (c *Conn) Replace(msgseq string, mailbox string, msg Append) (untagged []Untagged, result Result, rerr error) {
422 // todo: parse msgseq, must be nznumber, with a known msgseq. or "*" with at least one message.
423 return c.replace("replace", msgseq, mailbox, msg)
424}
425
426// UIDReplace is like Replace, but operates on a UID instead of message
427// sequence number.
428func (c *Conn) UIDReplace(uid string, mailbox string, msg Append) (untagged []Untagged, result Result, rerr error) {
429 // todo: parse uid, must be nznumber, with a known uid. or "*" with at least one message.
430 return c.replace("uid replace", uid, mailbox, msg)
431}
432
433func (c *Conn) replace(cmd string, num string, mailbox string, msg Append) (untagged []Untagged, result Result, rerr error) {
434 defer c.recover(&rerr)
435
436 // todo: use synchronizing literal for larger messages.
437
438 var date string
439 if msg.Received != nil {
440 date = ` "` + msg.Received.Format("_2-Jan-2006 15:04:05 -0700") + `"`
441 }
442 // todo: only use literal8 if needed, possibly with "UTF8()"
443 // todo: encode mailbox
444 err := c.Commandf("", "%s %s %s (%s)%s ~{%d+}", cmd, num, astring(mailbox), strings.Join(msg.Flags, " "), date, msg.Size)
445 c.xcheckf(err, "writing replace command")
446
447 defer c.xtrace(mlog.LevelTracedata)()
448 _, err = io.Copy(c.xbw, msg.Data)
449 c.xcheckf(err, "write message data")
450 c.xtrace(mlog.LevelTrace)
451
452 fmt.Fprintf(c.xbw, "\r\n")
453 c.xflush()
454
455 return c.Response()
456}
457