PKEF:,_]]EGG-INFO/SOURCES.txtsetup.py pymta/__init__.py pymta/api.py pymta/command_parser.py pymta/compat.py pymta/exceptions.py pymta/model.py pymta/mta.py pymta/release.py pymta/session.py pymta/test_util.py pymta.egg-info/PKG-INFO pymta.egg-info/SOURCES.txt pymta.egg-info/dependency_links.txt pymta.egg-info/requires.txt pymta.egg-info/top_level.txt pymta.egg-info/zip-safe PKEF:nEGG-INFO/requires.txtrepoze.workflowPKEF:&o;EGG-INFO/PKG-INFOMetadata-Version: 1.0 Name: pymta Version: 0.4.0 Summary: library to build a custom SMTP server Home-page: http://www.schwarz.eu/opensource/projects/pymta Author: Felix Schwarz Author-email: felix.schwarz@oss.schwarz.eu License: MIT Download-URL: http://www.schwarz.eu/opensource/projects/pymta/download/0.4.0/pymta-0.4.0.tar.gz Description: pymta is a library to build a custom SMTP server in Python. This is useful if you want to... * test mail-sending code against a real SMTP server even in your unit tests. * build a custom SMTP server with non-standard behavior without reimplementing the whole SMTP protocol. * have a low-volume SMTP server which can be easily extended using Python Changelog ****************************** 0.4.0 (08.06.2009) ================== - Compatibility fixes for Python 2.3-2.6 - Policies can drop connection to the client before or after the response - CommandParser is more robust against various socket errors - Better infrastructure and documentation to use pymta in third-party tests 0.3.1 (27.02.2009) ================== - Fixed bug which caused hang after unexpected connection drop by client 0.3 (15.02.2009) ================== - Switch to process-based architecture, got rid of asyncore - Support for size-limitations of messages, huge messages will not be stored in memory if they will be rejected anyway (denial of service prevention) - API documentation is now auto-generated - Renamed DefaultMTAPolicy to IMTAPolicy and moved all interfaces to pymta.api - Added the debugging_server as an extremely simple example of a pymta-based server Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Communications :: Email Classifier: Topic :: Software Development :: Libraries :: Python Modules PKwH:2EGG-INFO/zip-safe PKEF:vrHEGG-INFO/top_level.txtpymta PKEF:2EGG-INFO/dependency_links.txt PKBF:MkEpymta/release.py# -*- coding: UTF-8 -*- """Release information about pymta.""" name = "pymta" version = "0.4.0" description = "library to build a custom SMTP server" long_description = """ pymta is a library to build a custom SMTP server in Python. This is useful if you want to... * test mail-sending code against a real SMTP server even in your unit tests. * build a custom SMTP server with non-standard behavior without reimplementing the whole SMTP protocol. * have a low-volume SMTP server which can be easily extended using Python Changelog ****************************** 0.4.0 (08.06.2009) ================== - Compatibility fixes for Python 2.3-2.6 - Policies can drop connection to the client before or after the response - CommandParser is more robust against various socket errors - Better infrastructure and documentation to use pymta in third-party tests 0.3.1 (27.02.2009) ================== - Fixed bug which caused hang after unexpected connection drop by client 0.3 (15.02.2009) ================== - Switch to process-based architecture, got rid of asyncore - Support for size-limitations of messages, huge messages will not be stored in memory if they will be rejected anyway (denial of service prevention) - API documentation is now auto-generated - Renamed DefaultMTAPolicy to IMTAPolicy and moved all interfaces to pymta.api - Added the debugging_server as an extremely simple example of a pymta-based server """ author = "Felix Schwarz" email = "felix.schwarz@oss.schwarz.eu" url = "http://www.schwarz.eu/opensource/projects/pymta" download_url = "http://www.schwarz.eu/opensource/projects/pymta/download/%(version)s/pymta-%(version)s.tar.gz" % dict(version=version) copyright = "© 2008-2009 Felix Schwarz" license="MIT" PKEF:F@m0pymta/compat.pyc; ,Jc@s@dZdgZy eZWn ej odklZnXdS(shThis module provides a unified view on certain symbols that are not present for all versions of Python.sset(sSetN(s__doc__s__all__ssets NameErrorssetssSet(ssets__all__((s,build/bdist.linux-x86_64/egg/pymta/compat.pys?s   PKEF:"OUR-- pymta/api.pyc; ,Jc@sdZdddddgZdefdYZdefdYZdefdYZdefd YZdefd YZd S( sThis package contains classes which from which you can derive your own classes easily to customize the behavior of your MTA. Everything in here is considered part of the public API which should be as stable as possible.sIAuthenticatorsIMessageDeliverers IMTAPolicysPolicyDecisionsPyMTAExceptioncBstZdZdZRS(sAuthenticators check if the user’s credentials are actually correct. This may involve some checking against external subsystems (e.g. a database or a LDAP directory).cCs tdS(szThis method is called after the client issued an AUTH PLAIN command and must return a boolean value (True/False).N(sNotImplementedError(sselfsusernamespasswordspeer((s)build/bdist.linux-x86_64/egg/pymta/api.pys authenticate%s(s__name__s __module__s__doc__s authenticate(((s)build/bdist.linux-x86_64/egg/pymta/api.pysIAuthenticator s cBstZdZdZRS(sDeliverers take care of the message routing/delivery after a message was accepted (e.g. put it in a mailbox file, forward it to another server, ...). cCs tdS(scThis method is called when a new message was accepted by the server. Now the MTA is then in charge of delivering the message to the specified recipients. Please note that you can not reject the message anymore at this stage (if there are problems you must generate a non-delivery report aka bounce). There will be one deliverer instance per client connection so this method may does not have to be thread-safe. However this method may get called multiple times when the client transmits more than one message for the same connection.N(sNotImplementedError(sselfsmsg((s)build/bdist.linux-x86_64/egg/pymta/api.pysnew_message_accepted1s (s__name__s __module__s__doc__snew_message_accepted(((s)build/bdist.linux-x86_64/egg/pymta/api.pysIMessageDeliverer,s cBsDtZeedZdZdZdZdZdZ RS(NcCs(||_||_t|_t|_dS(N(sdecisionsselfs _decisionsreplys_replysFalses!_close_connection_before_responses _close_connection_after_response(sselfsdecisionsreply((s)build/bdist.linux-x86_64/egg/pymta/api.pys__init__@s   cCs |iSdS(sgReturn True if the server should close the client connection without any further communication.N(sselfs!_close_connection_before_response(sself((s)build/bdist.linux-x86_64/egg/pymta/api.pys close_connection_before_responseFscCs |iSdS(sfReturn True if the server should close the client connection after it sent the given response.N(sselfs _close_connection_after_response(sself((s)build/bdist.linux-x86_64/egg/pymta/api.pysclose_connection_after_responseKscCs |iSdS(N(sselfs _decision(sself((s)build/bdist.linux-x86_64/egg/pymta/api.pysis_command_acceptablePscCs|itj SdS(N(sselfs_replysNone(sself((s)build/bdist.linux-x86_64/egg/pymta/api.pysuse_custom_replySscCs)|i otdn|iSdS(NsNo custom reply set.(sselfsuse_custom_replys ValueErrors_reply(sself((s)build/bdist.linux-x86_64/egg/pymta/api.pysget_custom_replyVs( s__name__s __module__sTruesNones__init__s close_connection_before_responsesclose_connection_after_responsesis_command_acceptablesuse_custom_replysget_custom_reply(((s)build/bdist.linux-x86_64/egg/pymta/api.pysPolicyDecision?s     cBshtZdZdZdZdZdZdZdZdZ dZ d Z d Z RS( sPolicies can change with behavior of an MTA dynamically (e.g. don't allow relaying unless the client is located within the trusted company network, enable authentication only for some connections). In established MTAs like Exim and Postfix it's a very important task for every system administrator to configure the message acceptance policies which are normally part of the configuration file. A policy does not change the SMTP implementation itself (the state machine) but can send out custom replies to the client. A policy doesn't have to care if the commands were given in the correct order (the state machines will take care of that). The only thing is that the message object passed into many policy methods does not contain all data at certain stages (e.g. accept_mail_from can not access the recipients list because that was not submitted yet). 'IMTAPolicy' provides a very permissive policy (all commands are accepted) from which you can derive custom policies. Its methods are usually named 'accept_'. Every method in the 'IMTAPolicy' interface can return either a single boolean value (True/False) or a tuple. A boolean value specifies if the command should be accepted. The caller is responsible for sending the actual default replies. Alternatively a policy can choose to return a tuple to have more control over the reply sent to the client: (decision, (reply code, reply message)). The decision is the boolean known from the last paragraph. The reply code is an integer which should a be a valid SMTP code. reply message is either a basestring with a custom message or an iterable of basestrings (in case a a multi-line reply is sent). Last but not least a PolicyDecision can be returned which embodies the decision as well as (optionally) a custom reply. The reply has the same format as described in the paragraph before. The PolicyDecision can ask the server to close the connection unconditionally after or even before sending the response to the client (in the latter case no response will be sent). cCstSdS(sThis method is called directly after a new connection is received. The policy can decide if the given peer is allowed to connect to the SMTP server. If it declines, the connection will be closed immediately.N(sTrue(sselfspeer((s)build/bdist.linux-x86_64/egg/pymta/api.pysaccept_new_connectionscCstSdS(sReturn the maximum size (in bytes) for messages from this peer. When this method returns an integer, there pymta will check the actual message size after the message was received (before the accept_msgdata method is called) and will respond with the appropriate error message if necessary. If you return None, no size limit will be enforced by pymta (however you can always reject a message using accept_msgdata().N(sNone(sselfspeer((s)build/bdist.linux-x86_64/egg/pymta/api.pysmax_message_sizescCs3|i|}|tjod|fSnfSdS(sReturn an iterable for SMTP extensions to advertise after EHLO. By default support for SMTP SIZE extension will be announced if you set a max message size.sSIZE %dN(sselfsmax_message_sizespeersmax_sizesNone(sselfspeersmax_size((s)build/bdist.linux-x86_64/egg/pymta/api.pys ehlo_liness  cCstSdS(sQDecides if the HELO command with the given helo_name should be accepted.N(sTrue(sselfs helo_stringsmessage((s)build/bdist.linux-x86_64/egg/pymta/api.pys accept_heloscCstSdS(sQDecides if the EHLO command with the given helo_name should be accepted.N(sTrue(sselfs ehlo_stringsmessage((s)build/bdist.linux-x86_64/egg/pymta/api.pys accept_ehloscCstSdS(sGDecides if AUTH plain should be allowed for this client. Please note that username and password are not verified before, the authenticator will check them after the policy allowed this command. The method must not return a response by itself in case it accepts the AUTH PLAIN command!N(sTrue(sselfsusernamespasswordsmessage((s)build/bdist.linux-x86_64/egg/pymta/api.pysaccept_auth_plainscCstSdS(sEDecides if the sender of this message (MAIL FROM) should be accepted.N(sTrue(sselfssendersmessage((s)build/bdist.linux-x86_64/egg/pymta/api.pys accept_fromscCstSdS(sDecides if recipient of this message (RCPT TO) should be accepted. If a message should be delivered to multiple recipients this method is called for every recipient.N(sTrue(sselfs new_recipientsmessage((s)build/bdist.linux-x86_64/egg/pymta/api.pysaccept_rcpt_toscCstSdS(sDecides if we allow the client to start a message transfer (the actual message contents will be transferred after this method allowed it).N(sTrue(sselfsmessage((s)build/bdist.linux-x86_64/egg/pymta/api.pys accept_datascCstSdS(sThis method actually matches no real SMTP command. It is called after a message was transferred completely and this is the last check before the SMTP server takes the responsibility of transferring it to the recipients.N(sTrue(sselfsmsgdatasmessage((s)build/bdist.linux-x86_64/egg/pymta/api.pysaccept_msgdatas( s__name__s __module__s__doc__saccept_new_connectionsmax_message_sizes ehlo_liness accept_helos accept_ehlosaccept_auth_plains accept_fromsaccept_rcpt_tos accept_datasaccept_msgdata(((s)build/bdist.linux-x86_64/egg/pymta/api.pys IMTAPolicy\s %      cBstZdZRS(s,Base class for all exceptions used in pymta.(s__name__s __module__s__doc__(((s)build/bdist.linux-x86_64/egg/pymta/api.pysPyMTAExceptions N( s__doc__s__all__sobjectsIAuthenticatorsIMessageDeliverersPolicyDecisions IMTAPolicys ExceptionsPyMTAException(sIAuthenticatorsPyMTAExceptionsIMessageDeliverers__all__s IMTAPolicysPolicyDecision((s)build/bdist.linux-x86_64/egg/pymta/api.pys?s  lPKE:j pymta/mta.py# -*- coding: UTF-8 -*- # # The MIT License # # Copyright (c) 2008 Felix Schwarz # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. import socket from threading import Event import time from pymta.command_parser import WorkerProcess __all__ = ['PythonMTA'] def forked_child(queue, server_socket, deliverer_class, policy_class, authenticator_class): child = WorkerProcess(queue, server_socket, deliverer_class, policy_class, authenticator_class) child.run() class PythonMTA(object): """Create a new MTA which listens for new connections afterwards. local_address is a string containing either the IP oder the DNS host name of the interface on which PythonMTA should listen. deliverer_class, policy_class and authenticator_class are callables which can be used to add custom behavior. Please note that they must be picklable if you use forked worker processes (default). Every new connection gets their own instance of policy_class and authenticator_class so these classes don't have to be thread-safe. If you omit the policy, all syntactically valid SMTP commands are accepted. If there is no authenticator specified, authentication will not be available.""" def __init__(self, local_address, bind_port, deliverer_class, policy_class=None, authenticator_class=None): self._local_address = local_address self._bind_port = bind_port self._deliverer_class = deliverer_class self._policy_class = policy_class self._authenticator_class = authenticator_class self._queue = None self._processes = [] self._shutdown_server = Event() def _try_to_bind_to_socket(self, server_socket): tries = 0 while tries < 10: try: server_socket.bind((self._local_address, self._bind_port)) except socket.error: tries += 1 time.sleep(0.1) else: break def _build_server_socket(self): server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # If the server crashed and we restarted it within a very short time # frame, prevent 'address already in use' errors. server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # We want to terminate all children within a reasonable time server_socket.settimeout(1) self._try_to_bind_to_socket(server_socket) # Don't loose connections in the time frame when a new connection was # accepted. Python's documentation says the maximum is system dependent # but usually 5 so we take that. server_socket.listen(5) return server_socket def _get_child_args(self, server_socket): return (self._queue, server_socket, self._deliverer_class, self._policy_class, self._authenticator_class) def _start_new_worker_process(self, server_socket): """Start a new child worker process which will listen on the given socket and return a reference to the new process.""" from multiprocessing import Process p = Process(target=forked_child, args=self._get_child_args(server_socket)) p.start() return p def serve_forever(self, use_multiprocessing=True): if use_multiprocessing: try: from multiprocessing import Queue except ImportError: use_multiprocessing = False if not use_multiprocessing: from Queue import Queue self._shutdown_server.clear() self._queue = Queue() # Put the initial token in the Queue self._queue.put(True) server_socket = self._build_server_socket() if use_multiprocessing: for i in range(5): p = self._start_new_worker_process(server_socket) self._processes.append(p) while not self._shutdown_server.isSet(): time.sleep(1) for process in self._processes: process.join() else: forked_child(*self._get_child_args(server_socket)) server_socket.close() self._queue = None def shutdown_server(self, timeout_seconds=None): """This method notifies the server that it should stop listening for new messages and shut down itself. If timeout_seconds was given, the method will block for this many seconds at most.""" self._queue.put(None) self._shutdown_server.set() # TODO: Looks like we're quitting too fast here. PKE:H ## pymta/api.py# -*- coding: UTF-8 -*- """This package contains classes which from which you can derive your own classes easily to customize the behavior of your MTA. Everything in here is considered part of the public API which should be as stable as possible.""" # # The MIT License # # Copyright (c) 2008-2009 Felix Schwarz # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. __all__ = ['IAuthenticator', 'IMessageDeliverer', 'IMTAPolicy', 'PolicyDecision', 'PyMTAException'] class IAuthenticator(object): """Authenticators check if the user’s credentials are actually correct. This may involve some checking against external subsystems (e.g. a database or a LDAP directory).""" def authenticate(self, username, password, peer): """This method is called after the client issued an AUTH PLAIN command and must return a boolean value (True/False).""" raise NotImplementedError class IMessageDeliverer(object): """Deliverers take care of the message routing/delivery after a message was accepted (e.g. put it in a mailbox file, forward it to another server, ...). """ def new_message_accepted(self, msg): """This method is called when a new message was accepted by the server. Now the MTA is then in charge of delivering the message to the specified recipients. Please note that you can not reject the message anymore at this stage (if there are problems you must generate a non-delivery report aka bounce). There will be one deliverer instance per client connection so this method may does not have to be thread-safe. However this method may get called multiple times when the client transmits more than one message for the same connection.""" raise NotImplementedError class PolicyDecision(object): def __init__(self, decision=True, reply=None): self._decision = decision self._reply = reply self._close_connection_before_response = False self._close_connection_after_response = False def close_connection_before_response(self): """Return True if the server should close the client connection without any further communication.""" return self._close_connection_before_response def close_connection_after_response(self): """Return True if the server should close the client connection after it sent the given response.""" return self._close_connection_after_response def is_command_acceptable(self): return self._decision def use_custom_reply(self): return (self._reply is not None) def get_custom_reply(self): if not self.use_custom_reply(): raise ValueError('No custom reply set.') return self._reply class IMTAPolicy(object): """Policies can change with behavior of an MTA dynamically (e.g. don't allow relaying unless the client is located within the trusted company network, enable authentication only for some connections). In established MTAs like Exim and Postfix it's a very important task for every system administrator to configure the message acceptance policies which are normally part of the configuration file. A policy does not change the SMTP implementation itself (the state machine) but can send out custom replies to the client. A policy doesn't have to care if the commands were given in the correct order (the state machines will take care of that). The only thing is that the message object passed into many policy methods does not contain all data at certain stages (e.g. accept_mail_from can not access the recipients list because that was not submitted yet). 'IMTAPolicy' provides a very permissive policy (all commands are accepted) from which you can derive custom policies. Its methods are usually named 'accept_'. Every method in the 'IMTAPolicy' interface can return either a single boolean value (True/False) or a tuple. A boolean value specifies if the command should be accepted. The caller is responsible for sending the actual default replies. Alternatively a policy can choose to return a tuple to have more control over the reply sent to the client: (decision, (reply code, reply message)). The decision is the boolean known from the last paragraph. The reply code is an integer which should a be a valid SMTP code. reply message is either a basestring with a custom message or an iterable of basestrings (in case a a multi-line reply is sent). Last but not least a PolicyDecision can be returned which embodies the decision as well as (optionally) a custom reply. The reply has the same format as described in the paragraph before. The PolicyDecision can ask the server to close the connection unconditionally after or even before sending the response to the client (in the latter case no response will be sent). """ def accept_new_connection(self, peer): """This method is called directly after a new connection is received. The policy can decide if the given peer is allowed to connect to the SMTP server. If it declines, the connection will be closed immediately.""" return True def max_message_size(self, peer): """Return the maximum size (in bytes) for messages from this peer. When this method returns an integer, there pymta will check the actual message size after the message was received (before the accept_msgdata method is called) and will respond with the appropriate error message if necessary. If you return None, no size limit will be enforced by pymta (however you can always reject a message using accept_msgdata().""" return None def ehlo_lines(self, peer): """Return an iterable for SMTP extensions to advertise after EHLO. By default support for SMTP SIZE extension will be announced if you set a max message size.""" max_size = self.max_message_size(peer) if max_size != None: return ('SIZE %d' % max_size,) return () def accept_helo(self, helo_string, message): """Decides if the HELO command with the given helo_name should be accepted.""" return True def accept_ehlo(self, ehlo_string, message): """Decides if the EHLO command with the given helo_name should be accepted.""" return True def accept_auth_plain(self, username, password, message): """Decides if AUTH plain should be allowed for this client. Please note that username and password are not verified before, the authenticator will check them after the policy allowed this command. The method must not return a response by itself in case it accepts the AUTH PLAIN command!""" return True def accept_from(self, sender, message): "Decides if the sender of this message (MAIL FROM) should be accepted." return True def accept_rcpt_to(self, new_recipient, message): """Decides if recipient of this message (RCPT TO) should be accepted. If a message should be delivered to multiple recipients this method is called for every recipient.""" return True def accept_data(self, message): """Decides if we allow the client to start a message transfer (the actual message contents will be transferred after this method allowed it).""" return True def accept_msgdata(self, msgdata, message): """This method actually matches no real SMTP command. It is called after a message was transferred completely and this is the last check before the SMTP server takes the responsibility of transferring it to the recipients.""" return True class PyMTAException(Exception): """Base class for all exceptions used in pymta.""" pass PKE:YYpymta/test_util.py# -*- coding: UTF-8 -*- """ This module contains some classes which are probably useful for writing unit tests using pymta: - MTAThread enables you to run a MTA in a separate thread so that you can test interaction with an in-process MTA. - DebuggingMTA provides a very simple MTA which just collects all incoming messages so that you can examine then afterwards. """ # The MIT License # # Copyright (c) 2008 Felix Schwarz # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. from Queue import Queue import random import socket import threading import time from unittest import TestCase from pymta.api import IMessageDeliverer, IMTAPolicy from pymta.mta import PythonMTA __all__ = ['BlackholeDeliverer', 'DebuggingMTA', 'MTAThread', 'SMTPTestCase'] class BlackholeDeliverer(IMessageDeliverer): """BlackholeDeliverer just stores all received messages in memory in the class attribute 'received_messages' (which implements a Queue-like interface) so that you can examine the received messages later. """ received_messages = None def __init__(self): super(BlackholeDeliverer, self).__init__() self.__class__.received_messages = Queue() def new_message_accepted(self, msg): self.__class__.received_messages.put(msg) class DebuggingMTA(PythonMTA): """DebuggingMTA is a very simple implementation of PythonMTA which just collects all incoming messages so that you can examine then afterwards.""" def __init__(self, *args, **kwargs): PythonMTA.__init__(self, *args, **kwargs) self.queue = Queue() def serve_forever(self): return super(DebuggingMTA, self).serve_forever(use_multiprocessing=False) class MTAThread(threading.Thread): """This class runs a PythonMTA in a separate thread which is helpful for unit testing. Attention: Do not use this class together with multiprocessing! http://www.viraj.org/b2evolution/blogs/index.php/2007/02/10/threads_and_fork_a_bad_idea """ def __init__(self, server): threading.Thread.__init__(self) self.server = server def run(self): """Create a new thread which runs the server until stop() is called.""" self.server.serve_forever() def stop(self, timeout_seconds=5.0): """Stop the mail sink and shut down this thread. timeout_seconds specifies how long the caller should wait for the mailsink server to close down (default: 5 seconds). If the server did not stop in time, a warning message is printed.""" self.server.shutdown_server() threading.Thread.join(self, timeout=timeout_seconds) if self.isAlive(): print "WARNING: Thread still alive. Timeout while waiting for " + \ "termination!" class SMTPTestCase(TestCase): """The SMTPTestCase is a unittest.TestCase and provides you with a running MTA listening on 'localhost:[8000-40000]' which you can use in your tests. No messages will be delivered to the outside world because the MTA configured by default uses the BlackholeDeliverer. Please make sure that you call the super method for setUp and tearDown.""" def setUp(self): super(SMTPTestCase, self).setUp() self.hostname = 'localhost' self.listen_port = random.randint(8000, 40000) self.mta_thread = None self.init_mta() def build_mta(self, hostname, listen_port, deliverer, policy_class=None): """Return a PythonMTA instance which is configured according to your needs.""" return DebuggingMTA(hostname, listen_port, deliverer, policy_class=policy_class) def init_mta(self, policy_class=IMTAPolicy): """Starts the MTA in a separate thread with a BlackholeDeliver. This method also ensures that the MTA is really listening on the specified port.""" self.stop_mta() self.deliverer = BlackholeDeliverer self.mta = self.build_mta(self.hostname, self.listen_port, self.deliverer, policy_class) self.mta_thread = MTAThread(self.mta) self.mta_thread.start() self._try_to_connect_to_mta(self.hostname, self.listen_port) def _try_to_connect_to_mta(self, host, port): tries = 0 while tries < 10: try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((host, port)) except socket.error: tries += 1 time.sleep(0.1) else: return assert False, 'MTA not reachable' def stop_mta(self): if self.mta_thread is not None: self.mta_thread.stop() self.mta_thread = None def tearDown(self): """Stops the MTA thread.""" self.stop_mta() super(SMTPTestCase, self).tearDown() def get_received_messages(self): """Return a list of received messages which are stored in the BlackholeDeliverer.""" return self.deliverer.received_messages PKE:^``pymta/compat.py# -*- coding: UTF-8 -*- # # The MIT License # # Copyright (c) 2009 Felix Schwarz # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """This module provides a unified view on certain symbols that are not present for all versions of Python.""" __all__ = ['set'] try: set = set except NameError: from sets import Set as set PKE:m4m4pymta/command_parser.py# -*- coding: UTF-8 -*- # # The MIT License # # Copyright (c) 2008-2009 Felix Schwarz # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. from Queue import Empty import re import socket from repoze.workflow.statemachine import StateMachine, StateMachineError from pymta.exceptions import SMTPViolationError from pymta.session import SMTPSession __all__ = ['SMTPCommandParser'] class ParserImplementation(object): """The SMTPCommandParser needs a connected socket to operate. This is very inconvenient for testing therefore all 'interesting' functionality is moved in this class which is easily testable.""" def __init__(self, allowed_commands): self._allowed_commands = allowed_commands regex_string = '^(%s)(?: |:)\s*(.*)$' % '|'.join(allowed_commands) self.parse_regex = re.compile(regex_string, re.IGNORECASE) def parse(self, command): assert isinstance(command, basestring) parameter = None match = self.parse_regex.search(command) if match != None: command = match.group(1) parameter = match.group(2).strip() return command, parameter class SMTPCommandParser(object): """This class is a tiny abstraction layer above the real communication with the client. It knows about the two basic SMTP modes (sending commands vs. sending message data) and can assemble SMTP-like replies (Code-Message) in a convenient manner. However all handling of SMTP commands will take place in an upper layer. The original 'SMTPChannel' class from Python.org handled all communication with asynchat, implemented a extremely simple state machine and processed the data. Implementing hooks in that design (or adding fine-grained policies) was not possible at all with the previous design.""" LINE_TERMINATOR = '\r\n' def __init__(self, channel, remote_ip_string, remote_port, deliverer, policy=None, authenticator=None): self._channel = channel self.data = None self.terminator = self.LINE_TERMINATOR self._build_state_machine() self.session = SMTPSession(command_parser=self, deliverer=deliverer, policy=policy, authenticator=authenticator) allowed_commands = self.session.get_all_allowed_internal_commands() self._parser = ParserImplementation(allowed_commands) self.session.new_connection(remote_ip_string, remote_port) def _build_state_machine(self): def _command_completed(from_state, to_state, smtp_command, instance): self.data = None def _start_receiving_message(from_state, to_state, smtp_command, instance): self.terminator = '%s.%s' % (self.LINE_TERMINATOR, self.LINE_TERMINATOR) self.data = [] def _finished_receiving_message(from_state, to_state, smtp_command, instance): self.terminator = self.LINE_TERMINATOR self.data = None self.state = StateMachine('_state', initial_state='commands') self._state = 'commands' self.state.add('commands', 'COMMAND', 'commands', _command_completed) self.state.add('commands', 'DATA', 'data', _start_receiving_message) self.state.add('data', 'COMMAND', 'commands', _finished_receiving_message) def primary_hostname(self): # TODO: This should go into a config object! return socket.getfqdn() primary_hostname = property(primary_hostname) # ------------------------------------------------------------------------- # Communication helper methods def multiline_push(self, code, lines): """Send a multi-message to the peer (using the correct SMTP line terminators (usually only called from the SMTPSession).""" for i, line in enumerate(lines[:-1]): answer = '%s-%s' % (str(code), str(line)) self.push(answer) self.push(code, lines[-1]) def push(self, code, msg=None): """Send a message to the peer (using the correct SMTP line terminators (usually only called from the SMTPSession).""" if msg == None: msg = code else: msg = '%s %s' % (str(code), msg) if not msg.endswith(self.LINE_TERMINATOR): msg += self.LINE_TERMINATOR self._channel.write(msg) def collect_incoming_data(self, data): if self._state == 'commands': self.data = data elif data != '.': # In DATA mode '.' on a line by itself signals the 'end of message'. # So the dot is only needed in protocol itself but we don't add it # to our payload. self.data.append(data) def input_exceeds_limits(self): """Called from the underlying transport layer if the client input exceeded the configured maximum message size.""" self.session.input_exceeds_limits() self.switch_to_command_mode() def set_maximum_message_size(self, max_size): """Set the maximum allowed size (in bytes) of a command/message in the underlying transport layer so that big messages are not stored in memory before they are rejected.""" self._channel.set_max_input_size(max_size) def switch_to_command_mode(self): """Called from the SMTPSession when a message was received and the client is expected to send single commands again.""" self.state.execute(self, 'COMMAND') def switch_to_data_mode(self): """Called from the SMTPSession when the client should start transfering the actual message data.""" self.state.execute(self, 'DATA') def _assemble_msgdata(self, input_data): """Uses the input data to recover the original payload (includes transparency support as specified in RFC 821, Section 4.5.2).""" lines = [] for part in input_data: for line in part.split(self.LINE_TERMINATOR): if line.startswith('.'): line = line[1:] lines.append(line) return '\n'.join(lines) def get_terminator(self): return self._terminator def set_terminator(self, terminator): self._terminator = terminator terminator = property(get_terminator, set_terminator) def found_terminator(self): input_data = self.data if self._state == 'commands': command, parameter = self._parser.parse(input_data) self.session.handle_input(command, parameter) else: assert isinstance(input_data, list) msgdata = self._assemble_msgdata(input_data) self.session.handle_input('MSGDATA', msgdata) def close_when_done(self): self._channel.close() class ClientDisconnectedError(SMTPViolationError): """Raised when the SMTP client closed the connection unexpectedly.""" pass class WorkerProcess(object): """The WorkerProcess handles the real communication. with the client. It does not know anything about the SMTP protocol (besides the fact that it is a line-based protocol).""" def __init__(self, queue, server_socket, deliverer_class, policy_class=None, authenticator_class=None): self._queue = queue self._server_socket = server_socket self._deliverer = self._get_instance_from_class(deliverer_class) self._policy = self._get_instance_from_class(policy_class) self._authenticator = self._get_instance_from_class(authenticator_class) self._connection = None self._chatter = None self._max_size = None self._input_too_big = False def _get_instance_from_class(self, class_reference): instance = None if class_reference != None: instance = class_reference() return instance def _wait_for_connection(self): while True: # We want to check periodically if we need to abort try: connection, remote_address = self._server_socket.accept() break except socket.timeout: try: new_token = self._queue.get_nowait() self._queue.put(new_token) if new_token == None: return None except Empty: pass connection.settimeout(socket.getdefaulttimeout()) return connection, remote_address def _get_token_with_timeout(self, seconds): # wait at max 1 second for the token so that we can abort the whole # process in a reasonable time token = None while True: try: token = self._queue.get(timeout=seconds) break except Empty: pass return token def run(self): token = None def have_token(): return (token != None) try: while True: token = self._get_token_with_timeout(1) if not have_token(): break assert token == True connection_info = self._wait_for_connection() self._queue.put(token) token = None if connection_info == None: break self.chat_with_peer(connection_info) finally: if have_token(): # If we possess the token, put it back in the queue so other can # continue doing stuff. self._queue.put(True) def set_max_input_size(self, max_size): """Set the maximum size of client input (in bytes) before the input is discarded. When the client finished transmitting a message which was too big, the 'input_exceeds_limits' method is called on the chatter which is responsible for notifying the peer. Setting a maximum size of None disables any size-checking.""" self._max_size = max_size def chat_with_peer(self, connection_info): self._connection, (remote_ip_string, remote_port) = connection_info self._chatter = SMTPCommandParser(self, remote_ip_string, remote_port, self._deliverer, self._policy, self._authenticator) while self.is_connected(): try: data = self.readline() if not self._input_too_big: self._chatter.collect_incoming_data(data) self._chatter.found_terminator() else: self._chatter.input_exceeds_limits() self._input_too_big = False except ClientDisconnectedError: if self.is_connected(): self._connection.close() self._connection = None def is_connected(self): return (self._connection != None) def readline(self): """Read as much data as possible until a line terminator was received.""" assert self.is_connected() data = '' self._input_too_big = False while True: try: more_data = self._connection.recv(4096) except socket.error: raise ClientDisconnectedError() if more_data == '': raise ClientDisconnectedError() elif more_data.endswith(self._chatter.terminator): data += more_data[:-len(self._chatter.terminator)] break elif not self._input_too_big: data += more_data if (self._max_size is not None) and (len(data) > self._max_size): self._input_too_big = True data = '' return data def close(self): """Closes the connection to the client.""" assert self.is_connected() self._connection.close() self._connection = None def write(self, data): """Sends some data to the client.""" assert self.is_connected() self._connection.send(data) PKEF:- {p  pymta/__init__.pyc; DIc@s'dkTdkTdkTdkTdkTdS((s*N(s pymta.apis pymta.models pymta.mtaspymta.command_parsers pymta.session(((s.build/bdist.linux-x86_64/egg/pymta/__init__.pys?sPKEF:݊YKKpymta/model.pyc; DIc@s<ddgZdefdYZdefdYZdS(sMessagesPeercBs tZeeeeedZRS(NcCsu||_||_||_|tjo g}n%t|ttf o |g}n||_||_ ||_ dS(N( speersselfs smtp_helos smtp_fromssmtp_tosNones isinstancesliststuplesmsg_datasusername(sselfspeers smtp_helos smtp_fromssmtp_tosmsg_datasusername((s+build/bdist.linux-x86_64/egg/pymta/model.pys__init__s        (s__name__s __module__sNones__init__(((s+build/bdist.linux-x86_64/egg/pymta/model.pysMessagescBstZdZdZRS(NcCs||_||_dS(N(s remote_ipsselfs remote_port(sselfs remote_ips remote_port((s+build/bdist.linux-x86_64/egg/pymta/model.pys__init__.s cCs!d|ii|i|ifSdS(Ns %s(%s, %s)(sselfs __class__s__name__s remote_ips remote_port(sself((s+build/bdist.linux-x86_64/egg/pymta/model.pys__repr__2s(s__name__s __module__s__init__s__repr__(((s+build/bdist.linux-x86_64/egg/pymta/model.pysPeer-s N(s__all__sobjectsMessagesPeer(sPeersMessages__all__((s+build/bdist.linux-x86_64/egg/pymta/model.pys?s PKEF:Iztjtjpymta/session.pyc; ,Jc@sdkZdkZdkZdklZlZdklZdkl Z l Z dk l Z l Z dgZdZde fdYZdefd YZdS( N(s StateMachinesStateMachineError(sset(sInvalidParametersErrorsSMTPViolationError(sMessagesPeers SMTPSessionse^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$s PolicyDenialcBstZRS(N(s__name__s __module__(((s-build/bdist.linux-x86_64/egg/pymta/session.pys PolicyDenial,scBstZdZeedZdZedZdZdZ dZ dZ dZ d Z d Zd Zd Zd ZdZdZdZdZedZdZdZdZedZdZdZdZdZdZdZ dZ!dZ"dZ#d Z$d!Z%d"Z&d#Z'd$Z(d%Z)d&Z*d'Z+d(Z,d)Z-d*Z.d+Z/d,Z0d-Z1d.Z2RS(/sThe SMTPSession processes all input data which were extracted from sockets previously. The idea behind this is that this class only knows about different SMTP commands and does not have to know things like command mode and data mode. The protocol parser will create a new session instance for every new connection so this class does not have to be thread-safe. cCsn||_||_||_||_t |_ t |_ t |_t |_titti|_|idS(N(scommand_parsersselfs_command_parsers deliverers _delivererspolicys_policys authenticators_authenticatorsNones_command_argumentssFalses _close_connection_after_responsesTrues _is_connecteds_messagesrescompiles regex_strings IGNORECASEshostname_regexs_build_state_machine(sselfscommand_parsers delivererspolicys authenticator((s-build/bdist.linux-x86_64/egg/pymta/session.pys__init__:s        cCs&|i}|ii||||dS(N(sselfs_dispatch_commandsshandler_functionsstatesadds from_states smtp_commandsto_state(sselfs from_states smtp_commandsto_stateshandler_function((s-build/bdist.linux-x86_64/egg/pymta/session.pys _add_stateLs cCst}xw|iiD]i}|d}|ii|}|d}|dgjo/|p |djo|i ||fqqqW|SdS(Niisnewsfinished( ssetsstatessselfsstateskeys command_names new_states state_namesincluding_quitsadd(sselfsincluding_quits new_states state_namesstatesskeys command_name((s-build/bdist.linux-x86_64/egg/pymta/session.pys_get_all_real_statesPs    cCsXt}xD|idtD]0\}}|ddgjo|i|qqW|SdS(sReturns an iterable which includes all allowed commands. This does not mean that a specific command from the result is executable right now in this session state (or that it can be executed at all in this connection). Please note that the returned values are /internal/ commands, not SMTP commands (use get_all_allowed_smtp_commands for that) so there will be 'MAIL FROM' instead of 'MAIL'.sincluding_quitsGREETsMSGDATAN(ssetsstatessselfs_get_all_real_statessTrues command_namesinvalidsadd(sselfsinvalidsstatess command_name((s-build/bdist.linux-x86_64/egg/pymta/session.pys!get_all_allowed_internal_commands[s  cCsHt}x4|iD]&}|idd}|i|qW|SdS(Ns i(ssetsstatessselfs!get_all_allowed_internal_commandss command_namessplitsadd(sselfsstatess command_name((s-build/bdist.linux-x86_64/egg/pymta/session.pysget_all_allowed_smtp_commandsjs   cCsXxQ|iD]C\}}|djo|i|d|q |i|ddq WdS(NsnewsRSETs initialized(sselfs_get_all_real_statess command_names state_names _add_state(sselfs state_names command_name((s-build/bdist.linux-x86_64/egg/pymta/session.pys_add_rset_transitionsqs   cCst}xR|iiD]D}|ii|}|d}|ddgjo|i|qqWxG|D]?}|i|d||i|d||i|ddqeWdS(sHELP, NOOP and QUIT should be possible from everywhere so we need to add these transitions to all states configured so far.isnewsfinishedsNOOPsHELPsQUITN( ssetsstatessselfsstateskeys new_states state_namesadds _add_state(sselfs new_statesstatessstateskeys state_name((s-build/bdist.linux-x86_64/egg/pymta/session.pys#_add_help_noop_and_quit_transitionsxs   cCs4tddd|_|iddd|iddd|iddd |idd d |id d d |id d d |id d d |id dd|iddd|iddd|iddd|i|igi}|iiD]\}}||q~|_ dS(Ns_states initial_statesnewsGREETsgreetedsHELOs initializedsEHLOsesmtp_initializeds MAIL FROMs sender_knowns AUTH PLAINs authenticatedsRCPT TOsrecipient_knownsDATAsreceiving_messagesMSGDATA( s StateMachinesselfsstates _add_states#_add_help_noop_and_quit_transitionss_add_rset_transitionssappends_[1]sstatess from_statescommandsvalid_commands(sselfs_[1]s from_statescommand((s-build/bdist.linux-x86_64/egg/pymta/session.pys_build_state_machines  cCsog}|itjo|idn|itjo#|i|ii|ii n|id|SdS(s4Return the capabilities to be advertised after EHLO.s AUTH PLAINsHELPN( slinessselfs_authenticatorsNonesappends_policysextends ehlo_liness_messagespeer(sselfslines((s-build/bdist.linux-x86_64/egg/pymta/session.pysget_ehlo_liness# cCs |i}|ii|dS(sSet the maximum allowed message in the underlying layer so that big messages are not hold in memory before they are rejected.N(sselfs!_get_max_message_size_from_policysmax_message_sizes_command_parsersset_maximum_message_size(sselfsmax_message_size((s-build/bdist.linux-x86_64/egg/pymta/session.pys_set_size_restrictionss cCsud|iidd}yt||}Wn8tj o,d}|||fGH|i ddn X|dS(sThis method dispatches a SMTP command to the appropriate handler method. It is called after a new command was received and a valid transition was found.ssmtp_%ss s_s=No handler for %s though transition is defined (no method %s)is/Temporary Local Problem: Please come back laterN( s smtp_commandslowersreplacesname_handler_methodsgetattrsselfshandler_methodsAttributeErrorsbase_msgsreply(sselfs from_statesto_states smtp_commandsobsbase_msgsname_handler_methodshandler_method((s-build/bdist.linux-x86_64/egg/pymta/session.pys_dispatch_commandsscCs|ttgjSdS(N(sdecisionsTruesNone(sselfsdecision((s-build/bdist.linux-x86_64/egg/pymta/session.pys_evaluate_decisionscCst|t SdS(N(s isinstances reply_messages basestring(sselfs reply_message((s-build/bdist.linux-x86_64/egg/pymta/session.pys_is_multiline_replyscCsD|\}}|i|o|i||n|i||dS(N(sreplyscodescustom_responsesselfs_is_multiline_replysmultiline_reply(sselfsreplyscodescustom_response((s-build/bdist.linux-x86_64/egg/pymta/session.pys_send_custom_responses cCs|i|i}|i}|io|it }n|io|i |i n|i o|i n||fSdS(N(sselfs_evaluate_decisionsresultsis_command_acceptablesdecisionsuse_custom_replys response_sents close_connection_before_responsesclose_connectionsTrues_send_custom_responsesget_custom_replysclose_connection_after_responses&please_close_connection_after_response(sselfsresults response_sentsdecision((s-build/bdist.linux-x86_64/egg/pymta/session.pys_evaluate_policydecision_results      cGs|itj ot|i|}||}|tt tgjo|i |t fSngt |do|i |SnFt |djo2|i |d}|i|d|tfSntdntt fSdS(Nsis_command_acceptableiiisUnknown policy response(sselfs_policysNonesgetattrsacl_namesdecidersargssresultsTruesFalses_evaluate_decisionshasattrs_evaluate_policydecision_resultslensdecisions_send_custom_responses ValueError(sselfsacl_namesargssdecidersresultsdecision((s-build/bdist.linux-x86_64/egg/pymta/session.pys is_alloweds cCsd|_tt|||_|id|ii\}}|o'| o|i dn|i n'| o|i ddn|idS(sVThis method is called when a new SMTP session is opened. [PUBLIC API] snewsaccept_new_connectionsgreeti*sSMTP service not availableN(sselfs_statesMessagesPeers remote_ips remote_ports_messages is_allowedspeersdecisions response_sents handle_inputs_set_size_restrictionssreplysclose_connection(sselfs remote_ips remote_portsdecisions response_sent((s-build/bdist.linux-x86_64/egg/pymta/session.pysnew_connections  cCsn||_|it|i}z y|ii ||Wnt j o||i jo|i dd|qAd|}|ii|}t|djo"|d|7}|i d|qAnxtj o2}|i od|}|i d|qAn;tj o.}|i o|i |i|iqAnXWd |io|int|_Xd S( sXProcesses the given SMTP command with the (optional data). [PUBLIC API] isunrecognized command "%s"s Command "%s" is not allowed hereis, expected on of %sis$Syntactically invalid %s argument(s)iN(sdatasselfs_command_argumentss&please_close_connection_after_responsesFalses smtp_commandsupperscommandsstatesexecutesStateMachineErrorsvalid_commandssreplysmsgs transitionssallowed_transitionsslensInvalidParametersErrorses response_sents PolicyDenialscodes reply_texts&should_close_connection_after_responsesclose_connectionsNone(sselfs smtp_commandsdatascommandsmsgsallowed_transitionsse((s-build/bdist.linux-x86_64/egg/pymta/session.pys handle_inputs4       # cCs|idddS(sNCalled when the client sent a message that exceeded the maximum size.i(s*message exceeds fixed maximum message sizeN(sselfsreply(sself((s-build/bdist.linux-x86_64/egg/pymta/session.pysinput_exceeds_limits!scCs|ii||dS(sThis method returns a message to the client (actually the session object is responsible of actually pushing the bits).N(sselfs_command_parserspushscodestext(sselfscodestext((s-build/bdist.linux-x86_64/egg/pymta/session.pysreply&scCs|ii||dS(sThis method returns a message with multiple lines to the client (actually the session object is responsible of actually pushing the bits).N(sselfs_command_parsersmultiline_pushscodes responses(sselfscodes responses((s-build/bdist.linux-x86_64/egg/pymta/session.pysmultiline_reply+scCs$|tjo t}n||_dS(N(svaluesNonesTruesselfs _close_connection_after_response(sselfsvalue((s-build/bdist.linux-x86_64/egg/pymta/session.pys&please_close_connection_after_response1s  cCs |iSdS(N(sselfs _close_connection_after_response(sself((s-build/bdist.linux-x86_64/egg/pymta/session.pys&should_close_connection_after_response6scCs(|iot|_|iindS(sCRequest a connection close from the SMTP session handling instance.N(sselfs _is_connectedsFalses_command_parsersclose_when_done(sself((s-build/bdist.linux-x86_64/egg/pymta/session.pysclose_connection9s  cCs9|ii}d||iiif}|id|dS(ssThis method handles not a real smtp command. It is called when a new connection was accepted by the server.s %s Hello %siN(sselfs_command_parsersprimary_hostnames_messagespeers remote_ips reply_textsreply(sselfs reply_textsprimary_hostname((s-build/bdist.linux-x86_64/egg/pymta/session.pys smtp_greetCs cCs7|ii}d|}|id||iidS(Ns%s closing connectioni(sselfs_command_parsersprimary_hostnames reply_textsreplysclose_when_done(sselfs reply_textsprimary_hostname((s-build/bdist.linux-x86_64/egg/pymta/session.pys smtp_quitLs  cCs|idddS(NisOK(sselfsreply(sself((s-build/bdist.linux-x86_64/egg/pymta/session.pys smtp_noopRscCs/|i}|idddi|fdS(NisCommands supporteds (sselfsget_all_allowed_smtp_commandssstatessmultiline_replysjoin(sselfsstates((s-build/bdist.linux-x86_64/egg/pymta/session.pys smtp_helpUs cCs8||i_| o |ii}|id|ndS(Ni(s helo_stringsselfs_messages smtp_helos response_sents_command_parsersprimary_hostnamesreply(sselfs helo_strings response_sentsprimary_hostname((s-build/bdist.linux-x86_64/egg/pymta/session.pys_reply_to_heloYs  cCs|ipdi}|ii|tj}| ot|nO|i |||i \}}|o|||n| ot|ndS(Ns(sselfs_command_argumentssstrips helo_stringshostname_regexsmatchsNonesvalid_hostname_syntaxsInvalidParametersErrors is_allowedspolicy_methodnames_messagesdecisions response_sents reply_methods PolicyDenial(sselfspolicy_methodnames reply_methods helo_strings response_sentsvalid_hostname_syntaxsdecision((s-build/bdist.linux-x86_64/egg/pymta/session.pys_process_helo_or_ehlo_scCs|id|idS(Ns accept_helo(sselfs_process_helo_or_ehlos_reply_to_helo(sself((s-build/bdist.linux-x86_64/egg/pymta/session.pys smtp_helokscCsK||i_| o3|ii}|g|i}|i d|ndS(Ni( s helo_stringsselfs_messages smtp_helos response_sents_command_parsersprimary_hostnamesget_ehlo_linesslinessmultiline_reply(sselfs helo_strings response_sentslinessprimary_hostname((s-build/bdist.linux-x86_64/egg/pymta/session.pys_reply_to_ehlons   cCs|id|idS(Ns accept_ehlo(sselfs_process_helo_or_ehlos_reply_to_ehlo(sself((s-build/bdist.linux-x86_64/egg/pymta/session.pys smtp_ehlouscCs|id|||i\}}| ot|n|tjpt |i t jo#|i ddt dtn|i i|||ii}|o ||i_|i ddn|i dddS(Nsaccept_auth_plainisAUTH not availables response_sentisAuthentication successfulsBad username or password(sselfs is_allowedsusernamespasswords_messagesdecisions response_sents PolicyDenialsFalsesAssertionErrors_authenticatorsNonesreplysInvalidParametersErrorsTrues authenticatespeerscredentials_correct(sselfsusernamespasswords response_sentsdecisionscredentials_correct((s-build/bdist.linux-x86_64/egg/pymta/session.pys_check_passwordxs! cCs|i}yti|}Wn"tij ot|n_Xt i d|}|o8|i d|i df\}}|i||n t|dS(Ns^[^]*([^]*)([^]*)$ii(sselfs_command_argumentssbase64_credentialssbase64s decodestrings credentialssbinasciisErrorsInvalidParametersErrorsressearchsmatchsgroupsusernamespasswords_check_password(sselfsusernamesbase64_credentialss credentialsspasswordsmatch((s-build/bdist.linux-x86_64/egg/pymta/session.pyssmtp_auth_plains $c Cs|}h}tid}|i|}|o|id}|id}|t j o^x[ti d|D]C}d|jo&|i dd\} }||| (?:\s*(\S+)\s*)*\s*$iis\s+s=(sdatassenders extensionssrescompiles verb_regexssearchsmatchsgroupsextension_stringsNonessplits extensionsnames parametersTrue( sselfsdatassenders extensions parameters extensionss verb_regexsextension_stringsmatchsname((s-build/bdist.linux-x86_64/egg/pymta/session.pys_split_mail_from_parameters  cCsud|jodt|d}|i}|tjo7||jo&|iddtddt qmqqndS(Nssizei(s*message exceeds fixed maximum message sizes MAIL FROMs response_sent( s extensionssintsannounced_sizesselfs!_get_max_message_size_from_policysmax_message_sizesNonesreplysInvalidParametersErrorsTrue(sselfs extensionssannounced_sizesmax_message_size((s-build/bdist.linux-x86_64/egg/pymta/session.pys_check_mail_extensionss    cCs|i}|i|\}}|iddgj}|o|i|n:t |djo&|i ddt ddt n|i d||i\}}|o,||i_| o|i d d qn| ot|ndS( Nsesmtp_initializeds authenticatediis)No SMTP extensions allowed for plain SMTPsMAILs response_sents accept_fromisOK(sselfs_command_argumentssdatas_split_mail_from_parameterssenders extensionss_states uses_esmtps_check_mail_extensionsslensreplysInvalidParametersErrorsTrues is_alloweds_messagesdecisions response_sents smtp_froms PolicyDenial(sselfssendersdecisions extensionss response_sentsdatas uses_esmtp((s-build/bdist.linux-x86_64/egg/pymta/session.pyssmtp_mail_froms  cCs:tid|}|o|idSnt|dS(Ns ^?$i(sressearchs parametersmatchsgroupsInvalidParametersError(sselfs parametersmatch((s-build/bdist.linux-x86_64/egg/pymta/session.pys_extract_email_addressscCs|i|i}|id||i\}}|o3|iii || o|i ddqn| ot |ddndS(Nsaccept_rcpt_toisOKi&srelay not permitted( sselfs_extract_email_addresss_command_argumentss email_addresss is_alloweds_messagesdecisions response_sentssmtp_tosappendsreplys PolicyDenial(sselfs email_addresssdecisions response_sent((s-build/bdist.linux-x86_64/egg/pymta/session.pys smtp_rcpt_toscCsg|id|i\}}|o| o!|ii|iddn| ot|ndS(Ns accept_dataibs2Enter message, ending with "." on a line by itself( sselfs is_alloweds_messagesdecisions response_sents_command_parsersswitch_to_data_modesreplys PolicyDenial(sselfsdecisions response_sent((s-build/bdist.linux-x86_64/egg/pymta/session.pys smtp_datas  cCsMt}|itj o|iitj o|ii|ii}n|SdS(N(sNonesmax_message_sizesselfs_policys_messagespeer(sselfsmax_message_size((s-build/bdist.linux-x86_64/egg/pymta/session.pys!_get_max_message_size_from_policys#cCs\|i}|tj o?t|t|j}|od}t t d|qXndS(Ns*message exceeds fixed maximum message sizei(( sselfs!_get_max_message_size_from_policysmax_message_sizesNoneslensmsg_datasints msg_too_bigsmsgs PolicyDenialsFalse(sselfsmsg_datas msg_too_bigsmax_message_sizesmsg((s-build/bdist.linux-x86_64/egg/pymta/session.pys_check_size_restrictionss   cCsJ|ii}tdt|i|id|iid|ii}|SdS(Nspeers smtp_helosusername( sselfs_messagespeersMessagesPeers remote_ips remote_ports smtp_helosusernames new_message(sselfsmsgs new_messagespeer((s-build/bdist.linux-x86_64/egg/pymta/session.pys_copy_basic_settingss   cCs|i}|ii|i||id||i\}}|oZ||i_|i |i}|i i |i| o|iddn||_n| ot|ddndS(sThis method handles not a real smtp command. It is called when the whole message was received (multi-line DATA command is completed).saccept_msgdataisOKi&s!Message content is not acceptableN(sselfs_command_argumentssmsg_datas_command_parsersswitch_to_command_modes_check_size_restrictionss is_alloweds_messagesdecisions response_sents_copy_basic_settingss new_messages _deliverersnew_message_acceptedsreplys PolicyDenial(sselfs new_messagesmsg_datas response_sentsdecision((s-build/bdist.linux-x86_64/egg/pymta/session.pys smtp_msgdatas     cCs8td|iid|ii|_|idddS(Nspeers smtp_heloisReset OK(sMessagesselfs_messagespeers smtp_helosreply(sself((s-build/bdist.linux-x86_64/egg/pymta/session.pys smtp_rsets(3s__name__s __module__s__doc__sNones__init__s _add_statesFalses_get_all_real_statess!get_all_allowed_internal_commandssget_all_allowed_smtp_commandss_add_rset_transitionss#_add_help_noop_and_quit_transitionss_build_state_machinesget_ehlo_liness_set_size_restrictionss_dispatch_commandss_evaluate_decisions_is_multiline_replys_send_custom_responses_evaluate_policydecision_results is_allowedsnew_connections handle_inputsinput_exceeds_limitssreplysmultiline_replys&please_close_connection_after_responses&should_close_connection_after_responsesclose_connections smtp_greets smtp_quits smtp_noops smtp_helps_reply_to_helos_process_helo_or_ehlos smtp_helos_reply_to_ehlos smtp_ehlos_check_passwordssmtp_auth_plains_split_mail_from_parameters_check_mail_extensionsssmtp_mail_froms_extract_email_addresss smtp_rcpt_tos smtp_datas!_get_max_message_size_from_policys_check_size_restrictionss_copy_basic_settingss smtp_msgdatas smtp_rset(((s-build/bdist.linux-x86_64/egg/pymta/session.pys SMTPSession0s^                                   (sbase64sbinasciisresrepoze.workflow.statemachines StateMachinesStateMachineErrors pymta.compatssetspymta.exceptionssInvalidParametersErrorsSMTPViolationErrors pymta.modelsMessagesPeers__all__s regex_strings PolicyDenialsobjects SMTPSession(ssets__all__s StateMachinesbase64sbinasciisresSMTPViolationErrors PolicyDenials SMTPSessionsPeersStateMachineErrorsMessages regex_stringsInvalidParametersError((s-build/bdist.linux-x86_64/egg/pymta/session.pys?s     PKEF:rZww pymta/mta.pyc; ,Jc@sXdkZdklZdkZdklZdgZdZdefdYZ dS(N(sEvent(s WorkerProcesss PythonMTAcCs&t|||||}|idS(N(s WorkerProcesssqueues server_socketsdeliverer_classs policy_classsauthenticator_classschildsrun(squeues server_socketsdeliverer_classs policy_classsauthenticator_classschild((s)build/bdist.linux-x86_64/egg/pymta/mta.pys forked_child#s cBsYtZdZeedZdZdZdZdZe dZ edZ RS(sCreate a new MTA which listens for new connections afterwards. local_address is a string containing either the IP oder the DNS host name of the interface on which PythonMTA should listen. deliverer_class, policy_class and authenticator_class are callables which can be used to add custom behavior. Please note that they must be picklable if you use forked worker processes (default). Every new connection gets their own instance of policy_class and authenticator_class so these classes don't have to be thread-safe. If you omit the policy, all syntactically valid SMTP commands are accepted. If there is no authenticator specified, authentication will not be available.cCsO||_||_||_||_||_ t |_ g|_ t|_dS(N(s local_addresssselfs_local_addresss bind_ports _bind_portsdeliverer_classs_deliverer_classs policy_classs _policy_classsauthenticator_classs_authenticator_classsNones_queues _processessEvents_shutdown_server(sselfs local_addresss bind_portsdeliverer_classs policy_classsauthenticator_class((s)build/bdist.linux-x86_64/egg/pymta/mta.pys__init__8s       cCsmd}x`|djoRy|i|i|ifWn-tij o|d7}ti dq XPq WdS(Nii if0.10000000000000001( striess server_socketsbindsselfs_local_addresss _bind_portssocketserrorstimessleep(sselfs server_socketstries((s)build/bdist.linux-x86_64/egg/pymta/mta.pys_try_to_bind_to_socketDs  cCs`tititi}|ititid|id|i ||i d|SdS(Nii( ssocketsAF_INETs SOCK_STREAMs server_sockets setsockopts SOL_SOCKETs SO_REUSEADDRs settimeoutsselfs_try_to_bind_to_socketslisten(sselfs server_socket((s)build/bdist.linux-x86_64/egg/pymta/mta.pys_build_server_socketOs    cCs#|i||i|i|ifSdS(N(sselfs_queues server_sockets_deliverer_classs _policy_classs_authenticator_class(sselfs server_socket((s)build/bdist.linux-x86_64/egg/pymta/mta.pys_get_child_args]scCs=dkl}|dtd|i|}|i|SdS(szStart a new child worker process which will listen on the given socket and return a reference to the new process.(sProcessstargetsargsN(smultiprocessingsProcesss forked_childsselfs_get_child_argss server_socketspsstart(sselfs server_socketsProcesssp((s)build/bdist.linux-x86_64/egg/pymta/mta.pys_start_new_worker_processas   cCs5|o1ydkl}Wq8tj o t}q8Xn| odkl}n|ii||_|ii t |i }|o~x3t dD]%}|i|}|ii|qWx#|ii otidqWx2|iD]}|iqWnt|i||it|_dS(N(sQueueii(suse_multiprocessingsmultiprocessingsQueues ImportErrorsFalsesselfs_shutdown_serversclears_queuesputsTrues_build_server_sockets server_socketsrangesis_start_new_worker_processsps _processessappendsisSetstimessleepsprocesssjoins forked_childs_get_child_argssclosesNone(sselfsuse_multiprocessingsprocessspsQueuesis server_socket((s)build/bdist.linux-x86_64/egg/pymta/mta.pys serve_foreveris2      cCs!|iit|iidS(sThis method notifies the server that it should stop listening for new messages and shut down itself. If timeout_seconds was given, the method will block for this many seconds at most.N(sselfs_queuesputsNones_shutdown_serversset(sselfstimeout_seconds((s)build/bdist.linux-x86_64/egg/pymta/mta.pysshutdown_servers( s__name__s __module__s__doc__sNones__init__s_try_to_bind_to_sockets_build_server_sockets_get_child_argss_start_new_worker_processsTrues serve_foreversshutdown_server(((s)build/bdist.linux-x86_64/egg/pymta/mta.pys PythonMTA+s     ( ssockets threadingsEventstimespymta.command_parsers WorkerProcesss__all__s forked_childsobjects PythonMTA(s forked_childssockets__all__s PythonMTAs WorkerProcessstimesEvent((s)build/bdist.linux-x86_64/egg/pymta/mta.pys?s      PK{:pymta/__init__.py# -*- coding: UTF-8 -*- from pymta.api import * from pymta.model import * from pymta.mta import * from pymta.command_parser import * from pymta.session import * PKEF:}QMMpymta/release.pyc; ,Jc@sSdZdZdZdZdZdZdZdZded eZ d Z d Z d S( s Release information about pymta.spymtas0.4.0s%library to build a custom SMTP servers pymta is a library to build a custom SMTP server in Python. This is useful if you want to... * test mail-sending code against a real SMTP server even in your unit tests. * build a custom SMTP server with non-standard behavior without reimplementing the whole SMTP protocol. * have a low-volume SMTP server which can be easily extended using Python Changelog ****************************** 0.4.0 (08.06.2009) ================== - Compatibility fixes for Python 2.3-2.6 - Policies can drop connection to the client before or after the response - CommandParser is more robust against various socket errors - Better infrastructure and documentation to use pymta in third-party tests 0.3.1 (27.02.2009) ================== - Fixed bug which caused hang after unexpected connection drop by client 0.3 (15.02.2009) ================== - Switch to process-based architecture, got rid of asyncore - Support for size-limitations of messages, huge messages will not be stored in memory if they will be rejected anyway (denial of service prevention) - API documentation is now auto-generated - Renamed DefaultMTAPolicy to IMTAPolicy and moved all interfaces to pymta.api - Added the debugging_server as an extremely simple example of a pymta-based server s Felix Schwarzsfelix.schwarz@oss.schwarz.eus/http://www.schwarz.eu/opensource/projects/pymtas]http://www.schwarz.eu/opensource/projects/pymta/download/%(version)s/pymta-%(version)s.tar.gzsversions© 2008-2009 Felix SchwarzsMITN( s__doc__snamesversions descriptionslong_descriptionsauthorsemailsurlsdicts download_urls copyrightslicense( slicensesnames copyrightsauthorsurls download_urlslong_descriptionsversionsemails description((s-build/bdist.linux-x86_64/egg/pymta/release.pys?s!PKE:!AApymta/exceptions.py# -*- coding: UTF-8 -*- # # The MIT License # # Copyright (c) 2008-2009 Felix Schwarz # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. from pymta.api import PyMTAException __all__ = ['InvalidParametersError', 'SMTPViolationError'] class SMTPViolationError(PyMTAException): """Raised when the SMTP client violated the protocol.""" def __init__(self, response_sent=False, code=550, reply_text='Administrative Prohibition', message=None): if message is None: message = '%s %s' % (code, reply_text) PyMTAException.__init__(self, message) self.response_sent = response_sent self.code = code self.reply_text = reply_text class InvalidParametersError(SMTPViolationError): """The SMTP client provided invalid parameters for a SMTP command.""" def __init__(self, parameter=None, *args, **kwargs): # In Python 2.3 Exception is an old-style classes so we can not use super SMTPViolationError.__init__(self, *args, **kwargs) self.parameter = parameter PKEF:YG? pymta/test_util.pyc; ,Jc@sdZdklZdkZdkZdkZdkZdklZdkl Z l Z dk l Z dddd gZ de fd YZde fd YZdeifd YZd efd YZdS(sV This module contains some classes which are probably useful for writing unit tests using pymta: - MTAThread enables you to run a MTA in a separate thread so that you can test interaction with an in-process MTA. - DebuggingMTA provides a very simple MTA which just collects all incoming messages so that you can examine then afterwards. (sQueueN(sTestCase(sIMessageDeliverers IMTAPolicy(s PythonMTAsBlackholeDeliverers DebuggingMTAs MTAThreads SMTPTestCasecBs&tZdZeZdZdZRS(sBlackholeDeliverer just stores all received messages in memory in the class attribute 'received_messages' (which implements a Queue-like interface) so that you can examine the received messages later. cCs&tt|it|i_dS(N(ssupersBlackholeDeliverersselfs__init__sQueues __class__sreceived_messages(sself((s/build/bdist.linux-x86_64/egg/pymta/test_util.pys__init__5scCs|iii|dS(N(sselfs __class__sreceived_messagessputsmsg(sselfsmsg((s/build/bdist.linux-x86_64/egg/pymta/test_util.pysnew_message_accepted9s(s__name__s __module__s__doc__sNonesreceived_messagess__init__snew_message_accepted(((s/build/bdist.linux-x86_64/egg/pymta/test_util.pysBlackholeDeliverer.s  cBs tZdZdZdZRS(sDebuggingMTA is a very simple implementation of PythonMTA which just collects all incoming messages so that you can examine then afterwards.cOs#ti|||t|_dS(N(s PythonMTAs__init__sselfsargsskwargssQueuesqueue(sselfsargsskwargs((s/build/bdist.linux-x86_64/egg/pymta/test_util.pys__init__AscCstt|idtSdS(Nsuse_multiprocessing(ssupers DebuggingMTAsselfs serve_foreversFalse(sself((s/build/bdist.linux-x86_64/egg/pymta/test_util.pys serve_foreverEs(s__name__s __module__s__doc__s__init__s serve_forever(((s/build/bdist.linux-x86_64/egg/pymta/test_util.pys DebuggingMTA=s  cBs,tZdZdZdZddZRS(sThis class runs a PythonMTA in a separate thread which is helpful for unit testing. Attention: Do not use this class together with multiprocessing! http://www.viraj.org/b2evolution/blogs/index.php/2007/02/10/threads_and_fork_a_bad_idea cCstii|||_dS(N(s threadingsThreads__init__sselfsserver(sselfsserver((s/build/bdist.linux-x86_64/egg/pymta/test_util.pys__init__QscCs|iidS(sACreate a new thread which runs the server until stop() is called.N(sselfsservers serve_forever(sself((s/build/bdist.linux-x86_64/egg/pymta/test_util.pysrunUsf5.0cCsA|iitii|d||io ddGHndS(sStop the mail sink and shut down this thread. timeout_seconds specifies how long the caller should wait for the mailsink server to close down (default: 5 seconds). If the server did not stop in time, a warning message is printed.stimeouts7WARNING: Thread still alive. Timeout while waiting for s termination!N(sselfsserversshutdown_servers threadingsThreadsjoinstimeout_secondssisAlive(sselfstimeout_seconds((s/build/bdist.linux-x86_64/egg/pymta/test_util.pysstopYs   (s__name__s __module__s__doc__s__init__srunsstop(((s/build/bdist.linux-x86_64/egg/pymta/test_util.pys MTAThreadIs   cBsStZdZdZedZedZdZdZ dZ dZ RS(sfThe SMTPTestCase is a unittest.TestCase and provides you with a running MTA listening on 'localhost:[8000-40000]' which you can use in your tests. No messages will be delivered to the outside world because the MTA configured by default uses the BlackholeDeliverer. Please make sure that you call the super method for setUp and tearDown.cCsHtt|id|_tidd|_t|_ |i dS(Ns localhosti@i@( ssupers SMTPTestCasesselfssetUpshostnamesrandomsrandints listen_portsNones mta_threadsinit_mta(sself((s/build/bdist.linux-x86_64/egg/pymta/test_util.pyssetUpms   cCst|||d|SdS(sPReturn a PythonMTA instance which is configured according to your needs.s policy_classN(s DebuggingMTAshostnames listen_ports deliverers policy_class(sselfshostnames listen_ports deliverers policy_class((s/build/bdist.linux-x86_64/egg/pymta/test_util.pys build_mtats cCsp|it|_|i|i|i|i||_t |i|_ |i i |i |i|idS(sStarts the MTA in a separate thread with a BlackholeDeliver. This method also ensures that the MTA is really listening on the specified port.N( sselfsstop_mtasBlackholeDeliverers deliverers build_mtashostnames listen_ports policy_classsmtas MTAThreads mta_threadsstarts_try_to_connect_to_mta(sselfs policy_class((s/build/bdist.linux-x86_64/egg/pymta/test_util.pysinit_mtazs   cCsd}xu|djogy/tititi}|i||fWn-tij o|d7}t i dq XdSq Wt p t ddS(Nii if0.10000000000000001sMTA not reachable( striesssocketsAF_INETs SOCK_STREAMssocksconnectshostsportserrorstimessleepsFalsesAssertionError(sselfshostsportssockstries((s/build/bdist.linux-x86_64/egg/pymta/test_util.pys_try_to_connect_to_mtas   cCs.|itj o|iit|_ndS(N(sselfs mta_threadsNonesstop(sself((s/build/bdist.linux-x86_64/egg/pymta/test_util.pysstop_mtas cCs!|itt|idS(sStops the MTA thread.N(sselfsstop_mtassupers SMTPTestCasestearDown(sself((s/build/bdist.linux-x86_64/egg/pymta/test_util.pystearDowns cCs|iiSdS(sWReturn a list of received messages which are stored in the BlackholeDeliverer.N(sselfs deliverersreceived_messages(sself((s/build/bdist.linux-x86_64/egg/pymta/test_util.pysget_received_messagess( s__name__s __module__s__doc__ssetUpsNones build_mtas IMTAPolicysinit_mtas_try_to_connect_to_mtasstop_mtastearDownsget_received_messages(((s/build/bdist.linux-x86_64/egg/pymta/test_util.pys SMTPTestCasees     (s__doc__sQueuesrandomssockets threadingstimesunittestsTestCases pymta.apisIMessageDeliverers IMTAPolicys pymta.mtas PythonMTAs__all__sBlackholeDeliverers DebuggingMTAsThreads MTAThreads SMTPTestCase(s SMTPTestCasesTestCases PythonMTAssockets__all__sBlackholeDeliverers MTAThreadsrandoms DebuggingMTAsQueues threadingstimes IMTAPolicysIMessageDeliverer((s/build/bdist.linux-x86_64/egg/pymta/test_util.pys? s        PK{:MѾpymta/model.py# -*- coding: UTF-8 -*- # # The MIT License # # Copyright (c) 2008 Felix Schwarz # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. __all__ = ['Message', 'Peer'] class Message(object): def __init__(self, peer, smtp_helo=None, smtp_from=None, smtp_to=None, msg_data=None, username=None): self.peer = peer self.smtp_helo = smtp_helo self.smtp_from = smtp_from if smtp_to == None: smtp_to = [] elif not isinstance(smtp_to, (list, tuple)): smtp_to = [smtp_to] self.smtp_to = smtp_to self.msg_data = msg_data self.username = username class Peer(object): def __init__(self, remote_ip, remote_port): self.remote_ip = remote_ip self.remote_port = remote_port def __repr__(self): return '%s(%s, %s)' % (self.__class__.__name__, self.remote_ip, self.remote_port) PKE:A [XXpymta/session.py# -*- coding: UTF-8 -*- # # The MIT License # # Copyright (c) 2008-2009 Felix Schwarz # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. import base64 import binascii import re from repoze.workflow.statemachine import StateMachine, StateMachineError from pymta.compat import set from pymta.exceptions import InvalidParametersError, SMTPViolationError from pymta.model import Message, Peer __all__ = ['SMTPSession'] # regular expression deliberately taken from # http://stackoverflow.com/questions/106179/regular-expression-to-match-hostname-or-ip-address#106223 regex_string = r'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$' class PolicyDenial(SMTPViolationError): pass class SMTPSession(object): """The SMTPSession processes all input data which were extracted from sockets previously. The idea behind this is that this class only knows about different SMTP commands and does not have to know things like command mode and data mode. The protocol parser will create a new session instance for every new connection so this class does not have to be thread-safe. """ def __init__(self, command_parser, deliverer, policy=None, authenticator=None): self._command_parser = command_parser self._deliverer = deliverer self._policy = policy self._authenticator = authenticator self._command_arguments = None self._close_connection_after_response = False self._is_connected = True self._message = None self.hostname_regex = re.compile(regex_string, re.IGNORECASE) self._build_state_machine() # ------------------------------------------------------------------------- # State machine building def _add_state(self, from_state, smtp_command, to_state): handler_function = self._dispatch_commands self.state.add(from_state, smtp_command, to_state, handler_function) def _get_all_real_states(self, including_quit=False): states = set() for key in self.state.states: command_name = key[1] new_state = self.state.states[key] state_name = new_state[0] if state_name not in ['new']: if including_quit or (state_name != 'finished'): states.add((command_name, state_name)) return states def get_all_allowed_internal_commands(self): """Returns an iterable which includes all allowed commands. This does not mean that a specific command from the result is executable right now in this session state (or that it can be executed at all in this connection). Please note that the returned values are /internal/ commands, not SMTP commands (use get_all_allowed_smtp_commands for that) so there will be 'MAIL FROM' instead of 'MAIL'.""" states = set() for command_name, invalid in self._get_all_real_states(including_quit=True): if command_name not in ['GREET', 'MSGDATA']: states.add(command_name) return states def get_all_allowed_smtp_commands(self): states = set() for command_name in self.get_all_allowed_internal_commands(): command_name = command_name.split(' ')[0] states.add(command_name) return states def _add_rset_transitions(self): for command_name, state_name in self._get_all_real_states(): if state_name == 'new': self._add_state(state_name, 'RSET', state_name) else: self._add_state(state_name, 'RSET', 'initialized') def _add_help_noop_and_quit_transitions(self): """HELP, NOOP and QUIT should be possible from everywhere so we need to add these transitions to all states configured so far.""" states = set() for key in self.state.states: new_state = self.state.states[key] state_name = new_state[0] if state_name not in ['new', 'finished']: states.add(state_name) for state in states: self._add_state(state, 'NOOP', state) self._add_state(state, 'HELP', state) self._add_state(state, 'QUIT', 'finished') def _build_state_machine(self): self.state = StateMachine('_state', initial_state='new') self._add_state('new', 'GREET', 'greeted') self._add_state('greeted', 'HELO', 'initialized') self._add_state('greeted', 'EHLO', 'esmtp_initialized') # ---- self._add_state('initialized', 'MAIL FROM', 'sender_known') self._add_state('esmtp_initialized', 'MAIL FROM', 'sender_known') self._add_state('esmtp_initialized', 'AUTH PLAIN', 'authenticated') self._add_state('authenticated', 'MAIL FROM', 'sender_known') # ---- self._add_state('sender_known', 'RCPT TO', 'recipient_known') # multiple recipients self._add_state('recipient_known', 'RCPT TO', 'recipient_known') self._add_state('recipient_known', 'DATA', 'receiving_message') self._add_state('receiving_message', 'MSGDATA', 'initialized') self._add_help_noop_and_quit_transitions() self._add_rset_transitions() self.valid_commands = [command for from_state, command in self.state.states] # ------------------------------------------------------------------------- def get_ehlo_lines(self): """Return the capabilities to be advertised after EHLO.""" lines = [] if self._authenticator != None: # TODO: Make the authentication pluggable but separate mechanism # from user look-up. lines.append('AUTH PLAIN') if self._policy != None: lines.extend(self._policy.ehlo_lines(self._message.peer)) lines.append('HELP') return lines def _set_size_restrictions(self): """Set the maximum allowed message in the underlying layer so that big messages are not hold in memory before they are rejected.""" max_message_size = self._get_max_message_size_from_policy() self._command_parser.set_maximum_message_size(max_message_size) def _dispatch_commands(self, from_state, to_state, smtp_command, ob): """This method dispatches a SMTP command to the appropriate handler method. It is called after a new command was received and a valid transition was found.""" #print from_state, ' -> ', to_state, ':', smtp_command name_handler_method = 'smtp_%s' % smtp_command.lower().replace(' ', '_') try: handler_method = getattr(self, name_handler_method) except AttributeError: base_msg = 'No handler for %s though transition is defined (no method %s)' print base_msg % (smtp_command, name_handler_method) self.reply(451, 'Temporary Local Problem: Please come back later') else: # Don't catch InvalidParametersError here - else the state would # be moved forward. Instead the handle_input will catch it and send # out the appropriate reply. handler_method() def _evaluate_decision(self, decision): return (decision in [True, None]) def _is_multiline_reply(self, reply_message): return (not isinstance(reply_message, basestring)) def _send_custom_response(self, reply): code, custom_response = reply if self._is_multiline_reply(custom_response): self.multiline_reply(code, custom_response) else: self.reply(code, custom_response) def _evaluate_policydecision_result(self, result): decision = self._evaluate_decision(result.is_command_acceptable()) response_sent = result.use_custom_reply() if result.close_connection_before_response(): self.close_connection() response_sent = True if result.use_custom_reply(): self._send_custom_response(result.get_custom_reply()) if result.close_connection_after_response(): self.please_close_connection_after_response() return decision, response_sent def is_allowed(self, acl_name, *args): if self._policy is not None: decider = getattr(self._policy, acl_name) result = decider(*args) if result in [True, False, None]: return self._evaluate_decision(result), False elif hasattr(result, 'is_command_acceptable'): return self._evaluate_policydecision_result(result) elif len(result) == 2: decision = self._evaluate_decision(result[0]) self._send_custom_response(result[1]) return decision, True raise ValueError('Unknown policy response') return True, False # ------------------------------------------------------------------------- def new_connection(self, remote_ip, remote_port): """This method is called when a new SMTP session is opened. [PUBLIC API] """ self._state = 'new' self._message = Message(Peer(remote_ip, remote_port)) decision, response_sent = self.is_allowed('accept_new_connection', self._message.peer) if decision: if not response_sent: self.handle_input('greet') self._set_size_restrictions() else: if not response_sent: self.reply(554, 'SMTP service not available') self.close_connection() def handle_input(self, smtp_command, data=None): """Processes the given SMTP command with the (optional data). [PUBLIC API] """ self._command_arguments = data self.please_close_connection_after_response(False) command = smtp_command.upper() try: try: # SMTP commands must be treated as case-insensitive self.state.execute(self, command) except StateMachineError: if command not in self.valid_commands: self.reply(500, 'unrecognized command "%s"' % smtp_command) else: msg = 'Command "%s" is not allowed here' % smtp_command allowed_transitions = self.state.transitions(self) if len(allowed_transitions) > 0: msg += ', expected on of %s' % allowed_transitions self.reply(503, msg) except InvalidParametersError, e: if not e.response_sent: msg = 'Syntactically invalid %s argument(s)' % smtp_command self.reply(501, msg) except PolicyDenial, e: if not e.response_sent: self.reply(e.code, e.reply_text) finally: if self.should_close_connection_after_response(): self.close_connection() self._command_arguments = None def input_exceeds_limits(self): """Called when the client sent a message that exceeded the maximum size.""" self.reply(552, 'message exceeds fixed maximum message size') def reply(self, code, text): """This method returns a message to the client (actually the session object is responsible of actually pushing the bits).""" self._command_parser.push(code, text) def multiline_reply(self, code, responses): """This method returns a message with multiple lines to the client (actually the session object is responsible of actually pushing the bits).""" self._command_parser.multiline_push(code, responses) def please_close_connection_after_response(self, value=None): if value is None: value = True self._close_connection_after_response = value def should_close_connection_after_response(self): return self._close_connection_after_response def close_connection(self): "Request a connection close from the SMTP session handling instance." if self._is_connected: self._is_connected = False self._command_parser.close_when_done() # ------------------------------------------------------------------------- # Protocol handling functions (not public) def smtp_greet(self): """This method handles not a real smtp command. It is called when a new connection was accepted by the server.""" # Policy check was done when accepting the connection so we don't have # to do it here again. primary_hostname = self._command_parser.primary_hostname reply_text = '%s Hello %s' % (primary_hostname, self._message.peer.remote_ip) self.reply(220, reply_text) def smtp_quit(self): primary_hostname = self._command_parser.primary_hostname reply_text = '%s closing connection' % primary_hostname self.reply(221, reply_text) self._command_parser.close_when_done() def smtp_noop(self): self.reply(250, 'OK') def smtp_help(self): states = self.get_all_allowed_smtp_commands() self.multiline_reply(214, (('Commands supported'), ' '.join(states))) def _reply_to_helo(self, helo_string, response_sent): self._message.smtp_helo = helo_string if not response_sent: primary_hostname = self._command_parser.primary_hostname self.reply(250, primary_hostname) def _process_helo_or_ehlo(self, policy_methodname, reply_method): helo_string = (self._command_arguments or '').strip() valid_hostname_syntax = (self.hostname_regex.match(helo_string) != None) if not valid_hostname_syntax: raise InvalidParametersError(helo_string) else: decision, response_sent = self.is_allowed(policy_methodname, helo_string, self._message) if decision: reply_method(helo_string, response_sent) elif not decision: raise PolicyDenial(response_sent) def smtp_helo(self): self._process_helo_or_ehlo('accept_helo', self._reply_to_helo) def _reply_to_ehlo(self, helo_string, response_sent): self._message.smtp_helo = helo_string if not response_sent: primary_hostname = self._command_parser.primary_hostname lines = [primary_hostname] + self.get_ehlo_lines() self.multiline_reply(250, lines) def smtp_ehlo(self): self._process_helo_or_ehlo('accept_ehlo', self._reply_to_ehlo) def _check_password(self, username, password): decision, response_sent = self.is_allowed('accept_auth_plain', username, password, self._message) if not decision: raise PolicyDenial(response_sent) assert response_sent == False if self._authenticator == None: self.reply(535, 'AUTH not available') raise InvalidParametersError(response_sent=True) credentials_correct = \ self._authenticator.authenticate(username, password, self._message.peer) if credentials_correct: self._message.username = username self.reply(235, 'Authentication successful') else: self.reply(535, 'Bad username or password') def smtp_auth_plain(self): base64_credentials = self._command_arguments try: credentials = base64.decodestring(base64_credentials) except binascii.Error: raise InvalidParametersError(base64_credentials) else: match = re.search('^[^\x00]*\x00([^\x00]*)\x00([^\x00]*)$', credentials) if match: username, password = match.group(1), match.group(2) self._check_password(username, password) else: raise InvalidParametersError(credentials) def _split_mail_from_parameter(self, data): sender = data extensions = {} # TODO: case insensitivity of extension names verb_regex = re.compile('^\s*<(.*)>(?:\s*(\S+)\s*)*\s*$') match = verb_regex.search(data) if match: sender = match.group(1) extension_string = match.group(2) if extension_string is not None: for extension in re.split('\s+', extension_string): if '=' in extension: name, parameter = extension.split('=', 1) extensions[name] = parameter else: extensions[extension] = True return (sender, extensions) def _check_mail_extensions(self, extensions): if 'size' in extensions: # TODO: protect against non-numeric size! announced_size = int(extensions['size']) max_message_size = self._get_max_message_size_from_policy() if max_message_size != None: if announced_size > max_message_size: self.reply(552, 'message exceeds fixed maximum message size') raise InvalidParametersError('MAIL FROM', response_sent=True) def smtp_mail_from(self): data = self._command_arguments sender, extensions = self._split_mail_from_parameter(data) # TODO: Check for good email address! # TODO: Check for single email address! uses_esmtp = (self._state in ['esmtp_initialized', 'authenticated']) if uses_esmtp: self._check_mail_extensions(extensions) elif len(extensions) > 0: self.reply(501, 'No SMTP extensions allowed for plain SMTP') raise InvalidParametersError('MAIL', response_sent=True) decision, response_sent = self.is_allowed('accept_from', sender, self._message) if decision: self._message.smtp_from = sender if not response_sent: self.reply(250, 'OK') elif not decision: raise PolicyDenial(response_sent) def _extract_email_address(self, parameter): match = re.search('^?$', parameter) if match: return match.group(1) raise InvalidParametersError(parameter) def smtp_rcpt_to(self): # TODO: Check for good email address! email_address = self._extract_email_address(self._command_arguments) decision, response_sent = self.is_allowed('accept_rcpt_to', email_address, self._message) if decision: self._message.smtp_to.append(email_address) if not response_sent: self.reply(250, 'OK') elif not decision: raise PolicyDenial(response_sent, 550, 'relay not permitted') def smtp_data(self): # TODO: Check no arguments decision, response_sent = self.is_allowed('accept_data', self._message) if decision and not response_sent: self._command_parser.switch_to_data_mode() self.reply(354, 'Enter message, ending with "." on a line by itself') elif not decision: raise PolicyDenial(response_sent) def _get_max_message_size_from_policy(self): max_message_size = None if (self._policy is not None) and (self._message.peer is not None): max_message_size = self._policy.max_message_size(self._message.peer) return max_message_size def _check_size_restrictions(self, msg_data): max_message_size = self._get_max_message_size_from_policy() if (max_message_size is not None): msg_too_big = (len(msg_data) > int(max_message_size)) if msg_too_big: msg = 'message exceeds fixed maximum message size' raise PolicyDenial(False, 552, msg) def _copy_basic_settings(self, msg): peer = self._message.peer new_message = Message(peer=Peer(peer.remote_ip, peer.remote_port), smtp_helo=self._message.smtp_helo, username=self._message.username) return new_message def smtp_msgdata(self): """This method handles not a real smtp command. It is called when the whole message was received (multi-line DATA command is completed).""" msg_data = self._command_arguments self._command_parser.switch_to_command_mode() self._check_size_restrictions(msg_data) decision, response_sent = self.is_allowed('accept_msgdata', msg_data, self._message) if decision: self._message.msg_data = msg_data new_message = self._copy_basic_settings(self._message) self._deliverer.new_message_accepted(self._message) if not response_sent: self.reply(250, 'OK') # Now we must not loose the message anymore! self._message = new_message elif not decision: raise PolicyDenial(response_sent, 550, 'Message content is not acceptable') def smtp_rset(self): self._message = Message(peer=self._message.peer, smtp_helo=self._message.smtp_helo) self.reply(250, 'Reset OK') PKEF:CCpymta/command_parser.pyc; ,Jc@sdklZdkZdkZdklZlZdklZdk l Z dgZ de fdYZ de fdYZd efd YZd e fd YZdS( (sEmptyN(s StateMachinesStateMachineError(sSMTPViolationError(s SMTPSessionsSMTPCommandParsersParserImplementationcBs tZdZdZdZRS(sThe SMTPCommandParser needs a connected socket to operate. This is very inconvenient for testing therefore all 'interesting' functionality is moved in this class which is easily testable.cCs8||_ddi|}ti|ti|_dS(Ns^(%s)(?: |:)\s*(.*)$s|( sallowed_commandssselfs_allowed_commandssjoins regex_stringsrescompiles IGNORECASEs parse_regex(sselfsallowed_commandss regex_string((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pys__init__*s cCsrt|tptt}|ii|}|tjo(|i d}|i di }n||fSdS(Nii( s isinstancescommands basestringsAssertionErrorsNones parametersselfs parse_regexssearchsmatchsgroupsstrip(sselfscommands parametersmatch((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pysparse/s (s__name__s __module__s__doc__s__init__sparse(((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pysParserImplementation%s  cBstZdZdZeedZdZdZeeZdZ edZ dZ dZ d Z d Zd Zd Zd ZdZeeeZdZdZRS(szThis class is a tiny abstraction layer above the real communication with the client. It knows about the two basic SMTP modes (sending commands vs. sending message data) and can assemble SMTP-like replies (Code-Message) in a convenient manner. However all handling of SMTP commands will take place in an upper layer. The original 'SMTPChannel' class from Python.org handled all communication with asynchat, implemented a extremely simple state machine and processed the data. Implementing hooks in that design (or adding fine-grained policies) was not possible at all with the previous design.s c Cs||_t|_|i|_|itd|d|d|d||_ |i i }t||_|i i||dS(Nscommand_parsers delivererspolicys authenticator(schannelsselfs_channelsNonesdatasLINE_TERMINATORs terminators_build_state_machines SMTPSessions delivererspolicys authenticatorssessions!get_all_allowed_internal_commandssallowed_commandssParserImplementations_parsersnew_connectionsremote_ip_strings remote_port(sselfschannelsremote_ip_strings remote_ports delivererspolicys authenticatorsallowed_commands((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pys__init__Hs    csd}d}d}tddd_d_iiddd|iiddd |iid dd|dS( Ncs t_dS(N(sNonesselfsdata(s from_statesto_states smtp_commandsinstance(sself(s4build/bdist.linux-x86_64/egg/pymta/command_parser.pys_command_completedWscs&diif_g_dS(Ns%s.%s(sselfsLINE_TERMINATORs terminatorsdata(s from_statesto_states smtp_commandsinstance(sself(s4build/bdist.linux-x86_64/egg/pymta/command_parser.pys_start_receiving_messageZscsi_t_dS(N(sselfsLINE_TERMINATORs terminatorsNonesdata(s from_statesto_states smtp_commandsinstance(sself(s4build/bdist.linux-x86_64/egg/pymta/command_parser.pys_finished_receiving_message^s s_states initial_statescommandssCOMMANDsDATAsdata(s_command_completeds_start_receiving_messages_finished_receiving_messages StateMachinesselfsstates_statesadd(sselfs_finished_receiving_messages_command_completeds_start_receiving_message((sselfs4build/bdist.linux-x86_64/egg/pymta/command_parser.pys_build_state_machineVs    cCstiSdS(N(ssocketsgetfqdn(sself((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pysprimary_hostnamehscCsbxGt|d D]5\}}dt|t|f}|i|qW|i||ddS(s~Send a multi-message to the peer (using the correct SMTP line terminators (usually only called from the SMTPSession).is%s-%sN( s enumerateslinessislinesstrscodesanswersselfspush(sselfscodeslinessisanswersline((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pysmultiline_pushps  cCsf|tjo |}ndt||f}|i|i o||i7}n|ii|dS(swSend a message to the peer (using the correct SMTP line terminators (usually only called from the SMTPSession).s%s %sN( smsgsNonescodesstrsendswithsselfsLINE_TERMINATORs_channelswrite(sselfscodesmsg((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pyspushxs  cCsB|idjo ||_n"|djo|ii|ndS(Nscommandss.(sselfs_statesdatasappend(sselfsdata((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pyscollect_incoming_datas  cCs|ii|idS(suCalled from the underlying transport layer if the client input exceeded the configured maximum message size.N(sselfssessionsinput_exceeds_limitssswitch_to_command_mode(sself((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pysinput_exceeds_limitss cCs|ii|dS(sSet the maximum allowed size (in bytes) of a command/message in the underlying transport layer so that big messages are not stored in memory before they are rejected.N(sselfs_channelsset_max_input_sizesmax_size(sselfsmax_size((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pysset_maximum_message_sizescCs|ii|ddS(szCalled from the SMTPSession when a message was received and the client is expected to send single commands again.sCOMMANDN(sselfsstatesexecute(sself((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pysswitch_to_command_modescCs|ii|ddS(seCalled from the SMTPSession when the client should start transfering the actual message data.sDATAN(sselfsstatesexecute(sself((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pysswitch_to_data_modescCspg}xV|D]N}xE|i|iD]1}|ido|d}n|i|q&Wq Wdi |SdS(sUses the input data to recover the original payload (includes transparency support as specified in RFC 821, Section 4.5.2).s.is N( sliness input_dataspartssplitsselfsLINE_TERMINATORslines startswithsappendsjoin(sselfs input_dataspartslinessline((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pys_assemble_msgdatascCs |iSdS(N(sselfs _terminator(sself((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pysget_terminatorscCs ||_dS(N(s terminatorsselfs _terminator(sselfs terminator((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pysset_terminatorscCs|i}|idjo/|ii|\}}|ii ||n:t |t pt |i |}|ii d|dS(NscommandssMSGDATA(sselfsdatas input_datas_states_parsersparsescommands parameterssessions handle_inputs isinstanceslistsAssertionErrors_assemble_msgdatasmsgdata(sselfsmsgdatas input_datascommands parameter((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pysfound_terminators cCs|iidS(N(sselfs_channelsclose(sself((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pysclose_when_dones(s__name__s __module__s__doc__sLINE_TERMINATORsNones__init__s_build_state_machinesprimary_hostnamespropertysmultiline_pushspushscollect_incoming_datasinput_exceeds_limitssset_maximum_message_sizesswitch_to_command_modesswitch_to_data_modes_assemble_msgdatasget_terminatorsset_terminators terminatorsfound_terminatorsclose_when_done(((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pysSMTPCommandParser:s&            sClientDisconnectedErrorcBstZdZRS(s?Raised when the SMTP client closed the connection unexpectedly.(s__name__s __module__s__doc__(((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pysClientDisconnectedErrors s WorkerProcesscBswtZdZeedZdZdZdZdZdZ dZ dZ d Z d Z d ZRS( sThe WorkerProcess handles the real communication. with the client. It does not know anything about the SMTP protocol (besides the fact that it is a line-based protocol).cCsp||_||_|i||_|i||_ |i||_ t |_ t |_t |_t|_dS(N(squeuesselfs_queues server_sockets_server_sockets_get_instance_from_classsdeliverer_classs _deliverers policy_classs_policysauthenticator_classs_authenticatorsNones _connections_chatters _max_sizesFalses_input_too_big(sselfsqueues server_socketsdeliverer_classs policy_classsauthenticator_class((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pys__init__s     cCs(t}|tjo |}n|SdS(N(sNonesinstancesclass_reference(sselfsclass_referencesinstance((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pys_get_instance_from_classs  cCsxtoy|ii\}}PWqtij oUy8|ii }|ii ||t jot SnWqt j oqXqXqW|iti||fSdS(N(sTruesselfs_server_socketsaccepts connectionsremote_addressssocketstimeouts_queues get_nowaits new_tokensputsNonesEmptys settimeoutsgetdefaulttimeout(sselfsremote_addresss connections new_token((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pys_wait_for_connections  cCsMt}x<to4y|iid|}PWq tj oq Xq W|SdS(Nstimeout(sNonestokensTruesselfs_queuesgetssecondssEmpty(sselfssecondsstoken((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pys_get_token_with_timeouts cstd}zxtox|id| oPntjpt|i}|i i t|tjoPn|i |qWWd|o|i i tnXdS(NcstjSdS(N(stokensNone((stoken(s4build/bdist.linux-x86_64/egg/pymta/command_parser.pys have_tokensi( sNonestokens have_tokensTruesselfs_get_token_with_timeoutsAssertionErrors_wait_for_connectionsconnection_infos_queuesputschat_with_peer(sselfsconnection_infos have_tokenstoken((stokens4build/bdist.linux-x86_64/egg/pymta/command_parser.pysruns$     cCs ||_dS(sUSet the maximum size of client input (in bytes) before the input is discarded. When the client finished transmitting a message which was too big, the 'input_exceeds_limits' method is called on the chatter which is responsible for notifying the peer. Setting a maximum size of None disables any size-checking.N(smax_sizesselfs _max_size(sselfsmax_size((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pysset_max_input_sizescCs|\|_\}}t||||i|i|i|_ x|i oyR|i }|i o!|i i||i in|i it|_ Wq?tj o.|i o|iit|_qq?Xq?WdS(N(sconnection_infosselfs _connectionsremote_ip_strings remote_portsSMTPCommandParsers _deliverers_policys_authenticators_chatters is_connectedsreadlinesdatas_input_too_bigscollect_incoming_datasfound_terminatorsinput_exceeds_limitssFalsesClientDisconnectedErrorsclosesNone(sselfsconnection_infosremote_ip_strings remote_portsdata((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pyschat_with_peers         cCs|itjSdS(N(sselfs _connectionsNone(sself((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pys is_connected/scCs|iptd}t|_xtoy|iid}Wnt i j ot nX|djo t nP|i |iio ||t|ii 7}Pn|i o||7}n|itj ot||ijot|_d}q&q&W|SdS(sLRead as much data as possible until a line terminator was received.siN(sselfs is_connectedsAssertionErrorsdatasFalses_input_too_bigsTrues _connectionsrecvs more_datassocketserrorsClientDisconnectedErrorsendswiths_chatters terminatorslens _max_sizesNone(sselfsdatas more_data((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pysreadline2s*    & cCs.|ipt|iit|_dS(s$Closes the connection to the client.N(sselfs is_connectedsAssertionErrors _connectionsclosesNone(sself((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pyscloseIs cCs(|ipt|ii|dS(sSends some data to the client.N(sselfs is_connectedsAssertionErrors _connectionssendsdata(sselfsdata((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pyswriteOs(s__name__s __module__s__doc__sNones__init__s_get_instance_from_classs_wait_for_connections_get_token_with_timeoutsrunsset_max_input_sizeschat_with_peers is_connectedsreadlinescloseswrite(((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pys WorkerProcesss         (sQueuesEmptysressocketsrepoze.workflow.statemachines StateMachinesStateMachineErrorspymta.exceptionssSMTPViolationErrors pymta.sessions SMTPSessions__all__sobjectsParserImplementationsSMTPCommandParsersClientDisconnectedErrors WorkerProcess( sSMTPViolationErrorssockets StateMachinesClientDisconnectedErrors WorkerProcesssresParserImplementations SMTPSessionsSMTPCommandParsersStateMachineErrorsEmptys__all__((s4build/bdist.linux-x86_64/egg/pymta/command_parser.pys?s      PKEF:^zpymta/exceptions.pyc; ,Jc@sIdklZddgZdefdYZdefdYZdS((sPyMTAExceptionsInvalidParametersErrorsSMTPViolationErrorcBs#tZdZeddedZRS(s2Raised when the SMTP client violated the protocol.i&sAdministrative ProhibitioncCsP|tjod||f}nti||||_||_||_dS(Ns%s %s(smessagesNonescodes reply_textsPyMTAExceptions__init__sselfs response_sent(sselfs response_sentscodes reply_textsmessage((s0build/bdist.linux-x86_64/egg/pymta/exceptions.pys__init__#s    (s__name__s __module__s__doc__sFalsesNones__init__(((s0build/bdist.linux-x86_64/egg/pymta/exceptions.pysSMTPViolationError s cBstZdZedZRS(s?The SMTP client provided invalid parameters for a SMTP command.cOs ti|||||_dS(N(sSMTPViolationErrors__init__sselfsargsskwargss parameter(sselfs parametersargsskwargs((s0build/bdist.linux-x86_64/egg/pymta/exceptions.pys__init__0s(s__name__s __module__s__doc__sNones__init__(((s0build/bdist.linux-x86_64/egg/pymta/exceptions.pysInvalidParametersError-s N(s pymta.apisPyMTAExceptions__all__sSMTPViolationErrorsInvalidParametersError(sPyMTAExceptionsSMTPViolationErrors__all__sInvalidParametersError((s0build/bdist.linux-x86_64/egg/pymta/exceptions.pys?s   PKEF:,_]]EGG-INFO/SOURCES.txtPKEF:nEGG-INFO/requires.txtPKEF:&o;EGG-INFO/PKG-INFOPKwH:2 EGG-INFO/zip-safePKEF:vrH EGG-INFO/top_level.txtPKEF:2 EGG-INFO/dependency_links.txtPKBF:MkEH pymta/release.pyPKEF:F@m0Npymta/compat.pycPKEF:"OUR-- pymta/api.pycPKE:j UApymta/mta.pyPKE:H ## Xpymta/api.pyPKE:YY(|pymta/test_util.pyPKE:^``pymta/compat.pyPKE:m4m4>pymta/command_parser.pyPKEF:- {p  pymta/__init__.pycPKEF:݊YKKpymta/model.pycPKEF:Iztjtjpymta/session.pycPKEF:rZww 6Apymta/mta.pycPK{:Wpymta/__init__.pyPKEF:}QMMXpymta/release.pycPKE:!AA'apymta/exceptions.pyPKEF:YG? ipymta/test_util.pycPK{:MѾŊpymta/model.pyPKE:A [XXpymta/session.pyPKEF:CCpymta/command_parser.pycPKEF:^z/pymta/exceptions.pycPKx6