This is the v1 version, had the v2 before.
This commit is contained in:
695
client/client.go
Normal file
695
client/client.go
Normal file
@@ -0,0 +1,695 @@
|
||||
// Package client provides an IMAP client.
|
||||
//
|
||||
// It is not safe to use the same Client from multiple goroutines. In general,
|
||||
// the IMAP protocol doesn't make it possible to send multiple independent
|
||||
// IMAP commands on the same connection.
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/commands"
|
||||
"github.com/emersion/go-imap/responses"
|
||||
)
|
||||
|
||||
// errClosed is used when a connection is closed while waiting for a command
|
||||
// response.
|
||||
var errClosed = fmt.Errorf("imap: connection closed")
|
||||
|
||||
// errUnregisterHandler is returned by a response handler to unregister itself.
|
||||
var errUnregisterHandler = fmt.Errorf("imap: unregister handler")
|
||||
|
||||
// Update is an unilateral server update.
|
||||
type Update interface {
|
||||
update()
|
||||
}
|
||||
|
||||
// StatusUpdate is delivered when a status update is received.
|
||||
type StatusUpdate struct {
|
||||
Status *imap.StatusResp
|
||||
}
|
||||
|
||||
func (u *StatusUpdate) update() {}
|
||||
|
||||
// MailboxUpdate is delivered when a mailbox status changes.
|
||||
type MailboxUpdate struct {
|
||||
Mailbox *imap.MailboxStatus
|
||||
}
|
||||
|
||||
func (u *MailboxUpdate) update() {}
|
||||
|
||||
// ExpungeUpdate is delivered when a message is deleted.
|
||||
type ExpungeUpdate struct {
|
||||
SeqNum uint32
|
||||
}
|
||||
|
||||
func (u *ExpungeUpdate) update() {}
|
||||
|
||||
// MessageUpdate is delivered when a message attribute changes.
|
||||
type MessageUpdate struct {
|
||||
Message *imap.Message
|
||||
}
|
||||
|
||||
func (u *MessageUpdate) update() {}
|
||||
|
||||
// Client is an IMAP client.
|
||||
type Client struct {
|
||||
conn *imap.Conn
|
||||
isTLS bool
|
||||
serverName string
|
||||
|
||||
loggedOut chan struct{}
|
||||
continues chan<- bool
|
||||
upgrading bool
|
||||
|
||||
handlers []responses.Handler
|
||||
handlersLocker sync.Mutex
|
||||
|
||||
// The current connection state.
|
||||
state imap.ConnState
|
||||
// The selected mailbox, if there is one.
|
||||
mailbox *imap.MailboxStatus
|
||||
// The cached server capabilities.
|
||||
caps map[string]bool
|
||||
// state, mailbox and caps may be accessed in different goroutines. Protect
|
||||
// access.
|
||||
locker sync.Mutex
|
||||
|
||||
// This flag is set when the first search query fails with a BADCHARSET
|
||||
// error. Subsequent queries will be performed with the US-ASCII
|
||||
// charset. According to RFC 3501, SEARCH must only support US-ASCII;
|
||||
// other charsets are optional.
|
||||
utf8SearchUnsupported bool
|
||||
|
||||
// A channel to which unilateral updates from the server will be sent. An
|
||||
// update can be one of: *StatusUpdate, *MailboxUpdate, *MessageUpdate,
|
||||
// *ExpungeUpdate. Note that blocking this channel blocks the whole client,
|
||||
// so it's recommended to use a separate goroutine and a buffered channel to
|
||||
// prevent deadlocks.
|
||||
Updates chan<- Update
|
||||
|
||||
// ErrorLog specifies an optional logger for errors accepting connections and
|
||||
// unexpected behavior from handlers. By default, logging goes to os.Stderr
|
||||
// via the log package's standard logger. The logger must be safe to use
|
||||
// simultaneously from multiple goroutines.
|
||||
ErrorLog imap.Logger
|
||||
|
||||
// Timeout specifies a maximum amount of time to wait on a command.
|
||||
//
|
||||
// A Timeout of zero means no timeout. This is the default.
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
func (c *Client) registerHandler(h responses.Handler) {
|
||||
if h == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.handlersLocker.Lock()
|
||||
c.handlers = append(c.handlers, h)
|
||||
c.handlersLocker.Unlock()
|
||||
}
|
||||
|
||||
func (c *Client) handle(resp imap.Resp) error {
|
||||
c.handlersLocker.Lock()
|
||||
for i := len(c.handlers) - 1; i >= 0; i-- {
|
||||
if err := c.handlers[i].Handle(resp); err != responses.ErrUnhandled {
|
||||
if err == errUnregisterHandler {
|
||||
c.handlers = append(c.handlers[:i], c.handlers[i+1:]...)
|
||||
err = nil
|
||||
}
|
||||
c.handlersLocker.Unlock()
|
||||
return err
|
||||
}
|
||||
}
|
||||
c.handlersLocker.Unlock()
|
||||
return responses.ErrUnhandled
|
||||
}
|
||||
|
||||
func (c *Client) reader() {
|
||||
defer close(c.loggedOut)
|
||||
// Loop while connected.
|
||||
for {
|
||||
connected, err := c.readOnce()
|
||||
if err != nil {
|
||||
c.ErrorLog.Println("error reading response:", err)
|
||||
}
|
||||
if !connected {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) readOnce() (bool, error) {
|
||||
if c.State() == imap.LogoutState {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
resp, err := imap.ReadResp(c.conn.Reader)
|
||||
if err == io.EOF || c.State() == imap.LogoutState {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
if imap.IsParseError(err) {
|
||||
return true, err
|
||||
} else {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.handle(resp); err != nil && err != responses.ErrUnhandled {
|
||||
c.ErrorLog.Println("cannot handle response ", resp, err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *Client) writeReply(reply []byte) error {
|
||||
if _, err := c.conn.Writer.Write(reply); err != nil {
|
||||
return err
|
||||
}
|
||||
// Flush reply
|
||||
return c.conn.Writer.Flush()
|
||||
}
|
||||
|
||||
type handleResult struct {
|
||||
status *imap.StatusResp
|
||||
err error
|
||||
}
|
||||
|
||||
func (c *Client) execute(cmdr imap.Commander, h responses.Handler) (*imap.StatusResp, error) {
|
||||
cmd := cmdr.Command()
|
||||
cmd.Tag = generateTag()
|
||||
|
||||
var replies <-chan []byte
|
||||
if replier, ok := h.(responses.Replier); ok {
|
||||
replies = replier.Replies()
|
||||
}
|
||||
|
||||
if c.Timeout > 0 {
|
||||
err := c.conn.SetDeadline(time.Now().Add(c.Timeout))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// It's possible the client had a timeout set from a previous command, but no
|
||||
// longer does. Ensure we respect that. The zero time means no deadline.
|
||||
if err := c.conn.SetDeadline(time.Time{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we are upgrading.
|
||||
upgrading := c.upgrading
|
||||
|
||||
// Add handler before sending command, to be sure to get the response in time
|
||||
// (in tests, the response is sent right after our command is received, so
|
||||
// sometimes the response was received before the setup of this handler)
|
||||
doneHandle := make(chan handleResult, 1)
|
||||
unregister := make(chan struct{})
|
||||
c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error {
|
||||
select {
|
||||
case <-unregister:
|
||||
// If an error occured while sending the command, abort
|
||||
return errUnregisterHandler
|
||||
default:
|
||||
}
|
||||
|
||||
if s, ok := resp.(*imap.StatusResp); ok && s.Tag == cmd.Tag {
|
||||
// This is the command's status response, we're done
|
||||
doneHandle <- handleResult{s, nil}
|
||||
// Special handling of connection upgrading.
|
||||
if upgrading {
|
||||
c.upgrading = false
|
||||
// Wait for upgrade to finish.
|
||||
c.conn.Wait()
|
||||
}
|
||||
// Cancel any pending literal write
|
||||
select {
|
||||
case c.continues <- false:
|
||||
default:
|
||||
}
|
||||
return errUnregisterHandler
|
||||
}
|
||||
|
||||
if h != nil {
|
||||
// Pass the response to the response handler
|
||||
if err := h.Handle(resp); err != nil && err != responses.ErrUnhandled {
|
||||
// If the response handler returns an error, abort
|
||||
doneHandle <- handleResult{nil, err}
|
||||
return errUnregisterHandler
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return responses.ErrUnhandled
|
||||
}))
|
||||
|
||||
// Send the command to the server
|
||||
if err := cmd.WriteTo(c.conn.Writer); err != nil {
|
||||
// Error while sending the command
|
||||
close(unregister)
|
||||
|
||||
if err, ok := err.(imap.LiteralLengthErr); ok {
|
||||
// Expected > Actual
|
||||
// The server is waiting for us to write
|
||||
// more bytes, we don't have them. Run.
|
||||
// Expected < Actual
|
||||
// We are about to send a potentially truncated message, we don't
|
||||
// want this (ths terminating CRLF is not sent at this point).
|
||||
c.conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
// Flush writer if we are upgrading
|
||||
if upgrading {
|
||||
if err := c.conn.Writer.Flush(); err != nil {
|
||||
// Error while sending the command
|
||||
close(unregister)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case reply := <-replies:
|
||||
// Response handler needs to send a reply (Used for AUTHENTICATE)
|
||||
if err := c.writeReply(reply); err != nil {
|
||||
close(unregister)
|
||||
return nil, err
|
||||
}
|
||||
case <-c.loggedOut:
|
||||
// If the connection is closed (such as from an I/O error), ensure we
|
||||
// realize this and don't block waiting on a response that will never
|
||||
// come. loggedOut is a channel that closes when the reader goroutine
|
||||
// ends.
|
||||
close(unregister)
|
||||
return nil, errClosed
|
||||
case result := <-doneHandle:
|
||||
return result.status, result.err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// State returns the current connection state.
|
||||
func (c *Client) State() imap.ConnState {
|
||||
c.locker.Lock()
|
||||
state := c.state
|
||||
c.locker.Unlock()
|
||||
return state
|
||||
}
|
||||
|
||||
// Mailbox returns the selected mailbox. It returns nil if there isn't one.
|
||||
func (c *Client) Mailbox() *imap.MailboxStatus {
|
||||
// c.Mailbox fields are not supposed to change, so we can return the pointer.
|
||||
c.locker.Lock()
|
||||
mbox := c.mailbox
|
||||
c.locker.Unlock()
|
||||
return mbox
|
||||
}
|
||||
|
||||
// SetState sets this connection's internal state.
|
||||
//
|
||||
// This function should not be called directly, it must only be used by
|
||||
// libraries implementing extensions of the IMAP protocol.
|
||||
func (c *Client) SetState(state imap.ConnState, mailbox *imap.MailboxStatus) {
|
||||
c.locker.Lock()
|
||||
c.state = state
|
||||
c.mailbox = mailbox
|
||||
c.locker.Unlock()
|
||||
}
|
||||
|
||||
// Execute executes a generic command. cmdr is a value that can be converted to
|
||||
// a raw command and h is a response handler. The function returns when the
|
||||
// command has completed or failed, in this case err is nil. A non-nil err value
|
||||
// indicates a network error.
|
||||
//
|
||||
// This function should not be called directly, it must only be used by
|
||||
// libraries implementing extensions of the IMAP protocol.
|
||||
func (c *Client) Execute(cmdr imap.Commander, h responses.Handler) (*imap.StatusResp, error) {
|
||||
return c.execute(cmdr, h)
|
||||
}
|
||||
|
||||
func (c *Client) handleContinuationReqs() {
|
||||
c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error {
|
||||
if _, ok := resp.(*imap.ContinuationReq); ok {
|
||||
go func() {
|
||||
c.continues <- true
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
return responses.ErrUnhandled
|
||||
}))
|
||||
}
|
||||
|
||||
func (c *Client) gotStatusCaps(args []interface{}) {
|
||||
c.locker.Lock()
|
||||
|
||||
c.caps = make(map[string]bool)
|
||||
for _, cap := range args {
|
||||
if cap, ok := cap.(string); ok {
|
||||
c.caps[cap] = true
|
||||
}
|
||||
}
|
||||
|
||||
c.locker.Unlock()
|
||||
}
|
||||
|
||||
// The server can send unilateral data. This function handles it.
|
||||
func (c *Client) handleUnilateral() {
|
||||
c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error {
|
||||
switch resp := resp.(type) {
|
||||
case *imap.StatusResp:
|
||||
if resp.Tag != "*" {
|
||||
return responses.ErrUnhandled
|
||||
}
|
||||
|
||||
switch resp.Type {
|
||||
case imap.StatusRespOk, imap.StatusRespNo, imap.StatusRespBad:
|
||||
if c.Updates != nil {
|
||||
c.Updates <- &StatusUpdate{resp}
|
||||
}
|
||||
case imap.StatusRespBye:
|
||||
c.locker.Lock()
|
||||
c.state = imap.LogoutState
|
||||
c.mailbox = nil
|
||||
c.locker.Unlock()
|
||||
|
||||
c.conn.Close()
|
||||
|
||||
if c.Updates != nil {
|
||||
c.Updates <- &StatusUpdate{resp}
|
||||
}
|
||||
default:
|
||||
return responses.ErrUnhandled
|
||||
}
|
||||
case *imap.DataResp:
|
||||
name, fields, ok := imap.ParseNamedResp(resp)
|
||||
if !ok {
|
||||
return responses.ErrUnhandled
|
||||
}
|
||||
|
||||
switch name {
|
||||
case "CAPABILITY":
|
||||
c.gotStatusCaps(fields)
|
||||
case "EXISTS":
|
||||
if c.Mailbox() == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if messages, err := imap.ParseNumber(fields[0]); err == nil {
|
||||
c.locker.Lock()
|
||||
c.mailbox.Messages = messages
|
||||
c.locker.Unlock()
|
||||
|
||||
c.mailbox.ItemsLocker.Lock()
|
||||
c.mailbox.Items[imap.StatusMessages] = nil
|
||||
c.mailbox.ItemsLocker.Unlock()
|
||||
}
|
||||
|
||||
if c.Updates != nil {
|
||||
c.Updates <- &MailboxUpdate{c.Mailbox()}
|
||||
}
|
||||
case "RECENT":
|
||||
if c.Mailbox() == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if recent, err := imap.ParseNumber(fields[0]); err == nil {
|
||||
c.locker.Lock()
|
||||
c.mailbox.Recent = recent
|
||||
c.locker.Unlock()
|
||||
|
||||
c.mailbox.ItemsLocker.Lock()
|
||||
c.mailbox.Items[imap.StatusRecent] = nil
|
||||
c.mailbox.ItemsLocker.Unlock()
|
||||
}
|
||||
|
||||
if c.Updates != nil {
|
||||
c.Updates <- &MailboxUpdate{c.Mailbox()}
|
||||
}
|
||||
case "EXPUNGE":
|
||||
seqNum, _ := imap.ParseNumber(fields[0])
|
||||
|
||||
if c.Updates != nil {
|
||||
c.Updates <- &ExpungeUpdate{seqNum}
|
||||
}
|
||||
case "FETCH":
|
||||
seqNum, _ := imap.ParseNumber(fields[0])
|
||||
fields, _ := fields[1].([]interface{})
|
||||
|
||||
msg := &imap.Message{SeqNum: seqNum}
|
||||
if err := msg.Parse(fields); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if c.Updates != nil {
|
||||
c.Updates <- &MessageUpdate{msg}
|
||||
}
|
||||
default:
|
||||
return responses.ErrUnhandled
|
||||
}
|
||||
default:
|
||||
return responses.ErrUnhandled
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
|
||||
func (c *Client) handleGreetAndStartReading() error {
|
||||
var greetErr error
|
||||
gotGreet := false
|
||||
|
||||
c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error {
|
||||
status, ok := resp.(*imap.StatusResp)
|
||||
if !ok {
|
||||
greetErr = fmt.Errorf("invalid greeting received from server: not a status response")
|
||||
return errUnregisterHandler
|
||||
}
|
||||
|
||||
c.locker.Lock()
|
||||
switch status.Type {
|
||||
case imap.StatusRespPreauth:
|
||||
c.state = imap.AuthenticatedState
|
||||
case imap.StatusRespBye:
|
||||
c.state = imap.LogoutState
|
||||
case imap.StatusRespOk:
|
||||
c.state = imap.NotAuthenticatedState
|
||||
default:
|
||||
c.state = imap.LogoutState
|
||||
c.locker.Unlock()
|
||||
greetErr = fmt.Errorf("invalid greeting received from server: %v", status.Type)
|
||||
return errUnregisterHandler
|
||||
}
|
||||
c.locker.Unlock()
|
||||
|
||||
if status.Code == imap.CodeCapability {
|
||||
c.gotStatusCaps(status.Arguments)
|
||||
}
|
||||
|
||||
gotGreet = true
|
||||
return errUnregisterHandler
|
||||
}))
|
||||
|
||||
// call `readOnce` until we get the greeting or an error
|
||||
for !gotGreet {
|
||||
connected, err := c.readOnce()
|
||||
// Check for read errors
|
||||
if err != nil {
|
||||
// return read errors
|
||||
return err
|
||||
}
|
||||
// Check for invalid greet
|
||||
if greetErr != nil {
|
||||
// return read errors
|
||||
return greetErr
|
||||
}
|
||||
// Check if connection was closed.
|
||||
if !connected {
|
||||
// connection closed.
|
||||
return io.EOF
|
||||
}
|
||||
}
|
||||
|
||||
// We got the greeting, now start the reader goroutine.
|
||||
go c.reader()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Upgrade a connection, e.g. wrap an unencrypted connection with an encrypted
|
||||
// tunnel.
|
||||
//
|
||||
// This function should not be called directly, it must only be used by
|
||||
// libraries implementing extensions of the IMAP protocol.
|
||||
func (c *Client) Upgrade(upgrader imap.ConnUpgrader) error {
|
||||
return c.conn.Upgrade(upgrader)
|
||||
}
|
||||
|
||||
// Writer returns the imap.Writer for this client's connection.
|
||||
//
|
||||
// This function should not be called directly, it must only be used by
|
||||
// libraries implementing extensions of the IMAP protocol.
|
||||
func (c *Client) Writer() *imap.Writer {
|
||||
return c.conn.Writer
|
||||
}
|
||||
|
||||
// IsTLS checks if this client's connection has TLS enabled.
|
||||
func (c *Client) IsTLS() bool {
|
||||
return c.isTLS
|
||||
}
|
||||
|
||||
// LoggedOut returns a channel which is closed when the connection to the server
|
||||
// is closed.
|
||||
func (c *Client) LoggedOut() <-chan struct{} {
|
||||
return c.loggedOut
|
||||
}
|
||||
|
||||
// SetDebug defines an io.Writer to which all network activity will be logged.
|
||||
// If nil is provided, network activity will not be logged.
|
||||
func (c *Client) SetDebug(w io.Writer) {
|
||||
// Need to send a command to unblock the reader goroutine.
|
||||
cmd := new(commands.Noop)
|
||||
err := c.Upgrade(func(conn net.Conn) (net.Conn, error) {
|
||||
// Flag connection as in upgrading
|
||||
c.upgrading = true
|
||||
if status, err := c.execute(cmd, nil); err != nil {
|
||||
return nil, err
|
||||
} else if err := status.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Wait for reader to block.
|
||||
c.conn.WaitReady()
|
||||
|
||||
c.conn.SetDebug(w)
|
||||
return conn, nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("SetDebug:", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// New creates a new client from an existing connection.
|
||||
func New(conn net.Conn) (*Client, error) {
|
||||
continues := make(chan bool)
|
||||
w := imap.NewClientWriter(nil, continues)
|
||||
r := imap.NewReader(nil)
|
||||
|
||||
c := &Client{
|
||||
conn: imap.NewConn(conn, r, w),
|
||||
loggedOut: make(chan struct{}),
|
||||
continues: continues,
|
||||
state: imap.ConnectingState,
|
||||
ErrorLog: log.New(os.Stderr, "imap/client: ", log.LstdFlags),
|
||||
}
|
||||
|
||||
c.handleContinuationReqs()
|
||||
c.handleUnilateral()
|
||||
if err := c.handleGreetAndStartReading(); err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
plusOk, _ := c.Support("LITERAL+")
|
||||
minusOk, _ := c.Support("LITERAL-")
|
||||
// We don't use non-sync literal if it is bigger than 4096 bytes, so
|
||||
// LITERAL- is fine too.
|
||||
c.conn.AllowAsyncLiterals = plusOk || minusOk
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Dial connects to an IMAP server using an unencrypted connection.
|
||||
func Dial(addr string) (*Client, error) {
|
||||
return DialWithDialer(new(net.Dialer), addr)
|
||||
}
|
||||
|
||||
type Dialer interface {
|
||||
// Dial connects to the given address.
|
||||
Dial(network, addr string) (net.Conn, error)
|
||||
}
|
||||
|
||||
// DialWithDialer connects to an IMAP server using an unencrypted connection
|
||||
// using dialer.Dial.
|
||||
//
|
||||
// Among other uses, this allows to apply a dial timeout.
|
||||
func DialWithDialer(dialer Dialer, addr string) (*Client, error) {
|
||||
conn, err := dialer.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We don't return to the caller until we try to receive a greeting. As such,
|
||||
// there is no way to set the client's Timeout for that action. As a
|
||||
// workaround, if the dialer has a timeout set, use that for the connection's
|
||||
// deadline.
|
||||
if netDialer, ok := dialer.(*net.Dialer); ok && netDialer.Timeout > 0 {
|
||||
err := conn.SetDeadline(time.Now().Add(netDialer.Timeout))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
c, err := New(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.serverName, _, _ = net.SplitHostPort(addr)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// DialTLS connects to an IMAP server using an encrypted connection.
|
||||
func DialTLS(addr string, tlsConfig *tls.Config) (*Client, error) {
|
||||
return DialWithDialerTLS(new(net.Dialer), addr, tlsConfig)
|
||||
}
|
||||
|
||||
// DialWithDialerTLS connects to an IMAP server using an encrypted connection
|
||||
// using dialer.Dial.
|
||||
//
|
||||
// Among other uses, this allows to apply a dial timeout.
|
||||
func DialWithDialerTLS(dialer Dialer, addr string, tlsConfig *tls.Config) (*Client, error) {
|
||||
conn, err := dialer.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serverName, _, _ := net.SplitHostPort(addr)
|
||||
if tlsConfig == nil {
|
||||
tlsConfig = &tls.Config{}
|
||||
}
|
||||
if tlsConfig.ServerName == "" {
|
||||
tlsConfig = tlsConfig.Clone()
|
||||
tlsConfig.ServerName = serverName
|
||||
}
|
||||
tlsConn := tls.Client(conn, tlsConfig)
|
||||
|
||||
// We don't return to the caller until we try to receive a greeting. As such,
|
||||
// there is no way to set the client's Timeout for that action. As a
|
||||
// workaround, if the dialer has a timeout set, use that for the connection's
|
||||
// deadline.
|
||||
if netDialer, ok := dialer.(*net.Dialer); ok && netDialer.Timeout > 0 {
|
||||
err := tlsConn.SetDeadline(time.Now().Add(netDialer.Timeout))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
c, err := New(tlsConn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.isTLS = true
|
||||
c.serverName = serverName
|
||||
return c, nil
|
||||
}
|
||||
187
client/client_test.go
Normal file
187
client/client_test.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
)
|
||||
|
||||
type cmdScanner struct {
|
||||
scanner *bufio.Scanner
|
||||
}
|
||||
|
||||
func (s *cmdScanner) ScanLine() string {
|
||||
s.scanner.Scan()
|
||||
return s.scanner.Text()
|
||||
}
|
||||
|
||||
func (s *cmdScanner) ScanCmd() (tag string, cmd string) {
|
||||
parts := strings.SplitN(s.ScanLine(), " ", 2)
|
||||
return parts[0], parts[1]
|
||||
}
|
||||
|
||||
func newCmdScanner(r io.Reader) *cmdScanner {
|
||||
return &cmdScanner{
|
||||
scanner: bufio.NewScanner(r),
|
||||
}
|
||||
}
|
||||
|
||||
type serverConn struct {
|
||||
*cmdScanner
|
||||
net.Conn
|
||||
net.Listener
|
||||
}
|
||||
|
||||
func (c *serverConn) Close() error {
|
||||
if err := c.Conn.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Listener.Close()
|
||||
}
|
||||
|
||||
func (c *serverConn) WriteString(s string) (n int, err error) {
|
||||
return io.WriteString(c.Conn, s)
|
||||
}
|
||||
|
||||
func newTestClient(t *testing.T) (c *Client, s *serverConn) {
|
||||
return newTestClientWithGreeting(t, "* OK [CAPABILITY IMAP4rev1 STARTTLS AUTH=PLAIN UNSELECT] Server ready.\r\n")
|
||||
}
|
||||
|
||||
func newTestClientWithGreeting(t *testing.T, greeting string) (c *Client, s *serverConn) {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if _, err := io.WriteString(conn, greeting); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
s = &serverConn{newCmdScanner(conn), conn, l}
|
||||
close(done)
|
||||
}()
|
||||
|
||||
c, err = Dial(l.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
<-done
|
||||
return
|
||||
}
|
||||
|
||||
func setClientState(c *Client, state imap.ConnState, mailbox *imap.MailboxStatus) {
|
||||
c.locker.Lock()
|
||||
c.state = state
|
||||
c.mailbox = mailbox
|
||||
c.locker.Unlock()
|
||||
}
|
||||
|
||||
func TestClient(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
if ok, err := c.Support("IMAP4rev1"); err != nil {
|
||||
t.Fatal("c.Support(IMAP4rev1) =", err)
|
||||
} else if !ok {
|
||||
t.Fatal("c.Support(IMAP4rev1) = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_SetDebug(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
var b bytes.Buffer
|
||||
done := make(chan error)
|
||||
|
||||
go func() {
|
||||
c.SetDebug(&b)
|
||||
done <- nil
|
||||
}()
|
||||
if tag, cmd := s.ScanCmd(); cmd != "NOOP" {
|
||||
t.Fatal("Bad command:", cmd)
|
||||
} else {
|
||||
s.WriteString(tag + " OK NOOP completed.\r\n")
|
||||
}
|
||||
// wait for SetDebug to finish.
|
||||
<-done
|
||||
|
||||
go func() {
|
||||
_, err := c.Capability()
|
||||
done <- err
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "CAPABILITY" {
|
||||
t.Fatal("Bad command:", cmd)
|
||||
}
|
||||
|
||||
s.WriteString("* CAPABILITY IMAP4rev1\r\n")
|
||||
s.WriteString(tag + " OK CAPABILITY completed.\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatal("c.Capability() =", err)
|
||||
}
|
||||
|
||||
if b.Len() == 0 {
|
||||
t.Error("empty debug buffer")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_unilateral(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.SelectedState, imap.NewMailboxStatus("INBOX", nil))
|
||||
|
||||
updates := make(chan Update, 1)
|
||||
c.Updates = updates
|
||||
|
||||
s.WriteString("* 42 EXISTS\r\n")
|
||||
if update, ok := (<-updates).(*MailboxUpdate); !ok || update.Mailbox.Messages != 42 {
|
||||
t.Errorf("Invalid messages count: expected %v but got %v", 42, update.Mailbox.Messages)
|
||||
}
|
||||
|
||||
s.WriteString("* 587 RECENT\r\n")
|
||||
if update, ok := (<-updates).(*MailboxUpdate); !ok || update.Mailbox.Recent != 587 {
|
||||
t.Errorf("Invalid recent count: expected %v but got %v", 587, update.Mailbox.Recent)
|
||||
}
|
||||
|
||||
s.WriteString("* 65535 EXPUNGE\r\n")
|
||||
if update, ok := (<-updates).(*ExpungeUpdate); !ok || update.SeqNum != 65535 {
|
||||
t.Errorf("Invalid expunged sequence number: expected %v but got %v", 65535, update.SeqNum)
|
||||
}
|
||||
|
||||
s.WriteString("* 431 FETCH (FLAGS (\\Seen))\r\n")
|
||||
if update, ok := (<-updates).(*MessageUpdate); !ok || update.Message.SeqNum != 431 {
|
||||
t.Errorf("Invalid expunged sequence number: expected %v but got %v", 431, update.Message.SeqNum)
|
||||
}
|
||||
|
||||
s.WriteString("* OK Reticulating splines...\r\n")
|
||||
if update, ok := (<-updates).(*StatusUpdate); !ok || update.Status.Info != "Reticulating splines..." {
|
||||
t.Errorf("Invalid info: got %v", update.Status.Info)
|
||||
}
|
||||
|
||||
s.WriteString("* NO Kansai band competition is in 30 seconds !\r\n")
|
||||
if update, ok := (<-updates).(*StatusUpdate); !ok || update.Status.Info != "Kansai band competition is in 30 seconds !" {
|
||||
t.Errorf("Invalid warning: got %v", update.Status.Info)
|
||||
}
|
||||
|
||||
s.WriteString("* BAD Battery level too low, shutting down.\r\n")
|
||||
if update, ok := (<-updates).(*StatusUpdate); !ok || update.Status.Info != "Battery level too low, shutting down." {
|
||||
t.Errorf("Invalid error: got %v", update.Status.Info)
|
||||
}
|
||||
}
|
||||
88
client/cmd_any.go
Normal file
88
client/cmd_any.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/commands"
|
||||
)
|
||||
|
||||
// ErrAlreadyLoggedOut is returned if Logout is called when the client is
|
||||
// already logged out.
|
||||
var ErrAlreadyLoggedOut = errors.New("Already logged out")
|
||||
|
||||
// Capability requests a listing of capabilities that the server supports.
|
||||
// Capabilities are often returned by the server with the greeting or with the
|
||||
// STARTTLS and LOGIN responses, so usually explicitly requesting capabilities
|
||||
// isn't needed.
|
||||
//
|
||||
// Most of the time, Support should be used instead.
|
||||
func (c *Client) Capability() (map[string]bool, error) {
|
||||
cmd := &commands.Capability{}
|
||||
|
||||
if status, err := c.execute(cmd, nil); err != nil {
|
||||
return nil, err
|
||||
} else if err := status.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.locker.Lock()
|
||||
caps := c.caps
|
||||
c.locker.Unlock()
|
||||
return caps, nil
|
||||
}
|
||||
|
||||
// Support checks if cap is a capability supported by the server. If the server
|
||||
// hasn't sent its capabilities yet, Support requests them.
|
||||
func (c *Client) Support(cap string) (bool, error) {
|
||||
c.locker.Lock()
|
||||
ok := c.caps != nil
|
||||
c.locker.Unlock()
|
||||
|
||||
// If capabilities are not cached, request them
|
||||
if !ok {
|
||||
if _, err := c.Capability(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
c.locker.Lock()
|
||||
supported := c.caps[cap]
|
||||
c.locker.Unlock()
|
||||
|
||||
return supported, nil
|
||||
}
|
||||
|
||||
// Noop always succeeds and does nothing.
|
||||
//
|
||||
// It can be used as a periodic poll for new messages or message status updates
|
||||
// during a period of inactivity. It can also be used to reset any inactivity
|
||||
// autologout timer on the server.
|
||||
func (c *Client) Noop() error {
|
||||
cmd := new(commands.Noop)
|
||||
|
||||
status, err := c.execute(cmd, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return status.Err()
|
||||
}
|
||||
|
||||
// Logout gracefully closes the connection.
|
||||
func (c *Client) Logout() error {
|
||||
if c.State() == imap.LogoutState {
|
||||
return ErrAlreadyLoggedOut
|
||||
}
|
||||
|
||||
cmd := new(commands.Logout)
|
||||
|
||||
if status, err := c.execute(cmd, nil); err == errClosed {
|
||||
// Server closed connection, that's what we want anyway
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else if status != nil {
|
||||
return status.Err()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
80
client/cmd_any_test.go
Normal file
80
client/cmd_any_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
)
|
||||
|
||||
func TestClient_Capability(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
var caps map[string]bool
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
var err error
|
||||
caps, err = c.Capability()
|
||||
done <- err
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "CAPABILITY" {
|
||||
t.Fatalf("client sent command %v, want CAPABILITY", cmd)
|
||||
}
|
||||
s.WriteString("* CAPABILITY IMAP4rev1 XTEST\r\n")
|
||||
s.WriteString(tag + " OK CAPABILITY completed.\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Error("c.Capability() = ", err)
|
||||
}
|
||||
|
||||
if !caps["XTEST"] {
|
||||
t.Error("XTEST capability missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Noop(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Noop()
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "NOOP" {
|
||||
t.Fatalf("client sent command %v, want NOOP", cmd)
|
||||
}
|
||||
s.WriteString(tag + " OK NOOP completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Error("c.Noop() = ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Logout(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Logout()
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "LOGOUT" {
|
||||
t.Fatalf("client sent command %v, want LOGOUT", cmd)
|
||||
}
|
||||
s.WriteString("* BYE Client asked to close the connection.\r\n")
|
||||
s.WriteString(tag + " OK LOGOUT completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Error("c.Logout() =", err)
|
||||
}
|
||||
|
||||
if state := c.State(); state != imap.LogoutState {
|
||||
t.Errorf("c.State() = %v, want %v", state, imap.LogoutState)
|
||||
}
|
||||
}
|
||||
380
client/cmd_auth.go
Normal file
380
client/cmd_auth.go
Normal file
@@ -0,0 +1,380 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/commands"
|
||||
"github.com/emersion/go-imap/responses"
|
||||
)
|
||||
|
||||
// ErrNotLoggedIn is returned if a function that requires the client to be
|
||||
// logged in is called then the client isn't.
|
||||
var ErrNotLoggedIn = errors.New("Not logged in")
|
||||
|
||||
func (c *Client) ensureAuthenticated() error {
|
||||
state := c.State()
|
||||
if state != imap.AuthenticatedState && state != imap.SelectedState {
|
||||
return ErrNotLoggedIn
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Select selects a mailbox so that messages in the mailbox can be accessed. Any
|
||||
// currently selected mailbox is deselected before attempting the new selection.
|
||||
// Even if the readOnly parameter is set to false, the server can decide to open
|
||||
// the mailbox in read-only mode.
|
||||
func (c *Client) Select(name string, readOnly bool) (*imap.MailboxStatus, error) {
|
||||
if err := c.ensureAuthenticated(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmd := &commands.Select{
|
||||
Mailbox: name,
|
||||
ReadOnly: readOnly,
|
||||
}
|
||||
|
||||
mbox := &imap.MailboxStatus{Name: name, Items: make(map[imap.StatusItem]interface{})}
|
||||
res := &responses.Select{
|
||||
Mailbox: mbox,
|
||||
}
|
||||
c.locker.Lock()
|
||||
c.mailbox = mbox
|
||||
c.locker.Unlock()
|
||||
|
||||
status, err := c.execute(cmd, res)
|
||||
if err != nil {
|
||||
c.locker.Lock()
|
||||
c.mailbox = nil
|
||||
c.locker.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
if err := status.Err(); err != nil {
|
||||
c.locker.Lock()
|
||||
c.mailbox = nil
|
||||
c.locker.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.locker.Lock()
|
||||
mbox.ReadOnly = (status.Code == imap.CodeReadOnly)
|
||||
c.state = imap.SelectedState
|
||||
c.locker.Unlock()
|
||||
return mbox, nil
|
||||
}
|
||||
|
||||
// Create creates a mailbox with the given name.
|
||||
func (c *Client) Create(name string) error {
|
||||
if err := c.ensureAuthenticated(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := &commands.Create{
|
||||
Mailbox: name,
|
||||
}
|
||||
|
||||
status, err := c.execute(cmd, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return status.Err()
|
||||
}
|
||||
|
||||
// Delete permanently removes the mailbox with the given name.
|
||||
func (c *Client) Delete(name string) error {
|
||||
if err := c.ensureAuthenticated(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := &commands.Delete{
|
||||
Mailbox: name,
|
||||
}
|
||||
|
||||
status, err := c.execute(cmd, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return status.Err()
|
||||
}
|
||||
|
||||
// Rename changes the name of a mailbox.
|
||||
func (c *Client) Rename(existingName, newName string) error {
|
||||
if err := c.ensureAuthenticated(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := &commands.Rename{
|
||||
Existing: existingName,
|
||||
New: newName,
|
||||
}
|
||||
|
||||
status, err := c.execute(cmd, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return status.Err()
|
||||
}
|
||||
|
||||
// Subscribe adds the specified mailbox name to the server's set of "active" or
|
||||
// "subscribed" mailboxes.
|
||||
func (c *Client) Subscribe(name string) error {
|
||||
if err := c.ensureAuthenticated(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := &commands.Subscribe{
|
||||
Mailbox: name,
|
||||
}
|
||||
|
||||
status, err := c.execute(cmd, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return status.Err()
|
||||
}
|
||||
|
||||
// Unsubscribe removes the specified mailbox name from the server's set of
|
||||
// "active" or "subscribed" mailboxes.
|
||||
func (c *Client) Unsubscribe(name string) error {
|
||||
if err := c.ensureAuthenticated(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := &commands.Unsubscribe{
|
||||
Mailbox: name,
|
||||
}
|
||||
|
||||
status, err := c.execute(cmd, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return status.Err()
|
||||
}
|
||||
|
||||
// List returns a subset of names from the complete set of all names available
|
||||
// to the client.
|
||||
//
|
||||
// An empty name argument is a special request to return the hierarchy delimiter
|
||||
// and the root name of the name given in the reference. The character "*" is a
|
||||
// wildcard, and matches zero or more characters at this position. The
|
||||
// character "%" is similar to "*", but it does not match a hierarchy delimiter.
|
||||
func (c *Client) List(ref, name string, ch chan *imap.MailboxInfo) error {
|
||||
defer close(ch)
|
||||
|
||||
if err := c.ensureAuthenticated(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := &commands.List{
|
||||
Reference: ref,
|
||||
Mailbox: name,
|
||||
}
|
||||
res := &responses.List{Mailboxes: ch}
|
||||
|
||||
status, err := c.execute(cmd, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return status.Err()
|
||||
}
|
||||
|
||||
// Lsub returns a subset of names from the set of names that the user has
|
||||
// declared as being "active" or "subscribed".
|
||||
func (c *Client) Lsub(ref, name string, ch chan *imap.MailboxInfo) error {
|
||||
defer close(ch)
|
||||
|
||||
if err := c.ensureAuthenticated(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := &commands.List{
|
||||
Reference: ref,
|
||||
Mailbox: name,
|
||||
Subscribed: true,
|
||||
}
|
||||
res := &responses.List{
|
||||
Mailboxes: ch,
|
||||
Subscribed: true,
|
||||
}
|
||||
|
||||
status, err := c.execute(cmd, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return status.Err()
|
||||
}
|
||||
|
||||
// Status requests the status of the indicated mailbox. It does not change the
|
||||
// currently selected mailbox, nor does it affect the state of any messages in
|
||||
// the queried mailbox.
|
||||
//
|
||||
// See RFC 3501 section 6.3.10 for a list of items that can be requested.
|
||||
func (c *Client) Status(name string, items []imap.StatusItem) (*imap.MailboxStatus, error) {
|
||||
if err := c.ensureAuthenticated(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmd := &commands.Status{
|
||||
Mailbox: name,
|
||||
Items: items,
|
||||
}
|
||||
res := &responses.Status{
|
||||
Mailbox: new(imap.MailboxStatus),
|
||||
}
|
||||
|
||||
status, err := c.execute(cmd, res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res.Mailbox, status.Err()
|
||||
}
|
||||
|
||||
// Append appends the literal argument as a new message to the end of the
|
||||
// specified destination mailbox. This argument SHOULD be in the format of an
|
||||
// RFC 2822 message. flags and date are optional arguments and can be set to
|
||||
// nil and the empty struct.
|
||||
func (c *Client) Append(mbox string, flags []string, date time.Time, msg imap.Literal) error {
|
||||
if err := c.ensureAuthenticated(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := &commands.Append{
|
||||
Mailbox: mbox,
|
||||
Flags: flags,
|
||||
Date: date,
|
||||
Message: msg,
|
||||
}
|
||||
|
||||
status, err := c.execute(cmd, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return status.Err()
|
||||
}
|
||||
|
||||
// Enable requests the server to enable the named extensions. The extensions
|
||||
// which were successfully enabled are returned.
|
||||
//
|
||||
// See RFC 5161 section 3.1.
|
||||
func (c *Client) Enable(caps []string) ([]string, error) {
|
||||
if ok, err := c.Support("ENABLE"); !ok || err != nil {
|
||||
return nil, ErrExtensionUnsupported
|
||||
}
|
||||
|
||||
// ENABLE is invalid if a mailbox has been selected.
|
||||
if c.State() != imap.AuthenticatedState {
|
||||
return nil, ErrNotLoggedIn
|
||||
}
|
||||
|
||||
cmd := &commands.Enable{Caps: caps}
|
||||
res := &responses.Enabled{}
|
||||
|
||||
if status, err := c.Execute(cmd, res); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return res.Caps, status.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) idle(stop <-chan struct{}) error {
|
||||
cmd := &commands.Idle{}
|
||||
|
||||
res := &responses.Idle{
|
||||
Stop: stop,
|
||||
RepliesCh: make(chan []byte, 10),
|
||||
}
|
||||
|
||||
if status, err := c.Execute(cmd, res); err != nil {
|
||||
return err
|
||||
} else {
|
||||
return status.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// IdleOptions holds options for Client.Idle.
|
||||
type IdleOptions struct {
|
||||
// LogoutTimeout is used to avoid being logged out by the server when
|
||||
// idling. Each LogoutTimeout, the IDLE command is restarted. If set to
|
||||
// zero, a default is used. If negative, this behavior is disabled.
|
||||
LogoutTimeout time.Duration
|
||||
// Poll interval when the server doesn't support IDLE. If zero, a default
|
||||
// is used. If negative, polling is always disabled.
|
||||
PollInterval time.Duration
|
||||
}
|
||||
|
||||
// Idle indicates to the server that the client is ready to receive unsolicited
|
||||
// mailbox update messages. When the client wants to send commands again, it
|
||||
// must first close stop.
|
||||
//
|
||||
// If the server doesn't support IDLE, go-imap falls back to polling.
|
||||
func (c *Client) Idle(stop <-chan struct{}, opts *IdleOptions) error {
|
||||
if ok, err := c.Support("IDLE"); err != nil {
|
||||
return err
|
||||
} else if !ok {
|
||||
return c.idleFallback(stop, opts)
|
||||
}
|
||||
|
||||
logoutTimeout := 25 * time.Minute
|
||||
if opts != nil {
|
||||
if opts.LogoutTimeout > 0 {
|
||||
logoutTimeout = opts.LogoutTimeout
|
||||
} else if opts.LogoutTimeout < 0 {
|
||||
return c.idle(stop)
|
||||
}
|
||||
}
|
||||
|
||||
t := time.NewTicker(logoutTimeout)
|
||||
defer t.Stop()
|
||||
|
||||
for {
|
||||
stopOrRestart := make(chan struct{})
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.idle(stopOrRestart)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-t.C:
|
||||
close(stopOrRestart)
|
||||
if err := <-done; err != nil {
|
||||
return err
|
||||
}
|
||||
case <-stop:
|
||||
close(stopOrRestart)
|
||||
return <-done
|
||||
case err := <-done:
|
||||
close(stopOrRestart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) idleFallback(stop <-chan struct{}, opts *IdleOptions) error {
|
||||
pollInterval := time.Minute
|
||||
if opts != nil {
|
||||
if opts.PollInterval > 0 {
|
||||
pollInterval = opts.PollInterval
|
||||
} else if opts.PollInterval < 0 {
|
||||
return ErrExtensionUnsupported
|
||||
}
|
||||
}
|
||||
|
||||
t := time.NewTicker(pollInterval)
|
||||
defer t.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-t.C:
|
||||
if err := c.Noop(); err != nil {
|
||||
return err
|
||||
}
|
||||
case <-stop:
|
||||
return nil
|
||||
case <-c.LoggedOut():
|
||||
return errors.New("disconnected while idling")
|
||||
}
|
||||
}
|
||||
}
|
||||
499
client/cmd_auth_test.go
Normal file
499
client/cmd_auth_test.go
Normal file
@@ -0,0 +1,499 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
)
|
||||
|
||||
func TestClient_Select(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.AuthenticatedState, nil)
|
||||
|
||||
var mbox *imap.MailboxStatus
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
var err error
|
||||
mbox, err = c.Select("INBOX", false)
|
||||
done <- err
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "SELECT INBOX" {
|
||||
t.Fatalf("client sent command %v, want SELECT \"INBOX\"", cmd)
|
||||
}
|
||||
|
||||
s.WriteString("* 172 EXISTS\r\n")
|
||||
s.WriteString("* 1 RECENT\r\n")
|
||||
s.WriteString("* OK [UNSEEN 12] Message 12 is first unseen\r\n")
|
||||
s.WriteString("* OK [UIDVALIDITY 3857529045] UIDs valid\r\n")
|
||||
s.WriteString("* OK [UIDNEXT 4392] Predicted next UID\r\n")
|
||||
s.WriteString("* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n")
|
||||
s.WriteString("* OK [PERMANENTFLAGS (\\Deleted \\Seen \\*)] Limited\r\n")
|
||||
s.WriteString(tag + " OK SELECT completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Select() = %v", err)
|
||||
}
|
||||
|
||||
want := &imap.MailboxStatus{
|
||||
Name: "INBOX",
|
||||
ReadOnly: false,
|
||||
Flags: []string{imap.AnsweredFlag, imap.FlaggedFlag, imap.DeletedFlag, imap.SeenFlag, imap.DraftFlag},
|
||||
PermanentFlags: []string{imap.DeletedFlag, imap.SeenFlag, "\\*"},
|
||||
UnseenSeqNum: 12,
|
||||
Messages: 172,
|
||||
Recent: 1,
|
||||
UidNext: 4392,
|
||||
UidValidity: 3857529045,
|
||||
}
|
||||
mbox.Items = nil
|
||||
if !reflect.DeepEqual(mbox, want) {
|
||||
t.Errorf("c.Select() = \n%+v\n want \n%+v", mbox, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Select_ReadOnly(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.AuthenticatedState, nil)
|
||||
|
||||
var mbox *imap.MailboxStatus
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
var err error
|
||||
mbox, err = c.Select("INBOX", true)
|
||||
done <- err
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "EXAMINE INBOX" {
|
||||
t.Fatalf("client sent command %v, want EXAMINE \"INBOX\"", cmd)
|
||||
}
|
||||
|
||||
s.WriteString(tag + " OK [READ-ONLY] EXAMINE completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Select() = %v", err)
|
||||
}
|
||||
|
||||
if !mbox.ReadOnly {
|
||||
t.Errorf("c.Select().ReadOnly = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Create(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.AuthenticatedState, nil)
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Create("New Mailbox")
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "CREATE \"New Mailbox\"" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "CREATE \"New Mailbox\"")
|
||||
}
|
||||
|
||||
s.WriteString(tag + " OK CREATE completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Create() = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Delete(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.AuthenticatedState, nil)
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Delete("Old Mailbox")
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "DELETE \"Old Mailbox\"" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "DELETE \"Old Mailbox\"")
|
||||
}
|
||||
|
||||
s.WriteString(tag + " OK DELETE completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Delete() = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Rename(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.AuthenticatedState, nil)
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Rename("Old Mailbox", "New Mailbox")
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "RENAME \"Old Mailbox\" \"New Mailbox\"" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "RENAME \"Old Mailbox\" \"New Mailbox\"")
|
||||
}
|
||||
|
||||
s.WriteString(tag + " OK RENAME completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Rename() = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Subscribe(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.AuthenticatedState, nil)
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Subscribe("Mailbox")
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "SUBSCRIBE \"Mailbox\"" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "SUBSCRIBE \"Mailbox\"")
|
||||
}
|
||||
|
||||
s.WriteString(tag + " OK SUBSCRIBE completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Subscribe() = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Unsubscribe(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.AuthenticatedState, nil)
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Unsubscribe("Mailbox")
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "UNSUBSCRIBE \"Mailbox\"" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "UNSUBSCRIBE \"Mailbox\"")
|
||||
}
|
||||
|
||||
s.WriteString(tag + " OK UNSUBSCRIBE completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Unsubscribe() = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_List(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.AuthenticatedState, nil)
|
||||
|
||||
done := make(chan error, 1)
|
||||
mailboxes := make(chan *imap.MailboxInfo, 3)
|
||||
go func() {
|
||||
done <- c.List("", "%", mailboxes)
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "LIST \"\" \"%\"" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "LIST \"\" \"%\"")
|
||||
}
|
||||
|
||||
s.WriteString("* LIST (flag1) \"/\" INBOX\r\n")
|
||||
s.WriteString("* LIST (flag2 flag3) \"/\" Drafts\r\n")
|
||||
s.WriteString("* LIST () \"/\" Sent\r\n")
|
||||
s.WriteString(tag + " OK LIST completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.List() = %v", err)
|
||||
}
|
||||
|
||||
want := []struct {
|
||||
name string
|
||||
attributes []string
|
||||
}{
|
||||
{"INBOX", []string{"flag1"}},
|
||||
{"Drafts", []string{"flag2", "flag3"}},
|
||||
{"Sent", []string{}},
|
||||
}
|
||||
|
||||
i := 0
|
||||
for mbox := range mailboxes {
|
||||
if mbox.Name != want[i].name {
|
||||
t.Errorf("Bad mailbox name for %v: %v, want %v", i, mbox.Name, want[i].name)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(mbox.Attributes, want[i].attributes) {
|
||||
t.Errorf("Bad mailbox attributes for %v: %v, want %v", i, mbox.Attributes, want[i].attributes)
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Lsub(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.AuthenticatedState, nil)
|
||||
|
||||
done := make(chan error, 1)
|
||||
mailboxes := make(chan *imap.MailboxInfo, 1)
|
||||
go func() {
|
||||
done <- c.Lsub("", "%", mailboxes)
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "LSUB \"\" \"%\"" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "LSUB \"\" \"%\"")
|
||||
}
|
||||
|
||||
s.WriteString("* LSUB () \"/\" INBOX\r\n")
|
||||
s.WriteString(tag + " OK LSUB completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Lsub() = %v", err)
|
||||
}
|
||||
|
||||
mbox := <-mailboxes
|
||||
if mbox.Name != "INBOX" {
|
||||
t.Errorf("Bad mailbox name: %v", mbox.Name)
|
||||
}
|
||||
if len(mbox.Attributes) != 0 {
|
||||
t.Errorf("Bad mailbox flags: %v", mbox.Attributes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Status(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.AuthenticatedState, nil)
|
||||
|
||||
done := make(chan error, 1)
|
||||
var mbox *imap.MailboxStatus
|
||||
go func() {
|
||||
var err error
|
||||
mbox, err = c.Status("INBOX", []imap.StatusItem{imap.StatusMessages, imap.StatusRecent})
|
||||
done <- err
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "STATUS INBOX (MESSAGES RECENT)" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "STATUS \"INBOX\" (MESSAGES RECENT)")
|
||||
}
|
||||
|
||||
s.WriteString("* STATUS INBOX (MESSAGES 42 RECENT 1)\r\n")
|
||||
s.WriteString(tag + " OK STATUS completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Status() = %v", err)
|
||||
}
|
||||
|
||||
if mbox.Messages != 42 {
|
||||
t.Errorf("Bad mailbox messages: %v", mbox.Messages)
|
||||
}
|
||||
if mbox.Recent != 1 {
|
||||
t.Errorf("Bad mailbox recent: %v", mbox.Recent)
|
||||
}
|
||||
}
|
||||
|
||||
type literalWrap struct {
|
||||
io.Reader
|
||||
L int
|
||||
}
|
||||
|
||||
func (lw literalWrap) Len() int {
|
||||
return lw.L
|
||||
}
|
||||
|
||||
func TestClient_Append_SmallerLiteral(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.AuthenticatedState, nil)
|
||||
|
||||
msg := "Hello World!\r\nHello Gophers!\r\n"
|
||||
date := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
|
||||
flags := []string{imap.SeenFlag, imap.DraftFlag}
|
||||
|
||||
r := bytes.NewBufferString(msg)
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Append("INBOX", flags, date, literalWrap{r, 35})
|
||||
|
||||
// The buffer is not flushed on error, force it so io.ReadFull can
|
||||
// continue.
|
||||
c.conn.Flush()
|
||||
}()
|
||||
|
||||
tag, _ := s.ScanCmd()
|
||||
s.WriteString("+ send literal\r\n")
|
||||
|
||||
b := make([]byte, 30)
|
||||
// The client will close connection.
|
||||
if _, err := io.ReadFull(s, b); err != io.EOF {
|
||||
t.Error("Expected EOF, got", err)
|
||||
}
|
||||
|
||||
s.WriteString(tag + " OK APPEND completed\r\n")
|
||||
|
||||
err, ok := (<-done).(imap.LiteralLengthErr)
|
||||
if !ok {
|
||||
t.Fatalf("c.Append() = %v", err)
|
||||
}
|
||||
if err.Expected != 35 {
|
||||
t.Fatalf("err.Expected = %v", err.Expected)
|
||||
}
|
||||
if err.Actual != 30 {
|
||||
t.Fatalf("err.Actual = %v", err.Actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Append_BiggerLiteral(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.AuthenticatedState, nil)
|
||||
|
||||
msg := "Hello World!\r\nHello Gophers!\r\n"
|
||||
date := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
|
||||
flags := []string{imap.SeenFlag, imap.DraftFlag}
|
||||
|
||||
r := bytes.NewBufferString(msg)
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Append("INBOX", flags, date, literalWrap{r, 25})
|
||||
|
||||
// The buffer is not flushed on error, force it so io.ReadFull can
|
||||
// continue.
|
||||
c.conn.Flush()
|
||||
}()
|
||||
|
||||
tag, _ := s.ScanCmd()
|
||||
s.WriteString("+ send literal\r\n")
|
||||
|
||||
// The client will close connection.
|
||||
b := make([]byte, 25)
|
||||
if _, err := io.ReadFull(s, b); err != io.EOF {
|
||||
t.Error("Expected EOF, got", err)
|
||||
}
|
||||
|
||||
s.WriteString(tag + " OK APPEND completed\r\n")
|
||||
|
||||
err, ok := (<-done).(imap.LiteralLengthErr)
|
||||
if !ok {
|
||||
t.Fatalf("c.Append() = %v", err)
|
||||
}
|
||||
if err.Expected != 25 {
|
||||
t.Fatalf("err.Expected = %v", err.Expected)
|
||||
}
|
||||
if err.Actual != 30 {
|
||||
t.Fatalf("err.Actual = %v", err.Actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Append(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.AuthenticatedState, nil)
|
||||
|
||||
msg := "Hello World!\r\nHello Gophers!\r\n"
|
||||
date := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
|
||||
flags := []string{imap.SeenFlag, imap.DraftFlag}
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Append("INBOX", flags, date, bytes.NewBufferString(msg))
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "APPEND INBOX (\\Seen \\Draft) \"10-Nov-2009 23:00:00 +0000\" {30}" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "APPEND \"INBOX\" (\\Seen \\Draft) \"10-Nov-2009 23:00:00 +0000\" {30}")
|
||||
}
|
||||
|
||||
s.WriteString("+ send literal\r\n")
|
||||
|
||||
b := make([]byte, 30)
|
||||
if _, err := io.ReadFull(s, b); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if string(b) != msg {
|
||||
t.Fatal("Bad literal:", string(b))
|
||||
}
|
||||
|
||||
s.WriteString(tag + " OK APPEND completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Append() = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Append_failed(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.AuthenticatedState, nil)
|
||||
|
||||
// First the server refuses
|
||||
|
||||
msg := "First try"
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Append("INBOX", nil, time.Time{}, bytes.NewBufferString(msg))
|
||||
}()
|
||||
|
||||
tag, _ := s.ScanCmd()
|
||||
s.WriteString(tag + " BAD APPEND failed\r\n")
|
||||
|
||||
if err := <-done; err == nil {
|
||||
t.Fatal("c.Append() = nil, want an error from the server")
|
||||
}
|
||||
|
||||
// Try a second time, the server accepts
|
||||
|
||||
msg = "Second try"
|
||||
go func() {
|
||||
done <- c.Append("INBOX", nil, time.Time{}, bytes.NewBufferString(msg))
|
||||
}()
|
||||
|
||||
tag, _ = s.ScanCmd()
|
||||
s.WriteString("+ send literal\r\n")
|
||||
|
||||
b := make([]byte, len(msg))
|
||||
if _, err := io.ReadFull(s, b); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if string(b) != msg {
|
||||
t.Fatal("Bad literal:", string(b))
|
||||
}
|
||||
|
||||
s.WriteString(tag + " OK APPEND completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Append() = %v", err)
|
||||
}
|
||||
}
|
||||
174
client/cmd_noauth.go
Normal file
174
client/cmd_noauth.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/commands"
|
||||
"github.com/emersion/go-imap/responses"
|
||||
"github.com/emersion/go-sasl"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrAlreadyLoggedIn is returned if Login or Authenticate is called when the
|
||||
// client is already logged in.
|
||||
ErrAlreadyLoggedIn = errors.New("Already logged in")
|
||||
// ErrTLSAlreadyEnabled is returned if StartTLS is called when TLS is already
|
||||
// enabled.
|
||||
ErrTLSAlreadyEnabled = errors.New("TLS is already enabled")
|
||||
// ErrLoginDisabled is returned if Login or Authenticate is called when the
|
||||
// server has disabled authentication. Most of the time, calling enabling TLS
|
||||
// solves the problem.
|
||||
ErrLoginDisabled = errors.New("Login is disabled in current state")
|
||||
)
|
||||
|
||||
// SupportStartTLS checks if the server supports STARTTLS.
|
||||
func (c *Client) SupportStartTLS() (bool, error) {
|
||||
return c.Support("STARTTLS")
|
||||
}
|
||||
|
||||
// StartTLS starts TLS negotiation.
|
||||
func (c *Client) StartTLS(tlsConfig *tls.Config) error {
|
||||
if c.isTLS {
|
||||
return ErrTLSAlreadyEnabled
|
||||
}
|
||||
|
||||
if tlsConfig == nil {
|
||||
tlsConfig = new(tls.Config)
|
||||
}
|
||||
if tlsConfig.ServerName == "" {
|
||||
tlsConfig = tlsConfig.Clone()
|
||||
tlsConfig.ServerName = c.serverName
|
||||
}
|
||||
|
||||
cmd := new(commands.StartTLS)
|
||||
|
||||
err := c.Upgrade(func(conn net.Conn) (net.Conn, error) {
|
||||
// Flag connection as in upgrading
|
||||
c.upgrading = true
|
||||
if status, err := c.execute(cmd, nil); err != nil {
|
||||
return nil, err
|
||||
} else if err := status.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Wait for reader to block.
|
||||
c.conn.WaitReady()
|
||||
tlsConn := tls.Client(conn, tlsConfig)
|
||||
if err := tlsConn.Handshake(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Capabilities change when TLS is enabled
|
||||
c.locker.Lock()
|
||||
c.caps = nil
|
||||
c.locker.Unlock()
|
||||
|
||||
return tlsConn, nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.isTLS = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// SupportAuth checks if the server supports a given authentication mechanism.
|
||||
func (c *Client) SupportAuth(mech string) (bool, error) {
|
||||
return c.Support("AUTH=" + mech)
|
||||
}
|
||||
|
||||
// Authenticate indicates a SASL authentication mechanism to the server. If the
|
||||
// server supports the requested authentication mechanism, it performs an
|
||||
// authentication protocol exchange to authenticate and identify the client.
|
||||
func (c *Client) Authenticate(auth sasl.Client) error {
|
||||
if c.State() != imap.NotAuthenticatedState {
|
||||
return ErrAlreadyLoggedIn
|
||||
}
|
||||
|
||||
mech, ir, err := auth.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := &commands.Authenticate{
|
||||
Mechanism: mech,
|
||||
}
|
||||
|
||||
irOk, err := c.Support("SASL-IR")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if irOk {
|
||||
cmd.InitialResponse = ir
|
||||
}
|
||||
|
||||
res := &responses.Authenticate{
|
||||
Mechanism: auth,
|
||||
InitialResponse: ir,
|
||||
RepliesCh: make(chan []byte, 10),
|
||||
}
|
||||
if irOk {
|
||||
res.InitialResponse = nil
|
||||
}
|
||||
|
||||
status, err := c.execute(cmd, res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = status.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.locker.Lock()
|
||||
c.state = imap.AuthenticatedState
|
||||
c.caps = nil // Capabilities change when user is logged in
|
||||
c.locker.Unlock()
|
||||
|
||||
if status.Code == "CAPABILITY" {
|
||||
c.gotStatusCaps(status.Arguments)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Login identifies the client to the server and carries the plaintext password
|
||||
// authenticating this user.
|
||||
func (c *Client) Login(username, password string) error {
|
||||
if state := c.State(); state == imap.AuthenticatedState || state == imap.SelectedState {
|
||||
return ErrAlreadyLoggedIn
|
||||
}
|
||||
|
||||
c.locker.Lock()
|
||||
loginDisabled := c.caps != nil && c.caps["LOGINDISABLED"]
|
||||
c.locker.Unlock()
|
||||
if loginDisabled {
|
||||
return ErrLoginDisabled
|
||||
}
|
||||
|
||||
cmd := &commands.Login{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
|
||||
status, err := c.execute(cmd, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = status.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.locker.Lock()
|
||||
c.state = imap.AuthenticatedState
|
||||
c.caps = nil // Capabilities change when user is logged in
|
||||
c.locker.Unlock()
|
||||
|
||||
if status.Code == "CAPABILITY" {
|
||||
c.gotStatusCaps(status.Arguments)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
299
client/cmd_noauth_test.go
Normal file
299
client/cmd_noauth_test.go
Normal file
@@ -0,0 +1,299 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/internal"
|
||||
"github.com/emersion/go-sasl"
|
||||
)
|
||||
|
||||
func TestClient_StartTLS(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
cert, err := tls.X509KeyPair(internal.LocalhostCert, internal.LocalhostKey)
|
||||
if err != nil {
|
||||
t.Fatal("cannot load test certificate:", err)
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
|
||||
if c.IsTLS() {
|
||||
t.Fatal("Client has TLS enabled before STARTTLS")
|
||||
}
|
||||
|
||||
if ok, err := c.SupportStartTLS(); err != nil {
|
||||
t.Fatalf("c.SupportStartTLS() = %v", err)
|
||||
} else if !ok {
|
||||
t.Fatalf("c.SupportStartTLS() = %v, want true", ok)
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.StartTLS(tlsConfig)
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "STARTTLS" {
|
||||
t.Fatalf("client sent command %v, want STARTTLS", cmd)
|
||||
}
|
||||
s.WriteString(tag + " OK Begin TLS negotiation now\r\n")
|
||||
|
||||
ss := tls.Server(s.Conn, tlsConfig)
|
||||
if err := ss.Handshake(); err != nil {
|
||||
t.Fatal("cannot perform TLS handshake:", err)
|
||||
}
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Error("c.StartTLS() =", err)
|
||||
}
|
||||
|
||||
if !c.IsTLS() {
|
||||
t.Errorf("Client has not TLS enabled after STARTTLS")
|
||||
}
|
||||
|
||||
go func() {
|
||||
_, err := c.Capability()
|
||||
done <- err
|
||||
}()
|
||||
|
||||
tag, cmd = newCmdScanner(ss).ScanCmd()
|
||||
if cmd != "CAPABILITY" {
|
||||
t.Fatalf("client sent command %v, want CAPABILITY", cmd)
|
||||
}
|
||||
io.WriteString(ss, "* CAPABILITY IMAP4rev1 AUTH=PLAIN\r\n")
|
||||
io.WriteString(ss, tag+" OK CAPABILITY completed.\r\n")
|
||||
}
|
||||
|
||||
func TestClient_Authenticate(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
if ok, err := c.SupportAuth(sasl.Plain); err != nil {
|
||||
t.Fatalf("c.SupportAuth(sasl.Plain) = %v", err)
|
||||
} else if !ok {
|
||||
t.Fatalf("c.SupportAuth(sasl.Plain) = %v, want true", ok)
|
||||
}
|
||||
|
||||
sasl := sasl.NewPlainClient("", "username", "password")
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Authenticate(sasl)
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "AUTHENTICATE PLAIN" {
|
||||
t.Fatalf("client sent command %v, want AUTHENTICATE PLAIN", cmd)
|
||||
}
|
||||
|
||||
s.WriteString("+ \r\n")
|
||||
|
||||
wantLine := "AHVzZXJuYW1lAHBhc3N3b3Jk"
|
||||
if line := s.ScanLine(); line != wantLine {
|
||||
t.Fatalf("client sent auth %v, want %v", line, wantLine)
|
||||
}
|
||||
|
||||
s.WriteString(tag + " OK AUTHENTICATE completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Authenticate() = %v", err)
|
||||
}
|
||||
|
||||
if state := c.State(); state != imap.AuthenticatedState {
|
||||
t.Errorf("c.State() = %v, want %v", state, imap.AuthenticatedState)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Authenticate_InitialResponse(t *testing.T) {
|
||||
c, s := newTestClientWithGreeting(t, "* OK [CAPABILITY IMAP4rev1 SASL-IR STARTTLS AUTH=PLAIN] Server ready.\r\n")
|
||||
defer s.Close()
|
||||
|
||||
if ok, err := c.SupportAuth(sasl.Plain); err != nil {
|
||||
t.Fatalf("c.SupportAuth(sasl.Plain) = %v", err)
|
||||
} else if !ok {
|
||||
t.Fatalf("c.SupportAuth(sasl.Plain) = %v, want true", ok)
|
||||
}
|
||||
|
||||
sasl := sasl.NewPlainClient("", "username", "password")
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Authenticate(sasl)
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk" {
|
||||
t.Fatalf("client sent command %v, want AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk", cmd)
|
||||
}
|
||||
|
||||
s.WriteString(tag + " OK AUTHENTICATE completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Authenticate() = %v", err)
|
||||
}
|
||||
|
||||
if state := c.State(); state != imap.AuthenticatedState {
|
||||
t.Errorf("c.State() = %v, want %v", state, imap.AuthenticatedState)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Login_Success(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Login("username", "password")
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "LOGIN \"username\" \"password\"" {
|
||||
t.Fatalf("client sent command %v, want LOGIN username password", cmd)
|
||||
}
|
||||
s.WriteString(tag + " OK LOGIN completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Login() = %v", err)
|
||||
}
|
||||
|
||||
if state := c.State(); state != imap.AuthenticatedState {
|
||||
t.Errorf("c.State() = %v, want %v", state, imap.AuthenticatedState)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Login_8bitSync(t *testing.T) {
|
||||
c, s := newTestClientWithGreeting(t, "* OK [CAPABILITY IMAP4rev1 SASL-IR STARTTLS AUTH=PLAIN] Server ready.\r\n")
|
||||
defer s.Close()
|
||||
|
||||
// Use of UTF-8 will force go-imap to send password in literal.
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Login("username", "пароль")
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "LOGIN \"username\" {12}" {
|
||||
t.Fatalf("client sent command %v, want LOGIN \"username\" {12}", cmd)
|
||||
}
|
||||
s.WriteString("+ send literal\r\n")
|
||||
pass := s.ScanLine()
|
||||
if pass != "пароль" {
|
||||
t.Fatalf("client sent %v, want {12}'пароль' literal", pass)
|
||||
}
|
||||
s.WriteString(tag + " OK LOGIN completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Login() = %v", err)
|
||||
}
|
||||
|
||||
if state := c.State(); state != imap.AuthenticatedState {
|
||||
t.Errorf("c.State() = %v, want %v", state, imap.AuthenticatedState)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Login_8bitNonSync(t *testing.T) {
|
||||
c, s := newTestClientWithGreeting(t, "* OK [CAPABILITY IMAP4rev1 LITERAL- SASL-IR STARTTLS AUTH=PLAIN] Server ready.\r\n")
|
||||
defer s.Close()
|
||||
|
||||
// Use of UTF-8 will force go-imap to send password in literal.
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Login("username", "пароль")
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "LOGIN \"username\" {12+}" {
|
||||
t.Fatalf("client sent command %v, want LOGIN \"username\" {12+}", cmd)
|
||||
}
|
||||
pass := s.ScanLine()
|
||||
if pass != "пароль" {
|
||||
t.Fatalf("client sent %v, want {12+}'пароль' literal", pass)
|
||||
}
|
||||
s.WriteString(tag + " OK LOGIN completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Login() = %v", err)
|
||||
}
|
||||
|
||||
if state := c.State(); state != imap.AuthenticatedState {
|
||||
t.Errorf("c.State() = %v, want %v", state, imap.AuthenticatedState)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Login_Error(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Login("username", "password")
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "LOGIN \"username\" \"password\"" {
|
||||
t.Fatalf("client sent command %v, want LOGIN username password", cmd)
|
||||
}
|
||||
s.WriteString(tag + " NO LOGIN incorrect\r\n")
|
||||
|
||||
if err := <-done; err == nil {
|
||||
t.Fatal("c.Login() = nil, want LOGIN incorrect")
|
||||
}
|
||||
|
||||
if state := c.State(); state != imap.NotAuthenticatedState {
|
||||
t.Errorf("c.State() = %v, want %v", state, imap.NotAuthenticatedState)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Login_State_Allowed(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Login("username", "password")
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "LOGIN \"username\" \"password\"" {
|
||||
t.Fatalf("client sent command %v, want LOGIN \"username\" \"password\"", cmd)
|
||||
}
|
||||
s.WriteString(tag + " OK LOGIN completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Login() = %v", err)
|
||||
}
|
||||
|
||||
if state := c.State(); state != imap.AuthenticatedState {
|
||||
t.Errorf("c.State() = %v, want %v", state, imap.AuthenticatedState)
|
||||
}
|
||||
|
||||
go func() {
|
||||
done <- c.Login("username", "password")
|
||||
}()
|
||||
if err := <-done; err != ErrAlreadyLoggedIn {
|
||||
t.Fatalf("c.Login() = %v, want %v", err, ErrAlreadyLoggedIn)
|
||||
}
|
||||
|
||||
go func() {
|
||||
done <- c.Logout()
|
||||
}()
|
||||
|
||||
s.ScanCmd()
|
||||
s.WriteString("* BYE Client asked to close the connection.\r\n")
|
||||
s.WriteString(tag + " OK LOGOUT completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Logout() = %v", err)
|
||||
}
|
||||
|
||||
if err := c.Login("username", "password"); err == ErrAlreadyLoggedIn {
|
||||
t.Errorf("Client is logout, login must not give %v", ErrAlreadyLoggedIn)
|
||||
}
|
||||
}
|
||||
372
client/cmd_selected.go
Normal file
372
client/cmd_selected.go
Normal file
@@ -0,0 +1,372 @@
|
||||
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) {
|
||||
charset := "UTF-8"
|
||||
if c.utf8SearchUnsupported {
|
||||
charset = "US-ASCII"
|
||||
}
|
||||
ids, status, err := c.executeSearch(uid, criteria, charset)
|
||||
if status != nil && status.Code == imap.CodeBadCharset {
|
||||
// Some servers don't support UTF-8
|
||||
ids, _, err = c.executeSearch(uid, criteria, "US-ASCII")
|
||||
c.utf8SearchUnsupported = true
|
||||
}
|
||||
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
|
||||
}
|
||||
816
client/cmd_selected_test.go
Normal file
816
client/cmd_selected_test.go
Normal file
@@ -0,0 +1,816 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/textproto"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
)
|
||||
|
||||
func TestClient_Check(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.SelectedState, nil)
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Check()
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "CHECK" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "CHECK")
|
||||
}
|
||||
|
||||
s.WriteString(tag + " OK CHECK completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Check() = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Close(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.SelectedState, &imap.MailboxStatus{Name: "INBOX"})
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Close()
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "CLOSE" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "CLOSE")
|
||||
}
|
||||
|
||||
s.WriteString(tag + " OK CLOSE completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Check() = %v", err)
|
||||
}
|
||||
|
||||
if state := c.State(); state != imap.AuthenticatedState {
|
||||
t.Errorf("Bad state: %v", state)
|
||||
}
|
||||
if mailbox := c.Mailbox(); mailbox != nil {
|
||||
t.Errorf("Client selected mailbox is not nil: %v", mailbox)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Expunge(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.SelectedState, nil)
|
||||
|
||||
done := make(chan error, 1)
|
||||
expunged := make(chan uint32, 4)
|
||||
go func() {
|
||||
done <- c.Expunge(expunged)
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "EXPUNGE" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "EXPUNGE")
|
||||
}
|
||||
|
||||
s.WriteString("* 3 EXPUNGE\r\n")
|
||||
s.WriteString("* 3 EXPUNGE\r\n")
|
||||
s.WriteString("* 5 EXPUNGE\r\n")
|
||||
s.WriteString("* 8 EXPUNGE\r\n")
|
||||
s.WriteString(tag + " OK EXPUNGE completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Expunge() = %v", err)
|
||||
}
|
||||
|
||||
expected := []uint32{3, 3, 5, 8}
|
||||
|
||||
i := 0
|
||||
for id := range expunged {
|
||||
if id != expected[i] {
|
||||
t.Errorf("Bad expunged sequence number: got %v instead of %v", id, expected[i])
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Search(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.SelectedState, nil)
|
||||
|
||||
date, _ := time.Parse(imap.DateLayout, "1-Feb-1994")
|
||||
criteria := &imap.SearchCriteria{
|
||||
WithFlags: []string{imap.DeletedFlag},
|
||||
Header: textproto.MIMEHeader{"From": {"Smith"}},
|
||||
Since: date,
|
||||
Not: []*imap.SearchCriteria{{
|
||||
Header: textproto.MIMEHeader{"To": {"Pauline"}},
|
||||
}},
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
var results []uint32
|
||||
go func() {
|
||||
var err error
|
||||
results, err = c.Search(criteria)
|
||||
done <- err
|
||||
}()
|
||||
|
||||
wantCmd := `SEARCH CHARSET UTF-8 SINCE "1-Feb-1994" FROM "Smith" DELETED NOT (TO "Pauline")`
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != wantCmd {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, wantCmd)
|
||||
}
|
||||
|
||||
s.WriteString("* SEARCH 2 84 882\r\n")
|
||||
s.WriteString(tag + " OK SEARCH completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Search() = %v", err)
|
||||
}
|
||||
|
||||
want := []uint32{2, 84, 882}
|
||||
if !reflect.DeepEqual(results, want) {
|
||||
t.Errorf("c.Search() = %v, want %v", results, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Search_Badcharset(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.SelectedState, nil)
|
||||
|
||||
date, _ := time.Parse(imap.DateLayout, "1-Feb-1994")
|
||||
criteria := &imap.SearchCriteria{
|
||||
WithFlags: []string{imap.DeletedFlag},
|
||||
Header: textproto.MIMEHeader{"From": {"Smith"}},
|
||||
Since: date,
|
||||
Not: []*imap.SearchCriteria{{
|
||||
Header: textproto.MIMEHeader{"To": {"Pauline"}},
|
||||
}},
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
var results []uint32
|
||||
go func() {
|
||||
var err error
|
||||
results, err = c.Search(criteria)
|
||||
done <- err
|
||||
}()
|
||||
|
||||
// first search call with default UTF-8 charset (assume server does not support UTF-8)
|
||||
wantCmd := `SEARCH CHARSET UTF-8 SINCE "1-Feb-1994" FROM "Smith" DELETED NOT (TO "Pauline")`
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != wantCmd {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, wantCmd)
|
||||
}
|
||||
|
||||
s.WriteString(tag + " NO [BADCHARSET (US-ASCII)]\r\n")
|
||||
|
||||
// internal fall-back to US-ASCII which sets utf8SearchUnsupported = true
|
||||
wantCmd = `SEARCH CHARSET US-ASCII SINCE "1-Feb-1994" FROM "Smith" DELETED NOT (TO "Pauline")`
|
||||
tag, cmd = s.ScanCmd()
|
||||
if cmd != wantCmd {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, wantCmd)
|
||||
}
|
||||
|
||||
s.WriteString("* SEARCH 2 84 882\r\n")
|
||||
s.WriteString(tag + " OK SEARCH completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
want := []uint32{2, 84, 882}
|
||||
if !reflect.DeepEqual(results, want) {
|
||||
t.Errorf("c.Search() = %v, want %v", results, want)
|
||||
}
|
||||
|
||||
if !c.utf8SearchUnsupported {
|
||||
t.Fatal("client should have utf8SearchUnsupported set to true")
|
||||
}
|
||||
|
||||
// second call to search (with utf8SearchUnsupported=true)
|
||||
go func() {
|
||||
var err error
|
||||
results, err = c.Search(criteria)
|
||||
done <- err
|
||||
}()
|
||||
|
||||
tag, cmd = s.ScanCmd()
|
||||
if cmd != wantCmd {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, wantCmd)
|
||||
}
|
||||
|
||||
s.WriteString("* SEARCH 2 84 882\r\n")
|
||||
s.WriteString(tag + " OK SEARCH completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Search() = %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(results, want) {
|
||||
t.Errorf("c.Search() = %v, want %v", results, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Search_Uid(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.SelectedState, nil)
|
||||
|
||||
criteria := &imap.SearchCriteria{
|
||||
WithoutFlags: []string{imap.DeletedFlag},
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
var results []uint32
|
||||
go func() {
|
||||
var err error
|
||||
results, err = c.UidSearch(criteria)
|
||||
done <- err
|
||||
}()
|
||||
|
||||
wantCmd := "UID SEARCH CHARSET UTF-8 UNDELETED"
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != wantCmd {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, wantCmd)
|
||||
}
|
||||
|
||||
s.WriteString("* SEARCH 1 78 2010\r\n")
|
||||
s.WriteString(tag + " OK UID SEARCH completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Search() = %v", err)
|
||||
}
|
||||
|
||||
want := []uint32{1, 78, 2010}
|
||||
if !reflect.DeepEqual(results, want) {
|
||||
t.Errorf("c.Search() = %v, want %v", results, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Search_Uid_Badcharset(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.SelectedState, nil)
|
||||
|
||||
criteria := &imap.SearchCriteria{
|
||||
WithoutFlags: []string{imap.DeletedFlag},
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
var results []uint32
|
||||
go func() {
|
||||
var err error
|
||||
results, err = c.UidSearch(criteria)
|
||||
done <- err
|
||||
}()
|
||||
|
||||
// first search call with default UTF-8 charset (assume server does not support UTF-8)
|
||||
wantCmd := "UID SEARCH CHARSET UTF-8 UNDELETED"
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != wantCmd {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, wantCmd)
|
||||
}
|
||||
|
||||
s.WriteString(tag + " NO [BADCHARSET (US-ASCII)]\r\n")
|
||||
|
||||
// internal fall-back to US-ASCII which sets utf8SearchUnsupported = true
|
||||
wantCmd = "UID SEARCH CHARSET US-ASCII UNDELETED"
|
||||
tag, cmd = s.ScanCmd()
|
||||
if cmd != wantCmd {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, wantCmd)
|
||||
}
|
||||
|
||||
s.WriteString("* SEARCH 1 78 2010\r\n")
|
||||
s.WriteString(tag + " OK UID SEARCH completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.UidSearch() = %v", err)
|
||||
}
|
||||
|
||||
want := []uint32{1, 78, 2010}
|
||||
if !reflect.DeepEqual(results, want) {
|
||||
t.Errorf("c.UidSearch() = %v, want %v", results, want)
|
||||
}
|
||||
|
||||
if !c.utf8SearchUnsupported {
|
||||
t.Fatal("client should have utf8SearchUnsupported set to true")
|
||||
}
|
||||
|
||||
// second call to search (with utf8SearchUnsupported=true)
|
||||
go func() {
|
||||
var err error
|
||||
results, err = c.UidSearch(criteria)
|
||||
done <- err
|
||||
}()
|
||||
|
||||
tag, cmd = s.ScanCmd()
|
||||
if cmd != wantCmd {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, wantCmd)
|
||||
}
|
||||
|
||||
s.WriteString("* SEARCH 1 78 2010\r\n")
|
||||
s.WriteString(tag + " OK UID SEARCH completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.UidSearch() = %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(results, want) {
|
||||
t.Errorf("c.UidSearch() = %v, want %v", results, want)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestClient_Fetch(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.SelectedState, nil)
|
||||
|
||||
seqset, _ := imap.ParseSeqSet("2:3")
|
||||
fields := []imap.FetchItem{imap.FetchUid, imap.FetchItem("BODY[]")}
|
||||
|
||||
done := make(chan error, 1)
|
||||
messages := make(chan *imap.Message, 2)
|
||||
go func() {
|
||||
done <- c.Fetch(seqset, fields, messages)
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "FETCH 2:3 (UID BODY[])" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "FETCH 2:3 (UID BODY[])")
|
||||
}
|
||||
|
||||
s.WriteString("* 2 FETCH (UID 42 BODY[] {16}\r\n")
|
||||
s.WriteString("I love potatoes.")
|
||||
s.WriteString(")\r\n")
|
||||
|
||||
s.WriteString("* 3 FETCH (UID 28 BODY[] {12}\r\n")
|
||||
s.WriteString("Hello World!")
|
||||
s.WriteString(")\r\n")
|
||||
|
||||
s.WriteString(tag + " OK FETCH completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Fetch() = %v", err)
|
||||
}
|
||||
|
||||
section, _ := imap.ParseBodySectionName("BODY[]")
|
||||
|
||||
msg := <-messages
|
||||
if msg.SeqNum != 2 {
|
||||
t.Errorf("First message has bad sequence number: %v", msg.SeqNum)
|
||||
}
|
||||
if msg.Uid != 42 {
|
||||
t.Errorf("First message has bad UID: %v", msg.Uid)
|
||||
}
|
||||
if body, _ := ioutil.ReadAll(msg.GetBody(section)); string(body) != "I love potatoes." {
|
||||
t.Errorf("First message has bad body: %q", body)
|
||||
}
|
||||
|
||||
msg = <-messages
|
||||
if msg.SeqNum != 3 {
|
||||
t.Errorf("First message has bad sequence number: %v", msg.SeqNum)
|
||||
}
|
||||
if msg.Uid != 28 {
|
||||
t.Errorf("Second message has bad UID: %v", msg.Uid)
|
||||
}
|
||||
if body, _ := ioutil.ReadAll(msg.GetBody(section)); string(body) != "Hello World!" {
|
||||
t.Errorf("Second message has bad body: %q", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Fetch_ClosedState(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.AuthenticatedState, nil)
|
||||
|
||||
seqset, _ := imap.ParseSeqSet("2:3")
|
||||
fields := []imap.FetchItem{imap.FetchUid, imap.FetchItem("BODY[]")}
|
||||
|
||||
done := make(chan error, 1)
|
||||
messages := make(chan *imap.Message, 2)
|
||||
go func() {
|
||||
done <- c.Fetch(seqset, fields, messages)
|
||||
}()
|
||||
|
||||
_, more := <-messages
|
||||
|
||||
if more {
|
||||
t.Fatalf("Messages channel has more messages, but it must be closed with no messages sent")
|
||||
}
|
||||
|
||||
err := <-done
|
||||
|
||||
if err != ErrNoMailboxSelected {
|
||||
t.Fatalf("Expected error to be IMAP Client ErrNoMailboxSelected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Fetch_Partial(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.SelectedState, nil)
|
||||
|
||||
seqset, _ := imap.ParseSeqSet("1")
|
||||
fields := []imap.FetchItem{imap.FetchItem("BODY.PEEK[]<0.10>")}
|
||||
|
||||
done := make(chan error, 1)
|
||||
messages := make(chan *imap.Message, 1)
|
||||
go func() {
|
||||
done <- c.Fetch(seqset, fields, messages)
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "FETCH 1 (BODY.PEEK[]<0.10>)" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "FETCH 1 (BODY.PEEK[]<0.10>)")
|
||||
}
|
||||
|
||||
s.WriteString("* 1 FETCH (BODY[]<0> {10}\r\n")
|
||||
s.WriteString("I love pot")
|
||||
s.WriteString(")\r\n")
|
||||
|
||||
s.WriteString(tag + " OK FETCH completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Fetch() = %v", err)
|
||||
}
|
||||
|
||||
section, _ := imap.ParseBodySectionName("BODY.PEEK[]<0.10>")
|
||||
|
||||
msg := <-messages
|
||||
if body, _ := ioutil.ReadAll(msg.GetBody(section)); string(body) != "I love pot" {
|
||||
t.Errorf("Message has bad body: %q", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Fetch_part(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.SelectedState, nil)
|
||||
|
||||
seqset, _ := imap.ParseSeqSet("1")
|
||||
fields := []imap.FetchItem{imap.FetchItem("BODY.PEEK[1]")}
|
||||
|
||||
done := make(chan error, 1)
|
||||
messages := make(chan *imap.Message, 1)
|
||||
go func() {
|
||||
done <- c.Fetch(seqset, fields, messages)
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "FETCH 1 (BODY.PEEK[1])" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "FETCH 1 (BODY.PEEK[1])")
|
||||
}
|
||||
|
||||
s.WriteString("* 1 FETCH (BODY[1] {3}\r\n")
|
||||
s.WriteString("Hey")
|
||||
s.WriteString(")\r\n")
|
||||
|
||||
s.WriteString(tag + " OK FETCH completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Fetch() = %v", err)
|
||||
}
|
||||
|
||||
<-messages
|
||||
}
|
||||
|
||||
func TestClient_Fetch_Uid(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.SelectedState, nil)
|
||||
|
||||
seqset, _ := imap.ParseSeqSet("1:867")
|
||||
fields := []imap.FetchItem{imap.FetchFlags}
|
||||
|
||||
done := make(chan error, 1)
|
||||
messages := make(chan *imap.Message, 1)
|
||||
go func() {
|
||||
done <- c.UidFetch(seqset, fields, messages)
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "UID FETCH 1:867 (FLAGS)" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "UID FETCH 1:867 (FLAGS)")
|
||||
}
|
||||
|
||||
s.WriteString("* 23 FETCH (UID 42 FLAGS (\\Seen))\r\n")
|
||||
s.WriteString(tag + " OK UID FETCH completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.UidFetch() = %v", err)
|
||||
}
|
||||
|
||||
msg := <-messages
|
||||
if msg.SeqNum != 23 {
|
||||
t.Errorf("First message has bad sequence number: %v", msg.SeqNum)
|
||||
}
|
||||
if msg.Uid != 42 {
|
||||
t.Errorf("Message has bad UID: %v", msg.Uid)
|
||||
}
|
||||
if len(msg.Flags) != 1 || msg.Flags[0] != "\\Seen" {
|
||||
t.Errorf("Message has bad flags: %v", msg.Flags)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Fetch_Unilateral(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.SelectedState, nil)
|
||||
|
||||
seqset, _ := imap.ParseSeqSet("1:4")
|
||||
fields := []imap.FetchItem{imap.FetchFlags}
|
||||
|
||||
done := make(chan error, 1)
|
||||
messages := make(chan *imap.Message, 3)
|
||||
go func() {
|
||||
done <- c.Fetch(seqset, fields, messages)
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "FETCH 1:4 (FLAGS)" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "FETCH 1:4 (FLAGS)")
|
||||
}
|
||||
|
||||
s.WriteString("* 2 FETCH (FLAGS (\\Seen))\r\n")
|
||||
s.WriteString("* 123 FETCH (FLAGS (\\Deleted))\r\n")
|
||||
s.WriteString("* 4 FETCH (FLAGS (\\Seen))\r\n")
|
||||
s.WriteString(tag + " OK FETCH completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Fetch() = %v", err)
|
||||
}
|
||||
|
||||
msg := <-messages
|
||||
if msg.SeqNum != 2 {
|
||||
t.Errorf("First message has bad sequence number: %v", msg.SeqNum)
|
||||
}
|
||||
msg = <-messages
|
||||
if msg.SeqNum != 4 {
|
||||
t.Errorf("Second message has bad sequence number: %v", msg.SeqNum)
|
||||
}
|
||||
|
||||
_, ok := <-messages
|
||||
if ok {
|
||||
t.Errorf("More than two messages")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Fetch_Unilateral_Uid(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.SelectedState, nil)
|
||||
|
||||
seqset, _ := imap.ParseSeqSet("1:4")
|
||||
fields := []imap.FetchItem{imap.FetchFlags}
|
||||
|
||||
done := make(chan error, 1)
|
||||
messages := make(chan *imap.Message, 3)
|
||||
go func() {
|
||||
done <- c.UidFetch(seqset, fields, messages)
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "UID FETCH 1:4 (FLAGS)" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "UID FETCH 1:4 (FLAGS)")
|
||||
}
|
||||
|
||||
s.WriteString("* 23 FETCH (UID 2 FLAGS (\\Seen))\r\n")
|
||||
s.WriteString("* 123 FETCH (FLAGS (\\Deleted))\r\n")
|
||||
s.WriteString("* 49 FETCH (UID 4 FLAGS (\\Seen))\r\n")
|
||||
s.WriteString(tag + " OK FETCH completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Fetch() = %v", err)
|
||||
}
|
||||
|
||||
msg := <-messages
|
||||
if msg.Uid != 2 {
|
||||
t.Errorf("First message has bad UID: %v", msg.Uid)
|
||||
}
|
||||
msg = <-messages
|
||||
if msg.Uid != 4 {
|
||||
t.Errorf("Second message has bad UID: %v", msg.Uid)
|
||||
}
|
||||
|
||||
_, ok := <-messages
|
||||
if ok {
|
||||
t.Errorf("More than two messages")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Fetch_Uid_Dynamic(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.SelectedState, nil)
|
||||
|
||||
seqset, _ := imap.ParseSeqSet("4:*")
|
||||
fields := []imap.FetchItem{imap.FetchFlags}
|
||||
|
||||
done := make(chan error, 1)
|
||||
messages := make(chan *imap.Message, 1)
|
||||
go func() {
|
||||
done <- c.UidFetch(seqset, fields, messages)
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "UID FETCH 4:* (FLAGS)" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "UID FETCH 4:* (FLAGS)")
|
||||
}
|
||||
|
||||
s.WriteString("* 23 FETCH (UID 2 FLAGS (\\Seen))\r\n")
|
||||
s.WriteString(tag + " OK FETCH completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Fetch() = %v", err)
|
||||
}
|
||||
|
||||
msg, ok := <-messages
|
||||
if !ok {
|
||||
t.Errorf("No message supplied")
|
||||
} else if msg.Uid != 2 {
|
||||
t.Errorf("First message has bad UID: %v", msg.Uid)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Store(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.SelectedState, nil)
|
||||
|
||||
seqset, _ := imap.ParseSeqSet("2")
|
||||
|
||||
done := make(chan error, 1)
|
||||
updates := make(chan *imap.Message, 1)
|
||||
go func() {
|
||||
done <- c.Store(seqset, imap.AddFlags, []interface{}{imap.SeenFlag, "foobar"}, updates)
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "STORE 2 +FLAGS (\\Seen foobar)" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "STORE 2 +FLAGS (\\Seen foobar)")
|
||||
}
|
||||
|
||||
s.WriteString("* 2 FETCH (FLAGS (\\Seen foobar))\r\n")
|
||||
s.WriteString(tag + " OK STORE completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Store() = %v", err)
|
||||
}
|
||||
|
||||
msg := <-updates
|
||||
if len(msg.Flags) != 2 || msg.Flags[0] != "\\Seen" || msg.Flags[1] != "foobar" {
|
||||
t.Errorf("Bad message flags: %v", msg.Flags)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Store_Silent(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.SelectedState, nil)
|
||||
|
||||
seqset, _ := imap.ParseSeqSet("2:3")
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Store(seqset, imap.AddFlags, []interface{}{imap.SeenFlag, "foobar"}, nil)
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "STORE 2:3 +FLAGS.SILENT (\\Seen foobar)" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "STORE 2:3 +FLAGS.SILENT (\\Seen foobar)")
|
||||
}
|
||||
|
||||
s.WriteString(tag + " OK STORE completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Store() = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Store_Uid(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.SelectedState, nil)
|
||||
|
||||
seqset, _ := imap.ParseSeqSet("27:901")
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.UidStore(seqset, imap.AddFlags, []interface{}{imap.DeletedFlag, "foobar"}, nil)
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "UID STORE 27:901 +FLAGS.SILENT (\\Deleted foobar)" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "UID STORE 27:901 +FLAGS.SILENT (\\Deleted foobar)")
|
||||
}
|
||||
|
||||
s.WriteString(tag + " OK STORE completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.UidStore() = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Copy(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.SelectedState, nil)
|
||||
|
||||
seqset, _ := imap.ParseSeqSet("2:4")
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Copy(seqset, "Sent")
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "COPY 2:4 \"Sent\"" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "COPY 2:4 \"Sent\"")
|
||||
}
|
||||
|
||||
s.WriteString(tag + " OK COPY completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Copy() = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Copy_Uid(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.SelectedState, nil)
|
||||
|
||||
seqset, _ := imap.ParseSeqSet("78:102")
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.UidCopy(seqset, "Drafts")
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "UID COPY 78:102 \"Drafts\"" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "UID COPY 78:102 \"Drafts\"")
|
||||
}
|
||||
|
||||
s.WriteString(tag + " OK UID COPY completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.UidCopy() = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Unselect(t *testing.T) {
|
||||
c, s := newTestClient(t)
|
||||
defer s.Close()
|
||||
|
||||
setClientState(c, imap.SelectedState, nil)
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Unselect()
|
||||
}()
|
||||
|
||||
tag, cmd := s.ScanCmd()
|
||||
if cmd != "UNSELECT" {
|
||||
t.Fatalf("client sent command %v, want %v", cmd, "UNSELECT")
|
||||
}
|
||||
|
||||
s.WriteString(tag + " OK UNSELECT completed\r\n")
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("c.Unselect() = %v", err)
|
||||
}
|
||||
|
||||
if c.State() != imap.AuthenticatedState {
|
||||
t.Fatal("Client is not Authenticated after UNSELECT")
|
||||
}
|
||||
}
|
||||
323
client/example_test.go
Normal file
323
client/example_test.go
Normal file
@@ -0,0 +1,323 @@
|
||||
package client_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/mail"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/client"
|
||||
)
|
||||
|
||||
func ExampleClient() {
|
||||
log.Println("Connecting to server...")
|
||||
|
||||
// Connect to server
|
||||
c, err := client.DialTLS("mail.example.org:993", nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("Connected")
|
||||
|
||||
// Don't forget to logout
|
||||
defer c.Logout()
|
||||
|
||||
// Login
|
||||
if err := c.Login("username", "password"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("Logged in")
|
||||
|
||||
// List mailboxes
|
||||
mailboxes := make(chan *imap.MailboxInfo, 10)
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.List("", "*", mailboxes)
|
||||
}()
|
||||
|
||||
log.Println("Mailboxes:")
|
||||
for m := range mailboxes {
|
||||
log.Println("* " + m.Name)
|
||||
}
|
||||
|
||||
if err := <-done; err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Select INBOX
|
||||
mbox, err := c.Select("INBOX", false)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("Flags for INBOX:", mbox.Flags)
|
||||
|
||||
// Get the last 4 messages
|
||||
from := uint32(1)
|
||||
to := mbox.Messages
|
||||
if mbox.Messages > 3 {
|
||||
// We're using unsigned integers here, only substract if the result is > 0
|
||||
from = mbox.Messages - 3
|
||||
}
|
||||
seqset := new(imap.SeqSet)
|
||||
seqset.AddRange(from, to)
|
||||
items := []imap.FetchItem{imap.FetchEnvelope}
|
||||
|
||||
messages := make(chan *imap.Message, 10)
|
||||
done = make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Fetch(seqset, items, messages)
|
||||
}()
|
||||
|
||||
log.Println("Last 4 messages:")
|
||||
for msg := range messages {
|
||||
log.Println("* " + msg.Envelope.Subject)
|
||||
}
|
||||
|
||||
if err := <-done; err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Println("Done!")
|
||||
}
|
||||
|
||||
func ExampleClient_Fetch() {
|
||||
// Let's assume c is a client
|
||||
var c *client.Client
|
||||
|
||||
// Select INBOX
|
||||
mbox, err := c.Select("INBOX", false)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Get the last message
|
||||
if mbox.Messages == 0 {
|
||||
log.Fatal("No message in mailbox")
|
||||
}
|
||||
seqset := new(imap.SeqSet)
|
||||
seqset.AddRange(mbox.Messages, mbox.Messages)
|
||||
|
||||
// Get the whole message body
|
||||
section := &imap.BodySectionName{}
|
||||
items := []imap.FetchItem{section.FetchItem()}
|
||||
|
||||
messages := make(chan *imap.Message, 1)
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Fetch(seqset, items, messages)
|
||||
}()
|
||||
|
||||
log.Println("Last message:")
|
||||
msg := <-messages
|
||||
r := msg.GetBody(section)
|
||||
if r == nil {
|
||||
log.Fatal("Server didn't returned message body")
|
||||
}
|
||||
|
||||
if err := <-done; err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
m, err := mail.ReadMessage(r)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
header := m.Header
|
||||
log.Println("Date:", header.Get("Date"))
|
||||
log.Println("From:", header.Get("From"))
|
||||
log.Println("To:", header.Get("To"))
|
||||
log.Println("Subject:", header.Get("Subject"))
|
||||
|
||||
body, err := ioutil.ReadAll(m.Body)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println(body)
|
||||
}
|
||||
|
||||
func ExampleClient_Append() {
|
||||
// Let's assume c is a client
|
||||
var c *client.Client
|
||||
|
||||
// Write the message to a buffer
|
||||
var b bytes.Buffer
|
||||
b.WriteString("From: <root@nsa.gov>\r\n")
|
||||
b.WriteString("To: <root@gchq.gov.uk>\r\n")
|
||||
b.WriteString("Subject: Hey there\r\n")
|
||||
b.WriteString("\r\n")
|
||||
b.WriteString("Hey <3")
|
||||
|
||||
// Append it to INBOX, with two flags
|
||||
flags := []string{imap.FlaggedFlag, "foobar"}
|
||||
if err := c.Append("INBOX", flags, time.Now(), &b); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleClient_Expunge() {
|
||||
// Let's assume c is a client
|
||||
var c *client.Client
|
||||
|
||||
// Select INBOX
|
||||
mbox, err := c.Select("INBOX", false)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// We will delete the last message
|
||||
if mbox.Messages == 0 {
|
||||
log.Fatal("No message in mailbox")
|
||||
}
|
||||
seqset := new(imap.SeqSet)
|
||||
seqset.AddNum(mbox.Messages)
|
||||
|
||||
// First mark the message as deleted
|
||||
item := imap.FormatFlagsOp(imap.AddFlags, true)
|
||||
flags := []interface{}{imap.DeletedFlag}
|
||||
if err := c.Store(seqset, item, flags, nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Then delete it
|
||||
if err := c.Expunge(nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Println("Last message has been deleted")
|
||||
}
|
||||
|
||||
func ExampleClient_StartTLS() {
|
||||
log.Println("Connecting to server...")
|
||||
|
||||
// Connect to server
|
||||
c, err := client.Dial("mail.example.org:143")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("Connected")
|
||||
|
||||
// Don't forget to logout
|
||||
defer c.Logout()
|
||||
|
||||
// Start a TLS session
|
||||
tlsConfig := &tls.Config{ServerName: "mail.example.org"}
|
||||
if err := c.StartTLS(tlsConfig); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("TLS started")
|
||||
|
||||
// Now we can login
|
||||
if err := c.Login("username", "password"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("Logged in")
|
||||
}
|
||||
|
||||
func ExampleClient_Store() {
|
||||
// Let's assume c is a client
|
||||
var c *client.Client
|
||||
|
||||
// Select INBOX
|
||||
_, err := c.Select("INBOX", false)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Mark message 42 as seen
|
||||
seqSet := new(imap.SeqSet)
|
||||
seqSet.AddNum(42)
|
||||
item := imap.FormatFlagsOp(imap.AddFlags, true)
|
||||
flags := []interface{}{imap.SeenFlag}
|
||||
err = c.Store(seqSet, item, flags, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Println("Message has been marked as seen")
|
||||
}
|
||||
|
||||
func ExampleClient_Search() {
|
||||
// Let's assume c is a client
|
||||
var c *client.Client
|
||||
|
||||
// Select INBOX
|
||||
_, err := c.Select("INBOX", false)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Set search criteria
|
||||
criteria := imap.NewSearchCriteria()
|
||||
criteria.WithoutFlags = []string{imap.SeenFlag}
|
||||
ids, err := c.Search(criteria)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("IDs found:", ids)
|
||||
|
||||
if len(ids) > 0 {
|
||||
seqset := new(imap.SeqSet)
|
||||
seqset.AddNum(ids...)
|
||||
|
||||
messages := make(chan *imap.Message, 10)
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope}, messages)
|
||||
}()
|
||||
|
||||
log.Println("Unseen messages:")
|
||||
for msg := range messages {
|
||||
log.Println("* " + msg.Envelope.Subject)
|
||||
}
|
||||
|
||||
if err := <-done; err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Done!")
|
||||
}
|
||||
|
||||
func ExampleClient_Idle() {
|
||||
// Let's assume c is a client
|
||||
var c *client.Client
|
||||
|
||||
// Select a mailbox
|
||||
if _, err := c.Select("INBOX", false); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a channel to receive mailbox updates
|
||||
updates := make(chan client.Update)
|
||||
c.Updates = updates
|
||||
|
||||
// Start idling
|
||||
stopped := false
|
||||
stop := make(chan struct{})
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Idle(stop, nil)
|
||||
}()
|
||||
|
||||
// Listen for updates
|
||||
for {
|
||||
select {
|
||||
case update := <-updates:
|
||||
log.Println("New update:", update)
|
||||
if !stopped {
|
||||
close(stop)
|
||||
stopped = true
|
||||
}
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("Not idling anymore")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
24
client/tag.go
Normal file
24
client/tag.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
)
|
||||
|
||||
func randomString(n int) (string, error) {
|
||||
b := make([]byte, n)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
func generateTag() string {
|
||||
tag, err := randomString(4)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return tag
|
||||
}
|
||||
Reference in New Issue
Block a user