PageRenderTime 151ms CodeModel.GetById 12ms app.highlight 90ms RepoModel.GetById 1ms app.codeStats 1ms

/Lib/imaplib.py

http://unladen-swallow.googlecode.com/
Python | 1505 lines | 1442 code | 26 blank | 37 comment | 18 complexity | 46a1e2eca5ab889305c27b35b424156a MD5 | raw file
   1"""IMAP4 client.
   2
   3Based on RFC 2060.
   4
   5Public class:           IMAP4
   6Public variable:        Debug
   7Public functions:       Internaldate2tuple
   8                        Int2AP
   9                        ParseFlags
  10                        Time2Internaldate
  11"""
  12
  13# Author: Piers Lauder <piers@cs.su.oz.au> December 1997.
  14#
  15# Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
  16# String method conversion by ESR, February 2001.
  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
  23__version__ = "2.58"
  24
  25import binascii, os, random, re, socket, sys, time
  26
  27__all__ = ["IMAP4", "IMAP4_stream", "Internaldate2tuple",
  28           "Int2AP", "ParseFlags", "Time2Internaldate"]
  29
  30#       Globals
  31
  32CRLF = '\r\n'
  33Debug = 0
  34IMAP4_PORT = 143
  35IMAP4_SSL_PORT = 993
  36AllowedVersions = ('IMAP4REV1', 'IMAP4')        # Most recent first
  37
  38#       Commands
  39
  40Commands = {
  41        # name            valid states
  42        'APPEND':       ('AUTH', 'SELECTED'),
  43        'AUTHENTICATE': ('NONAUTH',),
  44        'CAPABILITY':   ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
  45        'CHECK':        ('SELECTED',),
  46        'CLOSE':        ('SELECTED',),
  47        'COPY':         ('SELECTED',),
  48        'CREATE':       ('AUTH', 'SELECTED'),
  49        'DELETE':       ('AUTH', 'SELECTED'),
  50        'DELETEACL':    ('AUTH', 'SELECTED'),
  51        'EXAMINE':      ('AUTH', 'SELECTED'),
  52        'EXPUNGE':      ('SELECTED',),
  53        'FETCH':        ('SELECTED',),
  54        'GETACL':       ('AUTH', 'SELECTED'),
  55        'GETANNOTATION':('AUTH', 'SELECTED'),
  56        'GETQUOTA':     ('AUTH', 'SELECTED'),
  57        'GETQUOTAROOT': ('AUTH', 'SELECTED'),
  58        'MYRIGHTS':     ('AUTH', 'SELECTED'),
  59        'LIST':         ('AUTH', 'SELECTED'),
  60        'LOGIN':        ('NONAUTH',),
  61        'LOGOUT':       ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
  62        'LSUB':         ('AUTH', 'SELECTED'),
  63        'NAMESPACE':    ('AUTH', 'SELECTED'),
  64        'NOOP':         ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
  65        'PARTIAL':      ('SELECTED',),                                  # NB: obsolete
  66        'PROXYAUTH':    ('AUTH',),
  67        'RENAME':       ('AUTH', 'SELECTED'),
  68        'SEARCH':       ('SELECTED',),
  69        'SELECT':       ('AUTH', 'SELECTED'),
  70        'SETACL':       ('AUTH', 'SELECTED'),
  71        'SETANNOTATION':('AUTH', 'SELECTED'),
  72        'SETQUOTA':     ('AUTH', 'SELECTED'),
  73        'SORT':         ('SELECTED',),
  74        'STATUS':       ('AUTH', 'SELECTED'),
  75        'STORE':        ('SELECTED',),
  76        'SUBSCRIBE':    ('AUTH', 'SELECTED'),
  77        'THREAD':       ('SELECTED',),
  78        'UID':          ('SELECTED',),
  79        'UNSUBSCRIBE':  ('AUTH', 'SELECTED'),
  80        }
  81
  82#       Patterns to match server responses
  83
  84Continuation = re.compile(r'\+( (?P<data>.*))?')
  85Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
  86InternalDate = 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])'
  88        r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
  89        r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
  90        r'"')
  91Literal = re.compile(r'.*{(?P<size>\d+)}$')
  92MapCRLF = re.compile(r'\r\n|\r|\n')
  93Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
  94Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
  95Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
  96
  97
  98
  99class IMAP4:
 100
 101    """IMAP4 client class.
 102
 103    Instantiate with: IMAP4([host[, port]])
 104
 105            host - host's name (default: localhost);
 106            port - port number (default: standard IMAP4 port).
 107
 108    All IMAP4rev1 commands are supported by methods of the same
 109    name (in lower-case).
 110
 111    All arguments to commands are converted to strings, except for
 112    AUTHENTICATE, and the last argument to APPEND which is passed as
 113    an IMAP4 literal.  If necessary (the string contains any
 114    non-printing characters or white-space and isn't enclosed with
 115    either parentheses or double quotes) each string is quoted.
 116    However, the 'password' argument to the LOGIN command is always
 117    quoted.  If you want to avoid having an argument string quoted
 118    (eg: the 'flags' argument to STORE) then enclose the string in
 119    parentheses (eg: "(\Deleted)").
 120
 121    Each command returns a tuple: (type, [data, ...]) where 'type'
 122    is usually 'OK' or 'NO', and 'data' is either the text from the
 123    tagged response, or untagged results from command. Each 'data'
 124    is either a string, or a tuple. If a tuple, then the first part
 125    is the header of the response, and the second part contains
 126    the data (ie: 'literal' value).
 127
 128    Errors raise the exception class <instance>.error("<reason>").
 129    IMAP4 server errors raise <instance>.abort("<reason>"),
 130    which is a sub-class of 'error'. Mailbox status changes
 131    from READ-WRITE to READ-ONLY raise the exception class
 132    <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
 133
 134    "error" exceptions imply a program error.
 135    "abort" exceptions imply the connection should be reset, and
 136            the command re-tried.
 137    "readonly" exceptions imply the command should be re-tried.
 138
 139    Note: to use this module, you must read the RFCs pertaining to the
 140    IMAP4 protocol, as the semantics of the arguments to each IMAP4
 141    command are left to the invoker, not to mention the results. Also,
 142    most IMAP servers implement a sub-set of the commands available here.
 143    """
 144
 145    class error(Exception): pass    # Logical errors - debug required
 146    class abort(error): pass        # Service errors - close and retry
 147    class readonly(abort): pass     # Mailbox status changed to READ-ONLY
 148
 149    mustquote = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]")
 150
 151    def __init__(self, host = '', port = IMAP4_PORT):
 152        self.debug = Debug
 153        self.state = 'LOGOUT'
 154        self.literal = None             # A literal argument to a command
 155        self.tagged_commands = {}       # Tagged commands awaiting response
 156        self.untagged_responses = {}    # {typ: [data, ...], ...}
 157        self.continuation_response = '' # Last continuation response
 158        self.is_readonly = False        # READ-ONLY desired state
 159        self.tagnum = 0
 160
 161        # Open socket to server.
 162
 163        self.open(host, port)
 164
 165        # Create unique tag for this session,
 166        # and compile tagged response matcher.
 167
 168        self.tagpre = Int2AP(random.randint(4096, 65535))
 169        self.tagre = re.compile(r'(?P<tag>'
 170                        + self.tagpre
 171                        + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
 172
 173        # Get server welcome message,
 174        # request and store CAPABILITY response.
 175
 176        if __debug__:
 177            self._cmd_log_len = 10
 178            self._cmd_log_idx = 0
 179            self._cmd_log = {}           # Last `_cmd_log_len' interactions
 180            if self.debug >= 1:
 181                self._mesg('imaplib version %s' % __version__)
 182                self._mesg('new IMAP4 connection, tag=%s' % self.tagpre)
 183
 184        self.welcome = self._get_response()
 185        if 'PREAUTH' in self.untagged_responses:
 186            self.state = 'AUTH'
 187        elif 'OK' in self.untagged_responses:
 188            self.state = 'NONAUTH'
 189        else:
 190            raise self.error(self.welcome)
 191
 192        typ, dat = self.capability()
 193        if dat == [None]:
 194            raise self.error('no CAPABILITY response from server')
 195        self.capabilities = tuple(dat[-1].upper().split())
 196
 197        if __debug__:
 198            if self.debug >= 3:
 199                self._mesg('CAPABILITIES: %r' % (self.capabilities,))
 200
 201        for version in AllowedVersions:
 202            if not version in self.capabilities:
 203                continue
 204            self.PROTOCOL_VERSION = version
 205            return
 206
 207        raise self.error('server not IMAP4 compliant')
 208
 209
 210    def __getattr__(self, attr):
 211        #       Allow UPPERCASE variants of IMAP4 command methods.
 212        if attr in Commands:
 213            return getattr(self, attr.lower())
 214        raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
 215
 216
 217
 218    #       Overridable methods
 219
 220
 221    def open(self, host = '', port = IMAP4_PORT):
 222        """Setup connection to remote server on "host:port"
 223            (default: localhost:standard IMAP4 port).
 224        This connection will be used by the routines:
 225            read, readline, send, shutdown.
 226        """
 227        self.host = host
 228        self.port = port
 229        self.sock = socket.create_connection((host, port))
 230        self.file = self.sock.makefile('rb')
 231
 232
 233    def read(self, size):
 234        """Read 'size' bytes from remote."""
 235        return self.file.read(size)
 236
 237
 238    def readline(self):
 239        """Read line from remote."""
 240        return self.file.readline()
 241
 242
 243    def send(self, data):
 244        """Send data to remote."""
 245        self.sock.sendall(data)
 246
 247
 248    def shutdown(self):
 249        """Close I/O established in "open"."""
 250        self.file.close()
 251        self.sock.close()
 252
 253
 254    def socket(self):
 255        """Return socket instance used to connect to IMAP4 server.
 256
 257        socket = <instance>.socket()
 258        """
 259        return self.sock
 260
 261
 262
 263    #       Utility methods
 264
 265
 266    def recent(self):
 267        """Return most recent 'RECENT' responses if any exist,
 268        else prompt server for an update using the 'NOOP' command.
 269
 270        (typ, [data]) = <instance>.recent()
 271
 272        'data' is None if no new messages,
 273        else list of RECENT responses, most recent last.
 274        """
 275        name = 'RECENT'
 276        typ, dat = self._untagged_response('OK', [None], name)
 277        if dat[-1]:
 278            return typ, dat
 279        typ, dat = self.noop()  # Prod server for response
 280        return self._untagged_response(typ, dat, name)
 281
 282
 283    def response(self, code):
 284        """Return data for response 'code' if received, or None.
 285
 286        Old value for response 'code' is cleared.
 287
 288        (code, [data]) = <instance>.response(code)
 289        """
 290        return self._untagged_response(code, [None], code.upper())
 291
 292
 293
 294    #       IMAP4 commands
 295
 296
 297    def append(self, mailbox, flags, date_time, message):
 298        """Append message to named mailbox.
 299
 300        (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
 301
 302                All args except `message' can be None.
 303        """
 304        name = 'APPEND'
 305        if not mailbox:
 306            mailbox = 'INBOX'
 307        if flags:
 308            if (flags[0],flags[-1]) != ('(',')'):
 309                flags = '(%s)' % flags
 310        else:
 311            flags = None
 312        if date_time:
 313            date_time = Time2Internaldate(date_time)
 314        else:
 315            date_time = None
 316        self.literal = MapCRLF.sub(CRLF, message)
 317        return self._simple_command(name, mailbox, flags, date_time)
 318
 319
 320    def authenticate(self, mechanism, authobject):
 321        """Authenticate command - requires response processing.
 322
 323        'mechanism' specifies which authentication mechanism is to
 324        be used - it must appear in <instance>.capabilities in the
 325        form AUTH=<mechanism>.
 326
 327        'authobject' must be a callable object:
 328
 329                data = authobject(response)
 330
 331        It will be called to process server continuation responses.
 332        It should return data that will be encoded and sent to server.
 333        It should return None if the client abort response '*' should
 334        be sent instead.
 335        """
 336        mech = mechanism.upper()
 337        # XXX: shouldn't this code be removed, not commented out?
 338        #cap = 'AUTH=%s' % mech
 339        #if not cap in self.capabilities:       # Let the server decide!
 340        #    raise self.error("Server doesn't allow %s authentication." % mech)
 341        self.literal = _Authenticator(authobject).process
 342        typ, dat = self._simple_command('AUTHENTICATE', mech)
 343        if typ != 'OK':
 344            raise self.error(dat[-1])
 345        self.state = 'AUTH'
 346        return typ, dat
 347
 348
 349    def capability(self):
 350        """(typ, [data]) = <instance>.capability()
 351        Fetch capabilities list from server."""
 352
 353        name = 'CAPABILITY'
 354        typ, dat = self._simple_command(name)
 355        return self._untagged_response(typ, dat, name)
 356
 357
 358    def check(self):
 359        """Checkpoint mailbox on server.
 360
 361        (typ, [data]) = <instance>.check()
 362        """
 363        return self._simple_command('CHECK')
 364
 365
 366    def close(self):
 367        """Close currently selected mailbox.
 368
 369        Deleted messages are removed from writable mailbox.
 370        This is the recommended command before 'LOGOUT'.
 371
 372        (typ, [data]) = <instance>.close()
 373        """
 374        try:
 375            typ, dat = self._simple_command('CLOSE')
 376        finally:
 377            self.state = 'AUTH'
 378        return typ, dat
 379
 380
 381    def copy(self, message_set, new_mailbox):
 382        """Copy 'message_set' messages onto end of 'new_mailbox'.
 383
 384        (typ, [data]) = <instance>.copy(message_set, new_mailbox)
 385        """
 386        return self._simple_command('COPY', message_set, new_mailbox)
 387
 388
 389    def create(self, mailbox):
 390        """Create new mailbox.
 391
 392        (typ, [data]) = <instance>.create(mailbox)
 393        """
 394        return self._simple_command('CREATE', mailbox)
 395
 396
 397    def delete(self, mailbox):
 398        """Delete old mailbox.
 399
 400        (typ, [data]) = <instance>.delete(mailbox)
 401        """
 402        return self._simple_command('DELETE', mailbox)
 403
 404    def deleteacl(self, mailbox, who):
 405        """Delete the ACLs (remove any rights) set for who on mailbox.
 406
 407        (typ, [data]) = <instance>.deleteacl(mailbox, who)
 408        """
 409        return self._simple_command('DELETEACL', mailbox, who)
 410
 411    def expunge(self):
 412        """Permanently remove deleted items from selected mailbox.
 413
 414        Generates 'EXPUNGE' response for each deleted message.
 415
 416        (typ, [data]) = <instance>.expunge()
 417
 418        'data' is list of 'EXPUNGE'd message numbers in order received.
 419        """
 420        name = 'EXPUNGE'
 421        typ, dat = self._simple_command(name)
 422        return self._untagged_response(typ, dat, name)
 423
 424
 425    def fetch(self, message_set, message_parts):
 426        """Fetch (parts of) messages.
 427
 428        (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
 429
 430        'message_parts' should be a string of selected parts
 431        enclosed in parentheses, eg: "(UID BODY[TEXT])".
 432
 433        'data' are tuples of message part envelope and data.
 434        """
 435        name = 'FETCH'
 436        typ, dat = self._simple_command(name, message_set, message_parts)
 437        return self._untagged_response(typ, dat, name)
 438
 439
 440    def getacl(self, mailbox):
 441        """Get the ACLs for a mailbox.
 442
 443        (typ, [data]) = <instance>.getacl(mailbox)
 444        """
 445        typ, dat = self._simple_command('GETACL', mailbox)
 446        return self._untagged_response(typ, dat, 'ACL')
 447
 448
 449    def getannotation(self, mailbox, entry, attribute):
 450        """(typ, [data]) = <instance>.getannotation(mailbox, entry, attribute)
 451        Retrieve ANNOTATIONs."""
 452
 453        typ, dat = self._simple_command('GETANNOTATION', mailbox, entry, attribute)
 454        return self._untagged_response(typ, dat, 'ANNOTATION')
 455
 456
 457    def getquota(self, root):
 458        """Get the quota root's resource usage and limits.
 459
 460        Part of the IMAP4 QUOTA extension defined in rfc2087.
 461
 462        (typ, [data]) = <instance>.getquota(root)
 463        """
 464        typ, dat = self._simple_command('GETQUOTA', root)
 465        return self._untagged_response(typ, dat, 'QUOTA')
 466
 467
 468    def getquotaroot(self, mailbox):
 469        """Get the list of quota roots for the named mailbox.
 470
 471        (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)
 472        """
 473        typ, dat = self._simple_command('GETQUOTAROOT', mailbox)
 474        typ, quota = self._untagged_response(typ, dat, 'QUOTA')
 475        typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT')
 476        return typ, [quotaroot, quota]
 477
 478
 479    def list(self, directory='""', pattern='*'):
 480        """List mailbox names in directory matching pattern.
 481
 482        (typ, [data]) = <instance>.list(directory='""', pattern='*')
 483
 484        'data' is list of LIST responses.
 485        """
 486        name = 'LIST'
 487        typ, dat = self._simple_command(name, directory, pattern)
 488        return self._untagged_response(typ, dat, name)
 489
 490
 491    def login(self, user, password):
 492        """Identify client using plaintext password.
 493
 494        (typ, [data]) = <instance>.login(user, password)
 495
 496        NB: 'password' will be quoted.
 497        """
 498        typ, dat = self._simple_command('LOGIN', user, self._quote(password))
 499        if typ != 'OK':
 500            raise self.error(dat[-1])
 501        self.state = 'AUTH'
 502        return typ, dat
 503
 504
 505    def login_cram_md5(self, user, password):
 506        """ Force use of CRAM-MD5 authentication.
 507
 508        (typ, [data]) = <instance>.login_cram_md5(user, password)
 509        """
 510        self.user, self.password = user, password
 511        return self.authenticate('CRAM-MD5', self._CRAM_MD5_AUTH)
 512
 513
 514    def _CRAM_MD5_AUTH(self, challenge):
 515        """ Authobject to use with CRAM-MD5 authentication. """
 516        import hmac
 517        return self.user + " " + hmac.HMAC(self.password, challenge).hexdigest()
 518
 519
 520    def logout(self):
 521        """Shutdown connection to server.
 522
 523        (typ, [data]) = <instance>.logout()
 524
 525        Returns server 'BYE' response.
 526        """
 527        self.state = 'LOGOUT'
 528        try: typ, dat = self._simple_command('LOGOUT')
 529        except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
 530        self.shutdown()
 531        if 'BYE' in self.untagged_responses:
 532            return 'BYE', self.untagged_responses['BYE']
 533        return typ, dat
 534
 535
 536    def lsub(self, directory='""', pattern='*'):
 537        """List 'subscribed' mailbox names in directory matching pattern.
 538
 539        (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
 540
 541        'data' are tuples of message part envelope and data.
 542        """
 543        name = 'LSUB'
 544        typ, dat = self._simple_command(name, directory, pattern)
 545        return self._untagged_response(typ, dat, name)
 546
 547    def myrights(self, mailbox):
 548        """Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).
 549
 550        (typ, [data]) = <instance>.myrights(mailbox)
 551        """
 552        typ,dat = self._simple_command('MYRIGHTS', mailbox)
 553        return self._untagged_response(typ, dat, 'MYRIGHTS')
 554
 555    def namespace(self):
 556        """ Returns IMAP namespaces ala rfc2342
 557
 558        (typ, [data, ...]) = <instance>.namespace()
 559        """
 560        name = 'NAMESPACE'
 561        typ, dat = self._simple_command(name)
 562        return self._untagged_response(typ, dat, name)
 563
 564
 565    def noop(self):
 566        """Send NOOP command.
 567
 568        (typ, [data]) = <instance>.noop()
 569        """
 570        if __debug__:
 571            if self.debug >= 3:
 572                self._dump_ur(self.untagged_responses)
 573        return self._simple_command('NOOP')
 574
 575
 576    def partial(self, message_num, message_part, start, length):
 577        """Fetch truncated part of a message.
 578
 579        (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
 580
 581        'data' is tuple of message part envelope and data.
 582        """
 583        name = 'PARTIAL'
 584        typ, dat = self._simple_command(name, message_num, message_part, start, length)
 585        return self._untagged_response(typ, dat, 'FETCH')
 586
 587
 588    def proxyauth(self, user):
 589        """Assume authentication as "user".
 590
 591        Allows an authorised administrator to proxy into any user's
 592        mailbox.
 593
 594        (typ, [data]) = <instance>.proxyauth(user)
 595        """
 596
 597        name = 'PROXYAUTH'
 598        return self._simple_command('PROXYAUTH', user)
 599
 600
 601    def rename(self, oldmailbox, newmailbox):
 602        """Rename old mailbox name to new.
 603
 604        (typ, [data]) = <instance>.rename(oldmailbox, newmailbox)
 605        """
 606        return self._simple_command('RENAME', oldmailbox, newmailbox)
 607
 608
 609    def search(self, charset, *criteria):
 610        """Search mailbox for matching messages.
 611
 612        (typ, [data]) = <instance>.search(charset, criterion, ...)
 613
 614        'data' is space separated list of matching message numbers.
 615        """
 616        name = 'SEARCH'
 617        if charset:
 618            typ, dat = self._simple_command(name, 'CHARSET', charset, *criteria)
 619        else:
 620            typ, dat = self._simple_command(name, *criteria)
 621        return self._untagged_response(typ, dat, name)
 622
 623
 624    def select(self, mailbox='INBOX', readonly=False):
 625        """Select a mailbox.
 626
 627        Flush all untagged responses.
 628
 629        (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=False)
 630
 631        'data' is count of messages in mailbox ('EXISTS' response).
 632
 633        Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
 634        other responses should be obtained via <instance>.response('FLAGS') etc.
 635        """
 636        self.untagged_responses = {}    # Flush old responses.
 637        self.is_readonly = readonly
 638        if readonly:
 639            name = 'EXAMINE'
 640        else:
 641            name = 'SELECT'
 642        typ, dat = self._simple_command(name, mailbox)
 643        if typ != 'OK':
 644            self.state = 'AUTH'     # Might have been 'SELECTED'
 645            return typ, dat
 646        self.state = 'SELECTED'
 647        if 'READ-ONLY' in self.untagged_responses \
 648                and not readonly:
 649            if __debug__:
 650                if self.debug >= 1:
 651                    self._dump_ur(self.untagged_responses)
 652            raise self.readonly('%s is not writable' % mailbox)
 653        return typ, self.untagged_responses.get('EXISTS', [None])
 654
 655
 656    def setacl(self, mailbox, who, what):
 657        """Set a mailbox acl.
 658
 659        (typ, [data]) = <instance>.setacl(mailbox, who, what)
 660        """
 661        return self._simple_command('SETACL', mailbox, who, what)
 662
 663
 664    def setannotation(self, *args):
 665        """(typ, [data]) = <instance>.setannotation(mailbox[, entry, attribute]+)
 666        Set ANNOTATIONs."""
 667
 668        typ, dat = self._simple_command('SETANNOTATION', *args)
 669        return self._untagged_response(typ, dat, 'ANNOTATION')
 670
 671
 672    def setquota(self, root, limits):
 673        """Set the quota root's resource limits.
 674
 675        (typ, [data]) = <instance>.setquota(root, limits)
 676        """
 677        typ, dat = self._simple_command('SETQUOTA', root, limits)
 678        return self._untagged_response(typ, dat, 'QUOTA')
 679
 680
 681    def sort(self, sort_criteria, charset, *search_criteria):
 682        """IMAP4rev1 extension SORT command.
 683
 684        (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
 685        """
 686        name = 'SORT'
 687        #if not name in self.capabilities:      # Let the server decide!
 688        #       raise self.error('unimplemented extension command: %s' % name)
 689        if (sort_criteria[0],sort_criteria[-1]) != ('(',')'):
 690            sort_criteria = '(%s)' % sort_criteria
 691        typ, dat = self._simple_command(name, sort_criteria, charset, *search_criteria)
 692        return self._untagged_response(typ, dat, name)
 693
 694
 695    def status(self, mailbox, names):
 696        """Request named status conditions for mailbox.
 697
 698        (typ, [data]) = <instance>.status(mailbox, names)
 699        """
 700        name = 'STATUS'
 701        #if self.PROTOCOL_VERSION == 'IMAP4':   # Let the server decide!
 702        #    raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
 703        typ, dat = self._simple_command(name, mailbox, names)
 704        return self._untagged_response(typ, dat, name)
 705
 706
 707    def store(self, message_set, command, flags):
 708        """Alters flag dispositions for messages in mailbox.
 709
 710        (typ, [data]) = <instance>.store(message_set, command, flags)
 711        """
 712        if (flags[0],flags[-1]) != ('(',')'):
 713            flags = '(%s)' % flags  # Avoid quoting the flags
 714        typ, dat = self._simple_command('STORE', message_set, command, flags)
 715        return self._untagged_response(typ, dat, 'FETCH')
 716
 717
 718    def subscribe(self, mailbox):
 719        """Subscribe to new mailbox.
 720
 721        (typ, [data]) = <instance>.subscribe(mailbox)
 722        """
 723        return self._simple_command('SUBSCRIBE', mailbox)
 724
 725
 726    def thread(self, threading_algorithm, charset, *search_criteria):
 727        """IMAPrev1 extension THREAD command.
 728
 729        (type, [data]) = <instance>.thread(threading_alogrithm, charset, search_criteria, ...)
 730        """
 731        name = 'THREAD'
 732        typ, dat = self._simple_command(name, threading_algorithm, charset, *search_criteria)
 733        return self._untagged_response(typ, dat, name)
 734
 735
 736    def uid(self, command, *args):
 737        """Execute "command arg ..." with messages identified by UID,
 738                rather than message number.
 739
 740        (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
 741
 742        Returns response appropriate to 'command'.
 743        """
 744        command = command.upper()
 745        if not command in Commands:
 746            raise self.error("Unknown IMAP4 UID command: %s" % command)
 747        if self.state not in Commands[command]:
 748            raise self.error("command %s illegal in state %s, "
 749                             "only allowed in states %s" %
 750                             (command, self.state,
 751                              ', '.join(Commands[command])))
 752        name = 'UID'
 753        typ, dat = self._simple_command(name, command, *args)
 754        if command in ('SEARCH', 'SORT'):
 755            name = command
 756        else:
 757            name = 'FETCH'
 758        return self._untagged_response(typ, dat, name)
 759
 760
 761    def unsubscribe(self, mailbox):
 762        """Unsubscribe from old mailbox.
 763
 764        (typ, [data]) = <instance>.unsubscribe(mailbox)
 765        """
 766        return self._simple_command('UNSUBSCRIBE', mailbox)
 767
 768
 769    def xatom(self, name, *args):
 770        """Allow simple extension commands
 771                notified by server in CAPABILITY response.
 772
 773        Assumes command is legal in current state.
 774
 775        (typ, [data]) = <instance>.xatom(name, arg, ...)
 776
 777        Returns response appropriate to extension command `name'.
 778        """
 779        name = name.upper()
 780        #if not name in self.capabilities:      # Let the server decide!
 781        #    raise self.error('unknown extension command: %s' % name)
 782        if not name in Commands:
 783            Commands[name] = (self.state,)
 784        return self._simple_command(name, *args)
 785
 786
 787
 788    #       Private methods
 789
 790
 791    def _append_untagged(self, typ, dat):
 792
 793        if dat is None: dat = ''
 794        ur = self.untagged_responses
 795        if __debug__:
 796            if self.debug >= 5:
 797                self._mesg('untagged_responses[%s] %s += ["%s"]' %
 798                        (typ, len(ur.get(typ,'')), dat))
 799        if typ in ur:
 800            ur[typ].append(dat)
 801        else:
 802            ur[typ] = [dat]
 803
 804
 805    def _check_bye(self):
 806        bye = self.untagged_responses.get('BYE')
 807        if bye:
 808            raise self.abort(bye[-1])
 809
 810
 811    def _command(self, name, *args):
 812
 813        if self.state not in Commands[name]:
 814            self.literal = None
 815            raise self.error("command %s illegal in state %s, "
 816                             "only allowed in states %s" %
 817                             (name, self.state,
 818                              ', '.join(Commands[name])))
 819
 820        for typ in ('OK', 'NO', 'BAD'):
 821            if typ in self.untagged_responses:
 822                del self.untagged_responses[typ]
 823
 824        if 'READ-ONLY' in self.untagged_responses \
 825        and not self.is_readonly:
 826            raise self.readonly('mailbox status changed to READ-ONLY')
 827
 828        tag = self._new_tag()
 829        data = '%s %s' % (tag, name)
 830        for arg in args:
 831            if arg is None: continue
 832            data = '%s %s' % (data, self._checkquote(arg))
 833
 834        literal = self.literal
 835        if literal is not None:
 836            self.literal = None
 837            if type(literal) is type(self._command):
 838                literator = literal
 839            else:
 840                literator = None
 841                data = '%s {%s}' % (data, len(literal))
 842
 843        if __debug__:
 844            if self.debug >= 4:
 845                self._mesg('> %s' % data)
 846            else:
 847                self._log('> %s' % data)
 848
 849        try:
 850            self.send('%s%s' % (data, CRLF))
 851        except (socket.error, OSError), val:
 852            raise self.abort('socket error: %s' % val)
 853
 854        if literal is None:
 855            return tag
 856
 857        while 1:
 858            # Wait for continuation response
 859
 860            while self._get_response():
 861                if self.tagged_commands[tag]:   # BAD/NO?
 862                    return tag
 863
 864            # Send literal
 865
 866            if literator:
 867                literal = literator(self.continuation_response)
 868
 869            if __debug__:
 870                if self.debug >= 4:
 871                    self._mesg('write literal size %s' % len(literal))
 872
 873            try:
 874                self.send(literal)
 875                self.send(CRLF)
 876            except (socket.error, OSError), val:
 877                raise self.abort('socket error: %s' % val)
 878
 879            if not literator:
 880                break
 881
 882        return tag
 883
 884
 885    def _command_complete(self, name, tag):
 886        self._check_bye()
 887        try:
 888            typ, data = self._get_tagged_response(tag)
 889        except self.abort, val:
 890            raise self.abort('command: %s => %s' % (name, val))
 891        except self.error, val:
 892            raise self.error('command: %s => %s' % (name, val))
 893        self._check_bye()
 894        if typ == 'BAD':
 895            raise self.error('%s command error: %s %s' % (name, typ, data))
 896        return typ, data
 897
 898
 899    def _get_response(self):
 900
 901        # Read response and store.
 902        #
 903        # Returns None for continuation responses,
 904        # otherwise first response line received.
 905
 906        resp = self._get_line()
 907
 908        # Command completion response?
 909
 910        if self._match(self.tagre, resp):
 911            tag = self.mo.group('tag')
 912            if not tag in self.tagged_commands:
 913                raise self.abort('unexpected tagged response: %s' % resp)
 914
 915            typ = self.mo.group('type')
 916            dat = self.mo.group('data')
 917            self.tagged_commands[tag] = (typ, [dat])
 918        else:
 919            dat2 = None
 920
 921            # '*' (untagged) responses?
 922
 923            if not self._match(Untagged_response, resp):
 924                if self._match(Untagged_status, resp):
 925                    dat2 = self.mo.group('data2')
 926
 927            if self.mo is None:
 928                # Only other possibility is '+' (continuation) response...
 929
 930                if self._match(Continuation, resp):
 931                    self.continuation_response = self.mo.group('data')
 932                    return None     # NB: indicates continuation
 933
 934                raise self.abort("unexpected response: '%s'" % resp)
 935
 936            typ = self.mo.group('type')
 937            dat = self.mo.group('data')
 938            if dat is None: dat = ''        # Null untagged response
 939            if dat2: dat = dat + ' ' + dat2
 940
 941            # Is there a literal to come?
 942
 943            while self._match(Literal, dat):
 944
 945                # Read literal direct from connection.
 946
 947                size = int(self.mo.group('size'))
 948                if __debug__:
 949                    if self.debug >= 4:
 950                        self._mesg('read literal size %s' % size)
 951                data = self.read(size)
 952
 953                # Store response with literal as tuple
 954
 955                self._append_untagged(typ, (dat, data))
 956
 957                # Read trailer - possibly containing another literal
 958
 959                dat = self._get_line()
 960
 961            self._append_untagged(typ, dat)
 962
 963        # Bracketed response information?
 964
 965        if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
 966            self._append_untagged(self.mo.group('type'), self.mo.group('data'))
 967
 968        if __debug__:
 969            if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
 970                self._mesg('%s response: %s' % (typ, dat))
 971
 972        return resp
 973
 974
 975    def _get_tagged_response(self, tag):
 976
 977        while 1:
 978            result = self.tagged_commands[tag]
 979            if result is not None:
 980                del self.tagged_commands[tag]
 981                return result
 982
 983            # Some have reported "unexpected response" exceptions.
 984            # Note that ignoring them here causes loops.
 985            # Instead, send me details of the unexpected response and
 986            # I'll update the code in `_get_response()'.
 987
 988            try:
 989                self._get_response()
 990            except self.abort, val:
 991                if __debug__:
 992                    if self.debug >= 1:
 993                        self.print_log()
 994                raise
 995
 996
 997    def _get_line(self):
 998
 999        line = self.readline()
1000        if not line:
1001            raise self.abort('socket error: EOF')
1002
1003        # Protocol mandates all lines terminated by CRLF
1004
1005        line = line[:-2]
1006        if __debug__:
1007            if self.debug >= 4:
1008                self._mesg('< %s' % line)
1009            else:
1010                self._log('< %s' % line)
1011        return line
1012
1013
1014    def _match(self, cre, s):
1015
1016        # Run compiled regular expression match method on 's'.
1017        # Save result, return success.
1018
1019        self.mo = cre.match(s)
1020        if __debug__:
1021            if self.mo is not None and self.debug >= 5:
1022                self._mesg("\tmatched r'%s' => %r" % (cre.pattern, self.mo.groups()))
1023        return self.mo is not None
1024
1025
1026    def _new_tag(self):
1027
1028        tag = '%s%s' % (self.tagpre, self.tagnum)
1029        self.tagnum = self.tagnum + 1
1030        self.tagged_commands[tag] = None
1031        return tag
1032
1033
1034    def _checkquote(self, arg):
1035
1036        # Must quote command args if non-alphanumeric chars present,
1037        # and not already quoted.
1038
1039        if type(arg) is not type(''):
1040            return arg
1041        if len(arg) >= 2 and (arg[0],arg[-1]) in (('(',')'),('"','"')):
1042            return arg
1043        if arg and self.mustquote.search(arg) is None:
1044            return arg
1045        return self._quote(arg)
1046
1047
1048    def _quote(self, arg):
1049
1050        arg = arg.replace('\\', '\\\\')
1051        arg = arg.replace('"', '\\"')
1052
1053        return '"%s"' % arg
1054
1055
1056    def _simple_command(self, name, *args):
1057
1058        return self._command_complete(name, self._command(name, *args))
1059
1060
1061    def _untagged_response(self, typ, dat, name):
1062
1063        if typ == 'NO':
1064            return typ, dat
1065        if not name in self.untagged_responses:
1066            return typ, [None]
1067        data = self.untagged_responses.pop(name)
1068        if __debug__:
1069            if self.debug >= 5:
1070                self._mesg('untagged_responses[%s] => %s' % (name, data))
1071        return typ, data
1072
1073
1074    if __debug__:
1075
1076        def _mesg(self, s, secs=None):
1077            if secs is None:
1078                secs = time.time()
1079            tm = time.strftime('%M:%S', time.localtime(secs))
1080            sys.stderr.write('  %s.%02d %s\n' % (tm, (secs*100)%100, s))
1081            sys.stderr.flush()
1082
1083        def _dump_ur(self, dict):
1084            # Dump untagged responses (in `dict').
1085            l = dict.items()
1086            if not l: return
1087            t = '\n\t\t'
1088            l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l)
1089            self._mesg('untagged responses dump:%s%s' % (t, t.join(l)))
1090
1091        def _log(self, line):
1092            # Keep log of last `_cmd_log_len' interactions for debugging.
1093            self._cmd_log[self._cmd_log_idx] = (line, time.time())
1094            self._cmd_log_idx += 1
1095            if self._cmd_log_idx >= self._cmd_log_len:
1096                self._cmd_log_idx = 0
1097
1098        def print_log(self):
1099            self._mesg('last %d IMAP4 interactions:' % len(self._cmd_log))
1100            i, n = self._cmd_log_idx, self._cmd_log_len
1101            while n:
1102                try:
1103                    self._mesg(*self._cmd_log[i])
1104                except:
1105                    pass
1106                i += 1
1107                if i >= self._cmd_log_len:
1108                    i = 0
1109                n -= 1
1110
1111
1112
1113try:
1114    import ssl
1115except ImportError:
1116    pass
1117else:
1118    class IMAP4_SSL(IMAP4):
1119
1120        """IMAP4 client class over SSL connection
1121
1122        Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile]]]])
1123
1124                host - host's name (default: localhost);
1125                port - port number (default: standard IMAP4 SSL port).
1126                keyfile - PEM formatted file that contains your private key (default: None);
1127                certfile - PEM formatted certificate chain file (default: None);
1128
1129        for more documentation see the docstring of the parent class IMAP4.
1130        """
1131
1132
1133        def __init__(self, host = '', port = IMAP4_SSL_PORT, keyfile = None, certfile = None):
1134            self.keyfile = keyfile
1135            self.certfile = certfile
1136            IMAP4.__init__(self, host, port)
1137
1138
1139        def open(self, host = '', port = IMAP4_SSL_PORT):
1140            """Setup connection to remote server on "host:port".
1141                (default: localhost:standard IMAP4 SSL port).
1142            This connection will be used by the routines:
1143                read, readline, send, shutdown.
1144            """
1145            self.host = host
1146            self.port = port
1147            self.sock = socket.create_connection((host, port))
1148            self.sslobj = ssl.wrap_socket(self.sock, self.keyfile, self.certfile)
1149
1150
1151        def read(self, size):
1152            """Read 'size' bytes from remote."""
1153            # sslobj.read() sometimes returns < size bytes
1154            chunks = []
1155            read = 0
1156            while read < size:
1157                data = self.sslobj.read(min(size-read, 16384))
1158                read += len(data)
1159                chunks.append(data)
1160
1161            return ''.join(chunks)
1162
1163
1164        def readline(self):
1165            """Read line from remote."""
1166            line = []
1167            while 1:
1168                char = self.sslobj.read(1)
1169                line.append(char)
1170                if char == "\n": return ''.join(line)
1171
1172
1173        def send(self, data):
1174            """Send data to remote."""
1175            bytes = len(data)
1176            while bytes > 0:
1177                sent = self.sslobj.write(data)
1178                if sent == bytes:
1179                    break    # avoid copy
1180                data = data[sent:]
1181                bytes = bytes - sent
1182
1183
1184        def shutdown(self):
1185            """Close I/O established in "open"."""
1186            self.sock.close()
1187
1188
1189        def socket(self):
1190            """Return socket instance used to connect to IMAP4 server.
1191
1192            socket = <instance>.socket()
1193            """
1194            return self.sock
1195
1196
1197        def ssl(self):
1198            """Return SSLObject instance used to communicate with the IMAP4 server.
1199
1200            ssl = ssl.wrap_socket(<instance>.socket)
1201            """
1202            return self.sslobj
1203
1204    __all__.append("IMAP4_SSL")
1205
1206
1207class IMAP4_stream(IMAP4):
1208
1209    """IMAP4 client class over a stream
1210
1211    Instantiate with: IMAP4_stream(command)
1212
1213            where "command" is a string that can be passed to os.popen2()
1214
1215    for more documentation see the docstring of the parent class IMAP4.
1216    """
1217
1218
1219    def __init__(self, command):
1220        self.command = command
1221        IMAP4.__init__(self)
1222
1223
1224    def open(self, host = None, port = None):
1225        """Setup a stream connection.
1226        This connection will be used by the routines:
1227            read, readline, send, shutdown.
1228        """
1229        self.host = None        # For compatibility with parent class
1230        self.port = None
1231        self.sock = None
1232        self.file = None
1233        self.writefile, self.readfile = os.popen2(self.command)
1234
1235
1236    def read(self, size):
1237        """Read 'size' bytes from remote."""
1238        return self.readfile.read(size)
1239
1240
1241    def readline(self):
1242        """Read line from remote."""
1243        return self.readfile.readline()
1244
1245
1246    def send(self, data):
1247        """Send data to remote."""
1248        self.writefile.write(data)
1249        self.writefile.flush()
1250
1251
1252    def shutdown(self):
1253        """Close I/O established in "open"."""
1254        self.readfile.close()
1255        self.writefile.close()
1256
1257
1258
1259class _Authenticator:
1260
1261    """Private class to provide en/decoding
1262            for base64-based authentication conversation.
1263    """
1264
1265    def __init__(self, mechinst):
1266        self.mech = mechinst    # Callable object to provide/process data
1267
1268    def process(self, data):
1269        ret = self.mech(self.decode(data))
1270        if ret is None:
1271            return '*'      # Abort conversation
1272        return self.encode(ret)
1273
1274    def encode(self, inp):
1275        #
1276        #  Invoke binascii.b2a_base64 iteratively with
1277        #  short even length buffers, strip the trailing
1278        #  line feed from the result and append.  "Even"
1279        #  means a number that factors to both 6 and 8,
1280        #  so when it gets to the end of the 8-bit input
1281        #  there's no partial 6-bit output.
1282        #
1283        oup = ''
1284        while inp:
1285            if len(inp) > 48:
1286                t = inp[:48]
1287                inp = inp[48:]
1288            else:
1289                t = inp
1290                inp = ''
1291            e = binascii.b2a_base64(t)
1292            if e:
1293                oup = oup + e[:-1]
1294        return oup
1295
1296    def decode(self, inp):
1297        if not inp:
1298            return ''
1299        return binascii.a2b_base64(inp)
1300
1301
1302
1303Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
1304        'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
1305
1306def Internaldate2tuple(resp):
1307    """Convert IMAP4 INTERNALDATE to UT.
1308
1309    Returns Python time module tuple.
1310    """
1311
1312    mo = InternalDate.match(resp)
1313    if not mo:
1314        return None
1315
1316    mon = Mon2num[mo.group('mon')]
1317    zonen = mo.group('zonen')
1318
1319    day = int(mo.group('day'))
1320    year = int(mo.group('year'))
1321    hour = int(mo.group('hour'))
1322    min = int(mo.group('min'))
1323    sec = int(mo.group('sec'))
1324    zoneh = int(mo.group('zoneh'))
1325    zonem = int(mo.group('zonem'))
1326
1327    # INTERNALDATE timezone must be subtracted to get UT
1328
1329    zone = (zoneh*60 + zonem)*60
1330    if zonen == '-':
1331        zone = -zone
1332
1333    tt = (year, mon, day, hour, min, sec, -1, -1, -1)
1334
1335    utc = time.mktime(tt)
1336
1337    # Following is necessary because the time module has no 'mkgmtime'.
1338    # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1339
1340    lt = time.localtime(utc)
1341    if time.daylight and lt[-1]:
1342        zone = zone + time.altzone
1343    else:
1344        zone = zone + time.timezone
1345
1346    return time.localtime(utc - zone)
1347
1348
1349
1350def Int2AP(num):
1351
1352    """Convert integer to A-P string representation."""
1353
1354    val = ''; AP = 'ABCDEFGHIJKLMNOP'
1355    num = int(abs(num))
1356    while num:
1357        num, mod = divmod(num, 16)
1358        val = AP[mod] + val
1359    return val
1360
1361
1362
1363def ParseFlags(resp):
1364
1365    """Convert IMAP4 flags response to python tuple."""
1366
1367    mo = Flags.match(resp)
1368    if not mo:
1369        return ()
1370
1371    return tuple(mo.group('flags').split())
1372
1373
1374def Time2Internaldate(date_time):
1375
1376    """Convert 'date_time' to IMAP4 INTERNALDATE representation.
1377
1378    Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
1379    """
1380
1381    if isinstance(date_time, (int, float)):
1382        tt = time.localtime(date_time)
1383    elif isinstance(date_time, (tuple, time.struct_time)):
1384        tt = date_time
1385    elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'):
1386        return date_time        # Assume in correct format
1387    else:
1388        raise ValueError("date_time not of a known type")
1389
1390    dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
1391    if dt[0] == '0':
1392        dt = ' ' + dt[1:]
1393    if time.daylight and tt[-1]:
1394        zone = -time.altzone
1395    else:
1396        zone = -time.timezone
1397    return '"' + dt + " %+03d%02d" % divmod(zone//60, 60) + '"'
1398
1399
1400
1401if __name__ == '__main__':
1402
1403    # To test: invoke either as 'python imaplib.py [IMAP4_server_hostname]'
1404    # or 'python imaplib.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"'
1405    # to test the IMAP4_stream class
1406
1407    import getopt, getpass
1408
1409    try:
1410        optlist, args = getopt.getopt(sys.argv[1:], 'd:s:')
1411    except getopt.error, val:
1412        optlist, args = (), ()
1413
1414    stream_command = None
1415    for opt,val in optlist:
1416        if opt == '-d':
1417            Debug = int(val)
1418        elif opt == '-s':
1419            stream_command = val
1420            if not args: args = (stream_command,)
1421
1422    if not args: args = ('',)
1423
1424    host = args[0]
1425
1426    USER = getpass.getuser()
1427    PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost"))
1428
1429    test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER, 'lf':'\n'}
1430    test_seq1 = (
1431    ('login', (USER, PASSWD)),
1432    ('create', ('/tmp/xxx 1',)),
1433    ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1434    ('CREATE', ('/tmp/yyz 2',)),
1435    ('append', ('/tmp/yyz 2', None, None, test_mesg)),
1436    ('list', ('/tmp', 'yy*')),
1437    ('select', ('/tmp/yyz 2',)),
1438    ('search', (None, 'SUBJECT', 'test')),
1439    ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
1440    ('store', ('1', 'FLAGS', '(\Deleted)')),
1441    ('namespace', ()),
1442    ('expunge', ()),
1443    ('recent', ()),
1444    ('close', ()),
1445    )
1446
1447    test_seq2 = (
1448    ('select', ()),
1449    ('response',('UIDVALIDITY',)),
1450    ('uid', ('SEARCH', 'ALL')),
1451    ('response', ('EXISTS',)),
1452    ('append', (None, None, None, test_mesg)),
1453    ('recent', ()),
1454    ('logout', ()),
1455    )
1456
1457    def run(cmd, args):
1458        M._mesg('%s %s' % (cmd, args))
1459        typ, dat = getattr(M, cmd)(*args)
1460        M._mesg('%s => %s %s' % (cmd, typ, dat))
1461        if typ == 'NO': raise dat[0]
1462        return dat
1463
1464    try:
1465        if stream_command:
1466            M = IMAP4_stream(stream_command)
1467        else:
1468            M = IMAP4(host)
1469        if M.state == 'AUTH':
1470            test_seq1 = test_seq1[1:]   # Login not needed
1471        M._mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
1472        M._mesg('CAPABILITIES = %r' % (M.capabilities,))
1473
1474        for cmd,args in test_seq1:
1475            run(cmd, args)
1476
1477        for ml in run('list', ('/tmp/', 'yy%')):
1478            mo = re.match(r'.*"([^"]+)"$', ml)
1479            if mo: path = mo.group(1)
1480            else: path = ml.split()[-1]
1481            run('delete', (path,))
1482
1483        for cmd,args in test_seq2:
1484            dat = run(cmd, args)
1485
1486            if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1487                continue
1488
1489            uid = dat[-1].split()
1490            if not uid: continue
1491            run('uid', ('FETCH', '%s' % uid[-1],
1492                    '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1493
1494        print '\nAll tests OK.'
1495
1496    except:
1497        print '\nTests failed.'
1498
1499        if not Debug:
1500            print '''
1501If you would like to see debugging output,
1502try: %s -d5
1503''' % sys.argv[0]
1504
1505        raise