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