imaplib

Module imaplib |
[hide private]
[frames] | no frames]

# Source Code for Module imaplib

       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(min(size-read, 16384))
    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
    1500