add Linux_i686

This commit is contained in:
j 2014-05-17 18:11:40 +00:00 committed by Ubuntu
commit 95cd9b11f2
1644 changed files with 564260 additions and 0 deletions

View file

@ -0,0 +1,15 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Twisted Mail: a Twisted E-Mail Server.
Maintainer: Jp Calderone
"""
from twisted.mail._version import version
__version__ = version.short()

View file

@ -0,0 +1,11 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
# This is an auto-generated file. Do not edit it.
"""
Provides Twisted version information.
"""
from twisted.python import versions
version = versions.Version('twisted.mail', 14, 0, 0)

View file

@ -0,0 +1,814 @@
# -*- test-case-name: twisted.mail.test.test_mail -*-
#
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Support for aliases(5) configuration files.
@author: Jp Calderone
"""
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):
"""
Parse a line from an aliases file.
@type result: L{dict} mapping L{bytes} to L{list} of L{bytes}
@param result: A dictionary mapping username to aliases to which
the results of parsing the line are added.
@type line: L{bytes}
@param line: A line from an aliases file.
@type filename: L{bytes}
@param filename: The full or relative path to the aliases file.
@type lineNo: L{int}
@param lineNo: The position of the line within the aliases file.
"""
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 C{|} will be treated as programs, will be run, and
the message will be written to their stdin.
Aliases beginning with a C{:} will be treated as a file containing
additional aliases for the username.
Aliases beginning with a C{/} will be treated as the full pathname to a file
to which the message will be appended.
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.
Lines beginning with a space or a tab are continuations of the previous
line.
Lines beginning with a C{#} are comments.
@type domains: L{dict} mapping L{bytes} to L{IDomain} provider
@param domains: A mapping of domain name to domain object.
@type filename: L{bytes} or L{NoneType <types.NoneType>}
@param filename: The full or relative path to a file from which to load
aliases. If omitted, the C{fp} parameter must be specified.
@type fp: file-like object or L{NoneType <types.NoneType>}
@param fp: The file from which to load aliases. If specified,
the C{filename} parameter is ignored.
@rtype: L{dict} mapping L{bytes} to L{AliasGroup}
@return: A mapping from username to group of aliases.
"""
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):
"""
An interface for aliases.
"""
def createMessageReceiver():
"""
Create a message receiver.
@rtype: L{IMessage <smtp.IMessage>} provider
@return: A message receiver.
"""
class AliasBase:
"""
The default base class for aliases.
@ivar domains: See L{__init__}.
@type original: L{Address}
@ivar original: The original address being aliased.
"""
def __init__(self, domains, original):
"""
@type domains: L{dict} mapping L{bytes} to L{IDomain} provider
@param domains: A mapping of domain name to domain object.
@type original: L{bytes}
@param original: The original address being aliased.
"""
self.domains = domains
self.original = smtp.Address(original)
def domain(self):
"""
Return the domain associated with original address.
@rtype: L{IDomain} provider
@return: The domain for the original address.
"""
return self.domains[self.original.domain]
def resolve(self, aliasmap, memo=None):
"""
Map this alias to its ultimate destination.
@type aliasmap: L{dict} mapping L{bytes} to L{AliasBase}
@param aliasmap: A mapping of username to alias or group of aliases.
@type memo: L{NoneType <types.NoneType>} or L{dict} of L{AliasBase}
@param memo: A record of the aliases already considered in the
resolution process. If provided, C{memo} is modified to include
this alias.
@rtype: L{IMessage <smtp.IMessage>} or L{NoneType <types.NoneType>}
@return: A message receiver for the ultimate destination or None for
an invalid destination.
"""
if memo is None:
memo = {}
if str(self) in memo:
return None
memo[str(self)] = None
return self.createMessageReceiver()
class AddressAlias(AliasBase):
"""
An alias which translates one email address into another.
@type alias : L{Address}
@ivar alias: The destination address.
"""
implements(IAlias)
def __init__(self, alias, *args):
"""
@type alias: L{Address}, L{User}, L{bytes} or object which can be
converted into L{bytes}
@param alias: The destination address.
@type args: 2-L{tuple} of (0) L{dict} mapping L{bytes} to L{IDomain}
provider, (1) L{bytes}
@param args: Arguments for L{AliasBase.__init__}.
"""
AliasBase.__init__(self, *args)
self.alias = smtp.Address(alias)
def __str__(self):
"""
Build a string representation of this L{AddressAlias} instance.
@rtype: L{bytes}
@return: A string containing the destination address.
"""
return '<Address %s>' % (self.alias,)
def createMessageReceiver(self):
"""
Create a message receiver which delivers a message to
the destination address.
@rtype: L{IMessage <smtp.IMessage>} provider
@return: A message receiver.
"""
return self.domain().exists(str(self.alias))
def resolve(self, aliasmap, memo=None):
"""
Map this alias to its ultimate destination.
@type aliasmap: L{dict} mapping L{bytes} to L{AliasBase}
@param aliasmap: A mapping of username to alias or group of aliases.
@type memo: L{NoneType <types.NoneType>} or L{dict} of L{AliasBase}
@param memo: A record of the aliases already considered in the
resolution process. If provided, C{memo} is modified to include
this alias.
@rtype: L{IMessage <smtp.IMessage>} or L{NoneType <types.NoneType>}
@return: A message receiver for the ultimate destination or None for
an invalid destination.
"""
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:
"""
A message receiver which delivers a message to a file.
@type fp: file-like object
@ivar fp: A file used for temporary storage of the message.
@type finalname: L{bytes}
@ivar finalname: The name of the file in which the message should be
stored.
"""
implements(smtp.IMessage)
def __init__(self, filename):
"""
@type filename: L{bytes}
@param filename: The name of the file in which the message should be
stored.
"""
self.fp = tempfile.TemporaryFile()
self.finalname = filename
def lineReceived(self, line):
"""
Write a received line to the temporary file.
@type line: L{bytes}
@param line: A received line of the message.
"""
self.fp.write(line + '\n')
def eomReceived(self):
"""
Handle end of message by writing the message to the file.
@rtype: L{Deferred <defer.Deferred>} which successfully results in
L{bytes}
@return: A deferred which succeeds with the name of the file to which
the message has been stored or fails if the message cannot be
saved to the file.
"""
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):
"""
Close the temporary file when the connection is lost.
"""
self.fp.close()
self.fp = None
def __str__(self):
"""
Build a string representation of this L{FileWrapper} instance.
@rtype: L{bytes}
@return: A string containing the file name of the message.
"""
return '<FileWrapper %s>' % (self.finalname,)
class FileAlias(AliasBase):
"""
An alias which translates an address to a file.
@ivar filename: See L{__init__}.
"""
implements(IAlias)
def __init__(self, filename, *args):
"""
@type filename: L{bytes}
@param filename: The name of the file in which to store the message.
@type args: 2-L{tuple} of (0) L{dict} mapping L{bytes} to L{IDomain}
provider, (1) L{bytes}
@param args: Arguments for L{AliasBase.__init__}.
"""
AliasBase.__init__(self, *args)
self.filename = filename
def __str__(self):
"""
Build a string representation of this L{FileAlias} instance.
@rtype: L{bytes}
@return: A string containing the name of the file.
"""
return '<File %s>' % (self.filename,)
def createMessageReceiver(self):
"""
Create a message receiver which delivers a message to the file.
@rtype: L{FileWrapper}
@return: A message receiver which writes a message to the file.
"""
return FileWrapper(self.filename)
class ProcessAliasTimeout(Exception):
"""
An error indicating that a timeout occurred while waiting for a process
to complete.
"""
class MessageWrapper:
"""
A message receiver which delivers a message to a child process.
@type completionTimeout: L{int} or L{float}
@ivar completionTimeout: The number of seconds to wait for the child
process to exit before reporting the delivery as a failure.
@type _timeoutCallID: L{NoneType <types.NoneType>} or
L{IDelayedCall <twisted.internet.interfaces.IDelayedCall>} provider
@ivar _timeoutCallID: The call used to time out delivery, started when the
connection to the child process is closed.
@type done: L{bool}
@ivar done: A flag indicating whether the child process has exited
(C{True}) or not (C{False}).
@type reactor: L{IReactorTime <twisted.internet.interfaces.IReactorTime>}
provider
@ivar reactor: A reactor which will be used to schedule timeouts.
@ivar protocol: See L{__init__}.
@type processName: L{bytes} or L{NoneType <types.NoneType>}
@ivar processName: The process name.
@type completion: L{Deferred <defer.Deferred>}
@ivar completion: The deferred which will be triggered by the protocol
when the child process exits.
"""
implements(smtp.IMessage)
done = False
completionTimeout = 60
_timeoutCallID = None
reactor = reactor
def __init__(self, protocol, process=None, reactor=None):
"""
@type protocol: L{ProcessAliasProtocol}
@param protocol: The protocol associated with the child process.
@type process: L{bytes} or L{NoneType <types.NoneType>}
@param process: The process name.
@type reactor: L{NoneType <types.NoneType>} or L{IReactorTime
<twisted.internet.interfaces.IReactorTime>} provider
@param reactor: A reactor which will be used to schedule timeouts.
"""
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.
@type result: L{Failure <failure.Failure>}
@param result: The reason the child process terminated.
@rtype: L{NoneType <types.NoneType>} or
L{Failure <failure.Failure>}
@return: None, if the process end is expected, or the reason the child
process terminated, if the process end is unexpected.
"""
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):
"""
Write a received line to the child process.
@type line: L{bytes}
@param line: A received line of the message.
"""
if self.done:
return
self.protocol.transport.write(line + '\n')
def eomReceived(self):
"""
Disconnect from the child process and set up a timeout to wait for it
to exit.
@rtype: L{Deferred <defer.Deferred>}
@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
L{completion} deferred.
"""
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):
"""
Ignore notification of lost connection.
"""
def __str__(self):
"""
Build a string representation of this L{MessageWrapper} instance.
@rtype: L{bytes}
@return: A string containing the name of the process.
"""
return '<ProcessWrapper %s>' % (self.processName,)
class ProcessAliasProtocol(protocol.ProcessProtocol):
"""
A process protocol which errbacks a deferred when the associated
process ends.
@type onEnd: L{NoneType <types.NoneType>} or L{Deferred <defer.Deferred>}
@ivar onEnd: If set, a deferred on which to errback when the process ends.
"""
onEnd = None
def processEnded(self, reason):
"""
Call an errback.
@type reason: L{Failure <failure.Failure>}
@param reason: The reason the child process terminated.
"""
if self.onEnd is not None:
self.onEnd.errback(reason)
class ProcessAlias(AliasBase):
"""
An alias which is handled by the execution of a program.
@type path: L{list} of L{bytes}
@ivar path: The arguments to pass to the process. The first string is
the executable's name.
@type program: L{bytes}
@ivar program: The path of the program to be executed.
@type reactor: L{IReactorTime <twisted.internet.interfaces.IReactorTime>}
and L{IReactorProcess <twisted.internet.interfaces.IReactorProcess>}
provider
@ivar reactor: A reactor which will be used to create and timeout the
child process.
"""
implements(IAlias)
reactor = reactor
def __init__(self, path, *args):
"""
@type path: L{bytes}
@param path: The command to invoke the program consisting of the path
to the executable followed by any arguments.
@type args: 2-L{tuple} of (0) L{dict} mapping L{bytes} to L{IDomain}
provider, (1) L{bytes}
@param args: Arguments for L{AliasBase.__init__}.
"""
AliasBase.__init__(self, *args)
self.path = path.split()
self.program = self.path[0]
def __str__(self):
"""
Build a string representation of this L{ProcessAlias} instance.
@rtype: L{bytes}
@return: A string containing the command used to invoke the process.
"""
return '<Process %s>' % (self.path,)
def spawnProcess(self, proto, program, path):
"""
Spawn a process.
This wraps the L{spawnProcess
<twisted.internet.interfaces.IReactorProcess.spawnProcess>} method on
L{reactor} so that it can be customized for test purposes.
@type proto: L{IProcessProtocol
<twisted.internet.interfaces.IProcessProtocol>} provider
@param proto: An object which will be notified of all events related to
the created process.
@type program: L{bytes}
@param program: The full path name of the file to execute.
@type path: L{list} of L{bytes}
@param path: The arguments to pass to the process. The first string
should be the executable's name.
@rtype: L{IProcessTransport
<twisted.internet.interfaces.IProcessTransport>} provider
@return: A process transport.
"""
return self.reactor.spawnProcess(proto, program, path)
def createMessageReceiver(self):
"""
Launch a process and create a message receiver to pass a message
to the process.
@rtype: L{MessageWrapper}
@return: A message receiver which delivers a message to the process.
"""
p = ProcessAliasProtocol()
m = MessageWrapper(p, self.program, self.reactor)
fd = self.spawnProcess(p, self.program, self.path)
return m
class MultiWrapper:
"""
A message receiver which delivers a single message to multiple other
message receivers.
@ivar objs: See L{__init__}.
"""
implements(smtp.IMessage)
def __init__(self, objs):
"""
@type objs: L{list} of L{IMessage <smtp.IMessage>} provider
@param objs: Message receivers to which the incoming message should be
directed.
"""
self.objs = objs
def lineReceived(self, line):
"""
Pass a received line to the message receivers.
@type line: L{bytes}
@param line: A line of the message.
"""
for o in self.objs:
o.lineReceived(line)
def eomReceived(self):
"""
Pass the end of message along to the message receivers.
@rtype: L{DeferredList <defer.DeferredList>} whose successful results
are L{bytes} or L{NoneType <types.NoneType>}
@return: A deferred list which triggers when all of the message
receivers have finished handling their end of message.
"""
return defer.DeferredList([
o.eomReceived() for o in self.objs
])
def connectionLost(self):
"""
Inform the message receivers that the connection has been lost.
"""
for o in self.objs:
o.connectionLost()
def __str__(self):
"""
Build a string representation of this L{MultiWrapper} instance.
@rtype: L{bytes}
@return: A string containing a list of the message receivers.
"""
return '<GroupWrapper %r>' % (map(str, self.objs),)
class AliasGroup(AliasBase):
"""
An alias which points to multiple destination aliases.
@type processAliasFactory: no-argument callable which returns
L{ProcessAlias}
@ivar processAliasFactory: A factory for process aliases.
@type aliases: L{list} of L{AliasBase} which implements L{IAlias}
@ivar aliases: The destination aliases.
"""
implements(IAlias)
processAliasFactory = ProcessAlias
def __init__(self, items, *args):
"""
Create a group of aliases.
Parse a list of alias strings and, for each, create an appropriate
alias object.
@type items: L{list} of L{bytes}
@param items: Aliases.
@type args: n-L{tuple} of (0) L{dict} mapping L{bytes} to L{IDomain}
provider, (1) L{bytes}
@param args: Arguments for L{AliasBase.__init__}.
"""
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 the number of aliases in the group.
@rtype: L{int}
@return: The number of aliases in the group.
"""
return len(self.aliases)
def __str__(self):
"""
Build a string representation of this L{AliasGroup} instance.
@rtype: L{bytes}
@return: A string containing the aliases in the group.
"""
return '<AliasGroup [%s]>' % (', '.join(map(str, self.aliases)))
def createMessageReceiver(self):
"""
Create a message receiver for each alias and return a message receiver
which will pass on a message to each of those.
@rtype: L{MultiWrapper}
@return: A message receiver which passes a message on to message
receivers for each alias in the group.
"""
return MultiWrapper([a.createMessageReceiver() for a in self.aliases])
def resolve(self, aliasmap, memo=None):
"""
Map each of the aliases in the group to its ultimate destination.
@type aliasmap: L{dict} mapping L{bytes} to L{AliasBase}
@param aliasmap: A mapping of username to alias or group of aliases.
@type memo: L{NoneType <types.NoneType>} or L{dict} of L{AliasBase}
@param memo: A record of the aliases already considered in the
resolution process. If provided, C{memo} is modified to include
this alias.
@rtype: L{MultiWrapper}
@return: A message receiver which passes the message on to message
receivers for the ultimate destination of each alias in the group.
"""
if memo is None:
memo = {}
r = []
for a in self.aliases:
r.append(a.resolve(aliasmap, memo))
return MultiWrapper(filter(None, r))

View file

@ -0,0 +1,86 @@
# -*- test-case-name: twisted.mail.test.test_bounce -*-
#
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Support for bounce message generation.
"""
import StringIO
import rfc822
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=''):
"""
Generate a bounce message for an undeliverable email message.
@type message: L{bytes}
@param message: The undeliverable message.
@type failedFrom: L{bytes}
@param failedFrom: The originator of the undeliverable message.
@type failedTo: L{bytes}
@param failedTo: The destination of the undeliverable message.
@type transcript: L{bytes}
@param transcript: An error message to include in the bounce message.
@rtype: 3-L{tuple} of (E{1}) L{bytes}, (E{2}) L{bytes}, (E{3}) L{bytes}
@return: The originator, the destination and the contents of the bounce
message. The destination of the bounce message is the originator of
the undeliverable message.
"""
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 = failedAddress.split('@', 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()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,819 @@
# -*- test-case-name: twisted.mail.test.test_mail -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Mail service support.
"""
# 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.cred.portal import Portal
# Sibling imports
from twisted.mail import protocols, smtp
# System imports
import os
from zope.interface import implements, Interface
class DomainWithDefaultDict:
"""
A simulated dictionary for mapping domain names to domain objects with
a default value for non-existing keys.
@ivar domains: See L{__init__}
@ivar default: See L{__init__}
"""
def __init__(self, domains, default):
"""
@type domains: L{dict} of L{bytes} -> L{IDomain} provider
@param domains: A mapping of domain name to domain object.
@type default: L{IDomain} provider
@param default: The default domain.
"""
self.domains = domains
self.default = default
def setDefaultDomain(self, domain):
"""
Set the default domain.
@type domain: L{IDomain} provider
@param domain: The default domain.
"""
self.default = domain
def has_key(self, name):
"""
Test for the presence of a domain name in this dictionary.
This always returns C{True} because a default value will be returned
if the name doesn't exist in this dictionary.
@type name: L{bytes}
@param name: A domain name.
@rtype: L{bool}
@return: C{True} to indicate that the domain name is in this
dictionary.
"""
return 1
def fromkeys(klass, keys, value=None):
"""
Create a new L{DomainWithDefaultDict} with the specified keys.
@type keys: iterable of L{bytes}
@param keys: Domain names to serve as keys in the new dictionary.
@type value: L{NoneType <types.NoneType>} or L{IDomain} provider
@param value: A domain object to serve as the value for all new keys
in the dictionary.
@rtype: L{DomainWithDefaultDict}
@return: A new dictionary.
"""
d = klass()
for k in keys:
d[k] = value
return d
fromkeys = classmethod(fromkeys)
def __contains__(self, name):
"""
Test for the presence of a domain name in this dictionary.
This always returns C{True} because a default value will be returned
if the name doesn't exist in this dictionary.
@type name: L{bytes}
@param name: A domain name.
@rtype: L{bool}
@return: C{True} to indicate that the domain name is in this
dictionary.
"""
return 1
def __getitem__(self, name):
"""
Look up a domain name and, if it is present, return the domain object
associated with it. Otherwise return the default domain.
@type name: L{bytes}
@param name: A domain name.
@rtype: L{IDomain} provider or L{NoneType <types.NoneType>}
@return: A domain object.
"""
return self.domains.get(name, self.default)
def __setitem__(self, name, value):
"""
Associate a domain object with a domain name in this dictionary.
@type name: L{bytes}
@param name: A domain name.
@type value: L{IDomain} provider
@param value: A domain object.
"""
self.domains[name] = value
def __delitem__(self, name):
"""
Delete the entry for a domain name in this dictionary.
@type name: L{bytes}
@param name: A domain name.
"""
del self.domains[name]
def __iter__(self):
"""
Return an iterator over the domain names in this dictionary.
@rtype: iterator over L{bytes}
@return: An iterator over the domain names.
"""
return iter(self.domains)
def __len__(self):
"""
Return the number of domains in this dictionary.
@rtype: L{int}
@return: The number of domains in this dictionary.
"""
return len(self.domains)
def __str__(self):
"""
Build an informal string representation of this dictionary.
@rtype: L{bytes}
@return: A string containing the mapping of domain names to domain
objects.
"""
return '<DomainWithDefaultDict %s>' % (self.domains,)
def __repr__(self):
"""
Build an "official" string representation of this dictionary.
@rtype: L{bytes}
@return: A pseudo-executable string describing the underlying domain
mapping of this object.
"""
return 'DomainWithDefaultDict(%s)' % (self.domains,)
def get(self, key, default=None):
"""
Look up a domain name in this dictionary.
@type key: L{bytes}
@param key: A domain name.
@type default: L{IDomain} provider or L{NoneType <types.NoneType>}
@param default: A domain object to be returned if the domain name is
not in this dictionary.
@rtype: L{IDomain} provider or L{NoneType <types.NoneType>}
@return: The domain object associated with the domain name if it is in
this dictionary. Otherwise, the default value.
"""
return self.domains.get(key, default)
def copy(self):
"""
Make a copy of this dictionary.
@rtype: L{DomainWithDefaultDict}
@return: A copy of this dictionary.
"""
return DomainWithDefaultDict(self.domains.copy(), self.default)
def iteritems(self):
"""
Return an iterator over the domain name/domain object pairs in the
dictionary.
Using the returned iterator while adding or deleting entries from the
dictionary may result in a L{RuntimeError <exceptions.RuntimeError>} or
failing to iterate over all the domain name/domain object pairs.
@rtype: iterator over 2-L{tuple} of (E{1}) L{bytes},
(E{2}) L{IDomain} provider or L{NoneType <types.NoneType>}
@return: An iterator over the domain name/domain object pairs.
"""
return self.domains.iteritems()
def iterkeys(self):
"""
Return an iterator over the domain names in this dictionary.
Using the returned iterator while adding or deleting entries from the
dictionary may result in a L{RuntimeError <exceptions.RuntimeError>} or
failing to iterate over all the domain names.
@rtype: iterator over L{bytes}
@return: An iterator over the domain names.
"""
return self.domains.iterkeys()
def itervalues(self):
"""
Return an iterator over the domain objects in this dictionary.
Using the returned iterator while adding or deleting entries from the
dictionary may result in a L{RuntimeError <exceptions.RuntimeError>}
or failing to iterate over all the domain objects.
@rtype: iterator over L{IDomain} provider or
L{NoneType <types.NoneType>}
@return: An iterator over the domain objects.
"""
return self.domains.itervalues()
def keys(self):
"""
Return a list of all domain names in this dictionary.
@rtype: L{list} of L{bytes}
@return: The domain names in this dictionary.
"""
return self.domains.keys()
def values(self):
"""
Return a list of all domain objects in this dictionary.
@rtype: L{list} of L{IDomain} provider or L{NoneType <types.NoneType>}
@return: The domain objects in this dictionary.
"""
return self.domains.values()
def items(self):
"""
Return a list of all domain name/domain object pairs in this
dictionary.
@rtype: L{list} of 2-L{tuple} of (E{1}) L{bytes}, (E{2}) L{IDomain}
provider or L{NoneType <types.NoneType>}
@return: Domain name/domain object pairs in this dictionary.
"""
return self.domains.items()
def popitem(self):
"""
Remove a random domain name/domain object pair from this dictionary and
return it as a tuple.
@rtype: 2-L{tuple} of (E{1}) L{bytes}, (E{2}) L{IDomain} provider or
L{NoneType <types.NoneType>}
@return: A domain name/domain object pair.
@raise KeyError: When this dictionary is empty.
"""
return self.domains.popitem()
def update(self, other):
"""
Update this dictionary with domain name/domain object pairs from
another dictionary.
When this dictionary contains a domain name which is in the other
dictionary, its value will be overwritten.
@type other: L{dict} of L{bytes} -> L{IDomain} provider and/or
L{bytes} -> L{NoneType <types.NoneType>}
@param other: Another dictionary of domain name/domain object pairs.
@rtype: L{NoneType <types.NoneType>}
@return: None.
"""
return self.domains.update(other)
def clear(self):
"""
Remove all items from this dictionary.
@rtype: L{NoneType <types.NoneType>}
@return: None.
"""
return self.domains.clear()
def setdefault(self, key, default):
"""
Return the domain object associated with the domain name if it is
present in this dictionary. Otherwise, set the value for the
domain name to the default and return that value.
@type key: L{bytes}
@param key: A domain name.
@type default: L{IDomain} provider
@param default: A domain object.
@rtype: L{IDomain} provider or L{NoneType <types.NoneType>}
@return: The domain object associated with the domain name.
"""
return self.domains.setdefault(key, default)
class IDomain(Interface):
"""
An interface for email domains.
"""
def exists(user):
"""
Check whether a user exists in this domain.
@type user: L{User}
@param user: A user.
@rtype: no-argument callable which returns L{IMessage <smtp.IMessage>}
provider
@return: A function which takes no arguments and returns a message
receiver for the user.
@raise SMTPBadRcpt: When the given user does not exist in this domain.
"""
def addUser(user, password):
"""
Add a user to this domain.
@type user: L{bytes}
@param user: A username.
@type password: L{bytes}
@param password: A password.
"""
def getCredentialsCheckers():
"""
Return credentials checkers for this domain.
@rtype: L{list} of L{ICredentialsChecker
<twisted.cred.checkers.ICredentialsChecker>} provider
@return: Credentials checkers for this domain.
"""
class IAliasableDomain(IDomain):
"""
An interface for email domains which can be aliased to other domains.
"""
def setAliasGroup(aliases):
"""
Set the group of defined aliases for this domain.
@type aliases: L{dict} of L{bytes} -> L{IAlias} provider
@param aliases: A mapping of domain name to alias.
"""
def exists(user, memo=None):
"""
Check whether a user exists in this domain or an alias of it.
@type user: L{User}
@param user: A user.
@type memo: L{NoneType <types.NoneType>} or L{dict} of L{AliasBase}
@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 which returns L{IMessage <smtp.IMessage>}
provider
@return: A function which takes no arguments and returns a message
receiver for the user.
@raise SMTPBadRcpt: When the given user does not exist in this domain
or an alias of it.
"""
class BounceDomain:
"""
A domain with no users.
This can be used to block off a domain.
"""
implements(IDomain)
def exists(self, user):
"""
Raise an exception to indicate that the user does not exist in this
domain.
@type user: L{User}
@param user: A user.
@raise SMTPBadRcpt: When the given user does not exist in this domain.
"""
raise smtp.SMTPBadRcpt(user)
def willRelay(self, user, protocol):
"""
Indicate that this domain will not relay.
@type user: L{Address}
@param user: The destination address.
@type protocol: L{Protocol <twisted.internet.protocol.Protocol>}
@param protocol: The protocol over which the message to be relayed is
being received.
@rtype: L{bool}
@return: C{False}.
"""
return False
def addUser(self, user, password):
"""
Ignore attempts to add a user to this domain.
@type user: L{bytes}
@param user: A username.
@type password: L{bytes}
@param password: A password.
"""
pass
def getCredentialsCheckers(self):
"""
Return no credentials checkers for this domain.
@rtype: L{list}
@return: The empty list.
"""
return []
class FileMessage:
"""
A message receiver which delivers a message to a file.
@ivar fp: See L{__init__}.
@ivar name: See L{__init__}.
@ivar finalName: See L{__init__}.
"""
implements(smtp.IMessage)
def __init__(self, fp, name, finalName):
"""
@type fp: file-like object
@param fp: The file in which to store the message while it is being
received.
@type name: L{bytes}
@param name: The full path name of the temporary file.
@type finalName: L{bytes}
@param finalName: The full path name that should be given to the file
holding the message after it has been fully received.
"""
self.fp = fp
self.name = name
self.finalName = finalName
def lineReceived(self, line):
"""
Write a received line to the file.
@type line: L{bytes}
@param line: A received line.
"""
self.fp.write(line+'\n')
def eomReceived(self):
"""
At the end of message, rename the file holding the message to its
final name.
@rtype: L{Deferred} which successfully results in L{bytes}
@return: A deferred which returns the final name of the file.
"""
self.fp.close()
os.rename(self.name, self.finalName)
return defer.succeed(self.finalName)
def connectionLost(self):
"""
Delete the file holding the partially received message.
"""
self.fp.close()
os.remove(self.name)
class MailService(service.MultiService):
"""
An email service.
@type queue: L{Queue} or L{NoneType <types.NoneType>}
@ivar queue: A queue for outgoing messages.
@type domains: L{dict} of L{bytes} -> L{IDomain} provider
@ivar domains: A mapping of supported domain name to domain object.
@type portals: L{dict} of L{bytes} -> L{Portal}
@ivar portals: A mapping of domain name to authentication portal.
@type aliases: L{NoneType <types.NoneType>} or L{dict} of
L{bytes} -> L{IAlias} provider
@ivar aliases: A mapping of domain name to alias.
@type smtpPortal: L{Portal}
@ivar smtpPortal: A portal for authentication for the SMTP server.
@type monitor: L{FileMonitoringService}
@ivar monitor: A service to monitor changes to files.
"""
queue = None
domains = None
portals = None
aliases = None
smtpPortal = None
def __init__(self):
"""
Initialize the mail service.
"""
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 = Portal(self)
def getPOP3Factory(self):
"""
Create a POP3 protocol factory.
@rtype: L{POP3Factory}
@return: A POP3 protocol factory.
"""
return protocols.POP3Factory(self)
def getSMTPFactory(self):
"""
Create an SMTP protocol factory.
@rtype: L{SMTPFactory <protocols.SMTPFactory>}
@return: An SMTP protocol factory.
"""
return protocols.SMTPFactory(self, self.smtpPortal)
def getESMTPFactory(self):
"""
Create an ESMTP protocol factory.
@rtype: L{ESMTPFactory <protocols.ESMTPFactory>}
@return: An ESMTP protocol factory.
"""
return protocols.ESMTPFactory(self, self.smtpPortal)
def addDomain(self, name, domain):
"""
Add a domain for which the service will accept email.
@type name: L{bytes}
@param name: A domain name.
@type domain: L{IDomain} provider
@param domain: A domain object.
"""
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.
@type queue: L{Queue}
@param queue: A queue for outgoing messages.
"""
self.queue = queue
def requestAvatar(self, avatarId, mind, *interfaces):
"""
Return a message delivery for an authenticated SMTP user.
@type avatarId: L{bytes}
@param avatarId: A string which identifies an authenticated user.
@type mind: L{NoneType <types.NoneType>}
@param mind: Unused.
@type interfaces: n-L{tuple} of C{zope.interface.Interface}
@param interfaces: A group of interfaces one of which the avatar must
support.
@rtype: 3-L{tuple} of (E{1}) L{IMessageDelivery},
(E{2}) L{ESMTPDomainDelivery}, (E{3}) no-argument callable
@return: A tuple of the supported interface, a message delivery, and
a logout function.
@raise NotImplementedError: When the given interfaces do not include
L{IMessageDelivery}.
"""
if smtp.IMessageDelivery in interfaces:
a = protocols.ESMTPDomainDelivery(self, avatarId)
return smtp.IMessageDelivery, a, lambda: None
raise NotImplementedError()
def lookupPortal(self, name):
"""
Find the portal for a domain.
@type name: L{bytes}
@param name: A domain name.
@rtype: L{Portal}
@return: A portal.
"""
return self.portals[name]
def defaultPortal(self):
"""
Return the portal for the default domain.
The default domain is named ''.
@rtype: L{Portal}
@return: The portal for the default domain.
"""
return self.portals['']
class FileMonitoringService(internet.TimerService):
"""
A service for monitoring changes to files.
@type files: L{list} of L{list} of (E{1}) L{float}, (E{2}) L{bytes},
(E{3}) callable which takes a L{bytes} argument, (E{4}) L{float}
@ivar files: Information about files to be monitored. Each list entry
provides the following information for a file: interval in seconds
between checks, filename, callback function, time of last modification
to the file.
@type intervals: L{_IntervalDifferentialIterator
<twisted.python.util._IntervalDifferentialIterator>}
@ivar intervals: Intervals between successive file checks.
@type _call: L{IDelayedCall <twisted.internet.interfaces.IDelayedCall>}
provider
@ivar _call: The next scheduled call to check a file.
@type index: L{int}
@ivar index: The index of the next file to be checked.
"""
def __init__(self):
"""
Initialize the file monitoring service.
"""
self.files = []
self.intervals = iter(util.IntervalDifferential([], 60))
def startService(self):
"""
Start the file monitoring service.
"""
service.Service.startService(self)
self._setupMonitor()
def _setupMonitor(self):
"""
Schedule the next monitoring call.
"""
from twisted.internet import reactor
t, self.index = self.intervals.next()
self._call = reactor.callLater(t, self._monitor)
def stopService(self):
"""
Stop the file monitoring service.
"""
service.Service.stopService(self)
if self._call:
self._call.cancel()
self._call = None
def monitorFile(self, name, callback, interval=10):
"""
Start monitoring a file for changes.
@type name: L{bytes}
@param name: The name of a file to monitor.
@type callback: callable which takes a L{bytes} argument
@param callback: The function to call when the file has changed.
@type interval: L{float}
@param interval: The interval in seconds between checks.
"""
try:
mtime = os.path.getmtime(name)
except:
mtime = 0
self.files.append([interval, name, callback, mtime])
self.intervals.addInterval(interval)
def unmonitorFile(self, name):
"""
Stop monitoring a file.
@type name: L{bytes}
@param name: A file 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):
"""
Monitor a file and make a callback if it has changed.
"""
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()

View file

@ -0,0 +1,942 @@
# -*- test-case-name: twisted.mail.test.test_mail -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Maildir-style mailbox support.
"""
import os
import stat
import socket
from hashlib import md5
from zope.interface import implementer
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.internet import interfaces, defer, reactor
from twisted.cred import portal, credentials, checkers
from twisted.cred.error import UnauthorizedLogin
INTERNAL_ERROR = '''\
From: Twisted.mail Internals
Subject: An Error Occurred
An internal server error has occurred. Please contact the
server administrator.
'''
class _MaildirNameGenerator:
"""
A utility class to generate a unique maildir name.
@type n: L{int}
@ivar n: A counter used to generate unique integers.
@type p: L{int}
@ivar p: The ID of the current process.
@type s: L{bytes}
@ivar s: A representation of the hostname.
@ivar _clock: See C{clock} parameter of L{__init__}.
"""
n = 0
p = os.getpid()
s = socket.gethostname().replace('/', r'\057').replace(':', r'\072')
def __init__(self, clock):
"""
@type clock: L{IReactorTime <interfaces.IReactorTime>} provider
@param clock: A reactor which will be used to learn the current time.
"""
self._clock = clock
def generate(self):
"""
Generate a string which is intended to be unique across all calls to
this function (across all processes, reboots, etc).
Strings returned by earlier calls to this method will compare less
than strings returned by later calls as long as the clock provided
doesn't go backwards.
@rtype: L{bytes}
@return: A unique string.
"""
self.n = self.n + 1
t = self._clock.seconds()
seconds = str(int(t))
microseconds = '%07d' % (int((t - int(t)) * 10e6),)
return '%s.M%sP%sQ%s.%s' % (seconds, microseconds,
self.p, self.n, self.s)
_generateMaildirName = _MaildirNameGenerator(reactor).generate
def initializeMaildir(dir):
"""
Create a maildir user directory if it doesn't already exist.
@type dir: L{bytes}
@param dir: The path name for a user directory.
"""
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):
"""
A message receiver which adds a header and delivers a message to a file
whose name includes the size of the message.
@type size: L{int}
@ivar size: The number of octets in the message.
"""
size = None
def __init__(self, address, fp, *a, **kw):
"""
@type address: L{bytes}
@param address: The address of the message recipient.
@type fp: file-like object
@param fp: The file in which to store the message while it is being
received.
@type a: 2-L{tuple} of (0) L{bytes}, (1) L{bytes}
@param a: Positional arguments for L{FileMessage.__init__}.
@type kw: L{dict}
@param kw: Keyword arguments for L{FileMessage.__init__}.
"""
header = "Delivered-To: %s\n" % address
fp.write(header)
self.size = len(header)
mail.FileMessage.__init__(self, fp, *a, **kw)
def lineReceived(self, line):
"""
Write a line to the file.
@type line: L{bytes}
@param line: A received line.
"""
mail.FileMessage.lineReceived(self, line)
self.size += len(line)+1
def eomReceived(self):
"""
At the end of message, rename the file holding the message to its final
name concatenated with the size of the file.
@rtype: L{Deferred <defer.Deferred>} which successfully results in
L{bytes}
@return: A deferred which returns the name of the file holding the
message.
"""
self.finalName = self.finalName+',S=%d' % self.size
return mail.FileMessage.eomReceived(self)
@implementer(mail.IAliasableDomain)
class AbstractMaildirDomain:
"""
An abstract maildir-backed domain.
@type alias: L{NoneType <types.NoneType>} or L{dict} mapping
L{bytes} to L{AliasBase}
@ivar alias: A mapping of username to alias.
@ivar root: See L{__init__}.
"""
alias = None
root = None
def __init__(self, service, root):
"""
@type service: L{MailService}
@param service: An email service.
@type root: L{bytes}
@param root: The maildir root directory.
"""
self.root = root
def userDirectory(self, user):
"""
Return the maildir directory for a user.
@type user: L{bytes}
@param user: A username.
@rtype: L{bytes} or L{NoneType <types.NoneType>}
@return: The user's mail directory for a valid user. Otherwise,
C{None}.
"""
return None
def setAliasGroup(self, alias):
"""
Set the group of defined aliases for this domain.
@type alias: L{dict} mapping L{bytes} to L{IAlias} provider.
@param alias: A mapping of domain name to alias.
"""
self.alias = alias
def exists(self, user, memo=None):
"""
Check whether a user exists in this domain or an alias of it.
@type user: L{User}
@param user: A user.
@type memo: L{NoneType <types.NoneType>} or L{dict} of L{AliasBase}
@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 which returns L{IMessage <smtp.IMessage>}
provider.
@return: A function which takes no arguments and returns a message
receiver for the user.
@raises SMTPBadRcpt: When the given user does not exist in this domain
or an alias of it.
"""
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):
"""
Create a maildir message for a user.
@type user: L{bytes}
@param user: A username.
@rtype: L{MaildirMessage}
@return: A message receiver for this 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):
"""
Check whether this domain will relay.
@type user: L{Address}
@param user: The destination address.
@type protocol: L{SMTP}
@param protocol: The protocol over which the message to be relayed is
being received.
@rtype: L{bool}
@return: An indication of whether this domain will relay the message to
the destination.
"""
return False
def addUser(self, user, password):
"""
Add a user to this domain.
Subclasses should override this method.
@type user: L{bytes}
@param user: A username.
@type password: L{bytes}
@param password: A password.
"""
raise NotImplementedError
def getCredentialsCheckers(self):
"""
Return credentials checkers for this domain.
Subclasses should override this method.
@rtype: L{list} of L{ICredentialsChecker
<checkers.ICredentialsChecker>} provider
@return: Credentials checkers for this domain.
"""
raise NotImplementedError
@implementer(interfaces.IConsumer)
class _MaildirMailboxAppendMessageTask:
"""
A task which adds a message to a maildir mailbox.
@ivar mbox: See L{__init__}.
@type defer: L{Deferred <defer.Deferred>} which successfully returns
L{NoneType <types.NoneType>}
@ivar defer: A deferred which fires when the task has completed.
@type opencall: L{IDelayedCall <interfaces.IDelayedCall>} provider or
L{NoneType <types.NoneType>}
@ivar opencall: A scheduled call to L{prodProducer}.
@type msg: file-like object
@ivar msg: The message to add.
@type tmpname: L{bytes}
@ivar tmpname: The pathname of the temporary file holding the message while
it is being transferred.
@type fh: file
@ivar fh: The new maildir file.
@type filesender: L{FileSender <basic.FileSender>}
@ivar filesender: A file sender which sends the message.
@type myproducer: L{IProducer <interfaces.IProducer>}
@ivar myproducer: The registered producer.
@type streaming: L{bool}
@ivar streaming: Indicates whether the registered producer provides a
streaming interface.
"""
osopen = staticmethod(os.open)
oswrite = staticmethod(os.write)
osclose = staticmethod(os.close)
osrename = staticmethod(os.rename)
def __init__(self, mbox, msg):
"""
@type mbox: L{MaildirMailbox}
@param mbox: A maildir mailbox.
@type msg: L{bytes} or file-like object
@param msg: The message to add.
"""
self.mbox = mbox
self.defer = defer.Deferred()
self.openCall = None
if not hasattr(msg, "read"):
msg = StringIO.StringIO(msg)
self.msg = msg
def startUp(self):
"""
Start transferring the message to the mailbox.
"""
self.createTempFile()
if self.fh != -1:
self.filesender = basic.FileSender()
self.filesender.beginFileTransfer(self.msg, self)
def registerProducer(self, producer, streaming):
"""
Register a producer and start asking it for data if it is
non-streaming.
@type producer: L{IProducer <interfaces.IProducer>}
@param producer: A producer.
@type streaming: L{bool}
@param streaming: A flag indicating whether the producer provides a
streaming interface.
"""
self.myproducer = producer
self.streaming = streaming
if not streaming:
self.prodProducer()
def prodProducer(self):
"""
Repeatedly prod a non-streaming producer to produce data.
"""
self.openCall = None
if self.myproducer is not None:
self.openCall = reactor.callLater(0, self.prodProducer)
self.myproducer.resumeProducing()
def unregisterProducer(self):
"""
Finish transferring the message to the mailbox.
"""
self.myproducer = None
self.streaming = None
self.osclose(self.fh)
self.moveFileToNew()
def write(self, data):
"""
Write data to the maildir file.
@type data: L{bytes}
@param data: Data to be written to the file.
"""
try:
self.oswrite(self.fh, data)
except:
self.fail()
def fail(self, err=None):
"""
Fire the deferred to indicate the task completed with a failure.
@type err: L{Failure <failure.Failure>}
@param err: The error that occurred.
"""
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):
"""
Place the message in the I{new/} directory, add it to the mailbox and
fire the deferred to indicate that the task has completed
successfully.
"""
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):
"""
Create a temporary file to hold the message as it is being transferred.
"""
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):
"""
A maildir-backed mailbox.
@ivar path: See L{__init__}.
@type list: L{list} of L{int} or 2-L{tuple} of (0) file-like object,
(1) L{bytes}
@ivar list: Information about the messages in the mailbox. For undeleted
messages, the file containing the message and the
full path name of the file are stored. Deleted messages are indicated
by 0.
@type deleted: L{dict} mapping 2-L{tuple} of (0) file-like object,
(1) L{bytes} to L{bytes}
@type deleted: A mapping of the information about a file before it was
deleted to the full path name of the deleted file in the I{.Trash/}
subfolder.
"""
AppendFactory = _MaildirMailboxAppendMessageTask
def __init__(self, path):
"""
@type path: L{bytes}
@param path: The directory name for a 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):
"""
Retrieve the size of a message, or, if none is specified, the size of
each message in the mailbox.
@type i: L{int} or L{NoneType <types.NoneType>}
@param i: The 0-based index of a message.
@rtype: L{int} or L{list} of L{int}
@return: The number of octets in the specified message, or, if an index
is not specified, a list of the number of octets for all messages
in the mailbox. Any value which corresponds to a deleted message
is set to 0.
@raise IndexError: When the index does not correspond to a message in
the mailbox.
"""
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):
"""
Retrieve a file-like object with the contents of a message.
@type i: L{int}
@param i: The 0-based index of a message.
@rtype: file-like object
@return: A file containing the message.
@raise IndexError: When the index does not correspond to a message in
the mailbox.
"""
return open(self.list[i])
def getUidl(self, i):
"""
Get a unique identifier for a message.
@type i: L{int}
@param i: The 0-based index of a message.
@rtype: L{bytes}
@return: A string of printable characters uniquely identifying the
message for all time.
@raise IndexError: When the index does not correspond to a message in
the mailbox.
"""
# Returning the actual filename is a mistake. Hash it.
base = os.path.basename(self.list[i])
return md5(base).hexdigest()
def deleteMessage(self, i):
"""
Mark a message for deletion.
Move the message to the I{.Trash/} subfolder so it can be undeleted
by an administrator.
@type i: L{int}
@param i: The 0-based index of a message.
@raise IndexError: When the index does not correspond to a message in
the mailbox.
"""
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 all messages marked for deletion.
Move each message marked for deletion from the I{.Trash/} subfolder back
to its original position.
"""
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):
"""
Add a message to the mailbox.
@type txt: L{bytes} or file-like object
@param txt: A message to add.
@rtype: L{Deferred <defer.Deferred>}
@return: A deferred which fires when the message has been added to
the mailbox.
"""
task = self.AppendFactory(self, txt)
result = task.defer
task.startUp()
return result
@implementer(pop3.IMailbox)
class StringListMailbox:
"""
An in-memory mailbox.
@ivar msgs: See L{__init__}.
@type _delete: L{set} of L{int}
@ivar _delete: The indices of messages which have been marked for deletion.
"""
def __init__(self, msgs):
"""
@type msgs: L{list} of L{bytes}
@param msgs: The contents of each message in the mailbox.
"""
self.msgs = msgs
self._delete = set()
def listMessages(self, i=None):
"""
Retrieve the size of a message, or, if none is specified, the size of
each message in the mailbox.
@type i: L{int} or L{NoneType <types.NoneType>}
@param i: The 0-based index of a message.
@rtype: L{int} or L{list} of L{int}
@return: The number of octets in the specified message, or, if an index
is not specified, a list of the number of octets in each message in
the mailbox. Any value which corresponds to a deleted message is
set to 0.
@raise IndexError: When the index does not correspond to a message in
the mailbox.
"""
if i is None:
return [self.listMessages(i) for i in range(len(self.msgs))]
if i in self._delete:
return 0
return len(self.msgs[i])
def getMessage(self, i):
"""
Return an in-memory file-like object with the contents of a message.
@type i: L{int}
@param i: The 0-based index of a message.
@rtype: L{StringIO <cStringIO.StringIO>}
@return: An in-memory file-like object containing the message.
@raise IndexError: When the index does not correspond to a message in
the mailbox.
"""
return StringIO.StringIO(self.msgs[i])
def getUidl(self, i):
"""
Get a unique identifier for a message.
@type i: L{int}
@param i: The 0-based index of a message.
@rtype: L{bytes}
@return: A hash of the contents of the message at the given index.
@raise IndexError: When the index does not correspond to a message in
the mailbox.
"""
return md5(self.msgs[i]).hexdigest()
def deleteMessage(self, i):
"""
Mark a message for deletion.
@type i: L{int}
@param i: The 0-based index of a message to delete.
@raise IndexError: When the index does not correspond to a message in
the mailbox.
"""
self._delete.add(i)
def undeleteMessages(self):
"""
Undelete any messages which have been marked for deletion.
"""
self._delete = set()
def sync(self):
"""
Discard the contents of any messages marked for deletion.
"""
for index in self._delete:
self.msgs[index] = ""
self._delete = set()
@implementer(portal.IRealm)
class MaildirDirdbmDomain(AbstractMaildirDomain):
"""
A maildir-backed domain where membership is checked with a
L{DirDBM <dirdbm.DirDBM>} database.
The directory structure of a MaildirDirdbmDomain is:
/passwd <-- a DirDBM directory
/USER/{cur, new, del} <-- each user has these three directories
@ivar postmaster: See L{__init__}.
@type dbm: L{DirDBM <dirdbm.DirDBM>}
@ivar dbm: The authentication database for the domain.
"""
portal = None
_credcheckers = None
def __init__(self, service, root, postmaster=0):
"""
@type service: L{MailService}
@param service: An email service.
@type root: L{bytes}
@param root: The maildir root directory.
@type postmaster: L{bool}
@param postmaster: A flag indicating whether non-existent addresses
should be forwarded to the postmaster (C{True}) or
bounced (C{False}).
"""
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):
"""
Return the path to a user's mail directory.
@type name: L{bytes}
@param name: A username.
@rtype: L{bytes} or L{NoneType <types.NoneType>}
@return: The path to the user's mail directory for a valid user. For
an invalid user, the path to the postmaster's mailbox if bounces
are redirected there. Otherwise, C{None}.
"""
if name not in self.dbm:
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
def addUser(self, user, password):
"""
Add a user to this domain by adding an entry in the authentication
database and initializing the user's mail directory.
@type user: L{bytes}
@param user: A username.
@type password: L{bytes}
@param password: A password.
"""
self.dbm[user] = password
# Ensure it is initialized
self.userDirectory(user)
def getCredentialsCheckers(self):
"""
Return credentials checkers for this domain.
@rtype: L{list} of L{ICredentialsChecker
<checkers.ICredentialsChecker>} provider
@return: Credentials checkers for this domain.
"""
if self._credcheckers is None:
self._credcheckers = [DirdbmDatabase(self.dbm)]
return self._credcheckers
def requestAvatar(self, avatarId, mind, *interfaces):
"""
Get the mailbox for an authenticated user.
The mailbox for the authenticated user will be returned only if the
given interfaces include L{IMailbox <pop3.IMailbox>}. Requests for
anonymous access will be met with a mailbox containing a message
indicating that an internal error has occured.
@type avatarId: L{bytes} or C{twisted.cred.checkers.ANONYMOUS}
@param avatarId: A string which identifies a user or an object which
signals a request for anonymous access.
@type mind: L{NoneType <types.NoneType>}
@param mind: Unused.
@type interfaces: n-L{tuple} of C{zope.interface.Interface}
@param interfaces: A group of interfaces, one of which the avatar
must support.
@rtype: 3-L{tuple} of (0) L{IMailbox <pop3.IMailbox>},
(1) L{IMailbox <pop3.IMailbox>} provider, (2) no-argument
callable
@return: A tuple of the supported interface, a mailbox, and a
logout function.
@raise NotImplementedError: When the given interfaces do not include
L{IMailbox <pop3.IMailbox>}.
"""
if pop3.IMailbox not in interfaces:
raise NotImplementedError("No interface")
if avatarId == checkers.ANONYMOUS:
mbox = StringListMailbox([INTERNAL_ERROR])
else:
mbox = MaildirMailbox(os.path.join(self.root, avatarId))
return (
pop3.IMailbox,
mbox,
lambda: None
)
@implementer(checkers.ICredentialsChecker)
class DirdbmDatabase:
"""
A credentials checker which authenticates users out of a
L{DirDBM <dirdbm.DirDBM>} database.
@type dirdbm: L{DirDBM <dirdbm.DirDBM>}
@ivar dirdbm: An authentication database.
"""
# credentialInterfaces is not used by the class
credentialInterfaces = (
credentials.IUsernamePassword,
credentials.IUsernameHashedPassword
)
def __init__(self, dbm):
"""
@type dbm: L{DirDBM <dirdbm.DirDBM>}
@param dbm: An authentication database.
"""
self.dirdbm = dbm
def requestAvatarId(self, c):
"""
Authenticate a user and, if successful, return their username.
@type c: L{IUsernamePassword <credentials.IUsernamePassword>} or
L{IUsernameHashedPassword <credentials.IUsernameHashedPassword>}
provider.
@param c: Credentials.
@rtype: L{bytes}
@return: A string which identifies an user.
@raise UnauthorizedLogin: When the credentials check fails.
"""
if c.username in self.dirdbm:
if c.checkPassword(self.dirdbm[c.username]):
return c.username
raise UnauthorizedLogin()

View file

@ -0,0 +1,115 @@
# Copyright (c) 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)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,505 @@
# -*- test-case-name: twisted.mail.test.test_mail -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Mail protocol support.
"""
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.python.deprecate import deprecatedModuleAttribute
from twisted.python.versions import Version
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 base class for message delivery using the domains of a mail service.
@ivar service: See L{__init__}
@ivar user: See L{__init__}
@ivar host: See L{__init__}
@type protocolName: L{bytes}
@ivar protocolName: The protocol being used to deliver the mail.
Sub-classes should set this appropriately.
"""
implements(smtp.IMessageDelivery)
service = None
protocolName = None
def __init__(self, service, user, host=smtp.DNSNAME):
"""
@type service: L{MailService}
@param service: A mail service.
@type user: L{bytes} or L{NoneType <types.NoneType>}
@param user: The authenticated SMTP user.
@type host: L{bytes}
@param host: The hostname.
"""
self.service = service
self.user = user
self.host = host
def receivedHeader(self, helo, origin, recipients):
"""
Generate a received header string for a message.
@type helo: 2-L{tuple} of (L{bytes}, L{bytes})
@param helo: The client's identity as sent in the HELO command and its
IP address.
@type origin: L{Address}
@param origin: The origination address of the message.
@type recipients: L{list} of L{User}
@param recipients: The destination addresses for the message.
@rtype: L{bytes}
@return: A received header string.
"""
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):
"""
Validate the address for which a message is destined.
@type user: L{User}
@param user: The destination address.
@rtype: L{Deferred <defer.Deferred>} which successfully fires with
no-argument callable which returns L{IMessage <smtp.IMessage>}
provider.
@return: A deferred which successfully fires with a no-argument
callable which returns a message receiver for the destination.
@raise SMTPBadRcpt: When messages cannot be accepted for the
destination address.
"""
# 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):
"""
Validate the address from which a message originates.
@type helo: 2-L{tuple} of (L{bytes}, L{bytes})
@param helo: The client's identity as sent in the HELO command and its
IP address.
@type origin: L{Address}
@param origin: The origination address of the message.
@rtype: L{Address}
@return: The origination address.
@raise SMTPBadSender: When messages cannot be accepted from the
origination address.
"""
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
class SMTPDomainDelivery(DomainDeliveryBase):
"""
A domain delivery base class for use in an SMTP server.
"""
protocolName = 'smtp'
class ESMTPDomainDelivery(DomainDeliveryBase):
"""
A domain delivery base class for use in an ESMTP server.
"""
protocolName = 'esmtp'
class DomainSMTP(SMTPDomainDelivery, smtp.SMTP):
"""
An SMTP server which uses the domains of a mail service.
"""
service = user = None
def __init__(self, *args, **kw):
"""
Initialize the SMTP server.
@type args: 2-L{tuple} of (L{IMessageDelivery} provider or
L{NoneType <types.NoneType>}, L{IMessageDeliveryFactory}
provider or L{NoneType <types.NoneType>})
@param args: Positional arguments for L{SMTP.__init__}
@type kw: L{dict}
@param kw: Keyword arguments for L{SMTP.__init__}.
"""
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):
"""
An ESMTP server which uses the domains of a mail service.
"""
service = user = None
def __init__(self, *args, **kw):
"""
Initialize the ESMTP server.
@type args: 2-L{tuple} of (L{IMessageDelivery} provider or
L{NoneType <types.NoneType>}, L{IMessageDeliveryFactory}
provider or L{NoneType <types.NoneType>})
@param args: Positional arguments for L{ESMTP.__init__}
@type kw: L{dict}
@param kw: Keyword arguments for L{ESMTP.__init__}.
"""
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):
"""
An SMTP server protocol factory.
@ivar service: See L{__init__}
@ivar portal: See L{__init__}
@type protocol: no-argument callable which returns a L{Protocol
<protocol.Protocol>} subclass
@ivar protocol: A callable which creates a protocol. The default value is
L{SMTP}.
"""
protocol = smtp.SMTP
portal = None
def __init__(self, service, portal = None):
"""
@type service: L{MailService}
@param service: An email service.
@type portal: L{Portal <twisted.cred.portal.Portal>} or
L{NoneType <types.NoneType>}
@param portal: A portal to use for authentication.
"""
smtp.SMTPFactory.__init__(self)
self.service = service
self.portal = portal
def buildProtocol(self, addr):
"""
Create an instance of an SMTP server protocol.
@type addr: L{IAddress <twisted.internet.interfaces.IAddress>} provider
@param addr: The address of the SMTP client.
@rtype: L{SMTP}
@return: An SMTP protocol.
"""
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):
"""
An ESMTP server protocol factory.
@type protocol: no-argument callable which returns a L{Protocol
<protocol.Protocol>} subclass
@ivar protocol: A callable which creates a protocol. The default value is
L{ESMTP}.
@type context: L{ContextFactory <twisted.internet.ssl.ContextFactory>} or
L{NoneType <types.NoneType>}
@ivar context: A factory to generate contexts to be used in negotiating
encrypted communication.
@type challengers: L{dict} mapping L{bytes} to no-argument callable which
returns L{ICredentials <twisted.cred.credentials.ICredentials>}
subclass provider.
@ivar challengers: A mapping of acceptable authorization mechanism to
callable which creates credentials to use for authentication.
"""
protocol = smtp.ESMTP
context = None
def __init__(self, *args):
"""
@param args: Arguments for L{SMTPFactory.__init__}
@see: L{SMTPFactory.__init__}
"""
SMTPFactory.__init__(self, *args)
self.challengers = {
'CRAM-MD5': cred.credentials.CramMD5Credentials
}
def buildProtocol(self, addr):
"""
Create an instance of an ESMTP server protocol.
@type addr: L{IAddress <twisted.internet.interfaces.IAddress>} provider
@param addr: The address of the ESMTP client.
@rtype: L{ESMTP}
@return: An ESMTP protocol.
"""
p = SMTPFactory.buildProtocol(self, addr)
p.challengers = self.challengers
p.ctx = self.context
return p
class VirtualPOP3(pop3.POP3):
"""
A virtual hosting POP3 server.
@type service: L{MailService}
@ivar service: The email service that created this server. This must be
set by the service.
@type domainSpecifier: L{bytes}
@ivar domainSpecifier: The character to use to split an email address into
local-part and domain. The default is '@'.
"""
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):
"""
Perform APOP authentication.
Override the default lookup scheme to allow virtual domains.
@type user: L{bytes}
@param user: The name of the user attempting to log in.
@type digest: L{bytes}
@param digest: The challenge response.
@rtype: L{Deferred} which successfully results in 3-L{tuple} of
(L{IMailbox <pop3.IMailbox>}, L{IMailbox <pop3.IMailbox>}
provider, no-argument callable)
@return: A deferred which fires when authentication is complete.
If successful, it returns an L{IMailbox <pop3.IMailbox>} interface,
a mailbox and a logout function. If authentication fails, the
deferred fails with an L{UnauthorizedLogin
<twisted.cred.error.UnauthorizedLogin>} error.
"""
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):
"""
Perform authentication for a username/password login.
Override the default lookup scheme to allow virtual domains.
@type user: L{bytes}
@param user: The name of the user attempting to log in.
@type password: L{bytes}
@param password: The password to authenticate with.
@rtype: L{Deferred} which successfully results in 3-L{tuple} of
(L{IMailbox <pop3.IMailbox>}, L{IMailbox <pop3.IMailbox>}
provider, no-argument callable)
@return: A deferred which fires when authentication is complete.
If successful, it returns an L{IMailbox <pop3.IMailbox>} interface,
a mailbox and a logout function. If authentication fails, the
deferred fails with an L{UnauthorizedLogin
<twisted.cred.error.UnauthorizedLogin>} error.
"""
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):
"""
Check whether a domain is among the virtual domains supported by the
mail service.
@type user: L{bytes}
@param user: An email address.
@rtype: 2-L{tuple} of (L{bytes}, L{bytes})
@return: The local part and the domain part of the email address if the
domain is supported.
@raise POP3Error: When the domain is not supported by the mail service.
"""
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):
"""
A POP3 server protocol factory.
@ivar service: See L{__init__}
@type protocol: no-argument callable which returns a L{Protocol
<protocol.Protocol>} subclass
@ivar protocol: A callable which creates a protocol. The default value is
L{VirtualPOP3}.
"""
protocol = VirtualPOP3
service = None
def __init__(self, service):
"""
@type service: L{MailService}
@param service: An email service.
"""
self.service = service
def buildProtocol(self, addr):
"""
Create an instance of a POP3 server protocol.
@type addr: L{IAddress <twisted.internet.interfaces.IAddress>} provider
@param addr: The address of the POP3 client.
@rtype: L{POP3}
@return: A POP3 protocol.
"""
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.
@ivar filename: See L{__init__}
"""
deprecatedModuleAttribute(
Version("Twisted", 12, 2, 0),
"Use twisted.internet.ssl.DefaultOpenSSLContextFactory instead.",
"twisted.mail.protocols", "SSLContextFactory")
def __init__(self, filename):
"""
@type filename: L{bytes}
@param filename: The name of a file containing a certificate and
private key.
"""
self.filename = filename
def getContext(self):
"""
Create an SSL context.
@rtype: C{OpenSSL.SSL.Context}
@return: An SSL context configured with the certificate and private key
from the file.
"""
from OpenSSL import SSL
ctx = SSL.Context(SSL.SSLv23_METHOD)
ctx.use_certificate_file(self.filename)
ctx.use_privatekey_file(self.filename)
return ctx

View file

@ -0,0 +1,185 @@
# -*- test-case-name: twisted.mail.test.test_mail -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Support for relaying 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 mail can be relayed to a user.
@type user: L{User}
@param user: A user.
@rtype: no-argument callable which returns L{IMessage <smtp.IMessage>}
provider
@return: A function which takes no arguments and returns a message
receiver for the user.
@raise SMTPBadRcpt: When mail cannot be relayed to the user.
"""
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):
"""
Create an envelope and a message receiver for the relay queue.
@type user: L{User}
@param user: A user.
@rtype: L{IMessage <smtp.IMessage>}
@return: A message receiver.
"""
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):
"""
A base class for SMTP relayers.
"""
def __init__(self, messagePaths, *args, **kw):
"""
@type messagePaths: L{list} of L{bytes}
@param messagePaths: The base filename for each message to be relayed.
@type args: 1-L{tuple} of (0) L{bytes} or 2-L{tuple} of
(0) L{bytes}, (1) L{int}
@param args: Positional arguments for L{SMTPClient.__init__}
@type kw: L{dict}
@param kw: Keyword arguments for L{SMTPClient.__init__}
"""
smtp.SMTPClient.__init__(self, *args, **kw)
self.loadMessages(messagePaths)
class ESMTPRelayer(RelayerMixin, smtp.ESMTPClient):
"""
A base class for ESMTP relayers.
"""
def __init__(self, messagePaths, *args, **kw):
"""
@type messagePaths: L{list} of L{bytes}
@param messagePaths: The base filename for each message to be relayed.
@type args: 3-L{tuple} of (0) L{bytes}, (1) L{NoneType
<types.NoneType>} or L{ClientContextFactory
<twisted.internet.ssl.ClientContextFactory>}, (2) L{bytes} or
4-L{tuple} of (0) L{bytes}, (1) L{NoneType <types.NoneType>}
or L{ClientContextFactory
<twisted.internet.ssl.ClientContextFactory>}, (2) L{bytes},
(3) L{int}
@param args: Positional arguments for L{ESMTPClient.__init__}
@type kw: L{dict}
@param kw: Keyword arguments for L{ESMTPClient.__init__}
"""
smtp.ESMTPClient.__init__(self, *args, **kw)
self.loadMessages(messagePaths)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
"mail scripts"

View file

@ -0,0 +1,366 @@
# -*- test-case-name: twisted.mail.test.test_mailmail -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Implementation module for the I{mailmail} command.
"""
import os
import sys
import rfc822
import getpass
from ConfigParser import ConfigParser
try:
import cStringIO as StringIO
except:
import StringIO
from twisted.copyright import version
from twisted.internet import reactor
from twisted.mail import 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()
_unsupportedOption = SystemExit("Unsupported option.")
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
# Add a non-standard option for querying the version of this tool.
if '--version' in argv:
print 'mailmail version:', version
raise SystemExit()
# -bp lists queue information. Screw that.
if '-bp' in argv:
raise _unsupportedOption
# -bs makes sendmail use stdin/stdout as its transport. Screw that.
if '-bs' in argv:
raise _unsupportedOption
# -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 _unsupportedOption
# -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 _unsupportedOption
# -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 SystemExit("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)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,467 @@
# -*- test-case-name: twisted.mail.test.test_options -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Support for creating mail servers with twistd.
"""
import os
import warnings
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.internet import endpoints
from twisted.python import usage
from twisted.cred import checkers
from twisted.cred import strcred
from twisted.application import internet
class Options(usage.Options, strcred.AuthOptionMixin):
"""
An options list parser for twistd mail.
@type synopsis: L{bytes}
@ivar synopsis: A description of options for use in the usage message.
@type optParameters: L{list} of L{list} of (0) L{bytes}, (1) L{bytes},
(2) L{object}, (3) L{bytes}, (4) L{NoneType <types.NoneType>} or
callable which takes L{bytes} and returns L{object}
@ivar optParameters: Information about supported parameters. See
L{Options <twisted.python.usage.Options>} for details.
@type optFlags: L{list} of L{list} of (0) L{bytes}, (1) L{bytes} or
L{NoneType <types.NoneType>}, (2) L{bytes}
@ivar optFlags: Information about supported flags. See
L{Options <twisted.python.usage.Options>} for details.
@type _protoDefaults: L{dict} mapping L{bytes} to L{int}
@ivar _protoDefaults: A mapping of default service to port.
@type compData: L{Completions <usage.Completions>}
@ivar compData: Metadata for the shell tab completion system.
@type longdesc: L{bytes}
@ivar longdesc: A long description of the plugin for use in the usage
message.
@type service: L{MailService}
@ivar service: The email service.
@type last_domain: L{IDomain} provider or L{NoneType <types.NoneType>}
@ivar last_domain: The most recently specified domain.
"""
synopsis = "[options]"
optParameters = [
["pop3s", "S", 0,
"Port to start the POP3-over-SSL server on (0 to disable). "
"DEPRECATED: use "
"'--pop3 ssl:port:privateKey=pkey.pem:certKey=cert.pem'"],
["certificate", "c", None,
"Certificate file to use for SSL connections. "
"DEPRECATED: use "
"'--pop3 ssl:port:privateKey=pkey.pem:certKey=cert.pem'"],
["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"],
["no-pop3", None, "Disable the default POP3 server."],
["no-smtp", None, "Disable the default SMTP server."],
]
_protoDefaults = {
"pop3": 8110,
"smtp": 8025,
}
compData = usage.Completions(
optActions={"hostname" : usage.CompleteHostnames(),
"certificate" : usage.CompleteFiles("*.pem")}
)
longdesc = """
An SMTP / POP3 email server plugin for twistd.
Examples:
1. SMTP and POP server
twistd mail --maildirdbmdomain=example.com=/tmp/example.com
--user=joe=password
Starts an SMTP server that only accepts emails to joe@example.com and saves
them to /tmp/example.com.
Also starts a POP mail server which will allow a client to log in using
username: joe@example.com and password: password and collect any email that
has been saved in /tmp/example.com.
2. SMTP relay
twistd mail --relay=/tmp/mail_queue
Starts an SMTP server that accepts emails to any email address and relays
them to an appropriate remote SMTP server. Queued emails will be
temporarily stored in /tmp/mail_queue.
"""
def __init__(self):
"""
Parse options and create a mail service.
"""
usage.Options.__init__(self)
self.service = mail.MailService()
self.last_domain = None
for service in self._protoDefaults:
self[service] = []
def addEndpoint(self, service, description, certificate=None):
"""
Add an endpoint to a service.
@type service: L{bytes}
@param service: A service, either C{b'smtp'} or C{b'pop3'}.
@type description: L{bytes}
@param description: An endpoint description string or a TCP port
number.
@type certificate: L{bytes} or L{NoneType <types.NoneType>}
@param certificate: The name of a file containing an SSL certificate.
"""
self[service].append(
_toEndpoint(description, certificate=certificate))
def opt_pop3(self, description):
"""
Add a POP3 port listener on the specified endpoint.
You can listen on multiple ports by specifying multiple --pop3 options.
For backwards compatibility, a bare TCP port number can be specified,
but this is deprecated. [SSL Example: ssl:8995:privateKey=mycert.pem]
[default: tcp:8110]
"""
self.addEndpoint('pop3', description)
opt_p = opt_pop3
def opt_smtp(self, description):
"""
Add an SMTP port listener on the specified endpoint.
You can listen on multiple ports by specifying multiple --smtp options.
For backwards compatibility, a bare TCP port number can be specified,
but this is deprecated. [SSL Example: ssl:8465:privateKey=mycert.pem]
[default: tcp:8025]
"""
self.addEndpoint('smtp', description)
opt_s = opt_smtp
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.
This option requires an argument of the form 'NAME=PATH' where NAME is
the DNS domain name for which email will be accepted and where PATH is
a the filesystem path to a Maildir folder.
[Example: 'example.com=/tmp/example.com']
"""
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 and password to the last specified domain.
"""
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):
"""
Send undeliverable messages 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 the last specified 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 _getEndpoints(self, reactor, service):
"""
Return a list of endpoints for the specified service, constructing
defaults if necessary.
If no endpoints were configured for the service and the protocol
was not explicitly disabled with a I{--no-*} option, a default
endpoint for the service is created.
@type reactor: L{IReactorTCP <twisted.internet.interfaces.IReactorTCP>}
provider
@param reactor: If any endpoints are created, the reactor with
which they are created.
@type service: L{bytes}
@param service: The type of service for which to retrieve endpoints,
either C{b'pop3'} or C{b'smtp'}.
@rtype: L{list} of L{IStreamServerEndpoint
<twisted.internet.interfaces.IStreamServerEndpoint>} provider
@return: The endpoints for the specified service as configured by the
command line parameters.
"""
if service == 'pop3' and self['pop3s'] and len(self[service]) == 1:
# The single endpoint here is the POP3S service we added in
# postOptions. Include the default endpoint alongside it.
return self[service] + [
endpoints.TCP4ServerEndpoint(
reactor, self._protoDefaults[service])]
elif self[service]:
# For any non-POP3S case, if there are any services set up, just
# return those.
return self[service]
elif self['no-' + service]:
# If there are no services, but the service was explicitly disabled,
# return nothing.
return []
else:
# Otherwise, return the old default service.
return [
endpoints.TCP4ServerEndpoint(
reactor, self._protoDefaults[service])]
def postOptions(self):
"""
Check the validity of the specified set of options and
configure authentication.
@raise UsageError: When the set of options is invalid.
"""
from twisted.internet import reactor
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'])
else:
self.addEndpoint(
'pop3', self['pop3s'], certificate=self['certificate'])
if self['esmtp'] and self['hostname'] is None:
raise usage.UsageError("--esmtp requires --hostname")
# If the --auth option was passed, this will be present -- otherwise,
# it won't be, which is also a perfectly valid state.
if 'credCheckers' in self:
for ch in self['credCheckers']:
self.service.smtpPortal.registerChecker(ch)
if not self['disable-anonymous']:
self.service.smtpPortal.registerChecker(checkers.AllowAnonymousAccess())
anything = False
for service in self._protoDefaults:
self[service] = self._getEndpoints(reactor, service)
if self[service]:
anything = True
if not anything:
raise usage.UsageError("You cannot disable all protocols")
class AliasUpdater:
"""
A callable object which updates the aliases for a domain from an aliases(5)
file.
@ivar domains: See L{__init__}.
@ivar domain: See L{__init__}.
"""
def __init__(self, domains, domain):
"""
@type domains: L{dict} mapping L{bytes} to L{IDomain} provider
@param domains: A mapping of domain name to domain object
@type domain: L{IAliasableDomain} provider
@param domain: The domain to update.
"""
self.domains = domains
self.domain = domain
def __call__(self, new):
"""
Update the aliases for a domain from an aliases(5) file.
@type new: L{bytes}
@param new: The name of an aliases(5) file.
"""
self.domain.setAliasGroup(alias.loadAliasFile(self.domains, new))
def _toEndpoint(description, certificate=None):
"""
Create an endpoint based on a description.
@type description: L{bytes}
@param description: An endpoint description string or a TCP port
number.
@type certificate: L{bytes} or L{NoneType <types.NoneType>}
@param certificate: The name of a file containing an SSL certificate.
@rtype: L{IStreamServerEndpoint
<twisted.internet.interfaces.IStreamServerEndpoint>} provider
@return: An endpoint.
"""
from twisted.internet import reactor
try:
port = int(description)
except ValueError:
return endpoints.serverFromString(reactor, description)
warnings.warn(
"Specifying plain ports and/or a certificate is deprecated since "
"Twisted 11.0; use endpoint descriptions instead.",
category=DeprecationWarning, stacklevel=3)
if certificate:
from twisted.internet.ssl import DefaultOpenSSLContextFactory
ctx = DefaultOpenSSLContextFactory(certificate, certificate)
return endpoints.SSL4ServerEndpoint(reactor, port, ctx)
return endpoints.TCP4ServerEndpoint(reactor, port)
def makeService(config):
"""
Configure a service for operating a mail server.
The returned service may include POP3 servers, SMTP servers, or both,
depending on the configuration passed in. If there are multiple servers,
they will share all of their non-network state (i.e. the same user accounts
are available on all of them).
@type config: L{Options <usage.Options>}
@param config: Configuration options specifying which servers to include in
the returned service and where they should keep mail data.
@rtype: L{IService <twisted.application.service.IService>} provider
@return: A service which contains the requested mail servers.
"""
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)
if config['pop3']:
f = config.service.getPOP3Factory()
for endpoint in config['pop3']:
svc = internet.StreamServerEndpointService(endpoint, f)
svc.setServiceParent(config.service)
if config['smtp']:
f = smtpFactory()
if config['hostname']:
f.domain = config['hostname']
f.fArgs = (f.domain,)
if config['esmtp']:
f.fArgs = (None, None) + f.fArgs
for endpoint in config['smtp']:
svc = internet.StreamServerEndpointService(endpoint, f)
svc.setServiceParent(config.service)
return config.service

View file

@ -0,0 +1 @@
"Tests for twistd.mail"

View file

@ -0,0 +1,314 @@
#!/usr/bin/env python
# -*- test-case-name: twisted.mail.test.test_pop3client -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
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()

View file

@ -0,0 +1,86 @@
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

View file

@ -0,0 +1,36 @@
-----BEGIN CERTIFICATE-----
MIIDBjCCAm+gAwIBAgIBATANBgkqhkiG9w0BAQQFADB7MQswCQYDVQQGEwJTRzER
MA8GA1UEChMITTJDcnlwdG8xFDASBgNVBAsTC00yQ3J5cHRvIENBMSQwIgYDVQQD
ExtNMkNyeXB0byBDZXJ0aWZpY2F0ZSBNYXN0ZXIxHTAbBgkqhkiG9w0BCQEWDm5n
cHNAcG9zdDEuY29tMB4XDTAwMDkxMDA5NTEzMFoXDTAyMDkxMDA5NTEzMFowUzEL
MAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRvMRIwEAYDVQQDEwlsb2NhbGhv
c3QxHTAbBgkqhkiG9w0BCQEWDm5ncHNAcG9zdDEuY29tMFwwDQYJKoZIhvcNAQEB
BQADSwAwSAJBAKy+e3dulvXzV7zoTZWc5TzgApr8DmeQHTYC8ydfzH7EECe4R1Xh
5kwIzOuuFfn178FBiS84gngaNcrFi0Z5fAkCAwEAAaOCAQQwggEAMAkGA1UdEwQC
MAAwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRl
MB0GA1UdDgQWBBTPhIKSvnsmYsBVNWjj0m3M2z0qVTCBpQYDVR0jBIGdMIGagBT7
hyNp65w6kxXlxb8pUU/+7Sg4AaF/pH0wezELMAkGA1UEBhMCU0cxETAPBgNVBAoT
CE0yQ3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UEAxMbTTJDcnlw
dG8gQ2VydGlmaWNhdGUgTWFzdGVyMR0wGwYJKoZIhvcNAQkBFg5uZ3BzQHBvc3Qx
LmNvbYIBADANBgkqhkiG9w0BAQQFAAOBgQA7/CqT6PoHycTdhEStWNZde7M/2Yc6
BoJuVwnW8YxGO8Sn6UJ4FeffZNcYZddSDKosw8LtPOeWoK3JINjAk5jiPQ2cww++
7QGG/g5NDjxFZNDJP1dGiLAxPW6JXwov4v0FmdzfLOZ01jDcgQQZqEpYlgpuI5JE
WUQ9Ho4EzbYCOQ==
-----END CERTIFICATE-----
-----BEGIN RSA PRIVATE KEY-----
MIIBPAIBAAJBAKy+e3dulvXzV7zoTZWc5TzgApr8DmeQHTYC8ydfzH7EECe4R1Xh
5kwIzOuuFfn178FBiS84gngaNcrFi0Z5fAkCAwEAAQJBAIqm/bz4NA1H++Vx5Ewx
OcKp3w19QSaZAwlGRtsUxrP7436QjnREM3Bm8ygU11BjkPVmtrKm6AayQfCHqJoT
ZIECIQDW0BoMoL0HOYM/mrTLhaykYAVqgIeJsPjvkEhTFXWBuQIhAM3deFAvWNu4
nklUQ37XsCT2c9tmNt1LAT+slG2JOTTRAiAuXDtC/m3NYVwyHfFm+zKHRzHkClk2
HjubeEgjpj32AQIhAJqMGTaZVOwevTXvvHwNEH+vRWsAYU/gbx+OQB+7VOcBAiEA
oolb6NMg/R3enNPvS1O4UU1H8wpaF77L4yiSWlE0p4w=
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE REQUEST-----
MIIBDTCBuAIBADBTMQswCQYDVQQGEwJTRzERMA8GA1UEChMITTJDcnlwdG8xEjAQ
BgNVBAMTCWxvY2FsaG9zdDEdMBsGCSqGSIb3DQEJARYObmdwc0Bwb3N0MS5jb20w
XDANBgkqhkiG9w0BAQEFAANLADBIAkEArL57d26W9fNXvOhNlZzlPOACmvwOZ5Ad
NgLzJ1/MfsQQJ7hHVeHmTAjM664V+fXvwUGJLziCeBo1ysWLRnl8CQIDAQABoAAw
DQYJKoZIhvcNAQEEBQADQQA7uqbrNTjVWpF6By5ZNPvhZ4YdFgkeXFVWi5ao/TaP
Vq4BG021fJ9nlHRtr4rotpgHDX1rr+iWeHKsx4+5DRSy
-----END CERTIFICATE REQUEST-----

View file

@ -0,0 +1,32 @@
# Copyright (c) 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.assertEqual(from_, '')
self.assertEqual(to, 'moshez@example.com')
mess = rfc822.Message(cStringIO.StringIO(s))
self.assertEqual(mess['To'], 'moshez@example.com')
self.assertEqual(mess['From'], 'postmaster@example.org')
self.assertEqual(mess['subject'], 'Returned Mail: see transcript for details')
def testBounceMIME(self):
pass

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,75 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.mail.scripts.mailmail}, the implementation of the
command line program I{mailmail}.
"""
import sys
from StringIO import StringIO
from twisted.trial.unittest import TestCase
from twisted.mail.scripts.mailmail import parseOptions
class OptionsTests(TestCase):
"""
Tests for L{parseOptions} which parses command line arguments and reads
message text from stdin to produce an L{Options} instance which can be
used to send a message.
"""
def test_unspecifiedRecipients(self):
"""
If no recipients are given in the argument list and there is no
recipient header in the message text, L{parseOptions} raises
L{SystemExit} with a string describing the problem.
"""
self.addCleanup(setattr, sys, 'stdin', sys.stdin)
sys.stdin = StringIO(
'Subject: foo\n'
'\n'
'Hello, goodbye.\n')
exc = self.assertRaises(SystemExit, parseOptions, [])
self.assertEqual(exc.args, ('No recipients specified.',))
def test_listQueueInformation(self):
"""
The I{-bp} option for listing queue information is unsupported and
if it is passed to L{parseOptions}, L{SystemExit} is raised.
"""
exc = self.assertRaises(SystemExit, parseOptions, ['-bp'])
self.assertEqual(exc.args, ("Unsupported option.",))
def test_stdioTransport(self):
"""
The I{-bs} option for using stdin and stdout as the SMTP transport
is unsupported and if it is passed to L{parseOptions}, L{SystemExit}
is raised.
"""
exc = self.assertRaises(SystemExit, parseOptions, ['-bs'])
self.assertEqual(exc.args, ("Unsupported option.",))
def test_ignoreFullStop(self):
"""
The I{-i} and I{-oi} options for ignoring C{"."} by itself on a line
are unsupported and if either is passed to L{parseOptions},
L{SystemExit} is raised.
"""
exc = self.assertRaises(SystemExit, parseOptions, ['-i'])
self.assertEqual(exc.args, ("Unsupported option.",))
exc = self.assertRaises(SystemExit, parseOptions, ['-oi'])
self.assertEqual(exc.args, ("Unsupported option.",))
def test_copyAliasedSender(self):
"""
The I{-om} option for copying the sender if they appear in an alias
expansion is unsupported and if it is passed to L{parseOptions},
L{SystemExit} is raised.
"""
exc = self.assertRaises(SystemExit, parseOptions, ['-om'])
self.assertEqual(exc.args, ("Unsupported option.",))

View file

@ -0,0 +1,247 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.mail.tap}.
"""
from twisted.trial.unittest import TestCase
from twisted.python.usage import UsageError
from twisted.mail import protocols
from twisted.mail.tap import Options, makeService
from twisted.python.filepath import FilePath
from twisted.internet import endpoints, defer
from twisted.python import util
try:
import OpenSSL
except ImportError, e:
sslSkip = str(e)
else:
sslSkip = None
class OptionsTestCase(TestCase):
"""
Tests for the command line option parser used for I{twistd 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])
def test_barePort(self):
"""
A bare port passed to I{--pop3} results in deprecation warning in
addition to a TCP4ServerEndpoint.
"""
options = Options()
options.parseOptions(['--pop3', '8110'])
self.assertEqual(len(options['pop3']), 1)
self.assertIsInstance(
options['pop3'][0], endpoints.TCP4ServerEndpoint)
warnings = self.flushWarnings([options.opt_pop3])
self.assertEqual(len(warnings), 1)
self.assertEqual(warnings[0]['category'], DeprecationWarning)
self.assertEqual(
warnings[0]['message'],
"Specifying plain ports and/or a certificate is deprecated since "
"Twisted 11.0; use endpoint descriptions instead.")
def _endpointTest(self, service):
"""
Use L{Options} to parse a single service configuration parameter and
verify that an endpoint of the correct type is added to the list for
that service.
"""
options = Options()
options.parseOptions(['--' + service, 'tcp:1234'])
self.assertEqual(len(options[service]), 1)
self.assertIsInstance(
options[service][0], endpoints.TCP4ServerEndpoint)
def test_endpointSMTP(self):
"""
When I{--smtp} is given a TCP endpoint description as an argument, a
TCPServerEndpoint is added to the list of SMTP endpoints.
"""
self._endpointTest('smtp')
def test_endpointPOP3(self):
"""
When I{--pop3} is given a TCP endpoint description as an argument, a
TCPServerEndpoint is added to the list of POP3 endpoints.
"""
self._endpointTest('pop3')
def test_protoDefaults(self):
"""
POP3 and SMTP each listen on a TCP4ServerEndpoint by default.
"""
options = Options()
options.parseOptions([])
self.assertEqual(len(options['pop3']), 1)
self.assertIsInstance(
options['pop3'][0], endpoints.TCP4ServerEndpoint)
self.assertEqual(len(options['smtp']), 1)
self.assertIsInstance(
options['smtp'][0], endpoints.TCP4ServerEndpoint)
def test_protoDisable(self):
"""
The I{--no-pop3} and I{--no-smtp} options disable POP3 and SMTP
respectively.
"""
options = Options()
options.parseOptions(['--no-pop3'])
self.assertEqual(options._getEndpoints(None, 'pop3'), [])
self.assertNotEquals(options._getEndpoints(None, 'smtp'), [])
options = Options()
options.parseOptions(['--no-smtp'])
self.assertNotEquals(options._getEndpoints(None, 'pop3'), [])
self.assertEqual(options._getEndpoints(None, 'smtp'), [])
def test_allProtosDisabledError(self):
"""
If all protocols are disabled, L{UsageError} is raised.
"""
options = Options()
self.assertRaises(
UsageError, options.parseOptions, (['--no-pop3', '--no-smtp']))
def test_pop3sBackwardCompatibility(self):
"""
The deprecated I{--pop3s} and I{--certificate} options set up a POP3 SSL
server.
"""
cert = FilePath(__file__).sibling("server.pem")
options = Options()
options.parseOptions(['--pop3s', '8995',
'--certificate', cert.path])
self.assertEqual(len(options['pop3']), 2)
self.assertIsInstance(
options['pop3'][0], endpoints.SSL4ServerEndpoint)
self.assertIsInstance(
options['pop3'][1], endpoints.TCP4ServerEndpoint)
warnings = self.flushWarnings([options.postOptions])
self.assertEqual(len(warnings), 1)
self.assertEqual(warnings[0]['category'], DeprecationWarning)
self.assertEqual(
warnings[0]['message'],
"Specifying plain ports and/or a certificate is deprecated since "
"Twisted 11.0; use endpoint descriptions instead.")
if sslSkip is not None:
test_pop3sBackwardCompatibility.skip = sslSkip
def test_esmtpWithoutHostname(self):
"""
If I{--esmtp} is given without I{--hostname}, L{Options.parseOptions}
raises L{UsageError}.
"""
options = Options()
exc = self.assertRaises(UsageError, options.parseOptions, ['--esmtp'])
self.assertEqual("--esmtp requires --hostname", str(exc))
def test_auth(self):
"""
Tests that the --auth option registers a checker.
"""
options = Options()
options.parseOptions(['--auth', 'memory:admin:admin:bob:password'])
self.assertEqual(len(options['credCheckers']), 1)
checker = options['credCheckers'][0]
interfaces = checker.credentialInterfaces
registered_checkers = options.service.smtpPortal.checkers
for iface in interfaces:
self.assertEqual(checker, registered_checkers[iface])
class SpyEndpoint(object):
"""
SpyEndpoint remembers what factory it is told to listen with.
"""
listeningWith = None
def listen(self, factory):
self.listeningWith = factory
return defer.succeed(None)
class MakeServiceTests(TestCase):
"""
Tests for L{twisted.mail.tap.makeService}
"""
def _endpointServerTest(self, key, factoryClass):
"""
Configure a service with two endpoints for the protocol associated with
C{key} and verify that when the service is started a factory of type
C{factoryClass} is used to listen on each of them.
"""
cleartext = SpyEndpoint()
secure = SpyEndpoint()
config = Options()
config[key] = [cleartext, secure]
service = makeService(config)
service.privilegedStartService()
service.startService()
self.addCleanup(service.stopService)
self.assertIsInstance(cleartext.listeningWith, factoryClass)
self.assertIsInstance(secure.listeningWith, factoryClass)
def test_pop3(self):
"""
If one or more endpoints is included in the configuration passed to
L{makeService} for the C{"pop3"} key, a service for starting a POP3
server is constructed for each of them and attached to the returned
service.
"""
self._endpointServerTest("pop3", protocols.POP3Factory)
def test_smtp(self):
"""
If one or more endpoints is included in the configuration passed to
L{makeService} for the C{"smtp"} key, a service for starting an SMTP
server is constructed for each of them and attached to the returned
service.
"""
self._endpointServerTest("smtp", protocols.SMTPFactory)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,620 @@
# -*- test-case-name: twisted.mail.test.test_pop3client -*-
# Copyright (c) 2001-2004 Divmod Inc.
# See LICENSE for details.
import sys
import inspect
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.assertEqual(exc.args[0], "Offline for maintenance"))
def testOkUser(self):
p, t = setUp()
d = p.user("username")
self.assertEqual(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.assertEqual(t.value(), "USER username\r\n")
p.dataReceived("-ERR account suspended\r\n")
return self.assertFailure(
d, ServerErrorResponse).addCallback(
lambda exc: self.assertEqual(exc.args[0], "account suspended"))
def testOkPass(self):
p, t = setUp()
d = p.password("password")
self.assertEqual(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.assertEqual(t.value(), "PASS password\r\n")
p.dataReceived("-ERR go away\r\n")
return self.assertFailure(
d, ServerErrorResponse).addCallback(
lambda exc: self.assertEqual(exc.args[0], "go away"))
def testOkLogin(self):
p, t = setUp()
p.allowInsecureLogin = True
d = p.login("username", "password")
self.assertEqual(t.value(), "USER username\r\n")
p.dataReceived("+OK go ahead\r\n")
self.assertEqual(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.assertEqual(t.value(), "USER username\r\n")
p.dataReceived("+OK waiting on you\r\n")
self.assertEqual(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.assertEqual(exc.args[0], "bogus login"))
def testBadUsernameLogin(self):
p, t = setUp()
p.allowInsecureLogin = True
d = p.login("username", "password")
self.assertEqual(t.value(), "USER username\r\n")
p.dataReceived("-ERR bogus login\r\n")
return self.assertFailure(
d, ServerErrorResponse).addCallback(
lambda exc: self.assertEqual(exc.args[0], "bogus login"))
def testServerGreeting(self):
p, t = setUp(greet=False)
p.dataReceived("+OK lalala this has no challenge\r\n")
self.assertEqual(p.serverChallenge, None)
def testServerGreetingWithChallenge(self):
p, t = setUp(greet=False)
p.dataReceived("+OK <here is the challenge>\r\n")
self.assertEqual(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.assertEqual(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.assertEqual(t.value(), "USER username\r\n")
t.clear()
p.dataReceived("+OK\r\n")
self.assertEqual(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.assertEqual(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.assertEqual(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.assertEqual(c.data, {0: [3], 1: [2], 2: [1]})
p.dataReceived("5 3\r\n6 2\r\n7 1\r\n")
self.assertEqual(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.assertEqual(t.value(), "LIST\r\n")
p.dataReceived("-ERR Fatal doom server exploded\r\n")
return self.assertFailure(
d, ServerErrorResponse).addCallback(
lambda exc: self.assertEqual(exc.args[0], "Fatal doom server exploded"))
def testListUID(self):
p, t = setUp()
d = p.listUID()
self.assertEqual(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.assertEqual(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.assertEqual(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.assertEqual(t.value(), "UIDL\r\n")
p.dataReceived("-ERR Fatal doom server exploded\r\n")
return self.assertFailure(
d, ServerErrorResponse).addCallback(
lambda exc: self.assertEqual(exc.args[0], "Fatal doom server exploded"))
class POP3ClientMessageTestCase(unittest.TestCase):
def testRetrieve(self):
p, t = setUp()
d = p.retrieve(7)
self.assertEqual(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.assertEqual(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.assertEqual(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.assertEqual(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.assertEqual(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.assertEqual(c.data, ["Line the first! Woop",
"Line the last! Bye"])
def testFailedRetrieve(self):
p, t = setUp()
d = p.retrieve(0)
self.assertEqual(t.value(), "RETR 1\r\n")
p.dataReceived("-ERR Fatal doom server exploded\r\n")
return self.assertFailure(
d, ServerErrorResponse).addCallback(
lambda exc: self.assertEqual(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.assertEqual,
["First line of %d." % (i + 1,),
"Second line of %d." % (i + 1,)])
for i
in range(3)]
for i in range(1, 4):
self.assertEqual(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.assertEqual(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.assertEqual(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.assertEqual(t.value(), "CAPA\r\n")
p.dataReceived("-ERR This server is lame!\r\n")
return d.addCallback(self.assertEqual, {})
def testStat(self):
p, t = setUp()
d = p.stat()
self.assertEqual(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.assertEqual(t.value(), "STAT\r\n")
p.dataReceived("-ERR This server is lame!\r\n")
return self.assertFailure(
d, ServerErrorResponse).addCallback(
lambda exc: self.assertEqual(exc.args[0], "This server is lame!"))
def testNoop(self):
p, t = setUp()
d = p.noop()
self.assertEqual(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.assertEqual(t.value(), "NOOP\r\n")
p.dataReceived("-ERR This server is lame!\r\n")
return self.assertFailure(
d, ServerErrorResponse).addCallback(
lambda exc: self.assertEqual(exc.args[0], "This server is lame!"))
def testRset(self):
p, t = setUp()
d = p.reset()
self.assertEqual(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.assertEqual(t.value(), "RSET\r\n")
p.dataReceived("-ERR This server is lame!\r\n")
return self.assertFailure(
d, ServerErrorResponse).addCallback(
lambda exc: self.assertEqual(exc.args[0], "This server is lame!"))
def testDelete(self):
p, t = setUp()
d = p.delete(3)
self.assertEqual(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.assertEqual(t.value(), "DELE 4\r\n")
p.dataReceived("-ERR Winner is not you.\r\n")
return self.assertFailure(
d, ServerErrorResponse).addCallback(
lambda exc: self.assertEqual(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):
"""
Tests for POP3Client's support for TLS connections.
"""
def test_startTLS(self):
"""
POP3Client.startTLS starts a TLS session over its existing TCP
connection.
"""
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')
self.addCleanup(port.stopListening)
H = port.getHost().host
P = port.getHost().port
connLostDeferred = defer.Deferred()
cp = SimpleClient(defer.Deferred(), ClientTLSContext())
def connectionLost(reason):
SimpleClient.connectionLost(cp, reason)
connLostDeferred.callback(None)
cp.connectionLost = connectionLost
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.assertEqual(
sf.input,
['CAPA', 'STLS', 'CAPA', 'QUIT'])
def cleanup(result):
log.msg("Asserted correct input; disconnecting client and shutting down server")
conn.disconnect()
return connLostDeferred
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"
import twisted.mail.pop3client
class POP3ClientMiscTestCase(unittest.TestCase):
"""
Miscellaneous tests more to do with module/package structure than
anything to do with the POP3 client.
"""
def test_all(self):
"""
twisted.mail.pop3client.__all__ should be empty because all classes
should be imported through twisted.mail.pop3.
"""
self.assertEqual(twisted.mail.pop3client.__all__, [])
def test_import(self):
"""
Every public class in twisted.mail.pop3client should be available as a
member of twisted.mail.pop3 with the exception of
twisted.mail.pop3client.POP3Client which should be available as
twisted.mail.pop3.AdvancedClient.
"""
publicClasses = [c[0] for c in inspect.getmembers(
sys.modules['twisted.mail.pop3client'],
inspect.isclass)
if not c[0][0] == '_']
for pc in publicClasses:
if not pc == 'POP3Client':
self.failUnless(hasattr(twisted.mail.pop3, pc))
else:
self.failUnless(hasattr(twisted.mail.pop3,
'AdvancedPOP3Client'))

View file

@ -0,0 +1,18 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for the command-line mailer tool provided by Twisted Mail.
"""
from twisted.trial.unittest import TestCase
from twisted.scripts.test.test_scripts import ScriptTestsMixin
class ScriptTests(TestCase, ScriptTestsMixin):
"""
Tests for all one of mail's scripts.
"""
def test_mailmail(self):
self.scriptTest("mail/mailmail")

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,411 @@
Ticket numbers in this file can be looked up by visiting
http://twistedmatrix.com/trac/ticket/<number>
Twisted Mail 14.0.0 (2014-05-08)
================================
Improved Documentation
----------------------
- twisted.mail.alias now has full API documentation. (#6637)
- twisted.mail.tap now has full API documentation. (#6648)
- twisted.mail.maildir now has full API documentation. (#6651)
- twisted.mail.pop3client now has full API documentation. (#6653)
- twisted.mail.protocols now has full API documentation. (#6654)
- twisted.mail.pop now has full API documentation. (#6666)
- twisted.mail.relay and twisted.mail.relaymanager now have full API
documentation. (#6739)
- twisted.mail.pop3client public classes now appear as part of the
twisted.mail.pop3 API. (#6761)
Other
-----
- #6696
Twisted Mail 13.2.0 (2013-10-29)
================================
Features
--------
- twisted.mail.smtp.sendmail now returns a cancellable Deferred.
(#6572)
Improved Documentation
----------------------
- twisted.mail.mail now has full API documentation. (#6649)
- twisted.mail.bounce now has full API documentation. (#6652)
Other
-----
- #5387, #6486
Twisted Mail 13.1.0 (2013-06-23)
================================
Bugfixes
--------
- twisted.mail.smtp.ESMTPClient no longer tries to use a STARTTLS
capability offered by a server after TLS has already been
negotiated. (#6524)
Deprecations and Removals
-------------------------
- twisted.mail.IDomain.startMessage, deprecated since 2003, is
removed now. (#4151)
Other
-----
- #6342
Twisted Mail 13.0.0 (2013-03-19)
================================
Bugfixes
--------
- twisted.mail.smtp.ESMTPClient no longer attempts to negotiate a TLS
session if transport security has been requested and the protocol
is already running on a TLS connection. (#3989)
- twisted.mail.imap4.Query now filters illegal characters from the
values of KEYWORD and UNKEYWORD and also emits them without adding
quotes (which are also illegal). (#4392)
- twisted.mail.imap4.IMAP4Client can now interpret the BODY response
for multipart/* messages with parts which are also multipart/*.
(#4631)
Deprecations and Removals
-------------------------
- tlsMode attribute of twisted.mail.smtp.ESMTPClient is deprecated.
(#5852)
Other
-----
- #6218, #6297
Twisted Mail 12.3.0 (2012-12-20)
================================
Bugfixes
--------
- twisted.mail.imap4._FetchParser now raises
IllegalClientResponse("Invalid Argument") when protocol encounters
extra bytes at the end of a valid FETCH command. (#4000)
Improved Documentation
----------------------
- twisted.mail.tap now documents example usage in its longdesc
output for the 'mail' plugin (#5922)
Other
-----
- #3751
Twisted Mail 12.2.0 (2012-08-26)
================================
Bugfixes
--------
- twisted.mail.imap4.IMAP4Server will now generate an error response
when it receives an illegal SEARCH term from a client. (#4080)
- twisted.mail.imap4 now serves BODYSTRUCTURE responses which provide
more information and conform to the IMAP4 RFC more closely. (#5763)
Deprecations and Removals
-------------------------
- twisted.mail.protocols.SSLContextFactory is now deprecated. (#4963)
- The --passwordfile option to twistd mail is now removed. (#5541)
Other
-----
- #5697, #5750, #5751, #5783
Twisted Mail 12.1.0 (2012-06-02)
================================
Bugfixes
--------
- twistd mail --auth, broken in 11.0, now correctly connects
authentication to the portal being used (#5219)
Other
-----
- #5686
Twisted Mail 12.0.0 (2012-02-10)
================================
No significant changes have been made for this release.
Twisted Mail 11.1.0 (2011-11-15)
================================
Features
--------
- twisted.mail.smtp.LOGINCredentials now generates challenges with
":" instead of "\0" for interoperability with Microsoft Outlook.
(#4692)
Bugfixes
--------
- When run from an unpacked source tarball or a VCS checkout,
bin/mail/mailmail will now use the version of Twisted it is part
of. (#3526)
Other
-----
- #4796, #5006
Twisted Mail 11.0.0 (2011-04-01)
================================
Features
--------
- The `twistd mail` command line now accepts endpoint descriptions
for POP3 and SMTP servers. (#4739)
- The twistd mail plugin now accepts new authentication options via
strcred.AuthOptionMixin. These include --auth, --auth-help, and
authentication type-specific help options. (#4740)
Bugfixes
--------
- twisted.mail.imap4.IMAP4Server now generates INTERNALDATE strings
which do not consider the locale. (#4937)
Improved Documentation
----------------------
- Added a simple SMTP example, showing how to use sendmail. (#4042)
Other
-----
- #4162
Twisted Mail 10.2.0 (2010-11-29)
================================
Improved Documentation
----------------------
- The email server example now demonstrates how to set up
authentication and authorization using twisted.cred. (#4609)
Deprecations and Removals
-------------------------
- twisted.mail.smtp.sendEmail, deprecated since mid 2003 (before
Twisted 2.0), has been removed. (#4529)
Other
-----
- #4038, #4572
Twisted Mail 10.1.0 (2010-06-27)
================================
Bugfixes
--------
- twisted.mail.imap4.IMAP4Server no longer fails on search queries
that contain wildcards. (#2278)
- A case which would cause twisted.mail.imap4.IMAP4Server to loop
indefinitely when handling a search command has been fixed. (#4385)
Other
-----
- #4069, #4271, #4467
Twisted Mail 10.0.0 (2010-03-01)
================================
Bugfixes
--------
- twisted.mail.smtp.ESMTPClient and
twisted.mail.smtp.LOGINAuthenticator now implement the (obsolete)
LOGIN SASL mechanism according to the draft specification. (#4031)
- twisted.mail.imap4.IMAP4Client will no longer misparse all html-
formatted message bodies received in response to a fetch command.
(#4049)
- The regression in IMAP4 search handling of "OR" and "NOT" terms has
been fixed. (#4178)
Other
-----
- #4028, #4170, #4200
Twisted Mail 9.0.0 (2009-11-24)
===============================
Features
--------
- maildir.StringListMailbox, an in-memory maildir mailbox, now supports
deletion, undeletion, and syncing (#3547)
- SMTPClient's callbacks are now more completely documented (#684)
Fixes
-----
- Parse UNSEEN response data and include it in the result of
IMAP4Client.examine (#3550)
- The IMAP4 client now delivers more unsolicited server responses to callbacks
rather than ignoring them, and also won't ignore solicited responses that
arrive on the same line as an unsolicited one (#1105)
- Several bugs in the SMTP client's idle timeout support were fixed (#3641,
#1219)
- A case where the SMTP client could skip some recipients when retrying
delivery has been fixed (#3638)
- Errors during certain data transfers will no longer be swallowed. They will
now bubble up to the higher-level API (such as the sendmail function) (#3642)
- Escape sequences inside quoted strings in IMAP4 should now be parsed
correctly by the IMAP4 server protocol (#3659)
- The "imap4-utf-7" codec that is registered by twisted.mail.imap4 had a number
of fixes that allow it to work better with the Python codecs system, and to
actually work (#3663)
- The Maildir implementation now ensures time-based ordering of filenames so
that the lexical sorting of messages matches the order in which they were
received (#3812)
- SASL PLAIN credentials generated by the IMAP4 protocol implementations
(client and server) should now be RFC-compliant (#3939)
- Searching for a set of sequences using the IMAP4 "SEARCH" command should
now work on the IMAP4 server protocol implementation. This at least improves
support for the Pine mail client (#1977)
Other
-----
- #2763, #3647, #3750, #3819, #3540, #3846, #2023, #4050
Mail 8.2.0 (2008-12-16)
=======================
Fixes
-----
- The mailmail tool now provides better error messages for usage errors (#3339)
- The SMTP protocol implementation now works on PyPy (#2976)
Other
-----
- #3475
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

View file

@ -0,0 +1,6 @@
Twisted Mail 14.0.0
Twisted Mail depends on Twisted Core and (sometimes) Twisted Names. For TLS
support, pyOpenSSL (<http://launchpad.net/pyopenssl>) is also required. Aside
from protocol implementations, much of Twisted Mail also only runs on POSIX
platforms.