247 lines
6.0 KiB
Go
247 lines
6.0 KiB
Go
package imap
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/emersion/go-imap/utf7"
|
|
)
|
|
|
|
// The primary mailbox, as defined in RFC 3501 section 5.1.
|
|
const InboxName = "INBOX"
|
|
|
|
// 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.ToUpper(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"
|
|
)
|
|
|
|
// 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")
|
|
}
|
|
|
|
info.Attributes, _ = ParseStringList(fields[0])
|
|
|
|
info.Delimiter, _ = fields[1].(string)
|
|
|
|
name, _ := fields[2].(string)
|
|
info.Name, _ = utf7.Decoder.String(name)
|
|
info.Name = CanonicalMailboxName(info.Name)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Format mailbox info to fields.
|
|
func (info *MailboxInfo) Format() []interface{} {
|
|
name, _ := utf7.Encoder.String(info.Name)
|
|
// Thunderbird doesn't understand delimiters if not quoted
|
|
return []interface{}{FormatStringList(info.Attributes), Quoted(info.Delimiter), 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 strings.HasPrefix(pattern, info.Delimiter) {
|
|
reference = ""
|
|
pattern = strings.TrimPrefix(pattern, info.Delimiter)
|
|
}
|
|
if reference != "" {
|
|
if !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)
|
|
}
|
|
|
|
// Mailbox status items.
|
|
const (
|
|
MailboxFlags = "FLAGS"
|
|
MailboxPermanentFlags = "PERMANENTFLAGS"
|
|
|
|
// Defined in RFC 3501 section 6.3.10.
|
|
MailboxMessages = "MESSAGES"
|
|
MailboxRecent = "RECENT"
|
|
MailboxUnseen = "UNSEEN"
|
|
MailboxUidNext = "UIDNEXT"
|
|
MailboxUidValidity = "UIDVALIDITY"
|
|
)
|
|
|
|
// 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[string]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 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
|
|
}
|
|
|
|
// Create a new mailbox status that will contain the specified items.
|
|
func NewMailboxStatus(name string, items []string) *MailboxStatus {
|
|
status := &MailboxStatus{
|
|
Name: name,
|
|
Items: make(map[string]interface{}),
|
|
}
|
|
|
|
for _, k := range items {
|
|
status.Items[k] = nil
|
|
}
|
|
|
|
return status
|
|
}
|
|
|
|
func (status *MailboxStatus) Parse(fields []interface{}) error {
|
|
status.Items = make(map[string]interface{})
|
|
|
|
var k string
|
|
for i, f := range fields {
|
|
if i%2 == 0 {
|
|
var ok bool
|
|
if k, ok = f.(string); !ok {
|
|
return errors.New("Key is not a string")
|
|
}
|
|
k = strings.ToUpper(k)
|
|
} else {
|
|
status.Items[k] = nil
|
|
|
|
var err error
|
|
switch k {
|
|
case MailboxMessages:
|
|
status.Messages, err = ParseNumber(f)
|
|
case MailboxRecent:
|
|
status.Recent, err = ParseNumber(f)
|
|
case MailboxUnseen:
|
|
status.Unseen, err = ParseNumber(f)
|
|
case MailboxUidNext:
|
|
status.UidNext, err = ParseNumber(f)
|
|
case MailboxUidValidity:
|
|
status.UidValidity, 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 MailboxMessages:
|
|
v = status.Messages
|
|
case MailboxRecent:
|
|
v = status.Recent
|
|
case MailboxUnseen:
|
|
v = status.Unseen
|
|
case MailboxUidNext:
|
|
v = status.UidNext
|
|
case MailboxUidValidity:
|
|
v = status.UidValidity
|
|
}
|
|
|
|
fields = append(fields, k, v)
|
|
}
|
|
return fields
|
|
}
|