Show
Ignore:
Timestamp:
01/11/10 19:11:54 (2 years ago)
Author:
Menno Smits <menno@…>
Branch:
default
Message:

Parse imaplib's fetch data structures instead of doing it all ourselves

This makes the code messier but means imaplib does a bit more of the
heavy lifting - this is probably safer from a bug standpoint.

From Mark Hammond.

Location:
imapclient
Files:
3 modified

Legend:

Unmodified
Added
Removed
  • imapclient/imapclient.py

    r110 r115  
    1414__all__ = ['IMAPClient', 'DELETED', 'SEEN', 'ANSWERED', 'FLAGGED', 'DRAFT', 
    1515    'RECENT'] 
     16 
     17from response_parser import parse_fetch_response 
    1618 
    1719# System flags 
     
    327329 
    328330        self._checkok('search', typ, data) 
     331        if data == [None]: # no untagged responses... 
     332            return [] 
    329333 
    330334        return [ long(i) for i in data[0].split() ] 
     
    436440        return parser(data) 
    437441 
    438  
    439442    def altfetch(self, messages, parts): 
    440443        if not messages: 
     
    448451        else: 
    449452            tag = self._imap._command('FETCH', msg_list, parts_list) 
    450  
    451         print tag 
    452         lines = [] 
    453         while True: 
    454             line = self._imap._get_line() 
    455             if line.startswith(tag): 
    456                 break 
    457             lines.append(line) 
    458         return lines 
    459      
    460         #self._checkok('fetch', typ, data) 
    461  
    462         #parser = FetchParser() 
    463         #return parser(data) 
    464  
     453        typ, data = self._imap._command_complete('FETCH', tag) 
     454        self._checkok('fetch', typ, data) 
     455        typ, data = self._imap._untagged_response(typ, data, 'FETCH') 
     456        # appears to be a special case - no 'untagged' responses (ie, no 
     457        # folders) results in [None] 
     458        if data == [None]: 
     459          return {} 
     460 
     461        return parse_fetch_response(data) 
    465462 
    466463    def append(self, folder, msg, flags=(), msg_time=None): 
  • imapclient/response_parser.py

    r112 r115  
    2222 
    2323 
    24  
    2524def parse_response(text): 
    2625    #XXX doc 
     
    3837    response = iter(parse_response(text)) 
    3938 
    40     def expect(expected_value): 
    41         next_value = response.next().upper() 
    42         if next_value != expected_value: 
    43             raise ParseError('expected %r, got %r' % (expected_value, next_value)) 
    44  
    4539    parsed_response = {} 
    4640    while True: 
    4741        try: 
    48             expect('*') 
     42            msg_id = _int_or_error(response.next(), 'invalid message ID') 
    4943        except StopIteration: 
    5044            break 
    5145 
    52         msg_id = _int_or_error(response.next(), 'invalid message ID') 
    53         expect('FETCH') 
     46        try: 
     47            msg_response = response.next() 
     48        except StopIteration: 
     49            raise ParseError('unexpected EOF') 
    5450 
    55         msg_response = response.next() 
    5651        if not isinstance(msg_response, tuple): 
    5752            raise ParseError('bad response type: %s' % repr(msg_response)) 
     
    5954            raise ParseError('uneven number of response items: %s' % repr(msg_response)) 
    6055 
    61         #XXX extract this 
    62         msg_data = {} 
     56        # always return the 'sequence' of the message, so it is available 
     57        # even if we return keyed by UID. 
     58        msg_data = {'SEQ': msg_id} 
    6359        for i in xrange(0, len(msg_response), 2): 
    6460            word = msg_response[i].upper() 
     
    8177        raise ParseError('%s: %s' % (error_text, repr(value))) 
    8278 
     79EOF = object() 
    8380 
     81# imaplib has poor handling of 'literals' - it both fails to remove the 
     82# {size} marker, and fails to keep responses grouped into the same logical 
     83# 'line'.  What we end up with is a list of response 'records', where each 
     84# record is either a simple string, or tuple of (str_with_lit, literal) - 
     85# where str_with_lit is a string with the {xxx} marker at its end.  Note 
     86# that each elt of this list does *not* correspond 1:1 with the untagged 
     87# responses. 
     88# (http://bugs.python.org/issue5045 also has comments about this) 
     89# So: we have a special file-like object for each of these records.  When 
     90# a string literal is finally processed, we peek into this file-like object 
     91# to grab the literal. 
     92class LiteralHandlingReader: 
     93    def __init__(self, lexer, resp_record): 
     94        self.pushed = None 
     95        self.lexer = lexer 
     96        if isinstance(resp_record, tuple): 
     97            # A 'record' with a string which includes a literal marker, and 
     98            # the literal itself. 
     99            src_text, self.literal = resp_record 
     100            assert src_text.endswith("}"), src_text 
     101            # add a token-sep after the text. 
     102            self.src = StringIO(src_text + " ") 
     103        else: 
     104            # just a line with no literals. 
     105            self.src = StringIO(resp_record) 
     106            self.literal = None 
    84107 
    85 EOF = object() 
     108    def read(self, n): 
     109        # We also hack into the lexer so we get special treatment for '\\' 
     110        # chars - they are only special inside a quoted string. 
     111        assert n==1 
     112        if self.pushed is not None: 
     113            ret = self.pushed 
     114            self.pushed = None 
     115        else: 
     116            ret = self.src.read(n) 
     117            if ret=="\\" and self.lexer.state not in '"\\': 
     118                self.pushed = "\\" 
     119        return ret 
     120 
     121    def close(self): 
     122        self.src.close() 
     123        self.src = None 
     124        self.literal = None 
     125 
    86126 
    87127class ResponseTokeniser(object): 
    88128 
    89129    CTRL_CHARS = ''.join([chr(ch) for ch in range(32)]) 
    90     ATOM_SPECIALS = r'()%*"]' + CTRL_CHARS 
     130    ATOM_SPECIALS = r'()%*"' + CTRL_CHARS 
    91131    ALL_CHARS = [chr(ch) for ch in range(256)] 
    92132    ATOM_NON_SPECIALS = [ch for ch in ALL_CHARS if ch not in ATOM_SPECIALS] 
    93133 
    94     def __init__(self, text): 
    95         self.lex = shlex.shlex(text) 
     134    def __init__(self, resp_chunks): 
     135        # initialize the lexer with all the chunks we read. 
     136        self.lex = shlex.shlex('', posix=True) 
     137        for chunk in reversed(resp_chunks): 
     138            self.lex.push_source(LiteralHandlingReader(self.lex, chunk)) 
     139 
    96140        self.lex.quotes = '"' 
    97141        self.lex.commenters = '' 
     
    106150        except StopIteration: 
    107151            return EOF 
    108  
    109     def read(self, bytes): 
    110         return self.lex.instream.read(bytes) 
    111152 
    112153 
     
    126167    elif token.startswith('{'): 
    127168        literal_len = int(token[1:-1]) 
    128         if src.read(1) != '\n': 
    129            raise ParseError('No CRLF after %s' % token) 
    130         return src.read(literal_len) 
     169        literal_text = src.lex.instream.literal 
     170        if literal_text is None: 
     171           raise ParseError('No literal corresponds to %r' % token) 
     172        if len(literal_text) != literal_len: 
     173            raise ParseError('Expecting literal of size %d, got %d' % ( 
     174                                literal_len, len(literal_text))) 
     175        return literal_text 
    131176    elif token.startswith('"'): 
    132177        return token[1:-1] 
     
    135180    else: 
    136181        return token 
    137  
    138  
    139  
  • imapclient/test/test_response_parser.py

    r114 r115  
    7474                    '<hi.there>', 'foo', 'BASE64', 4554, 73), 'MIXED')) 
    7575 
     76    def test_envelopey(self): 
     77        self._test('(UID 5 ENVELOPE ("internal_date" "subject" ' 
     78                   '(("name" NIL "address1" "domain1.com")) ' 
     79                   '((NIL NIL "address2" "domain2.com")) ' 
     80                   '(("name" NIL "address3" "domain3.com")) ' 
     81                   '((NIL NIL "address4" "domain4.com")) ' 
     82                   'NIL NIL "<reply-to-id>" "<msg_id>"))', 
     83                   ('UID', 
     84                    5, 
     85                    'ENVELOPE', 
     86                    ('internal_date', 
     87                     'subject', 
     88                     (('name', None, 'address1', 'domain1.com'),), 
     89                     ((None, None, 'address2', 'domain2.com'),), 
     90                     (('name', None, 'address3', 'domain3.com'),), 
     91                     ((None, None, 'address4', 'domain4.com'),), 
     92                     None, 
     93                     None, 
     94                     '<reply-to-id>', 
     95                     '<msg_id>'))) 
     96 
     97    def test_envelopey_quoted(self): 
     98        self._test('(UID 5 ENVELOPE ("internal_date" "subject with \\"quotes\\"" ' 
     99                   '(("name" NIL "address1" "domain1.com")) ' 
     100                   '((NIL NIL "address2" "domain2.com")) ' 
     101                   '(("name" NIL "address3" "domain3.com")) ' 
     102                   '((NIL NIL "address4" "domain4.com")) ' 
     103                   'NIL NIL "<reply-to-id>" "<msg_id>"))', 
     104                   ('UID', 
     105                    5, 
     106                    'ENVELOPE', 
     107                    ('internal_date', 
     108                     'subject with "quotes"', 
     109                     (('name', None, 'address1', 'domain1.com'),), 
     110                     ((None, None, 'address2', 'domain2.com'),), 
     111                     (('name', None, 'address3', 'domain3.com'),), 
     112                     ((None, None, 'address4', 'domain4.com'),), 
     113                     None, 
     114                     None, 
     115                     '<reply-to-id>', 
     116                     '<msg_id>'))) 
    76117 
    77118    def test_literal(self): 
     
    80121            abc def XYZ 
    81122            """)) 
    82         self._test('{18}' + CRLF + literal_text, literal_text) 
     123        self._test([('{18}', literal_text)], literal_text) 
    83124 
    84125 
     
    88129            abc def XYZ 
    89130            """)) 
    90         response = add_crlf(dedent("""\ 
    91             (12 "foo" {18} 
    92             %s) 
    93             """) % literal_text) 
     131        response = [('(12 "foo" {18}', literal_text), ")"] 
    94132        self._test(response, (12, 'foo', literal_text)) 
    95133 
    96134 
    97135    def test_quoted_specials(self): 
    98         self._test(r'"foo \"bar\""', ('foo "bar"',)) 
    99         self._test(r'"foo\\bar"', (r'foo\bar',)) 
     136        self._test(r'"foo \"bar\""', 'foo "bar"') 
     137        self._test(r'"foo\\bar"', r'foo\bar') 
    100138 
    101139 
     
    105143 
    106144    def test_bad_literal(self): 
    107         self._test_parse_error('{99} abc', 'No CRLF after {99}') 
     145        self._test_parse_error([('{99}', 'abc')], 
     146                               'Expecting literal of size 99, got 3') 
    108147 
    109148 
    110149    def test_bad_quoting(self): 
    111         self._test_parse_error('"abc next', 'No closing quotation: "abc next') 
    112  
    113  
     150        self._test_parse_error('"abc next', 'No closing quotation:') 
    114151 
    115152 
     
    118155            # convenience - expected value should be wrapped in another tuple 
    119156            expected = (expected,) 
     157        if not isinstance(to_parse, list): 
     158            to_parse = [to_parse] 
    120159        output = parse_response(to_parse) 
    121160        self.assert_( 
     
    129168            self.fail("didn't raise an exception") 
    130169        except ParseError, err: 
    131             self.assert_(expected_msg == str(err), 
     170            self.assert_(expected_msg == str(err)[:len(expected_msg)], 
    132171                         'got ParseError with wrong msg: %r' % str(err)) 
    133172 
     
    141180 
    142181    def test_basic(self): 
    143         self.assertEquals(parse_fetch_response('* 4 FETCH ()'), {4: {}}) 
    144         self.assertEquals(parse_fetch_response('* 4 fEtCh ()'), {4: {}}) 
    145  
    146  
    147     def test_non_fetch(self): 
    148         self.assertRaises(ParseError, parse_fetch_response, '* 4 OTHER ()') 
     182        self.assertEquals(parse_fetch_response('4 ()'), {4: {'SEQ': 4}}) 
     183 
     184 
     185#    def test_non_fetch(self): 
     186#        self.assertRaises(ParseError, parse_fetch_response, ['4 ()']) 
    149187 
    150188 
    151189    def test_bad_msgid(self): 
    152         self.assertRaises(ParseError, parse_fetch_response, '* abc FETCH ()') 
     190        self.assertRaises(ParseError, parse_fetch_response, ['abc ()']) 
    153191 
    154192 
    155193    def test_bad_data(self): 
    156         self.assertRaises(ParseError, parse_fetch_response, '* 2 FETCH WHAT') 
     194        self.assertRaises(ParseError, parse_fetch_response, ['2 WHAT']) 
    157195 
    158196 
    159197    def test_missing_data(self): 
    160         self.assertRaises(ParseError, parse_fetch_response, '* 2 FETCH') 
     198        self.assertRaises(ParseError, parse_fetch_response, ['2']) 
    161199 
    162200 
    163201    def test_simple_pairs(self): 
    164         self.assertEquals(parse_fetch_response('* 23 FETCH (ABC 123 StUfF "hello")'), 
     202        self.assertEquals(parse_fetch_response(['23 (ABC 123 StUfF "hello")']), 
    165203                          {23: {'ABC': 123, 
    166                                 'STUFF': 'hello'}}) 
     204                                'STUFF': 'hello', 
     205                                'SEQ': 23}}) 
    167206 
    168207 
    169208    def test_odd_pairs(self): 
    170         self.assertRaises(ParseError, parse_fetch_response, '* 2 FETCH (ONE)') 
    171         self.assertRaises(ParseError, parse_fetch_response, '* 2 FETCH (ONE TWO THREE)') 
     209        self.assertRaises(ParseError, parse_fetch_response, ['* 2 FETCH (ONE)']) 
     210        self.assertRaises(ParseError, parse_fetch_response, ['* 2 FETCH (ONE TWO THREE)']) 
    172211 
    173212 
    174213    def test_UID(self): 
    175         self.assertEquals(parse_fetch_response('* 23 FETCH (UID 76)'), 
    176                           {76: {}}) 
    177         self.assertEquals(parse_fetch_response('* 23 FETCH (uiD 76)'), 
    178                           {76: {}}) 
     214        self.assertEquals(parse_fetch_response(['23 (UID 76)']), 
     215                          {76: {'SEQ': 23}}) 
     216        self.assertEquals(parse_fetch_response(['23 (uiD 76)']), 
     217                          {76: {'SEQ': 23}}) 
    179218 
    180219 
     
    184223 
    185224    def test_FLAGS(self): 
    186         self.assertEquals(parse_fetch_response('* 23 FETCH (FLAGS (\Seen Stuff))'), 
    187                           {23: {'FLAGS': (r'\Seen', 'Stuff')}}) 
     225        self.assertEquals(parse_fetch_response(['23 (FLAGS (\Seen Stuff))']), 
     226                          {23: {'SEQ': 23, 'FLAGS': (r'\Seen', 'Stuff')}}) 
    188227 
    189228 
    190229    def test_multiple_messages(self): 
    191         self.fail() 
     230        self.assertEquals(parse_fetch_response( 
     231                                    ["2 (FLAGS (Foo Bar)) ", 
     232                                     "7 (FLAGS (Baz Sneeve))"]), 
     233                         { 
     234                            2: {'FLAGS': ('Foo', 'Bar'), 'SEQ': 2}, 
     235                            7: {'FLAGS': ('Baz', 'Sneeve'), 'SEQ': 7}, 
     236                         }) 
    192237 
    193238 
     
    217262#                    datetime.datetime(2007, 12, 9, 17, 8, 8, 0, FixedOffset(0))) 
    218263 
    219  
    220 #     def testMultipleMessages(self): 
    221 #         '''Test with multple messages in the response 
    222 #         ''' 
    223 #         self._parse_test( 
    224 #             [ 
    225 #                 r'2 (FLAGS (Foo Bar))', 
    226 #                 r'7 (FLAGS (Baz Sneeve))', 
    227 #                 ], 
    228 #             { 
    229 #                 2: {'FLAGS': ['Foo', 'Bar']}, 
    230 #                 7: {'FLAGS': ['Baz', 'Sneeve']}, 
    231 #                 } 
    232 #             ) 
    233264 
    234265#     def testLiteral(self):