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