Repository: aio-libs/aiosmtpd
Pull request #199 (2020-10-30 13:57:57)

Release 1.2.2
A version bump for uploading to PyPI


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/setup.py b/setup.py
index 47d51eb..eacb7b3 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')
 
 
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