The Postfix SMTP access policy delegation protocol (https://www.postfix.org/SMTPD_POLICY_README.html), for asking policy servers such as policyd-spf and postgrey what to do with an SMTP client, and for writing such servers in Elixir.
A request is a block of name=value lines, each ended by a newline,
followed by an empty line. The reply is a block of the same form with
an action attribute, whose value is anything allowed in a Postfix
access(5) table, such as DUNNO or REJECT go away. One connection
can carry any number of requests, one after the other.
request=smtpd_access_policy
protocol_state=RCPT
client_address=192.0.2.7
sender=alice@example.com
recipient=bob@example.net
action=DEFER_IF_PERMIT Greylisted, try again laterThis module encodes and decodes the blocks. Sovite.Policy.Client
asks a policy server, Sovite.Policy.Server runs one, and
Sovite.Policy.Action turns actions into terms and back.
Attributes
Requests take a map or a list of attributes: names are strings or atoms
(so a keyword list works), values are strings, integers, IP address
tuples, or nil for an empty value. postfix_attributes/0 lists the
names Postfix sends; any other valid name can be sent too.
Summary
Types
Request attributes, as a map or a list such as a keyword list.
Decoded attributes.
An attribute name: one of postfix_attribute(), or any other valid name.
The attributes Postfix sends to a policy server, as of Postfix 3.8
An attribute value. nil is sent as an empty value.
Functions
Decodes the first block in data: the attributes up to the first empty
line, and what follows it. A line may end with \r\n. When a name
comes more than once, the last value wins.
Encodes an action, see Sovite.Policy.Action.encode/1.
Encodes a reply with action: a string as is (newlines replaced by
spaces), or a term, see Sovite.Policy.Action.encode/1.
Encodes a request: the attributes, then an empty line.
Parses an action, see Sovite.Policy.Action.parse/1.
The names of the attributes Postfix sends, see postfix_attribute/0.
Types
Request attributes, as a map or a list such as a keyword list.
Decoded attributes.
@type name() :: postfix_attribute() | atom() | String.t()
An attribute name: one of postfix_attribute(), or any other valid name.
@type postfix_attribute() ::
:request
| :protocol_state
| :protocol_name
| :helo_name
| :queue_id
| :sender
| :recipient
| :recipient_count
| :client_address
| :client_name
| :reverse_client_name
| :instance
| :sasl_method
| :sasl_username
| :sasl_sender
| :size
| :ccert_subject
| :ccert_issuer
| :ccert_fingerprint
| :ccert_pubkey_fingerprint
| :encryption_protocol
| :encryption_cipher
| :encryption_keysize
| :etrn_domain
| :stress
| :client_port
| :policy_context
| :server_address
| :server_port
| :compatibility_level
| :mail_version
The attributes Postfix sends to a policy server, as of Postfix 3.8:
request- alwayssmtpd_access_policy.protocol_state- the SMTP command:CONNECT,EHLO,HELO,MAIL,RCPT,DATA,END-OF-MESSAGE,VRFY, orETRN.protocol_name-SMTP,ESMTP, orLMTP.helo_name,queue_id,sender,recipient,recipient_count.client_address,client_name(verified, orunknown),reverse_client_name(unverified),client_port.server_address,server_port- where the client connected to.instance- the same for every request of one message.sasl_method,sasl_username,sasl_sender.size- fromMAIL FROM ... SIZE=, or the actual size atEND-OF-MESSAGE.ccert_subject,ccert_issuer,ccert_fingerprint,ccert_pubkey_fingerprint- the client certificate.encryption_protocol,encryption_cipher,encryption_keysize.etrn_domain,stress(yesunder stress),policy_context,compatibility_level,mail_version.
@type value() :: String.t() | integer() | :inet.ip_address() | nil
An attribute value. nil is sent as an empty value.
Functions
Decodes the first block in data: the attributes up to the first empty
line, and what follows it. A line may end with \r\n. When a name
comes more than once, the last value wins.
Returns {:error, :incomplete} when data has no empty line yet, and
{:error, :malformed} for a line that is not name=value with a
valid name.
iex> Sovite.Policy.decode("action=DUNNO\nfoo=a=b\n\nrest")
{:ok, %{"action" => "DUNNO", "foo" => "a=b"}, "rest"}
@spec encode_action(Sovite.Policy.Action.t()) :: String.t()
Encodes an action, see Sovite.Policy.Action.encode/1.
@spec encode_reply(String.t() | Sovite.Policy.Action.t()) :: iodata()
Encodes a reply with action: a string as is (newlines replaced by
spaces), or a term, see Sovite.Policy.Action.encode/1.
iex> Sovite.Policy.encode_reply({:reject, "no"}) |> IO.iodata_to_binary()
"action=REJECT no\n\n"
@spec encode_request(attributes()) :: iodata()
Encodes a request: the attributes, then an empty line.
request=smtpd_access_policy is added unless attrs has a request.
Carriage returns and newlines in values are replaced by spaces.
Attributes whose name is not valid (letters, digits, _, -, and .,
not starting with - or .) are left out. When a name is given more
than once, the last value is sent, in the place of the last one.
iex> Sovite.Policy.encode_request(sender: "a@example.com", size: 120) |> IO.iodata_to_binary()
"request=smtpd_access_policy\nsender=a@example.com\nsize=120\n\n"
@spec parse_action(String.t()) :: {:ok, Sovite.Policy.Action.t()} | {:error, :invalid_action}
Parses an action, see Sovite.Policy.Action.parse/1.
@spec postfix_attributes() :: [String.t()]
The names of the attributes Postfix sends, see postfix_attribute/0.
iex> "client_address" in Sovite.Policy.postfix_attributes()
true