Changeset 116:a975e50ceb32 for imapclient/imapclient.py
- Timestamp:
- 01/11/10 19:14:38 (2 years ago)
- Branch:
- default
- Parents:
- 115:7aa870b75d11 (diff), 98:bd244d16fcea (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:
-
- 2 modified
-
imapclient/imapclient.py (modified) (46 diffs)
-
imapclient/imapclient.py (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
imapclient/imapclient.py
r115 r116 11 11 import imap_utf7 12 12 from fixed_offset import FixedOffset 13 13 14 14 15 __all__ = ['IMAPClient', 'DELETED', 'SEEN', 'ANSWERED', 'FLAGGED', 'DRAFT', … … 27 28 28 29 class IMAPClient(object): 29 '''30 """ 30 31 A Pythonic, easy-to-use IMAP client class. 31 32 … … 59 60 These are aliases for the imaplib.IMAP4 exceptions of the same name. Socket 60 61 errors may also be raised in the case of network errors. 61 '''62 """ 62 63 63 64 Error = imaplib.IMAP4.error … … 66 67 67 68 re_sep = re.compile('^\(\("[^"]*" "([^"]+)"\)\)') 68 re_folder = re.compile('\([^)]*\) "[^"]+" "?([^"]+)"?') 69 # re_folder = re.compile('\([^)]*\) "[^"]+" "?([^"]+)"?') 70 re_folder = re.compile(r'\([^)]*\) "[^"]+" (?P<qqq>"?)(?P<folder>.+)(?P=qqq)') 69 71 re_status = re.compile(r'^\s*"?(?P<folder>[^"]+)"?\s+' 70 72 r'\((?P<status_items>.*)\)$') 71 73 72 74 def __init__(self, host, port=None, use_uid=True, ssl=False): 73 '''Initialise object instance and connect to the remote IMAP server.75 """Initialise object instance and connect to the remote IMAP server. 74 76 75 77 @param host: The IMAP server address/hostname to connect to. … … 77 79 @param use_uid: Should message UIDs be used (default is True). 78 80 @param ssl: Make an SSL connection (default is False) 79 '''81 """ 80 82 if ssl: 81 83 ImapClass = imaplib.IMAP4_SSL … … 94 96 95 97 def login(self, username, password): 96 '''Perform a simple login97 '''98 """Perform a simple login 99 """ 98 100 typ, data = self._imap.login(username, password) 99 101 self._checkok('login', typ, data) … … 102 104 103 105 def logout(self): 104 '''Perform a logout105 '''106 """Perform a logout 107 """ 106 108 typ, data = self._imap.logout() 107 109 self._checkbye('logout', typ, data) … … 110 112 111 113 def capabilities(self): 112 '''Returns the server capability list113 '''114 """Returns the server capability list 115 """ 114 116 return self._imap.capabilities 115 117 116 118 117 119 def has_capability(self, capability): 118 '''Checks if the server has the given capability.120 """Checks if the server has the given capability. 119 121 120 122 @param capability: capability to test (eg 'SORT') 121 '''123 """ 122 124 # FIXME: this will not detect capabilities that are backwards 123 125 # compatible with the current level. For instance the SORT … … 131 133 132 134 def get_folder_delimiter(self): 133 '''Determine the folder separator used by the IMAP server.135 """Determine the folder separator used by the IMAP server. 134 136 135 137 @return: The folder separator. 136 138 @rtype: string 137 '''139 """ 138 140 typ, data = self._imap.namespace() 139 141 self._checkok('namespace', typ, data) … … 147 149 148 150 def list_folders(self, directory="", pattern="*"): 149 '''Get a listing of folders on the server.151 """Get a listing of folders on the server. 150 152 151 153 The default behaviour (no args) will list all folders for the logged in … … 159 161 decoding). If the folder_encode attribute is False, no decoding 160 162 will be performed and only ordinary strings will be returned. 161 '''163 """ 162 164 typ, data = self._imap.list(directory, pattern) 163 165 self._checkok('list', typ, data) 164 166 return self._proc_folder_list(data) 167 168 169 def list_sub_folders(self, directory="", pattern="*"): 170 """Get a listing of subscribed folders on the server. 171 172 The default behaviour (no args) will list all subscribed folders for the 173 logged in user. 174 175 @param directory: The base directory to look for folders from. 176 @param pattern: A pattern to match against folder names. Only folder 177 names matching this pattern will be returned. Wildcards accepted. 178 @return: A list of folder names. As per the return of list_folders(). 179 """ 180 typ, data = self._imap.lsub(directory, pattern) 181 self._checkok('lsub', typ, data) 182 return self._proc_folder_list(data) 183 184 185 def _proc_folder_list(self, folder_data): 165 186 folders = [] 166 for line in data:187 for line in folder_data: 167 188 #TODO can the FetchParser code be adapted for use here? 168 189 folder_text = None 169 190 if isinstance(line, tuple): 170 191 folder_text = line[-1] 171 el se:192 elif line: 172 193 match = self.re_folder.match(line) 173 194 if match: 174 folder_text = match.group(1) 195 folder_text = match.group('folder') 196 folder_text = folder_text.replace(r'\"', '"') 197 folder_text = folder_text.replace(r'\\', '\\') 175 198 if folder_text is not None: 176 199 folders.append(self._decode_folder_name(folder_text)) 177 200 return folders 178 179 180 def list_sub_folders(self, directory="", pattern="*"): 181 '''Get a listing of subscribed folders on the server. 182 183 The default behaviour (no args) will list all subscribed folders for the 184 logged in user. 185 186 @param directory: The base directory to look for folders from. 187 @param pattern: A pattern to match against folder names. Only folder 188 names matching this pattern will be returned. Wildcards accepted. 189 @return: A list of folder names. As per the return of list_folders(). 190 ''' 191 typ, data = self._imap.lsub(directory, pattern) 192 self._checkok('lsub', typ, data) 193 194 folders = [] 195 for line in data: 196 if line: 197 m = self.re_folder.match(line) 198 if m: 199 folders.append(self._decode_folder_name(m.group(1))) 200 return folders 201 201 202 202 203 203 def select_folder(self, folder): 204 '''Select the current folder on the server. Future calls to methods204 """Select the current folder on the server. Future calls to methods 205 205 such as search and fetch will act on the selected folder. 206 206 … … 208 208 @return: Number of messages in the folder. 209 209 @rtype: long int 210 '''210 """ 211 211 typ, data = self._imap.select(self._encode_folder_name(folder)) 212 212 self._checkok('select', typ, data) … … 215 215 216 216 def folder_status(self, folder, what=None): 217 '''Requests the status from folder.217 """Requests the status from folder. 218 218 219 219 @param folder: The folder name. … … 223 223 the items specified in the what parameter. 224 224 @rtype: dict 225 '''225 """ 226 226 if what is None: 227 227 what = ('MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN') … … 247 247 248 248 def close_folder(self): 249 '''Close the currently selected folder.249 """Close the currently selected folder. 250 250 251 251 @return: Server response. 252 '''252 """ 253 253 typ, data = self._imap.close() 254 254 self._checkok('close', typ, data) … … 257 257 258 258 def create_folder(self, folder): 259 '''Create a new folder on the server.259 """Create a new folder on the server. 260 260 261 261 @param folder: The folder name. 262 262 @return: Server response. 263 '''263 """ 264 264 typ, data = self._imap.create(self._encode_folder_name(folder)) 265 265 self._checkok('create', typ, data) … … 268 268 269 269 def delete_folder(self, folder): 270 '''Delete a new folder on the server.270 """Delete a new folder on the server. 271 271 272 272 @param folder: Folder name to delete. 273 273 @return: Server response. 274 '''274 """ 275 275 typ, data = self._imap.delete(self._encode_folder_name(folder)) 276 276 self._checkok('delete', typ, data) … … 279 279 280 280 def folder_exists(self, folder): 281 '''Determine if a folder exists on the server.281 """Determine if a folder exists on the server. 282 282 283 283 @param folder: Full folder name to look for. 284 284 @return: True if the folder exists. False otherwise. 285 '''285 """ 286 286 typ, data = self._imap.list('', self._encode_folder_name(folder)) 287 287 self._checkok('list', typ, data) … … 290 290 291 291 def subscribe_folder(self, folder): 292 '''Subscribe to a folder.292 """Subscribe to a folder. 293 293 294 294 @param folder: Folder name to subscribe to. 295 295 @return: Server response message. 296 '''296 """ 297 297 typ, data = self._imap.subscribe(self._encode_folder_name(folder)) 298 298 self._checkok('subscribe', typ, data) … … 301 301 302 302 def unsubscribe_folder(self, folder): 303 '''Unsubscribe a folder.303 """Unsubscribe a folder. 304 304 305 305 @param folder: Folder name to unsubscribe. 306 306 @return: Server response message. 307 '''307 """ 308 308 typ, data = self._imap.unsubscribe(self._encode_folder_name(folder)) 309 309 self._checkok('unsubscribe', typ, data) … … 336 336 337 337 def sort(self, sort_criteria, criteria='ALL', charset='UTF-8' ): 338 '''Returns a list of messages sorted by sort_criteria.338 """Returns a list of messages sorted by sort_criteria. 339 339 340 340 Note that this is an extension to the IMAP4: 341 341 http://www.ietf.org/internet-drafts/draft-ietf-imapext-sort-19.txt 342 '''342 """ 343 343 if not criteria: 344 344 raise ValueError('no criteria specified') … … 365 365 366 366 def get_flags(self, messages): 367 '''Return the flags set for messages367 """Return the flags set for messages 368 368 369 369 @param messages: Message IDs to check flags for 370 370 @return: As for add_f 371 371 { msgid1: [flag1, flag2, ... ], } 372 '''372 """ 373 373 response = self.fetch(messages, ['FLAGS']) 374 374 return self._flatten_dict(response) … … 376 376 377 377 def add_flags(self, messages, flags): 378 '''Add one or more flags to messages378 """Add one or more flags to messages 379 379 380 380 @param messages: Message IDs to add flags to … … 382 382 @return: The flags set for each message ID as a dictionary 383 383 { msgid1: [flag1, flag2, ... ], } 384 '''384 """ 385 385 return self._store('+FLAGS', messages, flags) 386 386 387 387 388 388 def remove_flags(self, messages, flags): 389 '''Remove one or more flags from messages389 """Remove one or more flags from messages 390 390 391 391 @param messages: Message IDs to remove flags from 392 392 @param flags: Sequence of flags to remove 393 393 @return: As for get_flags. 394 '''394 """ 395 395 return self._store('-FLAGS', messages, flags) 396 396 397 397 398 398 def set_flags(self, messages, flags): 399 '''Set the flags for messages399 """Set the flags for messages 400 400 401 401 @param messages: Message IDs to set flags for 402 402 @param flags: Sequence of flags to set 403 403 @return: As for get_flags. 404 '''404 """ 405 405 return self._store('FLAGS', messages, flags) 406 406 407 407 408 408 def delete_messages(self, messages): 409 '''Short-hand method for deleting one or more messages409 """Short-hand method for deleting one or more messages 410 410 411 411 @param messages: Message IDs to mark for deletion. 412 412 @return: Same as for get_flags. 413 '''413 """ 414 414 return self.add_flags(messages, DELETED) 415 415 416 416 417 417 def fetch(self, messages, parts): 418 '''Retrieve selected data items for one or more messages.418 """Retrieve selected data items for one or more messages. 419 419 420 420 @param messages: Message IDs to fetch. … … 424 424 INTERNALDATE parts will be returned as datetime objects converted 425 425 to the local machine's time zone. 426 '''426 """ 427 427 if not messages: 428 428 return {} … … 462 462 463 463 def append(self, folder, msg, flags=(), msg_time=None): 464 '''Append a message to a folder464 """Append a message to a folder 465 465 466 466 @param folder: Folder name to append to. … … 475 475 @return: The append response returned by the server. 476 476 @rtype: str 477 '''477 """ 478 478 if msg_time: 479 479 time_val = '"%s"' % datetime_to_imap(msg_time) … … 497 497 498 498 def getacl(self, folder): 499 '''Get the ACL for a folder499 """Get the ACL for a folder 500 500 501 501 @param folder: Folder name to get the ACL for. 502 502 @return: A list of (who, acl) tuples 503 '''503 """ 504 504 typ, data = self._imap.getacl(folder) 505 505 self._checkok('getacl', typ, data) … … 515 515 516 516 def setacl(self, folder, who, what): 517 '''Set an ACL for a folder517 """Set an ACL for a folder 518 518 519 519 @param folder: Folder name to set an ACL for. … … 521 521 @param what: A string describing the ACL. Set to '' to remove an ACL. 522 522 @return: Server response string. 523 '''523 """ 524 524 typ, data = self._imap.setacl(folder, who, what) 525 525 self._checkok('setacl', typ, data) … … 528 528 529 529 def _check_resp(self, expected, command, typ, data): 530 '''Check command responses for errors.530 """Check command responses for errors. 531 531 532 532 @raise: Error if a command failed. 533 '''533 """ 534 534 if typ != expected: 535 535 raise self.Error('%s failed: %r' % (command, data[0])) … … 545 545 546 546 def _store(self, cmd, messages, flags): 547 '''Worker function for flag manipulation functions547 """Worker function for flag manipulation functions 548 548 549 549 @param cmd: STORE command to use (eg. '+FLAGS') … … 552 552 @return: The flags set for each message ID as a dictionary 553 553 { msgid1: [flag1, flag2, ... ], } 554 '''554 """ 555 555 if not messages: 556 556 return {} … … 587 587 588 588 class FetchParser(object): 589 '''589 """ 590 590 Parse an IMAP FETCH response and convert the return values to useful Python 591 591 values. 592 '''592 """ 593 593 594 594 def parse(self, response): … … 623 623 624 624 data = data.lstrip() 625 msgid = None 625 626 if data[0].isdigit(): 626 627 # Get message ID … … 630 631 assert data.startswith('('), data 631 632 data = data[1:] 632 if data.endswith(')'): 633 data = data[:-1] 634 635 else: 636 msgid = None 633 634 if data.endswith(')'): 635 data = data[:-1] 637 636 638 637 for name, item in FetchTokeniser().process_pairs(data): … … 658 657 659 658 def do_INTERNALDATE(self, arg): 660 '''Process an INTERNALDATE response659 """Process an INTERNALDATE response 661 660 662 661 @param arg: A quoted IMAP INTERNALDATE string 663 662 (eg. " 9-Feb-2007 17:08:08 +0000") 664 663 @return: datetime.datetime instance for the given time (in UTC) 665 '''664 """ 666 665 arg = 'INTERNALDATE "%s"' % arg 667 666 mo = imaplib.InternalDate.match(arg) … … 693 692 694 693 class FetchTokeniser(object): 695 '''694 """ 696 695 General response tokenizer and converter 697 '''696 """ 698 697 699 698 QUOTED_STRING = '(?:".*?")' … … 715 714 716 715 def process_pairs(self, s): 717 '''Break up and convert a string of FETCH response pairs716 """Break up and convert a string of FETCH response pairs 718 717 719 718 @param s: FETCH response string eg. "FOO 12 BAH (1 abc def "foo bar")" 720 719 @return: Tokenised and converted input return as (name, data) pairs. 721 '''720 """ 722 721 out = [] 723 722 for m in strict_finditer(self.PAIR_RE, s): … … 727 726 728 727 def process_list(self, s): 729 '''Break up and convert a string of data items728 """Break up and convert a string of data items 730 729 731 730 @param s: FETCH response string eg. "(1 abc def "foo bar")" 732 731 @return: A list of converted items. 733 '''732 """ 734 733 if s == '': 735 734 return [] … … 755 754 756 755 class Literal(object): 757 '''756 """ 758 757 Simple class to represent a literal token in the fetch response 759 758 (eg. "{21}") 760 '''759 """ 761 760 762 761 def __init__(self, length): … … 771 770 772 771 def strict_finditer(regex, s): 773 '''Like re.finditer except the regex must match from exactly where the772 """Like re.finditer except the regex must match from exactly where the 774 773 previous match ended and all the entire input must be matched. 775 '''774 """ 776 775 i = 0 777 776 matched = False … … 791 790 792 791 def messages_to_str(messages): 793 '''Convert a sequence of messages ids or a single message id into an792 """Convert a sequence of messages ids or a single message id into an 794 793 message ID list for use with IMAP commands. 795 794 … … 797 796 (eg. [1,4,5,7,8]) 798 797 @return: Message list string (eg. "1,4,5,6,8") 799 '''798 """ 800 799 if isinstance(messages, (str, int, long)): 801 800 messages = (messages,) … … 806 805 807 806 def seq_to_parenlist(flags): 808 '''Convert a sequence into parenthised list for use with IMAP commands807 """Convert a sequence into parenthised list for use with IMAP commands 809 808 810 809 @param flags: Sequence to process (eg. ['abc', 'def']) 811 810 @return: IMAP parenthenised list (eg. '(abc def)') 812 '''811 """ 813 812 if isinstance(flags, str): 814 813 flags = (flags,) … … 819 818 820 819 def datetime_to_imap(dt): 821 '''Convert a datetime instance to a IMAP datetime string820 """Convert a datetime instance to a IMAP datetime string 822 821 823 822 If timezone information is missing the current system timezone is used. 824 '''823 """ 825 824 if not dt.tzinfo: 826 825 dt = dt.replace(tzinfo=FixedOffset.for_system()) -
imapclient/imapclient.py
r96 r116 15 15 __all__ = ['IMAPClient', 'DELETED', 'SEEN', 'ANSWERED', 'FLAGGED', 'DRAFT', 16 16 'RECENT'] 17 18 from response_parser import parse_fetch_response 17 19 18 20 # 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 442 def altfetch(self, messages, parts): 443 if not messages: 444 return {} 445 446 msg_list = messages_to_str(messages) 447 parts_list = seq_to_parenlist([p.upper() for p in parts]) 448 449 if self.use_uid: 450 tag = self._imap._command('UID', 'FETCH', msg_list, parts_list) 451 else: 452 tag = self._imap._command('FETCH', msg_list, parts_list) 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) 438 462 439 463 def append(self, folder, msg, flags=(), msg_time=None): … … 800 824 if not dt.tzinfo: 801 825 dt = dt.replace(tzinfo=FixedOffset.for_system()) 802 803 826 return dt.strftime("%d-%b-%Y %H:%M:%S %z") 804 805 827 828
