mirror of
https://github.com/mjl-/mox.git
synced 2025-07-12 18:24:35 +03:00
mox!
This commit is contained in:
293
imapclient/client.go
Normal file
293
imapclient/client.go
Normal file
@ -0,0 +1,293 @@
|
||||
/*
|
||||
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"
|
||||
"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
|
||||
|
||||
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.
|
||||
//
|
||||
// The initial untagged greeting response is read and must be "OK".
|
||||
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:
|
||||
c.xerrorf("greeting: unexpected preauth")
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (c *Conn) WriteSyncLiteral(s string) (rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
|
||||
_, err := fmt.Fprintf(c.conn, "{%d}\r\n", len(s))
|
||||
c.xcheckf(err, "write sync literal size")
|
||||
line, err := c.Readline()
|
||||
c.xcheckf(err, "read line")
|
||||
if !strings.HasPrefix(line, "+") {
|
||||
c.xerrorf("no continuation received for sync literal")
|
||||
}
|
||||
_, err = c.conn.Write([]byte(s))
|
||||
c.xcheckf(err, "write literal data")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
292
imapclient/cmds.go
Normal file
292
imapclient/cmds.go
Normal file
@ -0,0 +1,292 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mjl-/mox/scram"
|
||||
)
|
||||
|
||||
// Capability requests a list of capabilities from the server. They are returned in
|
||||
// an UntaggedCapability response. The server also sends capabilities in initial
|
||||
// server greeting, in the response code.
|
||||
func (c *Conn) Capability() (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
return c.Transactf("capability")
|
||||
}
|
||||
|
||||
// Noop does nothing on its own, but a server will return any pending untagged
|
||||
// responses for new message delivery and changes to mailboxes.
|
||||
func (c *Conn) Noop() (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
return c.Transactf("capability")
|
||||
}
|
||||
|
||||
// Logout ends the IMAP session by writing a LOGOUT command. Close must still be
|
||||
// closed on this client to close the socket.
|
||||
func (c *Conn) Logout() (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
return c.Transactf("logout")
|
||||
}
|
||||
|
||||
// Starttls enables TLS on the connection with the STARTTLS command.
|
||||
func (c *Conn) Starttls(config *tls.Config) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
untagged, result, rerr = c.Transactf("starttls")
|
||||
c.xcheckf(rerr, "starttls command")
|
||||
conn := tls.Client(c.conn, config)
|
||||
err := conn.Handshake()
|
||||
c.xcheckf(err, "tls handshake")
|
||||
c.conn = conn
|
||||
c.r = bufio.NewReader(conn)
|
||||
return untagged, result, nil
|
||||
}
|
||||
|
||||
// Login authenticates with username and password
|
||||
func (c *Conn) Login(username, password string) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
return c.Transactf("login %s %s", astring(username), astring(password))
|
||||
}
|
||||
|
||||
// Authenticate with plaintext password using AUTHENTICATE PLAIN.
|
||||
func (c *Conn) AuthenticatePlain(username, password string) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
|
||||
untagged, result, rerr = c.Transactf("authenticate plain %s", base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "\u0000%s\u0000%s", username, password)))
|
||||
return
|
||||
}
|
||||
|
||||
// Authenticate with SCRAM-SHA-256, where the password is not exchanged in original
|
||||
// plaintext form, but only derived hashes are exchanged by both parties as proof
|
||||
// of knowledge of password.
|
||||
func (c *Conn) AuthenticateSCRAMSHA256(username, password string) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
|
||||
sc := scram.NewClient(username, "")
|
||||
clientFirst, err := sc.ClientFirst()
|
||||
c.xcheckf(err, "scram clientFirst")
|
||||
c.LastTag = c.nextTag()
|
||||
err = c.Writelinef("%s authenticate scram-sha-256 %s", c.LastTag, base64.StdEncoding.EncodeToString([]byte(clientFirst)))
|
||||
c.xcheckf(err, "writing command line")
|
||||
|
||||
xreadContinuation := func() []byte {
|
||||
var line string
|
||||
line, untagged, result, rerr = c.ReadContinuation()
|
||||
c.xcheckf(err, "read continuation")
|
||||
if result.Status != "" {
|
||||
c.xerrorf("unexpected status %q", result.Status)
|
||||
}
|
||||
buf, err := base64.StdEncoding.DecodeString(line)
|
||||
c.xcheckf(err, "parsing base64 from remote")
|
||||
return buf
|
||||
}
|
||||
|
||||
serverFirst := xreadContinuation()
|
||||
clientFinal, err := sc.ServerFirst(serverFirst, password)
|
||||
c.xcheckf(err, "scram clientFinal")
|
||||
err = c.Writelinef("%s", base64.StdEncoding.EncodeToString([]byte(clientFinal)))
|
||||
c.xcheckf(err, "write scram clientFinal")
|
||||
|
||||
serverFinal := xreadContinuation()
|
||||
err = sc.ServerFinal(serverFinal)
|
||||
c.xcheckf(err, "scram serverFinal")
|
||||
|
||||
// We must send a response to the server continuation line, but we have nothing to say. ../rfc/9051:6221
|
||||
err = c.Writelinef("%s", base64.StdEncoding.EncodeToString(nil))
|
||||
c.xcheckf(err, "scram client end")
|
||||
|
||||
return c.ResponseOK()
|
||||
}
|
||||
|
||||
// Enable enables capabilities for use with the connection, verifying the server has indeed enabled them.
|
||||
func (c *Conn) Enable(capabilities ...string) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
|
||||
untagged, result, rerr = c.Transactf("enable %s", strings.Join(capabilities, " "))
|
||||
c.xcheck(rerr)
|
||||
var enabled UntaggedEnabled
|
||||
c.xgetUntagged(untagged, &enabled)
|
||||
got := map[string]struct{}{}
|
||||
for _, cap := range enabled {
|
||||
got[cap] = struct{}{}
|
||||
}
|
||||
for _, cap := range capabilities {
|
||||
if _, ok := got[cap]; !ok {
|
||||
c.xerrorf("capability %q not enabled by server", cap)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Select opens mailbox as active mailbox.
|
||||
func (c *Conn) Select(mailbox string) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
return c.Transactf("select %s", astring(mailbox))
|
||||
}
|
||||
|
||||
// Examine opens mailbox as active mailbox read-only.
|
||||
func (c *Conn) Examine(mailbox string) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
return c.Transactf("examine %s", astring(mailbox))
|
||||
}
|
||||
|
||||
// Create makes a new mailbox on the server.
|
||||
func (c *Conn) Create(mailbox string) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
return c.Transactf("create %s", astring(mailbox))
|
||||
}
|
||||
|
||||
// Delete removes an entire mailbox and its messages.
|
||||
func (c *Conn) Delete(mailbox string) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
return c.Transactf("delete %s", astring(mailbox))
|
||||
}
|
||||
|
||||
// Rename changes the name of a mailbox and all its child mailboxes.
|
||||
func (c *Conn) Rename(omailbox, nmailbox string) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
return c.Transactf("rename %s %s", astring(omailbox), astring(nmailbox))
|
||||
}
|
||||
|
||||
// Subscribe marks a mailbox as subscribed. The mailbox does not have to exist. It
|
||||
// is not an error if the mailbox is already subscribed.
|
||||
func (c *Conn) Subscribe(mailbox string) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
return c.Transactf("subscribe %s", astring(mailbox))
|
||||
}
|
||||
|
||||
// Unsubscribe marks a mailbox as unsubscribed.
|
||||
func (c *Conn) Unsubscribe(mailbox string) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
return c.Transactf("unsubscribe %s", astring(mailbox))
|
||||
}
|
||||
|
||||
// List lists mailboxes with the basic LIST syntax.
|
||||
// Pattern can contain * (match any) or % (match any except hierarchy delimiter).
|
||||
func (c *Conn) List(pattern string) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
return c.Transactf(`list "" %s`, astring(pattern))
|
||||
}
|
||||
|
||||
// ListFull lists mailboxes with the extended LIST syntax requesting all supported data.
|
||||
// Pattern can contain * (match any) or % (match any except hierarchy delimiter).
|
||||
func (c *Conn) ListFull(subscribedOnly bool, patterns ...string) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
var subscribedStr string
|
||||
if subscribedOnly {
|
||||
subscribedStr = "subscribed recursivematch"
|
||||
}
|
||||
for i, s := range patterns {
|
||||
patterns[i] = astring(s)
|
||||
}
|
||||
return c.Transactf(`list (%s) "" (%s) return (subscribed children special-use status (messages uidnext uidvalidity unseen deleted size recent appendlimit))`, subscribedStr, strings.Join(patterns, " "))
|
||||
}
|
||||
|
||||
// Namespace returns the hiearchy separator in an UntaggedNamespace response with personal/shared/other namespaces if present.
|
||||
func (c *Conn) Namespace() (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
return c.Transactf("namespace")
|
||||
}
|
||||
|
||||
// Status requests information about a mailbox, such as number of messages, size, etc.
|
||||
func (c *Conn) Status(mailbox string) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
return c.Transactf("status %s", astring(mailbox))
|
||||
}
|
||||
|
||||
// Append adds message to mailbox with flags and optional receive time.
|
||||
func (c *Conn) Append(mailbox string, flags []string, received *time.Time, message []byte) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
var date string
|
||||
if received != nil {
|
||||
date = ` "` + received.Format("_2-Jan-2006 15:04:05 -0700") + `"`
|
||||
}
|
||||
return c.Transactf("append %s (%s)%s {%d+}\r\n%s", astring(mailbox), strings.Join(flags, " "), date, len(message), message)
|
||||
}
|
||||
|
||||
// note: No idle command. Idle is better implemented by writing the request and reading and handling the responses as they come in.
|
||||
|
||||
// CloseMailbox closes the currently selected/active mailbox, permanently removing
|
||||
// any messages marked with \Deleted.
|
||||
func (c *Conn) CloseMailbox() (untagged []Untagged, result Result, rerr error) {
|
||||
return c.Transactf("close")
|
||||
}
|
||||
|
||||
// Unselect closes the currently selected/active mailbox, but unlike CloseMailbox
|
||||
// does not permanently remove any messages marked with \Deleted.
|
||||
func (c *Conn) Unselect() (untagged []Untagged, result Result, rerr error) {
|
||||
return c.Transactf("unselect")
|
||||
}
|
||||
|
||||
// Expunge removes messages marked as deleted for the selected mailbox.
|
||||
func (c *Conn) Expunge() (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
return c.Transactf("expunge")
|
||||
}
|
||||
|
||||
// UIDExpunge is like expunge, but only removes messages matched uidSet.
|
||||
func (c *Conn) UIDExpunge(uidSet NumSet) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
return c.Transactf("uid expunge %s", uidSet.String())
|
||||
}
|
||||
|
||||
// Note: No search, fetch command yet due to its large syntax.
|
||||
|
||||
// StoreFlagsSet stores a new set of flags for messages from seqset with the STORE command.
|
||||
// If silent, no untagged responses with the updated flags will be sent by the server.
|
||||
func (c *Conn) StoreFlagsSet(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
item := "flags"
|
||||
if silent {
|
||||
item += ".silent"
|
||||
}
|
||||
return c.Transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " "))
|
||||
}
|
||||
|
||||
// StoreFlagsAdd is like StoreFlagsSet, but only adds flags, leaving current flags on the message intact.
|
||||
func (c *Conn) StoreFlagsAdd(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
item := "+flags"
|
||||
if silent {
|
||||
item += ".silent"
|
||||
}
|
||||
return c.Transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " "))
|
||||
}
|
||||
|
||||
// StoreFlagsClear is like StoreFlagsSet, but only removes flags, leaving other flags on the message intact.
|
||||
func (c *Conn) StoreFlagsClear(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
item := "-flags"
|
||||
if silent {
|
||||
item += ".silent"
|
||||
}
|
||||
return c.Transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " "))
|
||||
}
|
||||
|
||||
// Copy adds messages from the sequences in seqSet in the currently selected/active mailbox to dstMailbox.
|
||||
func (c *Conn) Copy(seqSet NumSet, dstMailbox string) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
return c.Transactf("copy %s %s", seqSet.String(), astring(dstMailbox))
|
||||
}
|
||||
|
||||
// UIDCopy is like copy, but operates on UIDs.
|
||||
func (c *Conn) UIDCopy(uidSet NumSet, dstMailbox string) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
return c.Transactf("uid copy %s %s", uidSet.String(), astring(dstMailbox))
|
||||
}
|
||||
|
||||
// Move moves messages from the sequences in seqSet in the currently selected/active mailbox to dstMailbox.
|
||||
func (c *Conn) Move(seqSet NumSet, dstMailbox string) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
return c.Transactf("move %s %s", seqSet.String(), astring(dstMailbox))
|
||||
}
|
||||
|
||||
// UIDMove is like move, but operates on UIDs.
|
||||
func (c *Conn) UIDMove(uidSet NumSet, dstMailbox string) (untagged []Untagged, result Result, rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
return c.Transactf("uid move %s %s", uidSet.String(), astring(dstMailbox))
|
||||
}
|
1223
imapclient/parse.go
Normal file
1223
imapclient/parse.go
Normal file
File diff suppressed because it is too large
Load Diff
452
imapclient/protocol.go
Normal file
452
imapclient/protocol.go
Normal file
@ -0,0 +1,452 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Capability is a known string for with the ENABLED and CAPABILITY command.
|
||||
type Capability string
|
||||
|
||||
const (
|
||||
CapIMAP4rev1 Capability = "IMAP4rev1"
|
||||
CapIMAP4rev2 Capability = "IMAP4rev2"
|
||||
CapLoginDisabled Capability = "LOGINDISABLED"
|
||||
CapStarttls Capability = "STARTTLS"
|
||||
CapAuthPlain Capability = "AUTH=PLAIN"
|
||||
CapLiteralPlus Capability = "LITERAL+"
|
||||
CapLiteralMinus Capability = "LITERAL-"
|
||||
CapIdle Capability = "IDLE"
|
||||
CapNamespace Capability = "NAMESPACE"
|
||||
CapBinary Capability = "BINARY"
|
||||
CapUnselect Capability = "UNSELECT"
|
||||
CapUidplus Capability = "UIDPLUS"
|
||||
CapEsearch Capability = "ESEARCH"
|
||||
CapEnable Capability = "ENABLE"
|
||||
CapSave Capability = "SAVE"
|
||||
CapListExtended Capability = "LIST-EXTENDED"
|
||||
CapSpecialUse Capability = "SPECIAL-USE"
|
||||
CapMove Capability = "MOVE"
|
||||
CapUTF8Only Capability = "UTF8=ONLY"
|
||||
CapUTF8Accept Capability = "UTF8=ACCEPT"
|
||||
CapID Capability = "ID" // ../rfc/2971:80
|
||||
)
|
||||
|
||||
// Status is the tagged final result of a command.
|
||||
type Status string
|
||||
|
||||
const (
|
||||
BAD Status = "BAD" // Syntax error.
|
||||
NO Status = "NO" // Command failed.
|
||||
OK Status = "OK" // Command succeeded.
|
||||
)
|
||||
|
||||
// Result is the final response for a command, indicating success or failure.
|
||||
type Result struct {
|
||||
Status Status
|
||||
RespText
|
||||
}
|
||||
|
||||
// CodeArg represents a response code with arguments, i.e. the data between [] in the response line.
|
||||
type CodeArg interface {
|
||||
CodeString() string
|
||||
}
|
||||
|
||||
// CodeOther is a valid but unrecognized response code.
|
||||
type CodeOther struct {
|
||||
Code string
|
||||
Args []string
|
||||
}
|
||||
|
||||
func (c CodeOther) CodeString() string {
|
||||
return c.Code + " " + strings.Join(c.Args, " ")
|
||||
}
|
||||
|
||||
// CodeWords is a code with space-separated string parameters. E.g. CAPABILITY.
|
||||
type CodeWords struct {
|
||||
Code string
|
||||
Args []string
|
||||
}
|
||||
|
||||
func (c CodeWords) CodeString() string {
|
||||
s := c.Code
|
||||
for _, w := range c.Args {
|
||||
s += " " + w
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// CodeList is a code with a list with space-separated strings as parameters. E.g. BADCHARSET, PERMANENTFLAGS.
|
||||
type CodeList struct {
|
||||
Code string
|
||||
Args []string // If nil, no list was present. List can also be empty.
|
||||
}
|
||||
|
||||
func (c CodeList) CodeString() string {
|
||||
s := c.Code
|
||||
if c.Args == nil {
|
||||
return s
|
||||
}
|
||||
return s + "(" + strings.Join(c.Args, " ") + ")"
|
||||
}
|
||||
|
||||
// CodeUint is a code with a uint32 parameter, e.g. UIDNEXT and UIDVALIDITY.
|
||||
type CodeUint struct {
|
||||
Code string
|
||||
Num uint32
|
||||
}
|
||||
|
||||
func (c CodeUint) CodeString() string {
|
||||
return fmt.Sprintf("%s %d", c.Code, c.Num)
|
||||
}
|
||||
|
||||
// "APPENDUID" response code.
|
||||
type CodeAppendUID struct {
|
||||
UIDValidity uint32
|
||||
UID uint32
|
||||
}
|
||||
|
||||
func (c CodeAppendUID) CodeString() string {
|
||||
return fmt.Sprintf("APPENDUID %d %d", c.UIDValidity, c.UID)
|
||||
}
|
||||
|
||||
// "COPYUID" response code.
|
||||
type CodeCopyUID struct {
|
||||
DestUIDValidity uint32
|
||||
From []NumRange
|
||||
To []NumRange
|
||||
}
|
||||
|
||||
func (c CodeCopyUID) CodeString() string {
|
||||
str := func(l []NumRange) string {
|
||||
s := ""
|
||||
for i, e := range l {
|
||||
if i > 0 {
|
||||
s += ","
|
||||
}
|
||||
s += fmt.Sprintf("%d", e.First)
|
||||
if e.Last != nil {
|
||||
s += fmt.Sprintf(":%d", *e.Last)
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
return fmt.Sprintf("COPYUID %d %s %s", c.DestUIDValidity, str(c.From), str(c.To))
|
||||
}
|
||||
|
||||
// RespText represents a response line minus the leading tag.
|
||||
type RespText struct {
|
||||
Code string // The first word between [] after the status.
|
||||
CodeArg CodeArg // Set if code has a parameter.
|
||||
More string // Any remaining text.
|
||||
}
|
||||
|
||||
// atom or string.
|
||||
func astring(s string) string {
|
||||
if len(s) == 0 {
|
||||
return stringx(s)
|
||||
}
|
||||
for _, c := range s {
|
||||
if c <= ' ' || c >= 0x7f || c == '(' || c == ')' || c == '{' || c == '%' || c == '*' || c == '"' || c == '\\' {
|
||||
stringx(s)
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// imap "string", i.e. double-quoted string or syncliteral.
|
||||
func stringx(s string) string {
|
||||
r := `"`
|
||||
for _, c := range s {
|
||||
if c == '\x00' || c == '\r' || c == '\n' {
|
||||
return syncliteral(s)
|
||||
}
|
||||
if c == '\\' || c == '"' {
|
||||
r += `\`
|
||||
}
|
||||
r += string(c)
|
||||
}
|
||||
r += `"`
|
||||
return r
|
||||
}
|
||||
|
||||
// sync literal, i.e. {<num>}\r\n<num bytes>.
|
||||
func syncliteral(s string) string {
|
||||
return fmt.Sprintf("{%d}\r\n", len(s)) + s
|
||||
}
|
||||
|
||||
// Untagged is a parsed untagged response. See types starting with Untagged.
|
||||
// todo: make an interface that the untagged responses implement?
|
||||
type Untagged any
|
||||
|
||||
type UntaggedBye RespText
|
||||
type UntaggedPreauth RespText
|
||||
type UntaggedExpunge uint32
|
||||
type UntaggedExists uint32
|
||||
type UntaggedRecent uint32
|
||||
type UntaggedCapability []string
|
||||
type UntaggedEnabled []string
|
||||
type UntaggedResult Result
|
||||
type UntaggedFlags []string
|
||||
type UntaggedList struct {
|
||||
// ../rfc/9051:6690
|
||||
Flags []string
|
||||
Separator byte // 0 for NIL
|
||||
Mailbox string
|
||||
Extended []MboxListExtendedItem
|
||||
OldName string // If present, taken out of Extended.
|
||||
}
|
||||
type UntaggedFetch struct {
|
||||
Seq uint32
|
||||
Attrs []FetchAttr
|
||||
}
|
||||
type UntaggedSearch []uint32
|
||||
type UntaggedStatus struct {
|
||||
Mailbox string
|
||||
Attrs map[string]int64 // Upper case status attributes. ../rfc/9051:7059
|
||||
}
|
||||
type UntaggedNamespace struct {
|
||||
Personal, Other, Shared []NamespaceDescr
|
||||
}
|
||||
type UntaggedLsub struct {
|
||||
// ../rfc/3501:4833
|
||||
Flags []string
|
||||
Separator byte
|
||||
Mailbox string
|
||||
}
|
||||
|
||||
// Fields are optional and zero if absent.
|
||||
type UntaggedEsearch struct {
|
||||
// ../rfc/9051:6546
|
||||
Correlator string
|
||||
UID bool
|
||||
Min uint32
|
||||
Max uint32
|
||||
All NumSet
|
||||
Count *uint32
|
||||
Exts []EsearchDataExt
|
||||
}
|
||||
|
||||
// ../rfc/2971:184
|
||||
|
||||
type UntaggedID map[string]string
|
||||
|
||||
// Extended data in an ESEARCH response.
|
||||
type EsearchDataExt struct {
|
||||
Tag string
|
||||
Value TaggedExtVal
|
||||
}
|
||||
|
||||
type NamespaceDescr struct {
|
||||
// ../rfc/9051:6769
|
||||
Prefix string
|
||||
Separator byte // If 0 then separator was absent.
|
||||
Exts []NamespaceExtension
|
||||
}
|
||||
|
||||
type NamespaceExtension struct {
|
||||
// ../rfc/9051:6773
|
||||
Key string
|
||||
Values []string
|
||||
}
|
||||
|
||||
// FetchAttr represents a FETCH response attribute.
|
||||
type FetchAttr interface {
|
||||
Attr() string // Name of attribute.
|
||||
}
|
||||
|
||||
type NumSet struct {
|
||||
SearchResult bool // True if "$", in which case Ranges is irrelevant.
|
||||
Ranges []NumRange
|
||||
}
|
||||
|
||||
func (ns NumSet) IsZero() bool {
|
||||
return !ns.SearchResult && ns.Ranges == nil
|
||||
}
|
||||
|
||||
func (ns NumSet) String() string {
|
||||
if ns.SearchResult {
|
||||
return "$"
|
||||
}
|
||||
var r string
|
||||
for i, x := range ns.Ranges {
|
||||
if i > 0 {
|
||||
r += ","
|
||||
}
|
||||
r += x.String()
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// NumRange is a single number or range.
|
||||
type NumRange struct {
|
||||
First uint32 // 0 for "*".
|
||||
Last *uint32 // Nil if absent, 0 for "*".
|
||||
}
|
||||
|
||||
func (nr NumRange) String() string {
|
||||
var r string
|
||||
if nr.First == 0 {
|
||||
r += "*"
|
||||
} else {
|
||||
r += fmt.Sprintf("%d", nr.First)
|
||||
}
|
||||
if nr.Last == nil {
|
||||
return r
|
||||
}
|
||||
r += ":"
|
||||
v := *nr.Last
|
||||
if v == 0 {
|
||||
r += "*"
|
||||
} else {
|
||||
r += fmt.Sprintf("%d", v)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
type TaggedExtComp struct {
|
||||
String string
|
||||
Comps []TaggedExtComp // Used for both space-separated and ().
|
||||
}
|
||||
|
||||
type TaggedExtVal struct {
|
||||
// ../rfc/9051:7111
|
||||
Number *int64
|
||||
SeqSet *NumSet
|
||||
Comp *TaggedExtComp // If SimpleNumber and SimpleSeqSet is nil, this is a Comp. But Comp is optional and can also be nil. Not great.
|
||||
}
|
||||
|
||||
type MboxListExtendedItem struct {
|
||||
// ../rfc/9051:6699
|
||||
Tag string
|
||||
Val TaggedExtVal
|
||||
}
|
||||
|
||||
// "FLAGS" fetch response.
|
||||
type FetchFlags []string
|
||||
|
||||
func (f FetchFlags) Attr() string { return "FLAGS" }
|
||||
|
||||
// "ENVELOPE" fetch response.
|
||||
type FetchEnvelope Envelope
|
||||
|
||||
func (f FetchEnvelope) Attr() string { return "ENVELOPE" }
|
||||
|
||||
// Envelope holds the basic email message fields.
|
||||
type Envelope struct {
|
||||
Date string
|
||||
Subject string
|
||||
From, Sender, ReplyTo, To, CC, BCC []Address
|
||||
InReplyTo, MessageID string
|
||||
}
|
||||
|
||||
// Address is an address field in an email message, e.g. To.
|
||||
type Address struct {
|
||||
Name, Adl, Mailbox, Host string
|
||||
}
|
||||
|
||||
// "INTERNALDATE" fetch response.
|
||||
type FetchInternalDate string // todo: parsed time
|
||||
func (f FetchInternalDate) Attr() string { return "INTERNALDATE" }
|
||||
|
||||
// "RFC822.SIZE" fetch response.
|
||||
type FetchRFC822Size int64
|
||||
|
||||
func (f FetchRFC822Size) Attr() string { return "RFC822.SIZE" }
|
||||
|
||||
// "RFC822" fetch response.
|
||||
type FetchRFC822 string
|
||||
|
||||
func (f FetchRFC822) Attr() string { return "RFC822" }
|
||||
|
||||
// "RFC822.HEADER" fetch response.
|
||||
type FetchRFC822Header string
|
||||
|
||||
func (f FetchRFC822Header) Attr() string { return "RFC822.HEADER" }
|
||||
|
||||
// "RFC82.TEXT" fetch response.
|
||||
type FetchRFC822Text string
|
||||
|
||||
func (f FetchRFC822Text) Attr() string { return "RFC822.TEXT" }
|
||||
|
||||
// "BODYSTRUCTURE" fetch response.
|
||||
type FetchBodystructure struct {
|
||||
// ../rfc/9051:6355
|
||||
RespAttr string
|
||||
Body any // BodyType*
|
||||
}
|
||||
|
||||
func (f FetchBodystructure) Attr() string { return f.RespAttr }
|
||||
|
||||
// "BODY" fetch response.
|
||||
type FetchBody struct {
|
||||
// ../rfc/9051:6756 ../rfc/9051:6985
|
||||
RespAttr string
|
||||
Section string // todo: parse more ../rfc/9051:6985
|
||||
Offset int32
|
||||
Body string
|
||||
}
|
||||
|
||||
func (f FetchBody) Attr() string { return f.RespAttr }
|
||||
|
||||
// BodyFields is part of a FETCH BODY[] response.
|
||||
type BodyFields struct {
|
||||
Params [][2]string
|
||||
ContentID, ContentDescr, CTE string
|
||||
Octets int32
|
||||
}
|
||||
|
||||
// BodyTypeMpart represents the body structure a multipart message, with subparts and the multipart media subtype. Used in a FETCH response.
|
||||
type BodyTypeMpart struct {
|
||||
// ../rfc/9051:6411
|
||||
Bodies []any // BodyTypeBasic, BodyTypeMsg, BodyTypeText
|
||||
MediaSubtype string
|
||||
}
|
||||
|
||||
// BodyTypeBasic represents basic information about a part, used in a FETCH response.
|
||||
type BodyTypeBasic struct {
|
||||
// ../rfc/9051:6407
|
||||
MediaType, MediaSubtype string
|
||||
BodyFields BodyFields
|
||||
}
|
||||
|
||||
// BodyTypeMsg represents an email message as a body structure, used in a FETCH response.
|
||||
type BodyTypeMsg struct {
|
||||
// ../rfc/9051:6415
|
||||
MediaType, MediaSubtype string
|
||||
BodyFields BodyFields
|
||||
Envelope Envelope
|
||||
Bodystructure any // One of the BodyType*
|
||||
Lines int64
|
||||
}
|
||||
|
||||
// BodyTypeText represents a text part as a body structure, used in a FETCH response.
|
||||
type BodyTypeText struct {
|
||||
// ../rfc/9051:6418
|
||||
MediaType, MediaSubtype string
|
||||
BodyFields BodyFields
|
||||
Lines int64
|
||||
}
|
||||
|
||||
// "BINARY" fetch response.
|
||||
type FetchBinary struct {
|
||||
RespAttr string
|
||||
Parts []uint32 // Can be nil.
|
||||
Data string
|
||||
}
|
||||
|
||||
func (f FetchBinary) Attr() string { return f.RespAttr }
|
||||
|
||||
// "BINARY.SIZE" fetch response.
|
||||
type FetchBinarySize struct {
|
||||
RespAttr string
|
||||
Parts []uint32
|
||||
Size int64
|
||||
}
|
||||
|
||||
func (f FetchBinarySize) Attr() string { return f.RespAttr }
|
||||
|
||||
// "UID" fetch response.
|
||||
type FetchUID uint32
|
||||
|
||||
func (f FetchUID) Attr() string { return "UID" }
|
Reference in New Issue
Block a user