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