package pop3 import ( "context" "crypto/tls" "errors" "fmt" "net" "sync" ) // Dial dials a remote POP3 server and transitions into a TLS-secured // TRANSACTION state. // The resulting *Client must be closed iff a nil error got returned. // addr's hostname is used to authenticate the server's TLS certificate. func Dial(ctx context.Context, addr string, creds *Credentials) (*Client, error) { return dial(ctx, addr, false, creds) } // Dial dials over raw TCP, issues POP3's STARTTLS command and enables TLS. // (*Client).Greeting will always be empty since the greeting is transmitted // over unsecured raw TCP. // If available, use of Dial is recommended. // addr's hostname is used to authenticate the server's TLS certificate. func DialSTARTTLS(ctx context.Context, addr string, creds *Credentials) (*Client, error) { return dial(ctx, addr, true, creds) } // Client represents a live POP3 connection to a remote mail drop. // After use, (*Client).Close is expected to be called. type Client struct { // NOTE: When using STARTTLS, due to the POP3 protocol, Greeting cannot be // trusted and is thus always empty. Greeting string // Capabilities are requested in a TLS-secured TRANSACTION state. // Capabilities == nil iff dialing failed or the mail drop does not support // the "CAPA" capability. // When the mail drop supports the CAPA command, it should also report the // "CAPA" capability, making Capabilities non-zero. Capabilities []string mu sync.Mutex framed *Framed closed bool } // Live POP3 servers have been observed to close the network connection // prematurely, not answering the QUIT command. Thus, Close only propagates // an explicit -ERR answer and not net.ErrClosed or io.EOF. // XXX One could argue the above should be disabled with framed.Strict. func (client *Client) Close() error { client.mu.Lock() defer client.mu.Unlock() if client.closed { return net.ErrClosed } client.closed = true if client.framed == nil { return nil } defer client.framed.Close() err := client.framed.CommandQUIT() if concrete := new(ERR); errors.As(err, &concrete) { client.framed.Close() return err } return nil } // TODO ctx is not fully respected. func dial(ctx context.Context, addr string, usestarttls bool, creds *Credentials) (*Client, error) { host, _, err := net.SplitHostPort(addr) if err != nil { return nil, err } tlscnf := &tls.Config{ServerName: host,} tcpconn, err := new(net.Dialer).DialContext(ctx, "tcp", addr) if err != nil { return nil, err } if usestarttls { framedtcp := Frame(tcpconn) // drop untrustworthy greeting transmitted over bare TCP _, err := framedtcp.SingleLine() if err != nil { framedtcp.Close() return nil, err } err = framedtcp.CommandSTLS() if err != nil { framedtcp.Close() return nil, fmt.Errorf("%w: %v", ErrNoTLS, err) } // XXX hope everything has flushed itself } // NOTE: tlsconn.Close does close tcpconn. tlsconn := tls.Client(tcpconn, tlscnf) err = tlsconn.HandshakeContext(ctx) if err != nil { // [2023-09-13, jfrech] TODO Verify that crypto/tls.HandshakeContext does not itself close tcpconn. tcpconn.Close() return nil, err } framed := Frame(tlsconn) var greeting string if !usestarttls { var err error greeting, err = framed.SingleLine() if err != nil { framed.Close() return nil, err } } err = framed.CommandUSERPASS(creds) if err != nil { framed.Close() return nil, err } // The mail drop may not support CAPA. // Capabilities are only requested in a TLS-secured TRANSACTION state. capabilities := func() []string { _, resp, err := framed.CommandMultiLine(CAPA) if err != nil { return nil } lines, err := Lines(resp) if err != nil { return nil } if len(lines) == 0 { lines = make([]string, 0) } return lines }() return &Client{ Greeting: greeting, Capabilities: capabilities, framed: framed, }, nil }