Forked the emersion/go-imap v1 project.
This commit is contained in:
325
imapserver/list.go
Normal file
325
imapserver/list.go
Normal file
@@ -0,0 +1,325 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user