Sovite.SRS (sovite v0.6.0)

Copy Markdown View Source

The Sender Rewriting Scheme, so forwarded mail still passes SPF: the envelope sender is rewritten into an address at the forwarder's own domain, and bounces to that address are turned back into the original sender.

{:ok, srs} = Sovite.SRS.forward("alice@example.com", "fwd.example.net", secrets: ["s3cret"])
#=> "SRS0=HHHH=TT=example.com=alice@fwd.example.net"
Sovite.SRS.reverse(srs, secrets: ["s3cret"])
#=> {:ok, "alice@example.com"}

This is the Shevek scheme as libsrs2 and postsrsd implement it, so addresses can be reversed by either side:

  • SRS0=HHHH=TT=domain=local@srs-domain for a plain address. TT is the day number (days since the Unix epoch, mod 1024) in two base32 characters. HHHH is the first four characters of the base64 HMAC-SHA1, keyed with the secret, of TT <> domain <> local lower-cased (ASCII only).
  • An address that is already SRS0 at hop1 becomes SRS1=HHHH=hop1==HHHH=TT=domain=local@srs-domain, so the bounce goes back to hop1 rather than through every forwarder. The hash is over hop1 <> "=HHHH=TT=domain=local", again lower-cased.
  • An SRS1 address keeps its first hop; only the hash is replaced.

Bounces may come back with the local part case-mangled, so hashes are compared without regard to case, like libsrs2 does.

Summary

Functions

Rewrites address into an SRS address at srs_domain.

Turns an SRS address back into the address it was made from: an SRS0 address into the original sender, an SRS1 address into the SRS0 address at the first forwarder.

Whether address (or a bare local part) is an SRS address.

Functions

forward(address, srs_domain, opts)

@spec forward(String.t(), String.t(), keyword()) ::
  {:ok, String.t()} | {:error, :invalid_address}

Rewrites address into an SRS address at srs_domain.

An address already at srs_domain is returned unchanged.

Options

  • :secrets - required, a non-empty list of secrets. The first one signs; the others are only for reverse/2, so a secret can be rotated without breaking bounces in flight.
  • :now - the DateTime for the timestamp. Defaults to now.

reverse(address, opts)

@spec reverse(String.t(), keyword()) ::
  {:ok, String.t()} | {:error, :not_srs | :malformed | :bad_hash | :expired}

Turns an SRS address back into the address it was made from: an SRS0 address into the original sender, an SRS1 address into the SRS0 address at the first forwarder.

The separator after SRS0/SRS1 may be =, + or -.

Options

  • :secrets - required. Every secret is tried.
  • :max_age - how many days an SRS0 address stays valid. Defaults to 21.
  • :now - the DateTime to check the age against. Defaults to now.

srs?(address)

@spec srs?(String.t()) :: boolean()

Whether address (or a bare local part) is an SRS address.

iex> Sovite.SRS.srs?("srs0+HHHH=TT=example.com=alice@fwd.example.net")
true
iex> Sovite.SRS.srs?("alice@example.com")
false