offlineimap rev 552

"Automatic Subversion Change Mailer" <[email protected]> Tue, 5 Aug 2003 14:34:10 -0500 (CDT)
Newsgroups gmane.mail.imap.offlineimap.subversion
Message-ID <[email protected]>
You are receiving this message because
all commits get sent to this address.

Author: jgoerzen
Date: 2003-08-05 14:34:03 -0500 (Tue, 05 Aug 2003)
New Revision: 552

Modified:
  offlineimap/branches/twisted/twistedfixes/
  offlineimap/branches/twisted/twistedfixes/README
  offlineimap/branches/twisted/twistedfixes/__init__.py
  offlineimap/branches/twisted/twistedfixes/imap4.py

Log:
Imported imap4.py from Twisted source so I can hack it up.


Diff:
Added: offlineimap/branches/twisted/twistedfixes/README
==============================================================================
--- offlineimap/branches/twisted/twistedfixes/README	2003-08-01 18:01:59 UTC (rev 551)
+++ offlineimap/branches/twisted/twistedfixes/README	2003-08-05 19:34:03 UTC (rev 552)
@@ -0,0 +1,2 @@
+This directory contains fixes from 1.0.7 and SHOULD BE DELETED after 1.0.7
+is released.

Added: offlineimap/branches/twisted/twistedfixes/__init__.py
==============================================================================

Added: offlineimap/branches/twisted/twistedfixes/imap4.py
==============================================================================
--- offlineimap/branches/twisted/twistedfixes/imap4.py	2003-08-01 18:01:59 UTC (rev 551)
+++ offlineimap/branches/twisted/twistedfixes/imap4.py	2003-08-05 19:34:03 UTC (rev 552)
@@ -0,0 +1,4282 @@
+# -*- test-case-name: twisted.test.test_imap -*-
+# Twisted, the Framework of Your Internet
+# Copyright (C) 2001 Matthew W. Lefkowitz
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of version 2.1 of the GNU Lesser General Public
+# License as published by the Free Software Foundation.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+"""
+An IMAP4 protocol implementation
+
+API Stability: Semi-stable
+
+@author: U{Jp Calderone<mailto:[email protected]>}
+
+To do: 
+  Suspend idle timeout while server is processing
+  Use an async message parser instead of buffering in memory
+  Figure out a way to not queue multi-message client requests (Flow? A simple callback?)
+  Clarify some API docs (Query, etc)
+  Make APPEND recognize (again) non-existent mailboxes before accepting the literal
+"""
+
+from __future__ import nested_scopes
+from __future__ import generators
+
+from twisted.protocols import basic
+from twisted.protocols import policies
+from twisted.internet import defer
+from twisted.internet import error
+from twisted.internet.defer import maybeDeferred
+from twisted.python import log, components, util, failure
+from twisted.cred import perspective
+from twisted.python.components import implements
+from twisted.internet.interfaces import ITLSTransport
+
+from twisted import cred
+import twisted.cred.error
+import twisted.cred.credentials
+
+import inspect
+import base64
+import binascii
+import hmac
+import re
+import tempfile
+import string
+import time
+import types
+import sys
+
+try:
+    import cStringIO as StringIO
+except:
+    import StringIO
+
+infrangeobject = xrange(sys.maxint)
+
+class MessageSet(object):
+    """
+    Essentially an infinite bitfield, with some extra features.
+
+    @type getnext: Function taking C{int} returning C{int}
+    @ivar getnext: A function that returns the next message number,
+    used when iterating through the MessageSet. By default, a function
+    returning the next integer is supplied, but as this can be rather
+    inefficient for sparse UID iterations, it is recommended to supply
+    one when messages are requested by UID.
+    """
+    _empty = []
+    
+    def __init__(self, start=_empty, end=_empty):
+        """
+        Create a new MessageSet()
+
+        @type start: Optional C{int}
+        @param start: Start of range, or only message number
+
+        @type end: Optional C{int}
+        @param end: End of range.
+        """
+        self._last = self._empty # Last message/UID in use
+        self.ranges = [] # List of ranges included
+        self.getnext = lambda x: x+1 # A function which will return the next
+                                     # message id. Handy for UID requests.
+
+        if start is self._empty:
+            return
+
+        if isinstance(start, types.ListType):
+            self.ranges = start[:]
+            self.clean()
+        else:
+            self.add(start,end)
+
+    # Ooo.  A property.
+    def last():
+        def _setLast(self,value):
+            if self._last is not self._empty:
+                raise ValueError("last already set")
+            
+            self._last = value
+            for i,(l,h) in zip(infrangeobject,self.ranges):
+                if l is not None:
+                    break # There are no more Nones after this
+                l = value
+                if h is None:
+                    h = value
+                if l > h:
+                    l, h = h, l
+                self.ranges[i] = (l,h)
+
+            self.clean()
+
+        def _getLast(self):
+            return self._last
+        
+        doc = '''
+            "Highest" message number, refered to by "*".
+            Must be set before attempting to use the MessageSet.
+        '''
+        return _getLast, _setLast, None, doc
+    last = property(*last())
+
+    def add(self, start, end=_empty):
+        """
+        Add another range
+
+        @type start: C{int}
+        @param start: Start of range, or only message number
+
+        @type end: Optional C{int}
+        @param end: End of range.
+        """
+        if end is self._empty:
+            end = start
+
+        if self._last is not self._empty:
+            if start is None:
+                start = self.last
+            if end is None:
+                end = self.last
+
+        if start > end:
+            # Try to keep in low, high order if possible
+            # (But we don't know what None means, this will keep
+            # None at the start of the ranges list)
+            start, end = end, start
+
+        self.ranges.append((start,end))
+        self.clean()
+
+    def __add__(self, other):
+        if isinstance(other, MessageSet):
+            ranges = self.ranges + other.ranges
+            return MessageSet(ranges)
+        else:
+            res = MessageSet(self.ranges)
+            try:
+                res.add(*other)
+            except TypeError:
+                res.add(other)
+            return res
+
+    def extend(self, other):
+        if isinstance(other, MessageSet):
+            self.ranges.extend(other.ranges)
+            self.clean()
+        else:
+            try:
+                self.add(*other)
+            except TypeError:
+                self.add(other)
+
+        return self
+
+    def clean(self):
+        """
+        Clean ranges list, combining adjacent ranges
+        """
+        
+        self.ranges.sort()
+
+        oldl, oldh = None, None
+        for i,(l,h) in zip(infrangeobject,self.ranges):
+            if l is None:
+                continue
+            # l is >= oldl and h is >= oldh due to sort()
+            if oldl is not None and l <= oldh+1:
+                l = oldl
+                h = max(oldh,h)
+                self.ranges[i-1] = None
+                self.ranges[i] = (l,h)
+
+            oldl,oldh = l,h
+
+        self.ranges = filter(None, self.ranges)
+
+    def __contains__(self, value):
+        """
+        May raise TypeError if we encounter unknown "high" values
+        """
+        for l,h in self.ranges:
+            if l is None:
+                raise TypeError(
+                    "Can't determine membership; last value not set")
+            if l <= value <= h:
+                return True
+
+        return False
+
+    def _iterator(self):
+        for l,h in self.ranges:
+            l = self.getnext(l-1)
+            while l <= h:
+                yield l
+                l = self.getnext(l)
+                if l is None:
+                    break
+
+    def __iter__(self):
+        if self.ranges and self.ranges[0][0] is None:
+            raise TypeError("Can't iterate; last value not set")
+
+        return self._iterator()
+
+    def __len__(self):
+        res = 0
+        for l, h in self.ranges:
+            if l is None:
+                raise TypeError("Can't size object; last value not set")
+            res += (h - l) + 1
+
+        return res
+
+    def __str__(self):
+        p = []
+        for low, high in self.ranges:
+            if low == high:
+                if low is None:
+                    p.append('*')
+                else:
+                    p.append(str(low))
+            elif low is None:
+                p.append('%d:*' % (high,))
+            else:
+                p.append('%d:%d' % (low, high))
+        return ','.join(p)
+    
+    def __repr__(self):
+        return '<MessageSet %s>' % (str(self),)
+
+    def __eq__(self, other):
+        if isinstance(other, MessageSet):
+            return self.ranges == other.ranges
+        return False
+
+class LiteralString:
+
+
+
+    def __init__(self, size, defered):
+        self.size = size
+        self.data = []
+        self.defer = defered
+
+    def write(self, data):
+        self.size -= len(data)
+        passon = None
+        if self.size > 0:
+            self.data.append(data)
+        else:
+            if self.size:
+                data, passon = data[:self.size], data[self.size:]
+            else:
+                passon = ''
+            if data:
+                self.data.append(data)
+        return passon
+
+    def callback(self, line):
+        """
+        Call defered with data and rest of line
+        """
+        self.defer.callback((''.join(self.data), line))
+
+class LiteralFile:
+    _memoryFileLimit = 1024 * 1024 * 10
+
+    def __init__(self, size, defered):
+        self.size = size
+        self.defer = defered
+        if size > self._memoryFileLimit:
+            self.data = tempfile.TemporaryFile()
+        else:
+            self.data = StringIO.StringIO()
+
+    def write(self, data):
+        self.size -= len(data)
+        passon = None
+        if self.size > 0:
+            self.data.write(data)
+        else:
+            if self.size:
+                data, passon = data[:self.size], data[self.size:]
+            else:
+                passon = ''
+            if data:
+                self.data.write(data)
+        return passon
+
+    def callback(self, line):
+        """
+        Call defered with data and rest of line
+        """
+        self.data.seek(0,0)
+        self.defer.callback((self.data, line))
+
+        
+class Command:
+    _1_RESPONSES = ('CAPABILITY', 'FLAGS', 'LIST', 'LSUB', 'STATUS', 'SEARCH')
+    _2_RESPONSES = ('EXISTS', 'EXPUNGE', 'FETCH', 'RECENT')
+    _OK_RESPONSES = ('UIDVALIDITY', 'READ-WRITE', 'READ-ONLY')
+    defer = None
+    
+    def __init__(self, command, args='', continuation=None, wantResponse=()):
+        self.command = command
+        self.args = args
+        self.continuation = continuation
+        self.wantResponse = wantResponse
+        self.lines = []
+    
+    def finish(self, lastLine, unusedCallback):
+        send = []
+        unuse = []
+        for L in self.lines:
+            names = parseNestedParens(L)
+            N = len(names)
+            if (N >= 1 and names[0] in self._1_RESPONSES or
+                N >= 2 and names[1] in self._2_RESPONSES or
+                N >= 2 and names[0] == 'OK' and isinstance(names[1], types.ListType) and names[1][0] in self._OK_RESPONSES):
+                send.append(L)
+            else:
+                unuse.append(L)
+        self.defer.callback((send, lastLine))
+        if unuse:
+            unusedCallback(unuse)
+
+class IMAP4Exception(Exception):
+    def __init__(self, *args):
+        Exception.__init__(self, *args)
+
+class IllegalClientResponse(IMAP4Exception): pass
+
+class IllegalOperation(IMAP4Exception): pass
+
+class IllegalMailboxEncoding(IMAP4Exception): pass
+
+class IMailboxListener(components.Interface):
+    """Interface for objects interested in mailbox events"""
+    
+    def modeChanged(self, writeable):
+        """Indicates that the write status of a mailbox has changed.
+        
+        @type writeable: C{bool}
+        @param writeable: A true value if write is now allowed, false
+        otherwise.
+        """
+    
+    def flagsChanged(self, newFlags):
+        """Indicates that the flags of one or more messages have changed.
+        
+        @type newFlags: C{dict}
+        @param newFlags: A mapping of message identifiers to tuples of flags
+        new set on that message.
+        """
+
+    def newMessages(self, exists, recent):
+        """Indicates that the number of messages in a mailbox has changed.
+        
+        @type exists: C{int} or C{None}
+        @param exists: The total number of messages now in this mailbox.
+        If the total number of messages has not changed, this should be
+        C{None}.
+
+        @type recent: C{int}
+        @param recent: The number of messages now flagged \\Recent.
+        If the number of recent messages has not changed, this should be
+        C{None}.
+        """
+
+class IMAP4Server(basic.LineReceiver, policies.TimeoutMixin):
+    """
+    Protocol implementation for an IMAP4rev1 server.
+
+    The server can be in any of four states:
+        - Non-authenticated
+        - Authenticated
+        - Selected
+        - Logout
+    """
+    __implements__ = (IMailboxListener,)
+
+    # Identifier for this server software
+    IDENT = 'Twisted IMAP4rev1 Ready'
+    
+    # Number of seconds before idle timeout
+    # Initially 1 minute.  Raised to 30 minutes after login.
+    timeOut = 60
+
+    POSTAUTH_TIMEOUT = 60 * 30
+
+    # Whether STARTTLS has been issued successfully yet or not.
+    startedTLS = False
+    
+    # Whether our transport supports TLS
+    canStartTLS = False
+
+    # Mapping of tags to commands we have received
+    tags = None
+
+    # The object which will handle logins for us
+    portal = None
+
+    # The account object for this connection
+    account = None
+    
+    # Logout callback
+    _onLogout = None
+
+    # The currently selected mailbox
+    mbox = None
+
+    # Command data to be processed when literal data is received
+    _pendingLiteral = None
+
+    # Maximum length to accept for a "short" string literal
+    _literalStringLimit = 4096
+
+    # IChallengeResponse factories for AUTHENTICATE command
+    challengers = None
+    
+    state = 'unauth'
+
+    def __init__(self, chal = None, contextFactory = None):
+        if chal is None:
+            chal = {}
+        self.challengers = chal
+        self.ctx = contextFactory
+
+    def capabilities(self):
+        cap = {'AUTH': self.challengers.keys()}
+        if self.ctx and self.canStartTLS and not self.startedTLS:
+            cap['LOGINDISABLED'] = None
+            cap['STARTTLS'] = None
+        return cap
+
+    def connectionMade(self):
+        self.tags = {}
+        self.setTimeout(self.timeOut)
+        self.sendServerGreeting()
+        self.canStartTLS = implements(self.transport, ITLSTransport)
+    
+    def connectionLost(self, reason):
+        self.setTimeout(None)
+        if self._onLogout:
+            self._onLogout()
+            self._onLogout = None
+
+    def timeoutConnection(self):
+        self.sendLine('* BYE Autologout; connection idle too long')
+        self.transport.loseConnection()
+        if self.mbox:
+            self.mbox.removeListener(self)
+            self.mbox = None
+
+    def rawDataReceived(self, data):
+        self.resetTimeout()
+        passon = self._pendingLiteral.write(data)
+        if passon is not None:
+            self.setLineMode(passon)
+
+#    def sendLine(self, line):
+#        print 'C:', repr(line)
+#        return basic.LineReceiver.sendLine(self, line)
+
+    def lineReceived(self, line):
+        # print 'S:', repr(line)
+        self.resetTimeout()
+        if self._pendingLiteral:
+            self._pendingLiteral.callback(line)
+            self._pendingLiteral = None
+            return
+            
+        args = line.split(None, 2)
+        rest = None
+        if len(args) == 3:
+            tag, cmd, rest = args
+        elif len(args) == 2:
+            tag, cmd = args
+        elif len(args) == 1:
+            tag = args[0]
+            self.sendBadResponse(tag, 'Missing command')
+            return
+        else:
+            self.sendBadResponse(None, 'Null command')
+            return
+
+        cmd = cmd.upper()
+        self.dispatchCommand(tag, cmd, rest)
+
+    def dispatchCommand(self, tag, cmd, rest, uid=None):
+        f = self.lookupCommand(cmd)
+        if f:
+            try:
+                fn = f[0]
+                parseargs = f[1:]
+                self.__doCommand(tag, fn, [self, tag], parseargs, rest,
+                                 uid)
+            except IllegalClientResponse, e:
+                self.sendBadResponse(tag, 'Illegal syntax: ' + str(e))
+            except IllegalOperation, e:
+                self.sendNegativeResponse(tag, 'Illegal operation: ' + str(e))
+            except IllegalMailboxEncoding, e:
+                self.sendNegativeResponse(tag, 'Illegal mailbox name: ' + str(e))
+            except Exception, e:
+                self.sendBadResponse(tag, 'Server error: ' + str(e))
+                log.err()
+        else:
+            self.sendBadResponse(tag, 'Unsupported command')
+
+    def lookupCommand(self, cmd):
+        return getattr(self, '_'.join((self.state, cmd.upper())), None)
+
+    def __doCommand(self, tag, handler, args, parseargs, line, uid):
+        for (i, arg) in zip(infrangeobject, parseargs):
+            if callable(arg):
+                parseargs = parseargs[i+1:]
+                maybeDeferred(arg, self, line).addCallback(
+                    self.__cbDispatch, tag, handler, args,
+                    parseargs, uid).addErrback(self.__ebDispatch, tag)
+                return
+            else:
+                args.append(arg)
+
+        if line:
+            # Too many arguments
+            raise IllegalClientResponse("Too many arguments for command")
+
+        if uid is not None:
+            handler(uid=uid, *args)
+        else:
+            handler(*args)
+
+    def __cbDispatch(self, (arg, rest), tag, fn, args, parseargs, uid):
+        args.append(arg)
+        self.__doCommand(tag, fn, args, parseargs, rest, uid)
+
+    def __ebDispatch(self, failure, tag):
+
+        if failure.check(IllegalClientResponse):
+            self.sendBadResponse(tag, 'Illegal syntax: ' + str(failure.value))
+        elif failure.check(IllegalOperation):
+            self.sendNegativeResponse(tag, 'Illegal operation: ' +
+                                      str(failure.value))
+        elif failure.check(IllegalMailboxEncoding):
+            self.sendNegativeResponse(tag, 'Illegal mailbox name: ' +
+                                      str(failure.value))
+        else:
+            self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
+            log.err(failure)
+        
+    def _stringLiteral(self, size):
+        if size > self._literalStringLimit:
+            raise IllegalClientResponse(
+                "Literal too long! I accept at most %d octets" %
+                (self._literalStringLimit,))
+        d = defer.Deferred()
+        self._pendingLiteral = LiteralString(size, d)
+        self.sendContinuationRequest('Ready for %d octets of text' % size)
+        self.setRawMode()
+        return d
+
+    def _fileLiteral(self, size):
+        d = defer.Deferred()
+        self._pendingLiteral = LiteralFile(size, d)
+        self.sendContinuationRequest('Ready for %d octets of data' % size)
+        self.setRawMode()
+        return d
+
+    def arg_astring(self, line):
+        """
+        Parse an astring from the line, return (arg, rest), possibly
+        via a deferred (to handle literals)
+        """
+        # line = line.lstrip()
+        if not line:
+            raise IllegalClientResponse("Missing argument")
+        d = None
+        arg, rest = None, None
+        if line[0] == '"':
+            try:
+                spam, arg, rest = line.split('"',2)
+                rest = rest[1:] # Strip space
+            except ValueError:
+                raise IllegalClientResponse("Unmatched quotes")
+        elif line[0] == '{':
+            # literal
+            if line[-1] != '}':
+                raise IllegalClientResponse("Malformed literal")
+            try:
+                size = int(line[1:-1])
+            except ValueError:
+                raise IllegalClientResponse("Bad literal size: " + line[1:-1])
+            d = self._stringLiteral(size)
+        else:
+            arg = line.split(' ',1)
+            if len(arg) == 1:
+                arg.append('')
+            arg, rest = arg
+        return d or (arg, rest)
+
+    # ATOM: Any CHAR except ( ) { % * " \ ] CTL SP (CHAR is 7bit)
+    atomre = re.compile(r'(?P<atom>[^\](){%*"\\\x00-\x20\x80-\xff]+)( (?P<rest>.*$)|$)')
+
+    def arg_atom(self, line):
+        """
+        Parse an atom from the line
+        """
+        if not line:
+            raise IllegalClientResponse("Missing argument")
+        m = self.atomre.match(line)
+        if m:
+            return m.group('atom'), m.group('rest')
+        else:
+            raise IllegalClientResponse("Malformed ATOM")
+
+    def arg_plist(self, line):
+        """
+        Parse a (non-nested) parenthesised list from the line
+        """
+        if not line:
+            raise IllegalClientResponse("Missing argument")
+
+        if line[0] != "(":
+            raise IllegalClientResponse("Missing parenthesis")
+
+        i = line.find(")")
+
+        if i == -1:
+            raise IllegalClientResponse("Mismatched parenthesis")
+
+        return (parseNestedParens(line[1:i],0), line[i+2:])
+
+    def arg_literal(self, line):
+        """
+        Parse a literal from the line
+        """
+        if not line:
+            raise IllegalClientResponse("Missing argument")
+        
+        if line[0] != '{':
+            raise IllegalClientResponse("Missing literal")
+
+        if line[-1] != '}':
+            raise IllegalClientResponse("Malformed literal")
+        
+        try:
+            size = int(line[1:-1])
+        except ValueError:
+            raise IllegalClientResponse("Bad literal size: " + line[1:-1])
+        
+        return self._fileLiteral(size)
+
+    def arg_searchkeys(self, line):
+        """
+        searchkeys
+        """
+        query = parseNestedParens(line)
+        # XXX Should really use list of search terms and parse into
+        # a proper tree
+
+        return (query, '')
+
+    def arg_seqset(self, line):
+        """
+        sequence-set
+        """
+        rest = ''
+        arg = line.split(' ',1)
+        if len(arg) == 2:
+            rest = arg[1]
+        arg = arg[0]
+
+        return (parseIdList(arg), rest)
+
+    def arg_fetchatt(self, line):
+        """
+        fetch-att
+        """
+        p = _FetchParser()
+        p.parseString(line)
+        return (p.result, '')
+
+    def arg_flaglist(self, line):
+        """
+        Flag part of store-att-flag
+        """
+        flags = []
+        if line[0] == '(':
+            if line[-1] != ')':
+                raise IllegalClientResponse("Mismatched parenthesis")
+            line = line[1:-1]
+
+        while line:
+            m = self.atomre.search(line)
+            if not m:
+                raise IllegalClientResponse("Malformed flag")
+            if line[0] == '\\' and m.start() == 1:
+                flags.append('\\' + m.group('atom'))
+            elif m.start() == 0:
+                flags.append(m.group('atom'))
+            else:
+                raise IllegalClientResponse("Malformed flag")
+            line = m.group('rest')
+
+        return (flags, '')
+
+    def arg_line(self, line):
+        """
+        Command line of UID command
+        """
+        return (line, '')
+
+    def opt_plist(self, line):
+        """
+        Optional parenthesised list
+        """
+        if line.startswith('('):
+            return self.arg_plist(line)
+        else:
+            return (None, line)
+
+    def opt_datetime(self, line):
+        """
+        Optional date-time string
+        """
+        if line.startswith('"'):
+            try:
+                spam, date, rest = line.split('"',2)
+            except IndexError:
+                raise IllegalClientResponse("Malformed date-time")
+            return (date, rest[1:])
+        else:
+            return (None, line)
+
+    def opt_charset(self, line):
+        """
+        Optional charset of SEARCH command
+        """
+        if line[:7].upper() == 'CHARSET':
+            arg = line.split(' ',2)
+            if len(arg) == 1:
+                raise IllegalClientResponse("Missing charset identifier")
+            if len(arg) == 2:
+                arg.append('')
+            spam, arg, rest = arg
+            return (arg, rest)
+        else:
+            return (None, line)
+
+    def sendServerGreeting(self):
+        msg = '[CAPABILITY %s] %s' % (' '.join(self.listCapabilities()), self.IDENT)
+        self.sendPositiveResponse(message=msg)
+
+    def sendBadResponse(self, tag = None, message = ''):
+        self._respond('BAD', tag, message)
+
+    def sendPositiveResponse(self, tag = None, message = ''):
+        self._respond('OK', tag, message)
+
+    def sendNegativeResponse(self, tag = None, message = ''):
+        self._respond('NO', tag, message)
+
+    def sendUntaggedResponse(self, message):
+        self._respond(message, None, None)
+
+    def sendContinuationRequest(self, msg = 'Ready for additional command text'):
+        self.sendLine('+ ' + msg)
+
+    def _respond(self, state, tag, message):
+        if not tag:
+            tag = '*'
+        if message:
+            self.sendLine(' '.join((tag, state, message)))
+        else:
+            self.sendLine(' '.join((tag, state)))
+
+    def listCapabilities(self):
+        caps = ['IMAP4rev1']
+        for c, v in self.capabilities().iteritems():
+            if v is None:
+                caps.append(c)
+            elif len(v):
+                caps.extend([('%s=%s' % (c, cap)) for cap in v])
+        return caps
+
+    def do_CAPABILITY(self, tag):
+        self.sendUntaggedResponse('CAPABILITY ' + ' '.join(self.listCapabilities()))
+        self.sendPositiveResponse(tag, 'CAPABILITY completed')
+
+    unauth_CAPABILITY = (do_CAPABILITY,)
+    auth_CAPABILITY = unauth_CAPABILITY
+    select_CAPABILITY = unauth_CAPABILITY
+    logout_CAPABILITY = unauth_CAPABILITY
+
+    def do_LOGOUT(self, tag):
+        self.sendUntaggedResponse('BYE Nice talking to you')
+        self.sendPositiveResponse(tag, 'LOGOUT successful')
+        self.transport.loseConnection()
+
+    unauth_LOGOUT = (do_LOGOUT,)
+    auth_LOGOUT = unauth_LOGOUT
+    select_LOGOUT = unauth_LOGOUT
+    logout_LOGOUT = unauth_LOGOUT
+
+    def do_NOOP(self, tag):
+        self.sendPositiveResponse(tag, 'NOOP No operation performed')
+
+    unauth_NOOP = (do_NOOP,)
+    auth_NOOP = unauth_NOOP
+    select_NOOP = unauth_NOOP
+    logout_NOOP = unauth_NOOP
+
+    def do_AUTHENTICATE(self, tag, args):
+        args = args.upper().strip()
+        if args not in self.challengers:
+            self.sendNegativeResponse(tag, 'AUTHENTICATE method unsupported')
+        else:
+            self.authenticate(self.challengers[args](), tag)
+
+    unauth_AUTHENTICATE = (do_AUTHENTICATE, arg_atom)
+
+    def authenticate(self, chal, tag):
+        if self.portal is None:
+            self.sendNegativeResponse(tag, 'Temporary authentication failure')
+            return
+
+        try:
+            challenge = chal.getChallenge()
+        except Exception, e:
+            self.sendBadResponse(tag, 'Server error: ' + str(e))
+        else:
+            coded = base64.encodestring(challenge)[:-1]
+            self._pendingLiteral = defer.Deferred()
+            self.sendContinuationRequest(coded)
+            self._pendingLiteral.addCallback(self.__cbAuthChunk, chal, tag)
+            self._pendingLiteral.addErrback(self.__ebAuthChunk, chal, tag)
+
+    def __cbAuthChunk(self, result, chal, tag):
+        try:
+            uncoded = base64.decodestring(result)
+        except binascii.Error:
+            raise IllegalClientResponse("Malformed Response - not base64")
+
+        parts = uncoded.split(None, 1)
+        if len(parts) != 2:
+            raise IllegalClientResponse("Malformed Response - wrong number of parts")
+        chal.username = parts[0]
+        chal.response = parts[1]
+        self.portal.login(chal, None, IAccount).addCallbacks(
+            self.__cbAuthResp,
+            self.__ebAuthResp,
+            (tag,), None, (tag,), None
+        )
+
+    def __cbAuthResp(self, (iface, avatar, logout), tag):
+        assert iface is IAccount, "IAccount is the only supported interface"
+        self.account = avatar
+        self.state = 'auth'
+        self._onLogout = logout
+        self.sendPositiveResponse(tag, 'Authentication successful')
+        self.setTimeout(self.POSTAUTH_TIMEOUT)
+    
+    def __ebAuthResp(self, failure, tag):
+        if failure.check(cred.error.UnauthorizedLogin):
+            self.sendNegativeResponse(tag, 'Authentication failed: ' + str(failure.value))
+        else:
+            self.sendBadResponse(tag, 'Server error')
+            log.err(failure)
+    
+    def __ebAuthChunk(self, failure, tag):
+        self.sendNegativeResponse(tag, 'Authentication failed: ' + str(failure.value))
+
+    def do_STARTTLS(self, tag):
+        if self.startedTLS:
+            self.sendNegativeResponse(tag, 'TLS already negotiated')
+        elif self.ctx and self.canStartTLS:
+            self.sendPositiveResponse(tag, 'Begin TLS negotiation now')
+            self.transport.startTLS(self.ctx)
+            self.startedTLS = True
+        else:
+            self.sendNegativeResponse(tag, 'TLS not available')
+
+    unauth_STARTTLS = (do_STARTTLS,)
+
+    def do_LOGIN(self, tag, user, passwd):
+        if 'LOGINDISABLED' in self.capabilities():
+            self.sendBadResponse(tag, 'LOGIN is disabled before STARTTLS')
+            return
+
+        maybeDeferred(self.authenticateLogin, user, passwd).addCallbacks(
+            self.__cbLogin, self.__ebLogin, (tag,), None, (tag,), None
+        )
+
+    unauth_LOGIN = (do_LOGIN, arg_astring, arg_astring)
+
+    def authenticateLogin(self, user, passwd):
+        """Lookup the account associated with the given parameters
+
+        Override this method to define the desired authentication behavior.
+        
+        The default behavior is to defer authentication to C{self.portal}
+        if it is not None, or to deny the login otherwise.
+
+        @type user: C{str}
+        @param user: The username to lookup
+
+        @type passwd: C{str}
+        @param passwd: The password to login with
+        """
+        if self.portal:
+            return self.portal.login(
+                cred.credentials.UsernamePassword(user, passwd),
+                None, IAccount
+            )
+        raise cred.error.UnauthorizedLogin()
+
+    def __cbLogin(self, (iface, avatar, logout), tag):
+        if iface is not IAccount:
+            self.sendBadResponse(tag, 'Server error')
+            log.err("__cbLogin called with %r, IAccount expected" % (iface,))
+        else:
+            self.account = avatar
+            self._onLogout = logout
+            self.sendPositiveResponse(tag, 'LOGIN succeeded')
+            self.state = 'auth'
+            self.setTimeout(self.POSTAUTH_TIMEOUT)
+
+    def __ebLogin(self, failure, tag):
+        if failure.check(cred.error.UnauthorizedLogin):
+            self.sendNegativeResponse(tag, 'LOGIN failed')
+        else:
+            self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
+            log.err(failure)
+
+    def _parseMbox(self, name):
+        try:
+            return name.decode('imap4-utf-7')
+        except:
+            log.err()
+            raise IllegalMailboxEncoding(name)
+
+    def _selectWork(self, tag, name, rw, cmdName):
+        name = self._parseMbox(name)
+        defer.maybeDeferred(
+            self.account.select, self._parseMbox(name), rw
+        ).addCallback(self._cbSelectWork, cmdName, tag
+        ).addErrback(self._ebSelectWork, cmdName, tag
+        )
+
+    def _ebSelectWork(self, failure, cmdName, tag):
+        self.sendBadResponse(tag, "%s failed: Server error" % (cmdName,))
+        log.err(failure)
+
+    def _cbSelectWork(self, mbox, cmdName, tag):
+        if mbox is None:
+            self.sendNegativeResponse(tag, 'No such mailbox')
+            return
+
+        self.state = 'select'
+        flags = mbox.getFlags()
+        self.sendUntaggedResponse(str(mbox.getMessageCount()) + ' EXISTS')
+        self.sendUntaggedResponse(str(mbox.getRecentCount()) + ' RECENT')
+        self.sendUntaggedResponse('FLAGS (%s)' % ' '.join(flags))
+        self.sendPositiveResponse(None, '[UIDVALIDITY %d]' % mbox.getUIDValidity())
+
+        s = mbox.isWriteable() and 'READ-WRITE' or 'READ-ONLY'
+        if self.mbox:
+            self.mbox.removeListener(self)
+        self.mbox = mbox
+        self.mbox.addListener(self)
+        self.sendPositiveResponse(tag, '[%s] %s successful' % (s, cmdName))
+
+    auth_SELECT = ( _selectWork, arg_astring, 1, 'SELECT' )
+    select_SELECT = auth_SELECT
+    
+    auth_EXAMINE = ( _selectWork, arg_astring, 0, 'EXAMINE' )
+    select_EXAMINE = auth_EXAMINE
+
+    def do_CREATE(self, tag, name):
+        name = self._parseMbox(name)
+        try:
+            self.account.create(name)
+        except MailboxCollision, c:
+            self.sendNegativeResponse(tag, str(c))
+        else:
+            self.sendPositiveResponse(tag, 'Mailbox created')
+
+    auth_CREATE = (do_CREATE, arg_astring)
+    select_CREATE = auth_CREATE
+
+    def do_DELETE(self, tag, name):
+        name = self._parseMbox(name)
+        try:
+            self.account.delete(name)
+        except MailboxException, m:
+            self.sendNegativeResponse(tag, str(m))
+        else:
+            self.sendPositiveResponse(tag, 'Mailbox deleted')
+
+    auth_DELETE = (do_DELETE, arg_astring)
+    select_DELETE = auth_DELETE
+
+    def do_RENAME(self, tag, oldname, newname):
+        oldname, newname = [self._parseMbox(n) for n in oldname, newname]
+        try:
+            self.account.rename(oldname, newname)
+        except TypeError:
+            self.sendBadResponse(tag, 'Invalid command syntax')
+        except MailboxException, m:
+            self.sendNegativeResponse(tag, str(m))
+        else:
+            self.sendPositiveResponse(tag, 'Mailbox renamed')
+
+    auth_RENAME = (do_RENAME, arg_astring, arg_astring)
+    select_RENAME = auth_RENAME
+
+    def do_SUBSCRIBE(self, tag, name):
+        name = self._parseMbox(name)
+        try:
+            self.account.subscribe(name)
+        except MailboxException, m:
+            self.sendNegativeResponse(tag, str(m))
+        else:
+            self.sendPositiveResponse(tag, 'Subscribed')
+
+    auth_SUBSCRIBE = (do_SUBSCRIBE, arg_astring)
+    select_SUBSCRIBE = auth_SUBSCRIBE
+
+    def do_UNSUBSCRIBE(self, tag, name):
+        name = self._parseMbox(name)
+        try:
+            self.account.unsubscribe(name)
+        except MailboxException, m:
+            self.sendNegativeResponse(tag, str(m))
+        else:
+            self.sendPositiveResponse(tag, 'Unsubscribed')
+
+    auth_UNSUBSCRIBE = (do_UNSUBSCRIBE, arg_astring)
+    select_UNSUBSCRIBE = auth_UNSUBSCRIBE
+
+    def _listWork(self, tag, ref, mbox, sub, cmdName):
+        mbox = self._parseMbox(mbox)
+        mailboxes = self.account.listMailboxes(ref, mbox)
+        for (name, box) in mailboxes:
+            if not sub or self.account.isSubscribed(name):
+                flags = '(%s)' % ' '.join(box.getFlags())
+                delim = box.getHierarchicalDelimiter()
+                self.sendUntaggedResponse('%s %s "%s" %s' % (cmdName, flags, delim, name))
+        self.sendPositiveResponse(tag, '%s completed' % (cmdName,))
+
+    auth_LIST = (_listWork, arg_astring, arg_astring, 0, 'LIST')
+    select_LIST = auth_LIST
+
+    auth_LSUB = (_listWork, arg_astring, arg_astring, 1, 'LSUB')
+    select_LSUB = auth_LSUB
+
+    def do_STATUS(self, tag, mailbox, names):
+        mailbox = self._parseMbox(mailbox)
+        mbox = self.account.select(mailbox, 0)
+        if mbox:
+            maybeDeferred(mbox.requestStatus, names).addCallbacks(
+                self.__cbStatus, self.__ebStatus,
+                (tag, mailbox), None, (tag, mailbox), None
+            )
+        else:
+            self.sendNegativeResponse(tag, "Could not open mailbox")
+    
+    auth_STATUS = (do_STATUS, arg_astring, arg_plist)
+    select_STATUS = auth_STATUS
+
+    def __cbStatus(self, status, tag, box):
+        line = ' '.join(['%s %s' % x for x in status.iteritems()])
+        self.sendUntaggedResponse('STATUS %s (%s)' % (box, line))
+        self.sendPositiveResponse(tag, 'STATUS complete')
+
+    def __ebStatus(self, failure, tag, box):
+        self.sendBadResponse(tag, 'STATUS %s failed: %s' % (box, failure))
+
+    def do_APPEND(self, tag, mailbox, flags, date, message):
+        mailbox = self._parseMbox(mailbox)
+        mbox = self.account.select(mailbox)
+        if not mbox:
+            self.sendNegativeResponse(tag, '[TRYCREATE] No such mailbox')
+            return
+
+        d = mbox.addMessage(message, flags, date)
+        d.addCallback(self.__cbAppend, tag, mbox)
+        d.addErrback(self.__ebAppend, tag)
+
+    auth_APPEND = (do_APPEND, arg_astring, opt_plist, opt_datetime,
+                   arg_literal)
+    select_APPEND = auth_APPEND
+
+    def __cbAppend(self, result, tag, mbox):
+        self.sendUntaggedResponse('%d EXISTS' % mbox.getMessageCount())
+        self.sendPositiveResponse(tag, 'APPEND complete')
+
+    def __ebAppend(self, failure, tag):
+        self.sendBadResponse(tag, 'APPEND failed: ' + str(failure.value))
+
+    def do_CHECK(self, tag):
+        d = self.checkpoint()
+        if d is None:
+            self.__cbCheck(None, tag)
+        else:
+            d.addCallbacks(
+                self.__cbCheck,
+                self.__ebCheck,
+                callbackArgs=(tag,),
+                errbackArgs=(tag,)
+            )
+    select_CHECK = (do_CHECK,)
+    
+    def __cbCheck(self, result, tag):
+        self.sendPositiveResponse(tag, 'CHECK completed')
+
+    def __ebCheck(self, failure, tag):
+        self.sendBadResponse(tag, 'CHECK failed: ' + str(failure.value))
+
+    def checkpoint(self):
+        """Called when the client issues a CHECK command.
+
+        This should perform any checkpoint operations required by the server.
+        It may be a long running operation, but may not block.  If it returns
+        a deferred, the client will only be informed of success (or failure)
+        when the deferred's callback (or errback) is invoked.
+        """
+        return None
+
+    def do_CLOSE(self, tag):
+        if self.mbox.isWriteable():
+            maybeDeferred(self.mbox.expunge).addCallbacks(
+                self.__cbClose, self.__ebClose, (tag,), None, (tag,), None
+            )
+        else:
+            self.sendPositiveResponse(tag, 'CLOSE completed')
+            self.mbox.removeListener(self)
+            self.mbox = None
+            self.state = 'auth'
+
+    select_CLOSE = (do_CLOSE,)
+
+    def __cbClose(self, result, tag):
+        self.sendPositiveResponse(tag, 'CLOSE completed')
+        self.mbox.removeListener(self)
+        self.mbox = None
+        self.state = 'auth'
+
+    def __ebClose(self, failure, tag):
+        self.sendBadResponse(tag, 'CLOSE failed: ' + str(failure.value))
+
+    def do_EXPUNGE(self, tag):
+        if self.mbox.isWriteable():
+            maybeDeferred(self.mbox.expunge).addCallbacks(
+                self.__cbExpunge, self.__ebExpunge, (tag,), None, (tag,), None
+            )
+        else:
+            self.sendPositiveResponse(tag, 'CLOSE completed')
+            self.mbox.removeListener(self)
+            self.mbox = None
+            self.state = 'auth'
+
+    select_EXPUNGE = (do_EXPUNGE,)
+
+    def __cbExpunge(self, result, tag):
+        for e in result:
+            self.sendUntaggedResponse('%d EXPUNGE' % e)
+        self.sendPositiveResponse(tag, 'EXPUNGE completed')
+        self.mbox.removeListener(self)
+        self.mbox = None
+        self.state = 'auth'
+
+    def __ebExpunge(self, failure, tag):
+        self.sendBadResponse(tag, 'EXPUNGE failed: ' + str(failure.value))
+        log.err(failure)
+
+    def do_SEARCH(self, tag, charset, query, uid=0):
+        maybeDeferred(self.mbox.search, query, uid=uid).addCallbacks(
+            self.__cbSearch, self.__ebSearch,
+            (tag, self.mbox, uid), None, (tag,), None
+        )
+
+    select_SEARCH = (do_SEARCH, opt_charset, arg_searchkeys)
+
+    def __cbSearch(self, result, tag, mbox, uid):
+        if uid:
+            result = map(mbox.getUID, result)
+        ids = ' '.join([str(i) for i in result])
+        self.sendUntaggedResponse('SEARCH ' + ids)
+        self.sendPositiveResponse(tag, 'SEARCH completed')
+
+    def __ebSearch(self, failure, tag):
+        self.sendBadResponse(tag, 'SEARCH failed: ' + str(failure.value))
+        log.err(failure)
+
+    def do_FETCH(self, tag, messages, query, uid=0):
+        if len(inspect.getargs(self.mbox.fetch.im_func.func_code)[0]) == 4:
+            import warnings
+            warnings.warn(
+                "Accepting the query argument in IMailbox.fetch() is deprecated.",
+                DeprecationWarning, stacklevel=2
+            )
+            maybeDeferred(self.mbox.fetch, messages, query, uid=uid).addCallbacks(
+                self.__cbFetch, self.__ebFetch,
+                (tag, query, uid), None, (tag,), None
+            )
+        else:
+            if 'bodystructure' in [p.type for p in query]:
+                self.sendNegativeResponse(tag, "BODYSTRUCTURE is totally unsupported, dude.")
+                return
+            maybeDeferred(self.mbox.fetch, messages, uid=uid).addCallbacks(
+                self.__cbFetch, self.__ebFetch,
+                (tag, query, uid), None, (tag,), None
+            )
+
+    select_FETCH = (do_FETCH, arg_seqset, arg_fetchatt)
+
+    def __cbFetch(self, results, tag, query, uid):
+        if isinstance(results, dict):
+            import warnings
+            warnings.warn(
+                "Returning a dict from imap4.IMailbox.fetch is deprecated.  "
+                "Return an iterable of IMessage implementors instead.",
+                DeprecationWarning, stacklevel=2
+            )
+            results = results.iteritems()
+            self._consumeFetchResponse(results, tag, query, uid)
+        else:
+            self._consumeMessageIterable(results, tag, query, uid)
+
+    def _consumeFetchResponse(self, results, tag, query, uid):
+        try:
+            msgId, parts = results.next()
+        except StopIteration:
+            self.sendPositiveResponse(tag, 'FETCH completed')
+        else:
+            # Case normalization
+            parts = dict([(k.lower(), v) for (k, v) in parts.iteritems()])
+            finalParts = []
+            sawUID = False
+            for p in query:
+                key = str(p).lower()
+                ukey = key.upper()
+                if ukey == 'UID':
+                    sawUID = True
+                try:
+                    finalParts.extend((ukey, parts[key]))
+                except KeyError:
+                    log.err("imap4.IMailbox.fetch() did not return %s response" % (key,))
+            if uid and not sawUID:
+                finalParts[:0] = ['UID', str(msgId)]
+            self.sendUntaggedResponse(
+                '%d FETCH %s' % (msgId, collapseNestedLists([finalParts]))
+            )
+            from twisted.internet import reactor
+            reactor.callLater(0, self._consumeFetchResponse, results, tag, query, uid)
+
+    def _consumeMessageIterable(self, results, tag, query, uid):
+        try:
+            msgId, msg = results.next()
+        except StopIteration:
+            self.sendPositiveResponse(tag, 'FETCH completed')
+        else:
+            self._sendMessageFetchResponse(msgId, msg, query, uid)
+            from twisted.internet import reactor
+            reactor.callLater(0, self._consumeMessageIterable, results, tag, query, uid)
+
+
+    def _sendMessageFetchResponse(self, msgId, msg, query, uid):
+        seenUID = False
+        response = []
+        for part in query:
+            if part.type == 'envelope':
+                response.extend(('ENVELOPE', getEnvelope(msg)))
+            elif part.type == 'flags':
+                response.extend(('FLAGS', msg.getFlags()))
+            elif part.type == 'internaldate':
+                response.extend(('INTERNALDATE', msg.getInternalDate()))
+            elif part.type == 'rfc822header':
+                hdrs = msg.getHeaders((), True)
+                hdrs = [': '.join((k, v)) for (k. v) in hdrs.iteritems()]
+                hdrs = '\r\n'.join(hdrs)
+                response.extend(('RFC822.HEADER', hdrs + '\r\n\r\n'))
+            elif part.type == 'rfc822text':
+                response.extend(('RFC822.TEXT', msg.getBodyFile()))
+            elif part.type == 'rfc822size':
+                response.extend(('RFC822.SIZE', str(msg.getSize())))
+            elif part.type == 'rfc822':
+                hdrs = msg.getHeaders((), True)
+                hdrs = [': '.join((k, v)) for (k. v) in hdrs.iteritems()]
+                hdrs.append('')
+                hdrs = '\r\n'.join(hdrs)
+                response.extend(('RFC822', hdrs + '\r\n' + msg.getBodyFile().read()))
+            elif part.type == 'uid':
+                seenUID = True
+                if uid:
+                    response.extend(('UID', str(msgId)))
+                else:
+                    response.extend(('UID', str(msg.getUID())))
+            elif part.type == 'bodystructure':
+                response.extend(('BODYSTRUCTURE', getBodyStructure(msg)))
+            elif part.type == 'body':
+                subMsg = msg
+                for p in part.part or ():
+                    subMsg = subMsg.getSubPart(p)
+                if part.header:
+                    if not part.header.fields:
+                        hdrs = msg.getHeaders((), True)
+                        hdrs = [': '.join((k, v)) for (k. v) in hdrs.iteritems()]
+                        hdrs = '\r\n'.join(hdrs)
+                        response.extend((str(part), hdrs + '\r\n\r\n'))
+                    else:
+                        hdrs = subMsg.getHeaders(part.header.fields, part.header.negate)
+                        hdrs = ['%s: %s' % x for x in hdrs.iteritems()]
+                        hdrs.append('')
+                        hdrs = '\r\n'.join(hdrs) + '\r\n'
+                        response.extend((str(part), hdrs))
+                elif part.text:
+                    response.extend((str(part), subMsg.getBodyFile()))
+                elif part.mime:
+                    hdrs = msg.getHeaders((), True)
+                    hdrs = [': '.join((k, v)) for (k. v) in hdrs.iteritems()]
+                    hdrs = '\r\n'.join(hdrs)
+                    response.extend((str(part), hdrs + '\r\n\r\n'))
+                else:
+                    # Simplified body request
+                    hdrs = msg.getHeaders((), True)
+                    hdrs = [': '.join((k, v)) for (k. v) in hdrs.iteritems()]
+                    hdrs.append('')
+                    hdrs = '\r\n'.join(hdrs)
+                    response.extend((str(part), hdrs + '\r\n' + subMsg.getBodyFile().read()))
+        if uid and not seenUID:
+            response[:0] = ['UID', str(msgId)]
+        self.sendUntaggedResponse("%d FETCH %s" % (msgId, collapseNestedLists([response])))
+
+    def __ebFetch(self, failure, tag):
+        log.err(failure)
+        self.sendBadResponse(tag, 'FETCH failed: ' + str(failure.value))
+
+    def do_STORE(self, tag, messages, mode, flags, uid=0):
+        mode = mode.upper()
+        silent = mode.endswith('SILENT')
+        if mode.startswith('+'):
+            mode = 1
+        elif mode.startswith('-'):
+            mode = -1
+        else:
+            mode = 0
+
+        maybeDeferred(self.mbox.store, messages, flags, mode, uid=uid).addCallbacks(
+            self.__cbStore, self.__ebStore, (tag, self.mbox, uid, silent), None, (tag,), None
+        )
+
+    select_STORE = (do_STORE, arg_seqset, arg_atom, arg_flaglist)
+
+    def __cbStore(self, result, tag, mbox, uid, silent):
+        if result and not silent:
+              for (k, v) in result.iteritems():
+                  if uid:
+                      uidstr = ' UID %d' % mbox.getUID(k)
+                  else:
+                      uidstr = ''
+                  self.sendUntaggedResponse('%d FETCH (FLAGS (%s)%s)' %
+                                            (k, ' '.join(v), uidstr))
+        self.sendPositiveResponse(tag, 'STORE completed')
+
+    def __ebStore(self, failure, tag):
+        self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
+
+    def do_COPY(self, tag, messages, mailbox, uid=0):
+        mailbox = self._parseMbox(mailbox)
+        mbox = self.account.select(mailbox)
+        if not mbox:
+            self.sendNegativeResponse(tag, 'No such mailbox: ' + mailbox)
+        else:
+            maybeDeferred(
+                self.mbox.fetch, messages,
+                ['BODY', [], 'INTERNALDATE', 'FLAGS'],
+                uid=uid
+            ).addCallbacks(
+                self.__cbCopy, self.__ebCopy, (tag, mbox), None, (tag,),
+            )
+
+    select_COPY = (do_COPY, arg_seqset, arg_astring)
+
+    def __cbCopy(self, messages, tag, mbox):
+        # XXX - This should handle failures with a rollback or something
+        addedDeferreds = []
+        addedIDs = []
+        failures = []
+        for (id, msg) in messages.iteritems():
+            body = msg['BODY']
+            flags = msg['FLAGS']
+            date = msg['INTERNALDATE']
+            try:
+                d = mbox.addMessage(body, flags, date)
+            except Exception, e:
+                failures.append(e)
+            else:
+                if isinstance(d, defer.Deferred):
+                    addedDeferreds.append(d)
+                else:
+                    addedIDs.append(d)
+        d = defer.DeferredList(addedDeferreds)
+        d.addCallback(self.__cbCopied, addedIDs, failures, tag, mbox)
+
+    def __cbCopied(self, deferredIds, ids, failures, tag, mbox):
+        for (result, status) in deferredIds:
+            if status:
+                ids.append(result)
+            else:
+                failures.append(result.value)
+        if failures:
+            self.sendNegativeResponse(tag, '[ALERT] Some messages were not copied')
+        else:
+            self.sendPositiveResponse(tag, 'COPY completed')
+    
+    def __ebCopy(self, failure, tag):
+        self.sendBadResponse(tag, 'COPY failed')
+        log.err(failure)
+
+    def do_UID(self, tag, command, line):
+        command = command.upper()
+
+        if command not in ('COPY', 'FETCH', 'STORE', 'SEARCH'):
+            raise IllegalClientResponse(command)
+
+        self.dispatchCommand(tag, command, line, uid=1)
+
+    select_UID = (do_UID, arg_atom, arg_line)
+    #
+    # IMailboxListener implementation
+    # 
+    def modeChanged(self, writeable):
+        if writeable:
+            self.sendPositiveResponse(message='[READ-WRITE]')
+        else:
+            self.sendPositiveResponse(message='[READ-ONLY]')
+
+    def flagsChanged(self, newFlags):
+        for (mId, flags) in newFlags.iteritems():
+            self.sendUntaggedResponse('%d FETCH (FLAGS (%s))' % (mId, ' '.join(flags)))
+
+    def newMessages(self, exists, recent):
+        if exists is not None:
+            self.sendUntaggedResponse('%d EXISTS' % exists)
+        if recent is not None:
+            self.sendUntaggedResponse('%d RECENT' % recent)
+
+class UnhandledResponse(IMAP4Exception): pass
+
+class NegativeResponse(IMAP4Exception): pass
+
+class NoSupportedAuthentication(IMAP4Exception):
+    def __init__(self, serverSupports, clientSupports):
+        IMAP4Exception.__init__(self, 'No supported authentication schemes available')
+        self.serverSupports = serverSupports
+        self.clientSupports = clientSupports
+
+class IllegalServerResponse(IMAP4Exception): pass
+
+class IMAP4Client(basic.LineReceiver):
+    """IMAP4 client protocol implementation
+    
+    @ivar state: A string representing the state the connection is currently
+    in.
+    """
+    __implements__ = (IMailboxListener,)
+
+    tags = None
+    waiting = None
+    queued = None
+    tagID = 1
+    state = None
+    
+    # Capabilities are not allowed to change during the session
+    # So cache the first response and use that for all later
+    # lookups
+    _capCache = None
+
+    _memoryFileLimit = 1024 * 1024 * 10
+
+    # Authentication is pluggable.  This maps names to IClientAuthentication
+    # objects.
+    authenticators = None
+
+    STATUS_CODES = ('OK', 'NO', 'BAD', 'PREAUTH', 'BYE')
+
+    STATUS_TRANSFORMATIONS = {
+        'MESSAGES': int, 'RECENT': int, 'UNSEEN': int
+    }
+
+    context = None
+
+    def __init__(self, contextFactory = None):
+        self.tags = {}
+        self.queued = []
+        self.authenticators = {}
+        self.context = contextFactory
+        
+        self._tag = None
+        self._parts = None
+
+    def registerAuthenticator(self, auth):
+        """Register a new form of authentication
+
+        When invoking the authenticate() method of IMAP4Client, the first
+        matching authentication scheme found will be used.  The ordering is
+        that in which the server lists support authentication schemes.
+
+        @type auth: Implementor of C{IClientAuthentication}
+        @param auth: The object to use to perform the client
+        side of this authentication scheme.
+        """
+        self.authenticators[auth.getName().upper()] = auth
+
+    def rawDataReceived(self, data):
+        self._pendingSize -= len(data)
+        if self._pendingSize > 0:
+            self._pendingBuffer.write(data)
+        else:
+            passon = ''
+            if self._pendingSize < 0:
+                data, passon = data[:self._pendingSize], data[self._pendingSize:]
+            self._pendingBuffer.write(data)
+            rest = self._pendingBuffer
+            self._pendingBuffer = None
+            self._pendingSize = None
+            rest.seek(0, 0)
+            self._parts.append(rest.read())
+            self.setLineMode(passon.lstrip('\r\n'))
+
+    def lineReceived(self, line):
+        # print 'C: ' + line
+        if self._parts is None:
+            lastPart = line.rfind(' ')
+            if lastPart != -1:
+                lastPart = line[lastPart + 1:]
+                if lastPart.startswith('{') and lastPart.endswith('}'):
+                    # It's a literal a-comin' in
+                    try:
+                        octets = int(lastPart[1:-1])
+                    except ValueError:
+                        raise IllegalServerResponse(line)
+                    self._tag, parts = line.split(None, 1)
+                    self._setupForLiteral(parts, octets)
+                    return
+                else:
+                    # It isn't a literal at all
+                    self._regularDispatch(line)
+            else:
+                self._regularDispatch(line)
+        else:
+            # If an expression is in progress, no tag is required here
+            # Since we didn't find a literal indicator, this expression
+            # is done.
+            self._parts.append(line)
+            tag, rest = self._tag, ''.join(self._parts)
+            self._tag = self._parts = None
+            self.dispatchCommand(tag, rest)
+
+    def _regularDispatch(self, line):
+        parts = line.split(None, 1)
+        if len(parts) != 2:
+            raise IllegalServerResponse, line
+        tag, rest = parts
+        self.dispatchCommand(tag, rest)
+
+    def _setupForLiteral(self, rest, octets):
+        self._pendingBuffer = self.messageFile(octets)
+        self._pendingSize = octets
+        self._parts = [rest, '\r\n']
+        self.setRawMode()
+
+    def messageFile(self, octets):
+        """Create a file to which an incoming message may be written.
+        
+        @type octets: C{int}
+        @param octets: The number of octets which will be written to the file
+        
+        @rtype: Any object which implements C{write(string)} and
+        C{seek(int, int)}
+        @return: A file-like object
+        """
+        if octets > self._memoryFileLimit:
+            return tempfile.TemporaryFile()
+        else:
+            return StringIO.StringIO()
+
+    def makeTag(self):
+        tag = '%0.4X' % self.tagID
+        self.tagID += 1
+        return tag
+
+    def dispatchCommand(self, tag, rest):
+        if self.state is None:
+            f = self.response_UNAUTH
+        else:
+            f = getattr(self, 'response_' + self.state.upper(), None)
+        if f:
+            try:
+                f(tag, rest)
+            except:
+                log.err()
+                self.transport.loseConnection()
+        else:
+            log.err("Cannot dispatch: %s, %s, %s" % (self.state, tag, rest))
+            self.transport.loseConnection()
+
+    def response_UNAUTH(self, tag, rest):
+        if self.state is None:
+            # Server greeting, this is
+            status, rest = rest.split(None, 1)
+            if status.upper() == 'OK':
+                self.state = 'unauth'
+            elif status.upper() == 'PREAUTH':
+                self.state = 'auth'
+            else:
+                # XXX - This is rude.
+                self.transport.loseConnection()
+                raise IllegalServerResponse(tag + ' ' + rest)
+        else:
+            self._defaultHandler(tag, rest)
+
+    def response_AUTH(self, tag, rest):
+        self._defaultHandler(tag, rest)
+
+    def _defaultHandler(self, tag, rest):
+        if tag == '*' or tag == '+':
+            if not self.waiting:
+                self._extraInfo([rest])
+            else:
+                cmd = self.tags[self.waiting]
+                if tag == '+':
+                    cmd.continuation.callback(rest)
+                else:
+                    cmd.lines.append(rest)
+        else:
+            try:
+                cmd = self.tags[tag]
+            except KeyError:
+                # XXX - This is rude.
+                self.transport.loseConnection()
+                raise IllegalServerResponse(tag + ' ' + rest)
+            else:
+                status, line = rest.split(None, 1)
+                if status == 'OK':
+                    # Give them this last line, too
+                    cmd.finish(rest, self._extraInfo)
+                else:
+                    cmd.defer.errback(IMAP4Exception(line))
+                del self.tags[tag]
+                self.waiting = None
+                self._flushQueue()
+
+    def _flushQueue(self):
+        if self.queued:
+            cmd = self.queued.pop(0)
+            t = self.makeTag()
+            self.tags[t] = cmd
+            self.sendLine(' '.join((t, cmd.command, cmd.args)))
+            self.waiting = t
+    
+    def _extraInfo(self, lines):
+        # XXX - This is terrible.
+        # XXX - Also, this should collapse temporally proximate calls into single
+        #       invocations of IMailboxListener methods, where possible.
+        flags = {}
+        for L in lines:
+            if L.find('EXISTS') != -1:
+                self.newMessages(int(L.split()[0]), None)
+            elif L.find('RECENT') != -1:
+                self.newMessages(None, int(L.split()[0]))
+            elif L.find('READ-ONLY') != -1:
+                self.modeChanged(0)
+            elif L.find('READ-WRITE') != -1:
+                self.modeChanged(1)
+            elif L.find('FETCH') != -1:
+                for (mId, fetched) in self.__cbFetch(([L], None)).iteritems():
+                    sum = []
+                    for f in fetched.get('FLAGS', []):
+                        sum.append(f)
+                    flags.setdefault(mId, []).extend(sum)
+            else:
+                log.msg('Unhandled unsolicited response: ' + repr(L))
+        if flags:
+            self.flagsChanged(flags)
+
+    def sendCommand(self, cmd):
+        cmd.defer = defer.Deferred()
+        if self.waiting:
+            self.queued.append(cmd)
+            return cmd.defer
+        t = self.makeTag()
+        self.tags[t] = cmd
+        self.sendLine(' '.join((t, cmd.command, cmd.args)))
+        self.waiting = t
+        return cmd.defer
+
+    def getCapabilities(self, useCache=1):
+        """Request the capabilities available on this server.
+
+        This command is allowed in any state of connection.
+
+        @type useCache: C{bool}
+        @param useCache: Specify whether to use the capability-cache or to
+        re-retrieve the capabilities from the server.  Server capabilities
+        should never change, so for normal use, this flag should never be
+        false.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback will be invoked with a
+        dictionary mapping capability types to lists of supported
+        mechanisms, or to None if a support list is not applicable.
+        """
+        if useCache and self._capCache is not None:
+            return defer.succeed(self._capCache)
+        cmd = 'CAPABILITY'
+        args = ''
+        resp = ('CAPABILITY',)
+        d = self.sendCommand(Command(cmd, args, wantResponse=resp))
+        d.addCallback(self.__cbCapabilities)
+        return d
+
+    def __cbCapabilities(self, (lines, tagline)):
+        caps = {}
+        for rest in lines:
+            rest = rest.split()[1:]
+            for cap in rest:
+                eq = cap.find('=')
+                if eq == -1:
+                    caps[cap] = None
+                else:
+                    caps.setdefault(cap[:eq], []).append(cap[eq+1:])
+        self._capCache = caps
+        return caps
+
+    def logout(self):
+        """Inform the server that we are done with the connection.
+
+        This command is allowed in any state of connection.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback will be invoked with None
+        when the proper server acknowledgement has been received.
+        """
+        d = self.sendCommand(Command('LOGOUT', wantResponse=('BYE',)))
+        d.addCallback(self.__cbLogout)
+        return d
+
+    def __cbLogout(self, (lines, tagline)):
+        self.transport.loseConnection()
+        # We don't particularly care what the server said
+        return None
+
+
+    def noop(self):
+        """Perform no operation.
+
+        This command is allowed in any state of connection.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback will be invoked with a list
+        of untagged status updates the server responds with.
+        """
+        d = self.sendCommand(Command('NOOP'))
+        d.addCallback(self.__cbNoop)
+        return d
+
+    def __cbNoop(self, (lines, tagline)):
+        # Conceivable, this is elidable.
+        # It is, afterall, a no-op.
+        return lines
+
+
+    def authenticate(self, secret):
+        """Attempt to enter the authenticated state with the server
+
+        This command is allowed in the Non-Authenticated state.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked if the authentication
+        succeeds and whose errback will be invoked otherwise.
+        """
+        def getAuthMethods(caps):
+            return caps.get('AUTH', [])
+        if self._capCache is None:
+            d = self.getCapabilities()
+        else:
+            d = defer.succeed(self._capCache)
+        d.addCallback(getAuthMethods).addCallback(self.__cbAuthenticate, secret)
+        return d
+
+    def __cbAuthenticate(self, auths, secret):
+        for scheme in auths:
+            if scheme.upper() in self.authenticators:
+                break
+        else:
+            raise NoSupportedAuthentication(auths, self.authenticators.keys())
+
+        continuation = defer.Deferred()
+        continuation.addCallback(self.__cbContinueAuth, scheme, secret)
+        d = self.sendCommand(Command('AUTHENTICATE', scheme, continuation))
+        d.addCallback(self.__cbAuth)
+        return d
+
+    def __cbContinueAuth(self, rest, scheme, secret):
+        try:
+            chal = base64.decodestring(rest + '\n')
+        except binascii.Error:
+            # XXX - Uh
+            self.transport.loseConnection()
+        else:
+            auth = self.authenticators[scheme]
+            chal = auth.challengeResponse(secret, chal)
+            self.sendLine(base64.encodestring(chal))
+
+    def __cbAuth(self, *args, **kw):
+        return None
+
+    def login(self, username, password):
+        """Authenticate with the server using a username and password
+
+        This command is allowed in the Non-Authenticated state.  If the
+        server supports the STARTTLS capability and our transport support
+        TLS, TLS is negotiated before the login command is issued.
+
+        @type username: C{str}
+        @param username: The username to log in with
+
+        @type password: C{str}
+        @param password: The password to log in with
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked if login is successful
+        and whose errback is invoked otherwise.
+        """
+        d = maybeDeferred(self.getCapabilities)
+        d.addCallbacks(
+            self.__cbLoginCaps,
+            self.__ebLoginCaps,
+            callbackArgs=(username, password),
+        )
+        return d
+
+    def _getContextFactory(self):
+        if self.context is not None:
+            return self.context
+        try:
+            from twisted.internet import ssl
+        except ImportError:
+            return None
+        else:
+            context = ssl.ClientContextFactory()
+            context.method = ssl.SSL.TLSv1_METHOD
+            return context
+
+    def __cbLoginCaps(self, capabilities, username, password):
+        tryTLS = 'STARTTLS' in capabilities and implements(self.transport, ITLSTransport)
+        if tryTLS:
+            ctx = self._getContextFactory()
+            if ctx:
+                d = self.sendCommand(Command('STARTTLS'))
+                d.addCallbacks(
+                    self.__cbLoginTLS,
+                    self.__ebLoginTLS,
+                    callbackArgs=(username, password, ctx),
+                )
+                return d
+            else:
+                log.err("Server wants us to use TLS, but we don't have "
+                        "a Context Factory!")
+        else:
+            log.msg("Server has no TLS support. logging in over cleartext!")
+            args = ' '.join((username, password))
+            return self.sendCommand(Command('LOGIN', args))
+
+    def __ebLoginCaps(self, failure):
+        log.err(failure)
+        return failure
+    
+    def __cbLoginTLS(self, result, username, password, context):
+        self.transport.startTLS(context)
+        self.context = context
+        # Flush capability cache now!
+        self._capCache = None
+        args = ' '.join((username, password))
+        return self.sendCommand(Command('LOGIN', args))
+        
+    def __ebLoginTLS(self, failure):
+        log.err(failure)
+        return failure
+
+    def select(self, mailbox):
+        """Select a mailbox
+
+        This command is allowed in the Authenticated and Selected states.
+
+        @type mailbox: C{str}
+        @param mailbox: The name of the mailbox to select
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked with mailbox
+        information if the select is successful and whose errback is
+        invoked otherwise.
+        """
+        cmd = 'SELECT'
+        args = mailbox.encode('imap4-utf-7')
+        resp = ('FLAGS', 'EXISTS', 'RECENT', 'UNSEEN', 'PERMANENTFLAGS', 'UIDVALIDITY')
+        d = self.sendCommand(Command(cmd, args, wantResponse=resp))
+        d.addCallback(self.__cbSelect, 1)
+        return d
+
+    def examine(self, mailbox):
+        """Select a mailbox in read-only mode
+
+        This command is allowed in the Authenticated and Selected states.
+
+        @type mailbox: C{str}
+        @param mailbox: The name of the mailbox to examine
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked with mailbox
+        information if the examine is successful and whose errback
+        is invoked otherwise.
+        """
+        cmd = 'EXAMINE'
+        args = mailbox.encode('imap4-utf-7')
+        resp = ('FLAGS', 'EXISTS', 'RECENT', 'UNSEEN', 'PERMANENTFLAGS', 'UIDVALIDITY')
+        d = self.sendCommand(Command(cmd, args, wantResponse=resp))
+        d.addCallback(self.__cbSelect, 0)
+        return d
+
+    def __cbSelect(self, (lines, tagline), rw):
+        # In the absense of specification, we are free to assume:
+        #   READ-WRITE access
+        datum = {'READ-WRITE': rw}
+        lines.append(tagline)
+        for parts in lines:
+            split = parts.split()
+            if len(split) == 2:
+                if split[1].upper().strip() == 'EXISTS':
+                    try:
+                        datum['EXISTS'] = int(split[0])
+                    except ValueError:
+                        raise IllegalServerResponse(parts)
+                elif split[1].upper().strip() == 'RECENT':
+                    try:
+                        datum['RECENT'] = int(split[0])
+                    except ValueError:
+                        raise IllegalServerResponse(parts)
+                else:
+                    log.err('Unhandled SELECT response (1): ' + parts)
+            elif split[0].upper().strip() == 'FLAGS':
+                split = parts.split(None, 1)
+                datum['FLAGS'] = tuple(parseNestedParens(split[1])[0])
+            elif split[0].upper().strip() == 'OK':
+                begin = parts.find('[')
+                end = parts.find(']')
+                if begin == -1 or end == -1:
+                    raise IllegalServerResponse(parts)
+                else:
+                    content = parts[begin+1:end].split(None, 1)
+                    if len(content) >= 1:
+                        key = content[0].upper()
+                        if key == 'READ-ONLY':
+                            datum['READ-WRITE'] = 0
+                        elif key == 'READ-WRITE':
+                            datum['READ-WRITE'] = 1
+                        elif key == 'UIDVALIDITY':
+                            try:
+                                datum['UIDVALIDITY'] = int(content[1])
+                            except ValueError:
+                                raise IllegalServerResponse(parts)
+                        elif key == 'UNSEEN':
+                            try:
+                                datum['UNSEEN'] = int(content[1])
+                            except ValueError:
+                                raise IllegalServerResponse(parts)
+                        elif key == 'PERMANENTFLAGS':
+                            datum['PERMANENTFLAGS'] = tuple(parseNestedParens(content[1])[0])
+                        else:
+                            log.err('Unhandled SELECT response (2): ' + parts)
+                    else:
+                        log.err('Unhandled SELECT response (3): ' + parts)
+            else:
+                log.err('Unhandled SELECT response (4): ' + parts)
+        return datum
+
+    def create(self, name):
+        """Create a new mailbox on the server
+
+        This command is allowed in the Authenticated and Selected states.
+
+        @type name: C{str}
+        @param name: The name of the mailbox to create.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked if the mailbox creation
+        is successful and whose errback is invoked otherwise.
+        """
+        return self.sendCommand(Command('CREATE', name.encode('imap4-utf-7')))
+
+    def delete(self, name):
+        """Delete a mailbox
+
+        This command is allowed in the Authenticated and Selected states.
+
+        @type name: C{str}
+        @param name: The name of the mailbox to delete.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose calblack is invoked if the mailbox is
+        deleted successfully and whose errback is invoked otherwise.
+        """
+        return self.sendCommand(Command('DELETE', name.encode('imap4-utf-7')))
+
+    def rename(self, oldname, newname):
+        """Rename a mailbox
+
+        This command is allowed in the Authenticated and Selected states.
+
+        @type oldname: C{str}
+        @param oldname: The current name of the mailbox to rename.
+
+        @type newname: C{str}
+        @param newname: The new name to give the mailbox.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked if the rename is
+        successful and whose errback is invoked otherwise.
+        """
+        oldname = oldname.encode('imap4-utf-7')
+        newname = newname.encode('imap4-utf-7')
+        return self.sendCommand(Command('RENAME', ' '.join((oldname, newname))))
+
+    def subscribe(self, name):
+        """Add a mailbox to the subscription list
+
+        This command is allowed in the Authenticated and Selected states.
+
+        @type name: C{str}
+        @param name: The mailbox to mark as 'active' or 'subscribed'
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked if the subscription
+        is successful and whose errback is invoked otherwise.
+        """
+        return self.sendCommand(Command('SUBSCRIBE', name.encode('imap4-utf-7')))
+
+    def unsubscribe(self, name):
+        """Remove a mailbox from the subscription list
+
+        This command is allowed in the Authenticated and Selected states.
+
+        @type name: C{str}
+        @param name: The mailbox to unsubscribe
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked if the unsubscription
+        is successful and whose errback is invoked otherwise.
+        """
+        return self.sendCommand(Command('UNSUBSCRIBE', name.encode('imap4-utf-7')))
+
+    def list(self, reference, wildcard):
+        """List a subset of the available mailboxes
+
+        This command is allowed in the Authenticated and Selected states.
+
+        @type reference: C{str}
+        @param reference: The context in which to interpret C{wildcard}
+
+        @type wildcard: C{str}
+        @param wildcard: The pattern of mailbox names to match, optionally
+        including either or both of the '*' and '%' wildcards.  '*' will
+        match zero or more characters and cross hierarchical boundaries.
+        '%' will also match zero or more characters, but is limited to a
+        single hierarchical level.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked with a list of C{tuple}s,
+        the first element of which is a C{tuple} of mailbox flags, the second
+        element of which is the hierarchy delimiter for this mailbox, and the
+        third of which is the mailbox name; if the command is unsuccessful,
+        the deferred's errback is invoked instead.
+        """
+        cmd = 'LIST'
+        args = '"%s" "%s"' % (reference, wildcard.encode('imap4-utf-7'))
+        resp = ('LIST',)
+        d = self.sendCommand(Command(cmd, args, wantResponse=resp))
+        d.addCallback(self.__cbList, 'LIST')
+        return d
+
+    def lsub(self, reference, wildcard):
+        """List a subset of the subscribed available mailboxes
+
+        This command is allowed in the Authenticated and Selected states.
+
+        The parameters and returned object are the same as for the C{list}
+        method, with one slight difference: Only mailboxes which have been
+        subscribed can be included in the resulting list.
+        """
+        cmd = 'LSUB'
+        args = '"%s" "%s"' % (reference, wildcard.encode('imap4-utf-7'))
+        resp = ('LSUB',)
+        d = self.sendCommand(Command(cmd, args, wantResponse=resp))
+        d.addCallback(self.__cbList, 'LSUB')
+        return d
+
+    def __cbList(self, (lines, last), command):
+        results = []
+        for L in lines:
+            parts = parseNestedParens(L)
+            if len(parts) != 4:
+                raise IllegalServerResponse, L
+            if parts[0] == command:
+                parts[1] = tuple(parts[1])
+                results.append(tuple(parts[1:]))
+        return results
+
+    def status(self, mailbox, *names):
+        """Retrieve the status of the given mailbox
+
+        This command is allowed in the Authenticated and Selected states.
+
+        @type mailbox: C{str}
+        @param mailbox: The name of the mailbox to query
+
+        @type names: C{str}
+        @param names: The status names to query.  These may be any number of:
+        MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, and UNSEEN.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked with the status information
+        if the command is successful and whose errback is invoked otherwise.
+        """
+        cmd = 'STATUS'
+        args = "%s (%s)" % (mailbox.encode('imap4-utf-7'), ' '.join(names))
+        resp = ('STATUS',)
+        d = self.sendCommand(Command(cmd, args, resp))
+        d.addCallback(self.__cbStatus)
+        return d
+
+    def __cbStatus(self, (lines, last)):
+        status = {}
+        for line in lines:
+            parts = parseNestedParens(line)
+            if parts[0] == 'STATUS':
+                items = parts[2]
+                items = [items[i:i+2] for i in range(0, len(items), 2)]
+                status.update(dict(items))
+        for k in status.keys():
+            t = self.STATUS_TRANSFORMATIONS.get(k)
+            if t:
+                try:
+                    status[k] = t(status[k])
+                except Exception, e:
+                    raise IllegalServerResponse('(%s %s): %s' % (k, status[k], str(e)))
+        return status
+
+    def append(self, mailbox, message, flags = (), date = None):
+        """Add the given message to the currently selected mailbox
+
+        This command is allowed in the Authenticated and Selected states.
+
+        @type mailbox: C{str}
+        @param mailbox: The mailbox to which to add this message.
+
+        @type message: Any file-like object
+        @param message: The message to add, in RFC822 format.
+
+        @type flags: Any iterable of C{str}
+        @param flags: The flags to associated with this message.
+
+        @type date: C{str}
+        @param date: The date to associate with this message.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked when this command
+        succeeds or whose errback is invoked if it fails.
+        """
+        message.seek(0, 2)
+        L = message.tell()
+        message.seek(0, 0)
+        fmt = '%s (%s)%s%s {%d}'
+        if date:
+            date = '"%s"' % date
+        cmd = fmt % (
+            mailbox.encode('imap4-utf-7'), ' '.join(flags),
+            date and ' ' or '', date, L
+        )
+        continuation = defer.Deferred()
+        continuation.addCallback(self.__cbContinueAppend, message)
+        continuation.addCallback(self.__cbFinishAppend)
+        continuation.addErrback(self.__ebContinueAppend)
+        d = self.sendCommand(Command('APPEND', cmd, continuation))
+        d.addCallback(self.__cbAppend)
+        return d
+
+    def __cbContinueAppend(self, lines, message):
+        s = basic.FileSender()
+        return s.beginFileTransfer(message, self.transport, lambda x: x)
+
+    def __cbFinishAppend(self, foo):
+        self.sendLine('')
+
+    def __ebContinueAppend(self, failure):
+        log.err()
+        return failure
+
+    def __cbAppend(self, result):
+        return None
+
+    def check(self):
+        """Tell the server to perform a checkpoint
+
+        This command is allowed in the Selected state.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked when this command
+        succeeds or whose errback is invoked if it fails.
+        """
+        return self.sendCommand(Command('CHECK'))
+
+    def close(self):
+        """Return the connection to the Authenticated state.
+
+        This command is allowed in the Selected state.
+
+        Issuing this command will also remove all messages flagged \\Deleted
+        from the selected mailbox if it is opened in read-write mode,
+        otherwise it indicates success by no messages are removed.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked when the command
+        completes successfully or whose errback is invoked if it fails.
+        """
+        return self.sendCommand(Command('CLOSE'))
+
+    def expunge(self):
+        """Return the connection to the Authenticate state.
+
+        This command is allowed in the Selected state.
+
+        Issuing this command will perform the same actions as issuing the
+        close command, but will also generate an 'expunge' response for
+        every message deleted.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked with a list of the
+        'expunge' responses when this command is successful or whose errback
+        is invoked otherwise.
+        """
+        cmd = 'EXPUNGE'
+        resp = ('EXPUNGE',)
+        d = self.sendCommand(Command(cmd, wantResponse=resp))
+        d.addCallback(self.__cbExpunge)
+        return d
+
+    def __cbExpunge(self, (lines, last)):
+        ids = []
+        for line in lines:
+            parts = line.split(None, 1)
+            if len(parts) == 2:
+                if parts[1] == 'EXPUNGE':
+                    try:
+                        ids.append(int(parts[0]))
+                    except ValueError:
+                        raise IllegalServerResponse, line
+        return ids
+
+    def search(self, *queries, **kwarg):
+        """Search messages in the currently selected mailbox
+
+        This command is allowed in the Selected state.
+
+        Any non-zero number of queries are accepted by this method, as
+        returned by the C{Query}, C{Or}, and C{Not} functions.
+        
+        One keyword argument is accepted: if uid is passed in with a non-zero
+        value, the server is asked to return message UIDs instead of message
+        sequence numbers.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback will be invoked with a list of all
+        the message sequence numbers return by the search, or whose errback
+        will be invoked if there is an error.
+        """
+        if kwarg.get('uid'):
+            cmd = 'UID SEARCH'
+        else:
+            cmd = 'SEARCH'
+        args = ' '.join(queries)
+        d = self.sendCommand(Command(cmd, args, wantResponse=(cmd,)))
+        d.addCallback(self.__cbSearch)
+        return d
+
+    def __cbSearch(self, (lines, end)):
+        ids = []
+        for line in lines:
+            parts = line.split(None, 1)
+            if len(parts) == 2:
+                if parts[0] == 'SEARCH':
+                    try:
+                        ids.extend(map(int, parts[1].split()))
+                    except ValueError:
+                        raise IllegalServerResponse, line
+        return ids
+
+    def fetchUID(self, messages):
+        """Retrieve the unique identifier for one or more messages
+
+        This command is allowed in the Selected state.
+
+        @type messages: C{str}
+        @param messages: A message sequence set
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked with a dict mapping
+        message sequence numbers to unique message identifiers, or whose
+        errback is invoked if there is an error.
+        """
+        d = self._fetch(messages, useUID=0, uid=1)
+        d.addCallback(self.__cbFetch)
+        return d
+
+    def fetchFlags(self, messages, uid=0):
+        """Retrieve the flags for one or more messages
+
+        This command is allowed in the Selected state.
+
+        @type messages: C{MessageSet}
+        @param messages: The messages for which to retrieve flags.
+
+        @type uid: C{bool}
+        @param uid: Indicates whether the message sequence set is of message
+        numbers or of unique message IDs.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked with a dict mapping
+        message numbers to lists of flags, or whose errback is invoked if
+        there is an error.
+        """
+        d = self._fetch(str(messages), useUID=uid, flags=1)
+        d.addCallback(self.__cbFetch)
+        return d
+
+    def fetchInternalDate(self, messages, uid=0):
+        """Retrieve the internal date associated with one or more messages
+
+        This command is allowed in the Selected state.
+
+        @type messages: C{MessageSet}
+        @param messages: The messages for which to retrieve the internal date.
+
+        @type uid: C{bool}
+        @param uid: Indicates whether the message sequence set is of message
+        numbers or of unique message IDs.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked with a dict mapping
+        message numbers to date strings, or whose errback is invoked
+        if there is an error.  Date strings take the format of 
+        \"<day-month-year time timezone\".
+        """
+        d = self._fetch(str(messages), useUID=uid, internaldate=1)
+        d.addCallback(self.__cbFetch)
+        return d
+
+    def fetchEnvelope(self, messages, uid=0):
+        """Retrieve the envelope data for one or more messages
+
+        This command is allowed in the Selected state.
+
+        @type messages: C{MessageSet}
+        @param messages: The messages for which to retrieve envelope data.
+
+        @type uid: C{bool}
+        @param uid: Indicates whether the message sequence set is of message
+        numbers or of unique message IDs.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked with a dict mapping
+        message numbers to envelope data, or whose errback is invoked
+        if there is an error.  Envelope data consists of a sequence of the
+        date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to,
+        and message-id header fields.  The date, subject, in-reply-to, and
+        message-id fields are strings, while the from, sender, reply-to,
+        to, cc, and bcc fields contain address data.  Address data consists
+        of a sequence of name, source route, mailbox name, and hostname.
+        Fields which are not present for a particular address may be C{None}.
+        """
+        d = self._fetch(str(messages), useUID=uid, envelope=1)
+        d.addCallback(self.__cbFetch)
+        return d
+
+    def fetchBodyStructure(self, messages, uid=0):
+        """Retrieve the structure of the body of one or more messages
+
+        This command is allowed in the Selected state.
+
+        @type messages: C{MessageSet}
+        @param messages: The messages for which to retrieve body structure
+        data.
+
+        @type uid: C{bool}
+        @param uid: Indicates whether the message sequence set is of message
+        numbers or of unique message IDs.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked with a dict mapping
+        message numbers to body structure data, or whose errback is invoked
+        if there is an error.  Body structure data describes the MIME-IMB
+        format of a message and consists of a sequence of mime type, mime
+        subtype, parameters, content id, description, encoding, and size. 
+        The fields following the size field are variable: if the mime
+        type/subtype is message/rfc822, the contained message's envelope
+        information, body structure data, and number of lines of text; if
+        the mime type is text, the number of lines of text.  Extension fields
+        may also be included; if present, they are: the MD5 hash of the body,
+        body disposition, body language.
+        """
+        d = self._fetch(messages, useUID=uid, bodystructure=1)
+        d.addCallback(self.__cbFetch)
+        return d
+
+    def fetchSimplifiedBody(self, messages, uid=0):
+        """Retrieve the simplified body structure of one or more messages
+
+        This command is allowed in the Selected state.
+
+        @type messages: C{str}
+        @param messages: A message sequence set
+
+        @type uid: C{bool}
+        @param uid: Indicates whether the message sequence set is of message
+        numbers or of unique message IDs.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked with a dict mapping
+        message numbers to body data, or whose errback is invoked
+        if there is an error.  The simplified body structure is the same
+        as the body structure, except that extension fields will never be
+        present.
+        """
+        d = self._fetch(messages, useUID=uid, body=1)
+        d.addCallback(self.__cbFetch)
+        return d
+
+    def fetchMessage(self, messages, uid=0):
+        """Retrieve one or more entire messages
+
+        This command is allowed in the Selected state.
+
+        @type messages: C{str}
+        @param messages: A message sequence set
+
+        @type uid: C{bool}
+        @param uid: Indicates whether the message sequence set is of message
+        numbers or of unique message IDs.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked with a dict mapping
+        message numbers to message objects, or whose errback is invoked
+        if there is an error.
+        """
+        d = self._fetch(messages, useUID=uid, rfc822=1)
+        d.addCallback(self.__cbFetch)
+        return d
+
+    def fetchHeaders(self, messages, uid=0):
+        """Retrieve headers of one or more messages
+
+        This command is allowed in the Selected state.
+
+        @type messages: C{str}
+        @param messages: A message sequence set
+
+        @type uid: C{bool}
+        @param uid: Indicates whether the message sequence set is of message
+        numbers or of unique message IDs.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked with a dict mapping
+        message numbers to dicts of message headers, or whose errback is
+        invoked if there is an error.
+        """
+        d = self._fetch(messages, useUID=uid, rfc822header=1)
+        d.addCallback(self.__cbFetch)
+        return d
+
+    def fetchBody(self, messages, uid=0):
+        """Retrieve body text of one or more messages
+
+        This command is allowed in the Selected state.
+
+        @type messages: C{str}
+        @param messages: A message sequence set
+
+        @type uid: C{bool}
+        @param uid: Indicates whether the message sequence set is of message
+        numbers or of unique message IDs.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked with a dict mapping
+        message numbers to file-like objects containing body text, or whose
+        errback is invoked if there is an error.
+        """
+        d = self._fetch(messages, useUID=uid, rfc822text=1)
+        d.addCallback(self.__cbFetch)
+        return d
+
+    def fetchSize(self, messages, uid=0):
+        """Retrieve the size, in octets, of one or more messages
+
+        This command is allowed in the Selected state.
+
+        @type messages: C{str}
+        @param messages: A message sequence set
+
+        @type uid: C{bool}
+        @param uid: Indicates whether the message sequence set is of message
+        numbers or of unique message IDs.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked with a dict mapping
+        message numbers to sizes, or whose errback is invoked if there is
+        an error.
+        """
+        d = self._fetch(messages, useUID=uid, rfc822size=1)
+        d.addCallback(self.__cbFetch)
+        return d
+
+    def fetchFull(self, messages, uid=0):
+        """Retrieve several different fields of one or more messages
+
+        This command is allowed in the Selected state.  This is equivalent
+        to issuing all of the C{fetchFlags}, C{fetchInternalDate},
+        C{fetchSize}, C{fetchEnvelope}, and C{fetchSimplifiedBody}
+        functions.
+
+        @type messages: C{str}
+        @param messages: A message sequence set
+
+        @type uid: C{bool}
+        @param uid: Indicates whether the message sequence set is of message
+        numbers or of unique message IDs.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked with a dict mapping
+        message numbers to dict of the retrieved data values, or whose
+        errback is invoked if there is an error.  They dictionary keys
+        are "flags", "date", "size", "envelope", and "body".
+        """
+        d = self._fetch(
+            messages, useUID=uid, flags=1, internaldate=1,
+            rfc822size=1, envelope=1, body=1
+        )
+        d.addCallback(self.__cbFetch)
+        return d
+
+    def fetchAll(self, messages, uid=0):
+        """Retrieve several different fields of one or more messages
+
+        This command is allowed in the Selected state.  This is equivalent
+        to issuing all of the C{fetchFlags}, C{fetchInternalDate},
+        C{fetchSize}, and C{fetchEnvelope} functions.
+
+        @type messages: C{str}
+        @param messages: A message sequence set
+
+        @type uid: C{bool}
+        @param uid: Indicates whether the message sequence set is of message
+        numbers or of unique message IDs.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked with a dict mapping
+        message numbers to dict of the retrieved data values, or whose
+        errback is invoked if there is an error.  They dictionary keys
+        are "flags", "date", "size", and "envelope".
+        """
+        d = self._fetch(
+            messages, useUID=uid, flags=1, internaldate=1,
+            rfc822size=1, envelope=1
+        )
+        d.addCallback(self.__cbFetch)
+        return d
+
+    def fetchFast(self, messages, uid=0):
+        """Retrieve several different fields of one or more messages
+
+        This command is allowed in the Selected state.  This is equivalent
+        to issuing all of the C{fetchFlags}, C{fetchInternalDate}, and
+        C{fetchSize} functions.
+
+        @type messages: C{str}
+        @param messages: A message sequence set
+
+        @type uid: C{bool}
+        @param uid: Indicates whether the message sequence set is of message
+        numbers or of unique message IDs.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked with a dict mapping
+        message numbers to dict of the retrieved data values, or whose
+        errback is invoked if there is an error.  They dictionary keys are
+        "flags", "date", and "size".
+        """
+        d = self._fetch(
+            messages, useUID=uid, flags=1, internaldate=1, rfc822size=1
+        )
+        d.addCallback(self.__cbFetch)
+        return d
+
+    def __cbFetch(self, (lines, last)):
+        flags = {}
+        for line in lines:
+            parts = line.split(None, 2)
+            if len(parts) == 3:
+                if parts[1] == 'FETCH':
+                    try:
+                        id = int(parts[0])
+                    except ValueError:
+                        raise IllegalServerResponse, line
+                    else:
+                        data = parseNestedParens(parts[2])
+                        while len(data) == 1 and isinstance(data, types.ListType):
+                            data = data[0]
+                        while data:
+                            if len(data) < 2:
+                                raise IllegalServerResponse("Not enough arguments", data)
+                            flags.setdefault(id, {})[data[0]] = data[1]
+                            del data[:2]
+                else:
+                    print '(2)Ignoring ', parts
+            else:
+                print '(3)Ignoring ', parts
+        return flags
+
+    def fetchSpecific(self, messages, uid=0, headerType=None,
+                      headerNumber=None, headerArgs=None, peek=None,
+                      offset=None, length=None):
+        """Retrieve a specific section of one or more messages
+
+        @type messages: C{str}
+        @param messages: A message sequence set
+
+        @type uid: C{bool}
+        @param uid: Indicates whether the message sequence set is of message
+        numbers or of unique message IDs.
+
+        @type headerType: C{str}
+        @param headerType: If specified, must be one of HEADER,
+        HEADER.FIELDS, HEADER.FIELDS.NOT, MIME, or TEXT, and will determine
+        which part of the message is retrieved.  For HEADER.FIELDS and
+        HEADER.FIELDS.NOT, C{headerArgs} must be a sequence of header names.
+        For MIME, C{headerNumber} must be specified.
+
+        @type headerNumber: C{int} or C{int} sequence
+        @param headerNumber: The nested rfc822 index specifying the
+        entity to retrieve.  For example, C{1} retrieves the first
+        entity of the message, and C{(2, 1, 3}) retrieves the 3rd
+        entity inside the first entity inside the second entity of
+        the message.
+
+        @type headerArgs: A sequence of C{str}
+        @param headerArgs: If C{headerType} is HEADER.FIELDS, these are the
+        headers to retrieve.  If it is HEADER.FIELDS.NOT, these are the
+        headers to exclude from retrieval.
+
+        @type peek: C{bool}
+        @param peek: If true, cause the server to not set the \\Seen
+        flag on this message as a result of this command.
+
+        @type offset: C{int}
+        @param offset: The number of octets at the beginning of the result
+        to skip.
+
+        @type length: C{int}
+        @param length: The number of octets to retrieve.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked with a mapping of
+        message numbers to retrieved data, or whose errback is invoked
+        if there is an error.
+        """
+        fmt = '%s BODY%s[%s%s%s]%s'
+        if headerNumber is None:
+            number = ''
+        elif isinstance(headerNumber, types.IntType):
+            number = str(headerNumber)
+        else:
+            number = '.'.join(headerNumber)
+        if headerType is None:
+            header = ''
+        elif number:
+            header = '.' + headerType
+        else:
+            header = headerType
+        if header:
+            if headerArgs is not None:
+                payload = ' (%s)' % ' '.join(headerArgs)
+            else:
+                payload = ' ()'
+        else:
+            payload = ''
+        if offset is None:
+            extra = ''
+        else:
+            extra = '<%d.%d>' % (offset, length)
+        fetch = uid and 'UID FETCH' or 'FETCH'
+        cmd = fmt % (messages, peek and '.PEEK' or '', number, header, payload, extra)
+        d = self.sendCommand(Command(fetch, cmd, wantResponse=('FETCH',)))
+        d.addCallback(self.__cbFetchSpecific)
+        return d
+
+    def __cbFetchSpecific(self, (lines, last)):
+        info = {}
+        for line in lines:
+            parts = line.split(None, 2)
+            if len(parts) == 3:
+                if parts[1] == 'FETCH':
+                    try:
+                        id = int(parts[0])
+                    except ValueError:
+                        raise IllegalServerResponse, line
+                    else:
+                        info[id] = parseNestedParens(parts[2])
+        return info
+
+    def _fetch(self, messages, useUID=0, **terms):
+        fetch = useUID and 'UID FETCH' or 'FETCH'
+
+        if 'rfc822text' in terms:
+            del terms['rfc822text']
+            terms['rfc822.text'] = True
+        if 'rfc822size' in terms:
+            del terms['rfc822size']
+            terms['rfc822.size'] = True
+        if 'rfc822header' in terms:
+            del terms['rfc822header']
+            terms['rfc822.header'] = True
+                
+        cmd = '%s (%s)' % (messages, ' '.join([s.upper() for s in terms.keys()]))
+        d = self.sendCommand(Command(fetch, cmd, wantResponse=('FETCH',)))
+        return d
+
+    def setFlags(self, messages, flags, silent=1, uid=0):
+        """Set the flags for one or more messages.
+
+        This command is allowed in the Selected state.
+
+        @type messages: C{str}
+        @param messages: A message sequence set
+
+        @type flags: Any iterable of C{str}
+        @param flags: The flags to set
+
+        @type silent: C{bool}
+        @param silent: If true, cause the server to supress its verbose
+        response.
+
+        @type uid: C{bool}
+        @param uid: Indicates whether the message sequence set is of message
+        numbers or of unique message IDs.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked with a list of the
+        the server's responses (C{[]} if C{silent} is true) or whose
+        errback is invoked if there is an error.
+        """
+        return self._store(messages, silent and 'FLAGS.SILENT' or 'FLAGS', flags, uid)
+
+    def addFlags(self, messages, flags, silent=1, uid=0):
+        """Add to the set flags for one or more messages.
+
+        This command is allowed in the Selected state.
+
+        @type messages: C{str}
+        @param messages: A message sequence set
+
+        @type flags: Any iterable of C{str}
+        @param flags: The flags to set
+
+        @type silent: C{bool}
+        @param silent: If true, cause the server to supress its verbose
+        response.
+
+        @type uid: C{bool}
+        @param uid: Indicates whether the message sequence set is of message
+        numbers or of unique message IDs.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked with a list of the
+        the server's responses (C{[]} if C{silent} is true) or whose
+        errback is invoked if there is an error.
+        """
+        return self._store(messages, silent and '+FLAGS.SILENT' or '+FLAGS', flags, uid)
+
+    def removeFlags(self, messages, flags, silent=1, uid=0):
+        """Remove from the set flags for one or more messages.
+
+        This command is allowed in the Selected state.
+
+        @type messages: C{str}
+        @param messages: A message sequence set
+
+        @type flags: Any iterable of C{str}
+        @param flags: The flags to set
+
+        @type silent: C{bool}
+        @param silent: If true, cause the server to supress its verbose
+        response.
+
+        @type uid: C{bool}
+        @param uid: Indicates whether the message sequence set is of message
+        numbers or of unique message IDs.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked with a list of the
+        the server's responses (C{[]} if C{silent} is true) or whose
+        errback is invoked if there is an error.
+        """
+        return self._store(messages, silent and '-FLAGS.SILENT' or '-FLAGS', flags, uid)
+
+    def _store(self, messages, cmd, flags, uid):
+        store = uid and 'UID STORE' or 'STORE'
+        args = ' '.join((messages, cmd, '(%s)' % ' '.join(flags)))
+        d = self.sendCommand(Command(store, args, wantResponse=('FETCH',)))
+        d.addCallback(self.__cbFetch, lookFor='FLAGS')
+        return d
+    
+    def copy(self, messages, mailbox, uid):
+        """Copy the specified messages to the specified mailbox.
+        
+        This command is allowed in the Selected state.
+        
+        @type messages: C{str}
+        @param messages: A message sequence set
+        
+        @type mailbox: C{str}
+        @param mailbox: The mailbox to which to copy the messages
+        
+        @type uid: C{bool}
+        @param uid: If true, the C{messages} refers to message UIDs, rather
+        than message sequence numbers.
+        
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked with a true value
+        when the copy is successful, or whose errback is invoked if there
+        is an error.
+        """
+        if uid:
+            cmd = 'UID COPY'
+        else:
+            cmd = 'COPY'
+        args = '%s %s' % (messages, mailbox.encode('imap4-utf-7'))
+        return self.sendCommand(Command(cmd, args))
+
+    #
+    # IMailboxListener methods
+    #
+    def modeChanged(self, writeable):
+        """Override me"""
+    
+    def flagsChanged(self, newFlags):
+        """Override me"""
+    
+    def newMessages(self, exists, recent):
+        """Override me"""
+
+
+class IllegalIdentifierError(IMAP4Exception): pass
+
+def parseIdList(s):
+    res = MessageSet()
+    parts = s.split(',')
+    for p in parts:
+        if ':' in p:
+            low, high = p.split(':', 1)
+            try:
+                if low == '*':
+                    low = None
+                else:
+                    low = long(low)
+                if high == '*':
+                    high = None
+                else:
+                    high = long(high)
+                res.extend((low, high))
+            except ValueError:
+                raise IllegalIdentifierError(p)
+        else:
+            try:
+                if p == '*':
+                    p = None
+                else:
+                    p = long(p)
+            except ValueError:
+                raise IllegalIdentifierError(p)
+            else:
+                res.extend(p)
+    return res
+
+class IllegalQueryError(IMAP4Exception): pass
+
+_SIMPLE_BOOL = (
+    'ALL', 'ANSWERED', 'DELETED', 'DRAFT', 'FLAGGED', 'NEW', 'OLD', 'RECENT',
+    'SEEN', 'UNANSWERED', 'UNDELETED', 'UNDRAFT', 'UNFLAGGED', 'UNSEEN'
+)
+
+_NO_QUOTES = (
+    'LARGER', 'SMALLER', 'UID'
+)
+
+def Query(sorted=0, **kwarg):
+    """Create a query string
+
+    Among the accepted keywords are:
+
+        all         : If set to a true value, search all messages in the
+                      current mailbox
+
+        answered    : If set to a true value, search messages flagged with
+                      \\Answered
+
+        bcc         : A substring to search the BCC header field for
+
+        before      : Search messages with an internal date before this
+                      value.  The given date should be a string in the format
+                      of 'DD-Mon-YYYY'.  For example, '03-Mar-2003'.
+
+        body        : A substring to search the body of the messages for
+
+        cc          : A substring to search the CC header field for
+
+        deleted     : If set to a true value, search messages flagged with
+                      \\Deleted
+
+        draft       : If set to a true value, search messages flagged with
+                      \\Draft
+
+        flagged     : If set to a true value, search messages flagged with
+                      \\Flagged
+
+        from        : A substring to search the From header field for
+
+        header      : A two-tuple of a header name and substring to search
+                      for in that header
+
+        keyword     : Search for messages with the given keyword set
+
+        larger      : Search for messages larger than this number of octets
+
+        messages    : Search only the given message sequence set.
+
+        new         : If set to a true value, search messages flagged with
+                      \\Recent but not \\Seen
+
+        old         : If set to a true value, search messages not flagged with
+                      \\Recent
+
+        on          : Search messages with an internal date which is on this
+                      date.  The given date should be a string in the format
+                      of 'DD-Mon-YYYY'.  For example, '03-Mar-2003'.
+
+        recent      : If set to a true value, search for messages flagged with
+                      \\Recent
+
+        seen        : If set to a true value, search for messages flagged with
+                      \\Seen
+
+        sentbefore  : Search for messages with an RFC822 'Date' header before
+                      this date.  The given date should be a string in the format
+                      of 'DD-Mon-YYYY'.  For example, '03-Mar-2003'.
+
+        senton      : Search for messages with an RFC822 'Date' header which is
+                      on this date  The given date should be a string in the format
+                      of 'DD-Mon-YYYY'.  For example, '03-Mar-2003'.
+
+        sentsince   : Search for messages with an RFC822 'Date' header which is
+                      after this date.  The given date should be a string in the format
+                      of 'DD-Mon-YYYY'.  For example, '03-Mar-2003'.
+
+        since       : Search for messages with an internal date that is after
+                      this date..  The given date should be a string in the format
+                      of 'DD-Mon-YYYY'.  For example, '03-Mar-2003'.
+
+        smaller     : Search for messages smaller than this number of octets
+
+        subject     : A substring to search the 'subject' header for
+
+        text        : A substring to search the entire message for
+
+        to          : A substring to search the 'to' header for
+
+        uid         : Search only the messages in the given message set
+
+        unanswered  : If set to a true value, search for messages not
+                      flagged with \\Answered
+
+        undeleted   : If set to a true value, search for messages not
+                      flagged with \\Deleted
+
+        undraft     : If set to a true value, search for messages not
+                      flagged with \\Draft
+
+        unflagged   : If set to a true value, search for messages not
+                      flagged with \\Flagged
+
+        unkeyword   : Search for messages without the given keyword set
+
+        unseen      : If set to a true value, search for messages not
+                      flagged with \\Seen
+
+    @type sorted: C{bool}
+    @param sorted: If true, the output will be sorted, alphabetically.
+    The standard does not require it, but it makes testing this function
+    easier.  The default is zero, and this should be acceptable for any
+    application.
+
+    @rtype: C{str}
+    @return: The formatted query string
+    """
+    cmd = []
+    keys = kwarg.keys()
+    if sorted:
+        keys.sort()
+    for k in keys:
+        v = kwarg[k]
+        k = k.upper()
+        if k in _SIMPLE_BOOL and v:
+           cmd.append(k)
+        elif k == 'HEADER':
+            cmd.extend([k, v[0], '"%s"' % (v[1],)])
+        elif k not in _NO_QUOTES:
+           cmd.extend([k, '"%s"' % (v,)])
+        else:
+           cmd.extend([k, '%s' % (v,)])
+    if len(cmd) > 1:
+        return '(%s)' % ' '.join(cmd)
+    else:
+        return ' '.join(cmd)
+
+def Or(*args):
+    """The disjunction of two or more queries"""
+    if len(args) < 2:
+        raise IllegalQueryError, args
+    elif len(args) == 2:
+        return '(OR %s %s)' % args
+    else:
+        return '(OR %s %s)' % (args[0], Or(*args[1:]))
+
+def Not(query):
+    """The negation of a query"""
+    return '(NOT %s)' % (query,)
+
+class MismatchedNesting(IMAP4Exception):
+    pass
+
+class MismatchedQuoting(IMAP4Exception):
+    pass
+
+def wildcardToRegexp(wildcard, delim):
+    wildcard = wildcard.replace('*', '(?:.*?)')
+    wildcard = wildcard.replace('%', '(?:(?:[^%s])*?)' % re.escape(delim))
+    return re.compile(wildcard)
+
+def splitQuoted(s):
+    """Split a string into whitespace delimited tokens
+
+    Tokens that would otherwise be separated but are surrounded by \"
+    remain as a single token.  Any token that is not quoted and is
+    equal to \"NIL\" is tokenized as C{None}.
+
+    @type s: C{str}
+    @param s: The string to be split
+
+    @rtype: C{list} of C{str}
+    @return: A list of the resulting tokens
+
+    @raise MismatchedQuoting: Raised if an odd number of quotes are present
+    """
+    s = s.strip()
+    result = []
+    inQuote = inWord = start = 0
+    for (i, c) in zip(range(len(s)), s):
+        if c == '"' and not inQuote:
+            inQuote = 1
+            start = i + 1
+        elif c == '"' and inQuote:
+            inQuote = 0
+            result.append(s[start:i])
+            start = i + 1
+        elif not inWord and not inQuote and c not in ('"' + string.whitespace):
+            inWord = 1
+            start = i
+        elif inWord and not inQuote and c in string.whitespace:
+            if s[start:i] == 'NIL':
+                result.append(None)
+            else:
+                result.append(s[start:i])
+            start = i
+            inWord = 0
+    if inQuote:
+        raise MismatchedQuoting(s)
+    if inWord:
+        if s[start:] == 'NIL':
+            result.append(None)
+        else:
+            result.append(s[start:])
+    return result
+
+
+def splitOn(sequence, predicate, transformers):
+    result = []
+    mode = predicate(sequence[0])
+    tmp = [sequence[0]]
+    for e in sequence[1:]:
+        p = predicate(e)
+        if p != mode:
+            result.extend(transformers[mode](tmp))
+            tmp = [e]
+            mode = p
+        else:
+            tmp.append(e)
+    result.extend(transformers[mode](tmp))
+    return result
+
+def collapseStrings(results):
+    """
+    Turns a list of length-one strings and lists into a list of longer
+    strings and lists.  For example,
+
+    ['a', 'b', ['c', 'd']] is returned as ['ab', ['cd']]
+
+    @type results: C{list} of C{str} and C{list}
+    @param results: The list to be collapsed
+
+    @rtype: C{list} of C{str} and C{list}
+    @return: A new list which is the collapsed form of C{results}
+    """
+    copy = []
+    begun = None
+    listsList = [isinstance(s, types.ListType) for s in results]
+
+    pred = lambda e: isinstance(e, types.TupleType)
+    tran = {
+        0: lambda e: splitQuoted(''.join(e)), 
+        1: lambda e: [''.join([i[0] for i in e])]
+    }
+    for (i, c, isList) in zip(range(len(results)), results, listsList):
+        if isList:
+            if begun is not None:
+                copy.extend(splitOn(results[begun:i], pred, tran))
+                begun = None
+            copy.append(collapseStrings(c))
+        elif begun is None:
+            begun = i
+    if begun is not None:
+        copy.extend(splitOn(results[begun:], pred, tran))
+    return copy
+
+
+def parseNestedParens(s, handleLiteral = 1):
+    """Parse an s-exp-like string into a more useful data structure.
+
+    @type s: C{str}
+    @param s: The s-exp-like string to parse
+
+    @rtype: C{list} of C{str} and C{list}
+    @return: A list containing the tokens present in the input.
+
+    @raise MismatchedNesting: Raised if the number or placement
+    of opening or closing parenthesis is invalid.
+    """
+    s = s.strip()
+    inQuote = 0
+    contentStack = [[]]
+    try:
+        i = 0
+        L = len(s)
+        while i < L:
+            c = s[i]
+            if inQuote:
+                if c == '\\':
+                    contentStack[-1].append(s[i+1])
+                    i += 2
+                    continue
+                elif c == '"':
+                    inQuote = not inQuote
+                contentStack[-1].append(c)
+                i += 1
+            else:
+                if c == '"':
+                    contentStack[-1].append(c)
+                    inQuote = not inQuote
+                    i += 1
+                elif handleLiteral and c == '{':
+                    end = s.find('}', i)
+                    if end == -1:
+                        raise ValueError, "Malformed literal"
+                    literalSize = int(s[i+1:end])
+                    contentStack[-1].append((s[end+3:end+3+literalSize],))
+                    i = end + 3 + literalSize
+                elif c == '(' or c == '[':
+                    contentStack.append([])
+                    i += 1
+                elif c == ')' or c == ']':
+                    contentStack[-2].append(contentStack.pop())
+                    i += 1
+                else:
+                    contentStack[-1].append(c)
+                    i += 1
+    except IndexError:
+        raise MismatchedNesting(s)
+    if len(contentStack) != 1:
+        raise MismatchedNesting(s)
+    return collapseStrings(contentStack[0])
+
+def _quote(s):
+    return '"%s"' % (s.replace('\\', '\\\\').replace('"', '\\"'),)
+
+_ATOM_SPECIALS = '(){ %*"'
+def _needsQuote(s):
+    if s == '':
+        return 1
+    for c in s:
+        if c < '\x20' or c > '\x7f':
+            return 1
+        if c in _ATOM_SPECIALS:
+            return 1
+    return 0
+
+def _needsLiteral(s):
+    # Change this to "return 1" to wig out stupid clients
+    return '\n' in s or '\r' in s or len(s) > 1000
+
+def collapseNestedLists(items):
+    """Turn a nested list structure into an s-exp-like string.
+
+    Strings in C{items} will be sent as literals if they contain CR or LF,
+    quoted if they contain other whitespace, or sent unquoted otherwise.
+    References to None in C{items} will be translated to the atom NIL.
+    Objects with a 'read' attribute will have it called on them with no
+    arguments and the returned string will be inserted into the output as a
+    literal.
+
+    @type items: Any iterable
+
+    @rtype: C{str}
+    """
+    pieces = []
+    for i in items:
+        if i is None:
+            pieces.extend([' ', 'NIL'])
+        elif hasattr(i, 'read'):
+            d = i.read()
+            pieces.extend([' ', '{', str(len(d)), '}', IMAP4Server.delimiter, d])
+        elif isinstance(i, types.StringTypes):
+            if _needsLiteral(i):
+                pieces.extend([' ', '{', str(len(i)), '}', IMAP4Server.delimiter, i])
+            elif (not i.startswith('BODY')) and _needsQuote(i):
+                pieces.extend([' ', _quote(i)])
+            else:
+                pieces.extend([' ', i])
+        elif pieces and pieces[-1].upper() == 'BODY.PEEK':
+            pieces.append('[%s]' % (collapseNestedLists(i),))
+        else:
+            pieces.extend([' ', '(%s)' % (collapseNestedLists(i),)])
+    return ''.join(pieces[1:])
+
+class IClientAuthentication(components.Interface):
+    def getName(self):
+        """Return an identifier associated with this authentication scheme.
+        
+        @rtype: C{str}
+        """
+
+    def challengeResponse(self, secret, challenge):
+        """Generate a challenge response string"""
+
+class CramMD5ClientAuthenticator:
+    __implements__ = (IClientAuthentication,)
+
+    def __init__(self, user):
+        self.user = user
+
+    def getName(self):
+        return "CRAM-MD5"
+
+    def challengeResponse(self, secret, chal):
+        response = hmac.HMAC(secret, chal).hexdigest()
+        return '%s %s' % (self.user, response)
+
+class MailboxException(IMAP4Exception): pass
+
+class MailboxCollision(MailboxException):
+    def __str__(self):
+        return 'Mailbox named %s already exists' % self.args
+
+class NoSuchMailbox(MailboxException):
+    def __str__(self):
+        return 'No mailbox named %s exists' % self.args
+
+class ReadOnlyMailbox(MailboxException):
+    def __str__(self):
+        return 'Mailbox open in read-only state'
+
+class IAccount(components.Interface):
+    """Interface for Account classes
+    
+    Implementors of this interface must also subclass
+    C{twisted.cred.perspective.Perspective}
+    """
+
+    def addMailbox(self, name, mbox = None):
+        """Add a new mailbox to this account
+        
+        @type name: C{str}
+        @param name: The name associated with this mailbox.  It may not
+        contain multiple hierarchical parts.
+        
+        @type mbox: An object implementing C{IMailbox}
+        @param mbox: The mailbox to associate with this name.  If C{None},
+        a suitable default is created and used.
+        
+        @rtype: C{Deferred} or C{bool}
+        @return: A true value if the creation succeeds, or a deferred whose
+        callback will be invoked when the creation succeeds.
+        
+        @raise MailboxException: Raised if this mailbox cannot be added for
+        some reason.  This may also be raised asynchronously, if a C{Deferred}
+        is returned.
+        """
+    
+    def create(self, pathspec):
+        """Create a new mailbox from the given hierarchical name.
+        
+        @type pathspec: C{str}
+        @param pathspec: The full hierarchical name of a new mailbox to create.
+        If any of the inferior hierarchical names to this one do not exist,
+        they are created as well.
+        
+        @rtype: C{Deferred} or C{bool}
+        @return: A true value if the creation succeeds, or a deferred whose
+        callback will be invoked when the creation succeeds.
+        
+        @raise MailboxException: Raised if this mailbox cannot be added. 
+        This may also be raised asynchronously, if a C{Deferred} is
+        returned.
+        """
+    
+    def select(self, name, rw=1):
+        """Acquire a mailbox, given its name.
+        
+        @type name: C{str}
+        @param name: The mailbox to acquire
+        
+        @type rw: C{bool}
+        @param rw: If a true value, request a read-write version of this
+        mailbox.  If a false value, request a read-only version.
+        
+        @rtype: Any object implementing C{IMailbox} or C{Deferred}
+        @return: The mailbox object, or a C{Deferred} whose callback will
+        be invoked with the mailbox object.  None may be returned if the
+        specified mailbox may not be selected for any reason.
+        """
+    
+    def delete(self, name):
+        """Delete the mailbox with the specified name.
+        
+        @type name: C{str}
+        @param name: The mailbox to delete.
+        
+        @rtype: C{Deferred} or C{bool}
+        @return: A true value if the mailbox is successfully deleted, or a
+        C{Deferred} whose callback will be invoked when the deletion
+        completes.
+
+        @raise MailboxException: Raised if this mailbox cannot be deleted.
+        This may also be raised asynchronously, if a C{Deferred} is returned.
+        """
+    
+    def rename(self, oldname, newname):
+        """Rename a mailbox
+        
+        @type oldname: C{str}
+        @param oldname: The current name of the mailbox to rename.
+        
+        @type newname: C{str}
+        @param newname: The new name to associate with the mailbox.
+        
+        @rtype: C{Deferred} or C{bool}
+        @return: A true value if the mailbox is successfully renamed, or a
+        C{Deferred} whose callback will be invoked when the rename operation
+        is completed.
+        
+        @raise MailboxException: Raised if this mailbox cannot be
+        renamed.  This may also be raised asynchronously, if a C{Deferred}
+        is returned.
+        """
+    
+    def isSubscribed(self, name):
+        """Check the subscription status of a mailbox
+        
+        @type name: C{str}
+        @param name: The name of the mailbox to check
+        
+        @rtype: C{Deferred} or C{bool}
+        @return: A true value if the given mailbox is currently subscribed
+        to, a false value otherwise.  A C{Deferred} may also be returned
+        whose callback will be invoked with one of these values.
+        """
+    
+    def subscribe(self, name):
+        """Subscribe to a mailbox
+        
+        @type name: C{str}
+        @param name: The name of the mailbox to subscribe to
+        
+        @rtype: C{Deferred} or C{bool}
+        @return: A true value if the mailbox is subscribed to successfully,
+        or a Deferred whose callback will be invoked with this value when
+        the subscription is successful.
+
+        @raise MailboxException: Raised if this mailbox cannot be
+        subscribed to.  This may also be raised asynchronously, if a
+        C{Deferred} is returned.
+        """
+    
+    def unsubscribe(self, name):
+        """Unsubscribe from a mailbox
+        
+        @type name: C{str}
+        @param name: The name of the mailbox to unsubscribe from
+        
+        @rtype: C{Deferred} or C{bool}
+        @return: A true value if the mailbox is unsubscribed from successfully,
+        or a Deferred whose callback will be invoked with this value when
+        the unsubscription is successful.
+
+        @raise MailboxException: Raised if this mailbox cannot be
+        unsubscribed from.  This may also be raised asynchronously, if a
+        C{Deferred} is returned.
+        """
+    
+    def listMailboxes(self, ref, wildcard):
+        """List all the mailboxes that meet a certain criteria
+        
+        @type ref: C{str}
+        @param ref: The context in which to apply the wildcard
+        
+        @type wildcard: C{str}
+        @param wildcard: An expression against which to match mailbox names.
+        '*' matches any number of characters in a mailbox name, and '%'
+        matches similarly, but will not match across hierarchical boundaries.
+        
+        @rtype: C{list} of C{tuple}
+        @return: A list of C{(mailboxName, mailboxObject)} which meet the
+        given criteria.
+        """
+    
+class MemoryAccount(perspective.Perspective):
+    __implements__ = (perspective.Perspective.__implements__, IAccount)
+
+    mailboxes = None
+    subscriptions = None
+    top_id = 0
+    
+    def __init__(self, name):
+        perspective.Perspective.__init__(self, name, name)
+        self.mailboxes = {}
+        self.subscriptions = []
+
+    def allocateID(self):
+        id = self.top_id
+        self.top_id += 1
+        return id
+
+    def addMailbox(self, name, mbox = None):
+        name = name.upper()
+        if self.mailboxes.has_key(name):
+            raise MailboxCollision, name
+        if mbox is None:
+            mbox = self._emptyMailbox(name, self.allocateID())
+        self.mailboxes[name] = mbox
+        return 1
+
+    def create(self, pathspec):
+        paths = filter(None, pathspec.split('/'))
+        for accum in range(1, len(paths)):
+            try:
+                self.addMailbox('/'.join(paths[:accum]))
+            except MailboxCollision:
+                pass
+        try:
+            self.addMailbox('/'.join(paths))
+        except MailboxCollision:
+            if not pathspec.endswith('/'):
+                raise
+
+    def _emptyMailbox(self, name, id):
+        raise NotImplementedError
+
+    def select(self, name, readwrite=1):
+        return self.mailboxes.get(name.upper())
+
+    def delete(self, name):
+        name = name.upper()
+        # See if this mailbox exists at all
+        mbox = self.mailboxes.get(name)
+        if not mbox:
+            raise MailboxException("No such mailbox")
+        # See if this box is flagged \Noselect
+        if r'\Noselect' in mbox.getFlags():
+            # Check for hierarchically inferior mailboxes with this one
+            # as part of their root.
+            for others in self.mailboxes.keys():
+                if others != name and others.startswith(name):
+                    raise MailboxException, "Hierarchically inferior mailboxes exist and \\Noselect is set"
+        mbox.destroy()
+
+        # iff there are no hierarchically inferior names, we will
+        # delete it from our ken.
+        if self._inferiorNames(name) > 1:
+            del self.mailboxes[name]
+
+    def rename(self, oldname, newname):
+        oldname = oldname.upper()
+        newname = newname.upper()
+        if not self.mailboxes.has_key(oldname):
+            raise NoSuchMailbox, oldname
+
+        inferiors = self._inferiorNames(oldname)
+        inferiors = [(o, o.replace(oldname, newname, 1)) for o in inferiors]
+
+        for (old, new) in inferiors:
+            if self.mailboxes.has_key(new):
+                raise MailboxCollision, new
+
+        for (old, new) in inferiors:
+            self.mailboxes[new] = self.mailboxes[old]
+            del self.mailboxes[old]
+
+    def _inferiorNames(self, name):
+        inferiors = []
+        for infname in self.mailboxes.keys():
+            if infname.startswith(name):
+                inferiors.append(infname)
+        return inferiors
+
+    def isSubscribed(self, name):
+        return name.upper() in self.subscriptions
+
+    def subscribe(self, name):
+        name = name.upper()
+        if name not in self.subscriptions:
+            self.subscriptions.append(name)
+
+    def unsubscribe(self, name):
+        name = name.upper()
+        if name not in self.subscriptions:
+            raise MailboxException, "Not currently subscribed to " + name
+        self.subscriptions.remove(name)
+
+    def listMailboxes(self, ref, wildcard):
+        ref = self._inferiorNames(ref.upper())
+        wildcard = wildcardToRegexp(wildcard, '/')
+        return [(i, self.mailboxes[i]) for i in ref if wildcard.match(i)]
+
+_statusRequestDict = {
+    'MESSAGES': 'getMessageCount',
+    'RECENT': 'getRecentCount',
+    'UIDNEXT': 'getUIDNext',
+    'UIDVALIDITY': 'getUIDValidity',
+    'UNSEEN': 'getUnseenCount'
+}
+def statusRequestHelper(mbox, names):
+    r = {}
+    for n in names:
+        r[n] = getattr(mbox, _statusRequestDict[n])()
+    return r
+
+def getEnvelope(msg):
+    date = msg.getHeaders(False, 'date',).get('date')
+    subject = msg.getHeaders(False, 'subject').get('subject')
+    from_ = msg.getHeaders(False, 'from').get('from')
+    sender = msg.getHeaders(False, 'sender').get('sender', from_)
+    reply_to = msg.getHeaders(False, 'reply-to').get('reply-to', from_)
+    to = msg.getHeaders(False, 'to').get('to')
+    cc = msg.getHeaders(False, 'cc').get('cc')
+    bcc = msg.getHeaders(False, 'bcc').get('bcc')
+    in_reply_to = msg.getHeaders(False, 'in-reply-to').get('in-reply-to')
+    return date, subject, from_, sender, reply_to, to, cc, bcc, in_reply_to
+
+def bodyStructure(msg):
+    # XXX - This does not properly handle multipart messages
+    # BODYSTRUCTURE is obscenely complex and criminally under-documented.
+    
+    charset = 'US-ASCII'
+    mm = msg.getHeaders(False, 'content-type').get('content-type')
+    if mm:
+        mm = ''.join(mm.splitlines())
+        mimetype = mm.split(';')
+        if mimetype:
+            type = mimetype[0].split('/', 1)
+            if len(mimetype) == 1:
+                major = mimetype[0]
+                minor = None
+            else:
+                major, minor = mimtype
+            attrs = dict([x.lower().split('=', 1) for x in mimetype[1:]])
+            try:
+                charset = attrs['charset'].upper()
+            except KeyError:
+                pass
+        else:
+            major = minor = None
+    else:
+        major = minor = None
+    
+    size = str(msg.getSize())
+    lines = 0
+    for _ in msg.getBodyFile():
+        lines += 1
+    return (
+        major, minor,               # Main and Sub MIME types
+        ("CHARSET", charset),       # Character encoding
+        None, None,                 # Hell if I know
+        "7BIT",                     # Content-Transfer-Encoding??  Beats me
+        size,                       # Number of octets total
+        str(lines)                  # number of lines in body
+    )
+
+class IMessage(components.Interface):
+    def getHeaders(self, negate, *names):
+        """Retrieve a group of message headers.
+        
+        @type names: C{tuple} of C{str}
+        @param names: The names of the headers to retrieve or omit.
+        
+        @type negate: C{bool}
+        @param negate: If True, indicates that the headers listed in C{names}
+        should be omitted from the return value, rather than included.
+        
+        @rtype: C{dict}
+        @return: A mapping of header field names to header field values
+        """ 
+
+    def getFlags(self):
+        """Retrieve the flags associated with this message.
+        
+        @rtype: C{iterable}
+        @return: The flags, represented as strings.
+        """
+
+    def getInternalDate(self):
+        """Retrieve the date internally associated with this message.
+        
+        @rtype: C{str}
+        @return: An RFC822-formatted date string.
+        """
+
+    def getBodyFile(self):
+        """Retrieve a file object containing the body of this message.
+        """
+
+    def getSize(self):
+        """Retrieve the total size, in octets, of this message.
+        
+        @rtype: C{int}
+        """
+
+    def getUID(self):
+        """Retrieve the unique identifier associated with this message.
+        """
+
+    def getSubPart(self, part):
+        """Retrieve a MIME sub-message
+        
+        @type part: C{int}
+        @param part: The number of the part to retrieve, indexed from 0.
+        
+        @rtype: Any object implementing C{IMessage}.
+        @return: The specified sub-part.
+        """
+
+class IMailbox(components.Interface):
+    def getUIDValidity(self):
+        """Return the unique validity identifier for this mailbox.
+
+        @rtype: C{int}
+        """
+
+    def getUIDNext(self):
+        """Return the likely UID for the next message added to this mailbox.
+        
+        @rtype: C{int}
+        """
+
+    def getUID(self, message):
+        """Return the UID of a message in the mailbox
+
+        @type message: C{int}
+        @param message: The message sequence number
+
+        @rtype: C{int}
+        @return: The UID of the message.
+        """
+
+    def getFlags(self):
+        """Return the flags defined in this mailbox
+
+        Flags with the \\ prefix are reserved for use as system flags.
+
+        @rtype: C{list} of C{str}
+        @return: A list of the flags that can be set on messages in this mailbox.
+        """
+
+    def getMessageCount(self):
+        """Return the number of messages in this mailbox.
+        
+        @rtype: C{int}
+        """
+
+    def getRecentCount(self):
+        """Return the number of messages with the 'Recent' flag.
+        
+        @rtype: C{int}
+        """
+
+    def getUnseenCount(self):
+        """Return the number of messages with the 'Unseen' flag.
+        
+        @rtype: C{int}
+        """
+
+    def isWriteable(self):
+        """Get the read/write status of the mailbox.
+
+        @rtype: C{int}
+        @return: A true value if write permission is allowed, a false value otherwise.
+        """
+
+    def destroy(self):
+        """Called before this mailbox is deleted, permanently.
+
+        If necessary, all resources held by this mailbox should be cleaned
+        up here.  This function _must_ set the \\Noselect flag on this
+        mailbox.
+        """
+
+    def getHierarchicalDelimiter(self):
+        """Get the character which delimits namespaces for in this mailbox.
+
+        @rtype: C{str}
+        """
+
+    def requestStatus(self, names):
+        """Return status information about this mailbox.
+
+        Mailboxes which do not intend to do any special processing to
+        generate the return value, C{statusRequestHelper} can be used
+        to build the dictionary by calling the other interface methods
+        which return the data for each name.
+
+        @type names: Any iterable
+        @param names: The status names to return information regarding.
+        The possible values for each name are: MESSAGES, RECENT, UIDNEXT,
+        UIDVALIDITY, UNSEEN.
+
+        @rtype: C{dict} or C{Deferred}
+        @return: A dictionary containing status information about the
+        requested names is returned.  If the process of looking this
+        information up would be costly, a deferred whose callback will
+        eventually be passed this dictionary is returned instead.
+        """
+
+    def addListener(self, listener):
+        """Add a mailbox change listener
+        
+        @type listener: Any object which implements C{IMailboxListener}
+        @param listener: An object to add to the set of those which will
+        be notified when the contents of this mailbox change.
+        """
+    
+    def removeListener(self, listener):
+        """Remove a mailbox change listener
+        
+        @type listener: Any object previously added to and not removed from
+        this mailbox as a listener.
+        @param listener: The object to remove from the set of listeners.
+        
+        @raise ValueError: Raised when the given object is not a listener for
+        this mailbox.
+        """
+
+    def addMessage(self, message, flags = (), date = None):
+        """Add the given message to this mailbox.
+
+        @type message: A file-like object
+        @param message: The RFC822 formatted message
+
+        @type flags: Any iterable of C{str}
+        @param flags: The flags to associate with this message
+
+        @type date: C{str}
+        @param date: If specified, the date to associate with this
+        message.
+
+        @rtype: C{Deferred}
+        @return: A deferred whose callback is invoked with the message
+        id if the message is added successfully and whose errback is
+        invoked otherwise.
+
+        @raise ReadOnlyMailbox: Raised if this Mailbox is not open for
+        read-write.
+        """
+
+    def expunge(self):
+        """Remove all messages flagged \\Deleted.
+
+        @rtype: C{list} or C{Deferred}
+        @return: The list of message sequence numbers which were deleted,
+        or a C{Deferred} whose callback will be invoked with such a list.
+
+        @raise ReadOnlyMailbox: Raised if this Mailbox is not open for
+        read-write.
+        """
+
+    def search(self, query, uid):
+        """Search for messages that meet the given query criteria.
+
+        @type query: C{list}
+        @param query: The search criteria
+
+        @type uid: C{bool}
+        @param uid: If true, the IDs specified in the query are UIDs;
+        otherwise they are message sequence IDs.
+
+        @rtype: C{list} or C{Deferred}
+        @return: A list of message sequence numbers or message UIDs which
+        match the search criteria or a C{Deferred} whose callback will be
+        invoked with such a list.
+        """
+
+    def fetch(self, messages, uid):
+        """Retrieve one or more messages.
+
+        @type messages: A MessageSet object with the list of messages requested
+        @param messages: The identifiers of messages to retrieve information
+        about
+
+        @type uid: C{bool}
+        @param uid: If true, the IDs specified in the query are UIDs;
+        otherwise they are message sequence IDs.
+
+        @rtype: Any iterable of two-tuples of message IDs and implementors
+        of C{IMessage}.
+        """
+
+    def store(self, messages, flags, mode, uid):
+        """Set the flags of one or more messages.
+
+        @type messages: A MessageSet object with the list of messages requested
+        @param messages: The identifiers of the messages to set the flags of.
+
+        @type flags: sequence of C{str}
+        @param flags: The flags to set, unset, or add.
+
+        @type mode: -1, 0, or 1
+        @param mode: If mode is -1, these flags should be removed from the
+        specified messages.  If mode is 1, these flags should be added to
+        the specified messages.  If mode is 0, all existing flags should be
+        cleared and these flags should be added.
+
+        @type uid: C{bool}
+        @param uid: If true, the IDs specified in the query are UIDs;
+        otherwise they are message sequence IDs.
+
+        @rtype: C{dict} or C{Deferred}
+        @return: A C{dict} mapping message identifiers to sequences of C{str}
+        representing the flags set on the message after this operation has
+        been performed, or a C{Deferred} whose callback will be invoked with
+        such a C{dict}.
+
+        @raise ReadOnlyMailbox: Raised if this mailbox is not open for
+        read-write.
+        """
+
+
+class _FetchParser:
+    class Envelope:
+        # Response should be a list of fields from the message:
+        #   date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to,
+        #   and message-id.
+        #
+        # from, sender, reply-to, to, cc, and bcc are themselves lists of
+        # address information:
+        #   personal name, source route, mailbox name, host name
+        #
+        # reply-to and sender must not be None.  If not present in a message
+        # they should be defaulted to the value of the from field.
+        type = 'envelope'
+        __str__ = lambda self: 'envelope'
+
+    class Flags:
+        type = 'flags'
+        __str__ = lambda self: 'flags'
+
+    class InternalDate:
+        type = 'internaldate'
+        __str__ = lambda self: 'internaldate'
+
+    class RFC822Header:
+        type = 'rfc822header'
+        __str__ = lambda self: 'rfc822.header'
+
+    class RFC822Text:
+        type = 'rfc822text'
+        __str__ = lambda self: 'rfc822.text'
+
+    class RFC822Size:
+        type = 'rfc822size'
+        __str__ = lambda self: 'rfc822.size'
+
+    class RFC822:
+        type = 'rfc822'
+        __str__ = lambda self: 'rfc822'
+
+    class UID:
+        type = 'uid'
+        __str__ = lambda self: 'uid'
+
+    class Body:
+        type = 'body'
+        peek = False
+        header = None
+        mime = None
+        text = None
+        part = None
+        partialBegin = None
+        partialLength = None
+        def __str__(self):
+            base = 'BODY'
+            if self.peek:
+                base += '.PEEK'
+            if self.header:
+                base += '[%s]' % (self.header,)
+            elif self.text:
+                base += '[TEXT]'
+            elif self.mime:
+                base += '[MIME]'
+            if self.partialBegin is not None:
+                base += '<%d.%d>' % (self.partialBegin, self.partialLength)
+            return base
+
+    class BodyStructure:
+        type = 'bodystructure'
+        __str__ = lambda self: 'bodystructure'
+
+    # These three aren't top-level, they don't need type indicators
+    class Header:
+        negate = False
+        fields = None
+        def __str__(self):
+            base = 'HEADER'
+            if self.fields:
+                base += '.FIELDS'
+                if self.negate:
+                    base += '.NOT'
+                base += ' (%s)' % (' '.join(self.fields),)
+            if self.part:
+                base = '.'.join(map(str, self.part)) + '.'
+            return base
+
+    class Text:
+        pass
+
+    class MIME:
+        pass
+
+    parts = None
+
+    _simple_fetch_att = [
+        ('envelope', Envelope),
+        ('flags', Flags),
+        ('internaldate', InternalDate),
+        ('rfc822.header', RFC822Header),
+        ('rfc822.text', RFC822Text),
+        ('rfc822.size', RFC822Size),
+        ('rfc822', RFC822),
+        ('uid', UID),
+        ('bodystructure', BodyStructure),
+    ]
+
+    def __init__(self):
+        self.state = ['initial']
+        self.result = []
+        self.remaining = ''
+
+    def parseString(self, s):
+        s = self.remaining + s
+        try:
+            while s or self.state:
+                # print 'Entering state_' + self.state[-1] + ' with', repr(s)
+                state = self.state.pop()
+                try:
+                    s = s[getattr(self, 'state_' + state)(s):]
+                except:
+                    self.state.append(state)
+                    raise
+        finally:
+            self.remaining = s
+    
+    def state_initial(self, s):
+        # In the initial state, the literals "ALL", "FULL", and "FAST"
+        # are accepted, as is a ( indicating the beginning of a fetch_att
+        # token, as is the beginning of a fetch_att token.
+        if s == '':
+            return 0
+        
+        l = s.lower()
+        if l.startswith('all'):
+            self.result.extend((
+                self.Flags(), self.InternalDate(),
+                self.RFC822Size(), self.Envelope()
+            ))
+            return 3
+        if l.startswith('full'):
+            self.result.extend((
+                self.Flags(), self.InternalDate(),
+                self.RFC822Size(), self.Envelope(),
+                self.Body()
+            ))
+            return 4
+        if l.startswith('fast'):
+            self.result.extend((
+                self.Flags(), self.InternalDate(), self.RFC822Size(),
+            ))
+            return 4
+        
+        if l.startswith('('):
+            self.state.extend(('close_paren', 'maybe_fetch_att', 'fetch_att'))
+            return 1
+        
+        self.state.append('fetch_att')
+        return 0
+    
+    def state_close_paren(self, s):
+        if s.startswith(')'):
+            return 1
+        raise Exception("Missing )")
+    
+    def state_whitespace(self, s):
+        # Eat up all the leading whitespace
+        if not s or not s[0].isspace():
+            raise Exception("Whitespace expected, none found")
+        i = 0
+        for i in range(len(s)):
+            if not s[i].isspace():
+                break
+        return i
+
+    def state_maybe_fetch_att(self, s):
+        if not s.startswith(')'):
+            self.state.extend(('maybe_fetch_att', 'fetch_att', 'whitespace'))
+        return 0
+
+    def state_fetch_att(self, s):
+        # Allowed fetch_att tokens are "ENVELOPE", "FLAGS", "INTERNALDATE",
+        # "RFC822", "RFC822.HEADER", "RFC822.SIZE", "RFC822.TEXT", "BODY",
+        # "BODYSTRUCTURE", "UID",
+        # "BODY [".PEEK"] [<section>] ["<" <number> "." <nz_number> ">"]
+
+        l = s.lower()
+        for (name, cls) in self._simple_fetch_att:
+            if l.startswith(name):
+                self.result.append(cls())
+                return len(name)
+        
+        b = self.Body()
+        if l.startswith('body.peek'):
+            b.peek = True
+            used = 9
+        elif l.startswith('body'):
+            used = 4
+        else:
+            raise Exception("Nothing recognized in fetch_att: %s" % (l,))
+        
+        self.pending_body = b
+        self.state.extend(('got_body', 'maybe_partial', 'maybe_section'))
+        return used
+    
+    def state_got_body(self, s):
+        self.result.append(self.pending_body)
+        del self.pending_body
+        return 0
+    
+    def state_maybe_section(self, s):
+        if not s.startswith("["):
+            return 0
+        
+        self.state.extend(('section', 'part_number'))
+        return 1
+    
+    def state_part_number(self, s):
+        last = -1
+        dot = s.find('.')
+        parts = []
+        while dot != -1 and s[last + 1].isdigit():
+            parts.append(int(s[last + 1:dot]) - 1)
+            last, dot = dot, s.find('.', dot + 1)
+        self.parts = parts
+        return last + 1
+
+    def state_section(self, s):
+        # Grab [HEADER] or [HEADER.FIELDS (Header list)] or 
+        # [HEADER.FIELDS.NOT (Header list)], [TEXT], or [MIME]
+        l = s.lower()
+        used = 0
+        if l.startswith('header]'):
+            o = self.pending_body.header = self.Header()
+            used += 7
+        elif l.startswith('text]'):
+            o = self.pending_body.text = self.Text()
+            used += 5
+        elif l.startswith('mime]'):
+            o = self.pending_body.mime = self.MIME()
+            used += 5
+        else:
+            o = h = self.Header()
+            if l.startswith('header.fields.not'):
+                h.negate = True
+                used += 17
+            elif l.startswith('header.fields'):
+                used += 13
+            else:
+                raise Exception("Unhandled section contents")
+            
+            self.pending_body.header = h
+            self.state.extend(('finish_section', 'header_list', 'whitespace'))
+        self.pending_body.part = tuple(self.parts)
+        self.parts = None
+        return used
+    
+    def state_finish_section(self, s):
+        if not s.startswith(']'):
+            raise Exception("section must end with ]")
+        return 1
+
+    def state_header_list(self, s):
+        if not s.startswith('('):
+            raise Exception("Header list must begin with (")
+        end = s.find(')')
+        if end == -1:
+            raise Exception("Header list must end with )")
+        
+        headers = s[1:end].split()
+        self.pending_body.header.fields = map(str.upper, headers)
+        return end + 1
+    
+    def state_maybe_partial(self, s):
+        # Grab <number.number> or nothing at all
+        if not s.startswith('<'):
+            return 0
+        end = s.find('>')
+        if end == -1:
+            raise Exception("Found < but not >")
+        
+        partial = s[1:end]
+        parts = partial.split('.', 1)
+        if len(parts) != 2:
+            raise Exception("Partial specification did not include two .-delimited integers")
+        begin, length = map(int, parts)
+        self.pending_body.partialBegin = begin
+        self.pending_body.partialLength = length
+        
+        return end + 1
+
+import codecs
+def modified_base64(s):
+    return binascii.b2a_base64(s)[:-1].rstrip('=').replace('/', ',')
+
+def modified_unbase64(s):
+    return binascii.a2b_base64(s.replace(',', '/') + '===')
+
+def encoder(s):
+    r = []
+    _in = []
+    for c in s:
+        if ord(c) in (range(0x20, 0x25) + range(0x27, 0x7e)):
+            if _in:
+                r.extend(['&', modified_base64(''.join(_in)), '-'])
+                del _in[:]
+            r.append(c)
+        elif c == '&':
+            if _in:
+                r.extend(['&', modified_base64(''.join(_in)), '-'])
+                del _in[:]
+            r.append('&-')
+        else:
+            _in.append(c)
+    if _in:
+        r.extend(['&', modified_base64(''.join(_in)), '-'])
+    return (''.join(r), len(s))
+
+def decoder(s):
+    r = []
+    decode = []
+    for c in s:
+        if c == '&' and not decode:
+            decode.append('&')
+        elif c == '-' and decode:
+            if len(decode) == 1:
+                r.append('&')
+            else:
+                r.append(modified_unbase64(''.join(decode[1:])))
+            decode = []
+        elif decode:
+            decode.append(c)
+        else:
+            r.append(c)
+    if decode:
+        r.append(modified_unbase64(''.join(decode[1:])))
+    return (''.join(r), len(s))
+
+class StreamReader(codecs.StreamReader):
+    def decode(self, s, errors='strict'):
+        return decoder(s)
+
+class StreamWriter(codecs.StreamWriter):
+    def decode(self, s, errors='strict'):
+        return encoder(s)
+
+def imap4_utf_7(name):
+    if name == 'imap4-utf-7':
+        return (encoder, decoder, StreamReader, StreamWriter)
+codecs.register(imap4_utf_7)
+
+__all__ = [
+    'IMAP4Server', 'IMAP4Client', 'IMAP4Exception', 'IllegalClientResponse',
+    'IllegalOperation', 'IllegalMailboxEncoding', 'IMailboxListener',
+    'UnhandledResponse', 'NegativeResponse', 'NoSupportedAuthentication',
+    'IllegalServerResponse', 'IllegalIdentifierError', 'IllegalQueryError',
+    'MismatchedNesting', 'MismatchedQuoting', 'IClientAuthentication',
+    'CramMD5ClientAuthenticator', 'MailboxException', 'MailboxCollision',
+    'NoSuchMailbox', 'ReadOnlyMailbox', 'IAccount', 'MemoryAccount',
+    'IMailbox', 'statusRequestHelper',
+]