Statistics
| Revision:

root / tags / v1_0_2_Build_904 / extensions / extScripting / scripts / jython / Lib / imaplib.py @ 10724

History | View | Annotate | Download (33.2 KB)

1 5782 jmvivo
"""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
18
__version__ = "2.40"
19
20
import binascii, re, socket, time, random, sys
21
22
__all__ = ["IMAP4", "Internaldate2tuple",
23
           "Int2AP", "ParseFlags", "Time2Internaldate"]
24
25
#       Globals
26
27
CRLF = '\r\n'
28
Debug = 0
29
IMAP4_PORT = 143
30
AllowedVersions = ('IMAP4REV1', 'IMAP4')        # Most recent first
31
32
#       Commands
33
34
Commands = {
35
        # name            valid states
36
        'APPEND':       ('AUTH', 'SELECTED'),
37
        'AUTHENTICATE': ('NONAUTH',),
38
        'CAPABILITY':   ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
39
        'CHECK':        ('SELECTED',),
40
        'CLOSE':        ('SELECTED',),
41
        'COPY':         ('SELECTED',),
42
        'CREATE':       ('AUTH', 'SELECTED'),
43
        'DELETE':       ('AUTH', 'SELECTED'),
44
        'EXAMINE':      ('AUTH', 'SELECTED'),
45
        'EXPUNGE':      ('SELECTED',),
46
        'FETCH':        ('SELECTED',),
47
        'LIST':         ('AUTH', 'SELECTED'),
48
        'LOGIN':        ('NONAUTH',),
49
        'LOGOUT':       ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
50
        'LSUB':         ('AUTH', 'SELECTED'),
51
        'NOOP':         ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
52
        'PARTIAL':      ('SELECTED',),
53
        'RENAME':       ('AUTH', 'SELECTED'),
54
        'SEARCH':       ('SELECTED',),
55
        'SELECT':       ('AUTH', 'SELECTED'),
56
        'STATUS':       ('AUTH', 'SELECTED'),
57
        'STORE':        ('SELECTED',),
58
        'SUBSCRIBE':    ('AUTH', 'SELECTED'),
59
        'UID':          ('SELECTED',),
60
        'UNSUBSCRIBE':  ('AUTH', 'SELECTED'),
61
        }
62
63
#       Patterns to match server responses
64
65
Continuation = re.compile(r'\+( (?P<data>.*))?')
66
Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
67
InternalDate = re.compile(r'.*INTERNALDATE "'
68
        r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
69
        r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
70
        r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
71
        r'"')
72
Literal = re.compile(r'.*{(?P<size>\d+)}$')
73
Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
74
Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
75
Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
76
77
78
79
class IMAP4:
80
81
    """IMAP4 client class.
82

83
    Instantiate with: IMAP4([host[, port]])
84

85
            host - host's name (default: localhost);
86
            port - port number (default: standard IMAP4 port).
87

88
    All IMAP4rev1 commands are supported by methods of the same
89
    name (in lower-case).
90

91
    All arguments to commands are converted to strings, except for
92
    AUTHENTICATE, and the last argument to APPEND which is passed as
93
    an IMAP4 literal.  If necessary (the string contains any
94
    non-printing characters or white-space and isn't enclosed with
95
    either parentheses or double quotes) each string is quoted.
96
    However, the 'password' argument to the LOGIN command is always
97
    quoted.  If you want to avoid having an argument string quoted
98
    (eg: the 'flags' argument to STORE) then enclose the string in
99
    parentheses (eg: "(\Deleted)").
100

101
    Each command returns a tuple: (type, [data, ...]) where 'type'
102
    is usually 'OK' or 'NO', and 'data' is either the text from the
103
    tagged response, or untagged results from command.
104

105
    Errors raise the exception class <instance>.error("<reason>").
106
    IMAP4 server errors raise <instance>.abort("<reason>"),
107
    which is a sub-class of 'error'. Mailbox status changes
108
    from READ-WRITE to READ-ONLY raise the exception class
109
    <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
110

111
    "error" exceptions imply a program error.
112
    "abort" exceptions imply the connection should be reset, and
113
            the command re-tried.
114
    "readonly" exceptions imply the command should be re-tried.
115

116
    Note: to use this module, you must read the RFCs pertaining
117
    to the IMAP4 protocol, as the semantics of the arguments to
118
    each IMAP4 command are left to the invoker, not to mention
119
    the results.
120
    """
121
122
    class error(Exception): pass    # Logical errors - debug required
123
    class abort(error): pass        # Service errors - close and retry
124
    class readonly(abort): pass     # Mailbox status changed to READ-ONLY
125
126
    mustquote = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]")
127
128
    def __init__(self, host = '', port = IMAP4_PORT):
129
        self.host = host
130
        self.port = port
131
        self.debug = Debug
132
        self.state = 'LOGOUT'
133
        self.literal = None             # A literal argument to a command
134
        self.tagged_commands = {}       # Tagged commands awaiting response
135
        self.untagged_responses = {}    # {typ: [data, ...], ...}
136
        self.continuation_response = '' # Last continuation response
137
        self.is_readonly = None         # READ-ONLY desired state
138
        self.tagnum = 0
139
140
        # Open socket to server.
141
142
        self.open(host, port)
143
144
        # Create unique tag for this session,
145
        # and compile tagged response matcher.
146
147
        self.tagpre = Int2AP(random.randint(0, 31999))
148
        self.tagre = re.compile(r'(?P<tag>'
149
                        + self.tagpre
150
                        + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
151
152
        # Get server welcome message,
153
        # request and store CAPABILITY response.
154
155
        if __debug__:
156
            if self.debug >= 1:
157
                _mesg('new IMAP4 connection, tag=%s' % self.tagpre)
158
159
        self.welcome = self._get_response()
160
        if self.untagged_responses.has_key('PREAUTH'):
161
            self.state = 'AUTH'
162
        elif self.untagged_responses.has_key('OK'):
163
            self.state = 'NONAUTH'
164
        else:
165
            raise self.error(self.welcome)
166
167
        cap = 'CAPABILITY'
168
        self._simple_command(cap)
169
        if not self.untagged_responses.has_key(cap):
170
            raise self.error('no CAPABILITY response from server')
171
        self.capabilities = tuple(self.untagged_responses[cap][-1].upper().split())
172
173
        if __debug__:
174
            if self.debug >= 3:
175
                _mesg('CAPABILITIES: %s' % `self.capabilities`)
176
177
        for version in AllowedVersions:
178
            if not version in self.capabilities:
179
                continue
180
            self.PROTOCOL_VERSION = version
181
            return
182
183
        raise self.error('server not IMAP4 compliant')
184
185
186
    def __getattr__(self, attr):
187
        #       Allow UPPERCASE variants of IMAP4 command methods.
188
        if Commands.has_key(attr):
189
            return eval("self.%s" % attr.lower())
190
        raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
191
192
193
194
    #       Public methods
195
196
197
    def open(self, host, port):
198
        """Setup 'self.sock' and 'self.file'."""
199
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
200
        self.sock.connect((self.host, self.port))
201
        self.file = self.sock.makefile('rb')
202
203
204
    def recent(self):
205
        """Return most recent 'RECENT' responses if any exist,
206
        else prompt server for an update using the 'NOOP' command.
207

208
        (typ, [data]) = <instance>.recent()
209

210
        'data' is None if no new messages,
211
        else list of RECENT responses, most recent last.
212
        """
213
        name = 'RECENT'
214
        typ, dat = self._untagged_response('OK', [None], name)
215
        if dat[-1]:
216
            return typ, dat
217
        typ, dat = self.noop()  # Prod server for response
218
        return self._untagged_response(typ, dat, name)
219
220
221
    def response(self, code):
222
        """Return data for response 'code' if received, or None.
223

224
        Old value for response 'code' is cleared.
225

226
        (code, [data]) = <instance>.response(code)
227
        """
228
        return self._untagged_response(code, [None], code.upper())
229
230
231
    def socket(self):
232
        """Return socket instance used to connect to IMAP4 server.
233

234
        socket = <instance>.socket()
235
        """
236
        return self.sock
237
238
239
240
    #       IMAP4 commands
241
242
243
    def append(self, mailbox, flags, date_time, message):
244
        """Append message to named mailbox.
245

246
        (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
247

248
                All args except `message' can be None.
249
        """
250
        name = 'APPEND'
251
        if not mailbox:
252
            mailbox = 'INBOX'
253
        if flags:
254
            if (flags[0],flags[-1]) != ('(',')'):
255
                flags = '(%s)' % flags
256
        else:
257
            flags = None
258
        if date_time:
259
            date_time = Time2Internaldate(date_time)
260
        else:
261
            date_time = None
262
        self.literal = message
263
        return self._simple_command(name, mailbox, flags, date_time)
264
265
266
    def authenticate(self, mechanism, authobject):
267
        """Authenticate command - requires response processing.
268

269
        'mechanism' specifies which authentication mechanism is to
270
        be used - it must appear in <instance>.capabilities in the
271
        form AUTH=<mechanism>.
272

273
        'authobject' must be a callable object:
274

275
                data = authobject(response)
276

277
        It will be called to process server continuation responses.
278
        It should return data that will be encoded and sent to server.
279
        It should return None if the client abort response '*' should
280
        be sent instead.
281
        """
282
        mech = mechanism.upper()
283
        cap = 'AUTH=%s' % mech
284
        if not cap in self.capabilities:
285
            raise self.error("Server doesn't allow %s authentication." % mech)
286
        self.literal = _Authenticator(authobject).process
287
        typ, dat = self._simple_command('AUTHENTICATE', mech)
288
        if typ != 'OK':
289
            raise self.error(dat[-1])
290
        self.state = 'AUTH'
291
        return typ, dat
292
293
294
    def check(self):
295
        """Checkpoint mailbox on server.
296

297
        (typ, [data]) = <instance>.check()
298
        """
299
        return self._simple_command('CHECK')
300
301
302
    def close(self):
303
        """Close currently selected mailbox.
304

305
        Deleted messages are removed from writable mailbox.
306
        This is the recommended command before 'LOGOUT'.
307

308
        (typ, [data]) = <instance>.close()
309
        """
310
        try:
311
            typ, dat = self._simple_command('CLOSE')
312
        finally:
313
            self.state = 'AUTH'
314
        return typ, dat
315
316
317
    def copy(self, message_set, new_mailbox):
318
        """Copy 'message_set' messages onto end of 'new_mailbox'.
319

320
        (typ, [data]) = <instance>.copy(message_set, new_mailbox)
321
        """
322
        return self._simple_command('COPY', message_set, new_mailbox)
323
324
325
    def create(self, mailbox):
326
        """Create new mailbox.
327

328
        (typ, [data]) = <instance>.create(mailbox)
329
        """
330
        return self._simple_command('CREATE', mailbox)
331
332
333
    def delete(self, mailbox):
334
        """Delete old mailbox.
335

336
        (typ, [data]) = <instance>.delete(mailbox)
337
        """
338
        return self._simple_command('DELETE', mailbox)
339
340
341
    def expunge(self):
342
        """Permanently remove deleted items from selected mailbox.
343

344
        Generates 'EXPUNGE' response for each deleted message.
345

346
        (typ, [data]) = <instance>.expunge()
347

348
        'data' is list of 'EXPUNGE'd message numbers in order received.
349
        """
350
        name = 'EXPUNGE'
351
        typ, dat = self._simple_command(name)
352
        return self._untagged_response(typ, dat, name)
353
354
355
    def fetch(self, message_set, message_parts):
356
        """Fetch (parts of) messages.
357

358
        (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
359

360
        'message_parts' should be a string of selected parts
361
        enclosed in parentheses, eg: "(UID BODY[TEXT])".
362

363
        'data' are tuples of message part envelope and data.
364
        """
365
        name = 'FETCH'
366
        typ, dat = self._simple_command(name, message_set, message_parts)
367
        return self._untagged_response(typ, dat, name)
368
369
370
    def list(self, directory='""', pattern='*'):
371
        """List mailbox names in directory matching pattern.
372

373
        (typ, [data]) = <instance>.list(directory='""', pattern='*')
374

375
        'data' is list of LIST responses.
376
        """
377
        name = 'LIST'
378
        typ, dat = self._simple_command(name, directory, pattern)
379
        return self._untagged_response(typ, dat, name)
380
381
382
    def login(self, user, password):
383
        """Identify client using plaintext password.
384

385
        (typ, [data]) = <instance>.login(user, password)
386

387
        NB: 'password' will be quoted.
388
        """
389
        #if not 'AUTH=LOGIN' in self.capabilities:
390
        #       raise self.error("Server doesn't allow LOGIN authentication." % mech)
391
        typ, dat = self._simple_command('LOGIN', user, self._quote(password))
392
        if typ != 'OK':
393
            raise self.error(dat[-1])
394
        self.state = 'AUTH'
395
        return typ, dat
396
397
398
    def logout(self):
399
        """Shutdown connection to server.
400

401
        (typ, [data]) = <instance>.logout()
402

403
        Returns server 'BYE' response.
404
        """
405
        self.state = 'LOGOUT'
406
        try: typ, dat = self._simple_command('LOGOUT')
407
        except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
408
        self.file.close()
409
        self.sock.close()
410
        if self.untagged_responses.has_key('BYE'):
411
            return 'BYE', self.untagged_responses['BYE']
412
        return typ, dat
413
414
415
    def lsub(self, directory='""', pattern='*'):
416
        """List 'subscribed' mailbox names in directory matching pattern.
417

418
        (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
419

420
        'data' are tuples of message part envelope and data.
421
        """
422
        name = 'LSUB'
423
        typ, dat = self._simple_command(name, directory, pattern)
424
        return self._untagged_response(typ, dat, name)
425
426
427
    def noop(self):
428
        """Send NOOP command.
429

430
        (typ, data) = <instance>.noop()
431
        """
432
        if __debug__:
433
            if self.debug >= 3:
434
                _dump_ur(self.untagged_responses)
435
        return self._simple_command('NOOP')
436
437
438
    def partial(self, message_num, message_part, start, length):
439
        """Fetch truncated part of a message.
440

441
        (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
442

443
        'data' is tuple of message part envelope and data.
444
        """
445
        name = 'PARTIAL'
446
        typ, dat = self._simple_command(name, message_num, message_part, start, length)
447
        return self._untagged_response(typ, dat, 'FETCH')
448
449
450
    def rename(self, oldmailbox, newmailbox):
451
        """Rename old mailbox name to new.
452

453
        (typ, data) = <instance>.rename(oldmailbox, newmailbox)
454
        """
455
        return self._simple_command('RENAME', oldmailbox, newmailbox)
456
457
458
    def search(self, charset, *criteria):
459
        """Search mailbox for matching messages.
460

461
        (typ, [data]) = <instance>.search(charset, criterium, ...)
462

463
        'data' is space separated list of matching message numbers.
464
        """
465
        name = 'SEARCH'
466
        if charset:
467
            charset = 'CHARSET ' + charset
468
        typ, dat = apply(self._simple_command, (name, charset) + criteria)
469
        return self._untagged_response(typ, dat, name)
470
471
472
    def select(self, mailbox='INBOX', readonly=None):
473
        """Select a mailbox.
474

475
        Flush all untagged responses.
476

477
        (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
478

479
        'data' is count of messages in mailbox ('EXISTS' response).
480
        """
481
        # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
482
        self.untagged_responses = {}    # Flush old responses.
483
        self.is_readonly = readonly
484
        if readonly:
485
            name = 'EXAMINE'
486
        else:
487
            name = 'SELECT'
488
        typ, dat = self._simple_command(name, mailbox)
489
        if typ != 'OK':
490
            self.state = 'AUTH'     # Might have been 'SELECTED'
491
            return typ, dat
492
        self.state = 'SELECTED'
493
        if self.untagged_responses.has_key('READ-ONLY') \
494
                and not readonly:
495
            if __debug__:
496
                if self.debug >= 1:
497
                    _dump_ur(self.untagged_responses)
498
            raise self.readonly('%s is not writable' % mailbox)
499
        return typ, self.untagged_responses.get('EXISTS', [None])
500
501
502
    def status(self, mailbox, names):
503
        """Request named status conditions for mailbox.
504

505
        (typ, [data]) = <instance>.status(mailbox, names)
506
        """
507
        name = 'STATUS'
508
        if self.PROTOCOL_VERSION == 'IMAP4':
509
            raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
510
        typ, dat = self._simple_command(name, mailbox, names)
511
        return self._untagged_response(typ, dat, name)
512
513
514
    def store(self, message_set, command, flags):
515
        """Alters flag dispositions for messages in mailbox.
516

517
        (typ, [data]) = <instance>.store(message_set, command, flags)
518
        """
519
        if (flags[0],flags[-1]) != ('(',')'):
520
            flags = '(%s)' % flags  # Avoid quoting the flags
521
        typ, dat = self._simple_command('STORE', message_set, command, flags)
522
        return self._untagged_response(typ, dat, 'FETCH')
523
524
525
    def subscribe(self, mailbox):
526
        """Subscribe to new mailbox.
527

528
        (typ, [data]) = <instance>.subscribe(mailbox)
529
        """
530
        return self._simple_command('SUBSCRIBE', mailbox)
531
532
533
    def uid(self, command, *args):
534
        """Execute "command arg ..." with messages identified by UID,
535
                rather than message number.
536

537
        (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
538

539
        Returns response appropriate to 'command'.
540
        """
541
        command = command.upper()
542
        if not Commands.has_key(command):
543
            raise self.error("Unknown IMAP4 UID command: %s" % command)
544
        if self.state not in Commands[command]:
545
            raise self.error('command %s illegal in state %s'
546
                                    % (command, self.state))
547
        name = 'UID'
548
        typ, dat = apply(self._simple_command, (name, command) + args)
549
        if command == 'SEARCH':
550
            name = 'SEARCH'
551
        else:
552
            name = 'FETCH'
553
        return self._untagged_response(typ, dat, name)
554
555
556
    def unsubscribe(self, mailbox):
557
        """Unsubscribe from old mailbox.
558

559
        (typ, [data]) = <instance>.unsubscribe(mailbox)
560
        """
561
        return self._simple_command('UNSUBSCRIBE', mailbox)
562
563
564
    def xatom(self, name, *args):
565
        """Allow simple extension commands
566
                notified by server in CAPABILITY response.
567

568
        (typ, [data]) = <instance>.xatom(name, arg, ...)
569
        """
570
        if name[0] != 'X' or not name in self.capabilities:
571
            raise self.error('unknown extension command: %s' % name)
572
        return apply(self._simple_command, (name,) + args)
573
574
575
576
    #       Private methods
577
578
579
    def _append_untagged(self, typ, dat):
580
581
        if dat is None: dat = ''
582
        ur = self.untagged_responses
583
        if __debug__:
584
            if self.debug >= 5:
585
                _mesg('untagged_responses[%s] %s += ["%s"]' %
586
                        (typ, len(ur.get(typ,'')), dat))
587
        if ur.has_key(typ):
588
            ur[typ].append(dat)
589
        else:
590
            ur[typ] = [dat]
591
592
593
    def _check_bye(self):
594
        bye = self.untagged_responses.get('BYE')
595
        if bye:
596
            raise self.abort(bye[-1])
597
598
599
    def _command(self, name, *args):
600
601
        if self.state not in Commands[name]:
602
            self.literal = None
603
            raise self.error(
604
            'command %s illegal in state %s' % (name, self.state))
605
606
        for typ in ('OK', 'NO', 'BAD'):
607
            if self.untagged_responses.has_key(typ):
608
                del self.untagged_responses[typ]
609
610
        if self.untagged_responses.has_key('READ-ONLY') \
611
        and not self.is_readonly:
612
            raise self.readonly('mailbox status changed to READ-ONLY')
613
614
        tag = self._new_tag()
615
        data = '%s %s' % (tag, name)
616
        for arg in args:
617
            if arg is None: continue
618
            data = '%s %s' % (data, self._checkquote(arg))
619
620
        literal = self.literal
621
        if literal is not None:
622
            self.literal = None
623
            if type(literal) is type(self._command):
624
                literator = literal
625
            else:
626
                literator = None
627
                data = '%s {%s}' % (data, len(literal))
628
629
        if __debug__:
630
            if self.debug >= 4:
631
                _mesg('> %s' % data)
632
            else:
633
                _log('> %s' % data)
634
635
        try:
636
            self.sock.send('%s%s' % (data, CRLF))
637
        except socket.error, val:
638
            raise self.abort('socket error: %s' % val)
639
640
        if literal is None:
641
            return tag
642
643
        while 1:
644
            # Wait for continuation response
645
646
            while self._get_response():
647
                if self.tagged_commands[tag]:   # BAD/NO?
648
                    return tag
649
650
            # Send literal
651
652
            if literator:
653
                literal = literator(self.continuation_response)
654
655
            if __debug__:
656
                if self.debug >= 4:
657
                    _mesg('write literal size %s' % len(literal))
658
659
            try:
660
                self.sock.send(literal)
661
                self.sock.send(CRLF)
662
            except socket.error, val:
663
                raise self.abort('socket error: %s' % val)
664
665
            if not literator:
666
                break
667
668
        return tag
669
670
671
    def _command_complete(self, name, tag):
672
        self._check_bye()
673
        try:
674
            typ, data = self._get_tagged_response(tag)
675
        except self.abort, val:
676
            raise self.abort('command: %s => %s' % (name, val))
677
        except self.error, val:
678
            raise self.error('command: %s => %s' % (name, val))
679
        self._check_bye()
680
        if typ == 'BAD':
681
            raise self.error('%s command error: %s %s' % (name, typ, data))
682
        return typ, data
683
684
685
    def _get_response(self):
686
687
        # Read response and store.
688
        #
689
        # Returns None for continuation responses,
690
        # otherwise first response line received.
691
692
        resp = self._get_line()
693
694
        # Command completion response?
695
696
        if self._match(self.tagre, resp):
697
            tag = self.mo.group('tag')
698
            if not self.tagged_commands.has_key(tag):
699
                raise self.abort('unexpected tagged response: %s' % resp)
700
701
            typ = self.mo.group('type')
702
            dat = self.mo.group('data')
703
            self.tagged_commands[tag] = (typ, [dat])
704
        else:
705
            dat2 = None
706
707
            # '*' (untagged) responses?
708
709
            if not self._match(Untagged_response, resp):
710
                if self._match(Untagged_status, resp):
711
                    dat2 = self.mo.group('data2')
712
713
            if self.mo is None:
714
                # Only other possibility is '+' (continuation) response...
715
716
                if self._match(Continuation, resp):
717
                    self.continuation_response = self.mo.group('data')
718
                    return None     # NB: indicates continuation
719
720
                raise self.abort("unexpected response: '%s'" % resp)
721
722
            typ = self.mo.group('type')
723
            dat = self.mo.group('data')
724
            if dat is None: dat = ''        # Null untagged response
725
            if dat2: dat = dat + ' ' + dat2
726
727
            # Is there a literal to come?
728
729
            while self._match(Literal, dat):
730
731
                # Read literal direct from connection.
732
733
                size = int(self.mo.group('size'))
734
                if __debug__:
735
                    if self.debug >= 4:
736
                        _mesg('read literal size %s' % size)
737
                data = self.file.read(size)
738
739
                # Store response with literal as tuple
740
741
                self._append_untagged(typ, (dat, data))
742
743
                # Read trailer - possibly containing another literal
744
745
                dat = self._get_line()
746
747
            self._append_untagged(typ, dat)
748
749
        # Bracketed response information?
750
751
        if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
752
            self._append_untagged(self.mo.group('type'), self.mo.group('data'))
753
754
        if __debug__:
755
            if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
756
                _mesg('%s response: %s' % (typ, dat))
757
758
        return resp
759
760
761
    def _get_tagged_response(self, tag):
762
763
        while 1:
764
            result = self.tagged_commands[tag]
765
            if result is not None:
766
                del self.tagged_commands[tag]
767
                return result
768
769
            # Some have reported "unexpected response" exceptions.
770
            # Note that ignoring them here causes loops.
771
            # Instead, send me details of the unexpected response and
772
            # I'll update the code in `_get_response()'.
773
774
            try:
775
                self._get_response()
776
            except self.abort, val:
777
                if __debug__:
778
                    if self.debug >= 1:
779
                        print_log()
780
                raise
781
782
783
    def _get_line(self):
784
785
        line = self.file.readline()
786
        if not line:
787
            raise self.abort('socket error: EOF')
788
789
        # Protocol mandates all lines terminated by CRLF
790
791
        line = line[:-2]
792
        if __debug__:
793
            if self.debug >= 4:
794
                _mesg('< %s' % line)
795
            else:
796
                _log('< %s' % line)
797
        return line
798
799
800
    def _match(self, cre, s):
801
802
        # Run compiled regular expression match method on 's'.
803
        # Save result, return success.
804
805
        self.mo = cre.match(s)
806
        if __debug__:
807
            if self.mo is not None and self.debug >= 5:
808
                _mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`))
809
        return self.mo is not None
810
811
812
    def _new_tag(self):
813
814
        tag = '%s%s' % (self.tagpre, self.tagnum)
815
        self.tagnum = self.tagnum + 1
816
        self.tagged_commands[tag] = None
817
        return tag
818
819
820
    def _checkquote(self, arg):
821
822
        # Must quote command args if non-alphanumeric chars present,
823
        # and not already quoted.
824
825
        if type(arg) is not type(''):
826
            return arg
827
        if (arg[0],arg[-1]) in (('(',')'),('"','"')):
828
            return arg
829
        if self.mustquote.search(arg) is None:
830
            return arg
831
        return self._quote(arg)
832
833
834
    def _quote(self, arg):
835
836
        arg = arg.replace('\\', '\\\\')
837
        arg = arg.replace('"', '\\"')
838
839
        return '"%s"' % arg
840
841
842
    def _simple_command(self, name, *args):
843
844
        return self._command_complete(name, apply(self._command, (name,) + args))
845
846
847
    def _untagged_response(self, typ, dat, name):
848
849
        if typ == 'NO':
850
            return typ, dat
851
        if not self.untagged_responses.has_key(name):
852
            return typ, [None]
853
        data = self.untagged_responses[name]
854
        if __debug__:
855
            if self.debug >= 5:
856
                _mesg('untagged_responses[%s] => %s' % (name, data))
857
        del self.untagged_responses[name]
858
        return typ, data
859
860
861
862
class _Authenticator:
863
864
    """Private class to provide en/decoding
865
            for base64-based authentication conversation.
866
    """
867
868
    def __init__(self, mechinst):
869
        self.mech = mechinst    # Callable object to provide/process data
870
871
    def process(self, data):
872
        ret = self.mech(self.decode(data))
873
        if ret is None:
874
            return '*'      # Abort conversation
875
        return self.encode(ret)
876
877
    def encode(self, inp):
878
        #
879
        #  Invoke binascii.b2a_base64 iteratively with
880
        #  short even length buffers, strip the trailing
881
        #  line feed from the result and append.  "Even"
882
        #  means a number that factors to both 6 and 8,
883
        #  so when it gets to the end of the 8-bit input
884
        #  there's no partial 6-bit output.
885
        #
886
        oup = ''
887
        while inp:
888
            if len(inp) > 48:
889
                t = inp[:48]
890
                inp = inp[48:]
891
            else:
892
                t = inp
893
                inp = ''
894
            e = binascii.b2a_base64(t)
895
            if e:
896
                oup = oup + e[:-1]
897
        return oup
898
899
    def decode(self, inp):
900
        if not inp:
901
            return ''
902
        return binascii.a2b_base64(inp)
903
904
905
906
Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
907
        'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
908
909
def Internaldate2tuple(resp):
910
    """Convert IMAP4 INTERNALDATE to UT.
911

912
    Returns Python time module tuple.
913
    """
914
915
    mo = InternalDate.match(resp)
916
    if not mo:
917
        return None
918
919
    mon = Mon2num[mo.group('mon')]
920
    zonen = mo.group('zonen')
921
922
    day = int(mo.group('day'))
923
    year = int(mo.group('year'))
924
    hour = int(mo.group('hour'))
925
    min = int(mo.group('min'))
926
    sec = int(mo.group('sec'))
927
    zoneh = int(mo.group('zoneh'))
928
    zonem = int(mo.group('zonem'))
929
930
    # INTERNALDATE timezone must be subtracted to get UT
931
932
    zone = (zoneh*60 + zonem)*60
933
    if zonen == '-':
934
        zone = -zone
935
936
    tt = (year, mon, day, hour, min, sec, -1, -1, -1)
937
938
    utc = time.mktime(tt)
939
940
    # Following is necessary because the time module has no 'mkgmtime'.
941
    # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
942
943
    lt = time.localtime(utc)
944
    if time.daylight and lt[-1]:
945
        zone = zone + time.altzone
946
    else:
947
        zone = zone + time.timezone
948
949
    return time.localtime(utc - zone)
950
951
952
953
def Int2AP(num):
954
955
    """Convert integer to A-P string representation."""
956
957
    val = ''; AP = 'ABCDEFGHIJKLMNOP'
958
    num = int(abs(num))
959
    while num:
960
        num, mod = divmod(num, 16)
961
        val = AP[mod] + val
962
    return val
963
964
965
966
def ParseFlags(resp):
967
968
    """Convert IMAP4 flags response to python tuple."""
969
970
    mo = Flags.match(resp)
971
    if not mo:
972
        return ()
973
974
    return tuple(mo.group('flags').split())
975
976
977
def Time2Internaldate(date_time):
978
979
    """Convert 'date_time' to IMAP4 INTERNALDATE representation.
980

981
    Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
982
    """
983
984
    dttype = type(date_time)
985
    if dttype is type(1) or dttype is type(1.1):
986
        tt = time.localtime(date_time)
987
    elif dttype is type(()):
988
        tt = date_time
989
    elif dttype is type(""):
990
        return date_time        # Assume in correct format
991
    else: raise ValueError
992
993
    dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
994
    if dt[0] == '0':
995
        dt = ' ' + dt[1:]
996
    if time.daylight and tt[-1]:
997
        zone = -time.altzone
998
    else:
999
        zone = -time.timezone
1000
    return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"'
1001
1002
1003
1004
if __debug__:
1005
1006
    def _mesg(s, secs=None):
1007
        if secs is None:
1008
            secs = time.time()
1009
        tm = time.strftime('%M:%S', time.localtime(secs))
1010
        sys.stderr.write('  %s.%02d %s\n' % (tm, (secs*100)%100, s))
1011
        sys.stderr.flush()
1012
1013
    def _dump_ur(dict):
1014
        # Dump untagged responses (in `dict').
1015
        l = dict.items()
1016
        if not l: return
1017
        t = '\n\t\t'
1018
        l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l)
1019
        _mesg('untagged responses dump:%s%s' % (t, t.join(l)))
1020
1021
    _cmd_log = []           # Last `_cmd_log_len' interactions
1022
    _cmd_log_len = 10
1023
1024
    def _log(line):
1025
        # Keep log of last `_cmd_log_len' interactions for debugging.
1026
        if len(_cmd_log) == _cmd_log_len:
1027
            del _cmd_log[0]
1028
        _cmd_log.append((time.time(), line))
1029
1030
    def print_log():
1031
        _mesg('last %d IMAP4 interactions:' % len(_cmd_log))
1032
        for secs,line in _cmd_log:
1033
            _mesg(line, secs)
1034
1035
1036
1037
if __name__ == '__main__':
1038
1039
    import getopt, getpass, sys
1040
1041
    try:
1042
        optlist, args = getopt.getopt(sys.argv[1:], 'd:')
1043
    except getopt.error, val:
1044
        pass
1045
1046
    for opt,val in optlist:
1047
        if opt == '-d':
1048
            Debug = int(val)
1049
1050
    if not args: args = ('',)
1051
1052
    host = args[0]
1053
1054
    USER = getpass.getuser()
1055
    PASSWD = getpass.getpass("IMAP password for %s on %s:" % (USER, host or "localhost"))
1056
1057
    test_mesg = 'From: %s@localhost\nSubject: IMAP4 test\n\ndata...\n' % USER
1058
    test_seq1 = (
1059
    ('login', (USER, PASSWD)),
1060
    ('create', ('/tmp/xxx 1',)),
1061
    ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1062
    ('CREATE', ('/tmp/yyz 2',)),
1063
    ('append', ('/tmp/yyz 2', None, None, test_mesg)),
1064
    ('list', ('/tmp', 'yy*')),
1065
    ('select', ('/tmp/yyz 2',)),
1066
    ('search', (None, 'SUBJECT', 'test')),
1067
    ('partial', ('1', 'RFC822', 1, 1024)),
1068
    ('store', ('1', 'FLAGS', '(\Deleted)')),
1069
    ('expunge', ()),
1070
    ('recent', ()),
1071
    ('close', ()),
1072
    )
1073
1074
    test_seq2 = (
1075
    ('select', ()),
1076
    ('response',('UIDVALIDITY',)),
1077
    ('uid', ('SEARCH', 'ALL')),
1078
    ('response', ('EXISTS',)),
1079
    ('append', (None, None, None, test_mesg)),
1080
    ('recent', ()),
1081
    ('logout', ()),
1082
    )
1083
1084
    def run(cmd, args):
1085
        _mesg('%s %s' % (cmd, args))
1086
        typ, dat = apply(eval('M.%s' % cmd), args)
1087
        _mesg('%s => %s %s' % (cmd, typ, dat))
1088
        return dat
1089
1090
    try:
1091
        M = IMAP4(host)
1092
        _mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
1093
1094
        for cmd,args in test_seq1:
1095
            run(cmd, args)
1096
1097
        for ml in run('list', ('/tmp/', 'yy%')):
1098
            mo = re.match(r'.*"([^"]+)"$', ml)
1099
            if mo: path = mo.group(1)
1100
            else: path = ml.split()[-1]
1101
            run('delete', (path,))
1102
1103
        for cmd,args in test_seq2:
1104
            dat = run(cmd, args)
1105
1106
            if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1107
                continue
1108
1109
            uid = dat[-1].split()
1110
            if not uid: continue
1111
            run('uid', ('FETCH', '%s' % uid[-1],
1112
                    '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1113
1114
        print '\nAll tests OK.'
1115
1116
    except:
1117
        print '\nTests failed.'
1118
1119
        if not Debug:
1120
            print '''
1121
If you would like to see debugging output,
1122
try: %s -d5
1123
''' % sys.argv[0]
1124
1125
        raise