469 строки
9.0 KiB
Go
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:]
|
|
}
|