Files
mostlymatter/vendor/github.com/mattermost/rsc/imap/mail.go
Christopher Speller 38ee83e45b Moving to glide
2016-05-12 23:56:07 -04:00

469 строки
9.0 KiB
Go

package imap
import (
"bytes"
"fmt"
"log"
"regexp"
"sort"
"strings"
"time"
)
type Flags uint32
const (
FlagJunk Flags = 1 << iota
FlagNonJunk
FlagReplied
FlagFlagged
FlagDeleted
FlagDraft
FlagRecent
FlagSeen
FlagNoInferiors
FlagNoSelect
FlagMarked
FlagUnMarked
FlagHasChildren
FlagHasNoChildren
FlagInbox // Gmail extension
FlagAllMail // Gmail extension
FlagDrafts // Gmail extension
FlagSent // Gmail extension
FlagSpam // Gmail extension
FlagStarred // Gmail extension
FlagTrash // Gmail extension
FlagImportant // Gmail extension
)
var flagNames = []string{
"Junk",
"NonJunk",
"\\Answered",
"\\Flagged",
"\\Deleted",
"\\Draft",
"\\Recent",
"\\Seen",
"\\NoInferiors",
"\\NoSelect",
"\\Marked",
"\\UnMarked",
"\\HasChildren",
"\\HasNoChildren",
"\\Inbox",
"\\AllMail",
"\\Drafts",
"\\Sent",
"\\Spam",
"\\Starred",
"\\Trash",
"\\Important",
}
// A Box represents an IMAP mailbox.
type Box struct {
Name string // name of mailbox
Elem string // last element in name
Client *Client
parent *Box // parent in hierarchy
child []*Box // child boxes
dead bool // box no longer exists
inbox bool // box is inbox
flags Flags // allowed flags
permFlags Flags // client-modifiable permanent flags
readOnly bool // box is read-only
exists int // number of messages in box (according to server)
maxSeen int // maximum message number seen (for polling)
unseen int // number of first unseen message
validity uint32 // UID validity base number
load bool // if false, don't track full set of messages
firstNum int // 0 means box not loaded
msgByNum []*Msg
msgByUID map[uint64]*Msg
}
func (c *Client) Boxes() []*Box {
c.data.lock()
defer c.data.unlock()
box := make([]*Box, len(c.allBox))
copy(box, c.allBox)
return box
}
func (c *Client) Box(name string) *Box {
c.data.lock()
defer c.data.unlock()
return c.boxByName[name]
}
func (c *Client) Inbox() *Box {
c.data.lock()
defer c.data.unlock()
return c.inbox
}
func (c *Client) newBox(name, sep string, inbox bool) *Box {
c.data.mustBeLocked()
if b := c.boxByName[name]; b != nil {
return b
}
b := &Box{
Name: name,
Elem: name,
Client: c,
inbox: inbox,
}
if !inbox {
b.parent = c.rootBox
}
if !inbox && sep != "" && name != c.root {
if i := strings.LastIndex(name, sep); i >= 0 {
b.Elem = name[i+len(sep):]
b.parent = c.newBox(name[:i], sep, false)
}
}
c.allBox = append(c.allBox, b)
c.boxByName[name] = b
if b.parent != nil {
b.parent.child = append(b.parent.child, b)
}
return b
}
// A Msg represents an IMAP message.
type Msg struct {
Box *Box // box containing message
Date time.Time // date
Flags Flags // message flags
Bytes int64 // size in bytes
Lines int64 // number of lines
Hdr *MsgHdr // MIME header
Root MsgPart // top-level message part
GmailID uint64 // Gmail message id
GmailThread uint64 // Gmail thread id
UID uint64 // unique id for this message
deleted bool
dead bool
num int // message number in box (changes)
}
// TODO: Return os.Error too
type byUID []*Msg
func (x byUID) Len() int { return len(x) }
func (x byUID) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x byUID) Less(i, j int) bool { return x[i].UID < x[j].UID }
func (b *Box) Msgs() []*Msg {
b.Client.data.lock()
defer b.Client.data.unlock()
msgs := make([]*Msg, len(b.msgByUID))
n := 0
for _, m := range b.msgByUID {
msgs[n] = m
n++
}
sort.Sort(byUID(msgs))
return msgs
}
func (b *Box) newMsg(uid uint64, id int) *Msg {
b.Client.data.mustBeLocked()
if m := b.msgByUID[uid]; m != nil {
return m
}
if b.msgByUID == nil {
b.msgByUID = map[uint64]*Msg{}
}
m := &Msg{
UID: uid,
Box: b,
num: id,
}
m.Root.Msg = m
if b.load {
if b.firstNum == 0 {
b.firstNum = id
}
if id < b.firstNum {
log.Printf("warning: unexpected id %d < %d", id, b.firstNum)
byNum := make([]*Msg, len(b.msgByNum)+b.firstNum-id)
copy(byNum[b.firstNum-id:], b.msgByNum)
b.msgByNum = byNum
b.firstNum = id
}
if id-b.firstNum < len(b.msgByNum) {
b.msgByNum[id-b.firstNum] = m
} else {
if id-b.firstNum > len(b.msgByNum) {
log.Printf("warning: unexpected id %d > %d", id, b.firstNum+len(b.msgByNum))
byNum := make([]*Msg, id-b.firstNum)
copy(byNum, b.msgByNum)
b.msgByNum = byNum
}
b.msgByNum = append(b.msgByNum, m)
}
}
b.msgByUID[uid] = m
return m
}
func (b *Box) Delete(msgs []*Msg) error {
for _, m := range msgs {
if m.Box != b {
return fmt.Errorf("messages not from this box")
}
}
b.Client.io.lock()
defer b.Client.io.unlock()
err := b.Client.deleteList(msgs)
if err == nil {
b.Client.data.lock()
defer b.Client.data.unlock()
for _, m := range msgs {
if m.Flags&FlagDeleted != 0 {
delete(b.msgByUID, m.UID)
}
}
}
return err
}
func (b *Box) Copy(msgs []*Msg) error {
if len(msgs) == 0 {
return nil
}
src := msgs[0].Box
for _, m := range msgs {
if m.Box != src {
return fmt.Errorf("messages span boxes: %q and %q", src.Name, m.Box.Name)
}
}
b.Client.io.lock()
defer b.Client.io.unlock()
return b.Client.copyList(b, src, msgs)
}
func (b *Box) Mute(msgs []*Msg) error {
if len(msgs) == 0 {
return nil
}
for _, m := range msgs {
if m.Box != b {
return fmt.Errorf("messages not from this box")
}
}
b.Client.io.lock()
defer b.Client.io.unlock()
return b.Client.muteList(b, msgs)
}
func (b *Box) Check() error {
b.Client.io.lock()
defer b.Client.io.unlock()
return b.Client.check(b)
}
func (m *Msg) Deleted() bool {
// Racy but okay. Can add a lock later if it matters.
return m.Flags&FlagDeleted != 0
}
// A Hdr represents a message header.
type MsgHdr struct {
Date string
Subject string
From []Addr
Sender []Addr
ReplyTo []Addr
To []Addr
CC []Addr
BCC []Addr
InReplyTo string
MessageID string
Digest string
}
// An Addr represents a single, named email address.
// If Name is empty, only the email address is known.
// If Email is empty, the Addr represents an unspecified (but named) group.
type Addr struct {
Name string
Email string
}
func (a Addr) String() string {
if a.Email == "" {
return a.Name
}
if a.Name == "" {
return a.Email
}
return a.Name + " <" + a.Email + ">"
}
// A MsgPart represents a single part of a MIME-encoded message.
type MsgPart struct {
Msg *Msg // containing message
Type string
ContentID string
Desc string
Encoding string
Bytes int64
Lines int64
Charset string
Name string
Hdr *MsgHdr
ID string
Child []*MsgPart
raw []byte // raw message
rawHeader []byte // raw RFC-2822 header, for message/rfc822
rawBody []byte // raw RFC-2822 body, for message/rfc822
mimeHeader []byte // mime header, for attachments
}
func (p *MsgPart) newPart() *MsgPart {
p.Msg.Box.Client.data.mustBeLocked()
dot := "."
if p.ID == "" { // no dot at root
dot = ""
}
pp := &MsgPart{
Msg: p.Msg,
ID: fmt.Sprint(p.ID, dot, 1+len(p.Child)),
}
p.Child = append(p.Child, pp)
return pp
}
func (p *MsgPart) Text() []byte {
c := p.Msg.Box.Client
var raw []byte
c.data.lock()
if p == &p.Msg.Root {
raw = p.rawBody
c.data.unlock()
if raw == nil {
c.io.lock()
if raw = p.rawBody; raw == nil {
c.fetch(p, "TEXT")
raw = p.rawBody
}
c.io.unlock()
}
} else {
raw = p.raw
c.data.unlock()
if raw == nil {
c.io.lock()
if raw = p.raw; raw == nil {
c.fetch(p, "")
raw = p.raw
}
c.io.unlock()
}
}
return decodeText(raw, p.Encoding, p.Charset, false)
}
func (p *MsgPart) Raw() []byte {
c := p.Msg.Box.Client
var raw []byte
c.data.lock()
raw = p.rawBody
c.data.unlock()
if raw == nil {
c.io.lock()
if raw = p.rawBody; raw == nil {
c.fetch(p, "")
raw = p.rawBody
}
c.io.unlock()
}
return raw
}
var sigDash = []byte("\n--\n")
var quote = []byte("\n> ")
var nl = []byte("\n")
var onwrote = regexp.MustCompile(`\A\s*On .* wrote:\s*\z`)
func (p *MsgPart) ShortText() []byte {
t := p.Text()
return shortText(t)
}
func shortText(t []byte) []byte {
if t == nil {
return nil
}
// Cut signature.
i := bytes.LastIndex(t, sigDash)
j := bytes.LastIndex(t, quote)
if i > j && bytes.Count(t[i+1:], nl) <= 10 {
t = t[:i+1]
}
// Cut trailing quoted text.
for {
rest, last := lastLine(t)
trim := bytes.TrimSpace(last)
if len(rest) < len(t) && (len(trim) == 0 || trim[0] == '>') {
t = rest
continue
}
break
}
// Cut 'On foo.*wrote:' line.
rest, last := lastLine(t)
if onwrote.Match(last) {
t = rest
}
// Cut trailing blank lines.
for {
rest, last := lastLine(t)
trim := bytes.TrimSpace(last)
if len(rest) < len(t) && len(trim) == 0 {
t = rest
continue
}
break
}
// Cut signature again.
i = bytes.LastIndex(t, sigDash)
j = bytes.LastIndex(t, quote)
if i > j && bytes.Count(t[i+1:], nl) <= 10 {
t = t[:i+1]
}
return t
}
func lastLine(t []byte) (rest, last []byte) {
n := len(t)
if n > 0 && t[n-1] == '\n' {
n--
}
j := bytes.LastIndex(t[:n], nl)
return t[:j+1], t[j+1:]
}