PageRenderTime 399ms CodeModel.GetById 221ms app.highlight 135ms RepoModel.GetById 25ms app.codeStats 1ms

/lib-python/2.7/imaplib.py

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