Index: third_party/twisted_8_1/twisted/mail/imap4.py |
diff --git a/third_party/twisted_8_1/twisted/mail/imap4.py b/third_party/twisted_8_1/twisted/mail/imap4.py |
deleted file mode 100644 |
index bb3a15600259492bf4390a01d3040ecae7b62971..0000000000000000000000000000000000000000 |
--- a/third_party/twisted_8_1/twisted/mail/imap4.py |
+++ /dev/null |
@@ -1,5490 +0,0 @@ |
-# -*- test-case-name: twisted.mail.test.test_imap -*- |
-# Copyright (c) 2001-2008 Twisted Matrix Laboratories. |
-# See LICENSE for details. |
- |
- |
-""" |
-An IMAP4 protocol implementation |
- |
-@author: U{Jp Calderone<mailto:exarkun@twistedmatrix.com>} |
- |
-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 |
-""" |
- |
-import rfc822 |
-import base64 |
-import binascii |
-import hmac |
-import re |
-import tempfile |
-import string |
-import time |
-import random |
-import types |
- |
-import email.Utils |
- |
-try: |
- import cStringIO as StringIO |
-except: |
- import StringIO |
- |
-from zope.interface import implements, Interface |
- |
-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, text |
-from twisted.internet import interfaces |
- |
-from twisted import cred |
-import twisted.cred.error |
-import twisted.cred.credentials |
- |
-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. The argument is provided |
- as a hint to the implementation and may be ignored if it makes sense |
- to do so (eg, if an iterator is being used that maintains its own |
- state, it is guaranteed that it will not be called out-of-order). |
- """ |
- _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 enumerate(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 enumerate(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 WriteBuffer: |
- """Buffer up a bunch of writes before sending them all to a transport at once. |
- """ |
- def __init__(self, transport, size=8192): |
- self.bufferSize = size |
- self.transport = transport |
- self._length = 0 |
- self._writes = [] |
- |
- def write(self, s): |
- self._length += len(s) |
- self._writes.append(s) |
- if self._length > self.bufferSize: |
- self.flush() |
- |
- def flush(self): |
- if self._writes: |
- self.transport.writeSequence(self._writes) |
- self._writes = [] |
- self._length = 0 |
- |
- |
-class Command: |
- _1_RESPONSES = ('CAPABILITY', 'FLAGS', 'LIST', 'LSUB', 'STATUS', 'SEARCH', 'NAMESPACE') |
- _2_RESPONSES = ('EXISTS', 'EXPUNGE', 'FETCH', 'RECENT') |
- _OK_RESPONSES = ('UIDVALIDITY', 'READ-WRITE', 'READ-ONLY', 'UIDNEXT', 'PERMANENTFLAGS') |
- defer = None |
- |
- def __init__(self, command, args=None, wantResponse=(), |
- continuation=None, *contArgs, **contKw): |
- self.command = command |
- self.args = args |
- self.wantResponse = wantResponse |
- self.continuation = lambda x: continuation(x, *contArgs, **contKw) |
- self.lines = [] |
- |
- def format(self, tag): |
- if self.args is None: |
- return ' '.join((tag, self.command)) |
- return ' '.join((tag, self.command, self.args)) |
- |
- 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[0] == 'OK' and isinstance(names[1], types.ListType) and names[1][0] in self._OK_RESPONSES): |
- send.append(L) |
- elif N >= 3 and names[1] in self._2_RESPONSES: |
- if isinstance(names[2], list) and len(names[2]) >= 1 and names[2][0] == 'FLAGS' and 'FLAGS' not in self.args: |
- unuse.append(L) |
- else: |
- send.append(L) |
- elif N >= 2 and names[1] in self._2_RESPONSES: |
- send.append(L) |
- else: |
- unuse.append(L) |
- d, self.defer = self.defer, None |
- d.callback((send, lastLine)) |
- if unuse: |
- unusedCallback(unuse) |
- |
-class LOGINCredentials(cred.credentials.UsernamePassword): |
- def __init__(self): |
- self.challenges = ['Password\0', 'User Name\0'] |
- self.responses = ['password', 'username'] |
- cred.credentials.UsernamePassword.__init__(self, None, None) |
- |
- def getChallenge(self): |
- return self.challenges.pop() |
- |
- def setResponse(self, response): |
- setattr(self, self.responses.pop(), response) |
- |
- def moreChallenges(self): |
- return bool(self.challenges) |
- |
-class PLAINCredentials(cred.credentials.UsernamePassword): |
- def __init__(self): |
- cred.credentials.UsernamePassword.__init__(self, None, None) |
- |
- def getChallenge(self): |
- return '' |
- |
- def setResponse(self, response): |
- parts = response[:-1].split('\0', 1) |
- if len(parts) != 2: |
- raise IllegalClientResponse("Malformed Response - wrong number of parts") |
- self.username, self.password = parts |
- |
- def moreChallenges(self): |
- return False |
- |
-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(Interface): |
- """Interface for objects interested in mailbox events""" |
- |
- def modeChanged(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(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 |
- now set on that message. |
- """ |
- |
- def newMessages(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' |
- |
- parseState = 'command' |
- |
- def __init__(self, chal = None, contextFactory = None, scheduler = None): |
- if chal is None: |
- chal = {} |
- self.challengers = chal |
- self.ctx = contextFactory |
- if scheduler is None: |
- scheduler = iterateInReactor |
- self._scheduler = scheduler |
- self._queuedAsync = [] |
- |
- def capabilities(self): |
- cap = {'AUTH': self.challengers.keys()} |
- if self.ctx and self.canStartTLS: |
- if not self.startedTLS and interfaces.ISSLTransport(self.transport, None) is None: |
- cap['LOGINDISABLED'] = None |
- cap['STARTTLS'] = None |
- cap['NAMESPACE'] = None |
- cap['IDLE'] = None |
- return cap |
- |
- def connectionMade(self): |
- self.tags = {} |
- self.canStartTLS = interfaces.ITLSTransport(self.transport, None) is not None |
- self.setTimeout(self.timeOut) |
- self.sendServerGreeting() |
- |
- 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) |
- cmbx = ICloseableMailbox(self.mbox, None) |
- if cmbx is not None: |
- maybeDeferred(cmbx.close).addErrback(log.err) |
- self.mbox = None |
- self.state = 'timeout' |
- |
- def rawDataReceived(self, data): |
- self.resetTimeout() |
- passon = self._pendingLiteral.write(data) |
- if passon is not None: |
- self.setLineMode(passon) |
- |
- # Avoid processing commands while buffers are being dumped to |
- # our transport |
- blocked = None |
- |
- def _unblock(self): |
- commands = self.blocked |
- self.blocked = None |
- while commands and self.blocked is None: |
- self.lineReceived(commands.pop(0)) |
- if self.blocked is not None: |
- self.blocked.extend(commands) |
- |
-# def sendLine(self, line): |
-# print 'C:', repr(line) |
-# return basic.LineReceiver.sendLine(self, line) |
- |
- def lineReceived(self, line): |
-# print 'S:', repr(line) |
- if self.blocked is not None: |
- self.blocked.append(line) |
- return |
- |
- self.resetTimeout() |
- |
- f = getattr(self, 'parse_' + self.parseState) |
- try: |
- f(line) |
- except Exception, e: |
- self.sendUntaggedResponse('BAD Server error: ' + str(e)) |
- log.err() |
- |
- def parse_command(self, line): |
- 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 None |
- else: |
- self.sendBadResponse(None, 'Null command') |
- return None |
- |
- cmd = cmd.upper() |
- try: |
- return self.dispatchCommand(tag, cmd, rest) |
- 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)) |
- |
- def parse_pending(self, line): |
- d = self._pendingLiteral |
- self._pendingLiteral = None |
- self.parseState = 'command' |
- d.callback(line) |
- |
- def dispatchCommand(self, tag, cmd, rest, uid=None): |
- f = self.lookupCommand(cmd) |
- if f: |
- fn = f[0] |
- parseargs = f[1:] |
- self.__doCommand(tag, fn, [self, tag], parseargs, rest, uid) |
- 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 enumerate(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: " + repr(line)) |
- |
- 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.parseState = 'pending' |
- 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.parseState = 'pending' |
- 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.strip() |
- 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] |
- |
- try: |
- return (parseIdList(arg), rest) |
- except IllegalIdentifierError, e: |
- raise IllegalClientResponse("Bad message number " + str(e)) |
- |
- 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, async=False): |
- if not async or (self.blocked is None): |
- self._respond(message, None, None) |
- else: |
- self._queuedAsync.append(message) |
- |
- def sendContinuationRequest(self, msg = 'Ready for additional command text'): |
- if msg: |
- self.sendLine('+ ' + msg) |
- else: |
- self.sendLine('+') |
- |
- def _respond(self, state, tag, message): |
- if state in ('OK', 'NO', 'BAD') and self._queuedAsync: |
- lines = self._queuedAsync |
- self._queuedAsync = [] |
- for msg in lines: |
- self._respond(msg, None, None) |
- 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 |
- |
- self._setupChallenge(chal, tag) |
- |
- def _setupChallenge(self, chal, tag): |
- try: |
- challenge = chal.getChallenge() |
- except Exception, e: |
- self.sendBadResponse(tag, 'Server error: ' + str(e)) |
- else: |
- coded = base64.encodestring(challenge)[:-1] |
- self.parseState = 'pending' |
- self._pendingLiteral = defer.Deferred() |
- self.sendContinuationRequest(coded) |
- self._pendingLiteral.addCallback(self.__cbAuthChunk, chal, tag) |
- self._pendingLiteral.addErrback(self.__ebAuthChunk, tag) |
- |
- def __cbAuthChunk(self, result, chal, tag): |
- try: |
- uncoded = base64.decodestring(result) |
- except binascii.Error: |
- raise IllegalClientResponse("Malformed Response - not base64") |
- |
- chal.setResponse(uncoded) |
- if chal.moreChallenges(): |
- self._setupChallenge(chal, tag) |
- else: |
- 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: unauthorized') |
- elif failure.check(cred.error.UnhandledCredentials): |
- self.sendNegativeResponse(tag, 'Authentication failed: server misconfigured') |
- else: |
- self.sendBadResponse(tag, 'Server error: login failed unexpectedly') |
- 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 |
- self.challengers = self.challengers.copy() |
- if 'LOGIN' not in self.challengers: |
- self.challengers['LOGIN'] = LOGINCredentials |
- if 'PLAIN' not in self.challengers: |
- self.challengers['PLAIN'] = PLAINCredentials |
- 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 |
- ).addCallback(self.__cbLogin, tag |
- ).addErrback(self.__ebLogin, tag |
- ) |
- |
- 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: login returned unexpected value') |
- 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 do_NAMESPACE(self, tag): |
- personal = public = shared = None |
- np = INamespacePresenter(self.account, None) |
- if np is not None: |
- personal = np.getPersonalNamespaces() |
- public = np.getSharedNamespaces() |
- shared = np.getSharedNamespaces() |
- self.sendUntaggedResponse('NAMESPACE ' + collapseNestedLists([personal, public, shared])) |
- self.sendPositiveResponse(tag, "NAMESPACE command completed") |
- |
- auth_NAMESPACE = (do_NAMESPACE,) |
- select_NAMESPACE = auth_NAMESPACE |
- |
- def _parseMbox(self, name): |
- if isinstance(name, unicode): |
- return name |
- try: |
- return name.decode('imap4-utf-7') |
- except: |
- log.err() |
- raise IllegalMailboxEncoding(name) |
- |
- def _selectWork(self, tag, name, rw, cmdName): |
- if self.mbox: |
- self.mbox.removeListener(self) |
- cmbx = ICloseableMailbox(self.mbox, None) |
- if cmbx is not None: |
- maybeDeferred(cmbx.close).addErrback(log.err) |
- self.mbox = None |
- self.state = 'auth' |
- |
- name = self._parseMbox(name) |
- 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 |
- if '\\noselect' in [s.lower() for s in mbox.getFlags()]: |
- self.sendNegativeResponse(tag, 'Mailbox cannot be selected') |
- return |
- |
- 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' |
- mbox.addListener(self) |
- self.sendPositiveResponse(tag, '[%s] %s successful' % (s, cmdName)) |
- self.state = 'select' |
- self.mbox = mbox |
- |
- auth_SELECT = ( _selectWork, arg_astring, 1, 'SELECT' ) |
- select_SELECT = auth_SELECT |
- |
- auth_EXAMINE = ( _selectWork, arg_astring, 0, 'EXAMINE' ) |
- select_EXAMINE = auth_EXAMINE |
- |
- |
- def do_IDLE(self, tag): |
- self.sendContinuationRequest(None) |
- self.parseTag = tag |
- self.lastState = self.parseState |
- self.parseState = 'idle' |
- |
- def parse_idle(self, *args): |
- self.parseState = self.lastState |
- del self.lastState |
- self.sendPositiveResponse(self.parseTag, "IDLE terminated") |
- del self.parseTag |
- |
- select_IDLE = ( do_IDLE, ) |
- auth_IDLE = select_IDLE |
- |
- |
- def do_CREATE(self, tag, name): |
- name = self._parseMbox(name) |
- try: |
- result = self.account.create(name) |
- except MailboxException, c: |
- self.sendNegativeResponse(tag, str(c)) |
- except: |
- self.sendBadResponse(tag, "Server error encountered while creating mailbox") |
- log.err() |
- else: |
- if result: |
- self.sendPositiveResponse(tag, 'Mailbox created') |
- else: |
- self.sendNegativeResponse(tag, 'Mailbox not created') |
- |
- auth_CREATE = (do_CREATE, arg_astring) |
- select_CREATE = auth_CREATE |
- |
- def do_DELETE(self, tag, name): |
- name = self._parseMbox(name) |
- if name.lower() == 'inbox': |
- self.sendNegativeResponse(tag, 'You cannot delete the inbox') |
- return |
- try: |
- self.account.delete(name) |
- except MailboxException, m: |
- self.sendNegativeResponse(tag, str(m)) |
- except: |
- self.sendBadResponse(tag, "Server error encountered while deleting mailbox") |
- log.err() |
- 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] |
- if oldname.lower() == 'inbox' or newname.lower() == 'inbox': |
- self.sendNegativeResponse(tag, 'You cannot rename the inbox, or rename another mailbox to inbox.') |
- return |
- try: |
- self.account.rename(oldname, newname) |
- except TypeError: |
- self.sendBadResponse(tag, 'Invalid command syntax') |
- except MailboxException, m: |
- self.sendNegativeResponse(tag, str(m)) |
- except: |
- self.sendBadResponse(tag, "Server error encountered while renaming mailbox") |
- log.err() |
- 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)) |
- except: |
- self.sendBadResponse(tag, "Server error encountered while subscribing to mailbox") |
- log.err() |
- 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)) |
- except: |
- self.sendBadResponse(tag, "Server error encountered while unsubscribing from mailbox") |
- log.err() |
- 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) |
- maybeDeferred(self.account.listMailboxes, ref, mbox |
- ).addCallback(self._cbListWork, tag, sub, cmdName |
- ).addErrback(self._ebListWork, tag |
- ) |
- |
- def _cbListWork(self, mailboxes, tag, sub, cmdName): |
- for (name, box) in mailboxes: |
- if not sub or self.account.isSubscribed(name): |
- flags = box.getFlags() |
- delim = box.getHierarchicalDelimiter() |
- resp = (DontQuoteMe(cmdName), map(DontQuoteMe, flags), delim, name.encode('imap4-utf-7')) |
- self.sendUntaggedResponse(collapseNestedLists(resp)) |
- self.sendPositiveResponse(tag, '%s completed' % (cmdName,)) |
- |
- def _ebListWork(self, failure, tag): |
- self.sendBadResponse(tag, "Server error encountered while listing mailboxes.") |
- log.err(failure) |
- |
- 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) |
- maybeDeferred(self.account.select, mailbox, 0 |
- ).addCallback(self._cbStatusGotMailbox, tag, mailbox, names |
- ).addErrback(self._ebStatusGotMailbox, tag |
- ) |
- |
- def _cbStatusGotMailbox(self, mbox, tag, mailbox, names): |
- 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") |
- |
- def _ebStatusGotMailbox(self, failure, tag): |
- self.sendBadResponse(tag, "Server error encountered while opening mailbox.") |
- log.err(failure) |
- |
- 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, str(failure.value))) |
- |
- def do_APPEND(self, tag, mailbox, flags, date, message): |
- mailbox = self._parseMbox(mailbox) |
- maybeDeferred(self.account.select, mailbox |
- ).addCallback(self._cbAppendGotMailbox, tag, flags, date, message |
- ).addErrback(self._ebAppendGotMailbox, tag |
- ) |
- |
- def _cbAppendGotMailbox(self, mbox, tag, flags, date, message): |
- 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) |
- |
- def _ebAppendGotMailbox(self, failure, tag): |
- self.sendBadResponse(tag, "Server error encountered while opening mailbox.") |
- log.err(failure) |
- |
- 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): |
- d = None |
- if self.mbox.isWriteable(): |
- d = maybeDeferred(self.mbox.expunge) |
- cmbx = ICloseableMailbox(self.mbox, None) |
- if cmbx is not None: |
- if d is not None: |
- d.addCallback(lambda result: cmbx.close()) |
- else: |
- d = maybeDeferred(cmbx.close) |
- if d is not None: |
- d.addCallbacks(self.__cbClose, self.__ebClose, (tag,), None, (tag,), None) |
- else: |
- self.__cbClose(None, tag) |
- |
- 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.sendNegativeResponse(tag, 'EXPUNGE ignored on read-only mailbox') |
- |
- select_EXPUNGE = (do_EXPUNGE,) |
- |
- def __cbExpunge(self, result, tag): |
- for e in result: |
- self.sendUntaggedResponse('%d EXPUNGE' % e) |
- self.sendPositiveResponse(tag, 'EXPUNGE completed') |
- |
- 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): |
- sm = ISearchableMailbox(self.mbox, None) |
- if sm is not None: |
- maybeDeferred(sm.search, query, uid=uid).addCallbacks( |
- self.__cbSearch, self.__ebSearch, |
- (tag, self.mbox, uid), None, (tag,), None |
- ) |
- else: |
- s = parseIdList('1:*') |
- maybeDeferred(self.mbox.fetch, s, uid=uid).addCallbacks( |
- self.__cbManualSearch, self.__ebSearch, |
- (tag, self.mbox, query, 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 __cbManualSearch(self, result, tag, mbox, query, uid, searchResults = None): |
- if searchResults is None: |
- searchResults = [] |
- i = 0 |
- for (i, (id, msg)) in zip(range(5), result): |
- if self.searchFilter(query, id, msg): |
- if uid: |
- searchResults.append(str(msg.getUID())) |
- else: |
- searchResults.append(str(id)) |
- if i == 4: |
- from twisted.internet import reactor |
- reactor.callLater(0, self.__cbManualSearch, result, tag, mbox, query, uid, searchResults) |
- else: |
- if searchResults: |
- self.sendUntaggedResponse('SEARCH ' + ' '.join(searchResults)) |
- self.sendPositiveResponse(tag, 'SEARCH completed') |
- |
- def searchFilter(self, query, id, msg): |
- while query: |
- if not self.singleSearchStep(query, id, msg): |
- return False |
- return True |
- |
- def singleSearchStep(self, query, id, msg): |
- q = query.pop(0) |
- if isinstance(q, list): |
- if not self.searchFilter(q, id, msg): |
- return False |
- else: |
- c = q.upper() |
- f = getattr(self, 'search_' + c) |
- if f: |
- if not f(query, id, msg): |
- return False |
- else: |
- # IMAP goes *out of its way* to be complex |
- # Sequence sets to search should be specified |
- # with a command, like EVERYTHING ELSE. |
- try: |
- m = parseIdList(c) |
- except: |
- log.err('Unknown search term: ' + c) |
- else: |
- if id not in m: |
- return False |
- return True |
- |
- def search_ALL(self, query, id, msg): |
- return True |
- |
- def search_ANSWERED(self, query, id, msg): |
- return '\\Answered' in msg.getFlags() |
- |
- def search_BCC(self, query, id, msg): |
- bcc = msg.getHeaders(False, 'bcc').get('bcc', '') |
- return bcc.lower().find(query.pop(0).lower()) != -1 |
- |
- def search_BEFORE(self, query, id, msg): |
- date = parseTime(query.pop(0)) |
- return rfc822.parsedate(msg.getInternalDate()) < date |
- |
- def search_BODY(self, query, id, msg): |
- body = query.pop(0).lower() |
- return text.strFile(body, msg.getBodyFile(), False) |
- |
- def search_CC(self, query, id, msg): |
- cc = msg.getHeaders(False, 'cc').get('cc', '') |
- return cc.lower().find(query.pop(0).lower()) != -1 |
- |
- def search_DELETED(self, query, id, msg): |
- return '\\Deleted' in msg.getFlags() |
- |
- def search_DRAFT(self, query, id, msg): |
- return '\\Draft' in msg.getFlags() |
- |
- def search_FLAGGED(self, query, id, msg): |
- return '\\Flagged' in msg.getFlags() |
- |
- def search_FROM(self, query, id, msg): |
- fm = msg.getHeaders(False, 'from').get('from', '') |
- return fm.lower().find(query.pop(0).lower()) != -1 |
- |
- def search_HEADER(self, query, id, msg): |
- hdr = query.pop(0).lower() |
- hdr = msg.getHeaders(False, hdr).get(hdr, '') |
- return hdr.lower().find(query.pop(0).lower()) != -1 |
- |
- def search_KEYWORD(self, query, id, msg): |
- query.pop(0) |
- return False |
- |
- def search_LARGER(self, query, id, msg): |
- return int(query.pop(0)) < msg.getSize() |
- |
- def search_NEW(self, query, id, msg): |
- return '\\Recent' in msg.getFlags() and '\\Seen' not in msg.getFlags() |
- |
- def search_NOT(self, query, id, msg): |
- return not self.singleSearchStep(query, id, msg) |
- |
- def search_OLD(self, query, id, msg): |
- return '\\Recent' not in msg.getFlags() |
- |
- def search_ON(self, query, id, msg): |
- date = parseTime(query.pop(0)) |
- return rfc822.parsedate(msg.getInternalDate()) == date |
- |
- def search_OR(self, query, id, msg): |
- a = self.singleSearchStep(query, id, msg) |
- b = self.singleSearchStep(query, id, msg) |
- return a or b |
- |
- def search_RECENT(self, query, id, msg): |
- return '\\Recent' in msg.getFlags() |
- |
- def search_SEEN(self, query, id, msg): |
- return '\\Seen' in msg.getFlags() |
- |
- def search_SENTBEFORE(self, query, id, msg): |
- date = msg.getHeader(False, 'date').get('date', '') |
- date = rfc822.parsedate(date) |
- return date < parseTime(query.pop(0)) |
- |
- def search_SENTON(self, query, id, msg): |
- date = msg.getHeader(False, 'date').get('date', '') |
- date = rfc822.parsedate(date) |
- return date[:3] == parseTime(query.pop(0))[:3] |
- |
- def search_SENTSINCE(self, query, id, msg): |
- date = msg.getHeader(False, 'date').get('date', '') |
- date = rfc822.parsedate(date) |
- return date > parseTime(query.pop(0)) |
- |
- def search_SINCE(self, query, id, msg): |
- date = parseTime(query.pop(0)) |
- return rfc822.parsedate(msg.getInternalDate()) > date |
- |
- def search_SMALLER(self, query, id, msg): |
- return int(query.pop(0)) > msg.getSize() |
- |
- def search_SUBJECT(self, query, id, msg): |
- subj = msg.getHeaders(False, 'subject').get('subject', '') |
- return subj.lower().find(query.pop(0).lower()) != -1 |
- |
- def search_TEXT(self, query, id, msg): |
- # XXX - This must search headers too |
- body = query.pop(0).lower() |
- return text.strFile(body, msg.getBodyFile(), False) |
- |
- def search_TO(self, query, id, msg): |
- to = msg.getHeaders(False, 'to').get('to', '') |
- return to.lower().find(query.pop(0).lower()) != -1 |
- |
- def search_UID(self, query, id, msg): |
- c = query.pop(0) |
- m = parseIdList(c) |
- return msg.getUID() in m |
- |
- def search_UNANSWERED(self, query, id, msg): |
- return '\\Answered' not in msg.getFlags() |
- |
- def search_UNDELETED(self, query, id, msg): |
- return '\\Deleted' not in msg.getFlags() |
- |
- def search_UNDRAFT(self, query, id, msg): |
- return '\\Draft' not in msg.getFlags() |
- |
- def search_UNFLAGGED(self, query, id, msg): |
- return '\\Flagged' not in msg.getFlags() |
- |
- def search_UNKEYWORD(self, query, id, msg): |
- query.pop(0) |
- return False |
- |
- def search_UNSEEN(self, query, id, msg): |
- return '\\Seen' not in msg.getFlags() |
- |
- 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 query: |
- self._oldTimeout = self.setTimeout(None) |
- maybeDeferred(self.mbox.fetch, messages, uid=uid |
- ).addCallback(iter |
- ).addCallback(self.__cbFetch, tag, query, uid |
- ).addErrback(self.__ebFetch, tag |
- ) |
- else: |
- self.sendPositiveResponse(tag, 'FETCH complete') |
- |
- select_FETCH = (do_FETCH, arg_seqset, arg_fetchatt) |
- |
- def __cbFetch(self, results, tag, query, uid): |
- if self.blocked is None: |
- self.blocked = [] |
- try: |
- id, msg = results.next() |
- except StopIteration: |
- # The idle timeout was suspended while we delivered results, |
- # restore it now. |
- self.setTimeout(self._oldTimeout) |
- del self._oldTimeout |
- |
- # All results have been processed, deliver completion notification. |
- |
- # It's important to run this *after* resetting the timeout to "rig |
- # a race" in some test code. writing to the transport will |
- # synchronously call test code, which synchronously loses the |
- # connection, calling our connectionLost method, which cancels the |
- # timeout. We want to make sure that timeout is cancelled *after* |
- # we reset it above, so that the final state is no timed |
- # calls. This avoids reactor uncleanliness errors in the test |
- # suite. |
- # XXX: Perhaps loopback should be fixed to not call the user code |
- # synchronously in transport.write? |
- self.sendPositiveResponse(tag, 'FETCH completed') |
- |
- # Instance state is now consistent again (ie, it is as though |
- # the fetch command never ran), so allow any pending blocked |
- # commands to execute. |
- self._unblock() |
- else: |
- self.spewMessage(id, msg, query, uid |
- ).addCallback(lambda _: self.__cbFetch(results, tag, query, uid) |
- ).addErrback(self.__ebSpewMessage |
- ) |
- |
- def __ebSpewMessage(self, failure): |
- # This indicates a programming error. |
- # There's no reliable way to indicate anything to the client, since we |
- # may have already written an arbitrary amount of data in response to |
- # the command. |
- log.err(failure) |
- self.transport.loseConnection() |
- |
- def spew_envelope(self, id, msg, _w=None, _f=None): |
- if _w is None: |
- _w = self.transport.write |
- _w('ENVELOPE ' + collapseNestedLists([getEnvelope(msg)])) |
- |
- def spew_flags(self, id, msg, _w=None, _f=None): |
- if _w is None: |
- _w = self.transport.write |
- _w('FLAGS ' + '(%s)' % (' '.join(msg.getFlags()))) |
- |
- def spew_internaldate(self, id, msg, _w=None, _f=None): |
- if _w is None: |
- _w = self.transport.write |
- idate = msg.getInternalDate() |
- ttup = rfc822.parsedate_tz(idate) |
- if ttup is None: |
- log.msg("%d:%r: unpareseable internaldate: %r" % (id, msg, idate)) |
- raise IMAP4Exception("Internal failure generating INTERNALDATE") |
- |
- odate = time.strftime("%d-%b-%Y %H:%M:%S ", ttup[:9]) |
- if ttup[9] is None: |
- odate = odate + "+0000" |
- else: |
- if ttup[9] >= 0: |
- sign = "+" |
- else: |
- sign = "-" |
- odate = odate + sign + string.zfill(str(((abs(ttup[9]) / 3600) * 100 + (abs(ttup[9]) % 3600) / 60)), 4) |
- _w('INTERNALDATE ' + _quote(odate)) |
- |
- def spew_rfc822header(self, id, msg, _w=None, _f=None): |
- if _w is None: |
- _w = self.transport.write |
- hdrs = _formatHeaders(msg.getHeaders(True)) |
- _w('RFC822.HEADER ' + _literal(hdrs)) |
- |
- def spew_rfc822text(self, id, msg, _w=None, _f=None): |
- if _w is None: |
- _w = self.transport.write |
- _w('RFC822.TEXT ') |
- _f() |
- return FileProducer(msg.getBodyFile() |
- ).beginProducing(self.transport |
- ) |
- |
- def spew_rfc822size(self, id, msg, _w=None, _f=None): |
- if _w is None: |
- _w = self.transport.write |
- _w('RFC822.SIZE ' + str(msg.getSize())) |
- |
- def spew_rfc822(self, id, msg, _w=None, _f=None): |
- if _w is None: |
- _w = self.transport.write |
- _w('RFC822 ') |
- _f() |
- mf = IMessageFile(msg, None) |
- if mf is not None: |
- return FileProducer(mf.open() |
- ).beginProducing(self.transport |
- ) |
- return MessageProducer(msg, None, self._scheduler |
- ).beginProducing(self.transport |
- ) |
- |
- def spew_uid(self, id, msg, _w=None, _f=None): |
- if _w is None: |
- _w = self.transport.write |
- _w('UID ' + str(msg.getUID())) |
- |
- def spew_bodystructure(self, id, msg, _w=None, _f=None): |
- _w('BODYSTRUCTURE ' + collapseNestedLists([getBodyStructure(msg, True)])) |
- |
- def spew_body(self, part, id, msg, _w=None, _f=None): |
- if _w is None: |
- _w = self.transport.write |
- for p in part.part: |
- if msg.isMultipart(): |
- msg = msg.getSubPart(p) |
- elif p > 0: |
- # Non-multipart messages have an implicit first part but no |
- # other parts - reject any request for any other part. |
- raise TypeError("Requested subpart of non-multipart message") |
- |
- if part.header: |
- hdrs = msg.getHeaders(part.header.negate, *part.header.fields) |
- hdrs = _formatHeaders(hdrs) |
- _w(str(part) + ' ' + _literal(hdrs)) |
- elif part.text: |
- _w(str(part) + ' ') |
- _f() |
- return FileProducer(msg.getBodyFile() |
- ).beginProducing(self.transport |
- ) |
- elif part.mime: |
- hdrs = _formatHeaders(msg.getHeaders(True)) |
- _w(str(part) + ' ' + _literal(hdrs)) |
- elif part.empty: |
- _w(str(part) + ' ') |
- _f() |
- if part.part: |
- return FileProducer(msg.getBodyFile() |
- ).beginProducing(self.transport |
- ) |
- else: |
- mf = IMessageFile(msg, None) |
- if mf is not None: |
- return FileProducer(mf.open()).beginProducing(self.transport) |
- return MessageProducer(msg, None, self._scheduler).beginProducing(self.transport) |
- |
- else: |
- _w('BODY ' + collapseNestedLists([getBodyStructure(msg)])) |
- |
- def spewMessage(self, id, msg, query, uid): |
- wbuf = WriteBuffer(self.transport) |
- write = wbuf.write |
- flush = wbuf.flush |
- def start(): |
- write('* %d FETCH (' % (id,)) |
- def finish(): |
- write(')\r\n') |
- def space(): |
- write(' ') |
- |
- def spew(): |
- seenUID = False |
- start() |
- for part in query: |
- if part.type == 'uid': |
- seenUID = True |
- if part.type == 'body': |
- yield self.spew_body(part, id, msg, write, flush) |
- else: |
- f = getattr(self, 'spew_' + part.type) |
- yield f(id, msg, write, flush) |
- if part is not query[-1]: |
- space() |
- if uid and not seenUID: |
- space() |
- yield self.spew_uid(id, msg, write, flush) |
- finish() |
- flush() |
- return self._scheduler(spew()) |
- |
- def __ebFetch(self, failure, tag): |
- self.setTimeout(self._oldTimeout) |
- del self._oldTimeout |
- 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) |
- maybeDeferred(self.account.select, mailbox |
- ).addCallback(self._cbCopySelectedMailbox, tag, messages, mailbox, uid |
- ).addErrback(self._ebCopySelectedMailbox, tag |
- ) |
- select_COPY = (do_COPY, arg_seqset, arg_astring) |
- |
- def _cbCopySelectedMailbox(self, mbox, tag, messages, mailbox, uid): |
- if not mbox: |
- self.sendNegativeResponse(tag, 'No such mailbox: ' + mailbox) |
- else: |
- maybeDeferred(self.mbox.fetch, messages, uid |
- ).addCallback(self.__cbCopy, tag, mbox |
- ).addCallback(self.__cbCopied, tag, mbox |
- ).addErrback(self.__ebCopy, tag |
- ) |
- |
- def _ebCopySelectedMailbox(self, failure, tag): |
- self.sendBadResponse(tag, 'Server error: ' + str(failure.value)) |
- |
- def __cbCopy(self, messages, tag, mbox): |
- # XXX - This should handle failures with a rollback or something |
- addedDeferreds = [] |
- addedIDs = [] |
- failures = [] |
- |
- fastCopyMbox = IMessageCopier(mbox, None) |
- for (id, msg) in messages: |
- if fastCopyMbox is not None: |
- d = maybeDeferred(fastCopyMbox.copy, msg) |
- addedDeferreds.append(d) |
- continue |
- |
- # XXX - The following should be an implementation of IMessageCopier.copy |
- # on an IMailbox->IMessageCopier adapter. |
- |
- flags = msg.getFlags() |
- date = msg.getInternalDate() |
- |
- body = IMessageFile(msg, None) |
- if body is not None: |
- bodyFile = body.open() |
- d = maybeDeferred(mbox.addMessage, bodyFile, flags, date) |
- else: |
- def rewind(f): |
- f.seek(0) |
- return f |
- buffer = tempfile.TemporaryFile() |
- d = MessageProducer(msg, buffer, self._scheduler |
- ).beginProducing(None |
- ).addCallback(lambda _, b=buffer, f=flags, d=date: mbox.addMessage(rewind(b), f, d) |
- ) |
- addedDeferreds.append(d) |
- return defer.DeferredList(addedDeferreds) |
- |
- def __cbCopied(self, deferredIds, tag, mbox): |
- ids = [] |
- failures = [] |
- for (status, result) 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:' + str(failure.value)) |
- 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.sendUntaggedResponse(message='[READ-WRITE]', async=True) |
- else: |
- self.sendUntaggedResponse(message='[READ-ONLY]', async=True) |
- |
- def flagsChanged(self, newFlags): |
- for (mId, flags) in newFlags.iteritems(): |
- msg = '%d FETCH (FLAGS (%s))' % (mId, ' '.join(flags)) |
- self.sendUntaggedResponse(msg, async=True) |
- |
- def newMessages(self, exists, recent): |
- if exists is not None: |
- self.sendUntaggedResponse('%d EXISTS' % exists, async=True) |
- if recent is not None: |
- self.sendUntaggedResponse('%d RECENT' % recent, async=True) |
- |
- |
-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 |
- |
- def __str__(self): |
- return (IMAP4Exception.__str__(self) |
- + ': Server supports %r, client supports %r' |
- % (self.serverSupports, self.clientSupports)) |
- |
-class IllegalServerResponse(IMAP4Exception): pass |
- |
-TIMEOUT_ERROR = error.TimeoutError() |
- |
-class IMAP4Client(basic.LineReceiver, policies.TimeoutMixin): |
- """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 |
- |
- startedTLS = False |
- |
- # Number of seconds to wait before timing out a connection. |
- # If the number is <= 0 no timeout checking will be performed. |
- timeout = 0 |
- |
- # 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 |
- self._lastCmd = 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): |
- if self.timeout > 0: |
- self.resetTimeout() |
- |
- 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 sendLine(self, line): |
-# print 'S:', repr(line) |
-# return basic.LineReceiver.sendLine(self, line) |
- |
- def _setupForLiteral(self, rest, octets): |
- self._pendingBuffer = self.messageFile(octets) |
- self._pendingSize = octets |
- if self._parts is None: |
- self._parts = [rest, '\r\n'] |
- else: |
- self._parts.extend([rest, '\r\n']) |
- self.setRawMode() |
- |
- def connectionMade(self): |
- if self.timeout > 0: |
- self.setTimeout(self.timeout) |
- |
- def connectionLost(self, reason): |
- """We are no longer connected""" |
- if self.timeout > 0: |
- self.setTimeout(None) |
- if self.queued is not None: |
- queued = self.queued |
- self.queued = None |
- for cmd in queued: |
- cmd.defer.errback(reason) |
- if self.tags is not None: |
- tags = self.tags |
- self.tags = None |
- for cmd in tags.itervalues(): |
- if cmd is not None and cmd.defer is not None: |
- cmd.defer.errback(reason) |
- |
- |
- def lineReceived(self, line): |
- """ |
- Attempt to parse a single line from the server. |
- |
- @type line: C{str} |
- @param line: The line from the server, without the line delimiter. |
- |
- @raise IllegalServerResponse: If the line or some part of the line |
- does not represent an allowed message from the server at this time. |
- """ |
-# print 'C: ' + repr(line) |
- if self.timeout > 0: |
- self.resetTimeout() |
- |
- lastPart = line.rfind('{') |
- if lastPart != -1: |
- lastPart = line[lastPart + 1:] |
- if lastPart.endswith('}'): |
- # It's a literal a-comin' in |
- try: |
- octets = int(lastPart[:-1]) |
- except ValueError: |
- raise IllegalServerResponse(line) |
- if self._parts is None: |
- self._tag, parts = line.split(None, 1) |
- else: |
- parts = line |
- self._setupForLiteral(parts, octets) |
- return |
- |
- if self._parts is None: |
- # It isn't a literal at all |
- 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 timeoutConnection(self): |
- if self._lastCmd and self._lastCmd.defer is not None: |
- d, self._lastCmd.defer = self._lastCmd.defer, None |
- d.errback(TIMEOUT_ERROR) |
- |
- if self.queued: |
- for cmd in self.queued: |
- if cmd.defer is not None: |
- d, cmd.defer = cmd.defer, d |
- d.errback(TIMEOUT_ERROR) |
- |
- self.transport.loseConnection() |
- |
- def _regularDispatch(self, line): |
- parts = line.split(None, 1) |
- if len(parts) != 2: |
- parts.append('') |
- tag, rest = parts |
- self.dispatchCommand(tag, rest) |
- |
- 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) |
- |
- b, e = rest.find('['), rest.find(']') |
- if b != -1 and e != -1: |
- self.serverGreeting(self.__cbCapabilities(([rest[b:e]], None))) |
- else: |
- self.serverGreeting(None) |
- 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(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(cmd.format(t)) |
- 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 = {} |
- recent = exists = None |
- for L in lines: |
- if L.find('EXISTS') != -1: |
- exists = int(L.split()[0]) |
- elif L.find('RECENT') != -1: |
- recent = 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) |
- if recent is not None or exists is not None: |
- self.newMessages(exists, recent) |
- |
- 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(cmd.format(t)) |
- self.waiting = t |
- self._lastCmd = cmd |
- 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' |
- resp = ('CAPABILITY',) |
- d = self.sendCommand(Command(cmd, 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: |
- parts = cap.split('=', 1) |
- if len(parts) == 1: |
- category, value = parts[0], None |
- else: |
- category, value = parts |
- caps.setdefault(category, []).append(value) |
- |
- # Preserve a non-ideal API for backwards compatibility. It would |
- # probably be entirely sensible to have an object with a wider API than |
- # dict here so this could be presented less insanely. |
- for category in caps: |
- if caps[category] == [None]: |
- caps[category] = None |
- 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 startTLS(self, contextFactory=None): |
- """ |
- Initiates a 'STARTTLS' request and negotiates the TLS / SSL |
- Handshake. |
- |
- @param contextFactory: The TLS / SSL Context Factory to |
- leverage. If the contextFactory is None the IMAP4Client will |
- either use the current TLS / SSL Context Factory or attempt to |
- create a new one. |
- |
- @type contextFactory: C{ssl.ClientContextFactory} |
- |
- @return: A Deferred which fires when the transport has been |
- secured according to the given contextFactory, or which fails |
- if the transport cannot be secured. |
- """ |
- assert not self.startedTLS, "Client and Server are currently communicating via TLS" |
- |
- if contextFactory is None: |
- contextFactory = self._getContextFactory() |
- |
- if contextFactory is None: |
- return defer.fail(IMAP4Exception( |
- "IMAP4Client requires a TLS context to " |
- "initiate the STARTTLS handshake")) |
- |
- if 'STARTTLS' not in self._capCache: |
- return defer.fail(IMAP4Exception( |
- "Server does not support secure communication " |
- "via TLS / SSL")) |
- |
- tls = interfaces.ITLSTransport(self.transport, None) |
- if tls is None: |
- return defer.fail(IMAP4Exception( |
- "IMAP4Client transport does not implement " |
- "interfaces.ITLSTransport")) |
- |
- d = self.sendCommand(Command('STARTTLS')) |
- d.addCallback(self._startedTLS, contextFactory) |
- d.addCallback(lambda _: self.getCapabilities()) |
- return d |
- |
- |
- 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. |
- """ |
- if self._capCache is None: |
- d = self.getCapabilities() |
- else: |
- d = defer.succeed(self._capCache) |
- d.addCallback(self.__cbAuthenticate, secret) |
- return d |
- |
- def __cbAuthenticate(self, caps, secret): |
- auths = caps.get('AUTH', ()) |
- for scheme in auths: |
- if scheme.upper() in self.authenticators: |
- cmd = Command('AUTHENTICATE', scheme, (), |
- self.__cbContinueAuth, scheme, |
- secret) |
- return self.sendCommand(cmd) |
- |
- if self.startedTLS: |
- return defer.fail(NoSupportedAuthentication( |
- auths, self.authenticators.keys())) |
- else: |
- def ebStartTLS(err): |
- err.trap(IMAP4Exception) |
- # We couldn't negotiate TLS for some reason |
- return defer.fail(NoSupportedAuthentication( |
- auths, self.authenticators.keys())) |
- |
- d = self.startTLS() |
- d.addErrback(ebStartTLS) |
- d.addCallback(lambda _: self.getCapabilities()) |
- d.addCallback(self.__cbAuthTLS, secret) |
- return d |
- |
- |
- def __cbContinueAuth(self, rest, scheme, secret): |
- try: |
- chal = base64.decodestring(rest + '\n') |
- except binascii.Error: |
- self.sendLine('*') |
- raise IllegalServerResponse(rest) |
- self.transport.loseConnection() |
- else: |
- auth = self.authenticators[scheme] |
- chal = auth.challengeResponse(secret, chal) |
- self.sendLine(base64.encodestring(chal).strip()) |
- |
- def __cbAuthTLS(self, caps, secret): |
- auths = caps.get('AUTH', ()) |
- for scheme in auths: |
- if scheme.upper() in self.authenticators: |
- cmd = Command('AUTHENTICATE', scheme, (), |
- self.__cbContinueAuth, scheme, |
- secret) |
- return self.sendCommand(cmd) |
- raise NoSupportedAuthentication(auths, self.authenticators.keys()) |
- |
- |
- 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 supports |
- TLS, TLS is negotiated before the login command is issued. |
- |
- A more secure way to log in is to use C{startTLS} or |
- C{authenticate} or both. |
- |
- @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.addCallback(self.__cbLoginCaps, username, password) |
- return d |
- |
- def serverGreeting(self, caps): |
- """Called when the server has sent us a greeting. |
- |
- @type caps: C{dict} |
- @param caps: Capabilities the server advertised in its greeting. |
- """ |
- |
- 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): |
- # If the server advertises STARTTLS, we might want to try to switch to TLS |
- tryTLS = 'STARTTLS' in capabilities |
- |
- # If our transport supports switching to TLS, we might want to try to switch to TLS. |
- tlsableTransport = interfaces.ITLSTransport(self.transport, None) is not None |
- |
- # If our transport is not already using TLS, we might want to try to switch to TLS. |
- nontlsTransport = interfaces.ISSLTransport(self.transport, None) is None |
- |
- if not self.startedTLS and tryTLS and tlsableTransport and nontlsTransport: |
- d = self.startTLS() |
- |
- d.addCallbacks( |
- self.__cbLoginTLS, |
- self.__ebLoginTLS, |
- callbackArgs=(username, password), |
- ) |
- return d |
- else: |
- if nontlsTransport: |
- log.msg("Server has no TLS support. logging in over cleartext!") |
- args = ' '.join((_quote(username), _quote(password))) |
- return self.sendCommand(Command('LOGIN', args)) |
- |
- def _startedTLS(self, result, context): |
- self.transport.startTLS(context) |
- self._capCache = None |
- self.startedTLS = True |
- return result |
- |
- def __cbLoginTLS(self, result, username, password): |
- args = ' '.join((_quote(username), _quote(password))) |
- return self.sendCommand(Command('LOGIN', args)) |
- |
- def __ebLoginTLS(self, failure): |
- log.err(failure) |
- return failure |
- |
- def namespace(self): |
- """Retrieve information about the namespaces available to this account |
- |
- This command is allowed in the Authenticated and Selected states. |
- |
- @rtype: C{Deferred} |
- @return: A deferred whose callback is invoked with namespace |
- information. An example of this information is:: |
- |
- [[['', '/']], [], []] |
- |
- which indicates a single personal namespace called '' with '/' |
- as its hierarchical delimiter, and no shared or user namespaces. |
- """ |
- cmd = 'NAMESPACE' |
- resp = ('NAMESPACE',) |
- d = self.sendCommand(Command(cmd, wantResponse=resp)) |
- d.addCallback(self.__cbNamespace) |
- return d |
- |
- def __cbNamespace(self, (lines, last)): |
- for line in lines: |
- parts = line.split(None, 1) |
- if len(parts) == 2: |
- if parts[0] == 'NAMESPACE': |
- # XXX UGGG parsing hack :( |
- r = parseNestedParens('(' + parts[1] + ')')[0] |
- return [e or [] for e in r] |
- log.err("No NAMESPACE response to NAMESPACE command") |
- return [[], [], []] |
- |
- 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. Mailbox information consists of a dictionary |
- with the following keys and values:: |
- |
- FLAGS: A list of strings containing the flags settable on |
- messages in this mailbox. |
- |
- EXISTS: An integer indicating the number of messages in this |
- mailbox. |
- |
- RECENT: An integer indicating the number of \"recent\" |
- messages in this mailbox. |
- |
- UNSEEN: An integer indicating the number of messages not |
- flagged \\Seen in this mailbox. |
- |
- PERMANENTFLAGS: A list of strings containing the flags that |
- can be permanently set on messages in this mailbox. |
- |
- UIDVALIDITY: An integer uniquely identifying this mailbox. |
- """ |
- cmd = 'SELECT' |
- args = _prepareMailboxName(mailbox) |
- 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. Mailbox information consists of a dictionary |
- with the following keys and values:: |
- |
- 'FLAGS': A list of strings containing the flags settable on |
- messages in this mailbox. |
- |
- 'EXISTS': An integer indicating the number of messages in this |
- mailbox. |
- |
- 'RECENT': An integer indicating the number of \"recent\" |
- messages in this mailbox. |
- |
- 'UNSEEN': An integer indicating the number of messages not |
- flagged \\Seen in this mailbox. |
- |
- 'PERMANENTFLAGS': A list of strings containing the flags that |
- can be permanently set on messages in this mailbox. |
- |
- 'UIDVALIDITY': An integer uniquely identifying this mailbox. |
- """ |
- cmd = 'EXAMINE' |
- args = _prepareMailboxName(mailbox) |
- 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 == 'UIDNEXT': |
- datum['UIDNEXT'] = int(content[1]) |
- 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', _prepareMailboxName(name))) |
- |
- 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', _prepareMailboxName(name))) |
- |
- 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 = _prepareMailboxName(oldname) |
- newname = _prepareMailboxName(newname) |
- 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', _prepareMailboxName(name))) |
- |
- 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', _prepareMailboxName(name))) |
- |
- 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)" % (_prepareMailboxName(mailbox), ' '.join(names)) |
- resp = ('STATUS',) |
- d = self.sendCommand(Command(cmd, args, wantResponse=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 given 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. Newlines |
- in this file should be \\r\\n-style. |
- |
- @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. This should |
- be of the format DD-MM-YYYY HH:MM:SS +/-HHMM. For example, in |
- Eastern Standard Time, on July 1st 2004 at half past 1 PM, |
- \"01-07-2004 13:30:00 -0500\". |
- |
- @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 {%d}' |
- if date: |
- date = ' "%s"' % date |
- else: |
- date = '' |
- cmd = fmt % ( |
- _prepareMailboxName(mailbox), ' '.join(flags), |
- date, L |
- ) |
- d = self.sendCommand(Command('APPEND', cmd, (), self.__cbContinueAppend, message)) |
- return d |
- |
- def __cbContinueAppend(self, lines, message): |
- s = basic.FileSender() |
- return s.beginFileTransfer(message, self.transport, None |
- ).addCallback(self.__cbFinishAppend) |
- |
- def __cbFinishAppend(self, foo): |
- self.sendLine('') |
- |
- 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, uid=0): |
- """Retrieve the unique identifier for one or more messages |
- |
- This command is allowed in the Selected state. |
- |
- @type messages: C{MessageSet} or 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 sequence numbers to unique message identifiers, or whose |
- errback is invoked if there is an error. |
- """ |
- d = self._fetch(messages, useUID=uid, 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} or C{str} |
- @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} or C{str} |
- @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} or C{str} |
- @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} or C{str} |
- @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{MessageSet} or 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{MessageSet} or 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 objects (as returned by self.messageFile(), file objects by |
- default), to additional information, 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{MessageSet} or 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{MessageSet} or 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{MessageSet} or 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{MessageSet} or 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{MessageSet} or 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{MessageSet} or 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{MessageSet} or 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{MessageSet} or 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(str(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{MessageSet} or 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(str(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{MessageSet} or 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(str(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) |
- 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, _prepareMailboxName(mailbox)) |
- 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=None): |
- wildcard = wildcard.replace('*', '(?:.*?)') |
- if delim is None: |
- wildcard = wildcard.replace('%', '(?:.*?)') |
- else: |
- wildcard = wildcard.replace('%', '(?:(?:[^%s])*?)' % re.escape(delim)) |
- return re.compile(wildcard, re.I) |
- |
-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('"', '\\"'),) |
- |
-def _literal(s): |
- return '{%d}\r\n%s' % (len(s), s) |
- |
-class DontQuoteMe: |
- def __init__(self, value): |
- self.value = value |
- |
- def __str__(self): |
- return str(self.value) |
- |
-_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 _prepareMailboxName(name): |
- name = name.encode('imap4-utf-7') |
- if _needsQuote(name): |
- return _quote(name) |
- return name |
- |
-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, |
- otherwise they will be quoted. 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. Integers will be converted to |
- strings and inserted into the output unquoted. Instances of |
- C{DontQuoteMe} will be converted to strings and inserted into the output |
- unquoted. |
- |
- This function used to be much nicer, and only quote things that really |
- needed to be quoted (and C{DontQuoteMe} did not exist), however, many |
- broken IMAP4 clients were unable to deal with this level of sophistication, |
- forcing the current behavior to be adopted for practical reasons. |
- |
- @type items: Any iterable |
- |
- @rtype: C{str} |
- """ |
- pieces = [] |
- for i in items: |
- if i is None: |
- pieces.extend([' ', 'NIL']) |
- elif isinstance(i, (DontQuoteMe, int, long)): |
- pieces.extend([' ', str(i)]) |
- elif isinstance(i, types.StringTypes): |
- if _needsLiteral(i): |
- pieces.extend([' ', '{', str(len(i)), '}', IMAP4Server.delimiter, i]) |
- else: |
- pieces.extend([' ', _quote(i)]) |
- elif hasattr(i, 'read'): |
- d = i.read() |
- pieces.extend([' ', '{', str(len(d)), '}', IMAP4Server.delimiter, d]) |
- else: |
- pieces.extend([' ', '(%s)' % (collapseNestedLists(i),)]) |
- return ''.join(pieces[1:]) |
- |
- |
-class IClientAuthentication(Interface): |
- def getName(): |
- """Return an identifier associated with this authentication scheme. |
- |
- @rtype: C{str} |
- """ |
- |
- def challengeResponse(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 LOGINAuthenticator: |
- implements(IClientAuthentication) |
- |
- def __init__(self, user): |
- self.user = user |
- self.challengeResponse = self.challengeUsername |
- |
- def getName(self): |
- return "LOGIN" |
- |
- def challengeUsername(self, secret, chal): |
- # Respond to something like "Username:" |
- self.challengeResponse = self.challengeSecret |
- return self.user |
- |
- def challengeSecret(self, secret, chal): |
- # Respond to something like "Password:" |
- return secret |
- |
-class PLAINAuthenticator: |
- implements(IClientAuthentication) |
- |
- def __init__(self, user): |
- self.user = user |
- |
- def getName(self): |
- return "PLAIN" |
- |
- def challengeResponse(self, secret, chal): |
- return '%s\0%s\0' % (self.user, secret) |
- |
- |
-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(Interface): |
- """Interface for Account classes |
- |
- Implementors of this interface should consider implementing |
- C{INamespacePresenter}. |
- """ |
- |
- def addMailbox(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(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(name, rw=True): |
- """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(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(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(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(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(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(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. C{mailboxObject} should implement either |
- C{IMailboxInfo} or C{IMailbox}. A Deferred may also be returned. |
- """ |
- |
-class INamespacePresenter(Interface): |
- def getPersonalNamespaces(): |
- """Report the available personal namespaces. |
- |
- Typically there should be only one personal namespace. A common |
- name for it is \"\", and its hierarchical delimiter is usually |
- \"/\". |
- |
- @rtype: iterable of two-tuples of strings |
- @return: The personal namespaces and their hierarchical delimiters. |
- If no namespaces of this type exist, None should be returned. |
- """ |
- |
- def getSharedNamespaces(): |
- """Report the available shared namespaces. |
- |
- Shared namespaces do not belong to any individual user but are |
- usually to one or more of them. Examples of shared namespaces |
- might be \"#news\" for a usenet gateway. |
- |
- @rtype: iterable of two-tuples of strings |
- @return: The shared namespaces and their hierarchical delimiters. |
- If no namespaces of this type exist, None should be returned. |
- """ |
- |
- def getUserNamespaces(): |
- """Report the available user namespaces. |
- |
- These are namespaces that contain folders belonging to other users |
- access to which this account has been granted. |
- |
- @rtype: iterable of two-tuples of strings |
- @return: The user namespaces and their hierarchical delimiters. |
- If no namespaces of this type exist, None should be returned. |
- """ |
- |
- |
-class MemoryAccount(object): |
- implements(IAccount, INamespacePresenter) |
- |
- mailboxes = None |
- subscriptions = None |
- top_id = 0 |
- |
- def __init__(self, name): |
- self.name = name |
- self.mailboxes = {} |
- self.subscriptions = [] |
- |
- def allocateID(self): |
- id = self.top_id |
- self.top_id += 1 |
- return id |
- |
- ## |
- ## IAccount |
- ## |
- 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('/'): |
- return False |
- return True |
- |
- 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)] |
- |
- ## |
- ## INamespacePresenter |
- ## |
- def getPersonalNamespaces(self): |
- return [["", "/"]] |
- |
- def getSharedNamespaces(self): |
- return None |
- |
- def getOtherNamespaces(self): |
- return None |
- |
- |
- |
-_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.upper()])() |
- return r |
- |
-def parseAddr(addr): |
- if addr is None: |
- return [(None, None, None),] |
- addrs = email.Utils.getaddresses([addr]) |
- return [[fn or None, None] + addr.split('@') for fn, addr in addrs] |
- |
-def getEnvelope(msg): |
- headers = msg.getHeaders(True) |
- date = headers.get('date') |
- subject = headers.get('subject') |
- from_ = headers.get('from') |
- sender = headers.get('sender', from_) |
- reply_to = headers.get('reply-to', from_) |
- to = headers.get('to') |
- cc = headers.get('cc') |
- bcc = headers.get('bcc') |
- in_reply_to = headers.get('in-reply-to') |
- mid = headers.get('message-id') |
- return (date, subject, parseAddr(from_), parseAddr(sender), |
- reply_to and parseAddr(reply_to), to and parseAddr(to), |
- cc and parseAddr(cc), bcc and parseAddr(bcc), in_reply_to, mid) |
- |
-def getLineCount(msg): |
- # XXX - Super expensive, CACHE THIS VALUE FOR LATER RE-USE |
- # XXX - This must be the number of lines in the ENCODED version |
- lines = 0 |
- for _ in msg.getBodyFile(): |
- lines += 1 |
- return lines |
- |
-def unquote(s): |
- if s[0] == s[-1] == '"': |
- return s[1:-1] |
- return s |
- |
-def getBodyStructure(msg, extended=False): |
- # XXX - This does not properly handle multipart messages |
- # BODYSTRUCTURE is obscenely complex and criminally under-documented. |
- |
- attrs = {} |
- headers = 'content-type', 'content-id', 'content-description', 'content-transfer-encoding' |
- headers = msg.getHeaders(False, *headers) |
- mm = headers.get('content-type') |
- if mm: |
- mm = ''.join(mm.splitlines()) |
- mimetype = mm.split(';') |
- if mimetype: |
- type = mimetype[0].split('/', 1) |
- if len(type) == 1: |
- major = type[0] |
- minor = None |
- elif len(type) == 2: |
- major, minor = type |
- else: |
- major = minor = None |
- attrs = dict([x.strip().lower().split('=', 1) for x in mimetype[1:]]) |
- else: |
- major = minor = None |
- else: |
- major = minor = None |
- |
- |
- size = str(msg.getSize()) |
- unquotedAttrs = [(k, unquote(v)) for (k, v) in attrs.iteritems()] |
- result = [ |
- major, minor, # Main and Sub MIME types |
- unquotedAttrs, # content-type parameter list |
- headers.get('content-id'), |
- headers.get('content-description'), |
- headers.get('content-transfer-encoding'), |
- size, # Number of octets total |
- ] |
- |
- if major is not None: |
- if major.lower() == 'text': |
- result.append(str(getLineCount(msg))) |
- elif (major.lower(), minor.lower()) == ('message', 'rfc822'): |
- contained = msg.getSubPart(0) |
- result.append(getEnvelope(contained)) |
- result.append(getBodyStructure(contained, False)) |
- result.append(str(getLineCount(contained))) |
- |
- if not extended or major is None: |
- return result |
- |
- if major.lower() != 'multipart': |
- headers = 'content-md5', 'content-disposition', 'content-language' |
- headers = msg.getHeaders(False, *headers) |
- disp = headers.get('content-disposition') |
- |
- # XXX - I dunno if this is really right |
- if disp: |
- disp = disp.split('; ') |
- if len(disp) == 1: |
- disp = (disp[0].lower(), None) |
- elif len(disp) > 1: |
- disp = (disp[0].lower(), [x.split('=') for x in disp[1:]]) |
- |
- result.append(headers.get('content-md5')) |
- result.append(disp) |
- result.append(headers.get('content-language')) |
- else: |
- result = [result] |
- try: |
- i = 0 |
- while True: |
- submsg = msg.getSubPart(i) |
- result.append(getBodyStructure(submsg)) |
- i += 1 |
- except IndexError: |
- result.append(minor) |
- result.append(attrs.items()) |
- |
- # XXX - I dunno if this is really right |
- headers = msg.getHeaders(False, 'content-disposition', 'content-language') |
- disp = headers.get('content-disposition') |
- if disp: |
- disp = disp.split('; ') |
- if len(disp) == 1: |
- disp = (disp[0].lower(), None) |
- elif len(disp) > 1: |
- disp = (disp[0].lower(), [x.split('=') for x in disp[1:]]) |
- |
- result.append(disp) |
- result.append(headers.get('content-language')) |
- |
- return result |
- |
-class IMessagePart(Interface): |
- def getHeaders(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 getBodyFile(): |
- """Retrieve a file object containing only the body of this message. |
- """ |
- |
- def getSize(): |
- """Retrieve the total size, in octets, of this message. |
- |
- @rtype: C{int} |
- """ |
- |
- def isMultipart(): |
- """Indicate whether this message has subparts. |
- |
- @rtype: C{bool} |
- """ |
- |
- def getSubPart(part): |
- """Retrieve a MIME sub-message |
- |
- @type part: C{int} |
- @param part: The number of the part to retrieve, indexed from 0. |
- |
- @raise IndexError: Raised if the specified part does not exist. |
- @raise TypeError: Raised if this message is not multipart. |
- |
- @rtype: Any object implementing C{IMessagePart}. |
- @return: The specified sub-part. |
- """ |
- |
-class IMessage(IMessagePart): |
- def getUID(): |
- """Retrieve the unique identifier associated with this message. |
- """ |
- |
- def getFlags(): |
- """Retrieve the flags associated with this message. |
- |
- @rtype: C{iterable} |
- @return: The flags, represented as strings. |
- """ |
- |
- def getInternalDate(): |
- """Retrieve the date internally associated with this message. |
- |
- @rtype: C{str} |
- @return: An RFC822-formatted date string. |
- """ |
- |
-class IMessageFile(Interface): |
- """Optional message interface for representing messages as files. |
- |
- If provided by message objects, this interface will be used instead |
- the more complex MIME-based interface. |
- """ |
- def open(): |
- """Return an file-like object opened for reading. |
- |
- Reading from the returned file will return all the bytes |
- of which this message consists. |
- """ |
- |
-class ISearchableMailbox(Interface): |
- def search(query, uid): |
- """Search for messages that meet the given query criteria. |
- |
- If this interface is not implemented by the mailbox, L{IMailbox.fetch} |
- and various methods of L{IMessage} will be used instead. |
- |
- Implementations which wish to offer better performance than the |
- default implementation should implement this interface. |
- |
- @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. |
- """ |
- |
-class IMessageCopier(Interface): |
- def copy(messageObject): |
- """Copy the given message object into this mailbox. |
- |
- The message object will be one which was previously returned by |
- L{IMailbox.fetch}. |
- |
- Implementations which wish to offer better performance than the |
- default implementation should implement this interface. |
- |
- If this interface is not implemented by the mailbox, IMailbox.addMessage |
- will be used instead. |
- |
- @rtype: C{Deferred} or C{int} |
- @return: Either the UID of the message or a Deferred which fires |
- with the UID when the copy finishes. |
- """ |
- |
-class IMailboxInfo(Interface): |
- """Interface specifying only the methods required for C{listMailboxes}. |
- |
- Implementations can return objects implementing only these methods for |
- return to C{listMailboxes} if it can allow them to operate more |
- efficiently. |
- """ |
- |
- def getFlags(): |
- """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 getHierarchicalDelimiter(): |
- """Get the character which delimits namespaces for in this mailbox. |
- |
- @rtype: C{str} |
- """ |
- |
-class IMailbox(IMailboxInfo): |
- def getUIDValidity(): |
- """Return the unique validity identifier for this mailbox. |
- |
- @rtype: C{int} |
- """ |
- |
- def getUIDNext(): |
- """Return the likely UID for the next message added to this mailbox. |
- |
- @rtype: C{int} |
- """ |
- |
- def getUID(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 getMessageCount(): |
- """Return the number of messages in this mailbox. |
- |
- @rtype: C{int} |
- """ |
- |
- def getRecentCount(): |
- """Return the number of messages with the 'Recent' flag. |
- |
- @rtype: C{int} |
- """ |
- |
- def getUnseenCount(): |
- """Return the number of messages with the 'Unseen' flag. |
- |
- @rtype: C{int} |
- """ |
- |
- def isWriteable(): |
- """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(): |
- """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 requestStatus(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(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(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(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(): |
- """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 fetch(messages, uid): |
- """Retrieve one or more messages. |
- |
- @type messages: C{MessageSet} |
- @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 sequence numbers and |
- implementors of C{IMessage}. |
- """ |
- |
- def store(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 sequence numbers 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 ICloseableMailbox(Interface): |
- """A supplementary interface for mailboxes which require cleanup on close. |
- |
- Implementing this interface is optional. If it is implemented, the protocol |
- code will call the close method defined whenever a mailbox is closed. |
- """ |
- def close(): |
- """Close this mailbox. |
- |
- @return: A C{Deferred} which fires when this mailbox |
- has been closed, or None if the mailbox can be closed |
- immediately. |
- """ |
- |
-def _formatHeaders(headers): |
- hdrs = [': '.join((k.title(), '\r\n'.join(v.splitlines()))) for (k, v) |
- in headers.iteritems()] |
- hdrs = '\r\n'.join(hdrs) + '\r\n' |
- return hdrs |
- |
-def subparts(m): |
- i = 0 |
- try: |
- while True: |
- yield m.getSubPart(i) |
- i += 1 |
- except IndexError: |
- pass |
- |
-def iterateInReactor(i): |
- """Consume an interator at most a single iteration per reactor iteration. |
- |
- If the iterator produces a Deferred, the next iteration will not occur |
- until the Deferred fires, otherwise the next iteration will be taken |
- in the next reactor iteration. |
- |
- @rtype: C{Deferred} |
- @return: A deferred which fires (with None) when the iterator is |
- exhausted or whose errback is called if there is an exception. |
- """ |
- from twisted.internet import reactor |
- d = defer.Deferred() |
- def go(last): |
- try: |
- r = i.next() |
- except StopIteration: |
- d.callback(last) |
- except: |
- d.errback() |
- else: |
- if isinstance(r, defer.Deferred): |
- r.addCallback(go) |
- else: |
- reactor.callLater(0, go, r) |
- go(None) |
- return d |
- |
-class MessageProducer: |
- CHUNK_SIZE = 2 ** 2 ** 2 ** 2 |
- |
- def __init__(self, msg, buffer = None, scheduler = None): |
- """Produce this message. |
- |
- @param msg: The message I am to produce. |
- @type msg: L{IMessage} |
- |
- @param buffer: A buffer to hold the message in. If None, I will |
- use a L{tempfile.TemporaryFile}. |
- @type buffer: file-like |
- """ |
- self.msg = msg |
- if buffer is None: |
- buffer = tempfile.TemporaryFile() |
- self.buffer = buffer |
- if scheduler is None: |
- scheduler = iterateInReactor |
- self.scheduler = scheduler |
- self.write = self.buffer.write |
- |
- def beginProducing(self, consumer): |
- self.consumer = consumer |
- return self.scheduler(self._produce()) |
- |
- def _produce(self): |
- headers = self.msg.getHeaders(True) |
- boundary = None |
- if self.msg.isMultipart(): |
- content = headers.get('content-type') |
- parts = [x.split('=', 1) for x in content.split(';')[1:]] |
- parts = dict([(k.lower().strip(), v) for (k, v) in parts]) |
- boundary = parts.get('boundary') |
- if boundary is None: |
- # Bastards |
- boundary = '----=_%f_boundary_%f' % (time.time(), random.random()) |
- headers['content-type'] += '; boundary="%s"' % (boundary,) |
- else: |
- if boundary.startswith('"') and boundary.endswith('"'): |
- boundary = boundary[1:-1] |
- |
- self.write(_formatHeaders(headers)) |
- self.write('\r\n') |
- if self.msg.isMultipart(): |
- for p in subparts(self.msg): |
- self.write('\r\n--%s\r\n' % (boundary,)) |
- yield MessageProducer(p, self.buffer, self.scheduler |
- ).beginProducing(None |
- ) |
- self.write('\r\n--%s--\r\n' % (boundary,)) |
- else: |
- f = self.msg.getBodyFile() |
- while True: |
- b = f.read(self.CHUNK_SIZE) |
- if b: |
- self.buffer.write(b) |
- yield None |
- else: |
- break |
- if self.consumer: |
- self.buffer.seek(0, 0) |
- yield FileProducer(self.buffer |
- ).beginProducing(self.consumer |
- ).addCallback(lambda _: self |
- ) |
- |
-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 = () |
- empty = False |
- partialBegin = None |
- partialLength = None |
- def __str__(self): |
- base = 'BODY' |
- part = '' |
- separator = '' |
- if self.part: |
- part = '.'.join([str(x + 1) for x in self.part]) |
- separator = '.' |
-# if self.peek: |
-# base += '.PEEK' |
- if self.header: |
- base += '[%s%s%s]' % (part, separator, self.header,) |
- elif self.text: |
- base += '[%s%sTEXT]' % (part, separator) |
- elif self.mime: |
- base += '[%s%sMIME]' % (part, separator) |
- elif self.empty: |
- base += '[%s]' % (part,) |
- 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 |
- part = None |
- def __str__(self): |
- base = 'HEADER' |
- if self.fields: |
- base += '.FIELDS' |
- if self.negate: |
- base += '.NOT' |
- fields = [] |
- for f in self.fields: |
- f = f.title() |
- if _needsQuote(f): |
- f = _quote(f) |
- fields.append(f) |
- base += ' (%s)' % ' '.join(fields) |
- if self.part: |
- base = '.'.join([str(x + 1) for x in self.part]) + '.' + base |
- 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: |
- used = getattr(self, 'state_' + state)(s) |
- except: |
- self.state.append(state) |
- raise |
- else: |
- # print state, 'consumed', repr(s[:used]) |
- s = s[used:] |
- 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 |
- |
- _partExpr = re.compile(r'(\d+(?:\.\d+)*)\.?') |
- def state_part_number(self, s): |
- m = self._partExpr.match(s) |
- if m is not None: |
- self.parts = [int(p) - 1 for p in m.groups()[0].split('.')] |
- return m.end() |
- else: |
- self.parts = [] |
- return 0 |
- |
- def state_section(self, s): |
- # Grab "HEADER]" or "HEADER.FIELDS (Header list)]" or |
- # "HEADER.FIELDS.NOT (Header list)]" or "TEXT]" or "MIME]" or |
- # just "]". |
- |
- l = s.lower() |
- used = 0 |
- if l.startswith(']'): |
- self.pending_body.empty = True |
- used += 1 |
- elif l.startswith('header]'): |
- h = self.pending_body.header = self.Header() |
- h.negate = True |
- h.fields = () |
- used += 7 |
- elif l.startswith('text]'): |
- self.pending_body.text = self.Text() |
- used += 5 |
- elif l.startswith('mime]'): |
- self.pending_body.mime = self.MIME() |
- used += 5 |
- else: |
- 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: %r" % (l,)) |
- |
- 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 |
- |
-class FileProducer: |
- CHUNK_SIZE = 2 ** 2 ** 2 ** 2 |
- |
- firstWrite = True |
- |
- def __init__(self, f): |
- self.f = f |
- |
- def beginProducing(self, consumer): |
- self.consumer = consumer |
- self.produce = consumer.write |
- d = self._onDone = defer.Deferred() |
- self.consumer.registerProducer(self, False) |
- return d |
- |
- def resumeProducing(self): |
- b = '' |
- if self.firstWrite: |
- b = '{%d}\r\n' % self._size() |
- self.firstWrite = False |
- if not self.f: |
- return |
- b = b + self.f.read(self.CHUNK_SIZE) |
- if not b: |
- self.consumer.unregisterProducer() |
- self._onDone.callback(self) |
- self._onDone = self.f = self.consumer = None |
- else: |
- self.produce(b) |
- |
- def pauseProducing(self): |
- pass |
- |
- def stopProducing(self): |
- pass |
- |
- def _size(self): |
- b = self.f.tell() |
- self.f.seek(0, 2) |
- e = self.f.tell() |
- self.f.seek(b, 0) |
- return e - b |
- |
-def parseTime(s): |
- # XXX - This may require localization :( |
- months = [ |
- 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', |
- 'nov', 'dec', 'january', 'february', 'march', 'april', 'may', 'june', |
- 'july', 'august', 'september', 'october', 'november', 'december' |
- ] |
- expr = { |
- 'day': r"(?P<day>3[0-1]|[1-2]\d|0[1-9]|[1-9]| [1-9])", |
- 'mon': r"(?P<mon>\w+)", |
- 'year': r"(?P<year>\d\d\d\d)" |
- } |
- m = re.match('%(day)s-%(mon)s-%(year)s' % expr, s) |
- if not m: |
- raise ValueError, "Cannot parse time string %r" % (s,) |
- d = m.groupdict() |
- try: |
- d['mon'] = 1 + (months.index(d['mon'].lower()) % 12) |
- d['year'] = int(d['year']) |
- d['day'] = int(d['day']) |
- except ValueError: |
- raise ValueError, "Cannot parse time string %r" % (s,) |
- else: |
- return time.struct_time( |
- (d['year'], d['mon'], d['day'], 0, 0, 0, -1, -1, -1) |
- ) |
- |
-import codecs |
-def modified_base64(s): |
- s_utf7 = s.encode('utf-7') |
- return s_utf7[1:-1].replace('/', ',') |
- |
-def modified_unbase64(s): |
- s_utf7 = '+' + s.replace(',', '/') + '-' |
- return s_utf7.decode('utf-7') |
- |
-def encoder(s, errors=None): |
- """ |
- Encode the given C{unicode} string using the IMAP4 specific variation of |
- UTF-7. |
- |
- @type s: C{unicode} |
- @param s: The text to encode. |
- |
- @param errors: Policy for handling encoding errors. Currently ignored. |
- |
- @return: C{tuple} of a C{str} giving the encoded bytes and an C{int} |
- giving the number of code units consumed from the input. |
- """ |
- r = [] |
- _in = [] |
- for c in s: |
- if ord(c) in (range(0x20, 0x26) + range(0x27, 0x7f)): |
- if _in: |
- r.extend(['&', modified_base64(''.join(_in)), '-']) |
- del _in[:] |
- r.append(str(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, errors=None): |
- """ |
- Decode the given C{str} using the IMAP4 specific variation of UTF-7. |
- |
- @type s: C{str} |
- @param s: The bytes to decode. |
- |
- @param errors: Policy for handling decoding errors. Currently ignored. |
- |
- @return: a C{tuple} of a C{unicode} string giving the text which was |
- decoded and an C{int} giving the number of bytes consumed from the |
- input. |
- """ |
- 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__ = [ |
- # Protocol classes |
- 'IMAP4Server', 'IMAP4Client', |
- |
- # Interfaces |
- 'IMailboxListener', 'IClientAuthentication', 'IAccount', 'IMailbox', |
- 'INamespacePresenter', 'ICloseableMailbox', 'IMailboxInfo', |
- 'IMessage', 'IMessageCopier', 'IMessageFile', 'ISearchableMailbox', |
- |
- # Exceptions |
- 'IMAP4Exception', 'IllegalClientResponse', 'IllegalOperation', |
- 'IllegalMailboxEncoding', 'UnhandledResponse', 'NegativeResponse', |
- 'NoSupportedAuthentication', 'IllegalServerResponse', |
- 'IllegalIdentifierError', 'IllegalQueryError', 'MismatchedNesting', |
- 'MismatchedQuoting', 'MailboxException', 'MailboxCollision', |
- 'NoSuchMailbox', 'ReadOnlyMailbox', |
- |
- # Auth objects |
- 'CramMD5ClientAuthenticator', 'PLAINAuthenticator', 'LOGINAuthenticator', |
- 'PLAINCredentials', 'LOGINCredentials', |
- |
- # Simple query interface |
- 'Query', 'Not', 'Or', |
- |
- # Miscellaneous |
- 'MemoryAccount', |
- 'statusRequestHelper', |
-] |