root / trunk / twisted / mail / imap4.py

Revision 26520, 186.0 kB (checked in by exarkun, 3 months ago)

Merge imap4-unsolicited-responses-1105

Author: jhohm, exarkun
Reviewer: glyph
Fixes: #1105

Handle more unsolicited data received from the server instead of
printing it out and otherwise ignoring it.

In particular, now if unsolicited data is received on the same
line as solicited data, it is separated from the solicited data
and delivered to the appropriate callback. Previously, this case
could cause the entire line to be ignored or it could cause the
unsolicited data to be delivered to the application callback with
the solicited data.

Line 
1 # -*- test-case-name: twisted.mail.test.test_imap -*-
2 # Copyright (c) 2001-2009 Twisted Matrix Laboratories.
3 # See LICENSE for details.
4
5
6 """
7 An IMAP4 protocol implementation
8
9 @author: Jp Calderone
10
11 To do::
12   Suspend idle timeout while server is processing
13   Use an async message parser instead of buffering in memory
14   Figure out a way to not queue multi-message client requests (Flow? A simple callback?)
15   Clarify some API docs (Query, etc)
16   Make APPEND recognize (again) non-existent mailboxes before accepting the literal
17 """
18
19 import rfc822
20 import base64
21 import binascii
22 import hmac
23 import re
24 import tempfile
25 import string
26 import time
27 import random
28 import types
29
30 import email.Utils
31
32 try:
33     import cStringIO as StringIO
34 except:
35     import StringIO
36
37 from zope.interface import implements, Interface
38
39 from twisted.protocols import basic
40 from twisted.protocols import policies
41 from twisted.internet import defer
42 from twisted.internet import error
43 from twisted.internet.defer import maybeDeferred
44 from twisted.python import log, text
45 from twisted.internet import interfaces
46
47 from twisted import cred
48 import twisted.cred.error
49 import twisted.cred.credentials
50
51 class MessageSet(object):
52     """
53     Essentially an infinite bitfield, with some extra features.
54
55     @type getnext: Function taking C{int} returning C{int}
56     @ivar getnext: A function that returns the next message number,
57     used when iterating through the MessageSet. By default, a function
58     returning the next integer is supplied, but as this can be rather
59     inefficient for sparse UID iterations, it is recommended to supply
60     one when messages are requested by UID.  The argument is provided
61     as a hint to the implementation and may be ignored if it makes sense
62     to do so (eg, if an iterator is being used that maintains its own
63     state, it is guaranteed that it will not be called out-of-order).
64     """
65     _empty = []
66
67     def __init__(self, start=_empty, end=_empty):
68         """
69         Create a new MessageSet()
70
71         @type start: Optional C{int}
72         @param start: Start of range, or only message number
73
74         @type end: Optional C{int}
75         @param end: End of range.
76         """
77         self._last = self._empty # Last message/UID in use
78         self.ranges = [] # List of ranges included
79         self.getnext = lambda x: x+1 # A function which will return the next
80                                      # message id. Handy for UID requests.
81
82         if start is self._empty:
83             return
84
85         if isinstance(start, types.ListType):
86             self.ranges = start[:]
87             self.clean()
88         else:
89             self.add(start,end)
90
91     # Ooo.  A property.
92     def last():
93         def _setLast(self,value):
94             if self._last is not self._empty:
95                 raise ValueError("last already set")
96
97             self._last = value
98             for i,(l,h) in enumerate(self.ranges):
99                 if l is not None:
100                     break # There are no more Nones after this
101                 l = value
102                 if h is None:
103                     h = value
104                 if l > h:
105                     l, h = h, l
106                 self.ranges[i] = (l,h)
107
108             self.clean()
109
110         def _getLast(self):
111             return self._last
112
113         doc = '''
114             "Highest" message number, refered to by "*".
115             Must be set before attempting to use the MessageSet.
116         '''
117         return _getLast, _setLast, None, doc
118     last = property(*last())
119
120     def add(self, start, end=_empty):
121         """
122         Add another range
123
124         @type start: C{int}
125         @param start: Start of range, or only message number
126
127         @type end: Optional C{int}
128         @param end: End of range.
129         """
130         if end is self._empty:
131             end = start
132
133         if self._last is not self._empty:
134             if start is None:
135                 start = self.last
136             if end is None:
137                 end = self.last
138
139         if start > end:
140             # Try to keep in low, high order if possible
141             # (But we don't know what None means, this will keep
142             # None at the start of the ranges list)
143             start, end = end, start
144
145         self.ranges.append((start,end))
146         self.clean()
147
148     def __add__(self, other):
149         if isinstance(other, MessageSet):
150             ranges = self.ranges + other.ranges
151             return MessageSet(ranges)
152         else:
153             res = MessageSet(self.ranges)
154             try:
155                 res.add(*other)
156             except TypeError:
157                 res.add(other)
158             return res
159
160     def extend(self, other):
161         if isinstance(other, MessageSet):
162             self.ranges.extend(other.ranges)
163             self.clean()
164         else:
165             try:
166                 self.add(*other)
167             except TypeError:
168                 self.add(other)
169
170         return self
171
172     def clean(self):
173         """
174         Clean ranges list, combining adjacent ranges
175         """
176
177         self.ranges.sort()
178
179         oldl, oldh = None, None
180         for i,(l,h) in enumerate(self.ranges):
181             if l is None:
182                 continue
183             # l is >= oldl and h is >= oldh due to sort()
184             if oldl is not None and l <= oldh+1:
185                 l = oldl
186                 h = max(oldh,h)
187                 self.ranges[i-1] = None
188                 self.ranges[i] = (l,h)
189
190             oldl,oldh = l,h
191
192         self.ranges = filter(None, self.ranges)
193
194     def __contains__(self, value):
195         """
196         May raise TypeError if we encounter unknown "high" values
197         """
198         for l,h in self.ranges:
199             if l is None:
200                 raise TypeError(
201                     "Can't determine membership; last value not set")
202             if l <= value <= h:
203                 return True
204
205         return False
206
207     def _iterator(self):
208         for l,h in self.ranges:
209             l = self.getnext(l-1)
210             while l <= h:
211                 yield l
212                 l = self.getnext(l)
213                 if l is None:
214                     break
215
216     def __iter__(self):
217         if self.ranges and self.ranges[0][0] is None:
218             raise TypeError("Can't iterate; last value not set")
219
220         return self._iterator()
221
222     def __len__(self):
223         res = 0
224         for l, h in self.ranges:
225             if l is None:
226                 raise TypeError("Can't size object; last value not set")
227             res += (h - l) + 1
228
229         return res
230
231     def __str__(self):
232         p = []
233         for low, high in self.ranges:
234             if low == high:
235                 if low is None:
236                     p.append('*')
237                 else:
238                     p.append(str(low))
239             elif low is None:
240                 p.append('%d:*' % (high,))
241             else:
242                 p.append('%d:%d' % (low, high))
243         return ','.join(p)
244
245     def __repr__(self):
246         return '<MessageSet %s>' % (str(self),)
247
248     def __eq__(self, other):
249         if isinstance(other, MessageSet):
250             return self.ranges == other.ranges
251         return False
252
253
254 class LiteralString:
255     def __init__(self, size, defered):
256         self.size = size
257         self.data = []
258         self.defer = defered
259
260     def write(self, data):
261         self.size -= len(data)
262         passon = None
263         if self.size > 0:
264             self.data.append(data)
265         else:
266             if self.size:
267                 data, passon = data[:self.size], data[self.size:]
268             else:
269                 passon = ''
270             if data:
271                 self.data.append(data)
272         return passon
273
274     def callback(self, line):
275         """
276         Call defered with data and rest of line
277         """
278         self.defer.callback((''.join(self.data), line))
279
280 class LiteralFile:
281     _memoryFileLimit = 1024 * 1024 * 10
282
283     def __init__(self, size, defered):
284         self.size = size
285         self.defer = defered
286         if size > self._memoryFileLimit:
287             self.data = tempfile.TemporaryFile()
288         else:
289             self.data = StringIO.StringIO()
290
291     def write(self, data):
292         self.size -= len(data)
293         passon = None
294         if self.size > 0:
295             self.data.write(data)
296         else:
297             if self.size:
298                 data, passon = data[:self.size], data[self.size:]
299             else:
300                 passon = ''
301             if data:
302                 self.data.write(data)
303         return passon
304
305     def callback(self, line):
306         """
307         Call defered with data and rest of line
308         """
309         self.data.seek(0,0)
310         self.defer.callback((self.data, line))
311
312
313 class WriteBuffer:
314     """Buffer up a bunch of writes before sending them all to a transport at once.
315     """
316     def __init__(self, transport, size=8192):
317         self.bufferSize = size
318         self.transport = transport
319         self._length = 0
320         self._writes = []
321
322     def write(self, s):
323         self._length += len(s)
324         self._writes.append(s)
325         if self._length > self.bufferSize:
326             self.flush()
327
328     def flush(self):
329         if self._writes:
330             self.transport.writeSequence(self._writes)
331             self._writes = []
332             self._length = 0
333
334
335 class Command:
336     _1_RESPONSES = ('CAPABILITY', 'FLAGS', 'LIST', 'LSUB', 'STATUS', 'SEARCH', 'NAMESPACE')
337     _2_RESPONSES = ('EXISTS', 'EXPUNGE', 'FETCH', 'RECENT')
338     _OK_RESPONSES = ('UIDVALIDITY', 'UNSEEN', 'READ-WRITE', 'READ-ONLY', 'UIDNEXT', 'PERMANENTFLAGS')
339     defer = None
340
341     def __init__(self, command, args=None, wantResponse=(),
342                  continuation=None, *contArgs, **contKw):
343         self.command = command
344         self.args = args
345         self.wantResponse = wantResponse
346         self.continuation = lambda x: continuation(x, *contArgs, **contKw)
347         self.lines = []
348
349     def format(self, tag):
350         if self.args is None:
351             return ' '.join((tag, self.command))
352         return ' '.join((tag, self.command, self.args))
353
354     def finish(self, lastLine, unusedCallback):
355         send = []
356         unuse = []
357         for L in self.lines:
358             names = parseNestedParens(L)
359             N = len(names)
360             if (N >= 1 and names[0] in self._1_RESPONSES or
361                 N >= 2 and names[1] in self._2_RESPONSES or
362                 N >= 2 and names[0] == 'OK' and isinstance(names[1], types.ListType) and names[1][0] in self._OK_RESPONSES):
363                 send.append(names)
364             else:
365                 unuse.append(names)
366         d, self.defer = self.defer, None
367         d.callback((send, lastLine))
368         if unuse:
369             unusedCallback(unuse)
370
371 class LOGINCredentials(cred.credentials.UsernamePassword):
372     def __init__(self):
373         self.challenges = ['Password\0', 'User Name\0']
374         self.responses = ['password', 'username']
375         cred.credentials.UsernamePassword.__init__(self, None, None)
376
377     def getChallenge(self):
378         return self.challenges.pop()
379
380     def setResponse(self, response):
381         setattr(self, self.responses.pop(), response)
382
383     def moreChallenges(self):
384         return bool(self.challenges)
385
386 class PLAINCredentials(cred.credentials.UsernamePassword):
387     def __init__(self):
388         cred.credentials.UsernamePassword.__init__(self, None, None)
389
390     def getChallenge(self):
391         return ''
392
393     def setResponse(self, response):
394         parts = response[:-1].split('\0', 1)
395         if len(parts) != 2:
396             raise IllegalClientResponse("Malformed Response - wrong number of parts")
397         self.username, self.password = parts
398
399     def moreChallenges(self):
400         return False
401
402 class IMAP4Exception(Exception):
403     def __init__(self, *args):
404         Exception.__init__(self, *args)
405
406 class IllegalClientResponse(IMAP4Exception): pass
407
408 class IllegalOperation(IMAP4Exception): pass
409
410 class IllegalMailboxEncoding(IMAP4Exception): pass
411
412 class IMailboxListener(Interface):
413     """Interface for objects interested in mailbox events"""
414
415     def modeChanged(writeable):
416         """Indicates that the write status of a mailbox has changed.
417
418         @type writeable: C{bool}
419         @param writeable: A true value if write is now allowed, false
420         otherwise.
421         """
422
423     def flagsChanged(newFlags):
424         """Indicates that the flags of one or more messages have changed.
425
426         @type newFlags: C{dict}
427         @param newFlags: A mapping of message identifiers to tuples of flags
428         now set on that message.
429         """
430
431     def newMessages(exists, recent):
432         """Indicates that the number of messages in a mailbox has changed.
433
434         @type exists: C{int} or C{None}
435         @param exists: The total number of messages now in this mailbox.
436         If the total number of messages has not changed, this should be
437         C{None}.
438
439         @type recent: C{int}
440         @param recent: The number of messages now flagged \\Recent.
441         If the number of recent messages has not changed, this should be
442         C{None}.
443         """
444
445 class IMAP4Server(basic.LineReceiver, policies.TimeoutMixin):
446     """
447     Protocol implementation for an IMAP4rev1 server.
448
449     The server can be in any of four states:
450         - Non-authenticated
451         - Authenticated
452         - Selected
453         - Logout
454     """
455     implements(IMailboxListener)
456
457     # Identifier for this server software
458     IDENT = 'Twisted IMAP4rev1 Ready'
459
460     # Number of seconds before idle timeout
461     # Initially 1 minute.  Raised to 30 minutes after login.
462     timeOut = 60
463
464     POSTAUTH_TIMEOUT = 60 * 30
465
466     # Whether STARTTLS has been issued successfully yet or not.
467     startedTLS = False
468
469     # Whether our transport supports TLS
470     canStartTLS = False
471
472     # Mapping of tags to commands we have received
473     tags = None
474
475     # The object which will handle logins for us
476     portal = None
477
478     # The account object for this connection
479     account = None
480
481     # Logout callback
482     _onLogout = None
483
484     # The currently selected mailbox
485     mbox = None
486
487     # Command data to be processed when literal data is received
488     _pendingLiteral = None
489
490     # Maximum length to accept for a "short" string literal
491     _literalStringLimit = 4096
492
493     # IChallengeResponse factories for AUTHENTICATE command
494     challengers = None
495
496     state = 'unauth'
497
498     parseState = 'command'
499
500     def __init__(self, chal = None, contextFactory = None, scheduler = None):
501         if chal is None:
502             chal = {}
503         self.challengers = chal
504         self.ctx = contextFactory
505         if scheduler is None:
506             scheduler = iterateInReactor
507         self._scheduler = scheduler
508         self._queuedAsync = []
509
510     def capabilities(self):
511         cap = {'AUTH': self.challengers.keys()}
512         if self.ctx and self.canStartTLS:
513             if not self.startedTLS and interfaces.ISSLTransport(self.transport, None) is None:
514                 cap['LOGINDISABLED'] = None
515                 cap['STARTTLS'] = None
516         cap['NAMESPACE'] = None
517         cap['IDLE'] = None
518         return cap
519
520     def connectionMade(self):
521         self.tags = {}
522         self.canStartTLS = interfaces.ITLSTransport(self.transport, None) is not None
523         self.setTimeout(self.timeOut)
524         self.sendServerGreeting()
525
526     def connectionLost(self, reason):
527         self.setTimeout(None)
528         if self._onLogout:
529             self._onLogout()
530             self._onLogout = None
531
532     def timeoutConnection(self):
533         self.sendLine('* BYE Autologout; connection idle too long')
534         self.transport.loseConnection()
535         if self.mbox:
536             self.mbox.removeListener(self)
537             cmbx = ICloseableMailbox(self.mbox, None)
538             if cmbx is not None:
539                 maybeDeferred(cmbx.close).addErrback(log.err)
540             self.mbox = None
541         self.state = 'timeout'
542
543     def rawDataReceived(self, data):
544         self.resetTimeout()
545         passon = self._pendingLiteral.write(data)
546         if passon is not None:
547             self.setLineMode(passon)
548
549     # Avoid processing commands while buffers are being dumped to
550     # our transport
551     blocked = None
552
553     def _unblock(self):
554         commands = self.blocked
555         self.blocked = None
556         while commands and self.blocked is None:
557             self.lineReceived(commands.pop(0))
558         if self.blocked is not None:
559             self.blocked.extend(commands)
560
561 #    def sendLine(self, line):
562 #        print 'C:', repr(line)
563 #        return basic.LineReceiver.sendLine(self, line)
564
565     def lineReceived(self, line):
566 #        print 'S:', repr(line)
567         if self.blocked is not None:
568             self.blocked.append(line)
569             return
570
571         self.resetTimeout()
572
573         f = getattr(self, 'parse_' + self.parseState)
574         try:
575             f(line)
576         except Exception, e:
577             self.sendUntaggedResponse('BAD Server error: ' + str(e))
578             log.err()
579
580     def parse_command(self, line):
581         args = line.split(None, 2)
582         rest = None
583         if len(args) == 3:
584             tag, cmd, rest = args
585         elif len(args) == 2:
586             tag, cmd = args
587         elif len(args) == 1:
588             tag = args[0]
589             self.sendBadResponse(tag, 'Missing command')
590             return None
591         else:
592             self.sendBadResponse(None, 'Null command')
593             return None
594
595         cmd = cmd.upper()
596         try:
597             return self.dispatchCommand(tag, cmd, rest)
598         except IllegalClientResponse, e:
599             self.sendBadResponse(tag, 'Illegal syntax: ' + str(e))
600         except IllegalOperation, e:
601             self.sendNegativeResponse(tag, 'Illegal operation: ' + str(e))
602         except IllegalMailboxEncoding, e:
603             self.sendNegativeResponse(tag, 'Illegal mailbox name: ' + str(e))
604
605     def parse_pending(self, line):
606         d = self._pendingLiteral
607         self._pendingLiteral = None
608         self.parseState = 'command'
609         d.callback(line)
610
611     def dispatchCommand(self, tag, cmd, rest, uid=None):
612         f = self.lookupCommand(cmd)
613         if f:
614             fn = f[0]
615             parseargs = f[1:]
616             self.__doCommand(tag, fn, [self, tag], parseargs, rest, uid)
617         else:
618             self.sendBadResponse(tag, 'Unsupported command')
619
620     def lookupCommand(self, cmd):
621         return getattr(self, '_'.join((self.state, cmd.upper())), None)
622
623     def __doCommand(self, tag, handler, args, parseargs, line, uid):
624         for (i, arg) in enumerate(parseargs):
625             if callable(arg):
626                 parseargs = parseargs[i+1:]
627                 maybeDeferred(arg, self, line).addCallback(
628                     self.__cbDispatch, tag, handler, args,
629                     parseargs, uid).addErrback(self.__ebDispatch, tag)
630                 return
631             else:
632                 args.append(arg)
633
634         if line:
635             # Too many arguments
636             raise IllegalClientResponse("Too many arguments for command: " + repr(line))
637
638         if uid is not None:
639             handler(uid=uid, *args)
640         else:
641             handler(*args)
642
643     def __cbDispatch(self, (arg, rest), tag, fn, args, parseargs, uid):
644         args.append(arg)
645         self.__doCommand(tag, fn, args, parseargs, rest, uid)
646
647     def __ebDispatch(self, failure, tag):
648         if failure.check(IllegalClientResponse):
649             self.sendBadResponse(tag, 'Illegal syntax: ' + str(failure.value))
650         elif failure.check(IllegalOperation):
651             self.sendNegativeResponse(tag, 'Illegal operation: ' +
652                                       str(failure.value))
653         elif failure.check(IllegalMailboxEncoding):
654             self.sendNegativeResponse(tag, 'Illegal mailbox name: ' +
655                                       str(failure.value))
656         else:
657             self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
658             log.err(failure)
659
660     def _stringLiteral(self, size):
661         if size > self._literalStringLimit:
662             raise IllegalClientResponse(
663                 "Literal too long! I accept at most %d octets" %
664                 (self._literalStringLimit,))
665         d = defer.Deferred()
666         self.parseState = 'pending'
667         self._pendingLiteral = LiteralString(size, d)
668         self.sendContinuationRequest('Ready for %d octets of text' % size)
669         self.setRawMode()
670         return d
671
672     def _fileLiteral(self, size):
673         d = defer.Deferred()
674         self.parseState = 'pending'
675         self._pendingLiteral = LiteralFile(size, d)
676         self.sendContinuationRequest('Ready for %d octets of data' % size)
677         self.setRawMode()
678         return d
679
680     def arg_astring(self, line):
681         """
682         Parse an astring from the line, return (arg, rest), possibly
683         via a deferred (to handle literals)
684         """
685         line = line.strip()
686         if not line:
687             raise IllegalClientResponse("Missing argument")
688         d = None
689         arg, rest = None, None
690         if line[0] == '"':
691             try:
692                 spam, arg, rest = line.split('"',2)
693                 rest = rest[1:] # Strip space
694             except ValueError:
695                 raise IllegalClientResponse("Unmatched quotes")
696         elif line[0] == '{':
697             # literal
698             if line[-1] != '}':
699                 raise IllegalClientResponse("Malformed literal")
700             try:
701                 size = int(line[1:-1])
702             except ValueError:
703                 raise IllegalClientResponse("Bad literal size: " + line[1:-1])
704             d = self._stringLiteral(size)
705         else:
706             arg = line.split(' ',1)
707             if len(arg) == 1:
708                 arg.append('')
709             arg, rest = arg
710         return d or (arg, rest)
711
712     # ATOM: Any CHAR except ( ) { % * " \ ] CTL SP (CHAR is 7bit)
713     atomre = re.compile(r'(?P<atom>[^\](){%*"\\\x00-\x20\x80-\xff]+)( (?P<rest>.*$)|$)')
714
715     def arg_atom(self, line):
716         """
717         Parse an atom from the line
718         """
719         if not line:
720             raise IllegalClientResponse("Missing argument")
721         m = self.atomre.match(line)
722         if m:
723             return m.group('atom'), m.group('rest')
724         else:
725             raise IllegalClientResponse("Malformed ATOM")
726
727     def arg_plist(self, line):
728         """
729         Parse a (non-nested) parenthesised list from the line
730         """
731         if not line:
732             raise IllegalClientResponse("Missing argument")
733
734         if line[0] != "(":
735             raise IllegalClientResponse("Missing parenthesis")
736
737         i = line.find(")")
738
739         if i == -1:
740             raise IllegalClientResponse("Mismatched parenthesis")
741
742         return (parseNestedParens(line[1:i],0), line[i+2:])
743
744     def arg_literal(self, line):
745         """
746         Parse a literal from the line
747         """
748         if not line:
749             raise IllegalClientResponse("Missing argument")
750
751         if line[0] != '{':
752             raise IllegalClientResponse("Missing literal")
753
754         if line[-1] != '}':
755             raise IllegalClientResponse("Malformed literal")
756
757         try:
758             size = int(line[1:-1])
759         except ValueError:
760             raise IllegalClientResponse("Bad literal size: " + line[1:-1])
761
762         return self._fileLiteral(size)
763
764     def arg_searchkeys(self, line):
765         """
766         searchkeys
767         """
768         query = parseNestedParens(line)
769         # XXX Should really use list of search terms and parse into
770         # a proper tree
771
772         return (query, '')
773
774     def arg_seqset(self, line):
775         """
776         sequence-set
777         """
778         rest = ''
779         arg = line.split(' ',1)
780         if len(arg) == 2:
781             rest = arg[1]
782         arg = arg[0]
783
784         try:
785             return (parseIdList(arg), rest)
786         except IllegalIdentifierError, e:
787             raise IllegalClientResponse("Bad message number " + str(e))
788
789     def arg_fetchatt(self, line):
790         """
791         fetch-att
792         """
793         p = _FetchParser()
794         p.parseString(line)
795         return (p.result, '')
796
797     def arg_flaglist(self, line):
798         """
799         Flag part of store-att-flag
800         """
801         flags = []
802         if line[0] == '(':
803             if line[-1] != ')':
804                 raise IllegalClientResponse("Mismatched parenthesis")
805             line = line[1:-1]
806
807         while line:
808             m = self.atomre.search(line)
809             if not m:
810                 raise IllegalClientResponse("Malformed flag")
811             if line[0] == '\\' and m.start() == 1:
812                 flags.append('\\' + m.group('atom'))
813             elif m.start() == 0:
814                 flags.append(m.group('atom'))
815             else:
816                 raise IllegalClientResponse("Malformed flag")
817             line = m.group('rest')
818
819         return (flags, '')
820
821     def arg_line(self, line):
822         """
823         Command line of UID command
824         """
825         return (line, '')
826
827     def opt_plist(self, line):
828         """
829         Optional parenthesised list
830         """
831         if line.startswith('('):
832             return self.arg_plist(line)
833         else:
834             return (None, line)
835
836     def opt_datetime(self, line):
837         """
838         Optional date-time string
839         """
840         if line.startswith('"'):
841             try:
842                 spam, date, rest = line.split('"',2)
843             except IndexError:
844                 raise IllegalClientResponse("Malformed date-time")
845             return (date, rest[1:])
846         else:
847             return (None, line)
848
849     def opt_charset(self, line):
850         """
851         Optional charset of SEARCH command
852         """
853         if line[:7].upper() == 'CHARSET':
854             arg = line.split(' ',2)
855             if len(arg) == 1:
856                 raise IllegalClientResponse("Missing charset identifier")
857             if len(arg) == 2:
858                 arg.append('')
859             spam, arg, rest = arg
860             return (arg, rest)
861         else:
862             return (None, line)
863
864     def sendServerGreeting(self):
865         msg = '[CAPABILITY %s] %s' % (' '.join(self.listCapabilities()), self.IDENT)
866         self.sendPositiveResponse(message=msg)
867
868     def sendBadResponse(self, tag = None, message = ''):
869         self._respond('BAD', tag, message)
870
871     def sendPositiveResponse(self, tag = None, message = ''):
872         self._respond('OK', tag, message)
873
874     def sendNegativeResponse(self, tag = None, message = ''):
875         self._respond('NO', tag, message)
876
877     def sendUntaggedResponse(self, message, async=False):
878         if not async or (self.blocked is None):
879             self._respond(message, None, None)
880         else:
881             self._queuedAsync.append(message)
882
883     def sendContinuationRequest(self, msg = 'Ready for additional command text'):
884         if msg:
885             self.sendLine('+ ' + msg)
886         else:
887             self.sendLine('+')
888
889     def _respond(self, state, tag, message):
890         if state in ('OK', 'NO', 'BAD') and self._queuedAsync:
891             lines = self._queuedAsync
892             self._queuedAsync = []
893             for msg in lines:
894                 self._respond(msg, None, None)
895         if not tag:
896             tag = '*'
897         if message:
898             self.sendLine(' '.join((tag, state, message)))
899         else:
900             self.sendLine(' '.join((tag, state)))
901
902     def listCapabilities(self):
903         caps = ['IMAP4rev1']
904         for c, v in self.capabilities().iteritems():
905             if v is None:
906                 caps.append(c)
907             elif len(v):
908                 caps.extend([('%s=%s' % (c, cap)) for cap in v])
909         return caps
910
911     def do_CAPABILITY(self, tag):
912         self.sendUntaggedResponse('CAPABILITY ' + ' '.join(self.listCapabilities()))
913         self.sendPositiveResponse(tag, 'CAPABILITY completed')
914
915     unauth_CAPABILITY = (do_CAPABILITY,)
916     auth_CAPABILITY = unauth_CAPABILITY
917     select_CAPABILITY = unauth_CAPABILITY
918     logout_CAPABILITY = unauth_CAPABILITY
919
920     def do_LOGOUT(self, tag):
921         self.sendUntaggedResponse('BYE Nice talking to you')
922         self.sendPositiveResponse(tag, 'LOGOUT successful')
923         self.transport.loseConnection()
924
925     unauth_LOGOUT = (do_LOGOUT,)
926     auth_LOGOUT = unauth_LOGOUT
927     select_LOGOUT = unauth_LOGOUT
928     logout_LOGOUT = unauth_LOGOUT
929
930     def do_NOOP(self, tag):
931         self.sendPositiveResponse(tag, 'NOOP No operation performed')
932
933     unauth_NOOP = (do_NOOP,)
934     auth_NOOP = unauth_NOOP
935     select_NOOP = unauth_NOOP
936     logout_NOOP = unauth_NOOP
937
938     def do_AUTHENTICATE(self, tag, args):
939         args = args.upper().strip()
940         if args not in self.challengers:
941             self.sendNegativeResponse(tag, 'AUTHENTICATE method unsupported')
942         else:
943             self.authenticate(self.challengers[args](), tag)
944
945     unauth_AUTHENTICATE = (do_AUTHENTICATE, arg_atom)
946
947     def authenticate(self, chal, tag):
948         if self.portal is None:
949             self.sendNegativeResponse(tag, 'Temporary authentication failure')
950             return
951
952         self._setupChallenge(chal, tag)
953
954     def _setupChallenge(self, chal, tag):
955         try:
956             challenge = chal.getChallenge()
957         except Exception, e:
958             self.sendBadResponse(tag, 'Server error: ' + str(e))
959         else:
960             coded = base64.encodestring(challenge)[:-1]
961             self.parseState = 'pending'
962             self._pendingLiteral = defer.Deferred()
963             self.sendContinuationRequest(coded)
964             self._pendingLiteral.addCallback(self.__cbAuthChunk, chal, tag)
965             self._pendingLiteral.addErrback(self.__ebAuthChunk, tag)
966
967     def __cbAuthChunk(self, result, chal, tag):
968         try:
969             uncoded = base64.decodestring(result)
970         except binascii.Error:
971             raise IllegalClientResponse("Malformed Response - not base64")
972
973         chal.setResponse(uncoded)
974         if chal.moreChallenges():
975             self._setupChallenge(chal, tag)
976         else:
977             self.portal.login(chal, None, IAccount).addCallbacks(
978                 self.__cbAuthResp,
979                 self.__ebAuthResp,
980                 (tag,), None, (tag,), None
981             )
982
983     def __cbAuthResp(self, (iface, avatar, logout), tag):
984         assert iface is IAccount, "IAccount is the only supported interface"
985         self.account = avatar
986         self.state = 'auth'
987         self._onLogout = logout
988         self.sendPositiveResponse(tag, 'Authentication successful')
989         self.setTimeout(self.POSTAUTH_TIMEOUT)
990
991     def __ebAuthResp(self, failure, tag):
992         if failure.check(cred.error.UnauthorizedLogin):
993             self.sendNegativeResponse(tag, 'Authentication failed: unauthorized')
994         elif failure.check(cred.error.UnhandledCredentials):
995             self.sendNegativeResponse(tag, 'Authentication failed: server misconfigured')
996         else:
997             self.sendBadResponse(tag, 'Server error: login failed unexpectedly')
998             log.err(failure)
999
1000     def __ebAuthChunk(self, failure, tag):
1001         self.sendNegativeResponse(tag, 'Authentication failed: ' + str(failure.value))
1002
1003     def do_STARTTLS(self, tag):
1004         if self.startedTLS:
1005             self.sendNegativeResponse(tag, 'TLS already negotiated')
1006         elif self.ctx and self.canStartTLS:
1007             self.sendPositiveResponse(tag, 'Begin TLS negotiation now')
1008             self.transport.startTLS(self.ctx)
1009             self.startedTLS = True
1010             self.challengers = self.challengers.copy()
1011             if 'LOGIN' not in self.challengers:
1012                 self.challengers['LOGIN'] = LOGINCredentials
1013             if 'PLAIN' not in self.challengers:
1014                 self.challengers['PLAIN'] = PLAINCredentials
1015         else:
1016             self.sendNegativeResponse(tag, 'TLS not available')
1017
1018     unauth_STARTTLS = (do_STARTTLS,)
1019
1020     def do_LOGIN(self, tag, user, passwd):
1021         if 'LOGINDISABLED' in self.capabilities():
1022             self.sendBadResponse(tag, 'LOGIN is disabled before STARTTLS')
1023             return
1024
1025         maybeDeferred(self.authenticateLogin, user, passwd
1026             ).addCallback(self.__cbLogin, tag
1027             ).addErrback(self.__ebLogin, tag
1028             )
1029
1030     unauth_LOGIN = (do_LOGIN, arg_astring, arg_astring)
1031
1032     def authenticateLogin(self, user, passwd):
1033         """Lookup the account associated with the given parameters
1034
1035         Override this method to define the desired authentication behavior.
1036
1037         The default behavior is to defer authentication to C{self.portal}
1038         if it is not None, or to deny the login otherwise.
1039
1040         @type user: C{str}
1041         @param user: The username to lookup
1042
1043         @type passwd: C{str}
1044         @param passwd: The password to login with
1045         """
1046         if self.portal:
1047             return self.portal.login(
1048                 cred.credentials.UsernamePassword(user, passwd),
1049                 None, IAccount
1050             )
1051         raise cred.error.UnauthorizedLogin()
1052
1053     def __cbLogin(self, (iface, avatar, logout), tag):
1054         if iface is not IAccount:
1055             self.sendBadResponse(tag, 'Server error: login returned unexpected value')
1056             log.err("__cbLogin called with %r, IAccount expected" % (iface,))
1057         else:
1058             self.account = avatar
1059             self._onLogout = logout
1060             self.sendPositiveResponse(tag, 'LOGIN succeeded')
1061             self.state = 'auth'
1062             self.setTimeout(self.POSTAUTH_TIMEOUT)
1063
1064     def __ebLogin(self, failure, tag):
1065         if failure.check(cred.error.UnauthorizedLogin):
1066             self.sendNegativeResponse(tag, 'LOGIN failed')
1067         else:
1068             self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
1069             log.err(failure)
1070
1071     def do_NAMESPACE(self, tag):
1072         personal = public = shared = None
1073         np = INamespacePresenter(self.account, None)
1074         if np is not None:
1075             personal = np.getPersonalNamespaces()
1076             public = np.getSharedNamespaces()
1077             shared = np.getSharedNamespaces()
1078         self.sendUntaggedResponse('NAMESPACE ' + collapseNestedLists([personal, public, shared]))
1079         self.sendPositiveResponse(tag, "NAMESPACE command completed")
1080
1081     auth_NAMESPACE = (do_NAMESPACE,)
1082     select_NAMESPACE = auth_NAMESPACE
1083
1084     def _parseMbox(self, name):
1085         if isinstance(name, unicode):
1086             return name
1087         try:
1088             return name.decode('imap4-utf-7')
1089         except:
1090             log.err()
1091             raise IllegalMailboxEncoding(name)
1092
1093     def _selectWork(self, tag, name, rw, cmdName):
1094         if self.mbox:
1095             self.mbox.removeListener(self)
1096             cmbx = ICloseableMailbox(self.mbox, None)
1097             if cmbx is not None:
1098                 maybeDeferred(cmbx.close).addErrback(log.err)
1099             self.mbox = None
1100             self.state = 'auth'
1101
1102         name = self._parseMbox(name)
1103         maybeDeferred(self.account.select, self._parseMbox(name), rw
1104             ).addCallback(self._cbSelectWork, cmdName, tag
1105             ).addErrback(self._ebSelectWork, cmdName, tag
1106             )
1107
1108     def _ebSelectWork(self, failure, cmdName, tag):
1109         self.sendBadResponse(tag, "%s failed: Server error" % (cmdName,))
1110         log.err(failure)
1111
1112     def _cbSelectWork(self, mbox, cmdName, tag):
1113         if mbox is None:
1114             self.sendNegativeResponse(tag, 'No such mailbox')
1115             return
1116         if '\\noselect' in [s.lower() for s in mbox.getFlags()]:
1117             self.sendNegativeResponse(tag, 'Mailbox cannot be selected')
1118             return
1119
1120         flags = mbox.getFlags()
1121         self.sendUntaggedResponse(str(mbox.getMessageCount()) + ' EXISTS')
1122         self.sendUntaggedResponse(str(mbox.getRecentCount()) + ' RECENT')
1123         self.sendUntaggedResponse('FLAGS (%s)' % ' '.join(flags))
1124         self.sendPositiveResponse(None, '[UIDVALIDITY %d]' % mbox.getUIDValidity())
1125
1126         s = mbox.isWriteable() and 'READ-WRITE' or 'READ-ONLY'
1127         mbox.addListener(self)
1128         self.sendPositiveResponse(tag, '[%s] %s successful' % (s, cmdName))
1129         self.state = 'select'
1130         self.mbox = mbox
1131
1132     auth_SELECT = ( _selectWork, arg_astring, 1, 'SELECT' )
1133     select_SELECT = auth_SELECT
1134
1135     auth_EXAMINE = ( _selectWork, arg_astring, 0, 'EXAMINE' )
1136     select_EXAMINE = auth_EXAMINE
1137
1138
1139     def do_IDLE(self, tag):
1140         self.sendContinuationRequest(None)
1141         self.parseTag = tag
1142         self.lastState = self.parseState
1143         self.parseState = 'idle'
1144
1145     def parse_idle(self, *args):
1146         self.parseState = self.lastState
1147         del self.lastState
1148         self.sendPositiveResponse(self.parseTag, "IDLE terminated")
1149         del self.parseTag
1150
1151     select_IDLE = ( do_IDLE, )
1152     auth_IDLE = select_IDLE
1153
1154
1155     def do_CREATE(self, tag, name):
1156         name = self._parseMbox(name)
1157         try:
1158             result = self.account.create(name)
1159         except MailboxException, c:
1160             self.sendNegativeResponse(tag, str(c))
1161         except:
1162             self.sendBadResponse(tag, "Server error encountered while creating mailbox")
1163             log.err()
1164         else:
1165             if result:
1166                 self.sendPositiveResponse(tag, 'Mailbox created')
1167             else:
1168                 self.sendNegativeResponse(tag, 'Mailbox not created')
1169
1170     auth_CREATE = (do_CREATE, arg_astring)
1171     select_CREATE = auth_CREATE
1172
1173     def do_DELETE(self, tag, name):
1174         name = self._parseMbox(name)
1175         if name.lower() == 'inbox':
1176             self.sendNegativeResponse(tag, 'You cannot delete the inbox')
1177             return
1178         try:
1179             self.account.delete(name)
1180         except MailboxException, m:
1181             self.sendNegativeResponse(tag, str(m))
1182         except:
1183             self.sendBadResponse(tag, "Server error encountered while deleting mailbox")
1184             log.err()
1185         else:
1186             self.sendPositiveResponse(tag, 'Mailbox deleted')
1187
1188     auth_DELETE = (do_DELETE, arg_astring)
1189     select_DELETE = auth_DELETE
1190
1191     def do_RENAME(self, tag, oldname, newname):
1192         oldname, newname = [self._parseMbox(n) for n in oldname, newname]
1193         if oldname.lower() == 'inbox' or newname.lower() == 'inbox':
1194             self.sendNegativeResponse(tag, 'You cannot rename the inbox, or rename another mailbox to inbox.')
1195             return
1196         try:
1197             self.account.rename(oldname, newname)
1198         except TypeError:
1199             self.sendBadResponse(tag, 'Invalid command syntax')
1200         except MailboxException, m:
1201             self.sendNegativeResponse(tag, str(m))
1202         except:
1203             self.sendBadResponse(tag, "Server error encountered while renaming mailbox")
1204             log.err()
1205         else:
1206             self.sendPositiveResponse(tag, 'Mailbox renamed')
1207
1208     auth_RENAME = (do_RENAME, arg_astring, arg_astring)
1209     select_RENAME = auth_RENAME
1210
1211     def do_SUBSCRIBE(self, tag, name):
1212         name = self._parseMbox(name)
1213         try:
1214             self.account.subscribe(name)
1215         except MailboxException, m:
1216             self.sendNegativeResponse(tag, str(m))
1217         except:
1218             self.sendBadResponse(tag, "Server error encountered while subscribing to mailbox")
1219             log.err()
1220         else:
1221             self.sendPositiveResponse(tag, 'Subscribed')
1222
1223     auth_SUBSCRIBE = (do_SUBSCRIBE, arg_astring)
1224     select_SUBSCRIBE = auth_SUBSCRIBE
1225
1226     def do_UNSUBSCRIBE(self, tag, name):
1227         name = self._parseMbox(name)
1228         try:
1229             self.account.unsubscribe(name)
1230         except MailboxException, m:
1231             self.sendNegativeResponse(tag, str(m))
1232         except:
1233             self.sendBadResponse(tag, "Server error encountered while unsubscribing from mailbox")
1234             log.err()
1235         else:
1236             self.sendPositiveResponse(tag, 'Unsubscribed')
1237
1238     auth_UNSUBSCRIBE = (do_UNSUBSCRIBE, arg_astring)
1239     select_UNSUBSCRIBE = auth_UNSUBSCRIBE
1240
1241     def _listWork(self, tag, ref, mbox, sub, cmdName):
1242         mbox = self._parseMbox(mbox)
1243         maybeDeferred(self.account.listMailboxes, ref, mbox
1244             ).addCallback(self._cbListWork, tag, sub, cmdName
1245             ).addErrback(self._ebListWork, tag
1246             )
1247
1248     def _cbListWork(self, mailboxes, tag, sub, cmdName):
1249         for (name, box) in mailboxes:
1250             if not sub or self.account.isSubscribed(name):
1251                 flags = box.getFlags()
1252                 delim = box.getHierarchicalDelimiter()
1253                 resp = (DontQuoteMe(cmdName), map(DontQuoteMe, flags), delim, name.encode('imap4-utf-7'))
1254                 self.sendUntaggedResponse(collapseNestedLists(resp))
1255         self.sendPositiveResponse(tag, '%s completed' % (cmdName,))
1256
1257     def _ebListWork(self, failure, tag):
1258         self.sendBadResponse(tag, "Server error encountered while listing mailboxes.")
1259         log.err(failure)
1260
1261     auth_LIST = (_listWork, arg_astring, arg_astring, 0, 'LIST')
1262     select_LIST = auth_LIST
1263
1264     auth_LSUB = (_listWork, arg_astring, arg_astring, 1, 'LSUB')
1265     select_LSUB = auth_LSUB
1266
1267     def do_STATUS(self, tag, mailbox, names):
1268         mailbox = self._parseMbox(mailbox)
1269         maybeDeferred(self.account.select, mailbox, 0
1270             ).addCallback(self._cbStatusGotMailbox, tag, mailbox, names
1271             ).addErrback(self._ebStatusGotMailbox, tag
1272             )
1273
1274     def _cbStatusGotMailbox(self, mbox, tag, mailbox, names):
1275         if mbox:
1276             maybeDeferred(mbox.requestStatus, names).addCallbacks(
1277                 self.__cbStatus, self.__ebStatus,
1278                 (tag, mailbox), None, (tag, mailbox), None
1279             )
1280         else:
1281             self.sendNegativeResponse(tag, "Could not open mailbox")
1282
1283     def _ebStatusGotMailbox(self, failure, tag):
1284         self.sendBadResponse(tag, "Server error encountered while opening mailbox.")
1285         log.err(failure)
1286
1287     auth_STATUS = (do_STATUS, arg_astring, arg_plist)
1288     select_STATUS = auth_STATUS
1289
1290     def __cbStatus(self, status, tag, box):
1291         line = ' '.join(['%s %s' % x for x in status.iteritems()])
1292         self.sendUntaggedResponse('STATUS %s (%s)' % (box, line))
1293         self.sendPositiveResponse(tag, 'STATUS complete')
1294
1295     def __ebStatus(self, failure, tag, box):
1296         self.sendBadResponse(tag, 'STATUS %s failed: %s' % (box, str(failure.value)))
1297
1298     def do_APPEND(self, tag, mailbox, flags, date, message):
1299         mailbox = self._parseMbox(mailbox)
1300         maybeDeferred(self.account.select, mailbox
1301             ).addCallback(self._cbAppendGotMailbox, tag, flags, date, message
1302             ).addErrback(self._ebAppendGotMailbox, tag
1303             )
1304
1305     def _cbAppendGotMailbox(self, mbox, tag, flags, date, message):
1306         if not mbox:
1307             self.sendNegativeResponse(tag, '[TRYCREATE] No such mailbox')
1308             return
1309
1310         d = mbox.addMessage(message, flags, date)
1311         d.addCallback(self.__cbAppend, tag, mbox)
1312         d.addErrback(self.__ebAppend, tag)
1313
1314     def _ebAppendGotMailbox(self, failure, tag):
1315         self.sendBadResponse(tag, "Server error encountered while opening mailbox.")
1316         log.err(failure)
1317
1318     auth_APPEND = (do_APPEND, arg_astring, opt_plist, opt_datetime,
1319                    arg_literal)
1320     select_APPEND = auth_APPEND
1321
1322     def __cbAppend(self, result, tag, mbox):
1323         self.sendUntaggedResponse('%d EXISTS' % mbox.getMessageCount())
1324         self.sendPositiveResponse(tag, 'APPEND complete')
1325
1326     def __ebAppend(self, failure, tag):
1327         self.sendBadResponse(tag, 'APPEND failed: ' + str(failure.value))
1328
1329     def do_CHECK(self, tag):
1330         d = self.checkpoint()
1331         if d is None:
1332             self.__cbCheck(None, tag)
1333         else:
1334             d.addCallbacks(
1335                 self.__cbCheck,
1336                 self.__ebCheck,
1337                 callbackArgs=(tag,),
1338                 errbackArgs=(tag,)
1339             )
1340     select_CHECK = (do_CHECK,)
1341
1342     def __cbCheck(self, result, tag):
1343         self.sendPositiveResponse(tag, 'CHECK completed')
1344
1345     def __ebCheck(self, failure, tag):
1346         self.sendBadResponse(tag, 'CHECK failed: ' + str(failure.value))
1347
1348     def checkpoint(self):
1349         """Called when the client issues a CHECK command.
1350
1351         This should perform any checkpoint operations required by the server.
1352         It may be a long running operation, but may not block.  If it returns
1353         a deferred, the client will only be informed of success (or failure)
1354         when the deferred's callback (or errback) is invoked.
1355         """
1356         return None
1357
1358     def do_CLOSE(self, tag):
1359         d = None
1360         if self.mbox.isWriteable():
1361             d = maybeDeferred(self.mbox.expunge)
1362         cmbx = ICloseableMailbox(self.mbox, None)
1363         if cmbx is not None:
1364             if d is not None:
1365                 d.addCallback(lambda result: cmbx.close())
1366             else:
1367                 d = maybeDeferred(cmbx.close)
1368         if d is not None:
1369             d.addCallbacks(self.__cbClose, self.__ebClose, (tag,), None, (tag,), None)
1370         else:
1371             self.__cbClose(None, tag)
1372
1373     select_CLOSE = (do_CLOSE,)
1374
1375     def __cbClose(self, result, tag):
1376         self.sendPositiveResponse(tag, 'CLOSE completed')
1377         self.mbox.removeListener(self)
1378         self.mbox = None
1379         self.state = 'auth'
1380
1381     def __ebClose(self, failure, tag):
1382         self.sendBadResponse(tag, 'CLOSE failed: ' + str(failure.value))
1383
1384     def do_EXPUNGE(self, tag):
1385         if self.mbox.isWriteable():
1386             maybeDeferred(self.mbox.expunge).addCallbacks(
1387                 self.__cbExpunge, self.__ebExpunge, (tag,), None, (tag,), None
1388             )
1389         else:
1390             self.sendNegativeResponse(tag, 'EXPUNGE ignored on read-only mailbox')
1391
1392     select_EXPUNGE = (do_EXPUNGE,)
1393
1394     def __cbExpunge(self, result, tag):
1395         for e in result:
1396             self.sendUntaggedResponse('%d EXPUNGE' % e)
1397         self.sendPositiveResponse(tag, 'EXPUNGE completed')
1398
1399     def __ebExpunge(self, failure, tag):
1400         self.sendBadResponse(tag, 'EXPUNGE failed: ' + str(failure.value))
1401         log.err(failure)
1402
1403     def do_SEARCH(self, tag, charset, query, uid=0):
1404         sm = ISearchableMailbox(self.mbox, None)
1405         if sm is not None:
1406             maybeDeferred(sm.search, query, uid=uid).addCallbacks(
1407                 self.__cbSearch, self.__ebSearch,
1408                 (tag, self.mbox, uid), None, (tag,), None
1409             )
1410         else:
1411             s = parseIdList('1:*')
1412             maybeDeferred(self.mbox.fetch, s, uid=uid).addCallbacks(
1413                 self.__cbManualSearch, self.__ebSearch,
1414                 (tag, self.mbox, query, uid), None, (tag,), None
1415             )
1416
1417     select_SEARCH = (do_SEARCH, opt_charset, arg_searchkeys)
1418
1419     def __cbSearch(self, result, tag, mbox, uid):
1420         if uid:
1421             result = map(mbox.getUID, result)
1422         ids = ' '.join([str(i) for i in result])
1423         self.sendUntaggedResponse('SEARCH ' + ids)
1424         self.sendPositiveResponse(tag, 'SEARCH completed')
1425
1426     def __cbManualSearch(self, result, tag, mbox, query, uid, searchResults = None):
1427         if searchResults is None:
1428             searchResults = []
1429         i = 0
1430         for (i, (id, msg)) in zip(range(5), result):
1431             if self.searchFilter(query, id, msg):
1432                 if uid:
1433                     searchResults.append(str(msg.getUID()))
1434                 else:
1435                     searchResults.append(str(id))
1436         if i == 4:
1437             from twisted.internet import reactor
1438             reactor.callLater(0, self.__cbManualSearch, result, tag, mbox, query, uid, searchResults)
1439         else:
1440             if searchResults:
1441                 self.sendUntaggedResponse('SEARCH ' + ' '.join(searchResults))
1442             self.sendPositiveResponse(tag, 'SEARCH completed')
1443
1444     def searchFilter(self, query, id, msg):
1445         while query:
1446             if not self.singleSearchStep(query, id, msg):
1447                 return False
1448         return True
1449
1450     def singleSearchStep(self, query, id, msg):
1451         q = query.pop(0)
1452         if isinstance(q, list):
1453             if not self.searchFilter(q, id, msg):
1454                 return False
1455         else:
1456             c = q.upper()
1457             f = getattr(self, 'search_' + c)
1458             if f:
1459                 if not f(query, id, msg):
1460                     return False
1461             else:
1462                 # IMAP goes *out of its way* to be complex
1463                 # Sequence sets to search should be specified
1464                 # with a command, like EVERYTHING ELSE.
1465                 try:
1466                     m = parseIdList(c)
1467                 except:
1468                     log.err('Unknown search term: ' + c)
1469                 else:
1470                     if id not in m:
1471                         return False
1472         return True
1473
1474     def search_ALL(self, query, id, msg):
1475         return True
1476
1477     def search_ANSWERED(self, query, id, msg):
1478         return '\\Answered' in msg.getFlags()
1479
1480     def search_BCC(self, query, id, msg):
1481         bcc = msg.getHeaders(False, 'bcc').get('bcc', '')
1482         return bcc.lower().find(query.pop(0).lower()) != -1
1483
1484     def search_BEFORE(self, query, id, msg):
1485         date = parseTime(query.pop(0))
1486         return rfc822.parsedate(msg.getInternalDate()) < date
1487
1488     def search_BODY(self, query, id, msg):
1489         body = query.pop(0).lower()
1490         return text.strFile(body, msg.getBodyFile(), False)
1491
1492     def search_CC(self, query, id, msg):
1493         cc = msg.getHeaders(False, 'cc').get('cc', '')
1494         return cc.lower().find(query.pop(0).lower()) != -1
1495
1496     def search_DELETED(self, query, id, msg):
1497         return '\\Deleted' in msg.getFlags()
1498
1499     def search_DRAFT(self, query, id, msg):
1500         return '\\Draft' in msg.getFlags()
1501
1502     def search_FLAGGED(self, query, id, msg):
1503         return '\\Flagged' in msg.getFlags()
1504
1505     def search_FROM(self, query, id, msg):
1506         fm = msg.getHeaders(False, 'from').get('from', '')
1507         return fm.lower().find(query.pop(0).lower()) != -1
1508
1509     def search_HEADER(self, query, id, msg):
1510         hdr = query.pop(0).lower()
1511         hdr = msg.getHeaders(False, hdr).get(hdr, '')
1512         return hdr.lower().find(query.pop(0).lower()) != -1
1513
1514     def search_KEYWORD(self, query, id, msg):
1515         query.pop(0)
1516         return False
1517
1518     def search_LARGER(self, query, id, msg):
1519         return int(query.pop(0)) < msg.getSize()
1520
1521     def search_NEW(self, query, id, msg):
1522         return '\\Recent' in msg.getFlags() and '\\Seen' not in msg.getFlags()
1523
1524     def search_NOT(self, query, id, msg):
1525         return not self.singleSearchStep(query, id, msg)
1526
1527     def search_OLD(self, query, id, msg):
1528         return '\\Recent' not in msg.getFlags()
1529
1530     def search_ON(self, query, id, msg):
1531         date = parseTime(query.pop(0))
1532         return rfc822.parsedate(msg.getInternalDate()) == date
1533
1534     def search_OR(self, query, id, msg):
1535         a = self.singleSearchStep(query, id, msg)
1536         b = self.singleSearchStep(query, id, msg)
1537         return a or b
1538
1539     def search_RECENT(self, query, id, msg):
1540         return '\\Recent' in msg.getFlags()
1541
1542     def search_SEEN(self, query, id, msg):
1543         return '\\Seen' in msg.getFlags()
1544
1545     def search_SENTBEFORE(self, query, id, msg):
1546         date = msg.getHeader(False, 'date').get('date', '')
1547         date = rfc822.parsedate(date)
1548         return date < parseTime(query.pop(0))
1549
1550     def search_SENTON(self, query, id, msg):
1551         date = msg.getHeader(False, 'date').get('date', '')
1552         date = rfc822.parsedate(date)
1553         return date[:3] == parseTime(query.pop(0))[:3]
1554
1555     def search_SENTSINCE(self, query, id, msg):
1556         date = msg.getHeader(False, 'date').get('date', '')
1557         date = rfc822.parsedate(date)
1558         return date > parseTime(query.pop(0))
1559
1560     def search_SINCE(self, query, id, msg):
1561         date = parseTime(query.pop(0))
1562         return rfc822.parsedate(msg.getInternalDate()) > date
1563
1564     def search_SMALLER(self, query, id, msg):
1565         return int(query.pop(0)) > msg.getSize()
1566
1567     def search_SUBJECT(self, query, id, msg):
1568         subj = msg.getHeaders(False, 'subject').get('subject', '')
1569         return subj.lower().find(query.pop(0).lower()) != -1
1570
1571     def search_TEXT(self, query, id, msg):
1572         # XXX - This must search headers too
1573         body = query.pop(0).lower()
1574         return text.strFile(body, msg.getBodyFile(), False)
1575
1576     def search_TO(self, query, id, msg):
1577         to = msg.getHeaders(False, 'to').get('to', '')
1578         return to.lower().find(query.pop(0).lower()) != -1
1579
1580     def search_UID(self, query, id, msg):
1581         c = query.pop(0)
1582         m = parseIdList(c)
1583         return msg.getUID() in m
1584
1585     def search_UNANSWERED(self, query, id, msg):
1586         return '\\Answered' not in msg.getFlags()
1587
1588     def search_UNDELETED(self, query, id, msg):
1589         return '\\Deleted' not in msg.getFlags()
1590
1591     def search_UNDRAFT(self, query, id, msg):
1592         return '\\Draft' not in msg.getFlags()
1593
1594     def search_UNFLAGGED(self, query, id, msg):
1595         return '\\Flagged' not in msg.getFlags()
1596
1597     def search_UNKEYWORD(self, query, id, msg):
1598         query.pop(0)
1599         return False
1600
1601     def search_UNSEEN(self, query, id, msg):
1602         return '\\Seen' not in msg.getFlags()
1603
1604     def __ebSearch(self, failure, tag):
1605         self.sendBadResponse(tag, 'SEARCH failed: ' + str(failure.value))
1606         log.err(failure)
1607
1608     def do_FETCH(self, tag, messages, query, uid=0):
1609         if query:
1610             self._oldTimeout = self.setTimeout(None)
1611             maybeDeferred(self.mbox.fetch, messages, uid=uid
1612                 ).addCallback(iter
1613                 ).addCallback(self.__cbFetch, tag, query, uid
1614                 ).addErrback(self.__ebFetch, tag
1615                 )
1616         else:
1617             self.sendPositiveResponse(tag, 'FETCH complete')
1618
1619     select_FETCH = (do_FETCH, arg_seqset, arg_fetchatt)
1620
1621     def __cbFetch(self, results, tag, query, uid):
1622         if self.blocked is None:
1623             self.blocked = []
1624         try:
1625             id, msg = results.next()
1626         except StopIteration:
1627             # The idle timeout was suspended while we delivered results,
1628             # restore it now.
1629             self.setTimeout(self._oldTimeout)
1630             del self._oldTimeout
1631
1632             # All results have been processed, deliver completion notification.
1633
1634             # It's important to run this *after* resetting the timeout to "rig
1635             # a race" in some test code. writing to the transport will
1636             # synchronously call test code, which synchronously loses the
1637             # connection, calling our connectionLost method, which cancels the
1638             # timeout. We want to make sure that timeout is cancelled *after*
1639             # we reset it above, so that the final state is no timed
1640             # calls. This avoids reactor uncleanliness errors in the test
1641             # suite.
1642             # XXX: Perhaps loopback should be fixed to not call the user code
1643             # synchronously in transport.write?
1644             self.sendPositiveResponse(tag, 'FETCH completed')
1645
1646             # Instance state is now consistent again (ie, it is as though
1647             # the fetch command never ran), so allow any pending blocked
1648             # commands to execute.
1649             self._unblock()
1650         else:
1651             self.spewMessage(id, msg, query, uid
1652                 ).addCallback(lambda _: self.__cbFetch(results, tag, query, uid)
1653                 ).addErrback(self.__ebSpewMessage
1654                 )
1655
1656     def __ebSpewMessage(self, failure):
1657         # This indicates a programming error.
1658         # There's no reliable way to indicate anything to the client, since we
1659         # may have already written an arbitrary amount of data in response to
1660         # the command.
1661         log.err(failure)
1662         self.transport.loseConnection()
1663
1664     def spew_envelope(self, id, msg, _w=None, _f=None):
1665         if _w is None:
1666             _w = self.transport.write
1667         _w('ENVELOPE ' + collapseNestedLists([getEnvelope(msg)]))
1668
1669     def spew_flags(self, id, msg, _w=None, _f=None):
1670         if _w is None:
1671             _w = self.transport.write
1672         _w('FLAGS ' + '(%s)' % (' '.join(msg.getFlags())))
1673
1674     def spew_internaldate(self, id, msg, _w=None, _f=None):
1675         if _w is None:
1676             _w = self.transport.write
1677         idate = msg.getInternalDate()
1678         ttup = rfc822.parsedate_tz(idate)
1679         if ttup is None:
1680             log.msg("%d:%r: unpareseable internaldate: %r" % (id, msg, idate))
1681             raise IMAP4Exception("Internal failure generating INTERNALDATE")
1682
1683         odate = time.strftime("%d-%b-%Y %H:%M:%S ", ttup[:9])
1684         if ttup[9] is None:
1685             odate = odate + "+0000"
1686         else:
1687             if ttup[9] >= 0:
1688                 sign = "+"
1689             else:
1690                 sign = "-"
1691             odate = odate + sign + string.zfill(str(((abs(ttup[9]) / 3600) * 100 + (abs(ttup[9]) % 3600) / 60)), 4)
1692         _w('INTERNALDATE ' + _quote(odate))
1693
1694     def spew_rfc822header(self, id, msg, _w=None, _f=None):
1695         if _w is None:
1696             _w = self.transport.write
1697         hdrs = _formatHeaders(msg.getHeaders(True))
1698         _w('RFC822.HEADER ' + _literal(hdrs))
1699
1700     def spew_rfc822text(self, id, msg, _w=None, _f=None):
1701         if _w is None:
1702             _w = self.transport.write
1703         _w('RFC822.TEXT ')
1704         _f()
1705         return FileProducer(msg.getBodyFile()
1706             ).beginProducing(self.transport
1707             )
1708
1709     def spew_rfc822size(self, id, msg, _w=None, _f=None):
1710         if _w is None:
1711             _w = self.transport.write
1712         _w('RFC822.SIZE ' + str(msg.getSize()))
1713
1714     def spew_rfc822(self, id, msg, _w=None, _f=None):
1715         if _w is None:
1716             _w = self.transport.write
1717         _w('RFC822 ')
1718         _f()
1719         mf = IMessageFile(msg, None)
1720         if mf is not None:
1721             return FileProducer(mf.open()
1722                 ).beginProducing(self.transport
1723                 )
1724         return MessageProducer(msg, None, self._scheduler
1725             ).beginProducing(self.transport
1726             )
1727
1728     def spew_uid(self, id, msg, _w=None, _f=None):
1729         if _w is None:
1730             _w = self.transport.write
1731         _w('UID ' + str(msg.getUID()))
1732
1733     def spew_bodystructure(self, id, msg, _w=None, _f=None):
1734         _w('BODYSTRUCTURE ' + collapseNestedLists([getBodyStructure(msg, True)]))
1735
1736     def spew_body(self, part, id, msg, _w=None, _f=None):
1737         if _w is None:
1738             _w = self.transport.write
1739         for p in part.part:
1740             if msg.isMultipart():
1741                 msg = msg.getSubPart(p)
1742             elif p > 0:
1743                 # Non-multipart messages have an implicit first part but no
1744                 # other parts - reject any request for any other part.
1745                 raise TypeError("Requested subpart of non-multipart message")
1746
1747         if part.header:
1748             hdrs = msg.getHeaders(part.header.negate, *part.header.fields)
1749             hdrs = _formatHeaders(hdrs)
1750             _w(str(part) + ' ' + _literal(hdrs))
1751         elif part.text:
1752             _w(str(part) + ' ')
1753             _f()
1754             return FileProducer(msg.getBodyFile()
1755                 ).beginProducing(self.transport
1756                 )
1757         elif part.mime:
1758             hdrs = _formatHeaders(msg.getHeaders(True))
1759             _w(str(part) + ' ' + _literal(hdrs))
1760         elif part.empty:
1761             _w(str(part) + ' ')
1762             _f()
1763             if part.part:
1764                 return FileProducer(msg.getBodyFile()
1765                     ).beginProducing(self.transport
1766                     )
1767             else:
1768                 mf = IMessageFile(msg, None)
1769                 if mf is not None:
1770                     return FileProducer(mf.open()).beginProducing(self.transport)
1771                 return MessageProducer(msg, None, self._scheduler).beginProducing(self.transport)
1772
1773         else:
1774             _w('BODY ' + collapseNestedLists([getBodyStructure(msg)]))
1775
1776     def spewMessage(self, id, msg, query, uid):
1777         wbuf = WriteBuffer(self.transport)
1778         write = wbuf.write
1779         flush = wbuf.flush
1780         def start():
1781             write('* %d FETCH (' % (id,))
1782         def finish():
1783             write(')\r\n')
1784         def space():
1785             write(' ')
1786
1787         def spew():
1788             seenUID = False
1789             start()
1790             for part in query:
1791                 if part.type == 'uid':
1792                     seenUID = True
1793                 if part.type == 'body':
1794                     yield self.spew_body(part, id, msg, write, flush)
1795                 else:
1796                     f = getattr(self, 'spew_' + part.type)
1797                     yield f(id, msg, write, flush)
1798                 if part is not query[-1]:
1799                     space()
1800             if uid and not seenUID:
1801                 space()
1802                 yield self.spew_uid(id, msg, write, flush)
1803             finish()
1804             flush()
1805         return self._scheduler(spew())
1806
1807     def __ebFetch(self, failure, tag):
1808         self.setTimeout(self._oldTimeout)
1809         del self._oldTimeout
1810         log.err(failure)
1811         self.sendBadResponse(tag, 'FETCH failed: ' + str(failure.value))
1812
1813     def do_STORE(self, tag, messages, mode, flags, uid=0):
1814         mode = mode.upper()
1815         silent = mode.endswith('SILENT')
1816         if mode.startswith('+'):
1817             mode = 1
1818         elif mode.startswith('-'):
1819             mode = -1
1820         else:
1821             mode = 0
1822
1823         maybeDeferred(self.mbox.store, messages, flags, mode, uid=uid).addCallbacks(
1824             self.__cbStore, self.__ebStore, (tag, self.mbox, uid, silent), None, (tag,), None
1825         )
1826
1827     select_STORE = (do_STORE, arg_seqset, arg_atom, arg_flaglist)
1828
1829     def __cbStore(self, result, tag, mbox, uid, silent):
1830         if result and not silent:
1831               for (k, v) in result.iteritems():
1832                   if uid:
1833                       uidstr = ' UID %d' % mbox.getUID(k)
1834                   else:
1835                       uidstr = ''
1836                   self.sendUntaggedResponse('%d FETCH (FLAGS (%s)%s)' %
1837                                             (k, ' '.join(v), uidstr))
1838         self.sendPositiveResponse(tag, 'STORE completed')
1839
1840     def __ebStore(self, failure, tag):
1841         self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
1842
1843     def do_COPY(self, tag, messages, mailbox, uid=0):
1844         mailbox = self._parseMbox(mailbox)
1845         maybeDeferred(self.account.select, mailbox
1846             ).addCallback(self._cbCopySelectedMailbox, tag, messages, mailbox, uid
1847             ).addErrback(self._ebCopySelectedMailbox, tag
1848             )
1849     select_COPY = (do_COPY, arg_seqset, arg_astring)
1850
1851     def _cbCopySelectedMailbox(self, mbox, tag, messages, mailbox, uid):
1852         if not mbox:
1853             self.sendNegativeResponse(tag, 'No such mailbox: ' + mailbox)
1854         else:
1855             maybeDeferred(self.mbox.fetch, messages, uid
1856                 ).addCallback(self.__cbCopy, tag, mbox
1857                 ).addCallback(self.__cbCopied, tag, mbox
1858                 ).addErrback(self.__ebCopy, tag
1859                 )
1860
1861     def _ebCopySelectedMailbox(self, failure, tag):
1862         self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
1863
1864     def __cbCopy(self, messages, tag, mbox):
1865         # XXX - This should handle failures with a rollback or something
1866         addedDeferreds = []
1867         addedIDs = []
1868         failures = []
1869
1870         fastCopyMbox = IMessageCopier(mbox, None)
1871         for (id, msg) in messages:
1872             if fastCopyMbox is not None:
1873                 d = maybeDeferred(fastCopyMbox.copy, msg)
1874                 addedDeferreds.append(d)
1875                 continue
1876
1877             # XXX - The following should be an implementation of IMessageCopier.copy
1878             # on an IMailbox->IMessageCopier adapter.
1879
1880             flags = msg.getFlags()
1881             date = msg.getInternalDate()
1882
1883             body = IMessageFile(msg, None)
1884             if body is not None:
1885                 bodyFile = body.open()
1886                 d = maybeDeferred(mbox.addMessage, bodyFile, flags, date)
1887             else:
1888                 def rewind(f):
1889                     f.seek(0)
1890                     return f
1891                 buffer = tempfile.TemporaryFile()
1892                 d = MessageProducer(msg, buffer, self._scheduler
1893                     ).beginProducing(None
1894                     ).addCallback(lambda _, b=buffer, f=flags, d=date: mbox.addMessage(rewind(b), f, d)
1895                     )
1896             addedDeferreds.append(d)
1897         return defer.DeferredList(addedDeferreds)
1898
1899     def __cbCopied(self, deferredIds, tag, mbox):
1900         ids = []
1901         failures = []
1902         for (status, result) in deferredIds:
1903             if status:
1904                 ids.append(result)
1905             else:
1906                 failures.append(result.value)
1907         if failures:
1908             self.sendNegativeResponse(tag, '[ALERT] Some messages were not copied')
1909         else:
1910             self.sendPositiveResponse(tag, 'COPY completed')
1911
1912     def __ebCopy(self, failure, tag):
1913         self.sendBadResponse(tag, 'COPY failed:' + str(failure.value))
1914         log.err(failure)
1915
1916     def do_UID(self, tag, command, line):
1917         command = command.upper()
1918
1919         if command not in ('COPY', 'FETCH', 'STORE', 'SEARCH'):
1920             raise IllegalClientResponse(command)
1921
1922         self.dispatchCommand(tag, command, line, uid=1)
1923
1924     select_UID = (do_UID, arg_atom, arg_line)
1925     #
1926     # IMailboxListener implementation
1927     #
1928     def modeChanged(self, writeable):
1929         if writeable:
1930             self.sendUntaggedResponse(message='[READ-WRITE]', async=True)
1931         else:
1932             self.sendUntaggedResponse(message='[READ-ONLY]', async=True)
1933
1934     def flagsChanged(self, newFlags):
1935         for (mId, flags) in newFlags.iteritems():
1936             msg = '%d FETCH (FLAGS (%s))' % (mId, ' '.join(flags))
1937             self.sendUntaggedResponse(msg, async=True)
1938
1939     def newMessages(self, exists, recent):
1940         if exists is not None:
1941             self.sendUntaggedResponse('%d EXISTS' % exists, async=True)
1942         if recent is not None:
1943             self.sendUntaggedResponse('%d RECENT' % recent, async=True)
1944
1945
1946 class UnhandledResponse(IMAP4Exception): pass
1947
1948 class NegativeResponse(IMAP4Exception): pass
1949
1950 class NoSupportedAuthentication(IMAP4Exception):
1951     def __init__(self, serverSupports, clientSupports):
1952         IMAP4Exception.__init__(self, 'No supported authentication schemes available')
1953         self.serverSupports = serverSupports
1954         self.clientSupports = clientSupports
1955
1956     def __str__(self):
1957         return (IMAP4Exception.__str__(self)
1958             + ': Server supports %r, client supports %r'
1959             % (self.serverSupports, self.clientSupports))
1960
1961 class IllegalServerResponse(IMAP4Exception): pass
1962
1963 TIMEOUT_ERROR = error.TimeoutError()
1964
1965 class IMAP4Client(basic.LineReceiver, policies.TimeoutMixin):
1966     """IMAP4 client protocol implementation
1967
1968     @ivar state: A string representing the state the connection is currently
1969     in.
1970     """
1971     implements(IMailboxListener)
1972
1973     tags = None
1974     waiting = None
1975     queued = None
1976     tagID = 1
1977     state = None
1978
1979     startedTLS = False
1980
1981     # Number of seconds to wait before timing out a connection.
1982     # If the number is <= 0 no timeout checking will be performed.
1983     timeout = 0
1984
1985     # Capabilities are not allowed to change during the session
1986     # So cache the first response and use that for all later
1987     # lookups
1988     _capCache = None
1989
1990     _memoryFileLimit = 1024 * 1024 * 10
1991
1992     # Authentication is pluggable.  This maps names to IClientAuthentication
1993     # objects.
1994     authenticators = None
1995
1996     STATUS_CODES = ('OK', 'NO', 'BAD', 'PREAUTH', 'BYE')
1997
1998     STATUS_TRANSFORMATIONS = {
1999         'MESSAGES': int, 'RECENT': int, 'UNSEEN': int
2000     }
2001
2002     context = None
2003
2004     def __init__(self, contextFactory = None):
2005         self.tags = {}
2006         self.queued = []
2007         self.authenticators = {}
2008         self.context = contextFactory
2009
2010         self._tag = None
2011         self._parts = None
2012         self._lastCmd = None
2013
2014     def registerAuthenticator(self, auth):
2015         """Register a new form of authentication
2016
2017         When invoking the authenticate() method of IMAP4Client, the first
2018         matching authentication scheme found will be used.  The ordering is
2019         that in which the server lists support authentication schemes.
2020
2021         @type auth: Implementor of C{IClientAuthentication}
2022         @param auth: The object to use to perform the client
2023         side of this authentication scheme.
2024         """
2025         self.authenticators[auth.getName().upper()] = auth
2026
2027     def rawDataReceived(self, data):
2028         if self.timeout > 0:
2029             self.resetTimeout()
2030
2031         self._pendingSize -= len(data)
2032         if self._pendingSize > 0:
2033             self._pendingBuffer.write(data)
2034         else:
2035             passon = ''
2036             if self._pendingSize < 0:
2037                 data, passon = data[:self._pendingSize], data[self._pendingSize:]
2038             self._pendingBuffer.write(data)
2039             rest = self._pendingBuffer
2040             self._pendingBuffer = None
2041             self._pendingSize = None
2042             rest.seek(0, 0)
2043             self._parts.append(rest.read())
2044             self.setLineMode(passon.lstrip('\r\n'))
2045
2046 #    def sendLine(self, line):
2047 #        print 'S:', repr(line)
2048 #        return basic.LineReceiver.sendLine(self, line)
2049
2050     def _setupForLiteral(self, rest, octets):
2051         self._pendingBuffer = self.messageFile(octets)
2052         self._pendingSize = octets
2053         if self._parts is None:
2054             self._parts = [rest, '\r\n']
2055         else:
2056             self._parts.extend([rest, '\r\n'])
2057         self.setRawMode()
2058
2059     def connectionMade(self):
2060         if self.timeout > 0:
2061             self.setTimeout(self.timeout)
2062
2063     def connectionLost(self, reason):
2064         """We are no longer connected"""
2065         if self.timeout > 0:
2066             self.setTimeout(None)
2067         if self.queued is not None:
2068             queued = self.queued
2069             self.queued = None
2070             for cmd in queued:
2071                 cmd.defer.errback(reason)
2072         if self.tags is not None:
2073             tags = self.tags
2074             self.tags = None
2075             for cmd in tags.itervalues():
2076                 if cmd is not None and cmd.defer is not None:
2077                     cmd.defer.errback(reason)
2078
2079
2080     def lineReceived(self, line):
2081         """
2082         Attempt to parse a single line from the server.
2083
2084         @type line: C{str}
2085         @param line: The line from the server, without the line delimiter.
2086
2087         @raise IllegalServerResponse: If the line or some part of the line
2088             does not represent an allowed message from the server at this time.
2089         """
2090 #        print 'C: ' + repr(line)
2091         if self.timeout > 0:
2092             self.resetTimeout()
2093
2094         lastPart = line.rfind('{')
2095         if lastPart != -1:
2096             lastPart = line[lastPart + 1:]
2097             if lastPart.endswith('}'):
2098                 # It's a literal a-comin' in
2099                 try:
2100                     octets = int(lastPart[:-1])
2101                 except ValueError:
2102                     raise IllegalServerResponse(line)
2103                 if self._parts is None:
2104                     self._tag, parts = line.split(None, 1)
2105                 else:
2106                     parts = line
2107                 self._setupForLiteral(parts, octets)
2108                 return
2109
2110         if self._parts is None:
2111             # It isn't a literal at all
2112             self._regularDispatch(line)
2113         else:
2114             # If an expression is in progress, no tag is required here
2115             # Since we didn't find a literal indicator, this expression
2116             # is done.
2117             self._parts.append(line)
2118             tag, rest = self._tag, ''.join(self._parts)
2119             self._tag = self._parts = None
2120             self.dispatchCommand(tag, rest)
2121
2122     def timeoutConnection(self):
2123         if self._lastCmd and self._lastCmd.defer is not None:
2124             d, self._lastCmd.defer = self._lastCmd.defer, None
2125             d.errback(TIMEOUT_ERROR)
2126
2127         if self.queued:
2128             for cmd in self.queued:
2129                 if cmd.defer is not None:
2130                     d, cmd.defer = cmd.defer, d
2131                     d.errback(TIMEOUT_ERROR)
2132
2133         self.transport.loseConnection()
2134
2135     def _regularDispatch(self, line):
2136         parts = line.split(None, 1)
2137         if len(parts) != 2:
2138             parts.append('')
2139         tag, rest = parts
2140         self.dispatchCommand(tag, rest)
2141
2142     def messageFile(self, octets):
2143         """Create a file to which an incoming message may be written.
2144
2145         @type octets: C{int}
2146         @param octets: The number of octets which will be written to the file
2147
2148         @rtype: Any object which implements C{write(string)} and
2149         C{seek(int, int)}
2150         @return: A file-like object
2151         """
2152         if octets > self._memoryFileLimit:
2153             return tempfile.TemporaryFile()
2154         else:
2155             return StringIO.StringIO()
2156
2157     def makeTag(self):
2158         tag = '%0.4X' % self.tagID
2159         self.tagID += 1
2160         return tag
2161
2162     def dispatchCommand(self, tag, rest):
2163         if self.state is None:
2164             f = self.response_UNAUTH
2165         else:
2166             f = getattr(self, 'response_' + self.state.upper(), None)
2167         if f:
2168             try:
2169                 f(tag, rest)
2170             except:
2171                 log.err()
2172                 self.transport.loseConnection()
2173         else:
2174             log.err("Cannot dispatch: %s, %s, %s" % (self.state, tag, rest))
2175             self.transport.loseConnection()
2176
2177     def response_UNAUTH(self, tag, rest):
2178         if self.state is None:
2179             # Server greeting, this is
2180             status, rest = rest.split(None, 1)
2181             if status.upper() == 'OK':
2182                 self.state = 'unauth'
2183             elif status.upper() == 'PREAUTH':
2184                 self.state = 'auth'
2185             else:
2186                 # XXX - This is rude.
2187                 self.transport.loseConnection()
2188                 raise IllegalServerResponse(tag + ' ' + rest)
2189
2190             b, e = rest.find('['), rest.find(']')
2191             if b != -1 and e != -1:
2192                 self.serverGreeting(
2193                     self.__cbCapabilities(
2194                         ([parseNestedParens(rest[b + 1:e])], None)))
2195             else:
2196                 self.serverGreeting(None)
2197         else:
2198             self._defaultHandler(tag, rest)
2199
2200     def response_AUTH(self, tag, rest):
2201         self._defaultHandler(tag, rest)
2202
2203     def _defaultHandler(self, tag, rest):
2204         if tag == '*' or tag == '+':
2205             if not self.waiting:
2206                 self._extraInfo([parseNestedParens(rest)])
2207             else:
2208                 cmd = self.tags[self.waiting]
2209                 if tag == '+':
2210                     cmd.continuation(rest)
2211                 else:
2212                     cmd.lines.append(rest)
2213         else:
2214             try:
2215                 cmd = self.tags[tag]
2216             except KeyError:
2217                 # XXX - This is rude.
2218                 self.transport.loseConnection()
2219                 raise IllegalServerResponse(tag + ' ' + rest)
2220             else:
2221                 status, line = rest.split(None, 1)
2222                 if status == 'OK':
2223                     # Give them this last line, too
2224                     cmd.finish(rest, self._extraInfo)
2225                 else:
2226                     cmd.defer.errback(IMAP4Exception(line))
2227                 del self.tags[tag]
2228                 self.waiting = None
2229                 self._flushQueue()
2230
2231     def _flushQueue(self):
2232         if self.queued:
2233             cmd = self.queued.pop(0)
2234             t = self.makeTag()
2235             self.tags[t] = cmd
2236             self.sendLine(cmd.format(t))
2237             self.waiting = t
2238
2239     def _extraInfo(self, lines):
2240         # XXX - This is terrible.
2241         # XXX - Also, this should collapse temporally proximate calls into single
2242         #       invocations of IMailboxListener methods, where possible.
2243         flags = {}
2244         recent = exists = None
2245         for response in lines:
2246             elements = len(response)
2247             if elements == 1 and response[0] == ['READ-ONLY']:
2248                 self.modeChanged(False)
2249             elif elements == 1 and response[0] == ['READ-WRITE']:
2250                 self.modeChanged(True)
2251             elif elements == 2 and response[1] == 'EXISTS':
2252                 exists = int(response[0])
2253             elif elements == 2 and response[1] == 'RECENT':
2254                 recent = int(response[0])
2255             elif elements == 3 and response[1] == 'FETCH':
2256                 mId = int(response[0])
2257                 values = self._parseFetchPairs(response[2])
2258                 flags.setdefault(mId, []).extend(values.get('FLAGS', ()))
2259             else:
2260                 log.msg('Unhandled unsolicited response: %s' % (response,))
2261
2262         if flags:
2263             self.flagsChanged(flags)
2264         if recent is not None or exists is not None:
2265             self.newMessages(exists, recent)
2266
2267     def sendCommand(self, cmd):
2268         cmd.defer = defer.Deferred()
2269         if self.waiting:
2270             self.queued.append(cmd)
2271             return cmd.defer
2272         t = self.makeTag()
2273         self.tags[t] = cmd
2274         self.sendLine(cmd.format(t))
2275         self.waiting = t
2276         self._lastCmd = cmd
2277         return cmd.defer
2278
2279     def getCapabilities(self, useCache=1):
2280         """Request the capabilities available on this server.
2281
2282         This command is allowed in any state of connection.
2283
2284         @type useCache: C{bool}
2285         @param useCache: Specify whether to use the capability-cache or to
2286         re-retrieve the capabilities from the server.  Server capabilities
2287         should never change, so for normal use, this flag should never be
2288         false.
2289
2290         @rtype: C{Deferred}
2291         @return: A deferred whose callback will be invoked with a
2292         dictionary mapping capability types to lists of supported
2293         mechanisms, or to None if a support list is not applicable.
2294         """
2295         if useCache and self._capCache is not None:
2296             return defer.succeed(self._capCache)
2297         cmd = 'CAPABILITY'
2298         resp = ('CAPABILITY',)
2299         d = self.sendCommand(Command(cmd, wantResponse=resp))
2300         d.addCallback(self.__cbCapabilities)
2301         return d
2302
2303     def __cbCapabilities(self, (lines, tagline)):
2304         caps = {}
2305         for rest in lines:
2306             for cap in rest[1:]:
2307                 parts = cap.split('=', 1)
2308                 if len(parts) == 1:
2309                     category, value = parts[0], None
2310                 else:
2311                     category, value = parts
2312                 caps.setdefault(category, []).append(value)
2313
2314         # Preserve a non-ideal API for backwards compatibility.  It would
2315         # probably be entirely sensible to have an object with a wider API than
2316         # dict here so this could be presented less insanely.
2317         for category in caps:
2318             if caps[category] == [None]:
2319                 caps[category] = None
2320         self._capCache = caps
2321         return caps
2322
2323     def logout(self):
2324         """Inform the server that we are done with the connection.
2325
2326         This command is allowed in any state of connection.
2327
2328         @rtype: C{Deferred}
2329         @return: A deferred whose callback will be invoked with None
2330         when the proper server acknowledgement has been received.
2331         """
2332         d = self.sendCommand(Command('LOGOUT', wantResponse=('BYE',)))
2333         d.addCallback(self.__cbLogout)
2334         return d
2335
2336     def __cbLogout(self, (lines, tagline)):
2337         self.transport.loseConnection()
2338         # We don't particularly care what the server said
2339         return None
2340
2341
2342     def noop(self):
2343         """Perform no operation.
2344
2345         This command is allowed in any state of connection.
2346
2347         @rtype: C{Deferred}
2348         @return: A deferred whose callback will be invoked with a list
2349         of untagged status updates the server responds with.
2350         """
2351         d = self.sendCommand(Command('NOOP'))
2352         d.addCallback(self.__cbNoop)
2353         return d
2354
2355     def __cbNoop(self, (lines, tagline)):
2356         # Conceivable, this is elidable.
2357         # It is, afterall, a no-op.
2358         return lines
2359
2360     def startTLS(self, contextFactory=None):
2361         """
2362         Initiates a 'STARTTLS' request and negotiates the TLS / SSL
2363         Handshake.
2364
2365         @param contextFactory: The TLS / SSL Context Factory to
2366         leverage.  If the contextFactory is None the IMAP4Client will
2367         either use the current TLS / SSL Context Factory or attempt to
2368         create a new one.
2369
2370         @type contextFactory: C{ssl.ClientContextFactory}
2371
2372         @return: A Deferred which fires when the transport has been
2373         secured according to the given contextFactory, or which fails
2374         if the transport cannot be secured.
2375         """
2376         assert not self.startedTLS, "Client and Server are currently communicating via TLS"
2377
2378         if contextFactory is None:
2379             contextFactory = self._getContextFactory()
2380
2381         if contextFactory is None:
2382             return defer.fail(IMAP4Exception(
2383                 "IMAP4Client requires a TLS context to "
2384                 "initiate the STARTTLS handshake"))
2385
2386         if 'STARTTLS' not in self._capCache:
2387             return defer.fail(IMAP4Exception(
2388                 "Server does not support secure communication "
2389                 "via TLS / SSL"))
2390
2391         tls = interfaces.ITLSTransport(self.transport, None)
2392         if tls is None:
2393             return defer.fail(IMAP4Exception(
2394                 "IMAP4Client transport does not implement "
2395                 "interfaces.ITLSTransport"))
2396
2397         d = self.sendCommand(Command('STARTTLS'))
2398         d.addCallback(self._startedTLS, contextFactory)
2399         d.addCallback(lambda _: self.getCapabilities())
2400         return d
2401
2402
2403     def authenticate(self, secret):
2404         """Attempt to enter the authenticated state with the server
2405
2406         This command is allowed in the Non-Authenticated state.
2407
2408         @rtype: C{Deferred}
2409         @return: A deferred whose callback is invoked if the authentication
2410         succeeds and whose errback will be invoked otherwise.
2411         """
2412         if self._capCache is None:
2413             d = self.getCapabilities()
2414         else:
2415             d = defer.succeed(self._capCache)
2416         d.addCallback(self.__cbAuthenticate, secret)
2417         return d
2418
2419     def __cbAuthenticate(self, caps, secret):
2420         auths = caps.get('AUTH', ())
2421         for scheme in auths:
2422             if scheme.upper() in self.authenticators:
2423                 cmd = Command('AUTHENTICATE', scheme, (),
2424                               self.__cbContinueAuth, scheme,
2425                               secret)
2426                 return self.sendCommand(cmd)
2427
2428         if self.startedTLS:
2429             return defer.fail(NoSupportedAuthentication(
2430                 auths, self.authenticators.keys()))
2431         else:
2432             def ebStartTLS(err):
2433                 err.trap(IMAP4Exception)
2434                 # We couldn't negotiate TLS for some reason
2435                 return defer.fail(NoSupportedAuthentication(
2436                     auths, self.authenticators.keys()))
2437
2438             d = self.startTLS()
2439             d.addErrback(ebStartTLS)
2440             d.addCallback(lambda _: self.getCapabilities())
2441             d.addCallback(self.__cbAuthTLS, secret)
2442             return d
2443
2444
2445     def __cbContinueAuth(self, rest, scheme, secret):
2446         try:
2447             chal = base64.decodestring(rest + '\n')
2448         except binascii.Error:
2449             self.sendLine('*')
2450             raise IllegalServerResponse(rest)
2451             self.transport.loseConnection()
2452         else:
2453             auth = self.authenticators[scheme]
2454             chal = auth.challengeResponse(secret, chal)
2455             self.sendLine(base64.encodestring(chal).strip())
2456
2457     def __cbAuthTLS(self, caps, secret):
2458         auths = caps.get('AUTH', ())
2459         for scheme in auths:
2460             if scheme.upper() in self.authenticators:
2461                 cmd = Command('AUTHENTICATE', scheme, (),
2462                               self.__cbContinueAuth, scheme,
2463                               secret)
2464                 return self.sendCommand(cmd)
2465         raise NoSupportedAuthentication(auths, self.authenticators.keys())
2466
2467
2468     def login(self, username, password):
2469         """Authenticate with the server using a username and password
2470
2471         This command is allowed in the Non-Authenticated state.  If the
2472         server supports the STARTTLS capability and our transport supports
2473         TLS, TLS is negotiated before the login command is issued.
2474
2475         A more secure way to log in is to use C{startTLS} or
2476         C{authenticate} or both.
2477
2478         @type username: C{str}
2479         @param username: The username to log in with
2480
2481         @type password: C{str}
2482         @param password: The password to log in with
2483
2484         @rtype: C{Deferred}
2485         @return: A deferred whose callback is invoked if login is successful
2486         and whose errback is invoked otherwise.
2487         """
2488         d = maybeDeferred(self.getCapabilities)
2489         d.addCallback(self.__cbLoginCaps, username, password)
2490         return d
2491
2492     def serverGreeting(self, caps):
2493         """Called when the server has sent us a greeting.
2494
2495         @type caps: C{dict}
2496         @param caps: Capabilities the server advertised in its greeting.
2497         """
2498
2499     def _getContextFactory(self):
2500         if self.context is not None:
2501             return self.context
2502         try:
2503             from twisted.internet import ssl
2504         except ImportError:
2505             return None
2506         else:
2507             context = ssl.ClientContextFactory()
2508             context.method = ssl.SSL.TLSv1_METHOD
2509             return context
2510
2511     def __cbLoginCaps(self, capabilities, username, password):
2512         # If the server advertises STARTTLS, we might want to try to switch to TLS
2513         tryTLS = 'STARTTLS' in capabilities
2514
2515         # If our transport supports switching to TLS, we might want to try to switch to TLS.
2516         tlsableTransport = interfaces.ITLSTransport(self.transport, None) is not None
2517
2518         # If our transport is not already using TLS, we might want to try to switch to TLS.
2519         nontlsTransport = interfaces.ISSLTransport(self.transport, None) is None
2520
2521         if not self.startedTLS and tryTLS and tlsableTransport and nontlsTransport:
2522             d = self.startTLS()
2523
2524             d.addCallbacks(
2525                 self.__cbLoginTLS,
2526                 self.__ebLoginTLS,
2527                 callbackArgs=(username, password),
2528                 )
2529             return d
2530         else:
2531             if nontlsTransport:
2532                 log.msg("Server has no TLS support. logging in over cleartext!")
2533             args = ' '.join((_quote(username), _quote(password)))
2534             return self.sendCommand(Command('LOGIN', args))
2535
2536     def _startedTLS(self, result, context):
2537         self.transport.startTLS(context)
2538         self._capCache = None
2539         self.startedTLS = True
2540         return result
2541
2542     def __cbLoginTLS(self, result, username, password):
2543         args = ' '.join((_quote(username), _quote(password)))
2544         return self.sendCommand(Command('LOGIN', args))
2545
2546     def __ebLoginTLS(self, failure):
2547         log.err(failure)
2548         return failure
2549
2550     def namespace(self):
2551         """Retrieve information about the namespaces available to this account
2552
2553         This command is allowed in the Authenticated and Selected states.
2554
2555         @rtype: C{Deferred}
2556         @return: A deferred whose callback is invoked with namespace
2557         information.  An example of this information is::
2558
2559             [[['', '/']], [], []]
2560
2561         which indicates a single personal namespace called '' with '/'
2562         as its hierarchical delimiter, and no shared or user namespaces.
2563         """
2564         cmd = 'NAMESPACE'
2565         resp = ('NAMESPACE',)
2566         d = self.sendCommand(Command(cmd, wantResponse=resp))
2567         d.addCallback(self.__cbNamespace)
2568         return d
2569
2570     def __cbNamespace(self, (lines, last)):
2571         for parts in lines:
2572             if len(parts) == 4 and parts[0] == 'NAMESPACE':
2573                 return [e or [] for e in parts[1:]]
2574         log.err("No NAMESPACE response to NAMESPACE command")
2575         return [[], [], []]
2576
2577     def select(self, mailbox):
2578         """Select a mailbox
2579
2580         This command is allowed in the Authenticated and Selected states.
2581
2582         @type mailbox: C{str}
2583         @param mailbox: The name of the mailbox to select
2584
2585         @rtype: C{Deferred}
2586         @return: A deferred whose callback is invoked with mailbox
2587         information if the select is successful and whose errback is
2588         invoked otherwise.  Mailbox information consists of a dictionary
2589         with the following keys and values::
2590
2591                 FLAGS: A list of strings containing the flags settable on
2592                         messages in this mailbox.
2593
2594                 EXISTS: An integer indicating the number of messages in this
2595                         mailbox.
2596
2597                 RECENT: An integer indicating the number of \"recent\"
2598                         messages in this mailbox.
2599
2600                 UNSEEN: An integer indicating the number of messages not
2601                         flagged \\Seen in this mailbox.
2602
2603                 PERMANENTFLAGS: A list of strings containing the flags that
2604                         can be permanently set on messages in this mailbox.
2605
2606                 UIDVALIDITY: An integer uniquely identifying this mailbox.
2607         """
2608         cmd = 'SELECT'
2609         args = _prepareMailboxName(mailbox)
2610         resp = ('FLAGS', 'EXISTS', 'RECENT', 'UNSEEN', 'PERMANENTFLAGS', 'UIDVALIDITY')
2611         d = self.sendCommand(Command(cmd, args, wantResponse=resp))
2612         d.addCallback(self.__cbSelect, 1)
2613         return d
2614
2615     def examine(self, mailbox):
2616         """Select a mailbox in read-only mode
2617
2618         This command is allowed in the Authenticated and Selected states.
2619
2620         @type mailbox: C{str}
2621         @param mailbox: The name of the mailbox to examine
2622
2623         @rtype: C{Deferred}
2624         @return: A deferred whose callback is invoked with mailbox
2625         information if the examine is successful and whose errback
2626         is invoked otherwise.  Mailbox information consists of a dictionary
2627         with the following keys and values::
2628
2629             'FLAGS': A list of strings containing the flags settable on
2630                         messages in this mailbox.
2631
2632             'EXISTS': An integer indicating the number of messages in this
2633                         mailbox.
2634
2635             'RECENT': An integer indicating the number of \"recent\"
2636                         messages in this mailbox.
2637
2638             'UNSEEN': An integer indicating the number of messages not
2639                         flagged \\Seen in this mailbox.
2640
2641             'PERMANENTFLAGS': A list of strings containing the flags that
2642                         can be permanently set on messages in this mailbox.
2643
2644             'UIDVALIDITY': An integer uniquely identifying this mailbox.
2645         """
2646         cmd = 'EXAMINE'
2647         args = _prepareMailboxName(mailbox)
2648         resp = ('FLAGS', 'EXISTS', 'RECENT', 'UNSEEN', 'PERMANENTFLAGS', 'UIDVALIDITY')
2649         d = self.sendCommand(Command(cmd, args, wantResponse=resp))
2650         d.addCallback(self.__cbSelect, 0)
2651         return d
2652
2653
2654     def _intOrRaise(self, value, phrase):
2655         """
2656         Parse C{value} as an integer and return the result or raise
2657         L{IllegalServerResponse} with C{phrase} as an argument if C{value}
2658         cannot be parsed as an integer.
2659         """
2660         try:
2661             return int(value)
2662         except ValueError:
2663             raise IllegalServerResponse(phrase)
2664
2665
2666     def __cbSelect(self, (lines, tagline), rw):
2667         """
2668         Handle lines received in response to a SELECT or EXAMINE command.
2669
2670         See RFC 3501, section 6.3.1.
2671         """
2672         # In the absense of specification, we are free to assume:
2673         #   READ-WRITE access
2674         datum = {'READ-WRITE': rw}
2675         lines.append(parseNestedParens(tagline))
2676         for split in lines:
2677             if len(split) > 0 and split[0].upper() == 'OK':
2678                 # Handle all the kinds of OK response.
2679                 content = split[1]
2680                 key = content[0].upper()
2681                 if key == 'READ-ONLY':
2682                     datum['READ-WRITE'] = False
2683                 elif key == 'READ-WRITE':
2684                     datum['READ-WRITE'] = True
2685                 elif key == 'UIDVALIDITY':
2686                     datum['UIDVALIDITY'] = self._intOrRaise(
2687                         content[1], split)
2688                 elif key == 'UNSEEN':
2689                     datum['UNSEEN'] = self._intOrRaise(content[1], split)
2690                 elif key == 'UIDNEXT':
2691                     datum['UIDNEXT'] = self._intOrRaise(content[1], split)
2692                 elif key == 'PERMANENTFLAGS':
2693                     datum['PERMANENTFLAGS'] = tuple(content[1])
2694                 else:
2695                     log.err('Unhandled SELECT response (2): %s' % (split,))
2696             elif len(split) == 2:
2697                 # Handle FLAGS, EXISTS, and RECENT
2698                 if split[0].upper() == 'FLAGS':
2699                     datum['FLAGS'] = tuple(split[1])
2700                 elif isinstance(split[1], str):
2701                     # Must make sure things are strings before treating them as
2702                     # strings since some other forms of response have nesting in
2703                     # places which results in lists instead.
2704                     if split[1].upper() == 'EXISTS':
2705                         datum['EXISTS'] = self._intOrRaise(split[0], split)
2706                     elif split[1].upper() == 'RECENT':
2707                         datum['RECENT'] = self._intOrRaise(split[0], split)
2708                     else:
2709                         log.err('Unhandled SELECT response (0): %s' % (split,))
2710                 else:
2711                     log.err('Unhandled SELECT response (1): %s' % (split,))
2712             else:
2713                 log.err('Unhandled SELECT response (4): %s' % (split,))
2714         return datum
2715
2716
2717     def create(self, name):
2718         """Create a new mailbox on the server
2719
2720         This command is allowed in the Authenticated and Selected states.
2721
2722         @type name: C{str}
2723         @param name: The name of the mailbox to create.
2724
2725         @rtype: C{Deferred}
2726         @return: A deferred whose callback is invoked if the mailbox creation
2727         is successful and whose errback is invoked otherwise.
2728         """
2729         return self.sendCommand(Command('CREATE', _prepareMailboxName(name)))
2730
2731     def delete(self, name):
2732         """Delete a mailbox
2733
2734         This command is allowed in the Authenticated and Selected states.
2735
2736         @type name: C{str}
2737         @param name: The name of the mailbox to delete.
2738
2739         @rtype: C{Deferred}
2740         @return: A deferred whose calblack is invoked if the mailbox is
2741         deleted successfully and whose errback is invoked otherwise.
2742         """
2743         return self.sendCommand(Command('DELETE', _prepareMailboxName(name)))
2744
2745     def rename(self, oldname, newname):
2746         """Rename a mailbox
2747
2748         This command is allowed in the Authenticated and Selected states.
2749
2750         @type oldname: C{str}
2751         @param oldname: The current name of the mailbox to rename.
2752
2753         @type newname: C{str}
2754         @param newname: The new name to give the mailbox.
2755
2756         @rtype: C{Deferred}
2757         @return: A deferred whose callback is invoked if the rename is
2758         successful and whose errback is invoked otherwise.
2759         """
2760         oldname = _prepareMailboxName(oldname)
2761         newname = _prepareMailboxName(newname)
2762         return self.sendCommand(Command('RENAME', ' '.join((oldname, newname))))
2763
2764     def subscribe(self, name):
2765         """Add a mailbox to the subscription list
2766
2767         This command is allowed in the Authenticated and Selected states.
2768
2769         @type name: C{str}
2770         @param name: The mailbox to mark as 'active' or 'subscribed'
2771
2772         @rtype: C{Deferred}
2773         @return: A deferred whose callback is invoked if the subscription
2774         is successful and whose errback is invoked otherwise.
2775         """
2776         return self.sendCommand(Command('SUBSCRIBE', _prepareMailboxName(name)))
2777
2778     def unsubscribe(self, name):
2779         """Remove a mailbox from the subscription list
2780
2781         This command is allowed in the Authenticated and Selected states.
2782
2783         @type name: C{str}
2784         @param name: The mailbox to unsubscribe
2785
2786         @rtype: C{Deferred}
2787         @return: A deferred whose callback is invoked if the unsubscription
2788         is successful and whose errback is invoked otherwise.
2789         """
2790         return self.sendCommand(Command('UNSUBSCRIBE', _prepareMailboxName(name)))
2791
2792     def list(self, reference, wildcard):
2793         """List a subset of the available mailboxes
2794
2795         This command is allowed in the Authenticated and Selected states.
2796
2797         @type reference: C{str}
2798         @param reference: The context in which to interpret C{wildcard}
2799
2800         @type wildcard: C{str}
2801         @param wildcard: The pattern of mailbox names to match, optionally
2802         including either or both of the '*' and '%' wildcards.  '*' will
2803         match zero or more characters and cross hierarchical boundaries.
2804         '%' will also match zero or more characters, but is limited to a
2805         single hierarchical level.
2806
2807         @rtype: C{Deferred}
2808         @return: A deferred whose callback is invoked with a list of C{tuple}s,
2809         the first element of which is a C{tuple} of mailbox flags, the second
2810         element of which is the hierarchy delimiter for this mailbox, and the
2811         third of which is the mailbox name; if the command is unsuccessful,
2812         the deferred's errback is invoked instead.
2813         """
2814         cmd = 'LIST'
2815         args = '"%s" "%s"' % (reference, wildcard.encode('imap4-utf-7'))
2816         resp = ('LIST',)
2817         d = self.sendCommand(Command(cmd, args, wantResponse=resp))
2818         d.addCallback(self.__cbList, 'LIST')
2819         return d
2820
2821     def lsub(self, reference, wildcard):
2822         """List a subset of the subscribed available mailboxes
2823
2824         This command is allowed in the Authenticated and Selected states.
2825
2826         The parameters and returned object are the same as for the C{list}
2827         method, with one slight difference: Only mailboxes which have been
2828         subscribed can be included in the resulting list.
2829         """
2830         cmd = 'LSUB'
2831         args = '"%s" "%s"' % (reference, wildcard.encode('imap4-utf-7'))
2832         resp = ('LSUB',)
2833         d = self.sendCommand(Command(cmd, args, wantResponse=resp))
2834         d.addCallback(self.__cbList, 'LSUB')
2835         return d
2836
2837     def __cbList(self, (lines, last), command):
2838         results = []
2839         for parts in lines:
2840             if len(parts) == 4 and parts[0] == command:
2841                 parts[1] = tuple(parts[1])
2842                 results.append(tuple(parts[1:]))
2843         return results
2844
2845     def status(self, mailbox, *names):
2846         """
2847         Retrieve the status of the given mailbox
2848
2849         This command is allowed in the Authenticated and Selected states.
2850
2851         @type mailbox: C{str}
2852         @param mailbox: The name of the mailbox to query
2853
2854         @type *names: C{str}
2855         @param *names: The status names to query.  These may be any number of:
2856             C{'MESSAGES'}, C{'RECENT'}, C{'UIDNEXT'}, C{'UIDVALIDITY'}, and
2857             C{'UNSEEN'}.
2858
2859         @rtype: C{Deferred}
2860         @return: A deferred which fires with with the status information if the
2861             command is successful and whose errback is invoked otherwise.  The
2862             status information is in the form of a C{dict}.  Each element of
2863             C{names} is a key in the dictionary.  The value for each key is the
2864             corresponding response from the server.
2865         """
2866         cmd = 'STATUS'
2867         args = "%s (%s)" % (_prepareMailboxName(mailbox), ' '.join(names))
2868         resp = ('STATUS',)
2869         d = self.sendCommand(Command(cmd, args, wantResponse=resp))
2870         d.addCallback(self.__cbStatus)
2871         return d
2872
2873     def __cbStatus(self, (lines, last)):
2874         status = {}
2875         for parts in lines:
2876             if parts[0] == 'STATUS':
2877                 items = parts[2]
2878                 items = [items[i:i+2] for i in range(0, len(items), 2)]
2879                 status.update(dict(items))
2880         for k in status.keys():
2881             t = self.STATUS_TRANSFORMATIONS.get(k)
2882             if t:
2883                 try:
2884                     status[k] = t(status[k])
2885                 except Exception, e:
2886                     raise IllegalServerResponse('(%s %s): %s' % (k, status[k], str(e)))
2887         return status
2888
2889     def append(self, mailbox, message, flags = (), date = None):
2890         """Add the given message to the given mailbox.
2891
2892         This command is allowed in the Authenticated and Selected states.
2893
2894         @type mailbox: C{str}
2895         @param mailbox: The mailbox to which to add this message.
2896
2897         @type message: Any file-like object
2898         @param message: The message to add, in RFC822 format.  Newlines
2899         in this file should be \\r\\n-style.
2900
2901         @type flags: Any iterable of C{str}
2902         @param flags: The flags to associated with this message.
2903
2904         @type date: C{str}
2905         @param date: The date to associate with this message.  This should
2906         be of the format DD-MM-YYYY HH:MM:SS +/-HHMM.  For example, in
2907         Eastern Standard Time, on July 1st 2004 at half past 1 PM,
2908         \"01-07-2004 13:30:00 -0500\".
2909
2910         @rtype: C{Deferred}
2911         @return: A deferred whose callback is invoked when this command
2912         succeeds or whose errback is invoked if it fails.
2913         """
2914         message.seek(0, 2)
2915         L = message.tell()
2916         message.seek(0, 0)
2917         fmt = '%s (%s)%s {%d}'
2918         if date:
2919             date = ' "%s"' % date
2920         else:
2921             date = ''
2922         cmd = fmt % (
2923             _prepareMailboxName(mailbox), ' '.join(flags),
2924             date, L
2925         )
2926         d = self.sendCommand(Command('APPEND', cmd, (), self.__cbContinueAppend, message))
2927         return d
2928
2929     def __cbContinueAppend(self, lines, message):
2930         s = basic.FileSender()
2931         return s.beginFileTransfer(message, self.transport, None
2932             ).addCallback(self.__cbFinishAppend)
2933
2934     def __cbFinishAppend(self, foo):
2935         self.sendLine('')
2936
2937     def check(self):
2938         """Tell the server to perform a checkpoint
2939
2940         This command is allowed in the Selected state.
2941
2942         @rtype: C{Deferred}
2943         @return: A deferred whose callback is invoked when this command
2944         succeeds or whose errback is invoked if it fails.
2945         """
2946         return self.sendCommand(Command('CHECK'))
2947
2948     def close(self):
2949         """Return the connection to the Authenticated state.
2950
2951         This command is allowed in the Selected state.
2952
2953         Issuing this command will also remove all messages flagged \\Deleted
2954         from the selected mailbox if it is opened in read-write mode,
2955         otherwise it indicates success by no messages are removed.
2956
2957         @rtype: C{Deferred}
2958         @return: A deferred whose callback is invoked when the command
2959         completes successfully or whose errback is invoked if it fails.
2960         """
2961         return self.sendCommand(Command('CLOSE'))
2962
2963
2964     def expunge(self):
2965         """Return the connection to the Authenticate state.
2966
2967         This command is allowed in the Selected state.
2968
2969         Issuing this command will perform the same actions as issuing the
2970         close command, but will also generate an 'expunge' response for
2971         every message deleted.
2972
2973         @rtype: C{Deferred}
2974         @return: A deferred whose callback is invoked with a list of the
2975         'expunge' responses when this command is successful or whose errback
2976         is invoked otherwise.
2977         """
2978         cmd = 'EXPUNGE'
2979         resp = ('EXPUNGE',)
2980         d = self.sendCommand(Command(cmd, wantResponse=resp))
2981         d.addCallback(self.__cbExpunge)
2982         return d
2983
2984
2985     def __cbExpunge(self, (lines, last)):
2986         ids = []
2987         for parts in lines:
2988             if len(parts) == 2 and parts[1] == 'EXPUNGE':
2989                 ids.append(self._intOrRaise(parts[0], parts))
2990         return ids
2991
2992
2993     def search(self, *queries, **kwarg):
2994         """Search messages in the currently selected mailbox
2995
2996         This command is allowed in the Selected state.
2997
2998         Any non-zero number of queries are accepted by this method, as
2999         returned by the C{Query}, C{Or}, and C{Not} functions.
3000
3001         One keyword argument is accepted: if uid is passed in with a non-zero
3002         value, the server is asked to return message UIDs instead of message
3003         sequence numbers.
3004
3005         @rtype: C{Deferred}
3006         @return: A deferred whose callback will be invoked with a list of all
3007         the message sequence numbers return by the search, or whose errback
3008         will be invoked if there is an error.
3009         """
3010         if kwarg.get('uid'):
3011             cmd = 'UID SEARCH'
3012         else:
3013             cmd = 'SEARCH'
3014         args = ' '.join(queries)
3015         d = self.sendCommand(Command(cmd, args, wantResponse=(cmd,)))
3016         d.addCallback(self.__cbSearch)
3017         return d
3018
3019
3020     def __cbSearch(self, (lines, end)):
3021         ids = []
3022         for parts in lines:
3023             if len(parts) > 0 and parts[0] == 'SEARCH':
3024                 ids.extend([self._intOrRaise(p, parts) for p in parts[1:]])
3025         return ids
3026
3027
3028     def fetchUID(self, messages, uid=0):
3029         """Retrieve the unique identifier for one or more messages
3030
3031         This command is allowed in the Selected state.
3032
3033         @type messages: C{MessageSet} or C{str}
3034         @param messages: A message sequence set
3035
3036         @type uid: C{bool}
3037         @param uid: Indicates whether the message sequence set is of message
3038         numbers or of unique message IDs.
3039
3040         @rtype: C{Deferred}
3041         @return: A deferred whose callback is invoked with a dict mapping
3042         message sequence numbers to unique message identifiers, or whose
3043         errback is invoked if there is an error.
3044         """
3045         return self._fetch(messages, useUID=uid, uid=1)
3046
3047
3048     def fetchFlags(self, messages, uid=0):
3049         """Retrieve the flags for one or more messages
3050
3051         This command is allowed in the Selected state.
3052
3053         @type messages: C{MessageSet} or C{str}
3054         @param messages: The messages for which to retrieve flags.
3055
3056         @type uid: C{bool}
3057         @param uid: Indicates whether the message sequence set is of message
3058         numbers or of unique message IDs.
3059
3060         @rtype: C{Deferred}
3061         @return: A deferred whose callback is invoked with a dict mapping
3062         message numbers to lists of flags, or whose errback is invoked if
3063         there is an error.
3064         """
3065         return self._fetch(str(messages), useUID=uid, flags=1)
3066
3067
3068     def fetchInternalDate(self, messages, uid=0):
3069         """Retrieve the internal date associated with one or more messages
3070
3071         This command is allowed in the Selected state.
3072
3073         @type messages: C{MessageSet} or C{str}
3074         @param messages: The messages for which to retrieve the internal date.
3075
3076         @type uid: C{bool}
3077         @param uid: Indicates whether the message sequence set is of message
3078         numbers or of unique message IDs.
3079
3080         @rtype: C{Deferred}
3081         @return: A deferred whose callback is invoked with a dict mapping
3082         message numbers to date strings, or whose errback is invoked
3083         if there is an error.  Date strings take the format of
3084         \"day-month-year time timezone\".
3085         """
3086         return self._fetch(str(messages), useUID=uid, internaldate=1)
3087
3088
3089     def fetchEnvelope(self, messages, uid=0):
3090         """Retrieve the envelope data for one or more messages
3091
3092         This command is allowed in the Selected state.
3093
3094         @type messages: C{MessageSet} or C{str}
3095         @param messages: The messages for which to retrieve envelope data.
3096
3097         @type uid: C{bool}
3098         @param uid: Indicates whether the message sequence set is of message
3099         numbers or of unique message IDs.
3100
3101         @rtype: C{Deferred}
3102         @return: A deferred whose callback is invoked with a dict mapping
3103         message numbers to envelope data, or whose errback is invoked
3104         if there is an error.  Envelope data consists of a sequence of the
3105         date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to,
3106         and message-id header fields.  The date, subject, in-reply-to, and
3107         message-id fields are strings, while the from, sender, reply-to,
3108         to, cc, and bcc fields contain address data.  Address data consists
3109         of a sequence of name, source route, mailbox name, and hostname.
3110         Fields which are not present for a particular address may be C{None}.
3111         """
3112         return self._fetch(str(messages), useUID=uid, envelope=1)
3113
3114
3115     def fetchBodyStructure(self, messages, uid=0):
3116         """Retrieve the structure of the body of one or more messages
3117
3118         This command is allowed in the Selected state.
3119
3120         @type messages: C{MessageSet} or C{str}
3121         @param messages: The messages for which to retrieve body structure
3122         data.
3123
3124         @type uid: C{bool}
3125         @param uid: Indicates whether the message sequence set is of message
3126         numbers or of unique message IDs.
3127
3128         @rtype: C{Deferred}
3129         @return: A deferred whose callback is invoked with a dict mapping
3130         message numbers to body structure data, or whose errback is invoked
3131         if there is an error.  Body structure data describes the MIME-IMB
3132         format of a message and consists of a sequence of mime type, mime
3133         subtype, parameters, content id, description, encoding, and size.
3134         The fields following the size field are variable: if the mime
3135         type/subtype is message/rfc822, the contained message's envelope
3136         information, body structure data, and number of lines of text; if
3137         the mime type is text, the number of lines of text.  Extension fields
3138         may also be included; if present, they are: the MD5 hash of the body,
3139         body disposition, body language.
3140         """
3141         return self._fetch(messages, useUID=uid, bodystructure=1)
3142
3143
3144     def fetchSimplifiedBody(self, messages, uid=0):
3145         """Retrieve the simplified body structure of one or more messages
3146
3147         This command is allowed in the Selected state.
3148
3149         @type messages: C{MessageSet} or C{str}
3150         @param messages: A message sequence set
3151
3152         @type uid: C{bool}
3153         @param uid: Indicates whether the message sequence set is of message
3154         numbers or of unique message IDs.
3155
3156         @rtype: C{Deferred}
3157         @return: A deferred whose callback is invoked with a dict mapping
3158         message numbers to body data, or whose errback is invoked
3159         if there is an error.  The simplified body structure is the same
3160         as the body structure, except that extension fields will never be
3161         present.
3162         """
3163         return self._fetch(messages, useUID=uid, body=1)
3164
3165
3166     def fetchMessage(self, messages, uid=0):
3167         """Retrieve one or more entire messages
3168
3169         This command is allowed in the Selected state.
3170
3171         @type messages: L{MessageSet} or C{str}
3172         @param messages: A message sequence set
3173
3174         @type uid: C{bool}
3175         @param uid: Indicates whether the message sequence set is of message
3176         numbers or of unique message IDs.
3177
3178         @rtype: L{Deferred}
3179
3180         @return: A L{Deferred} which will fire with a C{dict} mapping message
3181             sequence numbers to C{dict}s giving message data for the
3182             corresponding message.  If C{uid} is true, the inner dictionaries
3183             have a C{'UID'} key mapped to a C{str} giving the UID for the
3184             message.  The text of the message is a C{str} associated with the
3185             C{'RFC822'} key in each dictionary.
3186         """
3187         return self._fetch(messages, useUID=uid, rfc822=1)
3188
3189
3190     def fetchHeaders(self, messages, uid=0):
3191         """Retrieve headers of one or more messages
3192
3193         This command is allowed in the Selected state.
3194
3195         @type messages: C{MessageSet} or C{str}
3196         @param messages: A message sequence set
3197
3198         @type uid: C{bool}
3199         @param uid: Indicates whether the message sequence set is of message
3200         numbers or of unique message IDs.
3201
3202         @rtype: C{Deferred}
3203         @return: A deferred whose callback is invoked with a dict mapping
3204         message numbers to dicts of message headers, or whose errback is
3205         invoked if there is an error.
3206         """
3207         return self._fetch(messages, useUID=uid, rfc822header=1)
3208
3209
3210     def fetchBody(self, messages, uid=0):
3211         """Retrieve body text of one or more messages
3212
3213         This command is allowed in the Selected state.
3214
3215         @type messages: C{MessageSet} or C{str}
3216         @param messages: A message sequence set
3217
3218         @type uid: C{bool}
3219         @param uid: Indicates whether the message sequence set is of message
3220         numbers or of unique message IDs.
3221
3222         @rtype: C{Deferred}
3223         @return: A deferred whose callback is invoked with a dict mapping
3224         message numbers to file-like objects containing body text, or whose
3225         errback is invoked if there is an error.
3226         """
3227         return self._fetch(messages, useUID=uid, rfc822text=1)
3228
3229
3230     def fetchSize(self, messages, uid=0):
3231         """Retrieve the size, in octets, of one or more messages
3232
3233         This command is allowed in the Selected state.
3234
3235         @type messages: C{MessageSet} or C{str}
3236         @param messages: A message sequence set
3237
3238         @type uid: C{bool}
3239         @param uid: Indicates whether the message sequence set is of message
3240         numbers or of unique message IDs.
3241
3242         @rtype: C{Deferred}
3243         @return: A deferred whose callback is invoked with a dict mapping
3244         message numbers to sizes, or whose errback is invoked if there is
3245         an error.
3246         """
3247         return self._fetch(messages, useUID=uid, rfc822size=1)
3248
3249
3250     def fetchFull(self, messages, uid=0):
3251         """Retrieve several different fields of one or more messages
3252
3253         This command is allowed in the Selected state.  This is equivalent
3254         to issuing all of the C{fetchFlags}, C{fetchInternalDate},
3255         C{fetchSize}, C{fetchEnvelope}, and C{fetchSimplifiedBody}
3256         functions.
3257
3258         @type messages: C{MessageSet} or C{str}
3259         @param messages: A message sequence set
3260
3261         @type uid: C{bool}
3262         @param uid: Indicates whether the message sequence set is of message
3263         numbers or of unique message IDs.
3264
3265         @rtype: C{Deferred}
3266         @return: A deferred whose callback is invoked with a dict mapping
3267         message numbers to dict of the retrieved data values, or whose
3268         errback is invoked if there is an error.  They dictionary keys
3269         are "flags", "date", "size", "envelope", and "body".
3270         """
3271         return self._fetch(
3272             messages, useUID=uid, flags=1, internaldate=1,
3273             rfc822size=1, envelope=1, body=1)
3274
3275
3276     def fetchAll(self, messages, uid=0):
3277         """Retrieve several different fields of one or more messages
3278
3279         This command is allowed in the Selected state.  This is equivalent
3280         to issuing all of the C{fetchFlags}, C{fetchInternalDate},
3281         C{fetchSize}, and C{fetchEnvelope} functions.
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 numbers to dict of the retrieved data values, or whose
3293         errback is invoked if there is an error.  They dictionary keys
3294         are "flags", "date", "size", and "envelope".
3295         """
3296         return self._fetch(
3297             messages, useUID=uid, flags=1, internaldate=1,
3298             rfc822size=1, envelope=1)
3299
3300
3301     def fetchFast(self, messages, uid=0):
3302         """Retrieve several different fields of one or more messages
3303
3304         This command is allowed in the Selected state.  This is equivalent
3305         to issuing all of the C{fetchFlags}, C{fetchInternalDate}, and
3306         C{fetchSize} functions.
3307
3308         @type messages: C{MessageSet} or C{str}
3309         @param messages: A message sequence set
3310
3311         @type uid: C{bool}
3312         @param uid: Indicates whether the message sequence set is of message
3313         numbers or of unique message IDs.
3314
3315         @rtype: C{Deferred}
3316         @return: A deferred whose callback is invoked with a dict mapping
3317         message numbers to dict of the retrieved data values, or whose
3318         errback is invoked if there is an error.  They dictionary keys are
3319         "flags", "date", and "size".
3320         """
3321         return self._fetch(
3322             messages, useUID=uid, flags=1, internaldate=1, rfc822size=1)
3323
3324
3325     def _parseFetchPairs(self, fetchResponseList):
3326         """
3327         Given the result of parsing a single I{FETCH} response, construct a
3328         C{dict} mapping response keys to response values.
3329
3330         @param fetchResponseList: The result of parsing a I{FETCH} response
3331             with L{parseNestedParens} and extracting just the response data
3332             (that is, just the part that comes after C{"FETCH"}).  The form
3333             of this input (and therefore the output of this method) is very
3334             disagreable.  A valuable improvement would be to enumerate the
3335             possible keys (representing them as structured objects of some
3336             sort) rather than using strings and tuples of tuples of strings
3337             and so forth.  This would allow the keys to be documented more
3338             easily and would allow for a much simpler application-facing API
3339             (one not based on looking up somewhat hard to predict keys in a
3340             dict).  Since C{fetchResponseList} notionally represents a
3341             flattened sequence of pairs (identifying keys followed by their
3342             associated values), collapsing such complex elements of this
3343             list as C{["BODY", ["HEADER.FIELDS", ["SUBJECT"]]]} into a
3344             single object would also greatly simplify the implementation of
3345             this method.
3346
3347<