~malept/ubuntu/lucid/python2.6/dev-dependency-fix

« back to all changes in this revision

Viewing changes to Lib/imaplib.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2009-02-13 12:51:00 UTC
  • Revision ID: james.westby@ubuntu.com-20090213125100-uufgcb9yeqzujpqw
Tags: upstream-2.6.1
ImportĀ upstreamĀ versionĀ 2.6.1

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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_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
                             "only allowed in states %s" %
 
751
                             (command, self.state,
 
752
                              ', '.join(Commands[command])))
 
753
        name = 'UID'
 
754
        typ, dat = self._simple_command(name, command, *args)
 
755
        if command in ('SEARCH', 'SORT'):
 
756
            name = command
 
757
        else:
 
758
            name = 'FETCH'
 
759
        return self._untagged_response(typ, dat, name)
 
760
 
 
761
 
 
762
    def unsubscribe(self, mailbox):
 
763
        """Unsubscribe from old mailbox.
 
764
 
 
765
        (typ, [data]) = <instance>.unsubscribe(mailbox)
 
766
        """
 
767
        return self._simple_command('UNSUBSCRIBE', mailbox)
 
768
 
 
769
 
 
770
    def xatom(self, name, *args):
 
771
        """Allow simple extension commands
 
772
                notified by server in CAPABILITY response.
 
773
 
 
774
        Assumes command is legal in current state.
 
775
 
 
776
        (typ, [data]) = <instance>.xatom(name, arg, ...)
 
777
 
 
778
        Returns response appropriate to extension command `name'.
 
779
        """
 
780
        name = name.upper()
 
781
        #if not name in self.capabilities:      # Let the server decide!
 
782
        #    raise self.error('unknown extension command: %s' % name)
 
783
        if not name in Commands:
 
784
            Commands[name] = (self.state,)
 
785
        return self._simple_command(name, *args)
 
786
 
 
787
 
 
788
 
 
789
    #       Private methods
 
790
 
 
791
 
 
792
    def _append_untagged(self, typ, dat):
 
793
 
 
794
        if dat is None: dat = ''
 
795
        ur = self.untagged_responses
 
796
        if __debug__:
 
797
            if self.debug >= 5:
 
798
                self._mesg('untagged_responses[%s] %s += ["%s"]' %
 
799
                        (typ, len(ur.get(typ,'')), dat))
 
800
        if typ in ur:
 
801
            ur[typ].append(dat)
 
802
        else:
 
803
            ur[typ] = [dat]
 
804
 
 
805
 
 
806
    def _check_bye(self):
 
807
        bye = self.untagged_responses.get('BYE')
 
808
        if bye:
 
809
            raise self.abort(bye[-1])
 
810
 
 
811
 
 
812
    def _command(self, name, *args):
 
813
 
 
814
        if self.state not in Commands[name]:
 
815
            self.literal = None
 
816
            raise self.error("command %s illegal in state %s, "
 
817
                             "only allowed in states %s" %
 
818
                             (name, self.state,
 
819
                              ', '.join(Commands[name])))
 
820
 
 
821
        for typ in ('OK', 'NO', 'BAD'):
 
822
            if typ in self.untagged_responses:
 
823
                del self.untagged_responses[typ]
 
824
 
 
825
        if 'READ-ONLY' in self.untagged_responses \
 
826
        and not self.is_readonly:
 
827
            raise self.readonly('mailbox status changed to READ-ONLY')
 
828
 
 
829
        tag = self._new_tag()
 
830
        data = '%s %s' % (tag, name)
 
831
        for arg in args:
 
832
            if arg is None: continue
 
833
            data = '%s %s' % (data, self._checkquote(arg))
 
834
 
 
835
        literal = self.literal
 
836
        if literal is not None:
 
837
            self.literal = None
 
838
            if type(literal) is type(self._command):
 
839
                literator = literal
 
840
            else:
 
841
                literator = None
 
842
                data = '%s {%s}' % (data, len(literal))
 
843
 
 
844
        if __debug__:
 
845
            if self.debug >= 4:
 
846
                self._mesg('> %s' % data)
 
847
            else:
 
848
                self._log('> %s' % data)
 
849
 
 
850
        try:
 
851
            self.send('%s%s' % (data, CRLF))
 
852
        except (socket.error, OSError), val:
 
853
            raise self.abort('socket error: %s' % val)
 
854
 
 
855
        if literal is None:
 
856
            return tag
 
857
 
 
858
        while 1:
 
859
            # Wait for continuation response
 
860
 
 
861
            while self._get_response():
 
862
                if self.tagged_commands[tag]:   # BAD/NO?
 
863
                    return tag
 
864
 
 
865
            # Send literal
 
866
 
 
867
            if literator:
 
868
                literal = literator(self.continuation_response)
 
869
 
 
870
            if __debug__:
 
871
                if self.debug >= 4:
 
872
                    self._mesg('write literal size %s' % len(literal))
 
873
 
 
874
            try:
 
875
                self.send(literal)
 
876
                self.send(CRLF)
 
877
            except (socket.error, OSError), val:
 
878
                raise self.abort('socket error: %s' % val)
 
879
 
 
880
            if not literator:
 
881
                break
 
882
 
 
883
        return tag
 
884
 
 
885
 
 
886
    def _command_complete(self, name, tag):
 
887
        self._check_bye()
 
888
        try:
 
889
            typ, data = self._get_tagged_response(tag)
 
890
        except self.abort, val:
 
891
            raise self.abort('command: %s => %s' % (name, val))
 
892
        except self.error, val:
 
893
            raise self.error('command: %s => %s' % (name, val))
 
894
        self._check_bye()
 
895
        if typ == 'BAD':
 
896
            raise self.error('%s command error: %s %s' % (name, typ, data))
 
897
        return typ, data
 
898
 
 
899
 
 
900
    def _get_response(self):
 
901
 
 
902
        # Read response and store.
 
903
        #
 
904
        # Returns None for continuation responses,
 
905
        # otherwise first response line received.
 
906
 
 
907
        resp = self._get_line()
 
908
 
 
909
        # Command completion response?
 
910
 
 
911
        if self._match(self.tagre, resp):
 
912
            tag = self.mo.group('tag')
 
913
            if not tag in self.tagged_commands:
 
914
                raise self.abort('unexpected tagged response: %s' % resp)
 
915
 
 
916
            typ = self.mo.group('type')
 
917
            dat = self.mo.group('data')
 
918
            self.tagged_commands[tag] = (typ, [dat])
 
919
        else:
 
920
            dat2 = None
 
921
 
 
922
            # '*' (untagged) responses?
 
923
 
 
924
            if not self._match(Untagged_response, resp):
 
925
                if self._match(Untagged_status, resp):
 
926
                    dat2 = self.mo.group('data2')
 
927
 
 
928
            if self.mo is None:
 
929
                # Only other possibility is '+' (continuation) response...
 
930
 
 
931
                if self._match(Continuation, resp):
 
932
                    self.continuation_response = self.mo.group('data')
 
933
                    return None     # NB: indicates continuation
 
934
 
 
935
                raise self.abort("unexpected response: '%s'" % resp)
 
936
 
 
937
            typ = self.mo.group('type')
 
938
            dat = self.mo.group('data')
 
939
            if dat is None: dat = ''        # Null untagged response
 
940
            if dat2: dat = dat + ' ' + dat2
 
941
 
 
942
            # Is there a literal to come?
 
943
 
 
944
            while self._match(Literal, dat):
 
945
 
 
946
                # Read literal direct from connection.
 
947
 
 
948
                size = int(self.mo.group('size'))
 
949
                if __debug__:
 
950
                    if self.debug >= 4:
 
951
                        self._mesg('read literal size %s' % size)
 
952
                data = self.read(size)
 
953
 
 
954
                # Store response with literal as tuple
 
955
 
 
956
                self._append_untagged(typ, (dat, data))
 
957
 
 
958
                # Read trailer - possibly containing another literal
 
959
 
 
960
                dat = self._get_line()
 
961
 
 
962
            self._append_untagged(typ, dat)
 
963
 
 
964
        # Bracketed response information?
 
965
 
 
966
        if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
 
967
            self._append_untagged(self.mo.group('type'), self.mo.group('data'))
 
968
 
 
969
        if __debug__:
 
970
            if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
 
971
                self._mesg('%s response: %s' % (typ, dat))
 
972
 
 
973
        return resp
 
974
 
 
975
 
 
976
    def _get_tagged_response(self, tag):
 
977
 
 
978
        while 1:
 
979
            result = self.tagged_commands[tag]
 
980
            if result is not None:
 
981
                del self.tagged_commands[tag]
 
982
                return result
 
983
 
 
984
            # Some have reported "unexpected response" exceptions.
 
985
            # Note that ignoring them here causes loops.
 
986
            # Instead, send me details of the unexpected response and
 
987
            # I'll update the code in `_get_response()'.
 
988
 
 
989
            try:
 
990
                self._get_response()
 
991
            except self.abort, val:
 
992
                if __debug__:
 
993
                    if self.debug >= 1:
 
994
                        self.print_log()
 
995
                raise
 
996
 
 
997
 
 
998
    def _get_line(self):
 
999
 
 
1000
        line = self.readline()
 
1001
        if not line:
 
1002
            raise self.abort('socket error: EOF')
 
1003
 
 
1004
        # Protocol mandates all lines terminated by CRLF
 
1005
 
 
1006
        line = line[:-2]
 
1007
        if __debug__:
 
1008
            if self.debug >= 4:
 
1009
                self._mesg('< %s' % line)
 
1010
            else:
 
1011
                self._log('< %s' % line)
 
1012
        return line
 
1013
 
 
1014
 
 
1015
    def _match(self, cre, s):
 
1016
 
 
1017
        # Run compiled regular expression match method on 's'.
 
1018
        # Save result, return success.
 
1019
 
 
1020
        self.mo = cre.match(s)
 
1021
        if __debug__:
 
1022
            if self.mo is not None and self.debug >= 5:
 
1023
                self._mesg("\tmatched r'%s' => %r" % (cre.pattern, self.mo.groups()))
 
1024
        return self.mo is not None
 
1025
 
 
1026
 
 
1027
    def _new_tag(self):
 
1028
 
 
1029
        tag = '%s%s' % (self.tagpre, self.tagnum)
 
1030
        self.tagnum = self.tagnum + 1
 
1031
        self.tagged_commands[tag] = None
 
1032
        return tag
 
1033
 
 
1034
 
 
1035
    def _checkquote(self, arg):
 
1036
 
 
1037
        # Must quote command args if non-alphanumeric chars present,
 
1038
        # and not already quoted.
 
1039
 
 
1040
        if type(arg) is not type(''):
 
1041
            return arg
 
1042
        if len(arg) >= 2 and (arg[0],arg[-1]) in (('(',')'),('"','"')):
 
1043
            return arg
 
1044
        if arg and self.mustquote.search(arg) is None:
 
1045
            return arg
 
1046
        return self._quote(arg)
 
1047
 
 
1048
 
 
1049
    def _quote(self, arg):
 
1050
 
 
1051
        arg = arg.replace('\\', '\\\\')
 
1052
        arg = arg.replace('"', '\\"')
 
1053
 
 
1054
        return '"%s"' % arg
 
1055
 
 
1056
 
 
1057
    def _simple_command(self, name, *args):
 
1058
 
 
1059
        return self._command_complete(name, self._command(name, *args))
 
1060
 
 
1061
 
 
1062
    def _untagged_response(self, typ, dat, name):
 
1063
 
 
1064
        if typ == 'NO':
 
1065
            return typ, dat
 
1066
        if not name in self.untagged_responses:
 
1067
            return typ, [None]
 
1068
        data = self.untagged_responses.pop(name)
 
1069
        if __debug__:
 
1070
            if self.debug >= 5:
 
1071
                self._mesg('untagged_responses[%s] => %s' % (name, data))
 
1072
        return typ, data
 
1073
 
 
1074
 
 
1075
    if __debug__:
 
1076
 
 
1077
        def _mesg(self, s, secs=None):
 
1078
            if secs is None:
 
1079
                secs = time.time()
 
1080
            tm = time.strftime('%M:%S', time.localtime(secs))
 
1081
            sys.stderr.write('  %s.%02d %s\n' % (tm, (secs*100)%100, s))
 
1082
            sys.stderr.flush()
 
1083
 
 
1084
        def _dump_ur(self, dict):
 
1085
            # Dump untagged responses (in `dict').
 
1086
            l = dict.items()
 
1087
            if not l: return
 
1088
            t = '\n\t\t'
 
1089
            l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l)
 
1090
            self._mesg('untagged responses dump:%s%s' % (t, t.join(l)))
 
1091
 
 
1092
        def _log(self, line):
 
1093
            # Keep log of last `_cmd_log_len' interactions for debugging.
 
1094
            self._cmd_log[self._cmd_log_idx] = (line, time.time())
 
1095
            self._cmd_log_idx += 1
 
1096
            if self._cmd_log_idx >= self._cmd_log_len:
 
1097
                self._cmd_log_idx = 0
 
1098
 
 
1099
        def print_log(self):
 
1100
            self._mesg('last %d IMAP4 interactions:' % len(self._cmd_log))
 
1101
            i, n = self._cmd_log_idx, self._cmd_log_len
 
1102
            while n:
 
1103
                try:
 
1104
                    self._mesg(*self._cmd_log[i])
 
1105
                except:
 
1106
                    pass
 
1107
                i += 1
 
1108
                if i >= self._cmd_log_len:
 
1109
                    i = 0
 
1110
                n -= 1
 
1111
 
 
1112
 
 
1113
 
 
1114
try:
 
1115
    import ssl
 
1116
except ImportError:
 
1117
    pass
 
1118
else:
 
1119
    class IMAP4_SSL(IMAP4):
 
1120
 
 
1121
        """IMAP4 client class over SSL connection
 
1122
 
 
1123
        Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile]]]])
 
1124
 
 
1125
                host - host's name (default: localhost);
 
1126
                port - port number (default: standard IMAP4 SSL port).
 
1127
                keyfile - PEM formatted file that contains your private key (default: None);
 
1128
                certfile - PEM formatted certificate chain file (default: None);
 
1129
 
 
1130
        for more documentation see the docstring of the parent class IMAP4.
 
1131
        """
 
1132
 
 
1133
 
 
1134
        def __init__(self, host = '', port = IMAP4_SSL_PORT, keyfile = None, certfile = None):
 
1135
            self.keyfile = keyfile
 
1136
            self.certfile = certfile
 
1137
            IMAP4.__init__(self, host, port)
 
1138
 
 
1139
 
 
1140
        def open(self, host = '', port = IMAP4_SSL_PORT):
 
1141
            """Setup connection to remote server on "host:port".
 
1142
                (default: localhost:standard IMAP4 SSL port).
 
1143
            This connection will be used by the routines:
 
1144
                read, readline, send, shutdown.
 
1145
            """
 
1146
            self.host = host
 
1147
            self.port = port
 
1148
            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
1149
            self.sock.connect((host, port))
 
1150
            self.sslobj = ssl.wrap_socket(self.sock, self.keyfile, self.certfile)
 
1151
 
 
1152
 
 
1153
        def read(self, size):
 
1154
            """Read 'size' bytes from remote."""
 
1155
            # sslobj.read() sometimes returns < size bytes
 
1156
            chunks = []
 
1157
            read = 0
 
1158
            while read < size:
 
1159
                data = self.sslobj.read(min(size-read, 16384))
 
1160
                read += len(data)
 
1161
                chunks.append(data)
 
1162
 
 
1163
            return ''.join(chunks)
 
1164
 
 
1165
 
 
1166
        def readline(self):
 
1167
            """Read line from remote."""
 
1168
            line = []
 
1169
            while 1:
 
1170
                char = self.sslobj.read(1)
 
1171
                line.append(char)
 
1172
                if char == "\n": return ''.join(line)
 
1173
 
 
1174
 
 
1175
        def send(self, data):
 
1176
            """Send data to remote."""
 
1177
            bytes = len(data)
 
1178
            while bytes > 0:
 
1179
                sent = self.sslobj.write(data)
 
1180
                if sent == bytes:
 
1181
                    break    # avoid copy
 
1182
                data = data[sent:]
 
1183
                bytes = bytes - sent
 
1184
 
 
1185
 
 
1186
        def shutdown(self):
 
1187
            """Close I/O established in "open"."""
 
1188
            self.sock.close()
 
1189
 
 
1190
 
 
1191
        def socket(self):
 
1192
            """Return socket instance used to connect to IMAP4 server.
 
1193
 
 
1194
            socket = <instance>.socket()
 
1195
            """
 
1196
            return self.sock
 
1197
 
 
1198
 
 
1199
        def ssl(self):
 
1200
            """Return SSLObject instance used to communicate with the IMAP4 server.
 
1201
 
 
1202
            ssl = ssl.wrap_socket(<instance>.socket)
 
1203
            """
 
1204
            return self.sslobj
 
1205
 
 
1206
    __all__.append("IMAP4_SSL")
 
1207
 
 
1208
 
 
1209
class IMAP4_stream(IMAP4):
 
1210
 
 
1211
    """IMAP4 client class over a stream
 
1212
 
 
1213
    Instantiate with: IMAP4_stream(command)
 
1214
 
 
1215
            where "command" is a string that can be passed to os.popen2()
 
1216
 
 
1217
    for more documentation see the docstring of the parent class IMAP4.
 
1218
    """
 
1219
 
 
1220
 
 
1221
    def __init__(self, command):
 
1222
        self.command = command
 
1223
        IMAP4.__init__(self)
 
1224
 
 
1225
 
 
1226
    def open(self, host = None, port = None):
 
1227
        """Setup a stream connection.
 
1228
        This connection will be used by the routines:
 
1229
            read, readline, send, shutdown.
 
1230
        """
 
1231
        self.host = None        # For compatibility with parent class
 
1232
        self.port = None
 
1233
        self.sock = None
 
1234
        self.file = None
 
1235
        self.writefile, self.readfile = os.popen2(self.command)
 
1236
 
 
1237
 
 
1238
    def read(self, size):
 
1239
        """Read 'size' bytes from remote."""
 
1240
        return self.readfile.read(size)
 
1241
 
 
1242
 
 
1243
    def readline(self):
 
1244
        """Read line from remote."""
 
1245
        return self.readfile.readline()
 
1246
 
 
1247
 
 
1248
    def send(self, data):
 
1249
        """Send data to remote."""
 
1250
        self.writefile.write(data)
 
1251
        self.writefile.flush()
 
1252
 
 
1253
 
 
1254
    def shutdown(self):
 
1255
        """Close I/O established in "open"."""
 
1256
        self.readfile.close()
 
1257
        self.writefile.close()
 
1258
 
 
1259
 
 
1260
 
 
1261
class _Authenticator:
 
1262
 
 
1263
    """Private class to provide en/decoding
 
1264
            for base64-based authentication conversation.
 
1265
    """
 
1266
 
 
1267
    def __init__(self, mechinst):
 
1268
        self.mech = mechinst    # Callable object to provide/process data
 
1269
 
 
1270
    def process(self, data):
 
1271
        ret = self.mech(self.decode(data))
 
1272
        if ret is None:
 
1273
            return '*'      # Abort conversation
 
1274
        return self.encode(ret)
 
1275
 
 
1276
    def encode(self, inp):
 
1277
        #
 
1278
        #  Invoke binascii.b2a_base64 iteratively with
 
1279
        #  short even length buffers, strip the trailing
 
1280
        #  line feed from the result and append.  "Even"
 
1281
        #  means a number that factors to both 6 and 8,
 
1282
        #  so when it gets to the end of the 8-bit input
 
1283
        #  there's no partial 6-bit output.
 
1284
        #
 
1285
        oup = ''
 
1286
        while inp:
 
1287
            if len(inp) > 48:
 
1288
                t = inp[:48]
 
1289
                inp = inp[48:]
 
1290
            else:
 
1291
                t = inp
 
1292
                inp = ''
 
1293
            e = binascii.b2a_base64(t)
 
1294
            if e:
 
1295
                oup = oup + e[:-1]
 
1296
        return oup
 
1297
 
 
1298
    def decode(self, inp):
 
1299
        if not inp:
 
1300
            return ''
 
1301
        return binascii.a2b_base64(inp)
 
1302
 
 
1303
 
 
1304
 
 
1305
Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
 
1306
        'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
 
1307
 
 
1308
def Internaldate2tuple(resp):
 
1309
    """Convert IMAP4 INTERNALDATE to UT.
 
1310
 
 
1311
    Returns Python time module tuple.
 
1312
    """
 
1313
 
 
1314
    mo = InternalDate.match(resp)
 
1315
    if not mo:
 
1316
        return None
 
1317
 
 
1318
    mon = Mon2num[mo.group('mon')]
 
1319
    zonen = mo.group('zonen')
 
1320
 
 
1321
    day = int(mo.group('day'))
 
1322
    year = int(mo.group('year'))
 
1323
    hour = int(mo.group('hour'))
 
1324
    min = int(mo.group('min'))
 
1325
    sec = int(mo.group('sec'))
 
1326
    zoneh = int(mo.group('zoneh'))
 
1327
    zonem = int(mo.group('zonem'))
 
1328
 
 
1329
    # INTERNALDATE timezone must be subtracted to get UT
 
1330
 
 
1331
    zone = (zoneh*60 + zonem)*60
 
1332
    if zonen == '-':
 
1333
        zone = -zone
 
1334
 
 
1335
    tt = (year, mon, day, hour, min, sec, -1, -1, -1)
 
1336
 
 
1337
    utc = time.mktime(tt)
 
1338
 
 
1339
    # Following is necessary because the time module has no 'mkgmtime'.
 
1340
    # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
 
1341
 
 
1342
    lt = time.localtime(utc)
 
1343
    if time.daylight and lt[-1]:
 
1344
        zone = zone + time.altzone
 
1345
    else:
 
1346
        zone = zone + time.timezone
 
1347
 
 
1348
    return time.localtime(utc - zone)
 
1349
 
 
1350
 
 
1351
 
 
1352
def Int2AP(num):
 
1353
 
 
1354
    """Convert integer to A-P string representation."""
 
1355
 
 
1356
    val = ''; AP = 'ABCDEFGHIJKLMNOP'
 
1357
    num = int(abs(num))
 
1358
    while num:
 
1359
        num, mod = divmod(num, 16)
 
1360
        val = AP[mod] + val
 
1361
    return val
 
1362
 
 
1363
 
 
1364
 
 
1365
def ParseFlags(resp):
 
1366
 
 
1367
    """Convert IMAP4 flags response to python tuple."""
 
1368
 
 
1369
    mo = Flags.match(resp)
 
1370
    if not mo:
 
1371
        return ()
 
1372
 
 
1373
    return tuple(mo.group('flags').split())
 
1374
 
 
1375
 
 
1376
def Time2Internaldate(date_time):
 
1377
 
 
1378
    """Convert 'date_time' to IMAP4 INTERNALDATE representation.
 
1379
 
 
1380
    Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
 
1381
    """
 
1382
 
 
1383
    if isinstance(date_time, (int, float)):
 
1384
        tt = time.localtime(date_time)
 
1385
    elif isinstance(date_time, (tuple, time.struct_time)):
 
1386
        tt = date_time
 
1387
    elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'):
 
1388
        return date_time        # Assume in correct format
 
1389
    else:
 
1390
        raise ValueError("date_time not of a known type")
 
1391
 
 
1392
    dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
 
1393
    if dt[0] == '0':
 
1394
        dt = ' ' + dt[1:]
 
1395
    if time.daylight and tt[-1]:
 
1396
        zone = -time.altzone
 
1397
    else:
 
1398
        zone = -time.timezone
 
1399
    return '"' + dt + " %+03d%02d" % divmod(zone//60, 60) + '"'
 
1400
 
 
1401
 
 
1402
 
 
1403
if __name__ == '__main__':
 
1404
 
 
1405
    # To test: invoke either as 'python imaplib.py [IMAP4_server_hostname]'
 
1406
    # or 'python imaplib.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"'
 
1407
    # to test the IMAP4_stream class
 
1408
 
 
1409
    import getopt, getpass
 
1410
 
 
1411
    try:
 
1412
        optlist, args = getopt.getopt(sys.argv[1:], 'd:s:')
 
1413
    except getopt.error, val:
 
1414
        optlist, args = (), ()
 
1415
 
 
1416
    stream_command = None
 
1417
    for opt,val in optlist:
 
1418
        if opt == '-d':
 
1419
            Debug = int(val)
 
1420
        elif opt == '-s':
 
1421
            stream_command = val
 
1422
            if not args: args = (stream_command,)
 
1423
 
 
1424
    if not args: args = ('',)
 
1425
 
 
1426
    host = args[0]
 
1427
 
 
1428
    USER = getpass.getuser()
 
1429
    PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost"))
 
1430
 
 
1431
    test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER, 'lf':'\n'}
 
1432
    test_seq1 = (
 
1433
    ('login', (USER, PASSWD)),
 
1434
    ('create', ('/tmp/xxx 1',)),
 
1435
    ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
 
1436
    ('CREATE', ('/tmp/yyz 2',)),
 
1437
    ('append', ('/tmp/yyz 2', None, None, test_mesg)),
 
1438
    ('list', ('/tmp', 'yy*')),
 
1439
    ('select', ('/tmp/yyz 2',)),
 
1440
    ('search', (None, 'SUBJECT', 'test')),
 
1441
    ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
 
1442
    ('store', ('1', 'FLAGS', '(\Deleted)')),
 
1443
    ('namespace', ()),
 
1444
    ('expunge', ()),
 
1445
    ('recent', ()),
 
1446
    ('close', ()),
 
1447
    )
 
1448
 
 
1449
    test_seq2 = (
 
1450
    ('select', ()),
 
1451
    ('response',('UIDVALIDITY',)),
 
1452
    ('uid', ('SEARCH', 'ALL')),
 
1453
    ('response', ('EXISTS',)),
 
1454
    ('append', (None, None, None, test_mesg)),
 
1455
    ('recent', ()),
 
1456
    ('logout', ()),
 
1457
    )
 
1458
 
 
1459
    def run(cmd, args):
 
1460
        M._mesg('%s %s' % (cmd, args))
 
1461
        typ, dat = getattr(M, cmd)(*args)
 
1462
        M._mesg('%s => %s %s' % (cmd, typ, dat))
 
1463
        if typ == 'NO': raise dat[0]
 
1464
        return dat
 
1465
 
 
1466
    try:
 
1467
        if stream_command:
 
1468
            M = IMAP4_stream(stream_command)
 
1469
        else:
 
1470
            M = IMAP4(host)
 
1471
        if M.state == 'AUTH':
 
1472
            test_seq1 = test_seq1[1:]   # Login not needed
 
1473
        M._mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
 
1474
        M._mesg('CAPABILITIES = %r' % (M.capabilities,))
 
1475
 
 
1476
        for cmd,args in test_seq1:
 
1477
            run(cmd, args)
 
1478
 
 
1479
        for ml in run('list', ('/tmp/', 'yy%')):
 
1480
            mo = re.match(r'.*"([^"]+)"$', ml)
 
1481
            if mo: path = mo.group(1)
 
1482
            else: path = ml.split()[-1]
 
1483
            run('delete', (path,))
 
1484
 
 
1485
        for cmd,args in test_seq2:
 
1486
            dat = run(cmd, args)
 
1487
 
 
1488
            if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
 
1489
                continue
 
1490
 
 
1491
            uid = dat[-1].split()
 
1492
            if not uid: continue
 
1493
            run('uid', ('FETCH', '%s' % uid[-1],
 
1494
                    '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
 
1495
 
 
1496
        print '\nAll tests OK.'
 
1497
 
 
1498
    except:
 
1499
        print '\nTests failed.'
 
1500
 
 
1501
        if not Debug:
 
1502
            print '''
 
1503
If you would like to see debugging output,
 
1504
try: %s -d5
 
1505
''' % sys.argv[0]
 
1506
 
 
1507
        raise