Package ProcImap :: Module imaplib2
[hide private]
[frames] | no frames]

Source Code for Module ProcImap.imaplib2

   1  #!/usr/bin/env python2.5 
   2   
   3  """Threaded IMAP4 client. 
   4   
   5  Based on RFC 2060 and original imaplib module. 
   6   
   7  Public classes:   IMAP4 
   8                    IMAP4_SSL 
   9                    IMAP4_stream 
  10   
  11  Public functions: Internaldate2Time 
  12                    ParseFlags 
  13                    Time2Internaldate 
  14  """ 
  15   
  16   
  17  __all__ = ("IMAP4", "IMAP4_SSL", "IMAP4_stream" 
  18             "Internaldate2Time", "ParseFlags", "Time2Internaldate") 
  19   
  20  __version__ = "2.4" 
  21  __release__ = "2" 
  22  __revision__ = "4" 
  23  __credits__ = """ 
  24  Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998. 
  25  String method conversion by ESR, February 2001. 
  26  GET/SETACL contributed by Anthony Baxter <anthony@interlink.com.au> April 2001. 
  27  IMAP4_SSL contributed by Tino Lange <Tino.Lange@isg.de> March 2002. 
  28  GET/SETQUOTA contributed by Andreas Zeidler <az@kreativkombinat.de> June 2002. 
  29  PROXYAUTH contributed by Rick Holbert <holbert.13@osu.edu> November 2002. 
  30  IDLE via threads suggested by Philippe Normand <phil@respyre.org> January 2005. 
  31  GET/SETANNOTATION contributed by Tomas Lindroos <skitta@abo.fi> June 2005. 
  32  New socket open code from http://www.python.org/doc/lib/socket-example.html.""" 
  33  __author__ = "Piers Lauder <piers@janeelix.com>" 
  34   
  35  import binascii, os, Queue, random, re, select, socket, sys, time, threading 
  36   
  37  select_module = select 
  38   
  39  #       Globals 
  40   
  41  CRLF = '\r\n' 
  42  Debug = None                                    # Backward compatibility 
  43  IMAP4_PORT = 143 
  44  IMAP4_SSL_PORT = 993 
  45   
  46  IDLE_TIMEOUT_RESPONSE = '* IDLE TIMEOUT' 
  47  IDLE_TIMEOUT = 60*29                            # Don't stay in IDLE state longer 
  48   
  49  AllowedVersions = ('IMAP4REV1', 'IMAP4')        # Most recent first 
  50   
  51  #       Commands 
  52   
  53  CMD_VAL_STATES = 0 
  54  CMD_VAL_ASYNC = 1 
  55  NONAUTH, AUTH, SELECTED, LOGOUT = 'NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT' 
  56   
  57  Commands = { 
  58          # name            valid states             asynchronous 
  59          'APPEND':       ((AUTH, SELECTED),            False), 
  60          'AUTHENTICATE': ((NONAUTH,),                  False), 
  61          'CAPABILITY':   ((NONAUTH, AUTH, SELECTED),   True), 
  62          'CHECK':        ((SELECTED,),                 True), 
  63          'CLOSE':        ((SELECTED,),                 False), 
  64          'COPY':         ((SELECTED,),                 True), 
  65          'CREATE':       ((AUTH, SELECTED),            True), 
  66          'DELETE':       ((AUTH, SELECTED),            True), 
  67          'DELETEACL':    ((AUTH, SELECTED),            True), 
  68          'EXAMINE':      ((AUTH, SELECTED),            False), 
  69          'EXPUNGE':      ((SELECTED,),                 True), 
  70          'FETCH':        ((SELECTED,),                 True), 
  71          'GETACL':       ((AUTH, SELECTED),            True), 
  72          'GETANNOTATION':((AUTH, SELECTED),            True), 
  73          'GETQUOTA':     ((AUTH, SELECTED),            True), 
  74          'GETQUOTAROOT': ((AUTH, SELECTED),            True), 
  75          'IDLE':         ((SELECTED,),                 False), 
  76          'LIST':         ((AUTH, SELECTED),            True), 
  77          'LOGIN':        ((NONAUTH,),                  False), 
  78          'LOGOUT':       ((NONAUTH, AUTH, LOGOUT, SELECTED),   False), 
  79          'LSUB':         ((AUTH, SELECTED),            True), 
  80          'MYRIGHTS':     ((AUTH, SELECTED),            True), 
  81          'NAMESPACE':    ((AUTH, SELECTED),            True), 
  82          'NOOP':         ((NONAUTH, AUTH, SELECTED),   True), 
  83          'PARTIAL':      ((SELECTED,),                 True), 
  84          'PROXYAUTH':    ((AUTH,),                     False), 
  85          'RENAME':       ((AUTH, SELECTED),            True), 
  86          'SEARCH':       ((SELECTED,),                 True), 
  87          'SELECT':       ((AUTH, SELECTED),            False), 
  88          'SETACL':       ((AUTH, SELECTED),            False), 
  89          'SETANNOTATION':((AUTH, SELECTED),            True), 
  90          'SETQUOTA':     ((AUTH, SELECTED),            False), 
  91          'SORT':         ((SELECTED,),                 True), 
  92          'STATUS':       ((AUTH, SELECTED),            True), 
  93          'STORE':        ((SELECTED,),                 True), 
  94          'SUBSCRIBE':    ((AUTH, SELECTED),            False), 
  95          'THREAD':       ((SELECTED,),                 True), 
  96          'UID':          ((SELECTED,),                 True), 
  97          'UNSUBSCRIBE':  ((AUTH, SELECTED),            False), 
  98          } 
  99   
 100   
101 -def Int2AP(num):
102 103 """string = Int2AP(num) 104 Return 'num' converted to a string using characters from the set 'A'..'P' 105 """ 106 107 val, a2p = [], 'ABCDEFGHIJKLMNOP' 108 num = int(abs(num)) 109 while num: 110 num, mod = divmod(num, 16) 111 val.insert(0, a2p[mod]) 112 return ''.join(val)
113 114 115
116 -class Request(object):
117 118 """Private class to represent a request awaiting response.""" 119
120 - def __init__(self, parent, name=None, callback=None, cb_arg=None):
121 self.name = name 122 self.callback = callback # Function called to process result 123 self.callback_arg = cb_arg # Optional arg passed to "callback" 124 125 self.tag = '%s%s' % (parent.tagpre, parent.tagnum) 126 parent.tagnum += 1 127 128 self.ready = threading.Event() 129 self.response = None 130 self.aborted = None 131 self.data = None
132 133
134 - def abort(self, typ, val):
135 self.aborted = (typ, val) 136 self.deliver(None)
137 138
139 - def get_response(self, exc_fmt=None):
140 self.callback = None 141 self.ready.wait() 142 143 if self.aborted is not None: 144 typ, val = self.aborted 145 if exc_fmt is None: 146 exc_fmt = '%s - %%s' % typ 147 raise typ(exc_fmt % str(val)) 148 149 return self.response
150 151
152 - def deliver(self, response):
153 if self.callback is not None: 154 self.callback((response, self.callback_arg, self.aborted)) 155 return 156 157 self.response = response 158 self.ready.set()
159 160 161 162
163 -class IMAP4(object):
164 165 """Threaded IMAP4 client class. 166 167 Instantiate with: 168 IMAP4(host=None, port=None, debug=None, debug_file=None) 169 170 host - host's name (default: localhost); 171 port - port number (default: standard IMAP4 port); 172 debug - debug level (default: 0 - no debug); 173 debug_file - debug stream (default: sys.stderr). 174 175 All IMAP4rev1 commands are supported by methods of the same name. 176 177 Each command returns a tuple: (type, [data, ...]) where 'type' 178 is usually 'OK' or 'NO', and 'data' is either the text from the 179 tagged response, or untagged results from command. Each 'data' is 180 either a string, or a tuple. If a tuple, then the first part is the 181 header of the response, and the second part contains the data (ie: 182 'literal' value). 183 184 Errors raise the exception class <instance>.error("<reason>"). 185 IMAP4 server errors raise <instance>.abort("<reason>"), which is 186 a sub-class of 'error'. Mailbox status changes from READ-WRITE to 187 READ-ONLY raise the exception class <instance>.readonly("<reason>"), 188 which is a sub-class of 'abort'. 189 190 "error" exceptions imply a program error. 191 "abort" exceptions imply the connection should be reset, and 192 the command re-tried. 193 "readonly" exceptions imply the command should be re-tried. 194 195 All commands take two optional named arguments: 196 'callback' and 'cb_arg' 197 If 'callback' is provided then the command is asynchronous, so after 198 the command is queued for transmission, the call returns immediately 199 with the tuple (None, None). 200 The result will be posted by invoking "callback" with one arg, a tuple: 201 callback((result, cb_arg, None)) 202 or, if there was a problem: 203 callback((None, cb_arg, (exception class, reason))) 204 205 Otherwise the command is synchronous (waits for result). But note 206 that state-changing commands will both block until previous commands 207 have completed, and block subsequent commands until they have finished. 208 209 All (non-callback) arguments to commands are converted to strings, 210 except for AUTHENTICATE, and the last argument to APPEND which is 211 passed as an IMAP4 literal. If necessary (the string contains any 212 non-printing characters or white-space and isn't enclosed with either 213 parentheses or double quotes) each string is quoted. However, the 214 'password' argument to the LOGIN command is always quoted. If you 215 want to avoid having an argument string quoted (eg: the 'flags' 216 argument to STORE) then enclose the string in parentheses (eg: 217 "(\Deleted)"). 218 219 There is one instance variable, 'state', that is useful for tracking 220 whether the client needs to login to the server. If it has the 221 value "AUTH" after instantiating the class, then the connection 222 is pre-authenticated (otherwise it will be "NONAUTH"). Selecting a 223 mailbox changes the state to be "SELECTED", closing a mailbox changes 224 back to "AUTH", and once the client has logged out, the state changes 225 to "LOGOUT" and no further commands may be issued. 226 227 Note: to use this module, you must read the RFCs pertaining to the 228 IMAP4 protocol, as the semantics of the arguments to each IMAP4 229 command are left to the invoker, not to mention the results. Also, 230 most IMAP servers implement a sub-set of the commands available here. 231 232 Note also that you must call logout() to shut down threads before 233 discarding an instance. 234 """ 235
236 - class error(Exception): pass # Logical errors - debug required
237 - class abort(error): pass # Service errors - close and retry
238 - class readonly(abort): pass # Mailbox status changed to READ-ONLY
239 240 241 continuation_cre = re.compile(r'\+( (?P<data>.*))?') 242 literal_cre = re.compile(r'.*{(?P<size>\d+)}$') 243 mapCRLF_cre = re.compile(r'\r\n|\r|\n') 244 mustquote_cre = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]") 245 response_code_cre = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]') 246 untagged_response_cre = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?') 247 untagged_status_cre = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?') 248 249
250 - def __init__(self, host=None, port=None, debug=None, debug_file=None):
251 252 self.state = NONAUTH # IMAP4 protocol state 253 self.literal = None # A literal argument to a command 254 self.tagged_commands = {} # Tagged commands awaiting response 255 self.untagged_responses = {} # {typ: [data, ...], ...} 256 self.is_readonly = False # READ-ONLY desired state 257 self.idle_rqb = None # Server IDLE Request - see _IdleCont 258 self.idle_timeout = None # Must prod server occasionally 259 260 self._expecting_data = 0 # Expecting message data 261 self._accumulated_data = [] # Message data accumulated so far 262 self._literal_expected = None # Message data descriptor 263 264 # Create unique tag for this session, 265 # and compile tagged response matcher. 266 267 self.tagnum = 0 268 self.tagpre = Int2AP(random.randint(4096, 65535)) 269 self.tagre = re.compile(r'(?P<tag>' 270 + self.tagpre 271 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)') 272 273 if __debug__: self._init_debug(debug, debug_file) 274 275 # Open socket to server. 276 277 self.open(host, port) 278 279 if __debug__: 280 if debug: 281 self._mesg('connected to %s on port %s' % (self.host, self.port)) 282 283 # Threading 284 285 self.Terminate = False 286 287 self.state_change_free = threading.Event() 288 self.state_change_pending = threading.Lock() 289 self.commands_lock = threading.Lock() 290 291 self.ouq = Queue.Queue(10) 292 self.inq = Queue.Queue() 293 294 self.wrth = threading.Thread(target=self._writer) 295 self.wrth.start() 296 self.rdth = threading.Thread(target=self._reader) 297 self.rdth.start() 298 self.inth = threading.Thread(target=self._handler) 299 self.inth.start() 300 301 # Get server welcome message, 302 # request and store CAPABILITY response. 303 304 try: 305 self.welcome = self._request_push(tag='continuation').get_response('IMAP4 protocol error: %s')[1] 306 307 if 'PREAUTH' in self.untagged_responses: 308 self.state = AUTH 309 if __debug__: self._log(1, 'state => AUTH') 310 elif 'OK' in self.untagged_responses: 311 if __debug__: self._log(1, 'state => NONAUTH') 312 else: 313 raise self.error(self.welcome) 314 315 typ, dat = self.capability() 316 if dat == [None]: 317 raise self.error('no CAPABILITY response from server') 318 self.capabilities = tuple(dat[-1].upper().split()) 319 if __debug__: self._log(3, 'CAPABILITY: %r' % (self.capabilities,)) 320 321 for version in AllowedVersions: 322 if not version in self.capabilities: 323 continue 324 self.PROTOCOL_VERSION = version 325 break 326 else: 327 raise self.error('server not IMAP4 compliant') 328 except: 329 self._close_threads() 330 raise
331 332
333 - def __getattr__(self, attr):
334 # Allow UPPERCASE variants of IMAP4 command methods. 335 if attr in Commands: 336 return getattr(self, attr.lower()) 337 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
338 339 340 341 # Overridable methods 342 343
344 - def open(self, host=None, port=None):
345 """open(host=None, port=None) 346 Setup connection to remote server on "host:port" 347 (default: localhost:standard IMAP4 port). 348 This connection will be used by the routines: 349 read, send, shutdown, socket.""" 350 351 self.host = host is not None and host or '' 352 self.port = port is not None and port or IMAP4_PORT 353 self.sock = self.open_socket() 354 self.read_fd = self.sock.fileno()
355 356
357 - def open_socket(self):
358 """Open socket choosing first address family available.""" 359 360 msg = (-1, 'could not open socket') 361 for res in socket.getaddrinfo(self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM): 362 af, socktype, proto, canonname, sa = res 363 try: 364 s = socket.socket(af, socktype, proto) 365 except socket.error, msg: 366 continue 367 try: 368 s.connect(sa) 369 except socket.error, msg: 370 s.close() 371 continue 372 break 373 else: 374 raise socket.error(msg) 375 376 return s
377 378
379 - def read(self, size):
380 """data = read(size) 381 Read at most 'size' bytes from remote.""" 382 383 return self.sock.recv(size)
384 385
386 - def send(self, data):
387 """send(data) 388 Send 'data' to remote.""" 389 390 self.sock.sendall(data)
391 392
393 - def shutdown(self):
394 """shutdown() 395 Close I/O established in "open".""" 396 397 self.sock.close()
398 399
400 - def socket(self):
401 """socket = socket() 402 Return socket instance used to connect to IMAP4 server.""" 403 404 return self.sock
405 406 407 408 # Utility methods 409 410
411 - def recent(self, **kw):
412 """(typ, [data]) = recent() 413 Return most recent 'RECENT' responses if any exist, 414 else prompt server for an update using the 'NOOP' command. 415 'data' is None if no new messages, 416 else list of RECENT responses, most recent last.""" 417 418 name = 'RECENT' 419 typ, dat = self._untagged_response('OK', [None], name) 420 if dat[-1]: 421 return self._deliver_dat(typ, dat, kw) 422 kw['untagged_response'] = name 423 return self.noop(**kw) # Prod server for response
424 425
426 - def response(self, code, **kw):
427 """(code, [data]) = response(code) 428 Return data for response 'code' if received, or None. 429 Old value for response 'code' is cleared.""" 430 431 typ, dat = self._untagged_response(code, [None], code.upper()) 432 return self._deliver_dat(typ, dat, kw)
433 434 435 436 437 # IMAP4 commands 438 439
440 - def append(self, mailbox, flags, date_time, message, **kw):
441 """(typ, [data]) = append(mailbox, flags, date_time, message) 442 Append message to named mailbox. 443 All args except `message' can be None.""" 444 445 name = 'APPEND' 446 if not mailbox: 447 mailbox = 'INBOX' 448 if flags: 449 if (flags[0],flags[-1]) != ('(',')'): 450 flags = '(%s)' % flags 451 else: 452 flags = None 453 if date_time: 454 date_time = Time2Internaldate(date_time) 455 else: 456 date_time = None 457 self.literal = self.mapCRLF_cre.sub(CRLF, message) 458 try: 459 return self._simple_command(name, mailbox, flags, date_time, **kw) 460 finally: 461 self.state_change_pending.release()
462 463
464 - def authenticate(self, mechanism, authobject, **kw):
465 """(typ, [data]) = authenticate(mechanism, authobject) 466 Authenticate command - requires response processing. 467 468 'mechanism' specifies which authentication mechanism is to 469 be used - it must appear in <instance>.capabilities in the 470 form AUTH=<mechanism>. 471 472 'authobject' must be a callable object: 473 474 data = authobject(response) 475 476 It will be called to process server continuation responses. 477 It should return data that will be encoded and sent to server. 478 It should return None if the client abort response '*' should 479 be sent instead.""" 480 481 self.literal = _Authenticator(authobject).process 482 try: 483 typ, dat = self._simple_command('AUTHENTICATE', mechanism.upper()) 484 if typ != 'OK': 485 self._deliver_exc(self.error, dat[-1]) 486 self.state = AUTH 487 if __debug__: self._log(1, 'state => AUTH') 488 finally: 489 self.state_change_pending.release() 490 return self._deliver_dat(typ, dat, kw)
491 492
493 - def capability(self, **kw):
494 """(typ, [data]) = capability() 495 Fetch capabilities list from server.""" 496 497 name = 'CAPABILITY' 498 kw['untagged_response'] = name 499 return self._simple_command(name, **kw)
500 501
502 - def check(self, **kw):
503 """(typ, [data]) = check() 504 Checkpoint mailbox on server.""" 505 506 return self._simple_command('CHECK', **kw)
507 508
509 - def close(self, **kw):
510 """(typ, [data]) = close() 511 Close currently selected mailbox. 512 513 Deleted messages are removed from writable mailbox. 514 This is the recommended command before 'LOGOUT'.""" 515 516 if self.state != 'SELECTED': 517 raise self.error('No mailbox selected.') 518 try: 519 typ, dat = self._simple_command('CLOSE') 520 finally: 521 self.state = AUTH 522 if __debug__: self._log(1, 'state => AUTH') 523 self.state_change_pending.release() 524 return self._deliver_dat(typ, dat, kw)
525 526
527 - def copy(self, message_set, new_mailbox, **kw):
528 """(typ, [data]) = copy(message_set, new_mailbox) 529 Copy 'message_set' messages onto end of 'new_mailbox'.""" 530 531 return self._simple_command('COPY', message_set, new_mailbox, **kw)
532 533
534 - def create(self, mailbox, **kw):
535 """(typ, [data]) = create(mailbox) 536 Create new mailbox.""" 537 538 return self._simple_command('CREATE', mailbox, **kw)
539 540
541 - def delete(self, mailbox, **kw):
542 """(typ, [data]) = delete(mailbox) 543 Delete old mailbox.""" 544 545 return self._simple_command('DELETE', mailbox, **kw)
546 547
548 - def deleteacl(self, mailbox, who, **kw):
549 """(typ, [data]) = deleteacl(mailbox, who) 550 Delete the ACLs (remove any rights) set for who on mailbox.""" 551 552 return self._simple_command('DELETEACL', mailbox, who, **kw)
553 554
555 - def examine(self, mailbox='INBOX', **kw):
556 """(typ, [data]) = examine(mailbox='INBOX', readonly=False) 557 Select a mailbox for READ-ONLY access. (Flushes all untagged responses.) 558 'data' is count of messages in mailbox ('EXISTS' response). 559 Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so 560 other responses should be obtained via "response('FLAGS')" etc.""" 561 562 return self.select(mailbox=mailbox, readonly=True, **kw)
563 564
565 - def expunge(self, **kw):
566 """(typ, [data]) = expunge() 567 Permanently remove deleted items from selected mailbox. 568 Generates 'EXPUNGE' response for each deleted message. 569 'data' is list of 'EXPUNGE'd message numbers in order received.""" 570 571 name = 'EXPUNGE' 572 kw['untagged_response'] = name 573 return self._simple_command(name, **kw)
574 575
576 - def fetch(self, message_set, message_parts, **kw):
577 """(typ, [data, ...]) = fetch(message_set, message_parts) 578 Fetch (parts of) messages. 579 'message_parts' should be a string of selected parts 580 enclosed in parentheses, eg: "(UID BODY[TEXT])". 581 'data' are tuples of message part envelope and data, 582 followed by a string containing the trailer.""" 583 584 name = 'FETCH' 585 kw['untagged_response'] = name 586 return self._simple_command(name, message_set, message_parts, **kw)
587 588
589 - def getacl(self, mailbox, **kw):
590 """(typ, [data]) = getacl(mailbox) 591 Get the ACLs for a mailbox.""" 592 593 kw['untagged_response'] = 'ACL' 594 return self._simple_command('GETACL', mailbox, **kw)
595 596
597 - def getannotation(self, mailbox, entry, attribute, **kw):
598 """(typ, [data]) = getannotation(mailbox, entry, attribute) 599 Retrieve ANNOTATIONs.""" 600 601 kw['untagged_response'] = 'ANNOTATION' 602 return self._simple_command('GETANNOTATION', mailbox, entry, attribute, **kw)
603 604
605 - def getquota(self, root, **kw):
606 """(typ, [data]) = getquota(root) 607 Get the quota root's resource usage and limits. 608 (Part of the IMAP4 QUOTA extension defined in rfc2087.)""" 609 610 kw['untagged_response'] = 'QUOTA' 611 return self._simple_command('GETQUOTA', root, **kw)
612 613
614 - def getquotaroot(self, mailbox, **kw):
615 # Hmmm, this is non-std! Left for backwards-compatibility, sigh. 616 # NB: usage should have been defined as: 617 # (typ, [QUOTAROOT responses...]) = getquotaroot(mailbox) 618 # (typ, [QUOTA responses...]) = response('QUOTA') 619 """(typ, [[QUOTAROOT responses...], [QUOTA responses...]]) = getquotaroot(mailbox) 620 Get the list of quota roots for the named mailbox.""" 621 622 typ, dat = self._simple_command('GETQUOTAROOT', mailbox) 623 typ, quota = self._untagged_response(typ, dat, 'QUOTA') 624 typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT') 625 return self._deliver_dat(typ, [quotaroot, quota], kw)
626 627
628 - def idle(self, timeout=None, **kw):
629 """"(typ, [data]) = idle(timeout=None) 630 Put server into IDLE mode until server notifies some change, 631 or 'timeout' (secs) occurs (default: 29 minutes), 632 or another IMAP4 command is scheduled.""" 633 634 name = 'IDLE' 635 self.literal = _IdleCont(self, timeout).process 636 try: 637 return self._simple_command(name, **kw) 638 finally: 639 self.state_change_pending.release()
640 641
642 - def list(self, directory='""', pattern='*', **kw):
643 """(typ, [data]) = list(directory='""', pattern='*') 644 List mailbox names in directory matching pattern. 645 'data' is list of LIST responses. 646 647 NB: for 'pattern': 648 % matches all except separator ( so LIST "" "%" returns names at root) 649 * matches all (so LIST "" "*" returns whole directory tree from root)""" 650 651 name = 'LIST' 652 kw['untagged_response'] = name 653 return self._simple_command(name, directory, pattern, **kw)
654 655
656 - def login(self, user, password, **kw):
657 """(typ, [data]) = login(user, password) 658 Identify client using plaintext password. 659 NB: 'password' will be quoted.""" 660 661 try: 662 typ, dat = self._simple_command('LOGIN', user, self._quote(password)) 663 if typ != 'OK': 664 self._deliver_exc(self.error, dat[-1], kw) 665 self.state = AUTH 666 if __debug__: self._log(1, 'state => AUTH') 667 finally: 668 self.state_change_pending.release() 669 return self._deliver_dat(typ, dat, kw)
670 671
672 - def login_cram_md5(self, user, password, **kw):
673 """(typ, [data]) = login_cram_md5(user, password) 674 Force use of CRAM-MD5 authentication.""" 675 676 self.user, self.password = user, password 677 return self.authenticate('CRAM-MD5', self._CRAM_MD5_AUTH, **kw)
678 679
680 - def _CRAM_MD5_AUTH(self, challenge):
681 """Authobject to use with CRAM-MD5 authentication.""" 682 import hmac 683 return self.user + " " + hmac.HMAC(self.password, challenge).hexdigest()
684 685
686 - def logout(self, **kw):
687 """(typ, [data]) = logout() 688 Shutdown connection to server. 689 Returns server 'BYE' response.""" 690 691 self.state = LOGOUT 692 if __debug__: self._log(1, 'state => LOGOUT') 693 694 try: 695 typ, dat = self._simple_command('LOGOUT') 696 except: 697 typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]] 698 if __debug__: self._log(1, dat) 699 700 self._close_threads() 701 702 self.state_change_pending.release() 703 704 if __debug__: self._log(1, 'connection closed') 705 706 bye = self.untagged_responses.get('BYE') 707 if bye: 708 typ, dat = 'BYE', bye 709 return self._deliver_dat(typ, dat, kw)
710 711
712 - def lsub(self, directory='""', pattern='*', **kw):
713 """(typ, [data, ...]) = lsub(directory='""', pattern='*') 714 List 'subscribed' mailbox names in directory matching pattern. 715 'data' are tuples of message part envelope and data.""" 716 717 name = 'LSUB' 718 kw['untagged_response'] = name 719 return self._simple_command(name, directory, pattern, **kw)
720 721
722 - def myrights(self, mailbox):
723 """(typ, [data]) = myrights(mailbox) 724 Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).""" 725 726 name = 'MYRIGHTS' 727 kw['untagged_response'] = name 728 return self._simple_command(name, mailbox, **kw)
729 730
731 - def namespace(self, **kw):
732 """(typ, [data, ...]) = namespace() 733 Returns IMAP namespaces ala rfc2342.""" 734 735 name = 'NAMESPACE' 736 kw['untagged_response'] = name 737 return self._simple_command(name, **kw)
738 739
740 - def noop(self, **kw):
741 """(typ, [data]) = noop() 742 Send NOOP command.""" 743 744 if __debug__: self._dump_ur(3) 745 return self._simple_command('NOOP', **kw)
746 747
748 - def partial(self, message_num, message_part, start, length, **kw):
749 """(typ, [data, ...]) = partial(message_num, message_part, start, length) 750 Fetch truncated part of a message. 751 'data' is tuple of message part envelope and data. 752 NB: obsolete.""" 753 754 name = 'PARTIAL' 755 kw['untagged_response'] = 'FETCH' 756 return self._simple_command(name, message_num, message_part, start, length, **kw)
757 758
759 - def proxyauth(self, user, **kw):
760 """(typ, [data]) = proxyauth(user) 761 Assume authentication as 'user'. 762 (Allows an authorised administrator to proxy into any user's mailbox.)""" 763 764 try: 765 return self._simple_command('PROXYAUTH', user, **kw) 766 finally: 767 self.state_change_pending.release()
768 769
770 - def rename(self, oldmailbox, newmailbox, **kw):
771 """(typ, [data]) = rename(oldmailbox, newmailbox) 772 Rename old mailbox name to new.""" 773 774 return self._simple_command('RENAME', oldmailbox, newmailbox, **kw)
775 776
777 - def search(self, charset, *criteria, **kw):
778 """(typ, [data]) = search(charset, criterion, ...) 779 Search mailbox for matching messages. 780 'data' is space separated list of matching message numbers.""" 781 782 name = 'SEARCH' 783 kw['untagged_response'] = name 784 if charset: 785 return self._simple_command(name, 'CHARSET', charset, *criteria, **kw) 786 return self._simple_command(name, *criteria, **kw)
787 788
789 - def select(self, mailbox='INBOX', readonly=False, **kw):
790 """(typ, [data]) = select(mailbox='INBOX', readonly=False) 791 Select a mailbox. (Flushes all untagged responses.) 792 'data' is count of messages in mailbox ('EXISTS' response). 793 Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so 794 other responses should be obtained via "response('FLAGS')" etc.""" 795 796 self.commands_lock.acquire() 797 self.untagged_responses = {} # Flush old responses. 798 self.commands_lock.release() 799 800 self.is_readonly = readonly and True or False 801 if readonly: 802 name = 'EXAMINE' 803 else: 804 name = 'SELECT' 805 try: 806 rqb = self._command(name, mailbox) 807 typ, dat = rqb.get_response('command: %s => %%s' % rqb.name) 808 if typ != 'OK': 809 if self.state == SELECTED: 810 self.state = AUTH 811 if __debug__: self._log(1, 'state => AUTH') 812 if typ == 'BAD': 813 self._deliver_exc(self.error, '%s command error: %s %s' % (name, typ, dat), kw) 814 return self._deliver_dat(typ, dat, kw) 815 self.state = SELECTED 816 if __debug__: self._log(1, 'state => SELECTED') 817 finally: 818 self.state_change_pending.release() 819 if 'READ-ONLY' in self.untagged_responses and not readonly: 820 if __debug__: self._dump_ur(1) 821 self._deliver_exc(self.readonly, '%s is not writable' % mailbox, kw) 822 return self._deliver_dat(typ, self.untagged_responses.get('EXISTS', [None]), kw)
823 824
825 - def setacl(self, mailbox, who, what, **kw):
826 """(typ, [data]) = setacl(mailbox, who, what) 827 Set a mailbox acl.""" 828 829 try: 830 return self._simple_command('SETACL', mailbox, who, what, **kw) 831 finally: 832 self.state_change_pending.release()
833 834
835 - def setannotation(self, *args, **kw):
836 """(typ, [data]) = setannotation(mailbox[, entry, attribute]+) 837 Set ANNOTATIONs.""" 838 839 kw['untagged_response'] = 'ANNOTATION' 840 return self._simple_command('SETANNOTATION', *args, **kw)
841 842
843 - def setquota(self, root, limits, **kw):
844 """(typ, [data]) = setquota(root, limits) 845 Set the quota root's resource limits.""" 846 847 kw['untagged_response'] = 'QUOTA' 848 try: 849 return self._simple_command('SETQUOTA', root, limits, **kw) 850 finally: 851 self.state_change_pending.release()
852 853
854 - def sort(self, sort_criteria, charset, *search_criteria, **kw):
855 """(typ, [data]) = sort(sort_criteria, charset, search_criteria, ...) 856 IMAP4rev1 extension SORT command.""" 857 858 name = 'SORT' 859 if (sort_criteria[0],sort_criteria[-1]) != ('(',')'): 860 sort_criteria = '(%s)' % sort_criteria 861 kw['untagged_response'] = name 862 return self._simple_command(name, sort_criteria, charset, *search_criteria, **kw)
863 864
865 - def status(self, mailbox, names, **kw):
866 """(typ, [data]) = status(mailbox, names) 867 Request named status conditions for mailbox.""" 868 869 name = 'STATUS' 870 kw['untagged_response'] = name 871 return self._simple_command(name, mailbox, names, **kw)
872 873
874 - def store(self, message_set, command, flags, **kw):
875 """(typ, [data]) = store(message_set, command, flags) 876 Alters flag dispositions for messages in mailbox.""" 877 878 if (flags[0],flags[-1]) != ('(',')'): 879 flags = '(%s)' % flags # Avoid quoting the flags 880 kw['untagged_response'] = 'FETCH' 881 return self._simple_command('STORE', message_set, command, flags, **kw)
882 883
884 - def subscribe(self, mailbox, **kw):
885 """(typ, [data]) = subscribe(mailbox) 886 Subscribe to new mailbox.""" 887 888 try: 889 return self._simple_command('SUBSCRIBE', mailbox, **kw) 890 finally: 891 self.state_change_pending.release()
892 893
894 - def thread(self, threading_algorithm, charset, *search_criteria, **kw):
895 """(type, [data]) = thread(threading_alogrithm, charset, search_criteria, ...) 896 IMAPrev1 extension THREAD command.""" 897 898 name = 'THREAD' 899 kw['untagged_response'] = name 900 return self._simple_command(name, threading_algorithm, charset, *search_criteria, **kw)
901 902
903 - def uid(self, command, *args, **kw):
904 """(typ, [data]) = uid(command, arg, ...) 905 Execute "command arg ..." with messages identified by UID, 906 rather than message number. 907 Returns response appropriate to 'command'.""" 908 909 command = command.upper() 910 if command in ('SEARCH', 'SORT'): 911 resp = command 912 else: 913 resp = 'FETCH' 914 kw['untagged_response'] = resp 915 return self._simple_command('UID', command, *args, **kw)
916 917
918 - def unsubscribe(self, mailbox, **kw):
919 """(typ, [data]) = unsubscribe(mailbox) 920 Unsubscribe from old mailbox.""" 921 922 try: 923 return self._simple_command('UNSUBSCRIBE', mailbox, **kw) 924 finally: 925 self.state_change_pending.release()
926 927
928 - def xatom(self, name, *args, **kw):
929 """(typ, [data]) = xatom(name, arg, ...) 930 Allow simple extension commands notified by server in CAPABILITY response. 931 Assumes extension command 'name' is legal in current state. 932 Returns response appropriate to extension command 'name'.""" 933 934 name = name.upper() 935 if not name in Commands: 936 Commands[name] = ((self.state,), False) 937 try: 938 return self._simple_command(name, *args, **kw) 939 finally: 940 if self.state_change_pending.locked(): 941 self.state_change_pending.release()
942 943 944 945 # Internal methods 946 947
948 - def _append_untagged(self, typ, dat):
949 950 if dat is None: dat = '' 951 952 self.commands_lock.acquire() 953 ur = self.untagged_responses.setdefault(typ, []) 954 ur.append(dat) 955 self.commands_lock.release() 956 957 if __debug__: self._log(5, 'untagged_responses[%s] %s += ["%s"]' % (typ, len(ur)-1, dat))
958 959
960 - def _check_bye(self):
961 962 bye = self.untagged_responses.get('BYE') 963 if bye: 964 raise self.abort(bye[-1])
965 966
967 - def _checkquote(self, arg):
968 969 # Must quote command args if non-alphanumeric chars present, 970 # and not already quoted. 971 972 if not isinstance(arg, basestring): 973 return arg 974 if len(arg) >= 2 and (arg[0],arg[-1]) in (('(',')'),('"','"')): 975 return arg 976 if arg and self.mustquote_cre.search(arg) is None: 977 return arg 978 return self._quote(arg)
979 980
981 - def _command(self, name, *args, **kw):
982 983 if Commands[name][CMD_VAL_ASYNC]: 984 cmdtyp = 'async' 985 else: 986 cmdtyp = 'sync' 987 988 if __debug__: self._log(1, '[%s] %s %s' % (cmdtyp, name, args)) 989 990 self.state_change_pending.acquire() 991 992 self._end_idle() 993 994 if cmdtyp == 'async': 995 self.state_change_pending.release() 996 else: 997 # Need to wait for all async commands to complete 998 self._check_bye() 999 self.commands_lock.acquire() 1000 if self.tagged_commands: 1001 self.state_change_free.clear() 1002 need_event = True 1003 else: 1004 need_event = False 1005 self.commands_lock.release() 1006 if need_event: 1007 if __debug__: self._log(4, 'sync command %s waiting for empty commands Q' % name) 1008 self.state_change_free.wait() 1009 if __debug__: self._log(4, 'sync command %s proceeding' % name) 1010 1011 if self.state not in Commands[name][CMD_VAL_STATES]: 1012 self.literal = None 1013 raise self.error('command %s illegal in state %s' 1014 % (name, self.state)) 1015 1016 self._check_bye() 1017 1018 self.commands_lock.acquire() 1019 for typ in ('OK', 'NO', 'BAD'): 1020 if typ in self.untagged_responses: 1021 del self.untagged_responses[typ] 1022 self.commands_lock.release() 1023 1024 if 'READ-ONLY' in self.untagged_responses \ 1025 and not self.is_readonly: 1026 self.literal = None 1027 raise self.readonly('mailbox status changed to READ-ONLY') 1028 1029 if self.Terminate: 1030 raise self.abort('connection closed') 1031 1032 rqb = self._request_push(name=name, **kw) 1033 1034 data = '%s %s' % (rqb.tag, name) 1035 for arg in args: 1036 if arg is None: continue 1037 data = '%s %s' % (data, self._checkquote(arg)) 1038 1039 literal = self.literal 1040 if literal is not None: 1041 self.literal = None 1042 if isinstance(literal, str): 1043 literator = None 1044 data = '%s {%s}' % (data, len(literal)) 1045 else: 1046 literator = literal 1047 1048 rqb.data = '%s%s' % (data, CRLF) 1049 self.ouq.put(rqb) 1050 1051 if literal is None: 1052 return rqb 1053 1054 crqb = self._request_push(tag='continuation') 1055 1056 while True: 1057 # Wait for continuation response 1058 1059 ok, data = crqb.get_response('command: %s => %%s' % name) 1060 if __debug__: self._log(3, 'continuation => %s, %s' % (ok, data)) 1061 1062 # NO/BAD response? 1063 1064 if not ok: 1065 break 1066 1067 # Send literal 1068 1069 if literator is not None: 1070 literal = literator(data, rqb) 1071 1072 if literal is None: 1073 break 1074 1075 if __debug__: self._log(4, 'write literal size %s' % len(literal)) 1076 crqb.data = '%s%s' % (literal, CRLF) 1077 self.ouq.put(crqb) 1078 1079 if literator is None: 1080 break 1081 1082 self.commands_lock.acquire() 1083 self.tagged_commands['continuation'] = crqb 1084 self.commands_lock.release() 1085 1086 return rqb
1087 1088
1089 - def _command_complete(self, rqb, kw):
1090 1091 # Called for non-callback commands 1092 1093 typ, dat = rqb.get_response('command: %s => %%s' % rqb.name) 1094 self._check_bye() 1095 if typ == 'BAD': 1096 if __debug__: self._print_log() 1097 raise self.error('%s command error: %s %s' % (rqb.name, typ, dat)) 1098 if 'untagged_response' in kw: 1099 return self._untagged_response(typ, dat, kw['untagged_response']) 1100 return typ, dat
1101 1102
1103 - def _command_completer(self, (response, cb_arg, error)):
1104 1105 # Called for callback commands 1106 rqb, kw = cb_arg 1107 rqb.callback = kw['callback'] 1108 rqb.callback_arg = kw.get('cb_arg') 1109 if error is not None: 1110 if __debug__: self._print_log() 1111 typ, val = error 1112 rqb.abort(typ, val) 1113 return 1114 bye = self.untagged_responses.get('BYE') 1115 if bye: 1116 rqb.abort(self.abort, bye[-1]) 1117 return 1118 typ, dat = response 1119 if typ == 'BAD': 1120 if __debug__: self._print_log() 1121 rqb.abort(self.error, '%s command error: %s %s' % (rqb.name, typ, dat)) 1122 return 1123 if 'untagged_response' in kw: 1124 rqb.deliver(self._untagged_response(typ, dat, kw['untagged_response'])) 1125 else: 1126 rqb.deliver(response)
1127 1128
1129 - def _deliver_dat(self, typ, dat, kw):
1130 1131 if 'callback' in kw: 1132 kw['callback'](((typ, dat), kw.get('cb_arg'), None)) 1133 return typ, dat
1134 1135
1136 - def _deliver_exc(self, exc, dat, kw):
1137 1138 if 'callback' in kw: 1139 kw['callback']((None, kw.get('cb_arg'), (exc, dat))) 1140 raise exc(dat)
1141 1142
1143 - def _end_idle(self):
1144 1145 irqb = self.idle_rqb 1146 if irqb is not None: 1147 self.idle_rqb = None 1148 self.idle_timeout = None 1149 irqb.data = 'DONE%s' % CRLF 1150 self.ouq.put(irqb) 1151 if __debug__: self._log(2, 'server IDLE finished')
1152 1153
1154 - def _match(self, cre, s):
1155 1156 # Run compiled regular expression 'cre' match method on 's'. 1157 # Save result, return success. 1158 1159 self.mo = cre.match(s) 1160 return self.mo is not None
1161 1162
1163 - def _put_response(self, resp):
1164 1165 if self._expecting_data > 0: 1166 rlen = len(resp) 1167 dlen = min(self._expecting_data, rlen) 1168 self._expecting_data -= dlen 1169 if rlen <= dlen: 1170 self._accumulated_data.append(resp) 1171 return 1172 self._accumulated_data.append(resp[:dlen]) 1173 resp = resp[dlen:] 1174 1175 if self._accumulated_data: 1176 typ, dat = self._literal_expected 1177 self._append_untagged(typ, (dat, ''.join(self._accumulated_data))) 1178 self._accumulated_data = [] 1179 1180 # Protocol mandates all lines terminated by CRLF 1181 resp = resp[:-2] 1182 1183 if 'continuation' in self.tagged_commands: 1184 continuation_expected = True 1185 else: 1186 continuation_expected = False 1187 1188 if self._literal_expected is not None: 1189 dat = resp 1190 if self._match(self.literal_cre, dat): 1191 self._literal_expected[1] = dat 1192 self._expecting_data = int(self.mo.group('size')) 1193 if __debug__: self._log(4, 'expecting literal size %s' % self._expecting_data) 1194 return 1195 typ = self._literal_expected[0] 1196 self._literal_expected = None 1197 self._append_untagged(typ, dat) # Tail 1198 if __debug__: self._log(4, 'literal completed') 1199 else: 1200 # Command completion response? 1201 if self._match(self.tagre, resp): 1202 tag = self.mo.group('tag') 1203 typ = self.mo.group('type') 1204 dat = self.mo.group('data') 1205 if not tag in self.tagged_commands: 1206 if __debug__: self._log(1, 'unexpected tagged response: %s' % resp) 1207 else: 1208 self._request_pop(tag, (typ, [dat])) 1209 else: 1210 dat2 = None 1211 1212 # '*' (untagged) responses? 1213 1214 if not self._match(self.untagged_response_cre, resp): 1215 if self._match(self.untagged_status_cre, resp): 1216 dat2 = self.mo.group('data2') 1217 1218 if self.mo is None: 1219 # Only other possibility is '+' (continuation) response... 1220 1221 if self._match(self.continuation_cre, resp): 1222 if not continuation_expected: 1223 if __debug__: self._log(1, "unexpected continuation response: '%s'" % resp) 1224 return 1225 self._request_pop('continuation', (True, self.mo.group('data'))) 1226 return 1227 1228 if __debug__: self._log(1, "unexpected response: '%s'" % resp) 1229 return 1230 1231 typ = self.mo.group('type') 1232 dat = self.mo.group('data') 1233 if dat is None: dat = '' # Null untagged response 1234 if dat2: dat = dat + ' ' + dat2 1235 1236 # Is there a literal to come? 1237 1238 if self._match(self.literal_cre, dat): 1239 self._expecting_data = int(self.mo.group('size')) 1240 if __debug__: self._log(4, 'read literal size %s' % self._expecting_data) 1241 self._literal_expected = [typ, dat] 1242 return 1243 1244 self._append_untagged(typ, dat) 1245 1246 if typ != 'OK': 1247 self._end_idle() 1248 1249 # Bracketed response information? 1250 1251 if typ in ('OK', 'NO', 'BAD') and self._match(self.response_code_cre, dat): 1252 self._append_untagged(self.mo.group('type'), self.mo.group('data')) 1253 1254 # Command waiting for aborted continuation response? 1255 1256 if continuation_expected: 1257 self._request_pop('continuation', (False, resp)) 1258 1259 # Bad news? 1260 1261 if typ in ('NO', 'BAD', 'BYE'): 1262 if typ == 'BYE': 1263 self.Terminate = True 1264 if __debug__: self._log(1, '%s response: %s' % (typ, dat))
1265 1266
1267 - def _quote(self, arg):
1268 1269 return '"%s"' % arg.replace('\\', '\\\\').replace('"', '\\"')
1270 1271
1272 - def _request_pop(self, name, data):
1273 1274 if __debug__: self._log(4, '_request_pop(%s, %s)' % (name, data)) 1275 self.commands_lock.acquire() 1276 rqb = self.tagged_commands.pop(name) 1277 if not self.tagged_commands: 1278 self.state_change_free.set() 1279 self.commands_lock.release() 1280 rqb.deliver(data)
1281 1282
1283 - def _request_push(self, tag=None, name=None, **kw):
1284 1285 self.commands_lock.acquire() 1286 rqb = Request(self, name=name, **kw) 1287 if tag is None: 1288 tag = rqb.tag 1289 self.tagged_commands[tag] = rqb 1290 self.commands_lock.release() 1291 if __debug__: self._log(4, '_request_push(%s, %s, %s)' % (tag, name, `kw`)) 1292 return rqb
1293 1294
1295 - def _simple_command(self, name, *args, **kw):
1296 1297 if 'callback' in kw: 1298 rqb = self._command(name, callback=self._command_completer, *args) 1299 rqb.callback_arg = (rqb, kw) 1300 return (None, None) 1301 return self._command_complete(self._command(name, *args), kw)
1302 1303
1304 - def _untagged_response(self, typ, dat, name):
1305 1306 if typ == 'NO': 1307 return typ, dat 1308 if not name in self.untagged_responses: 1309 return typ, [None] 1310 self.commands_lock.acquire() 1311 data = self.untagged_responses.pop(name) 1312 self.commands_lock.release() 1313 if __debug__: self._log(5, 'pop untagged_responses[%s] => %s' % (name, (typ, data))) 1314 return typ, data
1315 1316 1317 1318 # Threads 1319 1320
1321 - def _close_threads(self):
1322 1323 self.ouq.put(None) 1324 self.wrth.join() 1325 1326 self.shutdown() 1327 1328 self.rdth.join() 1329 self.inth.join()
1330 1331
1332 - def _handler(self):
1333 1334 threading.currentThread().setName('hdlr') 1335 1336 if __debug__: self._log(1, 'starting') 1337 1338 typ, val = self.abort, 'connection terminated' 1339 1340 while not self.Terminate: 1341 try: 1342 if self.idle_timeout is not None: 1343 timeout = self.idle_timeout - time.time() 1344 if timeout <= 0: 1345 timeout = 1 1346 if __debug__: 1347 if self.idle_rqb is not None: 1348 self._log(5, 'server IDLING, timeout=%.2f' % timeout) 1349 else: 1350 timeout = None 1351 line = self.inq.get(True, timeout) 1352 except Queue.Empty: 1353 if self.idle_rqb is None: 1354 continue 1355 if self.idle_timeout > time.time(): 1356 continue 1357 if __debug__: self._log(2, 'server IDLE timedout') 1358 line = IDLE_TIMEOUT_RESPONSE 1359 1360 if line is None: 1361 break 1362 1363 if not isinstance(line, str): 1364 typ, val = line 1365 break 1366 1367 try: 1368 self._put_response(line) 1369 except: 1370 typ, val = self.error, 'program error: %s - %s' % sys.exc_info()[:2] 1371 break 1372 1373 self.Terminate = True 1374 1375 while not self.ouq.empty(): 1376 try: 1377 self.ouq.get_nowait().abort(typ, val) 1378 except Queue.Empty: 1379 break 1380 self.ouq.put(None) 1381 1382 self.commands_lock.acquire() 1383 for name in self.tagged_commands.keys(): 1384 rqb = self.tagged_commands.pop(name) 1385 rqb.abort(typ, val) 1386 self.state_change_free.set() 1387 self.commands_lock.release() 1388 1389 if __debug__: self._log(1, 'finished')
1390 1391 1392 if hasattr(select_module, "poll"): 1393
1394 - def _reader(self):
1395 1396 threading.currentThread().setName('redr') 1397 1398 if __debug__: self._log(1, 'starting using poll') 1399 1400 def poll_error(state): 1401 PollErrors = { 1402 select.POLLERR: 'Error', 1403 select.POLLHUP: 'Hang up', 1404 select.POLLNVAL: 'Invalid request: descriptor not open', 1405 } 1406 return ' '.join([PollErrors[s] for s in PollErrors.keys() if (s & state)])
1407 1408 line_part = '' 1409 1410 poll = select.poll() 1411 1412 poll.register(self.read_fd, select.POLLIN) 1413 1414 while not self.Terminate: 1415 if self.state == LOGOUT: 1416 timeout = 1 1417 else: 1418 timeout = None 1419 try: 1420 r = poll.poll(timeout) 1421 if __debug__: self._log(5, 'poll => %s' % `r`) 1422 if not r: 1423 continue # Timeout 1424 1425 fd,state = r[0] 1426 1427 if state & select.POLLIN: 1428 data = self.read(32768) # Drain ssl buffer if present 1429 start = 0 1430 dlen = len(data) 1431 if __debug__: self._log(5, 'rcvd %s' % dlen) 1432 if dlen == 0: 1433 time.sleep(0.1) 1434 while True: 1435 stop = data.find('\n', start) 1436 if stop < 0: 1437 line_part += data[start:] 1438 break 1439 stop += 1 1440 line_part, start, line = \ 1441 '', stop, line_part + data[start:stop] 1442 if __debug__: self._log(4, '< %s' % line) 1443 self.inq.put(line) 1444 1445 if state & ~(select.POLLIN): 1446 raise IOError(poll_error(state)) 1447 except: 1448 reason = 'socket error: %s - %s' % sys.exc_info()[:2] 1449 if __debug__: 1450 if not self.Terminate: 1451 self._print_log() 1452 if self.debug: self.debug += 4 # Output all 1453 self._log(1, reason) 1454 self.inq.put((self.abort, reason)) 1455 break 1456 1457 poll.unregister(self.read_fd) 1458 1459 if __debug__: self._log(1, 'finished')
1460 1461 else: 1462 1463 # No "poll" - use select() 1464
1465 - def _reader(self):
1466 1467 threading.currentThread().setName('redr') 1468 1469 if __debug__: self._log(1, 'starting using select') 1470 1471 line_part = '' 1472 1473 while not self.Terminate: 1474 if self.state == LOGOUT: 1475 timeout = 1 1476 else: 1477 timeout = None 1478 try: 1479 r,w,e = select.select([self.read_fd], [], [], timeout) 1480 if __debug__: self._log(5, 'select => %s, %s, %s' % (r,w,e)) 1481 if not r: # Timeout 1482 continue 1483 1484 data = self.read(32768) # Drain ssl buffer if present 1485 start = 0 1486 dlen = len(data) 1487 if __debug__: self._log(5, 'rcvd %s' % dlen) 1488 if dlen == 0: 1489 time.sleep(0.1) 1490 while True: 1491 stop = data.find('\n', start) 1492 if stop < 0: 1493 line_part += data[start:] 1494 break 1495 stop += 1 1496 line_part, start, line = \ 1497 '', stop, line_part + data[start:stop] 1498 if __debug__: self._log(4, '< %s' % line) 1499 self.inq.put(line) 1500 except: 1501 reason = 'socket error: %s - %s' % sys.exc_info()[:2] 1502 if __debug__: 1503 if not self.Terminate: 1504 self._print_log() 1505 if self.debug: self.debug += 4 # Output all 1506 self._log(1, reason) 1507 self.inq.put((self.abort, reason)) 1508 break 1509 1510 if __debug__: self._log(1, 'finished')
1511 1512
1513 - def _writer(self):
1514 1515 threading.currentThread().setName('wrtr') 1516 1517 if __debug__: self._log(1, 'starting') 1518 1519 reason = 'Terminated' 1520 1521 while not self.Terminate: 1522 rqb = self.ouq.get() 1523 if rqb is None: 1524 break # Outq flushed 1525 1526 try: 1527 self.send(rqb.data) 1528 if __debug__: self._log(4, '> %s' % rqb.data) 1529 except: 1530 reason = 'socket error: %s - %s' % sys.exc_info()[:2] 1531 if __debug__: 1532 if not self.Terminate: 1533 self._print_log() 1534 if self.debug: self.debug += 4 # Output all 1535 self._log(1, reason) 1536 rqb.abort(self.abort, reason) 1537 break 1538 1539 self.inq.put((self.abort, reason)) 1540 1541 if __debug__: self._log(1, 'finished')
1542 1543 1544 1545 # Debugging 1546 1547 1548 if __debug__: 1549
1550 - def _init_debug(self, debug=None, debug_file=None):
1551 self.debug = debug is not None and debug or Debug is not None and Debug or 0 1552 self.debug_file = debug_file is not None and debug_file or sys.stderr 1553 1554 self.debug_lock = threading.Lock() 1555 self._cmd_log_len = 20 1556 self._cmd_log_idx = 0 1557 self._cmd_log = {} # Last `_cmd_log_len' interactions 1558 if self.debug: 1559 self._mesg('imaplib2 version %s' % __version__) 1560 self._mesg('imaplib2 debug level %s' % self.debug)
1561 1562
1563 - def _dump_ur(self, lvl):
1564 if lvl > self.debug: 1565 return 1566 1567 l = self.untagged_responses.items() 1568 if not l: 1569 return 1570 1571 t = '\n\t\t' 1572 l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l) 1573 self.debug_lock.acquire() 1574 self._mesg('untagged responses dump:%s%s' % (t, t.join(l))) 1575 self.debug_lock.release()
1576 1577
1578 - def _log(self, lvl, line):
1579 if lvl > self.debug: 1580 return 1581 1582 if line[-2:] == CRLF: 1583 line = line[:-2] + '\\r\\n' 1584 1585 tn = threading.currentThread().getName() 1586 1587 if self.debug >= 4: 1588 self.debug_lock.acquire() 1589 self._mesg(line, tn) 1590 self.debug_lock.release() 1591 return 1592 1593 # Keep log of last `_cmd_log_len' interactions for debugging. 1594 self._cmd_log[self._cmd_log_idx] = (line, tn, time.time()) 1595 self._cmd_log_idx += 1 1596 if self._cmd_log_idx >= self._cmd_log_len: 1597 self._cmd_log_idx = 0
1598 1599
1600 - def _mesg(self, s, tn=None, secs=None):
1601 if secs is None: 1602 secs = time.time() 1603 if tn is None: 1604 tn = threading.currentThread().getName() 1605 tm = time.strftime('%M:%S', time.localtime(secs)) 1606 self.debug_file.write(' %s.%02d %s %s\n' % (tm, (secs*100)%100, tn, s)) 1607 self.debug_file.flush()
1608 1609
1610 - def _print_log(self):
1611 self.debug_lock.acquire() 1612 i, n = self._cmd_log_idx, self._cmd_log_len 1613 if n: self._mesg('last %d imaplib2 reports:' % n) 1614 while n: 1615 try: 1616 self._mesg(*self._cmd_log[i]) 1617 except: 1618 pass 1619 i += 1 1620 if i >= self._cmd_log_len: 1621 i = 0 1622 n -= 1 1623 self.debug_lock.release()
1624 1625 1626
1627 -class IMAP4_SSL(IMAP4):
1628 1629 """IMAP4 client class over SSL connection 1630 1631 Instantiate with: 1632 IMAP4_SSL(host=None, port=None, keyfile=None, certfile=None, debug=None, debug_file=None) 1633 1634 host - host's name (default: localhost); 1635 port - port number (default: standard IMAP4 SSL port); 1636 keyfile - PEM formatted file that contains your private key (default: None); 1637 certfile - PEM formatted certificate chain file (default: None); 1638 debug - debug level (default: 0 - no debug); 1639 debug_file - debug stream (default: sys.stderr). 1640 1641 For more documentation see the docstring of the parent class IMAP4. 1642 """ 1643 1644
1645 - def __init__(self, host=None, port=None, keyfile=None, certfile=None, debug=None, debug_file=None):
1646 self.keyfile = keyfile 1647 self.certfile = certfile 1648 IMAP4.__init__(self, host, port, debug, debug_file)
1649 1650
1651 - def open(self, host=None, port=None):
1652 """open(host=None, port=None) 1653 Setup secure connection to remote server on "host:port" 1654 (default: localhost:standard IMAP4 SSL port). 1655 This connection will be used by the routines: 1656 read, send, shutdown, socket, ssl.""" 1657 1658 self.host = host is not None and host or '' 1659 self.port = port is not None and port or IMAP4_SSL_PORT 1660 self.sock = self.open_socket() 1661 self.sslobj = socket.ssl(self.sock, self.keyfile, self.certfile) 1662 1663 self.read_fd = self.sock.fileno()
1664 1665
1666 - def read(self, size):
1667 """data = read(size) 1668 Read at most 'size' bytes from remote.""" 1669 1670 return self.sslobj.read(size)
1671 1672
1673 - def send(self, data):
1674 """send(data) 1675 Send 'data' to remote.""" 1676 1677 # NB: socket.ssl needs a "sendall" method to match socket objects. 1678 bytes = len(data) 1679 while bytes > 0: 1680 sent = self.sslobj.write(data) 1681 if sent == bytes: 1682 break # avoid copy 1683 data = data[sent:] 1684 bytes = bytes - sent
1685 1686
1687 - def ssl(self):
1688 """ssl = ssl() 1689 Return socket.ssl instance used to communicate with the IMAP4 server.""" 1690 1691 return self.sslobj
1692 1693 1694
1695 -class IMAP4_stream(IMAP4):
1696 1697 """IMAP4 client class over a stream 1698 1699 Instantiate with: 1700 IMAP4_stream(command, debug=None, debug_file=None) 1701 1702 command - string that can be passed to os.popen2(); 1703 debug - debug level (default: 0 - no debug); 1704 debug_file - debug stream (default: sys.stderr). 1705 1706 For more documentation see the docstring of the parent class IMAP4. 1707 """ 1708 1709
1710 - def __init__(self, command, debug=None, debug_file=None):
1711 self.command = command 1712 self.host = command 1713 self.port = None 1714 self.sock = None 1715 self.writefile, self.readfile = None, None 1716 self.read_fd = None 1717 IMAP4.__init__(self, debug=debug, debug_file=debug_file)
1718 1719
1720 - def open(self, host=None, port=None):
1721 """open(host=None, port=None) 1722 Setup a stream connection via 'self.command'. 1723 This connection will be used by the routines: 1724 read, send, shutdown, socket.""" 1725 1726 self.writefile, self.readfile = os.popen2(self.command) 1727 self.read_fd = self.readfile.fileno()
1728 1729
1730 - def read(self, size):
1731 """Read 'size' bytes from remote.""" 1732 1733 return os.read(self.read_fd, size)
1734 1735
1736 - def send(self, data):
1737 """Send data to remote.""" 1738 1739 self.writefile.write(data) 1740 self.writefile.flush()
1741 1742
1743 - def shutdown(self):
1744 """Close I/O established in "open".""" 1745 1746 self.readfile.close() 1747 self.writefile.close()
1748 1749 1750
1751 -class _Authenticator(object):
1752 1753 """Private class to provide en/de-coding 1754 for base64 authentication conversation.""" 1755
1756 - def __init__(self, mechinst):
1757 self.mech = mechinst # Callable object to provide/process data
1758
1759 - def process(self, data, rqb):
1760 ret = self.mech(self.decode(data)) 1761 if ret is None: 1762 return '*' # Abort conversation 1763 return self.encode(ret)
1764
1765 - def encode(self, inp):
1766 # 1767 # Invoke binascii.b2a_base64 iteratively with 1768 # short even length buffers, strip the trailing 1769 # line feed from the result and append. "Even" 1770 # means a number that factors to both 6 and 8, 1771 # so when it gets to the end of the 8-bit input 1772 # there's no partial 6-bit output. 1773 # 1774 oup = '' 1775 while inp: 1776 if len(inp) > 48: 1777 t = inp[:48] 1778 inp = inp[48:] 1779 else: 1780 t = inp 1781 inp = '' 1782 e = binascii.b2a_base64(t) 1783 if e: 1784 oup = oup + e[:-1] 1785 return oup
1786
1787 - def decode(self, inp):
1788 if not inp: 1789 return '' 1790 return binascii.a2b_base64(inp)
1791 1792 1793 1794
1795 -class _IdleCont(object):
1796 1797 """When process is called, server is in IDLE state 1798 and will send asynchronous changes.""" 1799
1800 - def __init__(self, parent, timeout):
1801 self.parent = parent 1802 self.timeout = timeout is not None and timeout or IDLE_TIMEOUT 1803 self.parent.idle_timeout = self.timeout + time.time()
1804
1805 - def process(self, data, rqb):
1806 self.parent.idle_rqb = rqb 1807 self.parent.idle_timeout = self.timeout + time.time() 1808 if __debug__: self.parent._log(2, 'server IDLE started, timeout in %.2f secs' % self.timeout) 1809 return None
1810 1811 1812 1813 Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6, 1814 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12} 1815 1816 InternalDate = re.compile(r'.*INTERNALDATE "' 1817 r'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])' 1818 r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])' 1819 r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])' 1820 r'"') 1821 1822
1823 -def Internaldate2Time(resp):
1824 1825 """time_tuple = Internaldate2Time(resp) 1826 Convert IMAP4 INTERNALDATE to UT.""" 1827 1828 mo = InternalDate.match(resp) 1829 if not mo: 1830 return None 1831 1832 mon = Mon2num[mo.group('mon')] 1833 zonen = mo.group('zonen') 1834 1835 day = int(mo.group('day')) 1836 year = int(mo.group('year')) 1837 hour = int(mo.group('hour')) 1838 min = int(mo.group('min')) 1839 sec = int(mo.group('sec')) 1840 zoneh = int(mo.group('zoneh')) 1841 zonem = int(mo.group('zonem')) 1842 1843 # INTERNALDATE timezone must be subtracted to get UT 1844 1845 zone = (zoneh*60 + zonem)*60 1846 if zonen == '-': 1847 zone = -zone 1848 1849 tt = (year, mon, day, hour, min, sec, -1, -1, -1) 1850 1851 utc = time.mktime(tt) 1852 1853 # Following is necessary because the time module has no 'mkgmtime'. 1854 # 'mktime' assumes arg in local timezone, so adds timezone/altzone. 1855 1856 lt = time.localtime(utc) 1857 if time.daylight and lt[-1]: 1858 zone = zone + time.altzone 1859 else: 1860 zone = zone + time.timezone 1861 1862 return time.localtime(utc - zone)
1863 1864 Internaldate2tuple = Internaldate2Time # (Backward compatible) 1865 1866 1867
1868 -def Time2Internaldate(date_time):
1869 1870 """'"DD-Mmm-YYYY HH:MM:SS +HHMM"' = Time2Internaldate(date_time) 1871 Convert 'date_time' to IMAP4 INTERNALDATE representation.""" 1872 1873 if isinstance(date_time, (int, float)): 1874 tt = time.localtime(date_time) 1875 elif isinstance(date_time, (tuple, time.struct_time)): 1876 tt = date_time 1877 elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'): 1878 return date_time # Assume in correct format 1879 else: 1880 raise ValueError("date_time not of a known type") 1881 1882 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt) 1883 if dt[0] == '0': 1884 dt = ' ' + dt[1:] 1885 if time.daylight and tt[-1]: 1886 zone = -time.altzone 1887 else: 1888 zone = -time.timezone 1889 return '"' + dt + " %+03d%02d" % divmod(zone//60, 60) + '"'
1890 1891 1892 1893 FLAGS_cre = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)') 1894
1895 -def ParseFlags(resp):
1896 1897 """('flag', ...) = ParseFlags(line) 1898 Convert IMAP4 flags response to python tuple.""" 1899 1900 mo = FLAGS_cre.match(resp) 1901 if not mo: 1902 return () 1903 1904 return tuple(mo.group('flags').split())
1905 1906 1907 1908 if __name__ == '__main__': 1909 1910 # To test: invoke either as 'python imaplib2.py [IMAP4_server_hostname]', 1911 # or as 'python imaplib2.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"' 1912 # or as 'python imaplib2.py -l "keyfile[:certfile]" [IMAP4_SSL_server_hostname]' 1913 1914 import getopt, getpass 1915 1916 try: 1917 optlist, args = getopt.getopt(sys.argv[1:], 'd:l:s:p:') 1918 except getopt.error, val: 1919 optlist, args = (), () 1920 1921 debug, port, stream_command, keyfile, certfile = (None,)*5 1922 for opt,val in optlist: 1923 if opt == '-d': 1924 debug = int(val) 1925 elif opt == '-l': 1926 try: 1927 keyfile,certfile = val.split(':') 1928 except ValueError: 1929 keyfile,certfile = val,val 1930 elif opt == '-p': 1931 port = int(val) 1932 elif opt == '-s': 1933 stream_command = val 1934 if not args: args = (stream_command,) 1935 1936 if not args: args = ('',) 1937 if not port: port = (keyfile is not None) and IMAP4_SSL_PORT or IMAP4_PORT 1938 1939 host = args[0] 1940 1941 USER = getpass.getuser() 1942 PASSWD = getpass.getpass("IMAP%s password for %s on %s: " 1943 % ((keyfile is not None) and 'S' or '', USER, host or "localhost")) 1944 1945 test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)s%(data)s' \ 1946 % {'user':USER, 'lf':'\n', 'data':open(__file__).read()} 1947 test_seq1 = ( 1948 ('login', (USER, PASSWD)), 1949 ('list', ('""', '%')), 1950 ('create', ('/tmp/imaplib2_test.0',)), 1951 ('rename', ('/tmp/imaplib2_test.0', '/tmp/imaplib2_test.1')), 1952 ('CREATE', ('/tmp/imaplib2_test.2',)), 1953 ('append', ('/tmp/imaplib2_test.2', None, None, test_mesg)), 1954 ('list', ('/tmp', 'imaplib2_test*')), 1955 ('select', ('/tmp/imaplib2_test.2',)), 1956 ('search', (None, 'SUBJECT', 'IMAP4 test')), 1957 ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')), 1958 ('store', ('1', 'FLAGS', '(\Deleted)')), 1959 ('namespace', ()), 1960 ('expunge', ()), 1961 ('recent', ()), 1962 ('close', ()), 1963 ) 1964 1965 test_seq2 = ( 1966 ('select', ()), 1967 ('response',('UIDVALIDITY',)), 1968 ('response', ('EXISTS',)), 1969 ('append', (None, None, None, test_mesg)), 1970 ('uid', ('SEARCH', 'SUBJECT', 'IMAP4 test')), 1971 ('uid', ('SEARCH', 'ALL')), 1972 ('recent', ()), 1973 ) 1974 1975 AsyncError = None 1976
1977 - def responder((response, cb_arg, error)):
1978 global AsyncError 1979 cmd, args = cb_arg 1980 if error is not None: 1981 AsyncError = error 1982 M._mesg('[cb] ERROR %s %.100s => %s' % (cmd, args, error)) 1983 return 1984 typ, dat = response 1985 M._mesg('[cb] %s %.100s => %s %.100s' % (cmd, args, typ, dat)) 1986 if typ == 'NO': 1987 AsyncError = (Exception, dat[0])
1988
1989 - def run(cmd, args, cb=None):
1990 if AsyncError: 1991 M.logout() 1992 typ, val = AsyncError 1993 raise typ(val) 1994 M._mesg('%s %.100s' % (cmd, args)) 1995 try: 1996 if cb is not None: 1997 typ, dat = getattr(M, cmd)(callback=responder, cb_arg=(cmd, args), *args) 1998 if M.debug: 1999 M._mesg('%s %.100s => %s %.100s' % (cmd, args, typ, dat)) 2000 else: 2001 typ, dat = getattr(M, cmd)(*args) 2002 M._mesg('%s %.100s => %s %.100s' % (cmd, args, typ, dat)) 2003 except: 2004 M.logout() 2005 raise 2006 if typ == 'NO': 2007 M.logout() 2008 raise Exception(dat[0]) 2009 return dat
2010 2011 try: 2012 threading.currentThread().setName('main') 2013 2014 if keyfile is not None: 2015 if not keyfile: keyfile = None 2016 if not certfile: certfile = None 2017 M = IMAP4_SSL(host=host, port=port, keyfile=keyfile, certfile=certfile, debug=debug) 2018 elif stream_command: 2019 M = IMAP4_stream(stream_command, debug=debug) 2020 else: 2021 M = IMAP4(host=host, port=port, debug=debug) 2022 if M.state == 'AUTH': 2023 test_seq1 = test_seq1[1:] # Login not needed 2024 M._mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION) 2025 M._mesg('CAPABILITIES = %r' % (M.capabilities,)) 2026 2027 for cmd,args in test_seq1: 2028 run(cmd, args, cb=1) 2029 2030 for ml in run('list', ('/tmp/', 'imaplib2_test%')): 2031 mo = re.match(r'.*"([^"]+)"$', ml) 2032 if mo: path = mo.group(1) 2033 else: path = ml.split()[-1] 2034 run('delete', (path,), cb=1) 2035 2036 for cmd,args in test_seq2: 2037 if (cmd,args) != ('uid', ('SEARCH', 'SUBJECT', 'IMAP4 test')): 2038 run(cmd, args, cb=1) 2039 continue 2040 2041 dat = run(cmd, args) 2042 uid = dat[-1].split() 2043 if not uid: continue 2044 run('uid', ('FETCH', uid[-1], 2045 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'), cb=1) 2046 run('uid', ('STORE', uid[-1], 'FLAGS', '(\Deleted)'), cb=1) 2047 run('expunge', (), cb=1) 2048 2049 run('idle', (3,)) 2050 run('logout', ()) 2051 2052 if debug: 2053 print 2054 M._print_log() 2055 2056 print '\nAll tests OK.' 2057 2058 except: 2059 print '\nTests failed.' 2060 2061 if not debug: 2062 print ''' 2063 If you would like to see debugging output, 2064 try: %s -d5 2065 ''' % sys.argv[0] 2066 2067 raise 2068