Sovite.SMTP.Server.Session (sovite v0.2.0)

Copy Markdown View Source

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 and EHLO reply. Required.
  • :handler - {module, opts}. Required.
  • :max_message_size - bytes, advertised with SIZE and 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 - answer VRFY with the handler. Defaults to false.
  • :bare_line_endings - :reject (default) or :normalize, see Sovite.SMTP.DataDecoder. Under :reject, a bare LF or CR in a command or in the data closes the session with 521.
  • :starttls - offer STARTTLS. Defaults to false.
  • :require_tls - refuse MAIL, RCPT, DATA, VRFY, and AUTH with 530 5.7.0 until the connection is encrypted. Defaults to false.
  • :auth - offer AUTH. Defaults to false.
  • :auth_required - refuse MAIL with 530 5.7.0 until the client has authenticated. Defaults to false.
  • :plaintext_auth - offer AUTH on unencrypted connections too. Defaults to false: passwords are never sent in the clear.
  • :max_auth_failures - failed AUTH attempts before the session is closed. Defaults to 3.
  • :lmtp - speak LMTP instead of SMTP. Defaults to false.

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.

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.

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

connection()

@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.

mail_params()

@type mail_params() :: %{
  size: non_neg_integer() | nil,
  body: :"7bit" | :"8bitmime" | nil
}

MAIL FROM parameters. body is nil when not given.

result()

@type result() :: {:continue | :close | :starttls, iodata(), t()}

t()

@opaque t()

transaction()

@type transaction() :: %{
  sender: String.t(),
  params: mail_params(),
  recipients: [String.t()]
}

The current mail transaction. Recipients are in the order given.

Functions

handle_input(session, bytes)

@spec handle_input(t(), binary()) :: result()

Processes bytes received from the client.

handle_timeout(session)

@spec handle_timeout(t()) :: result()

The client sent nothing for timeout/1 milliseconds.

handle_tls(session, info)

@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.

identity(session)

@spec identity(t()) :: String.t() | nil

Returns the authenticated identity, or nil.

new(connection, opts)

@spec new(map(), keyword()) :: result()

Starts a session for connection and returns the greeting. A :session_id is generated if missing.

session_id(session)

@spec session_id(t()) :: String.t()

Returns the session ID.

terminate(session, reason)

@spec terminate(t(), term()) :: :ok

Ends the session, for example when the connection closed or the server shuts down. Calls the handler's terminate/2.

timeout(session)

@spec timeout(t()) :: timeout()

Milliseconds to wait for more input before calling handle_timeout/1.

tls(session)

@spec tls(t()) :: Sovite.TLS.info() | nil

Returns the TLS details if the connection is encrypted, else nil.