diff options
author | Jordan <me@jordan.im> | 2023-02-04 23:54:03 -0700 |
---|---|---|
committer | Jordan <me@jordan.im> | 2023-02-04 23:54:03 -0700 |
commit | c4159d895ac399ca55326f7b4ff8bfbf8402e654 (patch) | |
tree | 45340ca429c16f683b375695d01e03d65ebf22b0 /vendor/github.com/emersion/go-imap/read.go | |
download | pigeon-c4159d895ac399ca55326f7b4ff8bfbf8402e654.tar.gz pigeon-c4159d895ac399ca55326f7b4ff8bfbf8402e654.zip |
initial commit
Diffstat (limited to 'vendor/github.com/emersion/go-imap/read.go')
-rw-r--r-- | vendor/github.com/emersion/go-imap/read.go | 467 |
1 files changed, 467 insertions, 0 deletions
diff --git a/vendor/github.com/emersion/go-imap/read.go b/vendor/github.com/emersion/go-imap/read.go new file mode 100644 index 0000000..112ee28 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/read.go @@ -0,0 +1,467 @@ +package imap + +import ( + "bytes" + "errors" + "io" + "strconv" + "strings" +) + +const ( + sp = ' ' + cr = '\r' + lf = '\n' + dquote = '"' + literalStart = '{' + literalEnd = '}' + listStart = '(' + listEnd = ')' + respCodeStart = '[' + respCodeEnd = ']' +) + +const ( + crlf = "\r\n" + nilAtom = "NIL" +) + +// TODO: add CTL to atomSpecials +var ( + quotedSpecials = string([]rune{dquote, '\\'}) + respSpecials = string([]rune{respCodeEnd}) + atomSpecials = string([]rune{listStart, listEnd, literalStart, sp, '%', '*'}) + quotedSpecials + respSpecials +) + +type parseError struct { + error +} + +func newParseError(text string) error { + return &parseError{errors.New(text)} +} + +// IsParseError returns true if the provided error is a parse error produced by +// Reader. +func IsParseError(err error) bool { + _, ok := err.(*parseError) + return ok +} + +// A string reader. +type StringReader interface { + // ReadString reads until the first occurrence of delim in the input, + // returning a string containing the data up to and including the delimiter. + // See https://golang.org/pkg/bufio/#Reader.ReadString + ReadString(delim byte) (line string, err error) +} + +type reader interface { + io.Reader + io.RuneScanner + StringReader +} + +// ParseNumber parses a number. +func ParseNumber(f interface{}) (uint32, error) { + // Useful for tests + if n, ok := f.(uint32); ok { + return n, nil + } + + var s string + switch f := f.(type) { + case RawString: + s = string(f) + case string: + s = f + default: + return 0, newParseError("expected a number, got a non-atom") + } + + nbr, err := strconv.ParseUint(string(s), 10, 32) + if err != nil { + return 0, &parseError{err} + } + + return uint32(nbr), nil +} + +// ParseString parses a string, which is either a literal, a quoted string or an +// atom. +func ParseString(f interface{}) (string, error) { + if s, ok := f.(string); ok { + return s, nil + } + + // Useful for tests + if a, ok := f.(RawString); ok { + return string(a), nil + } + + if l, ok := f.(Literal); ok { + b := make([]byte, l.Len()) + if _, err := io.ReadFull(l, b); err != nil { + return "", err + } + return string(b), nil + } + + return "", newParseError("expected a string") +} + +// Convert a field list to a string list. +func ParseStringList(f interface{}) ([]string, error) { + fields, ok := f.([]interface{}) + if !ok { + return nil, newParseError("expected a string list, got a non-list") + } + + list := make([]string, len(fields)) + for i, f := range fields { + var err error + if list[i], err = ParseString(f); err != nil { + return nil, newParseError("cannot parse string in string list: " + err.Error()) + } + } + return list, nil +} + +func trimSuffix(str string, suffix rune) string { + return str[:len(str)-1] +} + +// An IMAP reader. +type Reader struct { + MaxLiteralSize uint32 // The maximum literal size. + + reader + + continues chan<- bool + + brackets int + inRespCode bool +} + +func (r *Reader) ReadSp() error { + char, _, err := r.ReadRune() + if err != nil { + return err + } + if char != sp { + return newParseError("expected a space") + } + return nil +} + +func (r *Reader) ReadCrlf() (err error) { + var char rune + + if char, _, err = r.ReadRune(); err != nil { + return + } + if char == lf { + return + } + if char != cr { + err = newParseError("line doesn't end with a CR") + return + } + + if char, _, err = r.ReadRune(); err != nil { + return + } + if char != lf { + err = newParseError("line doesn't end with a LF") + } + + return +} + +func (r *Reader) ReadAtom() (interface{}, error) { + r.brackets = 0 + + var atom string + for { + char, _, err := r.ReadRune() + if err != nil { + return nil, err + } + + // TODO: list-wildcards and \ + if r.brackets == 0 && (char == listStart || char == literalStart || char == dquote) { + return nil, newParseError("atom contains forbidden char: " + string(char)) + } + if char == cr || char == lf { + break + } + if r.brackets == 0 && (char == sp || char == listEnd) { + break + } + if char == respCodeEnd { + if r.brackets == 0 { + if r.inRespCode { + break + } else { + return nil, newParseError("atom contains bad brackets nesting") + } + } + r.brackets-- + } + if char == respCodeStart { + r.brackets++ + } + + atom += string(char) + } + + r.UnreadRune() + + if atom == nilAtom { + return nil, nil + } + + return atom, nil +} + +func (r *Reader) ReadLiteral() (Literal, error) { + char, _, err := r.ReadRune() + if err != nil { + return nil, err + } else if char != literalStart { + return nil, newParseError("literal string doesn't start with an open brace") + } + + lstr, err := r.ReadString(byte(literalEnd)) + if err != nil { + return nil, err + } + lstr = trimSuffix(lstr, literalEnd) + + nonSync := strings.HasSuffix(lstr, "+") + if nonSync { + lstr = trimSuffix(lstr, '+') + } + + n, err := strconv.ParseUint(lstr, 10, 32) + if err != nil { + return nil, newParseError("cannot parse literal length: " + err.Error()) + } + if r.MaxLiteralSize > 0 && uint32(n) > r.MaxLiteralSize { + return nil, newParseError("literal exceeding maximum size") + } + + if err := r.ReadCrlf(); err != nil { + return nil, err + } + + // Send continuation request if necessary + if r.continues != nil && !nonSync { + r.continues <- true + } + + // Read literal + b := make([]byte, n) + if _, err := io.ReadFull(r, b); err != nil { + return nil, err + } + return bytes.NewBuffer(b), nil +} + +func (r *Reader) ReadQuotedString() (string, error) { + if char, _, err := r.ReadRune(); err != nil { + return "", err + } else if char != dquote { + return "", newParseError("quoted string doesn't start with a double quote") + } + + var buf bytes.Buffer + var escaped bool + for { + char, _, err := r.ReadRune() + if err != nil { + return "", err + } + + if char == '\\' && !escaped { + escaped = true + } else { + if char == cr || char == lf { + r.UnreadRune() + return "", newParseError("CR or LF not allowed in quoted string") + } + if char == dquote && !escaped { + break + } + + if !strings.ContainsRune(quotedSpecials, char) && escaped { + return "", newParseError("quoted string cannot contain backslash followed by a non-quoted-specials char") + } + + buf.WriteRune(char) + escaped = false + } + } + + return buf.String(), nil +} + +func (r *Reader) ReadFields() (fields []interface{}, err error) { + var char rune + for { + if char, _, err = r.ReadRune(); err != nil { + return + } + if err = r.UnreadRune(); err != nil { + return + } + + var field interface{} + ok := true + switch char { + case literalStart: + field, err = r.ReadLiteral() + case dquote: + field, err = r.ReadQuotedString() + case listStart: + field, err = r.ReadList() + case listEnd: + ok = false + case cr: + return + default: + field, err = r.ReadAtom() + } + + if err != nil { + return + } + if ok { + fields = append(fields, field) + } + + if char, _, err = r.ReadRune(); err != nil { + return + } + if char == cr || char == lf || char == listEnd || char == respCodeEnd { + if char == cr || char == lf { + r.UnreadRune() + } + return + } + if char == listStart { + r.UnreadRune() + continue + } + if char != sp { + err = newParseError("fields are not separated by a space") + return + } + } +} + +func (r *Reader) ReadList() (fields []interface{}, err error) { + char, _, err := r.ReadRune() + if err != nil { + return + } + if char != listStart { + err = newParseError("list doesn't start with an open parenthesis") + return + } + + fields, err = r.ReadFields() + if err != nil { + return + } + + r.UnreadRune() + if char, _, err = r.ReadRune(); err != nil { + return + } + if char != listEnd { + err = newParseError("list doesn't end with a close parenthesis") + } + return +} + +func (r *Reader) ReadLine() (fields []interface{}, err error) { + fields, err = r.ReadFields() + if err != nil { + return + } + + r.UnreadRune() + err = r.ReadCrlf() + return +} + +func (r *Reader) ReadRespCode() (code StatusRespCode, fields []interface{}, err error) { + char, _, err := r.ReadRune() + if err != nil { + return + } + if char != respCodeStart { + err = newParseError("response code doesn't start with an open bracket") + return + } + + r.inRespCode = true + fields, err = r.ReadFields() + r.inRespCode = false + if err != nil { + return + } + + if len(fields) == 0 { + err = newParseError("response code doesn't contain any field") + return + } + + codeStr, ok := fields[0].(string) + if !ok { + err = newParseError("response code doesn't start with a string atom") + return + } + if codeStr == "" { + err = newParseError("response code is empty") + return + } + code = StatusRespCode(strings.ToUpper(codeStr)) + + fields = fields[1:] + + r.UnreadRune() + char, _, err = r.ReadRune() + if err != nil { + return + } + if char != respCodeEnd { + err = newParseError("response code doesn't end with a close bracket") + } + return +} + +func (r *Reader) ReadInfo() (info string, err error) { + info, err = r.ReadString(byte(lf)) + if err != nil { + return + } + info = strings.TrimSuffix(info, string(lf)) + info = strings.TrimSuffix(info, string(cr)) + info = strings.TrimLeft(info, " ") + + return +} + +func NewReader(r reader) *Reader { + return &Reader{reader: r} +} + +func NewServerReader(r reader, continues chan<- bool) *Reader { + return &Reader{reader: r, continues: continues} +} + +type Parser interface { + Parse(fields []interface{}) error +} |