175 lines
3.9 KiB
Go
175 lines
3.9 KiB
Go
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
|
|
}
|