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 #       Commands

     39 
     40 Commands = {
     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 
     84 Continuation = re.compile(r'\+( (?P<data>.*))?')
     85 Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
     86 InternalDate = re.compile(r'.*INTERNALDATE "'
     87         r'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
     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'"')
     91 Literal = re.compile(r'.*{(?P<size>\d+)}$')
     92 MapCRLF = re.compile(r'\r\n|\r|\n')
     93 Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
     94 Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
     95 Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
     96 
     97 
     98 
     99 class 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 
   1125 try:
   1126     import ssl
   1127 except ImportError:
   1128     pass
   1129 else:
   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 
   1209 class 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 
   1266 class _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 
   1310 Mon2num = {'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 
   1313 def 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 
   1358 def 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 
   1371 def 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 
   1382 def 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 
   1414 if __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 '''
   1514 If you would like to see debugging output,
   1515 try: %s -d5
   1516 ''' % sys.argv[0]
   1517 
   1518         raise
   1519