Repository: aio-libs/aiosmtpd
Pull request #54 (2017-03-25 23:41:21)

Plumb enable_SMTPUTF8 flag through the Controller constructor
Even though the flag must remain False by default for the SMTP class, there's
no such backward compatibility concern for the Controller class, so default it
to True there since that will likely be the most common case.

Also, make ready_timeout and enable_SMTPUTF8 keyword-only arguments.  We can't
change any of the others because of backward compatibility.

Mortal: I'm curious: What does aiosmtpd (in particular the SMTP class) aim to be backward compatible with?
warsaw: On Mar 25, 2017, at 04:56 PM, Mathias Rav wrote:

>I'm curious: What does aiosmtpd (in particular the SMTP class) aim to be
>backward compatible with?

Its own earlier releases.

kozzztik: Merged it to PR #4 

Patch:
diff --git a/aiosmtpd/controller.py b/aiosmtpd/controller.py
index cbcb6c1..2164967 100644
--- a/aiosmtpd/controller.py
+++ b/aiosmtpd/controller.py
@@ -14,11 +14,12 @@ except ImportError:                                          # pragma: nocover
 
 @public
 class Controller:
-    def __init__(self, handler, loop=None, hostname=None, port=8025,
-                 ready_timeout=1.0):
+    def __init__(self, handler, loop=None, hostname=None, port=8025, *,
+                 ready_timeout=1.0, enable_SMTPUTF8=True):
         self.handler = handler
         self.hostname = '::1' if hostname is None else hostname
         self.port = port
+        self.enable_SMTPUTF8 = enable_SMTPUTF8
         self.loop = asyncio.new_event_loop() if loop is None else loop
         self.server = None
         self.thread = None
@@ -39,7 +40,7 @@ class Controller:
 
     def factory(self):
         """Allow subclasses to customize the handler/server creation."""
-        return SMTP(self.handler)
+        return SMTP(self.handler, enable_SMTPUTF8=self.enable_SMTPUTF8)
 
     def _run(self, ready_event):
         asyncio.set_event_loop(self.loop)
diff --git a/aiosmtpd/docs/NEWS.rst b/aiosmtpd/docs/NEWS.rst
index efcd0b7..92adadb 100644
--- a/aiosmtpd/docs/NEWS.rst
+++ b/aiosmtpd/docs/NEWS.rst
@@ -22,11 +22,18 @@
   and ``rcpt_options`` (although the latter is still not support in ``SMTP``).
 * ``DATA`` method now respects original line endings, and passing size limits
   is now handled better.  Given by Konstantin Volkov.
-* Controller objects now have an optional timeout argument used to wait for
-  the server to become ready.  This can also be overridden with the
-  environment variable ``AIOSMTPD_CONTROLLER_TIMEOUT``. (Closes #35)
-* Handlers can define a ``handle_STARTTLS()`` method which is called if SSL is
-  enabled during the making of the connection. (Closes #48)
+* The ``Controller`` class has two new optional keyword arguments.
+
+  - ``ready_timeout`` specifies a timeout in seconds that can be used to limit
+    the amount of time it waits for the server to become ready.  This can also
+    be overridden with the environment variable
+    ``AIOSMTPD_CONTROLLER_TIMEOUT``. (Closes #35)
+  - ``enable_SMTPUTF8`` is passed through to the ``SMTP`` constructor in the
+    default factory.  If you override ``Controller.factory()`` you can pass
+    ``self.enable_SMTPUTF8`` yourself.
+* Handlers can define a ``handle_tls_handshake()`` method, which takes a
+  session object, and is called if SSL is enabled during the making of the
+  connection.  (Closes #48)
 * Better Windows compatibility.
 * Better Python 3.4 compatibility.
 * Use ``flufl.testing`` package for nose2 and flake8 plugins.
diff --git a/aiosmtpd/docs/controller.rst b/aiosmtpd/docs/controller.rst
index 828471a..8fee9f0 100644
--- a/aiosmtpd/docs/controller.rst
+++ b/aiosmtpd/docs/controller.rst
@@ -73,3 +73,34 @@ The server is guaranteed to be stopped.
     Traceback (most recent call last):
     ...
     ConnectionRefusedError: ...
+
+
+Enable SMTPUTF8
+===============
+
+It's very common to want to enable the ``SMTPUTF8`` ESMTP option, therefore
+this is the default for the ``Controller`` constructor.  For backward
+compatibility reasons, this is *not* the default for the ``SMTP`` class
+though.  If you want to disable this in the ``Controller``, you can pass this
+argument into the constructor::
+
+    >>> from aiosmtpd.handlers import Sink
+    >>> controller = Controller(Sink(), enable_SMTPUTF8=False)
+    >>> controller.start()
+
+    >>> client = SMTP(controller.hostname, controller.port)
+    >>> code, message = client.ehlo('me')
+    >>> code
+    250
+
+The EHLO response does not include the ``SMTPUTF8`` ESMTP option.  We have to
+skip the server host name line, since that's variable.
+
+    >>> lines = message.decode('utf-8').splitlines()
+    >>> for line in lines[1:]:
+    ...     print(line)
+    SIZE 33554432
+    8BITMIME
+    HELP
+
+    >>> controller.stop()


Test patch:
diff --git a/aiosmtpd/tests/test_handlers.py b/aiosmtpd/tests/test_handlers.py
index 6c7ba8c..0b0c58f 100644
--- a/aiosmtpd/tests/test_handlers.py
+++ b/aiosmtpd/tests/test_handlers.py
@@ -18,7 +18,7 @@ from unittest.mock import call, patch
 CRLF = '\r\n'
 
 
-class UTF8Controller(Controller):
+class DecodingController(Controller):
     def factory(self):
         return Server(self.handler, decode_data=True)
 
@@ -27,7 +27,7 @@ class TestDebugging(unittest.TestCase):
     def setUp(self):
         self.stream = StringIO()
         handler = Debugging(self.stream)
-        controller = UTF8Controller(handler)
+        controller = DecodingController(handler)
         controller.start()
         self.addCleanup(controller.stop)
         self.address = (controller.hostname, controller.port)
@@ -187,7 +187,7 @@ Testing
     def test_message_decoded(self):
         # In this test, the message content comes in as a string.
         handler = DataHandler()
-        controller = UTF8Controller(handler)
+        controller = DecodingController(handler)
         controller.start()
         self.addCleanup(controller.stop)
         with SMTP(controller.hostname, controller.port) as client:
@@ -241,7 +241,7 @@ Testing
         # strings.  There's no difference in the message seen by the
         # handler's handle_message() method, but internally this gives full
         # coverage.
-        controller = UTF8Controller(self.handler)
+        controller = DecodingController(self.handler)
         controller.start()
         self.addCleanup(controller.stop)
 
@@ -403,7 +403,7 @@ class TestProxy(unittest.TestCase):
     def setUp(self):
         self.stream = StringIO()
         handler = Proxy('localhost', 9025)
-        controller = UTF8Controller(handler)
+        controller = DecodingController(handler)
         controller.start()
         self.addCleanup(controller.stop)
         self.address = (controller.hostname, controller.port)
diff --git a/aiosmtpd/tests/test_server.py b/aiosmtpd/tests/test_server.py
index e99529e..2f3741d 100644
--- a/aiosmtpd/tests/test_server.py
+++ b/aiosmtpd/tests/test_server.py
@@ -10,11 +10,6 @@ from aiosmtpd.smtp import SMTP as Server
 from smtplib import SMTP
 
 
-class UTF8Controller(Controller):
-    def factory(self):
-        return Server(self.handler, enable_SMTPUTF8=True)
-
-
 class TestServer(unittest.TestCase):
     def test_constructor_contraints(self):
         # These two arguments cannot both be set.
@@ -23,7 +18,7 @@ class TestServer(unittest.TestCase):
                           decode_data=True)
 
     def test_smtp_utf8(self):
-        controller = UTF8Controller(Sink())
+        controller = Controller(Sink())
         controller.start()
         self.addCleanup(controller.stop)
         with SMTP(controller.hostname, controller.port) as client:
@@ -42,15 +37,15 @@ class TestServer(unittest.TestCase):
 
     def test_socket_error(self):
         # Testing starting a server with a port already in use
-        s1 = UTF8Controller(Sink(), port=8025)
-        s2 = UTF8Controller(Sink(), port=8025)
+        s1 = Controller(Sink(), port=8025)
+        s2 = Controller(Sink(), port=8025)
         self.addCleanup(s1.stop)
         self.addCleanup(s2.stop)
         s1.start()
         self.assertRaises(socket.error, s2.start)
 
     def test_server_attribute(self):
-        controller = UTF8Controller(Sink())
+        controller = Controller(Sink())
         self.assertIsNone(controller.server)
         try:
             controller.start()
diff --git a/aiosmtpd/tests/test_smtp.py b/aiosmtpd/tests/test_smtp.py
index ceef8d8..d37d0d8 100644
--- a/aiosmtpd/tests/test_smtp.py
+++ b/aiosmtpd/tests/test_smtp.py
@@ -17,7 +17,7 @@ CRLF = '\r\n'
 BCRLF = b'\r\n'
 
 
-class UTF8Controller(Controller):
+class DecodingController(Controller):
     def factory(self):
         return Server(self.handler, decode_data=True)
 
@@ -48,11 +48,6 @@ class SizedController(Controller):
         return Server(self.handler, data_size_limit=self.size)
 
 
-class SMTPUTF8Controller(Controller):
-    def factory(self):
-        return Server(self.handler, enable_SMTPUTF8=True)
-
-
 class CustomHostnameController(Controller):
     def factory(self):
         return Server(self.handler, hostname='custom.localhost')
@@ -150,7 +145,7 @@ class TestProtocol(unittest.TestCase):
 
 class TestSMTP(unittest.TestCase):
     def setUp(self):
-        controller = UTF8Controller(Sink)
+        controller = DecodingController(Sink)
         controller.start()
         self.addCleanup(controller.stop)
         self.address = (controller.hostname, controller.port)
@@ -616,7 +611,7 @@ class TestSMTPWithController(unittest.TestCase):
                 b'Error: message size exceeds fixed maximum message size')
 
     def test_mail_with_compatible_smtputf8(self):
-        controller = SMTPUTF8Controller(Sink())
+        controller = Controller(Sink())
         controller.start()
         self.addCleanup(controller.stop)
         with SMTP(controller.hostname, controller.port) as client:
@@ -627,7 +622,7 @@ class TestSMTPWithController(unittest.TestCase):
             self.assertEqual(response, b'OK')
 
     def test_mail_with_unrequited_smtputf8(self):
-        controller = SMTPUTF8Controller(Sink())
+        controller = Controller(Sink())
         controller.start()
         self.addCleanup(controller.stop)
         with SMTP(controller.hostname, controller.port) as client:
@@ -637,7 +632,7 @@ class TestSMTPWithController(unittest.TestCase):
             self.assertEqual(response, b'OK')
 
     def test_mail_with_incompatible_smtputf8(self):
-        controller = SMTPUTF8Controller(Sink())
+        controller = Controller(Sink())
         controller.start()
         self.addCleanup(controller.stop)
         with SMTP(controller.hostname, controller.port) as client:
@@ -703,7 +698,7 @@ Testing
 
     def test_dots_escaped(self):
         handler = ReceivingHandler()
-        controller = UTF8Controller(handler)
+        controller = DecodingController(handler)
         controller.start()
         self.addCleanup(controller.stop)
         with SMTP(controller.hostname, controller.port) as client: