Repository: aio-libs/aiosmtpd

Issue:
Set the connection identifier without setting a module global
Currently, there's an `__version__` module global in smtp.py that is used when the client connects to the SMTP instance.  There's also an unused module global `__ident__`.  It should be possible to set these as attributes or arguments to the `SMTP` class, or at least without having to modify the module global.

Discussion:

Patch:
diff --git a/.gitignore b/.gitignore
index 4a71485..74def74 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,3 +27,5 @@ __pycache__/
 py34-cov
 py35-cov
 htmlcov
+coverage.xml
+diffcov.html
diff --git a/aiosmtpd/docs/NEWS.rst b/aiosmtpd/docs/NEWS.rst
index 433ed66..1ca0483 100644
--- a/aiosmtpd/docs/NEWS.rst
+++ b/aiosmtpd/docs/NEWS.rst
@@ -4,6 +4,8 @@
 
 1.0a4 (20XX-XX-XX)
 ==================
+* The SMTP server connection identifier can be changed by setting the
+  `__ident__` attribute on the `SMTP` instance.  (Closes #20)
 
 1.0a3 (2016-11-24)
 ==================
diff --git a/aiosmtpd/smtp.py b/aiosmtpd/smtp.py
index 01349de..569a4a1 100644
--- a/aiosmtpd/smtp.py
+++ b/aiosmtpd/smtp.py
@@ -36,6 +36,7 @@ class SMTP(asyncio.StreamReaderProtocol):
                  decode_data=False,
                  hostname=None,
                  loop=None):
+        self.__ident__ = __ident__
         self.loop = loop if loop else asyncio.get_event_loop()
         super().__init__(
             asyncio.StreamReader(loop=self.loop),
@@ -120,7 +121,7 @@ class SMTP(asyncio.StreamReaderProtocol):
     @asyncio.coroutine
     def _handle_client(self):
         log.info('handling connection')
-        yield from self.push('220 %s %s' % (self.hostname, __version__))
+        yield from self.push('220 {} {}'.format(self.hostname, self.__ident__))
         while not self.connection_closed:
             # XXX Put the line limit stuff into the StreamReader?
             line = yield from self._reader.readline()
diff --git a/tox.ini b/tox.ini
index 1bdd04a..0755ccc 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,20 +1,24 @@
 [tox]
-envlist = {py34,py35,py36}-{cov,nocov},qa
+envlist = {py34,py35,py36}-{cov,nocov,diffcov},qa
 recreate = True
 skip_missing_interpreters=True
 
 [testenv]
 commands =
     nocov: python -m nose2 -v {posargs}
-    cov: python -m coverage run {[coverage]rc} -m nose2 -v
-    cov: python -m coverage combine {[coverage]rc}
+    {cov,diffcov}: python -m coverage run {[coverage]rc} -m nose2 -v
+    {cov,diffcov}: python -m coverage combine {[coverage]rc}
     cov: python -m coverage html {[coverage]rc}
     cov: python -m coverage report -m {[coverage]rc} --fail-under=96
+    diffcov: python -m coverage xml {[coverage]rc}
+    diffcov: diff-cover coverage.xml --html-report diffcov.html
+    diffcov: diff-cover coverage.xml
 #sitepackages = True
 usedevelop = True
 deps =
     nose2
-    cov: coverage
+    {cov,diffcov}: coverage
+    diffcov: diff_cover
 setenv =
     cov: COVERAGE_PROCESS_START={[coverage]rcfile}
     cov: COVERAGE_OPTIONS="-p"


Test patch:
diff --git a/aiosmtpd/tests/test_smtp.py b/aiosmtpd/tests/test_smtp.py
index cdb39f4..c0173ff 100644
--- a/aiosmtpd/tests/test_smtp.py
+++ b/aiosmtpd/tests/test_smtp.py
@@ -5,7 +5,7 @@ import unittest
 
 from aiosmtpd.controller import Controller
 from aiosmtpd.handlers import Sink
-from aiosmtpd.smtp import SMTP as Server
+from aiosmtpd.smtp import SMTP as Server, __ident__ as GREETING
 from smtplib import SMTP, SMTPDataError
 
 
@@ -33,6 +33,13 @@ class CustomHostnameController(Controller):
         return Server(self.handler, hostname='custom.localhost')
 
 
+class CustomIdentController(Controller):
+    def factory(self):
+        server = Server(self.handler)
+        server.__ident__ = 'Identifying SMTP v2112'
+        return server
+
+
 class ErroringHandler:
     def process_message(self, peer, mailfrom, rcpttos, data, **kws):
         return '499 Could not accept the message'
@@ -534,15 +541,32 @@ Testing
                                  b'Could not accept the message')
 
 
-class TestCustomHostname(unittest.TestCase):
-    def setUp(self):
+class TestCustomizations(unittest.TestCase):
+    def test_custom_hostname(self):
         controller = CustomHostnameController(Sink)
         controller.start()
         self.addCleanup(controller.stop)
-        self.address = (controller.hostname, controller.port)
-
-    def test_helo(self):
-        with SMTP(*self.address) as client:
+        with SMTP(controller.hostname, controller.port) as client:
             code, response = client.helo('example.com')
             self.assertEqual(code, 250)
             self.assertEqual(response, bytes('custom.localhost', 'utf-8'))
+
+    def test_custom_greeting(self):
+        controller = CustomIdentController(Sink)
+        controller.start()
+        self.addCleanup(controller.stop)
+        with SMTP() as client:
+            code, msg = client.connect(controller.hostname, controller.port)
+            self.assertEqual(code, 220)
+            # The hostname prefix is unpredictable.
+            self.assertEqual(msg[-22:], b'Identifying SMTP v2112')
+
+    def test_default_greeting(self):
+        controller = Controller(Sink)
+        controller.start()
+        self.addCleanup(controller.stop)
+        with SMTP() as client:
+            code, msg = client.connect(controller.hostname, controller.port)
+            self.assertEqual(code, 220)
+            # The hostname prefix is unpredictable.
+            self.assertEqual(msg[-len(GREETING):], bytes(GREETING, 'utf-8'))