~certify-web-dev/twisted/certify-trunk

« back to all changes in this revision

Viewing changes to twisted/mail/imap4.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2007-01-17 14:52:35 UTC
  • mfrom: (1.1.5 upstream) (2.1.2 etch)
  • Revision ID: james.westby@ubuntu.com-20070117145235-btmig6qfmqfen0om
Tags: 2.5.0-0ubuntu1
New upstream version, compatible with python2.5.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- test-case-name: twisted.mail.test.test_imap -*-
 
2
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
 
3
# See LICENSE for details.
 
4
 
 
5
 
 
6
"""
 
7
An IMAP4 protocol implementation
 
8
 
 
9
API Stability: Semi-stable
 
10
 
 
11
@author: U{Jp Calderone<mailto:exarkun@twistedmatrix.com>}
 
12
 
 
13
To do::
 
14
  Suspend idle timeout while server is processing
 
15
  Use an async message parser instead of buffering in memory
 
16
  Figure out a way to not queue multi-message client requests (Flow? A simple callback?)
 
17
  Clarify some API docs (Query, etc)
 
18
  Make APPEND recognize (again) non-existent mailboxes before accepting the literal
 
19
"""
 
20
 
 
21
from __future__ import nested_scopes
 
22
from __future__ import generators
 
23
 
 
24
from twisted.protocols import basic
 
25
from twisted.protocols import policies
 
26
from twisted.internet import defer
 
27
from twisted.internet import error
 
28
from twisted.internet.defer import maybeDeferred
 
29
from twisted.python import log, util, failure, text
 
30
from twisted.internet import interfaces
 
31
 
 
32
from twisted import cred
 
33
import twisted.cred.error
 
34
import twisted.cred.credentials
 
35
 
 
36
import rfc822
 
37
import base64
 
38
import binascii
 
39
import time
 
40
import hmac
 
41
import re
 
42
import tempfile
 
43
import string
 
44
import time
 
45
import random
 
46
import types
 
47
import sys
 
48
from zope.interface import implements, Interface
 
49
 
 
50
import email.Utils
 
51
 
 
52
try:
 
53
    import cStringIO as StringIO
 
54
except:
 
55
    import StringIO
 
56
 
 
57
class MessageSet(object):
 
58
    """
 
59
    Essentially an infinite bitfield, with some extra features.
 
60
 
 
61
    @type getnext: Function taking C{int} returning C{int}
 
62
    @ivar getnext: A function that returns the next message number,
 
63
    used when iterating through the MessageSet. By default, a function
 
64
    returning the next integer is supplied, but as this can be rather
 
65
    inefficient for sparse UID iterations, it is recommended to supply
 
66
    one when messages are requested by UID.  The argument is provided
 
67
    as a hint to the implementation and may be ignored if it makes sense
 
68
    to do so (eg, if an iterator is being used that maintains its own
 
69
    state, it is guaranteed that it will not be called out-of-order).
 
70
    """
 
71
    _empty = []
 
72
 
 
73
    def __init__(self, start=_empty, end=_empty):
 
74
        """
 
75
        Create a new MessageSet()
 
76
 
 
77
        @type start: Optional C{int}
 
78
        @param start: Start of range, or only message number
 
79
 
 
80
        @type end: Optional C{int}
 
81
        @param end: End of range.
 
82
        """
 
83
        self._last = self._empty # Last message/UID in use
 
84
        self.ranges = [] # List of ranges included
 
85
        self.getnext = lambda x: x+1 # A function which will return the next
 
86
                                     # message id. Handy for UID requests.
 
87
 
 
88
        if start is self._empty:
 
89
            return
 
90
 
 
91
        if isinstance(start, types.ListType):
 
92
            self.ranges = start[:]
 
93
            self.clean()
 
94
        else:
 
95
            self.add(start,end)
 
96
 
 
97
    # Ooo.  A property.
 
98
    def last():
 
99
        def _setLast(self,value):
 
100
            if self._last is not self._empty:
 
101
                raise ValueError("last already set")
 
102
 
 
103
            self._last = value
 
104
            for i,(l,h) in enumerate(self.ranges):
 
105
                if l is not None:
 
106
                    break # There are no more Nones after this
 
107
                l = value
 
108
                if h is None:
 
109
                    h = value
 
110
                if l > h:
 
111
                    l, h = h, l
 
112
                self.ranges[i] = (l,h)
 
113
 
 
114
            self.clean()
 
115
 
 
116
        def _getLast(self):
 
117
            return self._last
 
118
 
 
119
        doc = '''
 
120
            "Highest" message number, refered to by "*".
 
121
            Must be set before attempting to use the MessageSet.
 
122
        '''
 
123
        return _getLast, _setLast, None, doc
 
124
    last = property(*last())
 
125
 
 
126
    def add(self, start, end=_empty):
 
127
        """
 
128
        Add another range
 
129
 
 
130
        @type start: C{int}
 
131
        @param start: Start of range, or only message number
 
132
 
 
133
        @type end: Optional C{int}
 
134
        @param end: End of range.
 
135
        """
 
136
        if end is self._empty:
 
137
            end = start
 
138
 
 
139
        if self._last is not self._empty:
 
140
            if start is None:
 
141
                start = self.last
 
142
            if end is None:
 
143
                end = self.last
 
144
 
 
145
        if start > end:
 
146
            # Try to keep in low, high order if possible
 
147
            # (But we don't know what None means, this will keep
 
148
            # None at the start of the ranges list)
 
149
            start, end = end, start
 
150
 
 
151
        self.ranges.append((start,end))
 
152
        self.clean()
 
153
 
 
154
    def __add__(self, other):
 
155
        if isinstance(other, MessageSet):
 
156
            ranges = self.ranges + other.ranges
 
157
            return MessageSet(ranges)
 
158
        else:
 
159
            res = MessageSet(self.ranges)
 
160
            try:
 
161
                res.add(*other)
 
162
            except TypeError:
 
163
                res.add(other)
 
164
            return res
 
165
 
 
166
    def extend(self, other):
 
167
        if isinstance(other, MessageSet):
 
168
            self.ranges.extend(other.ranges)
 
169
            self.clean()
 
170
        else:
 
171
            try:
 
172
                self.add(*other)
 
173
            except TypeError:
 
174
                self.add(other)
 
175
 
 
176
        return self
 
177
 
 
178
    def clean(self):
 
179
        """
 
180
        Clean ranges list, combining adjacent ranges
 
181
        """
 
182
 
 
183
        self.ranges.sort()
 
184
 
 
185
        oldl, oldh = None, None
 
186
        for i,(l,h) in enumerate(self.ranges):
 
187
            if l is None:
 
188
                continue
 
189
            # l is >= oldl and h is >= oldh due to sort()
 
190
            if oldl is not None and l <= oldh+1:
 
191
                l = oldl
 
192
                h = max(oldh,h)
 
193
                self.ranges[i-1] = None
 
194
                self.ranges[i] = (l,h)
 
195
 
 
196
            oldl,oldh = l,h
 
197
 
 
198
        self.ranges = filter(None, self.ranges)
 
199
 
 
200
    def __contains__(self, value):
 
201
        """
 
202
        May raise TypeError if we encounter unknown "high" values
 
203
        """
 
204
        for l,h in self.ranges:
 
205
            if l is None:
 
206
                raise TypeError(
 
207
                    "Can't determine membership; last value not set")
 
208
            if l <= value <= h:
 
209
                return True
 
210
 
 
211
        return False
 
212
 
 
213
    def _iterator(self):
 
214
        for l,h in self.ranges:
 
215
            l = self.getnext(l-1)
 
216
            while l <= h:
 
217
                yield l
 
218
                l = self.getnext(l)
 
219
                if l is None:
 
220
                    break
 
221
 
 
222
    def __iter__(self):
 
223
        if self.ranges and self.ranges[0][0] is None:
 
224
            raise TypeError("Can't iterate; last value not set")
 
225
 
 
226
        return self._iterator()
 
227
 
 
228
    def __len__(self):
 
229
        res = 0
 
230
        for l, h in self.ranges:
 
231
            if l is None:
 
232
                raise TypeError("Can't size object; last value not set")
 
233
            res += (h - l) + 1
 
234
 
 
235
        return res
 
236
 
 
237
    def __str__(self):
 
238
        p = []
 
239
        for low, high in self.ranges:
 
240
            if low == high:
 
241
                if low is None:
 
242
                    p.append('*')
 
243
                else:
 
244
                    p.append(str(low))
 
245
            elif low is None:
 
246
                p.append('%d:*' % (high,))
 
247
            else:
 
248
                p.append('%d:%d' % (low, high))
 
249
        return ','.join(p)
 
250
 
 
251
    def __repr__(self):
 
252
        return '<MessageSet %s>' % (str(self),)
 
253
 
 
254
    def __eq__(self, other):
 
255
        if isinstance(other, MessageSet):
 
256
            return self.ranges == other.ranges
 
257
        return False
 
258
 
 
259
 
 
260
class LiteralString:
 
261
    def __init__(self, size, defered):
 
262
        self.size = size
 
263
        self.data = []
 
264
        self.defer = defered
 
265
 
 
266
    def write(self, data):
 
267
        self.size -= len(data)
 
268
        passon = None
 
269
        if self.size > 0:
 
270
            self.data.append(data)
 
271
        else:
 
272
            if self.size:
 
273
                data, passon = data[:self.size], data[self.size:]
 
274
            else:
 
275
                passon = ''
 
276
            if data:
 
277
                self.data.append(data)
 
278
        return passon
 
279
 
 
280
    def callback(self, line):
 
281
        """
 
282
        Call defered with data and rest of line
 
283
        """
 
284
        self.defer.callback((''.join(self.data), line))
 
285
 
 
286
class LiteralFile:
 
287
    _memoryFileLimit = 1024 * 1024 * 10
 
288
 
 
289
    def __init__(self, size, defered):
 
290
        self.size = size
 
291
        self.defer = defered
 
292
        if size > self._memoryFileLimit:
 
293
            self.data = tempfile.TemporaryFile()
 
294
        else:
 
295
            self.data = StringIO.StringIO()
 
296
 
 
297
    def write(self, data):
 
298
        self.size -= len(data)
 
299
        passon = None
 
300
        if self.size > 0:
 
301
            self.data.write(data)
 
302
        else:
 
303
            if self.size:
 
304
                data, passon = data[:self.size], data[self.size:]
 
305
            else:
 
306
                passon = ''
 
307
            if data:
 
308
                self.data.write(data)
 
309
        return passon
 
310
 
 
311
    def callback(self, line):
 
312
        """
 
313
        Call defered with data and rest of line
 
314
        """
 
315
        self.data.seek(0,0)
 
316
        self.defer.callback((self.data, line))
 
317
 
 
318
 
 
319
class WriteBuffer:
 
320
    """Buffer up a bunch of writes before sending them all to a transport at once.
 
321
    """
 
322
    def __init__(self, transport, size=8192):
 
323
        self.bufferSize = size
 
324
        self.transport = transport
 
325
        self._length = 0
 
326
        self._writes = []
 
327
 
 
328
    def write(self, s):
 
329
        self._length += len(s)
 
330
        self._writes.append(s)
 
331
        if self._length > self.bufferSize:
 
332
            self.flush()
 
333
 
 
334
    def flush(self):
 
335
        if self._writes:
 
336
            self.transport.writeSequence(self._writes)
 
337
            self._writes = []
 
338
            self._length = 0
 
339
 
 
340
 
 
341
class Command:
 
342
    _1_RESPONSES = ('CAPABILITY', 'FLAGS', 'LIST', 'LSUB', 'STATUS', 'SEARCH', 'NAMESPACE')
 
343
    _2_RESPONSES = ('EXISTS', 'EXPUNGE', 'FETCH', 'RECENT')
 
344
    _OK_RESPONSES = ('UIDVALIDITY', 'READ-WRITE', 'READ-ONLY', 'UIDNEXT', 'PERMANENTFLAGS')
 
345
    defer = None
 
346
 
 
347
    def __init__(self, command, args=None, wantResponse=(),
 
348
                 continuation=None, *contArgs, **contKw):
 
349
        self.command = command
 
350
        self.args = args
 
351
        self.wantResponse = wantResponse
 
352
        self.continuation = lambda x: continuation(x, *contArgs, **contKw)
 
353
        self.lines = []
 
354
 
 
355
    def format(self, tag):
 
356
        if self.args is None:
 
357
            return ' '.join((tag, self.command))
 
358
        return ' '.join((tag, self.command, self.args))
 
359
 
 
360
    def finish(self, lastLine, unusedCallback):
 
361
        send = []
 
362
        unuse = []
 
363
        for L in self.lines:
 
364
            names = parseNestedParens(L)
 
365
            N = len(names)
 
366
            if (N >= 1 and names[0] in self._1_RESPONSES or
 
367
                N >= 2 and names[0] == 'OK' and isinstance(names[1], types.ListType) and names[1][0] in self._OK_RESPONSES):
 
368
                send.append(L)
 
369
            elif N >= 3 and names[1] in self._2_RESPONSES:
 
370
                if isinstance(names[2], list) and len(names[2]) >= 1 and names[2][0] == 'FLAGS' and 'FLAGS' not in self.args:
 
371
                    unuse.append(L)
 
372
                else:
 
373
                    send.append(L)
 
374
            elif N >= 2 and names[1] in self._2_RESPONSES:
 
375
                send.append(L)
 
376
            else:
 
377
                unuse.append(L)
 
378
        d, self.defer = self.defer, None
 
379
        d.callback((send, lastLine))
 
380
        if unuse:
 
381
            unusedCallback(unuse)
 
382
 
 
383
class LOGINCredentials(cred.credentials.UsernamePassword):
 
384
    def __init__(self):
 
385
        self.challenges = ['Password\0', 'User Name\0']
 
386
        self.responses = ['password', 'username']
 
387
        cred.credentials.UsernamePassword.__init__(self, None, None)
 
388
 
 
389
    def getChallenge(self):
 
390
        return self.challenges.pop()
 
391
 
 
392
    def setResponse(self, response):
 
393
        setattr(self, self.responses.pop(), response)
 
394
 
 
395
    def moreChallenges(self):
 
396
        return bool(self.challenges)
 
397
 
 
398
class PLAINCredentials(cred.credentials.UsernamePassword):
 
399
    def __init__(self):
 
400
        cred.credentials.UsernamePassword.__init__(self, None, None)
 
401
 
 
402
    def getChallenge(self):
 
403
        return ''
 
404
 
 
405
    def setResponse(self, response):
 
406
        parts = response[:-1].split('\0', 1)
 
407
        if len(parts) != 2:
 
408
            raise IllegalClientResponse("Malformed Response - wrong number of parts")
 
409
        self.username, self.password = parts
 
410
 
 
411
    def moreChallenges(self):
 
412
        return False
 
413
 
 
414
class IMAP4Exception(Exception):
 
415
    def __init__(self, *args):
 
416
        Exception.__init__(self, *args)
 
417
 
 
418
class IllegalClientResponse(IMAP4Exception): pass
 
419
 
 
420
class IllegalOperation(IMAP4Exception): pass
 
421
 
 
422
class IllegalMailboxEncoding(IMAP4Exception): pass
 
423
 
 
424
class IMailboxListener(Interface):
 
425
    """Interface for objects interested in mailbox events"""
 
426
 
 
427
    def modeChanged(writeable):
 
428
        """Indicates that the write status of a mailbox has changed.
 
429
 
 
430
        @type writeable: C{bool}
 
431
        @param writeable: A true value if write is now allowed, false
 
432
        otherwise.
 
433
        """
 
434
 
 
435
    def flagsChanged(newFlags):
 
436
        """Indicates that the flags of one or more messages have changed.
 
437
 
 
438
        @type newFlags: C{dict}
 
439
        @param newFlags: A mapping of message identifiers to tuples of flags
 
440
        now set on that message.
 
441
        """
 
442
 
 
443
    def newMessages(exists, recent):
 
444
        """Indicates that the number of messages in a mailbox has changed.
 
445
 
 
446
        @type exists: C{int} or C{None}
 
447
        @param exists: The total number of messages now in this mailbox.
 
448
        If the total number of messages has not changed, this should be
 
449
        C{None}.
 
450
 
 
451
        @type recent: C{int}
 
452
        @param recent: The number of messages now flagged \\Recent.
 
453
        If the number of recent messages has not changed, this should be
 
454
        C{None}.
 
455
        """
 
456
 
 
457
class IMAP4Server(basic.LineReceiver, policies.TimeoutMixin):
 
458
    """
 
459
    Protocol implementation for an IMAP4rev1 server.
 
460
 
 
461
    The server can be in any of four states:
 
462
        - Non-authenticated
 
463
        - Authenticated
 
464
        - Selected
 
465
        - Logout
 
466
    """
 
467
    implements(IMailboxListener)
 
468
 
 
469
    # Identifier for this server software
 
470
    IDENT = 'Twisted IMAP4rev1 Ready'
 
471
 
 
472
    # Number of seconds before idle timeout
 
473
    # Initially 1 minute.  Raised to 30 minutes after login.
 
474
    timeOut = 60
 
475
 
 
476
    POSTAUTH_TIMEOUT = 60 * 30
 
477
 
 
478
    # Whether STARTTLS has been issued successfully yet or not.
 
479
    startedTLS = False
 
480
 
 
481
    # Whether our transport supports TLS
 
482
    canStartTLS = False
 
483
 
 
484
    # Mapping of tags to commands we have received
 
485
    tags = None
 
486
 
 
487
    # The object which will handle logins for us
 
488
    portal = None
 
489
 
 
490
    # The account object for this connection
 
491
    account = None
 
492
 
 
493
    # Logout callback
 
494
    _onLogout = None
 
495
 
 
496
    # The currently selected mailbox
 
497
    mbox = None
 
498
 
 
499
    # Command data to be processed when literal data is received
 
500
    _pendingLiteral = None
 
501
 
 
502
    # Maximum length to accept for a "short" string literal
 
503
    _literalStringLimit = 4096
 
504
 
 
505
    # IChallengeResponse factories for AUTHENTICATE command
 
506
    challengers = None
 
507
 
 
508
    state = 'unauth'
 
509
 
 
510
    parseState = 'command'
 
511
 
 
512
    def __init__(self, chal = None, contextFactory = None, scheduler = None):
 
513
        if chal is None:
 
514
            chal = {}
 
515
        self.challengers = chal
 
516
        self.ctx = contextFactory
 
517
        if scheduler is None:
 
518
            scheduler = iterateInReactor
 
519
        self._scheduler = scheduler
 
520
        self._queuedAsync = []
 
521
 
 
522
    def capabilities(self):
 
523
        cap = {'AUTH': self.challengers.keys()}
 
524
        if self.ctx and self.canStartTLS:
 
525
            if not self.startedTLS and interfaces.ISSLTransport(self.transport, None) is None:
 
526
                cap['LOGINDISABLED'] = None
 
527
                cap['STARTTLS'] = None
 
528
        cap['NAMESPACE'] = None
 
529
        cap['IDLE'] = None
 
530
        return cap
 
531
 
 
532
    def connectionMade(self):
 
533
        self.tags = {}
 
534
        self.canStartTLS = interfaces.ITLSTransport(self.transport, None) is not None
 
535
        self.setTimeout(self.timeOut)
 
536
        self.sendServerGreeting()
 
537
 
 
538
    def connectionLost(self, reason):
 
539
        self.setTimeout(None)
 
540
        if self._onLogout:
 
541
            self._onLogout()
 
542
            self._onLogout = None
 
543
 
 
544
    def timeoutConnection(self):
 
545
        self.sendLine('* BYE Autologout; connection idle too long')
 
546
        self.transport.loseConnection()
 
547
        if self.mbox:
 
548
            self.mbox.removeListener(self)
 
549
            cmbx = ICloseableMailbox(self.mbox, None)
 
550
            if cmbx is not None:
 
551
                maybeDeferred(cmbx.close).addErrback(log.err)
 
552
            self.mbox = None
 
553
        self.state = 'timeout'
 
554
 
 
555
    def rawDataReceived(self, data):
 
556
        self.resetTimeout()
 
557
        passon = self._pendingLiteral.write(data)
 
558
        if passon is not None:
 
559
            self.setLineMode(passon)
 
560
 
 
561
    # Avoid processing commands while buffers are being dumped to
 
562
    # our transport
 
563
    blocked = None
 
564
 
 
565
    def _unblock(self):
 
566
        commands = self.blocked
 
567
        self.blocked = None
 
568
        while commands and self.blocked is None:
 
569
            self.lineReceived(commands.pop(0))
 
570
        if self.blocked is not None:
 
571
            self.blocked.extend(commands)
 
572
 
 
573
#    def sendLine(self, line):
 
574
#        print 'C:', repr(line)
 
575
#        return basic.LineReceiver.sendLine(self, line)
 
576
 
 
577
    def lineReceived(self, line):
 
578
#        print 'S:', repr(line)
 
579
        if self.blocked is not None:
 
580
            self.blocked.append(line)
 
581
            return
 
582
 
 
583
        self.resetTimeout()
 
584
 
 
585
        f = getattr(self, 'parse_' + self.parseState)
 
586
        try:
 
587
            f(line)
 
588
        except Exception, e:
 
589
            self.sendUntaggedResponse('BAD Server error: ' + str(e))
 
590
            log.err()
 
591
 
 
592
    def parse_command(self, line):
 
593
        args = line.split(None, 2)
 
594
        rest = None
 
595
        if len(args) == 3:
 
596
            tag, cmd, rest = args
 
597
        elif len(args) == 2:
 
598
            tag, cmd = args
 
599
        elif len(args) == 1:
 
600
            tag = args[0]
 
601
            self.sendBadResponse(tag, 'Missing command')
 
602
            return None
 
603
        else:
 
604
            self.sendBadResponse(None, 'Null command')
 
605
            return None
 
606
 
 
607
        cmd = cmd.upper()
 
608
        try:
 
609
            return self.dispatchCommand(tag, cmd, rest)
 
610
        except IllegalClientResponse, e:
 
611
            self.sendBadResponse(tag, 'Illegal syntax: ' + str(e))
 
612
        except IllegalOperation, e:
 
613
            self.sendNegativeResponse(tag, 'Illegal operation: ' + str(e))
 
614
        except IllegalMailboxEncoding, e:
 
615
            self.sendNegativeResponse(tag, 'Illegal mailbox name: ' + str(e))
 
616
 
 
617
    def parse_pending(self, line):
 
618
        d = self._pendingLiteral
 
619
        self._pendingLiteral = None
 
620
        self.parseState = 'command'
 
621
        d.callback(line)
 
622
 
 
623
    def dispatchCommand(self, tag, cmd, rest, uid=None):
 
624
        f = self.lookupCommand(cmd)
 
625
        if f:
 
626
            fn = f[0]
 
627
            parseargs = f[1:]
 
628
            self.__doCommand(tag, fn, [self, tag], parseargs, rest, uid)
 
629
        else:
 
630
            self.sendBadResponse(tag, 'Unsupported command')
 
631
 
 
632
    def lookupCommand(self, cmd):
 
633
        return getattr(self, '_'.join((self.state, cmd.upper())), None)
 
634
 
 
635
    def __doCommand(self, tag, handler, args, parseargs, line, uid):
 
636
        for (i, arg) in enumerate(parseargs):
 
637
            if callable(arg):
 
638
                parseargs = parseargs[i+1:]
 
639
                maybeDeferred(arg, self, line).addCallback(
 
640
                    self.__cbDispatch, tag, handler, args,
 
641
                    parseargs, uid).addErrback(self.__ebDispatch, tag)
 
642
                return
 
643
            else:
 
644
                args.append(arg)
 
645
 
 
646
        if line:
 
647
            # Too many arguments
 
648
            raise IllegalClientResponse("Too many arguments for command: " + repr(line))
 
649
 
 
650
        if uid is not None:
 
651
            handler(uid=uid, *args)
 
652
        else:
 
653
            handler(*args)
 
654
 
 
655
    def __cbDispatch(self, (arg, rest), tag, fn, args, parseargs, uid):
 
656
        args.append(arg)
 
657
        self.__doCommand(tag, fn, args, parseargs, rest, uid)
 
658
 
 
659
    def __ebDispatch(self, failure, tag):
 
660
        if failure.check(IllegalClientResponse):
 
661
            self.sendBadResponse(tag, 'Illegal syntax: ' + str(failure.value))
 
662
        elif failure.check(IllegalOperation):
 
663
            self.sendNegativeResponse(tag, 'Illegal operation: ' +
 
664
                                      str(failure.value))
 
665
        elif failure.check(IllegalMailboxEncoding):
 
666
            self.sendNegativeResponse(tag, 'Illegal mailbox name: ' +
 
667
                                      str(failure.value))
 
668
        else:
 
669
            self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
 
670
            log.err(failure)
 
671
 
 
672
    def _stringLiteral(self, size):
 
673
        if size > self._literalStringLimit:
 
674
            raise IllegalClientResponse(
 
675
                "Literal too long! I accept at most %d octets" %
 
676
                (self._literalStringLimit,))
 
677
        d = defer.Deferred()
 
678
        self.parseState = 'pending'
 
679
        self._pendingLiteral = LiteralString(size, d)
 
680
        self.sendContinuationRequest('Ready for %d octets of text' % size)
 
681
        self.setRawMode()
 
682
        return d
 
683
 
 
684
    def _fileLiteral(self, size):
 
685
        d = defer.Deferred()
 
686
        self.parseState = 'pending'
 
687
        self._pendingLiteral = LiteralFile(size, d)
 
688
        self.sendContinuationRequest('Ready for %d octets of data' % size)
 
689
        self.setRawMode()
 
690
        return d
 
691
 
 
692
    def arg_astring(self, line):
 
693
        """
 
694
        Parse an astring from the line, return (arg, rest), possibly
 
695
        via a deferred (to handle literals)
 
696
        """
 
697
        line = line.strip()
 
698
        if not line:
 
699
            raise IllegalClientResponse("Missing argument")
 
700
        d = None
 
701
        arg, rest = None, None
 
702
        if line[0] == '"':
 
703
            try:
 
704
                spam, arg, rest = line.split('"',2)
 
705
                rest = rest[1:] # Strip space
 
706
            except ValueError:
 
707
                raise IllegalClientResponse("Unmatched quotes")
 
708
        elif line[0] == '{':
 
709
            # literal
 
710
            if line[-1] != '}':
 
711
                raise IllegalClientResponse("Malformed literal")
 
712
            try:
 
713
                size = int(line[1:-1])
 
714
            except ValueError:
 
715
                raise IllegalClientResponse("Bad literal size: " + line[1:-1])
 
716
            d = self._stringLiteral(size)
 
717
        else:
 
718
            arg = line.split(' ',1)
 
719
            if len(arg) == 1:
 
720
                arg.append('')
 
721
            arg, rest = arg
 
722
        return d or (arg, rest)
 
723
 
 
724
    # ATOM: Any CHAR except ( ) { % * " \ ] CTL SP (CHAR is 7bit)
 
725
    atomre = re.compile(r'(?P<atom>[^\](){%*"\\\x00-\x20\x80-\xff]+)( (?P<rest>.*$)|$)')
 
726
 
 
727
    def arg_atom(self, line):
 
728
        """
 
729
        Parse an atom from the line
 
730
        """
 
731
        if not line:
 
732
            raise IllegalClientResponse("Missing argument")
 
733
        m = self.atomre.match(line)
 
734
        if m:
 
735
            return m.group('atom'), m.group('rest')
 
736
        else:
 
737
            raise IllegalClientResponse("Malformed ATOM")
 
738
 
 
739
    def arg_plist(self, line):
 
740
        """
 
741
        Parse a (non-nested) parenthesised list from the line
 
742
        """
 
743
        if not line:
 
744
            raise IllegalClientResponse("Missing argument")
 
745
 
 
746
        if line[0] != "(":
 
747
            raise IllegalClientResponse("Missing parenthesis")
 
748
 
 
749
        i = line.find(")")
 
750
 
 
751
        if i == -1:
 
752
            raise IllegalClientResponse("Mismatched parenthesis")
 
753
 
 
754
        return (parseNestedParens(line[1:i],0), line[i+2:])
 
755
 
 
756
    def arg_literal(self, line):
 
757
        """
 
758
        Parse a literal from the line
 
759
        """
 
760
        if not line:
 
761
            raise IllegalClientResponse("Missing argument")
 
762
 
 
763
        if line[0] != '{':
 
764
            raise IllegalClientResponse("Missing literal")
 
765
 
 
766
        if line[-1] != '}':
 
767
            raise IllegalClientResponse("Malformed literal")
 
768
 
 
769
        try:
 
770
            size = int(line[1:-1])
 
771
        except ValueError:
 
772
            raise IllegalClientResponse("Bad literal size: " + line[1:-1])
 
773
 
 
774
        return self._fileLiteral(size)
 
775
 
 
776
    def arg_searchkeys(self, line):
 
777
        """
 
778
        searchkeys
 
779
        """
 
780
        query = parseNestedParens(line)
 
781
        # XXX Should really use list of search terms and parse into
 
782
        # a proper tree
 
783
 
 
784
        return (query, '')
 
785
 
 
786
    def arg_seqset(self, line):
 
787
        """
 
788
        sequence-set
 
789
        """
 
790
        rest = ''
 
791
        arg = line.split(' ',1)
 
792
        if len(arg) == 2:
 
793
            rest = arg[1]
 
794
        arg = arg[0]
 
795
 
 
796
        try:
 
797
            return (parseIdList(arg), rest)
 
798
        except IllegalIdentifierError, e:
 
799
            raise IllegalClientResponse("Bad message number " + str(e))
 
800
 
 
801
    def arg_fetchatt(self, line):
 
802
        """
 
803
        fetch-att
 
804
        """
 
805
        p = _FetchParser()
 
806
        p.parseString(line)
 
807
        return (p.result, '')
 
808
 
 
809
    def arg_flaglist(self, line):
 
810
        """
 
811
        Flag part of store-att-flag
 
812
        """
 
813
        flags = []
 
814
        if line[0] == '(':
 
815
            if line[-1] != ')':
 
816
                raise IllegalClientResponse("Mismatched parenthesis")
 
817
            line = line[1:-1]
 
818
 
 
819
        while line:
 
820
            m = self.atomre.search(line)
 
821
            if not m:
 
822
                raise IllegalClientResponse("Malformed flag")
 
823
            if line[0] == '\\' and m.start() == 1:
 
824
                flags.append('\\' + m.group('atom'))
 
825
            elif m.start() == 0:
 
826
                flags.append(m.group('atom'))
 
827
            else:
 
828
                raise IllegalClientResponse("Malformed flag")
 
829
            line = m.group('rest')
 
830
 
 
831
        return (flags, '')
 
832
 
 
833
    def arg_line(self, line):
 
834
        """
 
835
        Command line of UID command
 
836
        """
 
837
        return (line, '')
 
838
 
 
839
    def opt_plist(self, line):
 
840
        """
 
841
        Optional parenthesised list
 
842
        """
 
843
        if line.startswith('('):
 
844
            return self.arg_plist(line)
 
845
        else:
 
846
            return (None, line)
 
847
 
 
848
    def opt_datetime(self, line):
 
849
        """
 
850
        Optional date-time string
 
851
        """
 
852
        if line.startswith('"'):
 
853
            try:
 
854
                spam, date, rest = line.split('"',2)
 
855
            except IndexError:
 
856
                raise IllegalClientResponse("Malformed date-time")
 
857
            return (date, rest[1:])
 
858
        else:
 
859
            return (None, line)
 
860
 
 
861
    def opt_charset(self, line):
 
862
        """
 
863
        Optional charset of SEARCH command
 
864
        """
 
865
        if line[:7].upper() == 'CHARSET':
 
866
            arg = line.split(' ',2)
 
867
            if len(arg) == 1:
 
868
                raise IllegalClientResponse("Missing charset identifier")
 
869
            if len(arg) == 2:
 
870
                arg.append('')
 
871
            spam, arg, rest = arg
 
872
            return (arg, rest)
 
873
        else:
 
874
            return (None, line)
 
875
 
 
876
    def sendServerGreeting(self):
 
877
        msg = '[CAPABILITY %s] %s' % (' '.join(self.listCapabilities()), self.IDENT)
 
878
        self.sendPositiveResponse(message=msg)
 
879
 
 
880
    def sendBadResponse(self, tag = None, message = ''):
 
881
        self._respond('BAD', tag, message)
 
882
 
 
883
    def sendPositiveResponse(self, tag = None, message = ''):
 
884
        self._respond('OK', tag, message)
 
885
 
 
886
    def sendNegativeResponse(self, tag = None, message = ''):
 
887
        self._respond('NO', tag, message)
 
888
 
 
889
    def sendUntaggedResponse(self, message, async=False):
 
890
        if not async or (self.blocked is None):
 
891
            self._respond(message, None, None)
 
892
        else:
 
893
            self._queuedAsync.append(message)
 
894
 
 
895
    def sendContinuationRequest(self, msg = 'Ready for additional command text'):
 
896
        if msg:
 
897
            self.sendLine('+ ' + msg)
 
898
        else:
 
899
            self.sendLine('+')
 
900
 
 
901
    def _respond(self, state, tag, message):
 
902
        if state in ('OK', 'NO', 'BAD') and self._queuedAsync:
 
903
            lines = self._queuedAsync
 
904
            self._queuedAsync = []
 
905
            for msg in lines:
 
906
                self._respond(msg, None, None)
 
907
        if not tag:
 
908
            tag = '*'
 
909
        if message:
 
910
            self.sendLine(' '.join((tag, state, message)))
 
911
        else:
 
912
            self.sendLine(' '.join((tag, state)))
 
913
 
 
914
    def listCapabilities(self):
 
915
        caps = ['IMAP4rev1']
 
916
        for c, v in self.capabilities().iteritems():
 
917
            if v is None:
 
918
                caps.append(c)
 
919
            elif len(v):
 
920
                caps.extend([('%s=%s' % (c, cap)) for cap in v])
 
921
        return caps
 
922
 
 
923
    def do_CAPABILITY(self, tag):
 
924
        self.sendUntaggedResponse('CAPABILITY ' + ' '.join(self.listCapabilities()))
 
925
        self.sendPositiveResponse(tag, 'CAPABILITY completed')
 
926
 
 
927
    unauth_CAPABILITY = (do_CAPABILITY,)
 
928
    auth_CAPABILITY = unauth_CAPABILITY
 
929
    select_CAPABILITY = unauth_CAPABILITY
 
930
    logout_CAPABILITY = unauth_CAPABILITY
 
931
 
 
932
    def do_LOGOUT(self, tag):
 
933
        self.sendUntaggedResponse('BYE Nice talking to you')
 
934
        self.sendPositiveResponse(tag, 'LOGOUT successful')
 
935
        self.transport.loseConnection()
 
936
 
 
937
    unauth_LOGOUT = (do_LOGOUT,)
 
938
    auth_LOGOUT = unauth_LOGOUT
 
939
    select_LOGOUT = unauth_LOGOUT
 
940
    logout_LOGOUT = unauth_LOGOUT
 
941
 
 
942
    def do_NOOP(self, tag):
 
943
        self.sendPositiveResponse(tag, 'NOOP No operation performed')
 
944
 
 
945
    unauth_NOOP = (do_NOOP,)
 
946
    auth_NOOP = unauth_NOOP
 
947
    select_NOOP = unauth_NOOP
 
948
    logout_NOOP = unauth_NOOP
 
949
 
 
950
    def do_AUTHENTICATE(self, tag, args):
 
951
        args = args.upper().strip()
 
952
        if args not in self.challengers:
 
953
            self.sendNegativeResponse(tag, 'AUTHENTICATE method unsupported')
 
954
        else:
 
955
            self.authenticate(self.challengers[args](), tag)
 
956
 
 
957
    unauth_AUTHENTICATE = (do_AUTHENTICATE, arg_atom)
 
958
 
 
959
    def authenticate(self, chal, tag):
 
960
        if self.portal is None:
 
961
            self.sendNegativeResponse(tag, 'Temporary authentication failure')
 
962
            return
 
963
 
 
964
        self._setupChallenge(chal, tag)
 
965
 
 
966
    def _setupChallenge(self, chal, tag):
 
967
        try:
 
968
            challenge = chal.getChallenge()
 
969
        except Exception, e:
 
970
            self.sendBadResponse(tag, 'Server error: ' + str(e))
 
971
        else:
 
972
            coded = base64.encodestring(challenge)[:-1]
 
973
            self.parseState = 'pending'
 
974
            self._pendingLiteral = defer.Deferred()
 
975
            self.sendContinuationRequest(coded)
 
976
            self._pendingLiteral.addCallback(self.__cbAuthChunk, chal, tag)
 
977
            self._pendingLiteral.addErrback(self.__ebAuthChunk, tag)
 
978
 
 
979
    def __cbAuthChunk(self, result, chal, tag):
 
980
        try:
 
981
            uncoded = base64.decodestring(result)
 
982
        except binascii.Error:
 
983
            raise IllegalClientResponse("Malformed Response - not base64")
 
984
 
 
985
        chal.setResponse(uncoded)
 
986
        if chal.moreChallenges():
 
987
            self._setupChallenge(chal, tag)
 
988
        else:
 
989
            self.portal.login(chal, None, IAccount).addCallbacks(
 
990
                self.__cbAuthResp,
 
991
                self.__ebAuthResp,
 
992
                (tag,), None, (tag,), None
 
993
            )
 
994
 
 
995
    def __cbAuthResp(self, (iface, avatar, logout), tag):
 
996
        assert iface is IAccount, "IAccount is the only supported interface"
 
997
        self.account = avatar
 
998
        self.state = 'auth'
 
999
        self._onLogout = logout
 
1000
        self.sendPositiveResponse(tag, 'Authentication successful')
 
1001
        self.setTimeout(self.POSTAUTH_TIMEOUT)
 
1002
 
 
1003
    def __ebAuthResp(self, failure, tag):
 
1004
        if failure.check(cred.error.UnauthorizedLogin):
 
1005
            self.sendNegativeResponse(tag, 'Authentication failed: unauthorized')
 
1006
        elif failure.check(cred.error.UnhandledCredentials):
 
1007
            self.sendNegativeResponse(tag, 'Authentication failed: server misconfigured')
 
1008
        else:
 
1009
            self.sendBadResponse(tag, 'Server error: login failed unexpectedly')
 
1010
            log.err(failure)
 
1011
 
 
1012
    def __ebAuthChunk(self, failure, tag):
 
1013
        self.sendNegativeResponse(tag, 'Authentication failed: ' + str(failure.value))
 
1014
 
 
1015
    def do_STARTTLS(self, tag):
 
1016
        if self.startedTLS:
 
1017
            self.sendNegativeResponse(tag, 'TLS already negotiated')
 
1018
        elif self.ctx and self.canStartTLS:
 
1019
            self.sendPositiveResponse(tag, 'Begin TLS negotiation now')
 
1020
            self.transport.startTLS(self.ctx)
 
1021
            self.startedTLS = True
 
1022
            self.challengers = self.challengers.copy()
 
1023
            if 'LOGIN' not in self.challengers:
 
1024
                self.challengers['LOGIN'] = LOGINCredentials
 
1025
            if 'PLAIN' not in self.challengers:
 
1026
                self.challengers['PLAIN'] = PLAINCredentials
 
1027
        else:
 
1028
            self.sendNegativeResponse(tag, 'TLS not available')
 
1029
 
 
1030
    unauth_STARTTLS = (do_STARTTLS,)
 
1031
 
 
1032
    def do_LOGIN(self, tag, user, passwd):
 
1033
        if 'LOGINDISABLED' in self.capabilities():
 
1034
            self.sendBadResponse(tag, 'LOGIN is disabled before STARTTLS')
 
1035
            return
 
1036
 
 
1037
        maybeDeferred(self.authenticateLogin, user, passwd
 
1038
            ).addCallback(self.__cbLogin, tag
 
1039
            ).addErrback(self.__ebLogin, tag
 
1040
            )
 
1041
 
 
1042
    unauth_LOGIN = (do_LOGIN, arg_astring, arg_astring)
 
1043
 
 
1044
    def authenticateLogin(self, user, passwd):
 
1045
        """Lookup the account associated with the given parameters
 
1046
 
 
1047
        Override this method to define the desired authentication behavior.
 
1048
 
 
1049
        The default behavior is to defer authentication to C{self.portal}
 
1050
        if it is not None, or to deny the login otherwise.
 
1051
 
 
1052
        @type user: C{str}
 
1053
        @param user: The username to lookup
 
1054
 
 
1055
        @type passwd: C{str}
 
1056
        @param passwd: The password to login with
 
1057
        """
 
1058
        if self.portal:
 
1059
            return self.portal.login(
 
1060
                cred.credentials.UsernamePassword(user, passwd),
 
1061
                None, IAccount
 
1062
            )
 
1063
        raise cred.error.UnauthorizedLogin()
 
1064
 
 
1065
    def __cbLogin(self, (iface, avatar, logout), tag):
 
1066
        if iface is not IAccount:
 
1067
            self.sendBadResponse(tag, 'Server error: login returned unexpected value')
 
1068
            log.err("__cbLogin called with %r, IAccount expected" % (iface,))
 
1069
        else:
 
1070
            self.account = avatar
 
1071
            self._onLogout = logout
 
1072
            self.sendPositiveResponse(tag, 'LOGIN succeeded')
 
1073
            self.state = 'auth'
 
1074
            self.setTimeout(self.POSTAUTH_TIMEOUT)
 
1075
 
 
1076
    def __ebLogin(self, failure, tag):
 
1077
        if failure.check(cred.error.UnauthorizedLogin):
 
1078
            self.sendNegativeResponse(tag, 'LOGIN failed')
 
1079
        else:
 
1080
            self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
 
1081
            log.err(failure)
 
1082
 
 
1083
    def do_NAMESPACE(self, tag):
 
1084
        personal = public = shared = None
 
1085
        np = INamespacePresenter(self.account, None)
 
1086
        if np is not None:
 
1087
            personal = np.getPersonalNamespaces()
 
1088
            public = np.getSharedNamespaces()
 
1089
            shared = np.getSharedNamespaces()
 
1090
        self.sendUntaggedResponse('NAMESPACE ' + collapseNestedLists([personal, public, shared]))
 
1091
        self.sendPositiveResponse(tag, "NAMESPACE command completed")
 
1092
 
 
1093
    auth_NAMESPACE = (do_NAMESPACE,)
 
1094
    select_NAMESPACE = auth_NAMESPACE
 
1095
 
 
1096
    def _parseMbox(self, name):
 
1097
        if isinstance(name, unicode):
 
1098
            return name
 
1099
        try:
 
1100
            return name.decode('imap4-utf-7')
 
1101
        except:
 
1102
            log.err()
 
1103
            raise IllegalMailboxEncoding(name)
 
1104
 
 
1105
    def _selectWork(self, tag, name, rw, cmdName):
 
1106
        if self.mbox:
 
1107
            self.mbox.removeListener(self)
 
1108
            cmbx = ICloseableMailbox(self.mbox, None)
 
1109
            if cmbx is not None:
 
1110
                maybeDeferred(cmbx.close).addErrback(log.err)
 
1111
            self.mbox = None
 
1112
            self.state = 'auth'
 
1113
 
 
1114
        name = self._parseMbox(name)
 
1115
        maybeDeferred(self.account.select, self._parseMbox(name), rw
 
1116
            ).addCallback(self._cbSelectWork, cmdName, tag
 
1117
            ).addErrback(self._ebSelectWork, cmdName, tag
 
1118
            )
 
1119
 
 
1120
    def _ebSelectWork(self, failure, cmdName, tag):
 
1121
        self.sendBadResponse(tag, "%s failed: Server error" % (cmdName,))
 
1122
        log.err(failure)
 
1123
 
 
1124
    def _cbSelectWork(self, mbox, cmdName, tag):
 
1125
        if mbox is None:
 
1126
            self.sendNegativeResponse(tag, 'No such mailbox')
 
1127
            return
 
1128
        if '\\noselect' in [s.lower() for s in mbox.getFlags()]:
 
1129
            self.sendNegativeResponse(tag, 'Mailbox cannot be selected')
 
1130
            return
 
1131
 
 
1132
        flags = mbox.getFlags()
 
1133
        self.sendUntaggedResponse(str(mbox.getMessageCount()) + ' EXISTS')
 
1134
        self.sendUntaggedResponse(str(mbox.getRecentCount()) + ' RECENT')
 
1135
        self.sendUntaggedResponse('FLAGS (%s)' % ' '.join(flags))
 
1136
        self.sendPositiveResponse(None, '[UIDVALIDITY %d]' % mbox.getUIDValidity())
 
1137
 
 
1138
        s = mbox.isWriteable() and 'READ-WRITE' or 'READ-ONLY'
 
1139
        mbox.addListener(self)
 
1140
        self.sendPositiveResponse(tag, '[%s] %s successful' % (s, cmdName))
 
1141
        self.state = 'select'
 
1142
        self.mbox = mbox
 
1143
 
 
1144
    auth_SELECT = ( _selectWork, arg_astring, 1, 'SELECT' )
 
1145
    select_SELECT = auth_SELECT
 
1146
 
 
1147
    auth_EXAMINE = ( _selectWork, arg_astring, 0, 'EXAMINE' )
 
1148
    select_EXAMINE = auth_EXAMINE
 
1149
 
 
1150
 
 
1151
    def do_IDLE(self, tag):
 
1152
        self.sendContinuationRequest(None)
 
1153
        self.parseTag = tag
 
1154
        self.lastState = self.parseState
 
1155
        self.parseState = 'idle'
 
1156
 
 
1157
    def parse_idle(self, *args):
 
1158
        self.parseState = self.lastState
 
1159
        del self.lastState
 
1160
        self.sendPositiveResponse(self.parseTag, "IDLE terminated")
 
1161
        del self.parseTag
 
1162
 
 
1163
    select_IDLE = ( do_IDLE, )
 
1164
    auth_IDLE = select_IDLE
 
1165
 
 
1166
 
 
1167
    def do_CREATE(self, tag, name):
 
1168
        name = self._parseMbox(name)
 
1169
        try:
 
1170
            result = self.account.create(name)
 
1171
        except MailboxException, c:
 
1172
            self.sendNegativeResponse(tag, str(c))
 
1173
        except:
 
1174
            self.sendBadResponse(tag, "Server error encountered while creating mailbox")
 
1175
            log.err()
 
1176
        else:
 
1177
            if result:
 
1178
                self.sendPositiveResponse(tag, 'Mailbox created')
 
1179
            else:
 
1180
                self.sendNegativeResponse(tag, 'Mailbox not created')
 
1181
 
 
1182
    auth_CREATE = (do_CREATE, arg_astring)
 
1183
    select_CREATE = auth_CREATE
 
1184
 
 
1185
    def do_DELETE(self, tag, name):
 
1186
        name = self._parseMbox(name)
 
1187
        if name.lower() == 'inbox':
 
1188
            self.sendNegativeResponse(tag, 'You cannot delete the inbox')
 
1189
            return
 
1190
        try:
 
1191
            self.account.delete(name)
 
1192
        except MailboxException, m:
 
1193
            self.sendNegativeResponse(tag, str(m))
 
1194
        except:
 
1195
            self.sendBadResponse(tag, "Server error encountered while deleting mailbox")
 
1196
            log.err()
 
1197
        else:
 
1198
            self.sendPositiveResponse(tag, 'Mailbox deleted')
 
1199
 
 
1200
    auth_DELETE = (do_DELETE, arg_astring)
 
1201
    select_DELETE = auth_DELETE
 
1202
 
 
1203
    def do_RENAME(self, tag, oldname, newname):
 
1204
        oldname, newname = [self._parseMbox(n) for n in oldname, newname]
 
1205
        if oldname.lower() == 'inbox' or newname.lower() == 'inbox':
 
1206
            self.sendNegativeResponse(tag, 'You cannot rename the inbox, or rename another mailbox to inbox.')
 
1207
            return
 
1208
        try:
 
1209
            self.account.rename(oldname, newname)
 
1210
        except TypeError:
 
1211
            self.sendBadResponse(tag, 'Invalid command syntax')
 
1212
        except MailboxException, m:
 
1213
            self.sendNegativeResponse(tag, str(m))
 
1214
        except:
 
1215
            self.sendBadResponse(tag, "Server error encountered while renaming mailbox")
 
1216
            log.err()
 
1217
        else:
 
1218
            self.sendPositiveResponse(tag, 'Mailbox renamed')
 
1219
 
 
1220
    auth_RENAME = (do_RENAME, arg_astring, arg_astring)
 
1221
    select_RENAME = auth_RENAME
 
1222
 
 
1223
    def do_SUBSCRIBE(self, tag, name):
 
1224
        name = self._parseMbox(name)
 
1225
        try:
 
1226
            self.account.subscribe(name)
 
1227
        except MailboxException, m:
 
1228
            self.sendNegativeResponse(tag, str(m))
 
1229
        except:
 
1230
            self.sendBadResponse(tag, "Server error encountered while subscribing to mailbox")
 
1231
            log.err()
 
1232
        else:
 
1233
            self.sendPositiveResponse(tag, 'Subscribed')
 
1234
 
 
1235
    auth_SUBSCRIBE = (do_SUBSCRIBE, arg_astring)
 
1236
    select_SUBSCRIBE = auth_SUBSCRIBE
 
1237
 
 
1238
    def do_UNSUBSCRIBE(self, tag, name):
 
1239
        name = self._parseMbox(name)
 
1240
        try:
 
1241
            self.account.unsubscribe(name)
 
1242
        except MailboxException, m:
 
1243
            self.sendNegativeResponse(tag, str(m))
 
1244
        except:
 
1245
            self.sendBadResponse(tag, "Server error encountered while unsubscribing from mailbox")
 
1246
            log.err()
 
1247
        else:
 
1248
            self.sendPositiveResponse(tag, 'Unsubscribed')
 
1249
 
 
1250
    auth_UNSUBSCRIBE = (do_UNSUBSCRIBE, arg_astring)
 
1251
    select_UNSUBSCRIBE = auth_UNSUBSCRIBE
 
1252
 
 
1253
    def _listWork(self, tag, ref, mbox, sub, cmdName):
 
1254
        mbox = self._parseMbox(mbox)
 
1255
        maybeDeferred(self.account.listMailboxes, ref, mbox
 
1256
            ).addCallback(self._cbListWork, tag, sub, cmdName
 
1257
            ).addErrback(self._ebListWork, tag
 
1258
            )
 
1259
 
 
1260
    def _cbListWork(self, mailboxes, tag, sub, cmdName):
 
1261
        for (name, box) in mailboxes:
 
1262
            if not sub or self.account.isSubscribed(name):
 
1263
                flags = box.getFlags()
 
1264
                delim = box.getHierarchicalDelimiter()
 
1265
                resp = (DontQuoteMe(cmdName), map(DontQuoteMe, flags), delim, name.encode('imap4-utf-7'))
 
1266
                self.sendUntaggedResponse(collapseNestedLists(resp))
 
1267
        self.sendPositiveResponse(tag, '%s completed' % (cmdName,))
 
1268
 
 
1269
    def _ebListWork(self, failure, tag):
 
1270
        self.sendBadResponse(tag, "Server error encountered while listing mailboxes.")
 
1271
        log.err(failure)
 
1272
 
 
1273
    auth_LIST = (_listWork, arg_astring, arg_astring, 0, 'LIST')
 
1274
    select_LIST = auth_LIST
 
1275
 
 
1276
    auth_LSUB = (_listWork, arg_astring, arg_astring, 1, 'LSUB')
 
1277
    select_LSUB = auth_LSUB
 
1278
 
 
1279
    def do_STATUS(self, tag, mailbox, names):
 
1280
        mailbox = self._parseMbox(mailbox)
 
1281
        maybeDeferred(self.account.select, mailbox, 0
 
1282
            ).addCallback(self._cbStatusGotMailbox, tag, mailbox, names
 
1283
            ).addErrback(self._ebStatusGotMailbox, tag
 
1284
            )
 
1285
 
 
1286
    def _cbStatusGotMailbox(self, mbox, tag, mailbox, names):
 
1287
        if mbox:
 
1288
            maybeDeferred(mbox.requestStatus, names).addCallbacks(
 
1289
                self.__cbStatus, self.__ebStatus,
 
1290
                (tag, mailbox), None, (tag, mailbox), None
 
1291
            )
 
1292
        else:
 
1293
            self.sendNegativeResponse(tag, "Could not open mailbox")
 
1294
 
 
1295
    def _ebStatusGotMailbox(self, failure, tag):
 
1296
        self.sendBadResponse(tag, "Server error encountered while opening mailbox.")
 
1297
        log.err(failure)
 
1298
 
 
1299
    auth_STATUS = (do_STATUS, arg_astring, arg_plist)
 
1300
    select_STATUS = auth_STATUS
 
1301
 
 
1302
    def __cbStatus(self, status, tag, box):
 
1303
        line = ' '.join(['%s %s' % x for x in status.iteritems()])
 
1304
        self.sendUntaggedResponse('STATUS %s (%s)' % (box, line))
 
1305
        self.sendPositiveResponse(tag, 'STATUS complete')
 
1306
 
 
1307
    def __ebStatus(self, failure, tag, box):
 
1308
        self.sendBadResponse(tag, 'STATUS %s failed: %s' % (box, str(failure.value)))
 
1309
 
 
1310
    def do_APPEND(self, tag, mailbox, flags, date, message):
 
1311
        mailbox = self._parseMbox(mailbox)
 
1312
        maybeDeferred(self.account.select, mailbox
 
1313
            ).addCallback(self._cbAppendGotMailbox, tag, flags, date, message
 
1314
            ).addErrback(self._ebAppendGotMailbox, tag
 
1315
            )
 
1316
 
 
1317
    def _cbAppendGotMailbox(self, mbox, tag, flags, date, message):
 
1318
        if not mbox:
 
1319
            self.sendNegativeResponse(tag, '[TRYCREATE] No such mailbox')
 
1320
            return
 
1321
 
 
1322
        d = mbox.addMessage(message, flags, date)
 
1323
        d.addCallback(self.__cbAppend, tag, mbox)
 
1324
        d.addErrback(self.__ebAppend, tag)
 
1325
 
 
1326
    def _ebAppendGotMailbox(self, failure, tag):
 
1327
        self.sendBadResponse(tag, "Server error encountered while opening mailbox.")
 
1328
        log.err(failure)
 
1329
 
 
1330
    auth_APPEND = (do_APPEND, arg_astring, opt_plist, opt_datetime,
 
1331
                   arg_literal)
 
1332
    select_APPEND = auth_APPEND
 
1333
 
 
1334
    def __cbAppend(self, result, tag, mbox):
 
1335
        self.sendUntaggedResponse('%d EXISTS' % mbox.getMessageCount())
 
1336
        self.sendPositiveResponse(tag, 'APPEND complete')
 
1337
 
 
1338
    def __ebAppend(self, failure, tag):
 
1339
        self.sendBadResponse(tag, 'APPEND failed: ' + str(failure.value))
 
1340
 
 
1341
    def do_CHECK(self, tag):
 
1342
        d = self.checkpoint()
 
1343
        if d is None:
 
1344
            self.__cbCheck(None, tag)
 
1345
        else:
 
1346
            d.addCallbacks(
 
1347
                self.__cbCheck,
 
1348
                self.__ebCheck,
 
1349
                callbackArgs=(tag,),
 
1350
                errbackArgs=(tag,)
 
1351
            )
 
1352
    select_CHECK = (do_CHECK,)
 
1353
 
 
1354
    def __cbCheck(self, result, tag):
 
1355
        self.sendPositiveResponse(tag, 'CHECK completed')
 
1356
 
 
1357
    def __ebCheck(self, failure, tag):
 
1358
        self.sendBadResponse(tag, 'CHECK failed: ' + str(failure.value))
 
1359
 
 
1360
    def checkpoint(self):
 
1361
        """Called when the client issues a CHECK command.
 
1362
 
 
1363
        This should perform any checkpoint operations required by the server.
 
1364
        It may be a long running operation, but may not block.  If it returns
 
1365
        a deferred, the client will only be informed of success (or failure)
 
1366
        when the deferred's callback (or errback) is invoked.
 
1367
        """
 
1368
        return None
 
1369
 
 
1370
    def do_CLOSE(self, tag):
 
1371
        d = None
 
1372
        if self.mbox.isWriteable():
 
1373
            d = maybeDeferred(self.mbox.expunge)
 
1374
        cmbx = ICloseableMailbox(self.mbox, None)
 
1375
        if cmbx is not None:
 
1376
            if d is not None:
 
1377
                d.addCallback(lambda result: cmbx.close())
 
1378
            else:
 
1379
                d = maybeDeferred(cmbx.close)
 
1380
        if d is not None:
 
1381
            d.addCallbacks(self.__cbClose, self.__ebClose, (tag,), None, (tag,), None)
 
1382
        else:
 
1383
            self.__cbClose(None, tag)
 
1384
 
 
1385
    select_CLOSE = (do_CLOSE,)
 
1386
 
 
1387
    def __cbClose(self, result, tag):
 
1388
        self.sendPositiveResponse(tag, 'CLOSE completed')
 
1389
        self.mbox.removeListener(self)
 
1390
        self.mbox = None
 
1391
        self.state = 'auth'
 
1392
 
 
1393
    def __ebClose(self, failure, tag):
 
1394
        self.sendBadResponse(tag, 'CLOSE failed: ' + str(failure.value))
 
1395
 
 
1396
    def do_EXPUNGE(self, tag):
 
1397
        if self.mbox.isWriteable():
 
1398
            maybeDeferred(self.mbox.expunge).addCallbacks(
 
1399
                self.__cbExpunge, self.__ebExpunge, (tag,), None, (tag,), None
 
1400
            )
 
1401
        else:
 
1402
            self.sendNegativeResponse(tag, 'EXPUNGE ignored on read-only mailbox')
 
1403
 
 
1404
    select_EXPUNGE = (do_EXPUNGE,)
 
1405
 
 
1406
    def __cbExpunge(self, result, tag):
 
1407
        for e in result:
 
1408
            self.sendUntaggedResponse('%d EXPUNGE' % e)
 
1409
        self.sendPositiveResponse(tag, 'EXPUNGE completed')
 
1410
 
 
1411
    def __ebExpunge(self, failure, tag):
 
1412
        self.sendBadResponse(tag, 'EXPUNGE failed: ' + str(failure.value))
 
1413
        log.err(failure)
 
1414
 
 
1415
    def do_SEARCH(self, tag, charset, query, uid=0):
 
1416
        sm = ISearchableMailbox(self.mbox, None)
 
1417
        if sm is not None:
 
1418
            maybeDeferred(sm.search, query, uid=uid).addCallbacks(
 
1419
                self.__cbSearch, self.__ebSearch,
 
1420
                (tag, self.mbox, uid), None, (tag,), None
 
1421
            )
 
1422
        else:
 
1423
            s = parseIdList('1:*')
 
1424
            maybeDeferred(self.mbox.fetch, s, uid=uid).addCallbacks(
 
1425
                self.__cbManualSearch, self.__ebSearch,
 
1426
                (tag, self.mbox, query, uid), None, (tag,), None
 
1427
            )
 
1428
 
 
1429
    select_SEARCH = (do_SEARCH, opt_charset, arg_searchkeys)
 
1430
 
 
1431
    def __cbSearch(self, result, tag, mbox, uid):
 
1432
        if uid:
 
1433
            result = map(mbox.getUID, result)
 
1434
        ids = ' '.join([str(i) for i in result])
 
1435
        self.sendUntaggedResponse('SEARCH ' + ids)
 
1436
        self.sendPositiveResponse(tag, 'SEARCH completed')
 
1437
 
 
1438
    def __cbManualSearch(self, result, tag, mbox, query, uid, searchResults = None):
 
1439
        if searchResults is None:
 
1440
            searchResults = []
 
1441
        i = 0
 
1442
        for (i, (id, msg)) in zip(range(5), result):
 
1443
            if self.searchFilter(query, id, msg):
 
1444
                if uid:
 
1445
                    searchResults.append(str(msg.getUID()))
 
1446
                else:
 
1447
                    searchResults.append(str(id))
 
1448
        if i == 4:
 
1449
            from twisted.internet import reactor
 
1450
            reactor.callLater(0, self.__cbManualSearch, result, tag, mbox, query, uid, searchResults)
 
1451
        else:
 
1452
            if searchResults:
 
1453
                self.sendUntaggedResponse('SEARCH ' + ' '.join(searchResults))
 
1454
            self.sendPositiveResponse(tag, 'SEARCH completed')
 
1455
 
 
1456
    def searchFilter(self, query, id, msg):
 
1457
        while query:
 
1458
            if not self.singleSearchStep(query, id, msg):
 
1459
                return False
 
1460
        return True
 
1461
 
 
1462
    def singleSearchStep(self, query, id, msg):
 
1463
        q = query.pop(0)
 
1464
        if isinstance(q, list):
 
1465
            if not self.searchFilter(q, id, msg):
 
1466
                return False
 
1467
        else:
 
1468
            c = q.upper()
 
1469
            f = getattr(self, 'search_' + c)
 
1470
            if f:
 
1471
                if not f(query, id, msg):
 
1472
                    return False
 
1473
            else:
 
1474
                # IMAP goes *out of its way* to be complex
 
1475
                # Sequence sets to search should be specified
 
1476
                # with a command, like EVERYTHING ELSE.
 
1477
                try:
 
1478
                    m = parseIdList(c)
 
1479
                except:
 
1480
                    log.err('Unknown search term: ' + c)
 
1481
                else:
 
1482
                    if id not in m:
 
1483
                        return False
 
1484
        return True
 
1485
 
 
1486
    def search_ALL(self, query, id, msg):
 
1487
        return True
 
1488
 
 
1489
    def search_ANSWERED(self, query, id, msg):
 
1490
        return '\\Answered' in msg.getFlags()
 
1491
 
 
1492
    def search_BCC(self, query, id, msg):
 
1493
        bcc = msg.getHeaders(False, 'bcc').get('bcc', '')
 
1494
        return bcc.lower().find(query.pop(0).lower()) != -1
 
1495
 
 
1496
    def search_BEFORE(self, query, id, msg):
 
1497
        date = parseTime(query.pop(0))
 
1498
        return rfc822.parsedate(msg.getInternalDate()) < date
 
1499
 
 
1500
    def search_BODY(self, query, id, msg):
 
1501
        body = query.pop(0).lower()
 
1502
        return text.strFile(body, msg.getBodyFile(), False)
 
1503
 
 
1504
    def search_CC(self, query, id, msg):
 
1505
        cc = msg.getHeaders(False, 'cc').get('cc', '')
 
1506
        return cc.lower().find(query.pop(0).lower()) != -1
 
1507
 
 
1508
    def search_DELETED(self, query, id, msg):
 
1509
        return '\\Deleted' in msg.getFlags()
 
1510
 
 
1511
    def search_DRAFT(self, query, id, msg):
 
1512
        return '\\Draft' in msg.getFlags()
 
1513
 
 
1514
    def search_FLAGGED(self, query, id, msg):
 
1515
        return '\\Flagged' in msg.getFlags()
 
1516
 
 
1517
    def search_FROM(self, query, id, msg):
 
1518
        fm = msg.getHeaders(False, 'from').get('from', '')
 
1519
        return fm.lower().find(query.pop(0).lower()) != -1
 
1520
 
 
1521
    def search_HEADER(self, query, id, msg):
 
1522
        hdr = query.pop(0).lower()
 
1523
        hdr = msg.getHeaders(False, hdr).get(hdr, '')
 
1524
        return hdr.lower().find(query.pop(0).lower()) != -1
 
1525
 
 
1526
    def search_KEYWORD(self, query, id, msg):
 
1527
        query.pop(0)
 
1528
        return False
 
1529
 
 
1530
    def search_LARGER(self, query, id, msg):
 
1531
        return int(query.pop(0)) < msg.getSize()
 
1532
 
 
1533
    def search_NEW(self, query, id, msg):
 
1534
        return '\\Recent' in msg.getFlags() and '\\Seen' not in msg.getFlags()
 
1535
 
 
1536
    def search_NOT(self, query, id, msg):
 
1537
        return not self.singleSearchStep(query, id, msg)
 
1538
 
 
1539
    def search_OLD(self, query, id, msg):
 
1540
        return '\\Recent' not in msg.getFlags()
 
1541
 
 
1542
    def search_ON(self, query, id, msg):
 
1543
        date = parseTime(query.pop(0))
 
1544
        return rfc822.parsedate(msg.getInternalDate()) == date
 
1545
 
 
1546
    def search_OR(self, query, id, msg):
 
1547
        a = self.singleSearchStep(query, id, msg)
 
1548
        b = self.singleSearchStep(query, id, msg)
 
1549
        return a or b
 
1550
 
 
1551
    def search_RECENT(self, query, id, msg):
 
1552
        return '\\Recent' in msg.getFlags()
 
1553
 
 
1554
    def search_SEEN(self, query, id, msg):
 
1555
        return '\\Seen' in msg.getFlags()
 
1556
 
 
1557
    def search_SENTBEFORE(self, query, id, msg):
 
1558
        date = msg.getHeader(False, 'date').get('date', '')
 
1559
        date = rfc822.parsedate(date)
 
1560
        return date < parseTime(query.pop(0))
 
1561
 
 
1562
    def search_SENTON(self, query, id, msg):
 
1563
        date = msg.getHeader(False, 'date').get('date', '')
 
1564
        date = rfc822.parsedate(date)
 
1565
        return date[:3] == parseTime(query.pop(0))[:3]
 
1566
 
 
1567
    def search_SENTSINCE(self, query, id, msg):
 
1568
        date = msg.getHeader(False, 'date').get('date', '')
 
1569
        date = rfc822.parsedate(date)
 
1570
        return date > parseTime(query.pop(0))
 
1571
 
 
1572
    def search_SINCE(self, query, id, msg):
 
1573
        date = parseTime(query.pop(0))
 
1574
        return rfc822.parsedate(msg.getInternalDate()) > date
 
1575
 
 
1576
    def search_SMALLER(self, query, id, msg):
 
1577
        return int(query.pop(0)) > msg.getSize()
 
1578
 
 
1579
    def search_SUBJECT(self, query, id, msg):
 
1580
        subj = msg.getHeaders(False, 'subject').get('subject', '')
 
1581
        return subj.lower().find(query.pop(0).lower()) != -1
 
1582
 
 
1583
    def search_TEXT(self, query, id, msg):
 
1584
        # XXX - This must search headers too
 
1585
        body = query.pop(0).lower()
 
1586
        return text.strFile(body, msg.getBodyFile(), False)
 
1587
 
 
1588
    def search_TO(self, query, id, msg):
 
1589
        to = msg.getHeaders(False, 'to').get('to', '')
 
1590
        return to.lower().find(query.pop(0).lower()) != -1
 
1591
 
 
1592
    def search_UID(self, query, id, msg):
 
1593
        c = query.pop(0)
 
1594
        m = parseIdList(c)
 
1595
        return msg.getUID() in m
 
1596
 
 
1597
    def search_UNANSWERED(self, query, id, msg):
 
1598
        return '\\Answered' not in msg.getFlags()
 
1599
 
 
1600
    def search_UNDELETED(self, query, id, msg):
 
1601
        return '\\Deleted' not in msg.getFlags()
 
1602
 
 
1603
    def search_UNDRAFT(self, query, id, msg):
 
1604
        return '\\Draft' not in msg.getFlags()
 
1605
 
 
1606
    def search_UNFLAGGED(self, query, id, msg):
 
1607
        return '\\Flagged' not in msg.getFlags()
 
1608
 
 
1609
    def search_UNKEYWORD(self, query, id, msg):
 
1610
        query.pop(0)
 
1611
        return False
 
1612
 
 
1613
    def search_UNSEEN(self, query, id, msg):
 
1614
        return '\\Seen' not in msg.getFlags()
 
1615
 
 
1616
    def __ebSearch(self, failure, tag):
 
1617
        self.sendBadResponse(tag, 'SEARCH failed: ' + str(failure.value))
 
1618
        log.err(failure)
 
1619
 
 
1620
    def do_FETCH(self, tag, messages, query, uid=0):
 
1621
        if query:
 
1622
            maybeDeferred(self.mbox.fetch, messages, uid=uid
 
1623
                ).addCallback(iter
 
1624
                ).addCallback(self.__cbFetch, tag, query, uid
 
1625
                ).addErrback(self.__ebFetch, tag
 
1626
                )
 
1627
        else:
 
1628
            self.sendPositiveResponse(tag, 'FETCH complete')
 
1629
 
 
1630
    select_FETCH = (do_FETCH, arg_seqset, arg_fetchatt)
 
1631
 
 
1632
    def __cbFetch(self, results, tag, query, uid):
 
1633
        if self.blocked is None:
 
1634
            self.blocked = []
 
1635
            self._oldTimeout = self.setTimeout(None)
 
1636
        try:
 
1637
            id, msg = results.next()
 
1638
        except StopIteration:
 
1639
            # All results have been processed, deliver completion notification.
 
1640
            self.sendPositiveResponse(tag, 'FETCH completed')
 
1641
 
 
1642
            # The idle timeout was suspended while we delivered results,
 
1643
            # restore it now.
 
1644
            self.setTimeout(self._oldTimeout)
 
1645
            del self._oldTimeout
 
1646
 
 
1647
            # Instance state is now consistent again (ie, it is as though
 
1648
            # the fetch command never ran), so allow any pending blocked
 
1649
            # commands to execute.
 
1650
            self._unblock()
 
1651
        else:
 
1652
            self.spewMessage(id, msg, query, uid
 
1653
                ).addCallback(lambda _: self.__cbFetch(results, tag, query, uid)
 
1654
                ).addErrback(self.__ebSpewMessage
 
1655
                )
 
1656
 
 
1657
    def __ebSpewMessage(self, failure):
 
1658
        # This indicates a programming error.
 
1659
        # There's no reliable way to indicate anything to the client, since we
 
1660
        # may have already written an arbitrary amount of data in response to
 
1661
        # the command.
 
1662
        log.err(failure)
 
1663
        self.transport.loseConnection()
 
1664
 
 
1665
    def spew_envelope(self, id, msg, _w=None, _f=None):
 
1666
        if _w is None:
 
1667
            _w = self.transport.write
 
1668
        _w('ENVELOPE ' + collapseNestedLists([getEnvelope(msg)]))
 
1669
 
 
1670
    def spew_flags(self, id, msg, _w=None, _f=None):
 
1671
        if _w is None:
 
1672
            _w = self.transport.write
 
1673
        _w('FLAGS ' + '(%s)' % (' '.join(msg.getFlags())))
 
1674
 
 
1675
    def spew_internaldate(self, id, msg, _w=None, _f=None):
 
1676
        if _w is None:
 
1677
            _w = self.transport.write
 
1678
        idate = msg.getInternalDate()
 
1679
        ttup = rfc822.parsedate_tz(idate)
 
1680
        if ttup is None:
 
1681
            log.msg("%d:%r: unpareseable internaldate: %r" % (id, msg, idate))
 
1682
            raise IMAP4Exception("Internal failure generating INTERNALDATE")
 
1683
 
 
1684
        odate = time.strftime("%d-%b-%Y %H:%M:%S ", ttup[:9])
 
1685
        if ttup[9] is None:
 
1686
            odate = odate + "+0000"
 
1687
        else:
 
1688
            if ttup[9] >= 0:
 
1689
                sign = "+"
 
1690
            else:
 
1691
                sign = "-"
 
1692
            odate = odate + sign + string.zfill(str(((abs(ttup[9]) / 3600) * 100 + (abs(ttup[9]) % 3600) / 60)), 4)
 
1693
        _w('INTERNALDATE ' + _quote(odate))
 
1694
 
 
1695
    def spew_rfc822header(self, id, msg, _w=None, _f=None):
 
1696
        if _w is None:
 
1697
            _w = self.transport.write
 
1698
        hdrs = _formatHeaders(msg.getHeaders(True))
 
1699
        _w('RFC822.HEADER ' + _literal(hdrs))
 
1700
 
 
1701
    def spew_rfc822text(self, id, msg, _w=None, _f=None):
 
1702
        if _w is None:
 
1703
            _w = self.transport.write
 
1704
        _w('RFC822.TEXT ')
 
1705
        _f()
 
1706
        return FileProducer(msg.getBodyFile()
 
1707
            ).beginProducing(self.transport
 
1708
            )
 
1709
 
 
1710
    def spew_rfc822size(self, id, msg, _w=None, _f=None):
 
1711
        if _w is None:
 
1712
            _w = self.transport.write
 
1713
        _w('RFC822.SIZE ' + str(msg.getSize()))
 
1714
 
 
1715
    def spew_rfc822(self, id, msg, _w=None, _f=None):
 
1716
        if _w is None:
 
1717
            _w = self.transport.write
 
1718
        _w('RFC822 ')
 
1719
        _f()
 
1720
        mf = IMessageFile(msg, None)
 
1721
        if mf is not None:
 
1722
            return FileProducer(mf.open()
 
1723
                ).beginProducing(self.transport
 
1724
                )
 
1725
        return MessageProducer(msg, None, self._scheduler
 
1726
            ).beginProducing(self.transport
 
1727
            )
 
1728
 
 
1729
    def spew_uid(self, id, msg, _w=None, _f=None):
 
1730
        if _w is None:
 
1731
            _w = self.transport.write
 
1732
        _w('UID ' + str(msg.getUID()))
 
1733
 
 
1734
    def spew_bodystructure(self, id, msg, _w=None, _f=None):
 
1735
        _w('BODYSTRUCTURE ' + collapseNestedLists([getBodyStructure(msg, True)]))
 
1736
 
 
1737
    def spew_body(self, part, id, msg, _w=None, _f=None):
 
1738
        if _w is None:
 
1739
            _w = self.transport.write
 
1740
        for p in part.part:
 
1741
            if msg.isMultipart():
 
1742
                msg = msg.getSubPart(p)
 
1743
            elif p > 0:
 
1744
                # Non-multipart messages have an implicit first part but no
 
1745
                # other parts - reject any request for any other part.
 
1746
                raise TypeError("Requested subpart of non-multipart message")
 
1747
 
 
1748
        if part.header:
 
1749
            hdrs = msg.getHeaders(part.header.negate, *part.header.fields)
 
1750
            hdrs = _formatHeaders(hdrs)
 
1751
            _w(str(part) + ' ' + _literal(hdrs))
 
1752
        elif part.text:
 
1753
            _w(str(part) + ' ')
 
1754
            _f()
 
1755
            return FileProducer(msg.getBodyFile()
 
1756
                ).beginProducing(self.transport
 
1757
                )
 
1758
        elif part.mime:
 
1759
            hdrs = _formatHeaders(msg.getHeaders(True))
 
1760
            _w(str(part) + ' ' + _literal(hdrs))
 
1761
        elif part.empty:
 
1762
            _w(str(part) + ' ')
 
1763
            _f()
 
1764
            if part.part:
 
1765
                return FileProducer(msg.getBodyFile()
 
1766
                    ).beginProducing(self.transport
 
1767
                    )
 
1768
            else:
 
1769
                mf = IMessageFile(msg, None)
 
1770
                if mf is not None:
 
1771
                    return FileProducer(mf.open()).beginProducing(self.transport)
 
1772
                return MessageProducer(msg, None, self._scheduler).beginProducing(self.transport)
 
1773
            
 
1774
        else:
 
1775
            _w('BODY ' + collapseNestedLists([getBodyStructure(msg)]))
 
1776
 
 
1777
    def spewMessage(self, id, msg, query, uid):
 
1778
        wbuf = WriteBuffer(self.transport)
 
1779
        write = wbuf.write
 
1780
        flush = wbuf.flush
 
1781
        def start():
 
1782
            write('* %d FETCH (' % (id,))
 
1783
        def finish():
 
1784
            write(')\r\n')
 
1785
        def space():
 
1786
            write(' ')
 
1787
 
 
1788
        def spew():
 
1789
            seenUID = False
 
1790
            start()
 
1791
            for part in query:
 
1792
                if part.type == 'uid':
 
1793
                    seenUID = True
 
1794
                if part.type == 'body':
 
1795
                    yield self.spew_body(part, id, msg, write, flush)
 
1796
                else:
 
1797
                    f = getattr(self, 'spew_' + part.type)
 
1798
                    yield f(id, msg, write, flush)
 
1799
                if part is not query[-1]:
 
1800
                    space()
 
1801
            if uid and not seenUID:
 
1802
                space()
 
1803
                yield self.spew_uid(id, msg, write, flush)
 
1804
            finish()
 
1805
            flush()
 
1806
        return self._scheduler(spew())
 
1807
 
 
1808
    def __ebFetch(self, failure, tag):
 
1809
        log.err(failure)
 
1810
        self.sendBadResponse(tag, 'FETCH failed: ' + str(failure.value))
 
1811
 
 
1812
    def do_STORE(self, tag, messages, mode, flags, uid=0):
 
1813
        mode = mode.upper()
 
1814
        silent = mode.endswith('SILENT')
 
1815
        if mode.startswith('+'):
 
1816
            mode = 1
 
1817
        elif mode.startswith('-'):
 
1818
            mode = -1
 
1819
        else:
 
1820
            mode = 0
 
1821
 
 
1822
        maybeDeferred(self.mbox.store, messages, flags, mode, uid=uid).addCallbacks(
 
1823
            self.__cbStore, self.__ebStore, (tag, self.mbox, uid, silent), None, (tag,), None
 
1824
        )
 
1825
 
 
1826
    select_STORE = (do_STORE, arg_seqset, arg_atom, arg_flaglist)
 
1827
 
 
1828
    def __cbStore(self, result, tag, mbox, uid, silent):
 
1829
        if result and not silent:
 
1830
              for (k, v) in result.iteritems():
 
1831
                  if uid:
 
1832
                      uidstr = ' UID %d' % mbox.getUID(k)
 
1833
                  else:
 
1834
                      uidstr = ''
 
1835
                  self.sendUntaggedResponse('%d FETCH (FLAGS (%s)%s)' %
 
1836
                                            (k, ' '.join(v), uidstr))
 
1837
        self.sendPositiveResponse(tag, 'STORE completed')
 
1838
 
 
1839
    def __ebStore(self, failure, tag):
 
1840
        self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
 
1841
 
 
1842
    def do_COPY(self, tag, messages, mailbox, uid=0):
 
1843
        mailbox = self._parseMbox(mailbox)
 
1844
        maybeDeferred(self.account.select, mailbox
 
1845
            ).addCallback(self._cbCopySelectedMailbox, tag, messages, mailbox, uid
 
1846
            ).addErrback(self._ebCopySelectedMailbox, tag
 
1847
            )
 
1848
    select_COPY = (do_COPY, arg_seqset, arg_astring)
 
1849
 
 
1850
    def _cbCopySelectedMailbox(self, mbox, tag, messages, mailbox, uid):
 
1851
        if not mbox:
 
1852
            self.sendNegativeResponse(tag, 'No such mailbox: ' + mailbox)
 
1853
        else:
 
1854
            maybeDeferred(self.mbox.fetch, messages, uid
 
1855
                ).addCallback(self.__cbCopy, tag, mbox
 
1856
                ).addCallback(self.__cbCopied, tag, mbox
 
1857
                ).addErrback(self.__ebCopy, tag
 
1858
                )
 
1859
 
 
1860
    def _ebCopySelectedMailbox(self, failure, tag):
 
1861
        self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
 
1862
 
 
1863
    def __cbCopy(self, messages, tag, mbox):
 
1864
        # XXX - This should handle failures with a rollback or something
 
1865
        addedDeferreds = []
 
1866
        addedIDs = []
 
1867
        failures = []
 
1868
 
 
1869
        fastCopyMbox = IMessageCopier(mbox, None)
 
1870
        for (id, msg) in messages:
 
1871
            if fastCopyMbox is not None:
 
1872
                d = maybeDeferred(fastCopyMbox.copy, msg)
 
1873
                addedDeferreds.append(d)
 
1874
                continue
 
1875
 
 
1876
            # XXX - The following should be an implementation of IMessageCopier.copy
 
1877
            # on an IMailbox->IMessageCopier adapter.
 
1878
 
 
1879
            flags = msg.getFlags()
 
1880
            date = msg.getInternalDate()
 
1881
 
 
1882
            body = IMessageFile(msg, None)
 
1883
            if body is not None:
 
1884
                bodyFile = body.open()
 
1885
                d = maybeDeferred(mbox.addMessage, bodyFile, flags, date)
 
1886
            else:
 
1887
                def rewind(f):
 
1888
                    f.seek(0)
 
1889
                    return f
 
1890
                buffer = tempfile.TemporaryFile()
 
1891
                d = MessageProducer(msg, buffer, self._scheduler
 
1892
                    ).beginProducing(None
 
1893
                    ).addCallback(lambda _, b=buffer, f=flags, d=date: mbox.addMessage(rewind(b), f, d)
 
1894
                    )
 
1895
            addedDeferreds.append(d)
 
1896
        return defer.DeferredList(addedDeferreds)
 
1897
 
 
1898
    def __cbCopied(self, deferredIds, tag, mbox):
 
1899
        ids = []
 
1900
        failures = []
 
1901
        for (status, result) in deferredIds:
 
1902
            if status:
 
1903
                ids.append(result)
 
1904
            else:
 
1905
                failures.append(result.value)
 
1906
        if failures:
 
1907
            self.sendNegativeResponse(tag, '[ALERT] Some messages were not copied')
 
1908
        else:
 
1909
            self.sendPositiveResponse(tag, 'COPY completed')
 
1910
 
 
1911
    def __ebCopy(self, failure, tag):
 
1912
        self.sendBadResponse(tag, 'COPY failed:' + str(failure.value))
 
1913
        log.err(failure)
 
1914
 
 
1915
    def do_UID(self, tag, command, line):
 
1916
        command = command.upper()
 
1917
 
 
1918
        if command not in ('COPY', 'FETCH', 'STORE', 'SEARCH'):
 
1919
            raise IllegalClientResponse(command)
 
1920
 
 
1921
        self.dispatchCommand(tag, command, line, uid=1)
 
1922
 
 
1923
    select_UID = (do_UID, arg_atom, arg_line)
 
1924
    #
 
1925
    # IMailboxListener implementation
 
1926
    #
 
1927
    def modeChanged(self, writeable):
 
1928
        if writeable:
 
1929
            self.sendUntaggedResponse(message='[READ-WRITE]', async=True)
 
1930
        else:
 
1931
            self.sendUntaggedResponse(message='[READ-ONLY]', async=True)
 
1932
 
 
1933
    def flagsChanged(self, newFlags):
 
1934
        for (mId, flags) in newFlags.iteritems():
 
1935
            msg = '%d FETCH (FLAGS (%s))' % (mId, ' '.join(flags))
 
1936
            self.sendUntaggedResponse(msg, async=True)
 
1937
 
 
1938
    def newMessages(self, exists, recent):
 
1939
        if exists is not None:
 
1940
            self.sendUntaggedResponse('%d EXISTS' % exists, async=True)
 
1941
        if recent is not None:
 
1942
            self.sendUntaggedResponse('%d RECENT' % recent, async=True)
 
1943
 
 
1944
 
 
1945
class UnhandledResponse(IMAP4Exception): pass
 
1946
 
 
1947
class NegativeResponse(IMAP4Exception): pass
 
1948
 
 
1949
class NoSupportedAuthentication(IMAP4Exception):
 
1950
    def __init__(self, serverSupports, clientSupports):
 
1951
        IMAP4Exception.__init__(self, 'No supported authentication schemes available')
 
1952
        self.serverSupports = serverSupports
 
1953
        self.clientSupports = clientSupports
 
1954
 
 
1955
    def __str__(self):
 
1956
        return (IMAP4Exception.__str__(self)
 
1957
            + ': Server supports %r, client supports %r'
 
1958
            % (self.serverSupports, self.clientSupports))
 
1959
 
 
1960
class IllegalServerResponse(IMAP4Exception): pass
 
1961
 
 
1962
TIMEOUT_ERROR = error.TimeoutError()
 
1963
 
 
1964
class IMAP4Client(basic.LineReceiver, policies.TimeoutMixin):
 
1965
    """IMAP4 client protocol implementation
 
1966
 
 
1967
    @ivar state: A string representing the state the connection is currently
 
1968
    in.
 
1969
    """
 
1970
    implements(IMailboxListener)
 
1971
 
 
1972
    tags = None
 
1973
    waiting = None
 
1974
    queued = None
 
1975
    tagID = 1
 
1976
    state = None
 
1977
 
 
1978
    startedTLS = False
 
1979
 
 
1980
    # Number of seconds to wait before timing out a connection.
 
1981
    # If the number is <= 0 no timeout checking will be performed.
 
1982
    timeout = 0
 
1983
 
 
1984
    # Capabilities are not allowed to change during the session
 
1985
    # So cache the first response and use that for all later
 
1986
    # lookups
 
1987
    _capCache = None
 
1988
 
 
1989
    _memoryFileLimit = 1024 * 1024 * 10
 
1990
 
 
1991
    # Authentication is pluggable.  This maps names to IClientAuthentication
 
1992
    # objects.
 
1993
    authenticators = None
 
1994
 
 
1995
    STATUS_CODES = ('OK', 'NO', 'BAD', 'PREAUTH', 'BYE')
 
1996
 
 
1997
    STATUS_TRANSFORMATIONS = {
 
1998
        'MESSAGES': int, 'RECENT': int, 'UNSEEN': int
 
1999
    }
 
2000
 
 
2001
    context = None
 
2002
 
 
2003
    def __init__(self, contextFactory = None):
 
2004
        self.tags = {}
 
2005
        self.queued = []
 
2006
        self.authenticators = {}
 
2007
        self.context = contextFactory
 
2008
 
 
2009
        self._tag = None
 
2010
        self._parts = None
 
2011
        self._lastCmd = None
 
2012
 
 
2013
    def registerAuthenticator(self, auth):
 
2014
        """Register a new form of authentication
 
2015
 
 
2016
        When invoking the authenticate() method of IMAP4Client, the first
 
2017
        matching authentication scheme found will be used.  The ordering is
 
2018
        that in which the server lists support authentication schemes.
 
2019
 
 
2020
        @type auth: Implementor of C{IClientAuthentication}
 
2021
        @param auth: The object to use to perform the client
 
2022
        side of this authentication scheme.
 
2023
        """
 
2024
        self.authenticators[auth.getName().upper()] = auth
 
2025
 
 
2026
    def rawDataReceived(self, data):
 
2027
        if self.timeout > 0:
 
2028
            self.resetTimeout()
 
2029
 
 
2030
        self._pendingSize -= len(data)
 
2031
        if self._pendingSize > 0:
 
2032
            self._pendingBuffer.write(data)
 
2033
        else:
 
2034
            passon = ''
 
2035
            if self._pendingSize < 0:
 
2036
                data, passon = data[:self._pendingSize], data[self._pendingSize:]
 
2037
            self._pendingBuffer.write(data)
 
2038
            rest = self._pendingBuffer
 
2039
            self._pendingBuffer = None
 
2040
            self._pendingSize = None
 
2041
            rest.seek(0, 0)
 
2042
            self._parts.append(rest.read())
 
2043
            self.setLineMode(passon.lstrip('\r\n'))
 
2044
 
 
2045
#    def sendLine(self, line):
 
2046
#        print 'S:', repr(line)
 
2047
#        return basic.LineReceiver.sendLine(self, line)
 
2048
 
 
2049
    def _setupForLiteral(self, rest, octets):
 
2050
        self._pendingBuffer = self.messageFile(octets)
 
2051
        self._pendingSize = octets
 
2052
        if self._parts is None:
 
2053
            self._parts = [rest, '\r\n']
 
2054
        else:
 
2055
            self._parts.extend([rest, '\r\n'])
 
2056
        self.setRawMode()
 
2057
 
 
2058
    def connectionMade(self):
 
2059
        if self.timeout > 0:
 
2060
            self.setTimeout(self.timeout)
 
2061
 
 
2062
    def connectionLost(self, reason):
 
2063
        """We are no longer connected"""
 
2064
        if self.timeout > 0:
 
2065
            self.setTimeout(None)
 
2066
        if self.queued is not None:
 
2067
            queued = self.queued
 
2068
            self.queued = None
 
2069
            for cmd in queued:
 
2070
                cmd.defer.errback(reason)
 
2071
        if self.tags is not None:
 
2072
            tags = self.tags
 
2073
            self.tags = None
 
2074
            for cmd in tags.itervalues():
 
2075
                if cmd is not None and cmd.defer is not None:
 
2076
                    cmd.defer.errback(reason)
 
2077
 
 
2078
 
 
2079
    def lineReceived(self, line):
 
2080
#        print 'C: ' + repr(line)
 
2081
        if self.timeout > 0:
 
2082
            self.resetTimeout()
 
2083
 
 
2084
        lastPart = line.rfind(' ')
 
2085
        if lastPart != -1:
 
2086
            lastPart = line[lastPart + 1:]
 
2087
            if lastPart.startswith('{') and lastPart.endswith('}'):
 
2088
                # It's a literal a-comin' in
 
2089
                try:
 
2090
                    octets = int(lastPart[1:-1])
 
2091
                except ValueError:
 
2092
                    raise IllegalServerResponse(line)
 
2093
                if self._parts is None:
 
2094
                    self._tag, parts = line.split(None, 1)
 
2095
                else:
 
2096
                    parts = line
 
2097
                self._setupForLiteral(parts, octets)
 
2098
                return
 
2099
 
 
2100
        if self._parts is None:
 
2101
            # It isn't a literal at all
 
2102
            self._regularDispatch(line)
 
2103
        else:
 
2104
            # If an expression is in progress, no tag is required here
 
2105
            # Since we didn't find a literal indicator, this expression
 
2106
            # is done.
 
2107
            self._parts.append(line)
 
2108
            tag, rest = self._tag, ''.join(self._parts)
 
2109
            self._tag = self._parts = None
 
2110
            self.dispatchCommand(tag, rest)
 
2111
 
 
2112
    def timeoutConnection(self):
 
2113
        if self._lastCmd and self._lastCmd.defer is not None:
 
2114
            d, self._lastCmd.defer = self._lastCmd.defer, None
 
2115
            d.errback(TIMEOUT_ERROR)
 
2116
 
 
2117
        if self.queued:
 
2118
            for cmd in self.queued:
 
2119
                if cmd.defer is not None:
 
2120
                    d, cmd.defer = cmd.defer, d
 
2121
                    d.errback(TIMEOUT_ERROR)
 
2122
 
 
2123
        self.transport.loseConnection()
 
2124
 
 
2125
    def _regularDispatch(self, line):
 
2126
        parts = line.split(None, 1)
 
2127
        if len(parts) != 2:
 
2128
            parts.append('')
 
2129
        tag, rest = parts
 
2130
        self.dispatchCommand(tag, rest)
 
2131
 
 
2132
    def messageFile(self, octets):
 
2133
        """Create a file to which an incoming message may be written.
 
2134
 
 
2135
        @type octets: C{int}
 
2136
        @param octets: The number of octets which will be written to the file
 
2137
 
 
2138
        @rtype: Any object which implements C{write(string)} and
 
2139
        C{seek(int, int)}
 
2140
        @return: A file-like object
 
2141
        """
 
2142
        if octets > self._memoryFileLimit:
 
2143
            return tempfile.TemporaryFile()
 
2144
        else:
 
2145
            return StringIO.StringIO()
 
2146
 
 
2147
    def makeTag(self):
 
2148
        tag = '%0.4X' % self.tagID
 
2149
        self.tagID += 1
 
2150
        return tag
 
2151
 
 
2152
    def dispatchCommand(self, tag, rest):
 
2153
        if self.state is None:
 
2154
            f = self.response_UNAUTH
 
2155
        else:
 
2156
            f = getattr(self, 'response_' + self.state.upper(), None)
 
2157
        if f:
 
2158
            try:
 
2159
                f(tag, rest)
 
2160
            except:
 
2161
                log.err()
 
2162
                self.transport.loseConnection()
 
2163
        else:
 
2164
            log.err("Cannot dispatch: %s, %s, %s" % (self.state, tag, rest))
 
2165
            self.transport.loseConnection()
 
2166
 
 
2167
    def response_UNAUTH(self, tag, rest):
 
2168
        if self.state is None:
 
2169
            # Server greeting, this is
 
2170
            status, rest = rest.split(None, 1)
 
2171
            if status.upper() == 'OK':
 
2172
                self.state = 'unauth'
 
2173
            elif status.upper() == 'PREAUTH':
 
2174
                self.state = 'auth'
 
2175
            else:
 
2176
                # XXX - This is rude.
 
2177
                self.transport.loseConnection()
 
2178
                raise IllegalServerResponse(tag + ' ' + rest)
 
2179
 
 
2180
            b, e = rest.find('['), rest.find(']')
 
2181
            if b != -1 and e != -1:
 
2182
                self.serverGreeting(self.__cbCapabilities(([rest[b:e]], None)))
 
2183
            else:
 
2184
                self.serverGreeting(None)
 
2185
        else:
 
2186
            self._defaultHandler(tag, rest)
 
2187
 
 
2188
    def response_AUTH(self, tag, rest):
 
2189
        self._defaultHandler(tag, rest)
 
2190
 
 
2191
    def _defaultHandler(self, tag, rest):
 
2192
        if tag == '*' or tag == '+':
 
2193
            if not self.waiting:
 
2194
                self._extraInfo([rest])
 
2195
            else:
 
2196
                cmd = self.tags[self.waiting]
 
2197
                if tag == '+':
 
2198
                    cmd.continuation(rest)
 
2199
                else:
 
2200
                    cmd.lines.append(rest)
 
2201
        else:
 
2202
            try:
 
2203
                cmd = self.tags[tag]
 
2204
            except KeyError:
 
2205
                # XXX - This is rude.
 
2206
                self.transport.loseConnection()
 
2207
                raise IllegalServerResponse(tag + ' ' + rest)
 
2208
            else:
 
2209
                status, line = rest.split(None, 1)
 
2210
                if status == 'OK':
 
2211
                    # Give them this last line, too
 
2212
                    cmd.finish(rest, self._extraInfo)
 
2213
                else:
 
2214
                    cmd.defer.errback(IMAP4Exception(line))
 
2215
                del self.tags[tag]
 
2216
                self.waiting = None
 
2217
                self._flushQueue()
 
2218
 
 
2219
    def _flushQueue(self):
 
2220
        if self.queued:
 
2221
            cmd = self.queued.pop(0)
 
2222
            t = self.makeTag()
 
2223
            self.tags[t] = cmd
 
2224
            self.sendLine(cmd.format(t))
 
2225
            self.waiting = t
 
2226
 
 
2227
    def _extraInfo(self, lines):
 
2228
        # XXX - This is terrible.
 
2229
        # XXX - Also, this should collapse temporally proximate calls into single
 
2230
        #       invocations of IMailboxListener methods, where possible.
 
2231
        flags = {}
 
2232
        recent = exists = None
 
2233
        for L in lines:
 
2234
            if L.find('EXISTS') != -1:
 
2235
                exists = int(L.split()[0])
 
2236
            elif L.find('RECENT') != -1:
 
2237
                recent = int(L.split()[0])
 
2238
            elif L.find('READ-ONLY') != -1:
 
2239
                self.modeChanged(0)
 
2240
            elif L.find('READ-WRITE') != -1:
 
2241
                self.modeChanged(1)
 
2242
            elif L.find('FETCH') != -1:
 
2243
                for (mId, fetched) in self.__cbFetch(([L], None)).iteritems():
 
2244
                    sum = []
 
2245
                    for f in fetched.get('FLAGS', []):
 
2246
                        sum.append(f)
 
2247
                    flags.setdefault(mId, []).extend(sum)
 
2248
            else:
 
2249
                log.msg('Unhandled unsolicited response: ' + repr(L))
 
2250
        if flags:
 
2251
            self.flagsChanged(flags)
 
2252
        if recent is not None or exists is not None:
 
2253
            self.newMessages(exists, recent)
 
2254
 
 
2255
    def sendCommand(self, cmd):
 
2256
        cmd.defer = defer.Deferred()
 
2257
        if self.waiting:
 
2258
            self.queued.append(cmd)
 
2259
            return cmd.defer
 
2260
        t = self.makeTag()
 
2261
        self.tags[t] = cmd
 
2262
        self.sendLine(cmd.format(t))
 
2263
        self.waiting = t
 
2264
        self._lastCmd = cmd
 
2265
        return cmd.defer
 
2266
 
 
2267
    def getCapabilities(self, useCache=1):
 
2268
        """Request the capabilities available on this server.
 
2269
 
 
2270
        This command is allowed in any state of connection.
 
2271
 
 
2272
        @type useCache: C{bool}
 
2273
        @param useCache: Specify whether to use the capability-cache or to
 
2274
        re-retrieve the capabilities from the server.  Server capabilities
 
2275
        should never change, so for normal use, this flag should never be
 
2276
        false.
 
2277
 
 
2278
        @rtype: C{Deferred}
 
2279
        @return: A deferred whose callback will be invoked with a
 
2280
        dictionary mapping capability types to lists of supported
 
2281
        mechanisms, or to None if a support list is not applicable.
 
2282
        """
 
2283
        if useCache and self._capCache is not None:
 
2284
            return defer.succeed(self._capCache)
 
2285
        cmd = 'CAPABILITY'
 
2286
        resp = ('CAPABILITY',)
 
2287
        d = self.sendCommand(Command(cmd, wantResponse=resp))
 
2288
        d.addCallback(self.__cbCapabilities)
 
2289
        return d
 
2290
 
 
2291
    def __cbCapabilities(self, (lines, tagline)):
 
2292
        caps = {}
 
2293
        for rest in lines:
 
2294
            rest = rest.split()[1:]
 
2295
            for cap in rest:
 
2296
                eq = cap.find('=')
 
2297
                if eq == -1:
 
2298
                    caps[cap] = None
 
2299
                else:
 
2300
                    caps.setdefault(cap[:eq], []).append(cap[eq+1:])
 
2301
        self._capCache = caps
 
2302
        return caps
 
2303
 
 
2304
    def logout(self):
 
2305
        """Inform the server that we are done with the connection.
 
2306
 
 
2307
        This command is allowed in any state of connection.
 
2308
 
 
2309
        @rtype: C{Deferred}
 
2310
        @return: A deferred whose callback will be invoked with None
 
2311
        when the proper server acknowledgement has been received.
 
2312
        """
 
2313
        d = self.sendCommand(Command('LOGOUT', wantResponse=('BYE',)))
 
2314
        d.addCallback(self.__cbLogout)
 
2315
        return d
 
2316
 
 
2317
    def __cbLogout(self, (lines, tagline)):
 
2318
        self.transport.loseConnection()
 
2319
        # We don't particularly care what the server said
 
2320
        return None
 
2321
 
 
2322
 
 
2323
    def noop(self):
 
2324
        """Perform no operation.
 
2325
 
 
2326
        This command is allowed in any state of connection.
 
2327
 
 
2328
        @rtype: C{Deferred}
 
2329
        @return: A deferred whose callback will be invoked with a list
 
2330
        of untagged status updates the server responds with.
 
2331
        """
 
2332
        d = self.sendCommand(Command('NOOP'))
 
2333
        d.addCallback(self.__cbNoop)
 
2334
        return d
 
2335
 
 
2336
    def __cbNoop(self, (lines, tagline)):
 
2337
        # Conceivable, this is elidable.
 
2338
        # It is, afterall, a no-op.
 
2339
        return lines
 
2340
 
 
2341
    def startTLS(self, contextFactory=None):
 
2342
        """
 
2343
        Initiates a 'STARTTLS' request and negotiates the TLS / SSL
 
2344
        Handshake.
 
2345
 
 
2346
        @param contextFactory: The TLS / SSL Context Factory to
 
2347
        leverage.  If the contextFactory is None the IMAP4Client will
 
2348
        either use the current TLS / SSL Context Factory or attempt to
 
2349
        create a new one.
 
2350
 
 
2351
        @type contextFactory: C{ssl.ClientContextFactory}
 
2352
 
 
2353
        @return: A Deferred which fires when the transport has been
 
2354
        secured according to the given contextFactory, or which fails
 
2355
        if the transport cannot be secured.
 
2356
        """
 
2357
        assert not self.startedTLS, "Client and Server are currently communicating via TLS"
 
2358
 
 
2359
        if contextFactory is None:
 
2360
            contextFactory = self._getContextFactory()
 
2361
 
 
2362
        if contextFactory is None:
 
2363
            return defer.fail(IMAP4Exception(
 
2364
                "IMAP4Client requires a TLS context to "
 
2365
                "initiate the STARTTLS handshake"))
 
2366
 
 
2367
        if 'STARTTLS' not in self._capCache:
 
2368
            return defer.fail(IMAP4Exception(
 
2369
                "Server does not support secure communication "
 
2370
                "via TLS / SSL"))
 
2371
 
 
2372
        tls = interfaces.ITLSTransport(self.transport, None)
 
2373
        if tls is None:
 
2374
            return defer.fail(IMAP4Exception(
 
2375
                "IMAP4Client transport does not implement "
 
2376
                "interfaces.ITLSTransport"))
 
2377
 
 
2378
        d = self.sendCommand(Command('STARTTLS'))
 
2379
        d.addCallback(self._startedTLS, contextFactory)
 
2380
        d.addCallback(lambda _: self.getCapabilities())
 
2381
        return d
 
2382
 
 
2383
 
 
2384
    def authenticate(self, secret):
 
2385
        """Attempt to enter the authenticated state with the server
 
2386
 
 
2387
        This command is allowed in the Non-Authenticated state.
 
2388
 
 
2389
        @rtype: C{Deferred}
 
2390
        @return: A deferred whose callback is invoked if the authentication
 
2391
        succeeds and whose errback will be invoked otherwise.
 
2392
        """
 
2393
        if self._capCache is None:
 
2394
            d = self.getCapabilities()
 
2395
        else:
 
2396
            d = defer.succeed(self._capCache)
 
2397
        d.addCallback(self.__cbAuthenticate, secret)
 
2398
        return d
 
2399
 
 
2400
    def __cbAuthenticate(self, caps, secret):
 
2401
        auths = caps.get('AUTH', ())
 
2402
        for scheme in auths:
 
2403
            if scheme.upper() in self.authenticators:
 
2404
                cmd = Command('AUTHENTICATE', scheme, (),
 
2405
                              self.__cbContinueAuth, scheme,
 
2406
                              secret)
 
2407
                return self.sendCommand(cmd)
 
2408
 
 
2409
        if self.startedTLS:
 
2410
            return defer.fail(NoSupportedAuthentication(
 
2411
                auths, self.authenticators.keys()))
 
2412
        else:
 
2413
            def ebStartTLS(err):
 
2414
                err.trap(IMAP4Exception)
 
2415
                # We couldn't negotiate TLS for some reason
 
2416
                return defer.fail(NoSupportedAuthentication(
 
2417
                    auths, self.authenticators.keys()))
 
2418
 
 
2419
            d = self.startTLS()
 
2420
            d.addErrback(ebStartTLS)
 
2421
            d.addCallback(lambda _: self.getCapabilities())
 
2422
            d.addCallback(self.__cbAuthTLS, secret)
 
2423
            return d
 
2424
 
 
2425
 
 
2426
    def __cbContinueAuth(self, rest, scheme, secret):
 
2427
        try:
 
2428
            chal = base64.decodestring(rest + '\n')
 
2429
        except binascii.Error:
 
2430
            self.sendLine('*')
 
2431
            raise IllegalServerResponse(rest)
 
2432
            self.transport.loseConnection()
 
2433
        else:
 
2434
            auth = self.authenticators[scheme]
 
2435
            chal = auth.challengeResponse(secret, chal)
 
2436
            self.sendLine(base64.encodestring(chal).strip())
 
2437
 
 
2438
    def __cbAuthTLS(self, caps, secret):
 
2439
        auths = caps.get('AUTH', ())
 
2440
        for scheme in auths:
 
2441
            if scheme.upper() in self.authenticators:
 
2442
                cmd = Command('AUTHENTICATE', scheme, (),
 
2443
                              self.__cbContinueAuth, scheme,
 
2444
                              secret)
 
2445
                return self.sendCommand(cmd)
 
2446
        raise NoSupportedAuthentication(auths, self.authenticators.keys())
 
2447
 
 
2448
 
 
2449
    def login(self, username, password):
 
2450
        """Authenticate with the server using a username and password
 
2451
 
 
2452
        This command is allowed in the Non-Authenticated state.  If the
 
2453
        server supports the STARTTLS capability and our transport supports
 
2454
        TLS, TLS is negotiated before the login command is issued.
 
2455
 
 
2456
        A more secure way to log in is to use C{startTLS} or
 
2457
        C{authenticate} or both.
 
2458
 
 
2459
        @type username: C{str}
 
2460
        @param username: The username to log in with
 
2461
 
 
2462
        @type password: C{str}
 
2463
        @param password: The password to log in with
 
2464
 
 
2465
        @rtype: C{Deferred}
 
2466
        @return: A deferred whose callback is invoked if login is successful
 
2467
        and whose errback is invoked otherwise.
 
2468
        """
 
2469
        d = maybeDeferred(self.getCapabilities)
 
2470
        d.addCallback(self.__cbLoginCaps, username, password)
 
2471
        return d
 
2472
 
 
2473
    def serverGreeting(self, caps):
 
2474
        """Called when the server has sent us a greeting.
 
2475
 
 
2476
        @type caps: C{dict}
 
2477
        @param caps: Capabilities the server advertised in its greeting.
 
2478
        """
 
2479
 
 
2480
    def _getContextFactory(self):
 
2481
        if self.context is not None:
 
2482
            return self.context
 
2483
        try:
 
2484
            from twisted.internet import ssl
 
2485
        except ImportError:
 
2486
            return None
 
2487
        else:
 
2488
            context = ssl.ClientContextFactory()
 
2489
            context.method = ssl.SSL.TLSv1_METHOD
 
2490
            return context
 
2491
 
 
2492
    def __cbLoginCaps(self, capabilities, username, password):
 
2493
        # If the server advertises STARTTLS, we might want to try to switch to TLS
 
2494
        tryTLS = 'STARTTLS' in capabilities
 
2495
 
 
2496
        # If our transport supports switching to TLS, we might want to try to switch to TLS.
 
2497
        tlsableTransport = interfaces.ITLSTransport(self.transport, None) is not None
 
2498
 
 
2499
        # If our transport is not already using TLS, we might want to try to switch to TLS.
 
2500
        nontlsTransport = interfaces.ISSLTransport(self.transport, None) is None
 
2501
 
 
2502
        if not self.startedTLS and tryTLS and tlsableTransport and nontlsTransport:
 
2503
            d = self.startTLS()
 
2504
 
 
2505
            d.addCallbacks(
 
2506
                self.__cbLoginTLS,
 
2507
                self.__ebLoginTLS,
 
2508
                callbackArgs=(username, password),
 
2509
                )
 
2510
            return d
 
2511
        else:
 
2512
            if nontlsTransport:
 
2513
                log.msg("Server has no TLS support. logging in over cleartext!")
 
2514
            args = ' '.join((_quote(username), _quote(password)))
 
2515
            return self.sendCommand(Command('LOGIN', args))
 
2516
 
 
2517
    def _startedTLS(self, result, context):
 
2518
        self.transport.startTLS(context)
 
2519
        self._capCache = None
 
2520
        self.startedTLS = True
 
2521
        return result
 
2522
 
 
2523
    def __cbLoginTLS(self, result, username, password):
 
2524
        args = ' '.join((_quote(username), _quote(password)))
 
2525
        return self.sendCommand(Command('LOGIN', args))
 
2526
 
 
2527
    def __ebLoginTLS(self, failure):
 
2528
        log.err(failure)
 
2529
        return failure
 
2530
 
 
2531
    def namespace(self):
 
2532
        """Retrieve information about the namespaces available to this account
 
2533
 
 
2534
        This command is allowed in the Authenticated and Selected states.
 
2535
 
 
2536
        @rtype: C{Deferred}
 
2537
        @return: A deferred whose callback is invoked with namespace
 
2538
        information.  An example of this information is::
 
2539
 
 
2540
            [[['', '/']], [], []]
 
2541
 
 
2542
        which indicates a single personal namespace called '' with '/'
 
2543
        as its hierarchical delimiter, and no shared or user namespaces.
 
2544
        """
 
2545
        cmd = 'NAMESPACE'
 
2546
        resp = ('NAMESPACE',)
 
2547
        d = self.sendCommand(Command(cmd, wantResponse=resp))
 
2548
        d.addCallback(self.__cbNamespace)
 
2549
        return d
 
2550
 
 
2551
    def __cbNamespace(self, (lines, last)):
 
2552
        for line in lines:
 
2553
            parts = line.split(None, 1)
 
2554
            if len(parts) == 2:
 
2555
                if parts[0] == 'NAMESPACE':
 
2556
                    # XXX UGGG parsing hack :(
 
2557
                    r = parseNestedParens('(' + parts[1] + ')')[0]
 
2558
                    return [e or [] for e in r]
 
2559
        log.err("No NAMESPACE response to NAMESPACE command")
 
2560
        return [[], [], []]
 
2561
 
 
2562
    def select(self, mailbox):
 
2563
        """Select a mailbox
 
2564
 
 
2565
        This command is allowed in the Authenticated and Selected states.
 
2566
 
 
2567
        @type mailbox: C{str}
 
2568
        @param mailbox: The name of the mailbox to select
 
2569
 
 
2570
        @rtype: C{Deferred}
 
2571
        @return: A deferred whose callback is invoked with mailbox
 
2572
        information if the select is successful and whose errback is
 
2573
        invoked otherwise.  Mailbox information consists of a dictionary
 
2574
        with the following keys and values::
 
2575
 
 
2576
                FLAGS: A list of strings containing the flags settable on
 
2577
                        messages in this mailbox.
 
2578
 
 
2579
                EXISTS: An integer indicating the number of messages in this
 
2580
                        mailbox.
 
2581
 
 
2582
                RECENT: An integer indicating the number of \"recent\"
 
2583
                        messages in this mailbox.
 
2584
 
 
2585
                UNSEEN: An integer indicating the number of messages not
 
2586
                        flagged \\Seen in this mailbox.
 
2587
 
 
2588
                PERMANENTFLAGS: A list of strings containing the flags that
 
2589
                        can be permanently set on messages in this mailbox.
 
2590
 
 
2591
                UIDVALIDITY: An integer uniquely identifying this mailbox.
 
2592
        """
 
2593
        cmd = 'SELECT'
 
2594
        args = _prepareMailboxName(mailbox)
 
2595
        resp = ('FLAGS', 'EXISTS', 'RECENT', 'UNSEEN', 'PERMANENTFLAGS', 'UIDVALIDITY')
 
2596
        d = self.sendCommand(Command(cmd, args, wantResponse=resp))
 
2597
        d.addCallback(self.__cbSelect, 1)
 
2598
        return d
 
2599
 
 
2600
    def examine(self, mailbox):
 
2601
        """Select a mailbox in read-only mode
 
2602
 
 
2603
        This command is allowed in the Authenticated and Selected states.
 
2604
 
 
2605
        @type mailbox: C{str}
 
2606
        @param mailbox: The name of the mailbox to examine
 
2607
 
 
2608
        @rtype: C{Deferred}
 
2609
        @return: A deferred whose callback is invoked with mailbox
 
2610
        information if the examine is successful and whose errback
 
2611
        is invoked otherwise.  Mailbox information consists of a dictionary
 
2612
        with the following keys and values::
 
2613
 
 
2614
            'FLAGS': A list of strings containing the flags settable on
 
2615
                        messages in this mailbox.
 
2616
 
 
2617
            'EXISTS': An integer indicating the number of messages in this
 
2618
                        mailbox.
 
2619
 
 
2620
            'RECENT': An integer indicating the number of \"recent\"
 
2621
                        messages in this mailbox.
 
2622
 
 
2623
            'UNSEEN': An integer indicating the number of messages not
 
2624
                        flagged \\Seen in this mailbox.
 
2625
 
 
2626
            'PERMANENTFLAGS': A list of strings containing the flags that
 
2627
                        can be permanently set on messages in this mailbox.
 
2628
 
 
2629
            'UIDVALIDITY': An integer uniquely identifying this mailbox.
 
2630
        """
 
2631
        cmd = 'EXAMINE'
 
2632
        args = _prepareMailboxName(mailbox)
 
2633
        resp = ('FLAGS', 'EXISTS', 'RECENT', 'UNSEEN', 'PERMANENTFLAGS', 'UIDVALIDITY')
 
2634
        d = self.sendCommand(Command(cmd, args, wantResponse=resp))
 
2635
        d.addCallback(self.__cbSelect, 0)
 
2636
        return d
 
2637
 
 
2638
    def __cbSelect(self, (lines, tagline), rw):
 
2639
        # In the absense of specification, we are free to assume:
 
2640
        #   READ-WRITE access
 
2641
        datum = {'READ-WRITE': rw}
 
2642
        lines.append(tagline)
 
2643
        for parts in lines:
 
2644
            split = parts.split()
 
2645
            if len(split) == 2:
 
2646
                if split[1].upper().strip() == 'EXISTS':
 
2647
                    try:
 
2648
                        datum['EXISTS'] = int(split[0])
 
2649
                    except ValueError:
 
2650
                        raise IllegalServerResponse(parts)
 
2651
                elif split[1].upper().strip() == 'RECENT':
 
2652
                    try:
 
2653
                        datum['RECENT'] = int(split[0])
 
2654
                    except ValueError:
 
2655
                        raise IllegalServerResponse(parts)
 
2656
                else:
 
2657
                    log.err('Unhandled SELECT response (1): ' + parts)
 
2658
            elif split[0].upper().strip() == 'FLAGS':
 
2659
                split = parts.split(None, 1)
 
2660
                datum['FLAGS'] = tuple(parseNestedParens(split[1])[0])
 
2661
            elif split[0].upper().strip() == 'OK':
 
2662
                begin = parts.find('[')
 
2663
                end = parts.find(']')
 
2664
                if begin == -1 or end == -1:
 
2665
                    raise IllegalServerResponse(parts)
 
2666
                else:
 
2667
                    content = parts[begin+1:end].split(None, 1)
 
2668
                    if len(content) >= 1:
 
2669
                        key = content[0].upper()
 
2670
                        if key == 'READ-ONLY':
 
2671
                            datum['READ-WRITE'] = 0
 
2672
                        elif key == 'READ-WRITE':
 
2673
                            datum['READ-WRITE'] = 1
 
2674
                        elif key == 'UIDVALIDITY':
 
2675
                            try:
 
2676
                                datum['UIDVALIDITY'] = int(content[1])
 
2677
                            except ValueError:
 
2678
                                raise IllegalServerResponse(parts)
 
2679
                        elif key == 'UNSEEN':
 
2680
                            try:
 
2681
                                datum['UNSEEN'] = int(content[1])
 
2682
                            except ValueError:
 
2683
                                raise IllegalServerResponse(parts)
 
2684
                        elif key == 'UIDNEXT':
 
2685
                            datum['UIDNEXT'] = int(content[1])
 
2686
                        elif key == 'PERMANENTFLAGS':
 
2687
                            datum['PERMANENTFLAGS'] = tuple(parseNestedParens(content[1])[0])
 
2688
                        else:
 
2689
                            log.err('Unhandled SELECT response (2): ' + parts)
 
2690
                    else:
 
2691
                        log.err('Unhandled SELECT response (3): ' + parts)
 
2692
            else:
 
2693
                log.err('Unhandled SELECT response (4): ' + parts)
 
2694
        return datum
 
2695
 
 
2696
    def create(self, name):
 
2697
        """Create a new mailbox on the server
 
2698
 
 
2699
        This command is allowed in the Authenticated and Selected states.
 
2700
 
 
2701
        @type name: C{str}
 
2702
        @param name: The name of the mailbox to create.
 
2703
 
 
2704
        @rtype: C{Deferred}
 
2705
        @return: A deferred whose callback is invoked if the mailbox creation
 
2706
        is successful and whose errback is invoked otherwise.
 
2707
        """
 
2708
        return self.sendCommand(Command('CREATE', _prepareMailboxName(name)))
 
2709
 
 
2710
    def delete(self, name):
 
2711
        """Delete a mailbox
 
2712
 
 
2713
        This command is allowed in the Authenticated and Selected states.
 
2714
 
 
2715
        @type name: C{str}
 
2716
        @param name: The name of the mailbox to delete.
 
2717
 
 
2718
        @rtype: C{Deferred}
 
2719
        @return: A deferred whose calblack is invoked if the mailbox is
 
2720
        deleted successfully and whose errback is invoked otherwise.
 
2721
        """
 
2722
        return self.sendCommand(Command('DELETE', _prepareMailboxName(name)))
 
2723
 
 
2724
    def rename(self, oldname, newname):
 
2725
        """Rename a mailbox
 
2726
 
 
2727
        This command is allowed in the Authenticated and Selected states.
 
2728
 
 
2729
        @type oldname: C{str}
 
2730
        @param oldname: The current name of the mailbox to rename.
 
2731
 
 
2732
        @type newname: C{str}
 
2733
        @param newname: The new name to give the mailbox.
 
2734
 
 
2735
        @rtype: C{Deferred}
 
2736
        @return: A deferred whose callback is invoked if the rename is
 
2737
        successful and whose errback is invoked otherwise.
 
2738
        """
 
2739
        oldname = _prepareMailboxName(oldname)
 
2740
        newname = _prepareMailboxName(newname)
 
2741
        return self.sendCommand(Command('RENAME', ' '.join((oldname, newname))))
 
2742
 
 
2743
    def subscribe(self, name):
 
2744
        """Add a mailbox to the subscription list
 
2745
 
 
2746
        This command is allowed in the Authenticated and Selected states.
 
2747
 
 
2748
        @type name: C{str}
 
2749
        @param name: The mailbox to mark as 'active' or 'subscribed'
 
2750
 
 
2751
        @rtype: C{Deferred}
 
2752
        @return: A deferred whose callback is invoked if the subscription
 
2753
        is successful and whose errback is invoked otherwise.
 
2754
        """
 
2755
        return self.sendCommand(Command('SUBSCRIBE', _prepareMailboxName(name)))
 
2756
 
 
2757
    def unsubscribe(self, name):
 
2758
        """Remove a mailbox from the subscription list
 
2759
 
 
2760
        This command is allowed in the Authenticated and Selected states.
 
2761
 
 
2762
        @type name: C{str}
 
2763
        @param name: The mailbox to unsubscribe
 
2764
 
 
2765
        @rtype: C{Deferred}
 
2766
        @return: A deferred whose callback is invoked if the unsubscription
 
2767
        is successful and whose errback is invoked otherwise.
 
2768
        """
 
2769
        return self.sendCommand(Command('UNSUBSCRIBE', _prepareMailboxName(name)))
 
2770
 
 
2771
    def list(self, reference, wildcard):
 
2772
        """List a subset of the available mailboxes
 
2773
 
 
2774
        This command is allowed in the Authenticated and Selected states.
 
2775
 
 
2776
        @type reference: C{str}
 
2777
        @param reference: The context in which to interpret C{wildcard}
 
2778
 
 
2779
        @type wildcard: C{str}
 
2780
        @param wildcard: The pattern of mailbox names to match, optionally
 
2781
        including either or both of the '*' and '%' wildcards.  '*' will
 
2782
        match zero or more characters and cross hierarchical boundaries.
 
2783
        '%' will also match zero or more characters, but is limited to a
 
2784
        single hierarchical level.
 
2785
 
 
2786
        @rtype: C{Deferred}
 
2787
        @return: A deferred whose callback is invoked with a list of C{tuple}s,
 
2788
        the first element of which is a C{tuple} of mailbox flags, the second
 
2789
        element of which is the hierarchy delimiter for this mailbox, and the
 
2790
        third of which is the mailbox name; if the command is unsuccessful,
 
2791
        the deferred's errback is invoked instead.
 
2792
        """
 
2793
        cmd = 'LIST'
 
2794
        args = '"%s" "%s"' % (reference, wildcard.encode('imap4-utf-7'))
 
2795
        resp = ('LIST',)
 
2796
        d = self.sendCommand(Command(cmd, args, wantResponse=resp))
 
2797
        d.addCallback(self.__cbList, 'LIST')
 
2798
        return d
 
2799
 
 
2800
    def lsub(self, reference, wildcard):
 
2801
        """List a subset of the subscribed available mailboxes
 
2802
 
 
2803
        This command is allowed in the Authenticated and Selected states.
 
2804
 
 
2805
        The parameters and returned object are the same as for the C{list}
 
2806
        method, with one slight difference: Only mailboxes which have been
 
2807
        subscribed can be included in the resulting list.
 
2808
        """
 
2809
        cmd = 'LSUB'
 
2810
        args = '"%s" "%s"' % (reference, wildcard.encode('imap4-utf-7'))
 
2811
        resp = ('LSUB',)
 
2812
        d = self.sendCommand(Command(cmd, args, wantResponse=resp))
 
2813
        d.addCallback(self.__cbList, 'LSUB')
 
2814
        return d
 
2815
 
 
2816
    def __cbList(self, (lines, last), command):
 
2817
        results = []
 
2818
        for L in lines:
 
2819
            parts = parseNestedParens(L)
 
2820
            if len(parts) != 4:
 
2821
                raise IllegalServerResponse, L
 
2822
            if parts[0] == command:
 
2823
                parts[1] = tuple(parts[1])
 
2824
                results.append(tuple(parts[1:]))
 
2825
        return results
 
2826
 
 
2827
    def status(self, mailbox, *names):
 
2828
        """Retrieve the status of the given mailbox
 
2829
 
 
2830
        This command is allowed in the Authenticated and Selected states.
 
2831
 
 
2832
        @type mailbox: C{str}
 
2833
        @param mailbox: The name of the mailbox to query
 
2834
 
 
2835
        @type names: C{str}
 
2836
        @param names: The status names to query.  These may be any number of:
 
2837
        MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, and UNSEEN.
 
2838
 
 
2839
        @rtype: C{Deferred}
 
2840
        @return: A deferred whose callback is invoked with the status information
 
2841
        if the command is successful and whose errback is invoked otherwise.
 
2842
        """
 
2843
        cmd = 'STATUS'
 
2844
        args = "%s (%s)" % (_prepareMailboxName(mailbox), ' '.join(names))
 
2845
        resp = ('STATUS',)
 
2846
        d = self.sendCommand(Command(cmd, args, wantResponse=resp))
 
2847
        d.addCallback(self.__cbStatus)
 
2848
        return d
 
2849
 
 
2850
    def __cbStatus(self, (lines, last)):
 
2851
        status = {}
 
2852
        for line in lines:
 
2853
            parts = parseNestedParens(line)
 
2854
            if parts[0] == 'STATUS':
 
2855
                items = parts[2]
 
2856
                items = [items[i:i+2] for i in range(0, len(items), 2)]
 
2857
                status.update(dict(items))
 
2858
        for k in status.keys():
 
2859
            t = self.STATUS_TRANSFORMATIONS.get(k)
 
2860
            if t:
 
2861
                try:
 
2862
                    status[k] = t(status[k])
 
2863
                except Exception, e:
 
2864
                    raise IllegalServerResponse('(%s %s): %s' % (k, status[k], str(e)))
 
2865
        return status
 
2866
 
 
2867
    def append(self, mailbox, message, flags = (), date = None):
 
2868
        """Add the given message to the given mailbox.
 
2869
 
 
2870
        This command is allowed in the Authenticated and Selected states.
 
2871
 
 
2872
        @type mailbox: C{str}
 
2873
        @param mailbox: The mailbox to which to add this message.
 
2874
 
 
2875
        @type message: Any file-like object
 
2876
        @param message: The message to add, in RFC822 format.  Newlines
 
2877
        in this file should be \\r\\n-style.
 
2878
 
 
2879
        @type flags: Any iterable of C{str}
 
2880
        @param flags: The flags to associated with this message.
 
2881
 
 
2882
        @type date: C{str}
 
2883
        @param date: The date to associate with this message.  This should
 
2884
        be of the format DD-MM-YYYY HH:MM:SS +/-HHMM.  For example, in
 
2885
        Eastern Standard Time, on July 1st 2004 at half past 1 PM,
 
2886
        \"01-07-2004 13:30:00 -0500\".
 
2887
 
 
2888
        @rtype: C{Deferred}
 
2889
        @return: A deferred whose callback is invoked when this command
 
2890
        succeeds or whose errback is invoked if it fails.
 
2891
        """
 
2892
        message.seek(0, 2)
 
2893
        L = message.tell()
 
2894
        message.seek(0, 0)
 
2895
        fmt = '%s (%s)%s {%d}'
 
2896
        if date:
 
2897
            date = ' "%s"' % date
 
2898
        else:
 
2899
            date = ''
 
2900
        cmd = fmt % (
 
2901
            _prepareMailboxName(mailbox), ' '.join(flags),
 
2902
            date, L
 
2903
        )
 
2904
        d = self.sendCommand(Command('APPEND', cmd, (), self.__cbContinueAppend, message))
 
2905
        return d
 
2906
 
 
2907
    def __cbContinueAppend(self, lines, message):
 
2908
        s = basic.FileSender()
 
2909
        return s.beginFileTransfer(message, self.transport, None
 
2910
            ).addCallback(self.__cbFinishAppend)
 
2911
 
 
2912
    def __cbFinishAppend(self, foo):
 
2913
        self.sendLine('')
 
2914
 
 
2915
    def check(self):
 
2916
        """Tell the server to perform a checkpoint
 
2917
 
 
2918
        This command is allowed in the Selected state.
 
2919
 
 
2920
        @rtype: C{Deferred}
 
2921
        @return: A deferred whose callback is invoked when this command
 
2922
        succeeds or whose errback is invoked if it fails.
 
2923
        """
 
2924
        return self.sendCommand(Command('CHECK'))
 
2925
 
 
2926
    def close(self):
 
2927
        """Return the connection to the Authenticated state.
 
2928
 
 
2929
        This command is allowed in the Selected state.
 
2930
 
 
2931
        Issuing this command will also remove all messages flagged \\Deleted
 
2932
        from the selected mailbox if it is opened in read-write mode,
 
2933
        otherwise it indicates success by no messages are removed.
 
2934
 
 
2935
        @rtype: C{Deferred}
 
2936
        @return: A deferred whose callback is invoked when the command
 
2937
        completes successfully or whose errback is invoked if it fails.
 
2938
        """
 
2939
        return self.sendCommand(Command('CLOSE'))
 
2940
 
 
2941
    def expunge(self):
 
2942
        """Return the connection to the Authenticate state.
 
2943
 
 
2944
        This command is allowed in the Selected state.
 
2945
 
 
2946
        Issuing this command will perform the same actions as issuing the
 
2947
        close command, but will also generate an 'expunge' response for
 
2948
        every message deleted.
 
2949
 
 
2950
        @rtype: C{Deferred}
 
2951
        @return: A deferred whose callback is invoked with a list of the
 
2952
        'expunge' responses when this command is successful or whose errback
 
2953
        is invoked otherwise.
 
2954
        """
 
2955
        cmd = 'EXPUNGE'
 
2956
        resp = ('EXPUNGE',)
 
2957
        d = self.sendCommand(Command(cmd, wantResponse=resp))
 
2958
        d.addCallback(self.__cbExpunge)
 
2959
        return d
 
2960
 
 
2961
    def __cbExpunge(self, (lines, last)):
 
2962
        ids = []
 
2963
        for line in lines:
 
2964
            parts = line.split(None, 1)
 
2965
            if len(parts) == 2:
 
2966
                if parts[1] == 'EXPUNGE':
 
2967
                    try:
 
2968
                        ids.append(int(parts[0]))
 
2969
                    except ValueError:
 
2970
                        raise IllegalServerResponse, line
 
2971
        return ids
 
2972
 
 
2973
    def search(self, *queries, **kwarg):
 
2974
        """Search messages in the currently selected mailbox
 
2975
 
 
2976
        This command is allowed in the Selected state.
 
2977
 
 
2978
        Any non-zero number of queries are accepted by this method, as
 
2979
        returned by the C{Query}, C{Or}, and C{Not} functions.
 
2980
 
 
2981
        One keyword argument is accepted: if uid is passed in with a non-zero
 
2982
        value, the server is asked to return message UIDs instead of message
 
2983
        sequence numbers.
 
2984
 
 
2985
        @rtype: C{Deferred}
 
2986
        @return: A deferred whose callback will be invoked with a list of all
 
2987
        the message sequence numbers return by the search, or whose errback
 
2988
        will be invoked if there is an error.
 
2989
        """
 
2990
        if kwarg.get('uid'):
 
2991
            cmd = 'UID SEARCH'
 
2992
        else:
 
2993
            cmd = 'SEARCH'
 
2994
        args = ' '.join(queries)
 
2995
        d = self.sendCommand(Command(cmd, args, wantResponse=(cmd,)))
 
2996
        d.addCallback(self.__cbSearch)
 
2997
        return d
 
2998
 
 
2999
    def __cbSearch(self, (lines, end)):
 
3000
        ids = []
 
3001
        for line in lines:
 
3002
            parts = line.split(None, 1)
 
3003
            if len(parts) == 2:
 
3004
                if parts[0] == 'SEARCH':
 
3005
                    try:
 
3006
                        ids.extend(map(int, parts[1].split()))
 
3007
                    except ValueError:
 
3008
                        raise IllegalServerResponse, line
 
3009
        return ids
 
3010
 
 
3011
    def fetchUID(self, messages, uid=0):
 
3012
        """Retrieve the unique identifier for one or more messages
 
3013
 
 
3014
        This command is allowed in the Selected state.
 
3015
 
 
3016
        @type messages: C{MessageSet} or C{str}
 
3017
        @param messages: A message sequence set
 
3018
 
 
3019
        @type uid: C{bool}
 
3020
        @param uid: Indicates whether the message sequence set is of message
 
3021
        numbers or of unique message IDs.
 
3022
 
 
3023
        @rtype: C{Deferred}
 
3024
        @return: A deferred whose callback is invoked with a dict mapping
 
3025
        message sequence numbers to unique message identifiers, or whose
 
3026
        errback is invoked if there is an error.
 
3027
        """
 
3028
        d = self._fetch(messages, useUID=uid, uid=1)
 
3029
        d.addCallback(self.__cbFetch)
 
3030
        return d
 
3031
 
 
3032
    def fetchFlags(self, messages, uid=0):
 
3033
        """Retrieve the flags for one or more messages
 
3034
 
 
3035
        This command is allowed in the Selected state.
 
3036
 
 
3037
        @type messages: C{MessageSet} or C{str}
 
3038
        @param messages: The messages for which to retrieve flags.
 
3039
 
 
3040
        @type uid: C{bool}
 
3041
        @param uid: Indicates whether the message sequence set is of message
 
3042
        numbers or of unique message IDs.
 
3043
 
 
3044
        @rtype: C{Deferred}
 
3045
        @return: A deferred whose callback is invoked with a dict mapping
 
3046
        message numbers to lists of flags, or whose errback is invoked if
 
3047
        there is an error.
 
3048
        """
 
3049
        d = self._fetch(str(messages), useUID=uid, flags=1)
 
3050
        d.addCallback(self.__cbFetch)
 
3051
        return d
 
3052
 
 
3053
    def fetchInternalDate(self, messages, uid=0):
 
3054
        """Retrieve the internal date associated with one or more messages
 
3055
 
 
3056
        This command is allowed in the Selected state.
 
3057
 
 
3058
        @type messages: C{MessageSet} or C{str}
 
3059
        @param messages: The messages for which to retrieve the internal date.
 
3060
 
 
3061
        @type uid: C{bool}
 
3062
        @param uid: Indicates whether the message sequence set is of message
 
3063
        numbers or of unique message IDs.
 
3064
 
 
3065
        @rtype: C{Deferred}
 
3066
        @return: A deferred whose callback is invoked with a dict mapping
 
3067
        message numbers to date strings, or whose errback is invoked
 
3068
        if there is an error.  Date strings take the format of
 
3069
        \"day-month-year time timezone\".
 
3070
        """
 
3071
        d = self._fetch(str(messages), useUID=uid, internaldate=1)
 
3072
        d.addCallback(self.__cbFetch)
 
3073
        return d
 
3074
 
 
3075
    def fetchEnvelope(self, messages, uid=0):
 
3076
        """Retrieve the envelope data for one or more messages
 
3077
 
 
3078
        This command is allowed in the Selected state.
 
3079
 
 
3080
        @type messages: C{MessageSet} or C{str}
 
3081
        @param messages: The messages for which to retrieve envelope data.
 
3082
 
 
3083
        @type uid: C{bool}
 
3084
        @param uid: Indicates whether the message sequence set is of message
 
3085
        numbers or of unique message IDs.
 
3086
 
 
3087
        @rtype: C{Deferred}
 
3088
        @return: A deferred whose callback is invoked with a dict mapping
 
3089
        message numbers to envelope data, or whose errback is invoked
 
3090
        if there is an error.  Envelope data consists of a sequence of the
 
3091
        date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to,
 
3092
        and message-id header fields.  The date, subject, in-reply-to, and
 
3093
        message-id fields are strings, while the from, sender, reply-to,
 
3094
        to, cc, and bcc fields contain address data.  Address data consists
 
3095
        of a sequence of name, source route, mailbox name, and hostname.
 
3096
        Fields which are not present for a particular address may be C{None}.
 
3097
        """
 
3098
        d = self._fetch(str(messages), useUID=uid, envelope=1)
 
3099
        d.addCallback(self.__cbFetch)
 
3100
        return d
 
3101
 
 
3102
    def fetchBodyStructure(self, messages, uid=0):
 
3103
        """Retrieve the structure of the body of one or more messages
 
3104
 
 
3105
        This command is allowed in the Selected state.
 
3106
 
 
3107
        @type messages: C{MessageSet} or C{str}
 
3108
        @param messages: The messages for which to retrieve body structure
 
3109
        data.
 
3110
 
 
3111
        @type uid: C{bool}
 
3112
        @param uid: Indicates whether the message sequence set is of message
 
3113
        numbers or of unique message IDs.
 
3114
 
 
3115
        @rtype: C{Deferred}
 
3116
        @return: A deferred whose callback is invoked with a dict mapping
 
3117
        message numbers to body structure data, or whose errback is invoked
 
3118
        if there is an error.  Body structure data describes the MIME-IMB
 
3119
        format of a message and consists of a sequence of mime type, mime
 
3120
        subtype, parameters, content id, description, encoding, and size.
 
3121
        The fields following the size field are variable: if the mime
 
3122
        type/subtype is message/rfc822, the contained message's envelope
 
3123
        information, body structure data, and number of lines of text; if
 
3124
        the mime type is text, the number of lines of text.  Extension fields
 
3125
        may also be included; if present, they are: the MD5 hash of the body,
 
3126
        body disposition, body language.
 
3127
        """
 
3128
        d = self._fetch(messages, useUID=uid, bodystructure=1)
 
3129
        d.addCallback(self.__cbFetch)
 
3130
        return d
 
3131
 
 
3132
    def fetchSimplifiedBody(self, messages, uid=0):
 
3133
        """Retrieve the simplified body structure of one or more messages
 
3134
 
 
3135
        This command is allowed in the Selected state.
 
3136
 
 
3137
        @type messages: C{MessageSet} or C{str}
 
3138
        @param messages: A message sequence set
 
3139
 
 
3140
        @type uid: C{bool}
 
3141
        @param uid: Indicates whether the message sequence set is of message
 
3142
        numbers or of unique message IDs.
 
3143
 
 
3144
        @rtype: C{Deferred}
 
3145
        @return: A deferred whose callback is invoked with a dict mapping
 
3146
        message numbers to body data, or whose errback is invoked
 
3147
        if there is an error.  The simplified body structure is the same
 
3148
        as the body structure, except that extension fields will never be
 
3149
        present.
 
3150
        """
 
3151
        d = self._fetch(messages, useUID=uid, body=1)
 
3152
        d.addCallback(self.__cbFetch)
 
3153
        return d
 
3154
 
 
3155
    def fetchMessage(self, messages, uid=0):
 
3156
        """Retrieve one or more entire messages
 
3157
 
 
3158
        This command is allowed in the Selected state.
 
3159
 
 
3160
        @type messages: C{MessageSet} or C{str}
 
3161
        @param messages: A message sequence set
 
3162
 
 
3163
        @type uid: C{bool}
 
3164
        @param uid: Indicates whether the message sequence set is of message
 
3165
        numbers or of unique message IDs.
 
3166
 
 
3167
        @rtype: C{Deferred}
 
3168
        @return: A deferred whose callback is invoked with a dict mapping
 
3169
        message objects (as returned by self.messageFile(), file objects by
 
3170
        default), to additional information, or whose errback is invoked if
 
3171
        there is an error.
 
3172
        """
 
3173
        d = self._fetch(messages, useUID=uid, rfc822=1)
 
3174
        d.addCallback(self.__cbFetch)
 
3175
        return d
 
3176
 
 
3177
    def fetchHeaders(self, messages, uid=0):
 
3178
        """Retrieve headers of one or more messages
 
3179
 
 
3180
        This command is allowed in the Selected state.
 
3181
 
 
3182
        @type messages: C{MessageSet} or C{str}
 
3183
        @param messages: A message sequence set
 
3184
 
 
3185
        @type uid: C{bool}
 
3186
        @param uid: Indicates whether the message sequence set is of message
 
3187
        numbers or of unique message IDs.
 
3188
 
 
3189
        @rtype: C{Deferred}
 
3190
        @return: A deferred whose callback is invoked with a dict mapping
 
3191
        message numbers to dicts of message headers, or whose errback is
 
3192
        invoked if there is an error.
 
3193
        """
 
3194
        d = self._fetch(messages, useUID=uid, rfc822header=1)
 
3195
        d.addCallback(self.__cbFetch)
 
3196
        return d
 
3197
 
 
3198
    def fetchBody(self, messages, uid=0):
 
3199
        """Retrieve body text of one or more messages
 
3200
 
 
3201
        This command is allowed in the Selected state.
 
3202
 
 
3203
        @type messages: C{MessageSet} or C{str}
 
3204
        @param messages: A message sequence set
 
3205
 
 
3206
        @type uid: C{bool}
 
3207
        @param uid: Indicates whether the message sequence set is of message
 
3208
        numbers or of unique message IDs.
 
3209
 
 
3210
        @rtype: C{Deferred}
 
3211
        @return: A deferred whose callback is invoked with a dict mapping
 
3212
        message numbers to file-like objects containing body text, or whose
 
3213
        errback is invoked if there is an error.
 
3214
        """
 
3215
        d = self._fetch(messages, useUID=uid, rfc822text=1)
 
3216
        d.addCallback(self.__cbFetch)
 
3217
        return d
 
3218
 
 
3219
    def fetchSize(self, messages, uid=0):
 
3220
        """Retrieve the size, in octets, of one or more messages
 
3221
 
 
3222
        This command is allowed in the Selected state.
 
3223
 
 
3224
        @type messages: C{MessageSet} or C{str}
 
3225
        @param messages: A message sequence set
 
3226
 
 
3227
        @type uid: C{bool}
 
3228
        @param uid: Indicates whether the message sequence set is of message
 
3229
        numbers or of unique message IDs.
 
3230
 
 
3231
        @rtype: C{Deferred}
 
3232
        @return: A deferred whose callback is invoked with a dict mapping
 
3233
        message numbers to sizes, or whose errback is invoked if there is
 
3234
        an error.
 
3235
        """
 
3236
        d = self._fetch(messages, useUID=uid, rfc822size=1)
 
3237
        d.addCallback(self.__cbFetch)
 
3238
        return d
 
3239
 
 
3240
    def fetchFull(self, messages, uid=0):
 
3241
        """Retrieve several different fields of one or more messages
 
3242
 
 
3243
        This command is allowed in the Selected state.  This is equivalent
 
3244
        to issuing all of the C{fetchFlags}, C{fetchInternalDate},
 
3245
        C{fetchSize}, C{fetchEnvelope}, and C{fetchSimplifiedBody}
 
3246
        functions.
 
3247
 
 
3248
        @type messages: C{MessageSet} or C{str}
 
3249
        @param messages: A message sequence set
 
3250
 
 
3251
        @type uid: C{bool}
 
3252
        @param uid: Indicates whether the message sequence set is of message
 
3253
        numbers or of unique message IDs.
 
3254
 
 
3255
        @rtype: C{Deferred}
 
3256
        @return: A deferred whose callback is invoked with a dict mapping
 
3257
        message numbers to dict of the retrieved data values, or whose
 
3258
        errback is invoked if there is an error.  They dictionary keys
 
3259
        are "flags", "date", "size", "envelope", and "body".
 
3260
        """
 
3261
        d = self._fetch(
 
3262
            messages, useUID=uid, flags=1, internaldate=1,
 
3263
            rfc822size=1, envelope=1, body=1
 
3264
        )
 
3265
        d.addCallback(self.__cbFetch)
 
3266
        return d
 
3267
 
 
3268
    def fetchAll(self, messages, uid=0):
 
3269
        """Retrieve several different fields of one or more messages
 
3270
 
 
3271
        This command is allowed in the Selected state.  This is equivalent
 
3272
        to issuing all of the C{fetchFlags}, C{fetchInternalDate},
 
3273
        C{fetchSize}, and C{fetchEnvelope} functions.
 
3274
 
 
3275
        @type messages: C{MessageSet} or C{str}
 
3276
        @param messages: A message sequence set
 
3277
 
 
3278
        @type uid: C{bool}
 
3279
        @param uid: Indicates whether the message sequence set is of message
 
3280
        numbers or of unique message IDs.
 
3281
 
 
3282
        @rtype: C{Deferred}
 
3283
        @return: A deferred whose callback is invoked with a dict mapping
 
3284
        message numbers to dict of the retrieved data values, or whose
 
3285
        errback is invoked if there is an error.  They dictionary keys
 
3286
        are "flags", "date", "size", and "envelope".
 
3287
        """
 
3288
        d = self._fetch(
 
3289
            messages, useUID=uid, flags=1, internaldate=1,
 
3290
            rfc822size=1, envelope=1
 
3291
        )
 
3292
        d.addCallback(self.__cbFetch)
 
3293
        return d
 
3294
 
 
3295
    def fetchFast(self, messages, uid=0):
 
3296
        """Retrieve several different fields of one or more messages
 
3297
 
 
3298
        This command is allowed in the Selected state.  This is equivalent
 
3299
        to issuing all of the C{fetchFlags}, C{fetchInternalDate}, and
 
3300
        C{fetchSize} functions.
 
3301
 
 
3302
        @type messages: C{MessageSet} or C{str}
 
3303
        @param messages: A message sequence set
 
3304
 
 
3305
        @type uid: C{bool}
 
3306
        @param uid: Indicates whether the message sequence set is of message
 
3307
        numbers or of unique message IDs.
 
3308
 
 
3309
        @rtype: C{Deferred}
 
3310
        @return: A deferred whose callback is invoked with a dict mapping
 
3311
        message numbers to dict of the retrieved data values, or whose
 
3312
        errback is invoked if there is an error.  They dictionary keys are
 
3313
        "flags", "date", and "size".
 
3314
        """
 
3315
        d = self._fetch(
 
3316
            messages, useUID=uid, flags=1, internaldate=1, rfc822size=1
 
3317
        )
 
3318
        d.addCallback(self.__cbFetch)
 
3319
        return d
 
3320
 
 
3321
    def __cbFetch(self, (lines, last)):
 
3322
        flags = {}
 
3323
        for line in lines:
 
3324
            parts = line.split(None, 2)
 
3325
            if len(parts) == 3:
 
3326
                if parts[1] == 'FETCH':
 
3327
                    try:
 
3328
                        id = int(parts[0])
 
3329
                    except ValueError:
 
3330
                        raise IllegalServerResponse, line
 
3331
                    else:
 
3332
                        data = parseNestedParens(parts[2])
 
3333
                        while len(data) == 1 and isinstance(data, types.ListType):
 
3334
                            data = data[0]
 
3335
                        while data:
 
3336
                            if len(data) < 2:
 
3337
                                raise IllegalServerResponse("Not enough arguments", data)
 
3338
                            flags.setdefault(id, {})[data[0]] = data[1]
 
3339
                            del data[:2]
 
3340
                else:
 
3341
                    print '(2)Ignoring ', parts
 
3342
            else:
 
3343
                print '(3)Ignoring ', parts
 
3344
        return flags
 
3345
 
 
3346
    def fetchSpecific(self, messages, uid=0, headerType=None,
 
3347
                      headerNumber=None, headerArgs=None, peek=None,
 
3348
                      offset=None, length=None):
 
3349
        """Retrieve a specific section of one or more messages
 
3350
 
 
3351
        @type messages: C{MessageSet} or C{str}
 
3352
        @param messages: A message sequence set
 
3353
 
 
3354
        @type uid: C{bool}
 
3355
        @param uid: Indicates whether the message sequence set is of message
 
3356
        numbers or of unique message IDs.
 
3357
 
 
3358
        @type headerType: C{str}
 
3359
        @param headerType: If specified, must be one of HEADER,
 
3360
        HEADER.FIELDS, HEADER.FIELDS.NOT, MIME, or TEXT, and will determine
 
3361
        which part of the message is retrieved.  For HEADER.FIELDS and
 
3362
        HEADER.FIELDS.NOT, C{headerArgs} must be a sequence of header names.
 
3363
        For MIME, C{headerNumber} must be specified.
 
3364
 
 
3365
        @type headerNumber: C{int} or C{int} sequence
 
3366
        @param headerNumber: The nested rfc822 index specifying the
 
3367
        entity to retrieve.  For example, C{1} retrieves the first
 
3368
        entity of the message, and C{(2, 1, 3}) retrieves the 3rd
 
3369
        entity inside the first entity inside the second entity of
 
3370
        the message.
 
3371
 
 
3372
        @type headerArgs: A sequence of C{str}
 
3373
        @param headerArgs: If C{headerType} is HEADER.FIELDS, these are the
 
3374
        headers to retrieve.  If it is HEADER.FIELDS.NOT, these are the
 
3375
        headers to exclude from retrieval.
 
3376
 
 
3377
        @type peek: C{bool}
 
3378
        @param peek: If true, cause the server to not set the \\Seen
 
3379
        flag on this message as a result of this command.
 
3380
 
 
3381
        @type offset: C{int}
 
3382
        @param offset: The number of octets at the beginning of the result
 
3383
        to skip.
 
3384
 
 
3385
        @type length: C{int}
 
3386
        @param length: The number of octets to retrieve.
 
3387
 
 
3388
        @rtype: C{Deferred}
 
3389
        @return: A deferred whose callback is invoked with a mapping of
 
3390
        message numbers to retrieved data, or whose errback is invoked
 
3391
        if there is an error.
 
3392
        """
 
3393
        fmt = '%s BODY%s[%s%s%s]%s'
 
3394
        if headerNumber is None:
 
3395
            number = ''
 
3396
        elif isinstance(headerNumber, types.IntType):
 
3397
            number = str(headerNumber)
 
3398
        else:
 
3399
            number = '.'.join(headerNumber)
 
3400
        if headerType is None:
 
3401
            header = ''
 
3402
        elif number:
 
3403
            header = '.' + headerType
 
3404
        else:
 
3405
            header = headerType
 
3406
        if header:
 
3407
            if headerArgs is not None:
 
3408
                payload = ' (%s)' % ' '.join(headerArgs)
 
3409
            else:
 
3410
                payload = ' ()'
 
3411
        else:
 
3412
            payload = ''
 
3413
        if offset is None:
 
3414
            extra = ''
 
3415
        else:
 
3416
            extra = '<%d.%d>' % (offset, length)
 
3417
        fetch = uid and 'UID FETCH' or 'FETCH'
 
3418
        cmd = fmt % (messages, peek and '.PEEK' or '', number, header, payload, extra)
 
3419
        d = self.sendCommand(Command(fetch, cmd, wantResponse=('FETCH',)))
 
3420
        d.addCallback(self.__cbFetchSpecific)
 
3421
        return d
 
3422
 
 
3423
    def __cbFetchSpecific(self, (lines, last)):
 
3424
        info = {}
 
3425
        for line in lines:
 
3426
            parts = line.split(None, 2)
 
3427
            if len(parts) == 3:
 
3428
                if parts[1] == 'FETCH':
 
3429
                    try:
 
3430
                        id = int(parts[0])
 
3431
                    except ValueError:
 
3432
                        raise IllegalServerResponse, line
 
3433
                    else:
 
3434
                        info[id] = parseNestedParens(parts[2])
 
3435
        return info
 
3436
 
 
3437
    def _fetch(self, messages, useUID=0, **terms):
 
3438
        fetch = useUID and 'UID FETCH' or 'FETCH'
 
3439
 
 
3440
        if 'rfc822text' in terms:
 
3441
            del terms['rfc822text']
 
3442
            terms['rfc822.text'] = True
 
3443
        if 'rfc822size' in terms:
 
3444
            del terms['rfc822size']
 
3445
            terms['rfc822.size'] = True
 
3446
        if 'rfc822header' in terms:
 
3447
            del terms['rfc822header']
 
3448
            terms['rfc822.header'] = True
 
3449
 
 
3450
        cmd = '%s (%s)' % (messages, ' '.join([s.upper() for s in terms.keys()]))
 
3451
        d = self.sendCommand(Command(fetch, cmd, wantResponse=('FETCH',)))
 
3452
        return d
 
3453
 
 
3454
    def setFlags(self, messages, flags, silent=1, uid=0):
 
3455
        """Set the flags for one or more messages.
 
3456
 
 
3457
        This command is allowed in the Selected state.
 
3458
 
 
3459
        @type messages: C{MessageSet} or C{str}
 
3460
        @param messages: A message sequence set
 
3461
 
 
3462
        @type flags: Any iterable of C{str}
 
3463
        @param flags: The flags to set
 
3464
 
 
3465
        @type silent: C{bool}
 
3466
        @param silent: If true, cause the server to supress its verbose
 
3467
        response.
 
3468
 
 
3469
        @type uid: C{bool}
 
3470
        @param uid: Indicates whether the message sequence set is of message
 
3471
        numbers or of unique message IDs.
 
3472
 
 
3473
        @rtype: C{Deferred}
 
3474
        @return: A deferred whose callback is invoked with a list of the
 
3475
        the server's responses (C{[]} if C{silent} is true) or whose
 
3476
        errback is invoked if there is an error.
 
3477
        """
 
3478
        return self._store(str(messages), silent and 'FLAGS.SILENT' or 'FLAGS', flags, uid)
 
3479
 
 
3480
    def addFlags(self, messages, flags, silent=1, uid=0):
 
3481
        """Add to the set flags for one or more messages.
 
3482
 
 
3483
        This command is allowed in the Selected state.
 
3484
 
 
3485
        @type messages: C{MessageSet} or C{str}
 
3486
        @param messages: A message sequence set
 
3487
 
 
3488
        @type flags: Any iterable of C{str}
 
3489
        @param flags: The flags to set
 
3490
 
 
3491
        @type silent: C{bool}
 
3492
        @param silent: If true, cause the server to supress its verbose
 
3493
        response.
 
3494
 
 
3495
        @type uid: C{bool}
 
3496
        @param uid: Indicates whether the message sequence set is of message
 
3497
        numbers or of unique message IDs.
 
3498
 
 
3499
        @rtype: C{Deferred}
 
3500
        @return: A deferred whose callback is invoked with a list of the
 
3501
        the server's responses (C{[]} if C{silent} is true) or whose
 
3502
        errback is invoked if there is an error.
 
3503
        """
 
3504
        return self._store(str(messages), silent and '+FLAGS.SILENT' or '+FLAGS', flags, uid)
 
3505
 
 
3506
    def removeFlags(self, messages, flags, silent=1, uid=0):
 
3507
        """Remove from the set flags for one or more messages.
 
3508
 
 
3509
        This command is allowed in the Selected state.
 
3510
 
 
3511
        @type messages: C{MessageSet} or C{str}
 
3512
        @param messages: A message sequence set
 
3513
 
 
3514
        @type flags: Any iterable of C{str}
 
3515
        @param flags: The flags to set
 
3516
 
 
3517
        @type silent: C{bool}
 
3518
        @param silent: If true, cause the server to supress its verbose
 
3519
        response.
 
3520
 
 
3521
        @type uid: C{bool}
 
3522
        @param uid: Indicates whether the message sequence set is of message
 
3523
        numbers or of unique message IDs.
 
3524
 
 
3525
        @rtype: C{Deferred}
 
3526
        @return: A deferred whose callback is invoked with a list of the
 
3527
        the server's responses (C{[]} if C{silent} is true) or whose
 
3528
        errback is invoked if there is an error.
 
3529
        """
 
3530
        return self._store(str(messages), silent and '-FLAGS.SILENT' or '-FLAGS', flags, uid)
 
3531
 
 
3532
    def _store(self, messages, cmd, flags, uid):
 
3533
        store = uid and 'UID STORE' or 'STORE'
 
3534
        args = ' '.join((messages, cmd, '(%s)' % ' '.join(flags)))
 
3535
        d = self.sendCommand(Command(store, args, wantResponse=('FETCH',)))
 
3536
        d.addCallback(self.__cbFetch)
 
3537
        return d
 
3538
 
 
3539
    def copy(self, messages, mailbox, uid):
 
3540
        """Copy the specified messages to the specified mailbox.
 
3541
 
 
3542
        This command is allowed in the Selected state.
 
3543
 
 
3544
        @type messages: C{str}
 
3545
        @param messages: A message sequence set
 
3546
 
 
3547
        @type mailbox: C{str}
 
3548
        @param mailbox: The mailbox to which to copy the messages
 
3549
 
 
3550
        @type uid: C{bool}
 
3551
        @param uid: If true, the C{messages} refers to message UIDs, rather
 
3552
        than message sequence numbers.
 
3553
 
 
3554
        @rtype: C{Deferred}
 
3555
        @return: A deferred whose callback is invoked with a true value
 
3556
        when the copy is successful, or whose errback is invoked if there
 
3557
        is an error.
 
3558
        """
 
3559
        if uid:
 
3560
            cmd = 'UID COPY'
 
3561
        else:
 
3562
            cmd = 'COPY'
 
3563
        args = '%s %s' % (messages, _prepareMailboxName(mailbox))
 
3564
        return self.sendCommand(Command(cmd, args))
 
3565
 
 
3566
    #
 
3567
    # IMailboxListener methods
 
3568
    #
 
3569
    def modeChanged(self, writeable):
 
3570
        """Override me"""
 
3571
 
 
3572
    def flagsChanged(self, newFlags):
 
3573
        """Override me"""
 
3574
 
 
3575
    def newMessages(self, exists, recent):
 
3576
        """Override me"""
 
3577
 
 
3578
 
 
3579
class IllegalIdentifierError(IMAP4Exception): pass
 
3580
 
 
3581
def parseIdList(s):
 
3582
    res = MessageSet()
 
3583
    parts = s.split(',')
 
3584
    for p in parts:
 
3585
        if ':' in p:
 
3586
            low, high = p.split(':', 1)
 
3587
            try:
 
3588
                if low == '*':
 
3589
                    low = None
 
3590
                else:
 
3591
                    low = long(low)
 
3592
                if high == '*':
 
3593
                    high = None
 
3594
                else:
 
3595
                    high = long(high)
 
3596
                res.extend((low, high))
 
3597
            except ValueError:
 
3598
                raise IllegalIdentifierError(p)
 
3599
        else:
 
3600
            try:
 
3601
                if p == '*':
 
3602
                    p = None
 
3603
                else:
 
3604
                    p = long(p)
 
3605
            except ValueError:
 
3606
                raise IllegalIdentifierError(p)
 
3607
            else:
 
3608
                res.extend(p)
 
3609
    return res
 
3610
 
 
3611
class IllegalQueryError(IMAP4Exception): pass
 
3612
 
 
3613
_SIMPLE_BOOL = (
 
3614
    'ALL', 'ANSWERED', 'DELETED', 'DRAFT', 'FLAGGED', 'NEW', 'OLD', 'RECENT',
 
3615
    'SEEN', 'UNANSWERED', 'UNDELETED', 'UNDRAFT', 'UNFLAGGED', 'UNSEEN'
 
3616
)
 
3617
 
 
3618
_NO_QUOTES = (
 
3619
    'LARGER', 'SMALLER', 'UID'
 
3620
)
 
3621
 
 
3622
def Query(sorted=0, **kwarg):
 
3623
    """Create a query string
 
3624
 
 
3625
    Among the accepted keywords are::
 
3626
 
 
3627
        all         : If set to a true value, search all messages in the
 
3628
                      current mailbox
 
3629
 
 
3630
        answered    : If set to a true value, search messages flagged with
 
3631
                      \\Answered
 
3632
 
 
3633
        bcc         : A substring to search the BCC header field for
 
3634
 
 
3635
        before      : Search messages with an internal date before this
 
3636
                      value.  The given date should be a string in the format
 
3637
                      of 'DD-Mon-YYYY'.  For example, '03-Mar-2003'.
 
3638
 
 
3639
        body        : A substring to search the body of the messages for
 
3640
 
 
3641
        cc          : A substring to search the CC header field for
 
3642
 
 
3643
        deleted     : If set to a true value, search messages flagged with
 
3644
                      \\Deleted
 
3645
 
 
3646
        draft       : If set to a true value, search messages flagged with
 
3647
                      \\Draft
 
3648
 
 
3649
        flagged     : If set to a true value, search messages flagged with
 
3650
                      \\Flagged
 
3651
 
 
3652
        from        : A substring to search the From header field for
 
3653
 
 
3654
        header      : A two-tuple of a header name and substring to search
 
3655
                      for in that header
 
3656
 
 
3657
        keyword     : Search for messages with the given keyword set
 
3658
 
 
3659
        larger      : Search for messages larger than this number of octets
 
3660
 
 
3661
        messages    : Search only the given message sequence set.
 
3662
 
 
3663
        new         : If set to a true value, search messages flagged with
 
3664
                      \\Recent but not \\Seen
 
3665
 
 
3666
        old         : If set to a true value, search messages not flagged with
 
3667
                      \\Recent
 
3668
 
 
3669
        on          : Search messages with an internal date which is on this
 
3670
                      date.  The given date should be a string in the format
 
3671
                      of 'DD-Mon-YYYY'.  For example, '03-Mar-2003'.
 
3672
 
 
3673
        recent      : If set to a true value, search for messages flagged with
 
3674
                      \\Recent
 
3675
 
 
3676
        seen        : If set to a true value, search for messages flagged with
 
3677
                      \\Seen
 
3678
 
 
3679
        sentbefore  : Search for messages with an RFC822 'Date' header before
 
3680
                      this date.  The given date should be a string in the format
 
3681
                      of 'DD-Mon-YYYY'.  For example, '03-Mar-2003'.
 
3682
 
 
3683
        senton      : Search for messages with an RFC822 'Date' header which is
 
3684
                      on this date  The given date should be a string in the format
 
3685
                      of 'DD-Mon-YYYY'.  For example, '03-Mar-2003'.
 
3686
 
 
3687
        sentsince   : Search for messages with an RFC822 'Date' header which is
 
3688
                      after this date.  The given date should be a string in the format
 
3689
                      of 'DD-Mon-YYYY'.  For example, '03-Mar-2003'.
 
3690
 
 
3691
        since       : Search for messages with an internal date that is after
 
3692
                      this date..  The given date should be a string in the format
 
3693
                      of 'DD-Mon-YYYY'.  For example, '03-Mar-2003'.
 
3694
 
 
3695
        smaller     : Search for messages smaller than this number of octets
 
3696
 
 
3697
        subject     : A substring to search the 'subject' header for
 
3698
 
 
3699
        text        : A substring to search the entire message for
 
3700
 
 
3701
        to          : A substring to search the 'to' header for
 
3702
 
 
3703
        uid         : Search only the messages in the given message set
 
3704
 
 
3705
        unanswered  : If set to a true value, search for messages not
 
3706
                      flagged with \\Answered
 
3707
 
 
3708
        undeleted   : If set to a true value, search for messages not
 
3709
                      flagged with \\Deleted
 
3710
 
 
3711
        undraft     : If set to a true value, search for messages not
 
3712
                      flagged with \\Draft
 
3713
 
 
3714
        unflagged   : If set to a true value, search for messages not
 
3715
                      flagged with \\Flagged
 
3716
 
 
3717
        unkeyword   : Search for messages without the given keyword set
 
3718
 
 
3719
        unseen      : If set to a true value, search for messages not
 
3720
                      flagged with \\Seen
 
3721
 
 
3722
    @type sorted: C{bool}
 
3723
    @param sorted: If true, the output will be sorted, alphabetically.
 
3724
    The standard does not require it, but it makes testing this function
 
3725
    easier.  The default is zero, and this should be acceptable for any
 
3726
    application.
 
3727
 
 
3728
    @rtype: C{str}
 
3729
    @return: The formatted query string
 
3730
    """
 
3731
    cmd = []
 
3732
    keys = kwarg.keys()
 
3733
    if sorted:
 
3734
        keys.sort()
 
3735
    for k in keys:
 
3736
        v = kwarg[k]
 
3737
        k = k.upper()
 
3738
        if k in _SIMPLE_BOOL and v:
 
3739
           cmd.append(k)
 
3740
        elif k == 'HEADER':
 
3741
            cmd.extend([k, v[0], '"%s"' % (v[1],)])
 
3742
        elif k not in _NO_QUOTES:
 
3743
           cmd.extend([k, '"%s"' % (v,)])
 
3744
        else:
 
3745
           cmd.extend([k, '%s' % (v,)])
 
3746
    if len(cmd) > 1:
 
3747
        return '(%s)' % ' '.join(cmd)
 
3748
    else:
 
3749
        return ' '.join(cmd)
 
3750
 
 
3751
def Or(*args):
 
3752
    """The disjunction of two or more queries"""
 
3753
    if len(args) < 2:
 
3754
        raise IllegalQueryError, args
 
3755
    elif len(args) == 2:
 
3756
        return '(OR %s %s)' % args
 
3757
    else:
 
3758
        return '(OR %s %s)' % (args[0], Or(*args[1:]))
 
3759
 
 
3760
def Not(query):
 
3761
    """The negation of a query"""
 
3762
    return '(NOT %s)' % (query,)
 
3763
 
 
3764
class MismatchedNesting(IMAP4Exception):
 
3765
    pass
 
3766
 
 
3767
class MismatchedQuoting(IMAP4Exception):
 
3768
    pass
 
3769
 
 
3770
def wildcardToRegexp(wildcard, delim=None):
 
3771
    wildcard = wildcard.replace('*', '(?:.*?)')
 
3772
    if delim is None:
 
3773
        wildcard = wildcard.replace('%', '(?:.*?)')
 
3774
    else:
 
3775
        wildcard = wildcard.replace('%', '(?:(?:[^%s])*?)' % re.escape(delim))
 
3776
    return re.compile(wildcard, re.I)
 
3777
 
 
3778
def splitQuoted(s):
 
3779
    """Split a string into whitespace delimited tokens
 
3780
 
 
3781
    Tokens that would otherwise be separated but are surrounded by \"
 
3782
    remain as a single token.  Any token that is not quoted and is
 
3783
    equal to \"NIL\" is tokenized as C{None}.
 
3784
 
 
3785
    @type s: C{str}
 
3786
    @param s: The string to be split
 
3787
 
 
3788
    @rtype: C{list} of C{str}
 
3789
    @return: A list of the resulting tokens
 
3790
 
 
3791
    @raise MismatchedQuoting: Raised if an odd number of quotes are present
 
3792
    """
 
3793
    s = s.strip()
 
3794
    result = []
 
3795
    inQuote = inWord = start = 0
 
3796
    for (i, c) in zip(range(len(s)), s):
 
3797
        if c == '"' and not inQuote:
 
3798
            inQuote = 1
 
3799
            start = i + 1
 
3800
        elif c == '"' and inQuote:
 
3801
            inQuote = 0
 
3802
            result.append(s[start:i])
 
3803
            start = i + 1
 
3804
        elif not inWord and not inQuote and c not in ('"' + string.whitespace):
 
3805
            inWord = 1
 
3806
            start = i
 
3807
        elif inWord and not inQuote and c in string.whitespace:
 
3808
            if s[start:i] == 'NIL':
 
3809
                result.append(None)
 
3810
            else:
 
3811
                result.append(s[start:i])
 
3812
            start = i
 
3813
            inWord = 0
 
3814
    if inQuote:
 
3815
        raise MismatchedQuoting(s)
 
3816
    if inWord:
 
3817
        if s[start:] == 'NIL':
 
3818
            result.append(None)
 
3819
        else:
 
3820
            result.append(s[start:])
 
3821
    return result
 
3822
 
 
3823
 
 
3824
def splitOn(sequence, predicate, transformers):
 
3825
    result = []
 
3826
    mode = predicate(sequence[0])
 
3827
    tmp = [sequence[0]]
 
3828
    for e in sequence[1:]:
 
3829
        p = predicate(e)
 
3830
        if p != mode:
 
3831
            result.extend(transformers[mode](tmp))
 
3832
            tmp = [e]
 
3833
            mode = p
 
3834
        else:
 
3835
            tmp.append(e)
 
3836
    result.extend(transformers[mode](tmp))
 
3837
    return result
 
3838
 
 
3839
def collapseStrings(results):
 
3840
    """
 
3841
    Turns a list of length-one strings and lists into a list of longer
 
3842
    strings and lists.  For example,
 
3843
 
 
3844
    ['a', 'b', ['c', 'd']] is returned as ['ab', ['cd']]
 
3845
 
 
3846
    @type results: C{list} of C{str} and C{list}
 
3847
    @param results: The list to be collapsed
 
3848
 
 
3849
    @rtype: C{list} of C{str} and C{list}
 
3850
    @return: A new list which is the collapsed form of C{results}
 
3851
    """
 
3852
    copy = []
 
3853
    begun = None
 
3854
    listsList = [isinstance(s, types.ListType) for s in results]
 
3855
 
 
3856
    pred = lambda e: isinstance(e, types.TupleType)
 
3857
    tran = {
 
3858
        0: lambda e: splitQuoted(''.join(e)),
 
3859
        1: lambda e: [''.join([i[0] for i in e])]
 
3860
    }
 
3861
    for (i, c, isList) in zip(range(len(results)), results, listsList):
 
3862
        if isList:
 
3863
            if begun is not None:
 
3864
                copy.extend(splitOn(results[begun:i], pred, tran))
 
3865
                begun = None
 
3866
            copy.append(collapseStrings(c))
 
3867
        elif begun is None:
 
3868
            begun = i
 
3869
    if begun is not None:
 
3870
        copy.extend(splitOn(results[begun:], pred, tran))
 
3871
    return copy
 
3872
 
 
3873
 
 
3874
def parseNestedParens(s, handleLiteral = 1):
 
3875
    """Parse an s-exp-like string into a more useful data structure.
 
3876
 
 
3877
    @type s: C{str}
 
3878
    @param s: The s-exp-like string to parse
 
3879
 
 
3880
    @rtype: C{list} of C{str} and C{list}
 
3881
    @return: A list containing the tokens present in the input.
 
3882
 
 
3883
    @raise MismatchedNesting: Raised if the number or placement
 
3884
    of opening or closing parenthesis is invalid.
 
3885
    """
 
3886
    s = s.strip()
 
3887
    inQuote = 0
 
3888
    contentStack = [[]]
 
3889
    try:
 
3890
        i = 0
 
3891
        L = len(s)
 
3892
        while i < L:
 
3893
            c = s[i]
 
3894
            if inQuote:
 
3895
                if c == '\\':
 
3896
                    contentStack[-1].append(s[i+1])
 
3897
                    i += 2
 
3898
                    continue
 
3899
                elif c == '"':
 
3900
                    inQuote = not inQuote
 
3901
                contentStack[-1].append(c)
 
3902
                i += 1
 
3903
            else:
 
3904
                if c == '"':
 
3905
                    contentStack[-1].append(c)
 
3906
                    inQuote = not inQuote
 
3907
                    i += 1
 
3908
                elif handleLiteral and c == '{':
 
3909
                    end = s.find('}', i)
 
3910
                    if end == -1:
 
3911
                        raise ValueError, "Malformed literal"
 
3912
                    literalSize = int(s[i+1:end])
 
3913
                    contentStack[-1].append((s[end+3:end+3+literalSize],))
 
3914
                    i = end + 3 + literalSize
 
3915
                elif c == '(' or c == '[':
 
3916
                    contentStack.append([])
 
3917
                    i += 1
 
3918
                elif c == ')' or c == ']':
 
3919
                    contentStack[-2].append(contentStack.pop())
 
3920
                    i += 1
 
3921
                else:
 
3922
                    contentStack[-1].append(c)
 
3923
                    i += 1
 
3924
    except IndexError:
 
3925
        raise MismatchedNesting(s)
 
3926
    if len(contentStack) != 1:
 
3927
        raise MismatchedNesting(s)
 
3928
    return collapseStrings(contentStack[0])
 
3929
 
 
3930
def _quote(s):
 
3931
    return '"%s"' % (s.replace('\\', '\\\\').replace('"', '\\"'),)
 
3932
 
 
3933
def _literal(s):
 
3934
    return '{%d}\r\n%s' % (len(s), s)
 
3935
 
 
3936
class DontQuoteMe:
 
3937
    def __init__(self, value):
 
3938
        self.value = value
 
3939
 
 
3940
    def __str__(self):
 
3941
        return str(self.value)
 
3942
 
 
3943
_ATOM_SPECIALS = '(){ %*"'
 
3944
def _needsQuote(s):
 
3945
    if s == '':
 
3946
        return 1
 
3947
    for c in s:
 
3948
        if c < '\x20' or c > '\x7f':
 
3949
            return 1
 
3950
        if c in _ATOM_SPECIALS:
 
3951
            return 1
 
3952
    return 0
 
3953
 
 
3954
def _prepareMailboxName(name):
 
3955
    name = name.encode('imap4-utf-7')
 
3956
    if _needsQuote(name):
 
3957
        return _quote(name)
 
3958
    return name
 
3959
 
 
3960
def _needsLiteral(s):
 
3961
    # Change this to "return 1" to wig out stupid clients
 
3962
    return '\n' in s or '\r' in s or len(s) > 1000
 
3963
 
 
3964
def collapseNestedLists(items):
 
3965
    """Turn a nested list structure into an s-exp-like string.
 
3966
 
 
3967
    Strings in C{items} will be sent as literals if they contain CR or LF,
 
3968
    otherwise they will be quoted.  References to None in C{items} will be
 
3969
    translated to the atom NIL.  Objects with a 'read' attribute will have
 
3970
    it called on them with no arguments and the returned string will be
 
3971
    inserted into the output as a literal.  Integers will be converted to
 
3972
    strings and inserted into the output unquoted.  Instances of
 
3973
    C{DontQuoteMe} will be converted to strings and inserted into the output
 
3974
    unquoted.
 
3975
 
 
3976
    This function used to be much nicer, and only quote things that really
 
3977
    needed to be quoted (and C{DontQuoteMe} did not exist), however, many
 
3978
    broken IMAP4 clients were unable to deal with this level of sophistication,
 
3979
    forcing the current behavior to be adopted for practical reasons.
 
3980
 
 
3981
    @type items: Any iterable
 
3982
 
 
3983
    @rtype: C{str}
 
3984
    """
 
3985
    pieces = []
 
3986
    for i in items:
 
3987
        if i is None:
 
3988
            pieces.extend([' ', 'NIL'])
 
3989
        elif isinstance(i, (DontQuoteMe, int, long)):
 
3990
            pieces.extend([' ', str(i)])
 
3991
        elif isinstance(i, types.StringTypes):
 
3992
            if _needsLiteral(i):
 
3993
                pieces.extend([' ', '{', str(len(i)), '}', IMAP4Server.delimiter, i])
 
3994
            else:
 
3995
                pieces.extend([' ', _quote(i)])
 
3996
        elif hasattr(i, 'read'):
 
3997
            d = i.read()
 
3998
            pieces.extend([' ', '{', str(len(d)), '}', IMAP4Server.delimiter, d])
 
3999
        else:
 
4000
            pieces.extend([' ', '(%s)' % (collapseNestedLists(i),)])
 
4001
    return ''.join(pieces[1:])
 
4002
 
 
4003
 
 
4004
class IClientAuthentication(Interface):
 
4005
    def getName():
 
4006
        """Return an identifier associated with this authentication scheme.
 
4007
 
 
4008
        @rtype: C{str}
 
4009
        """
 
4010
 
 
4011
    def challengeResponse(secret, challenge):
 
4012
        """Generate a challenge response string"""
 
4013
 
 
4014
class CramMD5ClientAuthenticator:
 
4015
    implements(IClientAuthentication)
 
4016
 
 
4017
    def __init__(self, user):
 
4018
        self.user = user
 
4019
 
 
4020
    def getName(self):
 
4021
        return "CRAM-MD5"
 
4022
 
 
4023
    def challengeResponse(self, secret, chal):
 
4024
        response = hmac.HMAC(secret, chal).hexdigest()
 
4025
        return '%s %s' % (self.user, response)
 
4026
 
 
4027
class LOGINAuthenticator:
 
4028
    implements(IClientAuthentication)
 
4029
 
 
4030
    def __init__(self, user):
 
4031
        self.user = user
 
4032
        self.challengeResponse = self.challengeUsername
 
4033
 
 
4034
    def getName(self):
 
4035
        return "LOGIN"
 
4036
 
 
4037
    def challengeUsername(self, secret, chal):
 
4038
        # Respond to something like "Username:"
 
4039
        self.challengeResponse = self.challengeSecret
 
4040
        return self.user
 
4041
 
 
4042
    def challengeSecret(self, secret, chal):
 
4043
        # Respond to something like "Password:"
 
4044
        return secret
 
4045
 
 
4046
class PLAINAuthenticator:
 
4047
    implements(IClientAuthentication)
 
4048
 
 
4049
    def __init__(self, user):
 
4050
        self.user = user
 
4051
 
 
4052
    def getName(self):
 
4053
        return "PLAIN"
 
4054
 
 
4055
    def challengeResponse(self, secret, chal):
 
4056
        return '%s\0%s\0' % (self.user, secret)
 
4057
 
 
4058
 
 
4059
class MailboxException(IMAP4Exception): pass
 
4060
 
 
4061
class MailboxCollision(MailboxException):
 
4062
    def __str__(self):
 
4063
        return 'Mailbox named %s already exists' % self.args
 
4064
 
 
4065
class NoSuchMailbox(MailboxException):
 
4066
    def __str__(self):
 
4067
        return 'No mailbox named %s exists' % self.args
 
4068
 
 
4069
class ReadOnlyMailbox(MailboxException):
 
4070
    def __str__(self):
 
4071
        return 'Mailbox open in read-only state'
 
4072
 
 
4073
 
 
4074
class IAccount(Interface):
 
4075
    """Interface for Account classes
 
4076
 
 
4077
    Implementors of this interface should consider implementing
 
4078
    C{INamespacePresenter}.
 
4079
    """
 
4080
 
 
4081
    def addMailbox(name, mbox = None):
 
4082
        """Add a new mailbox to this account
 
4083
 
 
4084
        @type name: C{str}
 
4085
        @param name: The name associated with this mailbox.  It may not
 
4086
        contain multiple hierarchical parts.
 
4087
 
 
4088
        @type mbox: An object implementing C{IMailbox}
 
4089
        @param mbox: The mailbox to associate with this name.  If C{None},
 
4090
        a suitable default is created and used.
 
4091
 
 
4092
        @rtype: C{Deferred} or C{bool}
 
4093
        @return: A true value if the creation succeeds, or a deferred whose
 
4094
        callback will be invoked when the creation succeeds.
 
4095
 
 
4096
        @raise MailboxException: Raised if this mailbox cannot be added for
 
4097
        some reason.  This may also be raised asynchronously, if a C{Deferred}
 
4098
        is returned.
 
4099
        """
 
4100
 
 
4101
    def create(pathspec):
 
4102
        """Create a new mailbox from the given hierarchical name.
 
4103
 
 
4104
        @type pathspec: C{str}
 
4105
        @param pathspec: The full hierarchical name of a new mailbox to create.
 
4106
        If any of the inferior hierarchical names to this one do not exist,
 
4107
        they are created as well.
 
4108
 
 
4109
        @rtype: C{Deferred} or C{bool}
 
4110
        @return: A true value if the creation succeeds, or a deferred whose
 
4111
        callback will be invoked when the creation succeeds.
 
4112
 
 
4113
        @raise MailboxException: Raised if this mailbox cannot be added.
 
4114
        This may also be raised asynchronously, if a C{Deferred} is
 
4115
        returned.
 
4116
        """
 
4117
 
 
4118
    def select(name, rw=True):
 
4119
        """Acquire a mailbox, given its name.
 
4120
 
 
4121
        @type name: C{str}
 
4122
        @param name: The mailbox to acquire
 
4123
 
 
4124
        @type rw: C{bool}
 
4125
        @param rw: If a true value, request a read-write version of this
 
4126
        mailbox.  If a false value, request a read-only version.
 
4127
 
 
4128
        @rtype: Any object implementing C{IMailbox} or C{Deferred}
 
4129
        @return: The mailbox object, or a C{Deferred} whose callback will
 
4130
        be invoked with the mailbox object.  None may be returned if the
 
4131
        specified mailbox may not be selected for any reason.
 
4132
        """
 
4133
 
 
4134
    def delete(name):
 
4135
        """Delete the mailbox with the specified name.
 
4136
 
 
4137
        @type name: C{str}
 
4138
        @param name: The mailbox to delete.
 
4139
 
 
4140
        @rtype: C{Deferred} or C{bool}
 
4141
        @return: A true value if the mailbox is successfully deleted, or a
 
4142
        C{Deferred} whose callback will be invoked when the deletion
 
4143
        completes.
 
4144
 
 
4145
        @raise MailboxException: Raised if this mailbox cannot be deleted.
 
4146
        This may also be raised asynchronously, if a C{Deferred} is returned.
 
4147
        """
 
4148
 
 
4149
    def rename(oldname, newname):
 
4150
        """Rename a mailbox
 
4151
 
 
4152
        @type oldname: C{str}
 
4153
        @param oldname: The current name of the mailbox to rename.
 
4154
 
 
4155
        @type newname: C{str}
 
4156
        @param newname: The new name to associate with the mailbox.
 
4157
 
 
4158
        @rtype: C{Deferred} or C{bool}
 
4159
        @return: A true value if the mailbox is successfully renamed, or a
 
4160
        C{Deferred} whose callback will be invoked when the rename operation
 
4161
        is completed.
 
4162
 
 
4163
        @raise MailboxException: Raised if this mailbox cannot be
 
4164
        renamed.  This may also be raised asynchronously, if a C{Deferred}
 
4165
        is returned.
 
4166
        """
 
4167
 
 
4168
    def isSubscribed(name):
 
4169
        """Check the subscription status of a mailbox
 
4170
 
 
4171
        @type name: C{str}
 
4172
        @param name: The name of the mailbox to check
 
4173
 
 
4174
        @rtype: C{Deferred} or C{bool}
 
4175
        @return: A true value if the given mailbox is currently subscribed
 
4176
        to, a false value otherwise.  A C{Deferred} may also be returned
 
4177
        whose callback will be invoked with one of these values.
 
4178
        """
 
4179
 
 
4180
    def subscribe(name):
 
4181
        """Subscribe to a mailbox
 
4182
 
 
4183
        @type name: C{str}
 
4184
        @param name: The name of the mailbox to subscribe to
 
4185
 
 
4186
        @rtype: C{Deferred} or C{bool}
 
4187
        @return: A true value if the mailbox is subscribed to successfully,
 
4188
        or a Deferred whose callback will be invoked with this value when
 
4189
        the subscription is successful.
 
4190
 
 
4191
        @raise MailboxException: Raised if this mailbox cannot be
 
4192
        subscribed to.  This may also be raised asynchronously, if a
 
4193
        C{Deferred} is returned.
 
4194
        """
 
4195
 
 
4196
    def unsubscribe(name):
 
4197
        """Unsubscribe from a mailbox
 
4198
 
 
4199
        @type name: C{str}
 
4200
        @param name: The name of the mailbox to unsubscribe from
 
4201
 
 
4202
        @rtype: C{Deferred} or C{bool}
 
4203
        @return: A true value if the mailbox is unsubscribed from successfully,
 
4204
        or a Deferred whose callback will be invoked with this value when
 
4205
        the unsubscription is successful.
 
4206
 
 
4207
        @raise MailboxException: Raised if this mailbox cannot be
 
4208
        unsubscribed from.  This may also be raised asynchronously, if a
 
4209
        C{Deferred} is returned.
 
4210
        """
 
4211
 
 
4212
    def listMailboxes(ref, wildcard):
 
4213
        """List all the mailboxes that meet a certain criteria
 
4214
 
 
4215
        @type ref: C{str}
 
4216
        @param ref: The context in which to apply the wildcard
 
4217
 
 
4218
        @type wildcard: C{str}
 
4219
        @param wildcard: An expression against which to match mailbox names.
 
4220
        '*' matches any number of characters in a mailbox name, and '%'
 
4221
        matches similarly, but will not match across hierarchical boundaries.
 
4222
 
 
4223
        @rtype: C{list} of C{tuple}
 
4224
        @return: A list of C{(mailboxName, mailboxObject)} which meet the
 
4225
        given criteria.  C{mailboxObject} should implement either
 
4226
        C{IMailboxInfo} or C{IMailbox}.  A Deferred may also be returned. 
 
4227
        """
 
4228
 
 
4229
class INamespacePresenter(Interface):
 
4230
    def getPersonalNamespaces():
 
4231
        """Report the available personal namespaces.
 
4232
 
 
4233
        Typically there should be only one personal namespace.  A common
 
4234
        name for it is \"\", and its hierarchical delimiter is usually
 
4235
        \"/\".
 
4236
 
 
4237
        @rtype: iterable of two-tuples of strings
 
4238
        @return: The personal namespaces and their hierarchical delimiters.
 
4239
        If no namespaces of this type exist, None should be returned.
 
4240
        """
 
4241
 
 
4242
    def getSharedNamespaces():
 
4243
        """Report the available shared namespaces.
 
4244
 
 
4245
        Shared namespaces do not belong to any individual user but are
 
4246
        usually to one or more of them.  Examples of shared namespaces
 
4247
        might be \"#news\" for a usenet gateway.
 
4248
 
 
4249
        @rtype: iterable of two-tuples of strings
 
4250
        @return: The shared namespaces and their hierarchical delimiters.
 
4251
        If no namespaces of this type exist, None should be returned.
 
4252
        """
 
4253
 
 
4254
    def getUserNamespaces():
 
4255
        """Report the available user namespaces.
 
4256
 
 
4257
        These are namespaces that contain folders belonging to other users
 
4258
        access to which this account has been granted.
 
4259
 
 
4260
        @rtype: iterable of two-tuples of strings
 
4261
        @return: The user namespaces and their hierarchical delimiters.
 
4262
        If no namespaces of this type exist, None should be returned.
 
4263
        """
 
4264
 
 
4265
 
 
4266
class MemoryAccount(object):
 
4267
    implements(IAccount, INamespacePresenter)
 
4268
 
 
4269
    mailboxes = None
 
4270
    subscriptions = None
 
4271
    top_id = 0
 
4272
 
 
4273
    def __init__(self, name):
 
4274
        self.name = name
 
4275
        self.mailboxes = {}
 
4276
        self.subscriptions = []
 
4277
 
 
4278
    def allocateID(self):
 
4279
        id = self.top_id
 
4280
        self.top_id += 1
 
4281
        return id
 
4282
 
 
4283
    ##
 
4284
    ## IAccount
 
4285
    ##
 
4286
    def addMailbox(self, name, mbox = None):
 
4287
        name = name.upper()
 
4288
        if self.mailboxes.has_key(name):
 
4289
            raise MailboxCollision, name
 
4290
        if mbox is None:
 
4291
            mbox = self._emptyMailbox(name, self.allocateID())
 
4292
        self.mailboxes[name] = mbox
 
4293
        return 1
 
4294
 
 
4295
    def create(self, pathspec):
 
4296
        paths = filter(None, pathspec.split('/'))
 
4297
        for accum in range(1, len(paths)):
 
4298
            try:
 
4299
                self.addMailbox('/'.join(paths[:accum]))
 
4300
            except MailboxCollision:
 
4301
                pass
 
4302
        try:
 
4303
            self.addMailbox('/'.join(paths))
 
4304
        except MailboxCollision:
 
4305
            if not pathspec.endswith('/'):
 
4306
                return False
 
4307
        return True
 
4308
 
 
4309
    def _emptyMailbox(self, name, id):
 
4310
        raise NotImplementedError
 
4311
 
 
4312
    def select(self, name, readwrite=1):
 
4313
        return self.mailboxes.get(name.upper())
 
4314
 
 
4315
    def delete(self, name):
 
4316
        name = name.upper()
 
4317
        # See if this mailbox exists at all
 
4318
        mbox = self.mailboxes.get(name)
 
4319
        if not mbox:
 
4320
            raise MailboxException("No such mailbox")
 
4321
        # See if this box is flagged \Noselect
 
4322
        if r'\Noselect' in mbox.getFlags():
 
4323
            # Check for hierarchically inferior mailboxes with this one
 
4324
            # as part of their root.
 
4325
            for others in self.mailboxes.keys():
 
4326
                if others != name and others.startswith(name):
 
4327
                    raise MailboxException, "Hierarchically inferior mailboxes exist and \\Noselect is set"
 
4328
        mbox.destroy()
 
4329
 
 
4330
        # iff there are no hierarchically inferior names, we will
 
4331
        # delete it from our ken.
 
4332
        if self._inferiorNames(name) > 1:
 
4333
            del self.mailboxes[name]
 
4334
 
 
4335
    def rename(self, oldname, newname):
 
4336
        oldname = oldname.upper()
 
4337
        newname = newname.upper()
 
4338
        if not self.mailboxes.has_key(oldname):
 
4339
            raise NoSuchMailbox, oldname
 
4340
 
 
4341
        inferiors = self._inferiorNames(oldname)
 
4342
        inferiors = [(o, o.replace(oldname, newname, 1)) for o in inferiors]
 
4343
 
 
4344
        for (old, new) in inferiors:
 
4345
            if self.mailboxes.has_key(new):
 
4346
                raise MailboxCollision, new
 
4347
 
 
4348
        for (old, new) in inferiors:
 
4349
            self.mailboxes[new] = self.mailboxes[old]
 
4350
            del self.mailboxes[old]
 
4351
 
 
4352
    def _inferiorNames(self, name):
 
4353
        inferiors = []
 
4354
        for infname in self.mailboxes.keys():
 
4355
            if infname.startswith(name):
 
4356
                inferiors.append(infname)
 
4357
        return inferiors
 
4358
 
 
4359
    def isSubscribed(self, name):
 
4360
        return name.upper() in self.subscriptions
 
4361
 
 
4362
    def subscribe(self, name):
 
4363
        name = name.upper()
 
4364
        if name not in self.subscriptions:
 
4365
            self.subscriptions.append(name)
 
4366
 
 
4367
    def unsubscribe(self, name):
 
4368
        name = name.upper()
 
4369
        if name not in self.subscriptions:
 
4370
            raise MailboxException, "Not currently subscribed to " + name
 
4371
        self.subscriptions.remove(name)
 
4372
 
 
4373
    def listMailboxes(self, ref, wildcard):
 
4374
        ref = self._inferiorNames(ref.upper())
 
4375
        wildcard = wildcardToRegexp(wildcard, '/')
 
4376
        return [(i, self.mailboxes[i]) for i in ref if wildcard.match(i)]
 
4377
 
 
4378
    ##
 
4379
    ## INamespacePresenter
 
4380
    ##
 
4381
    def getPersonalNamespaces(self):
 
4382
        return [["", "/"]]
 
4383
 
 
4384
    def getSharedNamespaces(self):
 
4385
        return None
 
4386
 
 
4387
    def getOtherNamespaces(self):
 
4388
        return None
 
4389
 
 
4390
 
 
4391
 
 
4392
_statusRequestDict = {
 
4393
    'MESSAGES': 'getMessageCount',
 
4394
    'RECENT': 'getRecentCount',
 
4395
    'UIDNEXT': 'getUIDNext',
 
4396
    'UIDVALIDITY': 'getUIDValidity',
 
4397
    'UNSEEN': 'getUnseenCount'
 
4398
}
 
4399
def statusRequestHelper(mbox, names):
 
4400
    r = {}
 
4401
    for n in names:
 
4402
        r[n] = getattr(mbox, _statusRequestDict[n.upper()])()
 
4403
    return r
 
4404
 
 
4405
def parseAddr(addr):
 
4406
    if addr is None:
 
4407
        return [(None, None, None),]
 
4408
    addrs = email.Utils.getaddresses([addr])
 
4409
    return [[fn or None, None] + addr.split('@') for fn, addr in addrs]
 
4410
 
 
4411
def getEnvelope(msg):
 
4412
    headers = msg.getHeaders(True)
 
4413
    date = headers.get('date')
 
4414
    subject = headers.get('subject')
 
4415
    from_ = headers.get('from')
 
4416
    sender = headers.get('sender', from_)
 
4417
    reply_to = headers.get('reply-to', from_)
 
4418
    to = headers.get('to')
 
4419
    cc = headers.get('cc')
 
4420
    bcc = headers.get('bcc')
 
4421
    in_reply_to = headers.get('in-reply-to')
 
4422
    mid = headers.get('message-id')
 
4423
    return (date, subject, parseAddr(from_), parseAddr(sender),
 
4424
        reply_to and parseAddr(reply_to), to and parseAddr(to),
 
4425
        cc and parseAddr(cc), bcc and parseAddr(bcc), in_reply_to, mid)
 
4426
 
 
4427
def getLineCount(msg):
 
4428
    # XXX - Super expensive, CACHE THIS VALUE FOR LATER RE-USE
 
4429
    # XXX - This must be the number of lines in the ENCODED version
 
4430
    lines = 0
 
4431
    for _ in msg.getBodyFile():
 
4432
        lines += 1
 
4433
    return lines
 
4434
 
 
4435
def unquote(s):
 
4436
    if s[0] == s[-1] == '"':
 
4437
        return s[1:-1]
 
4438
    return s
 
4439
 
 
4440
def getBodyStructure(msg, extended=False):
 
4441
    # XXX - This does not properly handle multipart messages
 
4442
    # BODYSTRUCTURE is obscenely complex and criminally under-documented.
 
4443
 
 
4444
    attrs = {}
 
4445
    headers = 'content-type', 'content-id', 'content-description', 'content-transfer-encoding'
 
4446
    headers = msg.getHeaders(False, *headers)
 
4447
    mm = headers.get('content-type')
 
4448
    if mm:
 
4449
        mm = ''.join(mm.splitlines())
 
4450
        mimetype = mm.split(';')
 
4451
        if mimetype:
 
4452
            type = mimetype[0].split('/', 1)
 
4453
            if len(type) == 1:
 
4454
                major = type[0]
 
4455
                minor = None
 
4456
            elif len(type) == 2:
 
4457
                major, minor = type
 
4458
            else:
 
4459
                major = minor = None
 
4460
            attrs = dict([x.strip().lower().split('=', 1) for x in mimetype[1:]])
 
4461
        else:
 
4462
            major = minor = None
 
4463
    else:
 
4464
        major = minor = None
 
4465
 
 
4466
 
 
4467
    size = str(msg.getSize())
 
4468
    unquotedAttrs = [(k, unquote(v)) for (k, v) in attrs.iteritems()]
 
4469
    result = [
 
4470
        major, minor,                       # Main and Sub MIME types
 
4471
        unquotedAttrs,                      # content-type parameter list
 
4472
        headers.get('content-id'),
 
4473
        headers.get('content-description'),
 
4474
        headers.get('content-transfer-encoding'),
 
4475
        size,                               # Number of octets total
 
4476
    ]
 
4477
 
 
4478
    if major is not None:
 
4479
        if major.lower() == 'text':
 
4480
            result.append(str(getLineCount(msg)))
 
4481
        elif (major.lower(), minor.lower()) == ('message', 'rfc822'):
 
4482
            contained = msg.getSubPart(0)
 
4483
            result.append(getEnvelope(contained))
 
4484
            result.append(getBodyStructure(contained, False))
 
4485
            result.append(str(getLineCount(contained)))
 
4486
 
 
4487
    if not extended or major is None:
 
4488
        return result
 
4489
 
 
4490
    if major.lower() != 'multipart':
 
4491
        headers = 'content-md5', 'content-disposition', 'content-language'
 
4492
        headers = msg.getHeaders(False, *headers)
 
4493
        disp = headers.get('content-disposition')
 
4494
 
 
4495
        # XXX - I dunno if this is really right
 
4496
        if disp:
 
4497
            disp = disp.split('; ')
 
4498
            if len(disp) == 1:
 
4499
                disp = (disp[0].lower(), None)
 
4500
            elif len(disp) > 1:
 
4501
                disp = (disp[0].lower(), [x.split('=') for x in disp[1:]])
 
4502
 
 
4503
        result.append(headers.get('content-md5'))
 
4504
        result.append(disp)
 
4505
        result.append(headers.get('content-language'))
 
4506
    else:
 
4507
        result = [result]
 
4508
        try:
 
4509
            i = 0
 
4510
            while True:
 
4511
                submsg = msg.getSubPart(i)
 
4512
                result.append(getBodyStructure(submsg))
 
4513
                i += 1
 
4514
        except IndexError:
 
4515
            result.append(minor)
 
4516
            result.append(attrs.items())
 
4517
 
 
4518
            # XXX - I dunno if this is really right
 
4519
            headers = msg.getHeaders(False, 'content-disposition', 'content-language')
 
4520
            disp = headers.get('content-disposition')
 
4521
            if disp:
 
4522
                disp = disp.split('; ')
 
4523
                if len(disp) == 1:
 
4524
                    disp = (disp[0].lower(), None)
 
4525
                elif len(disp) > 1:
 
4526
                    disp = (disp[0].lower(), [x.split('=') for x in disp[1:]])
 
4527
 
 
4528
            result.append(disp)
 
4529
            result.append(headers.get('content-language'))
 
4530
 
 
4531
    return result
 
4532
 
 
4533
class IMessagePart(Interface):
 
4534
    def getHeaders(negate, *names):
 
4535
        """Retrieve a group of message headers.
 
4536
 
 
4537
        @type names: C{tuple} of C{str}
 
4538
        @param names: The names of the headers to retrieve or omit.
 
4539
 
 
4540
        @type negate: C{bool}
 
4541
        @param negate: If True, indicates that the headers listed in C{names}
 
4542
        should be omitted from the return value, rather than included.
 
4543
 
 
4544
        @rtype: C{dict}
 
4545
        @return: A mapping of header field names to header field values
 
4546
        """
 
4547
 
 
4548
    def getBodyFile():
 
4549
        """Retrieve a file object containing only the body of this message.
 
4550
        """
 
4551
 
 
4552
    def getSize():
 
4553
        """Retrieve the total size, in octets, of this message.
 
4554
 
 
4555
        @rtype: C{int}
 
4556
        """
 
4557
 
 
4558
    def isMultipart():
 
4559
        """Indicate whether this message has subparts.
 
4560
 
 
4561
        @rtype: C{bool}
 
4562
        """
 
4563
 
 
4564
    def getSubPart(part):
 
4565
        """Retrieve a MIME sub-message
 
4566
 
 
4567
        @type part: C{int}
 
4568
        @param part: The number of the part to retrieve, indexed from 0.
 
4569
 
 
4570
        @raise IndexError: Raised if the specified part does not exist.
 
4571
        @raise TypeError: Raised if this message is not multipart.
 
4572
 
 
4573
        @rtype: Any object implementing C{IMessagePart}.
 
4574
        @return: The specified sub-part.
 
4575
        """
 
4576
 
 
4577
class IMessage(IMessagePart):
 
4578
    def getUID():
 
4579
        """Retrieve the unique identifier associated with this message.
 
4580
        """
 
4581
 
 
4582
    def getFlags():
 
4583
        """Retrieve the flags associated with this message.
 
4584
 
 
4585
        @rtype: C{iterable}
 
4586
        @return: The flags, represented as strings.
 
4587
        """
 
4588
 
 
4589
    def getInternalDate():
 
4590
        """Retrieve the date internally associated with this message.
 
4591
 
 
4592
        @rtype: C{str}
 
4593
        @return: An RFC822-formatted date string.
 
4594
        """
 
4595
 
 
4596
class IMessageFile(Interface):
 
4597
    """Optional message interface for representing messages as files.
 
4598
    
 
4599
    If provided by message objects, this interface will be used instead
 
4600
    the more complex MIME-based interface.
 
4601
    """
 
4602
    def open():
 
4603
        """Return an file-like object opened for reading.
 
4604
 
 
4605
        Reading from the returned file will return all the bytes
 
4606
        of which this message consists.
 
4607
        """
 
4608
 
 
4609
class ISearchableMailbox(Interface):
 
4610
    def search(query, uid):
 
4611
        """Search for messages that meet the given query criteria.
 
4612
 
 
4613
        If this interface is not implemented by the mailbox, L{IMailbox.fetch}
 
4614
        and various methods of L{IMessage} will be used instead.
 
4615
 
 
4616
        Implementations which wish to offer better performance than the
 
4617
        default implementation should implement this interface.
 
4618
 
 
4619
        @type query: C{list}
 
4620
        @param query: The search criteria
 
4621
 
 
4622
        @type uid: C{bool}
 
4623
        @param uid: If true, the IDs specified in the query are UIDs;
 
4624
        otherwise they are message sequence IDs.
 
4625
 
 
4626
        @rtype: C{list} or C{Deferred}
 
4627
        @return: A list of message sequence numbers or message UIDs which
 
4628
        match the search criteria or a C{Deferred} whose callback will be
 
4629
        invoked with such a list.
 
4630
        """
 
4631
 
 
4632
class IMessageCopier(Interface):
 
4633
    def copy(messageObject):
 
4634
        """Copy the given message object into this mailbox.
 
4635
 
 
4636
        The message object will be one which was previously returned by
 
4637
        L{IMailbox.fetch}.
 
4638
 
 
4639
        Implementations which wish to offer better performance than the
 
4640
        default implementation should implement this interface.
 
4641
 
 
4642
        If this interface is not implemented by the mailbox, IMailbox.addMessage
 
4643
        will be used instead.
 
4644
 
 
4645
        @rtype: C{Deferred} or C{int}
 
4646
        @return: Either the UID of the message or a Deferred which fires
 
4647
        with the UID when the copy finishes.
 
4648
        """
 
4649
 
 
4650
class IMailboxInfo(Interface):
 
4651
    """Interface specifying only the methods required for C{listMailboxes}.
 
4652
 
 
4653
    Implementations can return objects implementing only these methods for
 
4654
    return to C{listMailboxes} if it can allow them to operate more
 
4655
    efficiently.
 
4656
    """
 
4657
 
 
4658
    def getFlags():
 
4659
        """Return the flags defined in this mailbox
 
4660
 
 
4661
        Flags with the \\ prefix are reserved for use as system flags.
 
4662
 
 
4663
        @rtype: C{list} of C{str}
 
4664
        @return: A list of the flags that can be set on messages in this mailbox.
 
4665
        """
 
4666
 
 
4667
    def getHierarchicalDelimiter():
 
4668
        """Get the character which delimits namespaces for in this mailbox.
 
4669
 
 
4670
        @rtype: C{str}
 
4671
        """
 
4672
 
 
4673
class IMailbox(IMailboxInfo):
 
4674
    def getUIDValidity():
 
4675
        """Return the unique validity identifier for this mailbox.
 
4676
 
 
4677
        @rtype: C{int}
 
4678
        """
 
4679
 
 
4680
    def getUIDNext():
 
4681
        """Return the likely UID for the next message added to this mailbox.
 
4682
 
 
4683
        @rtype: C{int}
 
4684
        """
 
4685
 
 
4686
    def getUID(message):
 
4687
        """Return the UID of a message in the mailbox
 
4688
 
 
4689
        @type message: C{int}
 
4690
        @param message: The message sequence number
 
4691
 
 
4692
        @rtype: C{int}
 
4693
        @return: The UID of the message.
 
4694
        """
 
4695
 
 
4696
    def getMessageCount():
 
4697
        """Return the number of messages in this mailbox.
 
4698
 
 
4699
        @rtype: C{int}
 
4700
        """
 
4701
 
 
4702
    def getRecentCount():
 
4703
        """Return the number of messages with the 'Recent' flag.
 
4704
 
 
4705
        @rtype: C{int}
 
4706
        """
 
4707
 
 
4708
    def getUnseenCount():
 
4709
        """Return the number of messages with the 'Unseen' flag.
 
4710
 
 
4711
        @rtype: C{int}
 
4712
        """
 
4713
 
 
4714
    def isWriteable():
 
4715
        """Get the read/write status of the mailbox.
 
4716
 
 
4717
        @rtype: C{int}
 
4718
        @return: A true value if write permission is allowed, a false value otherwise.
 
4719
        """
 
4720
 
 
4721
    def destroy():
 
4722
        """Called before this mailbox is deleted, permanently.
 
4723
 
 
4724
        If necessary, all resources held by this mailbox should be cleaned
 
4725
        up here.  This function _must_ set the \\Noselect flag on this
 
4726
        mailbox.
 
4727
        """
 
4728
 
 
4729
    def requestStatus(names):
 
4730
        """Return status information about this mailbox.
 
4731
 
 
4732
        Mailboxes which do not intend to do any special processing to
 
4733
        generate the return value, C{statusRequestHelper} can be used
 
4734
        to build the dictionary by calling the other interface methods
 
4735
        which return the data for each name.
 
4736
 
 
4737
        @type names: Any iterable
 
4738
        @param names: The status names to return information regarding.
 
4739
        The possible values for each name are: MESSAGES, RECENT, UIDNEXT,
 
4740
        UIDVALIDITY, UNSEEN.
 
4741
 
 
4742
        @rtype: C{dict} or C{Deferred}
 
4743
        @return: A dictionary containing status information about the
 
4744
        requested names is returned.  If the process of looking this
 
4745
        information up would be costly, a deferred whose callback will
 
4746
        eventually be passed this dictionary is returned instead.
 
4747
        """
 
4748
 
 
4749
    def addListener(listener):
 
4750
        """Add a mailbox change listener
 
4751
 
 
4752
        @type listener: Any object which implements C{IMailboxListener}
 
4753
        @param listener: An object to add to the set of those which will
 
4754
        be notified when the contents of this mailbox change.
 
4755
        """
 
4756
 
 
4757
    def removeListener(listener):
 
4758
        """Remove a mailbox change listener
 
4759
 
 
4760
        @type listener: Any object previously added to and not removed from
 
4761
        this mailbox as a listener.
 
4762
        @param listener: The object to remove from the set of listeners.
 
4763
 
 
4764
        @raise ValueError: Raised when the given object is not a listener for
 
4765
        this mailbox.
 
4766
        """
 
4767
 
 
4768
    def addMessage(message, flags = (), date = None):
 
4769
        """Add the given message to this mailbox.
 
4770
 
 
4771
        @type message: A file-like object
 
4772
        @param message: The RFC822 formatted message
 
4773
 
 
4774
        @type flags: Any iterable of C{str}
 
4775
        @param flags: The flags to associate with this message
 
4776
 
 
4777
        @type date: C{str}
 
4778
        @param date: If specified, the date to associate with this
 
4779
        message.
 
4780
 
 
4781
        @rtype: C{Deferred}
 
4782
        @return: A deferred whose callback is invoked with the message
 
4783
        id if the message is added successfully and whose errback is
 
4784
        invoked otherwise.
 
4785
 
 
4786
        @raise ReadOnlyMailbox: Raised if this Mailbox is not open for
 
4787
        read-write.
 
4788
        """
 
4789
 
 
4790
    def expunge():
 
4791
        """Remove all messages flagged \\Deleted.
 
4792
 
 
4793
        @rtype: C{list} or C{Deferred}
 
4794
        @return: The list of message sequence numbers which were deleted,
 
4795
        or a C{Deferred} whose callback will be invoked with such a list.
 
4796
 
 
4797
        @raise ReadOnlyMailbox: Raised if this Mailbox is not open for
 
4798
        read-write.
 
4799
        """
 
4800
 
 
4801
    def fetch(messages, uid):
 
4802
        """Retrieve one or more messages.
 
4803
 
 
4804
        @type messages: C{MessageSet}
 
4805
        @param messages: The identifiers of messages to retrieve information
 
4806
        about
 
4807
 
 
4808
        @type uid: C{bool}
 
4809
        @param uid: If true, the IDs specified in the query are UIDs;
 
4810
        otherwise they are message sequence IDs.
 
4811
 
 
4812
        @rtype: Any iterable of two-tuples of message sequence numbers and
 
4813
        implementors of C{IMessage}.
 
4814
        """
 
4815
 
 
4816
    def store(messages, flags, mode, uid):
 
4817
        """Set the flags of one or more messages.
 
4818
 
 
4819
        @type messages: A MessageSet object with the list of messages requested
 
4820
        @param messages: The identifiers of the messages to set the flags of.
 
4821
 
 
4822
        @type flags: sequence of C{str}
 
4823
        @param flags: The flags to set, unset, or add.
 
4824
 
 
4825
        @type mode: -1, 0, or 1
 
4826
        @param mode: If mode is -1, these flags should be removed from the
 
4827
        specified messages.  If mode is 1, these flags should be added to
 
4828
        the specified messages.  If mode is 0, all existing flags should be
 
4829
        cleared and these flags should be added.
 
4830
 
 
4831
        @type uid: C{bool}
 
4832
        @param uid: If true, the IDs specified in the query are UIDs;
 
4833
        otherwise they are message sequence IDs.
 
4834
 
 
4835
        @rtype: C{dict} or C{Deferred}
 
4836
        @return: A C{dict} mapping message sequence numbers to sequences of C{str}
 
4837
        representing the flags set on the message after this operation has
 
4838
        been performed, or a C{Deferred} whose callback will be invoked with
 
4839
        such a C{dict}.
 
4840
 
 
4841
        @raise ReadOnlyMailbox: Raised if this mailbox is not open for
 
4842
        read-write.
 
4843
        """
 
4844
 
 
4845
class ICloseableMailbox(Interface):
 
4846
    """A supplementary interface for mailboxes which require cleanup on close.
 
4847
 
 
4848
    Implementing this interface is optional.  If it is implemented, the protocol
 
4849
    code will call the close method defined whenever a mailbox is closed.
 
4850
    """
 
4851
    def close():
 
4852
        """Close this mailbox.
 
4853
 
 
4854
        @return: A C{Deferred} which fires when this mailbox
 
4855
        has been closed, or None if the mailbox can be closed
 
4856
        immediately.
 
4857
        """
 
4858
 
 
4859
def _formatHeaders(headers):
 
4860
    hdrs = [': '.join((k.title(), '\r\n'.join(v.splitlines()))) for (k, v)
 
4861
            in headers.iteritems()]
 
4862
    hdrs = '\r\n'.join(hdrs) + '\r\n'
 
4863
    return hdrs
 
4864
 
 
4865
def subparts(m):
 
4866
    i = 0
 
4867
    try:
 
4868
        while True:
 
4869
            yield m.getSubPart(i)
 
4870
            i += 1
 
4871
    except IndexError:
 
4872
        pass
 
4873
 
 
4874
def iterateInReactor(i):
 
4875
    """Consume an interator at most a single iteration per reactor iteration.
 
4876
 
 
4877
    If the iterator produces a Deferred, the next iteration will not occur
 
4878
    until the Deferred fires, otherwise the next iteration will be taken
 
4879
    in the next reactor iteration.
 
4880
 
 
4881
    @rtype: C{Deferred}
 
4882
    @return: A deferred which fires (with None) when the iterator is
 
4883
    exhausted or whose errback is called if there is an exception.
 
4884
    """
 
4885
    from twisted.internet import reactor
 
4886
    d = defer.Deferred()
 
4887
    def go(last):
 
4888
        try:
 
4889
            r = i.next()
 
4890
        except StopIteration:
 
4891
            d.callback(last)
 
4892
        except:
 
4893
            d.errback()
 
4894
        else:
 
4895
            if isinstance(r, defer.Deferred):
 
4896
                r.addCallback(go)
 
4897
            else:
 
4898
                reactor.callLater(0, go, r)
 
4899
    go(None)
 
4900
    return d
 
4901
 
 
4902
class MessageProducer:
 
4903
    CHUNK_SIZE = 2 ** 2 ** 2 ** 2
 
4904
 
 
4905
    def __init__(self, msg, buffer = None, scheduler = None):
 
4906
        """Produce this message.
 
4907
 
 
4908
        @param msg: The message I am to produce.
 
4909
        @type msg: L{IMessage}
 
4910
 
 
4911
        @param buffer: A buffer to hold the message in.  If None, I will
 
4912
            use a L{tempfile.TemporaryFile}.
 
4913
        @type buffer: file-like
 
4914
        """
 
4915
        self.msg = msg
 
4916
        if buffer is None:
 
4917
            buffer = tempfile.TemporaryFile()
 
4918
        self.buffer = buffer
 
4919
        if scheduler is None:
 
4920
            scheduler = iterateInReactor
 
4921
        self.scheduler = scheduler
 
4922
        self.write = self.buffer.write
 
4923
 
 
4924
    def beginProducing(self, consumer):
 
4925
        self.consumer = consumer
 
4926
        return self.scheduler(self._produce())
 
4927
 
 
4928
    def _produce(self):
 
4929
        headers = self.msg.getHeaders(True)
 
4930
        boundary = None
 
4931
        if self.msg.isMultipart():
 
4932
            content = headers.get('content-type')
 
4933
            parts = [x.split('=', 1) for x in content.split(';')[1:]]
 
4934
            parts = dict([(k.lower().strip(), v) for (k, v) in parts])
 
4935
            boundary = parts.get('boundary')
 
4936
            if boundary is None:
 
4937
                # Bastards
 
4938
                boundary = '----=_%f_boundary_%f' % (time.time(), random.random())
 
4939
                headers['content-type'] += '; boundary="%s"' % (boundary,)
 
4940
            else:
 
4941
                if boundary.startswith('"') and boundary.endswith('"'):
 
4942
                    boundary = boundary[1:-1]
 
4943
 
 
4944
        self.write(_formatHeaders(headers))
 
4945
        self.write('\r\n')
 
4946
        if self.msg.isMultipart():
 
4947
            for p in subparts(self.msg):
 
4948
                self.write('\r\n--%s\r\n' % (boundary,))
 
4949
                yield MessageProducer(p, self.buffer, self.scheduler
 
4950
                    ).beginProducing(None
 
4951
                    )
 
4952
            self.write('\r\n--%s--\r\n' % (boundary,))
 
4953
        else:
 
4954
            f = self.msg.getBodyFile()
 
4955
            while True:
 
4956
                b = f.read(self.CHUNK_SIZE)
 
4957
                if b:
 
4958
                    self.buffer.write(b)
 
4959
                    yield None
 
4960
                else:
 
4961
                    break
 
4962
        if self.consumer:
 
4963
            self.buffer.seek(0, 0)
 
4964
            yield FileProducer(self.buffer
 
4965
                ).beginProducing(self.consumer
 
4966
                ).addCallback(lambda _: self
 
4967
                )
 
4968
 
 
4969
class _FetchParser:
 
4970
    class Envelope:
 
4971
        # Response should be a list of fields from the message:
 
4972
        #   date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to,
 
4973
        #   and message-id.
 
4974
        #
 
4975
        # from, sender, reply-to, to, cc, and bcc are themselves lists of
 
4976
        # address information:
 
4977
        #   personal name, source route, mailbox name, host name
 
4978
        #
 
4979
        # reply-to and sender must not be None.  If not present in a message
 
4980
        # they should be defaulted to the value of the from field.
 
4981
        type = 'envelope'
 
4982
        __str__ = lambda self: 'envelope'
 
4983
 
 
4984
    class Flags:
 
4985
        type = 'flags'
 
4986
        __str__ = lambda self: 'flags'
 
4987
 
 
4988
    class InternalDate:
 
4989
        type = 'internaldate'
 
4990
        __str__ = lambda self: 'internaldate'
 
4991
 
 
4992
    class RFC822Header:
 
4993
        type = 'rfc822header'
 
4994
        __str__ = lambda self: 'rfc822.header'
 
4995
 
 
4996
    class RFC822Text:
 
4997
        type = 'rfc822text'
 
4998
        __str__ = lambda self: 'rfc822.text'
 
4999
 
 
5000
    class RFC822Size:
 
5001
        type = 'rfc822size'
 
5002
        __str__ = lambda self: 'rfc822.size'
 
5003
 
 
5004
    class RFC822:
 
5005
        type = 'rfc822'
 
5006
        __str__ = lambda self: 'rfc822'
 
5007
 
 
5008
    class UID:
 
5009
        type = 'uid'
 
5010
        __str__ = lambda self: 'uid'
 
5011
 
 
5012
    class Body:
 
5013
        type = 'body'
 
5014
        peek = False
 
5015
        header = None
 
5016
        mime = None
 
5017
        text = None
 
5018
        part = ()
 
5019
        empty = False
 
5020
        partialBegin = None
 
5021
        partialLength = None
 
5022
        def __str__(self):
 
5023
            base = 'BODY'
 
5024
            part = ''
 
5025
            separator = ''
 
5026
            if self.part:
 
5027
                part = '.'.join([str(x + 1) for x in self.part])
 
5028
                separator = '.'
 
5029
#            if self.peek:
 
5030
#                base += '.PEEK'
 
5031
            if self.header:
 
5032
                base += '[%s%s%s]' % (part, separator, self.header,)
 
5033
            elif self.text:
 
5034
                base += '[%s%sTEXT]' % (part, separator)
 
5035
            elif self.mime:
 
5036
                base += '[%s%sMIME]' % (part, separator)
 
5037
            elif self.empty:
 
5038
                base += '[%s]' % (part,)
 
5039
            if self.partialBegin is not None:
 
5040
                base += '<%d.%d>' % (self.partialBegin, self.partialLength)
 
5041
            return base
 
5042
 
 
5043
    class BodyStructure:
 
5044
        type = 'bodystructure'
 
5045
        __str__ = lambda self: 'bodystructure'
 
5046
 
 
5047
    # These three aren't top-level, they don't need type indicators
 
5048
    class Header:
 
5049
        negate = False
 
5050
        fields = None
 
5051
        part = None
 
5052
        def __str__(self):
 
5053
            base = 'HEADER'
 
5054
            if self.fields:
 
5055
                base += '.FIELDS'
 
5056
                if self.negate:
 
5057
                    base += '.NOT'
 
5058
                fields = []
 
5059
                for f in self.fields:
 
5060
                    f = f.title()
 
5061
                    if _needsQuote(f):
 
5062
                        f = _quote(f)
 
5063
                    fields.append(f)
 
5064
                base += ' (%s)' % ' '.join(fields)
 
5065
            if self.part:
 
5066
                base = '.'.join([str(x + 1) for x in self.part]) + '.' + base
 
5067
            return base
 
5068
 
 
5069
    class Text:
 
5070
        pass
 
5071
 
 
5072
    class MIME:
 
5073
        pass
 
5074
 
 
5075
    parts = None
 
5076
 
 
5077
    _simple_fetch_att = [
 
5078
        ('envelope', Envelope),
 
5079
        ('flags', Flags),
 
5080
        ('internaldate', InternalDate),
 
5081
        ('rfc822.header', RFC822Header),
 
5082
        ('rfc822.text', RFC822Text),
 
5083
        ('rfc822.size', RFC822Size),
 
5084
        ('rfc822', RFC822),
 
5085
        ('uid', UID),
 
5086
        ('bodystructure', BodyStructure),
 
5087
    ]
 
5088
 
 
5089
    def __init__(self):
 
5090
        self.state = ['initial']
 
5091
        self.result = []
 
5092
        self.remaining = ''
 
5093
 
 
5094
    def parseString(self, s):
 
5095
        s = self.remaining + s
 
5096
        try:
 
5097
            while s or self.state:
 
5098
                # print 'Entering state_' + self.state[-1] + ' with', repr(s)
 
5099
                state = self.state.pop()
 
5100
                try:
 
5101
                    used = getattr(self, 'state_' + state)(s)
 
5102
                except:
 
5103
                    self.state.append(state)
 
5104
                    raise
 
5105
                else:
 
5106
                    # print state, 'consumed', repr(s[:used])
 
5107
                    s = s[used:]
 
5108
        finally:
 
5109
            self.remaining = s
 
5110
 
 
5111
    def state_initial(self, s):
 
5112
        # In the initial state, the literals "ALL", "FULL", and "FAST"
 
5113
        # are accepted, as is a ( indicating the beginning of a fetch_att
 
5114
        # token, as is the beginning of a fetch_att token.
 
5115
        if s == '':
 
5116
            return 0
 
5117
 
 
5118
        l = s.lower()
 
5119
        if l.startswith('all'):
 
5120
            self.result.extend((
 
5121
                self.Flags(), self.InternalDate(),
 
5122
                self.RFC822Size(), self.Envelope()
 
5123
            ))
 
5124
            return 3
 
5125
        if l.startswith('full'):
 
5126
            self.result.extend((
 
5127
                self.Flags(), self.InternalDate(),
 
5128
                self.RFC822Size(), self.Envelope(),
 
5129
                self.Body()
 
5130
            ))
 
5131
            return 4
 
5132
        if l.startswith('fast'):
 
5133
            self.result.extend((
 
5134
                self.Flags(), self.InternalDate(), self.RFC822Size(),
 
5135
            ))
 
5136
            return 4
 
5137
 
 
5138
        if l.startswith('('):
 
5139
            self.state.extend(('close_paren', 'maybe_fetch_att', 'fetch_att'))
 
5140
            return 1
 
5141
 
 
5142
        self.state.append('fetch_att')
 
5143
        return 0
 
5144
 
 
5145
    def state_close_paren(self, s):
 
5146
        if s.startswith(')'):
 
5147
            return 1
 
5148
        raise Exception("Missing )")
 
5149
 
 
5150
    def state_whitespace(self, s):
 
5151
        # Eat up all the leading whitespace
 
5152
        if not s or not s[0].isspace():
 
5153
            raise Exception("Whitespace expected, none found")
 
5154
        i = 0
 
5155
        for i in range(len(s)):
 
5156
            if not s[i].isspace():
 
5157
                break
 
5158
        return i
 
5159
 
 
5160
    def state_maybe_fetch_att(self, s):
 
5161
        if not s.startswith(')'):
 
5162
            self.state.extend(('maybe_fetch_att', 'fetch_att', 'whitespace'))
 
5163
        return 0
 
5164
 
 
5165
    def state_fetch_att(self, s):
 
5166
        # Allowed fetch_att tokens are "ENVELOPE", "FLAGS", "INTERNALDATE",
 
5167
        # "RFC822", "RFC822.HEADER", "RFC822.SIZE", "RFC822.TEXT", "BODY",
 
5168
        # "BODYSTRUCTURE", "UID",
 
5169
        # "BODY [".PEEK"] [<section>] ["<" <number> "." <nz_number> ">"]
 
5170
 
 
5171
        l = s.lower()
 
5172
        for (name, cls) in self._simple_fetch_att:
 
5173
            if l.startswith(name):
 
5174
                self.result.append(cls())
 
5175
                return len(name)
 
5176
 
 
5177
        b = self.Body()
 
5178
        if l.startswith('body.peek'):
 
5179
            b.peek = True
 
5180
            used = 9
 
5181
        elif l.startswith('body'):
 
5182
            used = 4
 
5183
        else:
 
5184
            raise Exception("Nothing recognized in fetch_att: %s" % (l,))
 
5185
 
 
5186
        self.pending_body = b
 
5187
        self.state.extend(('got_body', 'maybe_partial', 'maybe_section'))
 
5188
        return used
 
5189
 
 
5190
    def state_got_body(self, s):
 
5191
        self.result.append(self.pending_body)
 
5192
        del self.pending_body
 
5193
        return 0
 
5194
 
 
5195
    def state_maybe_section(self, s):
 
5196
        if not s.startswith("["):
 
5197
            return 0
 
5198
 
 
5199
        self.state.extend(('section', 'part_number'))
 
5200
        return 1
 
5201
 
 
5202
    _partExpr = re.compile(r'(\d+(?:\.\d+)*)\.?')
 
5203
    def state_part_number(self, s):
 
5204
        m = self._partExpr.match(s)
 
5205
        if m is not None:
 
5206
            self.parts = [int(p) - 1 for p in m.groups()[0].split('.')]
 
5207
            return m.end()
 
5208
        else:
 
5209
            self.parts = []
 
5210
            return 0
 
5211
 
 
5212
    def state_section(self, s):
 
5213
        # Grab "HEADER]" or "HEADER.FIELDS (Header list)]" or
 
5214
        # "HEADER.FIELDS.NOT (Header list)]" or "TEXT]" or "MIME]" or
 
5215
        # just "]".
 
5216
 
 
5217
        l = s.lower()
 
5218
        used = 0
 
5219
        if l.startswith(']'):
 
5220
            self.pending_body.empty = True
 
5221
            used += 1
 
5222
        elif l.startswith('header]'):
 
5223
            h = self.pending_body.header = self.Header()
 
5224
            h.negate = True
 
5225
            h.fields = ()
 
5226
            used += 7
 
5227
        elif l.startswith('text]'):
 
5228
            self.pending_body.text = self.Text()
 
5229
            used += 5
 
5230
        elif l.startswith('mime]'):
 
5231
            self.pending_body.mime = self.MIME()
 
5232
            used += 5
 
5233
        else:
 
5234
            h = self.Header()
 
5235
            if l.startswith('header.fields.not'):
 
5236
                h.negate = True
 
5237
                used += 17
 
5238
            elif l.startswith('header.fields'):
 
5239
                used += 13
 
5240
            else:
 
5241
                raise Exception("Unhandled section contents: %r" % (l,))
 
5242
 
 
5243
            self.pending_body.header = h
 
5244
            self.state.extend(('finish_section', 'header_list', 'whitespace'))
 
5245
        self.pending_body.part = tuple(self.parts)
 
5246
        self.parts = None
 
5247
        return used
 
5248
 
 
5249
    def state_finish_section(self, s):
 
5250
        if not s.startswith(']'):
 
5251
            raise Exception("section must end with ]")
 
5252
        return 1
 
5253
 
 
5254
    def state_header_list(self, s):
 
5255
        if not s.startswith('('):
 
5256
            raise Exception("Header list must begin with (")
 
5257
        end = s.find(')')
 
5258
        if end == -1:
 
5259
            raise Exception("Header list must end with )")
 
5260
 
 
5261
        headers = s[1:end].split()
 
5262
        self.pending_body.header.fields = map(str.upper, headers)
 
5263
        return end + 1
 
5264
 
 
5265
    def state_maybe_partial(self, s):
 
5266
        # Grab <number.number> or nothing at all
 
5267
        if not s.startswith('<'):
 
5268
            return 0
 
5269
        end = s.find('>')
 
5270
        if end == -1:
 
5271
            raise Exception("Found < but not >")
 
5272
 
 
5273
        partial = s[1:end]
 
5274
        parts = partial.split('.', 1)
 
5275
        if len(parts) != 2:
 
5276
            raise Exception("Partial specification did not include two .-delimited integers")
 
5277
        begin, length = map(int, parts)
 
5278
        self.pending_body.partialBegin = begin
 
5279
        self.pending_body.partialLength = length
 
5280
 
 
5281
        return end + 1
 
5282
 
 
5283
class FileProducer:
 
5284
    CHUNK_SIZE = 2 ** 2 ** 2 ** 2
 
5285
 
 
5286
    firstWrite = True
 
5287
 
 
5288
    def __init__(self, f):
 
5289
        self.f = f
 
5290
 
 
5291
    def beginProducing(self, consumer):
 
5292
        self.consumer = consumer
 
5293
        self.produce = consumer.write
 
5294
        d = self._onDone = defer.Deferred()
 
5295
        self.consumer.registerProducer(self, False)
 
5296
        return d
 
5297
 
 
5298
    def resumeProducing(self):
 
5299
        b = ''
 
5300
        if self.firstWrite:
 
5301
            b = '{%d}\r\n' % self._size()
 
5302
            self.firstWrite = False
 
5303
        if not self.f:
 
5304
            return
 
5305
        b = b + self.f.read(self.CHUNK_SIZE)
 
5306
        if not b:
 
5307
            self.consumer.unregisterProducer()
 
5308
            self._onDone.callback(self)
 
5309
            self._onDone = self.f = self.consumer = None
 
5310
        else:
 
5311
            self.produce(b)
 
5312
 
 
5313
    def pauseProducing(self):
 
5314
        pass
 
5315
 
 
5316
    def stopProducing(self):
 
5317
        pass
 
5318
 
 
5319
    def _size(self):
 
5320
        b = self.f.tell()
 
5321
        self.f.seek(0, 2)
 
5322
        e = self.f.tell()
 
5323
        self.f.seek(b, 0)
 
5324
        return e - b
 
5325
 
 
5326
def parseTime(s):
 
5327
    # XXX - This may require localization :(
 
5328
    months = [
 
5329
        'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct',
 
5330
        'nov', 'dec', 'january', 'february', 'march', 'april', 'may', 'june',
 
5331
        'july', 'august', 'september', 'october', 'november', 'december'
 
5332
    ]
 
5333
    expr = {
 
5334
        'day': r"(?P<day>3[0-1]|[1-2]\d|0[1-9]|[1-9]| [1-9])",
 
5335
        'mon': r"(?P<mon>\w+)",
 
5336
        'year': r"(?P<year>\d\d\d\d)"
 
5337
    }
 
5338
    m = re.match('%(day)s-%(mon)s-%(year)s' % expr, s)
 
5339
    if not m:
 
5340
        raise ValueError, "Cannot parse time string %r" % (s,)
 
5341
    d = m.groupdict()
 
5342
    try:
 
5343
        d['mon'] = 1 + (months.index(d['mon'].lower()) % 12)
 
5344
        d['year'] = int(d['year'])
 
5345
        d['day'] = int(d['day'])
 
5346
    except ValueError:
 
5347
        raise ValueError, "Cannot parse time string %r" % (s,)
 
5348
    else:
 
5349
        return time.struct_time(
 
5350
            (d['year'], d['mon'], d['day'], 0, 0, 0, -1, -1, -1)
 
5351
        )
 
5352
 
 
5353
import codecs
 
5354
def modified_base64(s):
 
5355
    s_utf7 = s.encode('utf-7')
 
5356
    return s_utf7[1:-1].replace('/', ',')
 
5357
 
 
5358
def modified_unbase64(s):
 
5359
    s_utf7 = '+' + s.replace(',', '/') + '-'
 
5360
    return s_utf7.decode('utf-7')
 
5361
 
 
5362
def encoder(s):
 
5363
    r = []
 
5364
    _in = []
 
5365
    for c in s:
 
5366
        if ord(c) in (range(0x20, 0x26) + range(0x27, 0x7f)):
 
5367
            if _in:
 
5368
                r.extend(['&', modified_base64(''.join(_in)), '-'])
 
5369
                del _in[:]
 
5370
            r.append(str(c))
 
5371
        elif c == '&':
 
5372
            if _in:
 
5373
                r.extend(['&', modified_base64(''.join(_in)), '-'])
 
5374
                del _in[:]
 
5375
            r.append('&-')
 
5376
        else:
 
5377
            _in.append(c)
 
5378
    if _in:
 
5379
        r.extend(['&', modified_base64(''.join(_in)), '-'])
 
5380
    return (''.join(r), len(s))
 
5381
 
 
5382
def decoder(s):
 
5383
    r = []
 
5384
    decode = []
 
5385
    for c in s:
 
5386
        if c == '&' and not decode:
 
5387
            decode.append('&')
 
5388
        elif c == '-' and decode:
 
5389
            if len(decode) == 1:
 
5390
                r.append('&')
 
5391
            else:
 
5392
                r.append(modified_unbase64(''.join(decode[1:])))
 
5393
            decode = []
 
5394
        elif decode:
 
5395
            decode.append(c)
 
5396
        else:
 
5397
            r.append(c)
 
5398
    if decode:
 
5399
        r.append(modified_unbase64(''.join(decode[1:])))
 
5400
    return (''.join(r), len(s))
 
5401
 
 
5402
class StreamReader(codecs.StreamReader):
 
5403
    def decode(self, s, errors='strict'):
 
5404
        return decoder(s)
 
5405
 
 
5406
class StreamWriter(codecs.StreamWriter):
 
5407
    def decode(self, s, errors='strict'):
 
5408
        return encoder(s)
 
5409
 
 
5410
def imap4_utf_7(name):
 
5411
    if name == 'imap4-utf-7':
 
5412
        return (encoder, decoder, StreamReader, StreamWriter)
 
5413
codecs.register(imap4_utf_7)
 
5414
 
 
5415
__all__ = [
 
5416
    # Protocol classes
 
5417
    'IMAP4Server', 'IMAP4Client',
 
5418
    
 
5419
    # Interfaces
 
5420
    'IMailboxListener', 'IClientAuthentication', 'IAccount', 'IMailbox',
 
5421
    'INamespacePresenter', 'ICloseableMailbox', 'IMailboxInfo',
 
5422
    'IMessage', 'IMessageCopier', 'IMessageFile', 'ISearchableMailbox',
 
5423
 
 
5424
    # Exceptions
 
5425
    'IMAP4Exception', 'IllegalClientResponse', 'IllegalOperation',
 
5426
    'IllegalMailboxEncoding', 'UnhandledResponse', 'NegativeResponse',
 
5427
    'NoSupportedAuthentication', 'IllegalServerResponse',
 
5428
    'IllegalIdentifierError', 'IllegalQueryError', 'MismatchedNesting',
 
5429
    'MismatchedQuoting', 'MailboxException', 'MailboxCollision',
 
5430
    'NoSuchMailbox', 'ReadOnlyMailbox',
 
5431
 
 
5432
    # Auth objects
 
5433
    'CramMD5ClientAuthenticator', 'PLAINAuthenticator', 'LOGINAuthenticator',
 
5434
    'PLAINCredentials', 'LOGINCredentials',
 
5435
 
 
5436
    # Simple query interface
 
5437
    'Query', 'Not', 'Or',
 
5438
    
 
5439
    # Miscellaneous
 
5440
    'MemoryAccount',
 
5441
    'statusRequestHelper',
 
5442
    
 
5443
    
 
5444
]