// Mailbox discovery for the IMAP client. // // Gmail exposes its special-use mailboxes with RFC 6154 attributes. The Sent // mailbox is named "[Gmail]/Sent Mail" on most accounts but "[Google Mail]/Sent // Mail" on others (and "Sent" on non-Gmail servers). Phase 5 asks the server for // the mailbox flagged \Sent instead of hardcoding a name. package mailfetch import ( "fmt" "strings" ) // Mailbox is one entry from an IMAP LIST response. type Mailbox struct { Name string Attributes []string } // Has reports whether the mailbox carries the given attribute (for example // `\Sent` or `\All`). The comparison is case-insensitive. func (m Mailbox) Has(attr string) bool { for _, a := range m.Attributes { if strings.EqualFold(a, attr) { return true } } return false } // ListMailboxes returns every mailbox the server advertises. func (c *Client) ListMailboxes() ([]Mailbox, error) { var out []Mailbox err := c.exec(`LIST "" "*"`, func(r *response) error { if mb, ok := parseList(r.line); ok { out = append(out, mb) } return nil }) return out, err } // sentFallbacks are used when the server does not advertise the \Sent // attribute. var sentFallbacks = []string{ "[Gmail]/Sent Mail", "[Google Mail]/Sent Mail", "Sent", "Sent Mail", } // SentMailbox returns the mailbox the server flags \Sent. It falls back to the // common names when the attribute is absent. func (c *Client) SentMailbox() (string, error) { boxes, err := c.ListMailboxes() if err != nil { return "", err } for _, mb := range boxes { if mb.Has(`\Sent`) { return mb.Name, nil } } for _, want := range sentFallbacks { for _, mb := range boxes { if strings.EqualFold(mb.Name, want) { return mb.Name, nil } } } return "", fmt.Errorf("no sent mailbox advertised by the server") } // parseList parses one "* LIST (attrs) delim name" response line. XLIST is // accepted as well, for servers that only implement the older extension. func parseList(line string) (Mailbox, bool) { upper := strings.ToUpper(line) if !strings.HasPrefix(upper, "* LIST ") && !strings.HasPrefix(upper, "* XLIST ") { return Mailbox{}, false } open := strings.IndexByte(line, '(') if open < 0 { return Mailbox{}, false } closeIdx := strings.IndexByte(line[open:], ')') if closeIdx < 0 { return Mailbox{}, false } closeIdx += open attrs := strings.Fields(line[open+1 : closeIdx]) tail := strings.TrimSpace(line[closeIdx+1:]) name := lastQuoted(tail) if name == "" { fields := strings.Fields(tail) if len(fields) == 0 { return Mailbox{}, false } name = strings.Trim(fields[len(fields)-1], `"`) } if name == "" { return Mailbox{}, false } return Mailbox{Name: name, Attributes: attrs}, true } // lastQuoted returns the contents of the last "..." segment in s. func lastQuoted(s string) string { end := strings.LastIndexByte(s, '"') if end <= 0 { return "" } start := strings.LastIndexByte(s[:end], '"') if start < 0 { return "" } return s[start+1 : end] }