326 lines
7.2 KiB
Go
326 lines
7.2 KiB
Go
package imapserver
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/emersion/go-imap/v2"
|
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
|
"github.com/emersion/go-imap/v2/internal/utf7"
|
|
)
|
|
|
|
func (c *Conn) handleList(dec *imapwire.Decoder) error {
|
|
ref, pattern, options, err := readListCmd(dec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
|
return err
|
|
}
|
|
|
|
w := &ListWriter{
|
|
conn: c,
|
|
options: options,
|
|
}
|
|
return c.session.List(w, ref, pattern, options)
|
|
}
|
|
|
|
func (c *Conn) handleLSub(dec *imapwire.Decoder) error {
|
|
var ref string
|
|
if !dec.ExpectSP() || !dec.ExpectMailbox(&ref) || !dec.ExpectSP() {
|
|
return dec.Err()
|
|
}
|
|
pattern, err := readListMailbox(dec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !dec.ExpectCRLF() {
|
|
return dec.Err()
|
|
}
|
|
|
|
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
|
return err
|
|
}
|
|
|
|
options := &imap.ListOptions{SelectSubscribed: true}
|
|
w := &ListWriter{
|
|
conn: c,
|
|
lsub: true,
|
|
}
|
|
return c.session.List(w, ref, []string{pattern}, options)
|
|
}
|
|
|
|
func (c *Conn) writeList(data *imap.ListData) error {
|
|
enc := newResponseEncoder(c)
|
|
defer enc.end()
|
|
|
|
enc.Atom("*").SP().Atom("LIST").SP()
|
|
enc.List(len(data.Attrs), func(i int) {
|
|
enc.MailboxAttr(data.Attrs[i])
|
|
})
|
|
enc.SP()
|
|
if data.Delim == 0 {
|
|
enc.NIL()
|
|
} else {
|
|
enc.Quoted(string(data.Delim))
|
|
}
|
|
enc.SP().Mailbox(data.Mailbox)
|
|
|
|
var ext []string
|
|
if data.ChildInfo != nil {
|
|
ext = append(ext, "CHILDINFO")
|
|
}
|
|
if data.OldName != "" {
|
|
ext = append(ext, "OLDNAME")
|
|
}
|
|
|
|
// TODO: omit extended data if the client didn't ask for it
|
|
if len(ext) > 0 {
|
|
enc.SP().List(len(ext), func(i int) {
|
|
name := ext[i]
|
|
enc.Atom(name).SP()
|
|
switch name {
|
|
case "CHILDINFO":
|
|
enc.Special('(')
|
|
if data.ChildInfo.Subscribed {
|
|
enc.Quoted("SUBSCRIBED")
|
|
}
|
|
enc.Special(')')
|
|
case "OLDNAME":
|
|
enc.Special('(').Mailbox(data.OldName).Special(')')
|
|
default:
|
|
panic(fmt.Errorf("imapserver: unknown LIST extended-item %v", name))
|
|
}
|
|
})
|
|
}
|
|
|
|
return enc.CRLF()
|
|
}
|
|
|
|
func (c *Conn) writeLSub(data *imap.ListData) error {
|
|
enc := newResponseEncoder(c)
|
|
defer enc.end()
|
|
|
|
enc.Atom("*").SP().Atom("LSUB").SP()
|
|
enc.List(len(data.Attrs), func(i int) {
|
|
enc.MailboxAttr(data.Attrs[i])
|
|
})
|
|
enc.SP()
|
|
if data.Delim == 0 {
|
|
enc.NIL()
|
|
} else {
|
|
enc.Quoted(string(data.Delim))
|
|
}
|
|
enc.SP().Mailbox(data.Mailbox)
|
|
return enc.CRLF()
|
|
}
|
|
|
|
func readListCmd(dec *imapwire.Decoder) (ref string, patterns []string, options *imap.ListOptions, err error) {
|
|
options = &imap.ListOptions{}
|
|
|
|
if !dec.ExpectSP() {
|
|
return "", nil, nil, dec.Err()
|
|
}
|
|
|
|
hasSelectOpts, err := dec.List(func() error {
|
|
var selectOpt string
|
|
if !dec.ExpectAString(&selectOpt) {
|
|
return dec.Err()
|
|
}
|
|
switch strings.ToUpper(selectOpt) {
|
|
case "SUBSCRIBED":
|
|
options.SelectSubscribed = true
|
|
case "REMOTE":
|
|
options.SelectRemote = true
|
|
case "RECURSIVEMATCH":
|
|
options.SelectRecursiveMatch = true
|
|
default:
|
|
return newClientBugError("Unknown LIST select option")
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return "", nil, nil, fmt.Errorf("in list-select-opts: %w", err)
|
|
}
|
|
if hasSelectOpts && !dec.ExpectSP() {
|
|
return "", nil, nil, dec.Err()
|
|
}
|
|
|
|
if !dec.ExpectMailbox(&ref) || !dec.ExpectSP() {
|
|
return "", nil, nil, dec.Err()
|
|
}
|
|
|
|
hasPatterns, err := dec.List(func() error {
|
|
pattern, err := readListMailbox(dec)
|
|
if err == nil && pattern != "" {
|
|
patterns = append(patterns, pattern)
|
|
}
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return "", nil, nil, err
|
|
} else if hasPatterns && len(patterns) == 0 {
|
|
return "", nil, nil, newClientBugError("LIST-EXTENDED requires a non-empty parenthesized pattern list")
|
|
} else if !hasPatterns {
|
|
pattern, err := readListMailbox(dec)
|
|
if err != nil {
|
|
return "", nil, nil, err
|
|
}
|
|
if pattern != "" {
|
|
patterns = append(patterns, pattern)
|
|
}
|
|
}
|
|
|
|
if dec.SP() { // list-return-opts
|
|
var atom string
|
|
if !dec.ExpectAtom(&atom) || !dec.Expect(strings.EqualFold(atom, "RETURN"), "RETURN") || !dec.ExpectSP() {
|
|
return "", nil, nil, dec.Err()
|
|
}
|
|
|
|
err := dec.ExpectList(func() error {
|
|
return readReturnOption(dec, options)
|
|
})
|
|
if err != nil {
|
|
return "", nil, nil, fmt.Errorf("in list-return-opts: %w", err)
|
|
}
|
|
}
|
|
|
|
if !dec.ExpectCRLF() {
|
|
return "", nil, nil, dec.Err()
|
|
}
|
|
|
|
if options.SelectRecursiveMatch && !options.SelectSubscribed {
|
|
return "", nil, nil, newClientBugError("The LIST RECURSIVEMATCH select option requires SUBSCRIBED")
|
|
}
|
|
|
|
return ref, patterns, options, nil
|
|
}
|
|
|
|
func readListMailbox(dec *imapwire.Decoder) (string, error) {
|
|
var mailbox string
|
|
if !dec.String(&mailbox) {
|
|
if !dec.Expect(dec.Func(&mailbox, isListChar), "list-char") {
|
|
return "", dec.Err()
|
|
}
|
|
}
|
|
return utf7.Decode(mailbox)
|
|
}
|
|
|
|
func isListChar(ch byte) bool {
|
|
switch ch {
|
|
case '%', '*': // list-wildcards
|
|
return true
|
|
case ']': // resp-specials
|
|
return true
|
|
default:
|
|
return imapwire.IsAtomChar(ch)
|
|
}
|
|
}
|
|
|
|
func readReturnOption(dec *imapwire.Decoder, options *imap.ListOptions) error {
|
|
var name string
|
|
if !dec.ExpectAtom(&name) {
|
|
return dec.Err()
|
|
}
|
|
|
|
switch strings.ToUpper(name) {
|
|
case "SUBSCRIBED":
|
|
options.ReturnSubscribed = true
|
|
case "CHILDREN":
|
|
options.ReturnChildren = true
|
|
case "STATUS":
|
|
if !dec.ExpectSP() {
|
|
return dec.Err()
|
|
}
|
|
options.ReturnStatus = new(imap.StatusOptions)
|
|
return dec.ExpectList(func() error {
|
|
return readStatusItem(dec, options.ReturnStatus)
|
|
})
|
|
default:
|
|
return newClientBugError("Unknown LIST RETURN options")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ListWriter writes LIST responses.
|
|
type ListWriter struct {
|
|
conn *Conn
|
|
options *imap.ListOptions
|
|
lsub bool
|
|
}
|
|
|
|
// WriteList writes a single LIST response for a mailbox.
|
|
func (w *ListWriter) WriteList(data *imap.ListData) error {
|
|
if w.lsub {
|
|
return w.conn.writeLSub(data)
|
|
}
|
|
|
|
if err := w.conn.writeList(data); err != nil {
|
|
return err
|
|
}
|
|
if w.options.ReturnStatus != nil && data.Status != nil {
|
|
if err := w.conn.writeStatus(data.Status, w.options.ReturnStatus); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MatchList checks whether a reference and a pattern matches a mailbox.
|
|
func MatchList(name string, delim rune, reference, pattern string) bool {
|
|
var delimStr string
|
|
if delim != 0 {
|
|
delimStr = string(delim)
|
|
}
|
|
|
|
if delimStr != "" && strings.HasPrefix(pattern, delimStr) {
|
|
reference = ""
|
|
pattern = strings.TrimPrefix(pattern, delimStr)
|
|
}
|
|
if reference != "" {
|
|
if delimStr != "" && !strings.HasSuffix(reference, delimStr) {
|
|
reference += delimStr
|
|
}
|
|
if !strings.HasPrefix(name, reference) {
|
|
return false
|
|
}
|
|
name = strings.TrimPrefix(name, reference)
|
|
}
|
|
|
|
return matchList(name, delimStr, pattern)
|
|
}
|
|
|
|
func matchList(name, delim, pattern string) bool {
|
|
// TODO: optimize
|
|
|
|
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]) == delim {
|
|
break // Stop on delimiter if wildcard is %
|
|
}
|
|
// Try to match the rest from here
|
|
if matchList(name[j:], delim, rest) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return matchList(name[j:], delim, rest)
|
|
}
|