Commits

"Fel...@schwarz.eu>"  committed 039af50

fixes #24 (Worker process must handle socket disconnect)

  • Participants
  • Parent commits 2ee9e70

Comments (0)

Files changed (4)

File Changelog.txt

 
 Changelog for pymta
 
-0.3 (February 2009)
+0.3.1 (XX.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)

File pymta/command_parser.py

         self._channel.close()
 
 
+# TODO: Make this inherit from pymtaexception, move that into api and make a new
+# smtpprotocolviolationError in smtpsession.
+class ClientDisconnectedError(Exception):
+    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
         self._chatter = SMTPCommandParser(self, remote_ip_string, remote_port, 
                             self._deliverer, self._policy, self._authenticator)
         while self.is_connected():
-            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
+            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)
         self._input_too_big = False
         while True:
             more_data = self._connection.recv(4096)
-            if more_data.endswith(self._chatter.terminator):
+            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:

File pymta/release.py

 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/pymta-%s.tar.gz" % version
+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"
 

File tests/basic_smtp_test.py

 # THE SOFTWARE.
 
 import smtplib
+import socket
 from unittest import TestCase
 
 from pymta.api import IMTAPolicy
             self.assertEqual('message exceeds fixed maximum message size', 
                              e.smtp_error)
     
-    def test_check_foobar(self):
-        """Check that there is transparency support for lines starting with a 
-        dot in the message body (RFC 821, section 4.5.2)."""
-        msg = rfc822_msg + '\n.Bar\nFoo'
-        self.connection.sendmail('from@example.com', 'foo@example.com', msg)
-        self.connection.quit()
-        self.connection.connect(self.hostname, self.listen_port)
-        self.connection.sendmail('from@example.com', 'foo@example.com', msg)
+    def test_workerprocess_detects_closed_connections(self):
+        """Check that the WorkerProcess gracefully handles connections which are
+        closed without QUIT. This can happen due to network problems or 
+        unfriendly clients."""
+        self.connection.helo('foo')
+        self.connection.close()
+        
+        # In 0.3 the WorkerProcess would hang and start to eat up the whole CPU
+        # so we need to set a sensible timeout so that this test will fail with
+        # an appropriate exception.
+        # On a normal system we should be able to reconnect after a dropped
+        # connection within two seconds under all circumstances.
+        old_default = socket.getdefaulttimeout()
+        socket.setdefaulttimeout(2)
+        try:
+            self.connection.connect(self.hostname, self.listen_port)
+        finally:
+            socket.setdefaulttimeout(old_default)
 
 
+