Developing new I/O implementations

The procedure to using the SMTP client protocol state machine to communicate with an SMTP server is as follows:

  1. Create the state machine (SMTPClientProtocol)

  2. Connect to the SMTP server using your chosen I/O backend

Sending commands and receiving responses:

  1. Call the appropriate method on the state machine

  2. Retrieve the outgoing data with get_outgoing_data()

  3. Use your I/O backend to send that data to the server

  4. Use your I/O backend to receive the response data

  5. Feed the response data to the state machine using feed_bytes()

  6. If the return value is an SMTPResponse (and not None), process the response as appropriate. You can use is_error() as a convenience to check if the response code means there was an error.

Establishing a TLS session after connection (optional):

  1. Check if the feature is supported by the server (STARTTLS is in extensions)

  2. Send the STARTTLS command using start_tls()

  3. Use your I/O backend to do the TLS handshake in client mode (wrap_socket() or whatever you prefer)

  4. Proceed with the session as usual

Developing new authenticators

To add support for a new authentication mechanism, you can create a new class that inherits from either SMTPAuthenticator or one of its subclasses. This subclass needs to implement:

The mechanism property should return the name of the authentication mechanism (in upper case letters). It is used to send the initial AUTH command. If mechanism returns FOOBAR, the client would send the command AUTH FOOBAR.

The authenticate method should return an asynchronous generator that yields strings. If the generator yields a nonempty string on the first call, it is added to the AUTH command. For example, given the following code, the client would authenticate with the command AUTH FOOBAR mysecret:

from smtpproto.auth import SMTPAuthenticator

class MyAuthenticator(SMTPAuthenticator):
    @property
    def mechanism(self) -> str:
        return 'FOOBAR'

    async def authenticate(self) -> AsyncGenerator[str, str]:
        yield 'mysecret'

For mechanisms such as LOGIN that involve more rounds of information exchange, the generator typically yields an empty string first. It will then be sent back the server response text as the yield result. The authenticator will then yield its own response, and so forth. See the source code of the LoginAuthenticator class for an example.