The SMTP server protocol as a state machine, without I/O.
Feed it the bytes read from the client; it returns the bytes to send
back and whether to close the connection. Policy and storage are left to
a Sovite.SMTP.Server.Handler. Sovite.SMTP.Server.Connection runs a
session on a socket.
{:continue, greeting, session} = Session.new(connection, hostname: "mx.example.com", handler: {MyHandler, []})
{:continue, replies, session} = Session.handle_input(session, "EHLO client.example\r\n")Pipelined commands (RFC 2920) need no special handling: all complete lines in the input are processed, and their replies returned together.
STARTTLS
With starttls: true, STARTTLS (RFC 3207) is offered until the
connection is encrypted. On STARTTLS the session replies 220 and
returns {:starttls, replies, session}: send the replies, run the TLS
handshake, then call handle_tls/2. Input received after the
STARTTLS command and before the handshake is discarded, so a
man-in-the-middle cannot inject commands into the encrypted session.
After the handshake the session starts over, as the RFC requires: the
client must send EHLO again.
LMTP
With lmtp: true the session speaks LMTP (RFC 2033): the client
greets with LHLO (EHLO and HELO are refused), and the end of the
data gets one reply per accepted recipient, all with the handler's
reply. The handler sees LHLO as handle_helo(:lhlo, name, state).
Without it, LHLO is an unknown command.
AUTH
With auth: true, AUTH (RFC 4954) is offered with the mechanisms the
handler's auth_mechanisms/1 returns, but only over TLS unless
plaintext_auth: true. The handler runs the SASL exchange, see
Sovite.SMTP.Server.Handler. Lines of up to 12288 bytes are accepted
while AUTH is offered, for large initial responses and tokens.
Options
:hostname- name in the greeting andEHLOreply. Required.:handler-{module, opts}. Required.:max_message_size- bytes, advertised withSIZEand enforced while streaming. Defaults to 10 MiB.:max_recipients- per transaction. Defaults to 100.:max_errors- error replies before the session is closed. Defaults to 10.:max_line_length- command line length, including CRLF. Defaults to 2048.:command_timeout/:data_timeout- milliseconds to wait for input. Default to 5 minutes (RFC 5321 §4.5.3.2.7).:vrfy- answerVRFYwith the handler. Defaults tofalse.:bare_line_endings-:reject(default) or:normalize, seeSovite.SMTP.DataDecoder. Under:reject, a bare LF or CR in a command or in the data closes the session with521.:starttls- offerSTARTTLS. Defaults tofalse.:require_tls- refuseMAIL,RCPT,DATA,VRFY, andAUTHwith530 5.7.0until the connection is encrypted. Defaults tofalse.:auth- offerAUTH. Defaults tofalse.:auth_required- refuseMAILwith530 5.7.0until the client has authenticated. Defaults tofalse.:plaintext_auth- offerAUTHon unencrypted connections too. Defaults tofalse: passwords are never sent in the clear.:max_auth_failures- failedAUTHattempts before the session is closed. Defaults to 3.:lmtp- speak LMTP instead of SMTP. Defaults tofalse.
The connection map may carry :tls (a Sovite.TLS.info()) when it is
encrypted from the start (implicit TLS, RFC 8314).
Telemetry
[:sovite, :smtp, :server, :command, :stop] for every reply, with
%{duration} and %{session_id, remote_ip, command, argument, reply_code, reply}. command is the verb ("MAIL"), "UNKNOWN", or
"END-OF-MESSAGE" for the final dot. argument is only set for EHLO,
HELO, LHLO, MAIL, RCPT, and VRFY, and is network input. For AUTH it
is the mechanism; SASL responses are never included.
Summary
Types
Who is connected. Passed to the handler's init/2.
MAIL FROM parameters. body is nil when not given.
The current mail transaction. Recipients are in the order given.
Functions
Processes bytes received from the client.
The client sent nothing for timeout/1 milliseconds.
The TLS handshake after STARTTLS succeeded. Resets the session to
its initial state, as RFC 3207 §4.2 requires, and tells the handler.
Returns the authenticated identity, or nil.
Starts a session for connection and returns the greeting. A
:session_id is generated if missing.
Returns the session ID.
Ends the session, for example when the connection closed or the server
shuts down. Calls the handler's terminate/2.
Milliseconds to wait for more input before calling handle_timeout/1.
Returns the TLS details if the connection is encrypted, else nil.
Types
@type connection() :: %{ :session_id => String.t(), :remote_ip => :inet.ip_address(), optional(:remote_port) => :inet.port_number(), optional(:local_ip) => :inet.ip_address(), optional(:local_port) => :inet.port_number(), optional(:listener) => String.t(), optional(:tls) => Sovite.TLS.info() | nil }
Who is connected. Passed to the handler's init/2.
@type mail_params() :: %{ size: non_neg_integer() | nil, body: :"7bit" | :"8bitmime" | nil }
MAIL FROM parameters. body is nil when not given.
@opaque t()
@type transaction() :: %{ sender: String.t(), params: mail_params(), recipients: [String.t()] }
The current mail transaction. Recipients are in the order given.
Functions
Processes bytes received from the client.
The client sent nothing for timeout/1 milliseconds.
@spec handle_tls(t(), Sovite.TLS.info()) :: result()
The TLS handshake after STARTTLS succeeded. Resets the session to
its initial state, as RFC 3207 §4.2 requires, and tells the handler.
Returns the authenticated identity, or nil.
Starts a session for connection and returns the greeting. A
:session_id is generated if missing.
Returns the session ID.
Ends the session, for example when the connection closed or the server
shuts down. Calls the handler's terminate/2.
Milliseconds to wait for more input before calling handle_timeout/1.
@spec tls(t()) :: Sovite.TLS.info() | nil
Returns the TLS details if the connection is encrypted, else nil.