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