mirror of
https://github.com/mjl-/mox.git
synced 2025-06-28 08:18:16 +03:00

The imapclient needs more changes, like more strict parsing, before it can be a generally usable IMAP client, these are a few steps towards that. - Fix a bug in the imapserver METADATA responses for TOOMANY and MAXSIZE. - Split low-level IMAP protocol handling (new Proto type) from the higher-level client command handling (existing Conn type). The idea is that some simple uses of IMAP can get by with just using these commands, while more intricate uses of IMAP (like a synchronizing client that needs to talk to all kinds of servers with different behaviours and implemented extensions) can write custom commands and read untagged responses or command completion results explicitly. The lower-level method names have clearer names now, like ReadResponse instead of Response. - Merge the untagged responses and (command completion) "Result" into a new type Response. Makes function signatures simpler. And make Response implement the error interface, and change command methods to return the Response as error if the result is NO or BAD. Simplifies error handling, and still provides the option to continue after a NO or BAD. - Add UIDSearch/MSNSearch commands, with a custom "search program", so mostly to indicate these commands exist. - More complete coverage of types for response codes, for easier handling. - Automatically handle any ENABLED or CAPABILITY untagged response or response code for IMAP command methods on type Conn. - Make difference between MSN vs UID versions of FETCH/STORE/SEARCH/COPY/MOVE/REPLACE commands more clear. The original MSN commands now have MSN prefixed to their name, so they are grouped together in the documentation. - Document which capabilities are needed for a command.
632 lines
17 KiB
Go
632 lines
17 KiB
Go
/*
|
|
Package imapclient provides an IMAP4 client implementing IMAP4rev1 (RFC 3501),
|
|
IMAP4rev2 (RFC 9051) and various extensions.
|
|
|
|
Warning: Currently primarily for testing the mox IMAP4 server. Behaviour that
|
|
may not be required by the IMAP4 specification may be expected by this client.
|
|
|
|
See [Conn] for a high-level client for executing IMAP commands. Use its embedded
|
|
[Proto] for lower-level writing of commands and reading of responses.
|
|
*/
|
|
package imapclient
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net"
|
|
"strings"
|
|
|
|
"github.com/mjl-/mox/mlog"
|
|
"github.com/mjl-/mox/moxio"
|
|
)
|
|
|
|
// Conn is an connection to an IMAP server.
|
|
//
|
|
// Method names on Conn are the names of IMAP commands. CloseMailbox, which
|
|
// executes the IMAP CLOSE command, is an exception. The Close method closes the
|
|
// connection.
|
|
//
|
|
// The methods starting with MSN are the original (old) IMAP commands. The variants
|
|
// starting with UID should almost always be used instead, if available.
|
|
//
|
|
// The methods on Conn typically return errors of type Error or Response. Error
|
|
// represents protocol and i/o level errors, including io.ErrDeadlineExceeded and
|
|
// various errors for closed connections. Response is returned as error if the IMAP
|
|
// result is NO or BAD instead of OK. The responses returned by the IMAP command
|
|
// methods can also be non-zero on errors. Callers may wish to process any untagged
|
|
// responses.
|
|
//
|
|
// The IMAP command methods defined on Conn don't interpret the untagged responses
|
|
// except for untagged CAPABILITY and untagged ENABLED responses, and the
|
|
// CAPABILITY response code. Fields CapAvailable and CapEnabled are updated when
|
|
// those untagged responses are received.
|
|
//
|
|
// Capabilities indicate which optional IMAP functionality is supported by a
|
|
// server. Capabilities are typically implicitly enabled when the client sends a
|
|
// command using syntax of an optional extension. Extensions without new syntax
|
|
// from client to server, but with new behaviour or syntax from server to client,
|
|
// the client needs to explicitly enable the capability with the ENABLE command,
|
|
// see the Enable method.
|
|
type Conn struct {
|
|
// If true, server sent a PREAUTH tag and the connection is already authenticated,
|
|
// e.g. based on TLS certificate authentication.
|
|
Preauth bool
|
|
|
|
// Capabilities available at server, from CAPABILITY command or response code.
|
|
CapAvailable []Capability
|
|
// Capabilities marked as enabled by the server, typically after an ENABLE command.
|
|
CapEnabled []Capability
|
|
|
|
// Proto provides lower-level functions for interacting with the IMAP connection,
|
|
// such as reading and writing individual lines/commands/responses.
|
|
Proto
|
|
}
|
|
|
|
// Proto provides low-level operations for writing requests and reading responses
|
|
// on an IMAP connection.
|
|
//
|
|
// To implement the IDLE command, write "IDLE" using [Proto.WriteCommandf], then
|
|
// read a line with [Proto.Readline]. If it starts with "+ ", the connection is in
|
|
// idle mode and untagged responses can be read using [Proto.ReadUntagged]. If the
|
|
// line doesn't start with "+ ", use [ParseResult] to interpret it as a response to
|
|
// IDLE, which should be a NO or BAD. To abort idle mode, write "DONE" using
|
|
// [Proto.Writelinef] and wait until a result line has been read.
|
|
type Proto struct {
|
|
// Connection, may be original TCP or TLS connection. Reads go through c.br, and
|
|
// writes through c.xbw. The "x" for the writes indicate that failed writes cause
|
|
// an i/o panic, which is either turned into a returned error, or passed on (see
|
|
// boolean panic). The reader and writer wrap a tracing reading/writer and may wrap
|
|
// flate compression.
|
|
conn net.Conn
|
|
connBroken bool // If connection is broken, we won't flush (and write) again.
|
|
br *bufio.Reader
|
|
tr *moxio.TraceReader
|
|
xbw *bufio.Writer
|
|
compress bool // If compression is enabled, we must flush flateWriter and its target original bufio writer.
|
|
xflateWriter *moxio.FlateWriter
|
|
xflateBW *bufio.Writer
|
|
xtw *moxio.TraceWriter
|
|
|
|
log mlog.Log
|
|
errHandle func(err error) // If set, called for all errors. Can panic. Used for imapserver tests.
|
|
tagGen int
|
|
record bool // If true, bytes read are added to recordBuf. recorded() resets.
|
|
recordBuf []byte
|
|
|
|
lastTag string
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Opts has optional fields that influence behaviour of a Conn.
|
|
type Opts struct {
|
|
Logger *slog.Logger
|
|
|
|
// Error is called for IMAP-level and connection-level errors during the IMAP
|
|
// command methods on Conn, not for errors in calls on Proto. Error is allowed to
|
|
// call panic.
|
|
Error func(err error)
|
|
}
|
|
|
|
// New initializes a new IMAP client on conn.
|
|
//
|
|
// Conn should normally be a TLS connection, typically connected to port 993 of an
|
|
// IMAP server. Alternatively, conn can be a plain TCP connection to port 143. TLS
|
|
// should be enabled on plain TCP connections with the [Conn.StartTLS] method.
|
|
//
|
|
// 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.
|
|
//
|
|
// Logging is written to opts.Logger. In particular, IMAP protocol traces are
|
|
// written with prefixes "CR: " and "CW: " (client read/write) as quoted strings at
|
|
// levels Debug-4, with authentication messages at Debug-6 and (user) data at level
|
|
// Debug-8.
|
|
func New(conn net.Conn, opts *Opts) (client *Conn, rerr error) {
|
|
c := Conn{
|
|
Proto: Proto{conn: conn},
|
|
}
|
|
|
|
var clog *slog.Logger
|
|
if opts != nil {
|
|
c.errHandle = opts.Error
|
|
clog = opts.Logger
|
|
} else {
|
|
clog = slog.Default()
|
|
}
|
|
c.log = mlog.New("imapclient", clog)
|
|
|
|
c.tr = moxio.NewTraceReader(c.log, "CR: ", &c)
|
|
c.br = bufio.NewReader(c.tr)
|
|
|
|
// Writes are buffered and write to Conn, which may panic.
|
|
c.xtw = moxio.NewTraceWriter(c.log, "CW: ", &c)
|
|
c.xbw = bufio.NewWriter(c.xtw)
|
|
|
|
defer c.recoverErr(&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)
|
|
}
|
|
if x.Code != nil {
|
|
if caps, ok := x.Code.(CodeCapability); ok {
|
|
c.CapAvailable = caps
|
|
}
|
|
}
|
|
return &c, nil
|
|
case UntaggedPreauth:
|
|
c.Preauth = true
|
|
return &c, nil
|
|
case UntaggedBye:
|
|
c.xerrorf("greeting: server sent bye")
|
|
default:
|
|
c.xerrorf("unexpected untagged %v", ut)
|
|
}
|
|
panic("not reached")
|
|
}
|
|
|
|
func (c *Conn) recoverErr(rerr *error) {
|
|
c.recover(rerr, nil)
|
|
}
|
|
|
|
func (c *Conn) recover(rerr *error, resp *Response) {
|
|
if *rerr != nil {
|
|
if r, ok := (*rerr).(Response); ok && resp != nil {
|
|
*resp = r
|
|
}
|
|
c.errHandle(*rerr)
|
|
return
|
|
}
|
|
|
|
x := recover()
|
|
if x == nil {
|
|
return
|
|
}
|
|
var err error
|
|
switch e := x.(type) {
|
|
case Error:
|
|
err = e
|
|
case Response:
|
|
err = e
|
|
if resp != nil {
|
|
*resp = e
|
|
}
|
|
default:
|
|
panic(x)
|
|
}
|
|
if c.errHandle != nil {
|
|
c.errHandle(err)
|
|
}
|
|
*rerr = err
|
|
}
|
|
|
|
func (p *Proto) recover(rerr *error) {
|
|
if *rerr != nil {
|
|
return
|
|
}
|
|
|
|
x := recover()
|
|
if x == nil {
|
|
return
|
|
}
|
|
switch e := x.(type) {
|
|
case Error:
|
|
*rerr = e
|
|
default:
|
|
panic(x)
|
|
}
|
|
}
|
|
|
|
func (p *Proto) xerrorf(format string, args ...any) {
|
|
panic(Error{fmt.Errorf(format, args...)})
|
|
}
|
|
|
|
func (p *Proto) xcheckf(err error, format string, args ...any) {
|
|
if err != nil {
|
|
p.xerrorf("%s: %w", fmt.Sprintf(format, args...), err)
|
|
}
|
|
}
|
|
|
|
func (p *Proto) xcheck(err error) {
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// xresponse sets resp if err is a Response and resp is not nil.
|
|
func (p *Proto) xresponse(err error, resp *Response) {
|
|
if err == nil {
|
|
return
|
|
}
|
|
if r, ok := err.(Response); ok && resp != nil {
|
|
*resp = r
|
|
}
|
|
panic(err)
|
|
}
|
|
|
|
// Write writes directly to underlying connection (TCP, TLS). For internal use
|
|
// only, to implement io.Writer. Write errors do take the connection's panic mode
|
|
// into account, i.e. Write can panic.
|
|
func (p *Proto) Write(buf []byte) (n int, rerr error) {
|
|
defer p.recover(&rerr)
|
|
|
|
n, rerr = p.conn.Write(buf)
|
|
if rerr != nil {
|
|
p.connBroken = true
|
|
}
|
|
p.xcheckf(rerr, "write")
|
|
return n, nil
|
|
}
|
|
|
|
// Read reads directly from the underlying connection (TCP, TLS). For internal use
|
|
// only, to implement io.Reader.
|
|
func (p *Proto) Read(buf []byte) (n int, err error) {
|
|
return p.conn.Read(buf)
|
|
}
|
|
|
|
func (p *Proto) xflush() {
|
|
// Not writing any more when connection is broken.
|
|
if p.connBroken {
|
|
return
|
|
}
|
|
|
|
err := p.xbw.Flush()
|
|
p.xcheckf(err, "flush")
|
|
|
|
// If compression is active, we need to flush the deflate stream.
|
|
if p.compress {
|
|
err := p.xflateWriter.Flush()
|
|
p.xcheckf(err, "flush deflate")
|
|
err = p.xflateBW.Flush()
|
|
p.xcheckf(err, "flush deflate buffer")
|
|
}
|
|
}
|
|
|
|
func (p *Proto) xtraceread(level slog.Level) func() {
|
|
if p.tr == nil {
|
|
// For ParseUntagged and other parse functions.
|
|
return func() {}
|
|
}
|
|
p.tr.SetTrace(level)
|
|
return func() {
|
|
p.tr.SetTrace(mlog.LevelTrace)
|
|
}
|
|
}
|
|
|
|
func (p *Proto) xtracewrite(level slog.Level) func() {
|
|
if p.xtw == nil {
|
|
// For ParseUntagged and other parse functions.
|
|
return func() {}
|
|
}
|
|
|
|
p.xflush()
|
|
p.xtw.SetTrace(level)
|
|
return func() {
|
|
p.xflush()
|
|
p.xtw.SetTrace(mlog.LevelTrace)
|
|
}
|
|
}
|
|
|
|
// Close closes the connection, flushing and closing any compression and TLS layer.
|
|
//
|
|
// You may want to call Logout first. Closing a connection with a mailbox with
|
|
// deleted messages not yet expunged will not expunge those messages.
|
|
//
|
|
// Closing a TLS connection that is logged out, or closing a TLS connection with
|
|
// compression enabled (i.e. two layered streams), may cause spurious errors
|
|
// because the server may immediate close the underlying connection when it sees
|
|
// the connection is being closed.
|
|
func (c *Conn) Close() (rerr error) {
|
|
defer c.recoverErr(&rerr)
|
|
|
|
if c.conn == nil {
|
|
return nil
|
|
}
|
|
if !c.connBroken && c.xflateWriter != nil {
|
|
err := c.xflateWriter.Close()
|
|
c.xcheckf(err, "close deflate writer")
|
|
err = c.xflateBW.Flush()
|
|
c.xcheckf(err, "flush deflate buffer")
|
|
c.xflateWriter = nil
|
|
c.xflateBW = nil
|
|
}
|
|
err := c.conn.Close()
|
|
c.xcheckf(err, "close connection")
|
|
c.conn = nil
|
|
return
|
|
}
|
|
|
|
// TLSConnectionState returns the TLS connection state if the connection uses TLS,
|
|
// either because the conn passed to [New] was a TLS connection, or because
|
|
// [Conn.StartTLS] was called.
|
|
func (c *Conn) TLSConnectionState() *tls.ConnectionState {
|
|
if conn, ok := c.conn.(*tls.Conn); ok {
|
|
cs := conn.ConnectionState()
|
|
return &cs
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WriteCommandf writes a free-form IMAP command to the server. An ending \r\n is
|
|
// written too.
|
|
//
|
|
// If tag is empty, a next unique tag is assigned.
|
|
func (p *Proto) WriteCommandf(tag string, format string, args ...any) (rerr error) {
|
|
defer p.recover(&rerr)
|
|
|
|
if tag == "" {
|
|
p.nextTag()
|
|
} else {
|
|
p.lastTag = tag
|
|
}
|
|
|
|
fmt.Fprintf(p.xbw, "%s %s\r\n", p.lastTag, fmt.Sprintf(format, args...))
|
|
p.xflush()
|
|
return
|
|
}
|
|
|
|
func (p *Proto) nextTag() string {
|
|
p.tagGen++
|
|
p.lastTag = fmt.Sprintf("x%03d", p.tagGen)
|
|
return p.lastTag
|
|
}
|
|
|
|
// LastTag returns the tag last used for a command. For checking against a command
|
|
// completion result.
|
|
func (p *Proto) LastTag() string {
|
|
return p.lastTag
|
|
}
|
|
|
|
// LastTagSet sets a new last tag, as used for checking against a command completion result.
|
|
func (p *Proto) LastTagSet(tag string) {
|
|
p.lastTag = tag
|
|
}
|
|
|
|
// ReadResponse 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.
|
|
//
|
|
// If an error is returned, resp can still be non-empty, and a caller may wish to
|
|
// process resp.Untagged.
|
|
//
|
|
// Caller should check resp.Status for the result of the command too.
|
|
//
|
|
// Common types for the return error:
|
|
// - Error, for protocol errors
|
|
// - Various I/O errors from the underlying connection, including os.ErrDeadlineExceeded
|
|
func (p *Proto) ReadResponse() (resp Response, rerr error) {
|
|
defer p.recover(&rerr)
|
|
|
|
for {
|
|
tag := p.xnonspace()
|
|
p.xspace()
|
|
if tag == "*" {
|
|
resp.Untagged = append(resp.Untagged, p.xuntagged())
|
|
continue
|
|
}
|
|
|
|
if tag != p.lastTag {
|
|
p.xerrorf("got tag %q, expected %q", tag, p.lastTag)
|
|
}
|
|
|
|
status := p.xstatus()
|
|
p.xspace()
|
|
resp.Result = p.xresult(status)
|
|
p.xcrlf()
|
|
return
|
|
}
|
|
}
|
|
|
|
// ParseCode parses a response code. The string must not have enclosing brackets.
|
|
//
|
|
// Example:
|
|
//
|
|
// "APPENDUID 123 10"
|
|
func ParseCode(s string) (code Code, rerr error) {
|
|
p := Proto{br: bufio.NewReader(strings.NewReader(s + "]"))}
|
|
defer p.recover(&rerr)
|
|
code = p.xrespCode()
|
|
p.xtake("]")
|
|
buf, err := io.ReadAll(p.br)
|
|
p.xcheckf(err, "read")
|
|
if len(buf) != 0 {
|
|
p.xerrorf("leftover data %q", buf)
|
|
}
|
|
return code, nil
|
|
}
|
|
|
|
// ParseResult parses a line, including required crlf, as a command result line.
|
|
//
|
|
// Example:
|
|
//
|
|
// "tag1 OK [APPENDUID 123 10] message added\r\n"
|
|
func ParseResult(s string) (tag string, result Result, rerr error) {
|
|
p := Proto{br: bufio.NewReader(strings.NewReader(s))}
|
|
defer p.recover(&rerr)
|
|
tag = p.xnonspace()
|
|
p.xspace()
|
|
status := p.xstatus()
|
|
p.xspace()
|
|
result = p.xresult(status)
|
|
p.xcrlf()
|
|
return
|
|
}
|
|
|
|
// ReadUntagged reads a single untagged response line.
|
|
func (p *Proto) ReadUntagged() (untagged Untagged, rerr error) {
|
|
defer p.recover(&rerr)
|
|
return p.readUntagged()
|
|
}
|
|
|
|
// ParseUntagged parses a line, including required crlf, as untagged response.
|
|
//
|
|
// Example:
|
|
//
|
|
// "* BYE shutting down connection\r\n"
|
|
func ParseUntagged(s string) (untagged Untagged, rerr error) {
|
|
p := Proto{br: bufio.NewReader(strings.NewReader(s))}
|
|
defer p.recover(&rerr)
|
|
untagged, rerr = p.readUntagged()
|
|
return
|
|
}
|
|
|
|
func (p *Proto) readUntagged() (untagged Untagged, rerr error) {
|
|
defer p.recover(&rerr)
|
|
tag := p.xnonspace()
|
|
if tag != "*" {
|
|
p.xerrorf("got tag %q, expected untagged", tag)
|
|
}
|
|
p.xspace()
|
|
ut := p.xuntagged()
|
|
return ut, nil
|
|
}
|
|
|
|
// Readline reads a line, including CRLF.
|
|
// Used with IDLE and synchronous literals.
|
|
func (p *Proto) Readline() (line string, rerr error) {
|
|
defer p.recover(&rerr)
|
|
|
|
line, err := p.br.ReadString('\n')
|
|
p.xcheckf(err, "read line")
|
|
return line, nil
|
|
}
|
|
|
|
func (c *Conn) readContinuation() (line string, rerr error) {
|
|
defer c.recover(&rerr, nil)
|
|
line, rerr = c.ReadContinuation()
|
|
if rerr != nil {
|
|
if resp, ok := rerr.(Response); ok {
|
|
c.processUntagged(resp.Untagged)
|
|
c.processResult(resp.Result)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// ReadContinuation reads a line. If it is a continuation, i.e. starts with "+", it
|
|
// is returned without leading "+ " and without trailing crlf. Otherwise, an error
|
|
// is returned, which can be a Response with Untagged that a caller may wish to
|
|
// process. A successfully read continuation can return an empty line.
|
|
func (p *Proto) ReadContinuation() (line string, rerr error) {
|
|
defer p.recover(&rerr)
|
|
|
|
if !p.peek('+') {
|
|
var resp Response
|
|
resp, rerr = p.ReadResponse()
|
|
if rerr == nil {
|
|
rerr = resp
|
|
}
|
|
return "", rerr
|
|
}
|
|
p.xtake("+ ")
|
|
line, err := p.Readline()
|
|
p.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 (p *Proto) Writelinef(format string, args ...any) (rerr error) {
|
|
defer p.recover(&rerr)
|
|
|
|
s := fmt.Sprintf(format, args...)
|
|
fmt.Fprintf(p.xbw, "%s\r\n", s)
|
|
p.xflush()
|
|
return nil
|
|
}
|
|
|
|
// WriteSyncLiteral first writes the synchronous literal size, then reads the
|
|
// continuation "+" and finally writes the data. If the literal is not accepted, an
|
|
// error is returned, which may be a Response.
|
|
func (p *Proto) WriteSyncLiteral(s string) (rerr error) {
|
|
defer p.recover(&rerr)
|
|
|
|
fmt.Fprintf(p.xbw, "{%d}\r\n", len(s))
|
|
p.xflush()
|
|
|
|
plus, err := p.br.Peek(1)
|
|
p.xcheckf(err, "read continuation")
|
|
if plus[0] == '+' {
|
|
_, err = p.Readline()
|
|
p.xcheckf(err, "read continuation line")
|
|
|
|
defer p.xtracewrite(mlog.LevelTracedata)()
|
|
_, err = p.xbw.Write([]byte(s))
|
|
p.xcheckf(err, "write literal data")
|
|
p.xtracewrite(mlog.LevelTrace)
|
|
return nil
|
|
}
|
|
var resp Response
|
|
resp, rerr = p.ReadResponse()
|
|
if rerr == nil {
|
|
rerr = resp
|
|
}
|
|
return
|
|
}
|
|
|
|
func (c *Conn) processUntagged(l []Untagged) {
|
|
for _, ut := range l {
|
|
switch e := ut.(type) {
|
|
case UntaggedCapability:
|
|
c.CapAvailable = []Capability(e)
|
|
case UntaggedEnabled:
|
|
c.CapEnabled = append(c.CapEnabled, e...)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Conn) processResult(r Result) {
|
|
if r.Code == nil {
|
|
return
|
|
}
|
|
switch e := r.Code.(type) {
|
|
case CodeCapability:
|
|
c.CapAvailable = []Capability(e)
|
|
}
|
|
}
|
|
|
|
// 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) (resp Response, rerr error) {
|
|
defer c.recover(&rerr, &resp)
|
|
|
|
err := c.WriteCommandf("", format, args...)
|
|
if err != nil {
|
|
return Response{}, err
|
|
}
|
|
|
|
return c.responseOK()
|
|
}
|
|
|
|
func (c *Conn) responseOK() (resp Response, rerr error) {
|
|
defer c.recover(&rerr, &resp)
|
|
|
|
resp, rerr = c.ReadResponse()
|
|
c.processUntagged(resp.Untagged)
|
|
c.processResult(resp.Result)
|
|
if rerr == nil && resp.Status != OK {
|
|
rerr = resp
|
|
}
|
|
return
|
|
}
|