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 }