~justin-fathomdb/nova/justinsb-openstack-api-volumes

« back to all changes in this revision

Viewing changes to vendor/Twisted-10.0.0/twisted/mail/imap4.py

  • Committer: Jesse Andrews
  • Date: 2010-05-28 06:05:26 UTC
  • Revision ID: git-v1:bf6e6e718cdc7488e2da87b21e258ccc065fe499
initial commit

Show diffs side-by-side

added added

removed removed

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