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