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

« back to all changes in this revision

Viewing changes to twisted/protocols/imap4.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2004-06-21 22:01:11 UTC
  • mto: (2.2.3 sid)
  • mto: This revision was merged to the branch mainline in revision 3.
  • Revision ID: james.westby@ubuntu.com-20040621220111-vkf909euqnyrp3nr
Tags: upstream-1.3.0
ImportĀ upstreamĀ versionĀ 1.3.0

Show diffs side-by-side

added added

removed removed

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