2023-01-30 16:27:06 +03:00
|
|
|
/*
|
|
|
|
Package imapclient provides an IMAP4 client, primarily for testing the IMAP4 server.
|
|
|
|
|
|
|
|
Commands can be sent to the server free-form, but responses are parsed strictly.
|
|
|
|
Behaviour that may not be required by the IMAP4 specification may be expected by
|
|
|
|
this client.
|
|
|
|
*/
|
|
|
|
package imapclient
|
|
|
|
|
|
|
|
/*
|
|
|
|
- Try to keep the parsing method names and the types similar to the ABNF names in the RFCs.
|
|
|
|
|
|
|
|
- todo: have mode for imap4rev1 vs imap4rev2, refusing what is not allowed. we are accepting too much now.
|
|
|
|
- todo: stricter parsing. xnonspace() and xword() should be replaced by proper parsers.
|
|
|
|
*/
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
2023-12-24 01:07:21 +03:00
|
|
|
"crypto/tls"
|
2023-01-30 16:27:06 +03:00
|
|
|
"fmt"
|
|
|
|
"net"
|
|
|
|
"reflect"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Conn is an IMAP connection to a server.
|
|
|
|
type Conn struct {
|
|
|
|
conn net.Conn
|
|
|
|
r *bufio.Reader
|
|
|
|
panic bool
|
|
|
|
tagGen int
|
|
|
|
record bool // If true, bytes read are added to recordBuf. recorded() resets.
|
|
|
|
recordBuf []byte
|
|
|
|
|
implement tls client certificate authentication
the imap & smtp servers now allow logging in with tls client authentication and
the "external" sasl authentication mechanism. email clients like thunderbird,
fairemail, k9, macos mail implement it. this seems to be the most secure among
the authentication mechanism commonly implemented by clients. a useful property
is that an account can have a separate tls public key for each device/email
client. with tls client cert auth, authentication is also bound to the tls
connection. a mitm cannot pass the credentials on to another tls connection,
similar to scram-*-plus. though part of scram-*-plus is that clients verify
that the server knows the client credentials.
for tls client auth with imap, we send a "preauth" untagged message by default.
that puts the connection in authenticated state. given the imap connection
state machine, further authentication commands are not allowed. some clients
don't recognize the preauth message, and try to authenticate anyway, which
fails. a tls public key has a config option to disable preauth, keeping new
connections in unauthenticated state, to work with such email clients.
for smtp (submission), we don't require an explicit auth command.
both for imap and smtp, we allow a client to authenticate with another
mechanism than "external". in that case, credentials are verified, and have to
be for the same account as the tls client auth, but the adress can be another
one than the login address configured with the tls public key.
only the public key is used to identify the account that is authenticating. we
ignore the rest of the certificate. expiration dates, names, constraints, etc
are not verified. no certificate authorities are involved.
users can upload their own (minimal) certificate. the account web interface
shows openssl commands you can run to generate a private key, minimal cert, and
a p12 file (the format that email clients seem to like...) containing both
private key and certificate.
the imapclient & smtpclient packages can now also use tls client auth. and so
does "mox sendmail", either with a pem file with private key and certificate,
or with just an ed25519 private key.
there are new subcommands "mox config tlspubkey ..." for
adding/removing/listing tls public keys from the cli, by the admin.
2024-12-06 00:41:49 +03:00
|
|
|
Preauth bool
|
2023-01-30 16:27:06 +03:00
|
|
|
LastTag string
|
|
|
|
CapAvailable map[Capability]struct{} // Capabilities available at server, from CAPABILITY command or response code.
|
|
|
|
CapEnabled map[Capability]struct{} // Capabilities enabled through ENABLE command.
|
|
|
|
}
|
|
|
|
|
|
|
|
// Error is a parse or other protocol error.
|
|
|
|
type Error struct{ err error }
|
|
|
|
|
|
|
|
func (e Error) Error() string {
|
|
|
|
return e.err.Error()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e Error) Unwrap() error {
|
|
|
|
return e.err
|
|
|
|
}
|
|
|
|
|
|
|
|
// New creates a new client on conn.
|
|
|
|
//
|
|
|
|
// If xpanic is true, functions that would return an error instead panic. For parse
|
|
|
|
// errors, the resulting stack traces show typically show what was being parsed.
|
|
|
|
//
|
implement tls client certificate authentication
the imap & smtp servers now allow logging in with tls client authentication and
the "external" sasl authentication mechanism. email clients like thunderbird,
fairemail, k9, macos mail implement it. this seems to be the most secure among
the authentication mechanism commonly implemented by clients. a useful property
is that an account can have a separate tls public key for each device/email
client. with tls client cert auth, authentication is also bound to the tls
connection. a mitm cannot pass the credentials on to another tls connection,
similar to scram-*-plus. though part of scram-*-plus is that clients verify
that the server knows the client credentials.
for tls client auth with imap, we send a "preauth" untagged message by default.
that puts the connection in authenticated state. given the imap connection
state machine, further authentication commands are not allowed. some clients
don't recognize the preauth message, and try to authenticate anyway, which
fails. a tls public key has a config option to disable preauth, keeping new
connections in unauthenticated state, to work with such email clients.
for smtp (submission), we don't require an explicit auth command.
both for imap and smtp, we allow a client to authenticate with another
mechanism than "external". in that case, credentials are verified, and have to
be for the same account as the tls client auth, but the adress can be another
one than the login address configured with the tls public key.
only the public key is used to identify the account that is authenticating. we
ignore the rest of the certificate. expiration dates, names, constraints, etc
are not verified. no certificate authorities are involved.
users can upload their own (minimal) certificate. the account web interface
shows openssl commands you can run to generate a private key, minimal cert, and
a p12 file (the format that email clients seem to like...) containing both
private key and certificate.
the imapclient & smtpclient packages can now also use tls client auth. and so
does "mox sendmail", either with a pem file with private key and certificate,
or with just an ed25519 private key.
there are new subcommands "mox config tlspubkey ..." for
adding/removing/listing tls public keys from the cli, by the admin.
2024-12-06 00:41:49 +03:00
|
|
|
// The initial untagged greeting response is read and must be "OK" or
|
|
|
|
// "PREAUTH". If preauth, the connection is already in authenticated state,
|
|
|
|
// typically through TLS client certificate. This is indicated in Conn.Preauth.
|
2023-01-30 16:27:06 +03:00
|
|
|
func New(conn net.Conn, xpanic bool) (client *Conn, rerr error) {
|
|
|
|
c := Conn{
|
|
|
|
conn: conn,
|
|
|
|
r: bufio.NewReader(conn),
|
|
|
|
panic: xpanic,
|
|
|
|
CapAvailable: map[Capability]struct{}{},
|
|
|
|
CapEnabled: map[Capability]struct{}{},
|
|
|
|
}
|
|
|
|
|
|
|
|
defer c.recover(&rerr)
|
|
|
|
tag := c.xnonspace()
|
|
|
|
if tag != "*" {
|
|
|
|
c.xerrorf("expected untagged *, got %q", tag)
|
|
|
|
}
|
|
|
|
c.xspace()
|
|
|
|
ut := c.xuntagged()
|
|
|
|
switch x := ut.(type) {
|
|
|
|
case UntaggedResult:
|
|
|
|
if x.Status != OK {
|
|
|
|
c.xerrorf("greeting, got status %q, expected OK", x.Status)
|
|
|
|
}
|
|
|
|
return &c, nil
|
|
|
|
case UntaggedPreauth:
|
implement tls client certificate authentication
the imap & smtp servers now allow logging in with tls client authentication and
the "external" sasl authentication mechanism. email clients like thunderbird,
fairemail, k9, macos mail implement it. this seems to be the most secure among
the authentication mechanism commonly implemented by clients. a useful property
is that an account can have a separate tls public key for each device/email
client. with tls client cert auth, authentication is also bound to the tls
connection. a mitm cannot pass the credentials on to another tls connection,
similar to scram-*-plus. though part of scram-*-plus is that clients verify
that the server knows the client credentials.
for tls client auth with imap, we send a "preauth" untagged message by default.
that puts the connection in authenticated state. given the imap connection
state machine, further authentication commands are not allowed. some clients
don't recognize the preauth message, and try to authenticate anyway, which
fails. a tls public key has a config option to disable preauth, keeping new
connections in unauthenticated state, to work with such email clients.
for smtp (submission), we don't require an explicit auth command.
both for imap and smtp, we allow a client to authenticate with another
mechanism than "external". in that case, credentials are verified, and have to
be for the same account as the tls client auth, but the adress can be another
one than the login address configured with the tls public key.
only the public key is used to identify the account that is authenticating. we
ignore the rest of the certificate. expiration dates, names, constraints, etc
are not verified. no certificate authorities are involved.
users can upload their own (minimal) certificate. the account web interface
shows openssl commands you can run to generate a private key, minimal cert, and
a p12 file (the format that email clients seem to like...) containing both
private key and certificate.
the imapclient & smtpclient packages can now also use tls client auth. and so
does "mox sendmail", either with a pem file with private key and certificate,
or with just an ed25519 private key.
there are new subcommands "mox config tlspubkey ..." for
adding/removing/listing tls public keys from the cli, by the admin.
2024-12-06 00:41:49 +03:00
|
|
|
c.Preauth = true
|
|
|
|
return &c, nil
|
2023-01-30 16:27:06 +03:00
|
|
|
case UntaggedBye:
|
|
|
|
c.xerrorf("greeting: server sent bye")
|
|
|
|
default:
|
|
|
|
c.xerrorf("unexpected untagged %v", ut)
|
|
|
|
}
|
|
|
|
panic("not reached")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Conn) recover(rerr *error) {
|
|
|
|
if c.panic {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
x := recover()
|
|
|
|
if x == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
err, ok := x.(Error)
|
|
|
|
if !ok {
|
|
|
|
panic(x)
|
|
|
|
}
|
|
|
|
*rerr = err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Conn) xerrorf(format string, args ...any) {
|
|
|
|
panic(Error{fmt.Errorf(format, args...)})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Conn) xcheckf(err error, format string, args ...any) {
|
|
|
|
if err != nil {
|
|
|
|
c.xerrorf("%s: %w", fmt.Sprintf(format, args...), err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Conn) xcheck(err error) {
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-24 01:07:21 +03:00
|
|
|
// TLSConnectionState returns the TLS connection state if the connection uses TLS.
|
|
|
|
func (c *Conn) TLSConnectionState() *tls.ConnectionState {
|
|
|
|
if conn, ok := c.conn.(*tls.Conn); ok {
|
|
|
|
cs := conn.ConnectionState()
|
|
|
|
return &cs
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-01-30 16:27:06 +03:00
|
|
|
// Commandf writes a free-form IMAP command to the server.
|
|
|
|
// If tag is empty, a next unique tag is assigned.
|
|
|
|
func (c *Conn) Commandf(tag string, format string, args ...any) (rerr error) {
|
|
|
|
defer c.recover(&rerr)
|
|
|
|
|
|
|
|
if tag == "" {
|
|
|
|
tag = c.nextTag()
|
|
|
|
}
|
|
|
|
c.LastTag = tag
|
|
|
|
|
|
|
|
_, err := fmt.Fprintf(c.conn, "%s %s\r\n", tag, fmt.Sprintf(format, args...))
|
|
|
|
c.xcheckf(err, "write command")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Conn) nextTag() string {
|
|
|
|
c.tagGen++
|
|
|
|
return fmt.Sprintf("x%03d", c.tagGen)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Response reads from the IMAP server until a tagged response line is found.
|
|
|
|
// The tag must be the same as the tag for the last written command.
|
|
|
|
// Result holds the status of the command. The caller must check if this the status is OK.
|
|
|
|
func (c *Conn) Response() (untagged []Untagged, result Result, rerr error) {
|
|
|
|
defer c.recover(&rerr)
|
|
|
|
|
|
|
|
for {
|
|
|
|
tag := c.xnonspace()
|
|
|
|
c.xspace()
|
|
|
|
if tag == "*" {
|
|
|
|
untagged = append(untagged, c.xuntagged())
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if tag != c.LastTag {
|
|
|
|
c.xerrorf("got tag %q, expected %q", tag, c.LastTag)
|
|
|
|
}
|
|
|
|
|
|
|
|
status := c.xstatus()
|
|
|
|
c.xspace()
|
|
|
|
result = c.xresult(status)
|
|
|
|
c.xcrlf()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ReadUntagged reads a single untagged response line.
|
|
|
|
// Useful for reading lines from IDLE.
|
|
|
|
func (c *Conn) ReadUntagged() (untagged Untagged, rerr error) {
|
|
|
|
defer c.recover(&rerr)
|
|
|
|
|
|
|
|
tag := c.xnonspace()
|
|
|
|
if tag != "*" {
|
|
|
|
c.xerrorf("got tag %q, expected untagged", tag)
|
|
|
|
}
|
|
|
|
c.xspace()
|
|
|
|
ut := c.xuntagged()
|
|
|
|
return ut, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Readline reads a line, including CRLF.
|
|
|
|
// Used with IDLE and synchronous literals.
|
|
|
|
func (c *Conn) Readline() (line string, rerr error) {
|
|
|
|
defer c.recover(&rerr)
|
|
|
|
|
|
|
|
line, err := c.r.ReadString('\n')
|
|
|
|
c.xcheckf(err, "read line")
|
|
|
|
return line, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ReadContinuation reads a line. If it is a continuation, i.e. starts with a +, it
|
|
|
|
// is returned without leading "+ " and without trailing crlf. Otherwise, a command
|
|
|
|
// response is returned. A successfully read continuation can return an empty line.
|
|
|
|
// Callers should check rerr and result.Status being empty to check if a
|
|
|
|
// continuation was read.
|
|
|
|
func (c *Conn) ReadContinuation() (line string, untagged []Untagged, result Result, rerr error) {
|
|
|
|
if !c.peek('+') {
|
|
|
|
untagged, result, rerr = c.Response()
|
|
|
|
c.xcheckf(rerr, "reading non-continuation response")
|
|
|
|
c.xerrorf("response status %q, expected OK", result.Status)
|
|
|
|
}
|
|
|
|
c.xtake("+ ")
|
|
|
|
line, err := c.Readline()
|
|
|
|
c.xcheckf(err, "read line")
|
|
|
|
line = strings.TrimSuffix(line, "\r\n")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Writelinef writes the formatted format and args as a single line, adding CRLF.
|
|
|
|
// Used with IDLE and synchronous literals.
|
|
|
|
func (c *Conn) Writelinef(format string, args ...any) (rerr error) {
|
|
|
|
defer c.recover(&rerr)
|
|
|
|
|
|
|
|
s := fmt.Sprintf(format, args...)
|
|
|
|
_, err := fmt.Fprintf(c.conn, "%s\r\n", s)
|
|
|
|
c.xcheckf(err, "writeline")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write writes directly to the connection. Write errors do take the connections
|
|
|
|
// panic mode into account, i.e. Write can panic.
|
|
|
|
func (c *Conn) Write(buf []byte) (n int, rerr error) {
|
|
|
|
defer c.recover(&rerr)
|
|
|
|
|
|
|
|
n, rerr = c.conn.Write(buf)
|
|
|
|
c.xcheckf(rerr, "write")
|
|
|
|
return n, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// WriteSyncLiteral first writes the synchronous literal size, then read the
|
|
|
|
// continuation "+" and finally writes the data.
|
2024-06-10 15:49:10 +03:00
|
|
|
func (c *Conn) WriteSyncLiteral(s string) (untagged []Untagged, rerr error) {
|
2023-01-30 16:27:06 +03:00
|
|
|
defer c.recover(&rerr)
|
|
|
|
|
|
|
|
_, err := fmt.Fprintf(c.conn, "{%d}\r\n", len(s))
|
|
|
|
c.xcheckf(err, "write sync literal size")
|
2024-06-10 15:49:10 +03:00
|
|
|
|
|
|
|
plus, err := c.r.Peek(1)
|
|
|
|
c.xcheckf(err, "read continuation")
|
|
|
|
if plus[0] == '+' {
|
|
|
|
_, err = c.Readline()
|
|
|
|
c.xcheckf(err, "read continuation line")
|
|
|
|
|
|
|
|
_, err = c.conn.Write([]byte(s))
|
|
|
|
c.xcheckf(err, "write literal data")
|
|
|
|
return nil, nil
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
2024-06-10 15:49:10 +03:00
|
|
|
untagged, result, err := c.Response()
|
|
|
|
if err == nil && result.Status == OK {
|
|
|
|
c.xerrorf("no continuation, but invalid ok response (%q)", result.More)
|
|
|
|
}
|
|
|
|
return untagged, fmt.Errorf("no continuation (%s)", result.Status)
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Transactf writes format and args as an IMAP command, using Commandf with an
|
|
|
|
// empty tag. I.e. format must not contain a tag. Transactf then reads a response
|
|
|
|
// using ReadResponse and checks the result status is OK.
|
|
|
|
func (c *Conn) Transactf(format string, args ...any) (untagged []Untagged, result Result, rerr error) {
|
|
|
|
defer c.recover(&rerr)
|
|
|
|
|
|
|
|
err := c.Commandf("", format, args...)
|
|
|
|
if err != nil {
|
|
|
|
return nil, Result{}, err
|
|
|
|
}
|
|
|
|
return c.ResponseOK()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Conn) ResponseOK() (untagged []Untagged, result Result, rerr error) {
|
|
|
|
untagged, result, rerr = c.Response()
|
|
|
|
if rerr != nil {
|
|
|
|
return nil, Result{}, rerr
|
|
|
|
}
|
|
|
|
if result.Status != OK {
|
|
|
|
c.xerrorf("response status %q, expected OK", result.Status)
|
|
|
|
}
|
|
|
|
return untagged, result, rerr
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Conn) xgetUntagged(l []Untagged, dst any) {
|
|
|
|
if len(l) != 1 {
|
|
|
|
c.xerrorf("got %d untagged, expected 1: %v", len(l), l)
|
|
|
|
}
|
|
|
|
got := l[0]
|
|
|
|
gotv := reflect.ValueOf(got)
|
|
|
|
dstv := reflect.ValueOf(dst)
|
|
|
|
if gotv.Type() != dstv.Type().Elem() {
|
|
|
|
c.xerrorf("got %v, expected %v", gotv.Type(), dstv.Type().Elem())
|
|
|
|
}
|
|
|
|
dstv.Elem().Set(gotv)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Close closes the connection without writing anything to the server.
|
|
|
|
// You may want to call Logout. Closing a connection with a mailbox with deleted
|
|
|
|
// message not yet expunged will not expunge those messages.
|
|
|
|
func (c *Conn) Close() error {
|
|
|
|
var err error
|
|
|
|
if c.conn != nil {
|
|
|
|
err = c.conn.Close()
|
|
|
|
c.conn = nil
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|