1#!/usr/local/bin/python3.8 2 3"""Threaded IMAP4 client for Python 3. 4 5Based on RFC 3501 and original imaplib module. 6 7Public classes: IMAP4 8 IMAP4_SSL 9 IMAP4_stream 10 11Public functions: Internaldate2Time 12 ParseFlags 13 Time2Internaldate 14""" 15 16 17__all__ = ("IMAP4", "IMAP4_SSL", "IMAP4_stream", 18 "Internaldate2Time", "ParseFlags", "Time2Internaldate", 19 "Mon2num", "MonthNames", "InternalDate") 20 21__version__ = "3.05" 22__release__ = "3" 23__revision__ = "05" 24__credits__ = """ 25Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998. 26String method conversion by ESR, February 2001. 27GET/SETACL contributed by Anthony Baxter <anthony@interlink.com.au> April 2001. 28IMAP4_SSL contributed by Tino Lange <Tino.Lange@isg.de> March 2002. 29GET/SETQUOTA contributed by Andreas Zeidler <az@kreativkombinat.de> June 2002. 30PROXYAUTH contributed by Rick Holbert <holbert.13@osu.edu> November 2002. 31IDLE via threads suggested by Philippe Normand <phil@respyre.org> January 2005. 32GET/SETANNOTATION contributed by Tomas Lindroos <skitta@abo.fi> June 2005. 33COMPRESS/DEFLATE contributed by Bron Gondwana <brong@brong.net> May 2009. 34STARTTLS from Jython's imaplib by Alan Kennedy. 35ID contributed by Dave Baggett <dave@baggett.org> November 2009. 36Improved untagged responses handling suggested by Dave Baggett <dave@baggett.org> November 2009. 37Improved thread naming, and 0 read detection contributed by Grant Edwards <grant.b.edwards@gmail.com> June 2010. 38Improved timeout handling contributed by Ivan Vovnenko <ivovnenko@gmail.com> October 2010. 39Timeout handling further improved by Ethan Glasser-Camp <glasse@cs.rpi.edu> December 2010. 40Time2Internaldate() patch to match RFC2060 specification of English month names from bugs.python.org/issue11024 March 2011. 41starttls() bug fixed with the help of Sebastian Spaeth <sebastian@sspaeth.de> April 2011. 42Threads now set the "daemon" flag (suggested by offlineimap-project) April 2011. 43Single quoting introduced with the help of Vladimir Marek <vladimir.marek@oracle.com> August 2011. 44Support for specifying SSL version by Ryan Kavanagh <rak@debian.org> July 2013. 45Fix for gmail "read 0" error provided by Jim Greenleaf <james.a.greenleaf@gmail.com> August 2013. 46Fix for offlineimap "indexerror: string index out of range" bug provided by Eygene Ryabinkin <rea@codelabs.ru> August 2013. 47Fix for missing idle_lock in _handler() provided by Franklin Brook <franklin@brook.se> August 2014. 48Conversion to Python3 provided by F. Malina <fmalina@gmail.com> February 2015. 49Fix for READ-ONLY error from multiple EXAMINE/SELECT calls by Pierre-Louis Bonicoli <pierre-louis.bonicoli@gmx.fr> March 2015. 50Fix for null strings appended to untagged responses by Pierre-Louis Bonicoli <pierre-louis.bonicoli@gmx.fr> March 2015. 51Fix for correct byte encoding for _CRAM_MD5_AUTH taken from python3.5 imaplib.py June 2015. 52Fix for correct Python 3 exception handling by Tobias Brink <tobias.brink@gmail.com> August 2015. 53Fix to allow interruptible IDLE command by Tim Peoples <dromedary512@users.sf.net> September 2015. 54Add support for TLS levels by Ben Boeckel <mathstuf@gmail.com> September 2015. 55Fix for shutown exception by Sebastien Gross <seb@chezwam.org> November 2015.""" 56__author__ = "Piers Lauder <piers@janeelix.com>" 57__URL__ = "http://imaplib2.sourceforge.net" 58__license__ = "Python License" 59 60import binascii, calendar, errno, os, queue, random, re, select, socket, sys, time, threading, zlib 61 62 63select_module = select 64 65# Globals 66 67CRLF = b'\r\n' 68IMAP4_PORT = 143 69IMAP4_SSL_PORT = 993 70 71IDLE_TIMEOUT_RESPONSE = b'* IDLE TIMEOUT\r\n' 72IDLE_TIMEOUT = 60*29 # Don't stay in IDLE state longer 73READ_POLL_TIMEOUT = 30 # Without this timeout interrupted network connections can hang reader 74READ_SIZE = 32768 # Consume all available in socket 75 76DFLT_DEBUG_BUF_LVL = 3 # Level above which the logging output goes directly to stderr 77 78TLS_SECURE = "tls_secure" # Recognised TLS levels 79TLS_NO_SSL = "tls_no_ssl" 80TLS_COMPAT = "tls_compat" 81 82AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first 83 84# Commands 85 86CMD_VAL_STATES = 0 87CMD_VAL_ASYNC = 1 88NONAUTH, AUTH, SELECTED, LOGOUT = 'NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT' 89 90Commands = { 91 # name valid states asynchronous 92 'APPEND': ((AUTH, SELECTED), False), 93 'AUTHENTICATE': ((NONAUTH,), False), 94 'CAPABILITY': ((NONAUTH, AUTH, SELECTED), True), 95 'CHECK': ((SELECTED,), True), 96 'CLOSE': ((SELECTED,), False), 97 'COMPRESS': ((AUTH,), False), 98 'COPY': ((SELECTED,), True), 99 'CREATE': ((AUTH, SELECTED), True), 100 'DELETE': ((AUTH, SELECTED), True), 101 'DELETEACL': ((AUTH, SELECTED), True), 102 'ENABLE': ((AUTH,), False), 103 'EXAMINE': ((AUTH, SELECTED), False), 104 'EXPUNGE': ((SELECTED,), True), 105 'FETCH': ((SELECTED,), True), 106 'GETACL': ((AUTH, SELECTED), True), 107 'GETANNOTATION':((AUTH, SELECTED), True), 108 'GETQUOTA': ((AUTH, SELECTED), True), 109 'GETQUOTAROOT': ((AUTH, SELECTED), True), 110 'ID': ((NONAUTH, AUTH, LOGOUT, SELECTED), True), 111 'IDLE': ((SELECTED,), False), 112 'LIST': ((AUTH, SELECTED), True), 113 'LOGIN': ((NONAUTH,), False), 114 'LOGOUT': ((NONAUTH, AUTH, LOGOUT, SELECTED), False), 115 'LSUB': ((AUTH, SELECTED), True), 116 'MYRIGHTS': ((AUTH, SELECTED), True), 117 'NAMESPACE': ((AUTH, SELECTED), True), 118 'NOOP': ((NONAUTH, AUTH, SELECTED), True), 119 'PARTIAL': ((SELECTED,), True), 120 'PROXYAUTH': ((AUTH,), False), 121 'RENAME': ((AUTH, SELECTED), True), 122 'SEARCH': ((SELECTED,), True), 123 'SELECT': ((AUTH, SELECTED), False), 124 'SETACL': ((AUTH, SELECTED), False), 125 'SETANNOTATION':((AUTH, SELECTED), True), 126 'SETQUOTA': ((AUTH, SELECTED), False), 127 'SORT': ((SELECTED,), True), 128 'STARTTLS': ((NONAUTH,), False), 129 'STATUS': ((AUTH, SELECTED), True), 130 'STORE': ((SELECTED,), True), 131 'SUBSCRIBE': ((AUTH, SELECTED), False), 132 'THREAD': ((SELECTED,), True), 133 'UID': ((SELECTED,), True), 134 'UNSUBSCRIBE': ((AUTH, SELECTED), False), 135 } 136 137UID_direct = ('SEARCH', 'SORT', 'THREAD') 138 139 140def Int2AP(num): 141 142 """string = Int2AP(num) 143 Return 'num' converted to bytes using characters from the set 'A'..'P' 144 """ 145 146 val = b''; AP = b'ABCDEFGHIJKLMNOP' 147 num = int(abs(num)) 148 while num: 149 num, mod = divmod(num, 16) 150 val = AP[mod:mod+1] + val 151 return val 152 153 154 155class Request(object): 156 157 """Private class to represent a request awaiting response.""" 158 159 def __init__(self, parent, name=None, callback=None, cb_arg=None, cb_self=False): 160 self.parent = parent 161 self.name = name 162 self.callback = callback # Function called to process result 163 if not cb_self: 164 self.callback_arg = cb_arg # Optional arg passed to "callback" 165 else: 166 self.callback_arg = (self, cb_arg) # Self reference required in callback arg 167 168 self.tag = parent.tagpre + bytes(str(parent.tagnum), 'ASCII') 169 parent.tagnum += 1 170 171 self.ready = threading.Event() 172 self.response = None 173 self.aborted = None 174 self.data = None 175 176 177 def abort(self, typ, val): 178 self.aborted = (typ, val) 179 self.deliver(None) 180 181 182 def get_response(self, exc_fmt=None): 183 self.callback = None 184 if __debug__: self.parent._log(3, '%s:%s.ready.wait' % (self.name, self.tag)) 185 self.ready.wait(threading.TIMEOUT_MAX) 186 187 if self.aborted is not None: 188 typ, val = self.aborted 189 if exc_fmt is None: 190 exc_fmt = '%s - %%s' % typ 191 raise typ(exc_fmt % str(val)) 192 193 return self.response 194 195 196 def deliver(self, response): 197 if self.callback is not None: 198 self.callback((response, self.callback_arg, self.aborted)) 199 return 200 201 self.response = response 202 self.ready.set() 203 if __debug__: self.parent._log(3, '%s:%s.ready.set' % (self.name, self.tag)) 204 205 206 207 208class IMAP4(object): 209 210 """Threaded IMAP4 client class. 211 212 Instantiate with: 213 IMAP4(host=None, port=None, debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None) 214 215 host - host's name (default: localhost); 216 port - port number (default: standard IMAP4 port); 217 debug - debug level (default: 0 - no debug); 218 debug_file - debug stream (default: sys.stderr); 219 identifier - thread identifier prefix (default: host); 220 timeout - timeout in seconds when expecting a command response (default: no timeout), 221 debug_buf_lvl - debug level at which buffering is turned off. 222 223 All IMAP4rev1 commands are supported by methods of the same name. 224 225 Each command returns a tuple: (type, [data, ...]) where 'type' 226 is usually 'OK' or 'NO', and 'data' is either the text from the 227 tagged response, or untagged results from command. Each 'data' is 228 either a string, or a tuple. If a tuple, then the first part is the 229 header of the response, and the second part contains the data (ie: 230 'literal' value). 231 232 Errors raise the exception class <instance>.error("<reason>"). 233 IMAP4 server errors raise <instance>.abort("<reason>"), which is 234 a sub-class of 'error'. Mailbox status changes from READ-WRITE to 235 READ-ONLY raise the exception class <instance>.readonly("<reason>"), 236 which is a sub-class of 'abort'. 237 238 "error" exceptions imply a program error. 239 "abort" exceptions imply the connection should be reset, and 240 the command re-tried. 241 "readonly" exceptions imply the command should be re-tried. 242 243 All commands take two optional named arguments: 244 'callback' and 'cb_arg' 245 If 'callback' is provided then the command is asynchronous, so after 246 the command is queued for transmission, the call returns immediately 247 with the tuple (None, None). 248 The result will be posted by invoking "callback" with one arg, a tuple: 249 callback((result, cb_arg, None)) 250 or, if there was a problem: 251 callback((None, cb_arg, (exception class, reason))) 252 253 Otherwise the command is synchronous (waits for result). But note 254 that state-changing commands will both block until previous commands 255 have completed, and block subsequent commands until they have finished. 256 257 All (non-callback) string arguments to commands are converted to bytes, 258 except for AUTHENTICATE, and the last argument to APPEND which is 259 passed as an IMAP4 literal. NB: the 'password' argument to the LOGIN 260 command is always quoted. 261 262 There is one instance variable, 'state', that is useful for tracking 263 whether the client needs to login to the server. If it has the 264 value "AUTH" after instantiating the class, then the connection 265 is pre-authenticated (otherwise it will be "NONAUTH"). Selecting a 266 mailbox changes the state to be "SELECTED", closing a mailbox changes 267 back to "AUTH", and once the client has logged out, the state changes 268 to "LOGOUT" and no further commands may be issued. 269 270 Note: to use this module, you must read the RFCs pertaining to the 271 IMAP4 protocol, as the semantics of the arguments to each IMAP4 272 command are left to the invoker, not to mention the results. Also, 273 most IMAP servers implement a sub-set of the commands available here. 274 275 Note also that you must call logout() to shut down threads before 276 discarding an instance. 277 """ 278 279 class error(Exception): pass # Logical errors - debug required 280 class abort(error): pass # Service errors - close and retry 281 class readonly(abort): pass # Mailbox status changed to READ-ONLY 282 283 # These must be encoded according to utf8 setting in _mode_xxx(): 284 _literal = br'.*{(?P<size>\d+)}$' 285 _untagged_status = br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?' 286 287 continuation_cre = re.compile(br'\+( (?P<data>.*))?') 288 mapCRLF_cre = re.compile(br'\r\n|\r|\n') 289 response_code_cre = re.compile(br'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]') 290 untagged_response_cre = re.compile(br'\* (?P<type>[A-Z-]+)( (?P<data>.*))?') 291 292 293 def __init__(self, host=None, port=None, debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None): 294 295 self.state = NONAUTH # IMAP4 protocol state 296 self.literal = None # A literal argument to a command 297 self.tagged_commands = {} # Tagged commands awaiting response 298 self.untagged_responses = [] # [[typ: [data, ...]], ...] 299 self.mailbox = None # Current mailbox selected 300 self.is_readonly = False # READ-ONLY desired state 301 self.idle_rqb = None # Server IDLE Request - see _IdleCont 302 self.idle_timeout = None # Must prod server occasionally 303 304 self._expecting_data = False # Expecting message data 305 self._expecting_data_len = 0 # How many characters we expect 306 self._accumulated_data = [] # Message data accumulated so far 307 self._literal_expected = None # Message data descriptor 308 309 self.compressor = None # COMPRESS/DEFLATE if not None 310 self.decompressor = None 311 self._tls_established = False 312 313 # Create unique tag for this session, 314 # and compile tagged response matcher. 315 316 self.tagnum = 0 317 self.tagpre = Int2AP(random.randint(4096, 65535)) 318 self.tagre = re.compile(br'(?P<tag>' 319 + self.tagpre 320 + br'\d+) (?P<type>[A-Z]+) (?P<data>.*)', re.ASCII) 321 322 self._mode_ascii() 323 324 if __debug__: self._init_debug(debug, debug_file, debug_buf_lvl) 325 326 self.resp_timeout = timeout # Timeout waiting for command response 327 328 if timeout is not None and timeout < READ_POLL_TIMEOUT: 329 self.read_poll_timeout = timeout 330 else: 331 self.read_poll_timeout = READ_POLL_TIMEOUT 332 self.read_size = READ_SIZE 333 334 # Open socket to server. 335 336 self.open(host, port) 337 338 if __debug__: 339 if debug: 340 self._mesg('connected to %s on port %s' % (self.host, self.port)) 341 342 # Threading 343 344 if identifier is not None: 345 self.identifier = identifier 346 else: 347 self.identifier = self.host 348 if self.identifier: 349 self.identifier += ' ' 350 351 self.Terminate = self.TerminateReader = False 352 353 self.state_change_free = threading.Event() 354 self.state_change_pending = threading.Lock() 355 self.commands_lock = threading.Lock() 356 self.idle_lock = threading.Lock() 357 358 self.ouq = queue.Queue(10) 359 self.inq = queue.Queue() 360 361 self.wrth = threading.Thread(target=self._writer) 362 self.wrth.setDaemon(True) 363 self.wrth.start() 364 self.rdth = threading.Thread(target=self._reader) 365 self.rdth.setDaemon(True) 366 self.rdth.start() 367 self.inth = threading.Thread(target=self._handler) 368 self.inth.setDaemon(True) 369 self.inth.start() 370 371 # Get server welcome message, 372 # request and store CAPABILITY response. 373 374 try: 375 self.welcome = self._request_push(name='welcome', tag='continuation').get_response('IMAP4 protocol error: %s')[1] 376 377 if self._get_untagged_response('PREAUTH'): 378 self.state = AUTH 379 if __debug__: self._log(1, 'state => AUTH') 380 elif self._get_untagged_response('OK'): 381 if __debug__: self._log(1, 'state => NONAUTH') 382 else: 383 raise self.error('unrecognised server welcome message: %s' % repr(self.welcome)) 384 385 self._get_capabilities() 386 if __debug__: self._log(1, 'CAPABILITY: %r' % (self.capabilities,)) 387 388 for version in AllowedVersions: 389 if not version in self.capabilities: 390 continue 391 self.PROTOCOL_VERSION = version 392 break 393 else: 394 raise self.error('server not IMAP4 compliant') 395 except: 396 self._close_threads() 397 raise 398 399 400 def __getattr__(self, attr): 401 # Allow UPPERCASE variants of IMAP4 command methods. 402 if attr in Commands: 403 return getattr(self, attr.lower()) 404 raise AttributeError("Unknown IMAP4 command: '%s'" % attr) 405 406 407 def __enter__(self): 408 return self 409 410 def __exit__(self, *args): 411 try: 412 self.logout() 413 except OSError: 414 pass 415 416 417 def _mode_ascii(self): 418 self.utf8_enabled = False 419 self._encoding = 'ascii' 420 self.literal_cre = re.compile(self._literal, re.ASCII) 421 self.untagged_status_cre = re.compile(self._untagged_status, re.ASCII) 422 423 424 def _mode_utf8(self): 425 self.utf8_enabled = True 426 self._encoding = 'utf-8' 427 self.literal_cre = re.compile(self._literal) 428 self.untagged_status_cre = re.compile(self._untagged_status) 429 430 431 432 # Overridable methods 433 434 435 def open(self, host=None, port=None): 436 """open(host=None, port=None) 437 Setup connection to remote server on "host:port" 438 (default: localhost:standard IMAP4 port). 439 This connection will be used by the routines: 440 read, send, shutdown, socket.""" 441 442 self.host = self._choose_nonull_or_dflt('', host) 443 self.port = self._choose_nonull_or_dflt(IMAP4_PORT, port) 444 self.sock = self.open_socket() 445 self.read_fd = self.sock.fileno() 446 447 448 def open_socket(self): 449 """open_socket() 450 Open socket choosing first address family available.""" 451 452 return socket.create_connection((self.host, self.port)) 453 454 455 def ssl_wrap_socket(self): 456 457 try: 458 import ssl 459 460 TLS_MAP = {} 461 if hasattr(ssl, "PROTOCOL_TLSv1_2"): 462 TLS_MAP[TLS_SECURE] = { 463 "tls1_2": ssl.PROTOCOL_TLSv1_2, 464 "tls1_1": ssl.PROTOCOL_TLSv1_1, 465 } 466 else: 467 TLS_MAP[TLS_SECURE] = {} 468 TLS_MAP[TLS_NO_SSL] = TLS_MAP[TLS_SECURE].copy() 469 TLS_MAP[TLS_NO_SSL].update({ 470 "tls1": ssl.PROTOCOL_TLSv1, 471 }) 472 TLS_MAP[TLS_COMPAT] = TLS_MAP[TLS_NO_SSL].copy() 473 TLS_MAP[TLS_COMPAT].update({ 474 "ssl23": ssl.PROTOCOL_SSLv23, 475 None: ssl.PROTOCOL_SSLv23, 476 }) 477 if hasattr(ssl, "PROTOCOL_SSLv3"): # Might not be available. 478 TLS_MAP[TLS_COMPAT].update({ 479 "ssl3": ssl.PROTOCOL_SSLv3 480 }) 481 482 if self.ca_certs is not None: 483 cert_reqs = ssl.CERT_REQUIRED 484 else: 485 cert_reqs = ssl.CERT_NONE 486 487 if self.tls_level not in TLS_MAP: 488 raise RuntimeError("unknown tls_level: %s" % self.tls_level) 489 490 if self.ssl_version not in TLS_MAP[self.tls_level]: 491 raise socket.sslerror("Invalid SSL version '%s' requested for tls_version '%s'" % (self.ssl_version, self.tls_level)) 492 493 ssl_version = TLS_MAP[self.tls_level][self.ssl_version] 494 495 self.sock = ssl.wrap_socket(self.sock, self.keyfile, self.certfile, ca_certs=self.ca_certs, cert_reqs=cert_reqs, ssl_version=ssl_version) 496 ssl_exc = ssl.SSLError 497 self.read_fd = self.sock.fileno() 498 except ImportError: 499 # No ssl module, and socket.ssl has no fileno(), and does not allow certificate verification 500 raise socket.sslerror("imaplib SSL mode does not work without ssl module") 501 502 if self.cert_verify_cb is not None: 503 cert_err = self.cert_verify_cb(self.sock.getpeercert(), self.host) 504 if cert_err: 505 raise ssl_exc(cert_err) 506 507 # Allow sending of keep-alive messages - seems to prevent some servers 508 # from closing SSL, leading to deadlocks. 509 self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) 510 511 512 513 def start_compressing(self): 514 """start_compressing() 515 Enable deflate compression on the socket (RFC 4978).""" 516 517 # rfc 1951 - pure DEFLATE, so use -15 for both windows 518 self.decompressor = zlib.decompressobj(-15) 519 self.compressor = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -15) 520 521 522 def read(self, size): 523 """data = read(size) 524 Read at most 'size' bytes from remote.""" 525 526 if self.decompressor is None: 527 return self.sock.recv(size) 528 529 if self.decompressor.unconsumed_tail: 530 data = self.decompressor.unconsumed_tail 531 else: 532 data = self.sock.recv(READ_SIZE) 533 534 return self.decompressor.decompress(data, size) 535 536 537 def send(self, data): 538 """send(data) 539 Send 'data' to remote.""" 540 541 if self.compressor is not None: 542 data = self.compressor.compress(data) 543 data += self.compressor.flush(zlib.Z_SYNC_FLUSH) 544 545 self.sock.sendall(data) 546 547 548 def shutdown(self): 549 """shutdown() 550 Close I/O established in "open".""" 551 552 try: 553 self.sock.shutdown(socket.SHUT_RDWR) 554 except Exception as e: 555 # The server might already have closed the connection 556 if e.errno != errno.ENOTCONN: 557 raise 558 finally: 559 self.sock.close() 560 561 562 def socket(self): 563 """socket = socket() 564 Return socket instance used to connect to IMAP4 server.""" 565 566 return self.sock 567 568 569 570 # Utility methods 571 572 573 def enable_compression(self): 574 """enable_compression() 575 Ask the server to start compressing the connection. 576 Should be called from user of this class after instantiation, as in: 577 if 'COMPRESS=DEFLATE' in imapobj.capabilities: 578 imapobj.enable_compression()""" 579 580 try: 581 typ, dat = self._simple_command('COMPRESS', 'DEFLATE') 582 if typ == 'OK': 583 self.start_compressing() 584 if __debug__: self._log(1, 'Enabled COMPRESS=DEFLATE') 585 finally: 586 self._release_state_change() 587 588 589 def pop_untagged_responses(self): 590 """ for typ,data in pop_untagged_responses(): pass 591 Generator for any remaining untagged responses. 592 Returns and removes untagged responses in order of reception. 593 Use at your own risk!""" 594 595 while self.untagged_responses: 596 self.commands_lock.acquire() 597 try: 598 yield self.untagged_responses.pop(0) 599 finally: 600 self.commands_lock.release() 601 602 603 def recent(self, **kw): 604 """(typ, [data]) = recent() 605 Return 'RECENT' responses if any exist, 606 else prompt server for an update using the 'NOOP' command. 607 'data' is None if no new messages, 608 else list of RECENT responses, most recent last.""" 609 610 name = 'RECENT' 611 typ, dat = self._untagged_response(None, [None], name) 612 if dat != [None]: 613 return self._deliver_dat(typ, dat, kw) 614 kw['untagged_response'] = name 615 return self.noop(**kw) # Prod server for response 616 617 618 def response(self, code, **kw): 619 """(code, [data]) = response(code) 620 Return data for response 'code' if received, or None. 621 Old value for response 'code' is cleared.""" 622 623 typ, dat = self._untagged_response(code, [None], code.upper()) 624 return self._deliver_dat(typ, dat, kw) 625 626 627 628 629 # IMAP4 commands 630 631 632 def append(self, mailbox, flags, date_time, message, **kw): 633 """(typ, [data]) = append(mailbox, flags, date_time, message) 634 Append message to named mailbox. 635 All args except `message' can be None.""" 636 637 name = 'APPEND' 638 if not mailbox: 639 mailbox = 'INBOX' 640 if flags: 641 if (flags[0],flags[-1]) != ('(',')'): 642 flags = '(%s)' % flags 643 else: 644 flags = None 645 if date_time: 646 date_time = Time2Internaldate(date_time) 647 else: 648 date_time = None 649 if isinstance(message, str): 650 message = bytes(message, 'ASCII') 651 literal = self.mapCRLF_cre.sub(CRLF, message) 652 if self.utf8_enabled: 653 literal = b'UTF8 (' + literal + b')' 654 self.literal = literal 655 try: 656 return self._simple_command(name, mailbox, flags, date_time, **kw) 657 finally: 658 self._release_state_change() 659 660 661 def authenticate(self, mechanism, authobject, **kw): 662 """(typ, [data]) = authenticate(mechanism, authobject) 663 Authenticate command - requires response processing. 664 665 'mechanism' specifies which authentication mechanism is to 666 be used - it must appear in <instance>.capabilities in the 667 form AUTH=<mechanism>. 668 669 'authobject' must be a callable object: 670 671 data = authobject(response) 672 673 It will be called to process server continuation responses, 674 the 'response' argument will be a 'bytes'. It should return 675 bytes that will be encoded and sent to server. It should 676 return None if the client abort response '*' should be sent 677 instead.""" 678 679 self.literal = _Authenticator(authobject).process 680 try: 681 typ, dat = self._simple_command('AUTHENTICATE', mechanism.upper()) 682 if typ != 'OK': 683 self._deliver_exc(self.error, dat[-1], kw) 684 self.state = AUTH 685 if __debug__: self._log(1, 'state => AUTH') 686 finally: 687 self._release_state_change() 688 return self._deliver_dat(typ, dat, kw) 689 690 691 def capability(self, **kw): 692 """(typ, [data]) = capability() 693 Fetch capabilities list from server.""" 694 695 name = 'CAPABILITY' 696 kw['untagged_response'] = name 697 return self._simple_command(name, **kw) 698 699 700 def check(self, **kw): 701 """(typ, [data]) = check() 702 Checkpoint mailbox on server.""" 703 704 return self._simple_command('CHECK', **kw) 705 706 707 def close(self, **kw): 708 """(typ, [data]) = close() 709 Close currently selected mailbox. 710 711 Deleted messages are removed from writable mailbox. 712 This is the recommended command before 'LOGOUT'.""" 713 714 if self.state != 'SELECTED': 715 raise self.error('No mailbox selected.') 716 try: 717 typ, dat = self._simple_command('CLOSE') 718 finally: 719 self.state = AUTH 720 if __debug__: self._log(1, 'state => AUTH') 721 self._release_state_change() 722 return self._deliver_dat(typ, dat, kw) 723 724 725 def copy(self, message_set, new_mailbox, **kw): 726 """(typ, [data]) = copy(message_set, new_mailbox) 727 Copy 'message_set' messages onto end of 'new_mailbox'.""" 728 729 return self._simple_command('COPY', message_set, new_mailbox, **kw) 730 731 732 def create(self, mailbox, **kw): 733 """(typ, [data]) = create(mailbox) 734 Create new mailbox.""" 735 736 return self._simple_command('CREATE', mailbox, **kw) 737 738 739 def delete(self, mailbox, **kw): 740 """(typ, [data]) = delete(mailbox) 741 Delete old mailbox.""" 742 743 return self._simple_command('DELETE', mailbox, **kw) 744 745 746 def deleteacl(self, mailbox, who, **kw): 747 """(typ, [data]) = deleteacl(mailbox, who) 748 Delete the ACLs (remove any rights) set for who on mailbox.""" 749 750 return self._simple_command('DELETEACL', mailbox, who, **kw) 751 752 753 def enable(self, capability): 754 """Send an RFC5161 enable string to the server. 755 756 (typ, [data]) = <intance>.enable(capability) 757 """ 758 if 'ENABLE' not in self.capabilities: 759 raise self.error("Server does not support ENABLE") 760 typ, data = self._simple_command('ENABLE', capability) 761 if typ == 'OK' and 'UTF8=ACCEPT' in capability.upper(): 762 self._mode_utf8() 763 return typ, data 764 765 766 def examine(self, mailbox='INBOX', **kw): 767 """(typ, [data]) = examine(mailbox='INBOX') 768 Select a mailbox for READ-ONLY access. (Flushes all untagged responses.) 769 'data' is count of messages in mailbox ('EXISTS' response). 770 Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so 771 other responses should be obtained via "response('FLAGS')" etc.""" 772 773 return self.select(mailbox=mailbox, readonly=True, **kw) 774 775 776 def expunge(self, **kw): 777 """(typ, [data]) = expunge() 778 Permanently remove deleted items from selected mailbox. 779 Generates 'EXPUNGE' response for each deleted message. 780 'data' is list of 'EXPUNGE'd message numbers in order received.""" 781 782 name = 'EXPUNGE' 783 kw['untagged_response'] = name 784 return self._simple_command(name, **kw) 785 786 787 def fetch(self, message_set, message_parts, **kw): 788 """(typ, [data, ...]) = fetch(message_set, message_parts) 789 Fetch (parts of) messages. 790 'message_parts' should be a string of selected parts 791 enclosed in parentheses, eg: "(UID BODY[TEXT])". 792 'data' are tuples of message part envelope and data, 793 followed by a string containing the trailer.""" 794 795 name = 'FETCH' 796 kw['untagged_response'] = name 797 return self._simple_command(name, message_set, message_parts, **kw) 798 799 800 def getacl(self, mailbox, **kw): 801 """(typ, [data]) = getacl(mailbox) 802 Get the ACLs for a mailbox.""" 803 804 kw['untagged_response'] = 'ACL' 805 return self._simple_command('GETACL', mailbox, **kw) 806 807 808 def getannotation(self, mailbox, entry, attribute, **kw): 809 """(typ, [data]) = getannotation(mailbox, entry, attribute) 810 Retrieve ANNOTATIONs.""" 811 812 kw['untagged_response'] = 'ANNOTATION' 813 return self._simple_command('GETANNOTATION', mailbox, entry, attribute, **kw) 814 815 816 def getquota(self, root, **kw): 817 """(typ, [data]) = getquota(root) 818 Get the quota root's resource usage and limits. 819 (Part of the IMAP4 QUOTA extension defined in rfc2087.)""" 820 821 kw['untagged_response'] = 'QUOTA' 822 return self._simple_command('GETQUOTA', root, **kw) 823 824 825 def getquotaroot(self, mailbox, **kw): 826 # Hmmm, this is non-std! Left for backwards-compatibility, sigh. 827 # NB: usage should have been defined as: 828 # (typ, [QUOTAROOT responses...]) = getquotaroot(mailbox) 829 # (typ, [QUOTA responses...]) = response('QUOTA') 830 """(typ, [[QUOTAROOT responses...], [QUOTA responses...]]) = getquotaroot(mailbox) 831 Get the list of quota roots for the named mailbox.""" 832 833 typ, dat = self._simple_command('GETQUOTAROOT', mailbox) 834 typ, quota = self._untagged_response(typ, dat, 'QUOTA') 835 typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT') 836 return self._deliver_dat(typ, [quotaroot, quota], kw) 837 838 839 def id(self, *kv_pairs, **kw): 840 """(typ, [data]) = <instance>.id(kv_pairs) 841 'kv_pairs' is a possibly empty list of keys and values. 842 'data' is a list of ID key value pairs or NIL. 843 NB: a single argument is assumed to be correctly formatted and is passed through unchanged 844 (for backward compatibility with earlier version). 845 Exchange information for problem analysis and determination. 846 The ID extension is defined in RFC 2971. """ 847 848 name = 'ID' 849 kw['untagged_response'] = name 850 851 if not kv_pairs: 852 data = 'NIL' 853 elif len(kv_pairs) == 1: 854 data = kv_pairs[0] # Assume invoker passing correctly formatted string (back-compat) 855 else: 856 data = '(%s)' % ' '.join([(arg and self._quote(arg) or 'NIL') for arg in kv_pairs]) 857 858 return self._simple_command(name, data, **kw) 859 860 861 def idle(self, timeout=None, **kw): 862 """"(typ, [data]) = idle(timeout=None) 863 Put server into IDLE mode until server notifies some change, 864 or 'timeout' (secs) occurs (default: 29 minutes), 865 or another IMAP4 command is scheduled.""" 866 867 name = 'IDLE' 868 self.literal = _IdleCont(self, timeout).process 869 try: 870 return self._simple_command(name, **kw) 871 finally: 872 self._release_state_change() 873 874 875 def list(self, directory='""', pattern='*', **kw): 876 """(typ, [data]) = list(directory='""', pattern='*') 877 List mailbox names in directory matching pattern. 878 'data' is list of LIST responses. 879 880 NB: for 'pattern': 881 % matches all except separator ( so LIST "" "%" returns names at root) 882 * matches all (so LIST "" "*" returns whole directory tree from root)""" 883 884 name = 'LIST' 885 kw['untagged_response'] = name 886 return self._simple_command(name, directory, pattern, **kw) 887 888 889 def login(self, user, password, **kw): 890 """(typ, [data]) = login(user, password) 891 Identify client using plaintext password. 892 NB: 'password' will be quoted.""" 893 894 try: 895 typ, dat = self._simple_command('LOGIN', user, self._quote(password)) 896 if typ != 'OK': 897 self._deliver_exc(self.error, dat[-1], kw) 898 self.state = AUTH 899 if __debug__: self._log(1, 'state => AUTH') 900 finally: 901 self._release_state_change() 902 return self._deliver_dat(typ, dat, kw) 903 904 905 def login_cram_md5(self, user, password, **kw): 906 """(typ, [data]) = login_cram_md5(user, password) 907 Force use of CRAM-MD5 authentication.""" 908 909 self.user, self.password = user, password 910 return self.authenticate('CRAM-MD5', self._CRAM_MD5_AUTH, **kw) 911 912 913 def _CRAM_MD5_AUTH(self, challenge): 914 """Authobject to use with CRAM-MD5 authentication.""" 915 import hmac 916 pwd = (self.password.encode('utf-8') if isinstance(self.password, str) 917 else self.password) 918 return self.user + " " + hmac.HMAC(pwd, challenge, 'md5').hexdigest() 919 920 921 def logout(self, **kw): 922 """(typ, [data]) = logout() 923 Shutdown connection to server. 924 Returns server 'BYE' response. 925 NB: You must call this to shut down threads before discarding an instance.""" 926 927 self.state = LOGOUT 928 if __debug__: self._log(1, 'state => LOGOUT') 929 930 try: 931 try: 932 typ, dat = self._simple_command('LOGOUT') 933 except: 934 typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]] 935 if __debug__: self._log(1, dat) 936 937 self._close_threads() 938 finally: 939 self._release_state_change() 940 941 if __debug__: self._log(1, 'connection closed') 942 943 bye = self._get_untagged_response('BYE', leave=True) 944 if bye: 945 typ, dat = 'BYE', bye 946 return self._deliver_dat(typ, dat, kw) 947 948 949 def lsub(self, directory='""', pattern='*', **kw): 950 """(typ, [data, ...]) = lsub(directory='""', pattern='*') 951 List 'subscribed' mailbox names in directory matching pattern. 952 'data' are tuples of message part envelope and data.""" 953 954 name = 'LSUB' 955 kw['untagged_response'] = name 956 return self._simple_command(name, directory, pattern, **kw) 957 958 959 def myrights(self, mailbox, **kw): 960 """(typ, [data]) = myrights(mailbox) 961 Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).""" 962 963 name = 'MYRIGHTS' 964 kw['untagged_response'] = name 965 return self._simple_command(name, mailbox, **kw) 966 967 968 def namespace(self, **kw): 969 """(typ, [data, ...]) = namespace() 970 Returns IMAP namespaces ala rfc2342.""" 971 972 name = 'NAMESPACE' 973 kw['untagged_response'] = name 974 return self._simple_command(name, **kw) 975 976 977 def noop(self, **kw): 978 """(typ, [data]) = noop() 979 Send NOOP command.""" 980 981 if __debug__: self._dump_ur(3) 982 return self._simple_command('NOOP', **kw) 983 984 985 def partial(self, message_num, message_part, start, length, **kw): 986 """(typ, [data, ...]) = partial(message_num, message_part, start, length) 987 Fetch truncated part of a message. 988 'data' is tuple of message part envelope and data. 989 NB: obsolete.""" 990 991 name = 'PARTIAL' 992 kw['untagged_response'] = 'FETCH' 993 return self._simple_command(name, message_num, message_part, start, length, **kw) 994 995 996 def proxyauth(self, user, **kw): 997 """(typ, [data]) = proxyauth(user) 998 Assume authentication as 'user'. 999 (Allows an authorised administrator to proxy into any user's mailbox.)""" 1000 1001 try: 1002 return self._simple_command('PROXYAUTH', user, **kw) 1003 finally: 1004 self._release_state_change() 1005 1006 1007 def rename(self, oldmailbox, newmailbox, **kw): 1008 """(typ, [data]) = rename(oldmailbox, newmailbox) 1009 Rename old mailbox name to new.""" 1010 1011 return self._simple_command('RENAME', oldmailbox, newmailbox, **kw) 1012 1013 1014 def search(self, charset, *criteria, **kw): 1015 """(typ, [data]) = search(charset, criterion, ...) 1016 Search mailbox for matching messages. 1017 If UTF8 is enabled, charset MUST be None. 1018 'data' is space separated list of matching message numbers.""" 1019 1020 name = 'SEARCH' 1021 kw['untagged_response'] = name 1022 if charset: 1023 if self.utf8_enabled: 1024 raise self.error("Non-None charset not valid in UTF8 mode") 1025 return self._simple_command(name, 'CHARSET', charset, *criteria, **kw) 1026 return self._simple_command(name, *criteria, **kw) 1027 1028 1029 def select(self, mailbox='INBOX', readonly=False, **kw): 1030 """(typ, [data]) = select(mailbox='INBOX', readonly=False) 1031 Select a mailbox. (Flushes all untagged responses.) 1032 'data' is count of messages in mailbox ('EXISTS' response). 1033 Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so 1034 other responses should be obtained via "response('FLAGS')" etc.""" 1035 1036 self.mailbox = mailbox 1037 1038 self.is_readonly = bool(readonly) 1039 if readonly: 1040 name = 'EXAMINE' 1041 else: 1042 name = 'SELECT' 1043 try: 1044 rqb = self._command(name, mailbox) 1045 typ, dat = rqb.get_response('command: %s => %%s' % rqb.name) 1046 if typ != 'OK': 1047 if self.state == SELECTED: 1048 self.state = AUTH 1049 if __debug__: self._log(1, 'state => AUTH') 1050 if typ == 'BAD': 1051 self._deliver_exc(self.error, '%s command error: %s %s. Data: %.100s' % (name, typ, dat, mailbox), kw) 1052 return self._deliver_dat(typ, dat, kw) 1053 self.state = SELECTED 1054 if __debug__: self._log(1, 'state => SELECTED') 1055 finally: 1056 self._release_state_change() 1057 1058 if self._get_untagged_response('READ-ONLY', leave=True) and not readonly: 1059 if __debug__: self._dump_ur(1) 1060 self._deliver_exc(self.readonly, '%s is not writable' % mailbox, kw) 1061 typ, dat = self._untagged_response(typ, [None], 'EXISTS') 1062 return self._deliver_dat(typ, dat, kw) 1063 1064 1065 def setacl(self, mailbox, who, what, **kw): 1066 """(typ, [data]) = setacl(mailbox, who, what) 1067 Set a mailbox acl.""" 1068 1069 try: 1070 return self._simple_command('SETACL', mailbox, who, what, **kw) 1071 finally: 1072 self._release_state_change() 1073 1074 1075 def setannotation(self, *args, **kw): 1076 """(typ, [data]) = setannotation(mailbox[, entry, attribute]+) 1077 Set ANNOTATIONs.""" 1078 1079 kw['untagged_response'] = 'ANNOTATION' 1080 return self._simple_command('SETANNOTATION', *args, **kw) 1081 1082 1083 def setquota(self, root, limits, **kw): 1084 """(typ, [data]) = setquota(root, limits) 1085 Set the quota root's resource limits.""" 1086 1087 kw['untagged_response'] = 'QUOTA' 1088 try: 1089 return self._simple_command('SETQUOTA', root, limits, **kw) 1090 finally: 1091 self._release_state_change() 1092 1093 1094 def sort(self, sort_criteria, charset, *search_criteria, **kw): 1095 """(typ, [data]) = sort(sort_criteria, charset, search_criteria, ...) 1096 IMAP4rev1 extension SORT command.""" 1097 1098 name = 'SORT' 1099 if (sort_criteria[0],sort_criteria[-1]) != ('(',')'): 1100 sort_criteria = '(%s)' % sort_criteria 1101 kw['untagged_response'] = name 1102 return self._simple_command(name, sort_criteria, charset, *search_criteria, **kw) 1103 1104 1105 def starttls(self, keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, ssl_version="ssl23", tls_level=TLS_COMPAT, **kw): 1106 """(typ, [data]) = starttls(keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, ssl_version="ssl23", tls_level="tls_compat") 1107 Start TLS negotiation as per RFC 2595.""" 1108 1109 name = 'STARTTLS' 1110 1111 if name not in self.capabilities: 1112 raise self.abort('TLS not supported by server') 1113 1114 if self._tls_established: 1115 raise self.abort('TLS session already established') 1116 1117 # Must now shutdown reader thread after next response, and restart after changing read_fd 1118 1119 self.read_size = 1 # Don't consume TLS handshake 1120 self.TerminateReader = True 1121 1122 try: 1123 typ, dat = self._simple_command(name) 1124 finally: 1125 self._release_state_change() 1126 self.rdth.join() 1127 self.TerminateReader = False 1128 self.read_size = READ_SIZE 1129 1130 if typ != 'OK': 1131 # Restart reader thread and error 1132 self.rdth = threading.Thread(target=self._reader) 1133 self.rdth.setDaemon(True) 1134 self.rdth.start() 1135 raise self.error("Couldn't establish TLS session: %s" % dat) 1136 1137 self.keyfile = keyfile 1138 self.certfile = certfile 1139 self.ca_certs = ca_certs 1140 self.cert_verify_cb = cert_verify_cb 1141 self.ssl_version = ssl_version 1142 self.tls_level = tls_level 1143 1144 try: 1145 self.ssl_wrap_socket() 1146 finally: 1147 # Restart reader thread 1148 self.rdth = threading.Thread(target=self._reader) 1149 self.rdth.setDaemon(True) 1150 self.rdth.start() 1151 1152 self._get_capabilities() 1153 1154 self._tls_established = True 1155 1156 typ, dat = self._untagged_response(typ, dat, name) 1157 return self._deliver_dat(typ, dat, kw) 1158 1159 1160 def status(self, mailbox, names, **kw): 1161 """(typ, [data]) = status(mailbox, names) 1162 Request named status conditions for mailbox.""" 1163 1164 name = 'STATUS' 1165 kw['untagged_response'] = name 1166 return self._simple_command(name, mailbox, names, **kw) 1167 1168 1169 def store(self, message_set, command, flags, **kw): 1170 """(typ, [data]) = store(message_set, command, flags) 1171 Alters flag dispositions for messages in mailbox.""" 1172 1173 if (flags[0],flags[-1]) != ('(',')'): 1174 flags = '(%s)' % flags # Avoid quoting the flags 1175 kw['untagged_response'] = 'FETCH' 1176 return self._simple_command('STORE', message_set, command, flags, **kw) 1177 1178 1179 def subscribe(self, mailbox, **kw): 1180 """(typ, [data]) = subscribe(mailbox) 1181 Subscribe to new mailbox.""" 1182 1183 try: 1184 return self._simple_command('SUBSCRIBE', mailbox, **kw) 1185 finally: 1186 self._release_state_change() 1187 1188 1189 def thread(self, threading_algorithm, charset, *search_criteria, **kw): 1190 """(type, [data]) = thread(threading_alogrithm, charset, search_criteria, ...) 1191 IMAPrev1 extension THREAD command.""" 1192 1193 name = 'THREAD' 1194 kw['untagged_response'] = name 1195 return self._simple_command(name, threading_algorithm, charset, *search_criteria, **kw) 1196 1197 1198 def uid(self, command, *args, **kw): 1199 """(typ, [data]) = uid(command, arg, ...) 1200 Execute "command arg ..." with messages identified by UID, 1201 rather than message number. 1202 Assumes 'command' is legal in current state. 1203 Returns response appropriate to 'command'.""" 1204 1205 command = command.upper() 1206 if command in UID_direct: 1207 resp = command 1208 else: 1209 resp = 'FETCH' 1210 kw['untagged_response'] = resp 1211 return self._simple_command('UID', command, *args, **kw) 1212 1213 1214 def unsubscribe(self, mailbox, **kw): 1215 """(typ, [data]) = unsubscribe(mailbox) 1216 Unsubscribe from old mailbox.""" 1217 1218 try: 1219 return self._simple_command('UNSUBSCRIBE', mailbox, **kw) 1220 finally: 1221 self._release_state_change() 1222 1223 1224 def xatom(self, name, *args, **kw): 1225 """(typ, [data]) = xatom(name, arg, ...) 1226 Allow simple extension commands notified by server in CAPABILITY response. 1227 Assumes extension command 'name' is legal in current state. 1228 Returns response appropriate to extension command 'name'.""" 1229 1230 name = name.upper() 1231 if not name in Commands: 1232 Commands[name] = ((self.state,), False) 1233 try: 1234 return self._simple_command(name, *args, **kw) 1235 finally: 1236 self._release_state_change() 1237 1238 1239 1240 # Internal methods 1241 1242 1243 def _append_untagged(self, typ, dat): 1244 1245 # Append new 'dat' to end of last untagged response if same 'typ', 1246 # else append new response. 1247 1248 if dat is None: dat = b'' 1249 1250 self.commands_lock.acquire() 1251 1252 if self.untagged_responses: 1253 urn, urd = self.untagged_responses[-1] 1254 if urn != typ: 1255 urd = None 1256 else: 1257 urd = None 1258 1259 if urd is None: 1260 urd = [] 1261 self.untagged_responses.append([typ, urd]) 1262 1263 urd.append(dat) 1264 1265 self.commands_lock.release() 1266 1267 if __debug__: self._log(5, 'untagged_responses[%s] %s += ["%.80r"]' % (typ, len(urd)-1, dat)) 1268 1269 1270 def _check_bye(self): 1271 1272 bye = self._get_untagged_response('BYE', leave=True) 1273 if bye: 1274 raise self.abort(bye[-1].decode('ASCII', 'replace')) 1275 1276 1277 def _choose_nonull_or_dflt(self, dflt, *args): 1278 if isinstance(dflt, str): 1279 dflttyp = str # Allow any string type 1280 else: 1281 dflttyp = type(dflt) 1282 for arg in args: 1283 if arg is not None: 1284 if isinstance(arg, dflttyp): 1285 return arg 1286 if __debug__: self._log(0, 'bad arg is %s, expecting %s' % (type(arg), dflttyp)) 1287 return dflt 1288 1289 1290 def _command(self, name, *args, **kw): 1291 1292 if Commands[name][CMD_VAL_ASYNC]: 1293 cmdtyp = 'async' 1294 else: 1295 cmdtyp = 'sync' 1296 1297 if __debug__: self._log(1, '[%s] %s %s' % (cmdtyp, name, args)) 1298 1299 if __debug__: self._log(3, 'state_change_pending.acquire') 1300 self.state_change_pending.acquire() 1301 1302 self._end_idle() 1303 1304 if cmdtyp == 'async': 1305 self.state_change_pending.release() 1306 if __debug__: self._log(3, 'state_change_pending.release') 1307 else: 1308 # Need to wait for all async commands to complete 1309 self._check_bye() 1310 self.commands_lock.acquire() 1311 if self.tagged_commands: 1312 self.state_change_free.clear() 1313 need_event = True 1314 else: 1315 need_event = False 1316 self.commands_lock.release() 1317 if need_event: 1318 if __debug__: self._log(3, 'sync command %s waiting for empty commands Q' % name) 1319 self.state_change_free.wait(threading.TIMEOUT_MAX) 1320 if __debug__: self._log(3, 'sync command %s proceeding' % name) 1321 1322 if self.state not in Commands[name][CMD_VAL_STATES]: 1323 self.literal = None 1324 raise self.error('command %s illegal in state %s' 1325 % (name, self.state)) 1326 1327 self._check_bye() 1328 1329 if name in ('EXAMINE', 'SELECT'): 1330 self.commands_lock.acquire() 1331 self.untagged_responses = [] # Flush all untagged responses 1332 self.commands_lock.release() 1333 else: 1334 for typ in ('OK', 'NO', 'BAD'): 1335 while self._get_untagged_response(typ): 1336 continue 1337 1338 if not self.is_readonly and self._get_untagged_response('READ-ONLY', leave=True): 1339 self.literal = None 1340 raise self.readonly('mailbox status changed to READ-ONLY') 1341 1342 if self.Terminate: 1343 raise self.abort('connection closed') 1344 1345 rqb = self._request_push(name=name, **kw) 1346 1347 name = bytes(name, self._encoding) 1348 data = rqb.tag + b' ' + name 1349 for arg in args: 1350 if arg is None: continue 1351 if isinstance(arg, str): 1352 arg = bytes(arg, self._encoding) 1353 data = data + b' ' + arg 1354 1355 literal = self.literal 1356 if literal is not None: 1357 self.literal = None 1358 if type(literal) is type(self._command): 1359 literator = literal 1360 else: 1361 literator = None 1362 data = data + bytes(' {%s}' % len(literal), self._encoding) 1363 1364 if __debug__: self._log(4, 'data=%r' % data) 1365 1366 rqb.data = data + CRLF 1367 1368 if literal is None: 1369 self.ouq.put(rqb) 1370 return rqb 1371 1372 # Must setup continuation expectancy *before* ouq.put 1373 crqb = self._request_push(name=name, tag='continuation') 1374 1375 self.ouq.put(rqb) 1376 1377 while True: 1378 # Wait for continuation response 1379 1380 ok, data = crqb.get_response('command: %s => %%s' % name) 1381 if __debug__: self._log(4, 'continuation => %s, %r' % (ok, data)) 1382 1383 # NO/BAD response? 1384 1385 if not ok: 1386 break 1387 1388 if data == 'go ahead': # Apparently not uncommon broken IMAP4 server response to AUTHENTICATE command 1389 data = '' 1390 1391 # Send literal 1392 1393 if literator is not None: 1394 literal = literator(data, rqb) 1395 1396 if literal is None: 1397 break 1398 1399 if literator is not None: 1400 # Need new request for next continuation response 1401 crqb = self._request_push(name=name, tag='continuation') 1402 1403 if __debug__: self._log(4, 'write literal size %s' % len(literal)) 1404 crqb.data = literal + CRLF 1405 self.ouq.put(crqb) 1406 1407 if literator is None: 1408 break 1409 1410 return rqb 1411 1412 1413 def _command_complete(self, rqb, kw): 1414 1415 # Called for non-callback commands 1416 1417 self._check_bye() 1418 typ, dat = rqb.get_response('command: %s => %%s' % rqb.name) 1419 if typ == 'BAD': 1420 if __debug__: self._print_log() 1421 raise self.error('%s command error: %s %s. Data: %.100s' % (rqb.name, typ, dat, rqb.data)) 1422 if 'untagged_response' in kw: 1423 return self._untagged_response(typ, dat, kw['untagged_response']) 1424 return typ, dat 1425 1426 1427 def _command_completer(self, cb_arg_list): 1428 1429 # Called for callback commands 1430 response, cb_arg, error = cb_arg_list 1431 rqb, kw = cb_arg 1432 rqb.callback = kw['callback'] 1433 rqb.callback_arg = kw.get('cb_arg') 1434 if error is not None: 1435 if __debug__: self._print_log() 1436 typ, val = error 1437 rqb.abort(typ, val) 1438 return 1439 bye = self._get_untagged_response('BYE', leave=True) 1440 if bye: 1441 rqb.abort(self.abort, bye[-1].decode('ASCII', 'replace')) 1442 return 1443 typ, dat = response 1444 if typ == 'BAD': 1445 if __debug__: self._print_log() 1446 rqb.abort(self.error, '%s command error: %s %s. Data: %.100s' % (rqb.name, typ, dat, rqb.data)) 1447 return 1448 if __debug__: self._log(4, '_command_completer(%s, %s, None) = %s' % (response, cb_arg, rqb.tag)) 1449 if 'untagged_response' in kw: 1450 response = self._untagged_response(typ, dat, kw['untagged_response']) 1451 rqb.deliver(response) 1452 1453 1454 def _deliver_dat(self, typ, dat, kw): 1455 1456 if 'callback' in kw: 1457 kw['callback'](((typ, dat), kw.get('cb_arg'), None)) 1458 return typ, dat 1459 1460 1461 def _deliver_exc(self, exc, dat, kw): 1462 1463 if 'callback' in kw: 1464 kw['callback']((None, kw.get('cb_arg'), (exc, dat))) 1465 raise exc(dat) 1466 1467 1468 def _end_idle(self): 1469 1470 self.idle_lock.acquire() 1471 irqb = self.idle_rqb 1472 if irqb is None: 1473 self.idle_lock.release() 1474 return 1475 self.idle_rqb = None 1476 self.idle_timeout = None 1477 self.idle_lock.release() 1478 irqb.data = bytes('DONE', 'ASCII') + CRLF 1479 self.ouq.put(irqb) 1480 if __debug__: self._log(2, 'server IDLE finished') 1481 1482 1483 def _get_capabilities(self): 1484 typ, dat = self.capability() 1485 if dat == [None]: 1486 raise self.error('no CAPABILITY response from server') 1487 dat = str(dat[-1], "ASCII") 1488 dat = dat.upper() 1489 self.capabilities = tuple(dat.split()) 1490 1491 1492 def _get_untagged_response(self, name, leave=False): 1493 1494 self.commands_lock.acquire() 1495 1496 for i, (typ, dat) in enumerate(self.untagged_responses): 1497 if typ == name: 1498 if not leave: 1499 del self.untagged_responses[i] 1500 self.commands_lock.release() 1501 if __debug__: self._log(5, '_get_untagged_response(%s) => %.80r' % (name, dat)) 1502 return dat 1503 1504 self.commands_lock.release() 1505 return None 1506 1507 1508 def _match(self, cre, s): 1509 1510 # Run compiled regular expression 'cre' match method on 's'. 1511 # Save result, return success. 1512 1513 self.mo = cre.match(s) 1514 return self.mo is not None 1515 1516 1517 def _put_response(self, resp): 1518 1519 if self._expecting_data: 1520 rlen = len(resp) 1521 dlen = min(self._expecting_data_len, rlen) 1522 if __debug__: self._log(5, '_put_response expecting data len %s, got %s' % (self._expecting_data_len, rlen)) 1523 self._expecting_data_len -= dlen 1524 self._expecting_data = (self._expecting_data_len != 0) 1525 if rlen <= dlen: 1526 self._accumulated_data.append(resp) 1527 return 1528 self._accumulated_data.append(resp[:dlen]) 1529 resp = resp[dlen:] 1530 1531 if self._accumulated_data: 1532 typ, dat = self._literal_expected 1533 self._append_untagged(typ, (dat, b''.join(self._accumulated_data))) 1534 self._accumulated_data = [] 1535 1536 # Protocol mandates all lines terminated by CRLF 1537 resp = resp[:-2] 1538 if __debug__: self._log(5, '_put_response(%r)' % resp) 1539 1540 if 'continuation' in self.tagged_commands: 1541 continuation_expected = True 1542 else: 1543 continuation_expected = False 1544 1545 if self._literal_expected is not None: 1546 dat = resp 1547 if self._match(self.literal_cre, dat): 1548 self._literal_expected[1] = dat 1549 self._expecting_data = True 1550 self._expecting_data_len = int(self.mo.group('size')) 1551 if __debug__: self._log(4, 'expecting literal size %s' % self._expecting_data_len) 1552 return 1553 typ = self._literal_expected[0] 1554 self._literal_expected = None 1555 if dat: 1556 self._append_untagged(typ, dat) # Tail 1557 if __debug__: self._log(4, 'literal completed') 1558 else: 1559 # Command completion response? 1560 if self._match(self.tagre, resp): 1561 tag = self.mo.group('tag') 1562 typ = str(self.mo.group('type'), 'ASCII') 1563 dat = self.mo.group('data') 1564 if typ in ('OK', 'NO', 'BAD') and self._match(self.response_code_cre, dat): 1565 self._append_untagged(str(self.mo.group('type'), 'ASCII'), self.mo.group('data')) 1566 if not tag in self.tagged_commands: 1567 if __debug__: self._log(1, 'unexpected tagged response: %r' % resp) 1568 else: 1569 self._request_pop(tag, (typ, [dat])) 1570 else: 1571 dat2 = None 1572 1573 # '*' (untagged) responses? 1574 1575 if not self._match(self.untagged_response_cre, resp): 1576 if self._match(self.untagged_status_cre, resp): 1577 dat2 = self.mo.group('data2') 1578 1579 if self.mo is None: 1580 # Only other possibility is '+' (continuation) response... 1581 1582 if self._match(self.continuation_cre, resp): 1583 if not continuation_expected: 1584 if __debug__: self._log(1, "unexpected continuation response: '%r'" % resp) 1585 return 1586 self._request_pop('continuation', (True, self.mo.group('data'))) 1587 return 1588 1589 if __debug__: self._log(1, "unexpected response: '%r'" % resp) 1590 return 1591 1592 typ = str(self.mo.group('type'), 'ASCII') 1593 dat = self.mo.group('data') 1594 if dat is None: dat = b'' # Null untagged response 1595 if dat2: dat = dat + b' ' + dat2 1596 1597 # Is there a literal to come? 1598 1599 if self._match(self.literal_cre, dat): 1600 self._expecting_data = True 1601 self._expecting_data_len = int(self.mo.group('size')) 1602 if __debug__: self._log(4, 'read literal size %s' % self._expecting_data_len) 1603 self._literal_expected = [typ, dat] 1604 return 1605 1606 self._append_untagged(typ, dat) 1607 if typ in ('OK', 'NO', 'BAD') and self._match(self.response_code_cre, dat): 1608 self._append_untagged(str(self.mo.group('type'), 'ASCII'), self.mo.group('data')) 1609 1610 if typ != 'OK': # NO, BYE, IDLE 1611 self._end_idle() 1612 1613 # Command waiting for aborted continuation response? 1614 1615 if continuation_expected: 1616 self._request_pop('continuation', (False, resp)) 1617 1618 # Bad news? 1619 1620 if typ in ('NO', 'BAD', 'BYE'): 1621 if typ == 'BYE': 1622 self.Terminate = True 1623 if __debug__: self._log(1, '%s response: %r' % (typ, dat)) 1624 1625 1626 def _quote(self, arg): 1627 1628 return '"%s"' % arg.replace('\\', '\\\\').replace('"', '\\"') 1629 1630 1631 def _release_state_change(self): 1632 1633 if self.state_change_pending.locked(): 1634 self.state_change_pending.release() 1635 if __debug__: self._log(3, 'state_change_pending.release') 1636 1637 1638 def _request_pop(self, name, data): 1639 1640 self.commands_lock.acquire() 1641 rqb = self.tagged_commands.pop(name) 1642 if not self.tagged_commands: 1643 need_event = True 1644 else: 1645 need_event = False 1646 self.commands_lock.release() 1647 1648 if __debug__: self._log(4, '_request_pop(%s, %r) [%d] = %s' % (name, data, len(self.tagged_commands), rqb.tag)) 1649 rqb.deliver(data) 1650 1651 if need_event: 1652 if __debug__: self._log(3, 'state_change_free.set') 1653 self.state_change_free.set() 1654 1655 1656 def _request_push(self, tag=None, name=None, **kw): 1657 1658 self.commands_lock.acquire() 1659 rqb = Request(self, name=name, **kw) 1660 if tag is None: 1661 tag = rqb.tag 1662 self.tagged_commands[tag] = rqb 1663 self.commands_lock.release() 1664 if __debug__: self._log(4, '_request_push(%s, %s, %s) = %s' % (tag, name, repr(kw), rqb.tag)) 1665 return rqb 1666 1667 1668 def _simple_command(self, name, *args, **kw): 1669 1670 if 'callback' in kw: 1671 # Note: old calling sequence for back-compat with python <2.6 1672 self._command(name, callback=self._command_completer, cb_arg=kw, cb_self=True, *args) 1673 return (None, None) 1674 return self._command_complete(self._command(name, *args), kw) 1675 1676 1677 def _untagged_response(self, typ, dat, name): 1678 1679 if typ == 'NO': 1680 return typ, dat 1681 data = self._get_untagged_response(name) 1682 if not data: 1683 return typ, [None] 1684 while True: 1685 dat = self._get_untagged_response(name) 1686 if not dat: 1687 break 1688 data += dat 1689 if __debug__: self._log(4, '_untagged_response(%s, ?, %s) => %.80r' % (typ, name, data)) 1690 return typ, data 1691 1692 1693 1694 # Threads 1695 1696 1697 def _close_threads(self): 1698 1699 if __debug__: self._log(1, '_close_threads') 1700 1701 self.ouq.put(None) 1702 self.wrth.join() 1703 1704 if __debug__: self._log(1, 'call shutdown') 1705 1706 self.shutdown() 1707 1708 self.rdth.join() 1709 self.inth.join() 1710 1711 1712 def _handler(self): 1713 1714 resp_timeout = self.resp_timeout 1715 1716 threading.currentThread().setName(self.identifier + 'handler') 1717 1718 time.sleep(0.1) # Don't start handling before main thread ready 1719 1720 if __debug__: self._log(1, 'starting') 1721 1722 typ, val = self.abort, 'connection terminated' 1723 1724 while not self.Terminate: 1725 1726 self.idle_lock.acquire() 1727 if self.idle_timeout is not None: 1728 timeout = self.idle_timeout - time.time() 1729 if timeout <= 0: 1730 timeout = 1 1731 if __debug__: 1732 if self.idle_rqb is not None: 1733 self._log(5, 'server IDLING, timeout=%.2f' % timeout) 1734 else: 1735 timeout = resp_timeout 1736 self.idle_lock.release() 1737 1738 try: 1739 line = self.inq.get(True, timeout) 1740 except queue.Empty: 1741 if self.idle_rqb is None: 1742 if resp_timeout is not None and self.tagged_commands: 1743 if __debug__: self._log(1, 'response timeout') 1744 typ, val = self.abort, 'no response after %s secs' % resp_timeout 1745 break 1746 continue 1747 if self.idle_timeout > time.time(): 1748 continue 1749 if __debug__: self._log(2, 'server IDLE timedout') 1750 line = IDLE_TIMEOUT_RESPONSE 1751 1752 if line is None: 1753 if __debug__: self._log(1, 'inq None - terminating') 1754 break 1755 1756 if not isinstance(line, bytes): 1757 typ, val = line 1758 break 1759 1760 try: 1761 self._put_response(line) 1762 except: 1763 typ, val = self.error, 'program error: %s - %s' % sys.exc_info()[:2] 1764 break 1765 1766 self.Terminate = True 1767 1768 if __debug__: self._log(1, 'terminating: %s' % repr(val)) 1769 1770 while not self.ouq.empty(): 1771 try: 1772 qel = self.ouq.get_nowait() 1773 if qel is not None: 1774 qel.abort(typ, val) 1775 except queue.Empty: 1776 break 1777 self.ouq.put(None) 1778 1779 self.commands_lock.acquire() 1780 for name in list(self.tagged_commands.keys()): 1781 rqb = self.tagged_commands.pop(name) 1782 rqb.abort(typ, val) 1783 self.state_change_free.set() 1784 self.commands_lock.release() 1785 if __debug__: self._log(3, 'state_change_free.set') 1786 1787 if __debug__: self._log(1, 'finished') 1788 1789 1790 if hasattr(select_module, "poll"): 1791 1792 def _reader(self): 1793 1794 threading.currentThread().setName(self.identifier + 'reader') 1795 1796 if __debug__: self._log(1, 'starting using poll') 1797 1798 def poll_error(state): 1799 PollErrors = { 1800 select.POLLERR: 'Error', 1801 select.POLLHUP: 'Hang up', 1802 select.POLLNVAL: 'Invalid request: descriptor not open', 1803 } 1804 return ' '.join([PollErrors[s] for s in PollErrors.keys() if (s & state)]) 1805 1806 line_part = b'' 1807 1808 poll = select.poll() 1809 1810 poll.register(self.read_fd, select.POLLIN) 1811 1812 rxzero = 0 1813 terminate = False 1814 read_poll_timeout = self.read_poll_timeout * 1000 # poll() timeout is in millisecs 1815 1816 while not (terminate or self.Terminate): 1817 if self.state == LOGOUT: 1818 timeout = 10 1819 else: 1820 timeout = read_poll_timeout 1821 try: 1822 r = poll.poll(timeout) 1823 if __debug__: self._log(5, 'poll => %s' % repr(r)) 1824 if not r: 1825 continue # Timeout 1826 1827 fd,state = r[0] 1828 1829 if state & select.POLLIN: 1830 data = self.read(self.read_size) # Drain ssl buffer if present 1831 start = 0 1832 dlen = len(data) 1833 if __debug__: self._log(5, 'rcvd %s' % dlen) 1834 if dlen == 0: 1835 rxzero += 1 1836 if rxzero > 5: 1837 raise IOError("Too many read 0") 1838 time.sleep(0.1) 1839 continue # Try again 1840 rxzero = 0 1841 1842 while True: 1843 stop = data.find(b'\n', start) 1844 if stop < 0: 1845 line_part += data[start:] 1846 break 1847 stop += 1 1848 line_part, start, line = \ 1849 b'', stop, line_part + data[start:stop] 1850 if __debug__: self._log(4, '< %r' % line) 1851 self.inq.put(line) 1852 if self.TerminateReader: 1853 terminate = True 1854 1855 if state & ~(select.POLLIN): 1856 raise IOError(poll_error(state)) 1857 except: 1858 reason = 'socket error: %s - %s' % sys.exc_info()[:2] 1859 if __debug__: 1860 if not self.Terminate: 1861 self._print_log() 1862 if self.debug: self.debug += 4 # Output all 1863 self._log(1, reason) 1864 self.inq.put((self.abort, reason)) 1865 break 1866 1867 poll.unregister(self.read_fd) 1868 1869 if __debug__: self._log(1, 'finished') 1870 1871 else: 1872 1873 # No "poll" - use select() 1874 1875 def _reader(self): 1876 1877 threading.currentThread().setName(self.identifier + 'reader') 1878 1879 if __debug__: self._log(1, 'starting using select') 1880 1881 line_part = b'' 1882 1883 rxzero = 0 1884 terminate = False 1885 1886 while not (terminate or self.Terminate): 1887 if self.state == LOGOUT: 1888 timeout = 1 1889 else: 1890 timeout = self.read_poll_timeout 1891 try: 1892 r,w,e = select.select([self.read_fd], [], [], timeout) 1893 if __debug__: self._log(5, 'select => %s, %s, %s' % (r,w,e)) 1894 if not r: # Timeout 1895 continue 1896 1897 data = self.read(self.read_size) # Drain ssl buffer if present 1898 start = 0 1899 dlen = len(data) 1900 if __debug__: self._log(5, 'rcvd %s' % dlen) 1901 if dlen == 0: 1902 rxzero += 1 1903 if rxzero > 5: 1904 raise IOError("Too many read 0") 1905 time.sleep(0.1) 1906 continue # Try again 1907 rxzero = 0 1908 1909 while True: 1910 stop = data.find(b'\n', start) 1911 if stop < 0: 1912 line_part += data[start:] 1913 break 1914 stop += 1 1915 line_part, start, line = \ 1916 b'', stop, (line_part + data[start:stop]).decode(errors='ignore') 1917 if __debug__: self._log(4, '< %r' % line) 1918 self.inq.put(line) 1919 if self.TerminateReader: 1920 terminate = True 1921 except: 1922 reason = 'socket error: %s - %s' % sys.exc_info()[:2] 1923 if __debug__: 1924 if not self.Terminate: 1925 self._print_log() 1926 if self.debug: self.debug += 4 # Output all 1927 self._log(1, reason) 1928 self.inq.put((self.abort, reason)) 1929 break 1930 1931 if __debug__: self._log(1, 'finished') 1932 1933 1934 def _writer(self): 1935 1936 threading.currentThread().setName(self.identifier + 'writer') 1937 1938 if __debug__: self._log(1, 'starting') 1939 1940 reason = 'Terminated' 1941 1942 while not self.Terminate: 1943 rqb = self.ouq.get() 1944 if rqb is None: 1945 break # Outq flushed 1946 1947 try: 1948 self.send(rqb.data) 1949 if __debug__: self._log(4, '> %r' % rqb.data) 1950 except: 1951 reason = 'socket error: %s - %s' % sys.exc_info()[:2] 1952 if __debug__: 1953 if not self.Terminate: 1954 self._print_log() 1955 if self.debug: self.debug += 4 # Output all 1956 self._log(1, reason) 1957 rqb.abort(self.abort, reason) 1958 break 1959 1960 self.inq.put((self.abort, reason)) 1961 1962 if __debug__: self._log(1, 'finished') 1963 1964 1965 1966 # Debugging 1967 1968 1969 if __debug__: 1970 1971 def _init_debug(self, debug=None, debug_file=None, debug_buf_lvl=None): 1972 self.debug_lock = threading.Lock() 1973 1974 self.debug = self._choose_nonull_or_dflt(0, debug) 1975 self.debug_file = self._choose_nonull_or_dflt(sys.stderr, debug_file) 1976 self.debug_buf_lvl = self._choose_nonull_or_dflt(DFLT_DEBUG_BUF_LVL, debug_buf_lvl) 1977 1978 self._cmd_log_len = 20 1979 self._cmd_log_idx = 0 1980 self._cmd_log = {} # Last `_cmd_log_len' interactions 1981 if self.debug: 1982 self._mesg('imaplib2 version %s' % __version__) 1983 self._mesg('imaplib2 debug level %s, buffer level %s' % (self.debug, self.debug_buf_lvl)) 1984 1985 1986 def _dump_ur(self, lvl): 1987 if lvl > self.debug: 1988 return 1989 1990 l = self.untagged_responses # NB: bytes array 1991 if not l: 1992 return 1993 1994 t = '\n\t\t' 1995 l = ['%s: "%s"' % (x[0], x[1][0] and b'" "'.join(x[1]) or '') for x in l] 1996 self.debug_lock.acquire() 1997 self._mesg('untagged responses dump:%s%s' % (t, t.join(l))) 1998 self.debug_lock.release() 1999 2000 2001 def _log(self, lvl, line): 2002 if lvl > self.debug: 2003 return 2004 2005 if line[-2:] == CRLF: 2006 line = line[:-2] + '\\r\\n' 2007 2008 tn = threading.currentThread().getName() 2009 2010 if lvl <= 1 or self.debug > self.debug_buf_lvl: 2011 self.debug_lock.acquire() 2012 self._mesg(line, tn) 2013 self.debug_lock.release() 2014 if lvl != 1: 2015 return 2016 2017 # Keep log of last `_cmd_log_len' interactions for debugging. 2018 self.debug_lock.acquire() 2019 self._cmd_log[self._cmd_log_idx] = (line, tn, time.time()) 2020 self._cmd_log_idx += 1 2021 if self._cmd_log_idx >= self._cmd_log_len: 2022 self._cmd_log_idx = 0 2023 self.debug_lock.release() 2024 2025 2026 def _mesg(self, s, tn=None, secs=None): 2027 if secs is None: 2028 secs = time.time() 2029 if tn is None: 2030 tn = threading.currentThread().getName() 2031 tm = time.strftime('%M:%S', time.localtime(secs)) 2032 try: 2033 self.debug_file.write(' %s.%02d %s %s\n' % (tm, (secs*100)%100, tn, s)) 2034 self.debug_file.flush() 2035 finally: 2036 pass 2037 2038 2039 def _print_log(self): 2040 self.debug_lock.acquire() 2041 i, n = self._cmd_log_idx, self._cmd_log_len 2042 if n: self._mesg('last %d log messages:' % n) 2043 while n: 2044 try: 2045 self._mesg(*self._cmd_log[i]) 2046 except: 2047 pass 2048 i += 1 2049 if i >= self._cmd_log_len: 2050 i = 0 2051 n -= 1 2052 self.debug_lock.release() 2053 2054 2055 2056class IMAP4_SSL(IMAP4): 2057 2058 """IMAP4 client class over SSL connection 2059 2060 Instantiate with: 2061 IMAP4_SSL(host=None, port=None, keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, ssl_version="ssl23", debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None, tls_level="tls_compat") 2062 2063 host - host's name (default: localhost); 2064 port - port number (default: standard IMAP4 SSL port); 2065 keyfile - PEM formatted file that contains your private key (default: None); 2066 certfile - PEM formatted certificate chain file (default: None); 2067 ca_certs - PEM formatted certificate chain file used to validate server certificates (default: None); 2068 cert_verify_cb - function to verify authenticity of server certificates (default: None); 2069 ssl_version - SSL version to use (default: "ssl23", choose from: "tls1","ssl3","ssl23"); 2070 debug - debug level (default: 0 - no debug); 2071 debug_file - debug stream (default: sys.stderr); 2072 identifier - thread identifier prefix (default: host); 2073 timeout - timeout in seconds when expecting a command response. 2074 debug_buf_lvl - debug level at which buffering is turned off. 2075 tls_level - TLS security level (default: "tls_compat"). 2076 2077 The recognized values for tls_level are: 2078 tls_secure: accept only TLS protocols recognized as "secure" 2079 tls_no_ssl: disable SSLv2 and SSLv3 support 2080 tls_compat: accept all SSL/TLS versions 2081 2082 For more documentation see the docstring of the parent class IMAP4. 2083 """ 2084 2085 2086 def __init__(self, host=None, port=None, keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, ssl_version="ssl23", debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None, tls_level=TLS_COMPAT): 2087 self.keyfile = keyfile 2088 self.certfile = certfile 2089 self.ca_certs = ca_certs 2090 self.cert_verify_cb = cert_verify_cb 2091 self.ssl_version = ssl_version 2092 self.tls_level = tls_level 2093 IMAP4.__init__(self, host, port, debug, debug_file, identifier, timeout, debug_buf_lvl) 2094 2095 2096 def open(self, host=None, port=None): 2097 """open(host=None, port=None) 2098 Setup secure connection to remote server on "host:port" 2099 (default: localhost:standard IMAP4 SSL port). 2100 This connection will be used by the routines: 2101 read, send, shutdown, socket, ssl.""" 2102 2103 self.host = self._choose_nonull_or_dflt('', host) 2104 self.port = self._choose_nonull_or_dflt(IMAP4_SSL_PORT, port) 2105 self.sock = self.open_socket() 2106 self.ssl_wrap_socket() 2107 2108 2109 def read(self, size): 2110 """data = read(size) 2111 Read at most 'size' bytes from remote.""" 2112 2113 if self.decompressor is None: 2114 return self.sock.read(size) 2115 2116 if self.decompressor.unconsumed_tail: 2117 data = self.decompressor.unconsumed_tail 2118 else: 2119 data = self.sock.read(READ_SIZE) 2120 2121 return self.decompressor.decompress(data, size) 2122 2123 2124 def send(self, data): 2125 """send(data) 2126 Send 'data' to remote.""" 2127 2128 if self.compressor is not None: 2129 data = self.compressor.compress(data) 2130 data += self.compressor.flush(zlib.Z_SYNC_FLUSH) 2131 2132 if hasattr(self.sock, "sendall"): 2133 self.sock.sendall(data) 2134 else: 2135 dlen = len(data) 2136 while dlen > 0: 2137 sent = self.sock.write(data) 2138 if sent == dlen: 2139 break # avoid copy 2140 data = data[sent:] 2141 dlen = dlen - sent 2142 2143 2144 def ssl(self): 2145 """ssl = ssl() 2146 Return ssl instance used to communicate with the IMAP4 server.""" 2147 2148 return self.sock 2149 2150 2151 2152class IMAP4_stream(IMAP4): 2153 2154 """IMAP4 client class over a stream 2155 2156 Instantiate with: 2157 IMAP4_stream(command, debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None) 2158 2159 command - string that can be passed to subprocess.Popen(); 2160 debug - debug level (default: 0 - no debug); 2161 debug_file - debug stream (default: sys.stderr); 2162 identifier - thread identifier prefix (default: host); 2163 timeout - timeout in seconds when expecting a command response. 2164 debug_buf_lvl - debug level at which buffering is turned off. 2165 2166 For more documentation see the docstring of the parent class IMAP4. 2167 """ 2168 2169 2170 def __init__(self, command, debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None): 2171 self.command = command 2172 self.host = command 2173 self.port = None 2174 self.sock = None 2175 self.writefile, self.readfile = None, None 2176 self.read_fd = None 2177 IMAP4.__init__(self, None, None, debug, debug_file, identifier, timeout, debug_buf_lvl) 2178 2179 2180 def open(self, host=None, port=None): 2181 """open(host=None, port=None) 2182 Setup a stream connection via 'self.command'. 2183 This connection will be used by the routines: 2184 read, send, shutdown, socket.""" 2185 2186 from subprocess import Popen, PIPE 2187 from io import DEFAULT_BUFFER_SIZE 2188 2189 if __debug__: self._log(0, 'opening stream from command "%s"' % self.command) 2190 self._P = Popen(self.command, shell=True, stdin=PIPE, stdout=PIPE, close_fds=True, bufsize=DEFAULT_BUFFER_SIZE) 2191 self.writefile, self.readfile = self._P.stdin, self._P.stdout 2192 self.read_fd = self.readfile.fileno() 2193 2194 2195 def read(self, size): 2196 """Read 'size' bytes from remote.""" 2197 2198 if self.decompressor is None: 2199 return os.read(self.read_fd, size) 2200 2201 if self.decompressor.unconsumed_tail: 2202 data = self.decompressor.unconsumed_tail 2203 else: 2204 data = os.read(self.read_fd, READ_SIZE) 2205 2206 return self.decompressor.decompress(data, size) 2207 2208 2209 def send(self, data): 2210 """Send data to remote.""" 2211 2212 if self.compressor is not None: 2213 data = self.compressor.compress(data) 2214 data += self.compressor.flush(zlib.Z_SYNC_FLUSH) 2215 2216 self.writefile.write(data) 2217 self.writefile.flush() 2218 2219 2220 def shutdown(self): 2221 """Close I/O established in "open".""" 2222 2223 self.readfile.close() 2224 self.writefile.close() 2225 self._P.wait() 2226 2227 2228class _Authenticator(object): 2229 2230 """Private class to provide en/de-coding 2231 for base64 authentication conversation.""" 2232 2233 def __init__(self, mechinst): 2234 self.mech = mechinst # Callable object to provide/process data 2235 2236 def process(self, data, rqb): 2237 ret = self.mech(self.decode(data)) 2238 if ret is None: 2239 return b'*' # Abort conversation 2240 return self.encode(ret) 2241 2242 def encode(self, inp): 2243 # 2244 # Invoke binascii.b2a_base64 iteratively with 2245 # short even length buffers, strip the trailing 2246 # line feed from the result and append. "Even" 2247 # means a number that factors to both 6 and 8, 2248 # so when it gets to the end of the 8-bit input 2249 # there's no partial 6-bit output. 2250 # 2251 oup = b'' 2252 if isinstance(inp, str): 2253 inp = inp.encode('utf-8') 2254 while inp: 2255 if len(inp) > 48: 2256 t = inp[:48] 2257 inp = inp[48:] 2258 else: 2259 t = inp 2260 inp = b'' 2261 e = binascii.b2a_base64(t) 2262 if e: 2263 oup = oup + e[:-1] 2264 return oup 2265 2266 def decode(self, inp): 2267 if not inp: 2268 return b'' 2269 return binascii.a2b_base64(inp) 2270 2271 2272 2273 2274class _IdleCont(object): 2275 2276 """When process is called, server is in IDLE state 2277 and will send asynchronous changes.""" 2278 2279 def __init__(self, parent, timeout): 2280 self.parent = parent 2281 self.timeout = parent._choose_nonull_or_dflt(IDLE_TIMEOUT, timeout) 2282 self.parent.idle_timeout = self.timeout + time.time() 2283 2284 def process(self, data, rqb): 2285 self.parent.idle_lock.acquire() 2286 self.parent.idle_rqb = rqb 2287 self.parent.idle_timeout = self.timeout + time.time() 2288 self.parent.idle_lock.release() 2289 if __debug__: self.parent._log(2, 'server IDLE started, timeout in %.2f secs' % self.timeout) 2290 return None 2291 2292 2293 2294MonthNames = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 2295 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] 2296 2297Mon2num = {s.encode():n+1 for n, s in enumerate(MonthNames[1:])} 2298 2299InternalDate = re.compile(br'.*INTERNALDATE "' 2300 br'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])' 2301 br' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])' 2302 br' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])' 2303 br'"') 2304 2305 2306def Internaldate2Time(resp): 2307 2308 """time_tuple = Internaldate2Time(resp) 2309 2310 Parse an IMAP4 INTERNALDATE string. 2311 2312 Return corresponding local time. The return value is a 2313 time.struct_time instance or None if the string has wrong format.""" 2314 2315 mo = InternalDate.match(resp) 2316 if not mo: 2317 return None 2318 2319 mon = Mon2num[mo.group('mon')] 2320 zonen = mo.group('zonen') 2321 2322 day = int(mo.group('day')) 2323 year = int(mo.group('year')) 2324 hour = int(mo.group('hour')) 2325 min = int(mo.group('min')) 2326 sec = int(mo.group('sec')) 2327 zoneh = int(mo.group('zoneh')) 2328 zonem = int(mo.group('zonem')) 2329 2330 # INTERNALDATE timezone must be subtracted to get UT 2331 2332 zone = (zoneh*60 + zonem)*60 2333 if zonen == b'-': 2334 zone = -zone 2335 2336 tt = (year, mon, day, hour, min, sec, -1, -1, -1) 2337 return time.localtime(calendar.timegm(tt) - zone) 2338 2339Internaldate2tuple = Internaldate2Time # (Backward compatible) 2340 2341 2342 2343def Time2Internaldate(date_time): 2344 2345 """'"DD-Mmm-YYYY HH:MM:SS +HHMM"' = Time2Internaldate(date_time) 2346 2347 Convert 'date_time' to IMAP4 INTERNALDATE representation. 2348 2349 The date_time argument can be a number (int or float) representing 2350 seconds since epoch (as returned by time.time()), a 9-tuple 2351 representing local time, an instance of time.struct_time (as 2352 returned by time.localtime()), an aware datetime instance or a 2353 double-quoted string. In the last case, it is assumed to already 2354 be in the correct format.""" 2355 2356 from datetime import datetime, timezone, timedelta 2357 2358 if isinstance(date_time, (int, float)): 2359 tt = time.localtime(date_time) 2360 elif isinstance(date_time, tuple): 2361 try: 2362 gmtoff = date_time.tm_gmtoff 2363 except AttributeError: 2364 if time.daylight: 2365 dst = date_time[8] 2366 if dst == -1: 2367 dst = time.localtime(time.mktime(date_time))[8] 2368 gmtoff = -(time.timezone, time.altzone)[dst] 2369 else: 2370 gmtoff = -time.timezone 2371 delta = timedelta(seconds=gmtoff) 2372 dt = datetime(*date_time[:6], tzinfo=timezone(delta)) 2373 elif isinstance(date_time, datetime): 2374 if date_time.tzinfo is None: 2375 raise ValueError("date_time must be aware") 2376 dt = date_time 2377 elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'): 2378 return date_time # Assume in correct format 2379 else: 2380 raise ValueError("date_time not of a known type") 2381 2382 fmt = '"%d-{}-%Y %H:%M:%S %z"'.format(MonthNames[dt.month]) 2383 return dt.strftime(fmt) 2384 2385 2386 2387FLAGS_cre = re.compile(br'.*FLAGS \((?P<flags>[^\)]*)\)') 2388 2389def ParseFlags(resp): 2390 2391 """('flag', ...) = ParseFlags(line) 2392 Convert IMAP4 flags response to python tuple.""" 2393 2394 mo = FLAGS_cre.match(resp) 2395 if not mo: 2396 return () 2397 2398 return tuple(mo.group('flags').split()) 2399 2400 2401 2402if __name__ == '__main__': 2403 2404 # To test: invoke either as 'python imaplib2.py [IMAP4_server_hostname]', 2405 # or as 'python imaplib2.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"' 2406 # or as 'python imaplib2.py -l keyfile[:certfile]|: [IMAP4_SSL_server_hostname]' 2407 # 2408 # Option "-d <level>" turns on debugging (use "-d 5" for everything) 2409 # Option "-i" tests that IDLE is interruptible 2410 # Option "-p <port>" allows alternate ports 2411 2412 if not __debug__: 2413 raise ValueError('Please run without -O') 2414 2415 import getopt, getpass 2416 2417 try: 2418 optlist, args = getopt.getopt(sys.argv[1:], 'd:il:s:p:') 2419 except getopt.error as val: 2420 optlist, args = (), () 2421 2422 debug, debug_buf_lvl, port, stream_command, keyfile, certfile, idle_intr = (None,)*7 2423 for opt,val in optlist: 2424 if opt == '-d': 2425 debug = int(val) 2426 debug_buf_lvl = debug - 1 2427 elif opt == '-i': 2428 idle_intr = 1 2429 elif opt == '-l': 2430 try: 2431 keyfile,certfile = val.split(':') 2432 except ValueError: 2433 keyfile,certfile = val,val 2434 elif opt == '-p': 2435 port = int(val) 2436 elif opt == '-s': 2437 stream_command = val 2438 if not args: args = (stream_command,) 2439 2440 if not args: args = ('',) 2441 if not port: port = (keyfile is not None) and IMAP4_SSL_PORT or IMAP4_PORT 2442 2443 host = args[0] 2444 2445 USER = getpass.getuser() 2446 2447 data = open(os.path.exists("test.data") and "test.data" or __file__).read(1000) 2448 test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)s%(data)s' \ 2449 % {'user':USER, 'lf':'\n', 'data':data} 2450 2451 test_seq1 = [ 2452 ('list', ('""', '""')), 2453 ('list', ('""', '"%"')), 2454 ('create', ('imaplib2_test0',)), 2455 ('rename', ('imaplib2_test0', 'imaplib2_test1')), 2456 ('CREATE', ('imaplib2_test2',)), 2457 ('append', ('imaplib2_test2', None, None, test_mesg)), 2458 ('list', ('""', '"imaplib2_test%"')), 2459 ('select', ('imaplib2_test2',)), 2460 ('search', (None, 'SUBJECT', '"IMAP4 test"')), 2461 ('fetch', ('1:*', '(FLAGS INTERNALDATE RFC822)')), 2462 ('store', ('1', 'FLAGS', '(\Deleted)')), 2463 ('namespace', ()), 2464 ('expunge', ()), 2465 ('recent', ()), 2466 ('close', ()), 2467 ] 2468 2469 test_seq2 = ( 2470 ('select', ()), 2471 ('response', ('UIDVALIDITY',)), 2472 ('response', ('EXISTS',)), 2473 ('append', (None, None, None, test_mesg)), 2474 ('examine', ()), 2475 ('select', ()), 2476 ('fetch', ('1:*', '(FLAGS UID)')), 2477 ('examine', ()), 2478 ('select', ()), 2479 ('uid', ('SEARCH', 'SUBJECT', '"IMAP4 test"')), 2480 ('uid', ('SEARCH', 'ALL')), 2481 ('uid', ('THREAD', 'references', 'UTF-8', '(SEEN)')), 2482 ('recent', ()), 2483 ) 2484 2485 2486 AsyncError, M = None, None 2487 2488 def responder(cb_arg_list): 2489 response, cb_arg, error = cb_arg_list 2490 global AsyncError 2491 cmd, args = cb_arg 2492 if error is not None: 2493 AsyncError = error 2494 M._log(0, '[cb] ERROR %s %.100s => %s' % (cmd, args, error)) 2495 return 2496 typ, dat = response 2497 M._log(0, '[cb] %s %.100s => %s %.100s' % (cmd, args, typ, dat)) 2498 if typ == 'NO': 2499 AsyncError = (Exception, dat[0]) 2500 2501 def run(cmd, args, cb=True): 2502 if AsyncError: 2503 M._log(1, 'AsyncError %s' % repr(AsyncError)) 2504 M.logout() 2505 typ, val = AsyncError 2506 raise typ(val) 2507 if not M.debug: M._log(0, '%s %.100s' % (cmd, args)) 2508 try: 2509 if cb: 2510 typ, dat = getattr(M, cmd)(callback=responder, cb_arg=(cmd, args), *args) 2511 M._log(1, '%s %.100s => %s %.100s' % (cmd, args, typ, dat)) 2512 else: 2513 typ, dat = getattr(M, cmd)(*args) 2514 M._log(1, '%s %.100s => %s %.100s' % (cmd, args, typ, dat)) 2515 except: 2516 M._log(1, '%s - %s' % sys.exc_info()[:2]) 2517 M.logout() 2518 raise 2519 if typ == 'NO': 2520 M._log(1, 'NO') 2521 M.logout() 2522 raise Exception(dat[0]) 2523 return dat 2524 2525 try: 2526 threading.currentThread().setName('main') 2527 2528 if keyfile is not None: 2529 if not keyfile: keyfile = None 2530 if not certfile: certfile = None 2531 M = IMAP4_SSL(host=host, port=port, keyfile=keyfile, certfile=certfile, ssl_version="tls1", debug=debug, identifier='', timeout=10, debug_buf_lvl=debug_buf_lvl, tls_level="tls_no_ssl") 2532 elif stream_command: 2533 M = IMAP4_stream(stream_command, debug=debug, identifier='', timeout=10, debug_buf_lvl=debug_buf_lvl) 2534 else: 2535 M = IMAP4(host=host, port=port, debug=debug, identifier='', timeout=10, debug_buf_lvl=debug_buf_lvl) 2536 if M.state != 'AUTH': # Login needed 2537 PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost")) 2538 test_seq1.insert(0, ('login', (USER, PASSWD))) 2539 M._log(0, 'PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION) 2540 if 'COMPRESS=DEFLATE' in M.capabilities: 2541 M.enable_compression() 2542 2543 for cmd,args in test_seq1: 2544 run(cmd, args) 2545 2546 for ml in run('list', ('""', '"imaplib2_test%"'), cb=False): 2547 mo = re.match(br'.*"([^"]+)"$', ml) 2548 if mo: path = mo.group(1) 2549 else: path = ml.split()[-1] 2550 run('delete', (path,)) 2551 2552 if 'ID' in M.capabilities: 2553 run('id', ()) 2554 run('id', ("(name imaplib2)",)) 2555 run('id', ("version", __version__, "os", os.uname()[0])) 2556 2557 for cmd,args in test_seq2: 2558 if (cmd,args) != ('uid', ('SEARCH', 'SUBJECT', 'IMAP4 test')): 2559 run(cmd, args) 2560 continue 2561 2562 dat = run(cmd, args, cb=False) 2563 uid = dat[-1].split() 2564 if not uid: continue 2565 run('uid', ('FETCH', uid[-1], 2566 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)')) 2567 run('uid', ('STORE', uid[-1], 'FLAGS', '(\Deleted)')) 2568 run('expunge', ()) 2569 2570 if 'IDLE' in M.capabilities: 2571 run('idle', (2,), cb=False) 2572 run('idle', (99,)) # Asynchronous, to test interruption of 'idle' by 'noop' 2573 time.sleep(1) 2574 run('noop', (), cb=False) 2575 2576 run('append', (None, None, None, test_mesg), cb=False) 2577 num = run('search', (None, 'ALL'), cb=False)[0].split()[0] 2578 dat = run('fetch', (num, '(FLAGS INTERNALDATE RFC822)'), cb=False) 2579 M._mesg('fetch %s => %s' % (num, repr(dat))) 2580 run('idle', (2,)) 2581 run('store', (num, '-FLAGS', '(\Seen)'), cb=False), 2582 dat = run('fetch', (num, '(FLAGS INTERNALDATE RFC822)'), cb=False) 2583 M._mesg('fetch %s => %s' % (num, repr(dat))) 2584 run('uid', ('STORE', num, 'FLAGS', '(\Deleted)')) 2585 run('expunge', ()) 2586 if idle_intr: 2587 M._mesg('HIT CTRL-C to interrupt IDLE') 2588 try: 2589 run('idle', (99,), cb=False) # Synchronous, to test interruption of 'idle' by INTR 2590 except KeyboardInterrupt: 2591 M._mesg('Thanks!') 2592 M._mesg('') 2593 raise 2594 elif idle_intr: 2595 M._mesg('chosen server does not report IDLE capability') 2596 2597 run('logout', (), cb=False) 2598 2599 if debug: 2600 M._mesg('') 2601 M._print_log() 2602 M._mesg('') 2603 M._mesg('unused untagged responses in order, most recent last:') 2604 for typ,dat in M.pop_untagged_responses(): M._mesg('\t%s %s' % (typ, dat)) 2605 2606 print('All tests OK.') 2607 2608 except: 2609 if not idle_intr or M is None or not 'IDLE' in M.capabilities: 2610 print('Tests failed.') 2611 2612 if not debug: 2613 print(''' 2614If you would like to see debugging output, 2615try: %s -d5 2616''' % sys.argv[0]) 2617 2618 raise 2619