~ubuntu-branches/ubuntu/maverick/python3.1/maverick

« back to all changes in this revision

Viewing changes to Lib/imaplib.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2009-03-23 00:01:27 UTC
  • Revision ID: james.westby@ubuntu.com-20090323000127-5fstfxju4ufrhthq
Tags: upstream-3.1~a1+20090322
ImportĀ upstreamĀ versionĀ 3.1~a1+20090322

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