Sovite.DSN (sovite v0.6.0)

Copy Markdown View Source

Builds delivery status notifications (RFC 3464), the messages that tell a sender their mail was not delivered (or not yet), or was delivered when they asked to know (DSN, RFC 3461).

A notification is a multipart/report (RFC 6522) with three parts: a human-readable explanation, a message/delivery-status report for programs, and the headers of the original message (text/rfc822-headers), or for a failure with :content the whole message (message/rfc822).

DSN.build(%{
  kind: :failure,
  reporting_mta: "mx.example.com",
  from: "MAILER-DAEMON@mx.example.com",
  to: "alice@example.com",
  recipients: [
    %{recipient: "bob@example.net", status: "5.1.1", remote_mta: "mx.example.net",
      diagnostic: "550 5.1.1 User unknown"}
  ],
  headers: "From: alice@example.com\r\nSubject: hi\r\n\r\n"
})

The result is a complete message with CRLF line endings. Send it with the null reverse-path (MAIL FROM:<>), as RFC 5321 §6.1 requires, so a failing notification never causes another one.

Text from remote servers (diagnostics) is untrusted: control and non-ASCII characters are replaced with ?, and long values are cut, so it cannot inject header fields or MIME boundaries.

Internationalized notifications

A notification about internationalized mail is an RFC 6533 one (see global?/1): the report is message/global-delivery-status, the headers are message/global-headers, the explanation is UTF-8, and internationalized addresses are given with the utf-8 address type (Final-Recipient: utf-8; jürgen@example.com). Untrusted text keeps its UTF-8 characters then, but never control characters. Such a notification must be sent with SMTPUTF8.

Summary

Types

One recipient in the report.

The report.

Functions

Builds the notification message. Returns the message and its body type: :"8bitmime" if it contains 8-bit bytes (the original headers, or an internationalized notification), otherwise :"7bit".

Whether the notification for report is an internationalized one (RFC 6533): an address in it is internationalized, or the original message was sent with SMTPUTF8 and has UTF-8 header fields.

Types

recipient()

@type recipient() :: %{
  :recipient => String.t(),
  :status => String.t(),
  optional(:action) => :delivered | :relayed | :expanded,
  optional(:original_recipient) => {String.t(), String.t()} | nil,
  optional(:remote_mta) => String.t() | nil,
  optional(:diagnostic) => String.t() | nil,
  optional(:reason) => String.t() | nil,
  optional(:last_attempt) => DateTime.t() | nil
}

One recipient in the report.

  • :recipient - the address. Required.
  • :status - enhanced status code, such as "5.1.1". Required.
  • :remote_mta - host name of the server that gave :diagnostic.
  • :diagnostic - the remote server's SMTP reply.
  • :reason - a local explanation, used when there is no SMTP reply (for example "Host or domain name not found").
  • :last_attempt - time of the last delivery attempt.
  • :action - for :success reports, what happened: :delivered (to the mailbox), :relayed (to a server that does not send notifications), or :expanded (to the members of an alias). Defaults to :delivered.
  • :original_recipient - the ORCPT the client gave, as Sovite.SMTP.DSNParams.orcpt().

report()

@type report() :: %{
  :kind => :failure | :delay | :success,
  :reporting_mta => String.t(),
  :from => String.t(),
  :to => String.t(),
  :recipients => [recipient(), ...],
  optional(:headers) => binary() | nil,
  optional(:content) => binary() | nil,
  optional(:envid) => String.t() | nil,
  optional(:future_release) => String.t() | nil,
  optional(:queue_id) => String.t() | nil,
  optional(:smtputf8) => boolean(),
  optional(:arrival_date) => DateTime.t() | nil,
  optional(:will_retry_until) => DateTime.t() | nil,
  optional(:date) => DateTime.t(),
  optional(:message_id) => String.t(),
  optional(:boundary) => String.t()
}

The report.

  • :kind - :failure (the listed recipients will never get the message), :delay (still trying), or :success (delivered). Required.
  • :reporting_mta - this server's host name. Required.
  • :from - address in the From: field, usually MAILER-DAEMON@<host>. Required.
  • :to - the original sender. Required.
  • :recipients - at least one. Required.
  • :headers - the original message's header section.
  • :content - for :failure, the whole original message, returned instead of :headers (RET=FULL).
  • :envid - the ENVID the client gave.
  • :future_release - the original message's FUTURERELEASE request (RFC 4865 §5), as "for;3600" or "until;<date-time>".
  • :queue_id - the original message's queue ID.
  • :smtputf8 - the original message was sent with SMTPUTF8, so its header fields may be UTF-8 (RFC 6532).
  • :arrival_date - when the original message was received.
  • :will_retry_until - for :delay, when delivery will be given up.
  • :date, :message_id, :boundary - default to now, a new ID, and a random boundary.

Functions

build(report)

@spec build(report()) :: {binary(), :"7bit" | :"8bitmime"}

Builds the notification message. Returns the message and its body type: :"8bitmime" if it contains 8-bit bytes (the original headers, or an internationalized notification), otherwise :"7bit".

global?(report)

@spec global?(report()) :: boolean()

Whether the notification for report is an internationalized one (RFC 6533): an address in it is internationalized, or the original message was sent with SMTPUTF8 and has UTF-8 header fields.