// Package imapclient implements an IMAP client. // // # Charset decoding // // By default, only basic charset decoding is performed. For non-UTF-8 decoding // of message subjects and e-mail address names, users can set // Options.WordDecoder. For instance, to use go-message's collection of // charsets: // // import ( // "mime" // // "github.com/emersion/go-message/charset" // ) // // options := &imapclient.Options{ // WordDecoder: &mime.WordDecoder{CharsetReader: charset.Reader}, // } // client, err := imapclient.DialTLS("imap.example.org:993", options) package imapclient import ( "bufio" "crypto/tls" "errors" "fmt" "io" "mime" "net" "runtime/debug" "strconv" "strings" "sync" "time" "github.com/emersion/go-imap/v2" "github.com/emersion/go-imap/v2/internal" "github.com/emersion/go-imap/v2/internal/imapwire" ) const ( idleReadTimeout = time.Duration(0) respReadTimeout = 30 * time.Second literalReadTimeout = 5 * time.Minute cmdWriteTimeout = 30 * time.Second literalWriteTimeout = 5 * time.Minute defaultDialTimeout = 30 * time.Second ) // SelectedMailbox contains metadata for the currently selected mailbox. type SelectedMailbox struct { Name string NumMessages uint32 Flags []imap.Flag PermanentFlags []imap.Flag } func (mbox *SelectedMailbox) copy() *SelectedMailbox { copy := *mbox return © } // Options contains options for Client. type Options struct { // TLS configuration for use by DialTLS and DialStartTLS. If nil, the // default configuration is used. TLSConfig *tls.Config // Raw ingress and egress data will be written to this writer, if any. // Note, this may include sensitive information such as credentials used // during authentication. DebugWriter io.Writer // Unilateral data handler. UnilateralDataHandler *UnilateralDataHandler // Decoder for RFC 2047 words. WordDecoder *mime.WordDecoder // Dialer to use when establishing connections with the Dial* functions. // If nil, a default dialer with a 30 second timeout is used. Dialer *net.Dialer } func (options *Options) wrapReadWriter(rw io.ReadWriter) io.ReadWriter { if options.DebugWriter == nil { return rw } return struct { io.Reader io.Writer }{ Reader: io.TeeReader(rw, options.DebugWriter), Writer: io.MultiWriter(rw, options.DebugWriter), } } func (options *Options) decodeText(s string) (string, error) { wordDecoder := options.WordDecoder if wordDecoder == nil { wordDecoder = &mime.WordDecoder{} } out, err := wordDecoder.DecodeHeader(s) if err != nil { return s, err } return out, nil } func (options *Options) unilateralDataHandler() *UnilateralDataHandler { if options.UnilateralDataHandler == nil { return &UnilateralDataHandler{} } return options.UnilateralDataHandler } func (options *Options) tlsConfig() *tls.Config { if options.TLSConfig != nil { return options.TLSConfig.Clone() } else { return new(tls.Config) } } func (options *Options) dialer() *net.Dialer { if options.Dialer == nil { return &net.Dialer{Timeout: defaultDialTimeout} } return options.Dialer } // Client is an IMAP client. // // IMAP commands are exposed as methods. These methods will block until the // command has been sent to the server, but won't block until the server sends // a response. They return a command struct which can be used to wait for the // server response. This can be used to execute multiple commands concurrently, // however care must be taken to avoid ambiguities. See RFC 9051 section 5.5. // // A client can be safely used from multiple goroutines, however this doesn't // guarantee any command ordering and is subject to the same caveats as command // pipelining (see above). Additionally, some commands (e.g. StartTLS, // Authenticate, Idle) block the client during their execution. type Client struct { conn net.Conn options Options br *bufio.Reader bw *bufio.Writer dec *imapwire.Decoder encMutex sync.Mutex greetingCh chan struct{} greetingRecv bool greetingErr error decCh chan struct{} decErr error mutex sync.Mutex state imap.ConnState caps imap.CapSet enabled imap.CapSet pendingCapCh chan struct{} mailbox *SelectedMailbox cmdTag uint64 pendingCmds []command contReqs []continuationRequest closed bool } // New creates a new IMAP client. // // This function doesn't perform I/O. // // A nil options pointer is equivalent to a zero options value. func New(conn net.Conn, options *Options) *Client { if options == nil { options = &Options{} } rw := options.wrapReadWriter(conn) br := bufio.NewReader(rw) bw := bufio.NewWriter(rw) client := &Client{ conn: conn, options: *options, br: br, bw: bw, dec: imapwire.NewDecoder(br, imapwire.ConnSideClient), greetingCh: make(chan struct{}), decCh: make(chan struct{}), state: imap.ConnStateNone, enabled: make(imap.CapSet), } go client.read() return client } // NewStartTLS creates a new IMAP client with STARTTLS. // // A nil options pointer is equivalent to a zero options value. func NewStartTLS(conn net.Conn, options *Options) (*Client, error) { if options == nil { options = &Options{} } client := New(conn, options) if err := client.startTLS(options.TLSConfig); err != nil { conn.Close() return nil, err } // Per section 7.1.4, refuse PREAUTH when using STARTTLS if client.State() != imap.ConnStateNotAuthenticated { client.Close() return nil, fmt.Errorf("imapclient: server sent PREAUTH on unencrypted connection") } return client, nil } // DialInsecure connects to an IMAP server without any encryption at all. func DialInsecure(address string, options *Options) (*Client, error) { if options == nil { options = &Options{} } conn, err := options.dialer().Dial("tcp", address) if err != nil { return nil, err } return New(conn, options), nil } // DialTLS connects to an IMAP server with implicit TLS. func DialTLS(address string, options *Options) (*Client, error) { if options == nil { options = &Options{} } tlsConfig := options.tlsConfig() if tlsConfig.NextProtos == nil { tlsConfig.NextProtos = []string{"imap"} } dialer := options.dialer() conn, err := tls.DialWithDialer(dialer, "tcp", address, tlsConfig) if err != nil { return nil, err } return New(conn, options), nil } // DialStartTLS connects to an IMAP server with STARTTLS. func DialStartTLS(address string, options *Options) (*Client, error) { if options == nil { options = &Options{} } host, _, err := net.SplitHostPort(address) if err != nil { return nil, err } conn, err := options.dialer().Dial("tcp", address) if err != nil { return nil, err } tlsConfig := options.tlsConfig() if tlsConfig.ServerName == "" { tlsConfig.ServerName = host } newOptions := *options newOptions.TLSConfig = tlsConfig return NewStartTLS(conn, &newOptions) } func (c *Client) setReadTimeout(dur time.Duration) { if dur > 0 { c.conn.SetReadDeadline(time.Now().Add(dur)) } else { c.conn.SetReadDeadline(time.Time{}) } } func (c *Client) setWriteTimeout(dur time.Duration) { if dur > 0 { c.conn.SetWriteDeadline(time.Now().Add(dur)) } else { c.conn.SetWriteDeadline(time.Time{}) } } // State returns the current connection state of the client. func (c *Client) State() imap.ConnState { c.mutex.Lock() defer c.mutex.Unlock() return c.state } func (c *Client) setState(state imap.ConnState) { c.mutex.Lock() c.state = state if c.state != imap.ConnStateSelected { c.mailbox = nil } c.mutex.Unlock() } // Caps returns the capabilities advertised by the server. // // When the server hasn't sent the capability list, this method will request it // and block until it's received. If the capabilities cannot be fetched, nil is // returned. func (c *Client) Caps() imap.CapSet { if err := c.WaitGreeting(); err != nil { return nil } c.mutex.Lock() caps := c.caps capCh := c.pendingCapCh c.mutex.Unlock() if caps != nil { return caps } if capCh == nil { capCmd := c.Capability() capCh := make(chan struct{}) go func() { capCmd.Wait() close(capCh) }() c.mutex.Lock() c.pendingCapCh = capCh c.mutex.Unlock() } timer := time.NewTimer(respReadTimeout) defer timer.Stop() select { case <-timer.C: return nil case <-capCh: // ok } // TODO: this is racy if caps are reset before we get the reply c.mutex.Lock() defer c.mutex.Unlock() return c.caps } func (c *Client) setCaps(caps imap.CapSet) { // If the capabilities are being reset, request the updated capabilities // from the server var capCh chan struct{} if caps == nil { capCh = make(chan struct{}) // We need to send the CAPABILITY command in a separate goroutine: // setCaps might be called with Client.encMutex locked go func() { c.Capability().Wait() close(capCh) }() } c.mutex.Lock() c.caps = caps c.pendingCapCh = capCh c.mutex.Unlock() } // Mailbox returns the state of the currently selected mailbox. // // If there is no currently selected mailbox, nil is returned. // // The returned struct must not be mutated. func (c *Client) Mailbox() *SelectedMailbox { c.mutex.Lock() defer c.mutex.Unlock() return c.mailbox } // Closed returns a channel that is closed when the connection is closed. // // This channel cannot be used to reliably determine whether a connection is healthy. If // the underlying connection times out, the channel will be closed eventually, but not // immediately. To check whether the connection is healthy, send a command (such as Noop). func (c *Client) Closed() <-chan struct{} { return c.decCh } // Close immediately closes the connection. func (c *Client) Close() error { c.mutex.Lock() alreadyClosed := c.closed c.closed = true c.mutex.Unlock() // Ignore net.ErrClosed here, because we also call conn.Close in c.read if err := c.conn.Close(); err != nil && !errors.Is(err, net.ErrClosed) && !errors.Is(err, io.ErrClosedPipe) { return err } <-c.decCh if err := c.decErr; err != nil { return err } if alreadyClosed { return net.ErrClosed } return nil } // beginCommand starts sending a command to the server. // // The command name and a space are written. // // The caller must call commandEncoder.end. func (c *Client) beginCommand(name string, cmd command) *commandEncoder { c.encMutex.Lock() // unlocked by commandEncoder.end c.mutex.Lock() c.cmdTag++ tag := fmt.Sprintf("T%v", c.cmdTag) baseCmd := cmd.base() *baseCmd = commandBase{ tag: tag, done: make(chan error, 1), } c.pendingCmds = append(c.pendingCmds, cmd) quotedUTF8 := c.caps.Has(imap.CapIMAP4rev2) || c.enabled.Has(imap.CapUTF8Accept) literalMinus := c.caps.Has(imap.CapLiteralMinus) literalPlus := c.caps.Has(imap.CapLiteralPlus) c.mutex.Unlock() c.setWriteTimeout(cmdWriteTimeout) wireEnc := imapwire.NewEncoder(c.bw, imapwire.ConnSideClient) wireEnc.QuotedUTF8 = quotedUTF8 wireEnc.LiteralMinus = literalMinus wireEnc.LiteralPlus = literalPlus wireEnc.NewContinuationRequest = func() *imapwire.ContinuationRequest { return c.registerContReq(cmd) } enc := &commandEncoder{ Encoder: wireEnc, client: c, cmd: baseCmd, } enc.Atom(tag).SP().Atom(name) return enc } func (c *Client) deletePendingCmdByTag(tag string) command { c.mutex.Lock() defer c.mutex.Unlock() for i, cmd := range c.pendingCmds { if cmd.base().tag == tag { c.pendingCmds = append(c.pendingCmds[:i], c.pendingCmds[i+1:]...) return cmd } } return nil } func (c *Client) findPendingCmdFunc(f func(cmd command) bool) command { c.mutex.Lock() defer c.mutex.Unlock() for _, cmd := range c.pendingCmds { if f(cmd) { return cmd } } return nil } func findPendingCmdByType[T command](c *Client) T { c.mutex.Lock() defer c.mutex.Unlock() for _, cmd := range c.pendingCmds { if cmd, ok := cmd.(T); ok { return cmd } } var cmd T return cmd } func (c *Client) completeCommand(cmd command, err error) { done := cmd.base().done done <- err close(done) // Ensure the command is not blocked waiting on continuation requests c.mutex.Lock() var filtered []continuationRequest for _, contReq := range c.contReqs { if contReq.cmd != cmd.base() { filtered = append(filtered, contReq) } else { contReq.Cancel(err) } } c.contReqs = filtered c.mutex.Unlock() switch cmd := cmd.(type) { case *authenticateCommand, *loginCommand: if err == nil { c.setState(imap.ConnStateAuthenticated) } case *unauthenticateCommand: if err == nil { c.mutex.Lock() c.state = imap.ConnStateNotAuthenticated c.mailbox = nil c.enabled = make(imap.CapSet) c.mutex.Unlock() } case *SelectCommand: if err == nil { c.mutex.Lock() c.state = imap.ConnStateSelected c.mailbox = &SelectedMailbox{ Name: cmd.mailbox, NumMessages: cmd.data.NumMessages, Flags: cmd.data.Flags, PermanentFlags: cmd.data.PermanentFlags, } c.mutex.Unlock() } case *unselectCommand: if err == nil { c.setState(imap.ConnStateAuthenticated) } case *logoutCommand: if err == nil { c.setState(imap.ConnStateLogout) } case *ListCommand: if cmd.pendingData != nil { cmd.mailboxes <- cmd.pendingData } close(cmd.mailboxes) case *FetchCommand: close(cmd.msgs) case *ExpungeCommand: close(cmd.seqNums) } } func (c *Client) registerContReq(cmd command) *imapwire.ContinuationRequest { contReq := imapwire.NewContinuationRequest() c.mutex.Lock() c.contReqs = append(c.contReqs, continuationRequest{ ContinuationRequest: contReq, cmd: cmd.base(), }) c.mutex.Unlock() return contReq } func (c *Client) closeWithError(err error) { c.conn.Close() c.mutex.Lock() c.state = imap.ConnStateLogout pendingCmds := c.pendingCmds c.pendingCmds = nil c.mutex.Unlock() for _, cmd := range pendingCmds { c.completeCommand(cmd, err) } } // read continuously reads data coming from the server. // // All the data is decoded in the read goroutine, then dispatched via channels // to pending commands. func (c *Client) read() { defer close(c.decCh) defer func() { if v := recover(); v != nil { c.decErr = fmt.Errorf("imapclient: panic reading response: %v\n%s", v, debug.Stack()) } cmdErr := c.decErr if cmdErr == nil { cmdErr = io.ErrUnexpectedEOF } c.closeWithError(cmdErr) }() c.setReadTimeout(respReadTimeout) // We're waiting for the greeting for { // Ignore net.ErrClosed here, because we also call conn.Close in c.Close if c.dec.EOF() || errors.Is(c.dec.Err(), net.ErrClosed) || errors.Is(c.dec.Err(), io.ErrClosedPipe) { break } if err := c.readResponse(); err != nil { c.decErr = err break } if c.greetingErr != nil { break } } } func (c *Client) readResponse() error { c.setReadTimeout(respReadTimeout) defer c.setReadTimeout(idleReadTimeout) if c.dec.Special('+') { if err := c.readContinueReq(); err != nil { return fmt.Errorf("in continue-req: %v", err) } return nil } var tag, typ string if !c.dec.Expect(c.dec.Special('*') || c.dec.Atom(&tag), "'*' or atom") { return fmt.Errorf("in response: cannot read tag: %v", c.dec.Err()) } if !c.dec.ExpectSP() { return fmt.Errorf("in response: %v", c.dec.Err()) } if !c.dec.ExpectAtom(&typ) { return fmt.Errorf("in response: cannot read type: %v", c.dec.Err()) } // Change typ to uppercase, as it's case-insensitive typ = strings.ToUpper(typ) var ( token string err error startTLS *startTLSCommand ) if tag != "" { token = "response-tagged" startTLS, err = c.readResponseTagged(tag, typ) } else { token = "response-data" err = c.readResponseData(typ) } if err != nil { return fmt.Errorf("in %v: %v", token, err) } if !c.dec.ExpectCRLF() { return fmt.Errorf("in response: %v", c.dec.Err()) } if startTLS != nil { c.upgradeStartTLS(startTLS) } return nil } func (c *Client) readContinueReq() error { var text string if c.dec.SP() { c.dec.Text(&text) } if !c.dec.ExpectCRLF() { return c.dec.Err() } var contReq *imapwire.ContinuationRequest c.mutex.Lock() if len(c.contReqs) > 0 { contReq = c.contReqs[0].ContinuationRequest c.contReqs = append(c.contReqs[:0], c.contReqs[1:]...) } c.mutex.Unlock() if contReq == nil { return fmt.Errorf("received unmatched continuation request") } contReq.Done(text) return nil } func (c *Client) readResponseTagged(tag, typ string) (startTLS *startTLSCommand, err error) { cmd := c.deletePendingCmdByTag(tag) if cmd == nil { return nil, fmt.Errorf("received tagged response with unknown tag %q", tag) } // We've removed the command from the pending queue above. Make sure we // don't stall it on error. defer func() { if err != nil { c.completeCommand(cmd, err) } }() // Some servers don't provide a text even if the RFC requires it, // see #500 and #502 hasSP := c.dec.SP() var code string if hasSP && c.dec.Special('[') { // resp-text-code if !c.dec.ExpectAtom(&code) { return nil, fmt.Errorf("in resp-text-code: %v", c.dec.Err()) } // TODO: LONGENTRIES and MAXSIZE from METADATA switch code { case "CAPABILITY": // capability-data caps, err := readCapabilities(c.dec) if err != nil { return nil, fmt.Errorf("in capability-data: %v", err) } c.setCaps(caps) case "APPENDUID": var ( uidValidity uint32 uid imap.UID ) if !c.dec.ExpectSP() || !c.dec.ExpectNumber(&uidValidity) || !c.dec.ExpectSP() || !c.dec.ExpectUID(&uid) { return nil, fmt.Errorf("in resp-code-apnd: %v", c.dec.Err()) } if cmd, ok := cmd.(*AppendCommand); ok { cmd.data.UID = uid cmd.data.UIDValidity = uidValidity } case "COPYUID": if !c.dec.ExpectSP() { return nil, c.dec.Err() } uidValidity, srcUIDs, dstUIDs, err := readRespCodeCopyUID(c.dec) if err != nil { return nil, fmt.Errorf("in resp-code-copy: %v", err) } switch cmd := cmd.(type) { case *CopyCommand: cmd.data.UIDValidity = uidValidity cmd.data.SourceUIDs = srcUIDs cmd.data.DestUIDs = dstUIDs case *MoveCommand: // This can happen when Client.Move falls back to COPY + // STORE + EXPUNGE cmd.data.UIDValidity = uidValidity cmd.data.SourceUIDs = srcUIDs cmd.data.DestUIDs = dstUIDs } default: // [SP 1*] if c.dec.SP() { c.dec.DiscardUntilByte(']') } } if !c.dec.ExpectSpecial(']') { return nil, fmt.Errorf("in resp-text: %v", c.dec.Err()) } hasSP = c.dec.SP() } var text string if hasSP && !c.dec.ExpectText(&text) { return nil, fmt.Errorf("in resp-text: %v", c.dec.Err()) } var cmdErr error switch typ { case "OK": // nothing to do case "NO", "BAD": cmdErr = &imap.Error{ Type: imap.StatusResponseType(typ), Code: imap.ResponseCode(code), Text: text, } default: return nil, fmt.Errorf("in resp-cond-state: expected OK, NO or BAD status condition, but got %v", typ) } c.completeCommand(cmd, cmdErr) if cmd, ok := cmd.(*startTLSCommand); ok && cmdErr == nil { startTLS = cmd } if cmdErr == nil && code != "CAPABILITY" { switch cmd.(type) { case *startTLSCommand, *loginCommand, *authenticateCommand, *unauthenticateCommand: // These commands invalidate the capabilities c.setCaps(nil) } } return startTLS, nil } func (c *Client) readResponseData(typ string) error { // number SP ("EXISTS" / "RECENT" / "FETCH" / "EXPUNGE") var num uint32 if typ[0] >= '0' && typ[0] <= '9' { v, err := strconv.ParseUint(typ, 10, 32) if err != nil { return err } num = uint32(v) if !c.dec.ExpectSP() || !c.dec.ExpectAtom(&typ) { return c.dec.Err() } } // All response type are case insensitive switch strings.ToUpper(typ) { case "OK", "PREAUTH", "NO", "BAD", "BYE": // resp-cond-state / resp-cond-bye / resp-cond-auth // Some servers don't provide a text even if the RFC requires it, // see #500 and #502 hasSP := c.dec.SP() var code string if hasSP && c.dec.Special('[') { // resp-text-code if !c.dec.ExpectAtom(&code) { return fmt.Errorf("in resp-text-code: %v", c.dec.Err()) } switch code { case "CAPABILITY": // capability-data caps, err := readCapabilities(c.dec) if err != nil { return fmt.Errorf("in capability-data: %v", err) } c.setCaps(caps) case "PERMANENTFLAGS": if !c.dec.ExpectSP() { return c.dec.Err() } flags, err := internal.ExpectFlagList(c.dec) if err != nil { return err } c.mutex.Lock() if c.state == imap.ConnStateSelected { c.mailbox = c.mailbox.copy() c.mailbox.PermanentFlags = flags } c.mutex.Unlock() if cmd := findPendingCmdByType[*SelectCommand](c); cmd != nil { cmd.data.PermanentFlags = flags } else if handler := c.options.unilateralDataHandler().Mailbox; handler != nil { handler(&UnilateralDataMailbox{PermanentFlags: flags}) } case "UIDNEXT": var uidNext imap.UID if !c.dec.ExpectSP() || !c.dec.ExpectUID(&uidNext) { return c.dec.Err() } if cmd := findPendingCmdByType[*SelectCommand](c); cmd != nil { cmd.data.UIDNext = uidNext } case "UIDVALIDITY": var uidValidity uint32 if !c.dec.ExpectSP() || !c.dec.ExpectNumber(&uidValidity) { return c.dec.Err() } if cmd := findPendingCmdByType[*SelectCommand](c); cmd != nil { cmd.data.UIDValidity = uidValidity } case "COPYUID": if !c.dec.ExpectSP() { return c.dec.Err() } uidValidity, srcUIDs, dstUIDs, err := readRespCodeCopyUID(c.dec) if err != nil { return fmt.Errorf("in resp-code-copy: %v", err) } if cmd := findPendingCmdByType[*MoveCommand](c); cmd != nil { cmd.data.UIDValidity = uidValidity cmd.data.SourceUIDs = srcUIDs cmd.data.DestUIDs = dstUIDs } case "HIGHESTMODSEQ": var modSeq uint64 if !c.dec.ExpectSP() || !c.dec.ExpectModSeq(&modSeq) { return c.dec.Err() } if cmd := findPendingCmdByType[*SelectCommand](c); cmd != nil { cmd.data.HighestModSeq = modSeq } case "NOMODSEQ": // ignore default: // [SP 1*] if c.dec.SP() { c.dec.DiscardUntilByte(']') } } if !c.dec.ExpectSpecial(']') { return fmt.Errorf("in resp-text: %v", c.dec.Err()) } hasSP = c.dec.SP() } var text string if hasSP && !c.dec.ExpectText(&text) { return fmt.Errorf("in resp-text: %v", c.dec.Err()) } if code == "CLOSED" { c.setState(imap.ConnStateAuthenticated) } if !c.greetingRecv { switch typ { case "OK": c.setState(imap.ConnStateNotAuthenticated) case "PREAUTH": c.setState(imap.ConnStateAuthenticated) default: c.setState(imap.ConnStateLogout) c.greetingErr = &imap.Error{ Type: imap.StatusResponseType(typ), Code: imap.ResponseCode(code), Text: text, } } c.greetingRecv = true if c.greetingErr == nil && code != "CAPABILITY" { c.setCaps(nil) // request initial capabilities } close(c.greetingCh) } case "ID": return c.handleID() case "CAPABILITY": return c.handleCapability() case "ENABLED": return c.handleEnabled() case "NAMESPACE": if !c.dec.ExpectSP() { return c.dec.Err() } return c.handleNamespace() case "FLAGS": if !c.dec.ExpectSP() { return c.dec.Err() } return c.handleFlags() case "EXISTS": return c.handleExists(num) case "RECENT": // ignore case "LIST": if !c.dec.ExpectSP() { return c.dec.Err() } return c.handleList() case "STATUS": if !c.dec.ExpectSP() { return c.dec.Err() } return c.handleStatus() case "FETCH": if !c.dec.ExpectSP() { return c.dec.Err() } return c.handleFetch(num) case "EXPUNGE": return c.handleExpunge(num) case "SEARCH": return c.handleSearch() case "ESEARCH": return c.handleESearch() case "SORT": return c.handleSort() case "THREAD": return c.handleThread() case "METADATA": if !c.dec.ExpectSP() { return c.dec.Err() } return c.handleMetadata() case "QUOTA": if !c.dec.ExpectSP() { return c.dec.Err() } return c.handleQuota() case "QUOTAROOT": if !c.dec.ExpectSP() { return c.dec.Err() } return c.handleQuotaRoot() case "MYRIGHTS": if !c.dec.ExpectSP() { return c.dec.Err() } return c.handleMyRights() case "ACL": if !c.dec.ExpectSP() { return c.dec.Err() } return c.handleGetACL() default: return fmt.Errorf("unsupported response type %q", typ) } return nil } // WaitGreeting waits for the server's initial greeting. func (c *Client) WaitGreeting() error { select { case <-c.greetingCh: return c.greetingErr case <-c.decCh: if c.decErr != nil { return fmt.Errorf("got error before greeting: %v", c.decErr) } return fmt.Errorf("connection closed before greeting") } } // Noop sends a NOOP command. func (c *Client) Noop() *Command { cmd := &Command{} c.beginCommand("NOOP", cmd).end() return cmd } // Logout sends a LOGOUT command. // // This command informs the server that the client is done with the connection. func (c *Client) Logout() *Command { cmd := &logoutCommand{} c.beginCommand("LOGOUT", cmd).end() return &cmd.Command } // Login sends a LOGIN command. func (c *Client) Login(username, password string) *Command { cmd := &loginCommand{} enc := c.beginCommand("LOGIN", cmd) enc.SP().String(username).SP().String(password) enc.end() return &cmd.Command } // Delete sends a DELETE command. func (c *Client) Delete(mailbox string) *Command { cmd := &Command{} enc := c.beginCommand("DELETE", cmd) enc.SP().Mailbox(mailbox) enc.end() return cmd } // Rename sends a RENAME command. // // A nil options pointer is equivalent to a zero options value. func (c *Client) Rename(mailbox, newName string, options *imap.RenameOptions) *Command { cmd := &Command{} enc := c.beginCommand("RENAME", cmd) enc.SP().Mailbox(mailbox).SP().Mailbox(newName) enc.end() return cmd } // Subscribe sends a SUBSCRIBE command. func (c *Client) Subscribe(mailbox string) *Command { cmd := &Command{} enc := c.beginCommand("SUBSCRIBE", cmd) enc.SP().Mailbox(mailbox) enc.end() return cmd } // Unsubscribe sends an UNSUBSCRIBE command. func (c *Client) Unsubscribe(mailbox string) *Command { cmd := &Command{} enc := c.beginCommand("UNSUBSCRIBE", cmd) enc.SP().Mailbox(mailbox) enc.end() return cmd } func uidCmdName(name string, kind imapwire.NumKind) string { switch kind { case imapwire.NumKindSeq: return name case imapwire.NumKindUID: return "UID " + name default: panic("imapclient: invalid imapwire.NumKind") } } type commandEncoder struct { *imapwire.Encoder client *Client cmd *commandBase } // end ends an outgoing command. // // A CRLF is written, the encoder is flushed and its lock is released. func (ce *commandEncoder) end() { if ce.Encoder != nil { ce.flush() } ce.client.setWriteTimeout(0) ce.client.encMutex.Unlock() } // flush sends an outgoing command, but keeps the encoder lock. // // A CRLF is written and the encoder is flushed. Callers must call // commandEncoder.end to release the lock. func (ce *commandEncoder) flush() { if err := ce.Encoder.CRLF(); err != nil { // TODO: consider stashing the error in Client to return it in future // calls ce.client.closeWithError(err) } ce.Encoder = nil } // Literal encodes a literal. func (ce *commandEncoder) Literal(size int64) io.WriteCloser { var contReq *imapwire.ContinuationRequest ce.client.mutex.Lock() hasCapLiteralMinus := ce.client.caps.Has(imap.CapLiteralMinus) ce.client.mutex.Unlock() if size > 4096 || !hasCapLiteralMinus { contReq = ce.client.registerContReq(ce.cmd) } ce.client.setWriteTimeout(literalWriteTimeout) return literalWriter{ WriteCloser: ce.Encoder.Literal(size, contReq), client: ce.client, } } type literalWriter struct { io.WriteCloser client *Client } func (lw literalWriter) Close() error { lw.client.setWriteTimeout(cmdWriteTimeout) return lw.WriteCloser.Close() } // continuationRequest is a pending continuation request. type continuationRequest struct { *imapwire.ContinuationRequest cmd *commandBase } // UnilateralDataMailbox describes a mailbox status update. // // If a field is nil, it hasn't changed. type UnilateralDataMailbox struct { NumMessages *uint32 Flags []imap.Flag PermanentFlags []imap.Flag } // UnilateralDataHandler handles unilateral data. // // The handler will block the client while running. If the caller intends to // perform slow operations, a buffered channel and a separate goroutine should // be used. // // The handler will be invoked in an arbitrary goroutine. // // See Options.UnilateralDataHandler. type UnilateralDataHandler struct { Expunge func(seqNum uint32) Mailbox func(data *UnilateralDataMailbox) Fetch func(msg *FetchMessageData) // requires ENABLE METADATA or ENABLE SERVER-METADATA Metadata func(mailbox string, entries []string) } // command is an interface for IMAP commands. // // Commands are represented by the Command type, but can be extended by other // types (e.g. CapabilityCommand). type command interface { base() *commandBase } type commandBase struct { tag string done chan error err error } func (cmd *commandBase) base() *commandBase { return cmd } func (cmd *commandBase) wait() error { if cmd.err == nil { cmd.err = <-cmd.done } return cmd.err } // Command is a basic IMAP command. type Command struct { commandBase } // Wait blocks until the command has completed. func (cmd *Command) Wait() error { return cmd.wait() } type loginCommand struct { Command } // logoutCommand is a LOGOUT command. type logoutCommand struct { Command }