[Python-modules-commits] [python-aiosmtpd] 01/03: New upstream version 1.1

Pierre-Elliott Bécue peb-guest at moszumanska.debian.org
Thu Jul 6 21:06:58 UTC 2017


This is an automated email from the git hooks/post-receive script.

peb-guest pushed a commit to branch master
in repository python-aiosmtpd.

commit 9eb16b8caa08ad14068c661a14d9d43a94f0f306
Author: Pierre-Elliott Bécue <becue at crans.org>
Date:   Thu Jul 6 22:44:53 2017 +0200

    New upstream version 1.1
---
 .appveyor.yml                   |   5 -
 .coverage.ini                   |   1 -
 .travis.yml                     |   2 -
 MANIFEST.in                     |   2 +-
 PKG-INFO                        |  10 +-
 README.rst                      |   9 +-
 _static/.ignore                 |   0
 aiosmtpd.egg-info/PKG-INFO      |  10 +-
 aiosmtpd.egg-info/SOURCES.txt   |   5 +-
 aiosmtpd/controller.py          |  33 ++--
 aiosmtpd/docs/.gitignore        |   1 -
 aiosmtpd/docs/Makefile          | 177 -----------------
 aiosmtpd/docs/NEWS.rst          |  27 +++
 aiosmtpd/docs/controller.rst    |  12 +-
 aiosmtpd/docs/handlers.rst      |   2 +-
 aiosmtpd/docs/manpage.rst       |  66 +++++++
 aiosmtpd/docs/migrating.rst     |   6 +-
 aiosmtpd/docs/smtp.rst          |  19 +-
 aiosmtpd/handlers.py            |  20 +-
 aiosmtpd/lmtp.py                |  21 +-
 aiosmtpd/main.py                |  13 +-
 aiosmtpd/smtp.py                | 411 +++++++++++++++++++---------------------
 aiosmtpd/tests/test_handlers.py |  46 ++---
 aiosmtpd/tests/test_lmtp.py     |  10 +
 aiosmtpd/tests/test_main.py     | 164 ++++++++--------
 aiosmtpd/tests/test_smtp.py     | 159 +++++++++++++---
 aiosmtpd/tests/test_smtps.py    |  62 ++++++
 aiosmtpd/tests/test_starttls.py |  45 ++---
 setup.cfg                       |   3 +-
 setup.py                        |   7 +-
 tox.ini                         |   8 +-
 31 files changed, 693 insertions(+), 663 deletions(-)

diff --git a/.appveyor.yml b/.appveyor.yml
index 65f6768..194621e 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -7,13 +7,8 @@ environment:
     # For Python versions available on Appveyor, see
     # http://www.appveyor.com/docs/installed-software#python
 
-    - PYTHON: "C:\\Python34"
-      INTERP: "py34"
     - PYTHON: "C:\\Python35"
       INTERP: "py35"
-    - PYTHON: "C:\\Python34-x64"
-      INTERP: "py34"
-      DISTUTILS_USE_SDK: "1"
     - PYTHON: "C:\\Python35-x64"
       INTERP: "py35"
     - PYTHON: "C:\\Python36-x64"
diff --git a/.coverage.ini b/.coverage.ini
index 01f7db9..0bdcb4e 100644
--- a/.coverage.ini
+++ b/.coverage.ini
@@ -15,5 +15,4 @@ source =
 [report]
 exclude_lines =
     pragma: nocover
-    pragma: no${INTERP}
     pragma: no${PLATFORM}
diff --git a/.travis.yml b/.travis.yml
index a4511c7..6f1e947 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,8 +5,6 @@ install:
   - pip install -r aiosmtpd.egg-info/requires.txt
 matrix:
   include:
-    - python: "3.4"
-      env: INTERP=py34 PYTHONASYNCIODEBUG=1
     - python: "3.5"
       env: INTERP=py35 PYTHONASYNCIODEBUG=1
     - python: "3.6"
diff --git a/MANIFEST.in b/MANIFEST.in
index 1b19070..6c44541 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,4 +1,4 @@
 include *.py MANIFEST.in
 global-include *.txt *.rst *.ini *.yml *.cfg *.crt *.key
-exclude .gitignore
+global-exclude .gitignore
 prune build
diff --git a/PKG-INFO b/PKG-INFO
index f78f704..beb4413 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,9 +1,9 @@
 Metadata-Version: 1.1
 Name: aiosmtpd
-Version: 1.0
+Version: 1.1
 Summary: aiosmtpd - asyncio based SMTP server
-Home-page: https://github.com/aio-libs/aiosmtpd
-Author: https://github.com/aio-libs
+Home-page: http://aiosmtpd.readthedocs.io/
+Author: UNKNOWN
 Author-email: UNKNOWN
 License: http://www.apache.org/licenses/LICENSE-2.0
 Description: This is a server for SMTP and related protocols, similar in utility to the
@@ -13,8 +13,6 @@ Keywords: email
 Platform: UNKNOWN
 Classifier: License :: OSI Approved
 Classifier: Intended Audience :: Developers
-Classifier: Programming Language :: Python :: 3.4
-Classifier: Programming Language :: Python :: 3.5
-Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3
 Classifier: Topic :: Communications :: Email :: Mail Transport Agents
 Classifier: Framework :: AsyncIO
diff --git a/README.rst b/README.rst
index 7789d13..2d1db4d 100644
--- a/README.rst
+++ b/README.rst
@@ -25,10 +25,8 @@ protocols.
 Requirements
 ============
 
-You need at least Python 3.4 to use this library.  Python 3.3 might work if
-you install the standalone `asyncio <https://pypi.python.org/pypi/asyncio>`__
-library, but this combination is untested and unsupported.  Python 3.5 or
-newer is required for SSL support.  Both Windows and \*nix are supported.
+You need at least Python 3.5 to use this library.  Both Windows and \*nix are
+supported.
 
 
 License
@@ -74,7 +72,7 @@ Developing
 ==========
 
 You'll need the `tox <https://pypi.python.org/pypi/tox>`__ tool to run the
-test suite for Python 3.4, 3.5, and 3.6.  Once you've got that, run::
+test suite for Python 3.  Once you've got that, run::
 
     $ tox
 
@@ -107,6 +105,7 @@ Contents
    aiosmtpd/docs/lmtp
    aiosmtpd/docs/handlers
    aiosmtpd/docs/migrating
+   aiosmtpd/docs/manpage
    aiosmtpd/docs/NEWS
 
 
diff --git a/_static/.ignore b/_static/.ignore
deleted file mode 100644
index e69de29..0000000
diff --git a/aiosmtpd.egg-info/PKG-INFO b/aiosmtpd.egg-info/PKG-INFO
index f78f704..beb4413 100644
--- a/aiosmtpd.egg-info/PKG-INFO
+++ b/aiosmtpd.egg-info/PKG-INFO
@@ -1,9 +1,9 @@
 Metadata-Version: 1.1
 Name: aiosmtpd
-Version: 1.0
+Version: 1.1
 Summary: aiosmtpd - asyncio based SMTP server
-Home-page: https://github.com/aio-libs/aiosmtpd
-Author: https://github.com/aio-libs
+Home-page: http://aiosmtpd.readthedocs.io/
+Author: UNKNOWN
 Author-email: UNKNOWN
 License: http://www.apache.org/licenses/LICENSE-2.0
 Description: This is a server for SMTP and related protocols, similar in utility to the
@@ -13,8 +13,6 @@ Keywords: email
 Platform: UNKNOWN
 Classifier: License :: OSI Approved
 Classifier: Intended Audience :: Developers
-Classifier: Programming Language :: Python :: 3.4
-Classifier: Programming Language :: Python :: 3.5
-Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3
 Classifier: Topic :: Communications :: Email :: Mail Transport Agents
 Classifier: Framework :: AsyncIO
diff --git a/aiosmtpd.egg-info/SOURCES.txt b/aiosmtpd.egg-info/SOURCES.txt
index 5573d7f..56c4a33 100644
--- a/aiosmtpd.egg-info/SOURCES.txt
+++ b/aiosmtpd.egg-info/SOURCES.txt
@@ -9,7 +9,6 @@ setup.py
 setup_helpers.py
 tox.ini
 unittest.cfg
-_static/.ignore
 aiosmtpd/__init__.py
 aiosmtpd/__main__.py
 aiosmtpd/controller.py
@@ -23,8 +22,6 @@ aiosmtpd.egg-info/dependency_links.txt
 aiosmtpd.egg-info/entry_points.txt
 aiosmtpd.egg-info/requires.txt
 aiosmtpd.egg-info/top_level.txt
-aiosmtpd/docs/.gitignore
-aiosmtpd/docs/Makefile
 aiosmtpd/docs/NEWS.rst
 aiosmtpd/docs/__init__.py
 aiosmtpd/docs/cli.rst
@@ -33,6 +30,7 @@ aiosmtpd/docs/controller.rst
 aiosmtpd/docs/handlers.rst
 aiosmtpd/docs/intro.rst
 aiosmtpd/docs/lmtp.rst
+aiosmtpd/docs/manpage.rst
 aiosmtpd/docs/migrating.rst
 aiosmtpd/docs/smtp.rst
 aiosmtpd/testing/__init__.py
@@ -43,6 +41,7 @@ aiosmtpd/tests/test_lmtp.py
 aiosmtpd/tests/test_main.py
 aiosmtpd/tests/test_server.py
 aiosmtpd/tests/test_smtp.py
+aiosmtpd/tests/test_smtps.py
 aiosmtpd/tests/test_starttls.py
 aiosmtpd/tests/certs/__init__.py
 aiosmtpd/tests/certs/server.crt
diff --git a/aiosmtpd/controller.py b/aiosmtpd/controller.py
index 8771cb1..c529ff9 100644
--- a/aiosmtpd/controller.py
+++ b/aiosmtpd/controller.py
@@ -1,42 +1,26 @@
 import os
-import socket
 import asyncio
 import threading
 
 from aiosmtpd.smtp import SMTP
 from public import public
 
-try:
-    from socket import socketpair
-except ImportError:                                          # pragma: nocover
-    from asyncio.windows_utils import socketpair
-
 
 @public
 class Controller:
     def __init__(self, handler, loop=None, hostname=None, port=8025, *,
-                 ready_timeout=1.0, enable_SMTPUTF8=True):
+                 ready_timeout=1.0, enable_SMTPUTF8=True, ssl_context=None):
         self.handler = handler
         self.hostname = '::1' if hostname is None else hostname
         self.port = port
         self.enable_SMTPUTF8 = enable_SMTPUTF8
+        self.ssl_context = ssl_context
         self.loop = asyncio.new_event_loop() if loop is None else loop
         self.server = None
         self._thread = None
         self._thread_exception = None
         self.ready_timeout = os.getenv(
             'AIOSMTPD_CONTROLLER_TIMEOUT', ready_timeout)
-        # For exiting the loop.
-        self._rsock, self._wsock = socketpair()
-        self.loop.add_reader(self._rsock, self._reader)
-
-    def _reader(self):
-        self.loop.remove_reader(self._rsock)
-        self.loop.stop()
-        for task in asyncio.Task.all_tasks(self.loop):
-            task.cancel()
-        self._rsock.close()
-        self._wsock.close()
 
     def factory(self):
         """Allow subclasses to customize the handler/server creation."""
@@ -47,8 +31,9 @@ class Controller:
         try:
             self.server = self.loop.run_until_complete(
                 self.loop.create_server(
-                    self.factory, host=self.hostname, port=self.port))
-        except socket.error as error:
+                    self.factory, host=self.hostname, port=self.port,
+                    ssl=self.ssl_context))
+        except Exception as error:
             self._thread_exception = error
             return
         self.loop.call_soon(ready_event.set)
@@ -69,7 +54,13 @@ class Controller:
         if self._thread_exception is not None:
             raise self._thread_exception
 
+    def _stop(self):
+        self.loop.stop()
+        for task in asyncio.Task.all_tasks(self.loop):
+            task.cancel()
+
     def stop(self):
         assert self._thread is not None, 'SMTP daemon not running'
-        self._wsock.send(b'x')
+        self.loop.call_soon_threadsafe(self._stop)
         self._thread.join()
+        self._thread = None
diff --git a/aiosmtpd/docs/.gitignore b/aiosmtpd/docs/.gitignore
deleted file mode 100644
index e35d885..0000000
--- a/aiosmtpd/docs/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-_build
diff --git a/aiosmtpd/docs/Makefile b/aiosmtpd/docs/Makefile
deleted file mode 100644
index be5d54d..0000000
--- a/aiosmtpd/docs/Makefile
+++ /dev/null
@@ -1,177 +0,0 @@
-# Makefile for Sphinx documentation
-#
-
-# You can set these variables from the command line.
-SPHINXOPTS    =
-SPHINXBUILD   = sphinx-build
-PAPER         =
-BUILDDIR      = _build
-
-# User-friendly check for sphinx-build
-ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
-$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
-endif
-
-# Internal variables.
-PAPEROPT_a4     = -D latex_paper_size=a4
-PAPEROPT_letter = -D latex_paper_size=letter
-ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-# the i18n builder cannot share the environment and doctrees with the others
-I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-
-.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
-
-help:
-	@echo "Please use \`make <target>' where <target> is one of"
-	@echo "  html       to make standalone HTML files"
-	@echo "  dirhtml    to make HTML files named index.html in directories"
-	@echo "  singlehtml to make a single large HTML file"
-	@echo "  pickle     to make pickle files"
-	@echo "  json       to make JSON files"
-	@echo "  htmlhelp   to make HTML files and a HTML help project"
-	@echo "  qthelp     to make HTML files and a qthelp project"
-	@echo "  devhelp    to make HTML files and a Devhelp project"
-	@echo "  epub       to make an epub"
-	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
-	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
-	@echo "  latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
-	@echo "  text       to make text files"
-	@echo "  man        to make manual pages"
-	@echo "  texinfo    to make Texinfo files"
-	@echo "  info       to make Texinfo files and run them through makeinfo"
-	@echo "  gettext    to make PO message catalogs"
-	@echo "  changes    to make an overview of all changed/added/deprecated items"
-	@echo "  xml        to make Docutils-native XML files"
-	@echo "  pseudoxml  to make pseudoxml-XML files for display purposes"
-	@echo "  linkcheck  to check all external links for integrity"
-	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
-
-clean:
-	rm -rf $(BUILDDIR)/*
-
-html:
-	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
-	@echo
-	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
-
-dirhtml:
-	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
-	@echo
-	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
-
-singlehtml:
-	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
-	@echo
-	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
-
-pickle:
-	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
-	@echo
-	@echo "Build finished; now you can process the pickle files."
-
-json:
-	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
-	@echo
-	@echo "Build finished; now you can process the JSON files."
-
-htmlhelp:
-	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
-	@echo
-	@echo "Build finished; now you can run HTML Help Workshop with the" \
-	      ".hhp project file in $(BUILDDIR)/htmlhelp."
-
-qthelp:
-	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
-	@echo
-	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
-	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
-	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/aiosmtpd.qhcp"
-	@echo "To view the help file:"
-	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/aiosmtpd.qhc"
-
-devhelp:
-	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
-	@echo
-	@echo "Build finished."
-	@echo "To view the help file:"
-	@echo "# mkdir -p $$HOME/.local/share/devhelp/aiosmtpd"
-	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/aiosmtpd"
-	@echo "# devhelp"
-
-epub:
-	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
-	@echo
-	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
-
-latex:
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
-	@echo
-	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
-	@echo "Run \`make' in that directory to run these through (pdf)latex" \
-	      "(use \`make latexpdf' here to do that automatically)."
-
-latexpdf:
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
-	@echo "Running LaTeX files through pdflatex..."
-	$(MAKE) -C $(BUILDDIR)/latex all-pdf
-	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
-
-latexpdfja:
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
-	@echo "Running LaTeX files through platex and dvipdfmx..."
-	$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
-	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
-
-text:
-	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
-	@echo
-	@echo "Build finished. The text files are in $(BUILDDIR)/text."
-
-man:
-	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
-	@echo
-	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
-
-texinfo:
-	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
-	@echo
-	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
-	@echo "Run \`make' in that directory to run these through makeinfo" \
-	      "(use \`make info' here to do that automatically)."
-
-info:
-	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
-	@echo "Running Texinfo files through makeinfo..."
-	make -C $(BUILDDIR)/texinfo info
-	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
-
-gettext:
-	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
-	@echo
-	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
-
-changes:
-	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
-	@echo
-	@echo "The overview file is in $(BUILDDIR)/changes."
-
-linkcheck:
-	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
-	@echo
-	@echo "Link check complete; look for any errors in the above output " \
-	      "or in $(BUILDDIR)/linkcheck/output.txt."
-
-doctest:
-	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
-	@echo "Testing of doctests in the sources finished, look at the " \
-	      "results in $(BUILDDIR)/doctest/output.txt."
-
-xml:
-	$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
-	@echo
-	@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
-
-pseudoxml:
-	$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
-	@echo
-	@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
diff --git a/aiosmtpd/docs/NEWS.rst b/aiosmtpd/docs/NEWS.rst
index 66084eb..5260f73 100644
--- a/aiosmtpd/docs/NEWS.rst
+++ b/aiosmtpd/docs/NEWS.rst
@@ -2,6 +2,33 @@
  NEWS for aiosmtpd
 ===================
 
+1.1 (2017-07-06)
+================
+* Drop support for Python 3.4.
+* As per RFC 5321, §4.1.4, multiple ``HELO`` / ``EHLO`` commands in the same
+  session are semantically equivalent to ``RSET``.  (Closes #78)
+* As per RFC 5321, $4.1.1.9, ``NOOP`` takes an optional argument, which is
+  ignored.  **API BREAK** If you have a handler that implements
+  ``handle_NOOP()``, it previously took zero arguments but now requires a
+  single argument.  (Closes #107)
+* The command line options ``--version`` / ``-v`` has been added to print the
+  package's current version number.  (Closes #111)
+* General improvements in the ``Controller`` class.  (Closes #104)
+* When aiosmtpd handles a ``STARTTLS`` it must arrange for the original
+  transport to be closed when the wrapped transport is closed.  This fixes a
+  hidden exception which occurs when an EOF is received on the original
+  tranport after the connection is lost.  (Closes #83)
+* Widen the catch of ``ConnectionResetError`` and ``CancelledError`` to also
+  catch such errors from handler methods.  (Closes #110)
+* Added a manpage for the ``aiosmtpd`` command line script.  (Closes #116)
+* Added much better support for the ``HELP``.  There's a new decorator called
+  ``@syntax()`` which you can use in derived classes to decorate ``smtp_*()``
+  methods.  These then show up in ``HELP`` responses.  This also fixes
+  ``HELP`` responses for the ``LMTP`` subclass.  (Closes #113)
+* The ``Controller`` class now takes an optional keyword argument
+  ``ssl_context`` which is passed directly to the asyncio ``create_server()``
+  call.
+
 1.0 (2017-05-15)
 ================
 * Release.
diff --git a/aiosmtpd/docs/controller.rst b/aiosmtpd/docs/controller.rst
index 72e860c..ca134b6 100644
--- a/aiosmtpd/docs/controller.rst
+++ b/aiosmtpd/docs/controller.rst
@@ -30,15 +30,13 @@ to the console.  Start by implementing a handler as follows::
 
     >>> import asyncio
     >>> class ExampleHandler:
-    ...     @asyncio.coroutine
-    ...     def handle_RCPT(self, server, session, envelope, address, rcpt_options):
+    ...     async def handle_RCPT(self, server, session, envelope, address, rcpt_options):
     ...         if not address.endswith('@example.com'):
     ...             return '550 not relaying to that domain'
     ...         envelope.rcpt_tos.append(address)
     ...         return '250 OK'
     ...
-    ...     @asyncio.coroutine
-    ...     def handle_DATA(self, server, session, envelope):
+    ...     async def handle_DATA(self, server, session, envelope):
     ...         print('Message from %s' % envelope.mail_from)
     ...         print('Message for %s' % envelope.rcpt_tos)
     ...         print('Message data:\n')
@@ -163,7 +161,7 @@ The EHLO response does not include the ``SMTPUTF8`` ESMTP option.
 Controller API
 ==============
 
-.. class:: Controller(handler, loop=None, hostname=None, port=8025, *, ready_timeout=1.0, enable_SMTPUTF8=True)
+.. class:: Controller(handler, loop=None, hostname=None, port=8025, *, ready_timeout=1.0, enable_SMTPUTF8=True, ssl_context=None)
 
    *handler* is an instance of a :ref:`handler <handlers>` class.
 
@@ -184,6 +182,10 @@ Controller API
    option is returned to the client in response to ``EHLO``, and UTF-8 content
    is accepted.
 
+   *ssl_context* is a ``SSLContext`` that will be used by the loops
+   server and is passed directly to :meth:`AbstractEventLoop.create_server`
+   method.
+
    .. attribute:: handler
 
       The instance of the event *handler* passed to the constructor.
diff --git a/aiosmtpd/docs/handlers.rst b/aiosmtpd/docs/handlers.rst
index ea1bfa7..6a1657e 100644
--- a/aiosmtpd/docs/handlers.rst
+++ b/aiosmtpd/docs/handlers.rst
@@ -52,7 +52,7 @@ The following hooks are currently defined:
     ``server.push(status)`` before returning ``250 HELP`` as the final
     response.
 
-``handle_NOOP(server, session, envelope)``
+``handle_NOOP(server, session, envelope, arg)``
     Called during ``NOOP``.
 
 ``handle_QUIT(server, session, envelope)``
diff --git a/aiosmtpd/docs/manpage.rst b/aiosmtpd/docs/manpage.rst
new file mode 100644
index 0000000..ddf099a
--- /dev/null
+++ b/aiosmtpd/docs/manpage.rst
@@ -0,0 +1,66 @@
+==========
+ aiosmtpd
+==========
+
+-----------------------------------------------------
+Provide a Simple Mail Transfer Procotol (SMTP) server
+-----------------------------------------------------
+
+:Author: The aiosmtpd developers
+:Date: 2017-07-01
+:Copyright: 2015-2017 The aiosmtpd developrs
+:Version: 1.1
+:Manual section: 1
+
+
+SYNOPSIS
+========
+
+aiosmtpd [options]
+
+
+Description
+===========
+
+This program provides an RFC 5321 compliant SMTP server that supports
+customizable extensions.
+
+
+OPTIONS
+=======
+-h, --help
+    Show this help message and exit
+
+-v, --version
+    Show program's version number and exit.
+
+-n, --nosetuid
+    This program generally tries to setuid ``nobody``, unless this flag is
+    set.  The setuid call will fail if this program is not run as root (in
+    which case, use this flag).
+
+-c CLASSPATH, --class CLASSPATH
+    Use the given class (as a Python dotted import path) as the handler class
+    for SMTP events.  This class can process received messages and do other
+    actions during the SMTP dialog.  If not give, this uses a debugging
+    handler by default.
+
+    When given all remaining positional arguments are passed as arguments to
+    the class's ``@classmethod from_cli()`` method, which should do any
+    appropriate type conversion, and then return an instance of the handler
+    class.
+
+-s SIZE, --size SIZE
+    Restrict the total size of the incoming message to SIZE number of bytes
+    via the RFC 1870 SIZE extension.  Defaults to 33554432 bytes.
+
+-u, --smtputf8
+    Enable the SMTPUTF8 extension and behave as an RFC 6531 SMTP proxy.
+
+-d, --debug
+    Increase debugging output.
+
+-l [HOST:PORT], --listen [HOST:PORT]
+    Optional host and port to listen on. If the PORT part is not given, then
+    port 8025 is used. If only :PORT is given, then localhost is used for the
+    hostname. If neither are given, localhost:8025 is used.
diff --git a/aiosmtpd/docs/migrating.rst b/aiosmtpd/docs/migrating.rst
index 5f09cb6..fdd7ab2 100644
--- a/aiosmtpd/docs/migrating.rst
+++ b/aiosmtpd/docs/migrating.rst
@@ -30,8 +30,7 @@ the ``handle_DATA()`` method::
     from aiosmtpd.controller import Controller
 
     class CustomHandler:
-        @asyncio.coroutine
-        def handle_DATA(self, server, session, envelope):
+        async def handle_DATA(self, server, session, envelope):
             peer = session.peer
             mail_from = envelope.mail_from
             rcpt_tos = envelope.rcpt_tos
@@ -55,7 +54,6 @@ Important differences to note:
 * Unlike ``process_message()`` in smtpd, ``handle_DATA()`` **must** return
   an SMTP response code for the sender such as ``"250 OK"``.
 * ``handle_DATA()`` must be a coroutine function, which means it must be
-  declared with ``@asyncio.coroutine`` (or ``async def`` for Python 3.5 and
-  newer).
+  declared with ``async def``.
 * ``controller.start()`` runs the SMTP server in a separate thread and can be
   stopped again by calling ``controller.stop()``.
diff --git a/aiosmtpd/docs/smtp.rst b/aiosmtpd/docs/smtp.rst
index 215a198..de2aebc 100644
--- a/aiosmtpd/docs/smtp.rst
+++ b/aiosmtpd/docs/smtp.rst
@@ -28,11 +28,11 @@ All methods implementing ``SMTP`` commands are prefixed with ``smtp_``; they
 must also be coroutines.  Here's how you could implement this use case::
 
     >>> import asyncio
-    >>> from aiosmtpd.smtp import SMTP as Server
+    >>> from aiosmtpd.smtp import SMTP as Server, syntax
     >>> class MyServer(Server):
-    ...     @asyncio.coroutine
-    ...     def smtp_PING(self, arg):
-    ...         yield from self.push('259 Pong')
+    ...     @syntax('PING [ignored]')
+    ...     async def smtp_PING(self, arg):
+    ...         await self.push('259 Pong')
 
 Now let's run this server in a controller::
 
@@ -62,6 +62,17 @@ command, we have to use the lower level interface to talk to it.
     >>> message
     b'Pong'
 
+Because we prefixed the ``smtp_PING()`` method with the ``@syntax()``
+decorator, the command shows up in the ``HELP`` output.
+
+    >>> print(client.help().decode('utf-8'))
+    Supported commands: DATA EHLO HELO HELP MAIL NOOP PING QUIT RCPT RSET VRFY
+
+And we can get more detailed help on the new command.
+
+    >>> print(client.help('PING').decode('utf-8'))
+    Syntax: PING [ignored]
+
 
 Server hooks
 ============
diff --git a/aiosmtpd/handlers.py b/aiosmtpd/handlers.py
index 30a1c56..0107c76 100644
--- a/aiosmtpd/handlers.py
+++ b/aiosmtpd/handlers.py
@@ -65,8 +65,7 @@ class Debugging:
                 line = line.decode('utf-8', 'replace')
             print(line, file=self.stream)
 
-    @asyncio.coroutine
-    def handle_DATA(self, server, session, envelope):
+    async def handle_DATA(self, server, session, envelope):
         print('---------- MESSAGE FOLLOWS ----------', file=self.stream)
         # Yes, actually test for truthiness since it's possible for either the
         # keywords to be missing, or for their values to be empty lists.
@@ -92,8 +91,7 @@ class Proxy:
         self._hostname = remote_hostname
         self._port = remote_port
 
-    @asyncio.coroutine
-    def handle_DATA(self, server, session, envelope):
+    async def handle_DATA(self, server, session, envelope):
         if isinstance(envelope.content, str):
             content = envelope.original_content
         else:
@@ -108,8 +106,7 @@ class Proxy:
                 break
             i += 1
         peer = session.peer[0].encode('ascii')
-        # XXX Python 3.4 does not support %-interpolation for bytes objects.
-        lines.insert(i, b'X-Peer: ' + peer + ending)
+        lines.insert(i, b'X-Peer: %s%s' % (peer, ending))
         data = EMPTYBYTES.join(lines)
         refused = self._deliver(envelope.mail_from, envelope.rcpt_tos, data)
         # TBD: what to do with refused addresses?
@@ -154,8 +151,7 @@ class Message:
     def __init__(self, message_class=None):
         self.message_class = message_class
 
-    @asyncio.coroutine
-    def handle_DATA(self, server, session, envelope):
+    async def handle_DATA(self, server, session, envelope):
         envelope = self.prepare_message(session, envelope)
         self.handle_message(envelope)
         return '250 OK'
@@ -185,14 +181,12 @@ class AsyncMessage(Message):
         super().__init__(message_class)
         self.loop = loop or asyncio.get_event_loop()
 
-    @asyncio.coroutine
-    def handle_DATA(self, server, session, envelope):
+    async def handle_DATA(self, server, session, envelope):
         message = self.prepare_message(session, envelope)
-        yield from self.handle_message(message)
+        await self.handle_message(message)
         return '250 OK'
 
-    @asyncio.coroutine
-    def handle_message(self, message):
+    async def handle_message(self, message):
         raise NotImplementedError                   # pragma: nocover
 
 
diff --git a/aiosmtpd/lmtp.py b/aiosmtpd/lmtp.py
index 0e29913..53ed087 100644
--- a/aiosmtpd/lmtp.py
+++ b/aiosmtpd/lmtp.py
@@ -1,22 +1,19 @@
-import asyncio
-
-from aiosmtpd.smtp import SMTP
+from aiosmtpd.smtp import SMTP, syntax
 from public import public
 
 
 @public
 class LMTP(SMTP):
-    @asyncio.coroutine
-    def smtp_LHLO(self, arg):
+    @syntax('LHLO hostname')
+    async def smtp_LHLO(self, arg):
         """The LMTP greeting, used instead of HELO/EHLO."""
-        yield from super().smtp_HELO(arg)
+        await super().smtp_HELO(arg)
+        self.show_smtp_greeting = False
 
-    @asyncio.coroutine
-    def smtp_HELO(self, arg):
+    async def smtp_HELO(self, arg):
         """HELO is not a valid LMTP command."""
-        yield from self.push('500 Error: command "HELO" not recognized')
+        await self.push('500 Error: command "HELO" not recognized')
 
-    @asyncio.coroutine
-    def smtp_EHLO(self, arg):
+    async def smtp_EHLO(self, arg):
         """EHLO is not a valid LMTP command."""
-        yield from self.push('500 Error: command "EHLO" not recognized')
+        await self.push('500 Error: command "EHLO" not recognized')
diff --git a/aiosmtpd/main.py b/aiosmtpd/main.py
index 40fca4c..5b92daa 100644
--- a/aiosmtpd/main.py
+++ b/aiosmtpd/main.py
@@ -4,8 +4,9 @@ import signal
 import asyncio
 import logging
 
-from aiosmtpd.smtp import DATA_SIZE_DEFAULT, SMTP
+from aiosmtpd.smtp import DATA_SIZE_DEFAULT, SMTP, __version__
 from argparse import ArgumentParser
+from contextlib import suppress
 from functools import partial
 from importlib import import_module
 from public import public
@@ -29,9 +30,12 @@ def parseargs(args=None):
         prog=PROGRAM,
         description='An RFC 5321 SMTP server with extensions.')
     parser.add_argument(
+        '-v', '--version', action='version',
+        version='%(prog)s {}'.format(__version__))
+    parser.add_argument(
         '-n', '--nosetuid',
         dest='setuid', default=True, action='store_false',
-        help="""This program generally tries to setuid `nobody', unless this
+        help="""This program generally tries to setuid `nobody`, unless this
                 flag is set.  The setuid call will fail if this program is not
                 run as root (in which case, use this flag).""")
     parser.add_argument(
@@ -134,7 +138,10 @@ def main(args=None):
 
     server = loop.run_until_complete(
         loop.create_server(factory, host=args.host, port=args.port))
-    loop.add_signal_handler(signal.SIGINT, loop.stop)
+    # Signal handlers are only supported on *nix, so just ignore the failure
+    # to set this on Windows.
+    with suppress(NotImplementedError):
+        loop.add_signal_handler(signal.SIGINT, loop.stop)
 
     log.info('Starting asyncio loop')
     try:
diff --git a/aiosmtpd/smtp.py b/aiosmtpd/smtp.py
index 60d2b4c..5830645 100644
--- a/aiosmtpd/smtp.py
+++ b/aiosmtpd/smtp.py
@@ -1,24 +1,17 @@
+import ssl
 import socket
 import asyncio
 import logging
 import collections
 
+from asyncio import sslproto
 from email._header_value_parser import get_addr_spec, get_angle_addr
 from email.errors import HeaderParseError
 from public import public
 from warnings import warn
 
 
-try:
-    import ssl
-    from asyncio import sslproto
-except ImportError:                                 # pragma: nocover
-    _has_ssl = False
-else:                                               # pragma: nocover
-    _has_ssl = sslproto and hasattr(ssl, 'MemoryBIO')
-
-
-__version__ = '1.0'
+__version__ = '1.1'
 __ident__ = 'Python SMTP {}'.format(__version__)
 log = logging.getLogger('mail.log')
 
@@ -58,6 +51,15 @@ def make_loop():
     return asyncio.get_event_loop()
 
 
+def syntax(text, extended=None, when=None):
+    def decorator(f):
+        f.__smtp_syntax__ = text
+        f.__smtp_syntax_extended__ = extended
+        f.__smtp_syntax_when__ = when
+        return f
+    return decorator
+
+
 @public
 class SMTP(asyncio.StreamReaderProtocol):
     command_size_limit = 512
@@ -97,6 +99,7 @@ class SMTP(asyncio.StreamReaderProtocol):
         self.require_starttls = tls_context and require_starttls
         self._tls_handshake_okay = True
         self._tls_protocol = None
+        self._original_transport = None
         self.session = None
         self.envelope = None
         self.transport = None
@@ -108,12 +111,11 @@ class SMTP(asyncio.StreamReaderProtocol):
     def _create_envelope(self):
         return Envelope()
 
-    @asyncio.coroutine
-    def _call_handler_hook(self, command, *args):
+    async def _call_handler_hook(self, command, *args):
         hook = getattr(self.event_handler, 'handle_' + command, None)
         if hook is None:
             return MISSING
-        status = yield from hook(self, self.session, self.envelope, *args)
+        status = await hook(self, self.session, self.envelope, *args)
         return status
 
     @property
@@ -128,15 +130,14 @@ class SMTP(asyncio.StreamReaderProtocol):
         self._set_rset_state()
         self.session = self._create_session()
         self.session.peer = transport.get_extra_info('peername')
-        is_instance = (_has_ssl and
-                       isinstance(transport, sslproto._SSLProtocolTransport))
-        if self.transport is not None and is_instance:   # pragma: nopy34
+        seen_starttls = (self._original_transport is not None)
+        if self.transport is not None and seen_starttls:
             # It is STARTTLS connection over normal connection.
             self._reader._transport = transport
             self._writer._transport = transport
             self.transport = transport
-            # Do SSL certificate checking as rfc3207 part 4.1 says.
-            # Why _extra is protected attribute?
+            # Do SSL certificate checking as rfc3207 part 4.1 says.  Why is
+            # _extra a protected attribute?
             self.session.ssl = self._tls_protocol._extra
             handler = getattr(self.event_handler, 'handle_STARTTLS', None)
             if handler is None:
@@ -154,21 +155,38 @@ class SMTP(asyncio.StreamReaderProtocol):
 
     def connection_lost(self, error):
         log.info('%r connection lost', self.session.peer)
+        # If STARTTLS was issued, then our transport is the SSL protocol
+        # transport, and we need to close the original transport explicitly,
+        # otherwise an unexpected eof_received() will be called *after* the
+        # connection_lost().  At that point the stream reader will already be
+        # destroyed and we'll get a traceback in super().eof_received() below.
+        if self._original_transport is not None:
+            self._original_transport.close()
         super().connection_lost(error)
-        self._writer.close()
+        self._handler_coroutine.cancel()
         self.transport = None
 
+    def eof_received(self):
+        log.info('%r EOF received', self.session.peer)
+        self._handler_coroutine.cancel()
+        if self.session.ssl is not None:            # pragma: nomswin
+            # If STARTTLS was issued, return False, because True has no effect
+            # on an SSL transport and raises a warning. Our superclass has no
+            # way of knowing we switched to SSL so it might return True.
+            #
+            # This entire method seems not to be called during any of the
+            # starttls tests on Windows.  I don't really know why, but it
+            # causes these lines to fail coverage, hence the `nomswin` pragma
+            # above.
+            return False
+        return super().eof_received()
+
     def _client_connected_cb(self, reader, writer):
         # This is redundant since we subclass StreamReaderProtocol, but I like
         # the shorter names.
         self._reader = reader
         self._writer = writer
 
-    def eof_received(self):
-        log.info('%r EOF received', self.session.peer)
-        self._handler_coroutine.cancel()
-        return super().eof_received()
-
     def _set_post_data_state(self):
         """Reset state variables to their post-DATA state."""
         self.envelope = self._create_envelope()
@@ -177,18 +195,16 @@ class SMTP(asyncio.StreamReaderProtocol):
         """Reset all state variables except the greeting."""
         self._set_post_data_state()
 
-    @asyncio.coroutine
-    def push(self, status):
+    async def push(self, status):
         response = bytes(
             status + '\r\n', 'utf-8' if self.enable_SMTPUTF8 else 'ascii')
         self._writer.write(response)
         log.debug(response)
-        yield from self._writer.drain()
+        await self._writer.drain()
 
-    @asyncio.coroutine
... 1479 lines suppressed ...

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-aiosmtpd.git



More information about the Python-modules-commits mailing list