Sovite.SPF.Macro (sovite v0.6.0)

Copy Markdown View Source

SPF macro strings (RFC 7208 §7).

A macro string is parsed once with parse/2 and expanded with expand/2 or expand_domain/2 against a context/0:

iex> {:ok, macro} = Sovite.SPF.Macro.parse("%{ir}.%{v}._spf.%{d2}")
iex> Sovite.SPF.Macro.expand(macro, %{
...>   sender: "strong-bad@email.example.com",
...>   domain: "email.example.com",
...>   ip: {192, 0, 2, 3}
...> })
"3.2.0.192.in-addr._spf.example.com"

Expansion is pure. The p macro needs a validated host name for the client, which takes DNS lookups, so the caller looks it up when uses_ptr?/1 says so and passes it as :ptr.

Summary

Types

The values macros expand to.

Where a macro string appears, which decides what it may contain.

A letter, digits (labels to keep), reverse, delimiters, URL-escape.

t()

A parsed macro string.

Functions

Formats an address for the i macro: dotted decimal for IPv4, and dot-separated nibbles for IPv6 (RFC 7208 §7.3).

Expands macro with the values in context.

Expands macro for use as a domain name: if the result is longer than 253 characters, labels are removed from the left until it fits (RFC 7208 §7.3).

Parses a macro string of the given kind.

Returns true if macro uses the p macro, which needs a PTR lookup.

Types

context()

@type context() :: %{
  optional(:sender) => String.t(),
  optional(:domain) => String.t(),
  optional(:ip) => :inet.ip_address(),
  optional(:helo) => String.t() | nil,
  optional(:ptr) => String.t() | nil,
  optional(:receiver) => String.t() | nil,
  optional(:now) => integer() | nil
}

The values macros expand to.

  • :sender - s, and l and o from its local part and domain.
  • :domain - d, the domain being checked.
  • :ip - i, v, and c, the client address.
  • :helo - h. Defaults to "unknown".
  • :ptr - p, the client's validated host name. Defaults to "unknown".
  • :receiver - r. Defaults to "unknown".
  • :now - t, in Unix seconds. Defaults to the current time.

kind()

@type kind() :: :domain_spec | :explanation | :macro_string

Where a macro string appears, which decides what it may contain.

  • :domain_spec - a mechanism or modifier target. Must end like a domain name, and may not use c, r, or t.
  • :explanation - the text of an exp= record. May contain spaces.
  • :macro_string - the value of an unknown modifier.

macro()

@type macro() ::
  {:macro, atom(), pos_integer() | nil, boolean(), [String.t()], boolean()}

A letter, digits (labels to keep), reverse, delimiters, URL-escape.

t()

@type t() :: [String.t() | {:escape, String.t()} | macro()]

A parsed macro string.

Functions

dotted(ip)

@spec dotted(:inet.ip_address()) :: String.t()

Formats an address for the i macro: dotted decimal for IPv4, and dot-separated nibbles for IPv6 (RFC 7208 §7.3).

iex> Sovite.SPF.Macro.dotted({0x2001, 0xDB8, 0, 0, 0, 0, 0, 0xCB01})
"2.0.0.1.0.d.b.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.c.b.0.1"

expand(macro, context)

@spec expand(t(), context()) :: String.t()

Expands macro with the values in context.

iex> {:ok, macro} = Sovite.SPF.Macro.parse("%{l1r-}.%{O}", :macro_string)
iex> Sovite.SPF.Macro.expand(macro, %{sender: "strong-bad@email.example.com"})
"strong.email.example.com"

expand_domain(macro, context)

@spec expand_domain(t(), context()) :: String.t()

Expands macro for use as a domain name: if the result is longer than 253 characters, labels are removed from the left until it fits (RFC 7208 §7.3).

parse(string, kind \\ :domain_spec)

@spec parse(String.t(), kind()) :: {:ok, t()} | {:error, String.t()}

Parses a macro string of the given kind.

iex> Sovite.SPF.Macro.parse("%{d}")
{:ok, [{:macro, :d, nil, false, [], false}]}
iex> Sovite.SPF.Macro.parse("%{c}.example.com")
{:error, "macro letter not allowed here"}
iex> Sovite.SPF.Macro.parse("%{c}", :explanation)
{:ok, [{:macro, :c, nil, false, [], false}]}

uses_ptr?(macro)

@spec uses_ptr?(t()) :: boolean()

Returns true if macro uses the p macro, which needs a PTR lookup.