diff options
Diffstat (limited to 'tools/buildbot/pylibs/twisted/mail')
30 files changed, 0 insertions, 20385 deletions
diff --git a/tools/buildbot/pylibs/twisted/mail/__init__.py b/tools/buildbot/pylibs/twisted/mail/__init__.py deleted file mode 100644 index 628b727..0000000 --- a/tools/buildbot/pylibs/twisted/mail/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ - -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. -# See LICENSE for details. - - -""" - -Twisted Mail: a Twisted E-Mail Server. - -Maintainer: U{Jp Calderone<mailto:exarkun@twistedmatrix.com>} - -""" - -from twisted.mail._version import version -__version__ = version.short() diff --git a/tools/buildbot/pylibs/twisted/mail/_version.py b/tools/buildbot/pylibs/twisted/mail/_version.py deleted file mode 100644 index 7398f41..0000000 --- a/tools/buildbot/pylibs/twisted/mail/_version.py +++ /dev/null @@ -1,3 +0,0 @@ -# This is an auto-generated file. Do not edit it. -from twisted.python import versions -version = versions.Version('twisted.mail', 8, 1, 0) diff --git a/tools/buildbot/pylibs/twisted/mail/alias.py b/tools/buildbot/pylibs/twisted/mail/alias.py deleted file mode 100644 index 4005c74..0000000 --- a/tools/buildbot/pylibs/twisted/mail/alias.py +++ /dev/null @@ -1,435 +0,0 @@ -# -*- test-case-name: twisted.mail.test.test_mail -*- -# -# Copyright (c) 2001-2007 Twisted Matrix Laboratories. -# See LICENSE for details. - - -""" -Support for aliases(5) configuration files - -@author: U{Jp Calderone<mailto:exarkun@twistedmatrix.com>} - -TODO:: - Monitor files for reparsing - Handle non-local alias targets - Handle maildir alias targets -""" - -import os -import tempfile - -from twisted.mail import smtp -from twisted.internet import reactor -from twisted.internet import protocol -from twisted.internet import defer -from twisted.python import failure -from twisted.python import log -from zope.interface import implements, Interface - - -def handle(result, line, filename, lineNo): - parts = [p.strip() for p in line.split(':', 1)] - if len(parts) != 2: - fmt = "Invalid format on line %d of alias file %s." - arg = (lineNo, filename) - log.err(fmt % arg) - else: - user, alias = parts - result.setdefault(user.strip(), []).extend(map(str.strip, alias.split(','))) - -def loadAliasFile(domains, filename=None, fp=None): - """Load a file containing email aliases. - - Lines in the file should be formatted like so:: - - username: alias1,alias2,...,aliasN - - Aliases beginning with a | will be treated as programs, will be run, and - the message will be written to their stdin. - - Aliases without a host part will be assumed to be addresses on localhost. - - If a username is specified multiple times, the aliases for each are joined - together as if they had all been on one line. - - @type domains: C{dict} of implementor of C{IDomain} - @param domains: The domains to which these aliases will belong. - - @type filename: C{str} - @param filename: The filename from which to load aliases. - - @type fp: Any file-like object. - @param fp: If specified, overrides C{filename}, and aliases are read from - it. - - @rtype: C{dict} - @return: A dictionary mapping usernames to C{AliasGroup} objects. - """ - result = {} - if fp is None: - fp = file(filename) - else: - filename = getattr(fp, 'name', '<unknown>') - i = 0 - prev = '' - for line in fp: - i += 1 - line = line.rstrip() - if line.lstrip().startswith('#'): - continue - elif line.startswith(' ') or line.startswith('\t'): - prev = prev + line - else: - if prev: - handle(result, prev, filename, i) - prev = line - if prev: - handle(result, prev, filename, i) - for (u, a) in result.items(): - addr = smtp.Address(u) - result[u] = AliasGroup(a, domains, u) - return result - -class IAlias(Interface): - def createMessageReceiver(): - pass - -class AliasBase: - def __init__(self, domains, original): - self.domains = domains - self.original = smtp.Address(original) - - def domain(self): - return self.domains[self.original.domain] - - def resolve(self, aliasmap, memo=None): - if memo is None: - memo = {} - if str(self) in memo: - return None - memo[str(self)] = None - return self.createMessageReceiver() - -class AddressAlias(AliasBase): - """The simplest alias, translating one email address into another.""" - - implements(IAlias) - - def __init__(self, alias, *args): - AliasBase.__init__(self, *args) - self.alias = smtp.Address(alias) - - def __str__(self): - return '<Address %s>' % (self.alias,) - - def createMessageReceiver(self): - return self.domain().startMessage(str(self.alias)) - - def resolve(self, aliasmap, memo=None): - if memo is None: - memo = {} - if str(self) in memo: - return None - memo[str(self)] = None - try: - return self.domain().exists(smtp.User(self.alias, None, None, None), memo)() - except smtp.SMTPBadRcpt: - pass - if self.alias.local in aliasmap: - return aliasmap[self.alias.local].resolve(aliasmap, memo) - return None - -class FileWrapper: - implements(smtp.IMessage) - - def __init__(self, filename): - self.fp = tempfile.TemporaryFile() - self.finalname = filename - - def lineReceived(self, line): - self.fp.write(line + '\n') - - def eomReceived(self): - self.fp.seek(0, 0) - try: - f = file(self.finalname, 'a') - except: - return defer.fail(failure.Failure()) - - f.write(self.fp.read()) - self.fp.close() - f.close() - - return defer.succeed(self.finalname) - - def connectionLost(self): - self.fp.close() - self.fp = None - - def __str__(self): - return '<FileWrapper %s>' % (self.finalname,) - - -class FileAlias(AliasBase): - - implements(IAlias) - - def __init__(self, filename, *args): - AliasBase.__init__(self, *args) - self.filename = filename - - def __str__(self): - return '<File %s>' % (self.filename,) - - def createMessageReceiver(self): - return FileWrapper(self.filename) - - - -class ProcessAliasTimeout(Exception): - """ - A timeout occurred while processing aliases. - """ - - - -class MessageWrapper: - """ - A message receiver which delivers content to a child process. - - @type completionTimeout: C{int} or C{float} - @ivar completionTimeout: The number of seconds to wait for the child - process to exit before reporting the delivery as a failure. - - @type _timeoutCallID: C{NoneType} or L{IDelayedCall} - @ivar _timeoutCallID: The call used to time out delivery, started when the - connection to the child process is closed. - - @type done: C{bool} - @ivar done: Flag indicating whether the child process has exited or not. - - @ivar reactor: An L{IReactorTime} provider which will be used to schedule - timeouts. - """ - implements(smtp.IMessage) - - done = False - - completionTimeout = 60 - _timeoutCallID = None - - reactor = reactor - - def __init__(self, protocol, process=None, reactor=None): - self.processName = process - self.protocol = protocol - self.completion = defer.Deferred() - self.protocol.onEnd = self.completion - self.completion.addBoth(self._processEnded) - - if reactor is not None: - self.reactor = reactor - - - def _processEnded(self, result): - """ - Record process termination and cancel the timeout call if it is active. - """ - self.done = True - if self._timeoutCallID is not None: - # eomReceived was called, we're actually waiting for the process to - # exit. - self._timeoutCallID.cancel() - self._timeoutCallID = None - else: - # eomReceived was not called, this is unexpected, propagate the - # error. - return result - - - def lineReceived(self, line): - if self.done: - return - self.protocol.transport.write(line + '\n') - - - def eomReceived(self): - """ - Disconnect from the child process, set up a timeout to wait for it to - exit, and return a Deferred which will be called back when the child - process exits. - """ - if not self.done: - self.protocol.transport.loseConnection() - self._timeoutCallID = self.reactor.callLater( - self.completionTimeout, self._completionCancel) - return self.completion - - - def _completionCancel(self): - """ - Handle the expiration of the timeout for the child process to exit by - terminating the child process forcefully and issuing a failure to the - completion deferred returned by L{eomReceived}. - """ - self._timeoutCallID = None - self.protocol.transport.signalProcess('KILL') - exc = ProcessAliasTimeout( - "No answer after %s seconds" % (self.completionTimeout,)) - self.protocol.onEnd = None - self.completion.errback(failure.Failure(exc)) - - - def connectionLost(self): - # Heh heh - pass - - - def __str__(self): - return '<ProcessWrapper %s>' % (self.processName,) - - - -class ProcessAliasProtocol(protocol.ProcessProtocol): - """ - Trivial process protocol which will callback a Deferred when the associated - process ends. - - @ivar onEnd: If not C{None}, a L{Deferred} which will be called back with - the failure passed to C{processEnded}, when C{processEnded} is called. - """ - - onEnd = None - - def processEnded(self, reason): - """ - Call back C{onEnd} if it is set. - """ - if self.onEnd is not None: - self.onEnd.errback(reason) - - - -class ProcessAlias(AliasBase): - """ - An alias which is handled by the execution of a particular program. - - @ivar reactor: An L{IReactorProcess} and L{IReactorTime} provider which - will be used to create and timeout the alias child process. - """ - implements(IAlias) - - reactor = reactor - - def __init__(self, path, *args): - AliasBase.__init__(self, *args) - self.path = path.split() - self.program = self.path[0] - - - def __str__(self): - """ - Build a string representation containing the path. - """ - return '<Process %s>' % (self.path,) - - - def spawnProcess(self, proto, program, path): - """ - Wrapper around C{reactor.spawnProcess}, to be customized for tests - purpose. - """ - return self.reactor.spawnProcess(proto, program, path) - - - def createMessageReceiver(self): - """ - Create a message receiver by launching a process. - """ - p = ProcessAliasProtocol() - m = MessageWrapper(p, self.program, self.reactor) - fd = self.spawnProcess(p, self.program, self.path) - return m - - - -class MultiWrapper: - """ - Wrapper to deliver a single message to multiple recipients. - """ - - implements(smtp.IMessage) - - def __init__(self, objs): - self.objs = objs - - def lineReceived(self, line): - for o in self.objs: - o.lineReceived(line) - - def eomReceived(self): - return defer.DeferredList([ - o.eomReceived() for o in self.objs - ]) - - def connectionLost(self): - for o in self.objs: - o.connectionLost() - - def __str__(self): - return '<GroupWrapper %r>' % (map(str, self.objs),) - - - -class AliasGroup(AliasBase): - """ - An alias which points to more than one recipient. - - @ivar processAliasFactory: a factory for resolving process aliases. - @type processAliasFactory: C{class} - """ - - implements(IAlias) - - processAliasFactory = ProcessAlias - - def __init__(self, items, *args): - AliasBase.__init__(self, *args) - self.aliases = [] - while items: - addr = items.pop().strip() - if addr.startswith(':'): - try: - f = file(addr[1:]) - except: - log.err("Invalid filename in alias file %r" % (addr[1:],)) - else: - addr = ' '.join([l.strip() for l in f]) - items.extend(addr.split(',')) - elif addr.startswith('|'): - self.aliases.append(self.processAliasFactory(addr[1:], *args)) - elif addr.startswith('/'): - if os.path.isdir(addr): - log.err("Directory delivery not supported") - else: - self.aliases.append(FileAlias(addr, *args)) - else: - self.aliases.append(AddressAlias(addr, *args)) - - def __len__(self): - return len(self.aliases) - - def __str__(self): - return '<AliasGroup [%s]>' % (', '.join(map(str, self.aliases))) - - def createMessageReceiver(self): - return MultiWrapper([a.createMessageReceiver() for a in self.aliases]) - - def resolve(self, aliasmap, memo=None): - if memo is None: - memo = {} - r = [] - for a in self.aliases: - r.append(a.resolve(aliasmap, memo)) - return MultiWrapper(filter(None, r)) - diff --git a/tools/buildbot/pylibs/twisted/mail/bounce.py b/tools/buildbot/pylibs/twisted/mail/bounce.py deleted file mode 100644 index d57c495..0000000 --- a/tools/buildbot/pylibs/twisted/mail/bounce.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- test-case-name: twisted.mail.test.test_bounce -*- -# -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. -# See LICENSE for details. - - -import StringIO -import rfc822 -import string -import time -import os - - -from twisted.mail import smtp - -BOUNCE_FORMAT = """\ -From: postmaster@%(failedDomain)s -To: %(failedFrom)s -Subject: Returned Mail: see transcript for details -Message-ID: %(messageID)s -Content-Type: multipart/report; report-type=delivery-status; - boundary="%(boundary)s" - ---%(boundary)s - -%(transcript)s - ---%(boundary)s -Content-Type: message/delivery-status -Arrival-Date: %(ctime)s -Final-Recipient: RFC822; %(failedTo)s -""" - -def generateBounce(message, failedFrom, failedTo, transcript=''): - if not transcript: - transcript = '''\ -I'm sorry, the following address has permanent errors: %(failedTo)s. -I've given up, and I will not retry the message again. -''' % vars() - - boundary = "%s_%s_%s" % (time.time(), os.getpid(), 'XXXXX') - failedAddress = rfc822.AddressList(failedTo)[0][1] - failedDomain = string.split(failedAddress, '@', 1)[1] - messageID = smtp.messageid(uniq='bounce') - ctime = time.ctime(time.time()) - - fp = StringIO.StringIO() - fp.write(BOUNCE_FORMAT % vars()) - orig = message.tell() - message.seek(2, 0) - sz = message.tell() - message.seek(0, orig) - if sz > 10000: - while 1: - line = message.readline() - if len(line)<=1: - break - fp.write(line) - else: - fp.write(message.read()) - return '', failedFrom, fp.getvalue() diff --git a/tools/buildbot/pylibs/twisted/mail/imap4.py b/tools/buildbot/pylibs/twisted/mail/imap4.py deleted file mode 100644 index bb3a156..0000000 --- a/tools/buildbot/pylibs/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', -] diff --git a/tools/buildbot/pylibs/twisted/mail/mail.py b/tools/buildbot/pylibs/twisted/mail/mail.py deleted file mode 100644 index 233c1b6..0000000 --- a/tools/buildbot/pylibs/twisted/mail/mail.py +++ /dev/null @@ -1,333 +0,0 @@ -# -*- test-case-name: twisted.mail.test.test_mail -*- -# Copyright (c) 2001-2007 Twisted Matrix Laboratories. -# See LICENSE for details. - - -"""Mail support for twisted python. -""" - -# Twisted imports -from twisted.internet import defer -from twisted.application import service, internet -from twisted.python import util -from twisted.python import log - -from twisted import cred -import twisted.cred.portal - -# Sibling imports -from twisted.mail import protocols, smtp - -# System imports -import os -from zope.interface import implements, Interface - - -class DomainWithDefaultDict: - '''Simulate a dictionary with a default value for non-existing keys. - ''' - def __init__(self, domains, default): - self.domains = domains - self.default = default - - def setDefaultDomain(self, domain): - self.default = domain - - def has_key(self, name): - return 1 - - def fromkeys(klass, keys, value=None): - d = klass() - for k in keys: - d[k] = value - return d - fromkeys = classmethod(fromkeys) - - def __contains__(self, name): - return 1 - - def __getitem__(self, name): - return self.domains.get(name, self.default) - - def __setitem__(self, name, value): - self.domains[name] = value - - def __delitem__(self, name): - del self.domains[name] - - def __iter__(self): - return iter(self.domains) - - def __len__(self): - return len(self.domains) - - - def __str__(self): - """ - Return a string describing the underlying domain mapping of this - object. - """ - return '<DomainWithDefaultDict %s>' % (self.domains,) - - - def __repr__(self): - """ - Return a pseudo-executable string describing the underlying domain - mapping of this object. - """ - return 'DomainWithDefaultDict(%s)' % (self.domains,) - - - def get(self, key, default=None): - return self.domains.get(key, default) - - def copy(self): - return DomainWithDefaultDict(self.domains.copy(), self.default) - - def iteritems(self): - return self.domains.iteritems() - - def iterkeys(self): - return self.domains.iterkeys() - - def itervalues(self): - return self.domains.itervalues() - - def keys(self): - return self.domains.keys() - - def values(self): - return self.domains.values() - - def items(self): - return self.domains.items() - - def popitem(self): - return self.domains.popitem() - - def update(self, other): - return self.domains.update(other) - - def clear(self): - return self.domains.clear() - - def setdefault(self, key, default): - return self.domains.setdefault(key, default) - -class IDomain(Interface): - """An email domain.""" - - def exists(user): - """ - Check whether or not the specified user exists in this domain. - - @type user: C{twisted.protocols.smtp.User} - @param user: The user to check - - @rtype: No-argument callable - @return: A C{Deferred} which becomes, or a callable which - takes no arguments and returns an object implementing C{IMessage}. - This will be called and the returned object used to deliver the - message when it arrives. - - @raise twisted.protocols.smtp.SMTPBadRcpt: Raised if the given - user does not exist in this domain. - """ - - def addUser(user, password): - """Add a username/password to this domain.""" - - def startMessage(user): - """Create and return a new message to be delivered to the given user. - - DEPRECATED. Implement validateTo() correctly instead. - """ - - def getCredentialsCheckers(): - """Return a list of ICredentialsChecker implementors for this domain. - """ - -class IAliasableDomain(IDomain): - def setAliasGroup(aliases): - """Set the group of defined aliases for this domain - - @type aliases: C{dict} - @param aliases: Mapping of domain names to objects implementing - C{IAlias} - """ - - def exists(user, memo=None): - """ - Check whether or not the specified user exists in this domain. - - @type user: C{twisted.protocols.smtp.User} - @param user: The user to check - - @type memo: C{dict} - @param memo: A record of the addresses already considered while - resolving aliases. The default value should be used by all - external code. - - @rtype: No-argument callable - @return: A C{Deferred} which becomes, or a callable which - takes no arguments and returns an object implementing C{IMessage}. - This will be called and the returned object used to deliver the - message when it arrives. - - @raise twisted.protocols.smtp.SMTPBadRcpt: Raised if the given - user does not exist in this domain. - """ - -class BounceDomain: - """A domain in which no user exists. - - This can be used to block off certain domains. - """ - - implements(IDomain) - - def exists(self, user): - raise smtp.SMTPBadRcpt(user) - - def willRelay(self, user, protocol): - return False - - def addUser(self, user, password): - pass - - def startMessage(self, user): - """ - No code should ever call this function. - """ - raise NotImplementedError( - "No code should ever call this method for any reason") - - def getCredentialsCheckers(self): - return [] - - -class FileMessage: - """A file we can write an email too.""" - - implements(smtp.IMessage) - - def __init__(self, fp, name, finalName): - self.fp = fp - self.name = name - self.finalName = finalName - - def lineReceived(self, line): - self.fp.write(line+'\n') - - def eomReceived(self): - self.fp.close() - os.rename(self.name, self.finalName) - return defer.succeed(self.finalName) - - def connectionLost(self): - self.fp.close() - os.remove(self.name) - - -class MailService(service.MultiService): - """An email service.""" - - queue = None - domains = None - portals = None - aliases = None - smtpPortal = None - - def __init__(self): - service.MultiService.__init__(self) - # Domains and portals for "client" protocols - POP3, IMAP4, etc - self.domains = DomainWithDefaultDict({}, BounceDomain()) - self.portals = {} - - self.monitor = FileMonitoringService() - self.monitor.setServiceParent(self) - self.smtpPortal = cred.portal.Portal(self) - - def getPOP3Factory(self): - return protocols.POP3Factory(self) - - def getSMTPFactory(self): - return protocols.SMTPFactory(self, self.smtpPortal) - - def getESMTPFactory(self): - return protocols.ESMTPFactory(self, self.smtpPortal) - - def addDomain(self, name, domain): - portal = cred.portal.Portal(domain) - map(portal.registerChecker, domain.getCredentialsCheckers()) - self.domains[name] = domain - self.portals[name] = portal - if self.aliases and IAliasableDomain.providedBy(domain): - domain.setAliasGroup(self.aliases) - - def setQueue(self, queue): - """Set the queue for outgoing emails.""" - self.queue = queue - - def requestAvatar(self, avatarId, mind, *interfaces): - if smtp.IMessageDelivery in interfaces: - a = protocols.ESMTPDomainDelivery(self, avatarId) - return smtp.IMessageDelivery, a, lambda: None - raise NotImplementedError() - - def lookupPortal(self, name): - return self.portals[name] - - def defaultPortal(self): - return self.portals[''] - - -class FileMonitoringService(internet.TimerService): - - def __init__(self): - self.files = [] - self.intervals = iter(util.IntervalDifferential([], 60)) - - def startService(self): - service.Service.startService(self) - self._setupMonitor() - - def _setupMonitor(self): - from twisted.internet import reactor - t, self.index = self.intervals.next() - self._call = reactor.callLater(t, self._monitor) - - def stopService(self): - service.Service.stopService(self) - if self._call: - self._call.cancel() - self._call = None - - def monitorFile(self, name, callback, interval=10): - try: - mtime = os.path.getmtime(name) - except: - mtime = 0 - self.files.append([interval, name, callback, mtime]) - self.intervals.addInterval(interval) - - def unmonitorFile(self, name): - for i in range(len(self.files)): - if name == self.files[i][1]: - self.intervals.removeInterval(self.files[i][0]) - del self.files[i] - break - - def _monitor(self): - self._call = None - if self.index is not None: - name, callback, mtime = self.files[self.index][1:] - try: - now = os.path.getmtime(name) - except: - now = 0 - if now > mtime: - log.msg("%s changed, notifying listener" % (name,)) - self.files[self.index][3] = now - callback(name) - self._setupMonitor() diff --git a/tools/buildbot/pylibs/twisted/mail/maildir.py b/tools/buildbot/pylibs/twisted/mail/maildir.py deleted file mode 100644 index 439e92e38..0000000 --- a/tools/buildbot/pylibs/twisted/mail/maildir.py +++ /dev/null @@ -1,459 +0,0 @@ -# -*- test-case-name: twisted.mail.test.test_mail -*- -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. -# See LICENSE for details. - - -"""Maildir-style mailbox support -""" - -from __future__ import generators - -import os -import stat -import socket -import time -import md5 -import cStringIO - -from zope.interface import implements - -try: - import cStringIO as StringIO -except ImportError: - import StringIO - -from twisted.mail import pop3 -from twisted.mail import smtp -from twisted.protocols import basic -from twisted.persisted import dirdbm -from twisted.python import log, failure -from twisted.mail import mail -from twisted.mail import alias -from twisted.internet import interfaces, defer, reactor - -from twisted import cred -import twisted.cred.portal -import twisted.cred.credentials -import twisted.cred.checkers -import twisted.cred.error - -INTERNAL_ERROR = '''\ -From: Twisted.mail Internals -Subject: An Error Occurred - - An internal server error has occurred. Please contact the - server administrator. -''' - -class _MaildirNameGenerator: - """Utility class to generate a unique maildir name - """ - n = 0 - p = os.getpid() - s = socket.gethostname().replace('/', r'\057').replace(':', r'\072') - - def generate(self): - self.n = self.n + 1 - t = time.time() - seconds = str(int(t)) - microseconds = str(int((t-int(t))*10e6)) - return '%s.M%sP%sQ%s.%s' % (seconds, microseconds, - self.p, self.n, self.s) - -_generateMaildirName = _MaildirNameGenerator().generate - -def initializeMaildir(dir): - if not os.path.isdir(dir): - os.mkdir(dir, 0700) - for subdir in ['new', 'cur', 'tmp', '.Trash']: - os.mkdir(os.path.join(dir, subdir), 0700) - for subdir in ['new', 'cur', 'tmp']: - os.mkdir(os.path.join(dir, '.Trash', subdir), 0700) - # touch - open(os.path.join(dir, '.Trash', 'maildirfolder'), 'w').close() - - -class MaildirMessage(mail.FileMessage): - size = None - - def __init__(self, address, fp, *a, **kw): - header = "Delivered-To: %s\n" % address - fp.write(header) - self.size = len(header) - mail.FileMessage.__init__(self, fp, *a, **kw) - - def lineReceived(self, line): - mail.FileMessage.lineReceived(self, line) - self.size += len(line)+1 - - def eomReceived(self): - self.finalName = self.finalName+',S=%d' % self.size - return mail.FileMessage.eomReceived(self) - -class AbstractMaildirDomain: - """Abstract maildir-backed domain. - """ - alias = None - root = None - - def __init__(self, service, root): - """Initialize. - """ - self.root = root - - def userDirectory(self, user): - """Get the maildir directory for a given user - - Override to specify where to save mails for users. - Return None for non-existing users. - """ - return None - - ## - ## IAliasableDomain - ## - - def setAliasGroup(self, alias): - self.alias = alias - - ## - ## IDomain - ## - def exists(self, user, memo=None): - """Check for existence of user in the domain - """ - if self.userDirectory(user.dest.local) is not None: - return lambda: self.startMessage(user) - try: - a = self.alias[user.dest.local] - except: - raise smtp.SMTPBadRcpt(user) - else: - aliases = a.resolve(self.alias, memo) - if aliases: - return lambda: aliases - log.err("Bad alias configuration: " + str(user)) - raise smtp.SMTPBadRcpt(user) - - def startMessage(self, user): - """Save a message for a given user - """ - if isinstance(user, str): - name, domain = user.split('@', 1) - else: - name, domain = user.dest.local, user.dest.domain - dir = self.userDirectory(name) - fname = _generateMaildirName() - filename = os.path.join(dir, 'tmp', fname) - fp = open(filename, 'w') - return MaildirMessage('%s@%s' % (name, domain), fp, filename, - os.path.join(dir, 'new', fname)) - - def willRelay(self, user, protocol): - return False - - def addUser(self, user, password): - raise NotImplementedError - - def getCredentialsCheckers(self): - raise NotImplementedError - ## - ## end of IDomain - ## - -class _MaildirMailboxAppendMessageTask: - implements(interfaces.IConsumer) - - osopen = staticmethod(os.open) - oswrite = staticmethod(os.write) - osclose = staticmethod(os.close) - osrename = staticmethod(os.rename) - - def __init__(self, mbox, msg): - self.mbox = mbox - self.defer = defer.Deferred() - self.openCall = None - if not hasattr(msg, "read"): - msg = StringIO.StringIO(msg) - self.msg = msg - # This is needed, as this startup phase might call defer.errback and zero out self.defer - # By doing it on the reactor iteration appendMessage is able to use .defer without problems. - reactor.callLater(0, self.startUp) - - def startUp(self): - self.createTempFile() - if self.fh != -1: - self.filesender = basic.FileSender() - self.filesender.beginFileTransfer(self.msg, self) - - def registerProducer(self, producer, streaming): - self.myproducer = producer - self.streaming = streaming - if not streaming: - self.prodProducer() - - def prodProducer(self): - self.openCall = None - if self.myproducer is not None: - self.openCall = reactor.callLater(0, self.prodProducer) - self.myproducer.resumeProducing() - - def unregisterProducer(self): - self.myproducer = None - self.streaming = None - self.osclose(self.fh) - self.moveFileToNew() - - def write(self, data): - try: - self.oswrite(self.fh, data) - except: - self.fail() - - def fail(self, err=None): - if err is None: - err = failure.Failure() - if self.openCall is not None: - self.openCall.cancel() - self.defer.errback(err) - self.defer = None - - def moveFileToNew(self): - while True: - newname = os.path.join(self.mbox.path, "new", _generateMaildirName()) - try: - self.osrename(self.tmpname, newname) - break - except OSError, (err, estr): - import errno - # if the newname exists, retry with a new newname. - if err != errno.EEXIST: - self.fail() - newname = None - break - if newname is not None: - self.mbox.list.append(newname) - self.defer.callback(None) - self.defer = None - - def createTempFile(self): - attr = (os.O_RDWR | os.O_CREAT | os.O_EXCL - | getattr(os, "O_NOINHERIT", 0) - | getattr(os, "O_NOFOLLOW", 0)) - tries = 0 - self.fh = -1 - while True: - self.tmpname = os.path.join(self.mbox.path, "tmp", _generateMaildirName()) - try: - self.fh = self.osopen(self.tmpname, attr, 0600) - return None - except OSError: - tries += 1 - if tries > 500: - self.defer.errback(RuntimeError("Could not create tmp file for %s" % self.mbox.path)) - self.defer = None - return None - -class MaildirMailbox(pop3.Mailbox): - """Implement the POP3 mailbox semantics for a Maildir mailbox - """ - AppendFactory = _MaildirMailboxAppendMessageTask - - def __init__(self, path): - """Initialize with name of the Maildir mailbox - """ - self.path = path - self.list = [] - self.deleted = {} - initializeMaildir(path) - for name in ('cur', 'new'): - for file in os.listdir(os.path.join(path, name)): - self.list.append((file, os.path.join(path, name, file))) - self.list.sort() - self.list = [e[1] for e in self.list] - - def listMessages(self, i=None): - """Return a list of lengths of all files in new/ and cur/ - """ - if i is None: - ret = [] - for mess in self.list: - if mess: - ret.append(os.stat(mess)[stat.ST_SIZE]) - else: - ret.append(0) - return ret - return self.list[i] and os.stat(self.list[i])[stat.ST_SIZE] or 0 - - def getMessage(self, i): - """Return an open file-pointer to a message - """ - return open(self.list[i]) - - def getUidl(self, i): - """Return a unique identifier for a message - - This is done using the basename of the filename. - It is globally unique because this is how Maildirs are designed. - """ - # Returning the actual filename is a mistake. Hash it. - base = os.path.basename(self.list[i]) - return md5.md5(base).hexdigest() - - def deleteMessage(self, i): - """Delete a message - - This only moves a message to the .Trash/ subfolder, - so it can be undeleted by an administrator. - """ - trashFile = os.path.join( - self.path, '.Trash', 'cur', os.path.basename(self.list[i]) - ) - os.rename(self.list[i], trashFile) - self.deleted[self.list[i]] = trashFile - self.list[i] = 0 - - def undeleteMessages(self): - """Undelete any deleted messages it is possible to undelete - - This moves any messages from .Trash/ subfolder back to their - original position, and empties out the deleted dictionary. - """ - for (real, trash) in self.deleted.items(): - try: - os.rename(trash, real) - except OSError, (err, estr): - import errno - # If the file has been deleted from disk, oh well! - if err != errno.ENOENT: - raise - # This is a pass - else: - try: - self.list[self.list.index(0)] = real - except ValueError: - self.list.append(real) - self.deleted.clear() - - def appendMessage(self, txt): - """Appends a message into the mailbox.""" - task = self.AppendFactory(self, txt) - return task.defer - -class StringListMailbox: - implements(pop3.IMailbox) - - def __init__(self, msgs): - self.msgs = msgs - - def listMessages(self, i=None): - if i is None: - return map(len, self.msgs) - return len(self.msgs[i]) - - def getMessage(self, i): - return StringIO.StringIO(self.msgs[i]) - - def getUidl(self, i): - return md5.new(self.msgs[i]).hexdigest() - - def deleteMessage(self, i): - pass - - def undeleteMessages(self): - pass - - def sync(self): - pass - -class MaildirDirdbmDomain(AbstractMaildirDomain): - """A Maildir Domain where membership is checked by a dirdbm file - """ - - implements(cred.portal.IRealm, mail.IAliasableDomain) - - portal = None - _credcheckers = None - - def __init__(self, service, root, postmaster=0): - """Initialize - - The first argument is where the Domain directory is rooted. - The second is whether non-existing addresses are simply - forwarded to postmaster instead of outright bounce - - The directory structure of a MailddirDirdbmDomain is: - - /passwd <-- a dirdbm file - /USER/{cur,new,del} <-- each user has these three directories - """ - AbstractMaildirDomain.__init__(self, service, root) - dbm = os.path.join(root, 'passwd') - if not os.path.exists(dbm): - os.makedirs(dbm) - self.dbm = dirdbm.open(dbm) - self.postmaster = postmaster - - def userDirectory(self, name): - """Get the directory for a user - - If the user exists in the dirdbm file, return the directory - os.path.join(root, name), creating it if necessary. - Otherwise, returns postmaster's mailbox instead if bounces - go to postmaster, otherwise return None - """ - if not self.dbm.has_key(name): - if not self.postmaster: - return None - name = 'postmaster' - dir = os.path.join(self.root, name) - if not os.path.exists(dir): - initializeMaildir(dir) - return dir - - ## - ## IDomain - ## - def addUser(self, user, password): - self.dbm[user] = password - # Ensure it is initialized - self.userDirectory(user) - - def getCredentialsCheckers(self): - if self._credcheckers is None: - self._credcheckers = [DirdbmDatabase(self.dbm)] - return self._credcheckers - - ## - ## IRealm - ## - def requestAvatar(self, avatarId, mind, *interfaces): - if pop3.IMailbox not in interfaces: - raise NotImplementedError("No interface") - if avatarId == cred.checkers.ANONYMOUS: - mbox = StringListMailbox([INTERNAL_ERROR]) - else: - mbox = MaildirMailbox(os.path.join(self.root, avatarId)) - - return ( - pop3.IMailbox, - mbox, - lambda: None - ) - -class DirdbmDatabase: - implements(cred.checkers.ICredentialsChecker) - - credentialInterfaces = ( - cred.credentials.IUsernamePassword, - cred.credentials.IUsernameHashedPassword - ) - - def __init__(self, dbm): - self.dirdbm = dbm - - def requestAvatarId(self, c): - if c.username in self.dirdbm: - if c.checkPassword(self.dirdbm[c.username]): - return c.username - raise cred.error.UnauthorizedLogin() diff --git a/tools/buildbot/pylibs/twisted/mail/pb.py b/tools/buildbot/pylibs/twisted/mail/pb.py deleted file mode 100644 index dfe72d3..0000000 --- a/tools/buildbot/pylibs/twisted/mail/pb.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. -# See LICENSE for details. - - -from twisted.spread import pb -from twisted.spread import banana - -import os -import types - -class Maildir(pb.Referenceable): - - def __init__(self, directory, rootDirectory): - self.virtualDirectory = directory - self.rootDirectory = rootDirectory - self.directory = os.path.join(rootDirectory, directory) - - def getFolderMessage(self, folder, name): - if '/' in name: - raise IOError("can only open files in '%s' directory'" % folder) - fp = open(os.path.join(self.directory, 'new', name)) - try: - return fp.read() - finally: - fp.close() - - def deleteFolderMessage(self, folder, name): - if '/' in name: - raise IOError("can only delete files in '%s' directory'" % folder) - os.rename(os.path.join(self.directory, folder, name), - os.path.join(self.rootDirectory, '.Trash', folder, name)) - - def deleteNewMessage(self, name): - return self.deleteFolderMessage('new', name) - remote_deleteNewMessage = deleteNewMessage - - def deleteCurMessage(self, name): - return self.deleteFolderMessage('cur', name) - remote_deleteCurMessage = deleteCurMessage - - def getNewMessages(self): - return os.listdir(os.path.join(self.directory, 'new')) - remote_getNewMessages = getNewMessages - - def getCurMessages(self): - return os.listdir(os.path.join(self.directory, 'cur')) - remote_getCurMessages = getCurMessages - - def getNewMessage(self, name): - return self.getFolderMessage('new', name) - remote_getNewMessage = getNewMessage - - def getCurMessage(self, name): - return self.getFolderMessage('cur', name) - remote_getCurMessage = getCurMessage - - def getSubFolder(self, name): - if name[0] == '.': - raise IOError("subfolder name cannot begin with a '.'") - name = name.replace('/', ':') - if self.virtualDirectoy == '.': - name = '.'+name - else: - name = self.virtualDirectory+':'+name - if not self._isSubFolder(name): - raise IOError("not a subfolder") - return Maildir(name, self.rootDirectory) - remote_getSubFolder = getSubFolder - - def _isSubFolder(self, name): - return (not os.path.isdir(os.path.join(self.rootDirectory, name)) or - not os.path.isfile(os.path.join(self.rootDirectory, name, - 'maildirfolder'))) - - -class MaildirCollection(pb.Referenceable): - - def __init__(self, root): - self.root = root - - def getSubFolders(self): - return os.listdir(self.getRoot()) - remote_getSubFolders = getSubFolders - - def getSubFolder(self, name): - if '/' in name or name[0] == '.': - raise IOError("invalid name") - return Maildir('.', os.path.join(self.getRoot(), name)) - remote_getSubFolder = getSubFolder - - -class MaildirBroker(pb.Broker): - - def proto_getCollection(self, requestID, name, domain, password): - collection = self._getCollection() - if collection is None: - self.sendError(requestID, "permission denied") - else: - self.sendAnswer(requestID, collection) - - def getCollection(self, name, domain, password): - if not self.domains.has_key(domain): - return - domain = self.domains[domain] - if (domain.dbm.has_key(name) and - domain.dbm[name] == password): - return MaildirCollection(domain.userDirectory(name)) - - -class MaildirClient(pb.Broker): - - def getCollection(self, name, domain, password, callback, errback): - requestID = self.newRequestID() - self.waitingForAnswers[requestID] = callback, errback - self.sendCall("getCollection", requestID, name, domain, password) diff --git a/tools/buildbot/pylibs/twisted/mail/pop3.py b/tools/buildbot/pylibs/twisted/mail/pop3.py deleted file mode 100644 index 5c3dd13..0000000 --- a/tools/buildbot/pylibs/twisted/mail/pop3.py +++ /dev/null @@ -1,1072 +0,0 @@ -# -*- test-case-name: twisted.mail.test.test_pop3 -*- -# -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. -# See LICENSE for details. - - -""" -Post-office Protocol version 3 - -@author: U{Glyph Lefkowitz<mailto:glyph@twistedmatrix.com>} -@author: U{Jp Calderone<mailto:exarkun@twistedmatrix.com>} -""" - -import string -import base64 -import binascii -import md5 -import warnings - -from zope.interface import implements, Interface - -from twisted.mail import smtp -from twisted.protocols import basic -from twisted.protocols import policies -from twisted.internet import task -from twisted.internet import defer -from twisted.internet import interfaces -from twisted.python import log - -from twisted import cred -import twisted.cred.error -import twisted.cred.credentials - -## -## Authentication -## -class APOPCredentials: - implements(cred.credentials.IUsernamePassword) - - def __init__(self, magic, username, digest): - self.magic = magic - self.username = username - self.digest = digest - - def checkPassword(self, password): - seed = self.magic + password - myDigest = md5.new(seed).hexdigest() - return myDigest == self.digest - - -class _HeadersPlusNLines: - def __init__(self, f, n): - self.f = f - self.n = n - self.linecount = 0 - self.headers = 1 - self.done = 0 - self.buf = '' - - def read(self, bytes): - if self.done: - return '' - data = self.f.read(bytes) - if not data: - return data - if self.headers: - df, sz = data.find('\r\n\r\n'), 4 - if df == -1: - df, sz = data.find('\n\n'), 2 - if df != -1: - df += sz - val = data[:df] - data = data[df:] - self.linecount = 1 - self.headers = 0 - else: - val = '' - if self.linecount > 0: - dsplit = (self.buf+data).split('\n') - self.buf = dsplit[-1] - for ln in dsplit[:-1]: - if self.linecount > self.n: - self.done = 1 - return val - val += (ln + '\n') - self.linecount += 1 - return val - else: - return data - - - -class _POP3MessageDeleted(Exception): - """ - Internal control-flow exception. Indicates the file of a deleted message - was requested. - """ - - -class POP3Error(Exception): - pass - - - -class _IteratorBuffer(object): - bufSize = 0 - - def __init__(self, write, iterable, memoryBufferSize=None): - """ - Create a _IteratorBuffer. - - @param write: A one-argument callable which will be invoked with a list - of strings which have been buffered. - - @param iterable: The source of input strings as any iterable. - - @param memoryBufferSize: The upper limit on buffered string length, - beyond which the buffer will be flushed to the writer. - """ - self.lines = [] - self.write = write - self.iterator = iter(iterable) - if memoryBufferSize is None: - memoryBufferSize = 2 ** 16 - self.memoryBufferSize = memoryBufferSize - - - def __iter__(self): - return self - - - def next(self): - try: - v = self.iterator.next() - except StopIteration: - if self.lines: - self.write(self.lines) - # Drop some references, in case they're edges in a cycle. - del self.iterator, self.lines, self.write - raise - else: - if v is not None: - self.lines.append(v) - self.bufSize += len(v) - if self.bufSize > self.memoryBufferSize: - self.write(self.lines) - self.lines = [] - self.bufSize = 0 - - - -def iterateLineGenerator(proto, gen): - """ - Hook the given protocol instance up to the given iterator with an - _IteratorBuffer and schedule the result to be exhausted via the protocol. - - @type proto: L{POP3} - @type gen: iterator - @rtype: L{twisted.internet.defer.Deferred} - """ - coll = _IteratorBuffer(proto.transport.writeSequence, gen) - return proto.schedule(coll) - - - -def successResponse(response): - """ - Format the given object as a positive response. - """ - response = str(response) - return '+OK %s\r\n' % (response,) - - - -def formatStatResponse(msgs): - """ - Format the list of message sizes appropriately for a STAT response. - - Yields None until it finishes computing a result, then yields a str - instance that is suitable for use as a response to the STAT command. - Intended to be used with a L{twisted.internet.task.Cooperator}. - """ - i = 0 - bytes = 0 - for size in msgs: - i += 1 - bytes += size - yield None - yield successResponse('%d %d' % (i, bytes)) - - - -def formatListLines(msgs): - """ - Format a list of message sizes appropriately for the lines of a LIST - response. - - Yields str instances formatted appropriately for use as lines in the - response to the LIST command. Does not include the trailing '.'. - """ - i = 0 - for size in msgs: - i += 1 - yield '%d %d\r\n' % (i, size) - - - -def formatListResponse(msgs): - """ - Format a list of message sizes appropriately for a complete LIST response. - - Yields str instances formatted appropriately for use as a LIST command - response. - """ - yield successResponse(len(msgs)) - for ele in formatListLines(msgs): - yield ele - yield '.\r\n' - - - -def formatUIDListLines(msgs, getUidl): - """ - Format the list of message sizes appropriately for the lines of a UIDL - response. - - Yields str instances formatted appropriately for use as lines in the - response to the UIDL command. Does not include the trailing '.'. - """ - for i, m in enumerate(msgs): - if m is not None: - uid = getUidl(i) - yield '%d %s\r\n' % (i + 1, uid) - - - -def formatUIDListResponse(msgs, getUidl): - """ - Format a list of message sizes appropriately for a complete UIDL response. - - Yields str instances formatted appropriately for use as a UIDL command - response. - """ - yield successResponse('') - for ele in formatUIDListLines(msgs, getUidl): - yield ele - yield '.\r\n' - - - -class POP3(basic.LineOnlyReceiver, policies.TimeoutMixin): - """ - POP3 server protocol implementation. - - @ivar portal: A reference to the L{twisted.cred.portal.Portal} instance we - will authenticate through. - - @ivar factory: A L{twisted.mail.pop3.IServerFactory} which will be used to - determine some extended behavior of the server. - - @ivar timeOut: An integer which defines the minimum amount of time which - may elapse without receiving any traffic after which the client will be - disconnected. - - @ivar schedule: A one-argument callable which should behave like - L{twisted.internet.task.coiterate}. - """ - implements(interfaces.IProducer) - - magic = None - _userIs = None - _onLogout = None - - AUTH_CMDS = ['CAPA', 'USER', 'PASS', 'APOP', 'AUTH', 'RPOP', 'QUIT'] - - portal = None - factory = None - - # The mailbox we're serving - mbox = None - - # Set this pretty low -- POP3 clients are expected to log in, download - # everything, and log out. - timeOut = 300 - - # Current protocol state - state = "COMMAND" - - # PIPELINE - blocked = None - - # Cooperate and suchlike. - schedule = staticmethod(task.coiterate) - - # Message index of the highest retrieved message. - _highest = 0 - - def connectionMade(self): - if self.magic is None: - self.magic = self.generateMagic() - self.successResponse(self.magic) - self.setTimeout(self.timeOut) - if getattr(self.factory, 'noisy', True): - log.msg("New connection from " + str(self.transport.getPeer())) - - - def connectionLost(self, reason): - if self._onLogout is not None: - self._onLogout() - self._onLogout = None - self.setTimeout(None) - - - def generateMagic(self): - return smtp.messageid() - - - def successResponse(self, message=''): - self.transport.write(successResponse(message)) - - def failResponse(self, message=''): - self.sendLine('-ERR ' + str(message)) - -# def sendLine(self, line): -# print 'S:', repr(line) -# basic.LineOnlyReceiver.sendLine(self, line) - - def lineReceived(self, line): -# print 'C:', repr(line) - self.resetTimeout() - getattr(self, 'state_' + self.state)(line) - - def _unblock(self, _): - commands = self.blocked - self.blocked = None - while commands and self.blocked is None: - cmd, args = commands.pop(0) - self.processCommand(cmd, *args) - if self.blocked is not None: - self.blocked.extend(commands) - - def state_COMMAND(self, line): - try: - return self.processCommand(*line.split(' ')) - except (ValueError, AttributeError, POP3Error, TypeError), e: - log.err() - self.failResponse('bad protocol or server: %s: %s' % (e.__class__.__name__, e)) - - def processCommand(self, command, *args): - if self.blocked is not None: - self.blocked.append((command, args)) - return - - command = string.upper(command) - authCmd = command in self.AUTH_CMDS - if not self.mbox and not authCmd: - raise POP3Error("not authenticated yet: cannot do " + command) - f = getattr(self, 'do_' + command, None) - if f: - return f(*args) - raise POP3Error("Unknown protocol command: " + command) - - - def listCapabilities(self): - baseCaps = [ - "TOP", - "USER", - "UIDL", - "PIPELINE", - "CELERITY", - "AUSPEX", - "POTENCE", - ] - - if IServerFactory.providedBy(self.factory): - # Oh my god. We can't just loop over a list of these because - # each has spectacularly different return value semantics! - try: - v = self.factory.cap_IMPLEMENTATION() - except NotImplementedError: - pass - except: - log.err() - else: - baseCaps.append("IMPLEMENTATION " + str(v)) - - try: - v = self.factory.cap_EXPIRE() - except NotImplementedError: - pass - except: - log.err() - else: - if v is None: - v = "NEVER" - if self.factory.perUserExpiration(): - if self.mbox: - v = str(self.mbox.messageExpiration) - else: - v = str(v) + " USER" - v = str(v) - baseCaps.append("EXPIRE " + v) - - try: - v = self.factory.cap_LOGIN_DELAY() - except NotImplementedError: - pass - except: - log.err() - else: - if self.factory.perUserLoginDelay(): - if self.mbox: - v = str(self.mbox.loginDelay) - else: - v = str(v) + " USER" - v = str(v) - baseCaps.append("LOGIN-DELAY " + v) - - try: - v = self.factory.challengers - except AttributeError: - pass - except: - log.err() - else: - baseCaps.append("SASL " + ' '.join(v.keys())) - return baseCaps - - def do_CAPA(self): - self.successResponse("I can do the following:") - for cap in self.listCapabilities(): - self.sendLine(cap) - self.sendLine(".") - - def do_AUTH(self, args=None): - if not getattr(self.factory, 'challengers', None): - self.failResponse("AUTH extension unsupported") - return - - if args is None: - self.successResponse("Supported authentication methods:") - for a in self.factory.challengers: - self.sendLine(a.upper()) - self.sendLine(".") - return - - auth = self.factory.challengers.get(args.strip().upper()) - if not self.portal or not auth: - self.failResponse("Unsupported SASL selected") - return - - self._auth = auth() - chal = self._auth.getChallenge() - - self.sendLine('+ ' + base64.encodestring(chal).rstrip('\n')) - self.state = 'AUTH' - - def state_AUTH(self, line): - self.state = "COMMAND" - try: - parts = base64.decodestring(line).split(None, 1) - except binascii.Error: - self.failResponse("Invalid BASE64 encoding") - else: - if len(parts) != 2: - self.failResponse("Invalid AUTH response") - return - self._auth.username = parts[0] - self._auth.response = parts[1] - d = self.portal.login(self._auth, None, IMailbox) - d.addCallback(self._cbMailbox, parts[0]) - d.addErrback(self._ebMailbox) - d.addErrback(self._ebUnexpected) - - def do_APOP(self, user, digest): - d = defer.maybeDeferred(self.authenticateUserAPOP, user, digest) - d.addCallbacks(self._cbMailbox, self._ebMailbox, callbackArgs=(user,) - ).addErrback(self._ebUnexpected) - - def _cbMailbox(self, (interface, avatar, logout), user): - if interface is not IMailbox: - self.failResponse('Authentication failed') - log.err("_cbMailbox() called with an interface other than IMailbox") - return - - self.mbox = avatar - self._onLogout = logout - self.successResponse('Authentication succeeded') - if getattr(self.factory, 'noisy', True): - log.msg("Authenticated login for " + user) - - def _ebMailbox(self, failure): - failure = failure.trap(cred.error.LoginDenied, cred.error.LoginFailed) - if issubclass(failure, cred.error.LoginDenied): - self.failResponse("Access denied: " + str(failure)) - elif issubclass(failure, cred.error.LoginFailed): - self.failResponse('Authentication failed') - if getattr(self.factory, 'noisy', True): - log.msg("Denied login attempt from " + str(self.transport.getPeer())) - - def _ebUnexpected(self, failure): - self.failResponse('Server error: ' + failure.getErrorMessage()) - log.err(failure) - - def do_USER(self, user): - self._userIs = user - self.successResponse('USER accepted, send PASS') - - def do_PASS(self, password): - if self._userIs is None: - self.failResponse("USER required before PASS") - return - user = self._userIs - self._userIs = None - d = defer.maybeDeferred(self.authenticateUserPASS, user, password) - d.addCallbacks(self._cbMailbox, self._ebMailbox, callbackArgs=(user,) - ).addErrback(self._ebUnexpected) - - - def _longOperation(self, d): - # Turn off timeouts and block further processing until the Deferred - # fires, then reverse those changes. - timeOut = self.timeOut - self.setTimeout(None) - self.blocked = [] - d.addCallback(self._unblock) - d.addCallback(lambda ign: self.setTimeout(timeOut)) - return d - - - def _coiterate(self, gen): - return self.schedule(_IteratorBuffer(self.transport.writeSequence, gen)) - - - def do_STAT(self): - d = defer.maybeDeferred(self.mbox.listMessages) - def cbMessages(msgs): - return self._coiterate(formatStatResponse(msgs)) - def ebMessages(err): - self.failResponse(err.getErrorMessage()) - log.msg("Unexpected do_STAT failure:") - log.err(err) - return self._longOperation(d.addCallbacks(cbMessages, ebMessages)) - - - def do_LIST(self, i=None): - if i is None: - d = defer.maybeDeferred(self.mbox.listMessages) - def cbMessages(msgs): - return self._coiterate(formatListResponse(msgs)) - def ebMessages(err): - self.failResponse(err.getErrorMessage()) - log.msg("Unexpected do_LIST failure:") - log.err(err) - return self._longOperation(d.addCallbacks(cbMessages, ebMessages)) - else: - try: - i = int(i) - if i < 1: - raise ValueError() - except ValueError: - self.failResponse("Invalid message-number: %r" % (i,)) - else: - d = defer.maybeDeferred(self.mbox.listMessages, i - 1) - def cbMessage(msg): - self.successResponse('%d %d' % (i, msg)) - def ebMessage(err): - errcls = err.check(ValueError, IndexError) - if errcls is not None: - if errcls is IndexError: - # IndexError was supported for a while, but really - # shouldn't be. One error condition, one exception - # type. - warnings.warn( - "twisted.mail.pop3.IMailbox.listMessages may not " - "raise IndexError for out-of-bounds message numbers: " - "raise ValueError instead.", - PendingDeprecationWarning) - self.failResponse("Invalid message-number: %r" % (i,)) - else: - self.failResponse(err.getErrorMessage()) - log.msg("Unexpected do_LIST failure:") - log.err(err) - return self._longOperation(d.addCallbacks(cbMessage, ebMessage)) - - - def do_UIDL(self, i=None): - if i is None: - d = defer.maybeDeferred(self.mbox.listMessages) - def cbMessages(msgs): - return self._coiterate(formatUIDListResponse(msgs, self.mbox.getUidl)) - def ebMessages(err): - self.failResponse(err.getErrorMessage()) - log.msg("Unexpected do_UIDL failure:") - log.err(err) - return self._longOperation(d.addCallbacks(cbMessages, ebMessages)) - else: - try: - i = int(i) - if i < 1: - raise ValueError() - except ValueError: - self.failResponse("Bad message number argument") - else: - try: - msg = self.mbox.getUidl(i - 1) - except IndexError: - # XXX TODO See above comment regarding IndexError. - warnings.warn( - "twisted.mail.pop3.IMailbox.getUidl may not " - "raise IndexError for out-of-bounds message numbers: " - "raise ValueError instead.", - PendingDeprecationWarning) - self.failResponse("Bad message number argument") - except ValueError: - self.failResponse("Bad message number argument") - else: - self.successResponse(str(msg)) - - - def _getMessageFile(self, i): - """ - Retrieve the size and contents of a given message, as a two-tuple. - - @param i: The number of the message to operate on. This is a base-ten - string representation starting at 1. - - @return: A Deferred which fires with a two-tuple of an integer and a - file-like object. - """ - try: - msg = int(i) - 1 - if msg < 0: - raise ValueError() - except ValueError: - self.failResponse("Bad message number argument") - return defer.succeed(None) - - sizeDeferred = defer.maybeDeferred(self.mbox.listMessages, msg) - def cbMessageSize(size): - if not size: - return defer.fail(_POP3MessageDeleted()) - fileDeferred = defer.maybeDeferred(self.mbox.getMessage, msg) - fileDeferred.addCallback(lambda fObj: (size, fObj)) - return fileDeferred - - def ebMessageSomething(err): - errcls = err.check(_POP3MessageDeleted, ValueError, IndexError) - if errcls is _POP3MessageDeleted: - self.failResponse("message deleted") - elif errcls in (ValueError, IndexError): - if errcls is IndexError: - # XXX TODO See above comment regarding IndexError. - warnings.warn( - "twisted.mail.pop3.IMailbox.listMessages may not " - "raise IndexError for out-of-bounds message numbers: " - "raise ValueError instead.", - PendingDeprecationWarning) - self.failResponse("Bad message number argument") - else: - log.msg("Unexpected _getMessageFile failure:") - log.err(err) - return None - - sizeDeferred.addCallback(cbMessageSize) - sizeDeferred.addErrback(ebMessageSomething) - return sizeDeferred - - - def _sendMessageContent(self, i, fpWrapper, successResponse): - d = self._getMessageFile(i) - def cbMessageFile(info): - if info is None: - # Some error occurred - a failure response has been sent - # already, just give up. - return - - self._highest = max(self._highest, int(i)) - resp, fp = info - fp = fpWrapper(fp) - self.successResponse(successResponse(resp)) - s = basic.FileSender() - d = s.beginFileTransfer(fp, self.transport, self.transformChunk) - - def cbFileTransfer(lastsent): - if lastsent != '\n': - line = '\r\n.' - else: - line = '.' - self.sendLine(line) - - def ebFileTransfer(err): - self.transport.loseConnection() - log.msg("Unexpected error in _sendMessageContent:") - log.err(err) - - d.addCallback(cbFileTransfer) - d.addErrback(ebFileTransfer) - return d - return self._longOperation(d.addCallback(cbMessageFile)) - - - def do_TOP(self, i, size): - try: - size = int(size) - if size < 0: - raise ValueError - except ValueError: - self.failResponse("Bad line count argument") - else: - return self._sendMessageContent( - i, - lambda fp: _HeadersPlusNLines(fp, size), - lambda size: "Top of message follows") - - - def do_RETR(self, i): - return self._sendMessageContent( - i, - lambda fp: fp, - lambda size: "%d" % (size,)) - - - def transformChunk(self, chunk): - return chunk.replace('\n', '\r\n').replace('\r\n.', '\r\n..') - - - def finishedFileTransfer(self, lastsent): - if lastsent != '\n': - line = '\r\n.' - else: - line = '.' - self.sendLine(line) - - - def do_DELE(self, i): - i = int(i)-1 - self.mbox.deleteMessage(i) - self.successResponse() - - - def do_NOOP(self): - """Perform no operation. Return a success code""" - self.successResponse() - - - def do_RSET(self): - """Unset all deleted message flags""" - try: - self.mbox.undeleteMessages() - except: - log.err() - self.failResponse() - else: - self._highest = 0 - self.successResponse() - - - def do_LAST(self): - """ - Return the index of the highest message yet downloaded. - """ - self.successResponse(self._highest) - - - def do_RPOP(self, user): - self.failResponse('permission denied, sucker') - - - def do_QUIT(self): - if self.mbox: - self.mbox.sync() - self.successResponse() - self.transport.loseConnection() - - - def authenticateUserAPOP(self, user, digest): - """Perform authentication of an APOP login. - - @type user: C{str} - @param user: The name of the user attempting to log in. - - @type digest: C{str} - @param digest: The response string with which the user replied. - - @rtype: C{Deferred} - @return: A deferred whose callback is invoked if the login is - successful, and whose errback will be invoked otherwise. The - callback will be passed a 3-tuple consisting of IMailbox, - an object implementing IMailbox, and a zero-argument callable - to be invoked when this session is terminated. - """ - if self.portal is not None: - return self.portal.login( - APOPCredentials(self.magic, user, digest), - None, - IMailbox - ) - raise cred.error.UnauthorizedLogin() - - def authenticateUserPASS(self, user, password): - """Perform authentication of a username/password login. - - @type user: C{str} - @param user: The name of the user attempting to log in. - - @type password: C{str} - @param password: The password to attempt to authenticate with. - - @rtype: C{Deferred} - @return: A deferred whose callback is invoked if the login is - successful, and whose errback will be invoked otherwise. The - callback will be passed a 3-tuple consisting of IMailbox, - an object implementing IMailbox, and a zero-argument callable - to be invoked when this session is terminated. - """ - if self.portal is not None: - return self.portal.login( - cred.credentials.UsernamePassword(user, password), - None, - IMailbox - ) - raise cred.error.UnauthorizedLogin() - - -class IServerFactory(Interface): - """Interface for querying additional parameters of this POP3 server. - - Any cap_* method may raise NotImplementedError if the particular - capability is not supported. If cap_EXPIRE() does not raise - NotImplementedError, perUserExpiration() must be implemented, otherwise - they are optional. If cap_LOGIN_DELAY() is implemented, - perUserLoginDelay() must be implemented, otherwise they are optional. - - @ivar challengers: A dictionary mapping challenger names to classes - implementing C{IUsernameHashedPassword}. - """ - - def cap_IMPLEMENTATION(): - """Return a string describing this POP3 server implementation.""" - - def cap_EXPIRE(): - """Return the minimum number of days messages are retained.""" - - def perUserExpiration(): - """Indicate whether message expiration is per-user. - - @return: True if it is, false otherwise. - """ - - def cap_LOGIN_DELAY(): - """Return the minimum number of seconds between client logins.""" - - def perUserLoginDelay(): - """Indicate whether the login delay period is per-user. - - @return: True if it is, false otherwise. - """ - -class IMailbox(Interface): - """ - @type loginDelay: C{int} - @ivar loginDelay: The number of seconds between allowed logins for the - user associated with this mailbox. None - - @type messageExpiration: C{int} - @ivar messageExpiration: The number of days messages in this mailbox will - remain on the server before being deleted. - """ - - def listMessages(index=None): - """Retrieve the size of one or more messages. - - @type index: C{int} or C{None} - @param index: The number of the message for which to retrieve the - size (starting at 0), or None to retrieve the size of all messages. - - @rtype: C{int} or any iterable of C{int} or a L{Deferred} which fires - with one of these. - - @return: The number of octets in the specified message, or an iterable - of integers representing the number of octets in all the messages. Any - value which would have referred to a deleted message should be set to 0. - - @raise ValueError: if C{index} is greater than the index of any message - in the mailbox. - """ - - def getMessage(index): - """Retrieve a file-like object for a particular message. - - @type index: C{int} - @param index: The number of the message to retrieve - - @rtype: A file-like object - @return: A file containing the message data with lines delimited by - C{\\n}. - """ - - def getUidl(index): - """Get a unique identifier for a particular message. - - @type index: C{int} - @param index: The number of the message for which to retrieve a UIDL - - @rtype: C{str} - @return: A string of printable characters uniquely identifying for all - time the specified message. - - @raise ValueError: if C{index} is greater than the index of any message - in the mailbox. - """ - - def deleteMessage(index): - """Delete a particular message. - - This must not change the number of messages in this mailbox. Further - requests for the size of deleted messages should return 0. Further - requests for the message itself may raise an exception. - - @type index: C{int} - @param index: The number of the message to delete. - """ - - def undeleteMessages(): - """ - Undelete any messages which have been marked for deletion since the - most recent L{sync} call. - - Any message which can be undeleted should be returned to its - original position in the message sequence and retain its original - UID. - """ - - def sync(): - """Perform checkpointing. - - This method will be called to indicate the mailbox should attempt to - clean up any remaining deleted messages. - """ - - - -class Mailbox: - implements(IMailbox) - - def listMessages(self, i=None): - return [] - def getMessage(self, i): - raise ValueError - def getUidl(self, i): - raise ValueError - def deleteMessage(self, i): - raise ValueError - def undeleteMessages(self): - pass - def sync(self): - pass - - -NONE, SHORT, FIRST_LONG, LONG = range(4) - -NEXT = {} -NEXT[NONE] = NONE -NEXT[SHORT] = NONE -NEXT[FIRST_LONG] = LONG -NEXT[LONG] = NONE - -class POP3Client(basic.LineOnlyReceiver): - - mode = SHORT - command = 'WELCOME' - import re - welcomeRe = re.compile('<(.*)>') - - def __init__(self): - import warnings - warnings.warn("twisted.mail.pop3.POP3Client is deprecated, " - "please use twisted.mail.pop3.AdvancedPOP3Client " - "instead.", DeprecationWarning, - stacklevel=3) - - def sendShort(self, command, params=None): - if params is not None: - self.sendLine('%s %s' % (command, params)) - else: - self.sendLine(command) - self.command = command - self.mode = SHORT - - def sendLong(self, command, params): - if params: - self.sendLine('%s %s' % (command, params)) - else: - self.sendLine(command) - self.command = command - self.mode = FIRST_LONG - - def handle_default(self, line): - if line[:-4] == '-ERR': - self.mode = NONE - - def handle_WELCOME(self, line): - code, data = line.split(' ', 1) - if code != '+OK': - self.transport.loseConnection() - else: - m = self.welcomeRe.match(line) - if m: - self.welcomeCode = m.group(1) - - def _dispatch(self, command, default, *args): - try: - method = getattr(self, 'handle_'+command, default) - if method is not None: - method(*args) - except: - log.err() - - def lineReceived(self, line): - if self.mode == SHORT or self.mode == FIRST_LONG: - self.mode = NEXT[self.mode] - self._dispatch(self.command, self.handle_default, line) - elif self.mode == LONG: - if line == '.': - self.mode = NEXT[self.mode] - self._dispatch(self.command+'_end', None) - return - if line[:1] == '.': - line = line[1:] - self._dispatch(self.command+"_continue", None, line) - - def apopAuthenticate(self, user, password, magic): - digest = md5.new(magic + password).hexdigest() - self.apop(user, digest) - - def apop(self, user, digest): - self.sendLong('APOP', ' '.join((user, digest))) - def retr(self, i): - self.sendLong('RETR', i) - def dele(self, i): - self.sendShort('DELE', i) - def list(self, i=''): - self.sendLong('LIST', i) - def uidl(self, i=''): - self.sendLong('UIDL', i) - def user(self, name): - self.sendShort('USER', name) - def pass_(self, pass_): - self.sendShort('PASS', pass_) - def quit(self): - self.sendShort('QUIT') - -from twisted.mail.pop3client import POP3Client as AdvancedPOP3Client -from twisted.mail.pop3client import POP3ClientError -from twisted.mail.pop3client import InsecureAuthenticationDisallowed -from twisted.mail.pop3client import ServerErrorResponse -from twisted.mail.pop3client import LineTooLong - -__all__ = [ - # Interfaces - 'IMailbox', 'IServerFactory', - - # Exceptions - 'POP3Error', 'POP3ClientError', 'InsecureAuthenticationDisallowed', - 'ServerErrorResponse', 'LineTooLong', - - # Protocol classes - 'POP3', 'POP3Client', 'AdvancedPOP3Client', - - # Misc - 'APOPCredentials', 'Mailbox'] diff --git a/tools/buildbot/pylibs/twisted/mail/pop3client.py b/tools/buildbot/pylibs/twisted/mail/pop3client.py deleted file mode 100644 index 5b7986c..0000000 --- a/tools/buildbot/pylibs/twisted/mail/pop3client.py +++ /dev/null @@ -1,704 +0,0 @@ -# -*- test-case-name: twisted.mail.test.test_pop3client -*- -# Copyright (c) 2001-2004 Divmod Inc. -# See LICENSE for details. - -""" -POP3 client protocol implementation - -Don't use this module directly. Use twisted.mail.pop3 instead. - -@author: U{Jp Calderone<mailto:exarkun@twistedmatrix.com>} -""" - -import re, md5 - -from twisted.python import log -from twisted.internet import defer -from twisted.protocols import basic -from twisted.protocols import policies -from twisted.internet import error -from twisted.internet import interfaces - -OK = '+OK' -ERR = '-ERR' - -class POP3ClientError(Exception): - """Base class for all exceptions raised by POP3Client. - """ - -class InsecureAuthenticationDisallowed(POP3ClientError): - """Secure authentication was required but no mechanism could be found. - """ - -class TLSError(POP3ClientError): - """ - Secure authentication was required but either the transport does - not support TLS or no TLS context factory was supplied. - """ - -class TLSNotSupportedError(POP3ClientError): - """ - Secure authentication was required but the server does not support - TLS. - """ - -class ServerErrorResponse(POP3ClientError): - """The server returned an error response to a request. - """ - def __init__(self, reason, consumer=None): - POP3ClientError.__init__(self, reason) - self.consumer = consumer - -class LineTooLong(POP3ClientError): - """The server sent an extremely long line. - """ - -class _ListSetter: - # Internal helper. POP3 responses sometimes occur in the - # form of a list of lines containing two pieces of data, - # a message index and a value of some sort. When a message - # is deleted, it is omitted from these responses. The - # setitem method of this class is meant to be called with - # these two values. In the cases where indexes are skipped, - # it takes care of padding out the missing values with None. - def __init__(self, L): - self.L = L - def setitem(self, (item, value)): - diff = item - len(self.L) + 1 - if diff > 0: - self.L.extend([None] * diff) - self.L[item] = value - - -def _statXform(line): - # Parse a STAT response - numMsgs, totalSize = line.split(None, 1) - return int(numMsgs), int(totalSize) - - -def _listXform(line): - # Parse a LIST response - index, size = line.split(None, 1) - return int(index) - 1, int(size) - - -def _uidXform(line): - # Parse a UIDL response - index, uid = line.split(None, 1) - return int(index) - 1, uid - -def _codeStatusSplit(line): - # Parse an +OK or -ERR response - parts = line.split(' ', 1) - if len(parts) == 1: - return parts[0], '' - return parts - -def _dotUnquoter(line): - """ - C{'.'} characters which begin a line of a message are doubled to avoid - confusing with the terminating C{'.\\r\\n'} sequence. This function - unquotes them. - """ - if line.startswith('..'): - return line[1:] - return line - -class POP3Client(basic.LineOnlyReceiver, policies.TimeoutMixin): - """POP3 client protocol implementation class - - Instances of this class provide a convenient, efficient API for - retrieving and deleting messages from a POP3 server. - - @type startedTLS: C{bool} - @ivar startedTLS: Whether TLS has been negotiated successfully. - - - @type allowInsecureLogin: C{bool} - @ivar allowInsecureLogin: Indicate whether login() should be - allowed if the server offers no authentication challenge and if - our transport does not offer any protection via encryption. - - @type serverChallenge: C{str} or C{None} - @ivar serverChallenge: Challenge received from the server - - @type timeout: C{int} - @ivar timeout: Number of seconds to wait before timing out a - connection. If the number is <= 0, no timeout checking will be - performed. - """ - - startedTLS = False - allowInsecureLogin = False - timeout = 0 - serverChallenge = None - - # Capabilities are not allowed to change during the session - # (except when TLS is negotiated), so cache the first response and - # use that for all later lookups - _capCache = None - - # Regular expression to search for in the challenge string in the server - # greeting line. - _challengeMagicRe = re.compile('(<[^>]+>)') - - # List of pending calls. - # We are a pipelining API but don't actually - # support pipelining on the network yet. - _blockedQueue = None - - # The Deferred to which the very next result will go. - _waiting = None - - # Whether we dropped the connection because of a timeout - _timedOut = False - - # If the server sends an initial -ERR, this is the message it sent - # with it. - _greetingError = None - - def _blocked(self, f, *a): - # Internal helper. If commands are being blocked, append - # the given command and arguments to a list and return a Deferred - # that will be chained with the return value of the function - # when it eventually runs. Otherwise, set up for commands to be - - # blocked and return None. - if self._blockedQueue is not None: - d = defer.Deferred() - self._blockedQueue.append((d, f, a)) - return d - self._blockedQueue = [] - return None - - def _unblock(self): - # Internal helper. Indicate that a function has completed. - # If there are blocked commands, run the next one. If there - # are not, set up for the next command to not be blocked. - if self._blockedQueue == []: - self._blockedQueue = None - elif self._blockedQueue is not None: - _blockedQueue = self._blockedQueue - self._blockedQueue = None - - d, f, a = _blockedQueue.pop(0) - d2 = f(*a) - d2.chainDeferred(d) - # f is a function which uses _blocked (otherwise it wouldn't - # have gotten into the blocked queue), which means it will have - # re-set _blockedQueue to an empty list, so we can put the rest - # of the blocked queue back into it now. - self._blockedQueue.extend(_blockedQueue) - - - def sendShort(self, cmd, args): - # Internal helper. Send a command to which a short response - # is expected. Return a Deferred that fires when the response - # is received. Block all further commands from being sent until - # the response is received. Transition the state to SHORT. - d = self._blocked(self.sendShort, cmd, args) - if d is not None: - return d - - if args: - self.sendLine(cmd + ' ' + args) - else: - self.sendLine(cmd) - self.state = 'SHORT' - self._waiting = defer.Deferred() - return self._waiting - - def sendLong(self, cmd, args, consumer, xform): - # Internal helper. Send a command to which a multiline - # response is expected. Return a Deferred that fires when - # the entire response is received. Block all further commands - # from being sent until the entire response is received. - # Transition the state to LONG_INITIAL. - d = self._blocked(self.sendLong, cmd, args, consumer, xform) - if d is not None: - return d - - if args: - self.sendLine(cmd + ' ' + args) - else: - self.sendLine(cmd) - self.state = 'LONG_INITIAL' - self._xform = xform - self._consumer = consumer - self._waiting = defer.Deferred() - return self._waiting - - # Twisted protocol callback - def connectionMade(self): - if self.timeout > 0: - self.setTimeout(self.timeout) - - self.state = 'WELCOME' - self._blockedQueue = [] - - def timeoutConnection(self): - self._timedOut = True - self.transport.loseConnection() - - def connectionLost(self, reason): - if self.timeout > 0: - self.setTimeout(None) - - if self._timedOut: - reason = error.TimeoutError() - elif self._greetingError: - reason = ServerErrorResponse(self._greetingError) - - d = [] - if self._waiting is not None: - d.append(self._waiting) - self._waiting = None - if self._blockedQueue is not None: - d.extend([deferred for (deferred, f, a) in self._blockedQueue]) - self._blockedQueue = None - for w in d: - w.errback(reason) - - def lineReceived(self, line): - if self.timeout > 0: - self.resetTimeout() - - state = self.state - self.state = None - state = getattr(self, 'state_' + state)(line) or state - if self.state is None: - self.state = state - - def lineLengthExceeded(self, buffer): - # XXX - We need to be smarter about this - if self._waiting is not None: - waiting, self._waiting = self._waiting, None - waiting.errback(LineTooLong()) - self.transport.loseConnection() - - # POP3 Client state logic - don't touch this. - def state_WELCOME(self, line): - # WELCOME is the first state. The server sends one line of text - # greeting us, possibly with an APOP challenge. Transition the - # state to WAITING. - code, status = _codeStatusSplit(line) - if code != OK: - self._greetingError = status - self.transport.loseConnection() - else: - m = self._challengeMagicRe.search(status) - - if m is not None: - self.serverChallenge = m.group(1) - - self.serverGreeting(status) - - self._unblock() - return 'WAITING' - - def state_WAITING(self, line): - # The server isn't supposed to send us anything in this state. - log.msg("Illegal line from server: " + repr(line)) - - def state_SHORT(self, line): - # This is the state we are in when waiting for a single - # line response. Parse it and fire the appropriate callback - # or errback. Transition the state back to WAITING. - deferred, self._waiting = self._waiting, None - self._unblock() - code, status = _codeStatusSplit(line) - if code == OK: - deferred.callback(status) - else: - deferred.errback(ServerErrorResponse(status)) - return 'WAITING' - - def state_LONG_INITIAL(self, line): - # This is the state we are in when waiting for the first - # line of a long response. Parse it and transition the - # state to LONG if it is an okay response; if it is an - # error response, fire an errback, clean up the things - # waiting for a long response, and transition the state - # to WAITING. - code, status = _codeStatusSplit(line) - if code == OK: - return 'LONG' - consumer = self._consumer - deferred = self._waiting - self._consumer = self._waiting = self._xform = None - self._unblock() - deferred.errback(ServerErrorResponse(status, consumer)) - return 'WAITING' - - def state_LONG(self, line): - # This is the state for each line of a long response. - # If it is the last line, finish things, fire the - # Deferred, and transition the state to WAITING. - # Otherwise, pass the line to the consumer. - if line == '.': - consumer = self._consumer - deferred = self._waiting - self._consumer = self._waiting = self._xform = None - self._unblock() - deferred.callback(consumer) - return 'WAITING' - else: - if self._xform is not None: - self._consumer(self._xform(line)) - else: - self._consumer(line) - return 'LONG' - - - # Callbacks - override these - def serverGreeting(self, greeting): - """Called when the server has sent us a greeting. - - @type greeting: C{str} or C{None} - @param greeting: The status message sent with the server - greeting. For servers implementing APOP authentication, this - will be a challenge string. . - """ - - - # External API - call these (most of 'em anyway) - def startTLS(self, contextFactory=None): - """ - Initiates a 'STLS' request and negotiates the TLS / SSL - Handshake. - - @type contextFactory: C{ssl.ClientContextFactory} @param - contextFactory: The context factory with which to negotiate - TLS. If C{None}, try to create a new one. - - @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. - """ - tls = interfaces.ITLSTransport(self.transport, None) - if tls is None: - return defer.fail(TLSError( - "POP3Client transport does not implement " - "interfaces.ITLSTransport")) - - if contextFactory is None: - contextFactory = self._getContextFactory() - - if contextFactory is None: - return defer.fail(TLSError( - "POP3Client requires a TLS context to " - "initiate the STLS handshake")) - - d = self.capabilities() - d.addCallback(self._startTLS, contextFactory, tls) - return d - - - def _startTLS(self, caps, contextFactory, tls): - assert not self.startedTLS, "Client and Server are currently communicating via TLS" - - if 'STLS' not in caps: - return defer.fail(TLSNotSupportedError( - "Server does not support secure communication " - "via TLS / SSL")) - - d = self.sendShort('STLS', None) - d.addCallback(self._startedTLS, contextFactory, tls) - d.addCallback(lambda _: self.capabilities()) - return d - - - def _startedTLS(self, result, context, tls): - self.transport = tls - self.transport.startTLS(context) - self._capCache = None - self.startedTLS = True - return result - - - def _getContextFactory(self): - try: - from twisted.internet import ssl - except ImportError: - return None - else: - context = ssl.ClientContextFactory() - context.method = ssl.SSL.TLSv1_METHOD - return context - - - def login(self, username, password): - """Log into the server. - - If APOP is available it will be used. Otherwise, if TLS is - available an 'STLS' session will be started and plaintext - login will proceed. Otherwise, if the instance attribute - allowInsecureLogin is set to True, insecure plaintext login - will proceed. Otherwise, InsecureAuthenticationDisallowed - will be raised (asynchronously). - - @param username: The username with which to log in. - @param password: The password with which to log in. - - @rtype: C{Deferred} - @return: A deferred which fires when login has - completed. - """ - d = self.capabilities() - d.addCallback(self._login, username, password) - return d - - - def _login(self, caps, username, password): - if self.serverChallenge is not None: - return self._apop(username, password, self.serverChallenge) - - tryTLS = 'STLS' in caps - - #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.addCallback(self._loginTLS, username, password) - return d - - elif self.startedTLS or not nontlsTransport or self.allowInsecureLogin: - return self._plaintext(username, password) - else: - return defer.fail(InsecureAuthenticationDisallowed()) - - - def _loginTLS(self, res, username, password): - return self._plaintext(username, password) - - def _plaintext(self, username, password): - # Internal helper. Send a username/password pair, returning a Deferred - # that fires when both have succeeded or fails when the server rejects - # either. - return self.user(username).addCallback(lambda r: self.password(password)) - - def _apop(self, username, password, challenge): - # Internal helper. Computes and sends an APOP response. Returns - # a Deferred that fires when the server responds to the response. - digest = md5.new(challenge + password).hexdigest() - return self.apop(username, digest) - - def apop(self, username, digest): - """Perform APOP login. - - This should be used in special circumstances only, when it is - known that the server supports APOP authentication, and APOP - authentication is absolutely required. For the common case, - use L{login} instead. - - @param username: The username with which to log in. - @param digest: The challenge response to authenticate with. - """ - return self.sendShort('APOP', username + ' ' + digest) - - def user(self, username): - """Send the user command. - - This performs the first half of plaintext login. Unless this - is absolutely required, use the L{login} method instead. - - @param username: The username with which to log in. - """ - return self.sendShort('USER', username) - - def password(self, password): - """Send the password command. - - This performs the second half of plaintext login. Unless this - is absolutely required, use the L{login} method instead. - - @param password: The plaintext password with which to authenticate. - """ - return self.sendShort('PASS', password) - - def delete(self, index): - """Delete a message from the server. - - @type index: C{int} - @param index: The index of the message to delete. - This is 0-based. - - @rtype: C{Deferred} - @return: A deferred which fires when the delete command - is successful, or fails if the server returns an error. - """ - return self.sendShort('DELE', str(index + 1)) - - def _consumeOrSetItem(self, cmd, args, consumer, xform): - # Internal helper. Send a long command. If no consumer is - # provided, create a consumer that puts results into a list - # and return a Deferred that fires with that list when it - # is complete. - if consumer is None: - L = [] - consumer = _ListSetter(L).setitem - return self.sendLong(cmd, args, consumer, xform).addCallback(lambda r: L) - return self.sendLong(cmd, args, consumer, xform) - - def _consumeOrAppend(self, cmd, args, consumer, xform): - # Internal helper. Send a long command. If no consumer is - # provided, create a consumer that appends results to a list - # and return a Deferred that fires with that list when it is - # complete. - if consumer is None: - L = [] - consumer = L.append - return self.sendLong(cmd, args, consumer, xform).addCallback(lambda r: L) - return self.sendLong(cmd, args, consumer, xform) - - def capabilities(self, useCache=True): - """Retrieve the capabilities supported by this server. - - Not all servers support this command. If the server does not - support this, it is treated as though it returned a successful - response listing no capabilities. At some future time, this may be - changed to instead seek out information about a server's - capabilities in some other fashion (only if it proves useful to do - so, and only if there are servers still in use which do not support - CAPA but which do support POP3 extensions that are useful). - - @type useCache: C{bool} - @param useCache: If set, and if capabilities have been - retrieved previously, just return the previously retrieved - results. - - @return: A Deferred which fires with a C{dict} mapping C{str} - to C{None} or C{list}s of C{str}. For example:: - - C: CAPA - S: +OK Capability list follows - S: TOP - S: USER - S: SASL CRAM-MD5 KERBEROS_V4 - S: RESP-CODES - S: LOGIN-DELAY 900 - S: PIPELINING - S: EXPIRE 60 - S: UIDL - S: IMPLEMENTATION Shlemazle-Plotz-v302 - S: . - - will be lead to a result of:: - - | {'TOP': None, - | 'USER': None, - | 'SASL': ['CRAM-MD5', 'KERBEROS_V4'], - | 'RESP-CODES': None, - | 'LOGIN-DELAY': ['900'], - | 'PIPELINING': None, - | 'EXPIRE': ['60'], - | 'UIDL': None, - | 'IMPLEMENTATION': ['Shlemazle-Plotz-v302']} - """ - if useCache and self._capCache is not None: - return defer.succeed(self._capCache) - - cache = {} - def consume(line): - tmp = line.split() - if len(tmp) == 1: - cache[tmp[0]] = None - elif len(tmp) > 1: - cache[tmp[0]] = tmp[1:] - - def capaNotSupported(err): - err.trap(ServerErrorResponse) - return None - - def gotCapabilities(result): - self._capCache = cache - return cache - - d = self._consumeOrAppend('CAPA', None, consume, None) - d.addErrback(capaNotSupported).addCallback(gotCapabilities) - return d - - - def noop(self): - """Do nothing, with the help of the server. - - No operation is performed. The returned Deferred fires when - the server responds. - """ - return self.sendShort("NOOP", None) - - - def reset(self): - """Remove the deleted flag from any messages which have it. - - The returned Deferred fires when the server responds. - """ - return self.sendShort("RSET", None) - - - def retrieve(self, index, consumer=None, lines=None): - """Retrieve a message from the server. - - If L{consumer} is not None, it will be called with - each line of the message as it is received. Otherwise, - the returned Deferred will be fired with a list of all - the lines when the message has been completely received. - """ - idx = str(index + 1) - if lines is None: - return self._consumeOrAppend('RETR', idx, consumer, _dotUnquoter) - - return self._consumeOrAppend('TOP', '%s %d' % (idx, lines), consumer, _dotUnquoter) - - - def stat(self): - """Get information about the size of this mailbox. - - The returned Deferred will be fired with a tuple containing - the number or messages in the mailbox and the size (in bytes) - of the mailbox. - """ - return self.sendShort('STAT', None).addCallback(_statXform) - - - def listSize(self, consumer=None): - """Retrieve a list of the size of all messages on the server. - - If L{consumer} is not None, it will be called with two-tuples - of message index number and message size as they are received. - Otherwise, a Deferred which will fire with a list of B{only} - message sizes will be returned. For messages which have been - deleted, None will be used in place of the message size. - """ - return self._consumeOrSetItem('LIST', None, consumer, _listXform) - - - def listUID(self, consumer=None): - """Retrieve a list of the UIDs of all messages on the server. - - If L{consumer} is not None, it will be called with two-tuples - of message index number and message UID as they are received. - Otherwise, a Deferred which will fire with of list of B{only} - message UIDs will be returned. For messages which have been - deleted, None will be used in place of the message UID. - """ - return self._consumeOrSetItem('UIDL', None, consumer, _uidXform) - - - def quit(self): - """Disconnect from the server. - """ - return self.sendShort('QUIT', None) - -__all__ = [ - # Exceptions - 'InsecureAuthenticationDisallowed', 'LineTooLong', 'POP3ClientError', - 'ServerErrorResponse', 'TLSError', 'TLSNotSupportedError', - - # Protocol classes - 'POP3Client'] diff --git a/tools/buildbot/pylibs/twisted/mail/protocols.py b/tools/buildbot/pylibs/twisted/mail/protocols.py deleted file mode 100644 index dc449af..0000000 --- a/tools/buildbot/pylibs/twisted/mail/protocols.py +++ /dev/null @@ -1,225 +0,0 @@ -# -*- test-case-name: twisted.mail.test.test_mail -*- -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. -# See LICENSE for details. - - -"""Protocol support for twisted.mail.""" - -# twisted imports -from twisted.mail import pop3 -from twisted.mail import smtp -from twisted.internet import protocol -from twisted.internet import defer -from twisted.copyright import longversion -from twisted.python import log - -from twisted import cred -import twisted.cred.error -import twisted.cred.credentials - -from twisted.mail import relay - -from zope.interface import implements - - -class DomainDeliveryBase: - """A server that uses twisted.mail service's domains.""" - - implements(smtp.IMessageDelivery) - - service = None - protocolName = None - - def __init__(self, service, user, host=smtp.DNSNAME): - self.service = service - self.user = user - self.host = host - - def receivedHeader(self, helo, origin, recipients): - authStr = heloStr = "" - if self.user: - authStr = " auth=%s" % (self.user.encode('xtext'),) - if helo[0]: - heloStr = " helo=%s" % (helo[0],) - from_ = "from %s ([%s]%s%s)" % (helo[0], helo[1], heloStr, authStr) - by = "by %s with %s (%s)" % ( - self.host, self.protocolName, longversion - ) - for_ = "for <%s>; %s" % (' '.join(map(str, recipients)), smtp.rfc822date()) - return "Received: %s\n\t%s\n\t%s" % (from_, by, for_) - - def validateTo(self, user): - # XXX - Yick. This needs cleaning up. - if self.user and self.service.queue: - d = self.service.domains.get(user.dest.domain, None) - if d is None: - d = relay.DomainQueuer(self.service, True) - else: - d = self.service.domains[user.dest.domain] - return defer.maybeDeferred(d.exists, user) - - def validateFrom(self, helo, origin): - if not helo: - raise smtp.SMTPBadSender(origin, 503, "Who are you? Say HELO first.") - if origin.local != '' and origin.domain == '': - raise smtp.SMTPBadSender(origin, 501, "Sender address must contain domain.") - return origin - - def startMessage(self, users): - ret = [] - for user in users: - ret.append(self.service.domains[user.dest.domain].startMessage(user)) - return ret - - -class SMTPDomainDelivery(DomainDeliveryBase): - protocolName = 'smtp' - -class ESMTPDomainDelivery(DomainDeliveryBase): - protocolName = 'esmtp' - -class DomainSMTP(SMTPDomainDelivery, smtp.SMTP): - service = user = None - - def __init__(self, *args, **kw): - import warnings - warnings.warn( - "DomainSMTP is deprecated. Use IMessageDelivery objects instead.", - DeprecationWarning, stacklevel=2, - ) - smtp.SMTP.__init__(self, *args, **kw) - if self.delivery is None: - self.delivery = self - -class DomainESMTP(ESMTPDomainDelivery, smtp.ESMTP): - service = user = None - - def __init__(self, *args, **kw): - import warnings - warnings.warn( - "DomainESMTP is deprecated. Use IMessageDelivery objects instead.", - DeprecationWarning, stacklevel=2, - ) - smtp.ESMTP.__init__(self, *args, **kw) - if self.delivery is None: - self.delivery = self - -class SMTPFactory(smtp.SMTPFactory): - """A protocol factory for SMTP.""" - - protocol = smtp.SMTP - portal = None - - def __init__(self, service, portal = None): - smtp.SMTPFactory.__init__(self) - self.service = service - self.portal = portal - - def buildProtocol(self, addr): - log.msg('Connection from %s' % (addr,)) - p = smtp.SMTPFactory.buildProtocol(self, addr) - p.service = self.service - p.portal = self.portal - return p - -class ESMTPFactory(SMTPFactory): - protocol = smtp.ESMTP - context = None - - def __init__(self, *args): - SMTPFactory.__init__(self, *args) - self.challengers = { - 'CRAM-MD5': cred.credentials.CramMD5Credentials - } - - def buildProtocol(self, addr): - p = SMTPFactory.buildProtocol(self, addr) - p.challengers = self.challengers - p.ctx = self.context - return p - -class VirtualPOP3(pop3.POP3): - """Virtual hosting POP3.""" - - service = None - - domainSpecifier = '@' # Gaagh! I hate POP3. No standardized way - # to indicate user@host. '@' doesn't work - # with NS, e.g. - - def authenticateUserAPOP(self, user, digest): - # Override the default lookup scheme to allow virtual domains - user, domain = self.lookupDomain(user) - try: - portal = self.service.lookupPortal(domain) - except KeyError: - return defer.fail(cred.error.UnauthorizedLogin()) - else: - return portal.login( - pop3.APOPCredentials(self.magic, user, digest), - None, - pop3.IMailbox - ) - - def authenticateUserPASS(self, user, password): - user, domain = self.lookupDomain(user) - try: - portal = self.service.lookupPortal(domain) - except KeyError: - return defer.fail(cred.error.UnauthorizedLogin()) - else: - return portal.login( - cred.credentials.UsernamePassword(user, password), - None, - pop3.IMailbox - ) - - def lookupDomain(self, user): - try: - user, domain = user.split(self.domainSpecifier, 1) - except ValueError: - domain = '' - if domain not in self.service.domains: - raise pop3.POP3Error("no such domain %s" % domain) - return user, domain - - -class POP3Factory(protocol.ServerFactory): - """POP3 protocol factory.""" - - protocol = VirtualPOP3 - service = None - - def __init__(self, service): - self.service = service - - def buildProtocol(self, addr): - p = protocol.ServerFactory.buildProtocol(self, addr) - p.service = self.service - return p - -# -# It is useful to know, perhaps, that the required file for this to work can -# be created thusly: -# -# openssl req -x509 -newkey rsa:2048 -keyout file.key -out file.crt \ -# -days 365 -nodes -# -# And then cat file.key and file.crt together. The number of days and bits -# can be changed, of course. -# -class SSLContextFactory: - """An SSL Context Factory - - This loads a certificate and private key from a specified file. - """ - def __init__(self, filename): - self.filename = filename - - def getContext(self): - """Create an SSL context.""" - from OpenSSL import SSL - ctx = SSL.Context(SSL.SSLv23_METHOD) - ctx.use_certificate_file(self.filename) - ctx.use_privatekey_file(self.filename) - return ctx diff --git a/tools/buildbot/pylibs/twisted/mail/relay.py b/tools/buildbot/pylibs/twisted/mail/relay.py deleted file mode 100644 index 5fbf48f..0000000 --- a/tools/buildbot/pylibs/twisted/mail/relay.py +++ /dev/null @@ -1,114 +0,0 @@ -# -*- test-case-name: twisted.mail.test.test_mail -*- -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. -# See LICENSE for details. - - -"""Support for relaying mail for twisted.mail""" - -from twisted.mail import smtp -from twisted.python import log -from twisted.internet.address import UNIXAddress - -import os - -try: - import cPickle as pickle -except ImportError: - import pickle - -class DomainQueuer: - """An SMTP domain which add messages to a queue intended for relaying.""" - - def __init__(self, service, authenticated=False): - self.service = service - self.authed = authenticated - - def exists(self, user): - """Check whether we will relay - - Call overridable willRelay method - """ - if self.willRelay(user.dest, user.protocol): - # The most cursor form of verification of the addresses - orig = filter(None, str(user.orig).split('@', 1)) - dest = filter(None, str(user.dest).split('@', 1)) - if len(orig) == 2 and len(dest) == 2: - return lambda: self.startMessage(user) - raise smtp.SMTPBadRcpt(user) - - def willRelay(self, address, protocol): - """Check whether we agree to relay - - The default is to relay for all connections over UNIX - sockets and all connections from localhost. - """ - peer = protocol.transport.getPeer() - return self.authed or isinstance(peer, UNIXAddress) or peer.host == '127.0.0.1' - - def startMessage(self, user): - """Add envelope to queue and returns ISMTPMessage.""" - queue = self.service.queue - envelopeFile, smtpMessage = queue.createNewMessage() - try: - log.msg('Queueing mail %r -> %r' % (str(user.orig), str(user.dest))) - pickle.dump([str(user.orig), str(user.dest)], envelopeFile) - finally: - envelopeFile.close() - return smtpMessage - -class RelayerMixin: - - # XXX - This is -totally- bogus - # It opens about a -hundred- -billion- files - # and -leaves- them open! - - def loadMessages(self, messagePaths): - self.messages = [] - self.names = [] - for message in messagePaths: - fp = open(message+'-H') - try: - messageContents = pickle.load(fp) - finally: - fp.close() - fp = open(message+'-D') - messageContents.append(fp) - self.messages.append(messageContents) - self.names.append(message) - - def getMailFrom(self): - if not self.messages: - return None - return self.messages[0][0] - - def getMailTo(self): - if not self.messages: - return None - return [self.messages[0][1]] - - def getMailData(self): - if not self.messages: - return None - return self.messages[0][2] - - def sentMail(self, code, resp, numOk, addresses, log): - """Since we only use one recipient per envelope, this - will be called with 0 or 1 addresses. We probably want - to do something with the error message if we failed. - """ - if code in smtp.SUCCESS: - # At least one, i.e. all, recipients successfully delivered - os.remove(self.names[0]+'-D') - os.remove(self.names[0]+'-H') - del self.messages[0] - del self.names[0] - -class SMTPRelayer(RelayerMixin, smtp.SMTPClient): - def __init__(self, messagePaths, *args, **kw): - smtp.SMTPClient.__init__(self, *args, **kw) - self.loadMessages(messagePaths) - -class ESMTPRelayer(RelayerMixin, smtp.ESMTPClient): - def __init__(self, messagePaths, *args, **kw): - smtp.ESMTPClient.__init__(self, *args, **kw) - self.loadMessages(messagePaths) diff --git a/tools/buildbot/pylibs/twisted/mail/relaymanager.py b/tools/buildbot/pylibs/twisted/mail/relaymanager.py deleted file mode 100644 index 8cf4edac..0000000 --- a/tools/buildbot/pylibs/twisted/mail/relaymanager.py +++ /dev/null @@ -1,631 +0,0 @@ -# -*- test-case-name: twisted.mail.test.test_mail -*- -# Copyright (c) 2001-2008 Twisted Matrix Laboratories. -# See LICENSE for details. - -""" -Infrastructure for relaying mail through smart host - -Today, internet e-mail has stopped being Peer-to-peer for many problems, -spam (unsolicited bulk mail) among them. Instead, most nodes on the -internet send all e-mail to a single computer, usually the ISP's though -sometimes other schemes, such as SMTP-after-POP, are used. This computer -is supposedly permanently up and traceable, and will do the work of -figuring out MXs and connecting to them. This kind of configuration -is usually termed "smart host", since the host we are connecting to -is "smart" (and will find MXs and connect to them) rather then just -accepting mail for a small set of domains. - -The classes here are meant to facilitate support for such a configuration -for the twisted.mail SMTP server -""" - -import rfc822 -import os -import time - -try: - import cPickle as pickle -except ImportError: - import pickle - -from twisted.python import log -from twisted.python.failure import Failure -from twisted.python.compat import set -from twisted.mail import relay -from twisted.mail import bounce -from twisted.internet import protocol -from twisted.internet.defer import Deferred, DeferredList -from twisted.internet.error import DNSLookupError -from twisted.mail import smtp -from twisted.application import internet - -class ManagedRelayerMixin: - """SMTP Relayer which notifies a manager - - Notify the manager about successful mail, failed mail - and broken connections - """ - - def __init__(self, manager): - self.manager = manager - - def sentMail(self, code, resp, numOk, addresses, log): - """called when e-mail has been sent - - we will always get 0 or 1 addresses. - """ - message = self.names[0] - if code in smtp.SUCCESS: - self.manager.notifySuccess(self.factory, message) - else: - self.manager.notifyFailure(self.factory, message) - del self.messages[0] - del self.names[0] - - def connectionLost(self, reason): - """called when connection is broken - - notify manager we will try to send no more e-mail - """ - self.manager.notifyDone(self.factory) - -class SMTPManagedRelayer(ManagedRelayerMixin, relay.SMTPRelayer): - def __init__(self, messages, manager, *args, **kw): - """ - @type messages: C{list} of C{str} - @param messages: Filenames of messages to relay - - manager should support .notifySuccess, .notifyFailure - and .notifyDone - """ - ManagedRelayerMixin.__init__(self, manager) - relay.SMTPRelayer.__init__(self, messages, *args, **kw) - -class ESMTPManagedRelayer(ManagedRelayerMixin, relay.ESMTPRelayer): - def __init__(self, messages, manager, *args, **kw): - """ - @type messages: C{list} of C{str} - @param messages: Filenames of messages to relay - - manager should support .notifySuccess, .notifyFailure - and .notifyDone - """ - ManagedRelayerMixin.__init__(self, manager) - relay.ESMTPRelayer.__init__(self, messages, *args, **kw) - -class SMTPManagedRelayerFactory(protocol.ClientFactory): - protocol = SMTPManagedRelayer - - def __init__(self, messages, manager, *args, **kw): - self.messages = messages - self.manager = manager - self.pArgs = args - self.pKwArgs = kw - - def buildProtocol(self, addr): - protocol = self.protocol(self.messages, self.manager, *self.pArgs, - **self.pKwArgs) - protocol.factory = self - return protocol - - def clientConnectionFailed(self, connector, reason): - """called when connection could not be made - - our manager should be notified that this happened, - it might prefer some other host in that case""" - self.manager.notifyNoConnection(self) - self.manager.notifyDone(self) - -class ESMTPManagedRelayerFactory(SMTPManagedRelayerFactory): - protocol = ESMTPManagedRelayer - - def __init__(self, messages, manager, secret, contextFactory, *args, **kw): - self.secret = secret - self.contextFactory = contextFactory - SMTPManagedRelayerFactory.__init__(self, messages, manager, *args, **kw) - - def buildProtocol(self, addr): - s = self.secret and self.secret(addr) - protocol = self.protocol(self.messages, self.manager, s, - self.contextFactory, *self.pArgs, **self.pKwArgs) - protocol.factory = self - return protocol - -class Queue: - """A queue of ougoing emails.""" - - noisy = True - - def __init__(self, directory): - self.directory = directory - self._init() - - def _init(self): - self.n = 0 - self.waiting = {} - self.relayed = {} - self.readDirectory() - - def __getstate__(self): - """(internal) delete volatile state""" - return {'directory' : self.directory} - - def __setstate__(self, state): - """(internal) restore volatile state""" - self.__dict__.update(state) - self._init() - - def readDirectory(self): - """Read the messages directory. - - look for new messages. - """ - for message in os.listdir(self.directory): - # Skip non data files - if message[-2:]!='-D': - continue - self.addMessage(message[:-2]) - - def getWaiting(self): - return self.waiting.keys() - - def hasWaiting(self): - return len(self.waiting) > 0 - - def getRelayed(self): - return self.relayed.keys() - - def setRelaying(self, message): - del self.waiting[message] - self.relayed[message] = 1 - - def setWaiting(self, message): - del self.relayed[message] - self.waiting[message] = 1 - - def addMessage(self, message): - if message not in self.relayed: - self.waiting[message] = 1 - if self.noisy: - log.msg('Set ' + message + ' waiting') - - def done(self, message): - """Remove message to from queue.""" - message = os.path.basename(message) - os.remove(self.getPath(message) + '-D') - os.remove(self.getPath(message) + '-H') - del self.relayed[message] - - def getPath(self, message): - """Get the path in the filesystem of a message.""" - return os.path.join(self.directory, message) - - def getEnvelope(self, message): - return pickle.load(self.getEnvelopeFile(message)) - - def getEnvelopeFile(self, message): - return open(os.path.join(self.directory, message+'-H'), 'rb') - - def createNewMessage(self): - """Create a new message in the queue. - - Return a tuple - file-like object for headers, and ISMTPMessage. - """ - fname = "%s_%s_%s_%s" % (os.getpid(), time.time(), self.n, id(self)) - self.n = self.n + 1 - headerFile = open(os.path.join(self.directory, fname+'-H'), 'wb') - tempFilename = os.path.join(self.directory, fname+'-C') - finalFilename = os.path.join(self.directory, fname+'-D') - messageFile = open(tempFilename, 'wb') - - from twisted.mail.mail import FileMessage - return headerFile,FileMessage(messageFile, tempFilename, finalFilename) - - -class _AttemptManager(object): - """ - Manage the state of a single attempt to flush the relay queue. - """ - def __init__(self, manager): - self.manager = manager - self._completionDeferreds = [] - - - def getCompletionDeferred(self): - self._completionDeferreds.append(Deferred()) - return self._completionDeferreds[-1] - - - def _finish(self, relay, message): - self.manager.managed[relay].remove(os.path.basename(message)) - self.manager.queue.done(message) - - - def notifySuccess(self, relay, message): - """a relay sent a message successfully - - Mark it as sent in our lists - """ - if self.manager.queue.noisy: - log.msg("success sending %s, removing from queue" % message) - self._finish(relay, message) - - - def notifyFailure(self, relay, message): - """Relaying the message has failed.""" - if self.manager.queue.noisy: - log.msg("could not relay "+message) - # Moshe - Bounce E-mail here - # Be careful: if it's a bounced bounce, silently - # discard it - message = os.path.basename(message) - fp = self.manager.queue.getEnvelopeFile(message) - from_, to = pickle.load(fp) - fp.close() - from_, to, bounceMessage = bounce.generateBounce(open(self.manager.queue.getPath(message)+'-D'), from_, to) - fp, outgoingMessage = self.manager.queue.createNewMessage() - pickle.dump([from_, to], fp) - fp.close() - for line in bounceMessage.splitlines(): - outgoingMessage.lineReceived(line) - outgoingMessage.eomReceived() - self._finish(relay, self.manager.queue.getPath(message)) - - - def notifyDone(self, relay): - """A relaying SMTP client is disconnected. - - unmark all pending messages under this relay's responsibility - as being relayed, and remove the relay. - """ - for message in self.manager.managed.get(relay, ()): - if self.manager.queue.noisy: - log.msg("Setting " + message + " waiting") - self.manager.queue.setWaiting(message) - try: - del self.manager.managed[relay] - except KeyError: - pass - notifications = self._completionDeferreds - self._completionDeferreds = None - for d in notifications: - d.callback(None) - - - def notifyNoConnection(self, relay): - """Relaying SMTP client couldn't connect. - - Useful because it tells us our upstream server is unavailable. - """ - # Back off a bit - try: - msgs = self.manager.managed[relay] - except KeyError: - log.msg("notifyNoConnection passed unknown relay!") - return - - if self.manager.queue.noisy: - log.msg("Backing off on delivery of " + str(msgs)) - def setWaiting(queue, messages): - map(queue.setWaiting, messages) - from twisted.internet import reactor - reactor.callLater(30, setWaiting, self.manager.queue, msgs) - del self.manager.managed[relay] - - - -class SmartHostSMTPRelayingManager: - """Manage SMTP Relayers - - Manage SMTP relayers, keeping track of the existing connections, - each connection's responsibility in term of messages. Create - more relayers if the need arises. - - Someone should press .checkState periodically - - @ivar fArgs: Additional positional arguments used to instantiate - C{factory}. - - @ivar fKwArgs: Additional keyword arguments used to instantiate - C{factory}. - - @ivar factory: A callable which returns a ClientFactory suitable for - making SMTP connections. - """ - - factory = SMTPManagedRelayerFactory - - PORT = 25 - - mxcalc = None - - def __init__(self, queue, maxConnections=2, maxMessagesPerConnection=10): - """ - @type queue: Any implementor of C{IQueue} - @param queue: The object used to queue messages on their way to - delivery. - - @type maxConnections: C{int} - @param maxConnections: The maximum number of SMTP connections to - allow to be opened at any given time. - - @type maxMessagesPerConnection: C{int} - @param maxMessagesPerConnection: The maximum number of messages a - relayer will be given responsibility for. - - Default values are meant for a small box with 1-5 users. - """ - self.maxConnections = maxConnections - self.maxMessagesPerConnection = maxMessagesPerConnection - self.managed = {} # SMTP clients we're managing - self.queue = queue - self.fArgs = () - self.fKwArgs = {} - - def __getstate__(self): - """(internal) delete volatile state""" - dct = self.__dict__.copy() - del dct['managed'] - return dct - - def __setstate__(self, state): - """(internal) restore volatile state""" - self.__dict__.update(state) - self.managed = {} - - def checkState(self): - """ - Synchronize with the state of the world, and maybe launch a new - relay. - - Call me periodically to check I am still up to date. - - @return: None or a Deferred which fires when all of the SMTP clients - started by this call have disconnected. - """ - self.queue.readDirectory() - if (len(self.managed) >= self.maxConnections): - return - if not self.queue.hasWaiting(): - return - - return self._checkStateMX() - - def _checkStateMX(self): - nextMessages = self.queue.getWaiting() - nextMessages.reverse() - - exchanges = {} - for msg in nextMessages: - from_, to = self.queue.getEnvelope(msg) - name, addr = rfc822.parseaddr(to) - parts = addr.split('@', 1) - if len(parts) != 2: - log.err("Illegal message destination: " + to) - continue - domain = parts[1] - - self.queue.setRelaying(msg) - exchanges.setdefault(domain, []).append(self.queue.getPath(msg)) - if len(exchanges) >= (self.maxConnections - len(self.managed)): - break - - if self.mxcalc is None: - self.mxcalc = MXCalculator() - - relays = [] - for (domain, msgs) in exchanges.iteritems(): - manager = _AttemptManager(self) - factory = self.factory(msgs, manager, *self.fArgs, **self.fKwArgs) - self.managed[factory] = map(os.path.basename, msgs) - relayAttemptDeferred = manager.getCompletionDeferred() - connectSetupDeferred = self.mxcalc.getMX(domain) - connectSetupDeferred.addCallback(lambda mx: str(mx.name)) - connectSetupDeferred.addCallback(self._cbExchange, self.PORT, factory) - connectSetupDeferred.addErrback(lambda err: (relayAttemptDeferred.errback(err), err)[1]) - connectSetupDeferred.addErrback(self._ebExchange, factory, domain) - relays.append(relayAttemptDeferred) - return DeferredList(relays) - - - def _cbExchange(self, address, port, factory): - from twisted.internet import reactor - reactor.connectTCP(address, port, factory) - - def _ebExchange(self, failure, factory, domain): - log.err('Error setting up managed relay factory for ' + domain) - log.err(failure) - def setWaiting(queue, messages): - map(queue.setWaiting, messages) - from twisted.internet import reactor - reactor.callLater(30, setWaiting, self.queue, self.managed[factory]) - del self.managed[factory] - -class SmartHostESMTPRelayingManager(SmartHostSMTPRelayingManager): - factory = ESMTPManagedRelayerFactory - -def _checkState(manager): - manager.checkState() - -def RelayStateHelper(manager, delay): - return internet.TimerService(delay, _checkState, manager) - - - -class CanonicalNameLoop(Exception): - """ - When trying to look up the MX record for a host, a set of CNAME records was - found which form a cycle and resolution was abandoned. - """ - - -class CanonicalNameChainTooLong(Exception): - """ - When trying to look up the MX record for a host, too many CNAME records - which point to other CNAME records were encountered and resolution was - abandoned. - """ - - -class MXCalculator: - """ - A utility for looking up mail exchange hosts and tracking whether they are - working or not. - - @ivar clock: L{IReactorTime} provider which will be used to decide when to - retry mail exchanges which have not been working. - """ - timeOutBadMX = 60 * 60 # One hour - fallbackToDomain = True - - def __init__(self, resolver=None, clock=None): - self.badMXs = {} - if resolver is None: - from twisted.names.client import createResolver - resolver = createResolver() - self.resolver = resolver - if clock is None: - from twisted.internet import reactor as clock - self.clock = clock - - - def markBad(self, mx): - """Indicate a given mx host is not currently functioning. - - @type mx: C{str} - @param mx: The hostname of the host which is down. - """ - self.badMXs[str(mx)] = self.clock.seconds() + self.timeOutBadMX - - def markGood(self, mx): - """Indicate a given mx host is back online. - - @type mx: C{str} - @param mx: The hostname of the host which is up. - """ - try: - del self.badMXs[mx] - except KeyError: - pass - - def getMX(self, domain, maximumCanonicalChainLength=3): - """ - Find an MX record for the given domain. - - @type domain: C{str} - @param domain: The domain name for which to look up an MX record. - - @type maximumCanonicalChainLength: C{int} - @param maximumCanonicalChainLength: The maximum number of unique CNAME - records to follow while looking up the MX record. - - @return: A L{Deferred} which is called back with a string giving the - name in the found MX record or which is errbacked if no MX record - can be found. - """ - mailExchangeDeferred = self.resolver.lookupMailExchange(domain) - mailExchangeDeferred.addCallback(self._filterRecords) - mailExchangeDeferred.addCallback( - self._cbMX, domain, maximumCanonicalChainLength) - mailExchangeDeferred.addErrback(self._ebMX, domain) - return mailExchangeDeferred - - - def _filterRecords(self, records): - """ - Convert a DNS response (a three-tuple of lists of RRHeaders) into a - mapping from record names to lists of corresponding record payloads. - """ - recordBag = {} - for answer in records[0]: - recordBag.setdefault(str(answer.name), []).append(answer.payload) - return recordBag - - - def _cbMX(self, answers, domain, cnamesLeft): - """ - Try to find the MX host from the given DNS information. - - This will attempt to resolve CNAME results. It can recognize loops - and will give up on non-cyclic chains after a specified number of - lookups. - """ - # Do this import here so that relaymanager.py doesn't depend on - # twisted.names, only MXCalculator will. - from twisted.names import dns, error - - seenAliases = set() - exchanges = [] - # Examine the answers for the domain we asked about - pertinentRecords = answers.get(domain, []) - while pertinentRecords: - record = pertinentRecords.pop() - - # If it's a CNAME, we'll need to do some more processing - if record.TYPE == dns.CNAME: - - # Remember that this name was an alias. - seenAliases.add(domain) - - canonicalName = str(record.name) - # See if we have some local records which might be relevant. - if canonicalName in answers: - - # Make sure it isn't a loop contained entirely within the - # results we have here. - if canonicalName in seenAliases: - return Failure(CanonicalNameLoop(record)) - - pertinentRecords = answers[canonicalName] - exchanges = [] - else: - if cnamesLeft: - # Request more information from the server. - return self.getMX(canonicalName, cnamesLeft - 1) - else: - # Give up. - return Failure(CanonicalNameChainTooLong(record)) - - # If it's an MX, collect it. - if record.TYPE == dns.MX: - exchanges.append((record.preference, record)) - - if exchanges: - exchanges.sort() - for (preference, record) in exchanges: - host = str(record.name) - if host not in self.badMXs: - return record - t = self.clock.seconds() - self.badMXs[host] - if t >= 0: - del self.badMXs[host] - return record - return exchanges[0][1] - else: - # Treat no answers the same as an error - jump to the errback to try - # to look up an A record. This provides behavior described as a - # special case in RFC 974 in the section headed I{Interpreting the - # List of MX RRs}. - return Failure( - error.DNSNameError("No MX records for %r" % (domain,))) - - - def _ebMX(self, failure, domain): - from twisted.names import error, dns - - if self.fallbackToDomain: - failure.trap(error.DNSNameError) - log.msg("MX lookup failed; attempting to use hostname (%s) directly" % (domain,)) - - # Alright, I admit, this is a bit icky. - d = self.resolver.getHostByName(domain) - def cbResolved(addr): - return dns.Record_MX(name=addr) - def ebResolved(err): - err.trap(error.DNSNameError) - raise DNSLookupError() - d.addCallbacks(cbResolved, ebResolved) - return d - elif failure.check(error.DNSNameError): - raise IOError("No MX found for %r" % (domain,)) - return failure diff --git a/tools/buildbot/pylibs/twisted/mail/scripts/__init__.py b/tools/buildbot/pylibs/twisted/mail/scripts/__init__.py deleted file mode 100644 index f653cc7..0000000 --- a/tools/buildbot/pylibs/twisted/mail/scripts/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"mail scripts" diff --git a/tools/buildbot/pylibs/twisted/mail/scripts/mailmail.py b/tools/buildbot/pylibs/twisted/mail/scripts/mailmail.py deleted file mode 100644 index 1f138e4..0000000 --- a/tools/buildbot/pylibs/twisted/mail/scripts/mailmail.py +++ /dev/null @@ -1,363 +0,0 @@ - -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. -# See LICENSE for details. - -# - -""" -Implementation module for the `newtexaco` command. - -The name is preliminary and subject to change. -""" - -import os -import sys -import rfc822 -import socket -import getpass -from ConfigParser import ConfigParser - -try: - import cStringIO as StringIO -except: - import StringIO - -from twisted.internet import reactor -from twisted.mail import bounce, smtp - -GLOBAL_CFG = "/etc/mailmail" -LOCAL_CFG = os.path.expanduser("~/.twisted/mailmail") -SMARTHOST = '127.0.0.1' - -ERROR_FMT = """\ -Subject: Failed Message Delivery - - Message delivery failed. The following occurred: - - %s --- -The Twisted sendmail application. -""" - -def log(message, *args): - sys.stderr.write(str(message) % args + '\n') - -class Options: - """ - @type to: C{list} of C{str} - @ivar to: The addresses to which to deliver this message. - - @type sender: C{str} - @ivar sender: The address from which this message is being sent. - - @type body: C{file} - @ivar body: The object from which the message is to be read. - """ - -def getlogin(): - try: - return os.getlogin() - except: - return getpass.getuser() - - -def parseOptions(argv): - o = Options() - o.to = [e for e in argv if not e.startswith('-')] - o.sender = getlogin() - - # Just be very stupid - - # Skip -bm -- it is the default - - # -bp lists queue information. Screw that. - if '-bp' in argv: - raise ValueError, "Unsupported option" - - # -bs makes sendmail use stdin/stdout as its transport. Screw that. - if '-bs' in argv: - raise ValueError, "Unsupported option" - - # -F sets who the mail is from, but is overridable by the From header - if '-F' in argv: - o.sender = argv[argv.index('-F') + 1] - o.to.remove(o.sender) - - # -i and -oi makes us ignore lone "." - if ('-i' in argv) or ('-oi' in argv): - raise ValueError, "Unsupported option" - - # -odb is background delivery - if '-odb' in argv: - o.background = True - else: - o.background = False - - # -odf is foreground delivery - if '-odf' in argv: - o.background = False - else: - o.background = True - - # -oem and -em cause errors to be mailed back to the sender. - # It is also the default. - - # -oep and -ep cause errors to be printed to stderr - if ('-oep' in argv) or ('-ep' in argv): - o.printErrors = True - else: - o.printErrors = False - - # -om causes a copy of the message to be sent to the sender if the sender - # appears in an alias expansion. We do not support aliases. - if '-om' in argv: - raise ValueError, "Unsupported option" - - # -t causes us to pick the recipients of the message from the To, Cc, and Bcc - # headers, and to remove the Bcc header if present. - if '-t' in argv: - o.recipientsFromHeaders = True - o.excludeAddresses = o.to - o.to = [] - else: - o.recipientsFromHeaders = False - o.exludeAddresses = [] - - requiredHeaders = { - 'from': [], - 'to': [], - 'cc': [], - 'bcc': [], - 'date': [], - } - - headers = [] - buffer = StringIO.StringIO() - while 1: - write = 1 - line = sys.stdin.readline() - if not line.strip(): - break - - hdrs = line.split(': ', 1) - - hdr = hdrs[0].lower() - if o.recipientsFromHeaders and hdr in ('to', 'cc', 'bcc'): - o.to.extend([ - a[1] for a in rfc822.AddressList(hdrs[1]).addresslist - ]) - if hdr == 'bcc': - write = 0 - elif hdr == 'from': - o.sender = rfc822.parseaddr(hdrs[1])[1] - - if hdr in requiredHeaders: - requiredHeaders[hdr].append(hdrs[1]) - - if write: - buffer.write(line) - - if not requiredHeaders['from']: - buffer.write('From: %s\r\n' % (o.sender,)) - if not requiredHeaders['to']: - if not o.to: - raise ValueError, "No recipients specified" - buffer.write('To: %s\r\n' % (', '.join(o.to),)) - if not requiredHeaders['date']: - buffer.write('Date: %s\r\n' % (smtp.rfc822date(),)) - - buffer.write(line) - - if o.recipientsFromHeaders: - for a in o.excludeAddresses: - try: - o.to.remove(a) - except: - pass - - buffer.seek(0, 0) - o.body = StringIO.StringIO(buffer.getvalue() + sys.stdin.read()) - return o - -class Configuration: - """ - @ivar allowUIDs: A list of UIDs which are allowed to send mail. - @ivar allowGIDs: A list of GIDs which are allowed to send mail. - @ivar denyUIDs: A list of UIDs which are not allowed to send mail. - @ivar denyGIDs: A list of GIDs which are not allowed to send mail. - - @type defaultAccess: C{bool} - @ivar defaultAccess: C{True} if access will be allowed when no other access - control rule matches or C{False} if it will be denied in that case. - - @ivar useraccess: Either C{'allow'} to check C{allowUID} first - or C{'deny'} to check C{denyUID} first. - - @ivar groupaccess: Either C{'allow'} to check C{allowGID} first or - C{'deny'} to check C{denyGID} first. - - @ivar identities: A C{dict} mapping hostnames to credentials to use when - sending mail to that host. - - @ivar smarthost: C{None} or a hostname through which all outgoing mail will - be sent. - - @ivar domain: C{None} or the hostname with which to identify ourselves when - connecting to an MTA. - """ - def __init__(self): - self.allowUIDs = [] - self.denyUIDs = [] - self.allowGIDs = [] - self.denyGIDs = [] - self.useraccess = 'deny' - self.groupaccess= 'deny' - - self.identities = {} - self.smarthost = None - self.domain = None - - self.defaultAccess = True - - -def loadConfig(path): - # [useraccess] - # allow=uid1,uid2,... - # deny=uid1,uid2,... - # order=allow,deny - # [groupaccess] - # allow=gid1,gid2,... - # deny=gid1,gid2,... - # order=deny,allow - # [identity] - # host1=username:password - # host2=username:password - # [addresses] - # smarthost=a.b.c.d - # default_domain=x.y.z - - c = Configuration() - - if not os.access(path, os.R_OK): - return c - - p = ConfigParser() - p.read(path) - - au = c.allowUIDs - du = c.denyUIDs - ag = c.allowGIDs - dg = c.denyGIDs - for (section, a, d) in (('useraccess', au, du), ('groupaccess', ag, dg)): - if p.has_section(section): - for (mode, L) in (('allow', a), ('deny', d)): - if p.has_option(section, mode) and p.get(section, mode): - for id in p.get(section, mode).split(','): - try: - id = int(id) - except ValueError: - log("Illegal %sID in [%s] section: %s", section[0].upper(), section, id) - else: - L.append(id) - order = p.get(section, 'order') - order = map(str.split, map(str.lower, order.split(','))) - if order[0] == 'allow': - setattr(c, section, 'allow') - else: - setattr(c, section, 'deny') - - if p.has_section('identity'): - for (host, up) in p.items('identity'): - parts = up.split(':', 1) - if len(parts) != 2: - log("Illegal entry in [identity] section: %s", up) - continue - p.identities[host] = parts - - if p.has_section('addresses'): - if p.has_option('addresses', 'smarthost'): - c.smarthost = p.get('addresses', 'smarthost') - if p.has_option('addresses', 'default_domain'): - c.domain = p.get('addresses', 'default_domain') - - return c - -def success(result): - reactor.stop() - -failed = None -def failure(f): - global failed - reactor.stop() - failed = f - -def sendmail(host, options, ident): - d = smtp.sendmail(host, options.sender, options.to, options.body) - d.addCallbacks(success, failure) - reactor.run() - -def senderror(failure, options): - recipient = [options.sender] - sender = '"Internally Generated Message (%s)"<postmaster@%s>' % (sys.argv[0], smtp.DNSNAME) - error = StringIO.StringIO() - failure.printTraceback(file=error) - body = StringIO.StringIO(ERROR_FMT % error.getvalue()) - - d = smtp.sendmail('localhost', sender, recipient, body) - d.addBoth(lambda _: reactor.stop()) - -def deny(conf): - uid = os.getuid() - gid = os.getgid() - - if conf.useraccess == 'deny': - if uid in conf.denyUIDs: - return True - if uid in conf.allowUIDs: - return False - else: - if uid in conf.allowUIDs: - return False - if uid in conf.denyUIDs: - return True - - if conf.groupaccess == 'deny': - if gid in conf.denyGIDs: - return True - if gid in conf.allowGIDs: - return False - else: - if gid in conf.allowGIDs: - return False - if gid in conf.denyGIDs: - return True - - return not conf.defaultAccess - -def run(): - o = parseOptions(sys.argv[1:]) - gConf = loadConfig(GLOBAL_CFG) - lConf = loadConfig(LOCAL_CFG) - - if deny(gConf) or deny(lConf): - log("Permission denied") - return - - host = lConf.smarthost or gConf.smarthost or SMARTHOST - - ident = gConf.identities.copy() - ident.update(lConf.identities) - - if lConf.domain: - smtp.DNSNAME = lConf.domain - elif gConf.domain: - smtp.DNSNAME = gConf.domain - - sendmail(host, o, ident) - - if failed: - if o.printErrors: - failed.printTraceback(file=sys.stderr) - raise SystemExit(1) - else: - senderror(failed, o) diff --git a/tools/buildbot/pylibs/twisted/mail/smtp.py b/tools/buildbot/pylibs/twisted/mail/smtp.py deleted file mode 100644 index 54b561e..0000000 --- a/tools/buildbot/pylibs/twisted/mail/smtp.py +++ /dev/null @@ -1,2016 +0,0 @@ -# -*- test-case-name: twisted.mail.test.test_smtp -*- -# -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. -# See LICENSE for details. - - -"""Simple Mail Transfer Protocol implementation. -""" - -from __future__ import generators - -# Twisted imports -from twisted.copyright import longversion -from twisted.protocols import basic -from twisted.protocols import policies -from twisted.internet import protocol -from twisted.internet import defer -from twisted.internet import error -from twisted.internet import reactor -from twisted.internet.interfaces import ITLSTransport -from twisted.python import log -from twisted.python import util -from twisted.python import failure - -from twisted import cred -import twisted.cred.checkers -import twisted.cred.credentials -from twisted.python.runtime import platform - -# System imports -import time, re, base64, types, socket, os, random, hmac -import MimeWriter, tempfile, rfc822 -import warnings -import binascii - -from zope.interface import implements, Interface - -try: - from email.base64MIME import encode as encode_base64 -except ImportError: - def encode_base64(s, eol='\n'): - return s.encode('base64').rstrip() + eol - -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO - -# Cache the hostname (XXX Yes - this is broken) -if platform.isMacOSX(): - # On OS X, getfqdn() is ridiculously slow - use the - # probably-identical-but-sometimes-not gethostname() there. - DNSNAME = socket.gethostname() -else: - DNSNAME = socket.getfqdn() - -# Used for fast success code lookup -SUCCESS = dict(map(None, range(200, 300), [])) - -class IMessageDelivery(Interface): - def receivedHeader(helo, origin, recipients): - """ - Generate the Received header for a message - - @type helo: C{(str, str)} - @param helo: The argument to the HELO command and the client's IP - address. - - @type origin: C{Address} - @param origin: The address the message is from - - @type recipients: C{list} of L{User} - @param recipients: A list of the addresses for which this message - is bound. - - @rtype: C{str} - @return: The full \"Received\" header string. - """ - - def validateTo(user): - """ - Validate the address for which the message is destined. - - @type user: C{User} - @param user: The address to validate. - - @rtype: no-argument callable - @return: A C{Deferred} which becomes, or a callable which - takes no arguments and returns an object implementing C{IMessage}. - This will be called and the returned object used to deliver the - message when it arrives. - - @raise SMTPBadRcpt: Raised if messages to the address are - not to be accepted. - """ - - def validateFrom(helo, origin): - """ - Validate the address from which the message originates. - - @type helo: C{(str, str)} - @param helo: The argument to the HELO command and the client's IP - address. - - @type origin: C{Address} - @param origin: The address the message is from - - @rtype: C{Deferred} or C{Address} - @return: C{origin} or a C{Deferred} whose callback will be - passed C{origin}. - - @raise SMTPBadSender: Raised of messages from this address are - not to be accepted. - """ - -class IMessageDeliveryFactory(Interface): - """An alternate interface to implement for handling message delivery. - - It is useful to implement this interface instead of L{IMessageDelivery} - directly because it allows the implementor to distinguish between - different messages delivery over the same connection. This can be - used to optimize delivery of a single message to multiple recipients, - something which cannot be done by L{IMessageDelivery} implementors - due to their lack of information. - """ - def getMessageDelivery(): - """Return an L{IMessageDelivery} object. - - This will be called once per message. - """ - -class SMTPError(Exception): - pass - -class SMTPClientError(SMTPError): - """Base class for SMTP client errors. - """ - def __init__(self, code, resp, log=None, addresses=None, isFatal=False, retry=False): - """ - @param code: The SMTP response code associated with this error. - @param resp: The string response associated with this error. - @param log: A string log of the exchange leading up to and including the error. - - @param isFatal: A boolean indicating whether this connection can proceed - or not. If True, the connection will be dropped. - @param retry: A boolean indicating whether the delivery should be retried. - If True and the factory indicates further retries are desirable, they will - be attempted, otherwise the delivery will be failed. - """ - self.code = code - self.resp = resp - self.log = log - self.addresses = addresses - self.isFatal = isFatal - self.retry = retry - - def __str__(self): - if self.code > 0: - res = ["%.3d %s" % (self.code, self.resp)] - else: - res = [self.resp] - if self.log: - res.append('') - res.append(self.log) - return '\n'.join(res) - - -class ESMTPClientError(SMTPClientError): - """Base class for ESMTP client errors. - """ - -class EHLORequiredError(ESMTPClientError): - """The server does not support EHLO. - - This is considered a non-fatal error (the connection will not be - dropped). - """ - -class AUTHRequiredError(ESMTPClientError): - """Authentication was required but the server does not support it. - - This is considered a non-fatal error (the connection will not be - dropped). - """ - -class TLSRequiredError(ESMTPClientError): - """Transport security was required but the server does not support it. - - This is considered a non-fatal error (the connection will not be - dropped). - """ - -class AUTHDeclinedError(ESMTPClientError): - """The server rejected our credentials. - - Either the username, password, or challenge response - given to the server was rejected. - - This is considered a non-fatal error (the connection will not be - dropped). - """ - -class AuthenticationError(ESMTPClientError): - """An error ocurred while authenticating. - - Either the server rejected our request for authentication or the - challenge received was malformed. - - This is considered a non-fatal error (the connection will not be - dropped). - """ - -class TLSError(ESMTPClientError): - """An error occurred while negiotiating for transport security. - - This is considered a non-fatal error (the connection will not be - dropped). - """ - -class SMTPConnectError(SMTPClientError): - """Failed to connect to the mail exchange host. - - This is considered a fatal error. A retry will be made. - """ - def __init__(self, code, resp, log=None, addresses=None, isFatal=True, retry=True): - SMTPClientError.__init__(self, code, resp, log, addresses, isFatal, retry) - -class SMTPTimeoutError(SMTPClientError): - """Failed to receive a response from the server in the expected time period. - - This is considered a fatal error. A retry will be made. - """ - def __init__(self, code, resp, log=None, addresses=None, isFatal=True, retry=True): - SMTPClientError.__init__(self, code, resp, log, addresses, isFatal, retry) - -class SMTPProtocolError(SMTPClientError): - """The server sent a mangled response. - - This is considered a fatal error. A retry will not be made. - """ - def __init__(self, code, resp, log=None, addresses=None, isFatal=True, retry=False): - SMTPClientError.__init__(self, code, resp, log, addresses, isFatal, retry) - -class SMTPDeliveryError(SMTPClientError): - """Indicates that a delivery attempt has had an error. - """ - -class SMTPServerError(SMTPError): - def __init__(self, code, resp): - self.code = code - self.resp = resp - - def __str__(self): - return "%.3d %s" % (self.code, self.resp) - -class SMTPAddressError(SMTPServerError): - def __init__(self, addr, code, resp): - SMTPServerError.__init__(self, code, resp) - self.addr = Address(addr) - - def __str__(self): - return "%.3d <%s>... %s" % (self.code, self.addr, self.resp) - -class SMTPBadRcpt(SMTPAddressError): - def __init__(self, addr, code=550, - resp='Cannot receive for specified address'): - SMTPAddressError.__init__(self, addr, code, resp) - -class SMTPBadSender(SMTPAddressError): - def __init__(self, addr, code=550, resp='Sender not acceptable'): - SMTPAddressError.__init__(self, addr, code, resp) - -def rfc822date(timeinfo=None,local=1): - """ - Format an RFC-2822 compliant date string. - - @param timeinfo: (optional) A sequence as returned by C{time.localtime()} - or C{time.gmtime()}. Default is now. - @param local: (optional) Indicates if the supplied time is local or - universal time, or if no time is given, whether now should be local or - universal time. Default is local, as suggested (SHOULD) by rfc-2822. - - @returns: A string representing the time and date in RFC-2822 format. - """ - if not timeinfo: - if local: - timeinfo = time.localtime() - else: - timeinfo = time.gmtime() - if local: - if timeinfo[8]: - # DST - tz = -time.altzone - else: - tz = -time.timezone - - (tzhr, tzmin) = divmod(abs(tz), 3600) - if tz: - tzhr *= int(abs(tz)/tz) - (tzmin, tzsec) = divmod(tzmin, 60) - else: - (tzhr, tzmin) = (0,0) - - return "%s, %02d %s %04d %02d:%02d:%02d %+03d%02d" % ( - ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][timeinfo[6]], - timeinfo[2], - ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][timeinfo[1] - 1], - timeinfo[0], timeinfo[3], timeinfo[4], timeinfo[5], - tzhr, tzmin) - -def idGenerator(): - i = 0 - while True: - yield i - i += 1 - -def messageid(uniq=None, N=idGenerator().next): - """Return a globally unique random string in RFC 2822 Message-ID format - - <datetime.pid.random@host.dom.ain> - - Optional uniq string will be added to strenghten uniqueness if given. - """ - datetime = time.strftime('%Y%m%d%H%M%S', time.gmtime()) - pid = os.getpid() - rand = random.randrange(2**31L-1) - if uniq is None: - uniq = '' - else: - uniq = '.' + uniq - - return '<%s.%s.%s%s.%s@%s>' % (datetime, pid, rand, uniq, N(), DNSNAME) - -def quoteaddr(addr): - """Turn an email address, possibly with realname part etc, into - a form suitable for and SMTP envelope. - """ - - if isinstance(addr, Address): - return '<%s>' % str(addr) - - res = rfc822.parseaddr(addr) - - if res == (None, None): - # It didn't parse, use it as-is - return '<%s>' % str(addr) - else: - return '<%s>' % str(res[1]) - -COMMAND, DATA, AUTH = 'COMMAND', 'DATA', 'AUTH' - -class AddressError(SMTPError): - "Parse error in address" - -# Character classes for parsing addresses -atom = r"[-A-Za-z0-9!\#$%&'*+/=?^_`{|}~]" - -class Address: - """Parse and hold an RFC 2821 address. - - Source routes are stipped and ignored, UUCP-style bang-paths - and %-style routing are not parsed. - - @type domain: C{str} - @ivar domain: The domain within which this address resides. - - @type local: C{str} - @ivar local: The local (\"user\") portion of this address. - """ - - tstring = re.compile(r'''( # A string of - (?:"[^"]*" # quoted string - |\\. # backslash-escaped characted - |''' + atom + r''' # atom character - )+|.) # or any single character''',re.X) - atomre = re.compile(atom) # match any one atom character - - def __init__(self, addr, defaultDomain=None): - if isinstance(addr, User): - addr = addr.dest - if isinstance(addr, Address): - self.__dict__ = addr.__dict__.copy() - return - elif not isinstance(addr, types.StringTypes): - addr = str(addr) - self.addrstr = addr - - # Tokenize - atl = filter(None,self.tstring.split(addr)) - - local = [] - domain = [] - - while atl: - if atl[0] == '<': - if atl[-1] != '>': - raise AddressError, "Unbalanced <>" - atl = atl[1:-1] - elif atl[0] == '@': - atl = atl[1:] - if not local: - # Source route - while atl and atl[0] != ':': - # remove it - atl = atl[1:] - if not atl: - raise AddressError, "Malformed source route" - atl = atl[1:] # remove : - elif domain: - raise AddressError, "Too many @" - else: - # Now in domain - domain = [''] - elif len(atl[0]) == 1 and not self.atomre.match(atl[0]) and atl[0] != '.': - raise AddressError, "Parse error at %r of %r" % (atl[0], (addr, atl)) - else: - if not domain: - local.append(atl[0]) - else: - domain.append(atl[0]) - atl = atl[1:] - - self.local = ''.join(local) - self.domain = ''.join(domain) - if self.local != '' and self.domain == '': - if defaultDomain is None: - defaultDomain = DNSNAME - self.domain = defaultDomain - - dequotebs = re.compile(r'\\(.)') - - def dequote(self,addr): - """Remove RFC-2821 quotes from address.""" - res = [] - - atl = filter(None,self.tstring.split(str(addr))) - - for t in atl: - if t[0] == '"' and t[-1] == '"': - res.append(t[1:-1]) - elif '\\' in t: - res.append(self.dequotebs.sub(r'\1',t)) - else: - res.append(t) - - return ''.join(res) - - def __str__(self): - if self.local or self.domain: - return '@'.join((self.local, self.domain)) - else: - return '' - - def __repr__(self): - return "%s.%s(%s)" % (self.__module__, self.__class__.__name__, - repr(str(self))) - -class User: - """Hold information about and SMTP message recipient, - including information on where the message came from - """ - - def __init__(self, destination, helo, protocol, orig): - host = getattr(protocol, 'host', None) - self.dest = Address(destination, host) - self.helo = helo - self.protocol = protocol - if isinstance(orig, Address): - self.orig = orig - else: - self.orig = Address(orig, host) - - def __getstate__(self): - """Helper for pickle. - - protocol isn't picklabe, but we want User to be, so skip it in - the pickle. - """ - return { 'dest' : self.dest, - 'helo' : self.helo, - 'protocol' : None, - 'orig' : self.orig } - - def __str__(self): - return str(self.dest) - -class IMessage(Interface): - """Interface definition for messages that can be sent via SMTP.""" - - def lineReceived(line): - """handle another line""" - - def eomReceived(): - """handle end of message - - return a deferred. The deferred should be called with either: - callback(string) or errback(error) - """ - - def connectionLost(): - """handle message truncated - - semantics should be to discard the message - """ - -class SMTP(basic.LineOnlyReceiver, policies.TimeoutMixin): - """SMTP server-side protocol.""" - - timeout = 600 - host = DNSNAME - portal = None - - # Control whether we log SMTP events - noisy = True - - # A factory for IMessageDelivery objects. If an - # avatar implementing IMessageDeliveryFactory can - # be acquired from the portal, it will be used to - # create a new IMessageDelivery object for each - # message which is received. - deliveryFactory = None - - # An IMessageDelivery object. A new instance is - # used for each message received if we can get an - # IMessageDeliveryFactory from the portal. Otherwise, - # a single instance is used throughout the lifetime - # of the connection. - delivery = None - - # Cred cleanup function. - _onLogout = None - - def __init__(self, delivery=None, deliveryFactory=None): - self.mode = COMMAND - self._from = None - self._helo = None - self._to = [] - self.delivery = delivery - self.deliveryFactory = deliveryFactory - - def timeoutConnection(self): - msg = '%s Timeout. Try talking faster next time!' % (self.host,) - self.sendCode(421, msg) - self.transport.loseConnection() - - def greeting(self): - return '%s NO UCE NO UBE NO RELAY PROBES' % (self.host,) - - def connectionMade(self): - # Ensure user-code always gets something sane for _helo - peer = self.transport.getPeer() - try: - host = peer.host - except AttributeError: # not an IPv4Address - host = str(peer) - self._helo = (None, host) - self.sendCode(220, self.greeting()) - self.setTimeout(self.timeout) - - def sendCode(self, code, message=''): - "Send an SMTP code with a message." - lines = message.splitlines() - lastline = lines[-1:] - for line in lines[:-1]: - self.sendLine('%3.3d-%s' % (code, line)) - self.sendLine('%3.3d %s' % (code, - lastline and lastline[0] or '')) - - def lineReceived(self, line): - self.resetTimeout() - return getattr(self, 'state_' + self.mode)(line) - - def state_COMMAND(self, line): - # Ignore leading and trailing whitespace, as well as an arbitrary - # amount of whitespace between the command and its argument, though - # it is not required by the protocol, for it is a nice thing to do. - line = line.strip() - - parts = line.split(None, 1) - if parts: - method = self.lookupMethod(parts[0]) or self.do_UNKNOWN - if len(parts) == 2: - method(parts[1]) - else: - method('') - else: - self.sendSyntaxError() - - def sendSyntaxError(self): - self.sendCode(500, 'Error: bad syntax') - - def lookupMethod(self, command): - return getattr(self, 'do_' + command.upper(), None) - - def lineLengthExceeded(self, line): - if self.mode is DATA: - for message in self.__messages: - message.connectionLost() - self.mode = COMMAND - del self.__messages - self.sendCode(500, 'Line too long') - - def do_UNKNOWN(self, rest): - self.sendCode(500, 'Command not implemented') - - def do_HELO(self, rest): - peer = self.transport.getPeer() - try: - host = peer.host - except AttributeError: - host = str(peer) - self._helo = (rest, host) - self._from = None - self._to = [] - self.sendCode(250, '%s Hello %s, nice to meet you' % (self.host, host)) - - def do_QUIT(self, rest): - self.sendCode(221, 'See you later') - self.transport.loseConnection() - - # A string of quoted strings, backslash-escaped character or - # atom characters + '@.,:' - qstring = r'("[^"]*"|\\.|' + atom + r'|[@.,:])+' - - mail_re = re.compile(r'''\s*FROM:\s*(?P<path><> # Empty <> - |<''' + qstring + r'''> # <addr> - |''' + qstring + r''' # addr - )\s*(\s(?P<opts>.*))? # Optional WS + ESMTP options - $''',re.I|re.X) - rcpt_re = re.compile(r'\s*TO:\s*(?P<path><' + qstring + r'''> # <addr> - |''' + qstring + r''' # addr - )\s*(\s(?P<opts>.*))? # Optional WS + ESMTP options - $''',re.I|re.X) - - def do_MAIL(self, rest): - if self._from: - self.sendCode(503,"Only one sender per message, please") - return - # Clear old recipient list - self._to = [] - m = self.mail_re.match(rest) - if not m: - self.sendCode(501, "Syntax error") - return - - try: - addr = Address(m.group('path'), self.host) - except AddressError, e: - self.sendCode(553, str(e)) - return - - validated = defer.maybeDeferred(self.validateFrom, self._helo, addr) - validated.addCallbacks(self._cbFromValidate, self._ebFromValidate) - - - def _cbFromValidate(self, from_, code=250, msg='Sender address accepted'): - self._from = from_ - self.sendCode(code, msg) - - - def _ebFromValidate(self, failure): - if failure.check(SMTPBadSender): - self.sendCode(failure.value.code, - 'Cannot receive from specified address %s: %s' - % (quoteaddr(failure.value.addr), failure.value.resp)) - elif failure.check(SMTPServerError): - self.sendCode(failure.value.code, failure.value.resp) - else: - log.err(failure, "SMTP sender validation failure") - self.sendCode( - 451, - 'Requested action aborted: local error in processing') - - - def do_RCPT(self, rest): - if not self._from: - self.sendCode(503, "Must have sender before recipient") - return - m = self.rcpt_re.match(rest) - if not m: - self.sendCode(501, "Syntax error") - return - - try: - user = User(m.group('path'), self._helo, self, self._from) - except AddressError, e: - self.sendCode(553, str(e)) - return - - d = defer.maybeDeferred(self.validateTo, user) - d.addCallbacks( - self._cbToValidate, - self._ebToValidate, - callbackArgs=(user,) - ) - - def _cbToValidate(self, to, user=None, code=250, msg='Recipient address accepted'): - if user is None: - user = to - self._to.append((user, to)) - self.sendCode(code, msg) - - def _ebToValidate(self, failure): - if failure.check(SMTPBadRcpt, SMTPServerError): - self.sendCode(failure.value.code, failure.value.resp) - else: - log.err(failure) - self.sendCode( - 451, - 'Requested action aborted: local error in processing' - ) - - def _disconnect(self, msgs): - for msg in msgs: - try: - msg.connectionLost() - except: - log.msg("msg raised exception from connectionLost") - log.err() - - def do_DATA(self, rest): - if self._from is None or (not self._to): - self.sendCode(503, 'Must have valid receiver and originator') - return - self.mode = DATA - helo, origin = self._helo, self._from - recipients = self._to - - self._from = None - self._to = [] - self.datafailed = None - - msgs = [] - for (user, msgFunc) in recipients: - try: - msg = msgFunc() - rcvdhdr = self.receivedHeader(helo, origin, [user]) - if rcvdhdr: - msg.lineReceived(rcvdhdr) - msgs.append(msg) - except SMTPServerError, e: - self.sendCode(e.code, e.resp) - self.mode = COMMAND - self._disconnect(msgs) - return - except: - log.err() - self.sendCode(550, "Internal server error") - self.mode = COMMAND - self._disconnect(msgs) - return - self.__messages = msgs - - self.__inheader = self.__inbody = 0 - self.sendCode(354, 'Continue') - - if self.noisy: - fmt = 'Receiving message for delivery: from=%s to=%s' - log.msg(fmt % (origin, [str(u) for (u, f) in recipients])) - - def connectionLost(self, reason): - # self.sendCode(421, 'Dropping connection.') # This does nothing... - # Ideally, if we (rather than the other side) lose the connection, - # we should be able to tell the other side that we are going away. - # RFC-2821 requires that we try. - if self.mode is DATA: - try: - for message in self.__messages: - try: - message.connectionLost() - except: - log.err() - del self.__messages - except AttributeError: - pass - if self._onLogout: - self._onLogout() - self._onLogout = None - self.setTimeout(None) - - def do_RSET(self, rest): - self._from = None - self._to = [] - self.sendCode(250, 'I remember nothing.') - - def dataLineReceived(self, line): - if line[:1] == '.': - if line == '.': - self.mode = COMMAND - if self.datafailed: - self.sendCode(self.datafailed.code, - self.datafailed.resp) - return - if not self.__messages: - self._messageHandled("thrown away") - return - defer.DeferredList([ - m.eomReceived() for m in self.__messages - ], consumeErrors=True).addCallback(self._messageHandled - ) - del self.__messages - return - line = line[1:] - - if self.datafailed: - return - - try: - # Add a blank line between the generated Received:-header - # and the message body if the message comes in without any - # headers - if not self.__inheader and not self.__inbody: - if ':' in line: - self.__inheader = 1 - elif line: - for message in self.__messages: - message.lineReceived('') - self.__inbody = 1 - - if not line: - self.__inbody = 1 - - for message in self.__messages: - message.lineReceived(line) - except SMTPServerError, e: - self.datafailed = e - for message in self.__messages: - message.connectionLost() - state_DATA = dataLineReceived - - def _messageHandled(self, resultList): - failures = 0 - for (success, result) in resultList: - if not success: - failures += 1 - log.err(result) - if failures: - msg = 'Could not send e-mail' - L = len(resultList) - if L > 1: - msg += ' (%d failures out of %d recipients)' % (failures, L) - self.sendCode(550, msg) - else: - self.sendCode(250, 'Delivery in progress') - - - def _cbAnonymousAuthentication(self, (iface, avatar, logout)): - """ - Save the state resulting from a successful anonymous cred login. - """ - if issubclass(iface, IMessageDeliveryFactory): - self.deliveryFactory = avatar - self.delivery = None - elif issubclass(iface, IMessageDelivery): - self.deliveryFactory = None - self.delivery = avatar - else: - raise RuntimeError("%s is not a supported interface" % (iface.__name__,)) - self._onLogout = logout - self.challenger = None - - - # overridable methods: - def validateFrom(self, helo, origin): - """ - Validate the address from which the message originates. - - @type helo: C{(str, str)} - @param helo: The argument to the HELO command and the client's IP - address. - - @type origin: C{Address} - @param origin: The address the message is from - - @rtype: C{Deferred} or C{Address} - @return: C{origin} or a C{Deferred} whose callback will be - passed C{origin}. - - @raise SMTPBadSender: Raised of messages from this address are - not to be accepted. - """ - if self.deliveryFactory is not None: - self.delivery = self.deliveryFactory.getMessageDelivery() - - if self.delivery is not None: - return defer.maybeDeferred(self.delivery.validateFrom, - helo, origin) - - # No login has been performed, no default delivery object has been - # provided: try to perform an anonymous login and then invoke this - # method again. - if self.portal: - - result = self.portal.login( - cred.credentials.Anonymous(), - None, - IMessageDeliveryFactory, IMessageDelivery) - - def ebAuthentication(err): - """ - Translate cred exceptions into SMTP exceptions so that the - protocol code which invokes C{validateFrom} can properly report - the failure. - """ - if err.check(cred.error.UnauthorizedLogin): - exc = SMTPBadSender(origin) - elif err.check(cred.error.UnhandledCredentials): - exc = SMTPBadSender( - origin, resp="Unauthenticated senders not allowed") - else: - return err - return defer.fail(exc) - - result.addCallbacks( - self._cbAnonymousAuthentication, ebAuthentication) - - def continueValidation(ignored): - """ - Re-attempt from address validation. - """ - return self.validateFrom(helo, origin) - - result.addCallback(continueValidation) - return result - - raise SMTPBadSender(origin) - - - def validateTo(self, user): - """ - Validate the address for which the message is destined. - - @type user: C{User} - @param user: The address to validate. - - @rtype: no-argument callable - @return: A C{Deferred} which becomes, or a callable which - takes no arguments and returns an object implementing C{IMessage}. - This will be called and the returned object used to deliver the - message when it arrives. - - @raise SMTPBadRcpt: Raised if messages to the address are - not to be accepted. - """ - if self.delivery is not None: - return self.delivery.validateTo(user) - raise SMTPBadRcpt(user) - - def receivedHeader(self, helo, origin, recipients): - if self.delivery is not None: - return self.delivery.receivedHeader(helo, origin, recipients) - - heloStr = "" - if helo[0]: - heloStr = " helo=%s" % (helo[0],) - domain = self.transport.getHost().host - from_ = "from %s ([%s]%s)" % (helo[0], helo[1], heloStr) - by = "by %s with %s (%s)" % (domain, - self.__class__.__name__, - longversion) - for_ = "for %s; %s" % (' '.join(map(str, recipients)), - rfc822date()) - return "Received: %s\n\t%s\n\t%s" % (from_, by, for_) - - def startMessage(self, recipients): - if self.delivery: - return self.delivery.startMessage(recipients) - return [] - - -class SMTPFactory(protocol.ServerFactory): - """Factory for SMTP.""" - - # override in instances or subclasses - domain = DNSNAME - timeout = 600 - protocol = SMTP - - portal = None - - def __init__(self, portal = None): - self.portal = portal - - def buildProtocol(self, addr): - p = protocol.ServerFactory.buildProtocol(self, addr) - p.portal = self.portal - p.host = self.domain - return p - -class SMTPClient(basic.LineReceiver, policies.TimeoutMixin): - """SMTP client for sending emails.""" - - # If enabled then log SMTP client server communication - debug = True - - # Number of seconds to wait before timing out a connection. If - # None, perform no timeout checking. - timeout = None - - def __init__(self, identity, logsize=10): - self.identity = identity or '' - self.toAddressesResult = [] - self.successAddresses = [] - self._from = None - self.resp = [] - self.code = -1 - self.log = util.LineLog(logsize) - - def sendLine(self, line): - # Log sendLine only if you are in debug mode for performance - if self.debug: - self.log.append('>>> ' + line) - - basic.LineReceiver.sendLine(self,line) - - def connectionMade(self): - self.setTimeout(self.timeout) - - self._expected = [ 220 ] - self._okresponse = self.smtpState_helo - self._failresponse = self.smtpConnectionFailed - - def connectionLost(self, reason=protocol.connectionDone): - """We are no longer connected""" - self.setTimeout(None) - self.mailFile = None - - def timeoutConnection(self): - self.sendError( - SMTPTimeoutError(-1, - "Timeout waiting for SMTP server response", - self.log)) - - def lineReceived(self, line): - self.resetTimeout() - - # Log lineReceived only if you are in debug mode for performance - if self.debug: - self.log.append('<<< ' + line) - - why = None - - try: - self.code = int(line[:3]) - except ValueError: - # This is a fatal error and will disconnect the transport lineReceived will not be called again - self.sendError(SMTPProtocolError(-1, "Invalid response from SMTP server: %s" % line, self.log.str())) - return - - if line[0] == '0': - # Verbose informational message, ignore it - return - - self.resp.append(line[4:]) - - if line[3:4] == '-': - # continuation - return - - if self.code in self._expected: - why = self._okresponse(self.code,'\n'.join(self.resp)) - else: - why = self._failresponse(self.code,'\n'.join(self.resp)) - - self.code = -1 - self.resp = [] - return why - - def smtpConnectionFailed(self, code, resp): - self.sendError(SMTPConnectError(code, resp, self.log.str())) - - def smtpTransferFailed(self, code, resp): - if code < 0: - self.sendError(SMTPProtocolError(code, resp, self.log.str())) - else: - self.smtpState_msgSent(code, resp) - - def smtpState_helo(self, code, resp): - self.sendLine('HELO ' + self.identity) - self._expected = SUCCESS - self._okresponse = self.smtpState_from - - def smtpState_from(self, code, resp): - self._from = self.getMailFrom() - self._failresponse = self.smtpTransferFailed - if self._from is not None: - self.sendLine('MAIL FROM:%s' % quoteaddr(self._from)) - self._expected = [250] - self._okresponse = self.smtpState_to - else: - # All messages have been sent, disconnect - self._disconnectFromServer() - - def smtpState_disconnect(self, code, resp): - self.transport.loseConnection() - - def smtpState_to(self, code, resp): - self.toAddresses = iter(self.getMailTo()) - self.toAddressesResult = [] - self.successAddresses = [] - self._okresponse = self.smtpState_toOrData - self._expected = xrange(0,1000) - self.lastAddress = None - return self.smtpState_toOrData(0, '') - - def smtpState_toOrData(self, code, resp): - if self.lastAddress is not None: - self.toAddressesResult.append((self.lastAddress, code, resp)) - if code in SUCCESS: - self.successAddresses.append(self.lastAddress) - try: - self.lastAddress = self.toAddresses.next() - except StopIteration: - if self.successAddresses: - self.sendLine('DATA') - self._expected = [ 354 ] - self._okresponse = self.smtpState_data - else: - return self.smtpState_msgSent(code,'No recipients accepted') - else: - self.sendLine('RCPT TO:%s' % quoteaddr(self.lastAddress)) - - def smtpState_data(self, code, resp): - s = basic.FileSender() - s.beginFileTransfer( - self.getMailData(), self.transport, self.transformChunk - ).addCallback(self.finishedFileTransfer) - self._expected = SUCCESS - self._okresponse = self.smtpState_msgSent - - def smtpState_msgSent(self, code, resp): - if self._from is not None: - self.sentMail(code, resp, len(self.successAddresses), - self.toAddressesResult, self.log) - - self.toAddressesResult = [] - self._from = None - self.sendLine('RSET') - self._expected = SUCCESS - self._okresponse = self.smtpState_from - - ## - ## Helpers for FileSender - ## - def transformChunk(self, chunk): - return chunk.replace('\n', '\r\n').replace('\r\n.', '\r\n..') - - def finishedFileTransfer(self, lastsent): - if lastsent != '\n': - line = '\r\n.' - else: - line = '.' - self.sendLine(line) - ## - - # these methods should be overriden in subclasses - def getMailFrom(self): - """Return the email address the mail is from.""" - raise NotImplementedError - - def getMailTo(self): - """Return a list of emails to send to.""" - raise NotImplementedError - - def getMailData(self): - """Return file-like object containing data of message to be sent. - - Lines in the file should be delimited by '\\n'. - """ - raise NotImplementedError - - def sendError(self, exc): - """ - If an error occurs before a mail message is sent sendError will be - called. This base class method sends a QUIT if the error is - non-fatal and disconnects the connection. - - @param exc: The SMTPClientError (or child class) raised - @type exc: C{SMTPClientError} - """ - assert isinstance(exc, SMTPClientError) - - if exc.isFatal: - # If the error was fatal then the communication channel with the SMTP Server is - # broken so just close the transport connection - self.smtpState_disconnect(-1, None) - else: - self._disconnectFromServer() - - def sentMail(self, code, resp, numOk, addresses, log): - """Called when an attempt to send an email is completed. - - If some addresses were accepted, code and resp are the response - to the DATA command. If no addresses were accepted, code is -1 - and resp is an informative message. - - @param code: the code returned by the SMTP Server - @param resp: The string response returned from the SMTP Server - @param numOK: the number of addresses accepted by the remote host. - @param addresses: is a list of tuples (address, code, resp) listing - the response to each RCPT command. - @param log: is the SMTP session log - """ - raise NotImplementedError - - def _disconnectFromServer(self): - self._expected = xrange(0, 1000) - self._okresponse = self.smtpState_disconnect - self.sendLine('QUIT') - -class ESMTPClient(SMTPClient): - # Fall back to HELO if the server does not support EHLO - heloFallback = True - - # Refuse to proceed if authentication cannot be performed - requireAuthentication = False - - # Refuse to proceed if TLS is not available - requireTransportSecurity = False - - # Indicate whether or not our transport can be considered secure. - tlsMode = False - - # ClientContextFactory to use for STARTTLS - context = None - - def __init__(self, secret, contextFactory=None, *args, **kw): - SMTPClient.__init__(self, *args, **kw) - self.authenticators = [] - self.secret = secret - self.context = contextFactory - self.tlsMode = False - - - def esmtpEHLORequired(self, code=-1, resp=None): - self.sendError(EHLORequiredError(502, "Server does not support ESMTP Authentication", self.log.str())) - - - def esmtpAUTHRequired(self, code=-1, resp=None): - tmp = [] - - for a in self.authenticators: - tmp.append(a.getName().upper()) - - auth = "[%s]" % ', '.join(tmp) - - self.sendError(AUTHRequiredError(502, "Server does not support Client Authentication schemes %s" % auth, - self.log.str())) - - - def esmtpTLSRequired(self, code=-1, resp=None): - self.sendError(TLSRequiredError(502, "Server does not support secure communication via TLS / SSL", - self.log.str())) - - def esmtpTLSFailed(self, code=-1, resp=None): - self.sendError(TLSError(code, "Could not complete the SSL/TLS handshake", self.log.str())) - - def esmtpAUTHDeclined(self, code=-1, resp=None): - self.sendError(AUTHDeclinedError(code, resp, self.log.str())) - - def esmtpAUTHMalformedChallenge(self, code=-1, resp=None): - str = "Login failed because the SMTP Server returned a malformed Authentication Challenge" - self.sendError(AuthenticationError(501, str, self.log.str())) - - def esmtpAUTHServerError(self, code=-1, resp=None): - self.sendError(AuthenticationError(code, resp, self.log.str())) - - def registerAuthenticator(self, auth): - """Registers an Authenticator with the ESMTPClient. The ESMTPClient - will attempt to login to the SMTP Server in the order the - Authenticators are registered. The most secure Authentication - mechanism should be registered first. - - @param auth: The Authentication mechanism to register - @type auth: class implementing C{IClientAuthentication} - """ - - self.authenticators.append(auth) - - def connectionMade(self): - SMTPClient.connectionMade(self) - self._okresponse = self.esmtpState_ehlo - - def esmtpState_ehlo(self, code, resp): - self._expected = SUCCESS - - self._okresponse = self.esmtpState_serverConfig - self._failresponse = self.esmtpEHLORequired - - if self.heloFallback: - self._failresponse = self.smtpState_helo - - self.sendLine('EHLO ' + self.identity) - - def esmtpState_serverConfig(self, code, resp): - items = {} - for line in resp.splitlines(): - e = line.split(None, 1) - if len(e) > 1: - items[e[0]] = e[1] - else: - items[e[0]] = None - - if self.tlsMode: - self.authenticate(code, resp, items) - else: - self.tryTLS(code, resp, items) - - def tryTLS(self, code, resp, items): - if self.context and 'STARTTLS' in items: - self._expected = [220] - self._okresponse = self.esmtpState_starttls - self._failresponse = self.esmtpTLSFailed - self.sendLine('STARTTLS') - elif self.requireTransportSecurity: - self.tlsMode = False - self.esmtpTLSRequired() - else: - self.tlsMode = False - self.authenticate(code, resp, items) - - def esmtpState_starttls(self, code, resp): - try: - self.transport.startTLS(self.context) - self.tlsMode = True - except: - log.err() - self.esmtpTLSFailed(451) - - # Send another EHLO once TLS has been started to - # get the TLS / AUTH schemes. Some servers only allow AUTH in TLS mode. - self.esmtpState_ehlo(code, resp) - - def authenticate(self, code, resp, items): - if self.secret and items.get('AUTH'): - schemes = items['AUTH'].split() - tmpSchemes = {} - - #XXX: May want to come up with a more efficient way to do this - for s in schemes: - tmpSchemes[s.upper()] = 1 - - for a in self.authenticators: - auth = a.getName().upper() - - if auth in tmpSchemes: - self._authinfo = a - - # Special condition handled - if auth == "PLAIN": - self._okresponse = self.smtpState_from - self._failresponse = self._esmtpState_plainAuth - self._expected = [235] - challenge = encode_base64(self._authinfo.challengeResponse(self.secret, 1), eol="") - self.sendLine('AUTH ' + auth + ' ' + challenge) - else: - self._expected = [334] - self._okresponse = self.esmtpState_challenge - # If some error occurs here, the server declined the AUTH - # before the user / password phase. This would be - # a very rare case - self._failresponse = self.esmtpAUTHServerError - self.sendLine('AUTH ' + auth) - return - - if self.requireAuthentication: - self.esmtpAUTHRequired() - else: - self.smtpState_from(code, resp) - - def _esmtpState_plainAuth(self, code, resp): - self._okresponse = self.smtpState_from - self._failresponse = self.esmtpAUTHDeclined - self._expected = [235] - challenge = encode_base64(self._authinfo.challengeResponse(self.secret, 2), eol="") - self.sendLine('AUTH PLAIN ' + challenge) - - def esmtpState_challenge(self, code, resp): - auth = self._authinfo - del self._authinfo - self._authResponse(auth, resp) - - def _authResponse(self, auth, challenge): - self._failresponse = self.esmtpAUTHDeclined - - try: - challenge = base64.decodestring(challenge) - except binascii.Error, e: - # Illegal challenge, give up, then quit - self.sendLine('*') - self._okresponse = self.esmtpAUTHMalformedChallenge - self._failresponse = self.esmtpAUTHMalformedChallenge - else: - resp = auth.challengeResponse(self.secret, challenge) - self._expected = [235] - self._okresponse = self.smtpState_from - self.sendLine(encode_base64(resp, eol="")) - - if auth.getName() == "LOGIN" and challenge == "Username:": - self._expected = [334] - self._authinfo = auth - self._okresponse = self.esmtpState_challenge - - -class ESMTP(SMTP): - - ctx = None - canStartTLS = False - startedTLS = False - - authenticated = False - - def __init__(self, chal = None, contextFactory = None): - SMTP.__init__(self) - if chal is None: - chal = {} - self.challengers = chal - self.authenticated = False - self.ctx = contextFactory - - def connectionMade(self): - SMTP.connectionMade(self) - self.canStartTLS = ITLSTransport.providedBy(self.transport) - self.canStartTLS = self.canStartTLS and (self.ctx is not None) - - - def greeting(self): - return SMTP.greeting(self) + ' ESMTP' - - - def extensions(self): - ext = {'AUTH': self.challengers.keys()} - if self.canStartTLS and not self.startedTLS: - ext['STARTTLS'] = None - return ext - - def lookupMethod(self, command): - m = SMTP.lookupMethod(self, command) - if m is None: - m = getattr(self, 'ext_' + command.upper(), None) - return m - - def listExtensions(self): - r = [] - for (c, v) in self.extensions().iteritems(): - if v is not None: - if v: - # Intentionally omit extensions with empty argument lists - r.append('%s %s' % (c, ' '.join(v))) - else: - r.append(c) - return '\n'.join(r) - - def do_EHLO(self, rest): - peer = self.transport.getPeer().host - self._helo = (rest, peer) - self._from = None - self._to = [] - self.sendCode( - 250, - '%s Hello %s, nice to meet you\n%s' % ( - self.host, peer, - self.listExtensions(), - ) - ) - - def ext_STARTTLS(self, rest): - if self.startedTLS: - self.sendCode(503, 'TLS already negotiated') - elif self.ctx and self.canStartTLS: - self.sendCode(220, 'Begin TLS negotiation now') - self.transport.startTLS(self.ctx) - self.startedTLS = True - else: - self.sendCode(454, 'TLS not available') - - def ext_AUTH(self, rest): - if self.authenticated: - self.sendCode(503, 'Already authenticated') - return - parts = rest.split(None, 1) - chal = self.challengers.get(parts[0].upper(), lambda: None)() - if not chal: - self.sendCode(504, 'Unrecognized authentication type') - return - - self.mode = AUTH - self.challenger = chal - - if len(parts) > 1: - chal.getChallenge() # Discard it, apparently the client does not - # care about it. - rest = parts[1] - else: - rest = None - self.state_AUTH(rest) - - - def _cbAuthenticated(self, loginInfo): - """ - Save the state resulting from a successful cred login and mark this - connection as authenticated. - """ - result = SMTP._cbAnonymousAuthentication(self, loginInfo) - self.authenticated = True - return result - - - def _ebAuthenticated(self, reason): - """ - Handle cred login errors by translating them to the SMTP authenticate - failed. Translate all other errors into a generic SMTP error code and - log the failure for inspection. Stop all errors from propagating. - """ - self.challenge = None - if reason.check(cred.error.UnauthorizedLogin): - self.sendCode(535, 'Authentication failed') - else: - log.err(failure, "SMTP authentication failure") - self.sendCode( - 451, - 'Requested action aborted: local error in processing') - - - def state_AUTH(self, response): - """ - Handle one step of challenge/response authentication. - - @param response: The text of a response. If None, this - function has been called as a result of an AUTH command with - no initial response. A response of '*' aborts authentication, - as per RFC 2554. - """ - if self.portal is None: - self.sendCode(454, 'Temporary authentication failure') - self.mode = COMMAND - return - - if response is None: - challenge = self.challenger.getChallenge() - encoded = challenge.encode('base64') - self.sendCode(334, encoded) - return - - if response == '*': - self.sendCode(501, 'Authentication aborted') - self.challenger = None - self.mode = COMMAND - return - - try: - uncoded = response.decode('base64') - except binascii.Error: - self.sendCode(501, 'Syntax error in parameters or arguments') - self.challenger = None - self.mode = COMMAND - return - - self.challenger.setResponse(uncoded) - if self.challenger.moreChallenges(): - challenge = self.challenger.getChallenge() - coded = challenge.encode('base64')[:-1] - self.sendCode(334, coded) - return - - self.mode = COMMAND - result = self.portal.login( - self.challenger, None, - IMessageDeliveryFactory, IMessageDelivery) - result.addCallback(self._cbAuthenticated) - result.addCallback(lambda ign: self.sendCode(235, 'Authentication successful.')) - result.addErrback(self._ebAuthenticated) - - - -class SenderMixin: - """Utility class for sending emails easily. - - Use with SMTPSenderFactory or ESMTPSenderFactory. - """ - done = 0 - - def getMailFrom(self): - if not self.done: - self.done = 1 - return str(self.factory.fromEmail) - else: - return None - - def getMailTo(self): - return self.factory.toEmail - - def getMailData(self): - return self.factory.file - - def sendError(self, exc): - # Call the base class to close the connection with the SMTP server - SMTPClient.sendError(self, exc) - - # Do not retry to connect to SMTP Server if: - # 1. No more retries left (This allows the correct error to be returned to the errorback) - # 2. retry is false - # 3. The error code is not in the 4xx range (Communication Errors) - - if (self.factory.retries >= 0 or - (not exc.retry and not (exc.code >= 400 and exc.code < 500))): - self.factory.sendFinished = 1 - self.factory.result.errback(exc) - - def sentMail(self, code, resp, numOk, addresses, log): - # Do not retry, the SMTP server acknowledged the request - self.factory.sendFinished = 1 - if code not in SUCCESS: - errlog = [] - for addr, acode, aresp in addresses: - if code not in SUCCESS: - errlog.append("%s: %03d %s" % (addr, acode, aresp)) - - errlog.append(log.str()) - - exc = SMTPDeliveryError(code, resp, '\n'.join(errlog), addresses) - self.factory.result.errback(exc) - else: - self.factory.result.callback((numOk, addresses)) - - -class SMTPSender(SenderMixin, SMTPClient): - pass - - -class SMTPSenderFactory(protocol.ClientFactory): - """ - Utility factory for sending emails easily. - """ - - domain = DNSNAME - protocol = SMTPSender - - def __init__(self, fromEmail, toEmail, file, deferred, retries=5, - timeout=None): - """ - @param fromEmail: The RFC 2821 address from which to send this - message. - - @param toEmail: A sequence of RFC 2821 addresses to which to - send this message. - - @param file: A file-like object containing the message to send. - - @param deferred: A Deferred to callback or errback when sending - of this message completes. - - @param retries: The number of times to retry delivery of this - message. - - @param timeout: Period, in seconds, for which to wait for - server responses, or None to wait forever. - """ - assert isinstance(retries, (int, long)) - - if isinstance(toEmail, types.StringTypes): - toEmail = [toEmail] - self.fromEmail = Address(fromEmail) - self.nEmails = len(toEmail) - self.toEmail = iter(toEmail) - self.file = file - self.result = deferred - self.result.addBoth(self._removeDeferred) - self.sendFinished = 0 - - self.retries = -retries - self.timeout = timeout - - def _removeDeferred(self, argh): - del self.result - return argh - - def clientConnectionFailed(self, connector, err): - self._processConnectionError(connector, err) - - def clientConnectionLost(self, connector, err): - self._processConnectionError(connector, err) - - def _processConnectionError(self, connector, err): - if self.retries < self.sendFinished <= 0: - log.msg("SMTP Client retrying server. Retry: %s" % -self.retries) - - connector.connect() - self.retries += 1 - elif self.sendFinished <= 0: - # If we were unable to communicate with the SMTP server a ConnectionDone will be - # returned. We want a more clear error message for debugging - if err.check(error.ConnectionDone): - err.value = SMTPConnectError(-1, "Unable to connect to server.") - self.result.errback(err.value) - - def buildProtocol(self, addr): - p = self.protocol(self.domain, self.nEmails*2+2) - p.factory = self - p.timeout = self.timeout - return p - - -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 - - def getName(self): - return "LOGIN" - - def challengeResponse(self, secret, chal): - if chal== "Username:": - return self.user - elif chal == 'Password:': - return secret - -class PLAINAuthenticator: - implements(IClientAuthentication) - - def __init__(self, user): - self.user = user - - def getName(self): - return "PLAIN" - - def challengeResponse(self, secret, chal=1): - if chal == 1: - return "%s\0%s\0%s" % (self.user, self.user, secret) - else: - return "%s\0%s" % (self.user, secret) - - -class ESMTPSender(SenderMixin, ESMTPClient): - - requireAuthentication = True - requireTransportSecurity = True - - def __init__(self, username, secret, contextFactory=None, *args, **kw): - self.heloFallback = 0 - self.username = username - - if contextFactory is None: - contextFactory = self._getContextFactory() - - ESMTPClient.__init__(self, secret, contextFactory, *args, **kw) - - self._registerAuthenticators() - - def _registerAuthenticators(self): - # Register Authenticator in order from most secure to least secure - self.registerAuthenticator(CramMD5ClientAuthenticator(self.username)) - self.registerAuthenticator(LOGINAuthenticator(self.username)) - self.registerAuthenticator(PLAINAuthenticator(self.username)) - - def _getContextFactory(self): - if self.context is not None: - return self.context - try: - from twisted.internet import ssl - except ImportError: - return None - else: - try: - context = ssl.ClientContextFactory() - context.method = ssl.SSL.TLSv1_METHOD - return context - except AttributeError: - return None - - -class ESMTPSenderFactory(SMTPSenderFactory): - """ - Utility factory for sending emails easily. - """ - - protocol = ESMTPSender - - def __init__(self, username, password, fromEmail, toEmail, file, - deferred, retries=5, timeout=None, - contextFactory=None, heloFallback=False, - requireAuthentication=True, - requireTransportSecurity=True): - - SMTPSenderFactory.__init__(self, fromEmail, toEmail, file, deferred, retries, timeout) - self.username = username - self.password = password - self._contextFactory = contextFactory - self._heloFallback = heloFallback - self._requireAuthentication = requireAuthentication - self._requireTransportSecurity = requireTransportSecurity - - def buildProtocol(self, addr): - p = self.protocol(self.username, self.password, self._contextFactory, self.domain, self.nEmails*2+2) - p.heloFallback = self._heloFallback - p.requireAuthentication = self._requireAuthentication - p.requireTransportSecurity = self._requireTransportSecurity - p.factory = self - p.timeout = self.timeout - return p - -def sendmail(smtphost, from_addr, to_addrs, msg, senderDomainName=None, port=25): - """Send an email - - This interface is intended to be a direct replacement for - smtplib.SMTP.sendmail() (with the obvious change that - you specify the smtphost as well). Also, ESMTP options - are not accepted, as we don't do ESMTP yet. I reserve the - right to implement the ESMTP options differently. - - @param smtphost: The host the message should be sent to - @param from_addr: The (envelope) address sending this mail. - @param to_addrs: A list of addresses to send this mail to. A string will - be treated as a list of one address - @param msg: The message, including headers, either as a file or a string. - File-like objects need to support read() and close(). Lines must be - delimited by '\\n'. If you pass something that doesn't look like a - file, we try to convert it to a string (so you should be able to - pass an email.Message directly, but doing the conversion with - email.Generator manually will give you more control over the - process). - - @param senderDomainName: Name by which to identify. If None, try - to pick something sane (but this depends on external configuration - and may not succeed). - - @param port: Remote port to which to connect. - - @rtype: L{Deferred} - @returns: A L{Deferred}, its callback will be called if a message is sent - to ANY address, the errback if no message is sent. - - The callback will be called with a tuple (numOk, addresses) where numOk - is the number of successful recipient addresses and addresses is a list - of tuples (address, code, resp) giving the response to the RCPT command - for each address. - """ - if not hasattr(msg,'read'): - # It's not a file - msg = StringIO(str(msg)) - - d = defer.Deferred() - factory = SMTPSenderFactory(from_addr, to_addrs, msg, d) - - if senderDomainName is not None: - factory.domain = senderDomainName - - reactor.connectTCP(smtphost, port, factory) - - return d - -def sendEmail(smtphost, fromEmail, toEmail, content, headers = None, attachments = None, multipartbody = "mixed"): - """Send an email, optionally with attachments. - - @type smtphost: str - @param smtphost: hostname of SMTP server to which to connect - - @type fromEmail: str - @param fromEmail: email address to indicate this email is from - - @type toEmail: str - @param toEmail: email address to which to send this email - - @type content: str - @param content: The body if this email. - - @type headers: dict - @param headers: Dictionary of headers to include in the email - - @type attachments: list of 3-tuples - @param attachments: Each 3-tuple should consist of the name of the - attachment, the mime-type of the attachment, and a string that is - the attachment itself. - - @type multipartbody: str - @param multipartbody: The type of MIME multi-part body. Generally - either "mixed" (as in text and images) or "alternative" (html email - with a fallback to text/plain). - - @rtype: Deferred - @return: The returned Deferred has its callback or errback invoked when - the mail is successfully sent or when an error occurs, respectively. - """ - warnings.warn("smtp.sendEmail may go away in the future.\n" - " Consider revising your code to use the email module\n" - " and smtp.sendmail.", - category=DeprecationWarning, stacklevel=2) - - f = tempfile.TemporaryFile() - writer = MimeWriter.MimeWriter(f) - - writer.addheader("Mime-Version", "1.0") - if headers: - # Setup the mail headers - for (header, value) in headers.items(): - writer.addheader(header, value) - - headkeys = [k.lower() for k in headers.keys()] - else: - headkeys = () - - # Add required headers if not present - if "message-id" not in headkeys: - writer.addheader("Message-ID", messageid()) - if "date" not in headkeys: - writer.addheader("Date", rfc822date()) - if "from" not in headkeys and "sender" not in headkeys: - writer.addheader("From", fromEmail) - if "to" not in headkeys and "cc" not in headkeys and "bcc" not in headkeys: - writer.addheader("To", toEmail) - - writer.startmultipartbody(multipartbody) - - # message body - part = writer.nextpart() - body = part.startbody("text/plain") - body.write(content) - - if attachments is not None: - # add attachments - for (file, mime, attachment) in attachments: - part = writer.nextpart() - if mime.startswith('text'): - encoding = "7bit" - else: - attachment = base64.encodestring(attachment) - encoding = "base64" - part.addheader("Content-Transfer-Encoding", encoding) - body = part.startbody("%s; name=%s" % (mime, file)) - body.write(attachment) - - # finish - writer.lastpart() - - # send message - f.seek(0, 0) - d = defer.Deferred() - factory = SMTPSenderFactory(fromEmail, toEmail, f, d) - reactor.connectTCP(smtphost, 25, factory) - - return d - -## -## Yerg. Codecs! -## -import codecs -def xtext_encode(s): - r = [] - for ch in s: - o = ord(ch) - if ch == '+' or ch == '=' or o < 33 or o > 126: - r.append('+%02X' % o) - else: - r.append(ch) - return (''.join(r), len(s)) - -try: - from twisted.protocols._c_urlarg import unquote as _helper_unquote -except ImportError: - def xtext_decode(s): - r = [] - i = 0 - while i < len(s): - if s[i] == '+': - try: - r.append(chr(int(s[i + 1:i + 3], 16))) - except ValueError: - r.append(s[i:i + 3]) - i += 3 - else: - r.append(s[i]) - i += 1 - return (''.join(r), len(s)) -else: - def xtext_decode(s): - return (_helper_unquote(s, '+'), len(s)) - -class xtextStreamReader(codecs.StreamReader): - def decode(self, s, errors='strict'): - return xtext_decode(s) - -class xtextStreamWriter(codecs.StreamWriter): - def decode(self, s, errors='strict'): - return xtext_encode(s) - -def xtext_codec(name): - if name == 'xtext': - return (xtext_encode, xtext_decode, xtextStreamReader, xtextStreamWriter) -codecs.register(xtext_codec) diff --git a/tools/buildbot/pylibs/twisted/mail/tap.py b/tools/buildbot/pylibs/twisted/mail/tap.py deleted file mode 100644 index 6ce3ebe..0000000 --- a/tools/buildbot/pylibs/twisted/mail/tap.py +++ /dev/null @@ -1,185 +0,0 @@ -# -*- test-case-name: twisted.mail.test.test_options -*- -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. -# See LICENSE for details. - - -"""I am the support module for creating mail servers with 'mktap' -""" - -import os -import sys - -from twisted.mail import mail -from twisted.mail import maildir -from twisted.mail import relay -from twisted.mail import relaymanager -from twisted.mail import alias - -from twisted.python import usage - -from twisted.cred import checkers -from twisted.application import internet - - -class Options(usage.Options): - synopsis = "Usage: mktap mail [options]" - - optParameters = [ - ["pop3", "p", 8110, "Port to start the POP3 server on (0 to disable).", usage.portCoerce], - ["pop3s", "S", 0, "Port to start the POP3-over-SSL server on (0 to disable).", usage.portCoerce], - ["smtp", "s", 8025, "Port to start the SMTP server on (0 to disable).", usage.portCoerce], - ["certificate", "c", None, "Certificate file to use for SSL connections"], - ["relay", "R", None, - "Relay messages according to their envelope 'To', using the given" - "path as a queue directory."], - ["hostname", "H", None, "The hostname by which to identify this server."], - ] - - optFlags = [ - ["esmtp", "E", "Use RFC 1425/1869 SMTP extensions"], - ["disable-anonymous", None, "Disallow non-authenticated SMTP connections"], - ] - zsh_actions = {"hostname" : "_hosts"} - - longdesc = "This creates a mail.tap file that can be used by twistd." - - def __init__(self): - usage.Options.__init__(self) - self.service = mail.MailService() - self.last_domain = None - - def opt_passwordfile(self, filename): - """Specify a file containing username:password login info for authenticated ESMTP connections.""" - ch = checkers.OnDiskUsernamePasswordDatabase(filename) - self.service.smtpPortal.registerChecker(ch) - opt_P = opt_passwordfile - - def opt_default(self): - """Make the most recently specified domain the default domain.""" - if self.last_domain: - self.service.addDomain('', self.last_domain) - else: - raise usage.UsageError("Specify a domain before specifying using --default") - opt_D = opt_default - - def opt_maildirdbmdomain(self, domain): - """generate an SMTP/POP3 virtual domain which saves to \"path\" - """ - try: - name, path = domain.split('=') - except ValueError: - raise usage.UsageError("Argument to --maildirdbmdomain must be of the form 'name=path'") - - self.last_domain = maildir.MaildirDirdbmDomain(self.service, os.path.abspath(path)) - self.service.addDomain(name, self.last_domain) - opt_d = opt_maildirdbmdomain - - def opt_user(self, user_pass): - """add a user/password to the last specified domains - """ - try: - user, password = user_pass.split('=', 1) - except ValueError: - raise usage.UsageError("Argument to --user must be of the form 'user=password'") - if self.last_domain: - self.last_domain.addUser(user, password) - else: - raise usage.UsageError("Specify a domain before specifying users") - opt_u = opt_user - - def opt_bounce_to_postmaster(self): - """undelivered mails are sent to the postmaster - """ - self.last_domain.postmaster = 1 - opt_b = opt_bounce_to_postmaster - - def opt_aliases(self, filename): - """Specify an aliases(5) file to use for this domain""" - if self.last_domain is not None: - if mail.IAliasableDomain.providedBy(self.last_domain): - aliases = alias.loadAliasFile(self.service.domains, filename) - self.last_domain.setAliasGroup(aliases) - self.service.monitor.monitorFile( - filename, - AliasUpdater(self.service.domains, self.last_domain) - ) - else: - raise usage.UsageError( - "%s does not support alias files" % ( - self.last_domain.__class__.__name__, - ) - ) - else: - raise usage.UsageError("Specify a domain before specifying aliases") - opt_A = opt_aliases - - def postOptions(self): - if self['pop3s']: - if not self['certificate']: - raise usage.UsageError("Cannot specify --pop3s without " - "--certificate") - elif not os.path.exists(self['certificate']): - raise usage.UsageError("Certificate file %r does not exist." - % self['certificate']) - - if not self['disable-anonymous']: - self.service.smtpPortal.registerChecker(checkers.AllowAnonymousAccess()) - - if not (self['pop3'] or self['smtp'] or self['pop3s']): - raise usage.UsageError("You cannot disable all protocols") - -class AliasUpdater: - def __init__(self, domains, domain): - self.domains = domains - self.domain = domain - def __call__(self, new): - self.domain.setAliasGroup(alias.loadAliasFile(self.domains, new)) - -def makeService(config): - if config['esmtp']: - rmType = relaymanager.SmartHostESMTPRelayingManager - smtpFactory = config.service.getESMTPFactory - else: - rmType = relaymanager.SmartHostSMTPRelayingManager - smtpFactory = config.service.getSMTPFactory - - if config['relay']: - dir = config['relay'] - if not os.path.isdir(dir): - os.mkdir(dir) - - config.service.setQueue(relaymanager.Queue(dir)) - default = relay.DomainQueuer(config.service) - - manager = rmType(config.service.queue) - if config['esmtp']: - manager.fArgs += (None, None) - manager.fArgs += (config['hostname'],) - - helper = relaymanager.RelayStateHelper(manager, 1) - helper.setServiceParent(config.service) - config.service.domains.setDefaultDomain(default) - - ctx = None - if config['certificate']: - from twisted.mail.protocols import SSLContextFactory - ctx = SSLContextFactory(config['certificate']) - - if config['pop3']: - s = internet.TCPServer(config['pop3'], config.service.getPOP3Factory()) - s.setServiceParent(config.service) - if config['pop3s']: - s = internet.SSLServer(config['pop3s'], - config.service.getPOP3Factory(), ctx) - s.setServiceParent(config.service) - if config['smtp']: - f = smtpFactory() - f.context = ctx - if config['hostname']: - f.domain = config['hostname'] - f.fArgs = (f.domain,) - if config['esmtp']: - f.fArgs = (None, None) + f.fArgs - s = internet.TCPServer(config['smtp'], f) - s.setServiceParent(config.service) - return config.service diff --git a/tools/buildbot/pylibs/twisted/mail/test/__init__.py b/tools/buildbot/pylibs/twisted/mail/test/__init__.py deleted file mode 100644 index f8ec705..0000000 --- a/tools/buildbot/pylibs/twisted/mail/test/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"Tests for twistd.mail" diff --git a/tools/buildbot/pylibs/twisted/mail/test/pop3testserver.py b/tools/buildbot/pylibs/twisted/mail/test/pop3testserver.py deleted file mode 100644 index 360aae0..0000000 --- a/tools/buildbot/pylibs/twisted/mail/test/pop3testserver.py +++ /dev/null @@ -1,311 +0,0 @@ -#!/usr/bin/python -# -*- test-case-name: twisted.mail.test.test_pop3client -*- - -from twisted.internet.protocol import Factory -from twisted.protocols import basic -from twisted.internet import reactor -import sys, time - -USER = "test" -PASS = "twisted" - -PORT = 1100 - -SSL_SUPPORT = True -UIDL_SUPPORT = True -INVALID_SERVER_RESPONSE = False -INVALID_CAPABILITY_RESPONSE = False -INVALID_LOGIN_RESPONSE = False -DENY_CONNECTION = False -DROP_CONNECTION = False -BAD_TLS_RESPONSE = False -TIMEOUT_RESPONSE = False -TIMEOUT_DEFERRED = False -SLOW_GREETING = False - -"""Commands""" -CONNECTION_MADE = "+OK POP3 localhost v2003.83 server ready" - -CAPABILITIES = [ -"TOP", -"LOGIN-DELAY 180", -"USER", -"SASL LOGIN" -] - -CAPABILITIES_SSL = "STLS" -CAPABILITIES_UIDL = "UIDL" - - -INVALID_RESPONSE = "-ERR Unknown request" -VALID_RESPONSE = "+OK Command Completed" -AUTH_DECLINED = "-ERR LOGIN failed" -AUTH_ACCEPTED = "+OK Mailbox open, 0 messages" -TLS_ERROR = "-ERR server side error start TLS handshake" -LOGOUT_COMPLETE = "+OK quit completed" -NOT_LOGGED_IN = "-ERR Unknown AUHORIZATION state command" -STAT = "+OK 0 0" -UIDL = "+OK Unique-ID listing follows\r\n." -LIST = "+OK Mailbox scan listing follows\r\n." -CAP_START = "+OK Capability list follows:" - - -class POP3TestServer(basic.LineReceiver): - def __init__(self, contextFactory = None): - self.loggedIn = False - self.caps = None - self.tmpUser = None - self.ctx = contextFactory - - def sendSTATResp(self, req): - self.sendLine(STAT) - - def sendUIDLResp(self, req): - self.sendLine(UIDL) - - def sendLISTResp(self, req): - self.sendLine(LIST) - - def sendCapabilities(self): - if self.caps is None: - self.caps = [CAP_START] - - if UIDL_SUPPORT: - self.caps.append(CAPABILITIES_UIDL) - - if SSL_SUPPORT: - self.caps.append(CAPABILITIES_SSL) - - for cap in CAPABILITIES: - self.caps.append(cap) - resp = '\r\n'.join(self.caps) - resp += "\r\n." - - self.sendLine(resp) - - - def connectionMade(self): - if DENY_CONNECTION: - self.disconnect() - return - - if SLOW_GREETING: - reactor.callLater(20, self.sendGreeting) - - else: - self.sendGreeting() - - def sendGreeting(self): - self.sendLine(CONNECTION_MADE) - - def lineReceived(self, line): - """Error Conditions""" - - uline = line.upper() - find = lambda s: uline.find(s) != -1 - - if TIMEOUT_RESPONSE: - # Do not respond to clients request - return - - if DROP_CONNECTION: - self.disconnect() - return - - elif find("CAPA"): - if INVALID_CAPABILITY_RESPONSE: - self.sendLine(INVALID_RESPONSE) - else: - self.sendCapabilities() - - elif find("STLS") and SSL_SUPPORT: - self.startTLS() - - elif find("USER"): - if INVALID_LOGIN_RESPONSE: - self.sendLine(INVALID_RESPONSE) - return - - resp = None - try: - self.tmpUser = line.split(" ")[1] - resp = VALID_RESPONSE - except: - resp = AUTH_DECLINED - - self.sendLine(resp) - - elif find("PASS"): - resp = None - try: - pwd = line.split(" ")[1] - - if self.tmpUser is None or pwd is None: - resp = AUTH_DECLINED - elif self.tmpUser == USER and pwd == PASS: - resp = AUTH_ACCEPTED - self.loggedIn = True - else: - resp = AUTH_DECLINED - except: - resp = AUTH_DECLINED - - self.sendLine(resp) - - elif find("QUIT"): - self.loggedIn = False - self.sendLine(LOGOUT_COMPLETE) - self.disconnect() - - elif INVALID_SERVER_RESPONSE: - self.sendLine(INVALID_RESPONSE) - - elif not self.loggedIn: - self.sendLine(NOT_LOGGED_IN) - - elif find("NOOP"): - self.sendLine(VALID_RESPONSE) - - elif find("STAT"): - if TIMEOUT_DEFERRED: - return - self.sendLine(STAT) - - elif find("LIST"): - if TIMEOUT_DEFERRED: - return - self.sendLine(LIST) - - elif find("UIDL"): - if TIMEOUT_DEFERRED: - return - elif not UIDL_SUPPORT: - self.sendLine(INVALID_RESPONSE) - return - - self.sendLine(UIDL) - - def startTLS(self): - if self.ctx is None: - self.getContext() - - if SSL_SUPPORT and self.ctx is not None: - self.sendLine('+OK Begin TLS negotiation now') - self.transport.startTLS(self.ctx) - else: - self.sendLine('-ERR TLS not available') - - def disconnect(self): - self.transport.loseConnection() - - def getContext(self): - try: - from twisted.internet import ssl - except ImportError: - self.ctx = None - else: - self.ctx = ssl.ClientContextFactory() - self.ctx.method = ssl.SSL.TLSv1_METHOD - - -usage = """popServer.py [arg] (default is Standard POP Server with no messages) -no_ssl - Start with no SSL support -no_uidl - Start with no UIDL support -bad_resp - Send a non-RFC compliant response to the Client -bad_cap_resp - send a non-RFC compliant response when the Client sends a 'CAPABILITY' request -bad_login_resp - send a non-RFC compliant response when the Client sends a 'LOGIN' request -deny - Deny the connection -drop - Drop the connection after sending the greeting -bad_tls - Send a bad response to a STARTTLS -timeout - Do not return a response to a Client request -to_deferred - Do not return a response on a 'Select' request. This - will test Deferred callback handling -slow - Wait 20 seconds after the connection is made to return a Server Greeting -""" - -def printMessage(msg): - print "Server Starting in %s mode" % msg - -def processArg(arg): - - if arg.lower() == 'no_ssl': - global SSL_SUPPORT - SSL_SUPPORT = False - printMessage("NON-SSL") - - elif arg.lower() == 'no_uidl': - global UIDL_SUPPORT - UIDL_SUPPORT = False - printMessage("NON-UIDL") - - elif arg.lower() == 'bad_resp': - global INVALID_SERVER_RESPONSE - INVALID_SERVER_RESPONSE = True - printMessage("Invalid Server Response") - - elif arg.lower() == 'bad_cap_resp': - global INVALID_CAPABILITY_RESPONSE - INVALID_CAPABILITY_RESPONSE = True - printMessage("Invalid Capability Response") - - elif arg.lower() == 'bad_login_resp': - global INVALID_LOGIN_RESPONSE - INVALID_LOGIN_RESPONSE = True - printMessage("Invalid Capability Response") - - elif arg.lower() == 'deny': - global DENY_CONNECTION - DENY_CONNECTION = True - printMessage("Deny Connection") - - elif arg.lower() == 'drop': - global DROP_CONNECTION - DROP_CONNECTION = True - printMessage("Drop Connection") - - - elif arg.lower() == 'bad_tls': - global BAD_TLS_RESPONSE - BAD_TLS_RESPONSE = True - printMessage("Bad TLS Response") - - elif arg.lower() == 'timeout': - global TIMEOUT_RESPONSE - TIMEOUT_RESPONSE = True - printMessage("Timeout Response") - - elif arg.lower() == 'to_deferred': - global TIMEOUT_DEFERRED - TIMEOUT_DEFERRED = True - printMessage("Timeout Deferred Response") - - elif arg.lower() == 'slow': - global SLOW_GREETING - SLOW_GREETING = True - printMessage("Slow Greeting") - - elif arg.lower() == '--help': - print usage - sys.exit() - - else: - print usage - sys.exit() - -def main(): - - if len(sys.argv) < 2: - printMessage("POP3 with no messages") - else: - args = sys.argv[1:] - - for arg in args: - processArg(arg) - - f = Factory() - f.protocol = POP3TestServer - reactor.listenTCP(PORT, f) - reactor.run() - -if __name__ == '__main__': - main() diff --git a/tools/buildbot/pylibs/twisted/mail/test/rfc822.message b/tools/buildbot/pylibs/twisted/mail/test/rfc822.message deleted file mode 100644 index ee97ab9..0000000 --- a/tools/buildbot/pylibs/twisted/mail/test/rfc822.message +++ /dev/null @@ -1,86 +0,0 @@ -Return-Path: <twisted-commits-admin@twistedmatrix.com> -Delivered-To: exarkun@meson.dyndns.org -Received: from localhost [127.0.0.1] - by localhost with POP3 (fetchmail-6.2.1) - for exarkun@localhost (single-drop); Thu, 20 Mar 2003 14:50:20 -0500 (EST) -Received: from pyramid.twistedmatrix.com (adsl-64-123-27-105.dsl.austtx.swbell.net [64.123.27.105]) - by intarweb.us (Postfix) with ESMTP id 4A4A513EA4 - for <exarkun@meson.dyndns.org>; Thu, 20 Mar 2003 14:49:27 -0500 (EST) -Received: from localhost ([127.0.0.1] helo=pyramid.twistedmatrix.com) - by pyramid.twistedmatrix.com with esmtp (Exim 3.35 #1 (Debian)) - id 18w648-0007Vl-00; Thu, 20 Mar 2003 13:51:04 -0600 -Received: from acapnotic by pyramid.twistedmatrix.com with local (Exim 3.35 #1 (Debian)) - id 18w63j-0007VK-00 - for <twisted-commits@twistedmatrix.com>; Thu, 20 Mar 2003 13:50:39 -0600 -To: twisted-commits@twistedmatrix.com -From: etrepum CVS <etrepum@twistedmatrix.com> -Reply-To: twisted-python@twistedmatrix.com -X-Mailer: CVSToys -Message-Id: <E18w63j-0007VK-00@pyramid.twistedmatrix.com> -Subject: [Twisted-commits] rebuild now works on python versions from 2.2.0 and up. -Sender: twisted-commits-admin@twistedmatrix.com -Errors-To: twisted-commits-admin@twistedmatrix.com -X-BeenThere: twisted-commits@twistedmatrix.com -X-Mailman-Version: 2.0.11 -Precedence: bulk -List-Help: <mailto:twisted-commits-request@twistedmatrix.com?subject=help> -List-Post: <mailto:twisted-commits@twistedmatrix.com> -List-Subscribe: <http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits>, - <mailto:twisted-commits-request@twistedmatrix.com?subject=subscribe> -List-Id: <twisted-commits.twistedmatrix.com> -List-Unsubscribe: <http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits>, - <mailto:twisted-commits-request@twistedmatrix.com?subject=unsubscribe> -List-Archive: <http://twistedmatrix.com/pipermail/twisted-commits/> -Date: Thu, 20 Mar 2003 13:50:39 -0600 - -Modified files: -Twisted/twisted/python/rebuild.py 1.19 1.20 - -Log message: -rebuild now works on python versions from 2.2.0 and up. - - -ViewCVS links: -http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/twisted/python/rebuild.py.diff?r1=text&tr1=1.19&r2=text&tr2=1.20&cvsroot=Twisted - -Index: Twisted/twisted/python/rebuild.py -diff -u Twisted/twisted/python/rebuild.py:1.19 Twisted/twisted/python/rebuild.py:1.20 ---- Twisted/twisted/python/rebuild.py:1.19 Fri Jan 17 13:50:49 2003 -+++ Twisted/twisted/python/rebuild.py Thu Mar 20 11:50:08 2003 -@@ -206,15 +206,27 @@ - clazz.__dict__.clear() - clazz.__getattr__ = __getattr__ - clazz.__module__ = module.__name__ -+ if newclasses: -+ import gc -+ if (2, 2, 0) <= sys.version_info[:3] < (2, 2, 2): -+ hasBrokenRebuild = 1 -+ gc_objects = gc.get_objects() -+ else: -+ hasBrokenRebuild = 0 - for nclass in newclasses: - ga = getattr(module, nclass.__name__) - if ga is nclass: - log.msg("WARNING: new-class %s not replaced by reload!" % reflect.qual(nclass)) - else: -- import gc -- for r in gc.get_referrers(nclass): -- if isinstance(r, nclass): -+ if hasBrokenRebuild: -+ for r in gc_objects: -+ if not getattr(r, '__class__', None) is nclass: -+ continue - r.__class__ = ga -+ else: -+ for r in gc.get_referrers(nclass): -+ if getattr(r, '__class__', None) is nclass: -+ r.__class__ = ga - if doLog: - log.msg('') - log.msg(' (fixing %s): ' % str(module.__name__)) - - -_______________________________________________ -Twisted-commits mailing list -Twisted-commits@twistedmatrix.com -http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits diff --git a/tools/buildbot/pylibs/twisted/mail/test/test_bounce.py b/tools/buildbot/pylibs/twisted/mail/test/test_bounce.py deleted file mode 100644 index ece9281..0000000 --- a/tools/buildbot/pylibs/twisted/mail/test/test_bounce.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. -# See LICENSE for details. - - -"""Test cases for bounce message generation -""" - -from twisted.trial import unittest -from twisted.mail import bounce -import rfc822, cStringIO - -class BounceTestCase(unittest.TestCase): - """ - testcases for bounce message generation - """ - - def testBounceFormat(self): - from_, to, s = bounce.generateBounce(cStringIO.StringIO('''\ -From: Moshe Zadka <moshez@example.com> -To: nonexistant@example.org -Subject: test - -'''), 'moshez@example.com', 'nonexistant@example.org') - self.assertEquals(from_, '') - self.assertEquals(to, 'moshez@example.com') - mess = rfc822.Message(cStringIO.StringIO(s)) - self.assertEquals(mess['To'], 'moshez@example.com') - self.assertEquals(mess['From'], 'postmaster@example.org') - self.assertEquals(mess['subject'], 'Returned Mail: see transcript for details') - - def testBounceMIME(self): - pass diff --git a/tools/buildbot/pylibs/twisted/mail/test/test_imap.py b/tools/buildbot/pylibs/twisted/mail/test/test_imap.py deleted file mode 100644 index 6d11e1e..0000000 --- a/tools/buildbot/pylibs/twisted/mail/test/test_imap.py +++ /dev/null @@ -1,3040 +0,0 @@ -# -*- test-case-name: twisted.mail.test.test_imap -*- -# Copyright (c) 2001-2008 Twisted Matrix Laboratories. -# See LICENSE for details. - - -""" -Test case for twisted.mail.imap4 -""" - -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO - -import os -import types - -from zope.interface import implements - -from twisted.mail.imap4 import MessageSet -from twisted.mail import imap4 -from twisted.protocols import loopback -from twisted.internet import defer -from twisted.internet import error -from twisted.internet import reactor -from twisted.internet import interfaces -from twisted.internet.task import Clock -from twisted.trial import unittest -from twisted.python import util -from twisted.python import failure - -from twisted import cred -import twisted.cred.error -import twisted.cred.checkers -import twisted.cred.credentials -import twisted.cred.portal - -from twisted.test.proto_helpers import StringTransport, StringTransportWithDisconnection - -try: - from twisted.test.ssl_helpers import ClientTLSContext, ServerTLSContext -except ImportError: - ClientTLSContext = ServerTLSContext = None - -def strip(f): - return lambda result, f=f: f() - -def sortNest(l): - l = l[:] - l.sort() - for i in range(len(l)): - if isinstance(l[i], types.ListType): - l[i] = sortNest(l[i]) - elif isinstance(l[i], types.TupleType): - l[i] = tuple(sortNest(list(l[i]))) - return l - -class IMAP4UTF7TestCase(unittest.TestCase): - tests = [ - [u'Hello world', 'Hello world'], - [u'Hello & world', 'Hello &- world'], - [u'Hello\xffworld', 'Hello&AP8-world'], - [u'\xff\xfe\xfd\xfc', '&AP8A,gD9APw-'], - [u'~peter/mail/\u65e5\u672c\u8a9e/\u53f0\u5317', - '~peter/mail/&ZeVnLIqe-/&U,BTFw-'], # example from RFC 2060 - ] - - def test_encodeWithErrors(self): - """ - Specifying an error policy to C{unicode.encode} with the - I{imap4-utf-7} codec should produce the same result as not - specifying the error policy. - """ - text = u'Hello world' - self.assertEqual( - text.encode('imap4-utf-7', 'strict'), - text.encode('imap4-utf-7')) - - - def test_decodeWithErrors(self): - """ - Similar to L{test_encodeWithErrors}, but for C{str.decode}. - """ - bytes = 'Hello world' - self.assertEqual( - bytes.decode('imap4-utf-7', 'strict'), - bytes.decode('imap4-utf-7')) - - - def testEncode(self): - for (input, output) in self.tests: - self.assertEquals(input.encode('imap4-utf-7'), output) - - def testDecode(self): - for (input, output) in self.tests: - # XXX - Piece of *crap* 2.1 - self.assertEquals(input, imap4.decoder(output)[0]) - - def testPrintableSingletons(self): - # All printables represent themselves - for o in range(0x20, 0x26) + range(0x27, 0x7f): - self.failUnlessEqual(chr(o), chr(o).encode('imap4-utf-7')) - self.failUnlessEqual(chr(o), chr(o).decode('imap4-utf-7')) - self.failUnlessEqual('&'.encode('imap4-utf-7'), '&-') - self.failUnlessEqual('&-'.decode('imap4-utf-7'), '&') - -class BufferingConsumer: - def __init__(self): - self.buffer = [] - - def write(self, bytes): - self.buffer.append(bytes) - if self.consumer: - self.consumer.resumeProducing() - - def registerProducer(self, consumer, streaming): - self.consumer = consumer - self.consumer.resumeProducing() - - def unregisterProducer(self): - self.consumer = None - -class MessageProducerTestCase(unittest.TestCase): - def testSinglePart(self): - body = 'This is body text. Rar.' - headers = util.OrderedDict() - headers['from'] = 'sender@host' - headers['to'] = 'recipient@domain' - headers['subject'] = 'booga booga boo' - headers['content-type'] = 'text/plain' - - msg = FakeyMessage(headers, (), None, body, 123, None ) - - c = BufferingConsumer() - p = imap4.MessageProducer(msg) - d = p.beginProducing(c) - - def cbProduced(result): - self.assertIdentical(result, p) - self.assertEquals( - ''.join(c.buffer), - - '{119}\r\n' - 'From: sender@host\r\n' - 'To: recipient@domain\r\n' - 'Subject: booga booga boo\r\n' - 'Content-Type: text/plain\r\n' - '\r\n' - + body) - return d.addCallback(cbProduced) - - - def testSingleMultiPart(self): - outerBody = '' - innerBody = 'Contained body message text. Squarge.' - headers = util.OrderedDict() - headers['from'] = 'sender@host' - headers['to'] = 'recipient@domain' - headers['subject'] = 'booga booga boo' - headers['content-type'] = 'multipart/alternative; boundary="xyz"' - - innerHeaders = util.OrderedDict() - innerHeaders['subject'] = 'this is subject text' - innerHeaders['content-type'] = 'text/plain' - msg = FakeyMessage(headers, (), None, outerBody, 123, - [FakeyMessage(innerHeaders, (), None, innerBody, - None, None)], - ) - - c = BufferingConsumer() - p = imap4.MessageProducer(msg) - d = p.beginProducing(c) - - def cbProduced(result): - self.failUnlessIdentical(result, p) - - self.assertEquals( - ''.join(c.buffer), - - '{239}\r\n' - 'From: sender@host\r\n' - 'To: recipient@domain\r\n' - 'Subject: booga booga boo\r\n' - 'Content-Type: multipart/alternative; boundary="xyz"\r\n' - '\r\n' - '\r\n' - '--xyz\r\n' - 'Subject: this is subject text\r\n' - 'Content-Type: text/plain\r\n' - '\r\n' - + innerBody - + '\r\n--xyz--\r\n') - - return d.addCallback(cbProduced) - - - def testMultipleMultiPart(self): - outerBody = '' - innerBody1 = 'Contained body message text. Squarge.' - innerBody2 = 'Secondary <i>message</i> text of squarge body.' - headers = util.OrderedDict() - headers['from'] = 'sender@host' - headers['to'] = 'recipient@domain' - headers['subject'] = 'booga booga boo' - headers['content-type'] = 'multipart/alternative; boundary="xyz"' - innerHeaders = util.OrderedDict() - innerHeaders['subject'] = 'this is subject text' - innerHeaders['content-type'] = 'text/plain' - innerHeaders2 = util.OrderedDict() - innerHeaders2['subject'] = '<b>this is subject</b>' - innerHeaders2['content-type'] = 'text/html' - msg = FakeyMessage(headers, (), None, outerBody, 123, [ - FakeyMessage(innerHeaders, (), None, innerBody1, None, None), - FakeyMessage(innerHeaders2, (), None, innerBody2, None, None) - ], - ) - - c = BufferingConsumer() - p = imap4.MessageProducer(msg) - d = p.beginProducing(c) - - def cbProduced(result): - self.failUnlessIdentical(result, p) - - self.assertEquals( - ''.join(c.buffer), - - '{354}\r\n' - 'From: sender@host\r\n' - 'To: recipient@domain\r\n' - 'Subject: booga booga boo\r\n' - 'Content-Type: multipart/alternative; boundary="xyz"\r\n' - '\r\n' - '\r\n' - '--xyz\r\n' - 'Subject: this is subject text\r\n' - 'Content-Type: text/plain\r\n' - '\r\n' - + innerBody1 - + '\r\n--xyz\r\n' - 'Subject: <b>this is subject</b>\r\n' - 'Content-Type: text/html\r\n' - '\r\n' - + innerBody2 - + '\r\n--xyz--\r\n') - return d.addCallback(cbProduced) - - -class IMAP4HelperTestCase(unittest.TestCase): - def testFileProducer(self): - b = (('x' * 1) + ('y' * 1) + ('z' * 1)) * 10 - c = BufferingConsumer() - f = StringIO(b) - p = imap4.FileProducer(f) - d = p.beginProducing(c) - - def cbProduced(result): - self.failUnlessIdentical(result, p) - self.assertEquals( - ('{%d}\r\n' % len(b))+ b, - ''.join(c.buffer)) - return d.addCallback(cbProduced) - - def testWildcard(self): - cases = [ - ['foo/%gum/bar', - ['foo/bar', 'oo/lalagum/bar', 'foo/gumx/bar', 'foo/gum/baz'], - ['foo/xgum/bar', 'foo/gum/bar'], - ], ['foo/x%x/bar', - ['foo', 'bar', 'fuz fuz fuz', 'foo/*/bar', 'foo/xyz/bar', 'foo/xx/baz'], - ['foo/xyx/bar', 'foo/xx/bar', 'foo/xxxxxxxxxxxxxx/bar'], - ], ['foo/xyz*abc/bar', - ['foo/xyz/bar', 'foo/abc/bar', 'foo/xyzab/cbar', 'foo/xyza/bcbar'], - ['foo/xyzabc/bar', 'foo/xyz/abc/bar', 'foo/xyz/123/abc/bar'], - ] - ] - - for (wildcard, fail, succeed) in cases: - wildcard = imap4.wildcardToRegexp(wildcard, '/') - for x in fail: - self.failIf(wildcard.match(x)) - for x in succeed: - self.failUnless(wildcard.match(x)) - - def testWildcardNoDelim(self): - cases = [ - ['foo/%gum/bar', - ['foo/bar', 'oo/lalagum/bar', 'foo/gumx/bar', 'foo/gum/baz'], - ['foo/xgum/bar', 'foo/gum/bar', 'foo/x/gum/bar'], - ], ['foo/x%x/bar', - ['foo', 'bar', 'fuz fuz fuz', 'foo/*/bar', 'foo/xyz/bar', 'foo/xx/baz'], - ['foo/xyx/bar', 'foo/xx/bar', 'foo/xxxxxxxxxxxxxx/bar', 'foo/x/x/bar'], - ], ['foo/xyz*abc/bar', - ['foo/xyz/bar', 'foo/abc/bar', 'foo/xyzab/cbar', 'foo/xyza/bcbar'], - ['foo/xyzabc/bar', 'foo/xyz/abc/bar', 'foo/xyz/123/abc/bar'], - ] - ] - - for (wildcard, fail, succeed) in cases: - wildcard = imap4.wildcardToRegexp(wildcard, None) - for x in fail: - self.failIf(wildcard.match(x), x) - for x in succeed: - self.failUnless(wildcard.match(x), x) - - def testHeaderFormatter(self): - cases = [ - ({'Header1': 'Value1', 'Header2': 'Value2'}, 'Header2: Value2\r\nHeader1: Value1\r\n'), - ] - - for (input, output) in cases: - self.assertEquals(imap4._formatHeaders(input), output) - - def testMessageSet(self): - m1 = MessageSet() - m2 = MessageSet() - - self.assertEquals(m1, m2) - - m1 = m1 + (1, 3) - self.assertEquals(len(m1), 3) - self.assertEquals(list(m1), [1, 2, 3]) - - m2 = m2 + (1, 3) - self.assertEquals(m1, m2) - self.assertEquals(list(m1 + m2), [1, 2, 3]) - - def testQuotedSplitter(self): - cases = [ - '''Hello World''', - '''Hello "World!"''', - '''World "Hello" "How are you?"''', - '''"Hello world" How "are you?"''', - '''foo bar "baz buz" NIL''', - '''foo bar "baz buz" "NIL"''', - '''foo NIL "baz buz" bar''', - '''foo "NIL" "baz buz" bar''', - '''"NIL" bar "baz buz" foo''', - ] - - answers = [ - ['Hello', 'World'], - ['Hello', 'World!'], - ['World', 'Hello', 'How are you?'], - ['Hello world', 'How', 'are you?'], - ['foo', 'bar', 'baz buz', None], - ['foo', 'bar', 'baz buz', 'NIL'], - ['foo', None, 'baz buz', 'bar'], - ['foo', 'NIL', 'baz buz', 'bar'], - ['NIL', 'bar', 'baz buz', 'foo'], - ] - - errors = [ - '"mismatched quote', - 'mismatched quote"', - 'mismatched"quote', - '"oops here is" another"', - ] - - for s in errors: - self.assertRaises(imap4.MismatchedQuoting, imap4.splitQuoted, s) - - for (case, expected) in zip(cases, answers): - self.assertEquals(imap4.splitQuoted(case), expected) - - - def testStringCollapser(self): - cases = [ - ['a', 'b', 'c', 'd', 'e'], - ['a', ' ', '"', 'b', 'c', ' ', '"', ' ', 'd', 'e'], - [['a', 'b', 'c'], 'd', 'e'], - ['a', ['b', 'c', 'd'], 'e'], - ['a', 'b', ['c', 'd', 'e']], - ['"', 'a', ' ', '"', ['b', 'c', 'd'], '"', ' ', 'e', '"'], - ['a', ['"', ' ', 'b', 'c', ' ', ' ', '"'], 'd', 'e'], - ] - - answers = [ - ['abcde'], - ['a', 'bc ', 'de'], - [['abc'], 'de'], - ['a', ['bcd'], 'e'], - ['ab', ['cde']], - ['a ', ['bcd'], ' e'], - ['a', [' bc '], 'de'], - ] - - for (case, expected) in zip(cases, answers): - self.assertEquals(imap4.collapseStrings(case), expected) - - def testParenParser(self): - s = '\r\n'.join(['xx'] * 4) - cases = [ - '(BODY.PEEK[HEADER.FIELDS.NOT (subject bcc cc)] {%d}\r\n%s)' % (len(s), s,), - -# '(FLAGS (\Seen) INTERNALDATE "17-Jul-1996 02:44:25 -0700" ' -# 'RFC822.SIZE 4286 ENVELOPE ("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)" ' -# '"IMAP4rev1 WG mtg summary and minutes" ' -# '(("Terry Gray" NIL "gray" "cac.washington.edu")) ' -# '(("Terry Gray" NIL "gray" "cac.washington.edu")) ' -# '(("Terry Gray" NIL "gray" "cac.washington.edu")) ' -# '((NIL NIL "imap" "cac.washington.edu")) ' -# '((NIL NIL "minutes" "CNRI.Reston.VA.US") ' -# '("John Klensin" NIL "KLENSIN" "INFOODS.MIT.EDU")) NIL NIL ' -# '"<B27397-0100000@cac.washington.edu>") ' -# 'BODY ("TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 3028 92))', - - '(FLAGS (\Seen) INTERNALDATE "17-Jul-1996 02:44:25 -0700" ' - 'RFC822.SIZE 4286 ENVELOPE ("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)" ' - '"IMAP4rev1 WG mtg summary and minutes" ' - '(("Terry Gray" NIL gray cac.washington.edu)) ' - '(("Terry Gray" NIL gray cac.washington.edu)) ' - '(("Terry Gray" NIL gray cac.washington.edu)) ' - '((NIL NIL imap cac.washington.edu)) ' - '((NIL NIL minutes CNRI.Reston.VA.US) ' - '("John Klensin" NIL KLENSIN INFOODS.MIT.EDU)) NIL NIL ' - '<B27397-0100000@cac.washington.edu>) ' - 'BODY (TEXT PLAIN (CHARSET US-ASCII) NIL NIL 7BIT 3028 92))', - ] - - answers = [ - ['BODY.PEEK', ['HEADER.FIELDS.NOT', ['subject', 'bcc', 'cc']], s], - - ['FLAGS', [r'\Seen'], 'INTERNALDATE', - '17-Jul-1996 02:44:25 -0700', 'RFC822.SIZE', '4286', 'ENVELOPE', - ['Wed, 17 Jul 1996 02:23:25 -0700 (PDT)', - 'IMAP4rev1 WG mtg summary and minutes', [["Terry Gray", None, - "gray", "cac.washington.edu"]], [["Terry Gray", None, - "gray", "cac.washington.edu"]], [["Terry Gray", None, - "gray", "cac.washington.edu"]], [[None, None, "imap", - "cac.washington.edu"]], [[None, None, "minutes", - "CNRI.Reston.VA.US"], ["John Klensin", None, "KLENSIN", - "INFOODS.MIT.EDU"]], None, None, - "<B27397-0100000@cac.washington.edu>"], "BODY", ["TEXT", "PLAIN", - ["CHARSET", "US-ASCII"], None, None, "7BIT", "3028", "92"]], - ] - - for (case, expected) in zip(cases, answers): - self.assertEquals(imap4.parseNestedParens(case), [expected]) - - # XXX This code used to work, but changes occurred within the - # imap4.py module which made it no longer necessary for *all* of it - # to work. In particular, only the part that makes - # 'BODY.PEEK[HEADER.FIELDS.NOT (Subject Bcc Cc)]' come out correctly - # no longer needs to work. So, I am loathe to delete the entire - # section of the test. --exarkun - # - -# for (case, expected) in zip(answers, cases): -# self.assertEquals('(' + imap4.collapseNestedLists(case) + ')', expected) - - def testFetchParserSimple(self): - cases = [ - ['ENVELOPE', 'Envelope'], - ['FLAGS', 'Flags'], - ['INTERNALDATE', 'InternalDate'], - ['RFC822.HEADER', 'RFC822Header'], - ['RFC822.SIZE', 'RFC822Size'], - ['RFC822.TEXT', 'RFC822Text'], - ['RFC822', 'RFC822'], - ['UID', 'UID'], - ['BODYSTRUCTURE', 'BodyStructure'], - ] - - for (inp, outp) in cases: - p = imap4._FetchParser() - p.parseString(inp) - self.assertEquals(len(p.result), 1) - self.failUnless(isinstance(p.result[0], getattr(p, outp))) - - def testFetchParserMacros(self): - cases = [ - ['ALL', (4, ['flags', 'internaldate', 'rfc822.size', 'envelope'])], - ['FULL', (5, ['flags', 'internaldate', 'rfc822.size', 'envelope', 'body'])], - ['FAST', (3, ['flags', 'internaldate', 'rfc822.size'])], - ] - - for (inp, outp) in cases: - p = imap4._FetchParser() - p.parseString(inp) - self.assertEquals(len(p.result), outp[0]) - p = [str(p).lower() for p in p.result] - p.sort() - outp[1].sort() - self.assertEquals(p, outp[1]) - - def testFetchParserBody(self): - P = imap4._FetchParser - - p = P() - p.parseString('BODY') - self.assertEquals(len(p.result), 1) - self.failUnless(isinstance(p.result[0], p.Body)) - self.assertEquals(p.result[0].peek, False) - self.assertEquals(p.result[0].header, None) - self.assertEquals(str(p.result[0]), 'BODY') - - p = P() - p.parseString('BODY.PEEK') - self.assertEquals(len(p.result), 1) - self.failUnless(isinstance(p.result[0], p.Body)) - self.assertEquals(p.result[0].peek, True) - self.assertEquals(str(p.result[0]), 'BODY') - - p = P() - p.parseString('BODY[]') - self.assertEquals(len(p.result), 1) - self.failUnless(isinstance(p.result[0], p.Body)) - self.assertEquals(p.result[0].empty, True) - self.assertEquals(str(p.result[0]), 'BODY[]') - - p = P() - p.parseString('BODY[HEADER]') - self.assertEquals(len(p.result), 1) - self.failUnless(isinstance(p.result[0], p.Body)) - self.assertEquals(p.result[0].peek, False) - self.failUnless(isinstance(p.result[0].header, p.Header)) - self.assertEquals(p.result[0].header.negate, True) - self.assertEquals(p.result[0].header.fields, ()) - self.assertEquals(p.result[0].empty, False) - self.assertEquals(str(p.result[0]), 'BODY[HEADER]') - - p = P() - p.parseString('BODY.PEEK[HEADER]') - self.assertEquals(len(p.result), 1) - self.failUnless(isinstance(p.result[0], p.Body)) - self.assertEquals(p.result[0].peek, True) - self.failUnless(isinstance(p.result[0].header, p.Header)) - self.assertEquals(p.result[0].header.negate, True) - self.assertEquals(p.result[0].header.fields, ()) - self.assertEquals(p.result[0].empty, False) - self.assertEquals(str(p.result[0]), 'BODY[HEADER]') - - p = P() - p.parseString('BODY[HEADER.FIELDS (Subject Cc Message-Id)]') - self.assertEquals(len(p.result), 1) - self.failUnless(isinstance(p.result[0], p.Body)) - self.assertEquals(p.result[0].peek, False) - self.failUnless(isinstance(p.result[0].header, p.Header)) - self.assertEquals(p.result[0].header.negate, False) - self.assertEquals(p.result[0].header.fields, ['SUBJECT', 'CC', 'MESSAGE-ID']) - self.assertEquals(p.result[0].empty, False) - self.assertEquals(str(p.result[0]), 'BODY[HEADER.FIELDS (Subject Cc Message-Id)]') - - p = P() - p.parseString('BODY.PEEK[HEADER.FIELDS (Subject Cc Message-Id)]') - self.assertEquals(len(p.result), 1) - self.failUnless(isinstance(p.result[0], p.Body)) - self.assertEquals(p.result[0].peek, True) - self.failUnless(isinstance(p.result[0].header, p.Header)) - self.assertEquals(p.result[0].header.negate, False) - self.assertEquals(p.result[0].header.fields, ['SUBJECT', 'CC', 'MESSAGE-ID']) - self.assertEquals(p.result[0].empty, False) - self.assertEquals(str(p.result[0]), 'BODY[HEADER.FIELDS (Subject Cc Message-Id)]') - - p = P() - p.parseString('BODY.PEEK[HEADER.FIELDS.NOT (Subject Cc Message-Id)]') - self.assertEquals(len(p.result), 1) - self.failUnless(isinstance(p.result[0], p.Body)) - self.assertEquals(p.result[0].peek, True) - self.failUnless(isinstance(p.result[0].header, p.Header)) - self.assertEquals(p.result[0].header.negate, True) - self.assertEquals(p.result[0].header.fields, ['SUBJECT', 'CC', 'MESSAGE-ID']) - self.assertEquals(p.result[0].empty, False) - self.assertEquals(str(p.result[0]), 'BODY[HEADER.FIELDS.NOT (Subject Cc Message-Id)]') - - p = P() - p.parseString('BODY[1.MIME]<10.50>') - self.assertEquals(len(p.result), 1) - self.failUnless(isinstance(p.result[0], p.Body)) - self.assertEquals(p.result[0].peek, False) - self.failUnless(isinstance(p.result[0].mime, p.MIME)) - self.assertEquals(p.result[0].part, (0,)) - self.assertEquals(p.result[0].partialBegin, 10) - self.assertEquals(p.result[0].partialLength, 50) - self.assertEquals(p.result[0].empty, False) - self.assertEquals(str(p.result[0]), 'BODY[1.MIME]<10.50>') - - p = P() - p.parseString('BODY.PEEK[1.3.9.11.HEADER.FIELDS.NOT (Message-Id Date)]<103.69>') - self.assertEquals(len(p.result), 1) - self.failUnless(isinstance(p.result[0], p.Body)) - self.assertEquals(p.result[0].peek, True) - self.failUnless(isinstance(p.result[0].header, p.Header)) - self.assertEquals(p.result[0].part, (0, 2, 8, 10)) - self.assertEquals(p.result[0].header.fields, ['MESSAGE-ID', 'DATE']) - self.assertEquals(p.result[0].partialBegin, 103) - self.assertEquals(p.result[0].partialLength, 69) - self.assertEquals(p.result[0].empty, False) - self.assertEquals(str(p.result[0]), 'BODY[1.3.9.11.HEADER.FIELDS.NOT (Message-Id Date)]<103.69>') - - - def testFiles(self): - inputStructure = [ - 'foo', 'bar', 'baz', StringIO('this is a file\r\n'), 'buz' - ] - - output = '"foo" "bar" "baz" {16}\r\nthis is a file\r\n "buz"' - - self.assertEquals(imap4.collapseNestedLists(inputStructure), output) - - def testQuoteAvoider(self): - input = [ - 'foo', imap4.DontQuoteMe('bar'), "baz", StringIO('this is a file\r\n'), - imap4.DontQuoteMe('buz'), "" - ] - - output = '"foo" bar "baz" {16}\r\nthis is a file\r\n buz ""' - - self.assertEquals(imap4.collapseNestedLists(input), output) - - def testLiterals(self): - cases = [ - ('({10}\r\n0123456789)', [['0123456789']]), - ] - - for (case, expected) in cases: - self.assertEquals(imap4.parseNestedParens(case), expected) - - def testQueryBuilder(self): - inputs = [ - imap4.Query(flagged=1), - imap4.Query(sorted=1, unflagged=1, deleted=1), - imap4.Or(imap4.Query(flagged=1), imap4.Query(deleted=1)), - imap4.Query(before='today'), - imap4.Or( - imap4.Query(deleted=1), - imap4.Query(unseen=1), - imap4.Query(new=1) - ), - imap4.Or( - imap4.Not( - imap4.Or( - imap4.Query(sorted=1, since='yesterday', smaller=1000), - imap4.Query(sorted=1, before='tuesday', larger=10000), - imap4.Query(sorted=1, unseen=1, deleted=1, before='today'), - imap4.Not( - imap4.Query(subject='spam') - ), - ), - ), - imap4.Not( - imap4.Query(uid='1:5') - ), - ) - ] - - outputs = [ - 'FLAGGED', - '(DELETED UNFLAGGED)', - '(OR FLAGGED DELETED)', - '(BEFORE "today")', - '(OR DELETED (OR UNSEEN NEW))', - '(OR (NOT (OR (SINCE "yesterday" SMALLER 1000) ' # Continuing - '(OR (BEFORE "tuesday" LARGER 10000) (OR (BEFORE ' # Some more - '"today" DELETED UNSEEN) (NOT (SUBJECT "spam")))))) ' # And more - '(NOT (UID 1:5)))', - ] - - for (query, expected) in zip(inputs, outputs): - self.assertEquals(query, expected) - - def testIdListParser(self): - inputs = [ - '1:*', - '5:*', - '1:2,5:*', - '1', - '1,2', - '1,3,5', - '1:10', - '1:10,11', - '1:5,10:20', - '1,5:10', - '1,5:10,15:20', - '1:10,15,20:25', - ] - - outputs = [ - MessageSet(1, None), - MessageSet(5, None), - MessageSet(5, None) + MessageSet(1, 2), - MessageSet(1), - MessageSet(1, 2), - MessageSet(1) + MessageSet(3) + MessageSet(5), - MessageSet(1, 10), - MessageSet(1, 11), - MessageSet(1, 5) + MessageSet(10, 20), - MessageSet(1) + MessageSet(5, 10), - MessageSet(1) + MessageSet(5, 10) + MessageSet(15, 20), - MessageSet(1, 10) + MessageSet(15) + MessageSet(20, 25), - ] - - lengths = [ - None, None, None, - 1, 2, 3, 10, 11, 16, 7, 13, 17, - ] - - for (input, expected) in zip(inputs, outputs): - self.assertEquals(imap4.parseIdList(input), expected) - - for (input, expected) in zip(inputs, lengths): - try: - L = len(imap4.parseIdList(input)) - except TypeError: - L = None - self.assertEquals(L, expected, - "len(%r) = %r != %r" % (input, L, expected)) - -class SimpleMailbox: - implements(imap4.IMailboxInfo, imap4.IMailbox, imap4.ICloseableMailbox) - - flags = ('\\Flag1', 'Flag2', '\\AnotherSysFlag', 'LastFlag') - messages = [] - mUID = 0 - rw = 1 - closed = False - - def __init__(self): - self.listeners = [] - self.addListener = self.listeners.append - self.removeListener = self.listeners.remove - - def getFlags(self): - return self.flags - - def getUIDValidity(self): - return 42 - - def getUIDNext(self): - return len(self.messages) + 1 - - def getMessageCount(self): - return 9 - - def getRecentCount(self): - return 3 - - def getUnseenCount(self): - return 4 - - def isWriteable(self): - return self.rw - - def destroy(self): - pass - - def getHierarchicalDelimiter(self): - return '/' - - def requestStatus(self, names): - r = {} - if 'MESSAGES' in names: - r['MESSAGES'] = self.getMessageCount() - if 'RECENT' in names: - r['RECENT'] = self.getRecentCount() - if 'UIDNEXT' in names: - r['UIDNEXT'] = self.getMessageCount() + 1 - if 'UIDVALIDITY' in names: - r['UIDVALIDITY'] = self.getUID() - if 'UNSEEN' in names: - r['UNSEEN'] = self.getUnseenCount() - return defer.succeed(r) - - def addMessage(self, message, flags, date = None): - self.messages.append((message, flags, date, self.mUID)) - self.mUID += 1 - return defer.succeed(None) - - def expunge(self): - delete = [] - for i in self.messages: - if '\\Deleted' in i[1]: - delete.append(i) - for i in delete: - self.messages.remove(i) - return [i[3] for i in delete] - - def close(self): - self.closed = True - -class Account(imap4.MemoryAccount): - mailboxFactory = SimpleMailbox - def _emptyMailbox(self, name, id): - return self.mailboxFactory() - - def select(self, name, rw=1): - mbox = imap4.MemoryAccount.select(self, name) - if mbox is not None: - mbox.rw = rw - return mbox - -class SimpleServer(imap4.IMAP4Server): - def __init__(self, *args, **kw): - imap4.IMAP4Server.__init__(self, *args, **kw) - realm = TestRealm() - realm.theAccount = Account('testuser') - portal = cred.portal.Portal(realm) - c = cred.checkers.InMemoryUsernamePasswordDatabaseDontUse() - self.checker = c - self.portal = portal - portal.registerChecker(c) - self.timeoutTest = False - - def lineReceived(self, line): - if self.timeoutTest: - #Do not send a respones - return - - imap4.IMAP4Server.lineReceived(self, line) - - _username = 'testuser' - _password = 'password-test' - def authenticateLogin(self, username, password): - if username == self._username and password == self._password: - return imap4.IAccount, self.theAccount, lambda: None - raise cred.error.UnauthorizedLogin() - - -class SimpleClient(imap4.IMAP4Client): - def __init__(self, deferred, contextFactory = None): - imap4.IMAP4Client.__init__(self, contextFactory) - self.deferred = deferred - self.events = [] - - def serverGreeting(self, caps): - self.deferred.callback(None) - - def modeChanged(self, writeable): - self.events.append(['modeChanged', writeable]) - self.transport.loseConnection() - - def flagsChanged(self, newFlags): - self.events.append(['flagsChanged', newFlags]) - self.transport.loseConnection() - - def newMessages(self, exists, recent): - self.events.append(['newMessages', exists, recent]) - self.transport.loseConnection() - - def fetchBodyParts(self, message, parts): - """Fetch some parts of the body. - - @param message: message with parts to fetch - @type message: C{str} - @param parts: a list of int/str - @type parts: C{list} - """ - cmd = "%s (BODY[%s]" % (message, parts[0]) - for p in parts[1:]: - cmd += " BODY[%s]" % p - cmd += ")" - d = self.sendCommand(imap4.Command("FETCH", cmd, - wantResponse=("FETCH",))) - d.addCallback(self.__cb_fetchBodyParts) - return d - - def __cb_fetchBodyParts(self, (lines, last)): - info = {} - for line in lines: - parts = line.split(None, 2) - if len(parts) == 3: - if parts[1] == "FETCH": - try: - mail_id = int(parts[0]) - except ValueError: - raise imap4.IllegalServerResponse, line - else: - body_parts = imap4.parseNestedParens(parts[2])[0] - dict_parts = {} - for i in range(len(body_parts)/3): - dict_parts[body_parts[3*i+1][0]] = body_parts[3*i+2] - info[mail_id] = dict_parts - return info - -class IMAP4HelperMixin: - serverCTX = None - clientCTX = None - - def setUp(self): - d = defer.Deferred() - self.server = SimpleServer(contextFactory=self.serverCTX) - self.client = SimpleClient(d, contextFactory=self.clientCTX) - self.connected = d - - SimpleMailbox.messages = [] - theAccount = Account('testuser') - theAccount.mboxType = SimpleMailbox - SimpleServer.theAccount = theAccount - - def tearDown(self): - del self.server - del self.client - del self.connected - - def _cbStopClient(self, ignore): - self.client.transport.loseConnection() - - def _ebGeneral(self, failure): - self.client.transport.loseConnection() - self.server.transport.loseConnection() - failure.printTraceback(open('failure.log', 'w')) - failure.printTraceback() - raise failure.value - - def loopback(self): - return loopback.loopbackAsync(self.server, self.client) - -class IMAP4ServerTestCase(IMAP4HelperMixin, unittest.TestCase): - def testCapability(self): - caps = {} - def getCaps(): - def gotCaps(c): - caps.update(c) - self.server.transport.loseConnection() - return self.client.getCapabilities().addCallback(gotCaps) - d1 = self.connected.addCallback(strip(getCaps)).addErrback(self._ebGeneral) - d = defer.gatherResults([self.loopback(), d1]) - expected = {'IMAP4rev1': None, 'NAMESPACE': None, 'IDLE': None} - return d.addCallback(lambda _: self.assertEquals(expected, caps)) - - def testCapabilityWithAuth(self): - caps = {} - self.server.challengers['CRAM-MD5'] = cred.credentials.CramMD5Credentials - def getCaps(): - def gotCaps(c): - caps.update(c) - self.server.transport.loseConnection() - return self.client.getCapabilities().addCallback(gotCaps) - d1 = self.connected.addCallback(strip(getCaps)).addErrback(self._ebGeneral) - d = defer.gatherResults([self.loopback(), d1]) - - expCap = {'IMAP4rev1': None, 'NAMESPACE': None, - 'IDLE': None, 'AUTH': ['CRAM-MD5']} - - return d.addCallback(lambda _: self.assertEquals(expCap, caps)) - - def testLogout(self): - self.loggedOut = 0 - def logout(): - def setLoggedOut(): - self.loggedOut = 1 - self.client.logout().addCallback(strip(setLoggedOut)) - self.connected.addCallback(strip(logout)).addErrback(self._ebGeneral) - d = self.loopback() - return d.addCallback(lambda _: self.assertEquals(self.loggedOut, 1)) - - def testNoop(self): - self.responses = None - def noop(): - def setResponses(responses): - self.responses = responses - self.server.transport.loseConnection() - self.client.noop().addCallback(setResponses) - self.connected.addCallback(strip(noop)).addErrback(self._ebGeneral) - d = self.loopback() - return d.addCallback(lambda _: self.assertEquals(self.responses, [])) - - def testLogin(self): - def login(): - d = self.client.login('testuser', 'password-test') - d.addCallback(self._cbStopClient) - d1 = self.connected.addCallback(strip(login)).addErrback(self._ebGeneral) - d = defer.gatherResults([d1, self.loopback()]) - return d.addCallback(self._cbTestLogin) - - def _cbTestLogin(self, ignored): - self.assertEquals(self.server.account, SimpleServer.theAccount) - self.assertEquals(self.server.state, 'auth') - - def testFailedLogin(self): - def login(): - d = self.client.login('testuser', 'wrong-password') - d.addBoth(self._cbStopClient) - - d1 = self.connected.addCallback(strip(login)).addErrback(self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - return d.addCallback(self._cbTestFailedLogin) - - def _cbTestFailedLogin(self, ignored): - self.assertEquals(self.server.account, None) - self.assertEquals(self.server.state, 'unauth') - - - def testLoginRequiringQuoting(self): - self.server._username = '{test}user' - self.server._password = '{test}password' - - def login(): - d = self.client.login('{test}user', '{test}password') - d.addBoth(self._cbStopClient) - - d1 = self.connected.addCallback(strip(login)).addErrback(self._ebGeneral) - d = defer.gatherResults([self.loopback(), d1]) - return d.addCallback(self._cbTestLoginRequiringQuoting) - - def _cbTestLoginRequiringQuoting(self, ignored): - self.assertEquals(self.server.account, SimpleServer.theAccount) - self.assertEquals(self.server.state, 'auth') - - - def testNamespace(self): - self.namespaceArgs = None - def login(): - return self.client.login('testuser', 'password-test') - def namespace(): - def gotNamespace(args): - self.namespaceArgs = args - self._cbStopClient(None) - return self.client.namespace().addCallback(gotNamespace) - - d1 = self.connected.addCallback(strip(login)) - d1.addCallback(strip(namespace)) - d1.addErrback(self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - d.addCallback(lambda _: self.assertEquals(self.namespaceArgs, - [[['', '/']], [], []])) - return d - - def testSelect(self): - SimpleServer.theAccount.addMailbox('test-mailbox') - self.selectedArgs = None - def login(): - return self.client.login('testuser', 'password-test') - def select(): - def selected(args): - self.selectedArgs = args - self._cbStopClient(None) - d = self.client.select('test-mailbox') - d.addCallback(selected) - return d - - d1 = self.connected.addCallback(strip(login)) - d1.addCallback(strip(select)) - d1.addErrback(self._ebGeneral) - d2 = self.loopback() - return defer.gatherResults([d1, d2]).addCallback(self._cbTestSelect) - - def _cbTestSelect(self, ignored): - mbox = SimpleServer.theAccount.mailboxes['TEST-MAILBOX'] - self.assertEquals(self.server.mbox, mbox) - self.assertEquals(self.selectedArgs, { - 'EXISTS': 9, 'RECENT': 3, 'UIDVALIDITY': 42, - 'FLAGS': ('\\Flag1', 'Flag2', '\\AnotherSysFlag', 'LastFlag'), - 'READ-WRITE': 1 - }) - - def testExamine(self): - SimpleServer.theAccount.addMailbox('test-mailbox') - self.examinedArgs = None - def login(): - return self.client.login('testuser', 'password-test') - def examine(): - def examined(args): - self.examinedArgs = args - self._cbStopClient(None) - d = self.client.examine('test-mailbox') - d.addCallback(examined) - return d - - d1 = self.connected.addCallback(strip(login)) - d1.addCallback(strip(examine)) - d1.addErrback(self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - return d.addCallback(self._cbTestExamine) - - def _cbTestExamine(self, ignored): - mbox = SimpleServer.theAccount.mailboxes['TEST-MAILBOX'] - self.assertEquals(self.server.mbox, mbox) - self.assertEquals(self.examinedArgs, { - 'EXISTS': 9, 'RECENT': 3, 'UIDVALIDITY': 42, - 'FLAGS': ('\\Flag1', 'Flag2', '\\AnotherSysFlag', 'LastFlag'), - 'READ-WRITE': 0 - }) - - def testCreate(self): - succeed = ('testbox', 'test/box', 'test/', 'test/box/box', 'INBOX') - fail = ('testbox', 'test/box') - - def cb(): self.result.append(1) - def eb(failure): self.result.append(0) - - def login(): - return self.client.login('testuser', 'password-test') - def create(): - for name in succeed + fail: - d = self.client.create(name) - d.addCallback(strip(cb)).addErrback(eb) - d.addCallbacks(self._cbStopClient, self._ebGeneral) - - self.result = [] - d1 = self.connected.addCallback(strip(login)).addCallback(strip(create)) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - return d.addCallback(self._cbTestCreate, succeed, fail) - - def _cbTestCreate(self, ignored, succeed, fail): - self.assertEquals(self.result, [1] * len(succeed) + [0] * len(fail)) - mbox = SimpleServer.theAccount.mailboxes.keys() - answers = ['inbox', 'testbox', 'test/box', 'test', 'test/box/box'] - mbox.sort() - answers.sort() - self.assertEquals(mbox, [a.upper() for a in answers]) - - def testDelete(self): - SimpleServer.theAccount.addMailbox('delete/me') - - def login(): - return self.client.login('testuser', 'password-test') - def delete(): - return self.client.delete('delete/me') - d1 = self.connected.addCallback(strip(login)) - d1.addCallbacks(strip(delete), self._ebGeneral) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - d.addCallback(lambda _: - self.assertEquals(SimpleServer.theAccount.mailboxes.keys(), [])) - return d - - def testIllegalInboxDelete(self): - self.stashed = None - def login(): - return self.client.login('testuser', 'password-test') - def delete(): - return self.client.delete('inbox') - def stash(result): - self.stashed = result - - d1 = self.connected.addCallback(strip(login)) - d1.addCallbacks(strip(delete), self._ebGeneral) - d1.addBoth(stash) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - d.addCallback(lambda _: self.failUnless(isinstance(self.stashed, - failure.Failure))) - return d - - - def testNonExistentDelete(self): - def login(): - return self.client.login('testuser', 'password-test') - def delete(): - return self.client.delete('delete/me') - def deleteFailed(failure): - self.failure = failure - - self.failure = None - d1 = self.connected.addCallback(strip(login)) - d1.addCallback(strip(delete)).addErrback(deleteFailed) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - d.addCallback(lambda _: self.assertEquals(str(self.failure.value), - 'No such mailbox')) - return d - - - def testIllegalDelete(self): - m = SimpleMailbox() - m.flags = (r'\Noselect',) - SimpleServer.theAccount.addMailbox('delete', m) - SimpleServer.theAccount.addMailbox('delete/me') - - def login(): - return self.client.login('testuser', 'password-test') - def delete(): - return self.client.delete('delete') - def deleteFailed(failure): - self.failure = failure - - self.failure = None - d1 = self.connected.addCallback(strip(login)) - d1.addCallback(strip(delete)).addErrback(deleteFailed) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - expected = "Hierarchically inferior mailboxes exist and \\Noselect is set" - d.addCallback(lambda _: - self.assertEquals(str(self.failure.value), expected)) - return d - - def testRename(self): - SimpleServer.theAccount.addMailbox('oldmbox') - def login(): - return self.client.login('testuser', 'password-test') - def rename(): - return self.client.rename('oldmbox', 'newname') - - d1 = self.connected.addCallback(strip(login)) - d1.addCallbacks(strip(rename), self._ebGeneral) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - d.addCallback(lambda _: - self.assertEquals(SimpleServer.theAccount.mailboxes.keys(), - ['NEWNAME'])) - return d - - def testIllegalInboxRename(self): - self.stashed = None - def login(): - return self.client.login('testuser', 'password-test') - def rename(): - return self.client.rename('inbox', 'frotz') - def stash(stuff): - self.stashed = stuff - - d1 = self.connected.addCallback(strip(login)) - d1.addCallbacks(strip(rename), self._ebGeneral) - d1.addBoth(stash) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - d.addCallback(lambda _: - self.failUnless(isinstance(self.stashed, failure.Failure))) - return d - - def testHierarchicalRename(self): - SimpleServer.theAccount.create('oldmbox/m1') - SimpleServer.theAccount.create('oldmbox/m2') - def login(): - return self.client.login('testuser', 'password-test') - def rename(): - return self.client.rename('oldmbox', 'newname') - - d1 = self.connected.addCallback(strip(login)) - d1.addCallbacks(strip(rename), self._ebGeneral) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - return d.addCallback(self._cbTestHierarchicalRename) - - def _cbTestHierarchicalRename(self, ignored): - mboxes = SimpleServer.theAccount.mailboxes.keys() - expected = ['newname', 'newname/m1', 'newname/m2'] - mboxes.sort() - self.assertEquals(mboxes, [s.upper() for s in expected]) - - def testSubscribe(self): - def login(): - return self.client.login('testuser', 'password-test') - def subscribe(): - return self.client.subscribe('this/mbox') - - d1 = self.connected.addCallback(strip(login)) - d1.addCallbacks(strip(subscribe), self._ebGeneral) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - d.addCallback(lambda _: - self.assertEquals(SimpleServer.theAccount.subscriptions, - ['THIS/MBOX'])) - return d - - def testUnsubscribe(self): - SimpleServer.theAccount.subscriptions = ['THIS/MBOX', 'THAT/MBOX'] - def login(): - return self.client.login('testuser', 'password-test') - def unsubscribe(): - return self.client.unsubscribe('this/mbox') - - d1 = self.connected.addCallback(strip(login)) - d1.addCallbacks(strip(unsubscribe), self._ebGeneral) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - d.addCallback(lambda _: - self.assertEquals(SimpleServer.theAccount.subscriptions, - ['THAT/MBOX'])) - return d - - def _listSetup(self, f): - SimpleServer.theAccount.addMailbox('root/subthing') - SimpleServer.theAccount.addMailbox('root/another-thing') - SimpleServer.theAccount.addMailbox('non-root/subthing') - - def login(): - return self.client.login('testuser', 'password-test') - def listed(answers): - self.listed = answers - - self.listed = None - d1 = self.connected.addCallback(strip(login)) - d1.addCallbacks(strip(f), self._ebGeneral) - d1.addCallbacks(listed, self._ebGeneral) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - return defer.gatherResults([d1, d2]).addCallback(lambda _: self.listed) - - def testList(self): - def list(): - return self.client.list('root', '%') - d = self._listSetup(list) - d.addCallback(lambda listed: self.assertEquals( - sortNest(listed), - sortNest([ - (SimpleMailbox.flags, "/", "ROOT/SUBTHING"), - (SimpleMailbox.flags, "/", "ROOT/ANOTHER-THING") - ]) - )) - return d - - def testLSub(self): - SimpleServer.theAccount.subscribe('ROOT/SUBTHING') - def lsub(): - return self.client.lsub('root', '%') - d = self._listSetup(lsub) - d.addCallback(self.assertEquals, - [(SimpleMailbox.flags, "/", "ROOT/SUBTHING")]) - return d - - def testStatus(self): - SimpleServer.theAccount.addMailbox('root/subthing') - def login(): - return self.client.login('testuser', 'password-test') - def status(): - return self.client.status('root/subthing', 'MESSAGES', 'UIDNEXT', 'UNSEEN') - def statused(result): - self.statused = result - - self.statused = None - d1 = self.connected.addCallback(strip(login)) - d1.addCallbacks(strip(status), self._ebGeneral) - d1.addCallbacks(statused, self._ebGeneral) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - d.addCallback(lambda _: self.assertEquals( - self.statused, - {'MESSAGES': 9, 'UIDNEXT': '10', 'UNSEEN': 4} - )) - return d - - def testFailedStatus(self): - def login(): - return self.client.login('testuser', 'password-test') - def status(): - return self.client.status('root/nonexistent', 'MESSAGES', 'UIDNEXT', 'UNSEEN') - def statused(result): - self.statused = result - def failed(failure): - self.failure = failure - - self.statused = self.failure = None - d1 = self.connected.addCallback(strip(login)) - d1.addCallbacks(strip(status), self._ebGeneral) - d1.addCallbacks(statused, failed) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - return defer.gatherResults([d1, d2]).addCallback(self._cbTestFailedStatus) - - def _cbTestFailedStatus(self, ignored): - self.assertEquals( - self.statused, None - ) - self.assertEquals( - self.failure.value.args, - ('Could not open mailbox',) - ) - - def testFullAppend(self): - infile = util.sibpath(__file__, 'rfc822.message') - message = open(infile) - SimpleServer.theAccount.addMailbox('root/subthing') - def login(): - return self.client.login('testuser', 'password-test') - def append(): - return self.client.append( - 'root/subthing', - message, - ('\\SEEN', '\\DELETED'), - 'Tue, 17 Jun 2003 11:22:16 -0600 (MDT)', - ) - - d1 = self.connected.addCallback(strip(login)) - d1.addCallbacks(strip(append), self._ebGeneral) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - return d.addCallback(self._cbTestFullAppend, infile) - - def _cbTestFullAppend(self, ignored, infile): - mb = SimpleServer.theAccount.mailboxes['ROOT/SUBTHING'] - self.assertEquals(1, len(mb.messages)) - self.assertEquals( - (['\\SEEN', '\\DELETED'], 'Tue, 17 Jun 2003 11:22:16 -0600 (MDT)', 0), - mb.messages[0][1:] - ) - self.assertEquals(open(infile).read(), mb.messages[0][0].getvalue()) - - def testPartialAppend(self): - infile = util.sibpath(__file__, 'rfc822.message') - message = open(infile) - SimpleServer.theAccount.addMailbox('PARTIAL/SUBTHING') - def login(): - return self.client.login('testuser', 'password-test') - def append(): - message = file(infile) - return self.client.sendCommand( - imap4.Command( - 'APPEND', - 'PARTIAL/SUBTHING (\\SEEN) "Right now" {%d}' % os.path.getsize(infile), - (), self.client._IMAP4Client__cbContinueAppend, message - ) - ) - d1 = self.connected.addCallback(strip(login)) - d1.addCallbacks(strip(append), self._ebGeneral) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - return d.addCallback(self._cbTestPartialAppend, infile) - - def _cbTestPartialAppend(self, ignored, infile): - mb = SimpleServer.theAccount.mailboxes['PARTIAL/SUBTHING'] - self.assertEquals(1, len(mb.messages)) - self.assertEquals( - (['\\SEEN'], 'Right now', 0), - mb.messages[0][1:] - ) - self.assertEquals(open(infile).read(), mb.messages[0][0].getvalue()) - - def testCheck(self): - SimpleServer.theAccount.addMailbox('root/subthing') - def login(): - return self.client.login('testuser', 'password-test') - def select(): - return self.client.select('root/subthing') - def check(): - return self.client.check() - - d = self.connected.addCallback(strip(login)) - d.addCallbacks(strip(select), self._ebGeneral) - d.addCallbacks(strip(check), self._ebGeneral) - d.addCallbacks(self._cbStopClient, self._ebGeneral) - return self.loopback() - - # Okay, that was fun - - def testClose(self): - m = SimpleMailbox() - m.messages = [ - ('Message 1', ('\\Deleted', 'AnotherFlag'), None, 0), - ('Message 2', ('AnotherFlag',), None, 1), - ('Message 3', ('\\Deleted',), None, 2), - ] - SimpleServer.theAccount.addMailbox('mailbox', m) - def login(): - return self.client.login('testuser', 'password-test') - def select(): - return self.client.select('mailbox') - def close(): - return self.client.close() - - d = self.connected.addCallback(strip(login)) - d.addCallbacks(strip(select), self._ebGeneral) - d.addCallbacks(strip(close), self._ebGeneral) - d.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - return defer.gatherResults([d, d2]).addCallback(self._cbTestClose, m) - - def _cbTestClose(self, ignored, m): - self.assertEquals(len(m.messages), 1) - self.assertEquals(m.messages[0], ('Message 2', ('AnotherFlag',), None, 1)) - self.failUnless(m.closed) - - def testExpunge(self): - m = SimpleMailbox() - m.messages = [ - ('Message 1', ('\\Deleted', 'AnotherFlag'), None, 0), - ('Message 2', ('AnotherFlag',), None, 1), - ('Message 3', ('\\Deleted',), None, 2), - ] - SimpleServer.theAccount.addMailbox('mailbox', m) - def login(): - return self.client.login('testuser', 'password-test') - def select(): - return self.client.select('mailbox') - def expunge(): - return self.client.expunge() - def expunged(results): - self.failIf(self.server.mbox is None) - self.results = results - - self.results = None - d1 = self.connected.addCallback(strip(login)) - d1.addCallbacks(strip(select), self._ebGeneral) - d1.addCallbacks(strip(expunge), self._ebGeneral) - d1.addCallbacks(expunged, self._ebGeneral) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - return d.addCallback(self._cbTestExpunge, m) - - def _cbTestExpunge(self, ignored, m): - self.assertEquals(len(m.messages), 1) - self.assertEquals(m.messages[0], ('Message 2', ('AnotherFlag',), None, 1)) - - self.assertEquals(self.results, [0, 2]) - -class TestRealm: - theAccount = None - - def requestAvatar(self, avatarId, mind, *interfaces): - return imap4.IAccount, self.theAccount, lambda: None - -class TestChecker: - credentialInterfaces = (cred.credentials.IUsernameHashedPassword, cred.credentials.IUsernamePassword) - - users = { - 'testuser': 'secret' - } - - def requestAvatarId(self, credentials): - if credentials.username in self.users: - return defer.maybeDeferred( - credentials.checkPassword, self.users[credentials.username] - ).addCallback(self._cbCheck, credentials.username) - - def _cbCheck(self, result, username): - if result: - return username - raise cred.error.UnauthorizedLogin() - -class AuthenticatorTestCase(IMAP4HelperMixin, unittest.TestCase): - def setUp(self): - IMAP4HelperMixin.setUp(self) - - realm = TestRealm() - realm.theAccount = Account('testuser') - portal = cred.portal.Portal(realm) - portal.registerChecker(TestChecker()) - self.server.portal = portal - - self.authenticated = 0 - self.account = realm.theAccount - - def testCramMD5(self): - self.server.challengers['CRAM-MD5'] = cred.credentials.CramMD5Credentials - cAuth = imap4.CramMD5ClientAuthenticator('testuser') - self.client.registerAuthenticator(cAuth) - - def auth(): - return self.client.authenticate('secret') - def authed(): - self.authenticated = 1 - - d1 = self.connected.addCallback(strip(auth)) - d1.addCallbacks(strip(authed), self._ebGeneral) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - return d.addCallback(self._cbTestCramMD5) - - def _cbTestCramMD5(self, ignored): - self.assertEquals(self.authenticated, 1) - self.assertEquals(self.server.account, self.account) - - def testFailedCramMD5(self): - self.server.challengers['CRAM-MD5'] = cred.credentials.CramMD5Credentials - cAuth = imap4.CramMD5ClientAuthenticator('testuser') - self.client.registerAuthenticator(cAuth) - - def misauth(): - return self.client.authenticate('not the secret') - def authed(): - self.authenticated = 1 - def misauthed(): - self.authenticated = -1 - - d1 = self.connected.addCallback(strip(misauth)) - d1.addCallbacks(strip(authed), strip(misauthed)) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d = defer.gatherResults([self.loopback(), d1]) - return d.addCallback(self._cbTestFailedCramMD5) - - def _cbTestFailedCramMD5(self, ignored): - self.assertEquals(self.authenticated, -1) - self.assertEquals(self.server.account, None) - - def testLOGIN(self): - self.server.challengers['LOGIN'] = imap4.LOGINCredentials - cAuth = imap4.LOGINAuthenticator('testuser') - self.client.registerAuthenticator(cAuth) - - def auth(): - return self.client.authenticate('secret') - def authed(): - self.authenticated = 1 - - d1 = self.connected.addCallback(strip(auth)) - d1.addCallbacks(strip(authed), self._ebGeneral) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d = defer.gatherResults([self.loopback(), d1]) - return d.addCallback(self._cbTestLOGIN) - - def _cbTestLOGIN(self, ignored): - self.assertEquals(self.authenticated, 1) - self.assertEquals(self.server.account, self.account) - - def testFailedLOGIN(self): - self.server.challengers['LOGIN'] = imap4.LOGINCredentials - cAuth = imap4.LOGINAuthenticator('testuser') - self.client.registerAuthenticator(cAuth) - - def misauth(): - return self.client.authenticate('not the secret') - def authed(): - self.authenticated = 1 - def misauthed(): - self.authenticated = -1 - - d1 = self.connected.addCallback(strip(misauth)) - d1.addCallbacks(strip(authed), strip(misauthed)) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d = defer.gatherResults([self.loopback(), d1]) - return d.addCallback(self._cbTestFailedLOGIN) - - def _cbTestFailedLOGIN(self, ignored): - self.assertEquals(self.authenticated, -1) - self.assertEquals(self.server.account, None) - - def testPLAIN(self): - self.server.challengers['PLAIN'] = imap4.PLAINCredentials - cAuth = imap4.PLAINAuthenticator('testuser') - self.client.registerAuthenticator(cAuth) - - def auth(): - return self.client.authenticate('secret') - def authed(): - self.authenticated = 1 - - d1 = self.connected.addCallback(strip(auth)) - d1.addCallbacks(strip(authed), self._ebGeneral) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d = defer.gatherResults([self.loopback(), d1]) - return d.addCallback(self._cbTestPLAIN) - - def _cbTestPLAIN(self, ignored): - self.assertEquals(self.authenticated, 1) - self.assertEquals(self.server.account, self.account) - - def testFailedPLAIN(self): - self.server.challengers['PLAIN'] = imap4.PLAINCredentials - cAuth = imap4.PLAINAuthenticator('testuser') - self.client.registerAuthenticator(cAuth) - - def misauth(): - return self.client.authenticate('not the secret') - def authed(): - self.authenticated = 1 - def misauthed(): - self.authenticated = -1 - - d1 = self.connected.addCallback(strip(misauth)) - d1.addCallbacks(strip(authed), strip(misauthed)) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d = defer.gatherResults([self.loopback(), d1]) - return d.addCallback(self._cbTestFailedPLAIN) - - def _cbTestFailedPLAIN(self, ignored): - self.assertEquals(self.authenticated, -1) - self.assertEquals(self.server.account, None) - - -class UnsolicitedResponseTestCase(IMAP4HelperMixin, unittest.TestCase): - def testReadWrite(self): - def login(): - return self.client.login('testuser', 'password-test') - def loggedIn(): - self.server.modeChanged(1) - - d1 = self.connected.addCallback(strip(login)) - d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral) - d = defer.gatherResults([self.loopback(), d1]) - return d.addCallback(self._cbTestReadWrite) - - def _cbTestReadWrite(self, ignored): - E = self.client.events - self.assertEquals(E, [['modeChanged', 1]]) - - def testReadOnly(self): - def login(): - return self.client.login('testuser', 'password-test') - def loggedIn(): - self.server.modeChanged(0) - - d1 = self.connected.addCallback(strip(login)) - d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral) - d = defer.gatherResults([self.loopback(), d1]) - return d.addCallback(self._cbTestReadOnly) - - def _cbTestReadOnly(self, ignored): - E = self.client.events - self.assertEquals(E, [['modeChanged', 0]]) - - def testFlagChange(self): - flags = { - 1: ['\\Answered', '\\Deleted'], - 5: [], - 10: ['\\Recent'] - } - def login(): - return self.client.login('testuser', 'password-test') - def loggedIn(): - self.server.flagsChanged(flags) - - d1 = self.connected.addCallback(strip(login)) - d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral) - d = defer.gatherResults([self.loopback(), d1]) - return d.addCallback(self._cbTestFlagChange, flags) - - def _cbTestFlagChange(self, ignored, flags): - E = self.client.events - expect = [['flagsChanged', {x[0]: x[1]}] for x in flags.items()] - E.sort() - expect.sort() - self.assertEquals(E, expect) - - def testNewMessages(self): - def login(): - return self.client.login('testuser', 'password-test') - def loggedIn(): - self.server.newMessages(10, None) - - d1 = self.connected.addCallback(strip(login)) - d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral) - d = defer.gatherResults([self.loopback(), d1]) - return d.addCallback(self._cbTestNewMessages) - - def _cbTestNewMessages(self, ignored): - E = self.client.events - self.assertEquals(E, [['newMessages', 10, None]]) - - def testNewRecentMessages(self): - def login(): - return self.client.login('testuser', 'password-test') - def loggedIn(): - self.server.newMessages(None, 10) - - d1 = self.connected.addCallback(strip(login)) - d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral) - d = defer.gatherResults([self.loopback(), d1]) - return d.addCallback(self._cbTestNewRecentMessages) - - def _cbTestNewRecentMessages(self, ignored): - E = self.client.events - self.assertEquals(E, [['newMessages', None, 10]]) - - def testNewMessagesAndRecent(self): - def login(): - return self.client.login('testuser', 'password-test') - def loggedIn(): - self.server.newMessages(20, 10) - - d1 = self.connected.addCallback(strip(login)) - d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral) - d = defer.gatherResults([self.loopback(), d1]) - return d.addCallback(self._cbTestNewMessagesAndRecent) - - def _cbTestNewMessagesAndRecent(self, ignored): - E = self.client.events - self.assertEquals(E, [['newMessages', 20, None], ['newMessages', None, 10]]) - - -class ClientCapabilityTests(unittest.TestCase): - """ - Tests for issuance of the CAPABILITY command and handling of its response. - """ - def setUp(self): - """ - Create an L{imap4.IMAP4Client} connected to a L{StringTransport}. - """ - self.transport = StringTransport() - self.protocol = imap4.IMAP4Client() - self.protocol.makeConnection(self.transport) - self.protocol.dataReceived('* OK [IMAP4rev1]\r\n') - - - def test_simpleAtoms(self): - """ - A capability response consisting only of atoms without C{'='} in them - should result in a dict mapping those atoms to C{None}. - """ - capabilitiesResult = self.protocol.getCapabilities(useCache=False) - self.protocol.dataReceived('* CAPABILITY IMAP4rev1 LOGINDISABLED\r\n') - self.protocol.dataReceived('0001 OK Capability completed.\r\n') - def gotCapabilities(capabilities): - self.assertEqual( - capabilities, {'IMAP4rev1': None, 'LOGINDISABLED': None}) - capabilitiesResult.addCallback(gotCapabilities) - return capabilitiesResult - - - def test_categoryAtoms(self): - """ - A capability response consisting of atoms including C{'='} should have - those atoms split on that byte and have capabilities in the same - category aggregated into lists in the resulting dictionary. - - (n.b. - I made up the word "category atom"; the protocol has no notion - of structure here, but rather allows each capability to define the - semantics of its entry in the capability response in a freeform manner. - If I had realized this earlier, the API for capabilities would look - different. As it is, we can hope that no one defines any crazy - semantics which are incompatible with this API, or try to figure out a - better API when someone does. -exarkun) - """ - capabilitiesResult = self.protocol.getCapabilities(useCache=False) - self.protocol.dataReceived('* CAPABILITY IMAP4rev1 AUTH=LOGIN AUTH=PLAIN\r\n') - self.protocol.dataReceived('0001 OK Capability completed.\r\n') - def gotCapabilities(capabilities): - self.assertEqual( - capabilities, {'IMAP4rev1': None, 'AUTH': ['LOGIN', 'PLAIN']}) - capabilitiesResult.addCallback(gotCapabilities) - return capabilitiesResult - - - def test_mixedAtoms(self): - """ - A capability response consisting of both simple and category atoms of - the same type should result in a list containing C{None} as well as the - values for the category. - """ - capabilitiesResult = self.protocol.getCapabilities(useCache=False) - # Exercise codepath for both orderings of =-having and =-missing - # capabilities. - self.protocol.dataReceived( - '* CAPABILITY IMAP4rev1 FOO FOO=BAR BAR=FOO BAR\r\n') - self.protocol.dataReceived('0001 OK Capability completed.\r\n') - def gotCapabilities(capabilities): - self.assertEqual(capabilities, {'IMAP4rev1': None, - 'FOO': [None, 'BAR'], - 'BAR': ['FOO', None]}) - capabilitiesResult.addCallback(gotCapabilities) - return capabilitiesResult - - - - -class HandCraftedTestCase(IMAP4HelperMixin, unittest.TestCase): - def testTrailingLiteral(self): - transport = StringTransport() - c = imap4.IMAP4Client() - c.makeConnection(transport) - c.lineReceived('* OK [IMAP4rev1]') - - def cbSelect(ignored): - d = c.fetchMessage('1') - c.dataReceived('* 1 FETCH (RFC822 {10}\r\n0123456789\r\n RFC822.SIZE 10)\r\n') - c.dataReceived('0003 OK FETCH\r\n') - return d - - def cbLogin(ignored): - d = c.select('inbox') - c.lineReceived('0002 OK SELECT') - d.addCallback(cbSelect) - return d - - d = c.login('blah', 'blah') - c.dataReceived('0001 OK LOGIN\r\n') - d.addCallback(cbLogin) - return d - - def testPathelogicalScatteringOfLiterals(self): - self.server.checker.addUser('testuser', 'password-test') - transport = StringTransport() - self.server.makeConnection(transport) - - transport.clear() - self.server.dataReceived("01 LOGIN {8}\r\n") - self.assertEquals(transport.value(), "+ Ready for 8 octets of text\r\n") - - transport.clear() - self.server.dataReceived("testuser {13}\r\n") - self.assertEquals(transport.value(), "+ Ready for 13 octets of text\r\n") - - transport.clear() - self.server.dataReceived("password-test\r\n") - self.assertEquals(transport.value(), "01 OK LOGIN succeeded\r\n") - self.assertEquals(self.server.state, 'auth') - - self.server.connectionLost(error.ConnectionDone("Connection done.")) - - def testUnsolicitedResponseMixedWithSolicitedResponse(self): - - class StillSimplerClient(imap4.IMAP4Client): - events = [] - def flagsChanged(self, newFlags): - self.events.append(['flagsChanged', newFlags]) - - transport = StringTransport() - c = StillSimplerClient() - c.makeConnection(transport) - c.lineReceived('* OK [IMAP4rev1]') - - def login(): - d = c.login('blah', 'blah') - c.dataReceived('0001 OK LOGIN\r\n') - return d - def select(): - d = c.select('inbox') - c.lineReceived('0002 OK SELECT') - return d - def fetch(): - d = c.fetchSpecific('1:*', - headerType='HEADER.FIELDS', - headerArgs=['SUBJECT']) - c.dataReceived('* 1 FETCH (BODY[HEADER.FIELDS ("SUBJECT")] {38}\r\n') - c.dataReceived('Subject: Suprise for your woman...\r\n') - c.dataReceived('\r\n') - c.dataReceived(')\r\n') - c.dataReceived('* 1 FETCH (FLAGS (\Seen))\r\n') - c.dataReceived('* 2 FETCH (BODY[HEADER.FIELDS ("SUBJECT")] {75}\r\n') - c.dataReceived('Subject: What you been doing. Order your meds here . ,. handcuff madsen\r\n') - c.dataReceived('\r\n') - c.dataReceived(')\r\n') - c.dataReceived('0003 OK FETCH completed\r\n') - return d - def test(res): - self.assertEquals(res, { - 1: [['BODY', ['HEADER.FIELDS', ['SUBJECT']], - 'Subject: Suprise for your woman...\r\n\r\n']], - 2: [['BODY', ['HEADER.FIELDS', ['SUBJECT']], - 'Subject: What you been doing. Order your meds here . ,. handcuff madsen\r\n\r\n']] - }) - - self.assertEquals(c.events, [['flagsChanged', {1: ['\\Seen']}]]) - - return login( - ).addCallback(strip(select) - ).addCallback(strip(fetch) - ).addCallback(test) - - - def test_literalWithoutPrecedingWhitespace(self): - """ - Literals should be recognized even when they are not preceded by - whitespace. - """ - transport = StringTransport() - protocol = imap4.IMAP4Client() - - protocol.makeConnection(transport) - protocol.lineReceived('* OK [IMAP4rev1]') - - def login(): - d = protocol.login('blah', 'blah') - protocol.dataReceived('0001 OK LOGIN\r\n') - return d - def select(): - d = protocol.select('inbox') - protocol.lineReceived('0002 OK SELECT') - return d - def fetch(): - d = protocol.fetchSpecific('1:*', - headerType='HEADER.FIELDS', - headerArgs=['SUBJECT']) - protocol.dataReceived( - '* 1 FETCH (BODY[HEADER.FIELDS ({7}\r\nSUBJECT)] "Hello")\r\n') - protocol.dataReceived('0003 OK FETCH completed\r\n') - return d - def test(result): - self.assertEqual( - result, {1: [['BODY', ['HEADER.FIELDS', ['SUBJECT']], 'Hello']]}) - - d = login() - d.addCallback(strip(select)) - d.addCallback(strip(fetch)) - d.addCallback(test) - return d - - - def test_nonIntegerLiteralLength(self): - """ - If the server sends a literal length which cannot be parsed as an - integer, L{IMAP4Client.lineReceived} should cause the protocol to be - disconnected by raising L{imap4.IllegalServerResponse}. - """ - transport = StringTransport() - protocol = imap4.IMAP4Client() - - protocol.makeConnection(transport) - protocol.lineReceived('* OK [IMAP4rev1]') - - def login(): - d = protocol.login('blah', 'blah') - protocol.dataReceived('0001 OK LOGIN\r\n') - return d - def select(): - d = protocol.select('inbox') - protocol.lineReceived('0002 OK SELECT') - return d - def fetch(): - d = protocol.fetchSpecific('1:*', - headerType='HEADER.FIELDS', - headerArgs=['SUBJECT']) - self.assertRaises( - imap4.IllegalServerResponse, - protocol.dataReceived, - '* 1 FETCH {xyz}\r\n...') - d = login() - d.addCallback(strip(select)) - d.addCallback(strip(fetch)) - return d - - - -class FakeyServer(imap4.IMAP4Server): - state = 'select' - timeout = None - - def sendServerGreeting(self): - pass - -class FakeyMessage: - implements(imap4.IMessage) - - def __init__(self, headers, flags, date, body, uid, subpart): - self.headers = headers - self.flags = flags - self.body = StringIO(body) - self.size = len(body) - self.date = date - self.uid = uid - self.subpart = subpart - - def getHeaders(self, negate, *names): - self.got_headers = negate, names - return self.headers - - def getFlags(self): - return self.flags - - def getInternalDate(self): - return self.date - - def getBodyFile(self): - return self.body - - def getSize(self): - return self.size - - def getUID(self): - return self.uid - - def isMultipart(self): - return self.subpart is not None - - def getSubPart(self, part): - self.got_subpart = part - return self.subpart[part] - -class NewStoreTestCase(unittest.TestCase, IMAP4HelperMixin): - result = None - storeArgs = None - - def setUp(self): - self.received_messages = self.received_uid = None - - self.server = imap4.IMAP4Server() - self.server.state = 'select' - self.server.mbox = self - self.connected = defer.Deferred() - self.client = SimpleClient(self.connected) - - def addListener(self, x): - pass - def removeListener(self, x): - pass - - def store(self, *args, **kw): - self.storeArgs = args, kw - return self.response - - def _storeWork(self): - def connected(): - return self.function(self.messages, self.flags, self.silent, self.uid) - def result(R): - self.result = R - - self.connected.addCallback(strip(connected) - ).addCallback(result - ).addCallback(self._cbStopClient - ).addErrback(self._ebGeneral) - - def check(ignored): - self.assertEquals(self.result, self.expected) - self.assertEquals(self.storeArgs, self.expectedArgs) - d = loopback.loopbackTCP(self.server, self.client, noisy=False) - d.addCallback(check) - return d - - def testSetFlags(self, uid=0): - self.function = self.client.setFlags - self.messages = '1,5,9' - self.flags = ['\\A', '\\B', 'C'] - self.silent = False - self.uid = uid - self.response = { - 1: ['\\A', '\\B', 'C'], - 5: ['\\A', '\\B', 'C'], - 9: ['\\A', '\\B', 'C'], - } - self.expected = { - 1: {'FLAGS': ['\\A', '\\B', 'C']}, - 5: {'FLAGS': ['\\A', '\\B', 'C']}, - 9: {'FLAGS': ['\\A', '\\B', 'C']}, - } - msg = imap4.MessageSet() - msg.add(1) - msg.add(5) - msg.add(9) - self.expectedArgs = ((msg, ['\\A', '\\B', 'C'], 0), {'uid': 0}) - return self._storeWork() - - -class NewFetchTestCase(unittest.TestCase, IMAP4HelperMixin): - def setUp(self): - self.received_messages = self.received_uid = None - self.result = None - - self.server = imap4.IMAP4Server() - self.server.state = 'select' - self.server.mbox = self - self.connected = defer.Deferred() - self.client = SimpleClient(self.connected) - - def addListener(self, x): - pass - def removeListener(self, x): - pass - - def fetch(self, messages, uid): - self.received_messages = messages - self.received_uid = uid - return iter(zip(range(len(self.msgObjs)), self.msgObjs)) - - def _fetchWork(self, uid): - if uid: - for (i, msg) in zip(range(len(self.msgObjs)), self.msgObjs): - self.expected[i]['UID'] = str(msg.getUID()) - - def result(R): - self.result = R - - self.connected.addCallback(lambda _: self.function(self.messages, uid) - ).addCallback(result - ).addCallback(self._cbStopClient - ).addErrback(self._ebGeneral) - - d = loopback.loopbackTCP(self.server, self.client, noisy=False) - d.addCallback(lambda x : self.assertEquals(self.result, self.expected)) - return d - - def testFetchUID(self): - self.function = lambda m, u: self.client.fetchUID(m) - - self.messages = '7' - self.msgObjs = [ - FakeyMessage({}, (), '', '', 12345, None), - FakeyMessage({}, (), '', '', 999, None), - FakeyMessage({}, (), '', '', 10101, None), - ] - self.expected = { - 0: {'UID': '12345'}, - 1: {'UID': '999'}, - 2: {'UID': '10101'}, - } - return self._fetchWork(0) - - def testFetchFlags(self, uid=0): - self.function = self.client.fetchFlags - self.messages = '9' - self.msgObjs = [ - FakeyMessage({}, ['FlagA', 'FlagB', '\\FlagC'], '', '', 54321, None), - FakeyMessage({}, ['\\FlagC', 'FlagA', 'FlagB'], '', '', 12345, None), - ] - self.expected = { - 0: {'FLAGS': ['FlagA', 'FlagB', '\\FlagC']}, - 1: {'FLAGS': ['\\FlagC', 'FlagA', 'FlagB']}, - } - return self._fetchWork(uid) - - def testFetchFlagsUID(self): - return self.testFetchFlags(1) - - def testFetchInternalDate(self, uid=0): - self.function = self.client.fetchInternalDate - self.messages = '13' - self.msgObjs = [ - FakeyMessage({}, (), 'Fri, 02 Nov 2003 21:25:10 GMT', '', 23232, None), - FakeyMessage({}, (), 'Thu, 29 Dec 2013 11:31:52 EST', '', 101, None), - FakeyMessage({}, (), 'Mon, 10 Mar 1992 02:44:30 CST', '', 202, None), - FakeyMessage({}, (), 'Sat, 11 Jan 2000 14:40:24 PST', '', 303, None), - ] - self.expected = { - 0: {'INTERNALDATE': '02-Nov-2003 21:25:10 +0000'}, - 1: {'INTERNALDATE': '29-Dec-2013 11:31:52 -0500'}, - 2: {'INTERNALDATE': '10-Mar-1992 02:44:30 -0600'}, - 3: {'INTERNALDATE': '11-Jan-2000 14:40:24 -0800'}, - } - return self._fetchWork(uid) - - def testFetchInternalDateUID(self): - return self.testFetchInternalDate(1) - - def testFetchEnvelope(self, uid=0): - self.function = self.client.fetchEnvelope - self.messages = '15' - self.msgObjs = [ - FakeyMessage({ - 'from': 'user@domain', 'to': 'resu@domain', - 'date': 'thursday', 'subject': 'it is a message', - 'message-id': 'id-id-id-yayaya'}, (), '', '', 65656, - None), - ] - self.expected = { - 0: {'ENVELOPE': - ['thursday', 'it is a message', - [[None, None, 'user', 'domain']], - [[None, None, 'user', 'domain']], - [[None, None, 'user', 'domain']], - [[None, None, 'resu', 'domain']], - None, None, None, 'id-id-id-yayaya'] - } - } - return self._fetchWork(uid) - - def testFetchEnvelopeUID(self): - return self.testFetchEnvelope(1) - - def testFetchBodyStructure(self, uid=0): - self.function = self.client.fetchBodyStructure - self.messages = '3:9,10:*' - self.msgObjs = [FakeyMessage({ - 'content-type': 'text/plain; name=thing; key="value"', - 'content-id': 'this-is-the-content-id', - 'content-description': 'describing-the-content-goes-here!', - 'content-transfer-encoding': '8BIT', - }, (), '', 'Body\nText\nGoes\nHere\n', 919293, None)] - self.expected = {0: {'BODYSTRUCTURE': [ - 'text', 'plain', [['name', 'thing'], ['key', 'value']], - 'this-is-the-content-id', 'describing-the-content-goes-here!', - '8BIT', '20', '4', None, None, None]}} - return self._fetchWork(uid) - - def testFetchBodyStructureUID(self): - return self.testFetchBodyStructure(1) - - def testFetchSimplifiedBody(self, uid=0): - self.function = self.client.fetchSimplifiedBody - self.messages = '21' - self.msgObjs = [FakeyMessage({}, (), '', 'Yea whatever', 91825, - [FakeyMessage({'content-type': 'image/jpg'}, (), '', - 'Body Body Body', None, None - )] - )] - self.expected = {0: - {'BODY': - [None, None, [], None, None, None, - '12' - ] - } - } - - return self._fetchWork(uid) - - def testFetchSimplifiedBodyUID(self): - return self.testFetchSimplifiedBody(1) - - def testFetchSimplifiedBodyText(self, uid=0): - self.function = self.client.fetchSimplifiedBody - self.messages = '21' - self.msgObjs = [FakeyMessage({'content-type': 'text/plain'}, - (), '', 'Yea whatever', 91825, None)] - self.expected = {0: - {'BODY': - ['text', 'plain', [], None, None, None, - '12', '1' - ] - } - } - - return self._fetchWork(uid) - - def testFetchSimplifiedBodyTextUID(self): - return self.testFetchSimplifiedBodyText(1) - - def testFetchSimplifiedBodyRFC822(self, uid=0): - self.function = self.client.fetchSimplifiedBody - self.messages = '21' - self.msgObjs = [FakeyMessage({'content-type': 'message/rfc822'}, - (), '', 'Yea whatever', 91825, - [FakeyMessage({'content-type': 'image/jpg'}, (), '', - 'Body Body Body', None, None - )] - )] - self.expected = {0: - {'BODY': - ['message', 'rfc822', [], None, None, None, - '12', [None, None, [[None, None, None]], - [[None, None, None]], None, None, None, - None, None, None], ['image', 'jpg', [], - None, None, None, '14'], '1' - ] - } - } - - return self._fetchWork(uid) - - def testFetchSimplifiedBodyRFC822UID(self): - return self.testFetchSimplifiedBodyRFC822(1) - - def testFetchMessage(self, uid=0): - self.function = self.client.fetchMessage - self.messages = '1,3,7,10101' - self.msgObjs = [ - FakeyMessage({'Header': 'Value'}, (), '', 'BODY TEXT\r\n', 91, None), - ] - self.expected = { - 0: {'RFC822': 'Header: Value\r\n\r\nBODY TEXT\r\n'} - } - return self._fetchWork(uid) - - def testFetchMessageUID(self): - return self.testFetchMessage(1) - - def testFetchHeaders(self, uid=0): - self.function = self.client.fetchHeaders - self.messages = '9,6,2' - self.msgObjs = [ - FakeyMessage({'H1': 'V1', 'H2': 'V2'}, (), '', '', 99, None), - ] - self.expected = { - 0: {'RFC822.HEADER': imap4._formatHeaders({'H1': 'V1', 'H2': 'V2'})}, - } - return self._fetchWork(uid) - - def testFetchHeadersUID(self): - return self.testFetchHeaders(1) - - def testFetchBody(self, uid=0): - self.function = self.client.fetchBody - self.messages = '1,2,3,4,5,6,7' - self.msgObjs = [ - FakeyMessage({'Header': 'Value'}, (), '', 'Body goes here\r\n', 171, None), - ] - self.expected = { - 0: {'RFC822.TEXT': 'Body goes here\r\n'}, - } - return self._fetchWork(uid) - - def testFetchBodyUID(self): - return self.testFetchBody(1) - - def testFetchBodyParts(self): - self.function = self.client.fetchBodyParts - self.messages = '1' - parts = [1, 2] - outerBody = '' - innerBody1 = 'Contained body message text. Squarge.' - innerBody2 = 'Secondary <i>message</i> text of squarge body.' - headers = util.OrderedDict() - headers['from'] = 'sender@host' - headers['to'] = 'recipient@domain' - headers['subject'] = 'booga booga boo' - headers['content-type'] = 'multipart/alternative; boundary="xyz"' - innerHeaders = util.OrderedDict() - innerHeaders['subject'] = 'this is subject text' - innerHeaders['content-type'] = 'text/plain' - innerHeaders2 = util.OrderedDict() - innerHeaders2['subject'] = '<b>this is subject</b>' - innerHeaders2['content-type'] = 'text/html' - self.msgObjs = [FakeyMessage( - headers, (), None, outerBody, 123, - [FakeyMessage(innerHeaders, (), None, innerBody1, None, None), - FakeyMessage(innerHeaders2, (), None, innerBody2, None, None)])] - self.expected = { - 0: {'1': innerBody1, '2': innerBody2}, - } - - def result(R): - self.result = R - - self.connected.addCallback(lambda _: self.function(self.messages, parts)) - self.connected.addCallback(result) - self.connected.addCallback(self._cbStopClient) - self.connected.addErrback(self._ebGeneral) - - d = loopback.loopbackTCP(self.server, self.client, noisy=False) - d.addCallback(lambda ign: self.assertEquals(self.result, self.expected)) - return d - - - def test_fetchBodyPartOfNonMultipart(self): - """ - Single-part messages have an implicit first part which clients - should be able to retrieve explicitly. Test that a client - requesting part 1 of a text/plain message receives the body of the - text/plain part. - """ - self.function = self.client.fetchBodyParts - self.messages = '1' - parts = [1] - outerBody = 'DA body' - headers = util.OrderedDict() - headers['from'] = 'sender@host' - headers['to'] = 'recipient@domain' - headers['subject'] = 'booga booga boo' - headers['content-type'] = 'text/plain' - self.msgObjs = [FakeyMessage( - headers, (), None, outerBody, 123, None)] - - self.expected = { - 0: {'1': outerBody}, - } - - def result(R): - self.result = R - - self.connected.addCallback(lambda _: self.function(self.messages, parts)) - self.connected.addCallback(result) - self.connected.addCallback(self._cbStopClient) - self.connected.addErrback(self._ebGeneral) - - d = loopback.loopbackTCP(self.server, self.client, noisy=False) - d.addCallback(lambda ign: self.assertEquals(self.result, self.expected)) - return d - - - def testFetchSize(self, uid=0): - self.function = self.client.fetchSize - self.messages = '1:100,2:*' - self.msgObjs = [ - FakeyMessage({}, (), '', 'x' * 20, 123, None), - ] - self.expected = { - 0: {'RFC822.SIZE': '20'}, - } - return self._fetchWork(uid) - - def testFetchSizeUID(self): - return self.testFetchSize(1) - - def testFetchFull(self, uid=0): - self.function = self.client.fetchFull - self.messages = '1,3' - self.msgObjs = [ - FakeyMessage({}, ('\\XYZ', '\\YZX', 'Abc'), - 'Sun, 25 Jul 2010 06:20:30 -0400 (EDT)', - 'xyz' * 2, 654, None), - FakeyMessage({}, ('\\One', '\\Two', 'Three'), - 'Mon, 14 Apr 2003 19:43:44 -0400', - 'abc' * 4, 555, None), - ] - self.expected = { - 0: {'FLAGS': ['\\XYZ', '\\YZX', 'Abc'], - 'INTERNALDATE': '25-Jul-2010 06:20:30 -0400', - 'RFC822.SIZE': '6', - 'ENVELOPE': [None, None, [[None, None, None]], [[None, None, None]], None, None, None, None, None, None], - 'BODY': [None, None, [], None, None, None, '6']}, - 1: {'FLAGS': ['\\One', '\\Two', 'Three'], - 'INTERNALDATE': '14-Apr-2003 19:43:44 -0400', - 'RFC822.SIZE': '12', - 'ENVELOPE': [None, None, [[None, None, None]], [[None, None, None]], None, None, None, None, None, None], - 'BODY': [None, None, [], None, None, None, '12']}, - } - return self._fetchWork(uid) - - def testFetchFullUID(self): - return self.testFetchFull(1) - - def testFetchAll(self, uid=0): - self.function = self.client.fetchAll - self.messages = '1,2:3' - self.msgObjs = [ - FakeyMessage({}, (), 'Mon, 14 Apr 2003 19:43:44 +0400', - 'Lalala', 10101, None), - FakeyMessage({}, (), 'Tue, 15 Apr 2003 19:43:44 +0200', - 'Alalal', 20202, None), - ] - self.expected = { - 0: {'ENVELOPE': [None, None, [[None, None, None]], [[None, None, None]], None, None, None, None, None, None], - 'RFC822.SIZE': '6', - 'INTERNALDATE': '14-Apr-2003 19:43:44 +0400', - 'FLAGS': []}, - 1: {'ENVELOPE': [None, None, [[None, None, None]], [[None, None, None]], None, None, None, None, None, None], - 'RFC822.SIZE': '6', - 'INTERNALDATE': '15-Apr-2003 19:43:44 +0200', - 'FLAGS': []}, - } - return self._fetchWork(uid) - - def testFetchAllUID(self): - return self.testFetchAll(1) - - def testFetchFast(self, uid=0): - self.function = self.client.fetchFast - self.messages = '1' - self.msgObjs = [ - FakeyMessage({}, ('\\X',), '19 Mar 2003 19:22:21 -0500', '', 9, None), - ] - self.expected = { - 0: {'FLAGS': ['\\X'], - 'INTERNALDATE': '19-Mar-2003 19:22:21 -0500', - 'RFC822.SIZE': '0'}, - } - return self._fetchWork(uid) - - def testFetchFastUID(self): - return self.testFetchFast(1) - - -class FetchSearchStoreTestCase(unittest.TestCase, IMAP4HelperMixin): - implements(imap4.ISearchableMailbox) - - def setUp(self): - self.expected = self.result = None - self.server_received_query = None - self.server_received_uid = None - self.server_received_parts = None - self.server_received_messages = None - - self.server = imap4.IMAP4Server() - self.server.state = 'select' - self.server.mbox = self - self.connected = defer.Deferred() - self.client = SimpleClient(self.connected) - - def search(self, query, uid): - self.server_received_query = query - self.server_received_uid = uid - return self.expected - - def addListener(self, *a, **kw): - pass - removeListener = addListener - - def _searchWork(self, uid): - def search(): - return self.client.search(self.query, uid=uid) - def result(R): - self.result = R - - self.connected.addCallback(strip(search) - ).addCallback(result - ).addCallback(self._cbStopClient - ).addErrback(self._ebGeneral) - - def check(ignored): - # Ensure no short-circuiting wierdness is going on - self.failIf(self.result is self.expected) - - self.assertEquals(self.result, self.expected) - self.assertEquals(self.uid, self.server_received_uid) - self.assertEquals( - imap4.parseNestedParens(self.query), - self.server_received_query - ) - d = loopback.loopbackTCP(self.server, self.client, noisy=False) - d.addCallback(check) - return d - - def testSearch(self): - self.query = imap4.Or( - imap4.Query(header=('subject', 'substring')), - imap4.Query(larger=1024, smaller=4096), - ) - self.expected = [1, 4, 5, 7] - self.uid = 0 - return self._searchWork(0) - - def testUIDSearch(self): - self.query = imap4.Or( - imap4.Query(header=('subject', 'substring')), - imap4.Query(larger=1024, smaller=4096), - ) - self.uid = 1 - self.expected = [1, 2, 3] - return self._searchWork(1) - - def getUID(self, msg): - try: - return self.expected[msg]['UID'] - except (TypeError, IndexError): - return self.expected[msg-1] - except KeyError: - return 42 - - def fetch(self, messages, uid): - self.server_received_uid = uid - self.server_received_messages = str(messages) - return self.expected - - def _fetchWork(self, fetch): - def result(R): - self.result = R - - self.connected.addCallback(strip(fetch) - ).addCallback(result - ).addCallback(self._cbStopClient - ).addErrback(self._ebGeneral) - - def check(ignored): - # Ensure no short-circuiting wierdness is going on - self.failIf(self.result is self.expected) - - self.parts and self.parts.sort() - self.server_received_parts and self.server_received_parts.sort() - - if self.uid: - for (k, v) in self.expected.items(): - v['UID'] = str(k) - - self.assertEquals(self.result, self.expected) - self.assertEquals(self.uid, self.server_received_uid) - self.assertEquals(self.parts, self.server_received_parts) - self.assertEquals(imap4.parseIdList(self.messages), - imap4.parseIdList(self.server_received_messages)) - - d = loopback.loopbackTCP(self.server, self.client, noisy=False) - d.addCallback(check) - return d - -class FakeMailbox: - def __init__(self): - self.args = [] - def addMessage(self, body, flags, date): - self.args.append((body, flags, date)) - return defer.succeed(None) - -class FeaturefulMessage: - implements(imap4.IMessageFile) - - def getFlags(self): - return 'flags' - - def getInternalDate(self): - return 'internaldate' - - def open(self): - return StringIO("open") - -class MessageCopierMailbox: - implements(imap4.IMessageCopier) - - def __init__(self): - self.msgs = [] - - def copy(self, msg): - self.msgs.append(msg) - return len(self.msgs) - -class CopyWorkerTestCase(unittest.TestCase): - def testFeaturefulMessage(self): - s = imap4.IMAP4Server() - - # Yes. I am grabbing this uber-non-public method to test it. - # It is complex. It needs to be tested directly! - # Perhaps it should be refactored, simplified, or split up into - # not-so-private components, but that is a task for another day. - - # Ha ha! Addendum! Soon it will be split up, and this test will - # be re-written to just use the default adapter for IMailbox to - # IMessageCopier and call .copy on that adapter. - f = s._IMAP4Server__cbCopy - - m = FakeMailbox() - d = f([(i, FeaturefulMessage()) for i in range(1, 11)], 'tag', m) - - def cbCopy(results): - for a in m.args: - self.assertEquals(a[0].read(), "open") - self.assertEquals(a[1], "flags") - self.assertEquals(a[2], "internaldate") - - for (status, result) in results: - self.failUnless(status) - self.assertEquals(result, None) - - return d.addCallback(cbCopy) - - - def testUnfeaturefulMessage(self): - s = imap4.IMAP4Server() - - # See above comment - f = s._IMAP4Server__cbCopy - - m = FakeMailbox() - msgs = [FakeyMessage({'Header-Counter': str(i)}, (), 'Date', 'Body %d' % (i,), i + 10, None) for i in range(1, 11)] - d = f([im for im in zip(range(1, 11), msgs)], 'tag', m) - - def cbCopy(results): - seen = [] - for a in m.args: - seen.append(a[0].read()) - self.assertEquals(a[1], ()) - self.assertEquals(a[2], "Date") - - seen.sort() - exp = ["Header-Counter: %d\r\n\r\nBody %d" % (i, i) for i in range(1, 11)] - exp.sort() - self.assertEquals(seen, exp) - - for (status, result) in results: - self.failUnless(status) - self.assertEquals(result, None) - - return d.addCallback(cbCopy) - - def testMessageCopier(self): - s = imap4.IMAP4Server() - - # See above comment - f = s._IMAP4Server__cbCopy - - m = MessageCopierMailbox() - msgs = [object() for i in range(1, 11)] - d = f([im for im in zip(range(1, 11), msgs)], 'tag', m) - - def cbCopy(results): - self.assertEquals(results, zip([1] * 10, range(1, 11))) - for (orig, new) in zip(msgs, m.msgs): - self.assertIdentical(orig, new) - - return d.addCallback(cbCopy) - - -class TLSTestCase(IMAP4HelperMixin, unittest.TestCase): - serverCTX = ServerTLSContext and ServerTLSContext() - clientCTX = ClientTLSContext and ClientTLSContext() - - def loopback(self): - return loopback.loopbackTCP(self.server, self.client, noisy=False) - - def testAPileOfThings(self): - SimpleServer.theAccount.addMailbox('inbox') - called = [] - def login(): - called.append(None) - return self.client.login('testuser', 'password-test') - def list(): - called.append(None) - return self.client.list('inbox', '%') - def status(): - called.append(None) - return self.client.status('inbox', 'UIDNEXT') - def examine(): - called.append(None) - return self.client.examine('inbox') - def logout(): - called.append(None) - return self.client.logout() - - self.client.requireTransportSecurity = True - - methods = [login, list, status, examine, logout] - map(self.connected.addCallback, map(strip, methods)) - self.connected.addCallbacks(self._cbStopClient, self._ebGeneral) - def check(ignored): - self.assertEquals(self.server.startedTLS, True) - self.assertEquals(self.client.startedTLS, True) - self.assertEquals(len(called), len(methods)) - d = self.loopback() - d.addCallback(check) - return d - - def testLoginLogin(self): - self.server.checker.addUser('testuser', 'password-test') - success = [] - self.client.registerAuthenticator(imap4.LOGINAuthenticator('testuser')) - self.connected.addCallback( - lambda _: self.client.authenticate('password-test') - ).addCallback( - lambda _: self.client.logout() - ).addCallback(success.append - ).addCallback(self._cbStopClient - ).addErrback(self._ebGeneral) - - d = self.loopback() - d.addCallback(lambda x : self.assertEquals(len(success), 1)) - return d - - def testStartTLS(self): - success = [] - self.connected.addCallback(lambda _: self.client.startTLS()) - self.connected.addCallback(lambda _: self.assertNotEquals(-1, self.client.transport.__class__.__name__.find('TLS'))) - self.connected.addCallback(self._cbStopClient) - self.connected.addCallback(success.append) - self.connected.addErrback(self._ebGeneral) - - d = self.loopback() - d.addCallback(lambda x : self.failUnless(success)) - return d - - def testFailedStartTLS(self): - failure = [] - def breakServerTLS(ign): - self.server.canStartTLS = False - - self.connected.addCallback(breakServerTLS) - self.connected.addCallback(lambda ign: self.client.startTLS()) - self.connected.addErrback(lambda err: failure.append(err.trap(imap4.IMAP4Exception))) - self.connected.addCallback(self._cbStopClient) - self.connected.addErrback(self._ebGeneral) - - def check(ignored): - self.failUnless(failure) - self.assertIdentical(failure[0], imap4.IMAP4Exception) - return self.loopback().addCallback(check) - - - -class SlowMailbox(SimpleMailbox): - howSlow = 2 - callLater = None - fetchDeferred = None - - # Not a very nice implementation of fetch(), but it'll - # do for the purposes of testing. - def fetch(self, messages, uid): - d = defer.Deferred() - self.callLater(self.howSlow, d.callback, ()) - self.fetchDeferred.callback(None) - return d - -class Timeout(IMAP4HelperMixin, unittest.TestCase): - - def test_serverTimeout(self): - """ - The *client* has a timeout mechanism which will close connections that - are inactive for a period. - """ - c = Clock() - self.server.timeoutTest = True - self.client.timeout = 5 #seconds - self.client.callLater = c.callLater - self.selectedArgs = None - - def login(): - d = self.client.login('testuser', 'password-test') - c.advance(5) - d.addErrback(timedOut) - return d - - def timedOut(failure): - self._cbStopClient(None) - failure.trap(error.TimeoutError) - - d = self.connected.addCallback(strip(login)) - d.addErrback(self._ebGeneral) - return defer.gatherResults([d, self.loopback()]) - - - def test_longFetchDoesntTimeout(self): - """ - The connection timeout does not take effect during fetches. - """ - c = Clock() - SlowMailbox.callLater = c.callLater - SlowMailbox.fetchDeferred = defer.Deferred() - self.server.callLater = c.callLater - SimpleServer.theAccount.mailboxFactory = SlowMailbox - SimpleServer.theAccount.addMailbox('mailbox-test') - - self.server.setTimeout(1) - - def login(): - return self.client.login('testuser', 'password-test') - def select(): - self.server.setTimeout(1) - return self.client.select('mailbox-test') - def fetch(): - return self.client.fetchUID('1:*') - def stillConnected(): - self.assertNotEquals(self.server.state, 'timeout') - - def cbAdvance(ignored): - for i in xrange(4): - c.advance(.5) - - SlowMailbox.fetchDeferred.addCallback(cbAdvance) - - d1 = self.connected.addCallback(strip(login)) - d1.addCallback(strip(select)) - d1.addCallback(strip(fetch)) - d1.addCallback(strip(stillConnected)) - d1.addCallback(self._cbStopClient) - d1.addErrback(self._ebGeneral) - d = defer.gatherResults([d1, self.loopback()]) - return d - - - def test_idleClientDoesDisconnect(self): - """ - The *server* has a timeout mechanism which will close connections that - are inactive for a period. - """ - c = Clock() - # Hook up our server protocol - transport = StringTransportWithDisconnection() - transport.protocol = self.server - self.server.callLater = c.callLater - self.server.makeConnection(transport) - - # Make sure we can notice when the connection goes away - lost = [] - connLost = self.server.connectionLost - self.server.connectionLost = lambda reason: (lost.append(None), connLost(reason))[1] - - # 2/3rds of the idle timeout elapses... - c.pump([0.0] + [self.server.timeOut / 3.0] * 2) - self.failIf(lost, lost) - - # Now some more - c.pump([0.0, self.server.timeOut / 2.0]) - self.failUnless(lost) - - - -class Disconnection(unittest.TestCase): - def testClientDisconnectFailsDeferreds(self): - c = imap4.IMAP4Client() - t = StringTransportWithDisconnection() - c.makeConnection(t) - d = self.assertFailure(c.login('testuser', 'example.com'), error.ConnectionDone) - c.connectionLost(error.ConnectionDone("Connection closed")) - return d - - - -class SynchronousMailbox(object): - """ - Trivial, in-memory mailbox implementation which can produce a message - synchronously. - """ - def __init__(self, messages): - self.messages = messages - - - def fetch(self, msgset, uid): - assert not uid, "Cannot handle uid requests." - for msg in msgset: - yield msg, self.messages[msg - 1] - - - -class StringTransportConsumer(StringTransport): - producer = None - streaming = None - - def registerProducer(self, producer, streaming): - self.producer = producer - self.streaming = streaming - - - -class Pipelining(unittest.TestCase): - """ - Tests for various aspects of the IMAP4 server's pipelining support. - """ - messages = [ - FakeyMessage({}, [], '', '0', None, None), - FakeyMessage({}, [], '', '1', None, None), - FakeyMessage({}, [], '', '2', None, None), - ] - - def setUp(self): - self.iterators = [] - - self.transport = StringTransportConsumer() - self.server = imap4.IMAP4Server(None, None, self.iterateInReactor) - self.server.makeConnection(self.transport) - - - def iterateInReactor(self, iterator): - d = defer.Deferred() - self.iterators.append((iterator, d)) - return d - - - def tearDown(self): - self.server.connectionLost(failure.Failure(error.ConnectionDone())) - - - def test_synchronousFetch(self): - """ - Test that pipelined FETCH commands which can be responded to - synchronously are responded to correctly. - """ - mailbox = SynchronousMailbox(self.messages) - - # Skip over authentication and folder selection - self.server.state = 'select' - self.server.mbox = mailbox - - # Get rid of any greeting junk - self.transport.clear() - - # Here's some pipelined stuff - self.server.dataReceived( - '01 FETCH 1 BODY[]\r\n' - '02 FETCH 2 BODY[]\r\n' - '03 FETCH 3 BODY[]\r\n') - - # Flush anything the server has scheduled to run - while self.iterators: - for e in self.iterators[0][0]: - break - else: - self.iterators.pop(0)[1].callback(None) - - # The bodies are empty because we aren't simulating a transport - # exactly correctly (we have StringTransportConsumer but we never - # call resumeProducing on its producer). It doesn't matter: just - # make sure the surrounding structure is okay, and that no - # exceptions occurred. - self.assertEquals( - self.transport.value(), - '* 1 FETCH (BODY[] )\r\n' - '01 OK FETCH completed\r\n' - '* 2 FETCH (BODY[] )\r\n' - '02 OK FETCH completed\r\n' - '* 3 FETCH (BODY[] )\r\n' - '03 OK FETCH completed\r\n') - - - -if ClientTLSContext is None: - for case in (TLSTestCase,): - case.skip = "OpenSSL not present" -elif interfaces.IReactorSSL(reactor, None) is None: - for case in (TLSTestCase,): - case.skip = "Reactor doesn't support SSL" diff --git a/tools/buildbot/pylibs/twisted/mail/test/test_mail.py b/tools/buildbot/pylibs/twisted/mail/test/test_mail.py deleted file mode 100644 index 5a888c8..0000000 --- a/tools/buildbot/pylibs/twisted/mail/test/test_mail.py +++ /dev/null @@ -1,1863 +0,0 @@ -# Copyright (c) 2001-2008 Twisted Matrix Laboratories. -# See LICENSE for details. - -""" -Tests for large portions of L{twisted.mail}. -""" - -import os -import errno -import md5 -import shutil -import pickle -import StringIO -import rfc822 -import tempfile -import signal - -from zope.interface import Interface, implements - -from twisted.trial import unittest -from twisted.mail import smtp -from twisted.mail import pop3 -from twisted.names import dns -from twisted.internet import protocol -from twisted.internet import defer -from twisted.internet.defer import Deferred -from twisted.internet import reactor -from twisted.internet import interfaces -from twisted.internet import task -from twisted.internet.error import DNSLookupError, CannotListenError -from twisted.internet.error import ProcessDone, ProcessTerminated -from twisted.internet import address -from twisted.python import failure -from twisted.python.filepath import FilePath - -from twisted import mail -import twisted.mail.mail -import twisted.mail.maildir -import twisted.mail.relay -import twisted.mail.relaymanager -import twisted.mail.protocols -import twisted.mail.alias - -from twisted.names.error import DNSNameError -from twisted.names.dns import RRHeader, Record_CNAME, Record_MX - -from twisted import cred -import twisted.cred.credentials -import twisted.cred.checkers -import twisted.cred.portal - -from twisted.test.proto_helpers import LineSendingProtocol - -class DomainWithDefaultsTestCase(unittest.TestCase): - def testMethods(self): - d = dict([(x, x + 10) for x in range(10)]) - d = mail.mail.DomainWithDefaultDict(d, 'Default') - - self.assertEquals(len(d), 10) - self.assertEquals(list(iter(d)), range(10)) - self.assertEquals(list(d.iterkeys()), list(iter(d))) - - items = list(d.iteritems()) - items.sort() - self.assertEquals(items, [(x, x + 10) for x in range(10)]) - - values = list(d.itervalues()) - values.sort() - self.assertEquals(values, range(10, 20)) - - items = d.items() - items.sort() - self.assertEquals(items, [(x, x + 10) for x in range(10)]) - - values = d.values() - values.sort() - self.assertEquals(values, range(10, 20)) - - for x in range(10): - self.assertEquals(d[x], x + 10) - self.assertEquals(d.get(x), x + 10) - self.failUnless(x in d) - self.failUnless(d.has_key(x)) - - del d[2], d[4], d[6] - - self.assertEquals(len(d), 7) - self.assertEquals(d[2], 'Default') - self.assertEquals(d[4], 'Default') - self.assertEquals(d[6], 'Default') - - d.update({'a': None, 'b': (), 'c': '*'}) - self.assertEquals(len(d), 10) - self.assertEquals(d['a'], None) - self.assertEquals(d['b'], ()) - self.assertEquals(d['c'], '*') - - d.clear() - self.assertEquals(len(d), 0) - - self.assertEquals(d.setdefault('key', 'value'), 'value') - self.assertEquals(d['key'], 'value') - - self.assertEquals(d.popitem(), ('key', 'value')) - self.assertEquals(len(d), 0) - - dcopy = d.copy() - self.assertEquals(d.domains, dcopy.domains) - self.assertEquals(d.default, dcopy.default) - - - def _stringificationTest(self, stringifier): - """ - Assert that the class name of a L{mail.mail.DomainWithDefaultDict} - instance and the string-formatted underlying domain dictionary both - appear in the string produced by the given string-returning function. - - @type stringifier: one-argument callable - @param stringifier: either C{str} or C{repr}, to be used to get a - string to make assertions against. - """ - domain = mail.mail.DomainWithDefaultDict({}, 'Default') - self.assertIn(domain.__class__.__name__, stringifier(domain)) - domain['key'] = 'value' - self.assertIn(str({'key': 'value'}), stringifier(domain)) - - - def test_str(self): - """ - L{DomainWithDefaultDict.__str__} should return a string including - the class name and the domain mapping held by the instance. - """ - self._stringificationTest(str) - - - def test_repr(self): - """ - L{DomainWithDefaultDict.__repr__} should return a string including - the class name and the domain mapping held by the instance. - """ - self._stringificationTest(repr) - - - -class BounceTestCase(unittest.TestCase): - def setUp(self): - self.domain = mail.mail.BounceDomain() - - def testExists(self): - self.assertRaises(smtp.AddressError, self.domain.exists, "any user") - - def testRelay(self): - self.assertEquals( - self.domain.willRelay("random q emailer", "protocol"), - False - ) - - def testMessage(self): - self.assertRaises(NotImplementedError, self.domain.startMessage, "whomever") - - def testAddUser(self): - self.domain.addUser("bob", "password") - self.assertRaises(smtp.SMTPBadRcpt, self.domain.exists, "bob") - -class FileMessageTestCase(unittest.TestCase): - def setUp(self): - self.name = "fileMessage.testFile" - self.final = "final.fileMessage.testFile" - self.f = file(self.name, 'w') - self.fp = mail.mail.FileMessage(self.f, self.name, self.final) - - def tearDown(self): - try: - self.f.close() - except: - pass - try: - os.remove(self.name) - except: - pass - try: - os.remove(self.final) - except: - pass - - def testFinalName(self): - return self.fp.eomReceived().addCallback(self._cbFinalName) - - def _cbFinalName(self, result): - self.assertEquals(result, self.final) - self.failUnless(self.f.closed) - self.failIf(os.path.exists(self.name)) - - def testContents(self): - contents = "first line\nsecond line\nthird line\n" - for line in contents.splitlines(): - self.fp.lineReceived(line) - self.fp.eomReceived() - self.assertEquals(file(self.final).read(), contents) - - def testInterrupted(self): - contents = "first line\nsecond line\n" - for line in contents.splitlines(): - self.fp.lineReceived(line) - self.fp.connectionLost() - self.failIf(os.path.exists(self.name)) - self.failIf(os.path.exists(self.final)) - -class MailServiceTestCase(unittest.TestCase): - def setUp(self): - self.service = mail.mail.MailService() - - def testFactories(self): - f = self.service.getPOP3Factory() - self.failUnless(isinstance(f, protocol.ServerFactory)) - self.failUnless(f.buildProtocol(('127.0.0.1', 12345)), pop3.POP3) - - f = self.service.getSMTPFactory() - self.failUnless(isinstance(f, protocol.ServerFactory)) - self.failUnless(f.buildProtocol(('127.0.0.1', 12345)), smtp.SMTP) - - f = self.service.getESMTPFactory() - self.failUnless(isinstance(f, protocol.ServerFactory)) - self.failUnless(f.buildProtocol(('127.0.0.1', 12345)), smtp.ESMTP) - - def testPortals(self): - o1 = object() - o2 = object() - self.service.portals['domain'] = o1 - self.service.portals[''] = o2 - - self.failUnless(self.service.lookupPortal('domain') is o1) - self.failUnless(self.service.defaultPortal() is o2) - -class FailingMaildirMailboxAppendMessageTask(mail.maildir._MaildirMailboxAppendMessageTask): - _openstate = True - _writestate = True - _renamestate = True - def osopen(self, fn, attr, mode): - if self._openstate: - return os.open(fn, attr, mode) - else: - raise OSError(errno.EPERM, "Faked Permission Problem") - def oswrite(self, fh, data): - if self._writestate: - return os.write(fh, data) - else: - raise OSError(errno.ENOSPC, "Faked Space problem") - def osrename(self, oldname, newname): - if self._renamestate: - return os.rename(oldname, newname) - else: - raise OSError(errno.EPERM, "Faked Permission Problem") - -class MaildirAppendStringTestCase(unittest.TestCase): - def setUp(self): - self.d = self.mktemp() - mail.maildir.initializeMaildir(self.d) - - def tearDown(self): - shutil.rmtree(self.d) - - def _append(self, ignored, mbox): - d = mbox.appendMessage('TEST') - return self.assertFailure(d, Exception) - - def _setState(self, ignored, mbox, rename=None, write=None, open=None): - if rename is not None: - mbox.AppendFactory._renameState = rename - if write is not None: - mbox.AppendFactory._writeState = write - if open is not None: - mbox.AppendFactory._openstate = open - - def testAppend(self): - mbox = mail.maildir.MaildirMailbox(self.d) - mbox.AppendFactory = FailingMaildirMailboxAppendMessageTask - ds = [] - for i in xrange(1, 11): - ds.append(mbox.appendMessage("X" * i)) - ds[-1].addCallback(self.assertEqual, None) - d = defer.gatherResults(ds) - d.addCallback(self._cbTestAppend, mbox) - return d - - def _cbTestAppend(self, result, mbox): - self.assertEquals(len(mbox.listMessages()), - 10) - self.assertEquals(len(mbox.getMessage(5).read()), 6) - # test in the right order: last to first error location. - mbox.AppendFactory._renamestate = False - d = self._append(None, mbox) - d.addCallback(self._setState, mbox, rename=True, write=False) - d.addCallback(self._append, mbox) - d.addCallback(self._setState, mbox, write=True, open=False) - d.addCallback(self._append, mbox) - d.addCallback(self._setState, mbox, open=True) - return d - - -class MaildirAppendFileTestCase(unittest.TestCase): - def setUp(self): - self.d = self.mktemp() - mail.maildir.initializeMaildir(self.d) - - def tearDown(self): - shutil.rmtree(self.d) - - def testAppend(self): - mbox = mail.maildir.MaildirMailbox(self.d) - ds = [] - def _check(res, t): - t.close() - self.assertEqual(res, None) - for i in xrange(1, 11): - temp = tempfile.TemporaryFile() - temp.write("X" * i) - temp.seek(0,0) - ds.append(mbox.appendMessage(temp)) - ds[-1].addCallback(_check, temp) - return defer.gatherResults(ds).addCallback(self._cbTestAppend, mbox) - - def _cbTestAppend(self, result, mbox): - self.assertEquals(len(mbox.listMessages()), - 10) - self.assertEquals(len(mbox.getMessage(5).read()), 6) - - -class MaildirTestCase(unittest.TestCase): - def setUp(self): - self.d = self.mktemp() - mail.maildir.initializeMaildir(self.d) - - def tearDown(self): - shutil.rmtree(self.d) - - def testInitializer(self): - d = self.d - trash = os.path.join(d, '.Trash') - - self.failUnless(os.path.exists(d) and os.path.isdir(d)) - self.failUnless(os.path.exists(os.path.join(d, 'new'))) - self.failUnless(os.path.exists(os.path.join(d, 'cur'))) - self.failUnless(os.path.exists(os.path.join(d, 'tmp'))) - self.failUnless(os.path.isdir(os.path.join(d, 'new'))) - self.failUnless(os.path.isdir(os.path.join(d, 'cur'))) - self.failUnless(os.path.isdir(os.path.join(d, 'tmp'))) - - self.failUnless(os.path.exists(os.path.join(trash, 'new'))) - self.failUnless(os.path.exists(os.path.join(trash, 'cur'))) - self.failUnless(os.path.exists(os.path.join(trash, 'tmp'))) - self.failUnless(os.path.isdir(os.path.join(trash, 'new'))) - self.failUnless(os.path.isdir(os.path.join(trash, 'cur'))) - self.failUnless(os.path.isdir(os.path.join(trash, 'tmp'))) - - def testMailbox(self): - j = os.path.join - n = mail.maildir._generateMaildirName - msgs = [j(b, n()) for b in ('cur', 'new') for x in range(5)] - - # Toss a few files into the mailbox - i = 1 - for f in msgs: - f = file(j(self.d, f), 'w') - f.write('x' * i) - f.close() - i = i + 1 - - mb = mail.maildir.MaildirMailbox(self.d) - self.assertEquals(mb.listMessages(), range(1, 11)) - self.assertEquals(mb.listMessages(1), 2) - self.assertEquals(mb.listMessages(5), 6) - - self.assertEquals(mb.getMessage(6).read(), 'x' * 7) - self.assertEquals(mb.getMessage(1).read(), 'x' * 2) - - d = {} - for i in range(10): - u = mb.getUidl(i) - self.failIf(u in d) - d[u] = None - - p, f = os.path.split(msgs[5]) - - mb.deleteMessage(5) - self.assertEquals(mb.listMessages(5), 0) - self.failUnless(os.path.exists(j(self.d, '.Trash', 'cur', f))) - self.failIf(os.path.exists(j(self.d, msgs[5]))) - - mb.undeleteMessages() - self.assertEquals(mb.listMessages(5), 6) - self.failIf(os.path.exists(j(self.d, '.Trash', 'cur', f))) - self.failUnless(os.path.exists(j(self.d, msgs[5]))) - -class MaildirDirdbmDomainTestCase(unittest.TestCase): - def setUp(self): - self.P = self.mktemp() - self.S = mail.mail.MailService() - self.D = mail.maildir.MaildirDirdbmDomain(self.S, self.P) - - def tearDown(self): - shutil.rmtree(self.P) - - def testAddUser(self): - toAdd = (('user1', 'pwd1'), ('user2', 'pwd2'), ('user3', 'pwd3')) - for (u, p) in toAdd: - self.D.addUser(u, p) - - for (u, p) in toAdd: - self.failUnless(u in self.D.dbm) - self.assertEquals(self.D.dbm[u], p) - self.failUnless(os.path.exists(os.path.join(self.P, u))) - - def testCredentials(self): - creds = self.D.getCredentialsCheckers() - - self.assertEquals(len(creds), 1) - self.failUnless(cred.checkers.ICredentialsChecker.providedBy(creds[0])) - self.failUnless(cred.credentials.IUsernamePassword in creds[0].credentialInterfaces) - - def testRequestAvatar(self): - class ISomething(Interface): - pass - - self.D.addUser('user', 'password') - self.assertRaises( - NotImplementedError, - self.D.requestAvatar, 'user', None, ISomething - ) - - t = self.D.requestAvatar('user', None, pop3.IMailbox) - self.assertEquals(len(t), 3) - self.failUnless(t[0] is pop3.IMailbox) - self.failUnless(pop3.IMailbox.providedBy(t[1])) - - t[2]() - - def testRequestAvatarId(self): - self.D.addUser('user', 'password') - database = self.D.getCredentialsCheckers()[0] - - creds = cred.credentials.UsernamePassword('user', 'wrong password') - self.assertRaises( - cred.error.UnauthorizedLogin, - database.requestAvatarId, creds - ) - - creds = cred.credentials.UsernamePassword('user', 'password') - self.assertEquals(database.requestAvatarId(creds), 'user') - - -class StubAliasableDomain(object): - """ - Minimal testable implementation of IAliasableDomain. - """ - implements(mail.mail.IAliasableDomain) - - def exists(self, user): - """ - No test coverage for invocations of this method on domain objects, - so we just won't implement it. - """ - raise NotImplementedError() - - - def addUser(self, user, password): - """ - No test coverage for invocations of this method on domain objects, - so we just won't implement it. - """ - raise NotImplementedError() - - - def getCredentialsCheckers(self): - """ - This needs to succeed in order for other tests to complete - successfully, but we don't actually assert anything about its - behavior. Return an empty list. Sometime later we should return - something else and assert that a portal got set up properly. - """ - return [] - - - def setAliasGroup(self, aliases): - """ - Just record the value so the test can check it later. - """ - self.aliasGroup = aliases - - -class ServiceDomainTestCase(unittest.TestCase): - def setUp(self): - self.S = mail.mail.MailService() - self.D = mail.protocols.DomainDeliveryBase(self.S, None) - self.D.service = self.S - self.D.protocolName = 'TEST' - self.D.host = 'hostname' - - self.tmpdir = self.mktemp() - domain = mail.maildir.MaildirDirdbmDomain(self.S, self.tmpdir) - domain.addUser('user', 'password') - self.S.addDomain('test.domain', domain) - - def tearDown(self): - shutil.rmtree(self.tmpdir) - - - def testAddAliasableDomain(self): - """ - Test that adding an IAliasableDomain to a mail service properly sets - up alias group references and such. - """ - aliases = object() - domain = StubAliasableDomain() - self.S.aliases = aliases - self.S.addDomain('example.com', domain) - self.assertIdentical(domain.aliasGroup, aliases) - - - def testReceivedHeader(self): - hdr = self.D.receivedHeader( - ('remotehost', '123.232.101.234'), - smtp.Address('<someguy@somplace>'), - ['user@host.name'] - ) - fp = StringIO.StringIO(hdr) - m = rfc822.Message(fp) - self.assertEquals(len(m.items()), 1) - self.failUnless(m.has_key('Received')) - - def testValidateTo(self): - user = smtp.User('user@test.domain', 'helo', None, 'wherever@whatever') - return defer.maybeDeferred(self.D.validateTo, user - ).addCallback(self._cbValidateTo - ) - - def _cbValidateTo(self, result): - self.failUnless(callable(result)) - - def testValidateToBadUsername(self): - user = smtp.User('resu@test.domain', 'helo', None, 'wherever@whatever') - return self.assertFailure( - defer.maybeDeferred(self.D.validateTo, user), - smtp.SMTPBadRcpt) - - def testValidateToBadDomain(self): - user = smtp.User('user@domain.test', 'helo', None, 'wherever@whatever') - return self.assertFailure( - defer.maybeDeferred(self.D.validateTo, user), - smtp.SMTPBadRcpt) - - def testValidateFrom(self): - helo = ('hostname', '127.0.0.1') - origin = smtp.Address('<user@hostname>') - self.failUnless(self.D.validateFrom(helo, origin) is origin) - - helo = ('hostname', '1.2.3.4') - origin = smtp.Address('<user@hostname>') - self.failUnless(self.D.validateFrom(helo, origin) is origin) - - helo = ('hostname', '1.2.3.4') - origin = smtp.Address('<>') - self.failUnless(self.D.validateFrom(helo, origin) is origin) - - self.assertRaises( - smtp.SMTPBadSender, - self.D.validateFrom, None, origin - ) - -class VirtualPOP3TestCase(unittest.TestCase): - def setUp(self): - self.tmpdir = self.mktemp() - self.S = mail.mail.MailService() - self.D = mail.maildir.MaildirDirdbmDomain(self.S, self.tmpdir) - self.D.addUser('user', 'password') - self.S.addDomain('test.domain', self.D) - - portal = cred.portal.Portal(self.D) - map(portal.registerChecker, self.D.getCredentialsCheckers()) - self.S.portals[''] = self.S.portals['test.domain'] = portal - - self.P = mail.protocols.VirtualPOP3() - self.P.service = self.S - self.P.magic = '<unit test magic>' - - def tearDown(self): - shutil.rmtree(self.tmpdir) - - def testAuthenticateAPOP(self): - resp = md5.new(self.P.magic + 'password').hexdigest() - return self.P.authenticateUserAPOP('user', resp - ).addCallback(self._cbAuthenticateAPOP - ) - - def _cbAuthenticateAPOP(self, result): - self.assertEquals(len(result), 3) - self.assertEquals(result[0], pop3.IMailbox) - self.failUnless(pop3.IMailbox.providedBy(result[1])) - result[2]() - - def testAuthenticateIncorrectUserAPOP(self): - resp = md5.new(self.P.magic + 'password').hexdigest() - return self.assertFailure( - self.P.authenticateUserAPOP('resu', resp), - cred.error.UnauthorizedLogin) - - def testAuthenticateIncorrectResponseAPOP(self): - resp = md5.new('wrong digest').hexdigest() - return self.assertFailure( - self.P.authenticateUserAPOP('user', resp), - cred.error.UnauthorizedLogin) - - def testAuthenticatePASS(self): - return self.P.authenticateUserPASS('user', 'password' - ).addCallback(self._cbAuthenticatePASS - ) - - def _cbAuthenticatePASS(self, result): - self.assertEquals(len(result), 3) - self.assertEquals(result[0], pop3.IMailbox) - self.failUnless(pop3.IMailbox.providedBy(result[1])) - result[2]() - - def testAuthenticateBadUserPASS(self): - return self.assertFailure( - self.P.authenticateUserPASS('resu', 'password'), - cred.error.UnauthorizedLogin) - - def testAuthenticateBadPasswordPASS(self): - return self.assertFailure( - self.P.authenticateUserPASS('user', 'wrong password'), - cred.error.UnauthorizedLogin) - -class empty(smtp.User): - def __init__(self): - pass - -class RelayTestCase(unittest.TestCase): - def testExists(self): - service = mail.mail.MailService() - domain = mail.relay.DomainQueuer(service) - - doRelay = [ - address.UNIXAddress('/var/run/mail-relay'), - address.IPv4Address('TCP', '127.0.0.1', 12345), - ] - - dontRelay = [ - address.IPv4Address('TCP', '192.168.2.1', 62), - address.IPv4Address('TCP', '1.2.3.4', 1943), - ] - - for peer in doRelay: - user = empty() - user.orig = 'user@host' - user.dest = 'tsoh@resu' - user.protocol = empty() - user.protocol.transport = empty() - user.protocol.transport.getPeer = lambda: peer - - self.failUnless(callable(domain.exists(user))) - - for peer in dontRelay: - user = empty() - user.orig = 'some@place' - user.protocol = empty() - user.protocol.transport = empty() - user.protocol.transport.getPeer = lambda: peer - user.dest = 'who@cares' - - self.assertRaises(smtp.SMTPBadRcpt, domain.exists, user) - -class RelayerTestCase(unittest.TestCase): - def setUp(self): - self.tmpdir = self.mktemp() - os.mkdir(self.tmpdir) - self.messageFiles = [] - for i in range(10): - name = os.path.join(self.tmpdir, 'body-%d' % (i,)) - f = file(name + '-H', 'w') - pickle.dump(['from-%d' % (i,), 'to-%d' % (i,)], f) - f.close() - - f = file(name + '-D', 'w') - f.write(name) - f.seek(0, 0) - self.messageFiles.append(name) - - self.R = mail.relay.RelayerMixin() - self.R.loadMessages(self.messageFiles) - - def tearDown(self): - shutil.rmtree(self.tmpdir) - - def testMailFrom(self): - for i in range(10): - self.assertEquals(self.R.getMailFrom(), 'from-%d' % (i,)) - self.R.sentMail(250, None, None, None, None) - self.assertEquals(self.R.getMailFrom(), None) - - def testMailTo(self): - for i in range(10): - self.assertEquals(self.R.getMailTo(), ['to-%d' % (i,)]) - self.R.sentMail(250, None, None, None, None) - self.assertEquals(self.R.getMailTo(), None) - - def testMailData(self): - for i in range(10): - name = os.path.join(self.tmpdir, 'body-%d' % (i,)) - self.assertEquals(self.R.getMailData().read(), name) - self.R.sentMail(250, None, None, None, None) - self.assertEquals(self.R.getMailData(), None) - -class Manager: - def __init__(self): - self.success = [] - self.failure = [] - self.done = [] - - def notifySuccess(self, factory, message): - self.success.append((factory, message)) - - def notifyFailure(self, factory, message): - self.failure.append((factory, message)) - - def notifyDone(self, factory): - self.done.append(factory) - -class ManagedRelayerTestCase(unittest.TestCase): - def setUp(self): - self.manager = Manager() - self.messages = range(0, 20, 2) - self.factory = object() - self.relay = mail.relaymanager.ManagedRelayerMixin(self.manager) - self.relay.messages = self.messages[:] - self.relay.names = self.messages[:] - self.relay.factory = self.factory - - def testSuccessfulSentMail(self): - for i in self.messages: - self.relay.sentMail(250, None, None, None, None) - - self.assertEquals( - self.manager.success, - [(self.factory, m) for m in self.messages] - ) - - def testFailedSentMail(self): - for i in self.messages: - self.relay.sentMail(550, None, None, None, None) - - self.assertEquals( - self.manager.failure, - [(self.factory, m) for m in self.messages] - ) - - def testConnectionLost(self): - self.relay.connectionLost(failure.Failure(Exception())) - self.assertEquals(self.manager.done, [self.factory]) - -class DirectoryQueueTestCase(unittest.TestCase): - def setUp(self): - # This is almost a test case itself. - self.tmpdir = self.mktemp() - os.mkdir(self.tmpdir) - self.queue = mail.relaymanager.Queue(self.tmpdir) - self.queue.noisy = False - for m in range(25): - hdrF, msgF = self.queue.createNewMessage() - pickle.dump(['header', m], hdrF) - hdrF.close() - msgF.lineReceived('body: %d' % (m,)) - msgF.eomReceived() - self.queue.readDirectory() - - def tearDown(self): - shutil.rmtree(self.tmpdir) - - def testWaiting(self): - self.failUnless(self.queue.hasWaiting()) - self.assertEquals(len(self.queue.getWaiting()), 25) - - waiting = self.queue.getWaiting() - self.queue.setRelaying(waiting[0]) - self.assertEquals(len(self.queue.getWaiting()), 24) - - self.queue.setWaiting(waiting[0]) - self.assertEquals(len(self.queue.getWaiting()), 25) - - def testRelaying(self): - for m in self.queue.getWaiting(): - self.queue.setRelaying(m) - self.assertEquals( - len(self.queue.getRelayed()), - 25 - len(self.queue.getWaiting()) - ) - - self.failIf(self.queue.hasWaiting()) - - relayed = self.queue.getRelayed() - self.queue.setWaiting(relayed[0]) - self.assertEquals(len(self.queue.getWaiting()), 1) - self.assertEquals(len(self.queue.getRelayed()), 24) - - def testDone(self): - msg = self.queue.getWaiting()[0] - self.queue.setRelaying(msg) - self.queue.done(msg) - - self.assertEquals(len(self.queue.getWaiting()), 24) - self.assertEquals(len(self.queue.getRelayed()), 0) - - self.failIf(msg in self.queue.getWaiting()) - self.failIf(msg in self.queue.getRelayed()) - - def testEnvelope(self): - envelopes = [] - - for msg in self.queue.getWaiting(): - envelopes.append(self.queue.getEnvelope(msg)) - - envelopes.sort() - for i in range(25): - self.assertEquals( - envelopes.pop(0), - ['header', i] - ) - -from twisted.names import server -from twisted.names import client -from twisted.names import common - -class TestAuthority(common.ResolverBase): - def __init__(self): - common.ResolverBase.__init__(self) - self.addresses = {} - - def _lookup(self, name, cls, type, timeout = None): - if name in self.addresses and type == dns.MX: - results = [] - for a in self.addresses[name]: - hdr = dns.RRHeader( - name, dns.MX, dns.IN, 60, dns.Record_MX(0, a) - ) - results.append(hdr) - return defer.succeed((results, [], [])) - return defer.fail(failure.Failure(dns.DomainError(name))) - -def setUpDNS(self): - self.auth = TestAuthority() - factory = server.DNSServerFactory([self.auth]) - protocol = dns.DNSDatagramProtocol(factory) - while 1: - self.port = reactor.listenTCP(0, factory, interface='127.0.0.1') - portNumber = self.port.getHost().port - - try: - self.udpPort = reactor.listenUDP(portNumber, protocol, interface='127.0.0.1') - except CannotListenError: - self.port.stopListening() - else: - break - self.resolver = client.Resolver(servers=[('127.0.0.1', portNumber)]) - - -def tearDownDNS(self): - dl = [] - dl.append(defer.maybeDeferred(self.port.stopListening)) - dl.append(defer.maybeDeferred(self.udpPort.stopListening)) - if self.resolver.protocol.transport is not None: - dl.append(defer.maybeDeferred(self.resolver.protocol.transport.stopListening)) - try: - self.resolver._parseCall.cancel() - except: - pass - return defer.DeferredList(dl) - -class MXTestCase(unittest.TestCase): - """ - Tests for L{mail.relaymanager.MXCalculator}. - """ - def setUp(self): - setUpDNS(self) - self.clock = task.Clock() - self.mx = mail.relaymanager.MXCalculator(self.resolver, self.clock) - - def tearDown(self): - return tearDownDNS(self) - - - def test_defaultClock(self): - """ - L{MXCalculator}'s default clock is C{twisted.internet.reactor}. - """ - self.assertIdentical( - mail.relaymanager.MXCalculator(self.resolver).clock, - reactor) - - - def testSimpleSuccess(self): - self.auth.addresses['test.domain'] = ['the.email.test.domain'] - return self.mx.getMX('test.domain').addCallback(self._cbSimpleSuccess) - - def _cbSimpleSuccess(self, mx): - self.assertEquals(mx.preference, 0) - self.assertEquals(str(mx.name), 'the.email.test.domain') - - def testSimpleFailure(self): - self.mx.fallbackToDomain = False - return self.assertFailure(self.mx.getMX('test.domain'), IOError) - - def testSimpleFailureWithFallback(self): - return self.assertFailure(self.mx.getMX('test.domain'), DNSLookupError) - - - def _exchangeTest(self, domain, records, correctMailExchange): - """ - Issue an MX request for the given domain and arrange for it to be - responded to with the given records. Verify that the resulting mail - exchange is the indicated host. - - @type domain: C{str} - @type records: C{list} of L{RRHeader} - @type correctMailExchange: C{str} - @rtype: L{Deferred} - """ - class DummyResolver(object): - def lookupMailExchange(self, name): - if name == domain: - return defer.succeed(( - records, - [], - [])) - return defer.fail(DNSNameError(domain)) - - self.mx.resolver = DummyResolver() - d = self.mx.getMX(domain) - def gotMailExchange(record): - self.assertEqual(str(record.name), correctMailExchange) - d.addCallback(gotMailExchange) - return d - - - def test_mailExchangePreference(self): - """ - The MX record with the lowest preference is returned by - L{MXCalculator.getMX}. - """ - domain = "example.com" - good = "good.example.com" - bad = "bad.example.com" - - records = [ - RRHeader(name=domain, - type=Record_MX.TYPE, - payload=Record_MX(1, bad)), - RRHeader(name=domain, - type=Record_MX.TYPE, - payload=Record_MX(0, good)), - RRHeader(name=domain, - type=Record_MX.TYPE, - payload=Record_MX(2, bad))] - return self._exchangeTest(domain, records, good) - - - def test_badExchangeExcluded(self): - """ - L{MXCalculator.getMX} returns the MX record with the lowest preference - which is not also marked as bad. - """ - domain = "example.com" - good = "good.example.com" - bad = "bad.example.com" - - records = [ - RRHeader(name=domain, - type=Record_MX.TYPE, - payload=Record_MX(0, bad)), - RRHeader(name=domain, - type=Record_MX.TYPE, - payload=Record_MX(1, good))] - self.mx.markBad(bad) - return self._exchangeTest(domain, records, good) - - - def test_fallbackForAllBadExchanges(self): - """ - L{MXCalculator.getMX} returns the MX record with the lowest preference - if all the MX records in the response have been marked bad. - """ - domain = "example.com" - bad = "bad.example.com" - worse = "worse.example.com" - - records = [ - RRHeader(name=domain, - type=Record_MX.TYPE, - payload=Record_MX(0, bad)), - RRHeader(name=domain, - type=Record_MX.TYPE, - payload=Record_MX(1, worse))] - self.mx.markBad(bad) - self.mx.markBad(worse) - return self._exchangeTest(domain, records, bad) - - - def test_badExchangeExpires(self): - """ - L{MXCalculator.getMX} returns the MX record with the lowest preference - if it was last marked bad longer than L{MXCalculator.timeOutBadMX} - seconds ago. - """ - domain = "example.com" - good = "good.example.com" - previouslyBad = "bad.example.com" - - records = [ - RRHeader(name=domain, - type=Record_MX.TYPE, - payload=Record_MX(0, previouslyBad)), - RRHeader(name=domain, - type=Record_MX.TYPE, - payload=Record_MX(1, good))] - self.mx.markBad(previouslyBad) - self.clock.advance(self.mx.timeOutBadMX) - return self._exchangeTest(domain, records, previouslyBad) - - - def test_goodExchangeUsed(self): - """ - L{MXCalculator.getMX} returns the MX record with the lowest preference - if it was marked good after it was marked bad. - """ - domain = "example.com" - good = "good.example.com" - previouslyBad = "bad.example.com" - - records = [ - RRHeader(name=domain, - type=Record_MX.TYPE, - payload=Record_MX(0, previouslyBad)), - RRHeader(name=domain, - type=Record_MX.TYPE, - payload=Record_MX(1, good))] - self.mx.markBad(previouslyBad) - self.mx.markGood(previouslyBad) - self.clock.advance(self.mx.timeOutBadMX) - return self._exchangeTest(domain, records, previouslyBad) - - - def test_successWithoutResults(self): - """ - If an MX lookup succeeds but the result set is empty, - L{MXCalculator.getMX} should try to look up an I{A} record for the - requested name and call back its returned Deferred with that - address. - """ - ip = '1.2.3.4' - domain = 'example.org' - - class DummyResolver(object): - """ - Fake resolver which will respond to an MX lookup with an empty - result set. - - @ivar mx: A dictionary mapping hostnames to three-tuples of - results to be returned from I{MX} lookups. - - @ivar a: A dictionary mapping hostnames to addresses to be - returned from I{A} lookups. - """ - mx = {domain: ([], [], [])} - a = {domain: ip} - - def lookupMailExchange(self, domain): - return defer.succeed(self.mx[domain]) - - def getHostByName(self, domain): - return defer.succeed(self.a[domain]) - - self.mx.resolver = DummyResolver() - d = self.mx.getMX(domain) - d.addCallback(self.assertEqual, Record_MX(name=ip)) - return d - - - def test_failureWithSuccessfulFallback(self): - """ - Test that if the MX record lookup fails, fallback is enabled, and an A - record is available for the name, then the Deferred returned by - L{MXCalculator.getMX} ultimately fires with a Record_MX instance which - gives the address in the A record for the name. - """ - class DummyResolver(object): - """ - Fake resolver which will fail an MX lookup but then succeed a - getHostByName call. - """ - def lookupMailExchange(self, domain): - return defer.fail(DNSNameError()) - - def getHostByName(self, domain): - return defer.succeed("1.2.3.4") - - self.mx.resolver = DummyResolver() - d = self.mx.getMX("domain") - d.addCallback(self.assertEqual, Record_MX(name="1.2.3.4")) - return d - - - def test_cnameWithoutGlueRecords(self): - """ - If an MX lookup returns a single CNAME record as a result, MXCalculator - will perform an MX lookup for the canonical name indicated and return - the MX record which results. - """ - alias = "alias.example.com" - canonical = "canonical.example.com" - exchange = "mail.example.com" - - class DummyResolver(object): - """ - Fake resolver which will return a CNAME for an MX lookup of a name - which is an alias and an MX for an MX lookup of the canonical name. - """ - def lookupMailExchange(self, domain): - if domain == alias: - return defer.succeed(( - [RRHeader(name=domain, - type=Record_CNAME.TYPE, - payload=Record_CNAME(canonical))], - [], [])) - elif domain == canonical: - return defer.succeed(( - [RRHeader(name=domain, - type=Record_MX.TYPE, - payload=Record_MX(0, exchange))], - [], [])) - else: - return defer.fail(DNSNameError(domain)) - - self.mx.resolver = DummyResolver() - d = self.mx.getMX(alias) - d.addCallback(self.assertEqual, Record_MX(name=exchange)) - return d - - - def test_cnameChain(self): - """ - If L{MXCalculator.getMX} encounters a CNAME chain which is longer than - the length specified, the returned L{Deferred} should errback with - L{CanonicalNameChainTooLong}. - """ - class DummyResolver(object): - """ - Fake resolver which generates a CNAME chain of infinite length in - response to MX lookups. - """ - chainCounter = 0 - - def lookupMailExchange(self, domain): - self.chainCounter += 1 - name = 'x-%d.example.com' % (self.chainCounter,) - return defer.succeed(( - [RRHeader(name=domain, - type=Record_CNAME.TYPE, - payload=Record_CNAME(name))], - [], [])) - - cnameLimit = 3 - self.mx.resolver = DummyResolver() - d = self.mx.getMX("mail.example.com", cnameLimit) - self.assertFailure( - d, twisted.mail.relaymanager.CanonicalNameChainTooLong) - def cbChainTooLong(error): - self.assertEqual(error.args[0], Record_CNAME("x-%d.example.com" % (cnameLimit + 1,))) - self.assertEqual(self.mx.resolver.chainCounter, cnameLimit + 1) - d.addCallback(cbChainTooLong) - return d - - - def test_cnameWithGlueRecords(self): - """ - If an MX lookup returns a CNAME and the MX record for the CNAME, the - L{Deferred} returned by L{MXCalculator.getMX} should be called back - with the name from the MX record without further lookups being - attempted. - """ - lookedUp = [] - alias = "alias.example.com" - canonical = "canonical.example.com" - exchange = "mail.example.com" - - class DummyResolver(object): - def lookupMailExchange(self, domain): - if domain != alias or lookedUp: - # Don't give back any results for anything except the alias - # or on any request after the first. - return ([], [], []) - return defer.succeed(( - [RRHeader(name=alias, - type=Record_CNAME.TYPE, - payload=Record_CNAME(canonical)), - RRHeader(name=canonical, - type=Record_MX.TYPE, - payload=Record_MX(name=exchange))], - [], [])) - - self.mx.resolver = DummyResolver() - d = self.mx.getMX(alias) - d.addCallback(self.assertEqual, Record_MX(name=exchange)) - return d - - - def test_cnameLoopWithGlueRecords(self): - """ - If an MX lookup returns two CNAME records which point to each other, - the loop should be detected and the L{Deferred} returned by - L{MXCalculator.getMX} should be errbacked with L{CanonicalNameLoop}. - """ - firstAlias = "cname1.example.com" - secondAlias = "cname2.example.com" - - class DummyResolver(object): - def lookupMailExchange(self, domain): - return defer.succeed(( - [RRHeader(name=firstAlias, - type=Record_CNAME.TYPE, - payload=Record_CNAME(secondAlias)), - RRHeader(name=secondAlias, - type=Record_CNAME.TYPE, - payload=Record_CNAME(firstAlias))], - [], [])) - - self.mx.resolver = DummyResolver() - d = self.mx.getMX(firstAlias) - self.assertFailure(d, twisted.mail.relaymanager.CanonicalNameLoop) - return d - - - def testManyRecords(self): - self.auth.addresses['test.domain'] = [ - 'mx1.test.domain', 'mx2.test.domain', 'mx3.test.domain' - ] - return self.mx.getMX('test.domain' - ).addCallback(self._cbManyRecordsSuccessfulLookup - ) - - def _cbManyRecordsSuccessfulLookup(self, mx): - self.failUnless(str(mx.name).split('.', 1)[0] in ('mx1', 'mx2', 'mx3')) - self.mx.markBad(str(mx.name)) - return self.mx.getMX('test.domain' - ).addCallback(self._cbManyRecordsDifferentResult, mx - ) - - def _cbManyRecordsDifferentResult(self, nextMX, mx): - self.assertNotEqual(str(mx.name), str(nextMX.name)) - self.mx.markBad(str(nextMX.name)) - - return self.mx.getMX('test.domain' - ).addCallback(self._cbManyRecordsLastResult, mx, nextMX - ) - - def _cbManyRecordsLastResult(self, lastMX, mx, nextMX): - self.assertNotEqual(str(mx.name), str(lastMX.name)) - self.assertNotEqual(str(nextMX.name), str(lastMX.name)) - - self.mx.markBad(str(lastMX.name)) - self.mx.markGood(str(nextMX.name)) - - return self.mx.getMX('test.domain' - ).addCallback(self._cbManyRecordsRepeatSpecificResult, nextMX - ) - - def _cbManyRecordsRepeatSpecificResult(self, againMX, nextMX): - self.assertEqual(str(againMX.name), str(nextMX.name)) - -class LiveFireExercise(unittest.TestCase): - if interfaces.IReactorUDP(reactor, None) is None: - skip = "UDP support is required to determining MX records" - - def setUp(self): - setUpDNS(self) - self.tmpdirs = [ - 'domainDir', 'insertionDomain', 'insertionQueue', - 'destinationDomain', 'destinationQueue' - ] - - def tearDown(self): - for d in self.tmpdirs: - if os.path.exists(d): - shutil.rmtree(d) - return tearDownDNS(self) - - def testLocalDelivery(self): - service = mail.mail.MailService() - service.smtpPortal.registerChecker(cred.checkers.AllowAnonymousAccess()) - domain = mail.maildir.MaildirDirdbmDomain(service, 'domainDir') - domain.addUser('user', 'password') - service.addDomain('test.domain', domain) - service.portals[''] = service.portals['test.domain'] - map(service.portals[''].registerChecker, domain.getCredentialsCheckers()) - - service.setQueue(mail.relay.DomainQueuer(service)) - manager = mail.relaymanager.SmartHostSMTPRelayingManager(service.queue, None) - helper = mail.relaymanager.RelayStateHelper(manager, 1) - - f = service.getSMTPFactory() - - self.smtpServer = reactor.listenTCP(0, f, interface='127.0.0.1') - - client = LineSendingProtocol([ - 'HELO meson', - 'MAIL FROM: <user@hostname>', - 'RCPT TO: <user@test.domain>', - 'DATA', - 'This is the message', - '.', - 'QUIT' - ]) - - done = Deferred() - f = protocol.ClientFactory() - f.protocol = lambda: client - f.clientConnectionLost = lambda *args: done.callback(None) - reactor.connectTCP('127.0.0.1', self.smtpServer.getHost().port, f) - - def finished(ign): - mbox = domain.requestAvatar('user', None, pop3.IMailbox)[1] - msg = mbox.getMessage(0).read() - self.failIfEqual(msg.find('This is the message'), -1) - - return self.smtpServer.stopListening() - done.addCallback(finished) - return done - - - def testRelayDelivery(self): - # Here is the service we will connect to and send mail from - insServ = mail.mail.MailService() - insServ.smtpPortal.registerChecker(cred.checkers.AllowAnonymousAccess()) - domain = mail.maildir.MaildirDirdbmDomain(insServ, 'insertionDomain') - insServ.addDomain('insertion.domain', domain) - os.mkdir('insertionQueue') - insServ.setQueue(mail.relaymanager.Queue('insertionQueue')) - insServ.domains.setDefaultDomain(mail.relay.DomainQueuer(insServ)) - manager = mail.relaymanager.SmartHostSMTPRelayingManager(insServ.queue) - manager.fArgs += ('test.identity.hostname',) - helper = mail.relaymanager.RelayStateHelper(manager, 1) - # Yoink! Now the internet obeys OUR every whim! - manager.mxcalc = mail.relaymanager.MXCalculator(self.resolver) - # And this is our whim. - self.auth.addresses['destination.domain'] = ['127.0.0.1'] - - f = insServ.getSMTPFactory() - self.insServer = reactor.listenTCP(0, f, interface='127.0.0.1') - - # Here is the service the previous one will connect to for final - # delivery - destServ = mail.mail.MailService() - destServ.smtpPortal.registerChecker(cred.checkers.AllowAnonymousAccess()) - domain = mail.maildir.MaildirDirdbmDomain(destServ, 'destinationDomain') - domain.addUser('user', 'password') - destServ.addDomain('destination.domain', domain) - os.mkdir('destinationQueue') - destServ.setQueue(mail.relaymanager.Queue('destinationQueue')) - manager2 = mail.relaymanager.SmartHostSMTPRelayingManager(destServ.queue) - helper = mail.relaymanager.RelayStateHelper(manager, 1) - helper.startService() - - f = destServ.getSMTPFactory() - self.destServer = reactor.listenTCP(0, f, interface='127.0.0.1') - - # Update the port number the *first* relay will connect to, because we can't use - # port 25 - manager.PORT = self.destServer.getHost().port - - client = LineSendingProtocol([ - 'HELO meson', - 'MAIL FROM: <user@wherever>', - 'RCPT TO: <user@destination.domain>', - 'DATA', - 'This is the message', - '.', - 'QUIT' - ]) - - done = Deferred() - f = protocol.ClientFactory() - f.protocol = lambda: client - f.clientConnectionLost = lambda *args: done.callback(None) - reactor.connectTCP('127.0.0.1', self.insServer.getHost().port, f) - - def finished(ign): - # First part of the delivery is done. Poke the queue manually now - # so we don't have to wait for the queue to be flushed. - delivery = manager.checkState() - def delivered(ign): - mbox = domain.requestAvatar('user', None, pop3.IMailbox)[1] - msg = mbox.getMessage(0).read() - self.failIfEqual(msg.find('This is the message'), -1) - - self.insServer.stopListening() - self.destServer.stopListening() - helper.stopService() - delivery.addCallback(delivered) - return delivery - done.addCallback(finished) - return done - - -aliasFile = StringIO.StringIO("""\ -# Here's a comment - # woop another one -testuser: address1,address2, address3, - continuation@address, |/bin/process/this - -usertwo:thisaddress,thataddress, lastaddress -lastuser: :/includable, /filename, |/program, address -""") - -class LineBufferMessage: - def __init__(self): - self.lines = [] - self.eom = False - self.lost = False - - def lineReceived(self, line): - self.lines.append(line) - - def eomReceived(self): - self.eom = True - return defer.succeed('<Whatever>') - - def connectionLost(self): - self.lost = True - -class AliasTestCase(unittest.TestCase): - lines = [ - 'First line', - 'Next line', - '', - 'After a blank line', - 'Last line' - ] - - def setUp(self): - aliasFile.seek(0) - - def testHandle(self): - result = {} - lines = [ - 'user: another@host\n', - 'nextuser: |/bin/program\n', - 'user: me@again\n', - 'moreusers: :/etc/include/filename\n', - 'multiuser: first@host, second@host,last@anotherhost', - ] - - for l in lines: - mail.alias.handle(result, l, 'TestCase', None) - - self.assertEquals(result['user'], ['another@host', 'me@again']) - self.assertEquals(result['nextuser'], ['|/bin/program']) - self.assertEquals(result['moreusers'], [':/etc/include/filename']) - self.assertEquals(result['multiuser'], ['first@host', 'second@host', 'last@anotherhost']) - - def testFileLoader(self): - domains = {'': object()} - result = mail.alias.loadAliasFile(domains, fp=aliasFile) - - self.assertEquals(len(result), 3) - - group = result['testuser'] - s = str(group) - for a in ('address1', 'address2', 'address3', 'continuation@address', '/bin/process/this'): - self.failIfEqual(s.find(a), -1) - self.assertEquals(len(group), 5) - - group = result['usertwo'] - s = str(group) - for a in ('thisaddress', 'thataddress', 'lastaddress'): - self.failIfEqual(s.find(a), -1) - self.assertEquals(len(group), 3) - - group = result['lastuser'] - s = str(group) - self.failUnlessEqual(s.find('/includable'), -1) - for a in ('/filename', 'program', 'address'): - self.failIfEqual(s.find(a), -1, '%s not found' % a) - self.assertEquals(len(group), 3) - - def testMultiWrapper(self): - msgs = LineBufferMessage(), LineBufferMessage(), LineBufferMessage() - msg = mail.alias.MultiWrapper(msgs) - - for L in self.lines: - msg.lineReceived(L) - return msg.eomReceived().addCallback(self._cbMultiWrapper, msgs) - - def _cbMultiWrapper(self, ignored, msgs): - for m in msgs: - self.failUnless(m.eom) - self.failIf(m.lost) - self.assertEquals(self.lines, m.lines) - - def testFileAlias(self): - tmpfile = self.mktemp() - a = mail.alias.FileAlias(tmpfile, None, None) - m = a.createMessageReceiver() - - for l in self.lines: - m.lineReceived(l) - return m.eomReceived().addCallback(self._cbTestFileAlias, tmpfile) - - def _cbTestFileAlias(self, ignored, tmpfile): - lines = file(tmpfile).readlines() - self.assertEquals([L[:-1] for L in lines], self.lines) - - - -class DummyProcess(object): - __slots__ = ['onEnd'] - - - -class MockProcessAlias(mail.alias.ProcessAlias): - """ - A alias processor that doesn't actually launch processes. - """ - - def spawnProcess(self, proto, program, path): - """ - Don't spawn a process. - """ - - - -class MockAliasGroup(mail.alias.AliasGroup): - """ - An alias group using C{MockProcessAlias}. - """ - processAliasFactory = MockProcessAlias - - - -class StubProcess(object): - """ - Fake implementation of L{IProcessTransport}. - - @ivar signals: A list of all the signals which have been sent to this fake - process. - """ - def __init__(self): - self.signals = [] - - - def loseConnection(self): - """ - No-op implementation of disconnection. - """ - - - def signalProcess(self, signal): - """ - Record a signal sent to this process for later inspection. - """ - self.signals.append(signal) - - - -class ProcessAliasTestCase(unittest.TestCase): - """ - Tests for alias resolution. - """ - - lines = [ - 'First line', - 'Next line', - '', - 'After a blank line', - 'Last line' - ] - - def exitStatus(self, code): - """ - Construct a status from the given exit code. - - @type code: L{int} between 0 and 255 inclusive. - @param code: The exit status which the code will represent. - - @rtype: L{int} - @return: A status integer for the given exit code. - """ - # /* Macros for constructing status values. */ - # #define __W_EXITCODE(ret, sig) ((ret) << 8 | (sig)) - status = (code << 8) | 0 - - # Sanity check - self.assertTrue(os.WIFEXITED(status)) - self.assertEqual(os.WEXITSTATUS(status), code) - self.assertFalse(os.WIFSIGNALED(status)) - - return status - - - def signalStatus(self, signal): - """ - Construct a status from the given signal. - - @type signal: L{int} between 0 and 255 inclusive. - @param signal: The signal number which the status will represent. - - @rtype: L{int} - @return: A status integer for the given signal. - """ - # /* If WIFSIGNALED(STATUS), the terminating signal. */ - # #define __WTERMSIG(status) ((status) & 0x7f) - # /* Nonzero if STATUS indicates termination by a signal. */ - # #define __WIFSIGNALED(status) \ - # (((signed char) (((status) & 0x7f) + 1) >> 1) > 0) - status = signal - - # Sanity check - self.assertTrue(os.WIFSIGNALED(status)) - self.assertEqual(os.WTERMSIG(status), signal) - self.assertFalse(os.WIFEXITED(status)) - - return status - - - def setUp(self): - """ - Replace L{smtp.DNSNAME} with a well-known value. - """ - self.DNSNAME = smtp.DNSNAME - smtp.DNSNAME = '' - - - def tearDown(self): - """ - Restore the original value of L{smtp.DNSNAME}. - """ - smtp.DNSNAME = self.DNSNAME - - - def test_processAlias(self): - """ - Standard call to C{mail.alias.ProcessAlias}: check that the specified - script is called, and that the input is correctly transferred to it. - """ - sh = FilePath(self.mktemp()) - sh.setContent("""\ -#!/bin/sh -rm -f process.alias.out -while read i; do - echo $i >> process.alias.out -done""") - os.chmod(sh.path, 0700) - a = mail.alias.ProcessAlias(sh.path, None, None) - m = a.createMessageReceiver() - - for l in self.lines: - m.lineReceived(l) - - def _cbProcessAlias(ignored): - lines = file('process.alias.out').readlines() - self.assertEquals([L[:-1] for L in lines], self.lines) - - return m.eomReceived().addCallback(_cbProcessAlias) - - - def test_processAliasTimeout(self): - """ - If the alias child process does not exit within a particular period of - time, the L{Deferred} returned by L{MessageWrapper.eomReceived} should - fail with L{ProcessAliasTimeout} and send the I{KILL} signal to the - child process.. - """ - reactor = task.Clock() - transport = StubProcess() - proto = mail.alias.ProcessAliasProtocol() - proto.makeConnection(transport) - - receiver = mail.alias.MessageWrapper(proto, None, reactor) - d = receiver.eomReceived() - reactor.advance(receiver.completionTimeout) - def timedOut(ignored): - self.assertEqual(transport.signals, ['KILL']) - # Now that it has been killed, disconnect the protocol associated - # with it. - proto.processEnded( - ProcessTerminated(self.signalStatus(signal.SIGKILL))) - self.assertFailure(d, mail.alias.ProcessAliasTimeout) - d.addCallback(timedOut) - return d - - - def test_earlyProcessTermination(self): - """ - If the process associated with an L{mail.alias.MessageWrapper} exits - before I{eomReceived} is called, the L{Deferred} returned by - I{eomReceived} should fail. - """ - transport = StubProcess() - protocol = mail.alias.ProcessAliasProtocol() - protocol.makeConnection(transport) - receiver = mail.alias.MessageWrapper(protocol, None, None) - protocol.processEnded(failure.Failure(ProcessDone(0))) - return self.assertFailure(receiver.eomReceived(), ProcessDone) - - - def _terminationTest(self, status): - """ - Verify that if the process associated with an - L{mail.alias.MessageWrapper} exits with the given status, the - L{Deferred} returned by I{eomReceived} fails with L{ProcessTerminated}. - """ - transport = StubProcess() - protocol = mail.alias.ProcessAliasProtocol() - protocol.makeConnection(transport) - receiver = mail.alias.MessageWrapper(protocol, None, None) - protocol.processEnded( - failure.Failure(ProcessTerminated(status))) - return self.assertFailure(receiver.eomReceived(), ProcessTerminated) - - - def test_errorProcessTermination(self): - """ - If the process associated with an L{mail.alias.MessageWrapper} exits - with a non-zero exit code, the L{Deferred} returned by I{eomReceived} - should fail. - """ - return self._terminationTest(self.exitStatus(1)) - - - def test_signalProcessTermination(self): - """ - If the process associated with an L{mail.alias.MessageWrapper} exits - because it received a signal, the L{Deferred} returned by - I{eomReceived} should fail. - """ - return self._terminationTest(self.signalStatus(signal.SIGHUP)) - - - def test_aliasResolution(self): - """ - Check that the C{resolve} method of alias processors produce the correct - set of objects: - - direct alias with L{mail.alias.AddressAlias} if a simple input is passed - - aliases in a file with L{mail.alias.FileWrapper} if an input in the format - '/file' is given - - aliases resulting of a process call wrapped by L{mail.alias.MessageWrapper} - if the format is '|process' - """ - aliases = {} - domain = {'': TestDomain(aliases, ['user1', 'user2', 'user3'])} - A1 = MockAliasGroup(['user1', '|echo', '/file'], domain, 'alias1') - A2 = MockAliasGroup(['user2', 'user3'], domain, 'alias2') - A3 = mail.alias.AddressAlias('alias1', domain, 'alias3') - aliases.update({ - 'alias1': A1, - 'alias2': A2, - 'alias3': A3, - }) - - res1 = A1.resolve(aliases) - r1 = map(str, res1.objs) - r1.sort() - expected = map(str, [ - mail.alias.AddressAlias('user1', None, None), - mail.alias.MessageWrapper(DummyProcess(), 'echo'), - mail.alias.FileWrapper('/file'), - ]) - expected.sort() - self.assertEquals(r1, expected) - - res2 = A2.resolve(aliases) - r2 = map(str, res2.objs) - r2.sort() - expected = map(str, [ - mail.alias.AddressAlias('user2', None, None), - mail.alias.AddressAlias('user3', None, None) - ]) - expected.sort() - self.assertEquals(r2, expected) - - res3 = A3.resolve(aliases) - r3 = map(str, res3.objs) - r3.sort() - expected = map(str, [ - mail.alias.AddressAlias('user1', None, None), - mail.alias.MessageWrapper(DummyProcess(), 'echo'), - mail.alias.FileWrapper('/file'), - ]) - expected.sort() - self.assertEquals(r3, expected) - - - def test_cyclicAlias(self): - """ - Check that a cycle in alias resolution is correctly handled. - """ - aliases = {} - domain = {'': TestDomain(aliases, [])} - A1 = mail.alias.AddressAlias('alias2', domain, 'alias1') - A2 = mail.alias.AddressAlias('alias3', domain, 'alias2') - A3 = mail.alias.AddressAlias('alias1', domain, 'alias3') - aliases.update({ - 'alias1': A1, - 'alias2': A2, - 'alias3': A3 - }) - - self.assertEquals(aliases['alias1'].resolve(aliases), None) - self.assertEquals(aliases['alias2'].resolve(aliases), None) - self.assertEquals(aliases['alias3'].resolve(aliases), None) - - A4 = MockAliasGroup(['|echo', 'alias1'], domain, 'alias4') - aliases['alias4'] = A4 - - res = A4.resolve(aliases) - r = map(str, res.objs) - r.sort() - expected = map(str, [ - mail.alias.MessageWrapper(DummyProcess(), 'echo') - ]) - expected.sort() - self.assertEquals(r, expected) - - - -if interfaces.IReactorProcess(reactor, None) is None: - ProcessAliasTestCase = "IReactorProcess not supported" - - - -class TestDomain: - def __init__(self, aliases, users): - self.aliases = aliases - self.users = users - - def exists(self, user, memo=None): - user = user.dest.local - if user in self.users: - return lambda: mail.alias.AddressAlias(user, None, None) - try: - a = self.aliases[user] - except: - raise smtp.SMTPBadRcpt(user) - else: - aliases = a.resolve(self.aliases, memo) - if aliases: - return lambda: aliases - raise smtp.SMTPBadRcpt(user) - - -from twisted.python.runtime import platformType -import types -if platformType != "posix": - for o in locals().values(): - if isinstance(o, (types.ClassType, type)) and issubclass(o, unittest.TestCase): - o.skip = "twisted.mail only works on posix" diff --git a/tools/buildbot/pylibs/twisted/mail/test/test_options.py b/tools/buildbot/pylibs/twisted/mail/test/test_options.py deleted file mode 100644 index 65c360b..0000000 --- a/tools/buildbot/pylibs/twisted/mail/test/test_options.py +++ /dev/null @@ -1,38 +0,0 @@ - -from twisted.trial.unittest import TestCase - -from twisted.python.usage import UsageError -from twisted.mail.tap import Options - - -class OptionsTestCase(TestCase): - """ - Tests for the command line option parser used for C{mktap mail}. - """ - def setUp(self): - self.aliasFilename = self.mktemp() - aliasFile = file(self.aliasFilename, 'w') - aliasFile.write('someuser:\tdifferentuser\n') - aliasFile.close() - - - def testAliasesWithoutDomain(self): - """ - Test that adding an aliases(5) file before adding a domain raises a - UsageError. - """ - self.assertRaises( - UsageError, - Options().parseOptions, - ['--aliases', self.aliasFilename]) - - - def testAliases(self): - """ - Test that adding an aliases(5) file to an IAliasableDomain at least - doesn't raise an unhandled exception. - """ - Options().parseOptions([ - '--maildirdbmdomain', 'example.com=example.com', - '--aliases', self.aliasFilename]) - diff --git a/tools/buildbot/pylibs/twisted/mail/test/test_pop3.py b/tools/buildbot/pylibs/twisted/mail/test/test_pop3.py deleted file mode 100644 index 2830218..0000000 --- a/tools/buildbot/pylibs/twisted/mail/test/test_pop3.py +++ /dev/null @@ -1,1071 +0,0 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. -# See LICENSE for details. - - -""" -Test cases for twisted.mail.pop3 module. -""" - -import StringIO -import string -import hmac -import base64 -import itertools - -from zope.interface import implements - -from twisted.internet import defer - -from twisted.trial import unittest, util -from twisted import mail -import twisted.mail.protocols -import twisted.mail.pop3 -import twisted.internet.protocol -from twisted import internet -from twisted.mail import pop3 -from twisted.protocols import loopback -from twisted.python import failure - -from twisted import cred -import twisted.cred.portal -import twisted.cred.checkers -import twisted.cred.credentials - -from twisted.test.proto_helpers import LineSendingProtocol - - -class UtilityTestCase(unittest.TestCase): - """ - Test the various helper functions and classes used by the POP3 server - protocol implementation. - """ - - def testLineBuffering(self): - """ - Test creating a LineBuffer and feeding it some lines. The lines should - build up in its internal buffer for a while and then get spat out to - the writer. - """ - output = [] - input = iter(itertools.cycle(['012', '345', '6', '7', '8', '9'])) - c = pop3._IteratorBuffer(output.extend, input, 6) - i = iter(c) - self.assertEquals(output, []) # nothing is buffer - i.next() - self.assertEquals(output, []) # '012' is buffered - i.next() - self.assertEquals(output, []) # '012345' is buffered - i.next() - self.assertEquals(output, ['012', '345', '6']) # nothing is buffered - for n in range(5): - i.next() - self.assertEquals(output, ['012', '345', '6', '7', '8', '9', '012', '345']) - - - def testFinishLineBuffering(self): - """ - Test that a LineBuffer flushes everything when its iterator is - exhausted, and itself raises StopIteration. - """ - output = [] - input = iter(['a', 'b', 'c']) - c = pop3._IteratorBuffer(output.extend, input, 5) - for i in c: - pass - self.assertEquals(output, ['a', 'b', 'c']) - - - def testSuccessResponseFormatter(self): - """ - Test that the thing that spits out POP3 'success responses' works - right. - """ - self.assertEquals( - pop3.successResponse('Great.'), - '+OK Great.\r\n') - - - def testStatLineFormatter(self): - """ - Test that the function which formats stat lines does so appropriately. - """ - statLine = list(pop3.formatStatResponse([]))[-1] - self.assertEquals(statLine, '+OK 0 0\r\n') - - statLine = list(pop3.formatStatResponse([10, 31, 0, 10101]))[-1] - self.assertEquals(statLine, '+OK 4 10142\r\n') - - - def testListLineFormatter(self): - """ - Test that the function which formats the lines in response to a LIST - command does so appropriately. - """ - listLines = list(pop3.formatListResponse([])) - self.assertEquals( - listLines, - ['+OK 0\r\n', '.\r\n']) - - listLines = list(pop3.formatListResponse([1, 2, 3, 100])) - self.assertEquals( - listLines, - ['+OK 4\r\n', '1 1\r\n', '2 2\r\n', '3 3\r\n', '4 100\r\n', '.\r\n']) - - - - def testUIDListLineFormatter(self): - """ - Test that the function which formats lines in response to a UIDL - command does so appropriately. - """ - UIDs = ['abc', 'def', 'ghi'] - listLines = list(pop3.formatUIDListResponse([], UIDs.__getitem__)) - self.assertEquals( - listLines, - ['+OK \r\n', '.\r\n']) - - listLines = list(pop3.formatUIDListResponse([123, 431, 591], UIDs.__getitem__)) - self.assertEquals( - listLines, - ['+OK \r\n', '1 abc\r\n', '2 def\r\n', '3 ghi\r\n', '.\r\n']) - - listLines = list(pop3.formatUIDListResponse([0, None, 591], UIDs.__getitem__)) - self.assertEquals( - listLines, - ['+OK \r\n', '1 abc\r\n', '3 ghi\r\n', '.\r\n']) - - - -class MyVirtualPOP3(mail.protocols.VirtualPOP3): - - magic = '<moshez>' - - def authenticateUserAPOP(self, user, digest): - user, domain = self.lookupDomain(user) - return self.service.domains['baz.com'].authenticateUserAPOP(user, digest, self.magic, domain) - -class DummyDomain: - - def __init__(self): - self.users = {} - - def addUser(self, name): - self.users[name] = [] - - def addMessage(self, name, message): - self.users[name].append(message) - - def authenticateUserAPOP(self, name, digest, magic, domain): - return pop3.IMailbox, ListMailbox(self.users[name]), lambda: None - - -class ListMailbox: - - def __init__(self, list): - self.list = list - - def listMessages(self, i=None): - if i is None: - return map(len, self.list) - return len(self.list[i]) - - def getMessage(self, i): - return StringIO.StringIO(self.list[i]) - - def getUidl(self, i): - return i - - def deleteMessage(self, i): - self.list[i] = '' - - def sync(self): - pass - -class MyPOP3Downloader(pop3.POP3Client): - - def handle_WELCOME(self, line): - pop3.POP3Client.handle_WELCOME(self, line) - self.apop('hello@baz.com', 'world') - - def handle_APOP(self, line): - parts = line.split() - code = parts[0] - data = (parts[1:] or ['NONE'])[0] - if code != '+OK': - print parts - raise AssertionError, 'code is ' + code - self.lines = [] - self.retr(1) - - def handle_RETR_continue(self, line): - self.lines.append(line) - - def handle_RETR_end(self): - self.message = '\n'.join(self.lines) + '\n' - self.quit() - - def handle_QUIT(self, line): - if line[:3] != '+OK': - raise AssertionError, 'code is ' + line - - -class POP3TestCase(unittest.TestCase): - - message = '''\ -Subject: urgent - -Someone set up us the bomb! -''' - - expectedOutput = '''\ -+OK <moshez>\015 -+OK Authentication succeeded\015 -+OK \015 -1 0\015 -.\015 -+OK %d\015 -Subject: urgent\015 -\015 -Someone set up us the bomb!\015 -.\015 -+OK \015 -''' % len(message) - - def setUp(self): - self.factory = internet.protocol.Factory() - self.factory.domains = {} - self.factory.domains['baz.com'] = DummyDomain() - self.factory.domains['baz.com'].addUser('hello') - self.factory.domains['baz.com'].addMessage('hello', self.message) - - def testMessages(self): - client = LineSendingProtocol([ - 'APOP hello@baz.com world', - 'UIDL', - 'RETR 1', - 'QUIT', - ]) - server = MyVirtualPOP3() - server.service = self.factory - def check(ignored): - output = '\r\n'.join(client.response) + '\r\n' - self.assertEquals(output, self.expectedOutput) - return loopback.loopbackTCP(server, client).addCallback(check) - - def testLoopback(self): - protocol = MyVirtualPOP3() - protocol.service = self.factory - clientProtocol = MyPOP3Downloader() - def check(ignored): - self.failUnlessEqual(clientProtocol.message, self.message) - protocol.connectionLost( - failure.Failure(Exception("Test harness disconnect"))) - d = loopback.loopbackAsync(protocol, clientProtocol) - return d.addCallback(check) - testLoopback.suppress = [util.suppress(message="twisted.mail.pop3.POP3Client is deprecated")] - - - -class DummyPOP3(pop3.POP3): - - magic = '<moshez>' - - def authenticateUserAPOP(self, user, password): - return pop3.IMailbox, DummyMailbox(ValueError), lambda: None - - - -class DummyMailbox(pop3.Mailbox): - - messages = [ -'''\ -From: moshe -To: moshe - -How are you, friend? -'''] - - def __init__(self, exceptionType): - self.messages = DummyMailbox.messages[:] - self.exceptionType = exceptionType - - def listMessages(self, i=None): - if i is None: - return map(len, self.messages) - if i >= len(self.messages): - raise self.exceptionType() - return len(self.messages[i]) - - def getMessage(self, i): - return StringIO.StringIO(self.messages[i]) - - def getUidl(self, i): - if i >= len(self.messages): - raise self.exceptionType() - return str(i) - - def deleteMessage(self, i): - self.messages[i] = '' - - -class AnotherPOP3TestCase(unittest.TestCase): - - def runTest(self, lines): - dummy = DummyPOP3() - client = LineSendingProtocol([ - "APOP moshez dummy", - "LIST", - "UIDL", - "RETR 1", - "RETR 2", - "DELE 1", - "RETR 1", - "QUIT", - ]) - d = loopback.loopbackAsync(dummy, client) - return d.addCallback(self._cbRunTest, client, dummy) - - def _cbRunTest(self, ignored, client, dummy): - expected_output = [ - '+OK <moshez>', - '+OK Authentication succeeded', - '+OK 1', - '1 44', - '.', - '+OK ', - '1 0', - '.', - '+OK 44', - 'From: moshe', - 'To: moshe', - '', - 'How are you, friend?', - '', - '.', - '-ERR Bad message number argument', - '+OK ', - '-ERR message deleted', - '+OK ', - '' - ] - self.failUnlessEqual('\r\n'.join(expected_output), - '\r\n'.join(client.response) + '\r\n') - dummy.connectionLost(failure.Failure(Exception("Test harness disconnect"))) - return ignored - - - def testBuffer(self): - lines = string.split('''\ -APOP moshez dummy -LIST -UIDL -RETR 1 -RETR 2 -DELE 1 -RETR 1 -QUIT''', '\n') - return self.runTest(lines) - - def testNoop(self): - lines = ['APOP spiv dummy', 'NOOP', 'QUIT'] - return self.runTest(lines) - - def testAuthListing(self): - p = DummyPOP3() - p.factory = internet.protocol.Factory() - p.factory.challengers = {'Auth1': None, 'secondAuth': None, 'authLast': None} - client = LineSendingProtocol([ - "AUTH", - "QUIT", - ]) - - d = loopback.loopbackAsync(p, client) - return d.addCallback(self._cbTestAuthListing, client) - - def _cbTestAuthListing(self, ignored, client): - self.failUnless(client.response[1].startswith('+OK')) - self.assertEquals(client.response[2:6], - ["AUTH1", "SECONDAUTH", "AUTHLAST", "."]) - - def testIllegalPASS(self): - dummy = DummyPOP3() - client = LineSendingProtocol([ - "PASS fooz", - "QUIT" - ]) - d = loopback.loopbackAsync(dummy, client) - return d.addCallback(self._cbTestIllegalPASS, client, dummy) - - def _cbTestIllegalPASS(self, ignored, client, dummy): - expected_output = '+OK <moshez>\r\n-ERR USER required before PASS\r\n+OK \r\n' - self.failUnlessEqual(expected_output, '\r\n'.join(client.response) + '\r\n') - dummy.connectionLost(failure.Failure(Exception("Test harness disconnect"))) - - def testEmptyPASS(self): - dummy = DummyPOP3() - client = LineSendingProtocol([ - "PASS ", - "QUIT" - ]) - d = loopback.loopbackAsync(dummy, client) - return d.addCallback(self._cbTestEmptyPASS, client, dummy) - - def _cbTestEmptyPASS(self, ignored, client, dummy): - expected_output = '+OK <moshez>\r\n-ERR USER required before PASS\r\n+OK \r\n' - self.failUnlessEqual(expected_output, '\r\n'.join(client.response) + '\r\n') - dummy.connectionLost(failure.Failure(Exception("Test harness disconnect"))) - - -class TestServerFactory: - implements(pop3.IServerFactory) - - def cap_IMPLEMENTATION(self): - return "Test Implementation String" - - def cap_EXPIRE(self): - return 60 - - challengers = {"SCHEME_1": None, "SCHEME_2": None} - - def cap_LOGIN_DELAY(self): - return 120 - - pue = True - def perUserExpiration(self): - return self.pue - - puld = True - def perUserLoginDelay(self): - return self.puld - - -class TestMailbox: - loginDelay = 100 - messageExpiration = 25 - - -class CapabilityTestCase(unittest.TestCase): - def setUp(self): - s = StringIO.StringIO() - p = pop3.POP3() - p.factory = TestServerFactory() - p.transport = internet.protocol.FileWrapper(s) - p.connectionMade() - p.do_CAPA() - - self.caps = p.listCapabilities() - self.pcaps = s.getvalue().splitlines() - - s = StringIO.StringIO() - p.mbox = TestMailbox() - p.transport = internet.protocol.FileWrapper(s) - p.do_CAPA() - - self.lpcaps = s.getvalue().splitlines() - p.connectionLost(failure.Failure(Exception("Test harness disconnect"))) - - def contained(self, s, *caps): - for c in caps: - self.assertIn(s, c) - - def testUIDL(self): - self.contained("UIDL", self.caps, self.pcaps, self.lpcaps) - - def testTOP(self): - self.contained("TOP", self.caps, self.pcaps, self.lpcaps) - - def testUSER(self): - self.contained("USER", self.caps, self.pcaps, self.lpcaps) - - def testEXPIRE(self): - self.contained("EXPIRE 60 USER", self.caps, self.pcaps) - self.contained("EXPIRE 25", self.lpcaps) - - def testIMPLEMENTATION(self): - self.contained( - "IMPLEMENTATION Test Implementation String", - self.caps, self.pcaps, self.lpcaps - ) - - def testSASL(self): - self.contained( - "SASL SCHEME_1 SCHEME_2", - self.caps, self.pcaps, self.lpcaps - ) - - def testLOGIN_DELAY(self): - self.contained("LOGIN-DELAY 120 USER", self.caps, self.pcaps) - self.assertIn("LOGIN-DELAY 100", self.lpcaps) - - - -class GlobalCapabilitiesTestCase(unittest.TestCase): - def setUp(self): - s = StringIO.StringIO() - p = pop3.POP3() - p.factory = TestServerFactory() - p.factory.pue = p.factory.puld = False - p.transport = internet.protocol.FileWrapper(s) - p.connectionMade() - p.do_CAPA() - - self.caps = p.listCapabilities() - self.pcaps = s.getvalue().splitlines() - - s = StringIO.StringIO() - p.mbox = TestMailbox() - p.transport = internet.protocol.FileWrapper(s) - p.do_CAPA() - - self.lpcaps = s.getvalue().splitlines() - p.connectionLost(failure.Failure(Exception("Test harness disconnect"))) - - def contained(self, s, *caps): - for c in caps: - self.assertIn(s, c) - - def testEXPIRE(self): - self.contained("EXPIRE 60", self.caps, self.pcaps, self.lpcaps) - - def testLOGIN_DELAY(self): - self.contained("LOGIN-DELAY 120", self.caps, self.pcaps, self.lpcaps) - - - -class TestRealm: - def requestAvatar(self, avatarId, mind, *interfaces): - if avatarId == 'testuser': - return pop3.IMailbox, DummyMailbox(ValueError), lambda: None - assert False - - - -class SASLTestCase(unittest.TestCase): - def testValidLogin(self): - p = pop3.POP3() - p.factory = TestServerFactory() - p.factory.challengers = {'CRAM-MD5': cred.credentials.CramMD5Credentials} - p.portal = cred.portal.Portal(TestRealm()) - ch = cred.checkers.InMemoryUsernamePasswordDatabaseDontUse() - ch.addUser('testuser', 'testpassword') - p.portal.registerChecker(ch) - - s = StringIO.StringIO() - p.transport = internet.protocol.FileWrapper(s) - p.connectionMade() - - p.lineReceived("CAPA") - self.failUnless(s.getvalue().find("SASL CRAM-MD5") >= 0) - - p.lineReceived("AUTH CRAM-MD5") - chal = s.getvalue().splitlines()[-1][2:] - chal = base64.decodestring(chal) - response = hmac.HMAC('testpassword', chal).hexdigest() - - p.lineReceived(base64.encodestring('testuser ' + response).rstrip('\n')) - self.failUnless(p.mbox) - self.failUnless(s.getvalue().splitlines()[-1].find("+OK") >= 0) - p.connectionLost(failure.Failure(Exception("Test harness disconnect"))) - - - -class CommandMixin: - """ - Tests for all the commands a POP3 server is allowed to receive. - """ - - extraMessage = '''\ -From: guy -To: fellow - -More message text for you. -''' - - - def setUp(self): - """ - Make a POP3 server protocol instance hooked up to a simple mailbox and - a transport that buffers output to a StringIO. - """ - p = pop3.POP3() - p.mbox = self.mailboxType(self.exceptionType) - p.schedule = list - self.pop3Server = p - - s = StringIO.StringIO() - p.transport = internet.protocol.FileWrapper(s) - p.connectionMade() - s.truncate(0) - self.pop3Transport = s - - - def tearDown(self): - """ - Disconnect the server protocol so it can clean up anything it might - need to clean up. - """ - self.pop3Server.connectionLost(failure.Failure(Exception("Test harness disconnect"))) - - - def _flush(self): - """ - Do some of the things that the reactor would take care of, if the - reactor were actually running. - """ - # Oh man FileWrapper is pooh. - self.pop3Server.transport._checkProducer() - - - def testLIST(self): - """ - Test the two forms of list: with a message index number, which should - return a short-form response, and without a message index number, which - should return a long-form response, one line per message. - """ - p = self.pop3Server - s = self.pop3Transport - - p.lineReceived("LIST 1") - self._flush() - self.assertEquals(s.getvalue(), "+OK 1 44\r\n") - s.truncate(0) - - p.lineReceived("LIST") - self._flush() - self.assertEquals(s.getvalue(), "+OK 1\r\n1 44\r\n.\r\n") - - - def testLISTWithBadArgument(self): - """ - Test that non-integers and out-of-bound integers produce appropriate - error responses. - """ - p = self.pop3Server - s = self.pop3Transport - - p.lineReceived("LIST a") - self.assertEquals( - s.getvalue(), - "-ERR Invalid message-number: 'a'\r\n") - s.truncate(0) - - p.lineReceived("LIST 0") - self.assertEquals( - s.getvalue(), - "-ERR Invalid message-number: 0\r\n") - s.truncate(0) - - p.lineReceived("LIST 2") - self.assertEquals( - s.getvalue(), - "-ERR Invalid message-number: 2\r\n") - s.truncate(0) - - - def testUIDL(self): - """ - Test the two forms of the UIDL command. These are just like the two - forms of the LIST command. - """ - p = self.pop3Server - s = self.pop3Transport - - p.lineReceived("UIDL 1") - self.assertEquals(s.getvalue(), "+OK 0\r\n") - s.truncate(0) - - p.lineReceived("UIDL") - self._flush() - self.assertEquals(s.getvalue(), "+OK \r\n1 0\r\n.\r\n") - - - def testUIDLWithBadArgument(self): - """ - Test that UIDL with a non-integer or an out-of-bounds integer produces - the appropriate error response. - """ - p = self.pop3Server - s = self.pop3Transport - - p.lineReceived("UIDL a") - self.assertEquals( - s.getvalue(), - "-ERR Bad message number argument\r\n") - s.truncate(0) - - p.lineReceived("UIDL 0") - self.assertEquals( - s.getvalue(), - "-ERR Bad message number argument\r\n") - s.truncate(0) - - p.lineReceived("UIDL 2") - self.assertEquals( - s.getvalue(), - "-ERR Bad message number argument\r\n") - s.truncate(0) - - - def testSTAT(self): - """ - Test the single form of the STAT command, which returns a short-form - response of the number of messages in the mailbox and their total size. - """ - p = self.pop3Server - s = self.pop3Transport - - p.lineReceived("STAT") - self._flush() - self.assertEquals(s.getvalue(), "+OK 1 44\r\n") - - - def testRETR(self): - """ - Test downloading a message. - """ - p = self.pop3Server - s = self.pop3Transport - - p.lineReceived("RETR 1") - self._flush() - self.assertEquals( - s.getvalue(), - "+OK 44\r\n" - "From: moshe\r\n" - "To: moshe\r\n" - "\r\n" - "How are you, friend?\r\n" - ".\r\n") - s.truncate(0) - - - def testRETRWithBadArgument(self): - """ - Test that trying to download a message with a bad argument, either not - an integer or an out-of-bounds integer, fails with the appropriate - error response. - """ - p = self.pop3Server - s = self.pop3Transport - - p.lineReceived("RETR a") - self.assertEquals( - s.getvalue(), - "-ERR Bad message number argument\r\n") - s.truncate(0) - - p.lineReceived("RETR 0") - self.assertEquals( - s.getvalue(), - "-ERR Bad message number argument\r\n") - s.truncate(0) - - p.lineReceived("RETR 2") - self.assertEquals( - s.getvalue(), - "-ERR Bad message number argument\r\n") - s.truncate(0) - - - def testTOP(self): - """ - Test downloading the headers and part of the body of a message. - """ - p = self.pop3Server - s = self.pop3Transport - p.mbox.messages.append(self.extraMessage) - - p.lineReceived("TOP 1 0") - self._flush() - self.assertEquals( - s.getvalue(), - "+OK Top of message follows\r\n" - "From: moshe\r\n" - "To: moshe\r\n" - "\r\n" - ".\r\n") - - - def testTOPWithBadArgument(self): - """ - Test that trying to download a message with a bad argument, either a - message number which isn't an integer or is an out-of-bounds integer or - a number of lines which isn't an integer or is a negative integer, - fails with the appropriate error response. - """ - p = self.pop3Server - s = self.pop3Transport - p.mbox.messages.append(self.extraMessage) - - p.lineReceived("TOP 1 a") - self.assertEquals( - s.getvalue(), - "-ERR Bad line count argument\r\n") - s.truncate(0) - - p.lineReceived("TOP 1 -1") - self.assertEquals( - s.getvalue(), - "-ERR Bad line count argument\r\n") - s.truncate(0) - - p.lineReceived("TOP a 1") - self.assertEquals( - s.getvalue(), - "-ERR Bad message number argument\r\n") - s.truncate(0) - - p.lineReceived("TOP 0 1") - self.assertEquals( - s.getvalue(), - "-ERR Bad message number argument\r\n") - s.truncate(0) - - p.lineReceived("TOP 3 1") - self.assertEquals( - s.getvalue(), - "-ERR Bad message number argument\r\n") - s.truncate(0) - - - def testLAST(self): - """ - Test the exceedingly pointless LAST command, which tells you the - highest message index which you have already downloaded. - """ - p = self.pop3Server - s = self.pop3Transport - p.mbox.messages.append(self.extraMessage) - - p.lineReceived('LAST') - self.assertEquals( - s.getvalue(), - "+OK 0\r\n") - s.truncate(0) - - - def testRetrieveUpdatesHighest(self): - """ - Test that issuing a RETR command updates the LAST response. - """ - p = self.pop3Server - s = self.pop3Transport - p.mbox.messages.append(self.extraMessage) - - p.lineReceived('RETR 2') - self._flush() - s.truncate(0) - p.lineReceived('LAST') - self.assertEquals( - s.getvalue(), - '+OK 2\r\n') - s.truncate(0) - - - def testTopUpdatesHighest(self): - """ - Test that issuing a TOP command updates the LAST response. - """ - p = self.pop3Server - s = self.pop3Transport - p.mbox.messages.append(self.extraMessage) - - p.lineReceived('TOP 2 10') - self._flush() - s.truncate(0) - p.lineReceived('LAST') - self.assertEquals( - s.getvalue(), - '+OK 2\r\n') - - - def testHighestOnlyProgresses(self): - """ - Test that downloading a message with a smaller index than the current - LAST response doesn't change the LAST response. - """ - p = self.pop3Server - s = self.pop3Transport - p.mbox.messages.append(self.extraMessage) - - p.lineReceived('RETR 2') - self._flush() - p.lineReceived('TOP 1 10') - self._flush() - s.truncate(0) - p.lineReceived('LAST') - self.assertEquals( - s.getvalue(), - '+OK 2\r\n') - - - def testResetClearsHighest(self): - """ - Test that issuing RSET changes the LAST response to 0. - """ - p = self.pop3Server - s = self.pop3Transport - p.mbox.messages.append(self.extraMessage) - - p.lineReceived('RETR 2') - self._flush() - p.lineReceived('RSET') - s.truncate(0) - p.lineReceived('LAST') - self.assertEquals( - s.getvalue(), - '+OK 0\r\n') - - - -_listMessageDeprecation = ( - "twisted.mail.pop3.IMailbox.listMessages may not " - "raise IndexError for out-of-bounds message numbers: " - "raise ValueError instead.") -_listMessageSuppression = util.suppress( - message=_listMessageDeprecation, - category=PendingDeprecationWarning) - -_getUidlDeprecation = ( - "twisted.mail.pop3.IMailbox.getUidl may not " - "raise IndexError for out-of-bounds message numbers: " - "raise ValueError instead.") -_getUidlSuppression = util.suppress( - message=_getUidlDeprecation, - category=PendingDeprecationWarning) - -class IndexErrorCommandTestCase(CommandMixin, unittest.TestCase): - """ - Run all of the command tests against a mailbox which raises IndexError - when an out of bounds request is made. This behavior will be deprecated - shortly and then removed. - """ - exceptionType = IndexError - mailboxType = DummyMailbox - - def testLISTWithBadArgument(self): - return CommandMixin.testLISTWithBadArgument(self) - testLISTWithBadArgument.suppress = [_listMessageSuppression] - - - def testUIDLWithBadArgument(self): - return CommandMixin.testUIDLWithBadArgument(self) - testUIDLWithBadArgument.suppress = [_getUidlSuppression] - - - def testTOPWithBadArgument(self): - return CommandMixin.testTOPWithBadArgument(self) - testTOPWithBadArgument.suppress = [_listMessageSuppression] - - - def testRETRWithBadArgument(self): - return CommandMixin.testRETRWithBadArgument(self) - testRETRWithBadArgument.suppress = [_listMessageSuppression] - - - -class ValueErrorCommandTestCase(CommandMixin, unittest.TestCase): - """ - Run all of the command tests against a mailbox which raises ValueError - when an out of bounds request is made. This is the correct behavior and - after support for mailboxes which raise IndexError is removed, this will - become just C{CommandTestCase}. - """ - exceptionType = ValueError - mailboxType = DummyMailbox - - - -class SyncDeferredMailbox(DummyMailbox): - """ - Mailbox which has a listMessages implementation which returns a Deferred - which has already fired. - """ - def listMessages(self, n=None): - return defer.succeed(DummyMailbox.listMessages(self, n)) - - - -class IndexErrorSyncDeferredCommandTestCase(IndexErrorCommandTestCase): - """ - Run all of the L{IndexErrorCommandTestCase} tests with a - synchronous-Deferred returning IMailbox implementation. - """ - mailboxType = SyncDeferredMailbox - - - -class ValueErrorSyncDeferredCommandTestCase(ValueErrorCommandTestCase): - """ - Run all of the L{ValueErrorCommandTestCase} tests with a - synchronous-Deferred returning IMailbox implementation. - """ - mailboxType = SyncDeferredMailbox - - - -class AsyncDeferredMailbox(DummyMailbox): - """ - Mailbox which has a listMessages implementation which returns a Deferred - which has not yet fired. - """ - def __init__(self, *a, **kw): - self.waiting = [] - DummyMailbox.__init__(self, *a, **kw) - - - def listMessages(self, n=None): - d = defer.Deferred() - # See AsyncDeferredMailbox._flush - self.waiting.append((d, DummyMailbox.listMessages(self, n))) - return d - - - -class IndexErrorAsyncDeferredCommandTestCase(IndexErrorCommandTestCase): - """ - Run all of the L{IndexErrorCommandTestCase} tests with an asynchronous-Deferred - returning IMailbox implementation. - """ - mailboxType = AsyncDeferredMailbox - - def _flush(self): - """ - Fire whatever Deferreds we've built up in our mailbox. - """ - while self.pop3Server.mbox.waiting: - d, a = self.pop3Server.mbox.waiting.pop() - d.callback(a) - IndexErrorCommandTestCase._flush(self) - - - -class ValueErrorAsyncDeferredCommandTestCase(ValueErrorCommandTestCase): - """ - Run all of the L{IndexErrorCommandTestCase} tests with an asynchronous-Deferred - returning IMailbox implementation. - """ - mailboxType = AsyncDeferredMailbox - - def _flush(self): - """ - Fire whatever Deferreds we've built up in our mailbox. - """ - while self.pop3Server.mbox.waiting: - d, a = self.pop3Server.mbox.waiting.pop() - d.callback(a) - ValueErrorCommandTestCase._flush(self) - -class POP3MiscTestCase(unittest.TestCase): - """ - Miscellaneous tests more to do with module/package structure than - anything to do with the Post Office Protocol. - """ - def test_all(self): - """ - This test checks that all names listed in - twisted.mail.pop3.__all__ are actually present in the module. - """ - mod = twisted.mail.pop3 - for attr in mod.__all__: - self.failUnless(hasattr(mod, attr)) diff --git a/tools/buildbot/pylibs/twisted/mail/test/test_pop3client.py b/tools/buildbot/pylibs/twisted/mail/test/test_pop3client.py deleted file mode 100644 index e2c3b8d..0000000 --- a/tools/buildbot/pylibs/twisted/mail/test/test_pop3client.py +++ /dev/null @@ -1,573 +0,0 @@ -# -*- test-case-name: twisted.mail.test.test_pop3client -*- -# Copyright (c) 2001-2004 Divmod Inc. -# See LICENSE for details. - -from zope.interface import directlyProvides - -from twisted.mail.pop3 import AdvancedPOP3Client as POP3Client -from twisted.mail.pop3 import InsecureAuthenticationDisallowed -from twisted.mail.pop3 import ServerErrorResponse -from twisted.protocols import loopback -from twisted.internet import reactor, defer, error, protocol, interfaces -from twisted.python import log - -from twisted.trial import unittest -from twisted.test.proto_helpers import StringTransport -from twisted.protocols import basic - -from twisted.mail.test import pop3testserver - -try: - from twisted.test.ssl_helpers import ClientTLSContext, ServerTLSContext -except ImportError: - ClientTLSContext = ServerTLSContext = None - - -class StringTransportWithConnectionLosing(StringTransport): - def loseConnection(self): - self.protocol.connectionLost(error.ConnectionDone()) - - -capCache = {"TOP": None, "LOGIN-DELAY": "180", "UIDL": None, \ - "STLS": None, "USER": None, "SASL": "LOGIN"} -def setUp(greet=True): - p = POP3Client() - - # Skip the CAPA login will issue if it doesn't already have a - # capability cache - p._capCache = capCache - - t = StringTransportWithConnectionLosing() - t.protocol = p - p.makeConnection(t) - - if greet: - p.dataReceived('+OK Hello!\r\n') - - return p, t - -def strip(f): - return lambda result, f=f: f() - -class POP3ClientLoginTestCase(unittest.TestCase): - def testNegativeGreeting(self): - p, t = setUp(greet=False) - p.allowInsecureLogin = True - d = p.login("username", "password") - p.dataReceived('-ERR Offline for maintenance\r\n') - return self.assertFailure( - d, ServerErrorResponse).addCallback( - lambda exc: self.assertEquals(exc.args[0], "Offline for maintenance")) - - - def testOkUser(self): - p, t = setUp() - d = p.user("username") - self.assertEquals(t.value(), "USER username\r\n") - p.dataReceived("+OK send password\r\n") - return d.addCallback(self.assertEqual, "send password") - - def testBadUser(self): - p, t = setUp() - d = p.user("username") - self.assertEquals(t.value(), "USER username\r\n") - p.dataReceived("-ERR account suspended\r\n") - return self.assertFailure( - d, ServerErrorResponse).addCallback( - lambda exc: self.assertEquals(exc.args[0], "account suspended")) - - def testOkPass(self): - p, t = setUp() - d = p.password("password") - self.assertEquals(t.value(), "PASS password\r\n") - p.dataReceived("+OK you're in!\r\n") - return d.addCallback(self.assertEqual, "you're in!") - - def testBadPass(self): - p, t = setUp() - d = p.password("password") - self.assertEquals(t.value(), "PASS password\r\n") - p.dataReceived("-ERR go away\r\n") - return self.assertFailure( - d, ServerErrorResponse).addCallback( - lambda exc: self.assertEquals(exc.args[0], "go away")) - - def testOkLogin(self): - p, t = setUp() - p.allowInsecureLogin = True - d = p.login("username", "password") - self.assertEquals(t.value(), "USER username\r\n") - p.dataReceived("+OK go ahead\r\n") - self.assertEquals(t.value(), "USER username\r\nPASS password\r\n") - p.dataReceived("+OK password accepted\r\n") - return d.addCallback(self.assertEqual, "password accepted") - - def testBadPasswordLogin(self): - p, t = setUp() - p.allowInsecureLogin = True - d = p.login("username", "password") - self.assertEquals(t.value(), "USER username\r\n") - p.dataReceived("+OK waiting on you\r\n") - self.assertEquals(t.value(), "USER username\r\nPASS password\r\n") - p.dataReceived("-ERR bogus login\r\n") - return self.assertFailure( - d, ServerErrorResponse).addCallback( - lambda exc: self.assertEquals(exc.args[0], "bogus login")) - - def testBadUsernameLogin(self): - p, t = setUp() - p.allowInsecureLogin = True - d = p.login("username", "password") - self.assertEquals(t.value(), "USER username\r\n") - p.dataReceived("-ERR bogus login\r\n") - return self.assertFailure( - d, ServerErrorResponse).addCallback( - lambda exc: self.assertEquals(exc.args[0], "bogus login")) - - def testServerGreeting(self): - p, t = setUp(greet=False) - p.dataReceived("+OK lalala this has no challenge\r\n") - self.assertEquals(p.serverChallenge, None) - - def testServerGreetingWithChallenge(self): - p, t = setUp(greet=False) - p.dataReceived("+OK <here is the challenge>\r\n") - self.assertEquals(p.serverChallenge, "<here is the challenge>") - - def testAPOP(self): - p, t = setUp(greet=False) - p.dataReceived("+OK <challenge string goes here>\r\n") - d = p.login("username", "password") - self.assertEquals(t.value(), "APOP username f34f1e464d0d7927607753129cabe39a\r\n") - p.dataReceived("+OK Welcome!\r\n") - return d.addCallback(self.assertEqual, "Welcome!") - - def testInsecureLoginRaisesException(self): - p, t = setUp(greet=False) - p.dataReceived("+OK Howdy\r\n") - d = p.login("username", "password") - self.failIf(t.value()) - return self.assertFailure( - d, InsecureAuthenticationDisallowed) - - - def testSSLTransportConsideredSecure(self): - """ - If a server doesn't offer APOP but the transport is secured using - SSL or TLS, a plaintext login should be allowed, not rejected with - an InsecureAuthenticationDisallowed exception. - """ - p, t = setUp(greet=False) - directlyProvides(t, interfaces.ISSLTransport) - p.dataReceived("+OK Howdy\r\n") - d = p.login("username", "password") - self.assertEquals(t.value(), "USER username\r\n") - t.clear() - p.dataReceived("+OK\r\n") - self.assertEquals(t.value(), "PASS password\r\n") - p.dataReceived("+OK\r\n") - return d - - - -class ListConsumer: - def __init__(self): - self.data = {} - - def consume(self, (item, value)): - self.data.setdefault(item, []).append(value) - -class MessageConsumer: - def __init__(self): - self.data = [] - - def consume(self, line): - self.data.append(line) - -class POP3ClientListTestCase(unittest.TestCase): - def testListSize(self): - p, t = setUp() - d = p.listSize() - self.assertEquals(t.value(), "LIST\r\n") - p.dataReceived("+OK Here it comes\r\n") - p.dataReceived("1 3\r\n2 2\r\n3 1\r\n.\r\n") - return d.addCallback(self.assertEqual, [3, 2, 1]) - - def testListSizeWithConsumer(self): - p, t = setUp() - c = ListConsumer() - f = c.consume - d = p.listSize(f) - self.assertEquals(t.value(), "LIST\r\n") - p.dataReceived("+OK Here it comes\r\n") - p.dataReceived("1 3\r\n2 2\r\n3 1\r\n") - self.assertEquals(c.data, {0: [3], 1: [2], 2: [1]}) - p.dataReceived("5 3\r\n6 2\r\n7 1\r\n") - self.assertEquals(c.data, {0: [3], 1: [2], 2: [1], 4: [3], 5: [2], 6: [1]}) - p.dataReceived(".\r\n") - return d.addCallback(self.assertIdentical, f) - - def testFailedListSize(self): - p, t = setUp() - d = p.listSize() - self.assertEquals(t.value(), "LIST\r\n") - p.dataReceived("-ERR Fatal doom server exploded\r\n") - return self.assertFailure( - d, ServerErrorResponse).addCallback( - lambda exc: self.assertEquals(exc.args[0], "Fatal doom server exploded")) - - def testListUID(self): - p, t = setUp() - d = p.listUID() - self.assertEquals(t.value(), "UIDL\r\n") - p.dataReceived("+OK Here it comes\r\n") - p.dataReceived("1 abc\r\n2 def\r\n3 ghi\r\n.\r\n") - return d.addCallback(self.assertEqual, ["abc", "def", "ghi"]) - - def testListUIDWithConsumer(self): - p, t = setUp() - c = ListConsumer() - f = c.consume - d = p.listUID(f) - self.assertEquals(t.value(), "UIDL\r\n") - p.dataReceived("+OK Here it comes\r\n") - p.dataReceived("1 xyz\r\n2 abc\r\n5 mno\r\n") - self.assertEquals(c.data, {0: ["xyz"], 1: ["abc"], 4: ["mno"]}) - p.dataReceived(".\r\n") - return d.addCallback(self.assertIdentical, f) - - def testFailedListUID(self): - p, t = setUp() - d = p.listUID() - self.assertEquals(t.value(), "UIDL\r\n") - p.dataReceived("-ERR Fatal doom server exploded\r\n") - return self.assertFailure( - d, ServerErrorResponse).addCallback( - lambda exc: self.assertEquals(exc.args[0], "Fatal doom server exploded")) - -class POP3ClientMessageTestCase(unittest.TestCase): - def testRetrieve(self): - p, t = setUp() - d = p.retrieve(7) - self.assertEquals(t.value(), "RETR 8\r\n") - p.dataReceived("+OK Message incoming\r\n") - p.dataReceived("La la la here is message text\r\n") - p.dataReceived("..Further message text tra la la\r\n") - p.dataReceived(".\r\n") - return d.addCallback( - self.assertEqual, - ["La la la here is message text", - ".Further message text tra la la"]) - - def testRetrieveWithConsumer(self): - p, t = setUp() - c = MessageConsumer() - f = c.consume - d = p.retrieve(7, f) - self.assertEquals(t.value(), "RETR 8\r\n") - p.dataReceived("+OK Message incoming\r\n") - p.dataReceived("La la la here is message text\r\n") - p.dataReceived("..Further message text\r\n.\r\n") - return d.addCallback(self._cbTestRetrieveWithConsumer, f, c) - - def _cbTestRetrieveWithConsumer(self, result, f, c): - self.assertIdentical(result, f) - self.assertEquals(c.data, ["La la la here is message text", - ".Further message text"]) - - def testPartialRetrieve(self): - p, t = setUp() - d = p.retrieve(7, lines=2) - self.assertEquals(t.value(), "TOP 8 2\r\n") - p.dataReceived("+OK 2 lines on the way\r\n") - p.dataReceived("Line the first! Woop\r\n") - p.dataReceived("Line the last! Bye\r\n") - p.dataReceived(".\r\n") - return d.addCallback( - self.assertEqual, - ["Line the first! Woop", - "Line the last! Bye"]) - - def testPartialRetrieveWithConsumer(self): - p, t = setUp() - c = MessageConsumer() - f = c.consume - d = p.retrieve(7, f, lines=2) - self.assertEquals(t.value(), "TOP 8 2\r\n") - p.dataReceived("+OK 2 lines on the way\r\n") - p.dataReceived("Line the first! Woop\r\n") - p.dataReceived("Line the last! Bye\r\n") - p.dataReceived(".\r\n") - return d.addCallback(self._cbTestPartialRetrieveWithConsumer, f, c) - - def _cbTestPartialRetrieveWithConsumer(self, result, f, c): - self.assertIdentical(result, f) - self.assertEquals(c.data, ["Line the first! Woop", - "Line the last! Bye"]) - - def testFailedRetrieve(self): - p, t = setUp() - d = p.retrieve(0) - self.assertEquals(t.value(), "RETR 1\r\n") - p.dataReceived("-ERR Fatal doom server exploded\r\n") - return self.assertFailure( - d, ServerErrorResponse).addCallback( - lambda exc: self.assertEquals(exc.args[0], "Fatal doom server exploded")) - - - def test_concurrentRetrieves(self): - """ - Issue three retrieve calls immediately without waiting for any to - succeed and make sure they all do succeed eventually. - """ - p, t = setUp() - messages = [ - p.retrieve(i).addCallback( - self.assertEquals, - ["First line of %d." % (i + 1,), - "Second line of %d." % (i + 1,)]) - for i - in range(3)] - - for i in range(1, 4): - self.assertEquals(t.value(), "RETR %d\r\n" % (i,)) - t.clear() - p.dataReceived("+OK 2 lines on the way\r\n") - p.dataReceived("First line of %d.\r\n" % (i,)) - p.dataReceived("Second line of %d.\r\n" % (i,)) - self.assertEquals(t.value(), "") - p.dataReceived(".\r\n") - - return defer.DeferredList(messages, fireOnOneErrback=True) - - - -class POP3ClientMiscTestCase(unittest.TestCase): - def testCapability(self): - p, t = setUp() - d = p.capabilities(useCache=0) - self.assertEquals(t.value(), "CAPA\r\n") - p.dataReceived("+OK Capabilities on the way\r\n") - p.dataReceived("X\r\nY\r\nZ\r\nA 1 2 3\r\nB 1 2\r\nC 1\r\n.\r\n") - return d.addCallback( - self.assertEqual, - {"X": None, "Y": None, "Z": None, - "A": ["1", "2", "3"], - "B": ["1", "2"], - "C": ["1"]}) - - def testCapabilityError(self): - p, t = setUp() - d = p.capabilities(useCache=0) - self.assertEquals(t.value(), "CAPA\r\n") - p.dataReceived("-ERR This server is lame!\r\n") - return d.addCallback(self.assertEquals, {}) - - def testStat(self): - p, t = setUp() - d = p.stat() - self.assertEquals(t.value(), "STAT\r\n") - p.dataReceived("+OK 1 1212\r\n") - return d.addCallback(self.assertEqual, (1, 1212)) - - def testStatError(self): - p, t = setUp() - d = p.stat() - self.assertEquals(t.value(), "STAT\r\n") - p.dataReceived("-ERR This server is lame!\r\n") - return self.assertFailure( - d, ServerErrorResponse).addCallback( - lambda exc: self.assertEquals(exc.args[0], "This server is lame!")) - - def testNoop(self): - p, t = setUp() - d = p.noop() - self.assertEquals(t.value(), "NOOP\r\n") - p.dataReceived("+OK No-op to you too!\r\n") - return d.addCallback(self.assertEqual, "No-op to you too!") - - def testNoopError(self): - p, t = setUp() - d = p.noop() - self.assertEquals(t.value(), "NOOP\r\n") - p.dataReceived("-ERR This server is lame!\r\n") - return self.assertFailure( - d, ServerErrorResponse).addCallback( - lambda exc: self.assertEquals(exc.args[0], "This server is lame!")) - - def testRset(self): - p, t = setUp() - d = p.reset() - self.assertEquals(t.value(), "RSET\r\n") - p.dataReceived("+OK Reset state\r\n") - return d.addCallback(self.assertEqual, "Reset state") - - def testRsetError(self): - p, t = setUp() - d = p.reset() - self.assertEquals(t.value(), "RSET\r\n") - p.dataReceived("-ERR This server is lame!\r\n") - return self.assertFailure( - d, ServerErrorResponse).addCallback( - lambda exc: self.assertEquals(exc.args[0], "This server is lame!")) - - def testDelete(self): - p, t = setUp() - d = p.delete(3) - self.assertEquals(t.value(), "DELE 4\r\n") - p.dataReceived("+OK Hasta la vista\r\n") - return d.addCallback(self.assertEqual, "Hasta la vista") - - def testDeleteError(self): - p, t = setUp() - d = p.delete(3) - self.assertEquals(t.value(), "DELE 4\r\n") - p.dataReceived("-ERR Winner is not you.\r\n") - return self.assertFailure( - d, ServerErrorResponse).addCallback( - lambda exc: self.assertEquals(exc.args[0], "Winner is not you.")) - - -class SimpleClient(POP3Client): - def __init__(self, deferred, contextFactory = None): - self.deferred = deferred - self.allowInsecureLogin = True - - def serverGreeting(self, challenge): - self.deferred.callback(None) - -class POP3HelperMixin: - serverCTX = None - clientCTX = None - - def setUp(self): - d = defer.Deferred() - self.server = pop3testserver.POP3TestServer(contextFactory=self.serverCTX) - self.client = SimpleClient(d, contextFactory=self.clientCTX) - self.client.timeout = 30 - self.connected = d - - def tearDown(self): - del self.server - del self.client - del self.connected - - def _cbStopClient(self, ignore): - self.client.transport.loseConnection() - - def _ebGeneral(self, failure): - self.client.transport.loseConnection() - self.server.transport.loseConnection() - return failure - - def loopback(self): - return loopback.loopbackTCP(self.server, self.client, noisy=False) - - -class TLSServerFactory(protocol.ServerFactory): - class protocol(basic.LineReceiver): - context = None - output = [] - def connectionMade(self): - self.factory.input = [] - self.output = self.output[:] - map(self.sendLine, self.output.pop(0)) - def lineReceived(self, line): - self.factory.input.append(line) - map(self.sendLine, self.output.pop(0)) - if line == 'STLS': - self.transport.startTLS(self.context) - - -class POP3TLSTestCase(unittest.TestCase): - def testStartTLS(self): - sf = TLSServerFactory() - sf.protocol.output = [ - ['+OK'], # Server greeting - ['+OK', 'STLS', '.'], # CAPA response - ['+OK'], # STLS response - ['+OK', '.'], # Second CAPA response - ['+OK'] # QUIT response - ] - sf.protocol.context = ServerTLSContext() - port = reactor.listenTCP(0, sf, interface='127.0.0.1') - H = port.getHost().host - P = port.getHost().port - - cp = SimpleClient(defer.Deferred(), ClientTLSContext()) - cf = protocol.ClientFactory() - cf.protocol = lambda: cp - - conn = reactor.connectTCP(H, P, cf) - - def cbConnected(ignored): - log.msg("Connected to server; starting TLS") - return cp.startTLS() - - def cbStartedTLS(ignored): - log.msg("Started TLS; disconnecting") - return cp.quit() - - def cbDisconnected(ign): - log.msg("Disconnected; asserting correct input received") - self.assertEquals( - sf.input, - ['CAPA', 'STLS', 'CAPA', 'QUIT']) - - def cleanup(result): - log.msg("Asserted correct input; disconnecting client and shutting down server") - conn.disconnect() - - def cbShutdown(ignored): - log.msg("Shut down server") - return result - - return defer.maybeDeferred(port.stopListening).addCallback(cbShutdown) - - cp.deferred.addCallback(cbConnected) - cp.deferred.addCallback(cbStartedTLS) - cp.deferred.addCallback(cbDisconnected) - cp.deferred.addBoth(cleanup) - - return cp.deferred - - -class POP3TimeoutTestCase(POP3HelperMixin, unittest.TestCase): - def testTimeout(self): - def login(): - d = self.client.login('test', 'twisted') - d.addCallback(loggedIn) - d.addErrback(timedOut) - return d - - def loggedIn(result): - self.fail("Successfully logged in!? Impossible!") - - - def timedOut(failure): - failure.trap(error.TimeoutError) - self._cbStopClient(None) - - def quit(): - return self.client.quit() - - self.client.timeout = 0.01 - - # Tell the server to not return a response to client. This - # will trigger a timeout. - pop3testserver.TIMEOUT_RESPONSE = True - - methods = [login, quit] - map(self.connected.addCallback, map(strip, methods)) - self.connected.addCallback(self._cbStopClient) - self.connected.addErrback(self._ebGeneral) - return self.loopback() - - -if ClientTLSContext is None: - for case in (POP3TLSTestCase,): - case.skip = "OpenSSL not present" -elif interfaces.IReactorSSL(reactor, None) is None: - for case in (POP3TLSTestCase,): - case.skip = "Reactor doesn't support SSL" - diff --git a/tools/buildbot/pylibs/twisted/mail/test/test_smtp.py b/tools/buildbot/pylibs/twisted/mail/test/test_smtp.py deleted file mode 100644 index 34d8bcf..0000000 --- a/tools/buildbot/pylibs/twisted/mail/test/test_smtp.py +++ /dev/null @@ -1,982 +0,0 @@ -# Copyright (c) 2001-2007 Twisted Matrix Laboratories. -# See LICENSE for details. - -""" -Test cases for twisted.mail.smtp module. -""" - -from zope.interface import implements - -from twisted.trial import unittest, util -from twisted.protocols import basic, loopback -from twisted.mail import smtp -from twisted.internet import defer, protocol, reactor, interfaces -from twisted.internet import address, error, task -from twisted.test.test_protocols import StringIOWithoutClosing -from twisted.test.proto_helpers import StringTransport - -from twisted import cred -import twisted.cred.error -import twisted.cred.portal -import twisted.cred.checkers -import twisted.cred.credentials - -from twisted.cred.portal import IRealm, Portal -from twisted.cred.checkers import ICredentialsChecker, AllowAnonymousAccess -from twisted.cred.credentials import IAnonymous -from twisted.cred.error import UnauthorizedLogin - -from twisted.mail import imap4 - - -try: - from twisted.test.ssl_helpers import ClientTLSContext, ServerTLSContext -except ImportError: - ClientTLSContext = ServerTLSContext = None - -import re - -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO - -def spameater(*spam, **eggs): - return None - -class DummyMessage: - - def __init__(self, domain, user): - self.domain = domain - self.user = user - self.buffer = [] - - def lineReceived(self, line): - # Throw away the generated Received: header - if not re.match('Received: From yyy.com \(\[.*\]\) by localhost;', line): - self.buffer.append(line) - - def eomReceived(self): - message = '\n'.join(self.buffer) + '\n' - self.domain.messages[self.user.dest.local].append(message) - deferred = defer.Deferred() - deferred.callback("saved") - return deferred - - -class DummyDomain: - - def __init__(self, names): - self.messages = {} - for name in names: - self.messages[name] = [] - - def exists(self, user): - if self.messages.has_key(user.dest.local): - return defer.succeed(lambda: self.startMessage(user)) - return defer.fail(smtp.SMTPBadRcpt(user)) - - def startMessage(self, user): - return DummyMessage(self, user) - -class SMTPTestCase(unittest.TestCase): - - messages = [('foo@bar.com', ['foo@baz.com', 'qux@baz.com'], '''\ -Subject: urgent\015 -\015 -Someone set up us the bomb!\015 -''')] - - mbox = {'foo': ['Subject: urgent\n\nSomeone set up us the bomb!\n']} - - def setUp(self): - self.factory = smtp.SMTPFactory() - self.factory.domains = {} - self.factory.domains['baz.com'] = DummyDomain(['foo']) - self.output = StringIOWithoutClosing() - self.transport = protocol.FileWrapper(self.output) - - def testMessages(self): - from twisted.mail import protocols - protocol = protocols.DomainSMTP() - protocol.service = self.factory - protocol.factory = self.factory - protocol.receivedHeader = spameater - protocol.makeConnection(self.transport) - protocol.lineReceived('HELO yyy.com') - for message in self.messages: - protocol.lineReceived('MAIL FROM:<%s>' % message[0]) - for target in message[1]: - protocol.lineReceived('RCPT TO:<%s>' % target) - protocol.lineReceived('DATA') - protocol.dataReceived(message[2]) - protocol.lineReceived('.') - protocol.lineReceived('QUIT') - if self.mbox != self.factory.domains['baz.com'].messages: - raise AssertionError(self.factory.domains['baz.com'].messages) - protocol.setTimeout(None) - - testMessages.suppress = [util.suppress(message='DomainSMTP', category=DeprecationWarning)] - -mail = '''\ -Subject: hello - -Goodbye -''' - -class MyClient: - def __init__(self): - self.mail = 'moshez@foo.bar', ['moshez@foo.bar'], mail - - def getMailFrom(self): - return self.mail[0] - - def getMailTo(self): - return self.mail[1] - - def getMailData(self): - return StringIO(self.mail[2]) - - def sentMail(self, code, resp, numOk, addresses, log): - self.mail = None, None, None - -class MySMTPClient(MyClient, smtp.SMTPClient): - def __init__(self): - smtp.SMTPClient.__init__(self, 'foo.baz') - MyClient.__init__(self) - -class MyESMTPClient(MyClient, smtp.ESMTPClient): - def __init__(self, secret = '', contextFactory = None): - smtp.ESMTPClient.__init__(self, secret, contextFactory, 'foo.baz') - MyClient.__init__(self) - -class LoopbackMixin: - def loopback(self, server, client): - return loopback.loopbackTCP(server, client) - -class LoopbackTestCase(LoopbackMixin): - def testMessages(self): - factory = smtp.SMTPFactory() - factory.domains = {} - factory.domains['foo.bar'] = DummyDomain(['moshez']) - from twisted.mail.protocols import DomainSMTP - protocol = DomainSMTP() - protocol.service = factory - protocol.factory = factory - clientProtocol = self.clientClass() - return self.loopback(protocol, clientProtocol) - testMessages.suppress = [util.suppress(message='DomainSMTP', category=DeprecationWarning)] - -class LoopbackSMTPTestCase(LoopbackTestCase, unittest.TestCase): - clientClass = MySMTPClient - -class LoopbackESMTPTestCase(LoopbackTestCase, unittest.TestCase): - clientClass = MyESMTPClient - - -class FakeSMTPServer(basic.LineReceiver): - - clientData = [ - '220 hello', '250 nice to meet you', - '250 great', '250 great', '354 go on, lad' - ] - - def connectionMade(self): - self.buffer = [] - self.clientData = self.clientData[:] - self.clientData.reverse() - self.sendLine(self.clientData.pop()) - - def lineReceived(self, line): - self.buffer.append(line) - if line == "QUIT": - self.transport.write("221 see ya around\r\n") - self.transport.loseConnection() - elif line == ".": - self.transport.write("250 gotcha\r\n") - elif line == "RSET": - self.transport.loseConnection() - - if self.clientData: - self.sendLine(self.clientData.pop()) - - -class SMTPClientTestCase(unittest.TestCase, LoopbackMixin): - - expected_output = [ - 'HELO foo.baz', 'MAIL FROM:<moshez@foo.bar>', - 'RCPT TO:<moshez@foo.bar>', 'DATA', - 'Subject: hello', '', 'Goodbye', '.', 'RSET' - ] - - def testMessages(self): - # this test is disabled temporarily - client = MySMTPClient() - server = FakeSMTPServer() - d = self.loopback(server, client) - d.addCallback(lambda x : - self.assertEquals(server.buffer, self.expected_output)) - return d - -class DummySMTPMessage: - - def __init__(self, protocol, users): - self.protocol = protocol - self.users = users - self.buffer = [] - - def lineReceived(self, line): - self.buffer.append(line) - - def eomReceived(self): - message = '\n'.join(self.buffer) + '\n' - helo, origin = self.users[0].helo[0], str(self.users[0].orig) - recipients = [] - for user in self.users: - recipients.append(str(user)) - self.protocol.message[tuple(recipients)] = (helo, origin, recipients, message) - return defer.succeed("saved") - -class DummyProto: - def connectionMade(self): - self.dummyMixinBase.connectionMade(self) - self.message = {} - - def startMessage(self, users): - return DummySMTPMessage(self, users) - - def receivedHeader(*spam): - return None - - def validateTo(self, user): - self.delivery = DummyDelivery() - return lambda: self.startMessage([user]) - - def validateFrom(self, helo, origin): - return origin - -class DummySMTP(DummyProto, smtp.SMTP): - dummyMixinBase = smtp.SMTP - -class DummyESMTP(DummyProto, smtp.ESMTP): - dummyMixinBase = smtp.ESMTP - -class AnotherTestCase: - serverClass = None - clientClass = None - - messages = [ ('foo.com', 'moshez@foo.com', ['moshez@bar.com'], - 'moshez@foo.com', ['moshez@bar.com'], '''\ -From: Moshe -To: Moshe - -Hi, -how are you? -'''), - ('foo.com', 'tttt@rrr.com', ['uuu@ooo', 'yyy@eee'], - 'tttt@rrr.com', ['uuu@ooo', 'yyy@eee'], '''\ -Subject: pass - -..rrrr.. -'''), - ('foo.com', '@this,@is,@ignored:foo@bar.com', - ['@ignore,@this,@too:bar@foo.com'], - 'foo@bar.com', ['bar@foo.com'], '''\ -Subject: apa -To: foo - -123 -. -456 -'''), - ] - - data = [ - ('', '220.*\r\n$', None, None), - ('HELO foo.com\r\n', '250.*\r\n$', None, None), - ('RSET\r\n', '250.*\r\n$', None, None), - ] - for helo_, from_, to_, realfrom, realto, msg in messages: - data.append(('MAIL FROM:<%s>\r\n' % from_, '250.*\r\n', - None, None)) - for rcpt in to_: - data.append(('RCPT TO:<%s>\r\n' % rcpt, '250.*\r\n', - None, None)) - - data.append(('DATA\r\n','354.*\r\n', - msg, ('250.*\r\n', - (helo_, realfrom, realto, msg)))) - - - def testBuffer(self): - output = StringIOWithoutClosing() - a = self.serverClass() - class fooFactory: - domain = 'foo.com' - - a.factory = fooFactory() - a.makeConnection(protocol.FileWrapper(output)) - for (send, expect, msg, msgexpect) in self.data: - if send: - a.dataReceived(send) - data = output.getvalue() - output.truncate(0) - if not re.match(expect, data): - raise AssertionError, (send, expect, data) - if data[:3] == '354': - for line in msg.splitlines(): - if line and line[0] == '.': - line = '.' + line - a.dataReceived(line + '\r\n') - a.dataReceived('.\r\n') - # Special case for DATA. Now we want a 250, and then - # we compare the messages - data = output.getvalue() - output.truncate() - resp, msgdata = msgexpect - if not re.match(resp, data): - raise AssertionError, (resp, data) - for recip in msgdata[2]: - expected = list(msgdata[:]) - expected[2] = [recip] - self.assertEquals( - a.message[(recip,)], - tuple(expected) - ) - a.setTimeout(None) - - -class AnotherESMTPTestCase(AnotherTestCase, unittest.TestCase): - serverClass = DummyESMTP - clientClass = MyESMTPClient - -class AnotherSMTPTestCase(AnotherTestCase, unittest.TestCase): - serverClass = DummySMTP - clientClass = MySMTPClient - - - -class DummyChecker: - implements(cred.checkers.ICredentialsChecker) - - users = { - 'testuser': 'testpassword' - } - - credentialInterfaces = (cred.credentials.IUsernameHashedPassword,) - - def requestAvatarId(self, credentials): - return defer.maybeDeferred( - credentials.checkPassword, self.users[credentials.username] - ).addCallback(self._cbCheck, credentials.username) - - def _cbCheck(self, result, username): - if result: - return username - raise cred.error.UnauthorizedLogin() - -class DummyDelivery: - implements(smtp.IMessageDelivery) - - def validateTo(self, user): - return user - - def validateFrom(self, helo, origin): - return origin - - def receivedHeader(*args): - return None - -class DummyRealm: - def requestAvatar(self, avatarId, mind, *interfaces): - return smtp.IMessageDelivery, DummyDelivery(), lambda: None - -class AuthTestCase(unittest.TestCase, LoopbackMixin): - def testAuth(self): - realm = DummyRealm() - p = cred.portal.Portal(realm) - p.registerChecker(DummyChecker()) - - server = DummyESMTP({'CRAM-MD5': cred.credentials.CramMD5Credentials}) - server.portal = p - client = MyESMTPClient('testpassword') - - cAuth = imap4.CramMD5ClientAuthenticator('testuser') - client.registerAuthenticator(cAuth) - - d = self.loopback(server, client) - d.addCallback(lambda x : self.assertEquals(server.authenticated, 1)) - return d - -class SMTPHelperTestCase(unittest.TestCase): - def testMessageID(self): - d = {} - for i in range(1000): - m = smtp.messageid('testcase') - self.failIf(m in d) - d[m] = None - - def testQuoteAddr(self): - cases = [ - ['user@host.name', '<user@host.name>'], - ['"User Name" <user@host.name>', '<user@host.name>'], - [smtp.Address('someguy@someplace'), '<someguy@someplace>'], - ['', '<>'], - [smtp.Address(''), '<>'], - ] - - for (c, e) in cases: - self.assertEquals(smtp.quoteaddr(c), e) - - def testUser(self): - u = smtp.User('user@host', 'helo.host.name', None, None) - self.assertEquals(str(u), 'user@host') - - def testXtextEncoding(self): - cases = [ - ('Hello world', 'Hello+20world'), - ('Hello+world', 'Hello+2Bworld'), - ('\0\1\2\3\4\5', '+00+01+02+03+04+05'), - ('e=mc2@example.com', 'e+3Dmc2@example.com') - ] - - for (case, expected) in cases: - self.assertEquals(case.encode('xtext'), expected) - self.assertEquals(expected.decode('xtext'), case) - - -class NoticeTLSClient(MyESMTPClient): - tls = False - - def esmtpState_starttls(self, code, resp): - MyESMTPClient.esmtpState_starttls(self, code, resp) - self.tls = True - -class TLSTestCase(unittest.TestCase, LoopbackMixin): - def testTLS(self): - clientCTX = ClientTLSContext() - serverCTX = ServerTLSContext() - - client = NoticeTLSClient(contextFactory=clientCTX) - server = DummyESMTP(contextFactory=serverCTX) - - def check(ignored): - self.assertEquals(client.tls, True) - self.assertEquals(server.startedTLS, True) - - return self.loopback(server, client).addCallback(check) - -if ClientTLSContext is None: - for case in (TLSTestCase,): - case.skip = "OpenSSL not present" - -if not interfaces.IReactorSSL.providedBy(reactor): - for case in (TLSTestCase,): - case.skip = "Reactor doesn't support SSL" - -class EmptyLineTestCase(unittest.TestCase): - def testEmptyLineSyntaxError(self): - proto = smtp.SMTP() - output = StringIOWithoutClosing() - transport = protocol.FileWrapper(output) - proto.makeConnection(transport) - proto.lineReceived('') - proto.setTimeout(None) - - out = output.getvalue().splitlines() - self.assertEquals(len(out), 2) - self.failUnless(out[0].startswith('220')) - self.assertEquals(out[1], "500 Error: bad syntax") - - - -class TimeoutTestCase(unittest.TestCase, LoopbackMixin): - """ - Check that SMTP client factories correctly use the timeout. - """ - - def _timeoutTest(self, onDone, clientFactory): - """ - Connect the clientFactory, and check the timeout on the request. - """ - clock = task.Clock() - client = clientFactory.buildProtocol( - address.IPv4Address('TCP', 'example.net', 25)) - client.callLater = clock.callLater - t = StringTransport() - client.makeConnection(t) - t.protocol = client - def check(ign): - self.assertEquals(clock.seconds(), 0.5) - d = self.assertFailure(onDone, smtp.SMTPTimeoutError - ).addCallback(check) - # The first call should not trigger the timeout - clock.advance(0.1) - # But this one should - clock.advance(0.4) - return d - - - def test_SMTPClient(self): - """ - Test timeout for L{smtp.SMTPSenderFactory}: the response L{Deferred} - should be errback with a L{smtp.SMTPTimeoutError}. - """ - onDone = defer.Deferred() - clientFactory = smtp.SMTPSenderFactory( - 'source@address', 'recipient@address', - StringIO("Message body"), onDone, - retries=0, timeout=0.5) - return self._timeoutTest(onDone, clientFactory) - - - def test_ESMTPClient(self): - """ - Test timeout for L{smtp.ESMTPSenderFactory}: the response L{Deferred} - should be errback with a L{smtp.SMTPTimeoutError}. - """ - onDone = defer.Deferred() - clientFactory = smtp.ESMTPSenderFactory( - 'username', 'password', - 'source@address', 'recipient@address', - StringIO("Message body"), onDone, - retries=0, timeout=0.5) - return self._timeoutTest(onDone, clientFactory) - - - -class SingletonRealm(object): - """ - Trivial realm implementation which is constructed with an interface and an - avatar and returns that avatar when asked for that interface. - """ - implements(IRealm) - - def __init__(self, interface, avatar): - self.interface = interface - self.avatar = avatar - - - def requestAvatar(self, avatarId, mind, *interfaces): - for iface in interfaces: - if iface is self.interface: - return iface, self.avatar, lambda: None - - - -class NotImplementedDelivery(object): - """ - Non-implementation of L{smtp.IMessageDelivery} which only has methods which - raise L{NotImplementedError}. Subclassed by various tests to provide the - particular behavior being tested. - """ - def validateFrom(self, helo, origin): - raise NotImplementedError("This oughtn't be called in the course of this test.") - - - def validateTo(self, user): - raise NotImplementedError("This oughtn't be called in the course of this test.") - - - def receivedHeader(self, helo, origin, recipients): - raise NotImplementedError("This oughtn't be called in the course of this test.") - - - -class SMTPServerTestCase(unittest.TestCase): - """ - Test various behaviors of L{twisted.mail.smtp.SMTP} and - L{twisted.mail.smtp.ESMTP}. - """ - def testSMTPGreetingHost(self, serverClass=smtp.SMTP): - """ - Test that the specified hostname shows up in the SMTP server's - greeting. - """ - s = serverClass() - s.host = "example.com" - t = StringTransport() - s.makeConnection(t) - s.connectionLost(error.ConnectionDone()) - self.assertIn("example.com", t.value()) - - - def testSMTPGreetingNotExtended(self): - """ - Test that the string "ESMTP" does not appear in the SMTP server's - greeting since that string strongly suggests the presence of support - for various SMTP extensions which are not supported by L{smtp.SMTP}. - """ - s = smtp.SMTP() - t = StringTransport() - s.makeConnection(t) - s.connectionLost(error.ConnectionDone()) - self.assertNotIn("ESMTP", t.value()) - - - def testESMTPGreetingHost(self): - """ - Similar to testSMTPGreetingHost, but for the L{smtp.ESMTP} class. - """ - self.testSMTPGreetingHost(smtp.ESMTP) - - - def testESMTPGreetingExtended(self): - """ - Test that the string "ESMTP" does appear in the ESMTP server's - greeting since L{smtp.ESMTP} does support the SMTP extensions which - that advertises to the client. - """ - s = smtp.ESMTP() - t = StringTransport() - s.makeConnection(t) - s.connectionLost(error.ConnectionDone()) - self.assertIn("ESMTP", t.value()) - - - def test_acceptSenderAddress(self): - """ - Test that a C{MAIL FROM} command with an acceptable address is - responded to with the correct success code. - """ - class AcceptanceDelivery(NotImplementedDelivery): - """ - Delivery object which accepts all senders as valid. - """ - def validateFrom(self, helo, origin): - return origin - - realm = SingletonRealm(smtp.IMessageDelivery, AcceptanceDelivery()) - portal = Portal(realm, [AllowAnonymousAccess()]) - proto = smtp.SMTP() - proto.portal = portal - trans = StringTransport() - proto.makeConnection(trans) - - # Deal with the necessary preliminaries - proto.dataReceived('HELO example.com\r\n') - trans.clear() - - # Try to specify our sender address - proto.dataReceived('MAIL FROM:<alice@example.com>\r\n') - - # Clean up the protocol before doing anything that might raise an - # exception. - proto.connectionLost(error.ConnectionLost()) - - # Make sure that we received exactly the correct response - self.assertEqual( - trans.value(), - '250 Sender address accepted\r\n') - - - def test_deliveryRejectedSenderAddress(self): - """ - Test that a C{MAIL FROM} command with an address rejected by a - L{smtp.IMessageDelivery} instance is responded to with the correct - error code. - """ - class RejectionDelivery(NotImplementedDelivery): - """ - Delivery object which rejects all senders as invalid. - """ - def validateFrom(self, helo, origin): - raise smtp.SMTPBadSender(origin) - - realm = SingletonRealm(smtp.IMessageDelivery, RejectionDelivery()) - portal = Portal(realm, [AllowAnonymousAccess()]) - proto = smtp.SMTP() - proto.portal = portal - trans = StringTransport() - proto.makeConnection(trans) - - # Deal with the necessary preliminaries - proto.dataReceived('HELO example.com\r\n') - trans.clear() - - # Try to specify our sender address - proto.dataReceived('MAIL FROM:<alice@example.com>\r\n') - - # Clean up the protocol before doing anything that might raise an - # exception. - proto.connectionLost(error.ConnectionLost()) - - # Make sure that we received exactly the correct response - self.assertEqual( - trans.value(), - '550 Cannot receive from specified address ' - '<alice@example.com>: Sender not acceptable\r\n') - - - def test_portalRejectedSenderAddress(self): - """ - Test that a C{MAIL FROM} command with an address rejected by an - L{smtp.SMTP} instance's portal is responded to with the correct error - code. - """ - class DisallowAnonymousAccess(object): - """ - Checker for L{IAnonymous} which rejects authentication attempts. - """ - implements(ICredentialsChecker) - - credentialInterfaces = (IAnonymous,) - - def requestAvatarId(self, credentials): - return defer.fail(UnauthorizedLogin()) - - realm = SingletonRealm(smtp.IMessageDelivery, NotImplementedDelivery()) - portal = Portal(realm, [DisallowAnonymousAccess()]) - proto = smtp.SMTP() - proto.portal = portal - trans = StringTransport() - proto.makeConnection(trans) - - # Deal with the necessary preliminaries - proto.dataReceived('HELO example.com\r\n') - trans.clear() - - # Try to specify our sender address - proto.dataReceived('MAIL FROM:<alice@example.com>\r\n') - - # Clean up the protocol before doing anything that might raise an - # exception. - proto.connectionLost(error.ConnectionLost()) - - # Make sure that we received exactly the correct response - self.assertEqual( - trans.value(), - '550 Cannot receive from specified address ' - '<alice@example.com>: Sender not acceptable\r\n') - - - def test_portalRejectedAnonymousSender(self): - """ - Test that a C{MAIL FROM} command issued without first authenticating - when a portal has been configured to disallow anonymous logins is - responded to with the correct error code. - """ - realm = SingletonRealm(smtp.IMessageDelivery, NotImplementedDelivery()) - portal = Portal(realm, []) - proto = smtp.SMTP() - proto.portal = portal - trans = StringTransport() - proto.makeConnection(trans) - - # Deal with the necessary preliminaries - proto.dataReceived('HELO example.com\r\n') - trans.clear() - - # Try to specify our sender address - proto.dataReceived('MAIL FROM:<alice@example.com>\r\n') - - # Clean up the protocol before doing anything that might raise an - # exception. - proto.connectionLost(error.ConnectionLost()) - - # Make sure that we received exactly the correct response - self.assertEqual( - trans.value(), - '550 Cannot receive from specified address ' - '<alice@example.com>: Unauthenticated senders not allowed\r\n') - - - -class ESMTPAuthenticationTestCase(unittest.TestCase): - def assertServerResponse(self, bytes, response): - """ - Assert that when the given bytes are delivered to the ESMTP server - instance, it responds with the indicated lines. - - @type bytes: str - @type response: list of str - """ - self.transport.clear() - self.server.dataReceived(bytes) - self.assertEqual( - response, - self.transport.value().splitlines()) - - - def assertServerAuthenticated(self, loginArgs, username="username", password="password"): - """ - Assert that a login attempt has been made, that the credentials and - interfaces passed to it are correct, and that when the login request - is satisfied, a successful response is sent by the ESMTP server - instance. - - @param loginArgs: A C{list} previously passed to L{portalFactory}. - """ - d, credentials, mind, interfaces = loginArgs.pop() - self.assertEqual(loginArgs, []) - self.failUnless(twisted.cred.credentials.IUsernamePassword.providedBy(credentials)) - self.assertEqual(credentials.username, username) - self.failUnless(credentials.checkPassword(password)) - self.assertIn(smtp.IMessageDeliveryFactory, interfaces) - self.assertIn(smtp.IMessageDelivery, interfaces) - d.callback((smtp.IMessageDeliveryFactory, None, lambda: None)) - - self.assertEqual( - ["235 Authentication successful."], - self.transport.value().splitlines()) - - - def setUp(self): - """ - Create an ESMTP instance attached to a StringTransport. - """ - self.server = smtp.ESMTP({ - 'LOGIN': imap4.LOGINCredentials}) - self.server.host = 'localhost' - self.transport = StringTransport( - peerAddress=address.IPv4Address('TCP', '127.0.0.1', 12345)) - self.server.makeConnection(self.transport) - - - def tearDown(self): - """ - Disconnect the ESMTP instance to clean up its timeout DelayedCall. - """ - self.server.connectionLost(error.ConnectionDone()) - - - def portalFactory(self, loginList): - class DummyPortal: - def login(self, credentials, mind, *interfaces): - d = defer.Deferred() - loginList.append((d, credentials, mind, interfaces)) - return d - return DummyPortal() - - - def test_authenticationCapabilityAdvertised(self): - """ - Test that AUTH is advertised to clients which issue an EHLO command. - """ - self.transport.clear() - self.server.dataReceived('EHLO\r\n') - responseLines = self.transport.value().splitlines() - self.assertEqual( - responseLines[0], - "250-localhost Hello 127.0.0.1, nice to meet you") - self.assertEqual( - responseLines[1], - "250 AUTH LOGIN") - self.assertEqual(len(responseLines), 2) - - - def test_plainAuthentication(self): - """ - Test that the LOGIN authentication mechanism can be used - """ - loginArgs = [] - self.server.portal = self.portalFactory(loginArgs) - - self.server.dataReceived('EHLO\r\n') - self.transport.clear() - - self.assertServerResponse( - 'AUTH LOGIN\r\n', - ["334 " + "User Name\0".encode('base64').strip()]) - - self.assertServerResponse( - 'username'.encode('base64') + '\r\n', - ["334 " + "Password\0".encode('base64').strip()]) - - self.assertServerResponse( - 'password'.encode('base64').strip() + '\r\n', - []) - - self.assertServerAuthenticated(loginArgs) - - - def test_plainAuthenticationEmptyPassword(self): - """ - Test that giving an empty password for plain auth succeeds. - """ - loginArgs = [] - self.server.portal = self.portalFactory(loginArgs) - - self.server.dataReceived('EHLO\r\n') - self.transport.clear() - - self.assertServerResponse( - 'AUTH LOGIN\r\n', - ["334 " + "User Name\0".encode('base64').strip()]) - - self.assertServerResponse( - 'username'.encode('base64') + '\r\n', - ["334 " + "Password\0".encode('base64').strip()]) - - self.assertServerResponse('\r\n', []) - self.assertServerAuthenticated(loginArgs, password='') - - def test_plainAuthenticationInitialResponse(self): - """ - The response to the first challenge may be included on the AUTH command - line. Test that this is also supported. - """ - loginArgs = [] - self.server.portal = self.portalFactory(loginArgs) - - self.server.dataReceived('EHLO\r\n') - self.transport.clear() - - self.assertServerResponse( - 'AUTH LOGIN ' + "username".encode('base64').strip() + '\r\n', - ["334 " + "Password\0".encode('base64').strip()]) - - self.assertServerResponse( - 'password'.encode('base64').strip() + '\r\n', - []) - - self.assertServerAuthenticated(loginArgs) - - - def test_abortAuthentication(self): - """ - Test that a challenge/response sequence can be aborted by the client. - """ - loginArgs = [] - self.server.portal = self.portalFactory(loginArgs) - - self.server.dataReceived('EHLO\r\n') - self.server.dataReceived('AUTH LOGIN\r\n') - - self.assertServerResponse( - '*\r\n', - ['501 Authentication aborted']) - - - def test_invalidBase64EncodedResponse(self): - """ - Test that a response which is not properly Base64 encoded results in - the appropriate error code. - """ - loginArgs = [] - self.server.portal = self.portalFactory(loginArgs) - - self.server.dataReceived('EHLO\r\n') - self.server.dataReceived('AUTH LOGIN\r\n') - - self.assertServerResponse( - 'x\r\n', - ['501 Syntax error in parameters or arguments']) - - self.assertEqual(loginArgs, []) - - - def test_invalidBase64EncodedInitialResponse(self): - """ - Like L{test_invalidBase64EncodedResponse} but for the case of an - initial response included with the C{AUTH} command. - """ - loginArgs = [] - self.server.portal = self.portalFactory(loginArgs) - - self.server.dataReceived('EHLO\r\n') - self.assertServerResponse( - 'AUTH LOGIN x\r\n', - ['501 Syntax error in parameters or arguments']) - - self.assertEqual(loginArgs, []) diff --git a/tools/buildbot/pylibs/twisted/mail/topfiles/NEWS b/tools/buildbot/pylibs/twisted/mail/topfiles/NEWS deleted file mode 100644 index 08785be..0000000 --- a/tools/buildbot/pylibs/twisted/mail/topfiles/NEWS +++ /dev/null @@ -1,113 +0,0 @@ -8.1.0 (2008-05-18) -================== - -Fixes ------ - - The deprecated mktap API is no longer used (#3127) - - -8.0.0 (2008-03-17) -================== - -Features --------- - - Support CAPABILITY responses that include atoms of the form "FOO" and - "FOO=BAR" in IMAP4 (#2695) - - Parameterize error handling behavior of imap4.encoder and imap4.decoder. - (#2929) - -Fixes ------ - - Handle empty passwords in SMTP auth. (#2521) - - Fix IMAP4Client's parsing of literals which are not preceeded by whitespace. - (#2700) - - Handle MX lookup suceeding without answers. (#2807) - - Fix issues with aliases(5) process support. (#2729) - -Misc ----- - - #2371, #2123, #2378, #739, #2640, #2746, #1917, #2266, #2864, #2832, #2063, - #2865, #2847 - - -0.4.0 (2007-01-06) -================== - -Features --------- - - Plaintext POP3 logins are now possible over SSL or TLS (#1809) - -Fixes ------ - - ESMTP servers now greet with an "ESMTP" string (#1891) - - The POP3 client can now correctly deal with concurrent POP3 - retrievals (#1988, #1691) - - In the IMAP4 server, a bug involving retrieving the first part - of a single-part message was fixed. This improves compatibility - with Pine (#1978) - - A bug in the IMAP4 server which caused corruption under heavy - pipelining was fixed (#1992) - - More strict support for the AUTH command was added to the SMTP - server, to support the AUTH <mechanism> - <initial-authentication-data> form of the command (#1552) - - An SMTP bug involving the interaction with validateFrom, which - caused multiple conflicting SMTP messages to be sent over the wire, - was fixed (#2158) - -Misc ----- - - #1648, #1801, #1636, #2003, #1936, #1202, #2051, #2072, #2248, #2250 - -0.3.0 (2006-05-21) -================== - -Features --------- - - Support Deferred results from POP3's IMailbox.listMessages (#1701). - -Fixes ------ - - Quote usernames and passwords automatically in the IMAP client (#1411). - - Improved parsing of literals in IMAP4 client and server (#1417). - - Recognize unsolicted FLAGS response in IMAP4 client (#1105). - - Parse and respond to requests with multiple BODY arguments in IMAP4 - server (#1307). - - Misc: #1356, #1290, #1602 - -0.2.0: - - SMTP server: - - Now gives application-level code opportunity to set a different - Received header for each recipient of a multi-recipient message. - - IMAP client: - - New `startTLS' method to allow explicit negotiation of transport - security. -- POP client: - - Support for per-command timeouts - - New `startTLS' method, similar to the one added to the IMAP - client. - - NOOP, RSET, and STAT support added -- POP server: - - Bug handling passwords of "" fixed - - -0.1.0: - - Tons of bugfixes in IMAP4, POP3, and SMTP protocols - - Maildir append support - - Brand new, improved POP3 client (twisted.mail.pop3.AdvancedPOP3Client) - - Deprecated the old POP3 client (twisted.mail.pop3.POP3Client) - - SMTP client: - - Support SMTP AUTH - - Allow user to supply SSL context - - Improved error handling, via new exception classes and an overridable - hook to customize handling. - - Order to try the authenication schemes is user-definable. - - Timeout support. - - SMTP server: - - Properly understand <> sender. - - Parameterize remote port - - IMAP4: - - LOGIN authentication compatibility improved - - Improved unicode mailbox support - - Fix parsing/handling of "FETCH BODY[HEADER]" - - Many many quoting fixes - - Timeout support on client diff --git a/tools/buildbot/pylibs/twisted/mail/topfiles/README b/tools/buildbot/pylibs/twisted/mail/topfiles/README deleted file mode 100644 index 7f70b08..0000000 --- a/tools/buildbot/pylibs/twisted/mail/topfiles/README +++ /dev/null @@ -1,5 +0,0 @@ -Twisted Mail 8.1.0 - -Mail was recently split out of Twisted. - -Twisted Mail depends on Twisted and (sometimes) Twisted Names. diff --git a/tools/buildbot/pylibs/twisted/mail/topfiles/setup.py b/tools/buildbot/pylibs/twisted/mail/topfiles/setup.py deleted file mode 100644 index 037d5f2..0000000 --- a/tools/buildbot/pylibs/twisted/mail/topfiles/setup.py +++ /dev/null @@ -1,48 +0,0 @@ -import sys - -try: - from twisted.python import dist -except ImportError: - raise SystemExit("twisted.python.dist module not found. Make sure you " - "have installed the Twisted core package before " - "attempting to install any other Twisted projects.") - -if __name__ == '__main__': - if sys.version_info[:2] >= (2, 4): - extraMeta = dict( - classifiers=[ - "Development Status :: 4 - Beta", - "Environment :: No Input/Output (Daemon)", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python", - "Topic :: Communications :: Email :: Post-Office :: IMAP", - "Topic :: Communications :: Email :: Post-Office :: POP3", - "Topic :: Software Development :: Libraries :: Python Modules", - ]) - else: - extraMeta = {} - - dist.setup( - twisted_subproject="mail", - scripts=dist.getScripts("mail"), - # metadata - name="Twisted Mail", - description="A Twisted Mail library, server and client.", - author="Twisted Matrix Laboratories", - author_email="twisted-python@twistedmatrix.com", - maintainer="Jp Calderone", - maintainer_email="exarkun@divmod.com", - url="http://twistedmatrix.com/trac/wiki/TwistedMail", - license="MIT", - long_description="""\ -An SMTP, IMAP and POP protocol implementation together with clients -and servers. - -Twisted Mail contains high-level, efficient protocol implementations -for both clients and servers of SMTP, POP3, and IMAP4. Additionally, -it contains an "out of the box" combination SMTP/POP3 virtual-hosting -mail server. Also included is a read/write Maildir implementation and -a basic Mail Exchange calculator. -""", - **extraMeta) |