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