1"""IMAP4 client. 2 3Based on RFC 2060. 4 5Public class: IMAP4 6Public variable: Debug 7Public functions: Internaldate2tuple 8 Int2AP 9 ParseFlags 10 Time2Internaldate 11""" 12 13# Author: Piers Lauder <piers@cs.su.oz.au> December 1997. 14# 15# Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998. 16# String method conversion by ESR, February 2001. 17# GET/SETACL contributed by Anthony Baxter <anthony@interlink.com.au> April 2001. 18# IMAP4_SSL contributed by Tino Lange <Tino.Lange@isg.de> March 2002. 19# GET/SETQUOTA contributed by Andreas Zeidler <az@kreativkombinat.de> June 2002. 20# PROXYAUTH contributed by Rick Holbert <holbert.13@osu.edu> November 2002. 21# GET/SETANNOTATION contributed by Tomas Lindroos <skitta@abo.fi> June 2005. 22 23__version__ = "2.58" 24 25import binascii, errno, random, re, socket, subprocess, sys, time 26 27__all__ = ["IMAP4", "IMAP4_stream", "Internaldate2tuple", 28 "Int2AP", "ParseFlags", "Time2Internaldate"] 29 30# Globals 31 32CRLF = '\r\n' 33Debug = 0 34IMAP4_PORT = 143 35IMAP4_SSL_PORT = 993 36AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first 37 38# Commands 39 40Commands = { 41 # name valid states 42 'APPEND': ('AUTH', 'SELECTED'), 43 'AUTHENTICATE': ('NONAUTH',), 44 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'), 45 'CHECK': ('SELECTED',), 46 'CLOSE': ('SELECTED',), 47 'COPY': ('SELECTED',), 48 'CREATE': ('AUTH', 'SELECTED'), 49 'DELETE': ('AUTH', 'SELECTED'), 50 'DELETEACL': ('AUTH', 'SELECTED'), 51 'EXAMINE': ('AUTH', 'SELECTED'), 52 'EXPUNGE': ('SELECTED',), 53 'FETCH': ('SELECTED',), 54 'GETACL': ('AUTH', 'SELECTED'), 55 'GETANNOTATION':('AUTH', 'SELECTED'), 56 'GETQUOTA': ('AUTH', 'SELECTED'), 57 'GETQUOTAROOT': ('AUTH', 'SELECTED'), 58 'MYRIGHTS': ('AUTH', 'SELECTED'), 59 'LIST': ('AUTH', 'SELECTED'), 60 'LOGIN': ('NONAUTH',), 61 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'), 62 'LSUB': ('AUTH', 'SELECTED'), 63 'NAMESPACE': ('AUTH', 'SELECTED'), 64 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'), 65 'PARTIAL': ('SELECTED',), # NB: obsolete 66 'PROXYAUTH': ('AUTH',), 67 'RENAME': ('AUTH', 'SELECTED'), 68 'SEARCH': ('SELECTED',), 69 'SELECT': ('AUTH', 'SELECTED'), 70 'SETACL': ('AUTH', 'SELECTED'), 71 'SETANNOTATION':('AUTH', 'SELECTED'), 72 'SETQUOTA': ('AUTH', 'SELECTED'), 73 'SORT': ('SELECTED',), 74 'STATUS': ('AUTH', 'SELECTED'), 75 'STORE': ('SELECTED',), 76 'SUBSCRIBE': ('AUTH', 'SELECTED'), 77 'THREAD': ('SELECTED',), 78 'UID': ('SELECTED',), 79 'UNSUBSCRIBE': ('AUTH', 'SELECTED'), 80 } 81 82# Patterns to match server responses 83 84Continuation = re.compile(r'\+( (?P<data>.*))?') 85Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)') 86InternalDate = re.compile(r'.*INTERNALDATE "' 87 r'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])' 88 r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])' 89 r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])' 90 r'"') 91Literal = re.compile(r'.*{(?P<size>\d+)}$') 92MapCRLF = re.compile(r'\r\n|\r|\n') 93Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]') 94Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?') 95Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?') 96 97 98 99class IMAP4: 100 101 """IMAP4 client class. 102 103 Instantiate with: IMAP4([host[, port]]) 104 105 host - host's name (default: localhost); 106 port - port number (default: standard IMAP4 port). 107 108 All IMAP4rev1 commands are supported by methods of the same 109 name (in lower-case). 110 111 All arguments to commands are converted to strings, except for 112 AUTHENTICATE, and the last argument to APPEND which is passed as 113 an IMAP4 literal. If necessary (the string contains any 114 non-printing characters or white-space and isn't enclosed with 115 either parentheses or double quotes) each string is quoted. 116 However, the 'password' argument to the LOGIN command is always 117 quoted. If you want to avoid having an argument string quoted 118 (eg: the 'flags' argument to STORE) then enclose the string in 119 parentheses (eg: "(\Deleted)"). 120 121 Each command returns a tuple: (type, [data, ...]) where 'type' 122 is usually 'OK' or 'NO', and 'data' is either the text from the 123 tagged response, or untagged results from command. Each 'data' 124 is either a string, or a tuple. If a tuple, then the first part 125 is the header of the response, and the second part contains 126 the data (ie: 'literal' value). 127 128 Errors raise the exception class <instance>.error("<reason>"). 129 IMAP4 server errors raise <instance>.abort("<reason>"), 130 which is a sub-class of 'error'. Mailbox status changes 131 from READ-WRITE to READ-ONLY raise the exception class 132 <instance>.readonly("<reason>"), which is a sub-class of 'abort'. 133 134 "error" exceptions imply a program error. 135 "abort" exceptions imply the connection should be reset, and 136 the command re-tried. 137 "readonly" exceptions imply the command should be re-tried. 138 139 Note: to use this module, you must read the RFCs pertaining to the 140 IMAP4 protocol, as the semantics of the arguments to each IMAP4 141 command are left to the invoker, not to mention the results. Also, 142 most IMAP servers implement a sub-set of the commands available here. 143 """ 144 145 class error(Exception): pass # Logical errors - debug required 146 class abort(error): pass # Service errors - close and retry 147 class readonly(abort): pass # Mailbox status changed to READ-ONLY 148 149 mustquote = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]") 150 151 def __init__(self, host = '', port = IMAP4_PORT): 152 self.debug = Debug 153 self.state = 'LOGOUT' 154 self.literal = None # A literal argument to a command 155 self.tagged_commands = {} # Tagged commands awaiting response 156 self.untagged_responses = {} # {typ: [data, ...], ...} 157 self.continuation_response = '' # Last continuation response 158 self.is_readonly = False # READ-ONLY desired state 159 self.tagnum = 0 160 161 # Open socket to server. 162 163 self.open(host, port) 164 165 # Create unique tag for this session, 166 # and compile tagged response matcher. 167 168 self.tagpre = Int2AP(random.randint(4096, 65535)) 169 self.tagre = re.compile(r'(?P<tag>' 170 + self.tagpre 171 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)') 172 173 # Get server welcome message, 174 # request and store CAPABILITY response. 175 176 if __debug__: 177 self._cmd_log_len = 10 178 self._cmd_log_idx = 0 179 self._cmd_log = {} # Last `_cmd_log_len' interactions 180 if self.debug >= 1: 181 self._mesg('imaplib version %s' % __version__) 182 self._mesg('new IMAP4 connection, tag=%s' % self.tagpre) 183 184 self.welcome = self._get_response() 185 if 'PREAUTH' in self.untagged_responses: 186 self.state = 'AUTH' 187 elif 'OK' in self.untagged_responses: 188 self.state = 'NONAUTH' 189 else: 190 raise self.error(self.welcome) 191 192 typ, dat = self.capability() 193 if dat == [None]: 194 raise self.error('no CAPABILITY response from server') 195 self.capabilities = tuple(dat[-1].upper().split()) 196 197 if __debug__: 198 if self.debug >= 3: 199 self._mesg('CAPABILITIES: %r' % (self.capabilities,)) 200 201 for version in AllowedVersions: 202 if not version in self.capabilities: 203 continue 204 self.PROTOCOL_VERSION = version 205 return 206 207 raise self.error('server not IMAP4 compliant') 208 209 210 def __getattr__(self, attr): 211 # Allow UPPERCASE variants of IMAP4 command methods. 212 if attr in Commands: 213 return getattr(self, attr.lower()) 214 raise AttributeError("Unknown IMAP4 command: '%s'" % attr) 215 216 217 218 # Overridable methods 219 220 221 def open(self, host = '', port = IMAP4_PORT): 222 """Setup connection to remote server on "host:port" 223 (default: localhost:standard IMAP4 port). 224 This connection will be used by the routines: 225 read, readline, send, shutdown. 226 """ 227 self.host = host 228 self.port = port 229 self.sock = socket.create_connection((host, port)) 230 self.file = self.sock.makefile('rb') 231 232 233 def read(self, size): 234 """Read 'size' bytes from remote.""" 235 return self.file.read(size) 236 237 238 def readline(self): 239 """Read line from remote.""" 240 return self.file.readline() 241 242 243 def send(self, data): 244 """Send data to remote.""" 245 self.sock.sendall(data) 246 247 248 def shutdown(self): 249 """Close I/O established in "open".""" 250 self.file.close() 251 try: 252 self.sock.shutdown(socket.SHUT_RDWR) 253 except socket.error as e: 254 # The server might already have closed the connection 255 if e.errno != errno.ENOTCONN: 256 raise 257 finally: 258 self.sock.close() 259 260 261 def socket(self): 262 """Return socket instance used to connect to IMAP4 server. 263 264 socket = <instance>.socket() 265 """ 266 return self.sock 267 268 269 270 # Utility methods 271 272 273 def recent(self): 274 """Return most recent 'RECENT' responses if any exist, 275 else prompt server for an update using the 'NOOP' command. 276 277 (typ, [data]) = <instance>.recent() 278 279 'data' is None if no new messages, 280 else list of RECENT responses, most recent last. 281 """ 282 name = 'RECENT' 283 typ, dat = self._untagged_response('OK', [None], name) 284 if dat[-1]: 285 return typ, dat 286 typ, dat = self.noop() # Prod server for response 287 return self._untagged_response(typ, dat, name) 288 289 290 def response(self, code): 291 """Return data for response 'code' if received, or None. 292 293 Old value for response 'code' is cleared. 294 295 (code, [data]) = <instance>.response(code) 296 """ 297 return self._untagged_response(code, [None], code.upper()) 298 299 300 301 # IMAP4 commands 302 303 304 def append(self, mailbox, flags, date_time, message): 305 """Append message to named mailbox. 306 307 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message) 308 309 All args except `message' can be None. 310 """ 311 name = 'APPEND' 312 if not mailbox: 313 mailbox = 'INBOX' 314 if flags: 315 if (flags[0],flags[-1]) != ('(',')'): 316 flags = '(%s)' % flags 317 else: 318 flags = None 319 if date_time: 320 date_time = Time2Internaldate(date_time) 321 else: 322 date_time = None 323 self.literal = MapCRLF.sub(CRLF, message) 324 return self._simple_command(name, mailbox, flags, date_time) 325 326 327 def authenticate(self, mechanism, authobject): 328 """Authenticate command - requires response processing. 329 330 'mechanism' specifies which authentication mechanism is to 331 be used - it must appear in <instance>.capabilities in the 332 form AUTH=<mechanism>. 333 334 'authobject' must be a callable object: 335 336 data = authobject(response) 337 338 It will be called to process server continuation responses. 339 It should return data that will be encoded and sent to server. 340 It should return None if the client abort response '*' should 341 be sent instead. 342 """ 343 mech = mechanism.upper() 344 # XXX: shouldn't this code be removed, not commented out? 345 #cap = 'AUTH=%s' % mech 346 #if not cap in self.capabilities: # Let the server decide! 347 # raise self.error("Server doesn't allow %s authentication." % mech) 348 self.literal = _Authenticator(authobject).process 349 typ, dat = self._simple_command('AUTHENTICATE', mech) 350 if typ != 'OK': 351 raise self.error(dat[-1]) 352 self.state = 'AUTH' 353 return typ, dat 354 355 356 def capability(self): 357 """(typ, [data]) = <instance>.capability() 358 Fetch capabilities list from server.""" 359 360 name = 'CAPABILITY' 361 typ, dat = self._simple_command(name) 362 return self._untagged_response(typ, dat, name) 363 364 365 def check(self): 366 """Checkpoint mailbox on server. 367 368 (typ, [data]) = <instance>.check() 369 """ 370 return self._simple_command('CHECK') 371 372 373 def close(self): 374 """Close currently selected mailbox. 375 376 Deleted messages are removed from writable mailbox. 377 This is the recommended command before 'LOGOUT'. 378 379 (typ, [data]) = <instance>.close() 380 """ 381 try: 382 typ, dat = self._simple_command('CLOSE') 383 finally: 384 self.state = 'AUTH' 385 return typ, dat 386 387 388 def copy(self, message_set, new_mailbox): 389 """Copy 'message_set' messages onto end of 'new_mailbox'. 390 391 (typ, [data]) = <instance>.copy(message_set, new_mailbox) 392 """ 393 return self._simple_command('COPY', message_set, new_mailbox) 394 395 396 def create(self, mailbox): 397 """Create new mailbox. 398 399 (typ, [data]) = <instance>.create(mailbox) 400 """ 401 return self._simple_command('CREATE', mailbox) 402 403 404 def delete(self, mailbox): 405 """Delete old mailbox. 406 407 (typ, [data]) = <instance>.delete(mailbox) 408 """ 409 return self._simple_command('DELETE', mailbox) 410 411 def deleteacl(self, mailbox, who): 412 """Delete the ACLs (remove any rights) set for who on mailbox. 413 414 (typ, [data]) = <instance>.deleteacl(mailbox, who) 415 """ 416 return self._simple_command('DELETEACL', mailbox, who) 417 418 def expunge(self): 419 """Permanently remove deleted items from selected mailbox. 420 421 Generates 'EXPUNGE' response for each deleted message. 422 423 (typ, [data]) = <instance>.expunge() 424 425 'data' is list of 'EXPUNGE'd message numbers in order received. 426 """ 427 name = 'EXPUNGE' 428 typ, dat = self._simple_command(name) 429 return self._untagged_response(typ, dat, name) 430 431 432 def fetch(self, message_set, message_parts): 433 """Fetch (parts of) messages. 434 435 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts) 436 437 'message_parts' should be a string of selected parts 438 enclosed in parentheses, eg: "(UID BODY[TEXT])". 439 440 'data' are tuples of message part envelope and data. 441 """ 442 name = 'FETCH' 443 typ, dat = self._simple_command(name, message_set, message_parts) 444 return self._untagged_response(typ, dat, name) 445 446 447 def getacl(self, mailbox): 448 """Get the ACLs for a mailbox. 449 450 (typ, [data]) = <instance>.getacl(mailbox) 451 """ 452 typ, dat = self._simple_command('GETACL', mailbox) 453 return self._untagged_response(typ, dat, 'ACL') 454 455 456 def getannotation(self, mailbox, entry, attribute): 457 """(typ, [data]) = <instance>.getannotation(mailbox, entry, attribute) 458 Retrieve ANNOTATIONs.""" 459 460 typ, dat = self._simple_command('GETANNOTATION', mailbox, entry, attribute) 461 return self._untagged_response(typ, dat, 'ANNOTATION') 462 463 464 def getquota(self, root): 465 """Get the quota root's resource usage and limits. 466 467 Part of the IMAP4 QUOTA extension defined in rfc2087. 468 469 (typ, [data]) = <instance>.getquota(root) 470 """ 471 typ, dat = self._simple_command('GETQUOTA', root) 472 return self._untagged_response(typ, dat, 'QUOTA') 473 474 475 def getquotaroot(self, mailbox): 476 """Get the list of quota roots for the named mailbox. 477 478 (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox) 479 """ 480 typ, dat = self._simple_command('GETQUOTAROOT', mailbox) 481 typ, quota = self._untagged_response(typ, dat, 'QUOTA') 482 typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT') 483 return typ, [quotaroot, quota] 484 485 486 def list(self, directory='""', pattern='*'): 487 """List mailbox names in directory matching pattern. 488 489 (typ, [data]) = <instance>.list(directory='""', pattern='*') 490 491 'data' is list of LIST responses. 492 """ 493 name = 'LIST' 494 typ, dat = self._simple_command(name, directory, pattern) 495 return self._untagged_response(typ, dat, name) 496 497 498 def login(self, user, password): 499 """Identify client using plaintext password. 500 501 (typ, [data]) = <instance>.login(user, password) 502 503 NB: 'password' will be quoted. 504 """ 505 typ, dat = self._simple_command('LOGIN', user, self._quote(password)) 506 if typ != 'OK': 507 raise self.error(dat[-1]) 508 self.state = 'AUTH' 509 return typ, dat 510 511 512 def login_cram_md5(self, user, password): 513 """ Force use of CRAM-MD5 authentication. 514 515 (typ, [data]) = <instance>.login_cram_md5(user, password) 516 """ 517 self.user, self.password = user, password 518 return self.authenticate('CRAM-MD5', self._CRAM_MD5_AUTH) 519 520 521 def _CRAM_MD5_AUTH(self, challenge): 522 """ Authobject to use with CRAM-MD5 authentication. """ 523 import hmac 524 return self.user + " " + hmac.HMAC(self.password, challenge).hexdigest() 525 526 527 def logout(self): 528 """Shutdown connection to server. 529 530 (typ, [data]) = <instance>.logout() 531 532 Returns server 'BYE' response. 533 """ 534 self.state = 'LOGOUT' 535 try: typ, dat = self._simple_command('LOGOUT') 536 except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]] 537 self.shutdown() 538 if 'BYE' in self.untagged_responses: 539 return 'BYE', self.untagged_responses['BYE'] 540 return typ, dat 541 542 543 def lsub(self, directory='""', pattern='*'): 544 """List 'subscribed' mailbox names in directory matching pattern. 545 546 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*') 547 548 'data' are tuples of message part envelope and data. 549 """ 550 name = 'LSUB' 551 typ, dat = self._simple_command(name, directory, pattern) 552 return self._untagged_response(typ, dat, name) 553 554 def myrights(self, mailbox): 555 """Show my ACLs for a mailbox (i.e. the rights that I have on mailbox). 556 557 (typ, [data]) = <instance>.myrights(mailbox) 558 """ 559 typ,dat = self._simple_command('MYRIGHTS', mailbox) 560 return self._untagged_response(typ, dat, 'MYRIGHTS') 561 562 def namespace(self): 563 """ Returns IMAP namespaces ala rfc2342 564 565 (typ, [data, ...]) = <instance>.namespace() 566 """ 567 name = 'NAMESPACE' 568 typ, dat = self._simple_command(name) 569 return self._untagged_response(typ, dat, name) 570 571 572 def noop(self): 573 """Send NOOP command. 574 575 (typ, [data]) = <instance>.noop() 576 """ 577 if __debug__: 578 if self.debug >= 3: 579 self._dump_ur(self.untagged_responses) 580 return self._simple_command('NOOP') 581 582 583 def partial(self, message_num, message_part, start, length): 584 """Fetch truncated part of a message. 585 586 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length) 587 588 'data' is tuple of message part envelope and data. 589 """ 590 name = 'PARTIAL' 591 typ, dat = self._simple_command(name, message_num, message_part, start, length) 592 return self._untagged_response(typ, dat, 'FETCH') 593 594 595 def proxyauth(self, user): 596 """Assume authentication as "user". 597 598 Allows an authorised administrator to proxy into any user's 599 mailbox. 600 601 (typ, [data]) = <instance>.proxyauth(user) 602 """ 603 604 name = 'PROXYAUTH' 605 return self._simple_command('PROXYAUTH', user) 606 607 608 def rename(self, oldmailbox, newmailbox): 609 """Rename old mailbox name to new. 610 611 (typ, [data]) = <instance>.rename(oldmailbox, newmailbox) 612 """ 613 return self._simple_command('RENAME', oldmailbox, newmailbox) 614 615 616 def search(self, charset, *criteria): 617 """Search mailbox for matching messages. 618 619 (typ, [data]) = <instance>.search(charset, criterion, ...) 620 621 'data' is space separated list of matching message numbers. 622 """ 623 name = 'SEARCH' 624 if charset: 625 typ, dat = self._simple_command(name, 'CHARSET', charset, *criteria) 626 else: 627 typ, dat = self._simple_command(name, *criteria) 628 return self._untagged_response(typ, dat, name) 629 630 631 def select(self, mailbox='INBOX', readonly=False): 632 """Select a mailbox. 633 634 Flush all untagged responses. 635 636 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=False) 637 638 'data' is count of messages in mailbox ('EXISTS' response). 639 640 Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so 641 other responses should be obtained via <instance>.response('FLAGS') etc. 642 """ 643 self.untagged_responses = {} # Flush old responses. 644 self.is_readonly = readonly 645 if readonly: 646 name = 'EXAMINE' 647 else: 648 name = 'SELECT' 649 typ, dat = self._simple_command(name, mailbox) 650 if typ != 'OK': 651 self.state = 'AUTH' # Might have been 'SELECTED' 652 return typ, dat 653 self.state = 'SELECTED' 654 if 'READ-ONLY' in self.untagged_responses \ 655 and not readonly: 656 if __debug__: 657 if self.debug >= 1: 658 self._dump_ur(self.untagged_responses) 659 raise self.readonly('%s is not writable' % mailbox) 660 return typ, self.untagged_responses.get('EXISTS', [None]) 661 662 663 def setacl(self, mailbox, who, what): 664 """Set a mailbox acl. 665 666 (typ, [data]) = <instance>.setacl(mailbox, who, what) 667 """ 668 return self._simple_command('SETACL', mailbox, who, what) 669 670 671 def setannotation(self, *args): 672 """(typ, [data]) = <instance>.setannotation(mailbox[, entry, attribute]+) 673 Set ANNOTATIONs.""" 674 675 typ, dat = self._simple_command('SETANNOTATION', *args) 676 return self._untagged_response(typ, dat, 'ANNOTATION') 677 678 679 def setquota(self, root, limits): 680 """Set the quota root's resource limits. 681 682 (typ, [data]) = <instance>.setquota(root, limits) 683 """ 684 typ, dat = self._simple_command('SETQUOTA', root, limits) 685 return self._untagged_response(typ, dat, 'QUOTA') 686 687 688 def sort(self, sort_criteria, charset, *search_criteria): 689 """IMAP4rev1 extension SORT command. 690 691 (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...) 692 """ 693 name = 'SORT' 694 #if not name in self.capabilities: # Let the server decide! 695 # raise self.error('unimplemented extension command: %s' % name) 696 if (sort_criteria[0],sort_criteria[-1]) != ('(',')'): 697 sort_criteria = '(%s)' % sort_criteria 698 typ, dat = self._simple_command(name, sort_criteria, charset, *search_criteria) 699 return self._untagged_response(typ, dat, name) 700 701 702 def status(self, mailbox, names): 703 """Request named status conditions for mailbox. 704 705 (typ, [data]) = <instance>.status(mailbox, names) 706 """ 707 name = 'STATUS' 708 #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide! 709 # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name) 710 typ, dat = self._simple_command(name, mailbox, names) 711 return self._untagged_response(typ, dat, name) 712 713 714 def store(self, message_set, command, flags): 715 """Alters flag dispositions for messages in mailbox. 716 717 (typ, [data]) = <instance>.store(message_set, command, flags) 718 """ 719 if (flags[0],flags[-1]) != ('(',')'): 720 flags = '(%s)' % flags # Avoid quoting the flags 721 typ, dat = self._simple_command('STORE', message_set, command, flags) 722 return self._untagged_response(typ, dat, 'FETCH') 723 724 725 def subscribe(self, mailbox): 726 """Subscribe to new mailbox. 727 728 (typ, [data]) = <instance>.subscribe(mailbox) 729 """ 730 return self._simple_command('SUBSCRIBE', mailbox) 731 732 733 def thread(self, threading_algorithm, charset, *search_criteria): 734 """IMAPrev1 extension THREAD command. 735 736 (type, [data]) = <instance>.thread(threading_algorithm, charset, search_criteria, ...) 737 """ 738 name = 'THREAD' 739 typ, dat = self._simple_command(name, threading_algorithm, charset, *search_criteria) 740 return self._untagged_response(typ, dat, name) 741 742 743 def uid(self, command, *args): 744 """Execute "command arg ..." with messages identified by UID, 745 rather than message number. 746 747 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...) 748 749 Returns response appropriate to 'command'. 750 """ 751 command = command.upper() 752 if not command in Commands: 753 raise self.error("Unknown IMAP4 UID command: %s" % command) 754 if self.state not in Commands[command]: 755 raise self.error("command %s illegal in state %s, " 756 "only allowed in states %s" % 757 (command, self.state, 758 ', '.join(Commands[command]))) 759 name = 'UID' 760 typ, dat = self._simple_command(name, command, *args) 761 if command in ('SEARCH', 'SORT', 'THREAD'): 762 name = command 763 else: 764 name = 'FETCH' 765 return self._untagged_response(typ, dat, name) 766 767 768 def unsubscribe(self, mailbox): 769 """Unsubscribe from old mailbox. 770 771 (typ, [data]) = <instance>.unsubscribe(mailbox) 772 """ 773 return self._simple_command('UNSUBSCRIBE', mailbox) 774 775 776 def xatom(self, name, *args): 777 """Allow simple extension commands 778 notified by server in CAPABILITY response. 779 780 Assumes command is legal in current state. 781 782 (typ, [data]) = <instance>.xatom(name, arg, ...) 783 784 Returns response appropriate to extension command `name'. 785 """ 786 name = name.upper() 787 #if not name in self.capabilities: # Let the server decide! 788 # raise self.error('unknown extension command: %s' % name) 789 if not name in Commands: 790 Commands[name] = (self.state,) 791 return self._simple_command(name, *args) 792 793 794 795 # Private methods 796 797 798 def _append_untagged(self, typ, dat): 799 800 if dat is None: dat = '' 801 ur = self.untagged_responses 802 if __debug__: 803 if self.debug >= 5: 804 self._mesg('untagged_responses[%s] %s += ["%s"]' % 805 (typ, len(ur.get(typ,'')), dat)) 806 if typ in ur: 807 ur[typ].append(dat) 808 else: 809 ur[typ] = [dat] 810 811 812 def _check_bye(self): 813 bye = self.untagged_responses.get('BYE') 814 if bye: 815 raise self.abort(bye[-1]) 816 817 818 def _command(self, name, *args): 819 820 if self.state not in Commands[name]: 821 self.literal = None 822 raise self.error("command %s illegal in state %s, " 823 "only allowed in states %s" % 824 (name, self.state, 825 ', '.join(Commands[name]))) 826 827 for typ in ('OK', 'NO', 'BAD'): 828 if typ in self.untagged_responses: 829 del self.untagged_responses[typ] 830 831 if 'READ-ONLY' in self.untagged_responses \ 832 and not self.is_readonly: 833 raise self.readonly('mailbox status changed to READ-ONLY') 834 835 tag = self._new_tag() 836 data = '%s %s' % (tag, name) 837 for arg in args: 838 if arg is None: continue 839 data = '%s %s' % (data, self._checkquote(arg)) 840 841 literal = self.literal 842 if literal is not None: 843 self.literal = None 844 if type(literal) is type(self._command): 845 literator = literal 846 else: 847 literator = None 848 data = '%s {%s}' % (data, len(literal)) 849 850 if __debug__: 851 if self.debug >= 4: 852 self._mesg('> %s' % data) 853 else: 854 self._log('> %s' % data) 855 856 try: 857 self.send('%s%s' % (data, CRLF)) 858 except (socket.error, OSError), val: 859 raise self.abort('socket error: %s' % val) 860 861 if literal is None: 862 return tag 863 864 while 1: 865 # Wait for continuation response 866 867 while self._get_response(): 868 if self.tagged_commands[tag]: # BAD/NO? 869 return tag 870 871 # Send literal 872 873 if literator: 874 literal = literator(self.continuation_response) 875 876 if __debug__: 877 if self.debug >= 4: 878 self._mesg('write literal size %s' % len(literal)) 879 880 try: 881 self.send(literal) 882 self.send(CRLF) 883 except (socket.error, OSError), val: 884 raise self.abort('socket error: %s' % val) 885 886 if not literator: 887 break 888 889 return tag 890 891 892 def _command_complete(self, name, tag): 893 # BYE is expected after LOGOUT 894 if name != 'LOGOUT': 895 self._check_bye() 896 try: 897 typ, data = self._get_tagged_response(tag) 898 except self.abort, val: 899 raise self.abort('command: %s => %s' % (name, val)) 900 except self.error, val: 901 raise self.error('command: %s => %s' % (name, val)) 902 if name != 'LOGOUT': 903 self._check_bye() 904 if typ == 'BAD': 905 raise self.error('%s command error: %s %s' % (name, typ, data)) 906 return typ, data 907 908 909 def _get_response(self): 910 911 # Read response and store. 912 # 913 # Returns None for continuation responses, 914 # otherwise first response line received. 915 916 resp = self._get_line() 917 918 # Command completion response? 919 920 if self._match(self.tagre, resp): 921 tag = self.mo.group('tag') 922 if not tag in self.tagged_commands: 923 raise self.abort('unexpected tagged response: %s' % resp) 924 925 typ = self.mo.group('type') 926 dat = self.mo.group('data') 927 self.tagged_commands[tag] = (typ, [dat]) 928 else: 929 dat2 = None 930 931 # '*' (untagged) responses? 932 933 if not self._match(Untagged_response, resp): 934 if self._match(Untagged_status, resp): 935 dat2 = self.mo.group('data2') 936 937 if self.mo is None: 938 # Only other possibility is '+' (continuation) response... 939 940 if self._match(Continuation, resp): 941 self.continuation_response = self.mo.group('data') 942 return None # NB: indicates continuation 943 944 raise self.abort("unexpected response: '%s'" % resp) 945 946 typ = self.mo.group('type') 947 dat = self.mo.group('data') 948 if dat is None: dat = '' # Null untagged response 949 if dat2: dat = dat + ' ' + dat2 950 951 # Is there a literal to come? 952 953 while self._match(Literal, dat): 954 955 # Read literal direct from connection. 956 957 size = int(self.mo.group('size')) 958 if __debug__: 959 if self.debug >= 4: 960 self._mesg('read literal size %s' % size) 961 data = self.read(size) 962 963 # Store response with literal as tuple 964 965 self._append_untagged(typ, (dat, data)) 966 967 # Read trailer - possibly containing another literal 968 969 dat = self._get_line() 970 971 self._append_untagged(typ, dat) 972 973 # Bracketed response information? 974 975 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat): 976 self._append_untagged(self.mo.group('type'), self.mo.group('data')) 977 978 if __debug__: 979 if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'): 980 self._mesg('%s response: %s' % (typ, dat)) 981 982 return resp 983 984 985 def _get_tagged_response(self, tag): 986 987 while 1: 988 result = self.tagged_commands[tag] 989 if result is not None: 990 del self.tagged_commands[tag] 991 return result 992 993 # Some have reported "unexpected response" exceptions. 994 # Note that ignoring them here causes loops. 995 # Instead, send me details of the unexpected response and 996 # I'll update the code in `_get_response()'. 997 998 try: 999 self._get_response() 1000 except self.abort, val: 1001 if __debug__: 1002 if self.debug >= 1: 1003 self.print_log() 1004 raise 1005 1006 1007 def _get_line(self): 1008 1009 line = self.readline() 1010 if not line: 1011 raise self.abort('socket error: EOF') 1012 1013 # Protocol mandates all lines terminated by CRLF 1014 if not line.endswith('\r\n'): 1015 raise self.abort('socket error: unterminated line') 1016 1017 line = line[:-2] 1018 if __debug__: 1019 if self.debug >= 4: 1020 self._mesg('< %s' % line) 1021 else: 1022 self._log('< %s' % line) 1023 return line 1024 1025 1026 def _match(self, cre, s): 1027 1028 # Run compiled regular expression match method on 's'. 1029 # Save result, return success. 1030 1031 self.mo = cre.match(s) 1032 if __debug__: 1033 if self.mo is not None and self.debug >= 5: 1034 self._mesg("\tmatched r'%s' => %r" % (cre.pattern, self.mo.groups())) 1035 return self.mo is not None 1036 1037 1038 def _new_tag(self): 1039 1040 tag = '%s%s' % (self.tagpre, self.tagnum) 1041 self.tagnum = self.tagnum + 1 1042 self.tagged_commands[tag] = None 1043 return tag 1044 1045 1046 def _checkquote(self, arg): 1047 1048 # Must quote command args if non-alphanumeric chars present, 1049 # and not already quoted. 1050 1051 if type(arg) is not type(''): 1052 return arg 1053 if len(arg) >= 2 and (arg[0],arg[-1]) in (('(',')'),('"','"')): 1054 return arg 1055 if arg and self.mustquote.search(arg) is None: 1056 return arg 1057 return self._quote(arg) 1058 1059 1060 def _quote(self, arg): 1061 1062 arg = arg.replace('\\', '\\\\') 1063 arg = arg.replace('"', '\\"') 1064 1065 return '"%s"' % arg 1066 1067 1068 def _simple_command(self, name, *args): 1069 1070 return self._command_complete(name, self._command(name, *args)) 1071 1072 1073 def _untagged_response(self, typ, dat, name): 1074 1075 if typ == 'NO': 1076 return typ, dat 1077 if not name in self.untagged_responses: 1078 return typ, [None] 1079 data = self.untagged_responses.pop(name) 1080 if __debug__: 1081 if self.debug >= 5: 1082 self._mesg('untagged_responses[%s] => %s' % (name, data)) 1083 return typ, data 1084 1085 1086 if __debug__: 1087 1088 def _mesg(self, s, secs=None): 1089 if secs is None: 1090 secs = time.time() 1091 tm = time.strftime('%M:%S', time.localtime(secs)) 1092 sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s)) 1093 sys.stderr.flush() 1094 1095 def _dump_ur(self, dict): 1096 # Dump untagged responses (in `dict'). 1097 l = dict.items() 1098 if not l: return 1099 t = '\n\t\t' 1100 l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l) 1101 self._mesg('untagged responses dump:%s%s' % (t, t.join(l))) 1102 1103 def _log(self, line): 1104 # Keep log of last `_cmd_log_len' interactions for debugging. 1105 self._cmd_log[self._cmd_log_idx] = (line, time.time()) 1106 self._cmd_log_idx += 1 1107 if self._cmd_log_idx >= self._cmd_log_len: 1108 self._cmd_log_idx = 0 1109 1110 def print_log(self): 1111 self._mesg('last %d IMAP4 interactions:' % len(self._cmd_log)) 1112 i, n = self._cmd_log_idx, self._cmd_log_len 1113 while n: 1114 try: 1115 self._mesg(*self._cmd_log[i]) 1116 except: 1117 pass 1118 i += 1 1119 if i >= self._cmd_log_len: 1120 i = 0 1121 n -= 1 1122 1123 1124 1125try: 1126 import ssl 1127except ImportError: 1128 pass 1129else: 1130 class IMAP4_SSL(IMAP4): 1131 1132 """IMAP4 client class over SSL connection 1133 1134 Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile]]]]) 1135 1136 host - host's name (default: localhost); 1137 port - port number (default: standard IMAP4 SSL port). 1138 keyfile - PEM formatted file that contains your private key (default: None); 1139 certfile - PEM formatted certificate chain file (default: None); 1140 1141 for more documentation see the docstring of the parent class IMAP4. 1142 """ 1143 1144 1145 def __init__(self, host = '', port = IMAP4_SSL_PORT, keyfile = None, certfile = None): 1146 self.keyfile = keyfile 1147 self.certfile = certfile 1148 IMAP4.__init__(self, host, port) 1149 1150 1151 def open(self, host = '', port = IMAP4_SSL_PORT): 1152 """Setup connection to remote server on "host:port". 1153 (default: localhost:standard IMAP4 SSL port). 1154 This connection will be used by the routines: 1155 read, readline, send, shutdown. 1156 """ 1157 self.host = host 1158 self.port = port 1159 self.sock = socket.create_connection((host, port)) 1160 self.sslobj = ssl.wrap_socket(self.sock, self.keyfile, self.certfile) 1161 self.file = self.sslobj.makefile('rb') 1162 1163 1164 def read(self, size): 1165 """Read 'size' bytes from remote.""" 1166 return self.file.read(size) 1167 1168 1169 def readline(self): 1170 """Read line from remote.""" 1171 return self.file.readline() 1172 1173 1174 def send(self, data): 1175 """Send data to remote.""" 1176 bytes = len(data) 1177 while bytes > 0: 1178 sent = self.sslobj.write(data) 1179 if sent == bytes: 1180 break # avoid copy 1181 data = data[sent:] 1182 bytes = bytes - sent 1183 1184 1185 def shutdown(self): 1186 """Close I/O established in "open".""" 1187 self.file.close() 1188 self.sock.close() 1189 1190 1191 def socket(self): 1192 """Return socket instance used to connect to IMAP4 server. 1193 1194 socket = <instance>.socket() 1195 """ 1196 return self.sock 1197 1198 1199 def ssl(self): 1200 """Return SSLObject instance used to communicate with the IMAP4 server. 1201 1202 ssl = ssl.wrap_socket(<instance>.socket) 1203 """ 1204 return self.sslobj 1205 1206 __all__.append("IMAP4_SSL") 1207 1208 1209class IMAP4_stream(IMAP4): 1210 1211 """IMAP4 client class over a stream 1212 1213 Instantiate with: IMAP4_stream(command) 1214 1215 where "command" is a string that can be passed to subprocess.Popen() 1216 1217 for more documentation see the docstring of the parent class IMAP4. 1218 """ 1219 1220 1221 def __init__(self, command): 1222 self.command = command 1223 IMAP4.__init__(self) 1224 1225 1226 def open(self, host = None, port = None): 1227 """Setup a stream connection. 1228 This connection will be used by the routines: 1229 read, readline, send, shutdown. 1230 """ 1231 self.host = None # For compatibility with parent class 1232 self.port = None 1233 self.sock = None 1234 self.file = None 1235 self.process = subprocess.Popen(self.command, 1236 stdin=subprocess.PIPE, stdout=subprocess.PIPE, 1237 shell=True, close_fds=True) 1238 self.writefile = self.process.stdin 1239 self.readfile = self.process.stdout 1240 1241 1242 def read(self, size): 1243 """Read 'size' bytes from remote.""" 1244 return self.readfile.read(size) 1245 1246 1247 def readline(self): 1248 """Read line from remote.""" 1249 return self.readfile.readline() 1250 1251 1252 def send(self, data): 1253 """Send data to remote.""" 1254 self.writefile.write(data) 1255 self.writefile.flush() 1256 1257 1258 def shutdown(self): 1259 """Close I/O established in "open".""" 1260 self.readfile.close() 1261 self.writefile.close() 1262 self.process.wait() 1263 1264 1265 1266class _Authenticator: 1267 1268 """Private class to provide en/decoding 1269 for base64-based authentication conversation. 1270 """ 1271 1272 def __init__(self, mechinst): 1273 self.mech = mechinst # Callable object to provide/process data 1274 1275 def process(self, data): 1276 ret = self.mech(self.decode(data)) 1277 if ret is None: 1278 return '*' # Abort conversation 1279 return self.encode(ret) 1280 1281 def encode(self, inp): 1282 # 1283 # Invoke binascii.b2a_base64 iteratively with 1284 # short even length buffers, strip the trailing 1285 # line feed from the result and append. "Even" 1286 # means a number that factors to both 6 and 8, 1287 # so when it gets to the end of the 8-bit input 1288 # there's no partial 6-bit output. 1289 # 1290 oup = '' 1291 while inp: 1292 if len(inp) > 48: 1293 t = inp[:48] 1294 inp = inp[48:] 1295 else: 1296 t = inp 1297 inp = '' 1298 e = binascii.b2a_base64(t) 1299 if e: 1300 oup = oup + e[:-1] 1301 return oup 1302 1303 def decode(self, inp): 1304 if not inp: 1305 return '' 1306 return binascii.a2b_base64(inp) 1307 1308 1309 1310Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6, 1311 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12} 1312 1313def Internaldate2tuple(resp): 1314 """Parse an IMAP4 INTERNALDATE string. 1315 1316 Return corresponding local time. The return value is a 1317 time.struct_time instance or None if the string has wrong format. 1318 """ 1319 1320 mo = InternalDate.match(resp) 1321 if not mo: 1322 return None 1323 1324 mon = Mon2num[mo.group('mon')] 1325 zonen = mo.group('zonen') 1326 1327 day = int(mo.group('day')) 1328 year = int(mo.group('year')) 1329 hour = int(mo.group('hour')) 1330 min = int(mo.group('min')) 1331 sec = int(mo.group('sec')) 1332 zoneh = int(mo.group('zoneh')) 1333 zonem = int(mo.group('zonem')) 1334 1335 # INTERNALDATE timezone must be subtracted to get UT 1336 1337 zone = (zoneh*60 + zonem)*60 1338 if zonen == '-': 1339 zone = -zone 1340 1341 tt = (year, mon, day, hour, min, sec, -1, -1, -1) 1342 1343 utc = time.mktime(tt) 1344 1345 # Following is necessary because the time module has no 'mkgmtime'. 1346 # 'mktime' assumes arg in local timezone, so adds timezone/altzone. 1347 1348 lt = time.localtime(utc) 1349 if time.daylight and lt[-1]: 1350 zone = zone + time.altzone 1351 else: 1352 zone = zone + time.timezone 1353 1354 return time.localtime(utc - zone) 1355 1356 1357 1358def Int2AP(num): 1359 1360 """Convert integer to A-P string representation.""" 1361 1362 val = ''; AP = 'ABCDEFGHIJKLMNOP' 1363 num = int(abs(num)) 1364 while num: 1365 num, mod = divmod(num, 16) 1366 val = AP[mod] + val 1367 return val 1368 1369 1370 1371def ParseFlags(resp): 1372 1373 """Convert IMAP4 flags response to python tuple.""" 1374 1375 mo = Flags.match(resp) 1376 if not mo: 1377 return () 1378 1379 return tuple(mo.group('flags').split()) 1380 1381 1382def Time2Internaldate(date_time): 1383 1384 """Convert date_time to IMAP4 INTERNALDATE representation. 1385 1386 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'. The 1387 date_time argument can be a number (int or float) representing 1388 seconds since epoch (as returned by time.time()), a 9-tuple 1389 representing local time (as returned by time.localtime()), or a 1390 double-quoted string. In the last case, it is assumed to already 1391 be in the correct format. 1392 """ 1393 1394 if isinstance(date_time, (int, float)): 1395 tt = time.localtime(date_time) 1396 elif isinstance(date_time, (tuple, time.struct_time)): 1397 tt = date_time 1398 elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'): 1399 return date_time # Assume in correct format 1400 else: 1401 raise ValueError("date_time not of a known type") 1402 1403 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt) 1404 if dt[0] == '0': 1405 dt = ' ' + dt[1:] 1406 if time.daylight and tt[-1]: 1407 zone = -time.altzone 1408 else: 1409 zone = -time.timezone 1410 return '"' + dt + " %+03d%02d" % divmod(zone//60, 60) + '"' 1411 1412 1413 1414if __name__ == '__main__': 1415 1416 # To test: invoke either as 'python imaplib.py [IMAP4_server_hostname]' 1417 # or 'python imaplib.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"' 1418 # to test the IMAP4_stream class 1419 1420 import getopt, getpass 1421 1422 try: 1423 optlist, args = getopt.getopt(sys.argv[1:], 'd:s:') 1424 except getopt.error, val: 1425 optlist, args = (), () 1426 1427 stream_command = None 1428 for opt,val in optlist: 1429 if opt == '-d': 1430 Debug = int(val) 1431 elif opt == '-s': 1432 stream_command = val 1433 if not args: args = (stream_command,) 1434 1435 if not args: args = ('',) 1436 1437 host = args[0] 1438 1439 USER = getpass.getuser() 1440 PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost")) 1441 1442 test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER, 'lf':'\n'} 1443 test_seq1 = ( 1444 ('login', (USER, PASSWD)), 1445 ('create', ('/tmp/xxx 1',)), 1446 ('rename', ('/tmp/xxx 1', '/tmp/yyy')), 1447 ('CREATE', ('/tmp/yyz 2',)), 1448 ('append', ('/tmp/yyz 2', None, None, test_mesg)), 1449 ('list', ('/tmp', 'yy*')), 1450 ('select', ('/tmp/yyz 2',)), 1451 ('search', (None, 'SUBJECT', 'test')), 1452 ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')), 1453 ('store', ('1', 'FLAGS', '(\Deleted)')), 1454 ('namespace', ()), 1455 ('expunge', ()), 1456 ('recent', ()), 1457 ('close', ()), 1458 ) 1459 1460 test_seq2 = ( 1461 ('select', ()), 1462 ('response',('UIDVALIDITY',)), 1463 ('uid', ('SEARCH', 'ALL')), 1464 ('response', ('EXISTS',)), 1465 ('append', (None, None, None, test_mesg)), 1466 ('recent', ()), 1467 ('logout', ()), 1468 ) 1469 1470 def run(cmd, args): 1471 M._mesg('%s %s' % (cmd, args)) 1472 typ, dat = getattr(M, cmd)(*args) 1473 M._mesg('%s => %s %s' % (cmd, typ, dat)) 1474 if typ == 'NO': raise dat[0] 1475 return dat 1476 1477 try: 1478 if stream_command: 1479 M = IMAP4_stream(stream_command) 1480 else: 1481 M = IMAP4(host) 1482 if M.state == 'AUTH': 1483 test_seq1 = test_seq1[1:] # Login not needed 1484 M._mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION) 1485 M._mesg('CAPABILITIES = %r' % (M.capabilities,)) 1486 1487 for cmd,args in test_seq1: 1488 run(cmd, args) 1489 1490 for ml in run('list', ('/tmp/', 'yy%')): 1491 mo = re.match(r'.*"([^"]+)"$', ml) 1492 if mo: path = mo.group(1) 1493 else: path = ml.split()[-1] 1494 run('delete', (path,)) 1495 1496 for cmd,args in test_seq2: 1497 dat = run(cmd, args) 1498 1499 if (cmd,args) != ('uid', ('SEARCH', 'ALL')): 1500 continue 1501 1502 uid = dat[-1].split() 1503 if not uid: continue 1504 run('uid', ('FETCH', '%s' % uid[-1], 1505 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)')) 1506 1507 print '\nAll tests OK.' 1508 1509 except: 1510 print '\nTests failed.' 1511 1512 if not Debug: 1513 print ''' 1514If you would like to see debugging output, 1515try: %s -d5 1516''' % sys.argv[0] 1517 1518 raise 1519