Behaviour for the application side of an SMTP server: policy decisions and what happens to received messages.
Sovite.SMTP.Server.Session handles the protocol (syntax, command
order, limits, dot-stuffing) and calls the handler only with
well-formed input. Addresses have been checked with
Sovite.Validators.split_mailbox/1, except that a recipient may be
"Postmaster" without a domain (RFC 5321 §4.5.1, any case).
Most callbacks return one of:
{:ok, state}- accept, with the standard reply.{:reply, reply, state}- sendreplyinstead. A 2xx/3xx reply accepts, 4xx/5xx rejects.{:close, reply, state}- sendreplyand close the connection.
Summary
Callbacks
SASL mechanisms to offer in the EHLO reply, upper-case, for example
["PLAIN", "SCRAM-SHA-256"]. Called only when the session offers
AUTH, and on every EHLO and AUTH, so the list may depend on the
state. Without this callback, no mechanism is offered.
AUTH with an offered mechanism. initial_response is the decoded
initial response, "" when the client sent =, and nil when it sent
none.
The exchange ended without a result: the client cancelled with *,
sent an invalid response, or the session ended.
The client's (decoded) response to a challenge.
DATA, when the transaction has at least one recipient. Accept to get 354.
The message was abandoned by the session: :too_large, :bare_lf,
:bare_cr, :timeout, or :closed (the connection ended).
Decoded message content, in order. Returning a reply aborts the message:
the rest of the data is read and discarded, the reply is sent after the
final dot, and handle_data_end/2 is not called.
The final dot. Accept only once the message is safely stored.
EHLO, HELO, or (in an LMTP session) LHLO. The name is a valid
domain or address literal.
MAIL FROM. sender is "" for the null reverse-path.
RCPT TO, after the session checked the recipient limit.
RSET, or a new EHLO/HELO that resets the transaction.
The connection is now encrypted (after STARTTLS). The session has
been reset: forget the EHLO name and anything else learned before.
VRFY, only when enabled. Without this callback the reply is 252.
Called when the connection opens, before the greeting. Return
{:close, reply, state} to refuse the client (typically with 554).
The session ended.
Types
@type auth_result() :: {:ok, identity :: String.t(), state()} | {:challenge, binary(), state()} | {:error, Sovite.SMTP.Reply.t(), state()} | {:close, Sovite.SMTP.Reply.t(), state()}
A step of the SASL exchange:
{:ok, identity, state}- authenticated asidentity:235.{:challenge, data, state}- senddata(raw bytes; the session encodes it) with334and wait for the client's response.{:error, reply, state}- the exchange failed: sendreply. Use535 5.7.8for bad credentials (counted towards:max_auth_failures),454 4.7.0for a temporary failure.{:close, reply, state}- sendreplyand close the connection.
@type result() :: {:ok, state()} | {:reply, Sovite.SMTP.Reply.t(), state()} | {:close, Sovite.SMTP.Reply.t(), state()}
@type state() :: term()
Callbacks
SASL mechanisms to offer in the EHLO reply, upper-case, for example
["PLAIN", "SCRAM-SHA-256"]. Called only when the session offers
AUTH, and on every EHLO and AUTH, so the list may depend on the
state. Without this callback, no mechanism is offered.
@callback handle_auth( mechanism :: String.t(), initial_response :: binary() | nil, state() ) :: auth_result()
AUTH with an offered mechanism. initial_response is the decoded
initial response, "" when the client sent =, and nil when it sent
none.
The exchange ended without a result: the client cancelled with *,
sent an invalid response, or the session ended.
@callback handle_auth_response(response :: binary(), state()) :: auth_result()
The client's (decoded) response to a challenge.
@callback handle_data(Sovite.SMTP.Server.Session.transaction(), state()) :: result()
DATA, when the transaction has at least one recipient. Accept to get 354.
The message was abandoned by the session: :too_large, :bare_lf,
:bare_cr, :timeout, or :closed (the connection ended).
@callback handle_data_chunk(iodata(), state()) :: {:ok, state()} | {:reply, Sovite.SMTP.Reply.t(), state()}
Decoded message content, in order. Returning a reply aborts the message:
the rest of the data is read and discarded, the reply is sent after the
final dot, and handle_data_end/2 is not called.
@callback handle_data_end(Sovite.SMTP.Server.Session.transaction(), state()) :: result()
The final dot. Accept only once the message is safely stored.
EHLO, HELO, or (in an LMTP session) LHLO. The name is a valid
domain or address literal.
@callback handle_mail( sender :: String.t(), Sovite.SMTP.Server.Session.mail_params(), state() ) :: result()
MAIL FROM. sender is "" for the null reverse-path.
RCPT TO, after the session checked the recipient limit.
RSET, or a new EHLO/HELO that resets the transaction.
@callback handle_tls(Sovite.TLS.info(), state()) :: state()
The connection is now encrypted (after STARTTLS). The session has
been reset: forget the EHLO name and anything else learned before.
VRFY, only when enabled. Without this callback the reply is 252.
@callback init(Sovite.SMTP.Server.Session.connection(), opts :: term()) :: {:ok, state()} | {:close, Sovite.SMTP.Reply.t(), state()}
Called when the connection opens, before the greeting. Return
{:close, reply, state} to refuse the client (typically with 554).
The session ended.