diff options
Diffstat (limited to 'vendor/github.com/mattermost/rsc/imap/imap.go')
-rw-r--r-- | vendor/github.com/mattermost/rsc/imap/imap.go | 1110 |
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)) +} |