package client import ( "errors" "github.com/emersion/go-imap" "github.com/emersion/go-imap/commands" "github.com/emersion/go-imap/responses" ) var ( // ErrNoMailboxSelected is returned if a command that requires a mailbox to be // selected is called when there isn't. ErrNoMailboxSelected = errors.New("No mailbox selected") // ErrExtensionUnsupported is returned if a command uses a extension that // is not supported by the server. ErrExtensionUnsupported = errors.New("The required extension is not supported by the server") ) // Check requests a checkpoint of the currently selected mailbox. A checkpoint // refers to any implementation-dependent housekeeping associated with the // mailbox that is not normally executed as part of each command. func (c *Client) Check() error { if c.State() != imap.SelectedState { return ErrNoMailboxSelected } cmd := new(commands.Check) status, err := c.execute(cmd, nil) if err != nil { return err } return status.Err() } // Close permanently removes all messages that have the \Deleted flag set from // the currently selected mailbox, and returns to the authenticated state from // the selected state. func (c *Client) Close() error { if c.State() != imap.SelectedState { return ErrNoMailboxSelected } cmd := new(commands.Close) status, err := c.execute(cmd, nil) if err != nil { return err } else if err := status.Err(); err != nil { return err } c.locker.Lock() c.state = imap.AuthenticatedState c.mailbox = nil c.locker.Unlock() return nil } // Terminate closes the tcp connection func (c *Client) Terminate() error { return c.conn.Close() } // Expunge permanently removes all messages that have the \Deleted flag set from // the currently selected mailbox. If ch is not nil, sends sequence IDs of each // deleted message to this channel. func (c *Client) Expunge(ch chan uint32) error { if ch != nil { defer close(ch) } if c.State() != imap.SelectedState { return ErrNoMailboxSelected } cmd := new(commands.Expunge) var h responses.Handler if ch != nil { h = &responses.Expunge{SeqNums: ch} } status, err := c.execute(cmd, h) if err != nil { return err } return status.Err() } func (c *Client) executeSearch(uid bool, criteria *imap.SearchCriteria, charset string) (ids []uint32, status *imap.StatusResp, err error) { if c.State() != imap.SelectedState { err = ErrNoMailboxSelected return } var cmd imap.Commander = &commands.Search{ Charset: charset, Criteria: criteria, } if uid { cmd = &commands.Uid{Cmd: cmd} } res := new(responses.Search) status, err = c.execute(cmd, res) if err != nil { return } err, ids = status.Err(), res.Ids return } func (c *Client) search(uid bool, criteria *imap.SearchCriteria) (ids []uint32, err error) { ids, status, err := c.executeSearch(uid, criteria, "UTF-8") if status != nil && status.Code == imap.CodeBadCharset { // Some servers don't support UTF-8 ids, _, err = c.executeSearch(uid, criteria, "US-ASCII") } return } // Search searches the mailbox for messages that match the given searching // criteria. Searching criteria consist of one or more search keys. The response // contains a list of message sequence IDs corresponding to those messages that // match the searching criteria. When multiple keys are specified, the result is // the intersection (AND function) of all the messages that match those keys. // Criteria must be UTF-8 encoded. See RFC 3501 section 6.4.4 for a list of // searching criteria. When no criteria has been set, all messages in the mailbox // will be searched using ALL criteria. func (c *Client) Search(criteria *imap.SearchCriteria) (seqNums []uint32, err error) { return c.search(false, criteria) } // UidSearch is identical to Search, but UIDs are returned instead of message // sequence numbers. func (c *Client) UidSearch(criteria *imap.SearchCriteria) (uids []uint32, err error) { return c.search(true, criteria) } func (c *Client) fetch(uid bool, seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error { defer close(ch) if c.State() != imap.SelectedState { return ErrNoMailboxSelected } var cmd imap.Commander = &commands.Fetch{ SeqSet: seqset, Items: items, } if uid { cmd = &commands.Uid{Cmd: cmd} } res := &responses.Fetch{Messages: ch, SeqSet: seqset, Uid: uid} status, err := c.execute(cmd, res) if err != nil { return err } return status.Err() } // Fetch retrieves data associated with a message in the mailbox. See RFC 3501 // section 6.4.5 for a list of items that can be requested. func (c *Client) Fetch(seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error { return c.fetch(false, seqset, items, ch) } // UidFetch is identical to Fetch, but seqset is interpreted as containing // unique identifiers instead of message sequence numbers. func (c *Client) UidFetch(seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error { return c.fetch(true, seqset, items, ch) } func (c *Client) store(uid bool, seqset *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) error { if ch != nil { defer close(ch) } if c.State() != imap.SelectedState { return ErrNoMailboxSelected } // TODO: this could break extensions (this only works when item is FLAGS) if fields, ok := value.([]interface{}); ok { for i, field := range fields { if s, ok := field.(string); ok { fields[i] = imap.RawString(s) } } } // If ch is nil, the updated values are data which will be lost, so don't // retrieve it. if ch == nil { op, _, err := imap.ParseFlagsOp(item) if err == nil { item = imap.FormatFlagsOp(op, true) } } var cmd imap.Commander = &commands.Store{ SeqSet: seqset, Item: item, Value: value, } if uid { cmd = &commands.Uid{Cmd: cmd} } var h responses.Handler if ch != nil { h = &responses.Fetch{Messages: ch, SeqSet: seqset, Uid: uid} } status, err := c.execute(cmd, h) if err != nil { return err } return status.Err() } // Store alters data associated with a message in the mailbox. If ch is not nil, // the updated value of the data will be sent to this channel. See RFC 3501 // section 6.4.6 for a list of items that can be updated. func (c *Client) Store(seqset *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) error { return c.store(false, seqset, item, value, ch) } // UidStore is identical to Store, but seqset is interpreted as containing // unique identifiers instead of message sequence numbers. func (c *Client) UidStore(seqset *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) error { return c.store(true, seqset, item, value, ch) } func (c *Client) copy(uid bool, seqset *imap.SeqSet, dest string) error { if c.State() != imap.SelectedState { return ErrNoMailboxSelected } var cmd imap.Commander = &commands.Copy{ SeqSet: seqset, Mailbox: dest, } if uid { cmd = &commands.Uid{Cmd: cmd} } status, err := c.execute(cmd, nil) if err != nil { return err } return status.Err() } // Copy copies the specified message(s) to the end of the specified destination // mailbox. func (c *Client) Copy(seqset *imap.SeqSet, dest string) error { return c.copy(false, seqset, dest) } // UidCopy is identical to Copy, but seqset is interpreted as containing unique // identifiers instead of message sequence numbers. func (c *Client) UidCopy(seqset *imap.SeqSet, dest string) error { return c.copy(true, seqset, dest) } func (c *Client) move(uid bool, seqset *imap.SeqSet, dest string) error { if c.State() != imap.SelectedState { return ErrNoMailboxSelected } if ok, err := c.Support("MOVE"); err != nil { return err } else if !ok { return c.moveFallback(uid, seqset, dest) } var cmd imap.Commander = &commands.Move{ SeqSet: seqset, Mailbox: dest, } if uid { cmd = &commands.Uid{Cmd: cmd} } if status, err := c.Execute(cmd, nil); err != nil { return err } else { return status.Err() } } // moveFallback uses COPY, STORE and EXPUNGE for servers which don't support // MOVE. func (c *Client) moveFallback(uid bool, seqset *imap.SeqSet, dest string) error { item := imap.FormatFlagsOp(imap.AddFlags, true) flags := []interface{}{imap.DeletedFlag} if uid { if err := c.UidCopy(seqset, dest); err != nil { return err } if err := c.UidStore(seqset, item, flags, nil); err != nil { return err } } else { if err := c.Copy(seqset, dest); err != nil { return err } if err := c.Store(seqset, item, flags, nil); err != nil { return err } } return c.Expunge(nil) } // Move moves the specified message(s) to the end of the specified destination // mailbox. // // If the server doesn't support the MOVE extension defined in RFC 6851, // go-imap will fallback to copy, store and expunge. func (c *Client) Move(seqset *imap.SeqSet, dest string) error { return c.move(false, seqset, dest) } // UidMove is identical to Move, but seqset is interpreted as containing unique // identifiers instead of message sequence numbers. func (c *Client) UidMove(seqset *imap.SeqSet, dest string) error { return c.move(true, seqset, dest) } // Unselect frees server's resources associated with the selected mailbox and // returns the server to the authenticated state. This command performs the same // actions as Close, except that no messages are permanently removed from the // currently selected mailbox. // // If client does not support the UNSELECT extension, ErrExtensionUnsupported // is returned. func (c *Client) Unselect() error { if ok, err := c.Support("UNSELECT"); !ok || err != nil { return ErrExtensionUnsupported } if c.State() != imap.SelectedState { return ErrNoMailboxSelected } cmd := &commands.Unselect{} if status, err := c.Execute(cmd, nil); err != nil { return err } else if err := status.Err(); err != nil { return err } c.SetState(imap.AuthenticatedState, nil) return nil }