Changeset 115:7aa870b75d11 for imapclient
- Timestamp:
- 01/11/10 19:11:54 (2 years ago)
- Branch:
- default
- Location:
- imapclient
- Files:
-
- 3 modified
-
imapclient.py (modified) (4 diffs)
-
response_parser.py (modified) (7 diffs)
-
test/test_response_parser.py (modified) (9 diffs)
Legend:
- Unmodified
- Added
- Removed
-
imapclient/imapclient.py
r110 r115 14 14 __all__ = ['IMAPClient', 'DELETED', 'SEEN', 'ANSWERED', 'FLAGGED', 'DRAFT', 15 15 'RECENT'] 16 17 from response_parser import parse_fetch_response 16 18 17 19 # System flags … … 327 329 328 330 self._checkok('search', typ, data) 331 if data == [None]: # no untagged responses... 332 return [] 329 333 330 334 return [ long(i) for i in data[0].split() ] … … 436 440 return parser(data) 437 441 438 439 442 def altfetch(self, messages, parts): 440 443 if not messages: … … 448 451 else: 449 452 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) 465 462 466 463 def append(self, folder, msg, flags=(), msg_time=None): -
imapclient/response_parser.py
r112 r115 22 22 23 23 24 25 24 def parse_response(text): 26 25 #XXX doc … … 38 37 response = iter(parse_response(text)) 39 38 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 45 39 parsed_response = {} 46 40 while True: 47 41 try: 48 expect('*')42 msg_id = _int_or_error(response.next(), 'invalid message ID') 49 43 except StopIteration: 50 44 break 51 45 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') 54 50 55 msg_response = response.next()56 51 if not isinstance(msg_response, tuple): 57 52 raise ParseError('bad response type: %s' % repr(msg_response)) … … 59 54 raise ParseError('uneven number of response items: %s' % repr(msg_response)) 60 55 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} 63 59 for i in xrange(0, len(msg_response), 2): 64 60 word = msg_response[i].upper() … … 81 77 raise ParseError('%s: %s' % (error_text, repr(value))) 82 78 79 EOF = object() 83 80 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. 92 class 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 84 107 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 86 126 87 127 class ResponseTokeniser(object): 88 128 89 129 CTRL_CHARS = ''.join([chr(ch) for ch in range(32)]) 90 ATOM_SPECIALS = r'()%*" ]' + CTRL_CHARS130 ATOM_SPECIALS = r'()%*"' + CTRL_CHARS 91 131 ALL_CHARS = [chr(ch) for ch in range(256)] 92 132 ATOM_NON_SPECIALS = [ch for ch in ALL_CHARS if ch not in ATOM_SPECIALS] 93 133 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 96 140 self.lex.quotes = '"' 97 141 self.lex.commenters = '' … … 106 150 except StopIteration: 107 151 return EOF 108 109 def read(self, bytes):110 return self.lex.instream.read(bytes)111 152 112 153 … … 126 167 elif token.startswith('{'): 127 168 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 131 176 elif token.startswith('"'): 132 177 return token[1:-1] … … 135 180 else: 136 181 return token 137 138 139 -
imapclient/test/test_response_parser.py
r114 r115 74 74 '<hi.there>', 'foo', 'BASE64', 4554, 73), 'MIXED')) 75 75 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>'))) 76 117 77 118 def test_literal(self): … … 80 121 abc def XYZ 81 122 """)) 82 self._test( '{18}' + CRLF + literal_text, literal_text)123 self._test([('{18}', literal_text)], literal_text) 83 124 84 125 … … 88 129 abc def XYZ 89 130 """)) 90 response = add_crlf(dedent("""\ 91 (12 "foo" {18} 92 %s) 93 """) % literal_text) 131 response = [('(12 "foo" {18}', literal_text), ")"] 94 132 self._test(response, (12, 'foo', literal_text)) 95 133 96 134 97 135 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') 100 138 101 139 … … 105 143 106 144 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') 108 147 109 148 110 149 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:') 114 151 115 152 … … 118 155 # convenience - expected value should be wrapped in another tuple 119 156 expected = (expected,) 157 if not isinstance(to_parse, list): 158 to_parse = [to_parse] 120 159 output = parse_response(to_parse) 121 160 self.assert_( … … 129 168 self.fail("didn't raise an exception") 130 169 except ParseError, err: 131 self.assert_(expected_msg == str(err) ,170 self.assert_(expected_msg == str(err)[:len(expected_msg)], 132 171 'got ParseError with wrong msg: %r' % str(err)) 133 172 … … 141 180 142 181 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 ()']) 149 187 150 188 151 189 def test_bad_msgid(self): 152 self.assertRaises(ParseError, parse_fetch_response, '* abc FETCH ()')190 self.assertRaises(ParseError, parse_fetch_response, ['abc ()']) 153 191 154 192 155 193 def test_bad_data(self): 156 self.assertRaises(ParseError, parse_fetch_response, '* 2 FETCH WHAT')194 self.assertRaises(ParseError, parse_fetch_response, ['2 WHAT']) 157 195 158 196 159 197 def test_missing_data(self): 160 self.assertRaises(ParseError, parse_fetch_response, '* 2 FETCH')198 self.assertRaises(ParseError, parse_fetch_response, ['2']) 161 199 162 200 163 201 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")']), 165 203 {23: {'ABC': 123, 166 'STUFF': 'hello'}}) 204 'STUFF': 'hello', 205 'SEQ': 23}}) 167 206 168 207 169 208 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)']) 172 211 173 212 174 213 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}}) 179 218 180 219 … … 184 223 185 224 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')}}) 188 227 189 228 190 229 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 }) 192 237 193 238 … … 217 262 # datetime.datetime(2007, 12, 9, 17, 8, 8, 0, FixedOffset(0))) 218 263 219 220 # def testMultipleMessages(self):221 # '''Test with multple messages in the response222 # '''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 # )233 264 234 265 # def testLiteral(self):
