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