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 read(self, size): 1186 """Read 'size' bytes from remote.""" 1187 return self.file.read(size) 1188 1189 1190 def readline(self): 1191 """Read line from remote.""" 1192 return self.file.readline() 1193 1194 1195 def send(self, data): 1196 """Send data to remote.""" 1197 bytes = len(data) 1198 while bytes > 0: 1199 sent = self.sslobj.write(data) 1200 if sent == bytes: 1201 break # avoid copy 1202 data = data[sent:] 1203 bytes = bytes - sent 1204 1205 1206 def shutdown(self): 1207 """Close I/O established in "open".""" 1208 self.file.close() 1209 self.sock.close() 1210 1211 1212 def socket(self): 1213 """Return socket instance used to connect to IMAP4 server. 1214 1215 socket = <instance>.socket() 1216 """ 1217 return self.sock 1218 1219 1220 def ssl(self): 1221 """Return SSLObject instance used to communicate with the IMAP4 server. 1222 1223 ssl = ssl.wrap_socket(<instance>.socket) 1224 """ 1225 return self.sslobj 1226 1227 __all__.append("IMAP4_SSL") 1228 1229 1230class IMAP4_stream(IMAP4): 1231 1232 """IMAP4 client class over a stream 1233 1234 Instantiate with: IMAP4_stream(command) 1235 1236 where "command" is a string that can be passed to subprocess.Popen() 1237 1238 for more documentation see the docstring of the parent class IMAP4. 1239 """ 1240 1241 1242 def __init__(self, command): 1243 self.command = command 1244 IMAP4.__init__(self) 1245 1246 1247 def open(self, host = None, port = None): 1248 """Setup a stream connection. 1249 This connection will be used by the routines: 1250 read, readline, send, shutdown. 1251 """ 1252 self.host = None # For compatibility with parent class 1253 self.port = None 1254 self.sock = None 1255 self.file = None 1256 self.process = subprocess.Popen(self.command, 1257 stdin=subprocess.PIPE, stdout=subprocess.PIPE, 1258 shell=True, close_fds=True) 1259 self.writefile = self.process.stdin 1260 self.readfile = self.process.stdout 1261 1262 1263 def read(self, size): 1264 """Read 'size' bytes from remote.""" 1265 return self.readfile.read(size) 1266 1267 1268 def readline(self): 1269 """Read line from remote.""" 1270 return self.readfile.readline() 1271 1272 1273 def send(self, data): 1274 """Send data to remote.""" 1275 self.writefile.write(data) 1276 self.writefile.flush() 1277 1278 1279 def shutdown(self): 1280 """Close I/O established in "open".""" 1281 self.readfile.close() 1282 self.writefile.close() 1283 self.process.wait() 1284 1285 1286 1287class _Authenticator: 1288 1289 """Private class to provide en/decoding 1290 for base64-based authentication conversation. 1291 """ 1292 1293 def __init__(self, mechinst): 1294 self.mech = mechinst # Callable object to provide/process data 1295 1296 def process(self, data): 1297 ret = self.mech(self.decode(data)) 1298 if ret is None: 1299 return '*' # Abort conversation 1300 return self.encode(ret) 1301 1302 def encode(self, inp): 1303 # 1304 # Invoke binascii.b2a_base64 iteratively with 1305 # short even length buffers, strip the trailing 1306 # line feed from the result and append. "Even" 1307 # means a number that factors to both 6 and 8, 1308 # so when it gets to the end of the 8-bit input 1309 # there's no partial 6-bit output. 1310 # 1311 oup = '' 1312 while inp: 1313 if len(inp) > 48: 1314 t = inp[:48] 1315 inp = inp[48:] 1316 else: 1317 t = inp 1318 inp = '' 1319 e = binascii.b2a_base64(t) 1320 if e: 1321 oup = oup + e[:-1] 1322 return oup 1323 1324 def decode(self, inp): 1325 if not inp: 1326 return '' 1327 return binascii.a2b_base64(inp) 1328 1329 1330 1331Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6, 1332 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12} 1333 1334def Internaldate2tuple(resp): 1335 """Parse an IMAP4 INTERNALDATE string. 1336 1337 Return corresponding local time. The return value is a 1338 time.struct_time instance or None if the string has wrong format. 1339 """ 1340 1341 mo = InternalDate.match(resp) 1342 if not mo: 1343 return None 1344 1345 mon = Mon2num[mo.group('mon')] 1346 zonen = mo.group('zonen') 1347 1348 day = int(mo.group('day')) 1349 year = int(mo.group('year')) 1350 hour = int(mo.group('hour')) 1351 min = int(mo.group('min')) 1352 sec = int(mo.group('sec')) 1353 zoneh = int(mo.group('zoneh')) 1354 zonem = int(mo.group('zonem')) 1355 1356 # INTERNALDATE timezone must be subtracted to get UT 1357 1358 zone = (zoneh*60 + zonem)*60 1359 if zonen == '-': 1360 zone = -zone 1361 1362 tt = (year, mon, day, hour, min, sec, -1, -1, -1) 1363 1364 utc = time.mktime(tt) 1365 1366 # Following is necessary because the time module has no 'mkgmtime'. 1367 # 'mktime' assumes arg in local timezone, so adds timezone/altzone. 1368 1369 lt = time.localtime(utc) 1370 if time.daylight and lt[-1]: 1371 zone = zone + time.altzone 1372 else: 1373 zone = zone + time.timezone 1374 1375 return time.localtime(utc - zone) 1376 1377 1378 1379def Int2AP(num): 1380 1381 """Convert integer to A-P string representation.""" 1382 1383 val = ''; AP = 'ABCDEFGHIJKLMNOP' 1384 num = int(abs(num)) 1385 while num: 1386 num, mod = divmod(num, 16) 1387 val = AP[mod] + val 1388 return val 1389 1390 1391 1392def ParseFlags(resp): 1393 1394 """Convert IMAP4 flags response to python tuple.""" 1395 1396 mo = Flags.match(resp) 1397 if not mo: 1398 return () 1399 1400 return tuple(mo.group('flags').split()) 1401 1402 1403def Time2Internaldate(date_time): 1404 1405 """Convert date_time to IMAP4 INTERNALDATE representation. 1406 1407 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'. The 1408 date_time argument can be a number (int or float) representing 1409 seconds since epoch (as returned by time.time()), a 9-tuple 1410 representing local time (as returned by time.localtime()), or a 1411 double-quoted string. In the last case, it is assumed to already 1412 be in the correct format. 1413 """ 1414 1415 if isinstance(date_time, (int, long, float)): 1416 tt = time.localtime(date_time) 1417 elif isinstance(date_time, (tuple, time.struct_time)): 1418 tt = date_time 1419 elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'): 1420 return date_time # Assume in correct format 1421 else: 1422 raise ValueError("date_time not of a known type") 1423 1424 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt) 1425 if dt[0] == '0': 1426 dt = ' ' + dt[1:] 1427 if time.daylight and tt[-1]: 1428 zone = -time.altzone 1429 else: 1430 zone = -time.timezone 1431 return '"' + dt + " %+03d%02d" % divmod(zone//60, 60) + '"' 1432 1433 1434 1435if __name__ == '__main__': 1436 1437 # To test: invoke either as 'python imaplib.py [IMAP4_server_hostname]' 1438 # or 'python imaplib.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"' 1439 # to test the IMAP4_stream class 1440 1441 import getopt, getpass 1442 1443 try: 1444 optlist, args = getopt.getopt(sys.argv[1:], 'd:s:') 1445 except getopt.error, val: 1446 optlist, args = (), () 1447 1448 stream_command = None 1449 for opt,val in optlist: 1450 if opt == '-d': 1451 Debug = int(val) 1452 elif opt == '-s': 1453 stream_command = val 1454 if not args: args = (stream_command,) 1455 1456 if not args: args = ('',) 1457 1458 host = args[0] 1459 1460 USER = getpass.getuser() 1461 PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost")) 1462 1463 test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER, 'lf':'\n'} 1464 test_seq1 = ( 1465 ('login', (USER, PASSWD)), 1466 ('create', ('/tmp/xxx 1',)), 1467 ('rename', ('/tmp/xxx 1', '/tmp/yyy')), 1468 ('CREATE', ('/tmp/yyz 2',)), 1469 ('append', ('/tmp/yyz 2', None, None, test_mesg)), 1470 ('list', ('/tmp', 'yy*')), 1471 ('select', ('/tmp/yyz 2',)), 1472 ('search', (None, 'SUBJECT', 'test')), 1473 ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')), 1474 ('store', ('1', 'FLAGS', '(\Deleted)')), 1475 ('namespace', ()), 1476 ('expunge', ()), 1477 ('recent', ()), 1478 ('close', ()), 1479 ) 1480 1481 test_seq2 = ( 1482 ('select', ()), 1483 ('response',('UIDVALIDITY',)), 1484 ('uid', ('SEARCH', 'ALL')), 1485 ('response', ('EXISTS',)), 1486 ('append', (None, None, None, test_mesg)), 1487 ('recent', ()), 1488 ('logout', ()), 1489 ) 1490 1491 def run(cmd, args): 1492 M._mesg('%s %s' % (cmd, args)) 1493 typ, dat = getattr(M, cmd)(*args) 1494 M._mesg('%s => %s %s' % (cmd, typ, dat)) 1495 if typ == 'NO': raise dat[0] 1496 return dat 1497 1498 try: 1499 if stream_command: 1500 M = IMAP4_stream(stream_command) 1501 else: 1502 M = IMAP4(host) 1503 if M.state == 'AUTH': 1504 test_seq1 = test_seq1[1:] # Login not needed 1505 M._mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION) 1506 M._mesg('CAPABILITIES = %r' % (M.capabilities,)) 1507 1508 for cmd,args in test_seq1: 1509 run(cmd, args) 1510 1511 for ml in run('list', ('/tmp/', 'yy%')): 1512 mo = re.match(r'.*"([^"]+)"$', ml) 1513 if mo: path = mo.group(1) 1514 else: path = ml.split()[-1] 1515 run('delete', (path,)) 1516 1517 for cmd,args in test_seq2: 1518 dat = run(cmd, args) 1519 1520 if (cmd,args) != ('uid', ('SEARCH', 'ALL')): 1521 continue 1522 1523 uid = dat[-1].split() 1524 if not uid: continue 1525 run('uid', ('FETCH', '%s' % uid[-1], 1526 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)')) 1527 1528 print '\nAll tests OK.' 1529 1530 except: 1531 print '\nTests failed.' 1532 1533 if not Debug: 1534 print ''' 1535If you would like to see debugging output, 1536try: %s -d5 1537''' % sys.argv[0] 1538 1539 raise 1540