This commit is contained in:
Bel LaPointe
2021-12-10 07:19:31 -07:00
parent e6a71d692e
commit dad1b3042c
152 changed files with 21753 additions and 9433 deletions

689
vendor/github.com/emersion/go-imap/client/client.go generated vendored Normal file
View File

@@ -0,0 +1,689 @@
// 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
// 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
}

88
vendor/github.com/emersion/go-imap/client/cmd_any.go generated vendored Normal file
View 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
}

380
vendor/github.com/emersion/go-imap/client/cmd_auth.go generated vendored Normal file
View 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")
}
}
}

174
vendor/github.com/emersion/go-imap/client/cmd_noauth.go generated vendored Normal file
View 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
}

View File

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

24
vendor/github.com/emersion/go-imap/client/tag.go generated vendored Normal file
View 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
}