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.
Greeting delay
A handler's init/2 may return {:pause, milliseconds, state} to hold
the greeting back, as postscreen does: clients must wait for it (RFC
5321 §3.1), so one that talks first is most likely a spam bot. The
session then returns no output, and timeout/1 is the time left. When
it runs out, or as soon as the client sends anything, the handler's
handle_greet/2 gets the early input ("" if none) and decides
whether to greet the client. Early input of a client let in is then
processed as commands.
Tarpit
With :tarpit_delay, every error reply from the :tarpit_afterth on
adds that many milliseconds to the session's delay: the caller waits
that long before sending the output (see take_delay/1). This slows
down dictionary attacks and other clients that keep getting errors.
Pipelining
With forbid_unauth_pipelining: true, a client that sends more input
after a command that must end a group (RFC 2920 §3.1: EHLO, DATA,
NOOP, ...), or after any command before PIPELINING was offered,
gets 554 5.5.0 and is disconnected: it did not wait for the reply.
XCLIENT and XFORWARD
Clients in :xclient_networks may use Postfix's XCLIENT command, and
clients in :xforward_networks its XFORWARD command (see
https://www.postfix.org/XCLIENT_README.html and XFORWARD_README.html).
Both are offered in the EHLO reply only to those clients.
XCLIENT is for proxies such as nginx: it replaces what the session
knows about the client (ADDR, PORT, NAME, REVERSE_NAME,
DESTADDR, DESTPORT, HELO, PROTO, LOGIN), as if the client
had connected itself. The handler's state is ended with terminate/2
and started again with init/2 and the new connection map, which has
:client_name, :reverse_name, and :login when given; HELO is
passed to handle_helo/3, and LOGIN counts as authenticated. The
reply is a new greeting. The client that may use XCLIENT is decided
at connect, so it may send it again.
XFORWARD is for content filters that send mail back: the attributes
(NAME, ADDR, PORT, PROTO, HELO, IDENT, SOURCE) describe
the original client of the next transaction only, and reach the
handler in the transaction's :xforward map. They change nothing
else.
SMTPUTF8
With smtputf8: true, SMTPUTF8 (RFC 6531) is offered in the EHLO
and LHLO replies. A MAIL FROM with the SMTPUTF8 parameter sets
smtputf8: true in the mail parameters, and lets the sender and the
recipients of that transaction be internationalized addresses, with
UTF-8 local parts and U-label domains (Sovite.Validators). Without
it, such an address gets 553 5.6.7. Domains in U-labels reach the
handler, and the transaction, converted to A-labels
(Sovite.Validators.ascii_domain/1), so every domain has one form.
The message may then have UTF-8 header fields (RFC 6532).
CHUNKING and BINARYMIME
With chunking: true, CHUNKING (RFC 3030) is offered: the client
may send the message in BDAT chunks instead of with DATA. Each
chunk is passed to the handler as it arrives, as DATA content is,
but there is no dot-stuffing; bare line endings get the
:bare_line_endings policy all the same. The first chunk calls
handle_data/2, every chunk but the last is answered with 250, and
BDAT ... LAST ends the message like the final dot. A chunk is always
read to its end, also when it is refused, so its content can never be
taken for commands.
With binarymime: true as well, BINARYMIME is offered: MAIL FROM
with BODY=BINARYMIME sets body: :binarymime, and the message must
be sent with BDAT. Its content is binary: it is passed on unchecked.
DSN
With dsn: true, DSN (RFC 3461) is offered. MAIL FROM may then
have RET and ENVID (in the mail parameters), and RCPT TO may
have NOTIFY and ORCPT (in the recipient parameters passed to
handle_rcpt/3), see Sovite.SMTP.DSNParams. Acting on them is up to
the handler.
ETRN
With etrn: true and a handler with handle_etrn/2, ETRN (RFC
- is offered: the client asks for the mail queued for its domain to be sent now. Not allowed in a mail transaction.
MT-PRIORITY, FUTURERELEASE, DELIVERBY, and RRVS
These are offered on request, and their parameters (see
Sovite.SMTP.Params) reach the handler checked but not acted on:
mt_priority: true-MT-PRIORITY(RFC 6710):MAIL FROMmay haveMT-PRIORITY=-9..9(priorityin the mail parameters). The handler must not let untrusted clients raise it.future_release: seconds-FUTURERELEASE(RFC 4865), withHOLDFORandHOLDUNTILup to that many seconds ahead (hold).deliverby: true-DELIVERBY(RFC 2852):BY=seconds;R|N[T](by). With a hold as well, the message must be due after its release.rrvs: true-RRVS(RFC 7293):RCPT TOmay haveRRVS=time[;R|;C](rrvsin the recipient parameters).
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.:requiretls- offerREQUIRETLS(RFC 8689) on encrypted connections. AMAIL FROMwith theREQUIRETLSparameter setsrequiretls: truein the mail parameters: the handler must then only relay the message over TLS that is verified with DANE or MTA-STS. Defaults tofalse.:smtputf8- offerSMTPUTF8, see "SMTPUTF8". Defaults tofalse.:chunking/:binarymime- offerCHUNKINGandBINARYMIME, see "CHUNKING and BINARYMIME". Default tofalse.:dsn- offerDSN, see "DSN". Defaults tofalse.:etrn- offerETRN, see "ETRN". Defaults tofalse.:mt_priority,:future_release,:deliverby,:rrvs- see "MT-PRIORITY, FUTURERELEASE, DELIVERBY, and RRVS". Default tofalse(nilfor:future_release).:tarpit_after- errors before the tarpit starts. Defaults to 3.:tarpit_delay- milliseconds per error reply after that. Defaults to 0: no tarpit.:forbid_unauth_pipelining- see "Pipelining". Defaults tofalse.:xclient_networks/:xforward_networks-Sovite.Netnetworks whose clients may useXCLIENT/XFORWARD. Default to[].
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 or BDAT ... LAST. argument is
only set for EHLO, HELO, LHLO, MAIL, RCPT, VRFY, and
ETRN, 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. Those not given are nil (false for
requiretls and smtputf8).
RCPT TO parameters (DSN and RRVS), nil when not given.
The current mail transaction. Recipients are in the order given.
What XFORWARD said about the original client, see "XCLIENT and
XFORWARD". :addr is an IP address, :port an integer, :source
"LOCAL" or "REMOTE"; the others are strings.
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.
Whether the session is between transactions: no mail transaction,
message data, AUTH exchange, or TLS handshake in progress. A server
that shuts down gracefully closes idle sessions first, and the others
once they become idle.
Starts a session for connection and returns the greeting. A
:session_id is generated if missing.
Returns the session ID.
Returns the milliseconds to wait before sending the output of the last call (see "Tarpit" above), and resets it.
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, optional(:client_name) => String.t() | nil, optional(:reverse_name) => String.t() | nil, optional(:login) => String.t() | nil }
Who is connected. Passed to the handler's init/2.
@type mail_params() :: %{ size: non_neg_integer() | nil, body: :"7bit" | :"8bitmime" | :binarymime | nil, requiretls: boolean(), smtputf8: boolean(), ret: Sovite.SMTP.DSNParams.ret() | nil, envid: String.t() | nil, priority: -9..9 | nil, hold: Sovite.SMTP.Params.hold() | nil, by: Sovite.SMTP.Params.by() | nil }
MAIL FROM parameters. Those not given are nil (false for
requiretls and smtputf8).
@type rcpt_params() :: %{ notify: Sovite.SMTP.DSNParams.notify() | nil, orcpt: Sovite.SMTP.DSNParams.orcpt() | nil, rrvs: Sovite.SMTP.Params.rrvs() | nil }
RCPT TO parameters (DSN and RRVS), nil when not given.
@opaque t()
@type transaction() :: %{ sender: String.t(), params: mail_params(), recipients: [String.t()], xforward: xforward() }
The current mail transaction. Recipients are in the order given.
@type xforward() :: %{ optional(:name) => String.t() | nil, optional(:addr) => :inet.ip_address() | nil, optional(:port) => :inet.port_number() | nil, optional(:proto) => String.t() | nil, optional(:helo) => String.t() | nil, optional(:ident) => String.t() | nil, optional(:source) => String.t() | nil }
What XFORWARD said about the original client, see "XCLIENT and
XFORWARD". :addr is an IP address, :port an integer, :source
"LOCAL" or "REMOTE"; the others are strings.
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.
Whether the session is between transactions: no mail transaction,
message data, AUTH exchange, or TLS handshake in progress. A server
that shuts down gracefully closes idle sessions first, and the others
once they become idle.
Starts a session for connection and returns the greeting. A
:session_id is generated if missing.
Returns the session ID.
@spec take_delay(t()) :: {non_neg_integer(), t()}
Returns the milliseconds to wait before sending the output of the last call (see "Tarpit" above), and resets it.
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.