The procedure to using the SMTP client protocol state machine to communicate with an SMTP server is as follows:
- Create the state machine (
~SMTPClientProtocol
) - Connect to the SMTP server using your chosen I/O backend
Sending commands and receiving responses:
- Call the appropriate method on the state machine
- Retrieve the outgoing data with
~SMTPClientProtocol.get_outgoing_data
- Use your I/O backend to send that data to the server
- Use your I/O backend to receive the response data
- Feed the response data to the state machine using
~SMTPClientProtocol.feed_bytes
- If the return value is an
~SMTPResponse
(and notNone
), process the response as appropriate. You can use~SMTPResponse.is_error
as a convenience to check if the response code means there was an error.
Establishing a TLS session after connection (optional):
- Check if the feature is supported by the server (
STARTTLS
is in~SMTPClientProtocol.extensions
) - Send the
STARTTLS
command using~SMTPClientProtocol.start_tls
- Use your I/O backend to do the TLS handshake in client mode (
~ssl.SSLContext.wrap_socket
or whatever you prefer) - Proceed with the session as usual
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
~SMTPAuthenticator.mechanism
property - The
~SMTPAuthenticator.authenticate
method
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.