Repository: aio-libs/aiosmtpd
Pull request #49 (2017-03-24 18:10:24)

Lots of new documentation.
Also, change the handle_tls_handshake() argument to be the session object,
fixing #48


Patch:
diff --git a/README.rst b/README.rst
index b9ff865..ff710c6 100644
--- a/README.rst
+++ b/README.rst
@@ -90,8 +90,11 @@ Contents
    :maxdepth: 2
 
    aiosmtpd/docs/intro
+   aiosmtpd/docs/smtp
+   aiosmtpd/docs/lmtp
    aiosmtpd/docs/controller
-   aiosmtpd/docs/mailbox
+   aiosmtpd/docs/handlers
+   aiosmtpd/docs/cli
    aiosmtpd/docs/NEWS
 
 
diff --git a/aiosmtpd/docs/NEWS.rst b/aiosmtpd/docs/NEWS.rst
index 0df7dc4..4f510d4 100644
--- a/aiosmtpd/docs/NEWS.rst
+++ b/aiosmtpd/docs/NEWS.rst
@@ -15,6 +15,9 @@
 * Controller objects now have an optional timeout argument used to wait for
   the server to become ready.  This can also be overridden with the
   environment variable ``AIOSMTPD_CONTROLLER_TIMEOUT``.
+* Handlers can define a ``handle_tls_handshake()`` method, which takes a
+  session object, and is called if SSL is enabled during the making of the
+  connection.
 * Better Python 3.4 compatibility.
 
 1.0a4 (2016-11-29)
diff --git a/aiosmtpd/docs/cli.rst b/aiosmtpd/docs/cli.rst
new file mode 100644
index 0000000..a16000b
--- /dev/null
+++ b/aiosmtpd/docs/cli.rst
@@ -0,0 +1,77 @@
+.. _cli:
+
+====================
+ Command line usage
+====================
+
+This package also provides a main entry point which can be used to run the
+server on the command line.  There are two ways to run the server, depending
+on how the package has been installed.
+
+You can run the server by passing it to Python directly::
+
+    $ python3 -m aiosmtpd -n
+
+This starts a server on localhost, port 8025 without setting the uid to
+'nobody' (i.e. because you aren't running it as root).  Once you've done that,
+you can connect directly to the server using your favorite command line
+protocol tool.  Type the ``QUIT`` command at the server once you see the
+greeting::
+
+    % telnet localhost 8025
+    Trying 127.0.0.1...
+    Connected to localhost.
+    Escape character is '^]'.
+    220 subdivisions Python SMTP 1.0a4+
+    QUIT
+    221 Bye
+    Connection closed by foreign host.
+
+Of course, you could use Python's smtplib_ module, or any other SMTP client to
+talk to the server.  Just hit control-C at the server to stop it.
+
+The entry point may also be installed as the ``smtpd`` command, so this is
+equivalent to the above ``python3`` invocation::
+
+    $ smtpd -n
+
+
+Options
+=======
+
+Optional arguments include:
+
+``-h``, ``--help``
+    Show this help message and exit.
+
+``-n``, ``--nosetuid``
+    This program generally tries to setuid ``nobody``, unless this flag is
+    set.  The setuid call will fail if this program is not run as root (in
+    which case, use this flag).
+
+``-c CLASSPATH``, ``--class CLASSPATH``
+    Use the given class, as a Python dotted import path, as the handler class
+    for SMTP events.  This class can process received messages and do other
+    actions during the SMTP dialog.  Uses a debugging handler by default.
+
+``-s SIZE``, ``--size SIZE``
+    Restrict the total size of the incoming message to ``SIZE`` number of
+    bytes via the RFC 1870 ``SIZE`` extension.  Defaults to 33554432 bytes.
+
+``-u``, ``--smtputf8``
+    Enable the SMTPUTF8 extension and behave as an RFC 6531 SMTP proxy.
+
+``-d``, ``--debug``
+    Increase debugging output.
+
+``-l [HOST:PORT]``, ``--listen [HOST:PORT]``
+    Optional host and port to listen on.  If the PORT part is not given, then
+    port 8025 is used.  If only :PORT is given, then localhost is used for the
+    hostname.  If neither are given, localhost:8025 is used.
+
+Optional positional arguments provide additional arguments to the handler
+class constructor named in the ``--class`` option.  Provide as many of these
+as supported by the handler class's ``from_cli()`` class method, if provided.
+
+
+.. _smtplib: https://docs.python.org/3/library/smtplib.html
diff --git a/aiosmtpd/docs/controller.rst b/aiosmtpd/docs/controller.rst
index 2b87e9e..828471a 100644
--- a/aiosmtpd/docs/controller.rst
+++ b/aiosmtpd/docs/controller.rst
@@ -1,10 +1,13 @@
+.. _controller:
+
 =======================
  The testing framework
 =======================
 
-The SMTP server can be used in a testing framework via a controller which runs
-in a separate thread.  This allows the main thread to run the test driver, and
-information can be passed between the SMTP thread and the main thread.
+The SMTP server can be used in a testing framework via a *controller* which
+runs in a separate thread.  This allows the main thread to run the test
+driver, and information can be passed between the SMTP thread and the main
+thread.
 
 For example, say you wanted to pass message objects between the SMTP thread
 and the main thread.  Start by implementing a handler which derives from a
diff --git a/aiosmtpd/docs/handlers.rst b/aiosmtpd/docs/handlers.rst
new file mode 100644
index 0000000..854523e
--- /dev/null
+++ b/aiosmtpd/docs/handlers.rst
@@ -0,0 +1,134 @@
+==========
+ Handlers
+==========
+
+Handlers are classes which can implement :ref:`hook methods <hooks>` that get
+called at various points in the SMTP dialog.  Handlers can also be named on
+the :ref:`command line <cli>`, but if the class's constructor takes arguments,
+you must define a ``@classmethod`` that converts the positional arguments and
+returns a handler instance:
+
+``from_cli(cls, parser, *args)``
+    Convert the positional arguments, as strings passed in on the command
+    line, into a handler instance.  ``parser`` is the ArgumentParser_ instance
+    in use.
+
+If ``from_cli()`` is not defined, the handler can still be used on the command
+line, but its constructor cannot accept arguments.
+
+
+Built-in handlers
+=================
+
+The following built-in handlers can be imported from ``aiosmtpd.handlers``:
+
+* ``Debugging`` - this class prints the contents of the received messages to a
+  given output stream.  Programmatically, you can pass the stream to print to
+  into the constructor.  When specified on the command line, the positional
+  argument must either be the string ``stdout`` or ``stderr`` indicating which
+  stream to use.
+
+* ``Proxy`` - this class is a relatively simple SMTP proxy; it forwards
+  messages to a remote host and port.  The constructor takes the host name and
+  port as positional arguments.  This class cannot be used on the command
+  line.
+
+* ``Sink`` - this class just consumes and discards messages.  It's essentiall
+  the "no op" handler.  It can be used on the command line, but accepts no
+  positional arguments.
+
+* ``Message`` - this class is a base class (it must be subclassed) which
+  converts the message content into a message instance.  The class used to
+  create these instances can be passed to the constructor, and defaults to
+  `email.message.Message`_.
+
+  This message instance gains a few additional headers (e.g. ``X-Peer``,
+  ``X-MailFrom``, and ``X-RcptTo``).  You can override this behavior by
+  overriding the ``prepare_message()`` method, which takes a session and an
+  envelope.  The message instance is then passed to the handler's
+  ``handle_message()`` method.  It is this method that must be implemented in
+  the subclass.  ``prepare_message()`` and ``handle_message()`` are both
+  called *synchronously*.  This handler cannot be used on the command line.
+
+* ``AsyncMessage`` - a subclass of the ``Message`` handler, with the only
+  difference being that ``handle_message()`` is called *asynchronously*.  This
+  handler cannot be used on the command line.
+
+* ``Mailbox`` - a subclass of the ``Message`` handler which adds the messages
+  to a Maildir_.  See below for details.
+
+
+The Mailbox handler
+===================
+
+A convenient handler is the ``Mailbox`` handler, which stores incoming
+messages into a maildir::
+
+    >>> import os
+    >>> from aiosmtpd.controller import Controller
+    >>> from aiosmtpd.handlers import Mailbox
+    >>> from tempfile import TemporaryDirectory
+    >>> # Clean up the temporary directory at the end of this doctest.
+    >>> tempdir = resources.enter_context(TemporaryDirectory())
+
+    >>> maildir_path = os.path.join(tempdir, 'maildir')
+    >>> controller = Controller(Mailbox(maildir_path))
+    >>> controller.start()
+    >>> # Arrange for the controller to be stopped at the end of this doctest.
+    >>> ignore = resources.callback(controller.stop)
+
+Now we can connect to the server and send it a message...
+
+    >>> from smtplib import SMTP
+    >>> client = SMTP(controller.hostname, controller.port)
+    >>> client.sendmail('aperson@example.com', ['bperson@example.com'], """\
+    ... From: Anne Person <anne@example.com>
+    ... To: Bart Person <bart@example.com>
+    ... Subject: A test
+    ... Message-ID: <ant>
+    ...
+    ... Hi Bart, this is Anne.
+    ... """)
+    {}
+
+...and a second message...
+
+    >>> client.sendmail('cperson@example.com', ['dperson@example.com'], """\
+    ... From: Cate Person <cate@example.com>
+    ... To: Dave Person <dave@example.com>
+    ... Subject: A test
+    ... Message-ID: <bee>
+    ...
+    ... Hi Dave, this is Cate.
+    ... """)
+    {}
+
+...and a third message.
+
+    >>> client.sendmail('eperson@example.com', ['fperson@example.com'], """\
+    ... From: Elle Person <elle@example.com>
+    ... To: Fred Person <fred@example.com>
+    ... Subject: A test
+    ... Message-ID: <cat>
+    ...
+    ... Hi Fred, this is Elle.
+    ... """)
+    {}
+
+We open up the mailbox again, and all three messages are waiting for us.
+
+    >>> from mailbox import Maildir
+    >>> from operator import itemgetter
+    >>> mailbox = Maildir(maildir_path)
+    >>> messages = sorted(mailbox, key=itemgetter('message-id'))
+    >>> for message in messages:
+    ...     print(message['Message-ID'], message['From'], message['To'])
+    <ant> Anne Person <anne@example.com> Bart Person <bart@example.com>
+    <bee> Cate Person <cate@example.com> Dave Person <dave@example.com>
+    <cat> Elle Person <elle@example.com> Fred Person <fred@example.com>
+
+
+
+.. _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
diff --git a/aiosmtpd/docs/lmtp.rst b/aiosmtpd/docs/lmtp.rst
new file mode 100644
index 0000000..fcb7358
--- /dev/null
+++ b/aiosmtpd/docs/lmtp.rst
@@ -0,0 +1,14 @@
+================
+ The LMTP class
+================
+
+`RFC 2033 <http://www.faqs.org/rfcs/rfc2033.html>`_ defines the Local Mail
+Transport Protocol.  In many ways, this is very similar to SMTP, but with no
+guarantees of queuing.  It is, in a sense, an alternative to ESMTP, and is
+often used for local mail routing (e.g. from a Mail Transport Agent to a local
+command or system) where the unreliability of internet connectivity is not an
+issue.
+
+The ``LMTP`` class subclasses the ``SMTP`` class and its only functional
+difference is that it implements the ``LHLO`` command, and prohibits the use
+of ``HELO`` and ``EHLO``.
diff --git a/aiosmtpd/docs/mailbox.rst b/aiosmtpd/docs/mailbox.rst
deleted file mode 100644
index 0b17714..0000000
--- a/aiosmtpd/docs/mailbox.rst
+++ /dev/null
@@ -1,67 +0,0 @@
-=====================
- The mailbox handler
-=====================
-
-A convenient handler is the mailbox handler, which stores incoming messages
-into a maildir.  Below, ``resources`` is a global that enables cleaning up
-after the tests are done.
-
-    >>> import os
-    >>> from aiosmtpd.controller import Controller
-    >>> from aiosmtpd.handlers import Mailbox
-    >>> from tempfile import TemporaryDirectory
-    >>> tempdir = resources.enter_context(TemporaryDirectory())
-    >>> maildir_path = os.path.join(tempdir, 'maildir')
-    >>> controller = Controller(Mailbox(maildir_path))
-    >>> controller.start()
-    >>> ignore = resources.callback(controller.stop)
-
-Now we can connect to the server and send it a message...
-
-    >>> from smtplib import SMTP
-    >>> client = SMTP(controller.hostname, controller.port)
-    >>> client.sendmail('aperson@example.com', ['bperson@example.com'], """\
-    ... From: Anne Person <anne@example.com>
-    ... To: Bart Person <bart@example.com>
-    ... Subject: A test
-    ... Message-ID: <ant>
-    ...
-    ... Hi Bart, this is Anne.
-    ... """)
-    {}
-
-...and a second message...
-
-    >>> client.sendmail('cperson@example.com', ['dperson@example.com'], """\
-    ... From: Cate Person <cate@example.com>
-    ... To: Dave Person <dave@example.com>
-    ... Subject: A test
-    ... Message-ID: <bee>
-    ...
-    ... Hi Dave, this is Cate.
-    ... """)
-    {}
-
-...and a third message.
-
-    >>> client.sendmail('eperson@example.com', ['fperson@example.com'], """\
-    ... From: Elle Person <elle@example.com>
-    ... To: Fred Person <fred@example.com>
-    ... Subject: A test
-    ... Message-ID: <cat>
-    ...
-    ... Hi Fred, this is Elle.
-    ... """)
-    {}
-
-We open up the mailbox again, and all three messages are waiting for us.
-
-    >>> from mailbox import Maildir
-    >>> from operator import itemgetter
-    >>> mailbox = Maildir(maildir_path)
-    >>> messages = sorted(mailbox, key=itemgetter('message-id'))
-    >>> for message in messages:
-    ...     print(message['Message-ID'], message['From'], message['To'])
-    <ant> Anne Person <anne@example.com> Bart Person <bart@example.com>
-    <bee> Cate Person <cate@example.com> Dave Person <dave@example.com>
-    <cat> Elle Person <elle@example.com> Fred Person <fred@example.com>
diff --git a/aiosmtpd/docs/smtp.rst b/aiosmtpd/docs/smtp.rst
new file mode 100644
index 0000000..2d8a6e5
--- /dev/null
+++ b/aiosmtpd/docs/smtp.rst
@@ -0,0 +1,211 @@
+================
+ The SMTP class
+================
+
+At the heart of this module is the ``SMTP`` class.  This class implements the
+`RFC 5321 <http://www.faqs.org/rfcs/rfc5321.html>`_ Simple Mail Transport
+Protocol.  Usually, you won't run an ``SMTP`` instance directly, but instead
+will use a :ref:`controller <controller>` instance wrapper, providing start
+and stop semantics in a subthread.
+
+    >>> from aiosmtpd.controller import Controller
+
+The ``SMTP`` class is itself a subclass of StreamReaderProtocol_.
+
+
+Subclassing
+===========
+
+The ``SMTP`` class is designed for derivation.  You can add new ``SMTP``
+methods or override existing semantics, and you can provide a *handler*
+instance to react to certain events during the ``SMTP`` dialog.
+
+For example, let's say you wanted to add a new method called ``PING`` and you
+wanted to count how many times the ``RSET`` command was called.  All methods
+implementing ``SMTP`` commands are prefixed with ``smtp_``.  Here's how you
+could implement these use cases::
+
+    >>> from aiosmtpd.smtp import SMTP as Server
+    >>> class MyServer(Server):
+    ...     def smtp_PING(self, arg):
+    ...         yield from self.push('259 OK')
+    ...
+    ...     def smtp_RSET(self, arg):
+    ...         self.event_handler.rset_calls += 1
+    ...         yield from super().smtp_RSET(arg)
+
+Now let's run this server in a controller::
+
+    >>> from aiosmtpd.handlers import Sink
+    >>> class Counter(Sink):
+    ...     def __init__(self):
+    ...         self.rset_calls = 0
+
+    >>> class MyController(Controller):
+    ...     def factory(self):
+    ...         return MyServer(self.handler)
+
+    >>> controller = MyController(Counter())
+    >>> controller.start()
+    >>> # Arrange for the controller to be stopped at the end of this doctest.
+    >>> ignore = resources.callback(controller.stop)
+
+We can now connect to this server with an ``SMTP`` client.
+
+    >>> from smtplib import SMTP
+    >>> client = SMTP(controller.hostname, controller.port)
+
+Let's ping the server.  Since the ``PING`` command isn't an official ``SMTP``
+command, we have to use the lower level interface to talk to it.
+
+    >>> code, message = client.docmd('PING')
+    >>> code
+    259
+    >>> message
+    b'OK'
+
+Now we can call ``RSET`` a few times and watch as the handler's counter gets
+incremented.
+
+    >>> code, message = client.rset()
+    >>> controller.handler.rset_calls
+    1
+    >>> code, message = client.rset()
+    >>> controller.handler.rset_calls
+    2
+    >>> code, message = client.rset()
+    >>> controller.handler.rset_calls
+    3
+
+
+Server hooks
+============
+
+The ``SMTP`` server class also implements some hooks which your subclass can
+override to provide additional responses.
+
+``ehlo_hook()``
+    This hook makes it possible for subclasses to return additional ``EHLO``
+    responses.  This method, called *asynchronously* and taking no arguments,
+    can do whatever it wants, including (most commonly) pushing new
+    ``250-<command>`` responses to the client.  This hook is called just
+    before the standard ``250 HELP`` which ends the ``EHLO`` response from the
+    server.
+
+``rset_hook()``
+    This hook makes it possible to return additional ``RSET`` responses.  This
+    method, called *asynchronously* and taking no arguments, is called just
+    before the standard ``250 OK`` which ends the ``RSET`` response from the
+    server.
+
+
+.. _hooks:
+
+Handler hooks
+=============
+
+Handlers can implement a few hooks that get called during the ``SMTP`` dialog,
+or in exceptional cases.  More hooks may be added in the future, but for now
+the following handler hooks are defined:
+
+``handle_tls_handshake(session)``
+    (*optional*, *synchronous*) If implemented, and if SSL is supported, this
+    handler method gets called during the TLS handshake phase of
+    ``connection_made()``.  It should return a boolean which specifies whether
+    the handshake failed or not.  ``session`` is an instance of the Session_
+    object.
+
+``handle_exception(error)``
+    (*optional*, *synchronous*) If implemented, this method is called when any
+    error occurs during the handling of a connection (e.g. if an
+    ``smtp_COMMAND()`` command raises an exception).  The exception object is
+    passed in.  Note that as part of the ``SMTP`` dialog, if an exception
+    occurs, a 500 code will be returned to the client.
+
+``handle_DATA(session, envelope)``
+    (*required*, *asynchronous*) This method is called on the handler so that
+    it can process the incoming ``DATA`` bytes.  The ``session`` and
+    ``envelope`` arguments are described below.  It returns the status string
+    to pass back to the client.  If status is not given (e.g. it is None),
+    then the string ``"250 OK"`` is used as the status.
+
+
+Sessions and envelopes
+======================
+
+To make current and future hooks easier to write, two helper classes are
+defined which provide attributes that can be of use to the
+``handle_COMMAND()`` methods on the handler.  You can actually override the
+use of these two classes by subclassing ``SMTP`` and defining the
+``_create_session()`` and ``_create_envelope()`` methods.  Both of these
+return the appropriate instance that will be used for the remainder of the
+connection.  New session instances are created when new connections are made,
+and new envelope instances are created at the beginning of an ``SMTP`` dialog,
+or whenver a ``RSET`` command is issued.
+
+
+Session
+-------
+
+``Session`` instances have the following attributes:
+
+``peer``
+    Defaulting to None, this attribute will contain the transport's socket's
+    peername_ value.
+
+``ssl``
+    Defaulting to None, this attribute will contain some extra information,
+    as a dictionary, from the ``asyncio.sslproto.SSLProtocol`` instance, which
+    can be used to pull additional information out about the connection.  This
+    attribute contains implementation-specific information so its contents may
+    change, but it should roughly correspond to the information available
+    `through this method`_.
+
+``host_name``
+    Defaulting to None, this attribute will contain the host name argument as
+    seen by the ``HELO`` or ``EHLO`` command.
+
+``extended_smtp``
+    Defaulting to False, this flag will be True when the ``EHLO`` greeting
+    was seen, indicating ESMTP_.
+
+``loop``
+    This is the asyncio event loop instance.
+
+
+Envelope
+--------
+
+``Envelope`` instances have the following attributes:
+
+``mail_from``
+    Defaulting to None, this attribute holds the email address given in the
+    ``MAIL FROM`` command.
+
+``mail_options``
+    Defaulting to None, this attribute contains a list of any ESMTP mail
+    options provided by the client, such as those passed in by `the smtplib
+    client`_.
+
+``content``
+    Defaulting to None, this attribute will contain the contents of the
+    message as provided by the ``DATA`` command.  If the ``decode_data``
+    parameter to the ``SMTP`` constructor was True (it defaults to False),
+    then this attribute will contain the UTF-8 decoded string, otherwise it
+    will contain the raw bytes.
+
+``rcpt_tos``
+    Defaulting to the empty list, this attribute will contain a list of the
+    email addresses provided in the ``RCPT TO`` command.
+
+``rcpt_options``
+    Defaulting to the empty list, this attribute will contain the list of any
+    recipient options provided by the client, such as those passed in by `the
+    smtplib client`_.
+
+
+.. _peername: https://docs.python.org/3/library/asyncio-protocol.html?highlight=peername#asyncio.BaseTransport.get_extra_info
+.. _`through this method`: https://docs.python.org/3/library/asyncio-protocol.html?highlight=get_extra_info#asyncio.BaseTransport.get_extra_info
+.. _ESMTP: http://www.faqs.org/rfcs/rfc1869.html
+.. _`the smtplib client`: https://docs.python.org/3/library/smtplib.html#smtplib.SMTP.sendmail
+.. _StreamReaderProtocol: https://docs.python.org/3/library/asyncio-stream.html#streamreaderprotocol
diff --git a/aiosmtpd/handlers.py b/aiosmtpd/handlers.py
index ad797cc..b374562 100644
--- a/aiosmtpd/handlers.py
+++ b/aiosmtpd/handlers.py
@@ -167,7 +167,6 @@ class Message:
         message['X-Peer'] = str(session.peer)
         message['X-MailFrom'] = envelope.mail_from
         message['X-RcptTo'] = COMMASPACE.join(envelope.rcpt_tos)
-
         return message
 
     def handle_message(self, message):
@@ -194,6 +193,7 @@ class AsyncMessage(Message):
 class Mailbox(Message):
     def __init__(self, mail_dir, message_class=None):
         self.mailbox = mailbox.Maildir(mail_dir)
+        self.mail_dir = mail_dir
         super().__init__(message_class)
 
     def handle_message(self, message):
@@ -201,3 +201,11 @@ class Mailbox(Message):
 
     def reset(self):
         self.mailbox.clear()
+
+    @classmethod
+    def from_cli(cls, parser, *args):
+        if len(args) < 1:
+            parser.error('The directory for the maildir is required')
+        elif len(args) > 1:
+            parser.error('Too many arguments for Mailbox handler')
+        return cls(args[0])
diff --git a/aiosmtpd/main.py b/aiosmtpd/main.py
index e623bb2..1bbb6ed 100644
--- a/aiosmtpd/main.py
+++ b/aiosmtpd/main.py
@@ -20,10 +20,15 @@ except ImportError:                                         # pragma: nocover
 DEFAULT_HOST = 'localhost'
 DEFAULT_PORT = 8025
 
+# Make the program name a little nicer, especially when `python3 -m aiosmtpd`
+# is used.
+PROGRAM = 'smtpd' if '__main__.py' in sys.argv[0] else sys.argv[0]
+
 
 def parseargs(args=None):
     parser = ArgumentParser(
-        description='An RFC 5321 SMTP server with extensions')
+        prog=PROGRAM,
+        description='An RFC 5321 SMTP server with extensions.')
     parser.add_argument(
         '-n', '--nosetuid',
         dest='setuid', default=True, action='store_false',
diff --git a/aiosmtpd/smtp.py b/aiosmtpd/smtp.py
index 3126689..42f876a 100644
--- a/aiosmtpd/smtp.py
+++ b/aiosmtpd/smtp.py
@@ -125,8 +125,7 @@ class SMTP(asyncio.StreamReaderProtocol):
             # Why _extra is protected attribute?
             self.session.ssl = self._tls_protocol._extra
             if hasattr(self.event_handler, 'handle_tls_handshake'):
-                auth = self.event_handler.handle_tls_handshake(
-                    self.session.ssl)
+                auth = self.event_handler.handle_tls_handshake(self.session)
                 self._tls_handshake_failed = not auth
             else:
                 self._tls_handshake_failed = False
@@ -168,9 +167,9 @@ class SMTP(asyncio.StreamReaderProtocol):
         yield from self._writer.drain()
 
     @asyncio.coroutine
-    def handle_exception(self, e):
+    def handle_exception(self, error):
         if hasattr(self.event_handler, 'handle_exception'):
-            yield from self.event_handler.handle_exception(e)
+            yield from self.event_handler.handle_exception(error)
 
     @asyncio.coroutine
     def _handle_client(self):


Test patch:
diff --git a/aiosmtpd/tests/test_handlers.py b/aiosmtpd/tests/test_handlers.py
index f40ec88..e433810 100644
--- a/aiosmtpd/tests/test_handlers.py
+++ b/aiosmtpd/tests/test_handlers.py
@@ -385,6 +385,25 @@ class TestCLI(unittest.TestCase):
         self.assertEqual(
             self.parser.message, 'Sink handler does not accept arguments')
 
+    def test_mailbox_cli_no_args(self):
+        self.assertRaises(SystemExit, Mailbox.from_cli, self.parser)
+        self.assertEqual(
+            self.parser.message,
+            'The directory for the maildir is required')
+
+    def test_mailbox_cli_too_many_args(self):
+        self.assertRaises(SystemExit, Mailbox.from_cli, self.parser,
+                          'foo', 'bar', 'baz')
+        self.assertEqual(
+            self.parser.message,
+            'Too many arguments for Mailbox handler')
+
+    def test_mailbox_cli(self):
+        with TemporaryDirectory() as tmpdir:
+            handler = Mailbox.from_cli(self.parser, tmpdir)
+            self.assertIsInstance(handler.mailbox, Maildir)
+            self.assertEqual(handler.mail_dir, tmpdir)
+
 
 class TestProxy(unittest.TestCase):
     def setUp(self):
diff --git a/aiosmtpd/tests/test_starttls.py b/aiosmtpd/tests/test_starttls.py
index 6fd6245..8fee782 100644
--- a/aiosmtpd/tests/test_starttls.py
+++ b/aiosmtpd/tests/test_starttls.py
@@ -61,7 +61,7 @@ class TLSController(Controller):
 
 class HandshakeFailingHandler:
     @staticmethod
-    def handle_tls_handshake(*args):
+    def handle_tls_handshake(session):
         return False