imaplib.py 48.2 KB
Newer Older
1 2 3 4
"""IMAP4 client.

Based on RFC 2060.

Tim Peters's avatar
Tim Peters committed
5 6 7 8 9 10
Public class:           IMAP4
Public variable:        Debug
Public functions:       Internaldate2tuple
                        Int2AP
                        ParseFlags
                        Time2Internaldate
11
"""
12

13
# Author: Piers Lauder <piers@cs.su.oz.au> December 1997.
Tim Peters's avatar
Tim Peters committed
14
#
15
# Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
16
# String method conversion by ESR, February 2001.
Piers Lauder's avatar
Piers Lauder committed
17
# GET/SETACL contributed by Anthony Baxter <anthony@interlink.com.au> April 2001.
18
# IMAP4_SSL contributed by Tino Lange <Tino.Lange@isg.de> March 2002.
19
# GET/SETQUOTA contributed by Andreas Zeidler <az@kreativkombinat.de> June 2002.
20
# PROXYAUTH contributed by Rick Holbert <holbert.13@osu.edu> November 2002.
21
# GET/SETANNOTATION contributed by Tomas Lindroos <skitta@abo.fi> June 2005.
22

Piers Lauder's avatar
Piers Lauder committed
23
__version__ = "2.58"
24

25
import binascii, errno, random, re, socket, subprocess, sys, time, calendar
26
from datetime import datetime, timezone, timedelta
27
from io import DEFAULT_BUFFER_SIZE
28

29 30 31
try:
    import ssl
    HAVE_SSL = True
32
except ImportError:
33 34
    HAVE_SSL = False

35
__all__ = ["IMAP4", "IMAP4_stream", "Internaldate2tuple",
36
           "Int2AP", "ParseFlags", "Time2Internaldate"]
37

Tim Peters's avatar
Tim Peters committed
38
#       Globals
39

40
CRLF = b'\r\n'
41 42
Debug = 0
IMAP4_PORT = 143
43
IMAP4_SSL_PORT = 993
Tim Peters's avatar
Tim Peters committed
44
AllowedVersions = ('IMAP4REV1', 'IMAP4')        # Most recent first
45

Tim Peters's avatar
Tim Peters committed
46
#       Commands
47 48

Commands = {
Tim Peters's avatar
Tim Peters committed
49 50 51 52 53 54 55 56 57
        # name            valid states
        'APPEND':       ('AUTH', 'SELECTED'),
        'AUTHENTICATE': ('NONAUTH',),
        'CAPABILITY':   ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
        'CHECK':        ('SELECTED',),
        'CLOSE':        ('SELECTED',),
        'COPY':         ('SELECTED',),
        'CREATE':       ('AUTH', 'SELECTED'),
        'DELETE':       ('AUTH', 'SELECTED'),
58
        'DELETEACL':    ('AUTH', 'SELECTED'),
Tim Peters's avatar
Tim Peters committed
59 60 61
        'EXAMINE':      ('AUTH', 'SELECTED'),
        'EXPUNGE':      ('SELECTED',),
        'FETCH':        ('SELECTED',),
Piers Lauder's avatar
Piers Lauder committed
62
        'GETACL':       ('AUTH', 'SELECTED'),
63
        'GETANNOTATION':('AUTH', 'SELECTED'),
64 65
        'GETQUOTA':     ('AUTH', 'SELECTED'),
        'GETQUOTAROOT': ('AUTH', 'SELECTED'),
66
        'MYRIGHTS':     ('AUTH', 'SELECTED'),
Tim Peters's avatar
Tim Peters committed
67 68 69 70
        'LIST':         ('AUTH', 'SELECTED'),
        'LOGIN':        ('NONAUTH',),
        'LOGOUT':       ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
        'LSUB':         ('AUTH', 'SELECTED'),
Piers Lauder's avatar
Piers Lauder committed
71
        'NAMESPACE':    ('AUTH', 'SELECTED'),
Tim Peters's avatar
Tim Peters committed
72
        'NOOP':         ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
73
        'PARTIAL':      ('SELECTED',),                                  # NB: obsolete
74
        'PROXYAUTH':    ('AUTH',),
Tim Peters's avatar
Tim Peters committed
75 76 77
        'RENAME':       ('AUTH', 'SELECTED'),
        'SEARCH':       ('SELECTED',),
        'SELECT':       ('AUTH', 'SELECTED'),
Piers Lauder's avatar
Piers Lauder committed
78
        'SETACL':       ('AUTH', 'SELECTED'),
79
        'SETANNOTATION':('AUTH', 'SELECTED'),
80
        'SETQUOTA':     ('AUTH', 'SELECTED'),
Piers Lauder's avatar
Piers Lauder committed
81
        'SORT':         ('SELECTED',),
82
        'STARTTLS':     ('NONAUTH',),
Tim Peters's avatar
Tim Peters committed
83 84 85
        'STATUS':       ('AUTH', 'SELECTED'),
        'STORE':        ('SELECTED',),
        'SUBSCRIBE':    ('AUTH', 'SELECTED'),
86
        'THREAD':       ('SELECTED',),
Tim Peters's avatar
Tim Peters committed
87 88 89 90 91
        'UID':          ('SELECTED',),
        'UNSUBSCRIBE':  ('AUTH', 'SELECTED'),
        }

#       Patterns to match server responses
92

93 94 95 96 97 98 99 100 101 102 103
Continuation = re.compile(br'\+( (?P<data>.*))?')
Flags = re.compile(br'.*FLAGS \((?P<flags>[^\)]*)\)')
InternalDate = re.compile(br'.*INTERNALDATE "'
        br'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
        br' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
        br' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
        br'"')
Literal = re.compile(br'.*{(?P<size>\d+)}$', re.ASCII)
MapCRLF = re.compile(br'\r\n|\r|\n')
Response_code = re.compile(br'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
Untagged_response = re.compile(br'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
104
Untagged_status = re.compile(
105
    br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?', re.ASCII)
106 107 108 109 110



class IMAP4:

Tim Peters's avatar
Tim Peters committed
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
    """IMAP4 client class.

    Instantiate with: IMAP4([host[, port]])

            host - host's name (default: localhost);
            port - port number (default: standard IMAP4 port).

    All IMAP4rev1 commands are supported by methods of the same
    name (in lower-case).

    All arguments to commands are converted to strings, except for
    AUTHENTICATE, and the last argument to APPEND which is passed as
    an IMAP4 literal.  If necessary (the string contains any
    non-printing characters or white-space and isn't enclosed with
    either parentheses or double quotes) each string is quoted.
    However, the 'password' argument to the LOGIN command is always
    quoted.  If you want to avoid having an argument string quoted
    (eg: the 'flags' argument to STORE) then enclose the string in
    parentheses (eg: "(\Deleted)").
130

Tim Peters's avatar
Tim Peters committed
131 132
    Each command returns a tuple: (type, [data, ...]) where 'type'
    is usually 'OK' or 'NO', and 'data' is either the text from the
133 134 135 136
    tagged response, or untagged results from command. Each 'data'
    is either a string, or a tuple. If a tuple, then the first part
    is the header of the response, and the second part contains
    the data (ie: 'literal' value).
137

Tim Peters's avatar
Tim Peters committed
138 139 140 141 142
    Errors raise the exception class <instance>.error("<reason>").
    IMAP4 server errors raise <instance>.abort("<reason>"),
    which is a sub-class of 'error'. Mailbox status changes
    from READ-WRITE to READ-ONLY raise the exception class
    <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
143

Tim Peters's avatar
Tim Peters committed
144 145 146 147
    "error" exceptions imply a program error.
    "abort" exceptions imply the connection should be reset, and
            the command re-tried.
    "readonly" exceptions imply the command should be re-tried.
Guido van Rossum's avatar
Guido van Rossum committed
148

149 150 151 152
    Note: to use this module, you must read the RFCs pertaining to the
    IMAP4 protocol, as the semantics of the arguments to each IMAP4
    command are left to the invoker, not to mention the results. Also,
    most IMAP servers implement a sub-set of the commands available here.
Tim Peters's avatar
Tim Peters committed
153
    """
154

Tim Peters's avatar
Tim Peters committed
155 156 157
    class error(Exception): pass    # Logical errors - debug required
    class abort(error): pass        # Service errors - close and retry
    class readonly(abort): pass     # Mailbox status changed to READ-ONLY
158

Tim Peters's avatar
Tim Peters committed
159 160 161 162 163 164 165
    def __init__(self, host = '', port = IMAP4_PORT):
        self.debug = Debug
        self.state = 'LOGOUT'
        self.literal = None             # A literal argument to a command
        self.tagged_commands = {}       # Tagged commands awaiting response
        self.untagged_responses = {}    # {typ: [data, ...], ...}
        self.continuation_response = '' # Last continuation response
166
        self.is_readonly = False        # READ-ONLY desired state
Tim Peters's avatar
Tim Peters committed
167
        self.tagnum = 0
168
        self._tls_established = False
169

Tim Peters's avatar
Tim Peters committed
170
        # Open socket to server.
171

Tim Peters's avatar
Tim Peters committed
172
        self.open(host, port)
173

174 175 176 177 178
        try:
            self._connect()
        except Exception:
            try:
                self.shutdown()
179
            except OSError:
180 181 182 183 184
                pass
            raise


    def _connect(self):
Tim Peters's avatar
Tim Peters committed
185 186
        # Create unique tag for this session,
        # and compile tagged response matcher.
187

188
        self.tagpre = Int2AP(random.randint(4096, 65535))
189
        self.tagre = re.compile(br'(?P<tag>'
Tim Peters's avatar
Tim Peters committed
190
                        + self.tagpre
191
                        + br'\d+) (?P<type>[A-Z]+) (?P<data>.*)', re.ASCII)
192

Tim Peters's avatar
Tim Peters committed
193 194
        # Get server welcome message,
        # request and store CAPABILITY response.
195

Tim Peters's avatar
Tim Peters committed
196
        if __debug__:
197 198 199
            self._cmd_log_len = 10
            self._cmd_log_idx = 0
            self._cmd_log = {}           # Last `_cmd_log_len' interactions
Tim Peters's avatar
Tim Peters committed
200
            if self.debug >= 1:
201 202
                self._mesg('imaplib version %s' % __version__)
                self._mesg('new IMAP4 connection, tag=%s' % self.tagpre)
203

Tim Peters's avatar
Tim Peters committed
204
        self.welcome = self._get_response()
205
        if 'PREAUTH' in self.untagged_responses:
Tim Peters's avatar
Tim Peters committed
206
            self.state = 'AUTH'
207
        elif 'OK' in self.untagged_responses:
Tim Peters's avatar
Tim Peters committed
208 209 210
            self.state = 'NONAUTH'
        else:
            raise self.error(self.welcome)
211

212
        self._get_capabilities()
Tim Peters's avatar
Tim Peters committed
213 214
        if __debug__:
            if self.debug >= 3:
215
                self._mesg('CAPABILITIES: %r' % (self.capabilities,))
216

Tim Peters's avatar
Tim Peters committed
217 218 219 220 221
        for version in AllowedVersions:
            if not version in self.capabilities:
                continue
            self.PROTOCOL_VERSION = version
            return
222

Tim Peters's avatar
Tim Peters committed
223
        raise self.error('server not IMAP4 compliant')
224

225

Tim Peters's avatar
Tim Peters committed
226 227
    def __getattr__(self, attr):
        #       Allow UPPERCASE variants of IMAP4 command methods.
228
        if attr in Commands:
Piers Lauder's avatar
Piers Lauder committed
229
            return getattr(self, attr.lower())
Tim Peters's avatar
Tim Peters committed
230 231
        raise AttributeError("Unknown IMAP4 command: '%s'" % attr)

232 233


Piers Lauder's avatar
Piers Lauder committed
234
    #       Overridable methods
235 236


237
    def _create_socket(self):
238
        return socket.create_connection((self.host, self.port))
239

240 241 242
    def open(self, host = '', port = IMAP4_PORT):
        """Setup connection to remote server on "host:port"
            (default: localhost:standard IMAP4 port).
Piers Lauder's avatar
Piers Lauder committed
243 244 245
        This connection will be used by the routines:
            read, readline, send, shutdown.
        """
246 247
        self.host = host
        self.port = port
248
        self.sock = self._create_socket()
249
        self.file = self.sock.makefile('rb')
250 251


Piers Lauder's avatar
Piers Lauder committed
252 253
    def read(self, size):
        """Read 'size' bytes from remote."""
254
        return self.file.read(size)
Piers Lauder's avatar
Piers Lauder committed
255 256 257 258 259 260 261 262 263


    def readline(self):
        """Read line from remote."""
        return self.file.readline()


    def send(self, data):
        """Send data to remote."""
264
        self.sock.sendall(data)
Piers Lauder's avatar
Piers Lauder committed
265

266

Piers Lauder's avatar
Piers Lauder committed
267 268 269
    def shutdown(self):
        """Close I/O established in "open"."""
        self.file.close()
270 271
        try:
            self.sock.shutdown(socket.SHUT_RDWR)
272
        except OSError as e:
273 274 275 276 277
            # The server might already have closed the connection
            if e.errno != errno.ENOTCONN:
                raise
        finally:
            self.sock.close()
Piers Lauder's avatar
Piers Lauder committed
278 279 280 281 282 283 284 285 286 287 288 289 290 291


    def socket(self):
        """Return socket instance used to connect to IMAP4 server.

        socket = <instance>.socket()
        """
        return self.sock



    #       Utility methods


Tim Peters's avatar
Tim Peters committed
292 293 294
    def recent(self):
        """Return most recent 'RECENT' responses if any exist,
        else prompt server for an update using the 'NOOP' command.
295

Tim Peters's avatar
Tim Peters committed
296
        (typ, [data]) = <instance>.recent()
297

Tim Peters's avatar
Tim Peters committed
298 299 300 301 302 303 304 305 306
        'data' is None if no new messages,
        else list of RECENT responses, most recent last.
        """
        name = 'RECENT'
        typ, dat = self._untagged_response('OK', [None], name)
        if dat[-1]:
            return typ, dat
        typ, dat = self.noop()  # Prod server for response
        return self._untagged_response(typ, dat, name)
307 308


Tim Peters's avatar
Tim Peters committed
309 310
    def response(self, code):
        """Return data for response 'code' if received, or None.
311

Tim Peters's avatar
Tim Peters committed
312
        Old value for response 'code' is cleared.
313

Tim Peters's avatar
Tim Peters committed
314 315
        (code, [data]) = <instance>.response(code)
        """
316
        return self._untagged_response(code, [None], code.upper())
317 318 319



Tim Peters's avatar
Tim Peters committed
320
    #       IMAP4 commands
321 322


Tim Peters's avatar
Tim Peters committed
323 324
    def append(self, mailbox, flags, date_time, message):
        """Append message to named mailbox.
325

Tim Peters's avatar
Tim Peters committed
326
        (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
Guido van Rossum's avatar
Guido van Rossum committed
327

Tim Peters's avatar
Tim Peters committed
328 329 330 331 332 333 334 335 336 337 338 339 340 341
                All args except `message' can be None.
        """
        name = 'APPEND'
        if not mailbox:
            mailbox = 'INBOX'
        if flags:
            if (flags[0],flags[-1]) != ('(',')'):
                flags = '(%s)' % flags
        else:
            flags = None
        if date_time:
            date_time = Time2Internaldate(date_time)
        else:
            date_time = None
342
        self.literal = MapCRLF.sub(CRLF, message)
Tim Peters's avatar
Tim Peters committed
343
        return self._simple_command(name, mailbox, flags, date_time)
344 345


Tim Peters's avatar
Tim Peters committed
346 347
    def authenticate(self, mechanism, authobject):
        """Authenticate command - requires response processing.
348

Tim Peters's avatar
Tim Peters committed
349 350 351
        'mechanism' specifies which authentication mechanism is to
        be used - it must appear in <instance>.capabilities in the
        form AUTH=<mechanism>.
352

Tim Peters's avatar
Tim Peters committed
353
        'authobject' must be a callable object:
354

Tim Peters's avatar
Tim Peters committed
355
                data = authobject(response)
356

357 358 359 360
        It will be called to process server continuation responses; the
        response argument it is passed will be a bytes.  It should return bytes
        data that will be base64 encoded and sent to the server.  It should
        return None if the client abort response '*' should be sent instead.
Tim Peters's avatar
Tim Peters committed
361
        """
362
        mech = mechanism.upper()
363 364
        # XXX: shouldn't this code be removed, not commented out?
        #cap = 'AUTH=%s' % mech
Tim Peters's avatar
Tim Peters committed
365
        #if not cap in self.capabilities:       # Let the server decide!
366
        #    raise self.error("Server doesn't allow %s authentication." % mech)
Tim Peters's avatar
Tim Peters committed
367 368 369 370 371 372
        self.literal = _Authenticator(authobject).process
        typ, dat = self._simple_command('AUTHENTICATE', mech)
        if typ != 'OK':
            raise self.error(dat[-1])
        self.state = 'AUTH'
        return typ, dat
373 374


375 376 377 378 379 380 381 382 383
    def capability(self):
        """(typ, [data]) = <instance>.capability()
        Fetch capabilities list from server."""

        name = 'CAPABILITY'
        typ, dat = self._simple_command(name)
        return self._untagged_response(typ, dat, name)


Tim Peters's avatar
Tim Peters committed
384 385
    def check(self):
        """Checkpoint mailbox on server.
386

Tim Peters's avatar
Tim Peters committed
387 388 389
        (typ, [data]) = <instance>.check()
        """
        return self._simple_command('CHECK')
390 391


Tim Peters's avatar
Tim Peters committed
392 393
    def close(self):
        """Close currently selected mailbox.
394

Tim Peters's avatar
Tim Peters committed
395 396
        Deleted messages are removed from writable mailbox.
        This is the recommended command before 'LOGOUT'.
397

Tim Peters's avatar
Tim Peters committed
398 399 400 401 402 403 404
        (typ, [data]) = <instance>.close()
        """
        try:
            typ, dat = self._simple_command('CLOSE')
        finally:
            self.state = 'AUTH'
        return typ, dat
405 406


Tim Peters's avatar
Tim Peters committed
407 408
    def copy(self, message_set, new_mailbox):
        """Copy 'message_set' messages onto end of 'new_mailbox'.
409

Tim Peters's avatar
Tim Peters committed
410 411 412
        (typ, [data]) = <instance>.copy(message_set, new_mailbox)
        """
        return self._simple_command('COPY', message_set, new_mailbox)
413 414


Tim Peters's avatar
Tim Peters committed
415 416
    def create(self, mailbox):
        """Create new mailbox.
417

Tim Peters's avatar
Tim Peters committed
418 419 420
        (typ, [data]) = <instance>.create(mailbox)
        """
        return self._simple_command('CREATE', mailbox)
421 422


Tim Peters's avatar
Tim Peters committed
423 424
    def delete(self, mailbox):
        """Delete old mailbox.
425

Tim Peters's avatar
Tim Peters committed
426 427 428
        (typ, [data]) = <instance>.delete(mailbox)
        """
        return self._simple_command('DELETE', mailbox)
429

430 431 432 433 434 435
    def deleteacl(self, mailbox, who):
        """Delete the ACLs (remove any rights) set for who on mailbox.

        (typ, [data]) = <instance>.deleteacl(mailbox, who)
        """
        return self._simple_command('DELETEACL', mailbox, who)
436

Tim Peters's avatar
Tim Peters committed
437 438
    def expunge(self):
        """Permanently remove deleted items from selected mailbox.
439

Tim Peters's avatar
Tim Peters committed
440
        Generates 'EXPUNGE' response for each deleted message.
441

Tim Peters's avatar
Tim Peters committed
442
        (typ, [data]) = <instance>.expunge()
443

Tim Peters's avatar
Tim Peters committed
444 445 446 447 448
        'data' is list of 'EXPUNGE'd message numbers in order received.
        """
        name = 'EXPUNGE'
        typ, dat = self._simple_command(name)
        return self._untagged_response(typ, dat, name)
449 450


Tim Peters's avatar
Tim Peters committed
451 452
    def fetch(self, message_set, message_parts):
        """Fetch (parts of) messages.
453

Tim Peters's avatar
Tim Peters committed
454
        (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
455

Tim Peters's avatar
Tim Peters committed
456 457
        'message_parts' should be a string of selected parts
        enclosed in parentheses, eg: "(UID BODY[TEXT])".
458

Tim Peters's avatar
Tim Peters committed
459 460 461 462 463
        'data' are tuples of message part envelope and data.
        """
        name = 'FETCH'
        typ, dat = self._simple_command(name, message_set, message_parts)
        return self._untagged_response(typ, dat, name)
464 465


Piers Lauder's avatar
Piers Lauder committed
466 467 468 469 470 471 472 473 474
    def getacl(self, mailbox):
        """Get the ACLs for a mailbox.

        (typ, [data]) = <instance>.getacl(mailbox)
        """
        typ, dat = self._simple_command('GETACL', mailbox)
        return self._untagged_response(typ, dat, 'ACL')


475 476 477 478 479 480 481 482
    def getannotation(self, mailbox, entry, attribute):
        """(typ, [data]) = <instance>.getannotation(mailbox, entry, attribute)
        Retrieve ANNOTATIONs."""

        typ, dat = self._simple_command('GETANNOTATION', mailbox, entry, attribute)
        return self._untagged_response(typ, dat, 'ANNOTATION')


483 484 485
    def getquota(self, root):
        """Get the quota root's resource usage and limits.

486
        Part of the IMAP4 QUOTA extension defined in rfc2087.
487 488

        (typ, [data]) = <instance>.getquota(root)
489
        """
490 491 492 493 494 495 496 497
        typ, dat = self._simple_command('GETQUOTA', root)
        return self._untagged_response(typ, dat, 'QUOTA')


    def getquotaroot(self, mailbox):
        """Get the list of quota roots for the named mailbox.

        (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)
498
        """
499
        typ, dat = self._simple_command('GETQUOTAROOT', mailbox)
500 501
        typ, quota = self._untagged_response(typ, dat, 'QUOTA')
        typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT')
502
        return typ, [quotaroot, quota]
503 504


Tim Peters's avatar
Tim Peters committed
505 506
    def list(self, directory='""', pattern='*'):
        """List mailbox names in directory matching pattern.
507

Tim Peters's avatar
Tim Peters committed
508
        (typ, [data]) = <instance>.list(directory='""', pattern='*')
509

Tim Peters's avatar
Tim Peters committed
510 511 512 513 514
        'data' is list of LIST responses.
        """
        name = 'LIST'
        typ, dat = self._simple_command(name, directory, pattern)
        return self._untagged_response(typ, dat, name)
515 516


Tim Peters's avatar
Tim Peters committed
517 518
    def login(self, user, password):
        """Identify client using plaintext password.
519

Tim Peters's avatar
Tim Peters committed
520
        (typ, [data]) = <instance>.login(user, password)
Guido van Rossum's avatar
Guido van Rossum committed
521

Tim Peters's avatar
Tim Peters committed
522 523 524 525 526 527 528
        NB: 'password' will be quoted.
        """
        typ, dat = self._simple_command('LOGIN', user, self._quote(password))
        if typ != 'OK':
            raise self.error(dat[-1])
        self.state = 'AUTH'
        return typ, dat
529 530


531 532 533 534 535 536 537 538 539 540 541 542
    def login_cram_md5(self, user, password):
        """ Force use of CRAM-MD5 authentication.

        (typ, [data]) = <instance>.login_cram_md5(user, password)
        """
        self.user, self.password = user, password
        return self.authenticate('CRAM-MD5', self._CRAM_MD5_AUTH)


    def _CRAM_MD5_AUTH(self, challenge):
        """ Authobject to use with CRAM-MD5 authentication. """
        import hmac
543 544 545
        pwd = (self.password.encode('ASCII') if isinstance(self.password, str)
                                             else self.password)
        return self.user + " " + hmac.HMAC(pwd, challenge).hexdigest()
546 547


Tim Peters's avatar
Tim Peters committed
548 549
    def logout(self):
        """Shutdown connection to server.
550

Tim Peters's avatar
Tim Peters committed
551
        (typ, [data]) = <instance>.logout()
552

Tim Peters's avatar
Tim Peters committed
553 554 555 556 557
        Returns server 'BYE' response.
        """
        self.state = 'LOGOUT'
        try: typ, dat = self._simple_command('LOGOUT')
        except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
Piers Lauder's avatar
Piers Lauder committed
558
        self.shutdown()
559
        if 'BYE' in self.untagged_responses:
Tim Peters's avatar
Tim Peters committed
560 561
            return 'BYE', self.untagged_responses['BYE']
        return typ, dat
562 563


Tim Peters's avatar
Tim Peters committed
564 565
    def lsub(self, directory='""', pattern='*'):
        """List 'subscribed' mailbox names in directory matching pattern.
566

Tim Peters's avatar
Tim Peters committed
567
        (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
568

Tim Peters's avatar
Tim Peters committed
569 570 571 572 573
        'data' are tuples of message part envelope and data.
        """
        name = 'LSUB'
        typ, dat = self._simple_command(name, directory, pattern)
        return self._untagged_response(typ, dat, name)
574

575 576 577 578 579 580 581
    def myrights(self, mailbox):
        """Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).

        (typ, [data]) = <instance>.myrights(mailbox)
        """
        typ,dat = self._simple_command('MYRIGHTS', mailbox)
        return self._untagged_response(typ, dat, 'MYRIGHTS')
582

Piers Lauder's avatar
Piers Lauder committed
583 584 585 586 587 588 589 590 591 592
    def namespace(self):
        """ Returns IMAP namespaces ala rfc2342

        (typ, [data, ...]) = <instance>.namespace()
        """
        name = 'NAMESPACE'
        typ, dat = self._simple_command(name)
        return self._untagged_response(typ, dat, name)


Tim Peters's avatar
Tim Peters committed
593 594
    def noop(self):
        """Send NOOP command.
595

596
        (typ, [data]) = <instance>.noop()
Tim Peters's avatar
Tim Peters committed
597 598 599
        """
        if __debug__:
            if self.debug >= 3:
600
                self._dump_ur(self.untagged_responses)
Tim Peters's avatar
Tim Peters committed
601
        return self._simple_command('NOOP')
602 603


Tim Peters's avatar
Tim Peters committed
604 605
    def partial(self, message_num, message_part, start, length):
        """Fetch truncated part of a message.
606

Tim Peters's avatar
Tim Peters committed
607
        (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
608

Tim Peters's avatar
Tim Peters committed
609 610 611 612 613
        'data' is tuple of message part envelope and data.
        """
        name = 'PARTIAL'
        typ, dat = self._simple_command(name, message_num, message_part, start, length)
        return self._untagged_response(typ, dat, 'FETCH')
614 615


616 617 618 619 620 621 622 623 624 625 626 627 628
    def proxyauth(self, user):
        """Assume authentication as "user".

        Allows an authorised administrator to proxy into any user's
        mailbox.

        (typ, [data]) = <instance>.proxyauth(user)
        """

        name = 'PROXYAUTH'
        return self._simple_command('PROXYAUTH', user)


Tim Peters's avatar
Tim Peters committed
629 630
    def rename(self, oldmailbox, newmailbox):
        """Rename old mailbox name to new.
631

632
        (typ, [data]) = <instance>.rename(oldmailbox, newmailbox)
Tim Peters's avatar
Tim Peters committed
633 634
        """
        return self._simple_command('RENAME', oldmailbox, newmailbox)
635 636


Tim Peters's avatar
Tim Peters committed
637 638
    def search(self, charset, *criteria):
        """Search mailbox for matching messages.
639

640
        (typ, [data]) = <instance>.search(charset, criterion, ...)
641

Tim Peters's avatar
Tim Peters committed
642 643 644 645
        'data' is space separated list of matching message numbers.
        """
        name = 'SEARCH'
        if charset:
646
            typ, dat = self._simple_command(name, 'CHARSET', charset, *criteria)
Piers Lauder's avatar
Piers Lauder committed
647
        else:
648
            typ, dat = self._simple_command(name, *criteria)
Tim Peters's avatar
Tim Peters committed
649
        return self._untagged_response(typ, dat, name)
650 651


652
    def select(self, mailbox='INBOX', readonly=False):
Tim Peters's avatar
Tim Peters committed
653
        """Select a mailbox.
654

Tim Peters's avatar
Tim Peters committed
655
        Flush all untagged responses.
656

657
        (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=False)
658

Tim Peters's avatar
Tim Peters committed
659
        'data' is count of messages in mailbox ('EXISTS' response).
660 661 662

        Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
        other responses should be obtained via <instance>.response('FLAGS') etc.
Tim Peters's avatar
Tim Peters committed
663 664 665
        """
        self.untagged_responses = {}    # Flush old responses.
        self.is_readonly = readonly
666
        if readonly:
Tim Peters's avatar
Tim Peters committed
667 668 669 670 671 672 673 674
            name = 'EXAMINE'
        else:
            name = 'SELECT'
        typ, dat = self._simple_command(name, mailbox)
        if typ != 'OK':
            self.state = 'AUTH'     # Might have been 'SELECTED'
            return typ, dat
        self.state = 'SELECTED'
675
        if 'READ-ONLY' in self.untagged_responses \
Tim Peters's avatar
Tim Peters committed
676 677 678
                and not readonly:
            if __debug__:
                if self.debug >= 1:
679
                    self._dump_ur(self.untagged_responses)
Tim Peters's avatar
Tim Peters committed
680 681
            raise self.readonly('%s is not writable' % mailbox)
        return typ, self.untagged_responses.get('EXISTS', [None])
682 683


Piers Lauder's avatar
Piers Lauder committed
684 685 686
    def setacl(self, mailbox, who, what):
        """Set a mailbox acl.

687
        (typ, [data]) = <instance>.setacl(mailbox, who, what)
Piers Lauder's avatar
Piers Lauder committed
688 689 690 691
        """
        return self._simple_command('SETACL', mailbox, who, what)


692 693 694 695 696 697 698 699
    def setannotation(self, *args):
        """(typ, [data]) = <instance>.setannotation(mailbox[, entry, attribute]+)
        Set ANNOTATIONs."""

        typ, dat = self._simple_command('SETANNOTATION', *args)
        return self._untagged_response(typ, dat, 'ANNOTATION')


700 701 702 703
    def setquota(self, root, limits):
        """Set the quota root's resource limits.

        (typ, [data]) = <instance>.setquota(root, limits)
704
        """
705 706 707 708
        typ, dat = self._simple_command('SETQUOTA', root, limits)
        return self._untagged_response(typ, dat, 'QUOTA')


Piers Lauder's avatar
Piers Lauder committed
709 710 711 712 713 714
    def sort(self, sort_criteria, charset, *search_criteria):
        """IMAP4rev1 extension SORT command.

        (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
        """
        name = 'SORT'
715 716
        #if not name in self.capabilities:      # Let the server decide!
        #       raise self.error('unimplemented extension command: %s' % name)
Piers Lauder's avatar
Piers Lauder committed
717
        if (sort_criteria[0],sort_criteria[-1]) != ('(',')'):
718
            sort_criteria = '(%s)' % sort_criteria
719
        typ, dat = self._simple_command(name, sort_criteria, charset, *search_criteria)
Piers Lauder's avatar
Piers Lauder committed
720 721 722
        return self._untagged_response(typ, dat, name)


723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740
    def starttls(self, ssl_context=None):
        name = 'STARTTLS'
        if not HAVE_SSL:
            raise self.error('SSL support missing')
        if self._tls_established:
            raise self.abort('TLS session already established')
        if name not in self.capabilities:
            raise self.abort('TLS not supported by server')
        # Generate a default SSL context if none was passed.
        if ssl_context is None:
            ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
            # SSLv2 considered harmful.
            ssl_context.options |= ssl.OP_NO_SSLv2
        typ, dat = self._simple_command(name)
        if typ == 'OK':
            self.sock = ssl_context.wrap_socket(self.sock)
            self.file = self.sock.makefile('rb')
            self._tls_established = True
741
            self._get_capabilities()
742 743 744 745 746
        else:
            raise self.error("Couldn't establish TLS session")
        return self._untagged_response(typ, dat, name)


Tim Peters's avatar
Tim Peters committed
747 748
    def status(self, mailbox, names):
        """Request named status conditions for mailbox.
749

Tim Peters's avatar
Tim Peters committed
750 751 752
        (typ, [data]) = <instance>.status(mailbox, names)
        """
        name = 'STATUS'
753
        #if self.PROTOCOL_VERSION == 'IMAP4':   # Let the server decide!
Piers Lauder's avatar
Piers Lauder committed
754
        #    raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
Tim Peters's avatar
Tim Peters committed
755 756
        typ, dat = self._simple_command(name, mailbox, names)
        return self._untagged_response(typ, dat, name)
757 758


Tim Peters's avatar
Tim Peters committed
759 760
    def store(self, message_set, command, flags):
        """Alters flag dispositions for messages in mailbox.
761

Tim Peters's avatar
Tim Peters committed
762 763 764 765 766 767
        (typ, [data]) = <instance>.store(message_set, command, flags)
        """
        if (flags[0],flags[-1]) != ('(',')'):
            flags = '(%s)' % flags  # Avoid quoting the flags
        typ, dat = self._simple_command('STORE', message_set, command, flags)
        return self._untagged_response(typ, dat, 'FETCH')
768 769


Tim Peters's avatar
Tim Peters committed
770 771
    def subscribe(self, mailbox):
        """Subscribe to new mailbox.
772

Tim Peters's avatar
Tim Peters committed
773 774 775
        (typ, [data]) = <instance>.subscribe(mailbox)
        """
        return self._simple_command('SUBSCRIBE', mailbox)
776 777


778 779 780
    def thread(self, threading_algorithm, charset, *search_criteria):
        """IMAPrev1 extension THREAD command.

781
        (type, [data]) = <instance>.thread(threading_algorithm, charset, search_criteria, ...)
782 783 784 785 786 787
        """
        name = 'THREAD'
        typ, dat = self._simple_command(name, threading_algorithm, charset, *search_criteria)
        return self._untagged_response(typ, dat, name)


Tim Peters's avatar
Tim Peters committed
788 789 790
    def uid(self, command, *args):
        """Execute "command arg ..." with messages identified by UID,
                rather than message number.
791

Tim Peters's avatar
Tim Peters committed
792
        (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
793

Tim Peters's avatar
Tim Peters committed
794 795
        Returns response appropriate to 'command'.
        """
796
        command = command.upper()
797
        if not command in Commands:
Tim Peters's avatar
Tim Peters committed
798 799
            raise self.error("Unknown IMAP4 UID command: %s" % command)
        if self.state not in Commands[command]:
800 801 802 803
            raise self.error("command %s illegal in state %s, "
                             "only allowed in states %s" %
                             (command, self.state,
                              ', '.join(Commands[command])))
Tim Peters's avatar
Tim Peters committed
804
        name = 'UID'
805
        typ, dat = self._simple_command(name, command, *args)
806
        if command in ('SEARCH', 'SORT', 'THREAD'):
Piers Lauder's avatar
Piers Lauder committed
807
            name = command
Tim Peters's avatar
Tim Peters committed
808 809 810
        else:
            name = 'FETCH'
        return self._untagged_response(typ, dat, name)
811 812


Tim Peters's avatar
Tim Peters committed
813 814
    def unsubscribe(self, mailbox):
        """Unsubscribe from old mailbox.
815

Tim Peters's avatar
Tim Peters committed
816 817 818 819 820 821 822 823
        (typ, [data]) = <instance>.unsubscribe(mailbox)
        """
        return self._simple_command('UNSUBSCRIBE', mailbox)


    def xatom(self, name, *args):
        """Allow simple extension commands
                notified by server in CAPABILITY response.
824

Piers Lauder's avatar
Piers Lauder committed
825 826
        Assumes command is legal in current state.

Tim Peters's avatar
Tim Peters committed
827
        (typ, [data]) = <instance>.xatom(name, arg, ...)
Piers Lauder's avatar
Piers Lauder committed
828 829

        Returns response appropriate to extension command `name'.
Tim Peters's avatar
Tim Peters committed
830
        """
Piers Lauder's avatar
Piers Lauder committed
831
        name = name.upper()
832
        #if not name in self.capabilities:      # Let the server decide!
Piers Lauder's avatar
Piers Lauder committed
833
        #    raise self.error('unknown extension command: %s' % name)
834
        if not name in Commands:
Piers Lauder's avatar
Piers Lauder committed
835
            Commands[name] = (self.state,)
836
        return self._simple_command(name, *args)
837 838


Tim Peters's avatar
Tim Peters committed
839 840 841 842 843

    #       Private methods


    def _append_untagged(self, typ, dat):
844 845
        if dat is None:
            dat = b''
Tim Peters's avatar
Tim Peters committed
846 847 848
        ur = self.untagged_responses
        if __debug__:
            if self.debug >= 5:
849
                self._mesg('untagged_responses[%s] %s += ["%r"]' %
Tim Peters's avatar
Tim Peters committed
850
                        (typ, len(ur.get(typ,'')), dat))
851
        if typ in ur:
Tim Peters's avatar
Tim Peters committed
852 853 854
            ur[typ].append(dat)
        else:
            ur[typ] = [dat]
855 856


Tim Peters's avatar
Tim Peters committed
857 858 859
    def _check_bye(self):
        bye = self.untagged_responses.get('BYE')
        if bye:
860
            raise self.abort(bye[-1].decode('ascii', 'replace'))
Guido van Rossum's avatar
Guido van Rossum committed
861 862


Tim Peters's avatar
Tim Peters committed
863
    def _command(self, name, *args):
864

Tim Peters's avatar
Tim Peters committed
865 866
        if self.state not in Commands[name]:
            self.literal = None
867 868 869 870
            raise self.error("command %s illegal in state %s, "
                             "only allowed in states %s" %
                             (name, self.state,
                              ', '.join(Commands[name])))
871

Tim Peters's avatar
Tim Peters committed
872
        for typ in ('OK', 'NO', 'BAD'):
873
            if typ in self.untagged_responses:
Tim Peters's avatar
Tim Peters committed
874
                del self.untagged_responses[typ]
875

876
        if 'READ-ONLY' in self.untagged_responses \
Tim Peters's avatar
Tim Peters committed
877 878
        and not self.is_readonly:
            raise self.readonly('mailbox status changed to READ-ONLY')
879

Tim Peters's avatar
Tim Peters committed
880
        tag = self._new_tag()
881 882
        name = bytes(name, 'ASCII')
        data = tag + b' ' + name
Tim Peters's avatar
Tim Peters committed
883 884
        for arg in args:
            if arg is None: continue
885 886 887
            if isinstance(arg, str):
                arg = bytes(arg, "ASCII")
            data = data + b' ' + arg
888

Tim Peters's avatar
Tim Peters committed
889 890 891 892 893 894 895
        literal = self.literal
        if literal is not None:
            self.literal = None
            if type(literal) is type(self._command):
                literator = literal
            else:
                literator = None
896
                data = data + bytes(' {%s}' % len(literal), 'ASCII')
897

Tim Peters's avatar
Tim Peters committed
898 899
        if __debug__:
            if self.debug >= 4:
900
                self._mesg('> %r' % data)
Tim Peters's avatar
Tim Peters committed
901
            else:
902
                self._log('> %r' % data)
Guido van Rossum's avatar
Guido van Rossum committed
903

Tim Peters's avatar
Tim Peters committed
904
        try:
905
            self.send(data + CRLF)
906
        except OSError as val:
Tim Peters's avatar
Tim Peters committed
907
            raise self.abort('socket error: %s' % val)
908

Tim Peters's avatar
Tim Peters committed
909 910
        if literal is None:
            return tag
911

Tim Peters's avatar
Tim Peters committed
912 913
        while 1:
            # Wait for continuation response
914

Tim Peters's avatar
Tim Peters committed
915 916 917
            while self._get_response():
                if self.tagged_commands[tag]:   # BAD/NO?
                    return tag
918

Tim Peters's avatar
Tim Peters committed
919
            # Send literal
920

Tim Peters's avatar
Tim Peters committed
921 922
            if literator:
                literal = literator(self.continuation_response)
923

Tim Peters's avatar
Tim Peters committed
924 925
            if __debug__:
                if self.debug >= 4:
926
                    self._mesg('write literal size %s' % len(literal))
927

Tim Peters's avatar
Tim Peters committed
928
            try:
Piers Lauder's avatar
Piers Lauder committed
929 930
                self.send(literal)
                self.send(CRLF)
931
            except OSError as val:
Tim Peters's avatar
Tim Peters committed
932
                raise self.abort('socket error: %s' % val)
933

Tim Peters's avatar
Tim Peters committed
934 935
            if not literator:
                break
936

Tim Peters's avatar
Tim Peters committed
937
        return tag
938 939


Tim Peters's avatar
Tim Peters committed
940
    def _command_complete(self, name, tag):
941 942 943
        # BYE is expected after LOGOUT
        if name != 'LOGOUT':
            self._check_bye()
Tim Peters's avatar
Tim Peters committed
944 945
        try:
            typ, data = self._get_tagged_response(tag)
946
        except self.abort as val:
Tim Peters's avatar
Tim Peters committed
947
            raise self.abort('command: %s => %s' % (name, val))
948
        except self.error as val:
Tim Peters's avatar
Tim Peters committed
949
            raise self.error('command: %s => %s' % (name, val))
950 951
        if name != 'LOGOUT':
            self._check_bye()
Tim Peters's avatar
Tim Peters committed
952 953 954
        if typ == 'BAD':
            raise self.error('%s command error: %s %s' % (name, typ, data))
        return typ, data
955 956


957 958 959 960 961 962 963 964 965
    def _get_capabilities(self):
        typ, dat = self.capability()
        if dat == [None]:
            raise self.error('no CAPABILITY response from server')
        dat = str(dat[-1], "ASCII")
        dat = dat.upper()
        self.capabilities = tuple(dat.split())


Tim Peters's avatar
Tim Peters committed
966
    def _get_response(self):
967

Tim Peters's avatar
Tim Peters committed
968 969 970 971
        # Read response and store.
        #
        # Returns None for continuation responses,
        # otherwise first response line received.
972

Tim Peters's avatar
Tim Peters committed
973
        resp = self._get_line()
974

Tim Peters's avatar
Tim Peters committed
975
        # Command completion response?
976

Tim Peters's avatar
Tim Peters committed
977 978
        if self._match(self.tagre, resp):
            tag = self.mo.group('tag')
979
            if not tag in self.tagged_commands:
Tim Peters's avatar
Tim Peters committed
980
                raise self.abort('unexpected tagged response: %s' % resp)
981

Tim Peters's avatar
Tim Peters committed
982
            typ = self.mo.group('type')
983
            typ = str(typ, 'ASCII')
Tim Peters's avatar
Tim Peters committed
984 985 986 987
            dat = self.mo.group('data')
            self.tagged_commands[tag] = (typ, [dat])
        else:
            dat2 = None
988

Tim Peters's avatar
Tim Peters committed
989
            # '*' (untagged) responses?
990

Tim Peters's avatar
Tim Peters committed
991 992 993
            if not self._match(Untagged_response, resp):
                if self._match(Untagged_status, resp):
                    dat2 = self.mo.group('data2')
994

Tim Peters's avatar
Tim Peters committed
995 996
            if self.mo is None:
                # Only other possibility is '+' (continuation) response...
997

Tim Peters's avatar
Tim Peters committed
998 999 1000
                if self._match(Continuation, resp):
                    self.continuation_response = self.mo.group('data')
                    return None     # NB: indicates continuation
1001

Tim Peters's avatar
Tim Peters committed
1002
                raise self.abort("unexpected response: '%s'" % resp)
1003

Tim Peters's avatar
Tim Peters committed
1004
            typ = self.mo.group('type')
1005
            typ = str(typ, 'ascii')
Tim Peters's avatar
Tim Peters committed
1006
            dat = self.mo.group('data')
1007 1008
            if dat is None: dat = b''        # Null untagged response
            if dat2: dat = dat + b' ' + dat2
1009

Tim Peters's avatar
Tim Peters committed
1010
            # Is there a literal to come?
1011

Tim Peters's avatar
Tim Peters committed
1012
            while self._match(Literal, dat):
1013

Tim Peters's avatar
Tim Peters committed
1014
                # Read literal direct from connection.
1015

1016
                size = int(self.mo.group('size'))
Tim Peters's avatar
Tim Peters committed
1017 1018
                if __debug__:
                    if self.debug >= 4:
1019
                        self._mesg('read literal size %s' % size)
Piers Lauder's avatar
Piers Lauder committed
1020
                data = self.read(size)
1021

Tim Peters's avatar
Tim Peters committed
1022
                # Store response with literal as tuple
1023

Tim Peters's avatar
Tim Peters committed
1024
                self._append_untagged(typ, (dat, data))
1025

Tim Peters's avatar
Tim Peters committed
1026
                # Read trailer - possibly containing another literal
1027

Tim Peters's avatar
Tim Peters committed
1028
                dat = self._get_line()
1029

Tim Peters's avatar
Tim Peters committed
1030
            self._append_untagged(typ, dat)
1031

Tim Peters's avatar
Tim Peters committed
1032
        # Bracketed response information?
1033

Tim Peters's avatar
Tim Peters committed
1034
        if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
1035 1036 1037
            typ = self.mo.group('type')
            typ = str(typ, "ASCII")
            self._append_untagged(typ, self.mo.group('data'))
1038

Tim Peters's avatar
Tim Peters committed
1039 1040
        if __debug__:
            if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
1041
                self._mesg('%s response: %r' % (typ, dat))
1042

Tim Peters's avatar
Tim Peters committed
1043
        return resp
1044 1045


Tim Peters's avatar
Tim Peters committed
1046
    def _get_tagged_response(self, tag):
1047

Tim Peters's avatar
Tim Peters committed
1048 1049 1050 1051 1052
        while 1:
            result = self.tagged_commands[tag]
            if result is not None:
                del self.tagged_commands[tag]
                return result
1053

Tim Peters's avatar
Tim Peters committed
1054 1055 1056 1057
            # Some have reported "unexpected response" exceptions.
            # Note that ignoring them here causes loops.
            # Instead, send me details of the unexpected response and
            # I'll update the code in `_get_response()'.
1058

Tim Peters's avatar
Tim Peters committed
1059 1060
            try:
                self._get_response()
1061
            except self.abort as val:
Tim Peters's avatar
Tim Peters committed
1062 1063
                if __debug__:
                    if self.debug >= 1:
1064
                        self.print_log()
Tim Peters's avatar
Tim Peters committed
1065
                raise
1066 1067


Tim Peters's avatar
Tim Peters committed
1068
    def _get_line(self):
1069

Piers Lauder's avatar
Piers Lauder committed
1070
        line = self.readline()
Tim Peters's avatar
Tim Peters committed
1071 1072
        if not line:
            raise self.abort('socket error: EOF')
1073

Tim Peters's avatar
Tim Peters committed
1074
        # Protocol mandates all lines terminated by CRLF
1075
        if not line.endswith(b'\r\n'):
1076
            raise self.abort('socket error: unterminated line: %r' % line)
1077

Tim Peters's avatar
Tim Peters committed
1078 1079 1080
        line = line[:-2]
        if __debug__:
            if self.debug >= 4:
1081
                self._mesg('< %r' % line)
Tim Peters's avatar
Tim Peters committed
1082
            else:
1083
                self._log('< %r' % line)
Tim Peters's avatar
Tim Peters committed
1084
        return line
1085 1086


Tim Peters's avatar
Tim Peters committed
1087
    def _match(self, cre, s):
1088

Tim Peters's avatar
Tim Peters committed
1089 1090
        # Run compiled regular expression match method on 's'.
        # Save result, return success.
1091

Tim Peters's avatar
Tim Peters committed
1092 1093 1094
        self.mo = cre.match(s)
        if __debug__:
            if self.mo is not None and self.debug >= 5:
1095
                self._mesg("\tmatched r'%r' => %r" % (cre.pattern, self.mo.groups()))
Tim Peters's avatar
Tim Peters committed
1096
        return self.mo is not None
1097 1098


Tim Peters's avatar
Tim Peters committed
1099
    def _new_tag(self):
1100

1101
        tag = self.tagpre + bytes(str(self.tagnum), 'ASCII')
Tim Peters's avatar
Tim Peters committed
1102 1103 1104
        self.tagnum = self.tagnum + 1
        self.tagged_commands[tag] = None
        return tag
1105 1106


Tim Peters's avatar
Tim Peters committed
1107
    def _quote(self, arg):
Guido van Rossum's avatar
Guido van Rossum committed
1108

1109 1110
        arg = arg.replace('\\', '\\\\')
        arg = arg.replace('"', '\\"')
Guido van Rossum's avatar
Guido van Rossum committed
1111

1112
        return '"' + arg + '"'
Guido van Rossum's avatar
Guido van Rossum committed
1113 1114


Tim Peters's avatar
Tim Peters committed
1115
    def _simple_command(self, name, *args):
1116

1117
        return self._command_complete(name, self._command(name, *args))
1118 1119


Tim Peters's avatar
Tim Peters committed
1120 1121 1122
    def _untagged_response(self, typ, dat, name):
        if typ == 'NO':
            return typ, dat
1123
        if not name in self.untagged_responses:
Tim Peters's avatar
Tim Peters committed
1124
            return typ, [None]
1125
        data = self.untagged_responses.pop(name)
Tim Peters's avatar
Tim Peters committed
1126 1127
        if __debug__:
            if self.debug >= 5:
1128
                self._mesg('untagged_responses[%s] => %s' % (name, data))
Tim Peters's avatar
Tim Peters committed
1129
        return typ, data
1130 1131


1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160
    if __debug__:

        def _mesg(self, s, secs=None):
            if secs is None:
                secs = time.time()
            tm = time.strftime('%M:%S', time.localtime(secs))
            sys.stderr.write('  %s.%02d %s\n' % (tm, (secs*100)%100, s))
            sys.stderr.flush()

        def _dump_ur(self, dict):
            # Dump untagged responses (in `dict').
            l = dict.items()
            if not l: return
            t = '\n\t\t'
            l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l)
            self._mesg('untagged responses dump:%s%s' % (t, t.join(l)))

        def _log(self, line):
            # Keep log of last `_cmd_log_len' interactions for debugging.
            self._cmd_log[self._cmd_log_idx] = (line, time.time())
            self._cmd_log_idx += 1
            if self._cmd_log_idx >= self._cmd_log_len:
                self._cmd_log_idx = 0

        def print_log(self):
            self._mesg('last %d IMAP4 interactions:' % len(self._cmd_log))
            i, n = self._cmd_log_idx, self._cmd_log_len
            while n:
                try:
1161
                    self._mesg(*self._cmd_log[i])
1162 1163 1164 1165 1166 1167 1168 1169
                except:
                    pass
                i += 1
                if i >= self._cmd_log_len:
                    i = 0
                n -= 1


1170
if HAVE_SSL:
1171

1172
    class IMAP4_SSL(IMAP4):
1173

1174
        """IMAP4 client class over SSL connection
1175

1176
        Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile[, ssl_context]]]]])
1177

1178
                host - host's name (default: localhost);
1179
                port - port number (default: standard IMAP4 SSL port);
1180 1181
                keyfile - PEM formatted file that contains your private key (default: None);
                certfile - PEM formatted certificate chain file (default: None);
1182 1183 1184
                ssl_context - a SSLContext object that contains your certificate chain
                              and private key (default: None)
                Note: if ssl_context is provided, then parameters keyfile or
1185
                certfile should not be set otherwise ValueError is raised.
1186

1187 1188
        for more documentation see the docstring of the parent class IMAP4.
        """
1189 1190


1191 1192 1193 1194 1195 1196 1197 1198
        def __init__(self, host='', port=IMAP4_SSL_PORT, keyfile=None, certfile=None, ssl_context=None):
            if ssl_context is not None and keyfile is not None:
                raise ValueError("ssl_context and keyfile arguments are mutually "
                                 "exclusive")
            if ssl_context is not None and certfile is not None:
                raise ValueError("ssl_context and certfile arguments are mutually "
                                 "exclusive")

1199 1200
            self.keyfile = keyfile
            self.certfile = certfile
1201
            self.ssl_context = ssl_context
1202
            IMAP4.__init__(self, host, port)
1203

1204 1205
        def _create_socket(self):
            sock = IMAP4._create_socket(self)
1206 1207 1208 1209
            if self.ssl_context:
                return self.ssl_context.wrap_socket(sock)
            else:
                return ssl.wrap_socket(sock, self.keyfile, self.certfile)
1210

1211
        def open(self, host='', port=IMAP4_SSL_PORT):
1212 1213 1214 1215 1216
            """Setup connection to remote server on "host:port".
                (default: localhost:standard IMAP4 SSL port).
            This connection will be used by the routines:
                read, readline, send, shutdown.
            """
1217
            IMAP4.open(self, host, port)
1218

1219
    __all__.append("IMAP4_SSL")
1220 1221


1222 1223 1224 1225 1226 1227
class IMAP4_stream(IMAP4):

    """IMAP4 client class over a stream

    Instantiate with: IMAP4_stream(command)

1228
            where "command" is a string that can be passed to subprocess.Popen()
1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247

    for more documentation see the docstring of the parent class IMAP4.
    """


    def __init__(self, command):
        self.command = command
        IMAP4.__init__(self)


    def open(self, host = None, port = None):
        """Setup a stream connection.
        This connection will be used by the routines:
            read, readline, send, shutdown.
        """
        self.host = None        # For compatibility with parent class
        self.port = None
        self.sock = None
        self.file = None
1248
        self.process = subprocess.Popen(self.command,
1249
            bufsize=DEFAULT_BUFFER_SIZE,
1250 1251 1252 1253
            stdin=subprocess.PIPE, stdout=subprocess.PIPE,
            shell=True, close_fds=True)
        self.writefile = self.process.stdin
        self.readfile = self.process.stdout
1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274

    def read(self, size):
        """Read 'size' bytes from remote."""
        return self.readfile.read(size)


    def readline(self):
        """Read line from remote."""
        return self.readfile.readline()


    def send(self, data):
        """Send data to remote."""
        self.writefile.write(data)
        self.writefile.flush()


    def shutdown(self):
        """Close I/O established in "open"."""
        self.readfile.close()
        self.writefile.close()
1275
        self.process.wait()
1276 1277 1278



1279 1280
class _Authenticator:

Tim Peters's avatar
Tim Peters committed
1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302
    """Private class to provide en/decoding
            for base64-based authentication conversation.
    """

    def __init__(self, mechinst):
        self.mech = mechinst    # Callable object to provide/process data

    def process(self, data):
        ret = self.mech(self.decode(data))
        if ret is None:
            return '*'      # Abort conversation
        return self.encode(ret)

    def encode(self, inp):
        #
        #  Invoke binascii.b2a_base64 iteratively with
        #  short even length buffers, strip the trailing
        #  line feed from the result and append.  "Even"
        #  means a number that factors to both 6 and 8,
        #  so when it gets to the end of the 8-bit input
        #  there's no partial 6-bit output.
        #
1303 1304 1305
        oup = b''
        if isinstance(inp, str):
            inp = inp.encode('ASCII')
Tim Peters's avatar
Tim Peters committed
1306 1307 1308 1309 1310 1311
        while inp:
            if len(inp) > 48:
                t = inp[:48]
                inp = inp[48:]
            else:
                t = inp
1312
                inp = b''
Tim Peters's avatar
Tim Peters committed
1313 1314 1315 1316 1317 1318 1319
            e = binascii.b2a_base64(t)
            if e:
                oup = oup + e[:-1]
        return oup

    def decode(self, inp):
        if not inp:
1320
            return b''
Tim Peters's avatar
Tim Peters committed
1321 1322
        return binascii.a2b_base64(inp)

1323 1324
Months = ' Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' ')
Mon2num = {s.encode():n+1 for n, s in enumerate(Months[1:])}
1325 1326

def Internaldate2tuple(resp):
1327
    """Parse an IMAP4 INTERNALDATE string.
1328

1329 1330
    Return corresponding local time.  The return value is a
    time.struct_time tuple or None if the string has wrong format.
Tim Peters's avatar
Tim Peters committed
1331
    """
1332

Tim Peters's avatar
Tim Peters committed
1333 1334 1335
    mo = InternalDate.match(resp)
    if not mo:
        return None
1336

Tim Peters's avatar
Tim Peters committed
1337 1338
    mon = Mon2num[mo.group('mon')]
    zonen = mo.group('zonen')
1339

1340 1341 1342 1343 1344 1345 1346
    day = int(mo.group('day'))
    year = int(mo.group('year'))
    hour = int(mo.group('hour'))
    min = int(mo.group('min'))
    sec = int(mo.group('sec'))
    zoneh = int(mo.group('zoneh'))
    zonem = int(mo.group('zonem'))
1347

Tim Peters's avatar
Tim Peters committed
1348
    # INTERNALDATE timezone must be subtracted to get UT
1349

Tim Peters's avatar
Tim Peters committed
1350
    zone = (zoneh*60 + zonem)*60
1351
    if zonen == b'-':
Tim Peters's avatar
Tim Peters committed
1352
        zone = -zone
1353

Tim Peters's avatar
Tim Peters committed
1354
    tt = (year, mon, day, hour, min, sec, -1, -1, -1)
1355
    utc = calendar.timegm(tt) - zone
1356

1357
    return time.localtime(utc)
1358 1359 1360 1361 1362



def Int2AP(num):

Tim Peters's avatar
Tim Peters committed
1363
    """Convert integer to A-P string representation."""
1364

1365
    val = b''; AP = b'ABCDEFGHIJKLMNOP'
Tim Peters's avatar
Tim Peters committed
1366 1367 1368
    num = int(abs(num))
    while num:
        num, mod = divmod(num, 16)
1369
        val = AP[mod:mod+1] + val
Tim Peters's avatar
Tim Peters committed
1370
    return val
1371 1372 1373 1374 1375



def ParseFlags(resp):

Tim Peters's avatar
Tim Peters committed
1376
    """Convert IMAP4 flags response to python tuple."""
1377

Tim Peters's avatar
Tim Peters committed
1378 1379 1380
    mo = Flags.match(resp)
    if not mo:
        return ()
1381

1382
    return tuple(mo.group('flags').split())
1383 1384 1385 1386


def Time2Internaldate(date_time):

1387
    """Convert date_time to IMAP4 INTERNALDATE representation.
1388

1389
    Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'.  The
Florent Xicluna's avatar
Florent Xicluna committed
1390
    date_time argument can be a number (int or float) representing
1391
    seconds since epoch (as returned by time.time()), a 9-tuple
1392 1393
    representing local time, an instance of time.struct_time (as
    returned by time.localtime()), an aware datetime instance or a
1394 1395
    double-quoted string.  In the last case, it is assumed to already
    be in the correct format.
Tim Peters's avatar
Tim Peters committed
1396
    """
1397
    if isinstance(date_time, (int, float)):
1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416
        dt = datetime.fromtimestamp(date_time,
                                    timezone.utc).astimezone()
    elif isinstance(date_time, tuple):
        try:
            gmtoff = date_time.tm_gmtoff
        except AttributeError:
            if time.daylight:
                dst = date_time[8]
                if dst == -1:
                    dst = time.localtime(time.mktime(date_time))[8]
                gmtoff = -(time.timezone, time.altzone)[dst]
            else:
                gmtoff = -time.timezone
        delta = timedelta(seconds=gmtoff)
        dt = datetime(*date_time[:6], tzinfo=timezone(delta))
    elif isinstance(date_time, datetime):
        if date_time.tzinfo is None:
            raise ValueError("date_time must be aware")
        dt = date_time
1417
    elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'):
Tim Peters's avatar
Tim Peters committed
1418
        return date_time        # Assume in correct format
1419 1420
    else:
        raise ValueError("date_time not of a known type")
1421 1422
    fmt = '"%d-{}-%Y %H:%M:%S %z"'.format(Months[dt.month])
    return dt.strftime(fmt)
1423 1424 1425



Guido van Rossum's avatar
Guido van Rossum committed
1426
if __name__ == '__main__':
1427

1428 1429 1430 1431
    # To test: invoke either as 'python imaplib.py [IMAP4_server_hostname]'
    # or 'python imaplib.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"'
    # to test the IMAP4_stream class

1432
    import getopt, getpass
Tim Peters's avatar
Tim Peters committed
1433 1434

    try:
1435
        optlist, args = getopt.getopt(sys.argv[1:], 'd:s:')
1436
    except getopt.error as val:
1437
        optlist, args = (), ()
Tim Peters's avatar
Tim Peters committed
1438

1439
    stream_command = None
Tim Peters's avatar
Tim Peters committed
1440 1441 1442
    for opt,val in optlist:
        if opt == '-d':
            Debug = int(val)
1443 1444 1445
        elif opt == '-s':
            stream_command = val
            if not args: args = (stream_command,)
Tim Peters's avatar
Tim Peters committed
1446 1447 1448 1449 1450 1451

    if not args: args = ('',)

    host = args[0]

    USER = getpass.getuser()
Piers Lauder's avatar
Piers Lauder committed
1452
    PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost"))
Tim Peters's avatar
Tim Peters committed
1453

1454
    test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER, 'lf':'\n'}
Tim Peters's avatar
Tim Peters committed
1455 1456 1457 1458 1459 1460 1461 1462 1463
    test_seq1 = (
    ('login', (USER, PASSWD)),
    ('create', ('/tmp/xxx 1',)),
    ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
    ('CREATE', ('/tmp/yyz 2',)),
    ('append', ('/tmp/yyz 2', None, None, test_mesg)),
    ('list', ('/tmp', 'yy*')),
    ('select', ('/tmp/yyz 2',)),
    ('search', (None, 'SUBJECT', 'test')),
1464
    ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
Tim Peters's avatar
Tim Peters committed
1465
    ('store', ('1', 'FLAGS', '(\Deleted)')),
Piers Lauder's avatar
Piers Lauder committed
1466
    ('namespace', ()),
Tim Peters's avatar
Tim Peters committed
1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482
    ('expunge', ()),
    ('recent', ()),
    ('close', ()),
    )

    test_seq2 = (
    ('select', ()),
    ('response',('UIDVALIDITY',)),
    ('uid', ('SEARCH', 'ALL')),
    ('response', ('EXISTS',)),
    ('append', (None, None, None, test_mesg)),
    ('recent', ()),
    ('logout', ()),
    )

    def run(cmd, args):
1483
        M._mesg('%s %s' % (cmd, args))
1484
        typ, dat = getattr(M, cmd)(*args)
1485
        M._mesg('%s => %s %s' % (cmd, typ, dat))
1486
        if typ == 'NO': raise dat[0]
Tim Peters's avatar
Tim Peters committed
1487 1488 1489
        return dat

    try:
1490 1491 1492 1493 1494
        if stream_command:
            M = IMAP4_stream(stream_command)
        else:
            M = IMAP4(host)
        if M.state == 'AUTH':
Tim Peters's avatar
Tim Peters committed
1495
            test_seq1 = test_seq1[1:]   # Login not needed
1496
        M._mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
1497
        M._mesg('CAPABILITIES = %r' % (M.capabilities,))
Tim Peters's avatar
Tim Peters committed
1498 1499 1500 1501 1502 1503 1504

        for cmd,args in test_seq1:
            run(cmd, args)

        for ml in run('list', ('/tmp/', 'yy%')):
            mo = re.match(r'.*"([^"]+)"$', ml)
            if mo: path = mo.group(1)
1505
            else: path = ml.split()[-1]
Tim Peters's avatar
Tim Peters committed
1506 1507 1508 1509 1510 1511 1512 1513
            run('delete', (path,))

        for cmd,args in test_seq2:
            dat = run(cmd, args)

            if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
                continue

1514
            uid = dat[-1].split()
Tim Peters's avatar
Tim Peters committed
1515 1516 1517 1518
            if not uid: continue
            run('uid', ('FETCH', '%s' % uid[-1],
                    '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))

1519
        print('\nAll tests OK.')
Tim Peters's avatar
Tim Peters committed
1520 1521

    except:
1522
        print('\nTests failed.')
Tim Peters's avatar
Tim Peters committed
1523 1524

        if not Debug:
1525
            print('''
Guido van Rossum's avatar
Guido van Rossum committed
1526 1527
If you would like to see debugging output,
try: %s -d5
1528
''' % sys.argv[0])
1529

Tim Peters's avatar
Tim Peters committed
1530
        raise