This is the v1 version, had the v2 before.
This commit is contained in:
314
mailbox.go
Normal file
314
mailbox.go
Normal file
@@ -0,0 +1,314 @@
|
||||
package imap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/emersion/go-imap/utf7"
|
||||
)
|
||||
|
||||
// The primary mailbox, as defined in RFC 3501 section 5.1.
|
||||
const InboxName = "INBOX"
|
||||
|
||||
// CanonicalMailboxName returns the canonical form of a mailbox name. Mailbox names can be
|
||||
// case-sensitive or case-insensitive depending on the backend implementation.
|
||||
// The special INBOX mailbox is case-insensitive.
|
||||
func CanonicalMailboxName(name string) string {
|
||||
if strings.EqualFold(name, InboxName) {
|
||||
return InboxName
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// Mailbox attributes definied in RFC 3501 section 7.2.2.
|
||||
const (
|
||||
// It is not possible for any child levels of hierarchy to exist under this\
|
||||
// name; no child levels exist now and none can be created in the future.
|
||||
NoInferiorsAttr = "\\Noinferiors"
|
||||
// It is not possible to use this name as a selectable mailbox.
|
||||
NoSelectAttr = "\\Noselect"
|
||||
// The mailbox has been marked "interesting" by the server; the mailbox
|
||||
// probably contains messages that have been added since the last time the
|
||||
// mailbox was selected.
|
||||
MarkedAttr = "\\Marked"
|
||||
// The mailbox does not contain any additional messages since the last time
|
||||
// the mailbox was selected.
|
||||
UnmarkedAttr = "\\Unmarked"
|
||||
)
|
||||
|
||||
// Mailbox attributes defined in RFC 6154 section 2 (SPECIAL-USE extension).
|
||||
const (
|
||||
// This mailbox presents all messages in the user's message store.
|
||||
AllAttr = "\\All"
|
||||
// This mailbox is used to archive messages.
|
||||
ArchiveAttr = "\\Archive"
|
||||
// This mailbox is used to hold draft messages -- typically, messages that are
|
||||
// being composed but have not yet been sent.
|
||||
DraftsAttr = "\\Drafts"
|
||||
// This mailbox presents all messages marked in some way as "important".
|
||||
FlaggedAttr = "\\Flagged"
|
||||
// This mailbox is where messages deemed to be junk mail are held.
|
||||
JunkAttr = "\\Junk"
|
||||
// This mailbox is used to hold copies of messages that have been sent.
|
||||
SentAttr = "\\Sent"
|
||||
// This mailbox is used to hold messages that have been deleted or marked for
|
||||
// deletion.
|
||||
TrashAttr = "\\Trash"
|
||||
)
|
||||
|
||||
// Mailbox attributes defined in RFC 3348 (CHILDREN extension)
|
||||
const (
|
||||
// The presence of this attribute indicates that the mailbox has child
|
||||
// mailboxes.
|
||||
HasChildrenAttr = "\\HasChildren"
|
||||
// The presence of this attribute indicates that the mailbox has no child
|
||||
// mailboxes.
|
||||
HasNoChildrenAttr = "\\HasNoChildren"
|
||||
)
|
||||
|
||||
// This mailbox attribute is a signal that the mailbox contains messages that
|
||||
// are likely important to the user. This attribute is defined in RFC 8457
|
||||
// section 3.
|
||||
const ImportantAttr = "\\Important"
|
||||
|
||||
// Basic mailbox info.
|
||||
type MailboxInfo struct {
|
||||
// The mailbox attributes.
|
||||
Attributes []string
|
||||
// The server's path separator.
|
||||
Delimiter string
|
||||
// The mailbox name.
|
||||
Name string
|
||||
}
|
||||
|
||||
// Parse mailbox info from fields.
|
||||
func (info *MailboxInfo) Parse(fields []interface{}) error {
|
||||
if len(fields) < 3 {
|
||||
return errors.New("Mailbox info needs at least 3 fields")
|
||||
}
|
||||
|
||||
var err error
|
||||
if info.Attributes, err = ParseStringList(fields[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ok bool
|
||||
if info.Delimiter, ok = fields[1].(string); !ok {
|
||||
// The delimiter may be specified as NIL, which gets converted to a nil interface.
|
||||
if fields[1] != nil {
|
||||
return errors.New("Mailbox delimiter must be a string")
|
||||
}
|
||||
info.Delimiter = ""
|
||||
}
|
||||
|
||||
if name, err := ParseString(fields[2]); err != nil {
|
||||
return err
|
||||
} else if name, err := utf7.Encoding.NewDecoder().String(name); err != nil {
|
||||
return err
|
||||
} else {
|
||||
info.Name = CanonicalMailboxName(name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Format mailbox info to fields.
|
||||
func (info *MailboxInfo) Format() []interface{} {
|
||||
name, _ := utf7.Encoding.NewEncoder().String(info.Name)
|
||||
attrs := make([]interface{}, len(info.Attributes))
|
||||
for i, attr := range info.Attributes {
|
||||
attrs[i] = RawString(attr)
|
||||
}
|
||||
|
||||
// If the delimiter is NIL, we need to treat it specially by inserting
|
||||
// a nil field (so that it's later converted to an unquoted NIL atom).
|
||||
var del interface{}
|
||||
|
||||
if info.Delimiter != "" {
|
||||
del = info.Delimiter
|
||||
}
|
||||
|
||||
// Thunderbird doesn't understand delimiters if not quoted
|
||||
return []interface{}{attrs, del, FormatMailboxName(name)}
|
||||
}
|
||||
|
||||
// TODO: optimize this
|
||||
func (info *MailboxInfo) match(name, pattern string) bool {
|
||||
i := strings.IndexAny(pattern, "*%")
|
||||
if i == -1 {
|
||||
// No more wildcards
|
||||
return name == pattern
|
||||
}
|
||||
|
||||
// Get parts before and after wildcard
|
||||
chunk, wildcard, rest := pattern[0:i], pattern[i], pattern[i+1:]
|
||||
|
||||
// Check that name begins with chunk
|
||||
if len(chunk) > 0 && !strings.HasPrefix(name, chunk) {
|
||||
return false
|
||||
}
|
||||
name = strings.TrimPrefix(name, chunk)
|
||||
|
||||
// Expand wildcard
|
||||
var j int
|
||||
for j = 0; j < len(name); j++ {
|
||||
if wildcard == '%' && string(name[j]) == info.Delimiter {
|
||||
break // Stop on delimiter if wildcard is %
|
||||
}
|
||||
// Try to match the rest from here
|
||||
if info.match(name[j:], rest) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return info.match(name[j:], rest)
|
||||
}
|
||||
|
||||
// Match checks if a reference and a pattern matches this mailbox name, as
|
||||
// defined in RFC 3501 section 6.3.8.
|
||||
func (info *MailboxInfo) Match(reference, pattern string) bool {
|
||||
name := info.Name
|
||||
|
||||
if info.Delimiter != "" && strings.HasPrefix(pattern, info.Delimiter) {
|
||||
reference = ""
|
||||
pattern = strings.TrimPrefix(pattern, info.Delimiter)
|
||||
}
|
||||
if reference != "" {
|
||||
if info.Delimiter != "" && !strings.HasSuffix(reference, info.Delimiter) {
|
||||
reference += info.Delimiter
|
||||
}
|
||||
if !strings.HasPrefix(name, reference) {
|
||||
return false
|
||||
}
|
||||
name = strings.TrimPrefix(name, reference)
|
||||
}
|
||||
|
||||
return info.match(name, pattern)
|
||||
}
|
||||
|
||||
// A mailbox status.
|
||||
type MailboxStatus struct {
|
||||
// The mailbox name.
|
||||
Name string
|
||||
// True if the mailbox is open in read-only mode.
|
||||
ReadOnly bool
|
||||
// The mailbox items that are currently filled in. This map's values
|
||||
// should not be used directly, they must only be used by libraries
|
||||
// implementing extensions of the IMAP protocol.
|
||||
Items map[StatusItem]interface{}
|
||||
|
||||
// The Items map may be accessed in different goroutines. Protect
|
||||
// concurrent writes.
|
||||
ItemsLocker sync.Mutex
|
||||
|
||||
// The mailbox flags.
|
||||
Flags []string
|
||||
// The mailbox permanent flags.
|
||||
PermanentFlags []string
|
||||
// The sequence number of the first unseen message in the mailbox.
|
||||
UnseenSeqNum uint32
|
||||
|
||||
// The number of messages in this mailbox.
|
||||
Messages uint32
|
||||
// The number of messages not seen since the last time the mailbox was opened.
|
||||
Recent uint32
|
||||
// The number of unread messages.
|
||||
Unseen uint32
|
||||
// The next UID.
|
||||
UidNext uint32
|
||||
// Together with a UID, it is a unique identifier for a message.
|
||||
// Must be greater than or equal to 1.
|
||||
UidValidity uint32
|
||||
|
||||
// Per-mailbox limit of message size. Set only if server supports the
|
||||
// APPENDLIMIT extension.
|
||||
AppendLimit uint32
|
||||
}
|
||||
|
||||
// Create a new mailbox status that will contain the specified items.
|
||||
func NewMailboxStatus(name string, items []StatusItem) *MailboxStatus {
|
||||
status := &MailboxStatus{
|
||||
Name: name,
|
||||
Items: make(map[StatusItem]interface{}),
|
||||
}
|
||||
|
||||
for _, k := range items {
|
||||
status.Items[k] = nil
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
func (status *MailboxStatus) Parse(fields []interface{}) error {
|
||||
status.Items = make(map[StatusItem]interface{})
|
||||
|
||||
var k StatusItem
|
||||
for i, f := range fields {
|
||||
if i%2 == 0 {
|
||||
if kstr, ok := f.(string); !ok {
|
||||
return fmt.Errorf("cannot parse mailbox status: key is not a string, but a %T", f)
|
||||
} else {
|
||||
k = StatusItem(strings.ToUpper(kstr))
|
||||
}
|
||||
} else {
|
||||
status.Items[k] = nil
|
||||
|
||||
var err error
|
||||
switch k {
|
||||
case StatusMessages:
|
||||
status.Messages, err = ParseNumber(f)
|
||||
case StatusRecent:
|
||||
status.Recent, err = ParseNumber(f)
|
||||
case StatusUnseen:
|
||||
status.Unseen, err = ParseNumber(f)
|
||||
case StatusUidNext:
|
||||
status.UidNext, err = ParseNumber(f)
|
||||
case StatusUidValidity:
|
||||
status.UidValidity, err = ParseNumber(f)
|
||||
case StatusAppendLimit:
|
||||
status.AppendLimit, err = ParseNumber(f)
|
||||
default:
|
||||
status.Items[k] = f
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (status *MailboxStatus) Format() []interface{} {
|
||||
var fields []interface{}
|
||||
for k, v := range status.Items {
|
||||
switch k {
|
||||
case StatusMessages:
|
||||
v = status.Messages
|
||||
case StatusRecent:
|
||||
v = status.Recent
|
||||
case StatusUnseen:
|
||||
v = status.Unseen
|
||||
case StatusUidNext:
|
||||
v = status.UidNext
|
||||
case StatusUidValidity:
|
||||
v = status.UidValidity
|
||||
case StatusAppendLimit:
|
||||
v = status.AppendLimit
|
||||
}
|
||||
|
||||
fields = append(fields, RawString(k), v)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func FormatMailboxName(name string) interface{} {
|
||||
// Some e-mails servers don't handle quoted INBOX names correctly so we special-case it.
|
||||
if strings.EqualFold(name, "INBOX") {
|
||||
return RawString(name)
|
||||
}
|
||||
return name
|
||||
}
|
||||
Reference in New Issue
Block a user