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