Module imaplib
[hide private]
[frames] | no frames]

Source Code for Module imaplib

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