Repository: aio-libs/aiosmtpd
Pull request #200 (2020-10-30 14:25:01)

Release helpers
This adds a release helper script - basically what I use to upload releases to pypi myself. I don't have any emotional investment in the configuration of this script - it's just me trying to improve that bus factor. If there are improvements then they are very welcome!

Anyway, the idea is that you should be able to create a pypi token and setup a `~/.pypirc`, install necessary packages (e.g. twine), and then you'd be able to run this script any time you needed to release!

In the process of setting up this script, the twine check informed me of the missing field, so I went ahead and fixed that.


Patch:
diff --git a/.coveragerc b/.coverage.ini
similarity index 67%
rename from .coveragerc
rename to .coverage.ini
index 0ed6f61..0bdcb4e 100644
--- a/.coveragerc
+++ b/.coverage.ini
@@ -6,16 +6,13 @@ omit =
      aiosmtpd/testing/*
      aiosmtpd/tests/*
     .tox/*/lib/python3.*/site-packages/*
-    .tox/*/Lib/site-packages/*
-    .tox/Lib/site-packages/*
-    .tox/*/site-packages/*
 
 [paths]
 source =
     aiosmtpd
+    .tox/*/lib/python*/site-packages/aiosmtpd
 
 [report]
 exclude_lines =
     pragma: nocover
     pragma: no${PLATFORM}
-    pragma: skipif_${PY_39}_py39
diff --git a/.gitignore b/.gitignore
index 34d0ea4..ddc3953 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,13 +20,10 @@ var/*
 .tox
 .DS_Store
 .coverage
-.coverage.*.*
 .noseids
 docs/_build/
 nosetests.xml
 __pycache__/
-htmlcov/
-_dynamic/
+htmlcov
 coverage.xml
 diffcov.html
-.python-version
diff --git a/.travis.yml b/.travis.yml
index 8394a76..7105782 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,19 +5,20 @@ install:
   - pip install -r aiosmtpd.egg-info/requires.txt
 matrix:
   include:
+    - python: "3.5"
+      env: INTERP=py35 PYTHONASYNCIODEBUG=1
     - python: "3.6"
-      env: INTERP=py36
+      env: INTERP=py36 PYTHONASYNCIODEBUG=1
     - python: "3.7"
-      env: INTERP=py37
-    - python: "3.8"
-      env: INTERP=py38
-    - python: "pypy3"
-      env: INTERP=pypy3
+      env: INTERP=py37 PYTHONASYNCIODEBUG=1
+      dist: xenial
 before_script:
   # Disable IPv6. Ref travis-ci/travis-ci#8711
   - echo 0 | sudo tee /proc/sys/net/ipv6/conf/all/disable_ipv6
 script:
   - tox -e $INTERP-nocov,$INTERP-cov,qa,docs
   - 'if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then tox -e $INTERP-diffcov; fi'
+before_script:
+  - echo 0 | sudo tee /proc/sys/net/ipv6/conf/all/disable_ipv6
 after_success:
   - bash <(curl -s https://codecov.io/bash)
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index d645695..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,202 +0,0 @@
-
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
-
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-   1. Definitions.
-
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "[]"
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same "printed page" as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright [yyyy] [name of copyright owner]
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
diff --git a/README.rst b/README.rst
index da032ab..2d1db4d 100644
--- a/README.rst
+++ b/README.rst
@@ -2,32 +2,6 @@
  aiosmtpd - An asyncio based SMTP server
 =========================================
 
-| |github license| |travis ci| |codecov| |LGTM.com| |readthedocs| |PyPI|
-|
-| |Discourse|
-
-.. |github license| image:: https://img.shields.io/github/license/aio-libs/aiosmtpd
-   :target: https://github.com/aio-libs/aiosmtpd/blob/master/LICENSE
-   :alt: Project License on GitHub
-.. |travis ci| image:: https://travis-ci.com/aio-libs/aiosmtpd.svg?branch=master
-   :target: https://travis-ci.com/github/aio-libs/aiosmtpd
-   :alt: Travis CI Build Status
-.. |codecov| image:: https://codecov.io/github/aio-libs/aiosmtpd/coverage.svg?branch=master
-   :target: https://codecov.io/github/aio-libs/aiosmtpd?branch=master
-   :alt: Code Coverage
-.. |LGTM.com| image:: https://img.shields.io/lgtm/grade/python/github/aio-libs/aiosmtpd.svg?logo=lgtm&logoWidth=18
-   :target: https://lgtm.com/projects/g/aio-libs/aiosmtpd/context:python
-   :alt: Semmle/LGTM.com quality
-.. |readthedocs| image:: https://readthedocs.org/projects/aiosmtpd/badge/?version=latest
-   :target: https://aiosmtpd.readthedocs.io/en/latest/?badge=latest
-   :alt: Documentation Status
-.. |PyPI| image:: https://badge.fury.io/py/aiosmtpd.svg
-   :target: https://badge.fury.io/py/aiosmtpd
-   :alt: PyPI Package
-.. |Discourse| image:: https://img.shields.io/discourse/status?server=https%3A%2F%2Faio-libs.discourse.group%2F&style=social
-   :target: https://aio-libs.discourse.group/
-   :alt: Discourse status
-
 The Python standard library includes a basic
 `SMTP <http://www.faqs.org/rfcs/rfc5321.html>`__ server in the
 `smtpd <https://docs.python.org/3/library/smtpd.html>`__ module, based on the
@@ -51,7 +25,7 @@ protocols.
 Requirements
 ============
 
-You need **at least Python 3.6** to use this library.  Both Windows and \*nix are
+You need at least Python 3.5 to use this library.  Both Windows and \*nix are
 supported.
 
 
@@ -104,84 +78,17 @@ test suite for Python 3.  Once you've got that, run::
 
 Individual tests can be run like this::
 
-    $ tox -e py36-nocov -- -P <pattern>
+    $ tox -e py35-nocov -- -P <pattern>
 
 where *<pattern>* is a Python regular expression matching a test name.
 
 You can also add the ``-E`` option to boost debugging output, e.g.::
 
-    $ tox -e py36-nocov -- -E
+    $ tox -e py35-nocov -- -E
 
 and these options can be combined::
 
-    $ tox -e py36-nocov -- -P test_connection_reset_during_DATA -E
-
-
-Supported 'testenvs'
-------------------------
-
-In general, the ``-e`` parameter to tox specifies one (or more) **testenv**
-to run (separate using comma if more than one testenv). The following testenvs
-have been configured and tested:
-
-* ``{py36,py37,py38,py39,pypy3}-{nocov,cov,diffcov,profile}``
-
-  Specifies the interpreter to run and the kind of testing to perform.
-
-  - ``nocov`` = no coverage testing. Tests will run verbosely.
-  - ``cov`` = with coverage testing. Tests will run in brief mode
-    (showing a single character per test run)
-  - ``diffcov`` = with diff-coverage report (showing difference in
-    coverage compared to previous commit). Tests will run in brief mode
-  - ``profile`` = no coverage testing, but code profiling instead.
-    This must be **invoked manually** using the ``-e`` parameter
-
-  **Note:** Due to possible 'complications' when setting up PyPy on
-  systems without pyenv, ``pypy3`` tests also will not be automatically
-  run; you must invoke them manually.
-
-* ``qa``
-
-  Perform ``flake8`` code style checking
-
-* ``docs``
-
-  Builds HTML documentation using Sphinx
-
-
-Environment Variables
--------------------------
-
-``PLATFORM``
-    Used on non-native-Linux operating systems to specify tests to skip.
-    Valid values:
-
-    * ``mswin`` -- when running tox on Microsoft Windows
-    * ``wsl`` -- when running tox on Windows Subsystem for Linux (WSL)
-
-
-Different Python Versions
------------------------------
-
-The tox configuration files have been created to cater for more than one
-Python versions `safely`: If an interpreter is not found for a certain
-Python version, tox will skip that whole testenv.
-
-However, with a little bit of effort, you can have multiple Python interpreter
-versions on your system by using ``pyenv``. General steps:
-
-1. Install ``pyenv`` from https://github.com/pyenv/pyenv#installation
-
-2. Install ``tox-pyenv`` from https://pypi.org/project/tox-pyenv/
-
-3. Using ``pyenv``, install the Python versions you want to test on
-
-4. Create a ``.python-version`` file in the root of the repo, listing the
-   Python interpreter versions you want to make available to tox (see pyenv's
-   documentation about this file)
-
-5. Invoke tox with the option ``--tox-pyenv-no-fallback`` (see tox-pyenv's
-   documentation about this option)
+    $ tox -e py35-nocov -- -P test_connection_reset_during_DATA -E
 
 
 Contents
diff --git a/aiosmtpd/controller.py b/aiosmtpd/controller.py
index 90c080f..1df9c2c 100644
--- a/aiosmtpd/controller.py
+++ b/aiosmtpd/controller.py
@@ -4,14 +4,12 @@ import threading
 
 from aiosmtpd.smtp import SMTP
 from public import public
-from typing import Any, Dict
 
 
 @public
 class Controller:
     def __init__(self, handler, loop=None, hostname=None, port=8025, *,
-                 ready_timeout=1.0, enable_SMTPUTF8=True, ssl_context=None,
-                 server_kwargs: Dict[str, Any] = None):
+                 ready_timeout=1.0, enable_SMTPUTF8=True, ssl_context=None):
         """
         `Documentation can be found here
         <http://aiosmtpd.readthedocs.io/en/latest/aiosmtpd\
@@ -28,12 +26,10 @@ class Controller:
         self._thread_exception = None
         self.ready_timeout = os.getenv(
             'AIOSMTPD_CONTROLLER_TIMEOUT', ready_timeout)
-        self.server_kwargs: Dict[str, Any] = server_kwargs or {}
 
     def factory(self):
         """Allow subclasses to customize the handler/server creation."""
-        return SMTP(self.handler, enable_SMTPUTF8=self.enable_SMTPUTF8,
-                    **self.server_kwargs)
+        return SMTP(self.handler, enable_SMTPUTF8=self.enable_SMTPUTF8)
 
     def _run(self, ready_event):
         asyncio.set_event_loop(self.loop)
@@ -42,11 +38,7 @@ class Controller:
                 self.loop.create_server(
                     self.factory, host=self.hostname, port=self.port,
                     ssl=self.ssl_context))
-        except Exception as error:  # pragma: nowsl
-            # Somehow WSL1.0 (Windows Subsystem for Linux) allows multiple
-            # listeners on one port?!
-            # That is why we add "pragma: nowsl" there, so when testing on
-            # WSL we can specify "PLATFORM=wsl".
+        except Exception as error:
             self._thread_exception = error
             return
         self.loop.call_soon(ready_event.set)
@@ -64,17 +56,12 @@ class Controller:
         self._thread.start()
         # Wait a while until the server is responding.
         ready_event.wait(self.ready_timeout)
-        if self._thread_exception is not None:  # pragma: nowsl
-            # See comment about WSL1.0 in the _run() method
+        if self._thread_exception is not None:
             raise self._thread_exception
 
     def _stop(self):
         self.loop.stop()
-        try:
-            _all_tasks = asyncio.Task.all_tasks
-        except AttributeError:   # pragma: skipif_lt_py39
-            _all_tasks = asyncio.all_tasks
-        for task in _all_tasks(self.loop):
+        for task in asyncio.Task.all_tasks(self.loop):
             task.cancel()
 
     def stop(self):
diff --git a/aiosmtpd/docs/NEWS.rst b/aiosmtpd/docs/NEWS.rst
index 28b892a..67c75f8 100644
--- a/aiosmtpd/docs/NEWS.rst
+++ b/aiosmtpd/docs/NEWS.rst
@@ -2,31 +2,12 @@
  NEWS for aiosmtpd
 ===================
 
-aiosmtpd-next
-=============
+unreleased
+==========
 
-Added
+Fixed
 -----
-* **Apache License version 2.0**
-* Support for SMTP ``AUTH``, with AUTH hooks feature
-* Built-in implementation for ``AUTH PLAIN`` and ``AUTH LOGIN`` logic (fixes #102)
-* Feature to inject keyword args during server class instantiation in ``Controller.factory``
-  (potentially fixes #194, #179)
-* Support for Python 3.8 and 3.9.0 (also fixes #188)
-
-Fixed/Improved
---------------
 * Don't strip last ``\r\n`` prior to terminating dot.
-* Slight improvement to make Test Suite more maintainable
-* No more failures/DeprecationWarnings for Python 3.8 (fixes #167)
-* Faster ``_handle_client()`` processing
-* Faster method access for ``smtp_*``, ``handle_*``, and ``auth_*`` hooks
-
-Removed
--------
-* Unit Tests that mocked too deep, possibly masking observable internal behaviors
-* Drop support for Python 3.5
-
 
 1.2 (2018-09-01)
 ================
diff --git a/aiosmtpd/docs/concepts.rst b/aiosmtpd/docs/concepts.rst
index 15f4e84..20b8bbd 100644
--- a/aiosmtpd/docs/concepts.rst
+++ b/aiosmtpd/docs/concepts.rst
@@ -71,12 +71,6 @@ object is created.
 
       This is the asyncio event loop instance.
 
-   .. attribute:: login_data
-
-      Contains the login information gathered during the ``AUTH`` procedure.
-      If it contains ``None``, that means authentication has not taken place
-      or has failed.
-
 
 Envelope
 --------
diff --git a/aiosmtpd/docs/controller.rst b/aiosmtpd/docs/controller.rst
index 351ac79..71c6ec0 100644
--- a/aiosmtpd/docs/controller.rst
+++ b/aiosmtpd/docs/controller.rst
@@ -161,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, ssl_context=None, server_kwargs=None)
+.. 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.
 
@@ -195,10 +195,6 @@ Controller API
    method. Note that this implies unconditional encryption of the connection,
    and prevents use of the ``STARTTLS`` mechanism.
 
-   *server_kwargs* is a dict that will be passed through as keyword arguments
-   to the server's class during server creation in the :meth:`Controller.factory`
-   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 1c4a569..6a1657e 100644
--- a/aiosmtpd/docs/handlers.rst
+++ b/aiosmtpd/docs/handlers.rst
@@ -89,22 +89,12 @@ The following hooks are currently defined:
     instantiated with ``decode_data=False`` (the default) or
     ``decode_data=True``.  In the former case, both ``envelope.content`` and
     ``envelope.original_content`` will be the content bytes (normalized
-    according to the transparency rules in |RFC 5321, §4.5.2|_).  In the latter
+    according to the transparency rules in `RFC 5321, §4.5.2
+    <https://tools.ietf.org/html/rfc5321#section-4.5.2>`_).  In the latter
     case, ``envelope.original_content`` will be the normalized bytes, but
     ``envelope.content`` will be the UTF-8 decoded string of the original
     content.
 
-``handle_AUTH(server, session, envelope, args)``
-    Called to handle ``AUTH`` command, if you need custom AUTH behavior.
-    You *must* comply with |RFC 4954|_.
-    Most of the time, you don't *need* to implement this hook;
-    :ref:`AUTH hooks <auth_hooks>` are provided to override/implement selctive
-    SMTP AUTH mechanisms (see below).
-
-    ``args`` will contain the list of words following the ``AUTH`` command.
-    You will need to call some ``server`` methods and modify some ``session``
-    properties. ``envelope`` is usually ignored.
-
 In addition to the SMTP command hooks, the following hooks can also be
 implemented by handlers.  These have different APIs, and are called
 synchronously (i.e. they are **not** coroutines).
@@ -119,59 +109,8 @@ synchronously (i.e. they are **not** coroutines).
     handling of a connection (e.g. if an ``smtp_<command>()`` method raises an
     exception).  The exception object is passed in.  This method *must* return
     a status string, such as ``'542 Internal server error'``.  If the method
-    returns ``None`` or raises an exception, an exception will be logged, and a
-    ``500`` code will be returned to the client.
-
-
-.. _auth_hooks:
-
-AUTH hooks
-=============
-
-In addition to the above SMTP hooks, you can also implement AUTH hooks.
-**These hooks are asynchronous**.
-Every AUTH hook is named ``auth_MECHANISM`` where ``MECHANISM`` is the all-uppercase
-mechanism that the hook will implement. AUTH hooks will be called with the SMTP
-server instance and a list of str following the ``AUTH`` command.
-
-The SMTP class provides built-in AUTH hooks for the ``LOGIN`` and ``PLAIN``
-mechanisms, named ``auth_LOGIN`` and ``auth_PLAIN``, respectively.
-If the handler class implements ``auth_LOGIN`` and/or ``auth_PLAIN``, then
-those methods of the handler instance will override the built-in methods.
-
-``auth_MECHANISM(server: SMTP, args: List[str])``
-
-  *server* is the instance of the ``SMTP`` class invoking the AUTH hook.
-  This allows the AUTH hook implementation to invoke facilities such as the
-  ``push()`` and ``_auth_interact()`` methods.
-
-  *args* is a list of string split from the string after the ``AUTH`` command.
-  ``args[0]`` is always equal to ``MECHANISM``.
-
-  The AUTH hook **must** perform the actual validation of AUTH credentials.
-  In the built-in AUTH hooks, this is done by invoking the function specified
-  by the ``auth_callback`` initialization argument. AUTH hooks in handlers
-  are NOT required to do the same.
-
-  The AUTH hook **must** return one of the following values:
-
-    * ``None`` -- an error happened during AUTH exchange/procedure, and has
-      been handled inside the hook. ``smtp_AUTH`` will not do anything more.
-
-    * ``MISSING`` -- no error during exchange, but the credentials received
-      are invalid/rejected. (``MISSING`` is a pre-instantiated object you
-      can import from ``aiosmtpd.smtp``)
-
-    * *Anything else* -- an 'identity' of the STMP user. Usually is the username
-      given during AUTH exchange/procedure, but not necessarily so; can also
-      be, for instance, a Session ID. This will be stored in the Session
-      object's ``login_data`` property (see
-      :ref:`Session and Envelopes <sessions_and_envelopes>`)
-
-**NOTE:** Defining *additional* AUTH hooks in your handler will NOT disable
-the built-in LOGIN and PLAIN hooks; if you do not want to offer the LOGIN and
-PLAIN mechanisms, specify them in the ``auth_exclude_mechanism`` parameter
-of the :ref:`SMTP class<smtp_api>`.
+    returns None or raises an exception, an exception will be logged, and a 500
+    code will be returned to the client.
 
 
 Built-in handlers
@@ -289,7 +228,3 @@ We open up the mailbox again, and all three messages are waiting for us.
 .. _ArgumentParser: https://docs.python.org/3/library/argparse.html#argumentparser-objects
 .. _`email.message.Message`: https://docs.python.org/3/library/email.compat32-message.html#email.message.Message
 .. _Maildir: https://docs.python.org/3/library/mailbox.html#maildir
-.. _RFC 4954: https://tools.ietf.org/html/rfc4954
-.. |RFC 4954| replace:: **RFC 4954**
-.. _RFC 5321, §4.5.2: https://tools.ietf.org/html/rfc5321#section-4.5.2
-.. |RFC 5321, §4.5.2| replace:: **RFC 5321, §4.5.2**
diff --git a/aiosmtpd/docs/intro.rst b/aiosmtpd/docs/intro.rst
index f8bccf6..b79eacb 100644
--- a/aiosmtpd/docs/intro.rst
+++ b/aiosmtpd/docs/intro.rst
@@ -32,8 +32,6 @@ Relevant RFCs
   Extension for Returning Enhanced Error Codes
 * `RFC 6531 <http://www.faqs.org/rfcs/rfc6531.html>`__ - SMTP Extension for
   Internationalized Email
-* `RFC 4954 <http://www.faqs.org/rfcs/rfc4954.html>`__ - SMTP Service Extension
-  for Authentication
 
 
 Other references
diff --git a/aiosmtpd/docs/smtp.rst b/aiosmtpd/docs/smtp.rst
index 38629eb..1dbeca3 100644
--- a/aiosmtpd/docs/smtp.rst
+++ b/aiosmtpd/docs/smtp.rst
@@ -66,7 +66,7 @@ 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: AUTH DATA EHLO HELO HELP MAIL NOOP PING QUIT RCPT RSET VRFY
+    Supported commands: DATA EHLO HELO HELP MAIL NOOP PING QUIT RCPT RSET VRFY
 
 And we can get more detailed help on the new command.
 
@@ -98,12 +98,10 @@ override to provide additional responses.
     server.
 
 
-.. _smtp_api:
-
 SMTP API
 ========
 
-.. class:: SMTP(handler, *, data_size_limit=33554432, enable_SMTPUTF8=False, decode_data=False, hostname=None, ident=None, tls_context=None, require_starttls=False, timeout=300, auth_required=False, auth_require_tls=True, auth_exclude_mechanism=None, auth_callback=None, loop=None)
+.. class:: SMTP(handler, *, data_size_limit=33554432, enable_SMTPUTF8=False, decode_data=False, hostname=None, ident=None, tls_context=None, require_starttls=False, timeout=300, loop=None)
 
    *handler* is an instance of a :ref:`handler <handlers>` class.
 
@@ -137,24 +135,6 @@ SMTP API
    After this time the connection will be closed by the server.  The default
    is 300 seconds, as per `RFC 2821`_.
 
-   *auth_required* specifies whether SMTP Authentication is mandatory or
-   not for the session. This impacts some SMTP commands such as HELP, MAIL
-   FROM, RCPT TO, and others.
-
-   *auth_require_tls* specifies whether ``STARTTLS`` must be used before
-   AUTH exchange or not. If you set this to ``False`` then AUTH exchange can
-   be done outside a TLS context, but the class will warn you of security
-   considerations. Please note that *require_starttls* takes precedence
-   over this setting.
-
-   *auth_exclude_mechanism* is an ``Iterable[str]`` that specifies SMTP AUTH
-   mechanisms to NOT use.
-
-   *auth_callback* is a function that accepts three arguments: ``mechanism: str``,
-   ``login: bytes``, and ``password: bytes``. Based on these args, the function
-   must return a ``bool`` that indicates whether the client's authentication
-   attempt is accepted/successful or not.
-
    *loop* is the asyncio event loop to use.  If not given,
    :meth:`asyncio.new_event_loop()` is called to create the event loop.
 
@@ -204,10 +184,6 @@ SMTP API
       The event loop being used.  This will either be the given *loop*
       argument, or the new event loop that was created.
 
-   .. attribute:: authenticated
-
-      A flag that indicates whether authentication had succeeded.
-
    .. method:: _create_session()
 
       A method subclasses can override to return custom ``Session`` instances.
diff --git a/aiosmtpd/handlers.py b/aiosmtpd/handlers.py
index 2d201f6..0107c76 100644
--- a/aiosmtpd/handlers.py
+++ b/aiosmtpd/handlers.py
@@ -100,10 +100,11 @@ class Proxy:
         # Look for the last header
         i = 0
         ending = CRLF
-        for i, line in enumerate(lines):  # pragma: nobranch
+        for line in lines:                          # pragma: nobranch
             if NLCRE.match(line):
                 ending = line
                 break
+            i += 1
         peer = session.peer[0].encode('ascii')
         lines.insert(i, b'X-Peer: %s%s' % (peer, ending))
         data = EMPTYBYTES.join(lines)
@@ -163,7 +164,7 @@ class Message:
             message = message_from_bytes(data, self.message_class)
         else:
             assert isinstance(data, str), (
-                'Expected str or bytes, got {}'.format(type(data)))
+              'Expected str or bytes, got {}'.format(type(data)))
             message = message_from_string(data, self.message_class)
         message['X-Peer'] = str(session.peer)
         message['X-MailFrom'] = envelope.mail_from
diff --git a/aiosmtpd/main.py b/aiosmtpd/main.py
index 36d3dfd..5b92daa 100644
--- a/aiosmtpd/main.py
+++ b/aiosmtpd/main.py
@@ -64,14 +64,11 @@ def parseargs(args=None):
     parser.add_argument(
         '-l', '--listen', metavar='HOST:PORT',
         nargs='?', default=None,
-        help=(
-            "Optional host and port to listen on.  If the PORT part is not "
-            "given, then port {port} is used.  If only :PORT is given, "
-            "then {host} is used for the hostname.  If neither are given, "
-            "{host}:{port} is used.".format(
-                host=DEFAULT_HOST, port=DEFAULT_PORT
-            ))
-        )
+        help="""Optional host and port to listen on.  If the PORT part is not
+                given, then port {port} is used.  If only :PORT is given,
+                then {host} is used for the hostname.  If neither are given,
+                {host}:{port} is used.""".format(
+                    host=DEFAULT_HOST, port=DEFAULT_PORT))
     parser.add_argument(
         'classargs', metavar='CLASSARGS',
         nargs='*', default=(),
@@ -137,31 +134,23 @@ def main(args=None):
     if args.debug > 2:
         loop.set_debug(enabled=True)
 
-    log.debug('Attempting to start server on %s:%s', args.host, args.port)
-    server = server_loop = None
-    try:
-        server = loop.create_server(factory, host=args.host, port=args.port)
-        server_loop = loop.run_until_complete(server)
-    except RuntimeError as e:  # pragma: nocover
-        log.critical("\u001b[33;1m" + "#" * 600 + "\u001b[0m",
-                     exc_info=e)
-        raise
-    log.debug(f"server_loop = {server_loop}")
-    log.info('Server is listening on %s:%s', args.host, args.port)
+    log.info('Server listening on %s:%s', args.host, args.port)
 
+    server = loop.run_until_complete(
+        loop.create_server(factory, host=args.host, port=args.port))
     # 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.debug('Starting asyncio loop')
+    log.info('Starting asyncio loop')
     try:
         loop.run_forever()
     except KeyboardInterrupt:
         pass
-    server_loop.close()
-    log.debug('Completed asyncio loop')
-    loop.run_until_complete(server_loop.wait_closed())
+    server.close()
+    log.info('Completed asyncio loop')
+    loop.run_until_complete(server.wait_closed())
     loop.close()
 
 
diff --git a/aiosmtpd/smtp.py b/aiosmtpd/smtp.py
index b810b4c..0072654 100644
--- a/aiosmtpd/smtp.py
+++ b/aiosmtpd/smtp.py
@@ -2,29 +2,16 @@ import ssl
 import socket
 import asyncio
 import logging
-import binascii
 import collections
-import asyncio.sslproto as sslproto
 
-from base64 import b64decode
+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 typing import (
-    Any,
-    Awaitable,
-    Callable,
-    Dict,
-    Iterable,
-    List,
-    NamedTuple,
-    Optional,
-    Union,
-)
 from warnings import warn
 
 
-__version__ = '1.2+'
+__version__ = '1.2.2'
 __ident__ = 'Python SMTP {}'.format(__version__)
 log = logging.getLogger('mail.log')
 
@@ -32,22 +19,7 @@ log = logging.getLogger('mail.log')
 DATA_SIZE_DEFAULT = 33554432
 EMPTYBYTES = b''
 NEWLINE = '\n'
-
-
-class _Missing:
-    pass
-
-
-MISSING = _Missing()
-
-
-AuthMechanismType = Callable[["SMTP", List[str]], Awaitable[Any]]
-_TriStateType = Union[None, _Missing, bytes]
-
-
-class _AuthMechAttr(NamedTuple):
-    method: AuthMechanismType
-    is_builtin: bool
+MISSING = object()
 
 
 @public
@@ -58,7 +30,6 @@ class Session:
         self.host_name = None
         self.extended_smtp = False
         self.loop = loop
-        self.login_data = None
 
 
 @public
@@ -67,7 +38,7 @@ class Envelope:
         self.mail_from = None
         self.mail_options = []
         self.smtp_utf8 = False
-        self.content: Union[None, bytes, str] = None
+        self.content = None
         self.original_content = None
         self.rcpt_tos = []
         self.rcpt_options = []
@@ -89,10 +60,6 @@ def syntax(text, extended=None, when=None):
     return decorator
 
 
-def login_always_fail(mechanism, login, password):
-    return False
-
-
 @public
 class SMTP(asyncio.StreamReaderProtocol):
     command_size_limit = 512
@@ -109,10 +76,6 @@ class SMTP(asyncio.StreamReaderProtocol):
                  tls_context=None,
                  require_starttls=False,
                  timeout=300,
-                 auth_required=False,
-                 auth_require_tls=True,
-                 auth_exclude_mechanism: Optional[Iterable[str]] = None,
-                 auth_callback: Callable[[str, bytes, bytes], bool] = None,
                  loop=None):
         self.__ident__ = ident or __ident__
         self.loop = loop if loop else make_loop()
@@ -145,37 +108,6 @@ class SMTP(asyncio.StreamReaderProtocol):
         self.envelope = None
         self.transport = None
         self._handler_coroutine = None
-        if not auth_require_tls and auth_required:
-            warn("Requiring AUTH while not requiring TLS "
-                 "can lead to security vulnerabilities!")
-        self._auth_require_tls = auth_require_tls
-        self._auth_callback = auth_callback or login_always_fail
-        self._auth_required = auth_required
-        self.authenticated = False
-        # Get hooks & methods to significantly speedup getattr's
-        self._auth_methods: Dict[str, _AuthMechAttr] = {
-            m.replace("auth_", ""): _AuthMechAttr(getattr(h, m), h is self)
-            for h in (self, handler)
-            for m in dir(h)
-            if m.startswith("auth_")
-        }
-        for m in (auth_exclude_mechanism or []):
-            self._auth_methods.pop(m, None)
-        msg = "Available AUTH mechanisms:"
-        for m, impl in sorted(
-                self._auth_methods.items()):  # type: str, _AuthMechAttr
-            msg += f" {m}{'(builtin)' if impl.is_builtin else ''}"
-        log.info(msg)
-        self._handle_hooks: Dict[str, Callable] = {
-            m.replace("handle_", ""): getattr(handler, m)
-            for m in dir(handler)
-            if m.startswith("handle_")
-        }
-        self._smtp_methods: Dict[str, Any] = {
-            m.replace("smtp_", ""): getattr(self, m)
-            for m in dir(self)
-            if m.startswith("smtp_")
-        }
 
     def _create_session(self):
         return Session(self.loop)
@@ -184,7 +116,7 @@ class SMTP(asyncio.StreamReaderProtocol):
         return Envelope()
 
     async def _call_handler_hook(self, command, *args):
-        hook = self._handle_hooks.get(command)
+        hook = getattr(self.event_handler, 'handle_' + command, None)
         if hook is None:
             return MISSING
         status = await hook(self, self.session, self.envelope, *args)
@@ -212,11 +144,11 @@ class SMTP(asyncio.StreamReaderProtocol):
             # Do SSL certificate checking as rfc3207 part 4.1 says.  Why is
             # _extra a protected attribute?
             self.session.ssl = self._tls_protocol._extra
-            hook = self._handle_hooks.get("STARTTLS")
-            if hook is None:
+            handler = getattr(self.event_handler, 'handle_STARTTLS', None)
+            if handler is None:
                 self._tls_handshake_okay = True
             else:
-                self._tls_handshake_okay = hook(
+                self._tls_handshake_okay = handler(
                     self, self.session, self.envelope)
         else:
             super().connection_made(transport)
@@ -260,7 +192,7 @@ class SMTP(asyncio.StreamReaderProtocol):
             self._timeout_handle.cancel()
 
         self._timeout_handle = self.loop.call_later(
-            self._timeout_duration, self._timeout_cb)
+                self._timeout_duration, self._timeout_cb)
 
     def _timeout_cb(self):
         log.info('%r connection timeout', self.session.peer)
@@ -307,7 +239,7 @@ class SMTP(asyncio.StreamReaderProtocol):
         while self.transport is not None:   # pragma: nobranch
             # XXX Put the line limit stuff into the StreamReader?
             try:
-                line: bytes = await self._reader.readline()
+                line = await self._reader.readline()
                 log.debug('_handle_client readline: %s', line)
                 # XXX this rstrip may not completely preserve old behavior.
                 line = line.rstrip(b'\r\n')
@@ -315,21 +247,28 @@ class SMTP(asyncio.StreamReaderProtocol):
                 if not line:
                     await self.push('500 Error: bad syntax')
                     continue
-                command, _, arg = line.partition(b" ")
+                i = line.find(b' ')
                 # Decode to string only the command name part, which must be
                 # ASCII as per RFC.  If there is an argument, it is decoded to
                 # UTF-8/surrogateescape so that non-UTF-8 data can be
                 # re-encoded back to the original bytes when the SMTP command
                 # is handled.
-                try:
-                    command = command.upper().decode(encoding='ascii')
-                except UnicodeDecodeError:
-                    await self.push('500 Error: bad syntax')
-                    continue
-                if not arg:
+                if i < 0:
+                    try:
+                        command = line.upper().decode(encoding='ascii')
+                    except UnicodeDecodeError:
+                        await self.push('500 Error: bad syntax')
+                        continue
+
                     arg = None
                 else:
-                    arg = arg.strip()
+                    try:
+                        command = line[:i].upper().decode(encoding='ascii')
+                    except UnicodeDecodeError:
+                        await self.push('500 Error: bad syntax')
+                        continue
+
+                    arg = line[i+1:].strip()
                     # Remote SMTP servers can send us UTF-8 content despite
                     # whether they've declared to do so or not.  Some old
                     # servers can send 8-bit data.  Use surrogateescape so
@@ -365,7 +304,7 @@ class SMTP(asyncio.StreamReaderProtocol):
                     # RFC3207 part 4
                     await self.push('530 Must issue a STARTTLS command first')
                     continue
-                method = self._smtp_methods.get(command)
+                method = getattr(self, 'smtp_' + command, None)
                 if method is None:
                     await self.push(
                         '500 Error: command "%s" not recognized' % command)
@@ -394,31 +333,6 @@ class SMTP(asyncio.StreamReaderProtocol):
                         status = '500 Error: Cannot describe error'
                     await self.push(status)
 
-    async def check_helo_needed(self, helo: str = "HELO") -> bool:
-        """
-        Check if HELO/EHLO is needed.
-
-        :param helo: The actual string of HELO/EHLO
-        :return: True if HELO/EHLO is needed
-        """
-        if not self.session.host_name:
-            await self.push(f'503 Error: send {helo} first')
-            return True
-        return False
-
-    async def check_auth_needed(self, caller_method: str) -> bool:
-        """
-        Check if AUTH is needed.
-
-        :param caller_method: The SMTP method needing a check (for logging)
-        :return: True if AUTH is needed
-        """
-        if self._auth_required and not self.authenticated:
-            log.info(f'{caller_method}: Authentication required')
-            await self.push('530 5.7.0 Authentication required')
-            return True
-        return False
-
     # SMTP and ESMTP commands
     @syntax('HELO hostname')
     async def smtp_HELO(self, hostname):
@@ -455,10 +369,6 @@ class SMTP(asyncio.StreamReaderProtocol):
             warn('Use handler.handle_EHLO() instead of .ehlo_hook()',
                  DeprecationWarning)
             await self.ehlo_hook()
-        if not self._auth_require_tls or self._tls_protocol:
-            await self.push(
-                "250-AUTH " + " ".join(sorted(self._auth_methods.keys()))
-            )
         status = await self._call_handler_hook('EHLO', hostname)
         if status is MISSING:
             self.session.host_name = hostname
@@ -491,7 +401,6 @@ class SMTP(asyncio.StreamReaderProtocol):
             return
         await self.push('220 Ready to start TLS')
         # Create SSL layer.
-        # noinspection PyTypeChecker
         self._tls_protocol = sslproto.SSLProtocol(
             self.loop,
             self,
@@ -508,149 +417,6 @@ class SMTP(asyncio.StreamReaderProtocol):
         self.transport = self._tls_protocol._app_transport
         self._tls_protocol.connection_made(self._original_transport)
 
-    @syntax("AUTH <mechanism>")
-    async def smtp_AUTH(self, arg: str) -> None:
-        if await self.check_helo_needed("EHLO"):
-            return
-        elif not self.session.extended_smtp:
-            await self.push("500 Error: command 'AUTH' not recognized")
-        elif self._auth_require_tls and not self._tls_protocol:
-            await self.push("538 5.7.11 Encryption required for requested "
-                            "authentication mechanism")
-        elif self.authenticated:
-            await self.push('503 Already authenticated')
-        elif not arg:
-            await self.push('501 Not enough value')
-        else:
-            args = arg.split()
-            if len(args) > 2:
-                await self.push('501 Too many values')
-                return
-
-            mechanism = args[0]
-            if mechanism not in self._auth_methods:
-                await self.push('504 5.5.4 Unrecognized authentication type')
-                return
-
-            status = await self._call_handler_hook('AUTH', args)
-            if status is MISSING:
-                method = self._auth_methods[mechanism]
-                if method.is_builtin:
-                    log.debug(f"Using builtin auth_ hook for {mechanism}")
-                else:
-                    log.debug(f"Using handler auth_ hook for {mechanism}")
-                # Pass 'self' to method so external methods can leverage this
-                # class's helper methods such as push()
-                login_data = await method.method(self, args)
-                log.debug(f"auth_{mechanism} returned {login_data}")
-                if login_data is None:
-                    # None means there's an error already handled by method and
-                    # we don't need to do anything more
-                    return
-                elif login_data is MISSING:
-                    # MISSING means no error in AUTH process, but credentials
-                    # is rejected / not valid
-                    status = '535 5.7.8 Authentication credentials invalid'
-                else:
-                    self.authenticated = True
-                    self.session.login_data = login_data
-                    status = '235 2.7.0 Authentication successful'
-            if status is not None:  # pragma: no branch
-                await self.push(status)
-
-    async def _auth_interact(self, server_message) -> _TriStateType:
-        blob: bytes
-        await self.push(server_message)
-        line = await self._reader.readline()
-        blob = line.strip()
-        # '=' and '*' handling are in accordance with RFC4954
-        if blob == b"=":
-            log.debug("User responded with '='")
-            return None
-        if blob == b"*":
-            log.warning("User requested abort with '*'")
-            await self.push("501 Auth aborted")
-            return MISSING
-        try:
-            decoded_blob = b64decode(blob, validate=True)
-        except binascii.Error:
-            await self.push("501 5.5.2 Can't decode base64")
-            return MISSING
-        return decoded_blob
-
-    # IMPORTANT NOTES FOR THE auth_* METHODS
-    #
-    # 1. For internal methods, due to how they are called, we must ignore
-    #    the first arg
-    # 2. All auth_* methods can return one of three values:
-    #    - None: An error happened and handled;
-    #            smtp_AUTH should do nothing more
-    #    - MISSING: No error during SMTP AUTH process, but authentication
-    #               failed
-    #    - [Any]: Authentication succeeded and this is the 'identity' of
-    #             the SMTP user
-    #      - 'identity' is not always username, depending on the auth mecha-
-    #        nism. Might be a session key, a one-time user ID, or any kind of
-    #        object, actually.
-    #      - If the client provides "=" for username during interaction, the
-    #        method MUST return b"" (empty bytes)
-    # 3. Auth credentials checking is performed in the auth_* methods because
-    #    more advanced auth mechanism might not return login+password pair
-    #    (see #2 above)
-
-    async def auth_PLAIN(self, _, args: List[str]):
-        loginpassword: _TriStateType
-        if len(args) == 1:
-            # Trailing space is MANDATORY
-            # See https://tools.ietf.org/html/rfc4954#page-4
-            loginpassword = await self._auth_interact("334 ")
-            if loginpassword is MISSING:
-                return
-        else:
-            blob = args[1].encode()
-            if blob == b"=":
-                loginpassword = None
-            else:
-                try:
-                    loginpassword = b64decode(blob, validate=True)
-                except Exception:
-                    await self.push("501 5.5.2 Can't decode base64")
-                    return
-        if loginpassword is None:
-            login = password = None
-        else:
-            try:
-                _, login, password = loginpassword.split(b"\x00")
-            except ValueError:  # not enough args
-                await self.push("501 5.5.2 Can't split auth value")
-                return
-        if self._auth_callback("PLAIN", login, password):
-            if login is None:
-                login = EMPTYBYTES
-            return login
-        else:
-            return MISSING
-
-    async def auth_LOGIN(self, _, args: List[str]):
-        login: _TriStateType
-        # 'User Name\x00'
-        login = await self._auth_interact("334 VXNlciBOYW1lAA==")
-        if login is MISSING:
-            return
-
-        password: _TriStateType
-        # 'Password\x00'
-        password = await self._auth_interact("334 UGFzc3dvcmQA")
-        if password is MISSING:
-            return
-
-        if self._auth_callback("LOGIN", login, password):
-            if login is None:  # pragma: no branch
-                login = EMPTYBYTES
-            return login
-        else:
-            return MISSING
-
     def _strip_command_keyword(self, keyword, arg):
         keylen = len(keyword)
         if arg[:keylen].upper() == keyword:
@@ -683,19 +449,17 @@ class SMTP(asyncio.StreamReaderProtocol):
         return result
 
     def _syntax_available(self, method):
-        if not hasattr(method, '__smtp_syntax__'):
+        if getattr(method, '__smtp_syntax__', None) is None:
             return False
         if method.__smtp_syntax_when__:
             return bool(getattr(self, method.__smtp_syntax_when__))
         return True
 
     @syntax('HELP [command]')
-    async def smtp_HELP(self, arg: str) -> None:
-        if await self.check_auth_needed("HELP"):
-            return
+    async def smtp_HELP(self, arg):
         code = 250
         if arg:
-            method = self._smtp_methods.get(arg.upper())
+            method = getattr(self, 'smtp_' + arg.upper(), None)
             if method and self._syntax_available(method):
                 help_str = method.__smtp_syntax__
                 if (self.session.extended_smtp
@@ -705,17 +469,18 @@ class SMTP(asyncio.StreamReaderProtocol):
                 return
             code = 501
         commands = []
-        for name, method in self._smtp_methods.items():
+        for name in dir(self):
+            if not name.startswith('smtp_'):
+                continue
+            method = getattr(self, name)
             if self._syntax_available(method):
-                commands.append(name)
+                commands.append(name.lstrip('smtp_'))
         commands.sort()
         await self.push(
             '{} Supported commands: {}'.format(code, ' '.join(commands)))
 
     @syntax('VRFY <address>')
-    async def smtp_VRFY(self, arg: str) -> None:
-        if await self.check_auth_needed("VRFY"):
-            return
+    async def smtp_VRFY(self, arg):
         if arg:
             try:
                 address, params = self._getaddr(arg)
@@ -733,10 +498,9 @@ class SMTP(asyncio.StreamReaderProtocol):
             await self.push('501 Syntax: VRFY <address>')
 
     @syntax('MAIL FROM: <address>', extended=' [SP <mail-parameters>]')
-    async def smtp_MAIL(self, arg: str) -> None:
-        if await self.check_helo_needed():
-            return
-        if await self.check_auth_needed("MAIL"):
+    async def smtp_MAIL(self, arg):
+        if not self.session.host_name:
+            await self.push('503 Error: send HELO first')
             return
         log.debug('===> MAIL %s', arg)
         syntaxerr = '501 Syntax: MAIL FROM: <address>'
@@ -801,10 +565,9 @@ class SMTP(asyncio.StreamReaderProtocol):
         await self.push(status)
 
     @syntax('RCPT TO: <address>', extended=' [SP <mail-parameters>]')
-    async def smtp_RCPT(self, arg: str) -> None:
-        if await self.check_helo_needed():
-            return
-        if await self.check_auth_needed("RCPT"):
+    async def smtp_RCPT(self, arg):
+        if not self.session.host_name:
+            await self.push('503 Error: send HELO first')
             return
         log.debug('===> RCPT %s', arg)
         if not self.envelope.mail_from:
@@ -821,6 +584,9 @@ class SMTP(asyncio.StreamReaderProtocol):
             await self.push(syntaxerr)
             return
         address, params = self._getaddr(arg)
+        if address is None:
+            await self.push(syntaxerr)
+            return
         if not address:
             await self.push(syntaxerr)
             return
@@ -859,10 +625,9 @@ class SMTP(asyncio.StreamReaderProtocol):
         await self.push('250 OK' if status is MISSING else status)
 
     @syntax('DATA')
-    async def smtp_DATA(self, arg: str) -> None:
-        if await self.check_helo_needed():
-            return
-        if await self.check_auth_needed("DATA"):
+    async def smtp_DATA(self, arg):
+        if not self.session.host_name:
+            await self.push('503 Error: send HELO first')
             return
         if not self.envelope.rcpt_tos:
             await self.push('503 Error: need RCPT command')
@@ -886,9 +651,9 @@ class SMTP(asyncio.StreamReaderProtocol):
             if line == b'.\r\n':
                 break
             num_bytes += len(line)
-            if (not size_exceeded
-                    and self.data_size_limit
-                    and num_bytes > self.data_size_limit):
+            if (not size_exceeded and
+                    self.data_size_limit and
+                    num_bytes > self.data_size_limit):
                 size_exceeded = True
                 await self.push('552 Error: Too much mail data')
             if not size_exceeded:
@@ -898,8 +663,9 @@ class SMTP(asyncio.StreamReaderProtocol):
             return
         # Remove extraneous carriage returns and de-transparency
         # according to RFC 5321, Section 4.5.2.
-        for i, text in enumerate(data):
-            if text.startswith(b'.'):
+        for i in range(len(data)):
+            text = data[i]
+            if text and text[:1] == b'.':
                 data[i] = text[1:]
         content = original_content = EMPTYBYTES.join(data)
         if self._decode_data:
@@ -918,7 +684,7 @@ class SMTP(asyncio.StreamReaderProtocol):
         self.envelope.content = content
         self.envelope.original_content = original_content
         # Call the new API first if it's implemented.
-        if "DATA" in self._handle_hooks:
+        if hasattr(self.event_handler, 'handle_DATA'):
             status = await self._call_handler_hook('DATA')
         else:
             # Backward compatibility.
diff --git a/release.py b/release.py
new file mode 100755
index 0000000..df088ea
--- /dev/null
+++ b/release.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python3
+
+import os
+import subprocess
+import sys
+
+import aiosmtpd.smtp as smtpd
+
+version = smtpd.__version__
+
+choice = input(f'Release aiosmtpd {version} - correct? [y/N]: ')
+if choice.lower() not in ('y', 'yes'):
+    sys.exit('Release aborted')
+else:
+    # We're probably already in the right place
+    os.chdir(os.path.dirname(os.path.abspath(__file__)))
+    # Let's use *this* python to build, please
+    subprocess.run([sys.executable, "setup.py", "sdist"])
+    # Assuming twine is installed. And that we're only building .tar.gz
+    subprocess.run(["twine", "check", f"dist/aiosmtpd-{version}.tar.gz"])
+    # You should have an aiosmtpd bit setup in your ~/.pypirc - for twine
+    subprocess.run(["twine", "upload", "--config-file", "~/.pypirc", "-r", "aiosmtpd", "dist/aiosmptd-{version}.tar.gz"])
+    # Only tag when we've actually built and uploaded. If something goes wrong
+    # we may need the tag somewhere else!
+    # The annotation information should come from the changelog
+    subprocess.run(["git", "tag", "-a", version])
+    # And now push the tag, of course.
+    subprocess.run(["git", "push", "upstream", "--tags"])
diff --git a/setup.py b/setup.py
index 47d51eb..a9a0232 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setup_helpers import require_python, get_version
 from setuptools import setup, find_packages
 
 
-require_python(0x30600f0)
+require_python(0x30400f0)
 __version__ = get_version('aiosmtpd/smtp.py')
 
 
@@ -14,6 +14,7 @@ setup(
 This is a server for SMTP and related protocols, similar in utility to the
 standard library's smtpd.py module, but rewritten to be based on asyncio for
 Python 3.""",
+    long_description_content_type="text/x-rst",
     url='http://aiosmtpd.readthedocs.io/',
     keywords='email',
     packages=find_packages(),
diff --git a/tox.ini b/tox.ini
index 6c27f1e..145b7c2 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,50 +1,46 @@
 [tox]
-envlist = {py36,py37,py38,py39}-{cov,nocov,diffcov}, qa, docs
+envlist = {py35,py36,py37}-{cov,nocov,diffcov},qa,docs
 skip_missing_interpreters = True
 
 [testenv]
 commands =
-    python -c "import sys; print(sys.version); print(sys.executable)"
-    python -c "import os,pprint; os.makedirs('_dynamic', exist_ok=True); pprint.pprint(dict(os.environ), stream=open('_dynamic/ENV."{envname}"', 'wt'))"
     nocov: python -m nose2 -v {posargs}
-    cov: python -m coverage run -m nose2
-    cov: python -m coverage combine
-    cov: python -m coverage html --directory=htmlcov/{envname}
-    cov: python -m coverage report -m --fail-under=100
-    diffcov: python -m coverage xml
+    {cov,diffcov}: python -m coverage run {[coverage]rc} -m nose2
+    {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=100
+    diffcov: python -m coverage xml {[coverage]rc}
     diffcov: diff-cover coverage.xml --html-report diffcov.html
     diffcov: diff-cover coverage.xml --fail-under=100
-    profile: python -m nose2 --profile {posargs}
-    profile: python -c "open('_dynamic/cPROFILE."{envname}"', 'wb').write(open('_dynamic/cPROFILE', 'rb').read())"
-    python -c "import sys; print(sys.version); print(sys.executable)"
 #sitepackages = True
 usedevelop = True
 deps =
     nose2
+    flufl.testing
     {cov,diffcov}: coverage
     diffcov: diff_cover
 setenv =
-    cov: COVERAGE_PROCESS_START=.coveragerc
+    cov: COVERAGE_PROCESS_START={[coverage]rcfile}
     cov: COVERAGE_OPTIONS="-p"
     cov: COVERAGE_FILE={toxinidir}/.coverage
+    py35: INTERP=py35
     py36: INTERP=py36
     py37: INTERP=py37
-    py38: INTERP=py38
-    py39: INTERP=py39
-    pypy3: INTERP=pypy3
-    nocov: PYTHONASYNCIODEBUG=1
-    !py39: PY_39=lt
-    py39: PY_39=ge
     PLATFORM={env:PLATFORM:linux}
 passenv =
     PYTHON*
 
+[coverage]
+rcfile = {toxinidir}/.coverage.ini
+rc = --rcfile={[coverage]rcfile}
+
 [testenv:qa]
 basepython = python3
 commands =
     python -m flake8 aiosmtpd
 deps =
     flake8
+    flufl.testing
 
 [testenv:docs]
 basepython = python3
@@ -54,7 +50,8 @@ deps:
     sphinx
 
 [flake8]
+enable-extensions = U4
 exclude = conf.py
+hang-closing = True
 jobs = 1
 max-line-length = 79
-ignore = E123, E133, W503, W504


Test patch:
diff --git a/aiosmtpd/testing/helpers.py b/aiosmtpd/testing/helpers.py
index 6999d32..a88b550 100644
--- a/aiosmtpd/testing/helpers.py
+++ b/aiosmtpd/testing/helpers.py
@@ -8,7 +8,6 @@ import logging
 import warnings
 
 from contextlib import ExitStack
-from unittest import TestCase
 from unittest.mock import patch
 
 
@@ -59,26 +58,3 @@ def start(plugin):
         logging.getLogger('asyncio').setLevel(logging.DEBUG)
         logging.getLogger('mail.log').setLevel(logging.DEBUG)
         warnings.filterwarnings('always', category=ResourceWarning)
-
-
-def assert_auth_success(testcase: TestCase, code, response):
-    testcase.assertEqual(code, 235)
-    testcase.assertEqual(response, b"2.7.0 Authentication successful")
-
-
-def assert_auth_invalid(testcase: TestCase, code, response):
-    testcase.assertEqual(code, 535)
-    testcase.assertEqual(response, b'5.7.8 Authentication credentials invalid')
-
-
-def assert_auth_required(testcase: TestCase, code, response):
-    testcase.assertEqual(code, 530)
-    testcase.assertEqual(response, b'5.7.0 Authentication required')
-
-
-SUPPORTED_COMMANDS_TLS: bytes = (
-    b'Supported commands: AUTH DATA EHLO HELO HELP MAIL '
-    b'NOOP QUIT RCPT RSET STARTTLS VRFY'
-)
-
-SUPPORTED_COMMANDS_NOTLS = SUPPORTED_COMMANDS_TLS.replace(b" STARTTLS", b"")
diff --git a/aiosmtpd/tests/test_handlers.py b/aiosmtpd/tests/test_handlers.py
index 49b69f4..dc5de7c 100644
--- a/aiosmtpd/tests/test_handlers.py
+++ b/aiosmtpd/tests/test_handlers.py
@@ -22,11 +22,6 @@ class DecodingController(Controller):
         return Server(self.handler, decode_data=True)
 
 
-class AUTHDecodingController(Controller):
-    def factory(self):
-        return Server(self.handler, decode_data=True, auth_require_tls=False)
-
-
 class DataHandler:
     def __init__(self):
         self.content = None
@@ -543,12 +538,6 @@ class DATAHandler:
         return '599 Not today'
 
 
-class AUTHHandler:
-    async def handle_AUTH(self, server, session, envelope, args):
-        server.authenticates = True
-        return '235 Authentication successful'
-
-
 class NoHooksHandler:
     pass
 
@@ -615,16 +604,6 @@ Yikes
             self.assertEqual(cm.exception.smtp_code, 599)
             self.assertEqual(cm.exception.smtp_error, b'Not today')
 
-    def test_auth_hook(self):
-        controller = AUTHDecodingController(AUTHHandler())
-        controller.start()
-        self.addCleanup(controller.stop)
-        with SMTP(controller.hostname, controller.port) as client:
-            client.ehlo('me')
-            code, response = client.login("test", "test")
-        self.assertEqual(code, 235)
-        self.assertEqual(response, b'Authentication successful')
-
     def test_no_hooks(self):
         controller = Controller(NoHooksHandler())
         controller.start()
@@ -654,10 +633,6 @@ class CapturingServer(Server):
 
 
 class CapturingController(Controller):
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        self.smtpd = None
-
     def factory(self):
         self.smtpd = CapturingServer(self.handler)
         return self.smtpd
@@ -696,10 +671,6 @@ class DeprecatedHookServer(Server):
 
 
 class DeprecatedHookController(Controller):
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        self.smtpd = None
-
     def factory(self):
         self.smtpd = DeprecatedHookServer(self.handler)
         return self.smtpd
diff --git a/aiosmtpd/tests/test_lmtp.py b/aiosmtpd/tests/test_lmtp.py
index a3855e4..d77816e 100644
--- a/aiosmtpd/tests/test_lmtp.py
+++ b/aiosmtpd/tests/test_lmtp.py
@@ -48,5 +48,5 @@ class TestLMTP(unittest.TestCase):
             code, response = client.docmd('HELP')
             self.assertEqual(code, 250)
             self.assertEqual(response,
-                             b'Supported commands: AUTH DATA HELP LHLO MAIL '
+                             b'Supported commands: DATA HELP LHLO MAIL '
                              b'NOOP QUIT RCPT RSET VRFY')
diff --git a/aiosmtpd/tests/test_main.py b/aiosmtpd/tests/test_main.py
index 0581080..a821c6a 100644
--- a/aiosmtpd/tests/test_main.py
+++ b/aiosmtpd/tests/test_main.py
@@ -1,11 +1,14 @@
 import os
+import signal
 import asyncio
 import logging
 import unittest
 
+from aiosmtpd.handlers import Debugging
 from aiosmtpd.main import main, parseargs
-from aiosmtpd.smtp import __version__
+from aiosmtpd.smtp import SMTP, __version__
 from contextlib import ExitStack
+from functools import partial
 from io import StringIO
 from unittest.mock import patch
 
@@ -49,18 +52,7 @@ class TestMain(unittest.TestCase):
         # information to know for sure.
         default_loop = asyncio.get_event_loop()
         loop = asyncio.new_event_loop()
-        # The original value of 0.1 is too small; on underpowered test benches
-        # (like my laptop) the initialization of the whole asyncio 'system'
-        # (i.e., create_server + run_until_complete + run_forever) *sometimes*
-        # takes more than 0.1 seconds, causing tests to fail intermittently
-        # with “Event loop stopped before Future completed.” error.
-        #
-        # Because the error is intermittent and infrequently happen (maybe
-        # only about 5-10% of testing attempts), I figure the actual time
-        # needed would be 0.1 +/- 20%; so raising this value by 900%
-        # *should* be enough. We can revisit this in the future if it needs
-        # to be longer.
-        loop.call_later(1.0, loop.stop)
+        loop.call_later(0.1, loop.stop)
         self.resources.callback(asyncio.set_event_loop, default_loop)
         asyncio.set_event_loop(loop)
         self.addCleanup(self.resources.close)
@@ -93,15 +85,9 @@ class TestMain(unittest.TestCase):
         with self.assertRaises(SystemExit) as cm:
             main(args=())
         self.assertEqual(cm.exception.code, 1)
-        # On Python 3.8 on Linux, a bunch of "RuntimeWarning: coroutine
-        # 'AsyncMockMixin._execute_mock_call' was never awaited" messages
-        # gets mixed up into stderr causing test fail.
-        # Therefore, we use assertIn instead of assertEqual here, because
-        # the string DOES appear in stderr, just buried.
-        self.assertIn(
-            'Cannot import module "pwd"; try running with -n option.\n',
+        self.assertEqual(
             stderr.getvalue(),
-        )
+            'Cannot import module "pwd"; try running with -n option.\n')
 
     @unittest.skipUnless(has_setuid, 'setuid is unvailable')
     def test_n(self):
@@ -130,11 +116,11 @@ class TestMain(unittest.TestCase):
     def test_debug_0(self):
         # For this test, the runner will have already set the log level so it
         # may not be logging.ERROR.
-        _log = logging.getLogger('mail.log')
-        default_level = _log.getEffectiveLevel()
-        with patch.object(_log, 'info'):
+        log = logging.getLogger('mail.log')
+        default_level = log.getEffectiveLevel()
+        with patch.object(log, 'info'):
             main(('-n',))
-            self.assertEqual(_log.getEffectiveLevel(), default_level)
+            self.assertEqual(log.getEffectiveLevel(), default_level)
 
     def test_debug_1(self):
         # Mock the logger to eliminate console noise.
@@ -156,6 +142,91 @@ class TestMain(unittest.TestCase):
             self.assertTrue(asyncio.get_event_loop().get_debug())
 
 
+class TestLoop(unittest.TestCase):
+    def setUp(self):
+        # We mock out so much of this, is it even worthwhile testing?  Well, it
+        # does give us coverage.
+        self.loop = asyncio.get_event_loop()
+        pfunc = partial(patch.object, self.loop)
+        resources = ExitStack()
+        self.addCleanup(resources.close)
+        self.create_server = resources.enter_context(pfunc('create_server'))
+        self.run_until_complete = resources.enter_context(
+            pfunc('run_until_complete'))
+        self.add_signal_handler = resources.enter_context(
+            pfunc('add_signal_handler'))
+        resources.enter_context(
+            patch.object(logging.getLogger('mail.log'), 'info'))
+        self.run_forever = resources.enter_context(pfunc('run_forever'))
+
+    def test_loop(self):
+        main(('-n',))
+        # create_server() is called with a partial as the factory, and a
+        # socket object.
+        self.assertEqual(self.create_server.call_count, 1)
+        positional, keywords = self.create_server.call_args
+        self.assertEqual(positional[0].func, SMTP)
+        self.assertEqual(len(positional[0].args), 1)
+        self.assertIsInstance(positional[0].args[0], Debugging)
+        self.assertEqual(positional[0].keywords, dict(
+            data_size_limit=None,
+            enable_SMTPUTF8=False))
+        self.assertEqual(sorted(keywords), ['host', 'port'])
+        # run_until_complete() was called once.  The argument isn't important.
+        self.assertTrue(self.run_until_complete.called)
+        # add_signal_handler() is called with two arguments.
+        self.assertEqual(self.add_signal_handler.call_count, 1)
+        signal_number, callback = self.add_signal_handler.call_args[0]
+        self.assertEqual(signal_number, signal.SIGINT)
+        self.assertEqual(callback, self.loop.stop)
+        # run_forever() was called once.
+        self.assertEqual(self.run_forever.call_count, 1)
+
+    def test_loop_keyboard_interrupt(self):
+        # We mock out so much of this, is it even a worthwhile test?  Well, it
+        # does give us coverage.
+        self.run_forever.side_effect = KeyboardInterrupt
+        main(('-n',))
+        # loop.run_until_complete() was still executed.
+        self.assertTrue(self.run_until_complete.called)
+
+    def test_s(self):
+        # We mock out so much of this, is it even a worthwhile test?  Well, it
+        # does give us coverage.
+        main(('-n', '-s', '3000'))
+        positional, keywords = self.create_server.call_args
+        self.assertEqual(positional[0].keywords, dict(
+            data_size_limit=3000,
+            enable_SMTPUTF8=False))
+
+    def test_size(self):
+        # We mock out so much of this, is it even a worthwhile test?  Well, it
+        # does give us coverage.
+        main(('-n', '--size', '3000'))
+        positional, keywords = self.create_server.call_args
+        self.assertEqual(positional[0].keywords, dict(
+            data_size_limit=3000,
+            enable_SMTPUTF8=False))
+
+    def test_u(self):
+        # We mock out so much of this, is it even a worthwhile test?  Well, it
+        # does give us coverage.
+        main(('-n', '-u'))
+        positional, keywords = self.create_server.call_args
+        self.assertEqual(positional[0].keywords, dict(
+            data_size_limit=None,
+            enable_SMTPUTF8=True))
+
+    def test_smtputf8(self):
+        # We mock out so much of this, is it even a worthwhile test?  Well, it
+        # does give us coverage.
+        main(('-n', '--smtputf8'))
+        positional, keywords = self.create_server.call_args
+        self.assertEqual(positional[0].keywords, dict(
+            data_size_limit=None,
+            enable_SMTPUTF8=True))
+
+
 class TestParseArgs(unittest.TestCase):
     def test_handler_from_cli(self):
         # Ignore the host:port positional argument.
@@ -246,27 +317,3 @@ class TestParseArgs(unittest.TestCase):
             parseargs(('-v',))
             self.assertEqual(cm.exception.code, 0)
         self.assertEqual(stdout.getvalue(), 'smtpd {}\n'.format(__version__))
-
-
-class TestSigint(unittest.TestCase):
-    def setUp(self):
-        default_loop = asyncio.get_event_loop()
-        self.loop = asyncio.new_event_loop()
-        asyncio.set_event_loop(self.loop)
-        self.addCleanup(asyncio.set_event_loop, default_loop)
-
-    def test_keyboard_interrupt(self):
-        """
-        main() must close loop gracefully on Ctrl-C.
-        """
-
-        def interrupt():
-            raise KeyboardInterrupt
-        self.loop.call_later(1.5, interrupt)
-
-        try:
-            main(("-n",))
-        except Exception:
-            self.fail("main() should've closed cleanly without exceptions!")
-        else:
-            self.assertFalse(self.loop.is_running())
diff --git a/aiosmtpd/tests/test_server.py b/aiosmtpd/tests/test_server.py
index 545d9c5..c24467e 100644
--- a/aiosmtpd/tests/test_server.py
+++ b/aiosmtpd/tests/test_server.py
@@ -1,6 +1,6 @@
 """Test other aspects of the server implementation."""
 
-import os
+
 import socket
 import unittest
 
@@ -10,13 +10,6 @@ from aiosmtpd.smtp import SMTP as Server
 from smtplib import SMTP
 
 
-def in_wsl():
-    # WSL 1.0 somehow allows more than one listener on one port.
-    # So when testing on WSL, we must set PLATFORM=wsl and skip the
-    # "test_socket_error" test.
-    return os.environ.get("PLATFORM") == "wsl"
-
-
 class TestServer(unittest.TestCase):
     def test_smtp_utf8(self):
         controller = Controller(Sink())
@@ -36,8 +29,6 @@ class TestServer(unittest.TestCase):
         server.command_size_limits['DATA'] = 1024
         self.assertEqual(server.max_command_size_limit, 1024)
 
-    @unittest.skipIf(in_wsl(), "WSL prevents socket collisions")
-    # See explanation in the in_wsl() function
     def test_socket_error(self):
         # Testing starting a server with a port already in use
         s1 = Controller(Sink(), port=8025)
diff --git a/aiosmtpd/tests/test_smtp.py b/aiosmtpd/tests/test_smtp.py
index 84782b7..280889b 100644
--- a/aiosmtpd/tests/test_smtp.py
+++ b/aiosmtpd/tests/test_smtp.py
@@ -7,15 +7,8 @@ import unittest
 
 from aiosmtpd.controller import Controller
 from aiosmtpd.handlers import Sink
-from aiosmtpd.smtp import MISSING, SMTP as Server, __ident__ as GREETING
-from aiosmtpd.testing.helpers import (
-    SUPPORTED_COMMANDS_NOTLS,
-    assert_auth_invalid,
-    assert_auth_required,
-    assert_auth_success,
-    reset_connection,
-)
-from base64 import b64encode
+from aiosmtpd.smtp import SMTP as Server, __ident__ as GREETING
+from aiosmtpd.testing.helpers import reset_connection
 from contextlib import ExitStack
 from smtplib import (
     SMTP, SMTPDataError, SMTPResponseException, SMTPServerDisconnected)
@@ -25,62 +18,9 @@ CRLF = '\r\n'
 BCRLF = b'\r\n'
 
 
-def authenticator(mechanism, login, password):
-    if login and login.decode() == 'goodlogin':
-        return True
-    else:
-        return False
-
-
 class DecodingController(Controller):
     def factory(self):
-        return Server(self.handler, decode_data=True, enable_SMTPUTF8=True,
-                      auth_require_tls=False, auth_callback=authenticator)
-
-
-class PeekerHandler:
-    def __init__(self):
-        self.session = None
-
-    async def handle_MAIL(
-            self, server, session, envelope, address, mail_options
-    ):
-        self.session = session
-        return "250 OK"
-
-    async def auth_NULL(
-            self, server, args
-    ):
-        return "NULL_login"
-
-    async def auth_DONT(
-            self, server, args
-    ):
-        return MISSING
-
-
-class PeekerAuth:
-    def __init__(self):
-        self.login = None
-        self.password = None
-
-    def authenticate(
-            self, mechanism: str, login: bytes, password: bytes
-    ) -> bool:
-        self.login = login
-        self.password = password
-        return True
-
-
-auth_peeker = PeekerAuth()
-
-
-class DecodingControllerPeekAuth(Controller):
-    def factory(self):
-        return Server(self.handler, decode_data=True, enable_SMTPUTF8=True,
-                      auth_require_tls=False,
-                      auth_callback=auth_peeker.authenticate,
-                      **self.server_kwargs)
+        return Server(self.handler, decode_data=True, enable_SMTPUTF8=True)
 
 
 class NoDecodeController(Controller):
@@ -89,17 +29,8 @@ class NoDecodeController(Controller):
 
 
 class TimeoutController(Controller):
-    Delay: float = 2.0
-
-    def factory(self):
-        return Server(self.handler, timeout=self.Delay)
-
-
-class RequiredAuthDecodingController(Controller):
     def factory(self):
-        return Server(self.handler, decode_data=True, enable_SMTPUTF8=True,
-                      auth_require_tls=False, auth_callback=authenticator,
-                      auth_required=True)
+        return Server(self.handler, timeout=0.1)
 
 
 class ReceivingHandler:
@@ -312,15 +243,10 @@ class TestSMTP(unittest.TestCase):
             code, response = client.ehlo('example.com')
             self.assertEqual(code, 250)
             lines = response.splitlines()
-            expecteds = (
-                bytes(socket.getfqdn(), 'utf-8'),
-                b'SIZE 33554432',
-                b'SMTPUTF8',
-                b'AUTH LOGIN PLAIN',
-                b'HELP',
-            )
-            for actual, expected in zip(lines, expecteds):
-                self.assertEqual(actual, expected)
+            self.assertEqual(lines[0], bytes(socket.getfqdn(), 'utf-8'))
+            self.assertEqual(lines[1], b'SIZE 33554432')
+            self.assertEqual(lines[2], b'SMTPUTF8')
+            self.assertEqual(lines[3], b'HELP')
 
     def test_ehlo_duplicate(self):
         with SMTP(*self.address) as client:
@@ -379,7 +305,9 @@ class TestSMTP(unittest.TestCase):
             # Don't get tricked by smtplib processing of the response.
             code, response = client.docmd('HELP')
             self.assertEqual(code, 250)
-            self.assertEqual(response, SUPPORTED_COMMANDS_NOTLS)
+            self.assertEqual(response,
+                             b'Supported commands: DATA EHLO HELO HELP MAIL '
+                             b'NOOP QUIT RCPT RSET VRFY')
 
     def test_help_helo(self):
         with SMTP(*self.address) as client:
@@ -459,18 +387,14 @@ class TestSMTP(unittest.TestCase):
             self.assertEqual(code, 250)
             self.assertEqual(response, b'Syntax: VRFY <address>')
 
-    def test_help_auth(self):
-        with SMTP(*self.address) as client:
-            code, response = client.docmd('HELP', 'AUTH')
-            self.assertEqual(code, 250)
-            self.assertEqual(response, b'Syntax: AUTH <mechanism>')
-
     def test_help_bad_arg(self):
         with SMTP(*self.address) as client:
             # Don't get tricked by smtplib processing of the response.
             code, response = client.docmd('HELP me!')
             self.assertEqual(code, 501)
-            self.assertEqual(response, SUPPORTED_COMMANDS_NOTLS)
+            self.assertEqual(response,
+                             b'Supported commands: DATA EHLO HELO HELP MAIL '
+                             b'NOOP QUIT RCPT RSET VRFY')
 
     def test_expn(self):
         with SMTP(*self.address) as client:
@@ -708,10 +632,9 @@ class TestSMTP(unittest.TestCase):
             code, response = client.docmd('VRFY <anne@example.com>')
             self.assertEqual(code, 252)
             self.assertEqual(
-                response,
-                b'Cannot VRFY user, but will accept message and '
-                b'attempt delivery'
-                )
+              response,
+              b'Cannot VRFY user, but will accept message and attempt delivery'
+              )
 
     def test_vrfy_no_arg(self):
         with SMTP(*self.address) as client:
@@ -771,343 +694,6 @@ class TestSMTP(unittest.TestCase):
                 response,
                 b'Error: command "FOOBAR" not recognized')
 
-    def test_auth_no_ehlo(self):
-        with SMTP(*self.address) as client:
-            code, response = client.docmd('AUTH')
-            self.assertEqual(code, 503)
-            self.assertEqual(response, b'Error: send EHLO first')
-
-    def test_auth_helo(self):
-        with SMTP(*self.address) as client:
-            client.helo('example.com')
-            code, response = client.docmd('AUTH')
-            self.assertEqual(code, 500)
-            self.assertEqual(response, b"Error: command 'AUTH' not recognized")
-
-    def test_auth_too_many_values(self):
-        with SMTP(*self.address) as client:
-            client.ehlo('example.com')
-            code, response = client.docmd('AUTH PLAIN NONE NONE')
-            self.assertEqual(code, 501)
-            self.assertEqual(response, b'Too many values')
-
-    def test_auth_not_enough_values(self):
-        with SMTP(*self.address) as client:
-            client.ehlo('example.com')
-            code, response = client.docmd('AUTH')
-            self.assertEqual(code, 501)
-            self.assertEqual(response, b'Not enough value')
-
-    def test_auth_not_supported_methods(self):
-        for method in ('GSSAPI', 'DIGEST-MD5', 'MD5', 'CRAM-MD5'):
-            with SMTP(*self.address) as client:
-                client.ehlo('example.com')
-                code, response = client.docmd('AUTH ' + method)
-                self.assertEqual(code, 504)
-                self.assertEqual(
-                    response, b'5.5.4 Unrecognized authentication type')
-
-    def test_auth_already_authenticated(self):
-        with SMTP(*self.address) as client:
-            client.ehlo('example.com')
-            code, response = client.docmd(
-                'AUTH PLAIN ' +
-                b64encode(b'\0goodlogin\0goodpasswd').decode()
-                )
-            assert_auth_success(self, code, response)
-            code, response = client.docmd('AUTH')
-            self.assertEqual(code, 503)
-            self.assertEqual(response, b'Already authenticated')
-
-    def test_auth_bad_base64_encoding(self):
-        with SMTP(*self.address) as client:
-            client.ehlo('example.com')
-            code, response = client.docmd('AUTH PLAIN not-b64')
-            self.assertEqual(code, 501)
-            self.assertEqual(response, b"5.5.2 Can't decode base64")
-
-    def test_auth_bad_base64_length(self):
-        with SMTP(*self.address) as client:
-            client.ehlo('example.com')
-            code, response = client.docmd(
-                'AUTH PLAIN ' + b64encode(b'\0onlylogin').decode())
-            self.assertEqual(code, 501)
-            self.assertEqual(response, b"5.5.2 Can't split auth value")
-
-    def test_auth_bad_credentials(self):
-        with SMTP(*self.address) as client:
-            client.ehlo('example.com')
-            code, response = client.docmd(
-                'AUTH PLAIN ' +
-                b64encode(b'\0badlogin\0badpasswd').decode()
-                )
-            assert_auth_invalid(self, code, response)
-
-    def test_auth_two_steps_good_credentials(self):
-        with SMTP(*self.address) as client:
-            client.ehlo('example.com')
-            code, response = client.docmd('AUTH PLAIN')
-            self.assertEqual(code, 334)
-            self.assertEqual(response, b'')
-            code, response = client.docmd(
-                b64encode(b'\0goodlogin\0goodpasswd').decode()
-            )
-            assert_auth_success(self, code, response)
-
-    def test_auth_two_steps_bad_credentials(self):
-        with SMTP(*self.address) as client:
-            client.ehlo('example.com')
-            code, response = client.docmd('AUTH PLAIN')
-            self.assertEqual(code, 334)
-            self.assertEqual(response, b'')
-            code, response = client.docmd(
-                b64encode(b'\0badlogin\0badpasswd').decode()
-            )
-            assert_auth_invalid(self, code, response)
-
-    def test_auth_two_steps_abort(self):
-        with SMTP(*self.address) as client:
-            client.ehlo('example.com')
-            code, response = client.docmd('AUTH PLAIN')
-            self.assertEqual(code, 334)
-            self.assertEqual(response, b'')
-            code, response = client.docmd('*')
-            self.assertEqual(code, 501)
-            self.assertEqual(response, b'Auth aborted')
-
-    def test_auth_two_steps_bad_base64_encoding(self):
-        with SMTP(*self.address) as client:
-            client.ehlo('example.com')
-            code, response = client.docmd('AUTH PLAIN')
-            self.assertEqual(code, 334)
-            code, response = client.docmd("ab@%")
-            self.assertEqual(code, 501)
-            self.assertEqual(response, b"5.5.2 Can't decode base64")
-
-    def test_auth_good_credentials(self):
-        with SMTP(*self.address) as client:
-            client.ehlo('example.com')
-            code, response = client.docmd(
-                'AUTH PLAIN ' +
-                b64encode(b'\0goodlogin\0goodpasswd').decode()
-            )
-            assert_auth_success(self, code, response)
-
-    def test_auth_no_credentials(self):
-        with SMTP(*self.address) as client:
-            client.ehlo('example.com')
-            code, response = client.docmd('AUTH PLAIN =')
-            assert_auth_invalid(self, code, response)
-
-    def test_auth_two_steps_no_credentials(self):
-        with SMTP(*self.address) as client:
-            client.ehlo('example.com')
-            code, response = client.docmd('AUTH PLAIN')
-            self.assertEqual(code, 334)
-            self.assertEqual(response, b'')
-            code, response = client.docmd('=')
-            assert_auth_invalid(self, code, response)
-
-    def test_auth_login_multisteps_no_credentials(self):
-        with SMTP(*self.address) as client:
-            client.ehlo("example.com")
-            code, response = client.docmd("AUTH LOGIN")
-            self.assertEqual(code, 334)
-            self.assertEqual(response, b"VXNlciBOYW1lAA==")
-            code, response = client.docmd('=')
-            self.assertEqual(code, 334)
-            self.assertEqual(response, b"UGFzc3dvcmQA")
-            code, response = client.docmd('=')
-            assert_auth_invalid(self, code, response)
-
-
-class TestSMTPAuth(unittest.TestCase):
-    def setUp(self):
-        self.handler = PeekerHandler()
-        controller = DecodingControllerPeekAuth(
-            self.handler, server_kwargs={"auth_exclude_mechanism": ["DONT"]}
-        )
-        controller.start()
-        self.addCleanup(controller.stop)
-        self.address = (controller.hostname, controller.port)
-
-    def test_ehlo(self):
-        with SMTP(*self.address) as client:
-            code, response = client.ehlo('example.com')
-            self.assertEqual(code, 250)
-            lines = response.splitlines()
-            expecteds = (
-                bytes(socket.getfqdn(), 'utf-8'),
-                b'SIZE 33554432',
-                b'SMTPUTF8',
-                b'AUTH LOGIN NULL PLAIN',
-                b'HELP',
-            )
-            for actual, expected in zip(lines, expecteds):
-                self.assertEqual(actual, expected)
-
-    def test_auth_plain_null_credential(self):
-        with SMTP(*self.address) as client:
-            client.ehlo("example.com")
-            code, response = client.docmd("AUTH PLAIN")
-            self.assertEqual(code, 334)
-            self.assertEqual(response, b"")
-            code, response = client.docmd('=')
-            assert_auth_success(self, code, response)
-            self.assertEqual(auth_peeker.login, None)
-            self.assertEqual(auth_peeker.password, None)
-            code, response = client.mail("alice@example.com")
-            self.assertEqual(self.handler.session.login_data, b"")
-
-    def test_auth_login_null_credential(self):
-        with SMTP(*self.address) as client:
-            client.ehlo("example.com")
-            code, response = client.docmd("AUTH LOGIN")
-            self.assertEqual(code, 334)
-            self.assertEqual(response, b"VXNlciBOYW1lAA==")
-            code, response = client.docmd('=')
-            self.assertEqual(code, 334)
-            self.assertEqual(response, b"UGFzc3dvcmQA")
-            code, response = client.docmd('=')
-            assert_auth_success(self, code, response)
-            self.assertEqual(auth_peeker.login, None)
-            self.assertEqual(auth_peeker.password, None)
-            code, response = client.mail("alice@example.com")
-            self.assertEqual(self.handler.session.login_data, b"")
-
-    def test_auth_login_abort_login(self):
-        with SMTP(*self.address) as client:
-            client.ehlo("example.com")
-            code, response = client.docmd("AUTH LOGIN")
-            self.assertEqual(code, 334)
-            self.assertEqual(response, b"VXNlciBOYW1lAA==")
-            code, response = client.docmd('*')
-            self.assertEqual(code, 501)
-            self.assertEqual(response, b"Auth aborted")
-
-    def test_auth_login_abort_password(self):
-        auth_peeker.return_val = False
-        with SMTP(*self.address) as client:
-            client.ehlo("example.com")
-            code, response = client.docmd("AUTH LOGIN")
-            self.assertEqual(code, 334)
-            self.assertEqual(response, b"VXNlciBOYW1lAA==")
-            code, response = client.docmd('=')
-            self.assertEqual(code, 334)
-            self.assertEqual(response, b"UGFzc3dvcmQA")
-            code, response = client.docmd('*')
-            self.assertEqual(code, 501)
-            self.assertEqual(response, b"Auth aborted")
-
-    def test_auth_custom_mechanism(self):
-        auth_peeker.return_val = False
-        with SMTP(*self.address) as client:
-            client.ehlo("example.com")
-            code, response = client.docmd("AUTH NULL")
-            assert_auth_success(self, code, response)
-
-    def test_auth_disabled_mechanism(self):
-        with SMTP(*self.address) as client:
-            client.ehlo("example.com")
-            code, response = client.docmd("AUTH DONT")
-            self.assertEqual(code, 504)
-            self.assertEqual(response,
-                             b"5.5.4 Unrecognized authentication type")
-
-
-class TestRequiredAuthentication(unittest.TestCase):
-    def setUp(self):
-        controller = RequiredAuthDecodingController(Sink)
-        controller.start()
-        self.addCleanup(controller.stop)
-        self.address = (controller.hostname, controller.port)
-
-    def test_help_unauthenticated(self):
-        with SMTP(*self.address) as client:
-            code, response = client.docmd('HELP')
-            assert_auth_required(self, code, response)
-
-    def test_vrfy_unauthenticated(self):
-        with SMTP(*self.address) as client:
-            code, response = client.docmd('VRFY <anne@example.com>')
-            assert_auth_required(self, code, response)
-
-    def test_mail_unauthenticated(self):
-        with SMTP(*self.address) as client:
-            client.ehlo('example.com')
-            code, response = client.docmd('MAIL FROM: <anne@example.com>')
-            assert_auth_required(self, code, response)
-
-    def test_rcpt_unauthenticated(self):
-        with SMTP(*self.address) as client:
-            client.ehlo('example.com')
-            code, response = client.docmd('RCPT TO: <anne@example.com>')
-            assert_auth_required(self, code, response)
-
-    def test_data_unauthenticated(self):
-        with SMTP(*self.address) as client:
-            client.ehlo('example.com')
-            code, response = client.docmd('DATA')
-            assert_auth_required(self, code, response)
-
-    def test_help_authenticated(self):
-        with SMTP(*self.address) as client:
-            client.ehlo('example.com')
-            code, response = client.docmd(
-                'AUTH PLAIN ' +
-                b64encode(b'\0goodlogin\0goodpasswd').decode()
-            )
-            assert_auth_success(self, code, response)
-            code, response = client.docmd('HELP')
-            self.assertEqual(code, 250)
-            self.assertEqual(response, SUPPORTED_COMMANDS_NOTLS)
-
-    def test_vrfy_authenticated(self):
-        with SMTP(*self.address) as client:
-            client.ehlo('example.com')
-            code, response = client.docmd(
-                'AUTH PLAIN ' +
-                b64encode(b'\0goodlogin\0goodpasswd').decode()
-            )
-            assert_auth_success(self, 235, response)
-            code, response = client.docmd('VRFY <anne@example.com>')
-            self.assertEqual(code, 252)
-            self.assertEqual(
-                response,
-                b'Cannot VRFY user, but will accept message and '
-                b'attempt delivery'
-            )
-
-    def test_mail_authenticated(self):
-        with SMTP(*self.address) as client:
-            client.ehlo('example.com')
-            code, response = client.docmd(
-                'AUTH PLAIN ' +
-                b64encode(b'\0goodlogin\0goodpasswd').decode()
-            )
-            assert_auth_success(self, code, response)
-            code, response = client.docmd('MAIL FROM: <anne@example.com>')
-            self.assertEqual(code, 250)
-            self.assertEqual(response, b'OK')
-
-    def test_rcpt_authenticated(self):
-        with SMTP(*self.address) as client:
-            client.ehlo('example.com')
-            code, response = client.docmd(
-                'AUTH PLAIN ' +
-                b64encode(b'\0goodlogin\0goodpasswd').decode()
-            )
-            assert_auth_success(self, code, response)
-            code, response = client.docmd('RCPT TO: <anne@example.com>')
-            self.assertEqual(code, 503)
-            self.assertEqual(response, b'Error: need MAIL command')
-
-    def test_data_authenticated(self):
-        with SMTP(*self.address) as client:
-            client.ehlo('example.com')
-            code, response = client.docmd('DATA')
-            assert_auth_required(self, code, response)
-
 
 class TestResetCommands(unittest.TestCase):
     """Test that sender and recipients are reset on RSET, HELO, and EHLO.
@@ -1601,5 +1187,5 @@ class TestTimeout(unittest.TestCase):
     def test_timeout(self):
         with SMTP(*self.address) as client:
             code, response = client.ehlo('example.com')
-            time.sleep(0.1 + TimeoutController.Delay)
+            time.sleep(0.3)
             self.assertRaises(SMTPServerDisconnected, client.getreply)
diff --git a/aiosmtpd/tests/test_starttls.py b/aiosmtpd/tests/test_starttls.py
index 4a53cfd..f7c1fde 100644
--- a/aiosmtpd/tests/test_starttls.py
+++ b/aiosmtpd/tests/test_starttls.py
@@ -5,10 +5,6 @@ import pkg_resources
 from aiosmtpd.controller import Controller as BaseController
 from aiosmtpd.handlers import Sink
 from aiosmtpd.smtp import SMTP as SMTPProtocol
-from aiosmtpd.testing.helpers import (
-    SUPPORTED_COMMANDS_TLS,
-    assert_auth_invalid,
-)
 from email.mime.text import MIMEText
 from smtplib import SMTP
 
@@ -53,15 +49,6 @@ class TLSController(Controller):
             tls_context=get_tls_context())
 
 
-class RequireTLSAuthDecodingController(Controller):
-    def factory(self):
-        return SMTPProtocol(
-            self.handler,
-            decode_data=True,
-            auth_require_tls=True,
-            tls_context=get_tls_context())
-
-
 class HandshakeFailingHandler:
     def handle_STARTTLS(self, server, session, envelope):
         return False
@@ -124,7 +111,9 @@ class TestStartTLS(unittest.TestCase):
             # Don't get tricked by smtplib processing of the response.
             code, response = client.docmd('HELP')
             self.assertEqual(code, 250)
-            self.assertEqual(response, SUPPORTED_COMMANDS_TLS)
+            self.assertEqual(response,
+                             b'Supported commands: DATA EHLO HELO HELP MAIL '
+                             b'NOOP QUIT RCPT RSET STARTTLS VRFY')
 
 
 class TestTLSForgetsSessionData(unittest.TestCase):
@@ -210,27 +199,3 @@ class TestRequireTLS(unittest.TestCase):
             client.ehlo('example.com')
             code, response = client.docmd('DATA')
             self.assertEqual(code, 530)
-
-
-class TestRequireTLSAUTH(unittest.TestCase):
-    def setUp(self):
-        controller = RequireTLSAuthDecodingController(Sink)
-        controller.start()
-        self.addCleanup(controller.stop)
-        self.address = (controller.hostname, controller.port)
-
-    def test_auth_notls(self):
-        with SMTP(*self.address) as client:
-            client.ehlo('example.com')
-            code, response = client.docmd("AUTH ")
-            self.assertEqual(code, 538)
-            self.assertEqual(response,
-                             b"5.7.11 Encryption required for requested "
-                             b"authentication mechanism")
-
-    def test_auth_tls(self):
-        with SMTP(*self.address) as client:
-            client.starttls()
-            client.ehlo('example.com')
-            code, response = client.docmd('AUTH PLAIN AHRlc3QAdGVzdA==')
-            assert_auth_invalid(self, code, response)
diff --git a/unittest.cfg b/unittest.cfg
index 139241e..0439102 100644
--- a/unittest.cfg
+++ b/unittest.cfg
@@ -1,19 +1,13 @@
 [unittest]
 verbose = 2
-plugins =
-    nose2.plugins.prof
+plugins = flufl.testing.nose
 
 [log-capture]
 always-on = False
-clear-handlers = True
-date-format = None
-; format = %%(name)s: %%(levelname)s: %%(message)s
-format = %%(asctime)s: %%(name)s: %%(levelname)s: %%(message)s
-filter = -nose
-log-level = DEBUG
 
-[profiler]
-always-on = False
-filename = _dynamic/cPROFILE
-restrict =
-sort = ncalls
+[flufl.testing]
+always-on = True
+package = aiosmtpd
+setup = aiosmtpd.testing.helpers.setup
+teardown = aiosmtpd.testing.helpers.teardown
+start_run = aiosmtpd.testing.helpers.start