From 2ef831feb8cd1b2393196877b7b7c93ac49093d7 Mon Sep 17 00:00:00 2001 From: Melody Horn Date: Fri, 30 Apr 2021 21:53:31 -0600 Subject: overhaul parsing to match ABNF from spec --- ctec/imap_response.py | 92 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 69 insertions(+), 23 deletions(-) (limited to 'ctec/imap_response.py') diff --git a/ctec/imap_response.py b/ctec/imap_response.py index 23ca943..cad76c9 100644 --- a/ctec/imap_response.py +++ b/ctec/imap_response.py @@ -1,46 +1,92 @@ # this should really be in the stdlib imo from dataclasses import dataclass -from typing import List as ListT, Union, ClassVar +from typing import List as ListT, Tuple, Optional -from .parse_utils import ParseResult, Parser, take_while1, tag, delimited, take_n, alt, map_parser, separated_many0, separated_triple, all_consuming +from .parse_utils import ( + Parser, take_while1, tag, delimited, take_n, alt, separated_many0, separated_triple, all_consuming, verify, + preceded, and_then, itag, take_while0, as_predicate +) __all__ = [ 'List', ] -atom: Parser[str] = take_while1(lambda c: c.isalnum() or c in r'\/') +# level 0 +ctl: Parser[str] = verify(take_n(1), lambda c: not c.isprintable()) -number: Parser[int] = map_parser(take_while1(str.isnumeric), int) +digit: Parser[str] = verify(take_n(1), lambda c: c in '0123456789') -def literal_string(text: str) -> ParseResult[str]: - delimited_result = delimited(tag('{'), number, tag('}\r\n'))(text) - if delimited_result is None: - return None - count, text = delimited_result - return take_n(count)(text) +dquote: Parser[str] = tag('"') -quoted_string: Parser[str] = delimited(tag('"'), take_while1(lambda c: c not in '\r\n"'), tag('"')) +list_wildcards: Parser[str] = alt(tag('%'), tag('*')) -string: Parser[str] = alt(literal_string, quoted_string) +nil: Parser[str] = itag('NIL') -astring: Parser[str] = alt(atom, string) +resp_specials: Parser[str] = tag(']') -data_item: Parser[Union[int, str]] = alt(number, atom, string) +text_char: Parser[str] = verify(take_n(1), lambda c: c not in '\r\n') -ParensList = ListT[Union[int, str, 'ParensList']] -def parens_list(text: str) -> ParseResult[ParensList]: - return delimited(tag('('), separated_many0(alt(data_item, parens_list), tag(' ')), tag(')'))(text) +# level 1 +number: Parser[str] = take_while1(as_predicate(digit)) + +quoted_specials: Parser[str] = alt(dquote, tag('\\')) + +# level 2 +atom_specials: Parser[str] = alt(verify(take_n(1), lambda c: c in '(){ '), ctl, list_wildcards, quoted_specials, resp_specials) + +literal: Parser[str] = and_then( + delimited(tag('{'), number, tag('}\r\n')), + lambda n: preceded(delimited(tag('{'), number, tag('}\r\n')), take_n(int(n))) +) + +quoted_char: Parser[str] = alt(verify(text_char, lambda c: quoted_specials(c) is None), preceded(tag('\\'), quoted_specials)) + +# level 3 +atom_char: Parser[str] = verify(take_n(1), lambda c: atom_specials(c) is None) + +quoted: Parser[str] = delimited(dquote, take_while0(as_predicate(quoted_char)), dquote) + +# level 4 +astring_char: Parser[str] = alt(atom_char, resp_specials) + +atom: Parser[str] = take_while1(as_predicate(atom_char)) + +string: Parser[str] = alt(quoted, literal) + +# level 5 +astring: Parser[str] = alt(take_while1(as_predicate(astring_char)), string) + +flag_extension: Parser[str] = preceded(tag('\\'), atom) + +# level 6 +mailbox: Parser[str] = alt(itag('INBOX'), astring) + +mbx_list_flag: Parser[str] = alt(itag(r'\Noselect'), itag(r'\Marked'), itag(r'\Unmarked'), itag(r'\Noinferiors'), flag_extension) + +# level 7 +mbx_list_flags: Parser[ListT[str]] = separated_many0(mbx_list_flag, tag(' ')) + +# level 8 +mailbox_list: Parser[Tuple[ListT[str], Optional[str], str]] = separated_triple( + delimited(tag('('), mbx_list_flags, tag(')')), + tag(' '), + alt(delimited(dquote, quoted_char, dquote), nil), + tag(' '), + mailbox, +) @dataclass class List: attributes: ListT[str] - delimiter: str + delimiter: Optional[str] name: str @staticmethod - def parse(response: bytes) -> 'List': - response = response.decode('ASCII') - print(response) - parser = all_consuming(separated_triple(parens_list, tag(' '), string, tag(' '), astring), debug=True) - (attributes, delimiter, name), _ = parser(response) + def parse(response_bytes: bytes) -> 'List': + response = response_bytes.decode('ASCII') + parser = all_consuming(mailbox_list, debug=True) + parse_result = parser(response) + if parse_result is None: + raise ValueError('invalid List.parse argument {}', repr(response)) + (attributes, delimiter, name), _ = parse_result return List(attributes, delimiter, name) -- cgit v1.2.3