Sovite.SMTP.Server.Handler behaviour (sovite v0.6.0)

Copy Markdown View Source

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/2, except that a recipient may be "Postmaster" without a domain (RFC 5321 §4.5.1, any case). They are only internationalized in transactions with SMTPUTF8, and their domains are in A-labels.

Most callbacks return one of:

  • {:ok, state} - accept, with the standard reply.
  • {:reply, reply, state} - send reply instead. A 2xx/3xx reply accepts, 4xx/5xx rejects.
  • {:close, reply, state} - send reply and close the connection.

Summary

Types

A step of the SASL exchange

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. With BDAT, called for the first chunk.

The message was abandoned by the session: :too_large, :bare_lf, :bare_cr, :timeout, :closed (the connection ended), or :reset (the transaction was reset between BDAT chunks; handle_rset/1 follows).

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, or BDAT ... LAST. Accept only once the message is safely stored.

ETRN (RFC 1985), only when enabled: start delivering the mail queued for node. Accept to get 250 Queuing started; reply 251 when no mail is waiting, 253 with the number of messages, 458 when it cannot be done now, and 459 when the node is not allowed.

Called just before the greeting with what the client sent before it: "" unless the greeting was delayed and the client did not wait. Return {:close, reply, state} to refuse the client instead.

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. params has the DSN parameters, when the session offers DSN.

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), or {:pause, milliseconds, state} to wait that long before the greeting (see "Greeting delay" in Sovite.SMTP.Server.Session).

The session ended.

Types

auth_result()

@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 as identity: 235.
  • {:challenge, data, state} - send data (raw bytes; the session encodes it) with 334 and wait for the client's response.
  • {:error, reply, state} - the exchange failed: send reply. Use 535 5.7.8 for bad credentials (counted towards :max_auth_failures), 454 4.7.0 for a temporary failure.
  • {:close, reply, state} - send reply and close the connection.

result()

@type result() ::
  {:ok, state()}
  | {:reply, Sovite.SMTP.Reply.t(), state()}
  | {:close, Sovite.SMTP.Reply.t(), state()}

state()

@type state() :: term()

Callbacks

auth_mechanisms(state)

(optional)
@callback auth_mechanisms(state()) :: [String.t()]

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.

handle_auth(mechanism, initial_response, state)

(optional)
@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.

handle_auth_abort(state)

(optional)
@callback handle_auth_abort(state()) :: state()

The exchange ended without a result: the client cancelled with *, sent an invalid response, or the session ended.

handle_auth_response(response, state)

(optional)
@callback handle_auth_response(response :: binary(), state()) :: auth_result()

The client's (decoded) response to a challenge.

handle_data(transaction, state)

@callback handle_data(Sovite.SMTP.Server.Session.transaction(), state()) :: result()

DATA, when the transaction has at least one recipient. Accept to get 354. With BDAT, called for the first chunk.

handle_data_abort(reason, state)

@callback handle_data_abort(reason :: atom(), state()) :: state()

The message was abandoned by the session: :too_large, :bare_lf, :bare_cr, :timeout, :closed (the connection ended), or :reset (the transaction was reset between BDAT chunks; handle_rset/1 follows).

handle_data_chunk(iodata, state)

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

handle_data_end(transaction, state)

@callback handle_data_end(Sovite.SMTP.Server.Session.transaction(), state()) :: result()

The final dot, or BDAT ... LAST. Accept only once the message is safely stored.

handle_etrn(etrn_node, state)

(optional)
@callback handle_etrn(Sovite.SMTP.Command.etrn_node(), state()) :: result()

ETRN (RFC 1985), only when enabled: start delivering the mail queued for node. Accept to get 250 Queuing started; reply 251 when no mail is waiting, 253 with the number of messages, 458 when it cannot be done now, and 459 when the node is not allowed.

handle_greet(early_input, state)

(optional)
@callback handle_greet(early_input :: binary(), state()) ::
  {:ok, state()} | {:close, Sovite.SMTP.Reply.t(), state()}

Called just before the greeting with what the client sent before it: "" unless the greeting was delayed and the client did not wait. Return {:close, reply, state} to refuse the client instead.

handle_helo(arg1, name, state)

@callback handle_helo(:ehlo | :helo | :lhlo, name :: String.t(), state()) :: result()

EHLO, HELO, or (in an LMTP session) LHLO. The name is a valid domain or address literal.

handle_mail(sender, mail_params, state)

@callback handle_mail(
  sender :: String.t(),
  Sovite.SMTP.Server.Session.mail_params(),
  state()
) :: result()

MAIL FROM. sender is "" for the null reverse-path.

handle_rcpt(recipient, rcpt_params, state)

@callback handle_rcpt(
  recipient :: String.t(),
  Sovite.SMTP.Server.Session.rcpt_params(),
  state()
) ::
  result()

RCPT TO, after the session checked the recipient limit. params has the DSN parameters, when the session offers DSN.

handle_rset(state)

(optional)
@callback handle_rset(state()) :: state()

RSET, or a new EHLO/HELO that resets the transaction.

handle_tls(info, state)

(optional)
@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.

handle_vrfy(argument, state)

(optional)
@callback handle_vrfy(argument :: String.t(), state()) :: result()

VRFY, only when enabled. Without this callback the reply is 252.

init(connection, opts)

@callback init(Sovite.SMTP.Server.Session.connection(), opts :: term()) ::
  {:ok, state()}
  | {:pause, non_neg_integer(), 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), or {:pause, milliseconds, state} to wait that long before the greeting (see "Greeting delay" in Sovite.SMTP.Server.Session).

Called again, after terminate/2, when a proxy names another client with XCLIENT; a pause is then skipped.

terminate(reason, state)

(optional)
@callback terminate(reason :: term(), state()) :: any()

The session ended.