imaplib.py 46.5 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
26

27
__all__ = ["IMAP4", "IMAP4_stream", "Internaldate2tuple",
28
           "Int2AP", "ParseFlags", "Time2Internaldate"]
29

Tim Peters's avatar
Tim Peters committed
30
#       Globals
31 32 33 34

CRLF = '\r\n'
Debug = 0
IMAP4_PORT = 143
35
IMAP4_SSL_PORT = 993
Tim Peters's avatar
Tim Peters committed
36
AllowedVersions = ('IMAP4REV1', 'IMAP4')        # Most recent first
37

Tim Peters's avatar
Tim Peters committed
38
#       Commands
39 40

Commands = {
Tim Peters's avatar
Tim Peters committed
41 42 43 44 45 46 47 48 49
        # 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'),
50
        'DELETEACL':    ('AUTH', 'SELECTED'),
Tim Peters's avatar
Tim Peters committed
51 52 53
        'EXAMINE':      ('AUTH', 'SELECTED'),
        'EXPUNGE':      ('SELECTED',),
        'FETCH':        ('SELECTED',),
Piers Lauder's avatar
Piers Lauder committed
54
        'GETACL':       ('AUTH', 'SELECTED'),
55
        'GETANNOTATION':('AUTH', 'SELECTED'),
56 57
        'GETQUOTA':     ('AUTH', 'SELECTED'),
        'GETQUOTAROOT': ('AUTH', 'SELECTED'),
58
        'MYRIGHTS':     ('AUTH', 'SELECTED'),
Tim Peters's avatar
Tim Peters committed
59 60 61 62
        'LIST':         ('AUTH', 'SELECTED'),
        'LOGIN':        ('NONAUTH',),
        'LOGOUT':       ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
        'LSUB':         ('AUTH', 'SELECTED'),
Piers Lauder's avatar
Piers Lauder committed
63
        'NAMESPACE':    ('AUTH', 'SELECTED'),
Tim Peters's avatar
Tim Peters committed
64
        'NOOP':         ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
65
        'PARTIAL':      ('SELECTED',),                                  # NB: obsolete
66
        'PROXYAUTH':    ('AUTH',),
Tim Peters's avatar
Tim Peters committed
67 68 69
        'RENAME':       ('AUTH', 'SELECTED'),
        'SEARCH':       ('SELECTED',),
        'SELECT':       ('AUTH', 'SELECTED'),
Piers Lauder's avatar
Piers Lauder committed
70
        'SETACL':       ('AUTH', 'SELECTED'),
71
        'SETANNOTATION':('AUTH', 'SELECTED'),
72
        'SETQUOTA':     ('AUTH', 'SELECTED'),
Piers Lauder's avatar
Piers Lauder committed
73
        'SORT':         ('SELECTED',),
Tim Peters's avatar
Tim Peters committed
74 75 76
        'STATUS':       ('AUTH', 'SELECTED'),
        'STORE':        ('SELECTED',),
        'SUBSCRIBE':    ('AUTH', 'SELECTED'),
77
        'THREAD':       ('SELECTED',),
Tim Peters's avatar
Tim Peters committed
78 79 80 81 82
        'UID':          ('SELECTED',),
        'UNSUBSCRIBE':  ('AUTH', 'SELECTED'),
        }

#       Patterns to match server responses
83

84
Continuation = re.compile(r'\+( (?P<data>.*))?')
85 86
Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
InternalDate = re.compile(r'.*INTERNALDATE "'
87
        r'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
Tim Peters's avatar
Tim Peters committed
88 89 90
        r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
        r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
        r'"')
91
Literal = re.compile(r'.*{(?P<size>\d+)}$')
92
MapCRLF = re.compile(r'\r\n|\r|\n')
93
Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
94
Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
95 96 97 98 99 100
Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')



class IMAP4:

Tim Peters's avatar
Tim Peters committed
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
    """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)").
120

Tim Peters's avatar
Tim Peters committed
121 122
    Each command returns a tuple: (type, [data, ...]) where 'type'
    is usually 'OK' or 'NO', and 'data' is either the text from the
123 124 125 126
    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).
127

Tim Peters's avatar
Tim Peters committed
128 129 130 131 132
    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'.
133

Tim Peters's avatar
Tim Peters committed
134 135 136 137
    "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
138

139 140 141 142
    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
143
    """
144

Tim Peters's avatar
Tim Peters committed
145 146 147
    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
148

Tim Peters's avatar
Tim Peters committed
149
    mustquote = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]")
150

Tim Peters's avatar
Tim Peters committed
151 152 153 154 155 156 157
    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
158
        self.is_readonly = False        # READ-ONLY desired state
Tim Peters's avatar
Tim Peters committed
159
        self.tagnum = 0
160

Tim Peters's avatar
Tim Peters committed
161
        # Open socket to server.
162

Tim Peters's avatar
Tim Peters committed
163
        self.open(host, port)
164

Tim Peters's avatar
Tim Peters committed
165 166
        # Create unique tag for this session,
        # and compile tagged response matcher.
167

168
        self.tagpre = Int2AP(random.randint(4096, 65535))
Tim Peters's avatar
Tim Peters committed
169 170 171
        self.tagre = re.compile(r'(?P<tag>'
                        + self.tagpre
                        + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
172

Tim Peters's avatar
Tim Peters committed
173 174
        # Get server welcome message,
        # request and store CAPABILITY response.
175

Tim Peters's avatar
Tim Peters committed
176
        if __debug__:
177 178 179
            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
180
            if self.debug >= 1:
181 182
                self._mesg('imaplib version %s' % __version__)
                self._mesg('new IMAP4 connection, tag=%s' % self.tagpre)
183

Tim Peters's avatar
Tim Peters committed
184
        self.welcome = self._get_response()
185
        if 'PREAUTH' in self.untagged_responses:
Tim Peters's avatar
Tim Peters committed
186
            self.state = 'AUTH'
187
        elif 'OK' in self.untagged_responses:
Tim Peters's avatar
Tim Peters committed
188 189 190
            self.state = 'NONAUTH'
        else:
            raise self.error(self.welcome)
191

192 193
        typ, dat = self.capability()
        if dat == [None]:
Tim Peters's avatar
Tim Peters committed
194
            raise self.error('no CAPABILITY response from server')
195
        self.capabilities = tuple(dat[-1].upper().split())
196

Tim Peters's avatar
Tim Peters committed
197 198
        if __debug__:
            if self.debug >= 3:
199
                self._mesg('CAPABILITIES: %r' % (self.capabilities,))
200

Tim Peters's avatar
Tim Peters committed
201 202 203 204 205
        for version in AllowedVersions:
            if not version in self.capabilities:
                continue
            self.PROTOCOL_VERSION = version
            return
206

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

209

Tim Peters's avatar
Tim Peters committed
210 211
    def __getattr__(self, attr):
        #       Allow UPPERCASE variants of IMAP4 command methods.
212
        if attr in Commands:
Piers Lauder's avatar
Piers Lauder committed
213
            return getattr(self, attr.lower())
Tim Peters's avatar
Tim Peters committed
214 215
        raise AttributeError("Unknown IMAP4 command: '%s'" % attr)

216 217


Piers Lauder's avatar
Piers Lauder committed
218
    #       Overridable methods
219 220


221 222 223
    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
224 225 226
        This connection will be used by the routines:
            read, readline, send, shutdown.
        """
227 228
        self.host = host
        self.port = port
229
        self.sock = socket.create_connection((host, port))
230
        self.file = self.sock.makefile('rb')
231 232


Piers Lauder's avatar
Piers Lauder committed
233 234 235 236 237 238 239 240 241 242 243 244
    def read(self, size):
        """Read 'size' bytes from remote."""
        return self.file.read(size)


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


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

247

Piers Lauder's avatar
Piers Lauder committed
248 249 250
    def shutdown(self):
        """Close I/O established in "open"."""
        self.file.close()
251 252 253 254 255 256 257 258
        try:
            self.sock.shutdown(socket.SHUT_RDWR)
        except socket.error as e:
            # 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
259 260 261 262 263 264 265 266 267 268 269 270 271 272


    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
273 274 275
    def recent(self):
        """Return most recent 'RECENT' responses if any exist,
        else prompt server for an update using the 'NOOP' command.
276

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

Tim Peters's avatar
Tim Peters committed
279 280 281 282 283 284 285 286 287
        '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)
288 289


Tim Peters's avatar
Tim Peters committed
290 291
    def response(self, code):
        """Return data for response 'code' if received, or None.
292

Tim Peters's avatar
Tim Peters committed
293
        Old value for response 'code' is cleared.
294

Tim Peters's avatar
Tim Peters committed
295 296
        (code, [data]) = <instance>.response(code)
        """
297
        return self._untagged_response(code, [None], code.upper())
298 299 300



Tim Peters's avatar
Tim Peters committed
301
    #       IMAP4 commands
302 303


Tim Peters's avatar
Tim Peters committed
304 305
    def append(self, mailbox, flags, date_time, message):
        """Append message to named mailbox.
306

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

Tim Peters's avatar
Tim Peters committed
309 310 311 312 313 314 315 316 317 318 319 320 321 322
                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
323
        self.literal = MapCRLF.sub(CRLF, message)
Tim Peters's avatar
Tim Peters committed
324
        return self._simple_command(name, mailbox, flags, date_time)
325 326


Tim Peters's avatar
Tim Peters committed
327 328
    def authenticate(self, mechanism, authobject):
        """Authenticate command - requires response processing.
329

Tim Peters's avatar
Tim Peters committed
330 331 332
        'mechanism' specifies which authentication mechanism is to
        be used - it must appear in <instance>.capabilities in the
        form AUTH=<mechanism>.
333

Tim Peters's avatar
Tim Peters committed
334
        'authobject' must be a callable object:
335

Tim Peters's avatar
Tim Peters committed
336
                data = authobject(response)
337

Tim Peters's avatar
Tim Peters committed
338 339 340 341 342
        It will be called to process server continuation responses.
        It should return data that will be encoded and sent to server.
        It should return None if the client abort response '*' should
        be sent instead.
        """
343
        mech = mechanism.upper()
344 345
        # XXX: shouldn't this code be removed, not commented out?
        #cap = 'AUTH=%s' % mech
Tim Peters's avatar
Tim Peters committed
346
        #if not cap in self.capabilities:       # Let the server decide!
347
        #    raise self.error("Server doesn't allow %s authentication." % mech)
Tim Peters's avatar
Tim Peters committed
348 349 350 351 352 353
        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
354 355


356 357 358 359 360 361 362 363 364
    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
365 366
    def check(self):
        """Checkpoint mailbox on server.
367

Tim Peters's avatar
Tim Peters committed
368 369 370
        (typ, [data]) = <instance>.check()
        """
        return self._simple_command('CHECK')
371 372


Tim Peters's avatar
Tim Peters committed
373 374
    def close(self):
        """Close currently selected mailbox.
375

Tim Peters's avatar
Tim Peters committed
376 377
        Deleted messages are removed from writable mailbox.
        This is the recommended command before 'LOGOUT'.
378

Tim Peters's avatar
Tim Peters committed
379 380 381 382 383 384 385
        (typ, [data]) = <instance>.close()
        """
        try:
            typ, dat = self._simple_command('CLOSE')
        finally:
            self.state = 'AUTH'
        return typ, dat
386 387


Tim Peters's avatar
Tim Peters committed
388 389
    def copy(self, message_set, new_mailbox):
        """Copy 'message_set' messages onto end of 'new_mailbox'.
390

Tim Peters's avatar
Tim Peters committed
391 392 393
        (typ, [data]) = <instance>.copy(message_set, new_mailbox)
        """
        return self._simple_command('COPY', message_set, new_mailbox)
394 395


Tim Peters's avatar
Tim Peters committed
396 397
    def create(self, mailbox):
        """Create new mailbox.
398

Tim Peters's avatar
Tim Peters committed
399 400 401
        (typ, [data]) = <instance>.create(mailbox)
        """
        return self._simple_command('CREATE', mailbox)
402 403


Tim Peters's avatar
Tim Peters committed
404 405
    def delete(self, mailbox):
        """Delete old mailbox.
406

Tim Peters's avatar
Tim Peters committed
407 408 409
        (typ, [data]) = <instance>.delete(mailbox)
        """
        return self._simple_command('DELETE', mailbox)
410

411 412 413 414 415 416
    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)
417

Tim Peters's avatar
Tim Peters committed
418 419
    def expunge(self):
        """Permanently remove deleted items from selected mailbox.
420

Tim Peters's avatar
Tim Peters committed
421
        Generates 'EXPUNGE' response for each deleted message.
422

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

Tim Peters's avatar
Tim Peters committed
425 426 427 428 429
        '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)
430 431


Tim Peters's avatar
Tim Peters committed
432 433
    def fetch(self, message_set, message_parts):
        """Fetch (parts of) messages.
434

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

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

Tim Peters's avatar
Tim Peters committed
440 441 442 443 444
        '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)
445 446


Piers Lauder's avatar
Piers Lauder committed
447 448 449 450 451 452 453 454 455
    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')


456 457 458 459 460 461 462 463
    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')


464 465 466
    def getquota(self, root):
        """Get the quota root's resource usage and limits.

467
        Part of the IMAP4 QUOTA extension defined in rfc2087.
468 469

        (typ, [data]) = <instance>.getquota(root)
470
        """
471 472 473 474 475 476 477 478
        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)
479
        """
480
        typ, dat = self._simple_command('GETQUOTAROOT', mailbox)
481 482
        typ, quota = self._untagged_response(typ, dat, 'QUOTA')
        typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT')
483
        return typ, [quotaroot, quota]
484 485


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

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

Tim Peters's avatar
Tim Peters committed
491 492 493 494 495
        'data' is list of LIST responses.
        """
        name = 'LIST'
        typ, dat = self._simple_command(name, directory, pattern)
        return self._untagged_response(typ, dat, name)
496 497


Tim Peters's avatar
Tim Peters committed
498 499
    def login(self, user, password):
        """Identify client using plaintext password.
500

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

Tim Peters's avatar
Tim Peters committed
503 504 505 506 507 508 509
        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
510 511


512 513 514 515 516 517 518 519 520 521 522 523 524 525 526
    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
        return self.user + " " + hmac.HMAC(self.password, challenge).hexdigest()


Tim Peters's avatar
Tim Peters committed
527 528
    def logout(self):
        """Shutdown connection to server.
529

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

Tim Peters's avatar
Tim Peters committed
532 533 534 535 536
        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
537
        self.shutdown()
538
        if 'BYE' in self.untagged_responses:
Tim Peters's avatar
Tim Peters committed
539 540
            return 'BYE', self.untagged_responses['BYE']
        return typ, dat
541 542


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

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

Tim Peters's avatar
Tim Peters committed
548 549 550 551 552
        '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)
553

554 555 556 557 558 559 560
    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')
561

Piers Lauder's avatar
Piers Lauder committed
562 563 564 565 566 567 568 569 570 571
    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
572 573
    def noop(self):
        """Send NOOP command.
574

575
        (typ, [data]) = <instance>.noop()
Tim Peters's avatar
Tim Peters committed
576 577 578
        """
        if __debug__:
            if self.debug >= 3:
579
                self._dump_ur(self.untagged_responses)
Tim Peters's avatar
Tim Peters committed
580
        return self._simple_command('NOOP')
581 582


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

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

Tim Peters's avatar
Tim Peters committed
588 589 590 591 592
        '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')
593 594


595 596 597 598 599 600 601 602 603 604 605 606 607
    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
608 609
    def rename(self, oldmailbox, newmailbox):
        """Rename old mailbox name to new.
610

611
        (typ, [data]) = <instance>.rename(oldmailbox, newmailbox)
Tim Peters's avatar
Tim Peters committed
612 613
        """
        return self._simple_command('RENAME', oldmailbox, newmailbox)
614 615


Tim Peters's avatar
Tim Peters committed
616 617
    def search(self, charset, *criteria):
        """Search mailbox for matching messages.
618

619
        (typ, [data]) = <instance>.search(charset, criterion, ...)
620

Tim Peters's avatar
Tim Peters committed
621 622 623 624
        'data' is space separated list of matching message numbers.
        """
        name = 'SEARCH'
        if charset:
625
            typ, dat = self._simple_command(name, 'CHARSET', charset, *criteria)
Piers Lauder's avatar
Piers Lauder committed
626
        else:
627
            typ, dat = self._simple_command(name, *criteria)
Tim Peters's avatar
Tim Peters committed
628
        return self._untagged_response(typ, dat, name)
629 630


631
    def select(self, mailbox='INBOX', readonly=False):
Tim Peters's avatar
Tim Peters committed
632
        """Select a mailbox.
633

Tim Peters's avatar
Tim Peters committed
634
        Flush all untagged responses.
635

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

Tim Peters's avatar
Tim Peters committed
638
        'data' is count of messages in mailbox ('EXISTS' response).
639 640 641

        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
642 643 644
        """
        self.untagged_responses = {}    # Flush old responses.
        self.is_readonly = readonly
645
        if readonly:
Tim Peters's avatar
Tim Peters committed
646 647 648 649 650 651 652 653
            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'
654
        if 'READ-ONLY' in self.untagged_responses \
Tim Peters's avatar
Tim Peters committed
655 656 657
                and not readonly:
            if __debug__:
                if self.debug >= 1:
658
                    self._dump_ur(self.untagged_responses)
Tim Peters's avatar
Tim Peters committed
659 660
            raise self.readonly('%s is not writable' % mailbox)
        return typ, self.untagged_responses.get('EXISTS', [None])
661 662


Piers Lauder's avatar
Piers Lauder committed
663 664 665
    def setacl(self, mailbox, who, what):
        """Set a mailbox acl.

666
        (typ, [data]) = <instance>.setacl(mailbox, who, what)
Piers Lauder's avatar
Piers Lauder committed
667 668 669 670
        """
        return self._simple_command('SETACL', mailbox, who, what)


671 672 673 674 675 676 677 678
    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')


679 680 681 682
    def setquota(self, root, limits):
        """Set the quota root's resource limits.

        (typ, [data]) = <instance>.setquota(root, limits)
683
        """
684 685 686 687
        typ, dat = self._simple_command('SETQUOTA', root, limits)
        return self._untagged_response(typ, dat, 'QUOTA')


Piers Lauder's avatar
Piers Lauder committed
688 689 690 691 692 693
    def sort(self, sort_criteria, charset, *search_criteria):
        """IMAP4rev1 extension SORT command.

        (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
        """
        name = 'SORT'
694 695
        #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
696
        if (sort_criteria[0],sort_criteria[-1]) != ('(',')'):
697
            sort_criteria = '(%s)' % sort_criteria
698
        typ, dat = self._simple_command(name, sort_criteria, charset, *search_criteria)
Piers Lauder's avatar
Piers Lauder committed
699 700 701
        return self._untagged_response(typ, dat, name)


Tim Peters's avatar
Tim Peters committed
702 703
    def status(self, mailbox, names):
        """Request named status conditions for mailbox.
704

Tim Peters's avatar
Tim Peters committed
705 706 707
        (typ, [data]) = <instance>.status(mailbox, names)
        """
        name = 'STATUS'
708
        #if self.PROTOCOL_VERSION == 'IMAP4':   # Let the server decide!
Piers Lauder's avatar
Piers Lauder committed
709
        #    raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
Tim Peters's avatar
Tim Peters committed
710 711
        typ, dat = self._simple_command(name, mailbox, names)
        return self._untagged_response(typ, dat, name)
712 713


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

Tim Peters's avatar
Tim Peters committed
717 718 719 720 721 722
        (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')
723 724


Tim Peters's avatar
Tim Peters committed
725 726
    def subscribe(self, mailbox):
        """Subscribe to new mailbox.
727

Tim Peters's avatar
Tim Peters committed
728 729 730
        (typ, [data]) = <instance>.subscribe(mailbox)
        """
        return self._simple_command('SUBSCRIBE', mailbox)
731 732


733 734 735
    def thread(self, threading_algorithm, charset, *search_criteria):
        """IMAPrev1 extension THREAD command.

736
        (type, [data]) = <instance>.thread(threading_algorithm, charset, search_criteria, ...)
737 738 739 740 741 742
        """
        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
743 744 745
    def uid(self, command, *args):
        """Execute "command arg ..." with messages identified by UID,
                rather than message number.
746

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

Tim Peters's avatar
Tim Peters committed
749 750
        Returns response appropriate to 'command'.
        """
751
        command = command.upper()
752
        if not command in Commands:
Tim Peters's avatar
Tim Peters committed
753 754
            raise self.error("Unknown IMAP4 UID command: %s" % command)
        if self.state not in Commands[command]:
755 756 757 758
            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
759
        name = 'UID'
760
        typ, dat = self._simple_command(name, command, *args)
761
        if command in ('SEARCH', 'SORT', 'THREAD'):
Piers Lauder's avatar
Piers Lauder committed
762
            name = command
Tim Peters's avatar
Tim Peters committed
763 764 765
        else:
            name = 'FETCH'
        return self._untagged_response(typ, dat, name)
766 767


Tim Peters's avatar
Tim Peters committed
768 769
    def unsubscribe(self, mailbox):
        """Unsubscribe from old mailbox.
770

Tim Peters's avatar
Tim Peters committed
771 772 773 774 775 776 777 778
        (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.
779

Piers Lauder's avatar
Piers Lauder committed
780 781
        Assumes command is legal in current state.

Tim Peters's avatar
Tim Peters committed
782
        (typ, [data]) = <instance>.xatom(name, arg, ...)
Piers Lauder's avatar
Piers Lauder committed
783 784

        Returns response appropriate to extension command `name'.
Tim Peters's avatar
Tim Peters committed
785
        """
Piers Lauder's avatar
Piers Lauder committed
786
        name = name.upper()
787
        #if not name in self.capabilities:      # Let the server decide!
Piers Lauder's avatar
Piers Lauder committed
788
        #    raise self.error('unknown extension command: %s' % name)
789
        if not name in Commands:
Piers Lauder's avatar
Piers Lauder committed
790
            Commands[name] = (self.state,)
791
        return self._simple_command(name, *args)
792 793


Tim Peters's avatar
Tim Peters committed
794 795 796 797 798 799 800 801 802 803

    #       Private methods


    def _append_untagged(self, typ, dat):

        if dat is None: dat = ''
        ur = self.untagged_responses
        if __debug__:
            if self.debug >= 5:
804
                self._mesg('untagged_responses[%s] %s += ["%s"]' %
Tim Peters's avatar
Tim Peters committed
805
                        (typ, len(ur.get(typ,'')), dat))
806
        if typ in ur:
Tim Peters's avatar
Tim Peters committed
807 808 809
            ur[typ].append(dat)
        else:
            ur[typ] = [dat]
810 811


Tim Peters's avatar
Tim Peters committed
812 813 814 815
    def _check_bye(self):
        bye = self.untagged_responses.get('BYE')
        if bye:
            raise self.abort(bye[-1])
Guido van Rossum's avatar
Guido van Rossum committed
816 817


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

Tim Peters's avatar
Tim Peters committed
820 821
        if self.state not in Commands[name]:
            self.literal = None
822 823 824 825
            raise self.error("command %s illegal in state %s, "
                             "only allowed in states %s" %
                             (name, self.state,
                              ', '.join(Commands[name])))
826

Tim Peters's avatar
Tim Peters committed
827
        for typ in ('OK', 'NO', 'BAD'):
828
            if typ in self.untagged_responses:
Tim Peters's avatar
Tim Peters committed
829
                del self.untagged_responses[typ]
830

831
        if 'READ-ONLY' in self.untagged_responses \
Tim Peters's avatar
Tim Peters committed
832 833
        and not self.is_readonly:
            raise self.readonly('mailbox status changed to READ-ONLY')
834

Tim Peters's avatar
Tim Peters committed
835 836 837 838 839
        tag = self._new_tag()
        data = '%s %s' % (tag, name)
        for arg in args:
            if arg is None: continue
            data = '%s %s' % (data, self._checkquote(arg))
840

Tim Peters's avatar
Tim Peters committed
841 842 843 844 845 846 847 848
        literal = self.literal
        if literal is not None:
            self.literal = None
            if type(literal) is type(self._command):
                literator = literal
            else:
                literator = None
                data = '%s {%s}' % (data, len(literal))
849

Tim Peters's avatar
Tim Peters committed
850 851
        if __debug__:
            if self.debug >= 4:
852
                self._mesg('> %s' % data)
Tim Peters's avatar
Tim Peters committed
853
            else:
854
                self._log('> %s' % data)
Guido van Rossum's avatar
Guido van Rossum committed
855

Tim Peters's avatar
Tim Peters committed
856
        try:
Piers Lauder's avatar
Piers Lauder committed
857 858
            self.send('%s%s' % (data, CRLF))
        except (socket.error, OSError), val:
Tim Peters's avatar
Tim Peters committed
859
            raise self.abort('socket error: %s' % val)
860

Tim Peters's avatar
Tim Peters committed
861 862
        if literal is None:
            return tag
863

Tim Peters's avatar
Tim Peters committed
864 865
        while 1:
            # Wait for continuation response
866

Tim Peters's avatar
Tim Peters committed
867 868 869
            while self._get_response():
                if self.tagged_commands[tag]:   # BAD/NO?
                    return tag
870

Tim Peters's avatar
Tim Peters committed
871
            # Send literal
872

Tim Peters's avatar
Tim Peters committed
873 874
            if literator:
                literal = literator(self.continuation_response)
875

Tim Peters's avatar
Tim Peters committed
876 877
            if __debug__:
                if self.debug >= 4:
878
                    self._mesg('write literal size %s' % len(literal))
879

Tim Peters's avatar
Tim Peters committed
880
            try:
Piers Lauder's avatar
Piers Lauder committed
881 882 883
                self.send(literal)
                self.send(CRLF)
            except (socket.error, OSError), val:
Tim Peters's avatar
Tim Peters committed
884
                raise self.abort('socket error: %s' % val)
885

Tim Peters's avatar
Tim Peters committed
886 887
            if not literator:
                break
888

Tim Peters's avatar
Tim Peters committed
889
        return tag
890 891


Tim Peters's avatar
Tim Peters committed
892
    def _command_complete(self, name, tag):
893 894 895
        # BYE is expected after LOGOUT
        if name != 'LOGOUT':
            self._check_bye()
Tim Peters's avatar
Tim Peters committed
896 897 898 899 900 901
        try:
            typ, data = self._get_tagged_response(tag)
        except self.abort, val:
            raise self.abort('command: %s => %s' % (name, val))
        except self.error, val:
            raise self.error('command: %s => %s' % (name, val))
902 903
        if name != 'LOGOUT':
            self._check_bye()
Tim Peters's avatar
Tim Peters committed
904 905 906
        if typ == 'BAD':
            raise self.error('%s command error: %s %s' % (name, typ, data))
        return typ, data
907 908


Tim Peters's avatar
Tim Peters committed
909
    def _get_response(self):
910

Tim Peters's avatar
Tim Peters committed
911 912 913 914
        # Read response and store.
        #
        # Returns None for continuation responses,
        # otherwise first response line received.
915

Tim Peters's avatar
Tim Peters committed
916
        resp = self._get_line()
917

Tim Peters's avatar
Tim Peters committed
918
        # Command completion response?
919

Tim Peters's avatar
Tim Peters committed
920 921
        if self._match(self.tagre, resp):
            tag = self.mo.group('tag')
922
            if not tag in self.tagged_commands:
Tim Peters's avatar
Tim Peters committed
923
                raise self.abort('unexpected tagged response: %s' % resp)
924

Tim Peters's avatar
Tim Peters committed
925 926 927 928 929
            typ = self.mo.group('type')
            dat = self.mo.group('data')
            self.tagged_commands[tag] = (typ, [dat])
        else:
            dat2 = None
930

Tim Peters's avatar
Tim Peters committed
931
            # '*' (untagged) responses?
932

Tim Peters's avatar
Tim Peters committed
933 934 935
            if not self._match(Untagged_response, resp):
                if self._match(Untagged_status, resp):
                    dat2 = self.mo.group('data2')
936

Tim Peters's avatar
Tim Peters committed
937 938
            if self.mo is None:
                # Only other possibility is '+' (continuation) response...
939

Tim Peters's avatar
Tim Peters committed
940 941 942
                if self._match(Continuation, resp):
                    self.continuation_response = self.mo.group('data')
                    return None     # NB: indicates continuation
943

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

Tim Peters's avatar
Tim Peters committed
946 947 948 949
            typ = self.mo.group('type')
            dat = self.mo.group('data')
            if dat is None: dat = ''        # Null untagged response
            if dat2: dat = dat + ' ' + dat2
950

Tim Peters's avatar
Tim Peters committed
951
            # Is there a literal to come?
952

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

Tim Peters's avatar
Tim Peters committed
955
                # Read literal direct from connection.
956

957
                size = int(self.mo.group('size'))
Tim Peters's avatar
Tim Peters committed
958 959
                if __debug__:
                    if self.debug >= 4:
960
                        self._mesg('read literal size %s' % size)
Piers Lauder's avatar
Piers Lauder committed
961
                data = self.read(size)
962

Tim Peters's avatar
Tim Peters committed
963
                # Store response with literal as tuple
964

Tim Peters's avatar
Tim Peters committed
965
                self._append_untagged(typ, (dat, data))
966

Tim Peters's avatar
Tim Peters committed
967
                # Read trailer - possibly containing another literal
968

Tim Peters's avatar
Tim Peters committed
969
                dat = self._get_line()
970

Tim Peters's avatar
Tim Peters committed
971
            self._append_untagged(typ, dat)
972

Tim Peters's avatar
Tim Peters committed
973
        # Bracketed response information?
974

Tim Peters's avatar
Tim Peters committed
975 976
        if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
            self._append_untagged(self.mo.group('type'), self.mo.group('data'))
977

Tim Peters's avatar
Tim Peters committed
978 979
        if __debug__:
            if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
980
                self._mesg('%s response: %s' % (typ, dat))
981

Tim Peters's avatar
Tim Peters committed
982
        return resp
983 984


Tim Peters's avatar
Tim Peters committed
985
    def _get_tagged_response(self, tag):
986

Tim Peters's avatar
Tim Peters committed
987 988 989 990 991
        while 1:
            result = self.tagged_commands[tag]
            if result is not None:
                del self.tagged_commands[tag]
                return result
992

Tim Peters's avatar
Tim Peters committed
993 994 995 996
            # 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()'.
997

Tim Peters's avatar
Tim Peters committed
998 999 1000 1001 1002
            try:
                self._get_response()
            except self.abort, val:
                if __debug__:
                    if self.debug >= 1:
1003
                        self.print_log()
Tim Peters's avatar
Tim Peters committed
1004
                raise
1005 1006


Tim Peters's avatar
Tim Peters committed
1007
    def _get_line(self):
1008

Piers Lauder's avatar
Piers Lauder committed
1009
        line = self.readline()
Tim Peters's avatar
Tim Peters committed
1010 1011
        if not line:
            raise self.abort('socket error: EOF')
1012

Tim Peters's avatar
Tim Peters committed
1013
        # Protocol mandates all lines terminated by CRLF
1014 1015
        if not line.endswith('\r\n'):
            raise self.abort('socket error: unterminated line')
1016

Tim Peters's avatar
Tim Peters committed
1017 1018 1019
        line = line[:-2]
        if __debug__:
            if self.debug >= 4:
1020
                self._mesg('< %s' % line)
Tim Peters's avatar
Tim Peters committed
1021
            else:
1022
                self._log('< %s' % line)
Tim Peters's avatar
Tim Peters committed
1023
        return line
1024 1025


Tim Peters's avatar
Tim Peters committed
1026
    def _match(self, cre, s):
1027

Tim Peters's avatar
Tim Peters committed
1028 1029
        # Run compiled regular expression match method on 's'.
        # Save result, return success.
1030

Tim Peters's avatar
Tim Peters committed
1031 1032 1033
        self.mo = cre.match(s)
        if __debug__:
            if self.mo is not None and self.debug >= 5:
1034
                self._mesg("\tmatched r'%s' => %r" % (cre.pattern, self.mo.groups()))
Tim Peters's avatar
Tim Peters committed
1035
        return self.mo is not None
1036 1037


Tim Peters's avatar
Tim Peters committed
1038
    def _new_tag(self):
1039

Tim Peters's avatar
Tim Peters committed
1040 1041 1042 1043
        tag = '%s%s' % (self.tagpre, self.tagnum)
        self.tagnum = self.tagnum + 1
        self.tagged_commands[tag] = None
        return tag
1044 1045


Tim Peters's avatar
Tim Peters committed
1046
    def _checkquote(self, arg):
Guido van Rossum's avatar
Guido van Rossum committed
1047

Tim Peters's avatar
Tim Peters committed
1048 1049
        # Must quote command args if non-alphanumeric chars present,
        # and not already quoted.
Guido van Rossum's avatar
Guido van Rossum committed
1050

Tim Peters's avatar
Tim Peters committed
1051 1052
        if type(arg) is not type(''):
            return arg
1053
        if len(arg) >= 2 and (arg[0],arg[-1]) in (('(',')'),('"','"')):
Tim Peters's avatar
Tim Peters committed
1054
            return arg
1055
        if arg and self.mustquote.search(arg) is None:
Tim Peters's avatar
Tim Peters committed
1056 1057
            return arg
        return self._quote(arg)
Guido van Rossum's avatar
Guido van Rossum committed
1058 1059


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

1062 1063
        arg = arg.replace('\\', '\\\\')
        arg = arg.replace('"', '\\"')
Guido van Rossum's avatar
Guido van Rossum committed
1064

Tim Peters's avatar
Tim Peters committed
1065
        return '"%s"' % arg
Guido van Rossum's avatar
Guido van Rossum committed
1066 1067


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

1070
        return self._command_complete(name, self._command(name, *args))
1071 1072


Tim Peters's avatar
Tim Peters committed
1073
    def _untagged_response(self, typ, dat, name):
1074

Tim Peters's avatar
Tim Peters committed
1075 1076
        if typ == 'NO':
            return typ, dat
1077
        if not name in self.untagged_responses:
Tim Peters's avatar
Tim Peters committed
1078
            return typ, [None]
1079
        data = self.untagged_responses.pop(name)
Tim Peters's avatar
Tim Peters committed
1080 1081
        if __debug__:
            if self.debug >= 5:
1082
                self._mesg('untagged_responses[%s] => %s' % (name, data))
Tim Peters's avatar
Tim Peters committed
1083
        return typ, data
1084 1085


1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114
    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:
1115
                    self._mesg(*self._cmd_log[i])
1116 1117 1118 1119 1120 1121 1122 1123
                except:
                    pass
                i += 1
                if i >= self._cmd_log_len:
                    i = 0
                n -= 1


1124

1125 1126 1127 1128 1129 1130
try:
    import ssl
except ImportError:
    pass
else:
    class IMAP4_SSL(IMAP4):
1131

1132
        """IMAP4 client class over SSL connection
1133

1134
        Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile]]]])
1135

1136 1137 1138 1139
                host - host's name (default: localhost);
                port - port number (default: standard IMAP4 SSL port).
                keyfile - PEM formatted file that contains your private key (default: None);
                certfile - PEM formatted certificate chain file (default: None);
1140

1141 1142
        for more documentation see the docstring of the parent class IMAP4.
        """
1143 1144


1145 1146 1147 1148
        def __init__(self, host = '', port = IMAP4_SSL_PORT, keyfile = None, certfile = None):
            self.keyfile = keyfile
            self.certfile = certfile
            IMAP4.__init__(self, host, port)
1149 1150


1151 1152 1153 1154 1155 1156 1157 1158
        def open(self, host = '', port = IMAP4_SSL_PORT):
            """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.
            """
            self.host = host
            self.port = port
1159
            self.sock = socket.create_connection((host, port))
Bill Janssen's avatar
Bill Janssen committed
1160
            self.sslobj = ssl.wrap_socket(self.sock, self.keyfile, self.certfile)
1161
            self.file = self.sslobj.makefile('rb')
1162 1163


1164 1165
        def read(self, size):
            """Read 'size' bytes from remote."""
1166
            return self.file.read(size)
1167 1168


1169 1170
        def readline(self):
            """Read line from remote."""
1171
            return self.file.readline()
1172 1173


1174 1175 1176 1177 1178 1179 1180 1181 1182
        def send(self, data):
            """Send data to remote."""
            bytes = len(data)
            while bytes > 0:
                sent = self.sslobj.write(data)
                if sent == bytes:
                    break    # avoid copy
                data = data[sent:]
                bytes = bytes - sent
1183 1184


1185 1186
        def shutdown(self):
            """Close I/O established in "open"."""
1187
            self.file.close()
1188
            self.sock.close()
1189 1190


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

1194 1195 1196
            socket = <instance>.socket()
            """
            return self.sock
1197 1198


1199 1200
        def ssl(self):
            """Return SSLObject instance used to communicate with the IMAP4 server.
1201

Bill Janssen's avatar
Bill Janssen committed
1202
            ssl = ssl.wrap_socket(<instance>.socket)
1203 1204
            """
            return self.sslobj
1205

1206
    __all__.append("IMAP4_SSL")
1207 1208


1209 1210 1211 1212 1213 1214
class IMAP4_stream(IMAP4):

    """IMAP4 client class over a stream

    Instantiate with: IMAP4_stream(command)

Georg Brandl's avatar
Georg Brandl committed
1215
            where "command" is a string that can be passed to subprocess.Popen()
1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234

    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
1235 1236 1237 1238 1239
        self.process = subprocess.Popen(self.command,
            stdin=subprocess.PIPE, stdout=subprocess.PIPE,
            shell=True, close_fds=True)
        self.writefile = self.process.stdin
        self.readfile = self.process.stdout
1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261


    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()
1262
        self.process.wait()
1263 1264 1265



1266 1267
class _Authenticator:

Tim Peters's avatar
Tim Peters committed
1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307
    """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.
        #
        oup = ''
        while inp:
            if len(inp) > 48:
                t = inp[:48]
                inp = inp[48:]
            else:
                t = inp
                inp = ''
            e = binascii.b2a_base64(t)
            if e:
                oup = oup + e[:-1]
        return oup

    def decode(self, inp):
        if not inp:
            return ''
        return binascii.a2b_base64(inp)

1308 1309


1310
Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
Tim Peters's avatar
Tim Peters committed
1311
        'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
1312 1313

def Internaldate2tuple(resp):
1314
    """Parse an IMAP4 INTERNALDATE string.
1315

1316 1317
    Return corresponding local time.  The return value is a
    time.struct_time instance or None if the string has wrong format.
Tim Peters's avatar
Tim Peters committed
1318
    """
1319

Tim Peters's avatar
Tim Peters committed
1320 1321 1322
    mo = InternalDate.match(resp)
    if not mo:
        return None
1323

Tim Peters's avatar
Tim Peters committed
1324 1325
    mon = Mon2num[mo.group('mon')]
    zonen = mo.group('zonen')
1326

1327 1328 1329 1330 1331 1332 1333
    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'))
1334

Tim Peters's avatar
Tim Peters committed
1335
    # INTERNALDATE timezone must be subtracted to get UT
1336

Tim Peters's avatar
Tim Peters committed
1337 1338 1339
    zone = (zoneh*60 + zonem)*60
    if zonen == '-':
        zone = -zone
1340

Tim Peters's avatar
Tim Peters committed
1341
    tt = (year, mon, day, hour, min, sec, -1, -1, -1)
1342

Tim Peters's avatar
Tim Peters committed
1343
    utc = time.mktime(tt)
1344

Tim Peters's avatar
Tim Peters committed
1345 1346
    # Following is necessary because the time module has no 'mkgmtime'.
    # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1347

Tim Peters's avatar
Tim Peters committed
1348 1349 1350 1351 1352
    lt = time.localtime(utc)
    if time.daylight and lt[-1]:
        zone = zone + time.altzone
    else:
        zone = zone + time.timezone
1353

Tim Peters's avatar
Tim Peters committed
1354
    return time.localtime(utc - zone)
1355 1356 1357 1358 1359



def Int2AP(num):

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

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



def ParseFlags(resp):

Tim Peters's avatar
Tim Peters committed
1373
    """Convert IMAP4 flags response to python tuple."""
1374

Tim Peters's avatar
Tim Peters committed
1375 1376 1377
    mo = Flags.match(resp)
    if not mo:
        return ()
1378

1379
    return tuple(mo.group('flags').split())
1380 1381 1382 1383


def Time2Internaldate(date_time):

1384
    """Convert date_time to IMAP4 INTERNALDATE representation.
1385

1386
    Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'.  The
1387
    date_time argument can be a number (int or float) representing
1388 1389 1390 1391
    seconds since epoch (as returned by time.time()), a 9-tuple
    representing local time (as returned by time.localtime()), or a
    double-quoted string.  In the last case, it is assumed to already
    be in the correct format.
Tim Peters's avatar
Tim Peters committed
1392
    """
1393

1394
    if isinstance(date_time, (int, float)):
Tim Peters's avatar
Tim Peters committed
1395
        tt = time.localtime(date_time)
1396
    elif isinstance(date_time, (tuple, time.struct_time)):
Tim Peters's avatar
Tim Peters committed
1397
        tt = date_time
1398
    elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'):
Tim Peters's avatar
Tim Peters committed
1399
        return date_time        # Assume in correct format
1400 1401
    else:
        raise ValueError("date_time not of a known type")
1402

Tim Peters's avatar
Tim Peters committed
1403 1404 1405 1406 1407 1408 1409
    dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
    if dt[0] == '0':
        dt = ' ' + dt[1:]
    if time.daylight and tt[-1]:
        zone = -time.altzone
    else:
        zone = -time.timezone
1410
    return '"' + dt + " %+03d%02d" % divmod(zone//60, 60) + '"'
1411 1412 1413



Guido van Rossum's avatar
Guido van Rossum committed
1414
if __name__ == '__main__':
1415

1416 1417 1418 1419
    # 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

1420
    import getopt, getpass
Tim Peters's avatar
Tim Peters committed
1421 1422

    try:
1423
        optlist, args = getopt.getopt(sys.argv[1:], 'd:s:')
Tim Peters's avatar
Tim Peters committed
1424
    except getopt.error, val:
1425
        optlist, args = (), ()
Tim Peters's avatar
Tim Peters committed
1426

1427
    stream_command = None
Tim Peters's avatar
Tim Peters committed
1428 1429 1430
    for opt,val in optlist:
        if opt == '-d':
            Debug = int(val)
1431 1432 1433
        elif opt == '-s':
            stream_command = val
            if not args: args = (stream_command,)
Tim Peters's avatar
Tim Peters committed
1434 1435 1436 1437 1438 1439

    if not args: args = ('',)

    host = args[0]

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

1442
    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
1443 1444 1445 1446 1447 1448 1449 1450 1451
    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')),
1452
    ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
Tim Peters's avatar
Tim Peters committed
1453
    ('store', ('1', 'FLAGS', '(\Deleted)')),
Piers Lauder's avatar
Piers Lauder committed
1454
    ('namespace', ()),
Tim Peters's avatar
Tim Peters committed
1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470
    ('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):
1471
        M._mesg('%s %s' % (cmd, args))
1472
        typ, dat = getattr(M, cmd)(*args)
1473
        M._mesg('%s => %s %s' % (cmd, typ, dat))
1474
        if typ == 'NO': raise dat[0]
Tim Peters's avatar
Tim Peters committed
1475 1476 1477
        return dat

    try:
1478 1479 1480 1481 1482
        if stream_command:
            M = IMAP4_stream(stream_command)
        else:
            M = IMAP4(host)
        if M.state == 'AUTH':
Tim Peters's avatar
Tim Peters committed
1483
            test_seq1 = test_seq1[1:]   # Login not needed
1484
        M._mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
1485
        M._mesg('CAPABILITIES = %r' % (M.capabilities,))
Tim Peters's avatar
Tim Peters committed
1486 1487 1488 1489 1490 1491 1492

        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)
1493
            else: path = ml.split()[-1]
Tim Peters's avatar
Tim Peters committed
1494 1495 1496 1497 1498 1499 1500 1501
            run('delete', (path,))

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

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

1502
            uid = dat[-1].split()
Tim Peters's avatar
Tim Peters committed
1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513
            if not uid: continue
            run('uid', ('FETCH', '%s' % uid[-1],
                    '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))

        print '\nAll tests OK.'

    except:
        print '\nTests failed.'

        if not Debug:
            print '''
Guido van Rossum's avatar
Guido van Rossum committed
1514 1515 1516
If you would like to see debugging output,
try: %s -d5
''' % sys.argv[0]
1517

Tim Peters's avatar
Tim Peters committed
1518
        raise