from test import support
from test.support import socket_helper

from contextlib import contextmanager
from email.message import EmailMessage
import imaplib
import os.path
import socketserver
import time
import calendar
import threading
import re
import select
import socket

from test.support import verbose, run_with_tz, run_with_locale, cpython_only
from test.support import hashlib_helper, threading_helper
import unittest
from unittest import mock
from datetime import datetime, timezone, timedelta
try:
    import ssl
except ImportError:
    ssl = None

support.requires_working_socket(module=True)

CERTFILE = os.path.join(os.path.dirname(__file__) or os.curdir, "certdata", "keycert3.pem")
CAFILE = os.path.join(os.path.dirname(__file__) or os.curdir, "certdata", "pycacert.pem")

_quoted_string = re.compile(r'"(?:[^"\\]|\\.)*"')


def splitargs(line):
    # Split a command line into IMAP arguments: quoted strings, balanced
    # (possibly nested) parenthesized lists, and bare atoms.
    args = []
    i, n = 0, len(line)
    while i < n:
        if line[i] == ' ':
            i += 1
            continue
        start = i
        depth = 0
        while i < n and (depth or line[i] != ' '):
            c = line[i]
            if c == '"':
                i = _quoted_string.match(line, i).end()
                continue
            elif c == '(':
                depth += 1
            elif c == ')':
                depth -= 1
            i += 1
        args.append(line[start:i])
    return args


def parse_seq_number(s, maxmsg):
    if s == '*':
        return maxmsg
    return int(s)


def parse_sequence_set(arg, maxmsg):
    for s in arg.split(','):
        if ':' in s:
            lo, hi = s.split(':')
            lo = parse_seq_number(lo, maxmsg)
            hi = parse_seq_number(hi, maxmsg)
            yield from range(min(lo, hi), max(lo, hi) + 1)
        else:
            yield parse_seq_number(s, maxmsg)


def make_simple_handler(command, untagged_response=(),
                        completed=None):
    if completed is None:
        completed = f'{command} completed'
    def cmd(self, tag, args):
        for msg in untagged_response:
            self._send_textline(msg)
        self._send_tagged(tag, 'OK', completed)
    cmd.__name__ = 'cmd_' + command
    class Handler(SimpleIMAPHandler):
        pass
    Handler.__name__ = command.title() + 'Handler'
    setattr(Handler, cmd.__name__, cmd)
    Handler.__qualname__ = Handler.__name__
    cmd.__qualname__ = Handler.__qualname__ + '.' + cmd.__name__
    return Handler


def _read_literal(handler, marker):
    # Read one literal, a raw octet sequence, by its count from the marker
    # ('{N}', or '(~{N}' in UTF8 mode).
    size = int(re.search(r'\{(\d+)\}', marker).group(1))
    # The client must wait for the continuation, so nothing should be readable.
    if select.select([handler.connection], [], [], 0)[0]:
        raise AssertionError('client sent the literal before the '
                             'continuation request')
    handler._send_textline('+')
    return handler.rfile.read(size)


class TestImaplib(unittest.TestCase):

    def test_Internaldate2tuple(self):
        t0 = calendar.timegm((2000, 1, 1, 0, 0, 0, -1, -1, -1))
        tt = imaplib.Internaldate2tuple(
            b'25 (INTERNALDATE "01-Jan-2000 00:00:00 +0000")')
        self.assertEqual(time.mktime(tt), t0)
        tt = imaplib.Internaldate2tuple(
            b'25 (INTERNALDATE "01-Jan-2000 11:30:00 +1130")')
        self.assertEqual(time.mktime(tt), t0)
        tt = imaplib.Internaldate2tuple(
            b'25 (INTERNALDATE "31-Dec-1999 12:30:00 -1130")')
        self.assertEqual(time.mktime(tt), t0)

    @run_with_tz('MST+07MDT,M4.1.0,M10.5.0')
    def test_Internaldate2tuple_issue10941(self):
        self.assertNotEqual(imaplib.Internaldate2tuple(
            b'25 (INTERNALDATE "02-Apr-2000 02:30:00 +0000")'),
            imaplib.Internaldate2tuple(
                b'25 (INTERNALDATE "02-Apr-2000 03:30:00 +0000")'))

    def timevalues(self):
        return [2000000000, 2000000000.0, time.localtime(2000000000),
                (2033, 5, 18, 5, 33, 20, -1, -1, -1),
                (2033, 5, 18, 5, 33, 20, -1, -1, 1),
                datetime.fromtimestamp(2000000000,
                                       timezone(timedelta(0, 2 * 60 * 60))),
                '"18-May-2033 05:33:20 +0200"']

    @run_with_locale('LC_ALL', 'de_DE', 'fr_FR', '')
    # DST rules included to work around quirk where the Gnu C library may not
    # otherwise restore the previous time zone
    @run_with_tz('STD-1DST,M3.2.0,M11.1.0')
    def test_Time2Internaldate(self):
        expected = '"18-May-2033 05:33:20 +0200"'

        for t in self.timevalues():
            internal = imaplib.Time2Internaldate(t)
            self.assertEqual(internal, expected)

    @run_with_tz('STD-1DST,M3.2.0,M11.1.0')
    def test_Time2Internaldate_datetime_timetuple(self):
        date_time = datetime.fromtimestamp(2000000000).timetuple()
        self.assertIsNone(date_time.tm_gmtoff)
        self.assertEqual(
            imaplib.Time2Internaldate(date_time),
            '"18-May-2033 05:33:20 +0200"',
        )

    def test_that_Time2Internaldate_returns_a_result(self):
        # Without tzset, we can check only that it successfully
        # produces a result, not the correctness of the result itself,
        # since the result depends on the timezone the machine is in.
        for t in self.timevalues():
            imaplib.Time2Internaldate(t)

    @socket_helper.skip_if_tcp_blackhole
    def test_imap4_host_default_value(self):
        # Check whether the IMAP4_PORT is truly unavailable.
        with socket.socket() as s:
            try:
                s.connect(('', imaplib.IMAP4_PORT))
                self.skipTest(
                    "Cannot run the test with local IMAP server running.")
            except socket.error:
                pass

        # This is the exception that should be raised.
        expected_errnos = socket_helper.get_socket_conn_refused_errs()
        with self.assertRaises(OSError) as cm:
            imaplib.IMAP4()
        self.assertIn(cm.exception.errno, expected_errnos)

    def test_astring(self):
        m = imaplib.IMAP4.__new__(imaplib.IMAP4)
        m._encoding = 'ascii'
        # Plain atoms are left unquoted.
        self.assertEqual(m._astring('INBOX'), b'INBOX')
        self.assertEqual(m._astring(b'INBOX'), b'INBOX')
        # Names with protocol-sensitive characters are quoted.
        self.assertEqual(m._astring('New folder'), b'"New folder"')
        self.assertEqual(m._astring('a"b'), rb'"a\"b"')
        self.assertEqual(m._astring(r'a\b'), rb'"a\\b"')
        self.assertEqual(m._astring(''), b'""')
        self.assertEqual(m._astring('*'), b'"*"')
        # A well-formed quoted string is passed through unchanged.
        self.assertEqual(m._astring('"New folder"'), b'"New folder"')
        self.assertEqual(m._astring('""'), b'""')
        # Including a lenient (non-RFC) backslash escape, which the server
        # may accept.
        self.assertEqual(m._astring(r'"a\b"'), rb'"a\b"')
        # A string that only looks quoted but is not a single token is
        # quoted as data, closing the argument injection vector.
        self.assertEqual(m._astring('"a" SELECT evil "'),
                         rb'"\"a\" SELECT evil \""')
        self.assertEqual(m._astring('"'), rb'"\""')
        # Non-ASCII names are only allowed in a quoted string or a
        # literal, never in an atom (RFC 6855).
        m._encoding = 'utf-8'
        self.assertEqual(m._astring('Entwürfe'), '"Entwürfe"'.encode())
        self.assertEqual(m._astring(b'Entw\xc3\xbcrfe'), b'"Entw\xc3\xbcrfe"')

    def test_encode_criteria(self):
        m = imaplib.IMAP4.__new__(imaplib.IMAP4)
        enc = m._encode_criteria
        # No charset: criteria are returned unchanged.
        self.assertEqual(enc(None, ('TEXT', 'x')), ('TEXT', 'x'))
        # str criteria are encoded to the charset; ASCII is charset-independent.
        self.assertEqual(enc('UTF-8', ('TEXT', 'XXXXXX')), (b'TEXT', b'XXXXXX'))
        # Non-ASCII text is encoded to the declared charset, including charsets
        # other than ASCII, Latin-1 and UTF-8.
        self.assertEqual(enc('UTF-8', ('"café"',)), ('"café"'.encode('utf-8'),))
        self.assertEqual(enc('ISO-8859-1', ('"café"',)),
                         ('"café"'.encode('latin-1'),))
        self.assertEqual(enc('KOI8-U', ('"Київ"',)),
                         ('"Київ"'.encode('koi8-u'),))
        self.assertEqual(enc('SHIFT_JIS', ('"日本"',)),
                         ('"日本"'.encode('shift_jis'),))
        # bytes criteria are already encoded and pass through unchanged.
        self.assertEqual(enc('SHIFT_JIS', (b'"already"',)), (b'"already"',))
        # The charset name may itself be bytes.
        self.assertEqual(enc(b'UTF-8', ('"café"',)), ('"café"'.encode('utf-8'),))
        # A charset with no codec at all (not even via the iconv codec) cannot
        # encode str criteria; bytes criteria must be used with such
        # server-only charsets.
        self.assertRaises(LookupError, enc, 'no-such-charset', ('TEXT',))
        self.assertEqual(enc('no-such-charset', (b'TEXT',)), (b'TEXT',))

    def test_astring_idempotent(self):
        # Quoting an already quoted argument should not change it, so that
        # quoting twice gives the same result as quoting once.
        m = imaplib.IMAP4.__new__(imaplib.IMAP4)
        m._encoding = 'ascii'
        for arg in ['INBOX', 'New folder', 'a"b', 'a\\b', '', '*', '%',
                    '"New folder"', '""', '"a\\b"', '"a" SELECT evil "',
                    '"', 'a\tb', 'a\rb', '\x7f', '(a)', b'Entw\xc3\xbcrfe']:
            with self.subTest(arg=arg):
                once = m._astring(arg)
                self.assertEqual(m._astring(once), once)
                twice = m._list_mailbox(arg)
                self.assertEqual(m._list_mailbox(twice), twice)

    def test_list_mailbox(self):
        m = imaplib.IMAP4.__new__(imaplib.IMAP4)
        m._encoding = 'ascii'
        # Wildcards are not quoted in a list pattern.
        self.assertEqual(m._list_mailbox('*'), b'*')
        self.assertEqual(m._list_mailbox('%'), b'%')
        self.assertEqual(m._list_mailbox('foo/%'), b'foo/%')
        # But spaces still require quoting.
        self.assertEqual(m._list_mailbox('New folder'), b'"New folder"')
        self.assertEqual(m._list_mailbox('"New folder"'), b'"New folder"')
        # A non-ASCII pattern is encoded as modified UTF-7; the wildcards keep
        # their meaning (they are printable ASCII, passed through directly).
        self.assertEqual(m._list_mailbox('Entwürfe/%'), b'Entw&APw-rfe/%')
        # Under UTF8=ACCEPT the name is sent as UTF-8 in a quoted string.
        m._encoding = 'utf-8'
        self.assertEqual(m._list_mailbox('Entwürfe/%'),
                         '"Entwürfe/%"'.encode())

    def test_mailbox(self):
        m = imaplib.IMAP4.__new__(imaplib.IMAP4)
        m._encoding = 'ascii'
        # A plain atom is left unquoted; bytes are sent verbatim.
        self.assertEqual(m._mailbox('INBOX'), b'INBOX')
        self.assertEqual(m._mailbox(b'Entw&APw-rfe'), b'Entw&APw-rfe')
        # A non-ASCII name is encoded as modified UTF-7 (RFC 3501 5.1.3).
        self.assertEqual(m._mailbox('Entwürfe'), b'Entw&APw-rfe')
        # A str that is already valid modified UTF-7 is passed through, so a
        # name obtained from LIST (raw bytes, decoded as ASCII) round-trips.
        self.assertEqual(m._mailbox('Entw&APw-rfe'), b'Entw&APw-rfe')
        self.assertEqual(m._mailbox('"Entw&APw-rfe"'), b'"Entw&APw-rfe"')
        # A literal '&' (not a valid shift on its own) is encoded as '&-'.
        self.assertEqual(m._mailbox('A&B'), b'A&-B')
        # Names needing quoting are still quoted.
        self.assertEqual(m._mailbox('New folder'), b'"New folder"')
        self.assertEqual(m._mailbox(''), b'""')
        # Control characters are Base64-encoded, never sent as raw bytes.
        self.assertEqual(m._mailbox('a\x01b'), b'a&AAE-b')
        # Under UTF8=ACCEPT a non-ASCII name is sent as UTF-8, quoted.
        m._encoding = 'utf-8'
        self.assertEqual(m._mailbox('Entwürfe'), '"Entwürfe"'.encode())
        self.assertEqual(m._mailbox('Entw&APw-rfe'), b'Entw&APw-rfe')

    def test_sequence_set(self):
        m = imaplib.IMAP4.__new__(imaplib.IMAP4)
        # A scalar is passed through as a string.
        self.assertEqual(m._sequence_set(5), '5')
        self.assertEqual(m._sequence_set('1:3,7'), '1:3,7')
        self.assertEqual(m._sequence_set(b'1:3,7'), '1:3,7')
        self.assertEqual(m._sequence_set(bytearray(b'1:3,7')), '1:3,7')
        # A sequence of numbers and ranges is formatted as a sequence set.
        self.assertEqual(m._sequence_set([1, 2, 5]), '1,2,5')
        self.assertEqual(m._sequence_set([1, (3, 5), (8, '*')]), '1,3:5,8:*')
        self.assertEqual(m._sequence_set([(5, None)]), '5:*')
        # A range is inclusive; a non-unit step falls back to explicit numbers.
        self.assertEqual(m._sequence_set([range(1, 4), 7]), '1:3,7')
        self.assertEqual(m._sequence_set([range(1, 10, 2)]), '1,3,5,7,9')
        # Message numbers must be integers: a string is not coerced (the
        # string form is the whole preformatted set), nor is a float.
        self.assertRaises(TypeError, m._sequence_set, ['7'])
        self.assertRaises(TypeError, m._sequence_set, [2.9])
        self.assertRaises(TypeError, m._sequence_set, [(1, 2.9)])

    def test_set_quote(self):
        m = imaplib.IMAP4.__new__(imaplib.IMAP4)
        # A string is parenthesized unless it already is.
        self.assertEqual(m._set_quote(r'\Seen'), r'(\Seen)')
        self.assertEqual(m._set_quote(r'(\Seen)'), r'(\Seen)')
        # A sequence of atoms is joined and parenthesized.
        self.assertEqual(m._set_quote([r'\Seen', r'\Answered']),
                         r'(\Seen \Answered)')
        self.assertEqual(m._set_quote(['MESSAGES', 'UNSEEN']),
                         '(MESSAGES UNSEEN)')

    def test_substitute(self):
        sub = imaplib._substitute
        # '?' quotes an astring; an atom-safe value is left unquoted.
        self.assertEqual(sub('FROM ?', ['me@host']), 'FROM me@host')
        self.assertEqual(sub('SUBJECT ?', ['hello world']),
                         'SUBJECT "hello world"')
        self.assertEqual(sub('SUBJECT ?', ['a"b']), r'SUBJECT "a\"b"')
        # An integer becomes a number, a list a parenthesized list.
        self.assertEqual(sub('LARGER ?', [1000]), 'LARGER 1000')
        self.assertEqual(sub('HEADER.FIELDS ?', [['DATE', 'FROM']]),
                         'HEADER.FIELDS (DATE FROM)')
        # '?f' emits flags verbatim, never quoted.
        self.assertEqual(sub('?f', [r'\Seen']), r'\Seen')
        self.assertEqual(sub('?f', [[r'\Seen', r'\Answered']]),
                         r'(\Seen \Answered)')
        # '?s' formats a message sequence set.
        self.assertEqual(sub('?s', [[1, (3, 5), (8, '*')]]), '1,3:5,8:*')
        self.assertEqual(sub('?s', [b'1:3,7']), '1:3,7')
        # '??' is a literal '?'.
        self.assertEqual(sub('a?? b', []), 'a? b')

    def test_substitute_errors(self):
        sub = imaplib._substitute
        self.assertRaises(TypeError, sub, '? ?', ['x'])    # too few parameters
        self.assertRaises(TypeError, sub, '?', ['x', 'y']) # too many parameters
        self.assertRaises(ValueError, sub, '?f', ['a b'])  # not a valid flag
        self.assertRaises(ValueError, sub, '?', ['a\r\nb'])  # CR/LF not inline
        self.assertRaises(TypeError, sub, '?', [True])     # bool is not a string
        self.assertRaises(TypeError, sub, '?', [1.5])      # float is not a string
        self.assertRaises(TypeError, sub, '?s', [['a']])   # not a message number
        self.assertRaises(TypeError, sub, '?s', [[1.5]])   # not a message number
        self.assertRaises(TypeError, sub, '?s', [[(1, 'a')]])  # not a message number
        self.assertRaises(ValueError, sub, '?s', [[(1,)]])       # not a range pair
        self.assertRaises(ValueError, sub, '?s', [[(1, 2, 3)]])  # not a range pair


if ssl:
    class SecureTCPServer(socketserver.TCPServer):

        def get_request(self):
            newsocket, fromaddr = self.socket.accept()
            context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
            context.load_cert_chain(CERTFILE)
            connstream = context.wrap_socket(newsocket, server_side=True)
            return connstream, fromaddr

    IMAP4_SSL = imaplib.IMAP4_SSL

else:

    class SecureTCPServer:
        pass

    IMAP4_SSL = None


class SimpleIMAPHandler(socketserver.StreamRequestHandler):
    timeout = support.LOOPBACK_TIMEOUT
    continuation = None
    capabilities = ''

    def setup(self):
        super().setup()
        self.server.is_selected = None
        self.server.logged = None

    def _send(self, message):
        if verbose:
            print("SENT: %r" % message.strip())
        self.wfile.write(message)

    def _send_line(self, message):
        self._send(message + b'\r\n')

    def _send_textline(self, message):
        self._send_line(message.encode('ASCII'))

    def _send_tagged(self, tag, code, message):
        self._send_textline(' '.join((tag, code, message)))

    welcome = '* OK IMAP4rev1'

    def handle(self):
        # Send a welcome message.
        self._send_textline(self.welcome)
        while 1:
            # Gather up input until we receive a line terminator or we timeout.
            # Accumulate read(1) because it's simpler to handle the differences
            # between naked sockets and SSL sockets.
            line = b''
            while 1:
                try:
                    part = self.rfile.read(1)
                    if part == b'':
                        # Naked sockets return empty strings..
                        return
                    line += part
                except OSError:
                    # ..but SSLSockets raise exceptions.
                    return
                if line.endswith(b'\r\n'):
                    break

            if verbose:
                print('GOT: %r' % line.strip())
            if self.continuation:
                try:
                    self.continuation.send(line)
                except StopIteration:
                    self.continuation = None
                continue
            self.server.line = line
            # surrogateescape so a criterion encoded in a non-UTF-8 charset
            # does not crash the handler; tests inspect server.line for bytes.
            splitline = splitargs(line.decode('utf-8', 'surrogateescape')
                                  .removesuffix('\r\n'))
            tag = splitline[0]
            cmd = splitline[1]
            args = splitline[2:]
            self.server.args = args

            if hasattr(self, 'cmd_' + cmd):
                continuation = getattr(self, 'cmd_' + cmd)(tag, args)
                if continuation:
                    self.continuation = continuation
                    next(continuation)
            else:
                self._send_tagged(tag, 'BAD', cmd + ' unknown')

    def cmd_CAPABILITY(self, tag, args):
        caps = ('IMAP4rev1 ' + self.capabilities
                if self.capabilities
                else 'IMAP4rev1')
        self._send_textline('* CAPABILITY ' + caps)
        self._send_tagged(tag, 'OK', 'CAPABILITY completed')

    def cmd_LOGOUT(self, tag, args):
        self.server.logged = None
        self._send_textline('* BYE IMAP4ref1 Server logging out')
        self._send_tagged(tag, 'OK', 'LOGOUT completed')

    def cmd_LOGIN(self, tag, args):
        self.server.logged = args
        self._send_tagged(tag, 'OK', 'LOGIN completed')

    def cmd_SELECT(self, tag, args):
        self.server.is_selected = args
        self._send_line(b'* 2 EXISTS')
        self._send_tagged(tag, 'OK', '[READ-WRITE] SELECT completed.')

    def cmd_EXAMINE(self, tag, args):
        self.server.is_selected = args
        self._send_line(b'* 2 EXISTS')
        self._send_tagged(tag, 'OK', '[READ-ONLY] EXAMINE completed.')

    def cmd_UNSELECT(self, tag, args):
        if self.server.is_selected is not None:
            self.server.is_selected = None
            self._send_tagged(tag, 'OK', 'Returned to authenticated state. (Success)')
        else:
            self._send_tagged(tag, 'BAD', 'No mailbox selected')


class IdleCmdDenyHandler(SimpleIMAPHandler):
    capabilities = 'IDLE'
    def cmd_IDLE(self, tag, args):
        self._send_tagged(tag, 'NO', 'IDLE is not allowed at this time')


class IdleCmdHandler(SimpleIMAPHandler):
    capabilities = 'IDLE'
    def cmd_IDLE(self, tag, args):
        # pre-idle-continuation response
        self._send_line(b'* 0 EXISTS')
        self._send_textline('+ idling')
        # simple response
        self._send_line(b'* 2 EXISTS')
        # complex response: fragmented data due to literal string
        self._send_line(b'* 1 FETCH (BODY[HEADER.FIELDS (DATE)] {41}')
        self._send(b'Date: Fri, 06 Dec 2024 06:00:00 +0000\r\n\r\n')
        self._send_line(b')')
        # simple response following a fragmented one
        self._send_line(b'* 3 EXISTS')
        # response arriving later
        time.sleep(1)
        self._send_line(b'* 1 RECENT')
        r = yield
        if r == b'DONE\r\n':
            self._send_line(b'* 9 RECENT')
            self._send_tagged(tag, 'OK', 'Idle completed')
        else:
            self._send_tagged(tag, 'BAD', 'Expected DONE')


class IdleCmdDelayedPacketHandler(SimpleIMAPHandler):
    capabilities = 'IDLE'
    def cmd_IDLE(self, tag, args):
        self._send_textline('+ idling')
        # response line spanning multiple packets, the last one delayed
        self._send(b'* 1 EX')
        time.sleep(0.2)
        self._send(b'IS')
        time.sleep(1)
        self._send(b'TS\r\n')
        r = yield
        if r == b'DONE\r\n':
            self._send_tagged(tag, 'OK', 'Idle completed')
        else:
            self._send_tagged(tag, 'BAD', 'Expected DONE')


class AuthHandler_CRAM_MD5(SimpleIMAPHandler):
    capabilities = 'LOGINDISABLED AUTH=CRAM-MD5'
    def cmd_AUTHENTICATE(self, tag, args):
        self._send_textline('+ PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2Uucm'
                            'VzdG9uLm1jaS5uZXQ=')
        r = yield
        if (r == b'dGltIGYxY2E2YmU0NjRiOWVmYT'
                 b'FjY2E2ZmZkNmNmMmQ5ZjMy\r\n'):
            self._send_tagged(tag, 'OK', 'CRAM-MD5 successful')
        else:
            self._send_tagged(tag, 'NO', 'No access')


class AuthHandler_PLAIN(SimpleIMAPHandler):
    capabilities = 'LOGINDISABLED AUTH=PLAIN'
    def cmd_AUTHENTICATE(self, tag, args):
        self.server.auth_args = args
        self._send_textline('+')
        self.server.response = yield
        self._send_tagged(tag, 'OK', 'Logged in.')


class NewIMAPTestsMixin:
    client = None

    def _setup(self, imap_handler, connect=True):
        """
        Sets up imap_handler for tests. imap_handler should inherit from either:
        - SimpleIMAPHandler - for testing IMAP commands,
        - socketserver.StreamRequestHandler - if raw access to stream is needed.
        Returns (client, server).
        """
        class TestTCPServer(self.server_class):
            def handle_error(self, request, client_address):
                """
                End request and raise the error if one occurs.
                """
                self.close_request(request)
                self.server_close()
                raise

        self.addCleanup(self._cleanup)
        self.server = self.server_class((socket_helper.HOST, 0), imap_handler)
        self.thread = threading.Thread(
            name=self._testMethodName+'-server',
            target=self.server.serve_forever,
            # Short poll interval to make the test finish quickly.
            # Time between requests is short enough that we won't wake
            # up spuriously too many times.
            kwargs={'poll_interval': 0.01})
        self.thread.daemon = True  # In case this function raises.
        self.thread.start()

        if connect:
            self.client = self.imap_class(*self.server.server_address)

        return self.client, self.server

    def _cleanup(self):
        """
        Cleans up the test server. This method should not be called manually,
        it is added to the cleanup queue in the _setup method already.
        """
        # if logout was called already we'd raise an exception trying to
        # shutdown the client once again
        if self.client is not None and self.client.state != 'LOGOUT':
            self.client.shutdown()
        # cleanup the server
        self.server.shutdown()
        self.server.server_close()
        threading_helper.join_thread(self.thread)
        # Explicitly clear the attribute to prevent dangling thread
        self.thread = None

    def test_EOF_without_complete_welcome_message(self):
        # http://bugs.python.org/issue5949
        class EOFHandler(socketserver.StreamRequestHandler):
            def handle(self):
                self.wfile.write(b'* OK')
        _, server = self._setup(EOFHandler, connect=False)
        self.assertRaises(imaplib.IMAP4.abort, self.imap_class,
                          *server.server_address)

    def test_invalid_greeting(self):
        # An invalid greeting, e.g. from a POP3 server on the IMAP port,
        # must not fail with "error: None" but report the server's line
        # (gh-108280).
        class Pop3Handler(socketserver.StreamRequestHandler):
            def handle(self):
                self.wfile.write(b'+OK POP3 server ready\r\n')
        _, server = self._setup(Pop3Handler, connect=False)
        with self.assertRaisesRegex(imaplib.IMAP4.error,
                                    r'invalid greeting: \+OK POP3 server ready'):
            self.imap_class(*server.server_address)

    def test_invalid_greeting_untagged(self):
        # An untagged greeting that is neither OK nor PREAUTH (e.g. BYE)
        # is reported as is (gh-108280).
        class ByeHandler(socketserver.StreamRequestHandler):
            def handle(self):
                self.wfile.write(b'* BYE Server unavailable\r\n')
        _, server = self._setup(ByeHandler, connect=False)
        with self.assertRaisesRegex(imaplib.IMAP4.error,
                                    r'invalid greeting: \* BYE Server unavailable'):
            self.imap_class(*server.server_address)

    def test_invalid_greeting_bare_continuation(self):
        # A bare continuation greeting is still reported (gh-108280).
        class BareHandler(socketserver.StreamRequestHandler):
            def handle(self):
                self.wfile.write(b'+\r\n')
        _, server = self._setup(BareHandler, connect=False)
        with self.assertRaisesRegex(imaplib.IMAP4.error,
                                    r'invalid greeting: \+'):
            self.imap_class(*server.server_address)

    def test_line_termination(self):
        class BadNewlineHandler(SimpleIMAPHandler):
            def cmd_CAPABILITY(self, tag, args):
                self._send(b'* CAPABILITY IMAP4rev1 AUTH\n')
                self._send_tagged(tag, 'OK', 'CAPABILITY completed')
        _, server = self._setup(BadNewlineHandler, connect=False)
        self.assertRaises(imaplib.IMAP4.abort, self.imap_class,
                          *server.server_address)

    def test_enable_raises_error_if_not_AUTH(self):
        class EnableHandler(SimpleIMAPHandler):
            capabilities = 'AUTH ENABLE UTF8=ACCEPT'
        client, _ = self._setup(EnableHandler)
        self.assertFalse(client.utf8_enabled)
        with self.assertRaisesRegex(imaplib.IMAP4.error, 'ENABLE.*NONAUTH'):
            client.enable('foo')
        self.assertFalse(client.utf8_enabled)

    def test_enable_raises_error_if_no_capability(self):
        client, _ = self._setup(SimpleIMAPHandler)
        with self.assertRaisesRegex(imaplib.IMAP4.error,
                'does not support ENABLE'):
            client.enable('foo')

    def test_enable_UTF8_raises_error_if_not_supported(self):
        client, _ = self._setup(SimpleIMAPHandler)
        typ, data = client.login('user', 'pass')
        self.assertEqual(typ, 'OK')
        with self.assertRaisesRegex(imaplib.IMAP4.error,
                'does not support ENABLE'):
            client.enable('UTF8=ACCEPT')

    def test_enable_UTF8_True_append(self):
        class UTF8AppendServer(SimpleIMAPHandler):
            capabilities = 'ENABLE UTF8=ACCEPT'
            def cmd_ENABLE(self, tag, args):
                self._send_tagged(tag, 'OK', 'ENABLE successful')
            def cmd_AUTHENTICATE(self, tag, args):
                self._send_textline('+')
                self.server.response = yield
                self._send_tagged(tag, 'OK', 'FAKEAUTH successful')
            def cmd_APPEND(self, tag, args):
                self.server.response = args
                self.server.response.append(_read_literal(self, args[-1]))
                literal = yield
                self.server.response.append(literal)
                self._send_tagged(tag, 'OK', 'okay')
        client, server = self._setup(UTF8AppendServer)
        self.assertEqual(client._encoding, 'ascii')
        code, _ = client.authenticate('MYAUTH', lambda x: b'fake')
        self.assertEqual(code, 'OK')
        self.assertEqual(server.response, b'ZmFrZQ==\r\n')  # b64 encoded 'fake'
        code, _ = client.enable('UTF8=ACCEPT')
        self.assertEqual(code, 'OK')
        self.assertEqual(client._encoding, 'utf-8')
        self.assertEqual(server.args, ['UTF8=ACCEPT'])
        msg_string = 'Subject: üñí©öðé'
        typ, data = client.append(
            None, None, None, (msg_string + '\n').encode('utf-8'))
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.response,
            ['INBOX', 'UTF8',
             '(~{25}', ('%s\r\n' % msg_string).encode('utf-8'),
             b')\r\n' ])

    def test_search_disallows_charset_in_utf8_mode(self):
        class UTF8Server(SimpleIMAPHandler):
            capabilities = 'AUTH ENABLE UTF8=ACCEPT'
            def cmd_ENABLE(self, tag, args):
                self._send_tagged(tag, 'OK', 'ENABLE successful')
            def cmd_AUTHENTICATE(self, tag, args):
                self._send_textline('+')
                self.server.response = yield
                self._send_tagged(tag, 'OK', 'FAKEAUTH successful')
        client, _ = self._setup(UTF8Server)
        typ, _ = client.authenticate('MYAUTH', lambda x: b'fake')
        self.assertEqual(typ, 'OK')
        typ, _ = client.enable('UTF8=ACCEPT')
        self.assertEqual(typ, 'OK')
        self.assertTrue(client.utf8_enabled)
        with self.assertRaisesRegex(imaplib.IMAP4.error, 'charset.*UTF8'):
            client.search('foo', 'bar')

    def test_utf8_mailbox_name(self):
        class UTF8Server(SimpleIMAPHandler):
            capabilities = 'AUTH ENABLE UTF8=ACCEPT'
            def cmd_ENABLE(self, tag, args):
                self._send_tagged(tag, 'OK', 'ENABLE successful')
            def cmd_AUTHENTICATE(self, tag, args):
                self._send_textline('+')
                self.server.response = yield
                self._send_tagged(tag, 'OK', 'FAKEAUTH successful')
        client, server = self._setup(UTF8Server)
        typ, _ = client.authenticate('MYAUTH', lambda x: b'fake')
        self.assertEqual(typ, 'OK')
        typ, _ = client.enable('UTF8=ACCEPT')
        self.assertEqual(typ, 'OK')
        # A non-ASCII mailbox name is only allowed in a quoted string
        # or a literal, never in an atom (RFC 6855).
        typ, _ = client.select('Entwürfe')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.is_selected, ['"Entwürfe"'])

    def test_bad_auth_name(self):
        class MyServer(SimpleIMAPHandler):
            def cmd_AUTHENTICATE(self, tag, args):
                self._send_tagged(tag, 'NO',
                    'unrecognized authentication type {}'.format(args[0]))
        client, _ = self._setup(MyServer)
        with self.assertRaisesRegex(imaplib.IMAP4.error,
                'unrecognized authentication type METHOD'):
            client.authenticate('METHOD', lambda: 1)

    def test_invalid_authentication(self):
        class MyServer(SimpleIMAPHandler):
            def cmd_AUTHENTICATE(self, tag, args):
                self._send_textline('+')
                self.response = yield
                self._send_tagged(tag, 'NO', '[AUTHENTICATIONFAILED] invalid')
        client, _ = self._setup(MyServer)
        with self.assertRaisesRegex(imaplib.IMAP4.error,
                r'\[AUTHENTICATIONFAILED\] invalid'):
            client.authenticate('MYAUTH', lambda x: b'fake')

    def test_invalid_login(self):
        class MyServer(SimpleIMAPHandler):
            def cmd_LOGIN(self, tag, args):
                self.server.logged = args[0]
                self._send_tagged(tag, 'NO', '[LOGIN] failed')
        client, _ = self._setup(MyServer)
        with self.assertRaisesRegex(imaplib.IMAP4.error,
                r'\[LOGIN\] failed'):
            client.login('user', 'wrongpass')

    def test_valid_authentication_bytes(self):
        class MyServer(SimpleIMAPHandler):
            def cmd_AUTHENTICATE(self, tag, args):
                self._send_textline('+')
                self.server.response = yield
                self._send_tagged(tag, 'OK', 'FAKEAUTH successful')
        client, server = self._setup(MyServer)
        code, _ = client.authenticate('MYAUTH', lambda x: b'fake')
        self.assertEqual(code, 'OK')
        self.assertEqual(server.response, b'ZmFrZQ==\r\n')  # b64 encoded 'fake'

    def test_valid_authentication_plain_text(self):
        class MyServer(SimpleIMAPHandler):
            def cmd_AUTHENTICATE(self, tag, args):
                self._send_textline('+')
                self.server.response = yield
                self._send_tagged(tag, 'OK', 'FAKEAUTH successful')
        client, server = self._setup(MyServer)
        code, _ = client.authenticate('MYAUTH', lambda x: 'fake')
        self.assertEqual(code, 'OK')
        self.assertEqual(server.response, b'ZmFrZQ==\r\n')  # b64 encoded 'fake'

    @hashlib_helper.requires_hashdigest('md5', openssl=True)
    def test_login_cram_md5_bytes(self):
        client, _ = self._setup(AuthHandler_CRAM_MD5)
        self.assertIn('AUTH=CRAM-MD5', client.capabilities)
        ret, _ = client.login_cram_md5("tim", b"tanstaaftanstaaf")
        self.assertEqual(ret, "OK")

    @hashlib_helper.requires_hashdigest('md5', openssl=True)
    def test_login_cram_md5_plain_text(self):
        client, _ = self._setup(AuthHandler_CRAM_MD5)
        self.assertIn('AUTH=CRAM-MD5', client.capabilities)
        ret, _ = client.login_cram_md5("tim", "tanstaaftanstaaf")
        self.assertEqual(ret, "OK")

    @hashlib_helper.block_algorithm("md5")
    def test_login_cram_md5_blocked(self):
        client, _ = self._setup(AuthHandler_CRAM_MD5)
        self.assertIn('AUTH=CRAM-MD5', client.capabilities)
        msg = re.escape("CRAM-MD5 authentication is not supported")
        with self.assertRaisesRegex(imaplib.IMAP4.error, msg):
            client.login_cram_md5("tim", b"tanstaaftanstaaf")

    def test_login_plain_ascii(self):
        client, server = self._setup(AuthHandler_PLAIN)
        self.assertIn('AUTH=PLAIN', client.capabilities)
        ret, _ = client.login_plain("prem", "pass")
        self.assertEqual(ret, "OK")
        self.assertEqual(server.auth_args, ['PLAIN'])
        self.assertEqual(server.response, b'AHByZW0AcGFzcw==\r\n')

    def test_login_plain_utf8(self):
        client, server = self._setup(AuthHandler_PLAIN)
        self.assertIn('AUTH=PLAIN', client.capabilities)
        ret, _ = client.login_plain("pręm", "żółć")
        self.assertEqual(ret, "OK")
        self.assertEqual(server.response, b'AHByxJltAMW8w7PFgsSH\r\n')

    def test_login_plain_bytes(self):
        # bytes are accepted and sent verbatim (not re-encoded).
        client, server = self._setup(AuthHandler_PLAIN)
        ret, _ = client.login_plain(b'pr\xc4\x99m', b'\xc5\xbc\xc3\xb3\xc5\x82\xc4\x87')
        self.assertEqual(ret, "OK")
        self.assertEqual(server.response, b'AHByxJltAMW8w7PFgsSH\r\n')

    def test_login_plain_nul(self):
        client, _ = self._setup(AuthHandler_PLAIN)
        with self.assertRaises(ValueError):
            client.login_plain('user\0', 'pass')
        with self.assertRaises(ValueError):
            client.login_plain('user', b'pa\0ss')

    def test_aborted_authentication(self):
        class MyServer(SimpleIMAPHandler):
            def cmd_AUTHENTICATE(self, tag, args):
                self._send_textline('+')
                self.response = yield
                if self.response == b'*\r\n':
                    self._send_tagged(
                        tag,
                        'NO',
                        '[AUTHENTICATIONFAILED] aborted')
                else:
                    self._send_tagged(tag, 'OK', 'MYAUTH successful')
        client, _ = self._setup(MyServer)
        with self.assertRaisesRegex(imaplib.IMAP4.error,
                r'\[AUTHENTICATIONFAILED\] aborted'):
            client.authenticate('MYAUTH', lambda x: None)

    @mock.patch('imaplib._MAXLINE', 10)
    def test_linetoolong(self):
        class TooLongHandler(SimpleIMAPHandler):
            def handle(self):
                # send response line longer than the limit set in the next line
                self.wfile.write(b'* OK ' + 11 * b'x' + b'\r\n')
        _, server = self._setup(TooLongHandler, connect=False)
        with self.assertRaisesRegex(imaplib.IMAP4.error,
                'got more than 10 bytes'):
            self.imap_class(*server.server_address)

    def test_simple_with_statement(self):
        _, server = self._setup(SimpleIMAPHandler, connect=False)
        with self.imap_class(*server.server_address):
            pass

    def test_imaplib_timeout_test(self):
        _, server = self._setup(SimpleIMAPHandler, connect=False)
        with self.imap_class(*server.server_address, timeout=None) as client:
            self.assertEqual(client.sock.timeout, None)
        with self.imap_class(*server.server_address, timeout=support.LOOPBACK_TIMEOUT) as client:
            self.assertEqual(client.sock.timeout, support.LOOPBACK_TIMEOUT)
        with self.assertRaises(ValueError):
            self.imap_class(*server.server_address, timeout=0)

    def test_imaplib_timeout_functionality_test(self):
        class TimeoutHandler(SimpleIMAPHandler):
            def handle(self):
                time.sleep(1)
                SimpleIMAPHandler.handle(self)

        _, server = self._setup(TimeoutHandler)
        addr = server.server_address[1]
        with self.assertRaises(TimeoutError):
            client = self.imap_class("localhost", addr, timeout=0.001)

    def test_with_statement(self):
        _, server = self._setup(SimpleIMAPHandler, connect=False)
        with self.imap_class(*server.server_address) as imap:
            imap.login('user', 'pass')
            self.assertEqual(server.logged, ['user', '"pass"'])
        self.assertIsNone(server.logged)

    def test_with_statement_logout(self):
        # It is legal to log out explicitly inside the with block
        _, server = self._setup(SimpleIMAPHandler, connect=False)
        with self.imap_class(*server.server_address) as imap:
            imap.login('user', 'pass')
            self.assertEqual(server.logged, ['user', '"pass"'])
            imap.logout()
            self.assertIsNone(server.logged)
        self.assertIsNone(server.logged)

    # command tests

    def test_idle_capability(self):
        client, _ = self._setup(SimpleIMAPHandler)
        with self.assertRaisesRegex(imaplib.IMAP4.error,
                'does not support IMAP4 IDLE'):
            with client.idle():
                pass

    def test_idle_denied(self):
        client, _ = self._setup(IdleCmdDenyHandler)
        client.login('user', 'pass')
        with self.assertRaises(imaplib.IMAP4.error):
            with client.idle() as idler:
                pass

    def test_idle_iter(self):
        client, _ = self._setup(IdleCmdHandler)
        client.login('user', 'pass')
        with client.idle() as idler:
            # iteration should include response between 'IDLE' & '+ idling'
            response = next(idler)
            self.assertEqual(response, ('EXISTS', [b'0']))
            # iteration should produce responses
            response = next(idler)
            self.assertEqual(response, ('EXISTS', [b'2']))
            # fragmented response (with literal string) should arrive whole
            expected_fetch_data = [
                (b'1 (BODY[HEADER.FIELDS (DATE)] {41}',
                    b'Date: Fri, 06 Dec 2024 06:00:00 +0000\r\n\r\n'),
                b')']
            typ, data = next(idler)
            self.assertEqual(typ, 'FETCH')
            self.assertEqual(data, expected_fetch_data)
            # response after a fragmented one should arrive separately
            response = next(idler)
            self.assertEqual(response, ('EXISTS', [b'3']))
        # iteration should have consumed untagged responses
        _, data = client.response('EXISTS')
        self.assertEqual(data, [None])
        # responses not iterated should be available after idle
        _, data = client.response('RECENT')
        self.assertEqual(data[0], b'1')
        # responses received after 'DONE' should be available after idle
        self.assertEqual(data[1], b'9')

    def test_idle_burst(self):
        client, _ = self._setup(IdleCmdHandler)
        client.login('user', 'pass')
        # burst() should yield immediately available responses
        with client.idle() as idler:
            batch = list(idler.burst())
            self.assertEqual(len(batch), 4)
        # burst() should not have consumed later responses
        _, data = client.response('RECENT')
        self.assertEqual(data, [b'1', b'9'])

    def test_idle_delayed_packet(self):
        client, _ = self._setup(IdleCmdDelayedPacketHandler)
        client.login('user', 'pass')
        # If our readline() implementation fails to preserve line fragments
        # when idle timeouts trigger, a response spanning delayed packets
        # can be corrupted, leaving the protocol stream in a bad state.
        try:
            with client.idle(0.5) as idler:
                self.assertRaises(StopIteration, next, idler)
        except client.abort as err:
            self.fail('multi-packet response was corrupted by idle timeout')

    def test_login(self):
        client, server = self._setup(SimpleIMAPHandler)
        typ, data = client.login('user', 'pass')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data[0], b'LOGIN completed')
        self.assertEqual(client.state, 'AUTH')
        # The user name is quoted only when necessary, but the password
        # is always quoted.
        self.assertEqual(server.logged, ['user', '"pass"'])
        self.assertRaises(imaplib.IMAP4.error, client.login, 'user', 'pass')

    def test_login_quoted(self):
        client, server = self._setup(SimpleIMAPHandler)
        typ, data = client.login('us*r', 'p%ss')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data[0], b'LOGIN completed')
        self.assertEqual(client.state, 'AUTH')
        self.assertEqual(server.logged, ['"us*r"', '"p%ss"'])

    def test_login_quoted2(self):
        # An already quoted user name is passed through unchanged, rather
        # than being quoted a second time; the password is always quoted.
        client, server = self._setup(SimpleIMAPHandler)
        typ, data = client.login('"user"', '"pass"')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data[0], b'LOGIN completed')
        self.assertEqual(client.state, 'AUTH')
        self.assertEqual(server.logged, ['"user"', r'"\"pass\""'])

    def test_append_translate_line_endings(self):
        # By default line endings are normalized to CRLF; False sends the
        # literal exactly (gh-49680).
        class AppendHandler(SimpleIMAPHandler):
            def cmd_APPEND(self, tag, args):
                self.server.response = _read_literal(self, args[-1])
                yield  # read the trailer line
                self._send_tagged(tag, 'OK', 'APPEND completed')
        client, server = self._setup(AppendHandler)
        client.login('user', 'pass')
        message = b'a\rb\nc\r\nd'
        client.append('INBOX', None, None, message)
        self.assertEqual(server.response, b'a\r\nb\r\nc\r\nd')
        client.append('INBOX', None, None, message,
                      translate_line_endings=False)
        self.assertEqual(server.response, message)

        # An email message uses bare LF by default; False sends it verbatim.
        message = EmailMessage()
        message['Subject'] = 'line endings'
        message.set_content('body line\n')
        message = message.as_bytes()
        self.assertNotIn(b'\r\n', message)
        client.append('INBOX', None, None, message,
                      translate_line_endings=False)
        self.assertEqual(server.response, message)

        # The mailbox is quoted and the flags are wrapped in parentheses
        # when necessary.
        client.append('New folder', r'\Seen', None, b'data')
        self.assertEqual(server.args, ['"New folder"', r'(\Seen)', '{4}'])

    def test_login_capabilities(self):
        # A server may advertise new capabilities after login (as an
        # untagged CAPABILITY response); imaplib must refresh its cached
        # capability list (gh-63121, gh-103451).
        class CapabilityLoginHandler(SimpleIMAPHandler):
            def cmd_LOGIN(self, tag, args):
                self.server.logged = args[0]
                self._send_textline('* CAPABILITY IMAP4rev1 ENABLE UTF8=ACCEPT')
                self._send_tagged(tag, 'OK', 'LOGIN completed')
            def cmd_ENABLE(self, tag, args):
                self._send_tagged(tag, 'OK', 'ENABLE completed')

        client, _ = self._setup(CapabilityLoginHandler)
        self.assertNotIn('ENABLE', client.capabilities)
        client.login('user', 'pass')
        self.assertIn('ENABLE', client.capabilities)
        self.assertIn('UTF8=ACCEPT', client.capabilities)
        self.assertEqual(client.response('CAPABILITY'),
                         ('CAPABILITY', [b'IMAP4rev1 ENABLE UTF8=ACCEPT']))
        typ, _ = client.enable('UTF8=ACCEPT')
        self.assertEqual(typ, 'OK')

    def test_authenticate_capabilities(self):
        # Capabilities are also refreshed after AUTHENTICATE, here from a
        # CAPABILITY response code in the tagged OK response.
        class CapabilityAuthHandler(SimpleIMAPHandler):
            def cmd_AUTHENTICATE(self, tag, args):
                self._send_textline('+')
                self.server.response = yield
                self._send_tagged(
                    tag, 'OK',
                    '[CAPABILITY IMAP4rev1 ENABLE] AUTHENTICATE completed')

        client, _ = self._setup(CapabilityAuthHandler)
        self.assertNotIn('ENABLE', client.capabilities)
        client.authenticate('MYAUTH', lambda x: b'fake')
        self.assertIn('ENABLE', client.capabilities)
        self.assertEqual(client.response('CAPABILITY'),
                         ('CAPABILITY', [b'IMAP4rev1 ENABLE']))

    def test_greeting_capabilities(self):
        # Capabilities advertised in the greeting are used directly,
        # without sending a separate CAPABILITY command.
        class GreetingHandler(SimpleIMAPHandler):
            welcome = '* OK [CAPABILITY IMAP4rev1 ENABLE] Server ready'
            def cmd_CAPABILITY(self, tag, args):
                self.server.capability_queried = True
                super().cmd_CAPABILITY(tag, args)

        client, server = self._setup(GreetingHandler)
        self.assertEqual(client.capabilities, ('IMAP4REV1', 'ENABLE'))
        self.assertFalse(getattr(server, 'capability_queried', False))
        # The greeting is not a response to a command, so it is consumed.
        self.assertEqual(client.response('CAPABILITY'), ('CAPABILITY', [None]))

    def test_login_requery_capabilities(self):
        # If the server does not advertise capabilities after login,
        # imaplib re-queries them (as it does after STARTTLS), so a
        # capability that becomes available only after authentication is
        # still recognized (gh-63121).
        class RequeryHandler(SimpleIMAPHandler):
            def cmd_CAPABILITY(self, tag, args):
                caps = 'IMAP4rev1 ENABLE' if self.server.logged else 'IMAP4rev1'
                self._send_textline('* CAPABILITY ' + caps)
                self._send_tagged(tag, 'OK', 'CAPABILITY completed')

        client, _ = self._setup(RequeryHandler)
        self.assertNotIn('ENABLE', client.capabilities)
        client.login('user', 'pass')
        self.assertIn('ENABLE', client.capabilities)

    def test_readonly_error_reports_mailbox(self):
        # The read-only error reports the mailbox via repr(), which also
        # avoids BytesWarning for a bytes mailbox under -bb.
        class ReadOnlyHandler(SimpleIMAPHandler):
            def cmd_SELECT(self, tag, args):
                self._send_line(b'* 2 EXISTS')
                self._send_tagged(tag, 'OK', '[READ-ONLY] SELECT completed.')
        client, _ = self._setup(ReadOnlyHandler)
        client.login('user', 'pass')
        for mailbox, expected in [('INBOX', "'INBOX'"), (b'INBOX', r"b'INBOX'")]:
            with self.subTest(mailbox=mailbox):
                with self.assertRaisesRegex(imaplib.IMAP4.readonly,
                                            r"%s is not writable" % expected):
                    client.select(mailbox)

    def test_uid_unknown_command_reports_command(self):
        # The unknown-UID-command error reports the command via repr(), which
        # also avoids BytesWarning for a bytes command under -bb.
        client, _ = self._setup(SimpleIMAPHandler)
        for command, expected in [('BOGUS', "'BOGUS'"), (b'BOGUS', r"b'BOGUS'")]:
            with self.subTest(command=command):
                with self.assertRaisesRegex(
                        imaplib.IMAP4.error,
                        r"Unknown IMAP4 UID command: %s" % expected):
                    client.uid(command, '1')

    def test_logout(self):
        client, _ = self._setup(SimpleIMAPHandler)
        typ, data = client.login('user', 'pass')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data[0], b'LOGIN completed')
        typ, data = client.logout()
        self.assertEqual(typ, 'BYE', (typ, data))
        self.assertEqual(data[0], b'IMAP4ref1 Server logging out', (typ, data))
        self.assertEqual(client.state, 'LOGOUT')

    def test_lsub(self):
        client, server = self._setup(make_simple_handler('LSUB',
            ['* LSUB () "." directoryA', '* LSUB () "." directoryB']))
        client.login('user', 'pass')
        typ, data = client.lsub()
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'() "." directoryA', b'() "." directoryB'])
        self.assertEqual(server.args, ['""', '*'])

        typ, data = client.lsub('~/Mail/', '%')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['~/Mail/', '%'])

        # The directory is quoted when necessary; wildcards in the pattern
        # are preserved.
        typ, data = client.lsub('New folder', '%')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['"New folder"', '%'])

    def test_extra_blank_line_after_literal(self):
        # Some buggy servers send an extra blank line after the counted
        # literal data.  imaplib should skip it instead of failing.
        class BlankLineHandler(SimpleIMAPHandler):
            def cmd_FETCH(self, tag, args):
                self._send(b'* 1 FETCH (BODY[HEADER] {13}\r\n')
                self._send(b'Subject: test')       # 13-byte literal
                self._send(b'\r\n)\r\n')            # stray blank line, then ')'
                self._send_tagged(tag, 'OK', 'FETCH completed')
        client, _ = self._setup(BlankLineHandler)
        client.login('user', 'pass')
        client.select()
        typ, data = client.fetch('1', '(BODY[HEADER])')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [(b'1 (BODY[HEADER] {13}', b'Subject: test'),
                                b')'])

    def test_literal_terminating_response(self):
        # A literal ending a response (a LIST mailbox name sent as a literal)
        # has an empty trailer that must not be swallowed.  Conforming case:
        # no spurious blank lines.
        names = [b'My (box)"', b'Another', b'Third']
        class Handler(SimpleIMAPHandler):
            def cmd_LIST(self, tag, args):
                for name in names:
                    self._send(b'* LIST (\\HasNoChildren) "/" {%d}\r\n'
                               % len(name))
                    self._send(name)
                    self._send(b'\r\n')             # ends the response, no blank
                self._send_tagged(tag, 'OK', 'LIST completed')
        client, _ = self._setup(Handler)
        client.login('user', 'pass')
        typ, data = client.list()
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [
            (b'(\\HasNoChildren) "/" {9}', b'My (box)"'), b'',
            (b'(\\HasNoChildren) "/" {7}', b'Another'), b'',
            (b'(\\HasNoChildren) "/" {5}', b'Third'), b'',
        ])

    def test_spurious_blank_lines_between_responses(self):
        # A spurious blank line after each terminating literal falls between the
        # untagged responses and must be skipped, even several in a row.
        names = [b'My (box)"', b'Another', b'Third']
        class Handler(SimpleIMAPHandler):
            def cmd_LIST(self, tag, args):
                for name in names:
                    self._send(b'* LIST (\\HasNoChildren) "/" {%d}\r\n'
                               % len(name))
                    self._send(name)
                    self._send(b'\r\n\r\n')     # ends the response, then a blank
                self._send_tagged(tag, 'OK', 'LIST completed')
        client, _ = self._setup(Handler)
        client.login('user', 'pass')
        typ, data = client.list()
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [
            (b'(\\HasNoChildren) "/" {9}', b'My (box)"'), b'',
            (b'(\\HasNoChildren) "/" {7}', b'Another'), b'',
            (b'(\\HasNoChildren) "/" {5}', b'Third'), b'',
        ])

    def test_unselect(self):
        client, server = self._setup(SimpleIMAPHandler)
        client.login('user', 'pass')
        typ, data = client.select()
        self.assertEqual(typ, 'OK')
        self.assertEqual(data[0], b'2')

        typ, data = client.unselect()
        self.assertEqual(typ, 'OK')
        self.assertEqual(data[0], b'Returned to authenticated state. (Success)')
        self.assertEqual(client.state, 'AUTH')
        self.assertIsNone(server.is_selected)

    def test_enable(self):
        class EnableHandler(SimpleIMAPHandler):
            capabilities = 'IMAP4rev1 ID LITERAL+ ENABLE X-GOOD-IDEA'
            def cmd_ENABLE(self, tag, args):
                capabilities = self.capabilities.split()
                for arg in args:
                    if arg in capabilities:
                        self._send_textline('* ENABLED ' + arg)
                self._send_tagged(tag, 'OK', 'foo')

        client, server = self._setup(EnableHandler)
        client.login('user', 'pass')
        code, data = client.enable('CONDSTORE X-GOOD-IDEA')
        self.assertEqual(code, 'OK')
        self.assertEqual(data, [b'foo'])
        self.assertEqual(server.args, ['CONDSTORE', 'X-GOOD-IDEA'])

    def test_select(self):
        client, server = self._setup(SimpleIMAPHandler)
        client.login('user', 'pass')
        typ, data = client.select()
        self.assertEqual(typ, 'OK')
        self.assertEqual(data[0], b'2')
        self.assertEqual(server.is_selected, ['INBOX'])

        typ, data = client.select('Archive')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.is_selected, ['Archive'])

        # readonly=True issues EXAMINE instead of SELECT
        # (there is no separate examine() method).
        typ, data = client.select(readonly=True)
        self.assertEqual(typ, 'OK')
        self.assertEqual(data[0], b'2')
        self.assertEqual(server.is_selected, ['INBOX'])
        self.assertTrue(client.is_readonly)

        typ, data = client.select('New folder')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.is_selected, ['"New folder"'])

    def test_expunge(self):
        client, server = self._setup(make_simple_handler('EXPUNGE',
            ['* 3 EXPUNGE', '* 3 EXPUNGE', '* 5 EXPUNGE', '* 8 EXPUNGE']))
        client.login('user', 'pass')
        client.select()
        typ, data = client.expunge()
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'3', b'3', b'5', b'8'])

        # A message set is only accepted with uid=True (UID EXPUNGE).
        with self.assertRaises(imaplib.IMAP4.error):
            client.expunge('3:8')

    def test_uid_expunge(self):
        client, server = self._setup(make_simple_handler('UID',
            ['* 3 EXPUNGE', '* 3 EXPUNGE', '* 5 EXPUNGE', '* 8 EXPUNGE'],
            'UID EXPUNGE completed'))
        client.login('user', 'pass')
        client.select()
        typ, data = client.expunge('3:8', uid=True)
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'3', b'3', b'5', b'8'])
        self.assertEqual(server.args, ['EXPUNGE', '3:8'])

        # UID EXPUNGE requires a message set.
        with self.assertRaises(imaplib.IMAP4.error):
            client.expunge(uid=True)

    def test_close(self):
        client, server = self._setup(make_simple_handler('CLOSE'))
        client.login('user', 'pass')
        client.select()
        typ, data = client.close()
        self.assertEqual(typ, 'OK')
        self.assertEqual(client.state, 'AUTH')

    def test_check(self):
        client, server = self._setup(make_simple_handler('CHECK'))
        client.login('user', 'pass')
        client.select()
        typ, data = client.check()
        self.assertEqual(typ, 'OK')

    def test_noop(self):
        client, server = self._setup(make_simple_handler('NOOP',
            ['* 4 EXISTS']))
        client.login('user', 'pass')
        typ, data = client.noop()
        self.assertEqual(typ, 'OK')
        # NOOP is used to pick up server-pushed untagged responses.
        self.assertEqual(client.untagged_responses['EXISTS'], [b'4'])

    def test_namespace(self):
        client, server = self._setup(make_simple_handler('NAMESPACE',
            ['* NAMESPACE (("" "/")) NIL NIL']))
        client.login('user', 'pass')
        typ, data = client.namespace()
        self.assertEqual(typ, 'OK')
        self.assertEqual(data[0], b'(("" "/")) NIL NIL')

    def test_capability(self):
        client, server = self._setup(make_simple_handler('CAPABILITY',
            ['* CAPABILITY IMAP4rev1 IDLE NAMESPACE']))
        client.login('user', 'pass')
        typ, data = client.capability()
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'IMAP4rev1 IDLE NAMESPACE'])

    def test_recent(self):
        # recent() prods the server with NOOP if no RECENT was seen yet.
        client, server = self._setup(make_simple_handler('NOOP',
            ['* 5 RECENT']))
        client.login('user', 'pass')
        typ, data = client.recent()
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'5'])

    def test_response(self):
        client, server = self._setup(SimpleIMAPHandler)
        client.login('user', 'pass')
        client.select()  # the handler answers with '* 2 EXISTS'
        typ, data = client.response('EXISTS')
        self.assertEqual(typ, 'EXISTS')
        self.assertEqual(data, [b'2'])
        # The value is cleared once read.
        typ, data = client.response('EXISTS')
        self.assertEqual(data, [None])

    def test_create(self):
        client, server = self._setup(make_simple_handler('CREATE'))
        client.login('user', 'pass')
        typ, data = client.create('owatagusiam/')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'CREATE completed'])
        self.assertEqual(server.args, ['owatagusiam/'])

        typ, data = client.create('owatagusiam/blurdybloop')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'CREATE completed'])
        self.assertEqual(server.args, ['owatagusiam/blurdybloop'])

        typ, data = client.create('New folder')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'CREATE completed'])
        self.assertEqual(server.args, ['"New folder"'])

    def test_copy(self):
        client, server = self._setup(make_simple_handler('COPY'))
        client.login('user', 'pass')
        client.select()
        typ, data = client.copy('2:4', 'MEETING')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'COPY completed'])
        self.assertEqual(server.args, ['2:4', 'MEETING'])

        typ, data = client.copy('2:4', 'New folder')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'COPY completed'])
        self.assertEqual(server.args, ['2:4', '"New folder"'])

        # A structured message set is formatted into a sequence set.
        typ, data = client.copy([2, (3, 5)], 'MEETING')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['2,3:5', 'MEETING'])

    def test_uid_copy(self):
        client, server = self._setup(make_simple_handler('UID',
            completed='UID COPY completed'))
        client.login('user', 'pass')
        client.select()
        typ, data = client.uid('copy', '4827313:4828442', 'MEETING')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [None])
        self.assertEqual(server.args, ['COPY', '4827313:4828442', 'MEETING'])

        typ, data = client.uid('copy', '4827313:4828442', 'New folder')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [None])
        self.assertEqual(server.args, ['COPY', '4827313:4828442', '"New folder"'])

        # The uid=True keyword is a shorthand for uid('COPY', ...).
        typ, data = client.copy('4827313:4828442', 'MEETING', uid=True)
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'UID COPY completed'])
        self.assertEqual(server.args, ['COPY', '4827313:4828442', 'MEETING'])

        # A structured message set is formatted into a sequence set.
        typ, data = client.uid('copy', [1, (3, 5)], 'MEETING')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['COPY', '1,3:5', 'MEETING'])

    def test_move(self):
        client, server = self._setup(make_simple_handler('MOVE'))
        client.login('user', 'pass')
        client.select()
        typ, data = client.move('2:4', 'MEETING')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'MOVE completed'])
        self.assertEqual(server.args, ['2:4', 'MEETING'])

        typ, data = client.move('2:4', 'New folder')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'MOVE completed'])
        self.assertEqual(server.args, ['2:4', '"New folder"'])

        # A structured message set is formatted into a sequence set.
        typ, data = client.move([2, (3, 5)], 'MEETING')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['2,3:5', 'MEETING'])

    def test_uid_move(self):
        client, server = self._setup(make_simple_handler('UID',
            completed='UID MOVE completed'))
        client.login('user', 'pass')
        client.select()
        typ, data = client.uid('move', '4827313:4828442', 'MEETING')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [None])
        self.assertEqual(server.args, ['MOVE', '4827313:4828442', 'MEETING'])

        typ, data = client.uid('move', '4827313:4828442', 'New folder')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [None])
        self.assertEqual(server.args, ['MOVE', '4827313:4828442', '"New folder"'])

        # The uid=True keyword is a shorthand for uid('MOVE', ...).
        typ, data = client.move('4827313:4828442', 'MEETING', uid=True)
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'UID MOVE completed'])
        self.assertEqual(server.args, ['MOVE', '4827313:4828442', 'MEETING'])

        # A structured message set is formatted into a sequence set.
        typ, data = client.uid('move', [1, (3, 5)], 'MEETING')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['MOVE', '1,3:5', 'MEETING'])

    def test_store(self):
        client, server = self._setup(make_simple_handler('STORE', [
            r'* 2 FETCH (FLAGS (\Deleted \Seen))',
            r'* 3 FETCH (FLAGS (\Deleted))',
            r'* 4 FETCH (FLAGS (\Deleted \Flagged \Seen))',
        ]))
        client.login('user', 'pass')
        client.select()
        typ, data = client.store('2:4', '+FLAGS', r'(\Deleted)')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [
            br'2 (FLAGS (\Deleted \Seen))',
            br'3 (FLAGS (\Deleted))',
            br'4 (FLAGS (\Deleted \Flagged \Seen))',
        ])
        self.assertEqual(server.args, ['2:4', '+FLAGS', r'(\Deleted)'])

        typ, data = client.store('2:4', '+FLAGS', r'\Deleted')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['2:4', '+FLAGS', r'(\Deleted)'])

        # The flags may be a sequence, and the message set may be structured.
        typ, data = client.store([2, (3, 4)], '+FLAGS',
                                 [r'\Deleted', r'\Seen'])
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['2,3:4', '+FLAGS', r'(\Deleted \Seen)'])

    def test_uid_store(self):
        client, server = self._setup(make_simple_handler('UID', [
            r'* 23 FETCH (FLAGS (\Deleted \Seen) UID 4827313)',
            r'* 24 FETCH (FLAGS (\Deleted) UID 4827943)',
            r'* 25 FETCH (FLAGS (\Deleted \Flagged \Seen) UID 4828442)',
        ], 'UID STORE completed'))
        client.login('user', 'pass')
        client.select()
        typ, data = client.uid('store', '4827313:4828442', '+FLAGS', r'(\Deleted)')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [
            br'23 (FLAGS (\Deleted \Seen) UID 4827313)',
            br'24 (FLAGS (\Deleted) UID 4827943)',
            br'25 (FLAGS (\Deleted \Flagged \Seen) UID 4828442)',
        ])
        self.assertEqual(server.args, ['STORE', '4827313:4828442', '+FLAGS', r'(\Deleted)'])

        typ, data = client.uid('store', '4827313:4828442', '+FLAGS', r'\Deleted')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['STORE', '4827313:4828442', '+FLAGS', r'(\Deleted)'])

        # The uid=True keyword is a shorthand for uid('STORE', ...).
        typ, data = client.store('4827313:4828442', '+FLAGS', r'(\Deleted)',
                                 uid=True)
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [
            br'23 (FLAGS (\Deleted \Seen) UID 4827313)',
            br'24 (FLAGS (\Deleted) UID 4827943)',
            br'25 (FLAGS (\Deleted \Flagged \Seen) UID 4828442)',
        ])
        self.assertEqual(server.args, ['STORE', '4827313:4828442', '+FLAGS', r'(\Deleted)'])

        # The flags may be a sequence.
        typ, data = client.uid('store', '4827313:4828442', '+FLAGS',
                               [r'\Deleted', r'\Seen'])
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args,
                         ['STORE', '4827313:4828442', '+FLAGS', r'(\Deleted \Seen)'])

    def test_fetch(self):
        # The handler expands the requested sequence set and answers for
        # exactly those messages, so the test exercises the round trip of
        # the message set, not just a canned reply.
        class FetchHandler(SimpleIMAPHandler):
            messages = 4
            def cmd_SELECT(self, tag, args):
                self.server.is_selected = args
                self._send_line(b'* %d EXISTS' % self.messages)
                self._send_tagged(tag, 'OK', '[READ-WRITE] SELECT completed.')
            def cmd_FETCH(self, tag, args):
                for n in parse_sequence_set(args[0], self.messages):
                    self._send_textline(r'* %d FETCH (FLAGS (\Seen))' % n)
                self._send_tagged(tag, 'OK', 'FETCH completed')

        client, server = self._setup(FetchHandler)
        client.login('user', 'pass')
        client.select()
        typ, data = client.fetch('2:4', '(FLAGS)')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [
            br'2 (FLAGS (\Seen))',
            br'3 (FLAGS (\Seen))',
            br'4 (FLAGS (\Seen))',
        ])
        self.assertEqual(server.args, ['2:4', '(FLAGS)'])

        # A preformatted message set may be passed as bytes.
        typ, data = client.fetch(b'2:4', '(FLAGS)')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [
            br'2 (FLAGS (\Seen))',
            br'3 (FLAGS (\Seen))',
            br'4 (FLAGS (\Seen))',
        ])
        self.assertEqual(server.args, ['2:4', '(FLAGS)'])

        # A comma-separated set with an open range up to '*'.
        typ, data = client.fetch('1,3:*', '(FLAGS)')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [
            br'1 (FLAGS (\Seen))',
            br'3 (FLAGS (\Seen))',
            br'4 (FLAGS (\Seen))',
        ])
        self.assertEqual(server.args, ['1,3:*', '(FLAGS)'])

        # An item with nested parentheses is sent (and parsed) as a
        # single argument.
        typ, data = client.fetch('1', '(BODY[HEADER.FIELDS (DATE FROM)])')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [br'1 (FLAGS (\Seen))'])
        self.assertEqual(server.args, ['1', '(BODY[HEADER.FIELDS (DATE FROM)])'])

        # message_parts is wrapped in parentheses if it is not already.
        typ, data = client.fetch('2:4', 'FLAGS')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['2:4', '(FLAGS)'])

        # But the macros are not, as they are not data item names.
        typ, data = client.fetch('2:4', 'ALL')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['2:4', 'ALL'])
        typ, data = client.fetch('2:4', 'fast')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['2:4', 'fast'])

        # A structured message set is formatted into a sequence set.
        typ, data = client.fetch([2, (3, 4)], '(FLAGS)')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['2,3:4', '(FLAGS)'])

        # message_parts may be a sequence of items.
        typ, data = client.fetch('1', ['UID', 'FLAGS'])
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['1', '(UID FLAGS)'])

        # 'params' substitutes and quotes '?' placeholders.
        typ, data = client.fetch('1', 'FLAGS BODY[HEADER.FIELDS ?]',
                                 params=[['DATE', 'FROM']])
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args,
                         ['1', '(FLAGS BODY[HEADER.FIELDS (DATE FROM)])'])

    def test_uid_fetch(self):
        client, server = self._setup(make_simple_handler('UID', [
            r'* 23 FETCH (FLAGS (\Seen) UID 4827313)',
            r'* 24 FETCH (FLAGS (\Seen) UID 4827943)',
            r'* 25 FETCH (FLAGS (\Seen) UID 4828442)',
        ], 'UID FETCH completed'))
        client.login('user', 'pass')
        client.select()
        typ, data = client.uid('fetch', '4827313:4828442', '(FLAGS)')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [
            br'23 (FLAGS (\Seen) UID 4827313)',
            br'24 (FLAGS (\Seen) UID 4827943)',
            br'25 (FLAGS (\Seen) UID 4828442)',
        ])
        self.assertEqual(server.args, ['FETCH', '4827313:4828442', '(FLAGS)'])

        typ, data = client.uid('fetch', '4827313:4828442', 'FLAGS')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['FETCH', '4827313:4828442', '(FLAGS)'])

        typ, data = client.uid('fetch', '4827313:4828442', 'ALL')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['FETCH', '4827313:4828442', 'ALL'])

        # The uid=True keyword is a shorthand for uid('FETCH', ...).
        typ, data = client.fetch('4827313:4828442', '(FLAGS)', uid=True)
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [
            br'23 (FLAGS (\Seen) UID 4827313)',
            br'24 (FLAGS (\Seen) UID 4827943)',
            br'25 (FLAGS (\Seen) UID 4828442)',
        ])
        self.assertEqual(server.args, ['FETCH', '4827313:4828442', '(FLAGS)'])

        # message_parts may be a sequence, and 'params' substitutes '?'.
        typ, data = client.uid('fetch', '4827313:4828442', ['UID', 'FLAGS'])
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['FETCH', '4827313:4828442', '(UID FLAGS)'])

        typ, data = client.uid('fetch', '4827313:4828442',
                               'BODY[HEADER.FIELDS ?]', params=[['DATE', 'FROM']])
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args,
                         ['FETCH', '4827313:4828442',
                          '(BODY[HEADER.FIELDS (DATE FROM)])'])

    def test_partial(self):
        client, server = self._setup(make_simple_handler('PARTIAL',
            ['* 1 FETCH (RFC822.TEXT<0.10> "0123456789")']))
        client.login('user', 'pass')
        client.select()
        typ, data = client.partial('1', 'RFC822.TEXT', '0', '10')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'1 (RFC822.TEXT<0.10> "0123456789")'])
        self.assertEqual(server.args, ['1', 'RFC822.TEXT', '0', '10'])

    def test_search(self):
        response = []
        client, server = self._setup(make_simple_handler('SEARCH', response))
        client.login('user', 'pass')
        client.select()
        response[:] = ['* SEARCH 2 84 882']
        typ, data = client.search(None, 'FLAGGED', 'SINCE', '1-Feb-1994', 'NOT', 'FROM', '"Smith"')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'2 84 882'])
        self.assertEqual(server.args, ['FLAGGED', 'SINCE', '1-Feb-1994', 'NOT', 'FROM', '"Smith"'])

        response[:] = ['* SEARCH']
        typ, data = client.search(None, 'TEXT', '"string not in mailbox"')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b''])
        self.assertEqual(server.args, ['TEXT', '"string not in mailbox"'])

        response[:] = ['* SEARCH 43']
        typ, data = client.search('UTF-8', 'TEXT', 'XXXXXX')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'43'])
        self.assertEqual(server.args, ['CHARSET', 'UTF-8', 'TEXT', 'XXXXXX'])

        # A non-ASCII str criterion is encoded to the declared charset (KOI8-U
        # here, which is not UTF-8, so check the encoded bytes on the wire).
        response[:] = ['* SEARCH 43']
        typ, data = client.search('KOI8-U', 'SUBJECT', '"Київ"')
        self.assertEqual(typ, 'OK')
        self.assertIn(b'CHARSET KOI8-U ', server.line)
        self.assertIn('"Київ"'.encode('koi8-u'), server.line)

        # bytes criteria keep this focused on charset-name quoting (the
        # parentheses force the name to be quoted) without criteria encoding.
        typ, data = client.search('NF_Z_62-010_(1973)', b'TEXT', b'XXXXXX')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['CHARSET', '"NF_Z_62-010_(1973)"', 'TEXT', 'XXXXXX'])

        # 'params' substitutes and quotes '?' placeholders.
        response[:] = ['* SEARCH 1']
        typ, data = client.search(None, 'FROM ? SUBJECT ?',
                                  params=['me@host', 'trip report'])
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args,
                         ['FROM', 'me@host', 'SUBJECT', '"trip report"'])

        # Without 'params', a literal '?' is sent unchanged.
        typ, data = client.search(None, 'SUBJECT', '"what?"')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['SUBJECT', '"what?"'])

    def test_uid_search(self):
        response = []
        client, server = self._setup(make_simple_handler('UID', response,
            'UID SEARCH completed'))
        client.login('user', 'pass')
        client.select()
        response[:] = ['* SEARCH 2 84 882']
        typ, data = client.uid('SEARCH', 'FLAGGED', 'SINCE', '1-Feb-1994', 'NOT', 'FROM', '"Smith"')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'2 84 882'])
        self.assertEqual(server.args, ['SEARCH', 'FLAGGED', 'SINCE', '1-Feb-1994', 'NOT', 'FROM', '"Smith"'])

        response[:] = ['* SEARCH']
        typ, data = client.uid('SEARCH', 'TEXT', '"string not in mailbox"')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b''])
        self.assertEqual(server.args, ['SEARCH', 'TEXT', '"string not in mailbox"'])

        response[:] = ['* SEARCH 43']
        typ, data = client.uid('SEARCH', 'CHARSET', 'UTF-8', 'TEXT', 'XXXXXX')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'43'])
        self.assertEqual(server.args, ['SEARCH', 'CHARSET', 'UTF-8', 'TEXT', 'XXXXXX'])

        typ, data = client.uid('SEARCH', 'CHARSET', '"NF_Z_62-010_(1973)"', 'TEXT', 'XXXXXX')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['SEARCH', 'CHARSET', '"NF_Z_62-010_(1973)"', 'TEXT', 'XXXXXX'])

        # The uid=True keyword is a shorthand for uid('SEARCH', ...).
        response[:] = ['* SEARCH 2 84 882']
        typ, data = client.search(None, 'FLAGGED', 'SINCE', '1-Feb-1994',
                                  'NOT', 'FROM', '"Smith"', uid=True)
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'2 84 882'])
        self.assertEqual(server.args,
                ['SEARCH', 'FLAGGED', 'SINCE', '1-Feb-1994', 'NOT', 'FROM', '"Smith"'])

        response[:] = ['* SEARCH 43']
        typ, data = client.search('UTF-8', 'TEXT', 'XXXXXX', uid=True)
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'43'])
        self.assertEqual(server.args, ['SEARCH', 'CHARSET', 'UTF-8', 'TEXT', 'XXXXXX'])

        # 'params' substitutes and quotes '?' placeholders.
        response[:] = ['* SEARCH 1']
        typ, data = client.uid('SEARCH', 'FROM ? SUBJECT ?',
                               params=['me@host', 'trip report'])
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args,
                         ['SEARCH', 'FROM', 'me@host', 'SUBJECT', '"trip report"'])

    def test_sort(self):
        response = []
        client, server = self._setup(make_simple_handler('SORT', response))
        client.login('user', 'pass')
        client.select()
        response[:] = ['* SORT 2 84 882']
        typ, data = client.sort('(SUBJECT)', 'UTF-8', 'SINCE', '1-Feb-1994')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [br'2 84 882'])
        self.assertEqual(server.args, ['(SUBJECT)', 'UTF-8', 'SINCE', '1-Feb-1994'])

        response[:] = ['* SORT 5 3 4 1 2']
        typ, data = client.sort('(SUBJECT REVERSE DATE)', 'UTF-8', 'ALL')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [br'5 3 4 1 2'])
        self.assertEqual(server.args, ['(SUBJECT REVERSE DATE)', 'UTF-8', 'ALL'])

        response[:] = ['* SORT']
        typ, data = client.sort('(SUBJECT)', 'US-ASCII', 'TEXT', '"not in mailbox"')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [br''])
        self.assertEqual(server.args, ['(SUBJECT)', 'US-ASCII', 'TEXT', '"not in mailbox"'])

        typ, data = client.sort('SUBJECT', 'NF_Z_62-010_(1973)', b'TEXT', b'"not in mailbox"')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['(SUBJECT)', '"NF_Z_62-010_(1973)"', 'TEXT', '"not in mailbox"'])

        # A non-ASCII str criterion is encoded to the declared charset.
        response[:] = ['* SORT']
        typ, data = client.sort('(SUBJECT)', 'KOI8-U', 'TEXT', '"Київ"')
        self.assertEqual(typ, 'OK')
        self.assertIn('"Київ"'.encode('koi8-u'), server.line)

        # sort_criteria may be a sequence, and 'params' substitutes and
        # quotes '?' (a value with a space becomes a quoted string).
        response[:] = ['* SORT 1']
        typ, data = client.sort(['REVERSE', 'DATE'], 'UTF-8', 'SUBJECT ?',
                                params=['trip report'])
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args,
                         ['(REVERSE DATE)', 'UTF-8', 'SUBJECT', '"trip report"'])

    def test_uid_sort(self):
        response = []
        client, server = self._setup(make_simple_handler('UID', response,
            'UID SORT completed'))
        client.login('user', 'pass')
        client.select()
        response[:] = ['* SORT 2 84 882']
        typ, data = client.uid('sort', '(SUBJECT)', 'UTF-8', 'SINCE', '1-Feb-1994')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [br'2 84 882'])
        self.assertEqual(server.args, ['SORT', '(SUBJECT)', 'UTF-8', 'SINCE', '1-Feb-1994'])

        response[:] = ['* SORT 5 3 4 1 2']
        typ, data = client.uid('sort', '(SUBJECT REVERSE DATE)', 'UTF-8', 'ALL')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [br'5 3 4 1 2'])
        self.assertEqual(server.args, ['SORT', '(SUBJECT REVERSE DATE)', 'UTF-8', 'ALL'])

        response[:] = ['* SORT']
        typ, data = client.uid('sort', '(SUBJECT)', 'US-ASCII', 'TEXT', '"not in mailbox"')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [br''])
        self.assertEqual(server.args, ['SORT', '(SUBJECT)', 'US-ASCII', 'TEXT', '"not in mailbox"'])

        typ, data = client.uid('sort', 'SUBJECT', 'NF_Z_62-010_(1973)', b'TEXT', b'"not in mailbox"')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['SORT', '(SUBJECT)', '"NF_Z_62-010_(1973)"', 'TEXT', '"not in mailbox"'])

        # A non-ASCII str criterion is encoded to the declared charset.
        response[:] = ['* SORT']
        typ, data = client.uid('sort', '(SUBJECT)', 'KOI8-U', 'TEXT', '"Київ"')
        self.assertEqual(typ, 'OK')
        self.assertIn('"Київ"'.encode('koi8-u'), server.line)

        # The uid=True keyword is a shorthand for uid('SORT', ...).
        response[:] = ['* SORT 2 84 882']
        typ, data = client.sort('(SUBJECT)', 'UTF-8', 'SINCE', '1-Feb-1994', uid=True)
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [br'2 84 882'])
        self.assertEqual(server.args, ['SORT', '(SUBJECT)', 'UTF-8', 'SINCE', '1-Feb-1994'])

        # sort_criteria may be a sequence, and 'params' substitutes and
        # quotes '?' (a value with a space becomes a quoted string).
        response[:] = ['* SORT 1']
        typ, data = client.uid('sort', ['REVERSE', 'DATE'], 'UTF-8', 'SUBJECT ?',
                               params=['trip report'])
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args,
                         ['SORT', '(REVERSE DATE)', 'UTF-8', 'SUBJECT', '"trip report"'])

    def test_thread(self):
        response = []
        client, server = self._setup(make_simple_handler('THREAD', response))
        client.login('user', 'pass')
        client.select()
        response[:] = [
            '* THREAD (166)(167)(168)(169)(172)(170)(171)'
            '(173)(174 (175)(176)(178)(181)(180))(179)(177 '
            '(183)(182)(188)(184)(185)(186)(187)(189))(190)'
            '(191)(192)(193)(194 195)(196 (197)(198))(199)'
            '(200 202)(201)(203)(204)(205)(206 207)(208)']
        typ, data = client.thread('ORDEREDSUBJECT', 'UTF-8', 'SINCE', '5-MAR-2000')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [
            b'(166)(167)(168)(169)(172)(170)(171)'
            b'(173)(174 (175)(176)(178)(181)(180))(179)(177 '
            b'(183)(182)(188)(184)(185)(186)(187)(189))(190)'
            b'(191)(192)(193)(194 195)(196 (197)(198))(199)'
            b'(200 202)(201)(203)(204)(205)(206 207)(208)'])
        self.assertEqual(server.args, ['ORDEREDSUBJECT', 'UTF-8', 'SINCE', '5-MAR-2000'])

        response[:] = [
            '* THREAD (166)(167)(168)(169)(172)((170)(179))'
            '(171)(173)((174)(175)(176)(178)(181)(180))'
            '((177)(183)(182)(188 (184)(189))(185 186)(187))'
            '(190)(191)(192)(193)((194)(195 196))(197 198)'
            '(199)(200 202)(201)(203)(204)(205 206 207)(208)']
        typ, data = client.thread('ORDEREDSUBJECT', 'US-ASCII', 'TEXT', '"gewp"')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [
            b'(166)(167)(168)(169)(172)((170)(179))'
            b'(171)(173)((174)(175)(176)(178)(181)(180))'
            b'((177)(183)(182)(188 (184)(189))(185 186)(187))'
            b'(190)(191)(192)(193)((194)(195 196))(197 198)'
            b'(199)(200 202)(201)(203)(204)(205 206 207)(208)'])
        self.assertEqual(server.args, ['ORDEREDSUBJECT', 'US-ASCII', 'TEXT', '"gewp"'])

        typ, data = client.thread('ORDEREDSUBJECT', 'NF_Z_62-010_(1973)', b'TEXT', b'"gewp"')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['ORDEREDSUBJECT', '"NF_Z_62-010_(1973)"', 'TEXT', '"gewp"'])

        # A non-ASCII str criterion is encoded to the declared charset.
        response[:] = ['* THREAD (1)']
        typ, data = client.thread('ORDEREDSUBJECT', 'KOI8-U', 'TEXT', '"Київ"')
        self.assertEqual(typ, 'OK')
        self.assertIn('"Київ"'.encode('koi8-u'), server.line)

        # 'params' substitutes and quotes '?' (a value with a space becomes
        # a quoted string).
        response[:] = ['* THREAD (1)']
        typ, data = client.thread('REFERENCES', 'UTF-8', 'SUBJECT ?',
                                  params=['trip report'])
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args,
                         ['REFERENCES', 'UTF-8', 'SUBJECT', '"trip report"'])

    def test_uid_thread(self):
        response = []
        client, server = self._setup(make_simple_handler('UID', response,
            'UID THREAD completed'))
        client.login('user', 'pass')
        client.select()
        response[:] = [
            '* THREAD (166)(167)(168)(169)(172)(170)(171)'
            '(173)(174 (175)(176)(178)(181)(180))(179)(177 '
            '(183)(182)(188)(184)(185)(186)(187)(189))(190)'
            '(191)(192)(193)(194 195)(196 (197)(198))(199)'
            '(200 202)(201)(203)(204)(205)(206 207)(208)']
        typ, data = client.uid('THREAD', 'ORDEREDSUBJECT', 'UTF-8', 'SINCE', '5-MAR-2000')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [
            b'(166)(167)(168)(169)(172)(170)(171)'
            b'(173)(174 (175)(176)(178)(181)(180))(179)(177 '
            b'(183)(182)(188)(184)(185)(186)(187)(189))(190)'
            b'(191)(192)(193)(194 195)(196 (197)(198))(199)'
            b'(200 202)(201)(203)(204)(205)(206 207)(208)'])
        self.assertEqual(server.args, ['THREAD', 'ORDEREDSUBJECT', 'UTF-8', 'SINCE', '5-MAR-2000'])

        response[:] = [
            '* THREAD (166)(167)(168)(169)(172)((170)(179))'
            '(171)(173)((174)(175)(176)(178)(181)(180))'
            '((177)(183)(182)(188 (184)(189))(185 186)(187))'
            '(190)(191)(192)(193)((194)(195 196))(197 198)'
            '(199)(200 202)(201)(203)(204)(205 206 207)(208)']
        typ, data = client.uid('THREAD', 'ORDEREDSUBJECT', 'US-ASCII', 'TEXT', '"gewp"')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [
            b'(166)(167)(168)(169)(172)((170)(179))'
            b'(171)(173)((174)(175)(176)(178)(181)(180))'
            b'((177)(183)(182)(188 (184)(189))(185 186)(187))'
            b'(190)(191)(192)(193)((194)(195 196))(197 198)'
            b'(199)(200 202)(201)(203)(204)(205 206 207)(208)'])
        self.assertEqual(server.args, ['THREAD', 'ORDEREDSUBJECT', 'US-ASCII', 'TEXT', '"gewp"'])

        typ, data = client.uid('THREAD', 'ORDEREDSUBJECT', 'NF_Z_62-010_(1973)', b'TEXT', b'"gewp"')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['THREAD', 'ORDEREDSUBJECT', '"NF_Z_62-010_(1973)"', 'TEXT', '"gewp"'])

        # A non-ASCII str criterion is encoded to the declared charset.
        response[:] = ['* THREAD (1)']
        typ, data = client.uid('THREAD', 'ORDEREDSUBJECT', 'KOI8-U', 'TEXT', '"Київ"')
        self.assertEqual(typ, 'OK')
        self.assertIn('"Київ"'.encode('koi8-u'), server.line)

        # The uid=True keyword is a shorthand for uid('THREAD', ...).
        response[:] = ['* THREAD (166)(167)(168)']
        typ, data = client.thread('ORDEREDSUBJECT', 'UTF-8', 'SINCE', '5-MAR-2000',
                                  uid=True)
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'(166)(167)(168)'])
        self.assertEqual(server.args, ['THREAD', 'ORDEREDSUBJECT', 'UTF-8', 'SINCE', '5-MAR-2000'])

        # 'params' substitutes and quotes '?' (a value with a space becomes
        # a quoted string).
        response[:] = ['* THREAD (1)']
        typ, data = client.uid('THREAD', 'REFERENCES', 'UTF-8', 'SUBJECT ?',
                               params=['trip report'])
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args,
                         ['THREAD', 'REFERENCES', 'UTF-8', 'SUBJECT', '"trip report"'])

    def test_delete(self):
        client, server = self._setup(make_simple_handler('DELETE'))
        client.login('user', 'pass')
        typ, data = client.delete('blurdybloop')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'DELETE completed'])
        self.assertEqual(server.args, ['blurdybloop'])

        typ, data = client.delete('foo/bar')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['foo/bar'])

        typ, data = client.delete('New folder')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['"New folder"'])

    def test_rename(self):
        client, server = self._setup(make_simple_handler('RENAME'))
        client.login('user', 'pass')
        typ, data = client.rename('blurdybloop', 'sarasoop')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'RENAME completed'])
        self.assertEqual(server.args, ['blurdybloop', 'sarasoop'])

        typ, data = client.rename('Old folder', 'New folder')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['"Old folder"', '"New folder"'])

    def test_subscribe(self):
        client, server = self._setup(make_simple_handler('SUBSCRIBE'))
        client.login('user', 'pass')
        typ, data = client.subscribe('#news.comp.mail.mime')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'SUBSCRIBE completed'])
        self.assertEqual(server.args, ['#news.comp.mail.mime'])

        typ, data = client.subscribe('New folder')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['"New folder"'])

    def test_unsubscribe(self):
        client, server = self._setup(make_simple_handler('UNSUBSCRIBE'))
        client.login('user', 'pass')
        typ, data = client.unsubscribe('#news.comp.mail.mime')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'UNSUBSCRIBE completed'])
        self.assertEqual(server.args, ['#news.comp.mail.mime'])

        typ, data = client.unsubscribe('New folder')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['"New folder"'])

    def test_list(self):
        client, server = self._setup(make_simple_handler('LIST',
            [r'* LIST (\Noselect) "/" ""',
             r'* LIST (\Unmarked) "/" "~/Mail/foo"']))
        client.login('user', 'pass')
        typ, data = client.list()
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [br'(\Noselect) "/" ""',
                                br'(\Unmarked) "/" "~/Mail/foo"'])
        self.assertEqual(server.args, ['""', '*'])

        typ, data = client.list('~/Mail/', '%')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['~/Mail/', '%'])

        typ, data = client.list('New folder', '*')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['"New folder"', '*'])

        # A pattern without wildcards is quoted when necessary.
        typ, data = client.list('~/Mail/', 'My Folder')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['~/Mail/', '"My Folder"'])

    def test_status(self):
        client, server = self._setup(make_simple_handler('STATUS',
            ['* STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292)']))
        client.login('user', 'pass')
        typ, data = client.status('blurdybloop', '(UIDNEXT MESSAGES)')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'blurdybloop (MESSAGES 231 UIDNEXT 44292)'])
        self.assertEqual(server.args, ['blurdybloop', '(UIDNEXT MESSAGES)'])

        # The names argument is wrapped in parentheses if it is not already.
        typ, data = client.status('New folder', 'UIDNEXT MESSAGES')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['"New folder"', '(UIDNEXT MESSAGES)'])

        # The names argument may be a sequence of item names.
        typ, data = client.status('blurdybloop', ['UIDNEXT', 'MESSAGES'])
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['blurdybloop', '(UIDNEXT MESSAGES)'])

    def test_getacl(self):
        client, server = self._setup(make_simple_handler('GETACL',
            ['* ACL INBOX Fred rwipslxetad']))
        client.login('user', 'pass')
        typ, data = client.getacl('INBOX')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'INBOX Fred rwipslxetad'])
        self.assertEqual(server.args, ['INBOX'])

        typ, data = client.getacl('New folder')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['"New folder"'])

    def test_setacl(self):
        client, server = self._setup(make_simple_handler('SETACL'))
        client.login('user', 'pass')
        typ, data = client.setacl('INBOX', 'Fred', 'rwipslxetad')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'SETACL completed'])
        self.assertEqual(server.args, ['INBOX', 'Fred', 'rwipslxetad'])

        typ, data = client.setacl('New folder', 'Fred', '+lr')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['"New folder"', 'Fred', '+lr'])

        # The identifier and the rights are quoted when necessary too.
        typ, data = client.setacl('INBOX', 'John Doe', 'a b')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['INBOX', '"John Doe"', '"a b"'])

    def test_deleteacl(self):
        client, server = self._setup(make_simple_handler('DELETEACL'))
        client.login('user', 'pass')
        typ, data = client.deleteacl('INBOX', 'Fred')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'DELETEACL completed'])
        self.assertEqual(server.args, ['INBOX', 'Fred'])

        # The identifier is quoted when necessary too.
        typ, data = client.deleteacl('New folder', 'John Doe')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['"New folder"', '"John Doe"'])

    def test_myrights(self):
        client, server = self._setup(make_simple_handler('MYRIGHTS',
            ['* MYRIGHTS INBOX rwiptsldaex']))
        client.login('user', 'pass')
        typ, data = client.myrights('INBOX')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'INBOX rwiptsldaex'])
        self.assertEqual(server.args, ['INBOX'])

        typ, data = client.myrights('New folder')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['"New folder"'])

    def test_getquota(self):
        client, server = self._setup(make_simple_handler('GETQUOTA',
            ['* QUOTA "" (STORAGE 10 512)']))
        client.login('user', 'pass')
        typ, data = client.getquota('#news')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'"" (STORAGE 10 512)'])
        self.assertEqual(server.args, ['#news'])

        typ, data = client.getquota('')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['""'])

    def test_getquotaroot(self):
        client, server = self._setup(make_simple_handler('GETQUOTAROOT',
            ['* QUOTAROOT INBOX ""', '* QUOTA "" (STORAGE 10 512)']))
        client.login('user', 'pass')
        typ, data = client.getquotaroot('INBOX')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [[b'INBOX ""'], [b'"" (STORAGE 10 512)']])
        self.assertEqual(server.args, ['INBOX'])

        typ, data = client.getquotaroot('New folder')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['"New folder"'])

    def test_id(self):
        client, server = self._setup(make_simple_handler('ID',
            ['* ID ("name" "Cyrus" "version" "1.5")']))
        typ, data = client.id({'name': 'imaplib', 'version': '3.16'})
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'("name" "Cyrus" "version" "1.5")'])
        self.assertEqual(server.args, ['("name" "imaplib" "version" "3.16")'])

        typ, data = client.id()
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['NIL'])

        # Fields and values are quoted strings; a None value is sent
        # as NIL.
        typ, data = client.id({'name': 'my "client"', 'os': None})
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, [r'("name" "my \"client\"" "os" NIL)'])

    def test_setquota(self):
        client, server = self._setup(make_simple_handler('SETQUOTA',
            ['* QUOTA "" (STORAGE 512)']))
        client.login('user', 'pass')
        typ, data = client.setquota('#news', '(STORAGE 512)')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'"" (STORAGE 512)'])
        self.assertEqual(server.args, ['#news', '(STORAGE 512)'])

        typ, data = client.setquota('', '(STORAGE 512)')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['""', '(STORAGE 512)'])

        # The limits argument is wrapped in parentheses if it is not already.
        typ, data = client.setquota('', 'STORAGE 512')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['""', '(STORAGE 512)'])

    def test_getannotation(self):
        client, server = self._setup(make_simple_handler('GETANNOTATION',
            ['* ANNOTATION INBOX "/comment" ("value.shared" "Hello")']))
        client.login('user', 'pass')
        typ, data = client.getannotation('INBOX', '/comment', 'value.shared')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'INBOX "/comment" ("value.shared" "Hello")'])
        self.assertEqual(server.args, ['INBOX', '/comment', 'value.shared'])

        typ, data = client.getannotation('New folder', '/comment', 'value.shared')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['"New folder"', '/comment', 'value.shared'])

    def test_setannotation(self):
        client, server = self._setup(make_simple_handler('SETANNOTATION'))
        client.login('user', 'pass')
        typ, data = client.setannotation('INBOX', '/comment',
                                         '("value.shared" "My comment")')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args,
                         ['INBOX', '/comment', '("value.shared" "My comment")'])

        typ, data = client.setannotation('New folder', '/comment',
                                         '("value.shared" "My comment")')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args,
            ['"New folder"', '/comment', '("value.shared" "My comment")'])

    def test_proxyauth(self):
        client, server = self._setup(make_simple_handler('PROXYAUTH'))
        client.login('user', 'pass')
        typ, data = client.proxyauth('user')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'PROXYAUTH completed'])
        self.assertEqual(server.args, ['user'])

        typ, data = client.proxyauth('us er')
        self.assertEqual(typ, 'OK')
        self.assertEqual(server.args, ['"us er"'])

    def test_xatom(self):
        client, server = self._setup(make_simple_handler('MYCOMMAND',
            completed='MYCOMMAND completed'))
        client.login('user', 'pass')
        self.addCleanup(imaplib.Commands.pop, 'MYCOMMAND', None)
        typ, data = client.xatom('MYCOMMAND', 'arg1', 'arg2')
        self.assertEqual(typ, 'OK')
        self.assertEqual(data, [b'MYCOMMAND completed'])
        self.assertEqual(server.args, ['arg1', 'arg2'])

    def test_uppercase_command_names(self):
        client, server = self._setup(SimpleIMAPHandler)
        client.login('user', 'pass')
        self.assertEqual(client.CAPABILITY, client.capability)
        self.assertEqual(client.SELECT, client.select)
        typ, data = client.CAPABILITY()
        self.assertEqual(typ, 'OK')
        with self.assertRaises(AttributeError):
            client.NONEXISTENT

    def test_control_characters(self):
        client, server = self._setup(SimpleIMAPHandler)
        client.login('user', 'pass')
        # In the default (ASCII) mode a mailbox name is sent as modified UTF-7,
        # which Base64-encodes every non-printable character (NUL, CR and LF
        # included).  So a control character is never sent as a raw byte -- it
        # cannot corrupt the command -- and the name round-trips.
        for c in support.control_characters_c0():
            with self.subTest(c=c):
                typ, _ = client.select(f'a{c}b')
                self.assertEqual(typ, 'OK')
                [name] = server.is_selected
                self.assertNotIn(c, name)
                self.assertEqual(name.encode('ascii').decode('utf-7-imap'),
                                 f'a{c}b')

    def test_mutf7_mailbox_name(self):
        # Without UTF8=ACCEPT a non-ASCII mailbox name is sent as modified
        # UTF-7 (RFC 3501, 5.1.3).  A name that is already modified UTF-7 (e.g.
        # obtained from a raw LIST response) is sent unchanged, whether as str
        # or bytes, so it round-trips without being encoded again.
        client, server = self._setup(SimpleIMAPHandler)
        client.login('user', 'pass')
        for name in ['Entwürfe', 'Entw&APw-rfe', b'Entw&APw-rfe']:
            with self.subTest(name=name):
                server.is_selected = None
                typ, _ = client.select(name)
                self.assertEqual(typ, 'OK')
                self.assertEqual(server.is_selected, ['Entw&APw-rfe'])

    # property tests

    def test_file_property_getter(self):
        client, _ = self._setup(SimpleIMAPHandler)
        with self.assertWarns(DeprecationWarning):
            self.assertIsInstance(client.file.raw, socket.SocketIO)

    def test_file_property_setter(self):
        client, _ = self._setup(SimpleIMAPHandler)
        with self.assertWarns(DeprecationWarning):
            # ensure that the caller closes the existing file
            client.file.close()
        for new_file in [mock.Mock(), None]:
            with self.assertWarns(DeprecationWarning):
                client.file = new_file
            with self.assertWarns(DeprecationWarning):
                self.assertIs(client.file, new_file)

    def test_file_property_setter_should_not_close_previous_file(self):
        client, _ = self._setup(SimpleIMAPHandler)
        with mock.patch.object(client, "_imaplib_file", mock.Mock()) as f:
            f.close.assert_not_called()
            with self.assertWarns(DeprecationWarning):
                self.assertIs(client.file, f)
            with self.assertWarns(DeprecationWarning):
                client.file = None
            with self.assertWarns(DeprecationWarning):
                self.assertIsNone(client.file)
            f.close.assert_not_called()


class NewIMAPTests(NewIMAPTestsMixin, unittest.TestCase):
    imap_class = imaplib.IMAP4
    server_class = socketserver.TCPServer


@unittest.skipUnless(ssl, "SSL not available")
class NewIMAPSSLTests(NewIMAPTestsMixin, unittest.TestCase):
    imap_class = IMAP4_SSL
    server_class = SecureTCPServer

    def test_ssl_raises(self):
        ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
        self.assertEqual(ssl_context.verify_mode, ssl.CERT_REQUIRED)
        self.assertEqual(ssl_context.check_hostname, True)
        ssl_context.load_verify_locations(CAFILE)

        # Allow for flexible libssl error messages.
        regex = re.compile(r"""(
            IP address mismatch, certificate is not valid for '127.0.0.1'   # OpenSSL
            |
            CERTIFICATE_VERIFY_FAILED                                       # AWS-LC
        )""", re.X)
        with self.assertRaisesRegex(ssl.CertificateError, regex):
            _, server = self._setup(SimpleIMAPHandler, connect=False)
            client = self.imap_class(*server.server_address,
                                     ssl_context=ssl_context)
            client.shutdown()

    def test_ssl_verified(self):
        ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
        ssl_context.load_verify_locations(CAFILE)

        _, server = self._setup(SimpleIMAPHandler, connect=False)
        client = self.imap_class("localhost", server.server_address[1],
                                 ssl_context=ssl_context)
        client.shutdown()

class ThreadedNetworkedTests(unittest.TestCase):
    server_class = socketserver.TCPServer
    imap_class = imaplib.IMAP4

    def make_server(self, addr, hdlr):

        class MyServer(self.server_class):
            def handle_error(self, request, client_address):
                self.close_request(request)
                self.server_close()
                raise

        if verbose:
            print("creating server")
        server = MyServer(addr, hdlr)
        self.assertEqual(server.server_address, server.socket.getsockname())

        if verbose:
            print("server created")
            print("ADDR =", addr)
            print("CLASS =", self.server_class)
            print("HDLR =", server.RequestHandlerClass)

        t = threading.Thread(
            name='%s serving' % self.server_class,
            target=server.serve_forever,
            # Short poll interval to make the test finish quickly.
            # Time between requests is short enough that we won't wake
            # up spuriously too many times.
            kwargs={'poll_interval': 0.01})
        t.daemon = True  # In case this function raises.
        t.start()
        if verbose:
            print("server running")
        return server, t

    def reap_server(self, server, thread):
        if verbose:
            print("waiting for server")
        server.shutdown()
        server.server_close()
        thread.join()
        if verbose:
            print("done")

    @contextmanager
    def reaped_server(self, hdlr):
        server, thread = self.make_server((socket_helper.HOST, 0), hdlr)
        try:
            yield server
        finally:
            self.reap_server(server, thread)

    @contextmanager
    def reaped_pair(self, hdlr):
        with self.reaped_server(hdlr) as server:
            client = self.imap_class(*server.server_address)
            try:
                yield server, client
            finally:
                client.logout()

    @threading_helper.reap_threads
    def test_connect(self):
        with self.reaped_server(SimpleIMAPHandler) as server:
            client = self.imap_class(*server.server_address)
            client.shutdown()

    @threading_helper.reap_threads
    def test_bracket_flags(self):

        # This violates RFC 3501, which disallows ']' characters in flags,
        # but imaplib has allowed producing such flags forever, other programs
        # also produce them (eg: OtherInbox's Organizer app as of 20140716),
        # and Gmail, for example, accepts them and produces them.  So we
        # support them.  See issue #21815.

        class BracketFlagHandler(SimpleIMAPHandler):

            def handle(self):
                self.flags = ['Answered', 'Flagged', 'Deleted', 'Seen', 'Draft']
                super().handle()

            def cmd_AUTHENTICATE(self, tag, args):
                self._send_textline('+')
                self.server.response = yield
                self._send_tagged(tag, 'OK', 'FAKEAUTH successful')

            def cmd_SELECT(self, tag, args):
                flag_msg = ' \\'.join(self.flags)
                self._send_line(('* FLAGS (%s)' % flag_msg).encode('ascii'))
                self._send_line(b'* 2 EXISTS')
                self._send_line(b'* 0 RECENT')
                msg = ('* OK [PERMANENTFLAGS %s \\*)] Flags permitted.'
                        % flag_msg)
                self._send_line(msg.encode('ascii'))
                self._send_tagged(tag, 'OK', '[READ-WRITE] SELECT completed.')

            def cmd_STORE(self, tag, args):
                new_flags = args[2].strip('(').strip(')').split()
                self.flags.extend(new_flags)
                flags_msg = '(FLAGS (%s))' % ' \\'.join(self.flags)
                msg = '* %s FETCH %s' % (args[0], flags_msg)
                self._send_line(msg.encode('ascii'))
                self._send_tagged(tag, 'OK', 'STORE completed.')

        with self.reaped_pair(BracketFlagHandler) as (server, client):
            code, data = client.authenticate('MYAUTH', lambda x: b'fake')
            self.assertEqual(code, 'OK')
            self.assertEqual(server.response, b'ZmFrZQ==\r\n')
            client.select('test')
            typ, [data] = client.store(b'1', "+FLAGS", "[test]")
            self.assertIn(b'[test]', data)
            client.select('test')
            typ, [data] = client.response('PERMANENTFLAGS')
            self.assertIn(b'[test]', data)

    @threading_helper.reap_threads
    def test_issue5949(self):

        class EOFHandler(socketserver.StreamRequestHandler):
            def handle(self):
                # EOF without sending a complete welcome message.
                self.wfile.write(b'* OK')

        with self.reaped_server(EOFHandler) as server:
            self.assertRaises(imaplib.IMAP4.abort,
                              self.imap_class, *server.server_address)

    @threading_helper.reap_threads
    def test_line_termination(self):

        class BadNewlineHandler(SimpleIMAPHandler):

            def cmd_CAPABILITY(self, tag, args):
                self._send(b'* CAPABILITY IMAP4rev1 AUTH\n')
                self._send_tagged(tag, 'OK', 'CAPABILITY completed')

        with self.reaped_server(BadNewlineHandler) as server:
            self.assertRaises(imaplib.IMAP4.abort,
                              self.imap_class, *server.server_address)

    class UTF8Server(SimpleIMAPHandler):
        capabilities = 'AUTH ENABLE UTF8=ACCEPT'

        def cmd_ENABLE(self, tag, args):
            self._send_tagged(tag, 'OK', 'ENABLE successful')

        def cmd_AUTHENTICATE(self, tag, args):
            self._send_textline('+')
            self.server.response = yield
            self._send_tagged(tag, 'OK', 'FAKEAUTH successful')

    @threading_helper.reap_threads
    def test_enable_raises_error_if_not_AUTH(self):
        with self.reaped_pair(self.UTF8Server) as (server, client):
            self.assertFalse(client.utf8_enabled)
            self.assertRaises(imaplib.IMAP4.error, client.enable, 'foo')
            self.assertFalse(client.utf8_enabled)

    # XXX Also need a test that enable after SELECT raises an error.

    @threading_helper.reap_threads
    def test_enable_raises_error_if_no_capability(self):
        class NoEnableServer(self.UTF8Server):
            capabilities = 'AUTH'
        with self.reaped_pair(NoEnableServer) as (server, client):
            self.assertRaises(imaplib.IMAP4.error, client.enable, 'foo')

    @threading_helper.reap_threads
    def test_enable_UTF8_raises_error_if_not_supported(self):
        class NonUTF8Server(SimpleIMAPHandler):
            pass
        with self.assertRaises(imaplib.IMAP4.error):
            with self.reaped_pair(NonUTF8Server) as (server, client):
                typ, data = client.login('user', 'pass')
                self.assertEqual(typ, 'OK')
                client.enable('UTF8=ACCEPT')

    @threading_helper.reap_threads
    def test_enable_UTF8_True_append(self):

        class UTF8AppendServer(self.UTF8Server):
            def cmd_APPEND(self, tag, args):
                self.server.response = args
                self.server.response.append(_read_literal(self, args[-1]))
                literal = yield
                self.server.response.append(literal)
                self._send_tagged(tag, 'OK', 'okay')

        with self.reaped_pair(UTF8AppendServer) as (server, client):
            self.assertEqual(client._encoding, 'ascii')
            code, _ = client.authenticate('MYAUTH', lambda x: b'fake')
            self.assertEqual(code, 'OK')
            self.assertEqual(server.response,
                             b'ZmFrZQ==\r\n')  # b64 encoded 'fake'
            code, _ = client.enable('UTF8=ACCEPT')
            self.assertEqual(code, 'OK')
            self.assertEqual(client._encoding, 'utf-8')
            self.assertEqual(server.args, ['UTF8=ACCEPT'])
            msg_string = 'Subject: üñí©öðé'
            typ, data = client.append(
                None, None, None, (msg_string + '\n').encode('utf-8'))
            self.assertEqual(typ, 'OK')
            self.assertEqual(server.response,
                ['INBOX', 'UTF8',
                 '(~{25}', ('%s\r\n' % msg_string).encode('utf-8'),
                 b')\r\n' ])

    # XXX also need a test that makes sure that the Literal and Untagged_status
    # regexes uses unicode in UTF8 mode instead of the default ASCII.

    @threading_helper.reap_threads
    def test_search_disallows_charset_in_utf8_mode(self):
        with self.reaped_pair(self.UTF8Server) as (server, client):
            typ, _ = client.authenticate('MYAUTH', lambda x: b'fake')
            self.assertEqual(typ, 'OK')
            typ, _ = client.enable('UTF8=ACCEPT')
            self.assertEqual(typ, 'OK')
            self.assertTrue(client.utf8_enabled)
            self.assertRaises(imaplib.IMAP4.error, client.search, 'foo', 'bar')

    @threading_helper.reap_threads
    def test_bad_auth_name(self):

        class MyServer(SimpleIMAPHandler):

            def cmd_AUTHENTICATE(self, tag, args):
                self._send_tagged(tag, 'NO', 'unrecognized authentication '
                                  'type {}'.format(args[0]))

        with self.reaped_pair(MyServer) as (server, client):
            with self.assertRaises(imaplib.IMAP4.error):
                client.authenticate('METHOD', lambda: 1)

    @threading_helper.reap_threads
    def test_invalid_authentication(self):

        class MyServer(SimpleIMAPHandler):

            def cmd_AUTHENTICATE(self, tag, args):
                self._send_textline('+')
                self.response = yield
                self._send_tagged(tag, 'NO', '[AUTHENTICATIONFAILED] invalid')

        with self.reaped_pair(MyServer) as (server, client):
            with self.assertRaises(imaplib.IMAP4.error):
                code, data = client.authenticate('MYAUTH', lambda x: b'fake')

    @threading_helper.reap_threads
    def test_valid_authentication(self):

        class MyServer(SimpleIMAPHandler):

            def cmd_AUTHENTICATE(self, tag, args):
                self._send_textline('+')
                self.server.response = yield
                self._send_tagged(tag, 'OK', 'FAKEAUTH successful')

        with self.reaped_pair(MyServer) as (server, client):
            code, data = client.authenticate('MYAUTH', lambda x: b'fake')
            self.assertEqual(code, 'OK')
            self.assertEqual(server.response,
                             b'ZmFrZQ==\r\n')  # b64 encoded 'fake'

        with self.reaped_pair(MyServer) as (server, client):
            code, data = client.authenticate('MYAUTH', lambda x: 'fake')
            self.assertEqual(code, 'OK')
            self.assertEqual(server.response,
                             b'ZmFrZQ==\r\n')  # b64 encoded 'fake'

    @threading_helper.reap_threads
    @hashlib_helper.requires_hashdigest('md5', openssl=True)
    def test_login_cram_md5(self):

        class AuthHandler(SimpleIMAPHandler):

            capabilities = 'LOGINDISABLED AUTH=CRAM-MD5'

            def cmd_AUTHENTICATE(self, tag, args):
                self._send_textline('+ PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2Uucm'
                                    'VzdG9uLm1jaS5uZXQ=')
                r = yield
                if (r == b'dGltIGYxY2E2YmU0NjRiOWVmYT'
                         b'FjY2E2ZmZkNmNmMmQ5ZjMy\r\n'):
                    self._send_tagged(tag, 'OK', 'CRAM-MD5 successful')
                else:
                    self._send_tagged(tag, 'NO', 'No access')

        with self.reaped_pair(AuthHandler) as (server, client):
            self.assertTrue('AUTH=CRAM-MD5' in client.capabilities)
            ret, data = client.login_cram_md5("tim", "tanstaaftanstaaf")
            self.assertEqual(ret, "OK")

        with self.reaped_pair(AuthHandler) as (server, client):
            self.assertTrue('AUTH=CRAM-MD5' in client.capabilities)
            ret, data = client.login_cram_md5("tim", b"tanstaaftanstaaf")
            self.assertEqual(ret, "OK")


    @threading_helper.reap_threads
    def test_aborted_authentication(self):

        class MyServer(SimpleIMAPHandler):

            def cmd_AUTHENTICATE(self, tag, args):
                self._send_textline('+')
                self.response = yield

                if self.response == b'*\r\n':
                    self._send_tagged(tag, 'NO', '[AUTHENTICATIONFAILED] aborted')
                else:
                    self._send_tagged(tag, 'OK', 'MYAUTH successful')

        with self.reaped_pair(MyServer) as (server, client):
            with self.assertRaises(imaplib.IMAP4.error):
                code, data = client.authenticate('MYAUTH', lambda x: None)


    def test_linetoolong(self):
        class TooLongHandler(SimpleIMAPHandler):
            def handle(self):
                # Send a very long response line
                self.wfile.write(b'* OK ' + imaplib._MAXLINE * b'x' + b'\r\n')

        with self.reaped_server(TooLongHandler) as server:
            self.assertRaises(imaplib.IMAP4.error,
                              self.imap_class, *server.server_address)

    def test_truncated_large_literal(self):
        size = 0
        class BadHandler(SimpleIMAPHandler):
            def handle(self):
                self._send_textline('* OK {%d}' % size)
                self._send_textline('IMAP4rev1')

        for exponent in range(15, 64):
            size = 1 << exponent
            with self.subTest(f"size=2e{size}"):
                with self.reaped_server(BadHandler) as server:
                    with self.assertRaises(imaplib.IMAP4.abort):
                        self.imap_class(*server.server_address)

    @threading_helper.reap_threads
    def test_simple_with_statement(self):
        # simplest call
        with self.reaped_server(SimpleIMAPHandler) as server:
            with self.imap_class(*server.server_address):
                pass

    @threading_helper.reap_threads
    def test_with_statement(self):
        with self.reaped_server(SimpleIMAPHandler) as server:
            with self.imap_class(*server.server_address) as imap:
                imap.login('user', 'pass')
                self.assertEqual(server.logged, ['user', '"pass"'])
            self.assertIsNone(server.logged)

    @threading_helper.reap_threads
    def test_with_statement_logout(self):
        # what happens if already logout in the block?
        with self.reaped_server(SimpleIMAPHandler) as server:
            with self.imap_class(*server.server_address) as imap:
                imap.login('user', 'pass')
                self.assertEqual(server.logged, ['user', '"pass"'])
                imap.logout()
                self.assertIsNone(server.logged)
            self.assertIsNone(server.logged)

    @threading_helper.reap_threads
    @cpython_only
    @unittest.skipUnless(__debug__, "Won't work if __debug__ is False")
    def test_dump_ur(self):
        # See: http://bugs.python.org/issue26543
        untagged_resp_dict = {'READ-WRITE': [b'']}

        with self.reaped_server(SimpleIMAPHandler) as server:
            with self.imap_class(*server.server_address) as imap:
                with mock.patch.object(imap, '_mesg') as mock_mesg:
                    imap._dump_ur(untagged_resp_dict)
                    mock_mesg.assert_called_with(
                        "untagged responses dump:READ-WRITE: [b'']"
                    )


@unittest.skipUnless(ssl, "SSL not available")
class ThreadedNetworkedTestsSSL(ThreadedNetworkedTests):
    server_class = SecureTCPServer
    imap_class = IMAP4_SSL

    @threading_helper.reap_threads
    def test_ssl_verified(self):
        ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
        ssl_context.load_verify_locations(CAFILE)

        # Allow for flexible libssl error messages.
        regex = re.compile(r"""(
            IP address mismatch, certificate is not valid for '127.0.0.1'   # OpenSSL
            |
            CERTIFICATE_VERIFY_FAILED                                       # AWS-LC
        )""", re.X)
        with self.assertRaisesRegex(ssl.CertificateError, regex):
            with self.reaped_server(SimpleIMAPHandler) as server:
                client = self.imap_class(*server.server_address,
                                         ssl_context=ssl_context)
                client.shutdown()

        with self.reaped_server(SimpleIMAPHandler) as server:
            client = self.imap_class("localhost", server.server_address[1],
                                     ssl_context=ssl_context)
            client.shutdown()


class TestModule(unittest.TestCase):
    def test_deprecated__version__(self):
        with self.assertWarnsRegex(
            DeprecationWarning,
            "'__version__' is deprecated and slated for removal in Python 3.20",
        ) as cm:
            getattr(imaplib, "__version__")
        self.assertEqual(cm.filename, __file__)


if __name__ == "__main__":
    unittest.main()
