Changeset 130:c866b6ef9447
- Timestamp:
- 27/01/10 08:18:56 (2 years ago)
- Branch:
- default
- Parents:
- 126:4319998fc9b5 (diff), 129:b627e75c7088 (diff)
Note: this is a merge changeset, the changes displayed below correspond to the merge itself.
Use the (diff) links above to see all the changes relative to each parent. - Files:
-
- 1 removed
- 4 modified
-
imapclient/imapclient.py (modified) (1 diff)
-
imapclient/imapclient.py (modified) (8 diffs)
-
imapclient/test/test_FetchParser.py (deleted)
-
livetest.py (modified) (6 diffs)
-
livetest.py (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
imapclient/imapclient.py
r126 r130 500 500 501 501 502 def copy(self, messages, folder): 503 """Copy one or more messages from the current folder to another folder 504 505 @param messages: Message IDs to fetch. 506 @param folder: Folder name to append to. 507 @return: The COPY command response message returned by the 508 server. 509 """ 510 msg_list = messages_to_str(messages) 511 folder = self._encode_folder_name(folder) 512 513 if self.use_uid: 514 typ, data = self._imap.uid('COPY', msg_list, folder) 515 else: 516 typ, data = self._imap.copy(msg_list, folder) 517 self._checkok('copy', typ, data) 518 return data[0] 519 520 502 521 def expunge(self): 503 522 typ, data = self._imap.expunge() -
imapclient/imapclient.py
r129 r130 6 6 import imaplib 7 7 import shlex 8 import datetime9 8 #imaplib.Debug = 5 10 9 … … 15 14 __all__ = ['IMAPClient', 'DELETED', 'SEEN', 'ANSWERED', 'FLAGGED', 'DRAFT', 16 15 'RECENT'] 16 17 from response_parser import parse_response, parse_fetch_response 18 17 19 18 20 # System flags … … 204 206 205 207 @param folder: The folder name. 206 @return: Number of messages in the folder. 207 @rtype: long int 208 @return: A dictionary containing the SELECT response 209 values. At least the EXISTS, FLAGS and RECENT keys are 210 guaranteed to exist. Example: 211 {'EXISTS': 3, 212 'FLAGS': ('\\Answered', '\\Flagged', '\\Deleted', ... ), 213 'RECENT': 0, 214 'PERMANENTFLAGS': ('\\Answered', '\\Flagged', '\\Deleted', ... ), 215 'READ-WRITE': True, 216 'UIDNEXT': 11, 217 'UIDVALIDITY': 1239278212} 208 218 """ 209 219 typ, data = self._imap.select(self._encode_folder_name(folder)) 210 220 self._checkok('select', typ, data) 211 return long(data[0]) 221 return self._process_select_response(self._imap.untagged_responses) 222 223 224 def _process_select_response(self, resp): 225 out = {} 226 for key, value in resp.iteritems(): 227 key = key.upper() 228 if key == 'OK': 229 continue 230 elif key in ('EXISTS', 'RECENT', 'UIDNEXT', 'UIDVALIDITY'): 231 value = int(value[0]) 232 elif key in ('FLAGS', 'PERMANENTFLAGS'): 233 value = parse_response(value)[0] 234 elif key == 'READ-WRITE': 235 value = True 236 237 out[key] = value 238 return out 212 239 213 240 … … 327 354 328 355 self._checkok('search', typ, data) 356 if data == [None]: # no untagged responses... 357 return [] 329 358 330 359 return [ long(i) for i in data[0].split() ] … … 428 457 429 458 if self.use_uid: 430 typ, data = self._imap.uid('FETCH', msg_list, parts_list) 431 else: 432 typ, data = self._imap.fetch(msg_list, parts_list) 459 tag = self._imap._command('UID', 'FETCH', msg_list, parts_list) 460 else: 461 tag = self._imap._command('FETCH', msg_list, parts_list) 462 typ, data = self._imap._command_complete('FETCH', tag) 433 463 self._checkok('fetch', typ, data) 434 435 parser = FetchParser() 436 return parser(data) 464 typ, data = self._imap._untagged_response(typ, data, 'FETCH') 465 # appears to be a special case - no 'untagged' responses (ie, no 466 # folders) results in [None] 467 if data == [None]: 468 return {} 469 470 return parse_fetch_response(data) 437 471 438 472 … … 560 594 self._checkok('store', typ, data) 561 595 562 return self._flatten_dict( FetchParser()(data))596 return self._flatten_dict(parse_fetch_response((data))) 563 597 564 598 … … 579 613 return imap_utf7.encode(name) 580 614 return name 581 582 583 class FetchParser(object):584 """585 Parse an IMAP FETCH response and convert the return values to useful Python586 values.587 """588 589 def parse(self, response):590 out = {}591 for response_item in response:592 msgid, data = self.parse_data(response_item)593 594 if msgid != None:595 # Response for a new message596 current_msg_data = {}597 out[msgid] = current_msg_data598 599 current_msg_data.update(data)600 601 return out602 603 __call__ = parse604 605 def parse_data(self, data):606 out = {}607 608 if isinstance(data, str):609 if data == ')':610 # End of response for current message611 return None, {}612 613 elif isinstance(data, tuple):614 data, literal_data = data615 616 else:617 raise ValueError("don't know how to handle %r" % data)618 619 data = data.lstrip()620 msgid = None621 if data[0].isdigit():622 # Get message ID623 msgid, data = data.split(None, 1)624 msgid = long(msgid)625 626 assert data.startswith('('), data627 data = data[1:]628 629 if data.endswith(')'):630 data = data[:-1]631 632 for name, item in FetchTokeniser().process_pairs(data):633 name = name.upper()634 635 if name == 'UID':636 # Using UID's, override the message ID637 msgid = long(item)638 639 else:640 if isinstance(item, Literal):641 #assert len(data) == item.length642 arg = literal_data643 else:644 arg = item645 646 # Call handler function based on the response type647 methname = 'do_'+name.upper().replace('.', '_')648 meth = getattr(self, methname, self.do_default)649 out[name] = meth(arg)650 651 return msgid, out652 653 def do_INTERNALDATE(self, arg):654 """Process an INTERNALDATE response655 656 @param arg: A quoted IMAP INTERNALDATE string657 (eg. " 9-Feb-2007 17:08:08 +0000")658 @return: datetime.datetime instance for the given time (in UTC)659 """660 arg = 'INTERNALDATE "%s"' % arg661 mo = imaplib.InternalDate.match(arg)662 if not mo:663 raise ValueError("couldn't parse date %r" % arg)664 665 zoneh = int(mo.group('zoneh'))666 zonem = (zoneh * 60) + int(mo.group('zonem'))667 if mo.group('zonen') == '-':668 zonem = -zonem669 tz = FixedOffset(zonem)670 671 year = int(mo.group('year'))672 mon = imaplib.Mon2num[mo.group('mon')]673 day = int(mo.group('day'))674 hour = int(mo.group('hour'))675 min = int(mo.group('min'))676 sec = int(mo.group('sec'))677 678 dt = datetime.datetime(year, mon, day, hour, min, sec, 0, tz)679 680 # Normalise to host system's timezone681 return dt.astimezone(FixedOffset.for_system()).replace(tzinfo=None)682 683 684 def do_default(self, arg):685 return arg686 687 688 class FetchTokeniser(object):689 """690 General response tokenizer and converter691 """692 693 QUOTED_STRING = '(?:".*?")'694 PAREN_LIST = '(?:\(.*?\))'695 696 PAIR_RE = re.compile((697 '([\w\.]+(?:\[[^\]]+\]+)?)\s+' + # name (matches "FOO", "FOO.BAR" & "BODY[SECTION STUFF]")698 '((?:\d+)' + # bare integer699 '|(?:{\d+?})' + # IMAP literal700 '|' + QUOTED_STRING +701 '|' + PAREN_LIST +702 ')\s*'))703 704 DATA_RE = re.compile((705 '(' + QUOTED_STRING +706 '|' + PAREN_LIST +707 '|(?:\S+)' + # word708 ')\s*'))709 710 def process_pairs(self, s):711 """Break up and convert a string of FETCH response pairs712 713 @param s: FETCH response string eg. "FOO 12 BAH (1 abc def "foo bar")"714 @return: Tokenised and converted input return as (name, data) pairs.715 """716 out = []717 for m in strict_finditer(self.PAIR_RE, s):718 name, data = m.groups()719 out.append((name, self.nativefy(data)))720 return out721 722 def process_list(self, s):723 """Break up and convert a string of data items724 725 @param s: FETCH response string eg. "(1 abc def "foo bar")"726 @return: A list of converted items.727 """728 if s == '':729 return []730 out = []731 for m in strict_finditer(self.DATA_RE, s):732 out.append(self.nativefy(m.group(1)))733 return out734 735 def nativefy(self, s):736 if s.startswith('"'):737 return s[1:-1] # Debracket738 elif s.startswith('{'):739 return Literal(long(s[1:-1]))740 elif s.startswith('('):741 return self.process_list(s[1:-1])742 elif s.isdigit():743 return long(s)744 elif s.upper() == 'NIL':745 return None746 else:747 return s748 749 750 class Literal(object):751 """752 Simple class to represent a literal token in the fetch response753 (eg. "{21}")754 """755 756 def __init__(self, length):757 self.length = length758 759 def __eq__(self, other):760 return self.length == other.length761 762 def __str__(self):763 return '{%d}' % self.length764 765 766 def strict_finditer(regex, s):767 """Like re.finditer except the regex must match from exactly where the768 previous match ended and all the entire input must be matched.769 """770 i = 0771 matched = False772 while 1:773 match = regex.match(s[i:])774 if match:775 matched = True776 i += match.end()777 yield match778 else:779 if (len(s) > 0 and not matched) or i < len(s):780 raise ValueError("failed to match all of input. "781 "%r remains" % s[i:])782 else:783 return784 615 785 616 … … 819 650 if not dt.tzinfo: 820 651 dt = dt.replace(tzinfo=FixedOffset.for_system()) 821 822 652 return dt.strftime("%d-%b-%Y %H:%M:%S %z") 823 824 653 654 -
livetest.py
r126 r130 12 12 13 13 #TODO: more fetch() testing 14 15 SIMPLE_MESSAGE = 'Subject: something\r\n\r\nFoo\r\n' 14 16 15 17 def test_capabilities(client): … … 118 120 119 121 # Add a message to the folder, it should show up now. 120 body = 'Subject: something\r\n\r\nFoo' 121 client.append(new_folder, body) 122 client.append(new_folder, SIMPLE_MESSAGE) 122 123 123 124 status = client.folder_status(new_folder) … … 139 140 140 141 # Append message 141 body = 'Subject: something\r\n\r\nFoo\r\n' 142 resp = client.append('INBOX', body, ('abc', 'def'), msg_time) 142 resp = client.append('INBOX', SIMPLE_MESSAGE, ('abc', 'def'), msg_time) 143 143 assert isinstance(resp, str) 144 144 … … 164 164 165 165 # Message body should match 166 assert msginfo['RFC822'] == body166 assert msginfo['RFC822'] == SIMPLE_MESSAGE 167 167 168 168 … … 220 220 assert len(client.search(['NOT DELETED', 'SUBJECT "a"'])) == 1 221 221 assert len(client.search(['NOT DELETED', 'SUBJECT "c"'])) == 0 222 223 224 def test_copy(client): 225 clear_folders(client) 226 clear_folder(client, 'INBOX') 227 228 client.select_folder('INBOX') 229 client.append('INBOX', SIMPLE_MESSAGE) 230 client.create_folder('target') 231 msg_id = client.search()[0] 232 233 client.copy(msg_id, 'target') 234 235 client.select_folder('target') 236 msgs = client.search() 237 assert len(msgs) == 1 238 msg_id = msgs[0] 239 assert 'something' in client.fetch(msg_id, ['RFC822'])[msg_id]['RFC822'] 222 240 223 241 … … 245 263 test_flags(client) 246 264 test_search(client) 265 test_copy(client) 266 247 267 248 268 def clear_folder(client, folder): -
livetest.py
r128 r130 39 39 40 40 def test_select_and_close(client): 41 num_msgs = client.select_folder('INBOX') 42 assert isinstance(num_msgs, long) 43 assert num_msgs >= 0 41 resp = client.select_folder('INBOX') 42 assert isinstance(resp['EXISTS'], int) 43 assert resp['EXISTS'] > 1 44 assert isinstance(resp['RECENT'], int) 45 assert isinstance(resp['FLAGS'], tuple) 46 assert len(resp['FLAGS']) > 1 44 47 client.close_folder() 45 48 … … 141 144 142 145 # Retrieve the just added message and check that all looks well 143 num_msgs = client.select_folder('INBOX') 144 assert num_msgs == 1 146 assert client.select_folder('INBOX')['EXISTS'] == 1 145 147 146 148 resp = client.fetch( … … 175 177 176 178 assert answer.has_key(msgid) 177 answer_flags = answer[msgid]179 answer_flags = list(answer[msgid]) 178 180 179 181 # This is required because the order of the returned flags isn't
