13 "github.com/mjl-/flate"
15 "github.com/mjl-/mox/mlog"
16 "github.com/mjl-/mox/moxio"
17 "github.com/mjl-/mox/scram"
20// Capability writes the IMAP4 "CAPABILITY" command, requesting a list of
21// capabilities from the server. They are returned in an UntaggedCapability
22// response. The server also sends capabilities in initial server greeting, in the
24func (c *Conn) Capability() (resp Response, rerr error) {
25 defer c.recover(&rerr, &resp)
26 return c.transactf("capability")
29// Noop writes the IMAP4 "NOOP" command, which does nothing on its own, but a
30// server will return any pending untagged responses for new message delivery and
31// changes to mailboxes.
32func (c *Conn) Noop() (resp Response, rerr error) {
33 defer c.recover(&rerr, &resp)
34 return c.transactf("noop")
37// Logout ends the IMAP4 session by writing an IMAP "LOGOUT" command. [Conn.Close]
38// must still be called on this client to close the socket.
39func (c *Conn) Logout() (resp Response, rerr error) {
40 defer c.recover(&rerr, &resp)
41 return c.transactf("logout")
44// StartTLS enables TLS on the connection with the IMAP4 "STARTTLS" command.
45func (c *Conn) StartTLS(config *tls.Config) (resp Response, rerr error) {
46 defer c.recover(&rerr, &resp)
47 resp, rerr = c.transactf("starttls")
48 c.xcheckf(rerr, "starttls command")
50 conn := c.xprefixConn()
51 tlsConn := tls.Client(conn, config)
52 err := tlsConn.Handshake()
53 c.xcheckf(err, "tls handshake")
58// Login authenticates using the IMAP4 "LOGIN" command, sending the plain text
59// password to the server.
61// Authentication is not allowed while the "LOGINDISABLED" capability is announced.
62// Call [Conn.StartTLS] first.
64// See [Conn.AuthenticateSCRAM] for a better authentication mechanism.
65func (c *Conn) Login(username, password string) (resp Response, rerr error) {
66 defer c.recover(&rerr, &resp)
68 fmt.Fprintf(c.xbw, "%s login %s ", c.nextTag(), astring(username))
69 defer c.xtracewrite(mlog.LevelTraceauth)()
70 fmt.Fprintf(c.xbw, "%s\r\n", astring(password))
71 c.xtracewrite(mlog.LevelTrace) // Restore.
75// AuthenticatePlain executes the AUTHENTICATE command with SASL mechanism "PLAIN",
76// sending the password in plain text password to the server.
78// Required capability: "AUTH=PLAIN"
80// Authentication is not allowed while the "LOGINDISABLED" capability is announced.
81// Call [Conn.StartTLS] first.
83// See [Conn.AuthenticateSCRAM] for a better authentication mechanism.
84func (c *Conn) AuthenticatePlain(username, password string) (resp Response, rerr error) {
85 defer c.recover(&rerr, &resp)
87 err := c.WriteCommandf("", "authenticate plain")
88 c.xcheckf(err, "writing authenticate command")
89 _, rerr = c.readContinuation()
90 c.xresponse(rerr, &resp)
92 defer c.xtracewrite(mlog.LevelTraceauth)()
93 xw := base64.NewEncoder(base64.StdEncoding, c.xbw)
94 fmt.Fprintf(xw, "\u0000%s\u0000%s", username, password)
96 c.xtracewrite(mlog.LevelTrace) // Restore.
97 fmt.Fprintf(c.xbw, "\r\n")
102// todo: implement cram-md5, write its credentials as traceauth.
104// AuthenticateSCRAM executes the IMAP4 "AUTHENTICATE" command with one of the
105// following SASL mechanisms: SCRAM-SHA-256(-PLUS) or SCRAM-SHA-1(-PLUS).//
107// With SCRAM, the password is not sent to the server in plain text, but only
108// derived hashes are exchanged by both parties as proof of knowledge of password.
110// Authentication is not allowed while the "LOGINDISABLED" capability is announced.
111// Call [Conn.StartTLS] first.
113// Required capability: SCRAM-SHA-256-PLUS, SCRAM-SHA-256, SCRAM-SHA-1-PLUS,
116// The PLUS variants bind the authentication exchange to the TLS connection,
117// detecting MitM attacks.
118func (c *Conn) AuthenticateSCRAM(mechanism string, h func() hash.Hash, username, password string) (resp Response, rerr error) {
119 defer c.recover(&rerr, &resp)
121 var cs *tls.ConnectionState
122 lmech := strings.ToLower(mechanism)
123 if strings.HasSuffix(lmech, "-plus") {
124 tlsConn, ok := c.conn.(*tls.Conn)
126 c.xerrorf("cannot use scram plus without tls")
128 xcs := tlsConn.ConnectionState()
131 sc := scram.NewClient(h, username, "", false, cs)
132 clientFirst, err := sc.ClientFirst()
133 c.xcheckf(err, "scram clientFirst")
134 // todo: only send clientFirst if server has announced SASL-IR
135 err = c.Writelinef("%s authenticate %s %s", c.nextTag(), mechanism, base64.StdEncoding.EncodeToString([]byte(clientFirst)))
136 c.xcheckf(err, "writing command line")
138 xreadContinuation := func() []byte {
140 line, rerr = c.readContinuation()
141 c.xresponse(rerr, &resp)
142 buf, err := base64.StdEncoding.DecodeString(line)
143 c.xcheckf(err, "parsing base64 from remote")
147 serverFirst := xreadContinuation()
148 clientFinal, err := sc.ServerFirst(serverFirst, password)
149 c.xcheckf(err, "scram clientFinal")
150 err = c.Writelinef("%s", base64.StdEncoding.EncodeToString([]byte(clientFinal)))
151 c.xcheckf(err, "write scram clientFinal")
153 serverFinal := xreadContinuation()
154 err = sc.ServerFinal(serverFinal)
155 c.xcheckf(err, "scram serverFinal")
157 // We must send a response to the server continuation line, but we have nothing to say.
../rfc/9051:6221
158 err = c.Writelinef("%s", base64.StdEncoding.EncodeToString(nil))
159 c.xcheckf(err, "scram client end")
161 return c.responseOK()
164// CompressDeflate enables compression with deflate on the connection by executing
165// the IMAP4 "COMPRESS=DEFAULT" command.
167// Required capability: "COMPRESS=DEFLATE".
169// State: Authenticated or selected.
170func (c *Conn) CompressDeflate() (resp Response, rerr error) {
171 defer c.recover(&rerr, &resp)
173 resp, rerr = c.transactf("compress deflate")
176 c.xflateBW = bufio.NewWriter(c)
177 fw0, err := flate.NewWriter(c.xflateBW, flate.DefaultCompression)
178 c.xcheckf(err, "deflate") // Cannot happen.
179 fw := moxio.NewFlateWriter(fw0)
183 c.xtw = moxio.NewTraceWriter(mlog.New("imapclient", nil), "CW: ", fw)
184 c.xbw = bufio.NewWriter(c.xtw)
186 rc := c.xprefixConn()
187 fr := flate.NewReaderPartial(rc)
188 c.tr = moxio.NewTraceReader(mlog.New("imapclient", nil), "CR: ", fr)
189 c.br = bufio.NewReader(c.tr)
194// Enable enables capabilities for use with the connection by executing the IMAP4 "ENABLE" command.
196// Required capability: "ENABLE" or "IMAP4rev2"
197func (c *Conn) Enable(capabilities ...Capability) (resp Response, rerr error) {
198 defer c.recover(&rerr, &resp)
200 var caps strings.Builder
201 for _, c := range capabilities {
202 caps.WriteString(" " + string(c))
204 return c.transactf("enable%s", caps.String())
207// Select opens the mailbox with the IMAP4 "SELECT" command.
209// If a mailbox is selected/active, it is automatically deselected before
210// selecting the mailbox, without permanently removing ("expunging") messages
213// If the mailbox cannot be opened, the connection is left in Authenticated state,
215func (c *Conn) Select(mailbox string) (resp Response, rerr error) {
216 defer c.recover(&rerr, &resp)
217 return c.transactf("select %s", astring(mailbox))
220// Examine opens the mailbox like [Conn.Select], but read-only, with the IMAP4
222func (c *Conn) Examine(mailbox string) (resp Response, rerr error) {
223 defer c.recover(&rerr, &resp)
224 return c.transactf("examine %s", astring(mailbox))
227// Create makes a new mailbox on the server using the IMAP4 "CREATE" command.
229// SpecialUse can only be used on servers that announced the "CREATE-SPECIAL-USE"
230// capability. Specify flags like \Archive, \Drafts, \Junk, \Sent, \Trash, \All.
231func (c *Conn) Create(mailbox string, specialUse []string) (resp Response, rerr error) {
232 defer c.recover(&rerr, &resp)
234 if len(specialUse) > 0 {
235 useStr = fmt.Sprintf(" USE (%s)", strings.Join(specialUse, " "))
237 return c.transactf("create %s%s", astring(mailbox), useStr)
240// Delete removes an entire mailbox and its messages using the IMAP4 "DELETE"
242func (c *Conn) Delete(mailbox string) (resp Response, rerr error) {
243 defer c.recover(&rerr, &resp)
244 return c.transactf("delete %s", astring(mailbox))
247// Rename changes the name of a mailbox and all its child mailboxes
248// using the IMAP4 "RENAME" command.
249func (c *Conn) Rename(omailbox, nmailbox string) (resp Response, rerr error) {
250 defer c.recover(&rerr, &resp)
251 return c.transactf("rename %s %s", astring(omailbox), astring(nmailbox))
254// Subscribe marks a mailbox as subscribed using the IMAP4 "SUBSCRIBE" command.
256// The mailbox does not have to exist. It is not an error if the mailbox is already
258func (c *Conn) Subscribe(mailbox string) (resp Response, rerr error) {
259 defer c.recover(&rerr, &resp)
260 return c.transactf("subscribe %s", astring(mailbox))
263// Unsubscribe marks a mailbox as unsubscribed using the IMAP4 "UNSUBSCRIBE"
265func (c *Conn) Unsubscribe(mailbox string) (resp Response, rerr error) {
266 defer c.recover(&rerr, &resp)
267 return c.transactf("unsubscribe %s", astring(mailbox))
270// List lists mailboxes using the IMAP4 "LIST" command with the basic LIST syntax.
271// Pattern can contain * (match any) or % (match any except hierarchy delimiter).
272func (c *Conn) List(pattern string) (resp Response, rerr error) {
273 defer c.recover(&rerr, &resp)
274 return c.transactf(`list "" %s`, astring(pattern))
277// ListFull lists mailboxes using the LIST command with the extended LIST
278// syntax requesting all supported data.
280// Required capability: "LIST-EXTENDED". If "IMAP4rev2" is announced, the command
281// is also available but only with a single pattern.
283// Pattern can contain * (match any) or % (match any except hierarchy delimiter).
284func (c *Conn) ListFull(subscribedOnly bool, patterns ...string) (resp Response, rerr error) {
285 defer c.recover(&rerr, &resp)
286 var subscribedStr string
288 subscribedStr = "subscribed recursivematch"
290 for i, s := range patterns {
291 patterns[i] = astring(s)
293 return c.transactf(`list (%s) "" (%s) return (subscribed children special-use status (messages uidnext uidvalidity unseen deleted size recent appendlimit))`, subscribedStr, strings.Join(patterns, " "))
296// Namespace requests the hiearchy separator using the IMAP4 "NAMESPACE" command.
298// Required capability: "NAMESPACE" or "IMAP4rev2".
300// Server will return an UntaggedNamespace response with personal/shared/other
301// namespaces if present.
302func (c *Conn) Namespace() (resp Response, rerr error) {
303 defer c.recover(&rerr, &resp)
304 return c.transactf("namespace")
307// Status requests information about a mailbox using the IMAP4 "STATUS" command. For
308// example, number of messages, size, etc. At least one attribute required.
309func (c *Conn) Status(mailbox string, attrs ...StatusAttr) (resp Response, rerr error) {
310 defer c.recover(&rerr, &resp)
311 l := make([]string, len(attrs))
312 for i, a := range attrs {
315 return c.transactf("status %s (%s)", astring(mailbox), strings.Join(l, " "))
318// Append represents a parameter to the IMAP4 "APPEND" or "REPLACE" commands, for
319// adding a message to mailbox, or replacing a message with a new version in a
322 Flags []string // Optional, flags for the new message.
323 Received *time.Time // Optional, the INTERNALDATE field, typically time at which a message was received.
325 Data io.Reader // Required, must return Size bytes.
328// Append adds message to mailbox with flags and optional receive time using the
329// IMAP4 "APPEND" command.
330func (c *Conn) Append(mailbox string, message Append) (resp Response, rerr error) {
331 return c.MultiAppend(mailbox, message)
334// MultiAppend atomatically adds multiple messages to the mailbox.
336// Required capability: "MULTIAPPEND"
337func (c *Conn) MultiAppend(mailbox string, message Append, more ...Append) (resp Response, rerr error) {
338 defer c.recover(&rerr, &resp)
340 fmt.Fprintf(c.xbw, "%s append %s", c.nextTag(), astring(mailbox))
342 msgs := append([]Append{message}, more...)
343 for _, m := range msgs {
345 if m.Received != nil {
346 date = ` "` + m.Received.Format("_2-Jan-2006 15:04:05 -0700") + `"`
349 // todo: use literal8 if needed, with "UTF8()" if required.
350 // todo: for larger messages, use a synchronizing literal.
352 fmt.Fprintf(c.xbw, " (%s)%s {%d+}\r\n", strings.Join(m.Flags, " "), date, m.Size)
353 defer c.xtracewrite(mlog.LevelTracedata)()
354 _, err := io.Copy(c.xbw, m.Data)
355 c.xcheckf(err, "write message data")
356 c.xtracewrite(mlog.LevelTrace) // Restore
359 fmt.Fprintf(c.xbw, "\r\n")
361 return c.responseOK()
364// note: No Idle or Notify command. Idle/Notify is better implemented by
365// writing the request and reading and handling the responses as they come in.
367// CloseMailbox closes the selected/active mailbox using the IMAP4 "CLOSE" command,
368// permanently removing ("expunging") any messages marked with \Deleted.
370// See [Conn.Unselect] for closing a mailbox without permanently removing messages.
371func (c *Conn) CloseMailbox() (resp Response, rerr error) {
372 return c.transactf("close")
375// Unselect closes the selected/active mailbox using the IMAP4 "UNSELECT" command,
376// but unlike MailboxClose does not permanently remove ("expunge") any messages
377// marked with \Deleted.
379// Required capability: "UNSELECT" or "IMAP4rev2".
381// If Unselect is not available, call [Conn.Select] with a non-existent mailbox for
382// the same effect: Deselecting a mailbox without permanently removing messages
384func (c *Conn) Unselect() (resp Response, rerr error) {
385 return c.transactf("unselect")
388// Expunge removes all messages marked as deleted for the selected mailbox using
389// the IMAP4 "EXPUNGE" command. If other sessions marked messages as deleted, even
390// if they aren't visible in the session, they are removed as well.
392// UIDExpunge gives more control over which the messages that are removed.
393func (c *Conn) Expunge() (resp Response, rerr error) {
394 defer c.recover(&rerr, &resp)
395 return c.transactf("expunge")
398// UIDExpunge is like expunge, but only removes messages matching UID set, using
399// the IMAP4 "UID EXPUNGE" command.
401// Required capability: "UIDPLUS" or "IMAP4rev2".
402func (c *Conn) UIDExpunge(uidSet NumSet) (resp Response, rerr error) {
403 defer c.recover(&rerr, &resp)
404 return c.transactf("uid expunge %s", uidSet.String())
407// Note: No search, fetch command yet due to its large syntax.
409// MSNStoreFlagsSet stores a new set of flags for messages matching message
410// sequence numbers (MSNs) from sequence set with the IMAP4 "STORE" command.
412// If silent, no untagged responses with the updated flags will be sent by the
415// Method [Conn.UIDStoreFlagsSet], which operates on a uid set, should be
417func (c *Conn) MSNStoreFlagsSet(seqset string, silent bool, flags ...string) (resp Response, rerr error) {
418 defer c.recover(&rerr, &resp)
423 return c.transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " "))
426// MSNStoreFlagsAdd is like [Conn.MSNStoreFlagsSet], but only adds flags, leaving
427// current flags on the message intact.
429// Method [Conn.UIDStoreFlagsAdd], which operates on a uid set, should be
431func (c *Conn) MSNStoreFlagsAdd(seqset string, silent bool, flags ...string) (resp Response, rerr error) {
432 defer c.recover(&rerr, &resp)
437 return c.transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " "))
440// MSNStoreFlagsClear is like [Conn.MSNStoreFlagsSet], but only removes flags,
441// leaving other flags on the message intact.
443// Method [Conn.UIDStoreFlagsClear], which operates on a uid set, should be
445func (c *Conn) MSNStoreFlagsClear(seqset string, silent bool, flags ...string) (resp Response, rerr error) {
446 defer c.recover(&rerr, &resp)
451 return c.transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " "))
454// UIDStoreFlagsSet stores a new set of flags for messages matching UIDs from
455// uidSet with the IMAP4 "UID STORE" command.
457// If silent, no untagged responses with the updated flags will be sent by the
460// Required capability: "UIDPLUS" or "IMAP4rev2".
461func (c *Conn) UIDStoreFlagsSet(uidSet string, silent bool, flags ...string) (resp Response, rerr error) {
462 defer c.recover(&rerr, &resp)
467 return c.transactf("uid store %s %s (%s)", uidSet, item, strings.Join(flags, " "))
470// UIDStoreFlagsAdd is like UIDStoreFlagsSet, but only adds flags, leaving
471// current flags on the message intact.
473// Required capability: "UIDPLUS" or "IMAP4rev2".
474func (c *Conn) UIDStoreFlagsAdd(uidSet string, silent bool, flags ...string) (resp Response, rerr error) {
475 defer c.recover(&rerr, &resp)
480 return c.transactf("uid store %s %s (%s)", uidSet, item, strings.Join(flags, " "))
483// UIDStoreFlagsClear is like UIDStoreFlagsSet, but only removes flags, leaving
484// other flags on the message intact.
486// Required capability: "UIDPLUS" or "IMAP4rev2".
487func (c *Conn) UIDStoreFlagsClear(uidSet string, silent bool, flags ...string) (resp Response, rerr error) {
488 defer c.recover(&rerr, &resp)
493 return c.transactf("uid store %s %s (%s)", uidSet, item, strings.Join(flags, " "))
496// MSNCopy adds messages from the sequences in the sequence set in the
497// selected/active mailbox to destMailbox using the IMAP4 "COPY" command.
499// Method [Conn.UIDCopy], operating on UIDs instead of sequence numbers, should be
501func (c *Conn) MSNCopy(seqSet string, destMailbox string) (resp Response, rerr error) {
502 defer c.recover(&rerr, &resp)
503 return c.transactf("copy %s %s", seqSet, astring(destMailbox))
506// UIDCopy is like copy, but operates on UIDs, using the IMAP4 "UID COPY" command.
508// Required capability: "UIDPLUS" or "IMAP4rev2".
509func (c *Conn) UIDCopy(uidSet string, destMailbox string) (resp Response, rerr error) {
510 defer c.recover(&rerr, &resp)
511 return c.transactf("uid copy %s %s", uidSet, astring(destMailbox))
514// MSNSearch returns messages from the sequence set in the selected/active mailbox
515// that match the search critera using the IMAP4 "SEARCH" command.
517// Method [Conn.UIDSearch], operating on UIDs instead of sequence numbers, should be
519func (c *Conn) MSNSearch(seqSet string, criteria string) (resp Response, rerr error) {
520 defer c.recover(&rerr, &resp)
521 return c.transactf("seach %s %s", seqSet, criteria)
524// UIDSearch returns messages from the uid set in the selected/active mailbox that
525// match the search critera using the IMAP4 "SEARCH" command.
527// Criteria is a search program, see RFC 9051 and RFC 3501 for details.
529// Required capability: "UIDPLUS" or "IMAP4rev2".
530func (c *Conn) UIDSearch(seqSet string, criteria string) (resp Response, rerr error) {
531 defer c.recover(&rerr, &resp)
532 return c.transactf("seach %s %s", seqSet, criteria)
535// MSNMove moves messages from the sequence set in the selected/active mailbox to
536// destMailbox using the IMAP4 "MOVE" command.
538// Required capability: "MOVE" or "IMAP4rev2".
540// Method [Conn.UIDMove], operating on UIDs instead of sequence numbers, should be
542func (c *Conn) MSNMove(seqSet string, destMailbox string) (resp Response, rerr error) {
543 defer c.recover(&rerr, &resp)
544 return c.transactf("move %s %s", seqSet, astring(destMailbox))
547// UIDMove is like move, but operates on UIDs, using the IMAP4 "UID MOVE" command.
549// Required capability: "MOVE" or "IMAP4rev2".
550func (c *Conn) UIDMove(uidSet string, destMailbox string) (resp Response, rerr error) {
551 defer c.recover(&rerr, &resp)
552 return c.transactf("uid move %s %s", uidSet, astring(destMailbox))
555// MSNReplace is like the preferred [Conn.UIDReplace], but operates on a message
556// sequence number (MSN) instead of a UID.
558// Required capability: "REPLACE".
560// Method [Conn.UIDReplace], operating on UIDs instead of sequence numbers, should be
562func (c *Conn) MSNReplace(msgseq string, mailbox string, msg Append) (resp Response, rerr error) {
563 // todo: parse msgseq, must be nznumber, with a known msgseq. or "*" with at least one message.
564 return c.replace("replace", msgseq, mailbox, msg)
567// UIDReplace uses the IMAP4 "UID REPLACE" command to replace a message from the
568// selected/active mailbox with a new/different version of the message in the named
569// mailbox, which may be the same or different than the selected mailbox.
571// The replaced message is indicated by uid.
573// Required capability: "REPLACE".
574func (c *Conn) UIDReplace(uid string, mailbox string, msg Append) (resp Response, rerr error) {
575 // todo: parse uid, must be nznumber, with a known uid. or "*" with at least one message.
576 return c.replace("uid replace", uid, mailbox, msg)
579func (c *Conn) replace(cmd string, num string, mailbox string, msg Append) (resp Response, rerr error) {
580 defer c.recover(&rerr, &resp)
582 // todo: use synchronizing literal for larger messages.
585 if msg.Received != nil {
586 date = ` "` + msg.Received.Format("_2-Jan-2006 15:04:05 -0700") + `"`
588 // todo: only use literal8 if needed, possibly with "UTF8()"
589 // todo: encode mailbox
590 err := c.WriteCommandf("", "%s %s %s (%s)%s ~{%d+}", cmd, num, astring(mailbox), strings.Join(msg.Flags, " "), date, msg.Size)
591 c.xcheckf(err, "writing replace command")
593 defer c.xtracewrite(mlog.LevelTracedata)()
594 _, err = io.Copy(c.xbw, msg.Data)
595 c.xcheckf(err, "write message data")
596 c.xtracewrite(mlog.LevelTrace)
598 fmt.Fprintf(c.xbw, "\r\n")
601 return c.responseOK()