Forked the emersion/go-imap v1 project.

This commit is contained in:
2025-05-01 11:58:18 +03:00
commit bcc3f95e8e
107 changed files with 16268 additions and 0 deletions

325
imapserver/list.go Normal file
View 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)
}