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