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 }