summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/mattermost/rsc/imap/imap.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/mattermost/rsc/imap/imap.go')
-rw-r--r--vendor/github.com/mattermost/rsc/imap/imap.go1110
1 files changed, 1110 insertions, 0 deletions
diff --git a/vendor/github.com/mattermost/rsc/imap/imap.go b/vendor/github.com/mattermost/rsc/imap/imap.go
new file mode 100644
index 000000000..6555984d2
--- /dev/null
+++ b/vendor/github.com/mattermost/rsc/imap/imap.go
@@ -0,0 +1,1110 @@
+package imap
+
+import (
+ "bufio"
+ "bytes"
+ "crypto/md5"
+ "crypto/tls"
+ "fmt"
+ "io"
+ "log"
+ "net"
+ "os"
+ "os/exec"
+ "strconv"
+ "strings"
+ "sync"
+)
+
+var Debug = false
+
+const tag = "#"
+
+// A Mode specifies the IMAP connection mode.
+type Mode int
+
+const (
+ Unencrypted Mode = iota // unencrypted TCP connection
+ StartTLS // use IMAP STARTTLS command - unimplemented!
+ TLS // direct TLS connection
+ Command // exec shell command (server name)
+)
+
+type lock struct {
+ locked bool
+ mu sync.Mutex
+}
+
+func (l *lock) lock() {
+ l.mu.Lock()
+ l.locked = true
+}
+
+func (l *lock) unlock() {
+ l.mustBeLocked()
+ l.locked = false
+ l.mu.Unlock()
+}
+
+func (l *lock) mustBeLocked() {
+ if !l.locked {
+ panic("not locked")
+ }
+}
+
+type Client struct {
+ server string
+ user string
+ passwd string
+ mode Mode
+ root string
+
+ io lock
+ rw io.ReadWriteCloser // i/o to server
+ b *bufio.Reader // buffered rw
+ autoReconnect bool // reconnect on failure
+ connected bool // rw is active
+
+ data lock
+ capability map[string]bool
+ flags Flags
+ boxByName map[string]*Box // all known boxes
+ allBox []*Box // all known boxes (do we need this?)
+ rootBox *Box // root of box tree
+ inbox *Box // inbox (special, not in tree)
+ box *Box // selected (current) box
+ nextBox *Box // next box to select (do we need this?)
+}
+
+func NewClient(mode Mode, server, user, passwd string, root string) (*Client, error) {
+ c := &Client{
+ server: server,
+ user: user,
+ passwd: passwd,
+ mode: mode,
+ root: root,
+ boxByName: map[string]*Box{},
+ }
+ c.io.lock()
+ if err := c.reconnect(); err != nil {
+ return nil, err
+ }
+ c.autoReconnect = true
+ c.io.unlock()
+
+ return c, nil
+}
+
+func (c *Client) Close() error {
+ c.io.lock()
+ c.autoReconnect = false
+ c.connected = false
+ if c.rw != nil {
+ c.rw.Close()
+ c.rw = nil
+ }
+ c.io.unlock()
+ return nil
+}
+
+func (c *Client) reconnect() error {
+ c.io.mustBeLocked()
+ c.autoReconnect = false
+ if c.rw != nil {
+ c.rw.Close()
+ c.rw = nil
+ }
+
+ if Debug {
+ log.Printf("dial %s...", c.server)
+ }
+ rw, err := dial(c.server, c.mode)
+ if err != nil {
+ return err
+ }
+
+ c.rw = rw
+ c.connected = true
+ c.capability = nil
+ c.box = nil
+ if Debug {
+ c.b = bufio.NewReader(&tee{rw, os.Stderr})
+ } else {
+ c.b = bufio.NewReader(rw)
+ }
+ x, err := c.rdsx()
+ if x == nil {
+ err = fmt.Errorf("no greeting from %s: %v", c.server, err)
+ goto Error
+ }
+ if len(x.sx) < 2 || !x.sx[0].isAtom("*") || !x.sx[1].isAtom("PREAUTH") {
+ if !x.ok() {
+ err = fmt.Errorf("bad greeting - %s", x)
+ goto Error
+ }
+ if err = c.login(); err != nil {
+ goto Error
+ }
+ }
+ if c.capability == nil {
+ if err = c.cmd(nil, "CAPABILITY"); err != nil {
+ goto Error
+ }
+ if c.capability == nil {
+ err = fmt.Errorf("CAPABILITY command did not return capability list")
+ goto Error
+ }
+ }
+ if err := c.getBoxes(); err != nil {
+ goto Error
+ }
+ c.autoReconnect = true
+ return nil
+
+Error:
+ if c.rw != nil {
+ c.rw.Close()
+ c.rw = nil
+ }
+ c.autoReconnect = true
+ c.connected = false
+ return err
+}
+
+var testDial func(string, Mode) (io.ReadWriteCloser, error)
+
+func dial(server string, mode Mode) (io.ReadWriteCloser, error) {
+ if testDial != nil {
+ return testDial(server, mode)
+ }
+ switch mode {
+ default:
+ // also case Unencrypted
+ return net.Dial("tcp", server+":143")
+ case StartTLS:
+ return nil, fmt.Errorf("StartTLS not supported")
+ case TLS:
+ return tls.Dial("tcp", server+":993", nil)
+ case Command:
+ cmd := exec.Command("sh", "-c", server)
+ cmd.Stderr = os.Stderr
+ r, err := cmd.StdoutPipe()
+ if err != nil {
+ return nil, err
+ }
+ w, err := cmd.StdinPipe()
+ if err != nil {
+ r.Close()
+ return nil, err
+ }
+ if err := cmd.Start(); err != nil {
+ r.Close()
+ w.Close()
+ return nil, err
+ }
+ return &pipe2{r, w}, nil
+ }
+ panic("not reached")
+}
+
+type pipe2 struct {
+ io.ReadCloser
+ io.WriteCloser
+}
+
+func (p *pipe2) Close() error {
+ p.ReadCloser.Close()
+ p.WriteCloser.Close()
+ return nil
+}
+
+type tee struct {
+ r io.Reader
+ w io.Writer
+}
+
+func (t tee) Read(p []byte) (n int, err error) {
+ n, err = t.r.Read(p)
+ if n > 0 {
+ t.w.Write(p[0:n])
+ }
+ return
+}
+
+func (c *Client) rdsx() (*sx, error) {
+ c.io.mustBeLocked()
+ return rdsx(c.b)
+}
+
+type sxError struct {
+ x *sx
+}
+
+func (e *sxError) Error() string { return e.x.String() }
+
+func (c *Client) cmd(b *Box, format string, args ...interface{}) error {
+ x, err := c.cmdsx(b, format, args...)
+ if err != nil {
+ return err
+ }
+ if !x.ok() {
+ return &sxError{x}
+ }
+ return nil
+}
+
+// cmdsx0 runs a single command and return the sx. Does not redial.
+func (c *Client) cmdsx0(format string, args ...interface{}) (*sx, error) {
+ c.io.mustBeLocked()
+ if c.rw == nil || !c.connected {
+ return nil, fmt.Errorf("not connected")
+ }
+
+ cmd := fmt.Sprintf(format, args...)
+ if Debug {
+ fmt.Fprintf(os.Stderr, ">>> %s %s\n", tag, cmd)
+ }
+ if _, err := fmt.Fprintf(c.rw, "%s %s\r\n", tag, cmd); err != nil {
+ c.connected = false
+ return nil, err
+ }
+ return c.waitsx()
+}
+
+// cmdsx runs a command on box b. It does redial.
+func (c *Client) cmdsx(b *Box, format string, args ...interface{}) (*sx, error) {
+ c.io.mustBeLocked()
+ c.nextBox = b
+
+Trying:
+ for tries := 0; ; tries++ {
+ if c.rw == nil || !c.connected {
+ if !c.autoReconnect {
+ return nil, fmt.Errorf("not connected")
+ }
+ if err := c.reconnect(); err != nil {
+ return nil, err
+ }
+ if b != nil && c.nextBox == nil {
+ // box disappeared on reconnect
+ return nil, fmt.Errorf("box is gone")
+ }
+ }
+
+ if b != nil && b != c.box {
+ if c.box != nil {
+ // TODO c.box.init = false
+ }
+ c.box = b
+ if _, err := c.cmdsx0("SELECT %s", iquote(b.Name)); err != nil {
+ c.box = nil
+ if tries++; tries == 1 && (c.rw == nil || !c.connected) {
+ continue Trying
+ }
+ return nil, err
+ }
+ }
+
+ x, err := c.cmdsx0(format, args...)
+ if err != nil {
+ if tries++; tries == 1 && (c.rw == nil || !c.connected) {
+ continue Trying
+ }
+ return nil, err
+ }
+ return x, nil
+ }
+ panic("not reached")
+}
+
+func (c *Client) waitsx() (*sx, error) {
+ c.io.mustBeLocked()
+ for {
+ x, err := c.rdsx()
+ if err != nil {
+ c.connected = false
+ return nil, err
+ }
+ if len(x.sx) >= 1 && x.sx[0].kind == sxAtom {
+ if x.sx[0].isAtom(tag) {
+ return x, nil
+ }
+ if x.sx[0].isAtom("*") {
+ c.unexpected(x)
+ }
+ }
+ if x.kind == sxList && len(x.sx) == 0 {
+ c.connected = false
+ return nil, fmt.Errorf("empty response")
+ }
+ }
+ panic("not reached")
+}
+
+func iquote(s string) string {
+ if s == "" {
+ return `""`
+ }
+
+ for i := 0; i < len(s); i++ {
+ if s[i] >= 0x80 || s[i] <= ' ' || s[i] == '\\' || s[i] == '"' {
+ goto Quote
+ }
+ }
+ return s
+
+Quote:
+ var b bytes.Buffer
+ b.WriteByte('"')
+ for i := 0; i < len(s); i++ {
+ if s[i] == '\\' || s[i] == '"' {
+ b.WriteByte('\\')
+ }
+ b.WriteByte(s[i])
+ }
+ b.WriteByte('"')
+ return b.String()
+}
+
+func (c *Client) login() error {
+ c.io.mustBeLocked()
+ x, err := c.cmdsx(nil, "LOGIN %s %s", iquote(c.user), iquote(c.passwd))
+ if err != nil {
+ return err
+ }
+ if !x.ok() {
+ return fmt.Errorf("login rejected: %s", x)
+ }
+ return nil
+}
+
+func (c *Client) getBoxes() error {
+ c.io.mustBeLocked()
+ for _, b := range c.allBox {
+ b.dead = true
+ // b.exists = 0
+ // b.maxSeen = 0
+ }
+ list := "LIST"
+ if c.capability["XLIST"] { // Gmail extension
+ list = "XLIST"
+ }
+ if err := c.cmd(nil, "%s %s *", list, iquote(c.root)); err != nil {
+ return err
+ }
+ if err := c.cmd(nil, "%s %s INBOX", list, iquote(c.root)); err != nil {
+ return err
+ }
+ if c.nextBox != nil && c.nextBox.dead {
+ c.nextBox = nil
+ }
+ for _, b := range c.allBox {
+ if b.dead {
+ delete(c.boxByName, b.Name)
+ }
+ b.firstNum = 0
+ }
+ c.allBox = boxTrim(c.allBox)
+ for _, b := range c.allBox {
+ b.child = boxTrim(b.child)
+ }
+ return nil
+}
+
+func boxTrim(list []*Box) []*Box {
+ w := 0
+ for _, b := range list {
+ if !b.dead {
+ list[w] = b
+ w++
+ }
+ }
+ return list[:w]
+}
+
+const maxFetch = 1000
+
+func (c *Client) setAutoReconnect(b bool) {
+ c.autoReconnect = b
+}
+
+func (c *Client) check(b *Box) error {
+ c.io.mustBeLocked()
+ if b.dead {
+ return fmt.Errorf("box is gone")
+ }
+
+ b.load = true
+
+ // Update exists count.
+ if err := c.cmd(b, "NOOP"); err != nil {
+ return err
+ }
+
+ // Have to get through this in one session.
+ // Caller can call again if we get disconnected
+ // and return an error.
+ c.autoReconnect = false
+ defer c.setAutoReconnect(true)
+
+ // First load after reconnect: figure out what changed.
+ if b.firstNum == 0 && len(b.msgByUID) > 0 {
+ var lo, hi uint32 = 1<<32 - 1, 0
+ for _, m := range b.msgByUID {
+ m.dead = true
+ uid := uint32(m.UID)
+ if lo > uid {
+ lo = uid
+ }
+ if hi < uid {
+ hi = uid
+ }
+ m.num = 0
+ }
+ if err := c.cmd(b, "UID FETCH %d:%d FLAGS", lo, hi); err != nil {
+ return err
+ }
+ for _, m := range b.msgByUID {
+ if m.dead {
+ delete(b.msgByUID, m.UID)
+ }
+ }
+ }
+
+ // First-ever load.
+ if b.firstNum == 0 {
+ if b.exists <= maxFetch {
+ b.firstNum = 1
+ } else {
+ b.firstNum = b.exists - maxFetch + 1
+ }
+ n := b.exists - b.firstNum + 1
+ b.msgByNum = make([]*Msg, n)
+ return c.fetchBox(b, b.firstNum, 0)
+ }
+
+ if b.exists <= b.maxSeen {
+ return nil
+ }
+ return c.fetchBox(b, b.maxSeen, 0)
+}
+
+func (c *Client) fetchBox(b *Box, lo int, hi int) error {
+ c.io.mustBeLocked()
+ if b != c.box {
+ if err := c.cmd(b, "NOOP"); err != nil {
+ return err
+ }
+ }
+ extra := ""
+ if c.IsGmail() {
+ extra = " X-GM-MSGID X-GM-THRID X-GM-LABELS"
+ }
+ slo := fmt.Sprint(lo)
+ shi := "*"
+ if hi > 0 {
+ shi = fmt.Sprint(hi)
+ }
+ return c.cmd(b, "FETCH %s:%s (FLAGS UID INTERNALDATE RFC822.SIZE ENVELOPE BODY%s)", slo, shi, extra)
+}
+
+func (c *Client) IsGmail() bool {
+ return c.capability["X-GM-EXT-1"]
+}
+
+// Table-driven IMAP "unexpected response" parser.
+// All the interesting data is in the unexpected responses.
+
+var unextab = []struct {
+ num int
+ name string
+ fmt string
+ fn func(*Client, *sx)
+}{
+ {0, "BYE", "", xbye},
+ {0, "CAPABILITY", "", xcapability},
+ {0, "FLAGS", "AAL", xflags},
+ {0, "LIST", "AALSS", xlist},
+ {0, "XLIST", "AALSS", xlist},
+ {0, "OK", "", xok},
+ // {0, "SEARCH", "AAN*", xsearch},
+ {1, "EXISTS", "ANA", xexists},
+ {1, "EXPUNGE", "ANA", xexpunge},
+ {1, "FETCH", "ANAL", xfetch},
+ // {1, "RECENT", "ANA", xrecent}, // why do we care?
+}
+
+func (c *Client) unexpected(x *sx) {
+ c.io.mustBeLocked()
+ var num int
+ var name string
+
+ if len(x.sx) >= 3 && x.sx[1].kind == sxNumber && x.sx[2].kind == sxAtom {
+ num = 1
+ name = string(x.sx[2].data)
+ } else if len(x.sx) >= 2 && x.sx[1].kind == sxAtom {
+ num = 0
+ name = string(x.sx[1].data)
+ } else {
+ return
+ }
+
+ c.data.lock()
+ for _, t := range unextab {
+ if t.num == num && strings.EqualFold(t.name, name) {
+ if t.fmt != "" && !x.match(t.fmt) {
+ log.Printf("malformd %s: %s", name, x)
+ continue
+ }
+ t.fn(c, x)
+ }
+ }
+ c.data.unlock()
+}
+
+func xbye(c *Client, x *sx) {
+ c.io.mustBeLocked()
+ c.rw.Close()
+ c.rw = nil
+ c.connected = false
+}
+
+func xflags(c *Client, x *sx) {
+ c.data.mustBeLocked()
+ // This response contains in x.sx[2] the list of flags
+ // that can be validly attached to messages in c.box.
+ if b := c.box; b != nil {
+ c.flags = x.sx[2].parseFlags()
+ }
+}
+
+func xcapability(c *Client, x *sx) {
+ c.data.mustBeLocked()
+ c.capability = make(map[string]bool)
+ for _, xx := range x.sx[2:] {
+ if xx.kind == sxAtom {
+ c.capability[string(xx.data)] = true
+ }
+ }
+}
+
+func xlist(c *Client, x *sx) {
+ c.data.mustBeLocked()
+ s := string(x.sx[4].data)
+ t := string(x.sx[3].data)
+
+ // INBOX is the special name for the main mailbox.
+ // All the other mailbox names have the root prefix removed, if applicable.
+ inbox := strings.EqualFold(s, "inbox")
+ if inbox {
+ s = "inbox"
+ }
+
+ b := c.newBox(s, t, inbox)
+ if b == nil {
+ return
+ }
+ if inbox {
+ c.inbox = b
+ }
+ if s == c.root {
+ c.rootBox = b
+ }
+ b.dead = false
+ b.flags = x.sx[2].parseFlags()
+}
+
+func xexists(c *Client, x *sx) {
+ c.data.mustBeLocked()
+ if b := c.box; b != nil {
+ b.exists = int(x.sx[1].number)
+ if b.exists < b.maxSeen {
+ b.maxSeen = b.exists
+ }
+ }
+}
+
+func xexpunge(c *Client, x *sx) {
+ c.data.mustBeLocked()
+ if b := c.box; b != nil {
+ n := int(x.sx[1].number)
+ bynum := b.msgByNum
+ if bynum != nil {
+ if n < b.firstNum {
+ b.firstNum--
+ } else if n < b.firstNum+len(bynum) {
+ copy(bynum[n-b.firstNum:], bynum[n-b.firstNum+1:])
+ b.msgByNum = bynum[:len(bynum)-1]
+ } else {
+ log.Printf("expunge unexpected message %d %d %d", b.firstNum, b.exists, b.firstNum+len(bynum))
+ }
+ }
+ if n <= b.exists {
+ b.exists--
+ }
+ }
+}
+
+// Table-driven OK info parser.
+
+var oktab = []struct {
+ name string
+ kind sxKind
+ fn func(*Client, *Box, *sx)
+}{
+ {"UIDVALIDITY", sxNumber, xokuidvalidity},
+ {"PERMANENTFLAGS", sxList, xokpermflags},
+ {"UNSEEN", sxNumber, xokunseen},
+ {"READ-WRITE", 0, xokreadwrite},
+ {"READ-ONLY", 0, xokreadonly},
+}
+
+func xok(c *Client, x *sx) {
+ c.data.mustBeLocked()
+ b := c.box
+ if b == nil {
+ return
+ }
+ if len(x.sx) >= 4 && x.sx[2].kind == sxAtom && x.sx[2].data[0] == '[' {
+ var arg *sx
+ if x.sx[3].kind == sxAtom && x.sx[3].data[0] == ']' {
+ arg = nil
+ } else if x.sx[4].kind == sxAtom && x.sx[4].data[0] == ']' {
+ arg = x.sx[3]
+ } else {
+ log.Printf("cannot parse OK: %s", x)
+ return
+ }
+ x.sx[2].data = x.sx[2].data[1:]
+ for _, t := range oktab {
+ if x.sx[2].isAtom(t.name) {
+ if t.kind != 0 && (arg == nil || arg.kind != t.kind) {
+ log.Printf("malformed %s: %s", t.name, arg)
+ continue
+ }
+ t.fn(c, b, arg)
+ }
+ }
+ }
+}
+
+func xokuidvalidity(c *Client, b *Box, x *sx) {
+ c.data.mustBeLocked()
+ n := uint32(x.number)
+ if b.validity != n {
+ if b.msgByUID != nil {
+ log.Printf("imap: UID validity reset for %s", b.Name)
+ }
+ b.validity = n
+ b.maxSeen = 0
+ b.firstNum = 0
+ b.msgByNum = nil
+ b.msgByUID = nil
+ }
+}
+
+func xokpermflags(c *Client, b *Box, x *sx) {
+ c.data.mustBeLocked()
+ b.permFlags = x.parseFlags()
+}
+
+func xokunseen(c *Client, b *Box, x *sx) {
+ c.data.mustBeLocked()
+ b.unseen = int(x.number)
+}
+
+func xokreadwrite(c *Client, b *Box, x *sx) {
+ c.data.mustBeLocked()
+ b.readOnly = false
+}
+
+func xokreadonly(c *Client, b *Box, x *sx) {
+ c.data.mustBeLocked()
+ b.readOnly = true
+}
+
+// Table-driven FETCH message info parser.
+
+var msgtab = []struct {
+ name string
+ fn func(*Msg, *sx, *sx)
+}{
+ {"FLAGS", xmsgflags},
+ {"INTERNALDATE", xmsgdate},
+ {"RFC822.SIZE", xmsgrfc822size},
+ {"ENVELOPE", xmsgenvelope},
+ {"X-GM-MSGID", xmsggmmsgid},
+ {"X-GM-THRID", xmsggmthrid},
+ {"BODY", xmsgbody},
+ {"BODY[", xmsgbodydata},
+}
+
+func xfetch(c *Client, x *sx) {
+ c.data.mustBeLocked()
+ if c.box == nil {
+ log.Printf("FETCH but no open box: %s", x)
+ return
+ }
+
+ // * 152 FETCH (UID 185 FLAGS() ...)
+ n := x.sx[1].number
+ xx := x.sx[3]
+ if len(xx.sx)%2 != 0 {
+ log.Printf("malformed FETCH: %s", x)
+ return
+ }
+ var uid uint64
+ for i := 0; i < len(xx.sx); i += 2 {
+ if xx.sx[i].isAtom("UID") {
+ if xx.sx[i+1].kind == sxNumber {
+ uid = uint64(xx.sx[i+1].number) | uint64(c.box.validity)<<32
+ goto HaveUID
+ }
+ }
+ }
+ // This happens; too bad.
+ // log.Printf("FETCH without UID: %s", x)
+ return
+
+HaveUID:
+ if m := c.box.msgByUID[uid]; m != nil && m.dead {
+ // FETCH during box garbage collection.
+ m.dead = false
+ m.num = int(n)
+ return
+ }
+ m := c.box.newMsg(uid, int(n))
+ for i := 0; i < len(xx.sx); i += 2 {
+ k, v := xx.sx[i], xx.sx[i+1]
+ for _, t := range msgtab {
+ if k.isAtom(t.name) {
+ t.fn(m, k, v)
+ }
+ }
+ }
+}
+
+func xmsggmmsgid(m *Msg, k, v *sx) {
+ m.GmailID = uint64(v.number)
+}
+
+func xmsggmthrid(m *Msg, k, v *sx) {
+ m.GmailThread = uint64(v.number)
+}
+
+func xmsgflags(m *Msg, k, v *sx) {
+ m.Flags = v.parseFlags()
+}
+
+func xmsgrfc822size(m *Msg, k, v *sx) {
+ m.Bytes = v.number
+}
+
+func xmsgdate(m *Msg, k, v *sx) {
+ m.Date = v.parseDate()
+}
+
+func xmsgenvelope(m *Msg, k, v *sx) {
+ m.Hdr = parseEnvelope(v)
+}
+
+func parseEnvelope(v *sx) *MsgHdr {
+ if v.kind != sxList || !v.match("SSLLLLLLSS") {
+ log.Printf("bad envelope: %s", v)
+ return nil
+ }
+
+ hdr := &MsgHdr{
+ Date: v.sx[0].nstring(),
+ Subject: unrfc2047(v.sx[1].nstring()),
+ From: parseAddrs(v.sx[2]),
+ Sender: parseAddrs(v.sx[3]),
+ ReplyTo: parseAddrs(v.sx[4]),
+ To: parseAddrs(v.sx[5]),
+ CC: parseAddrs(v.sx[6]),
+ BCC: parseAddrs(v.sx[7]),
+ InReplyTo: unrfc2047(v.sx[8].nstring()),
+ MessageID: unrfc2047(v.sx[9].nstring()),
+ }
+
+ h := md5.New()
+ fmt.Fprintf(h, "date: %s\n", hdr.Date)
+ fmt.Fprintf(h, "subject: %s\n", hdr.Subject)
+ fmt.Fprintf(h, "from: %s\n", hdr.From)
+ fmt.Fprintf(h, "sender: %s\n", hdr.Sender)
+ fmt.Fprintf(h, "replyto: %s\n", hdr.ReplyTo)
+ fmt.Fprintf(h, "to: %s\n", hdr.To)
+ fmt.Fprintf(h, "cc: %s\n", hdr.CC)
+ fmt.Fprintf(h, "bcc: %s\n", hdr.BCC)
+ fmt.Fprintf(h, "inreplyto: %s\n", hdr.InReplyTo)
+ fmt.Fprintf(h, "messageid: %s\n", hdr.MessageID)
+ hdr.Digest = fmt.Sprintf("%x", h.Sum(nil))
+
+ return hdr
+}
+
+func parseAddrs(x *sx) []Addr {
+ var addr []Addr
+ for _, xx := range x.sx {
+ if !xx.match("SSSS") {
+ log.Printf("bad address: %s", x)
+ continue
+ }
+ name := unrfc2047(xx.sx[0].nstring())
+ // sx[1] is route
+ local := unrfc2047(xx.sx[2].nstring())
+ host := unrfc2047(xx.sx[3].nstring())
+ if local == "" || host == "" {
+ // rfc822 group syntax
+ addr = append(addr, Addr{name, ""})
+ continue
+ }
+ addr = append(addr, Addr{name, local + "@" + host})
+ }
+ return addr
+}
+
+func xmsgbody(m *Msg, k, v *sx) {
+ if v.isNil() {
+ return
+ }
+ if v.kind != sxList {
+ log.Printf("bad body: %s", v)
+ }
+
+ // To follow the structure exactly we should be doing this
+ // to m.NewPart(m.Part[0]) with type message/rfc822,
+ // but the extra layer is redundant - what else would be in
+ // a mailbox?
+ parseStructure(&m.Root, v)
+ n := m.num
+ if m.Box.maxSeen < n {
+ m.Box.maxSeen = n
+ }
+}
+
+func parseStructure(p *MsgPart, x *sx) {
+ if x.isNil() {
+ return
+ }
+ if x.kind != sxList {
+ log.Printf("bad structure: %s", x)
+ return
+ }
+ if x.sx[0].isList() {
+ // multipart
+ var i int
+ for i = 0; i < len(x.sx) && x.sx[i].isList(); i++ {
+ parseStructure(p.newPart(), x.sx[i])
+ }
+ if i != len(x.sx)-1 || !x.sx[i].isString() {
+ log.Printf("bad multipart structure: %s", x)
+ p.Type = "multipart/mixed"
+ return
+ }
+ s := strlwr(x.sx[i].nstring())
+ p.Type = "multipart/" + s
+ return
+ }
+
+ // single type
+ if len(x.sx) < 2 || !x.sx[0].isString() {
+ log.Printf("bad type structure: %s", x)
+ return
+ }
+ s := strlwr(x.sx[0].nstring())
+ t := strlwr(x.sx[1].nstring())
+ p.Type = s + "/" + t
+ if len(x.sx) < 7 || !x.sx[2].isList() || !x.sx[3].isString() || !x.sx[4].isString() || !x.sx[5].isString() || !x.sx[6].isNumber() {
+ log.Printf("bad part structure: %s", x)
+ return
+ }
+ parseParams(p, x.sx[2])
+ p.ContentID = x.sx[3].nstring()
+ p.Desc = x.sx[4].nstring()
+ p.Encoding = x.sx[5].nstring()
+ p.Bytes = x.sx[6].number
+ if p.Type == "message/rfc822" {
+ if len(x.sx) < 10 || !x.sx[7].isList() || !x.sx[8].isList() || !x.sx[9].isNumber() {
+ log.Printf("bad rfc822 structure: %s", x)
+ return
+ }
+ p.Hdr = parseEnvelope(x.sx[7])
+ parseStructure(p.newPart(), x.sx[8])
+ p.Lines = x.sx[9].number
+ }
+ if s == "text" {
+ if len(x.sx) < 8 || !x.sx[7].isNumber() {
+ log.Printf("bad text structure: %s", x)
+ return
+ }
+ p.Lines = x.sx[7].number
+ }
+}
+
+func parseParams(p *MsgPart, x *sx) {
+ if x.isNil() {
+ return
+ }
+ if len(x.sx)%2 != 0 {
+ log.Printf("bad message params: %s", x)
+ return
+ }
+
+ for i := 0; i < len(x.sx); i += 2 {
+ k, v := x.sx[i].nstring(), x.sx[i+1].nstring()
+ k = strlwr(k)
+ switch strlwr(k) {
+ case "charset":
+ p.Charset = strlwr(v)
+ case "name":
+ p.Name = v
+ }
+ }
+}
+
+func (c *Client) fetch(p *MsgPart, what string) {
+ c.io.mustBeLocked()
+ id := p.ID
+ if what != "" {
+ if id != "" {
+ id += "."
+ }
+ id += what
+ }
+ c.cmd(p.Msg.Box, "UID FETCH %d BODY[%s]", p.Msg.UID&(1<<32-1), id)
+}
+
+func xmsgbodydata(m *Msg, k, v *sx) {
+ // k.data is []byte("BODY[...")
+ name := string(k.data[5:])
+ if i := strings.Index(name, "]"); i >= 0 {
+ name = name[:i]
+ }
+
+ p := &m.Root
+ for name != "" && '1' <= name[0] && name[0] <= '9' {
+ var num int
+ num, name = parseNum(name)
+ if num == 0 {
+ log.Printf("unexpected body name: %s", k.data)
+ return
+ }
+ num--
+ if num >= len(p.Child) {
+ log.Printf("invalid body name: %s", k.data)
+ return
+ }
+ p = p.Child[num]
+ }
+
+ switch strlwr(name) {
+ case "":
+ p.raw = v.nbytes()
+ case "mime":
+ p.mimeHeader = nocr(v.nbytes())
+ case "header":
+ p.rawHeader = nocr(v.nbytes())
+ case "text":
+ p.rawBody = nocr(v.nbytes())
+ }
+}
+
+func parseNum(name string) (int, string) {
+ rest := ""
+ i := strings.Index(name, ".")
+ if i >= 0 {
+ name, rest = name[:i], name[i+1:]
+ }
+ n, _ := strconv.Atoi(name)
+ return n, rest
+}
+
+func nocr(b []byte) []byte {
+ w := 0
+ for _, c := range b {
+ if c != '\r' {
+ b[w] = c
+ w++
+ }
+ }
+ return b[:w]
+}
+
+type uidList []*Msg
+
+func (l uidList) String() string {
+ var b bytes.Buffer
+ for i, m := range l {
+ if i > 0 {
+ b.WriteByte(',')
+ }
+ fmt.Fprintf(&b, "%d", m.UID&(1<<32-1))
+ }
+ return b.String()
+}
+
+func (c *Client) deleteList(msgs []*Msg) error {
+ if len(msgs) == 0 {
+ return nil
+ }
+ c.io.mustBeLocked()
+
+ b := msgs[0].Box
+ for _, m := range msgs {
+ if m.Box != b {
+ return fmt.Errorf("messages span boxes: %q and %q", b.Name, m.Box.Name)
+ }
+ if uint32(m.UID>>32) != b.validity {
+ return fmt.Errorf("stale message")
+ }
+ }
+
+ err := c.cmd(b, "UID STORE %s +FLAGS (\\Deleted)", uidList(msgs))
+ if err == nil && c.box == b {
+ err = c.cmd(b, "EXPUNGE")
+ }
+ return err
+}
+
+func (c *Client) copyList(dst, src *Box, msgs []*Msg) error {
+ if len(msgs) == 0 {
+ return nil
+ }
+ c.io.mustBeLocked()
+
+ for _, m := range msgs {
+ if m.Box != src {
+ return fmt.Errorf("messages span boxes: %q and %q", src.Name, m.Box.Name)
+ }
+ if uint32(m.UID>>32) != src.validity {
+ return fmt.Errorf("stale message")
+ }
+ }
+
+ var name string
+ if dst == c.inbox {
+ name = "INBOX"
+ } else {
+ name = iquote(dst.Name)
+ }
+ return c.cmd(src, "UID COPY %s %s", uidList(msgs), name)
+}
+
+func (c *Client) muteList(src *Box, msgs []*Msg) error {
+ if len(msgs) == 0 {
+ return nil
+ }
+ c.io.mustBeLocked()
+
+ for _, m := range msgs {
+ if m.Box != src {
+ return fmt.Errorf("messages span boxes: %q and %q", src.Name, m.Box.Name)
+ }
+ if uint32(m.UID>>32) != src.validity {
+ return fmt.Errorf("stale message")
+ }
+ }
+
+ return c.cmd(src, "UID STORE %s +X-GM-LABELS (\\Muted)", uidList(msgs))
+}