package uspf

  1. Overview
  2. Docs

(Un)Sender Policy Framework.

uSPF is a framework to check the identity of the email's sender. When an email passes through an SMTP server, some informations are available such as the source of the email, the IP address (because the sender must initiate a TCP/IP connexion).

From these informations and via uSPF (and DNS records), we are able to authorize the given email or not. Indeed, the email submission process requires an identity with the SMTP MAILFROM command. At this stage, we are able to check if the domain name given by MAILFROM and the current IP address of the sender match!

The domain-name used by MAILFROM should have some DNS records which describe which IP address is allowed to send an email via the MAILFROM's identity. uSPF will check that and it will try to find a match. In any results - if uSPF fails or not - the SMTP server will put the result of such check into the given email.

Finally, it permits to check, at any step of the submission, the identity of the sender. However, it does not ensure a high level of securities when uSPF should be use with DKIM/DMARC to ensure some others aspects such as the integrity of the given email.

How to use uSPF.

uSPF requires some meta informations such as the MAILFROM identity and the IP address of the sender. The user can create a ctx and fill it with these information:

let ctx =
  Spf.empty |> Spf.with_sender (`MAILFROM path) |> Spf.with_ip ipaddr

From this ctx, then the user is able to check the identity of the sender via a DNS implementation. The user must get the SPF DNS record, analyze it and use it then with the ctx:

let res =
  Spf.get ctx sched dns (module DNS)
  >>= Spf.check ctx sched dns (module DNS)

From the result, the user is able to generate an header field. It optional to give your identity (your domain) to be exhaustive about meta information on the field value:

let field_name, value = Spf.to_field ~ctx ?receiver res 

The value is well-formed for the incoming email. You just need to prepend the field before the email.

Reproductibility.

The API provides a possibility to extract SPF results from an incoming email and regenerate the ctx from them. By this way, locally, you can reproduce the process above. By this way, you are able to reproduce the written result and check if it still is valid.

Indeed, due to the DNS record requirement to check the identity of the sender, it possible that meta informations from the given email are obsoletes (for any reasons).

As a server.

uSPF allows the end-user to craft its own record and publish it then into its primary/secondary DNS server. Multiple values exists such as:

They permits to describe via OCaml the SPF record. It can be serialized to a simple string then and the user can save it into its own primary/secondary DNS server.

module Sigs : sig ... end
type ctx

The type for contexts. It's a heterogeneous map of values to help uSPF to validate the sender. It requires the MAILFROM parameter given by the SMTP protocol (which can be filled via with_sender) and/or the IP address of the sender (which can be filled via with_ip).

val empty : ctx

empty is an empty context.

val with_sender : [ `HELO of [ `raw ] Domain_name.t | `MAILFROM of Colombe.Path.t ] -> ctx -> ctx

with_sender v ctx adds into the given ctx the sender of the incoming email (its simple domain name or the complete email address).

val with_ip : Ipaddr.t -> ctx -> ctx

with_ip v ctx adds into the given ctx the IP address of the sender.

val domain : ctx -> [ `raw ] Domain_name.t option

domain ctx returns the domain-name of the sender if it exists.

module Macro : sig ... end
module Term : sig ... end
type record

The type of SPF records.

type mechanism

The type of mechanisms.

A mechanism permits to design and identify a set of IP addresses as being permitted or not permitted to use the domain for sending mail.

val a : ?cidr_v4:int -> ?cidr_v6:int -> [ `raw ] Domain_name.t -> mechanism

This mechanism matches if the sender's IP address is one of the domain-name's IP addresses. For clarity, this means the a mechanism also matches AAAA records.

An address lookup is done on the domain-name using the type of lookup (A or AAAA) appropriate for the connection type. The IP is compared to the returned address(es). If any address matches, the mechanism matches.

A Classless Inter-Domain Routing can be applied to returned address(es) (IPv4 or IPv6) to compare with the sender's IP address. For instance, a=10.0.0.42/32 matches only 10.0.0.42 as the sender's IP address but a=10.0.0.42/24 matches any 10.0.0.* addresses. By default, cidr_v4 = 32 and cidr_v6 = 128.

val all : mechanism

The all mechanism is a test that always matches. It is used as the rightmost mechanism (the last mechanism) in a record to provide an explicit default. For example v=spf1 a mx -all.

Mechanisms after all will never be tested.

val exists : [ `raw ] Domain_name.t -> mechanism

This mechanism is used to construct an arbitrary domain name that is used for a DNS A record query. It allows for complicated schemes involving arbitrary parts on the mail envelope to determine what is permitted.

val inc : [ `raw ] Domain_name.t -> mechanism

The include mechanism triggers a recursive evaluation of check:

  1. The macro is expanded according to the given ctx
  2. We re-execute check with the produced domain-name (IP and sender arguments remain the same)
  3. The recursive evaluation returns match, not-match or an error.
  4. If it returns match, then the appropriate result for the include mechanism is used (see qualifier)
  5. It it returns not-match or an error, the check process tests the next mechanism.

Note: for instance, if the domain-name has -all, include does not strictly terminates the processus. It fails and let check to process the next mechanism.

val mx : ?cidr_v4:int -> ?cidr_v6:int -> [ `raw ] Domain_name.t -> mechanism

This mechanims matches if the sender's IP is one of the MX hosts for a domain-name. A domain-name should have a MX record which is an IP address. If this IP address is the same as the given IP address into the given ctx, we consider that the sender matches.

Note: if the domain-name has no MX record, check does not apply the implicit MX rules by querying for an A or AAAA record for the same name.

val v4 : Ipaddr.V4.Prefix.t -> mechanism

This mechanism test whether the given IP from the given ctx is contained within a given IPv4 network.

val v6 : Ipaddr.V6.Prefix.t -> mechanism

This mechanism test whether the given IP from the given ctx is contained within a given IPv: network.

type modifier

The type of modifiers.

They are not available because they mostly provide additional information which are not needed for check. By this way, it's not needed to let the user to define some when they are not effective on the user's sender policy.

type qualifier =
  1. | Pass
  2. | Fail
  3. | Softfail
  4. | Neutral
    (*

    The type of qualifiers.

    A qualifier specifies what the mechanism returns when it matches or not:

    • + returns pass if the mechanism matches
    • - returns fail if the mechanism matches
    • ~ returns softfail if the mechanism matches
    • ? returns neutral if the mechanism matches
    *)
val pass : mechanism -> qualifier * mechanism

pass m specifies the qualifier of the given mechanism m. If the mechanism matches from the given ctx, check returns `Pass. Otherwise, check tries the next mechanism.

val fail : mechanism -> qualifier * mechanism

fail m specifies the qualifier of the given mechanism m. If the mechanism matches from the given ctx, check tries the next mechanism (as it considers the current one as a failure). If the mechanism is the last one, check returns `Fail so.

val softfail : mechanism -> qualifier * mechanism

softfail m specifies the qualifier of the given mechanism m. If the mechanism matches from the given ctx, check tries the next mechanism (as it considers the current one as a soft failure). If the mechanism is the last one, check returns `Softfail so.

val neutral : mechanism -> qualifier * mechanism

neutral m specifies the qualifier of the given mechanism m. Regardless the result of the mechanism (if it matches or not), check tries the next mechanism. If the mechanism is the last one, check returns `Neutral so.

val record : (qualifier * mechanism) list -> modifier list -> record

record ms [] returns a record which can be serialized into the zone file of a specific domain-name as the sender policy.

val record_to_string : record -> string

record_to_string v returns the serialized version of the record to be able to save it into the zone file of a domain-name as the sender policy.

val record_of_string : ctx:ctx -> string -> (record, [> `Msg of string ]) Stdlib.result

record_of_string ~ctx str tries to parse and expand macro of the given string which should come from the TXT record of a domain-name as the sender policy of this domain-name.

val record_equal : record -> record -> bool
type res = [
  1. | `None
  2. | `Neutral
  3. | `Pass of mechanism
  4. | `Fail
  5. | `Softfail
  6. | `Temperror
  7. | `Permerror
]
val pp : record Fmt.t
val pp_res : res Fmt.t
val get : ctx:ctx -> 't Sigs.state -> 'dns -> (module Sigs.DNS with type backend = 't and type t = 'dns) -> (([ res | `Record of record ], [> `Msg of string ]) Stdlib.result, 't) Sigs.io

get ~ctx scheduler dns (module DNS) tries to get the sender policy of the domain-name given by ctx. It requires a scheduler which allows the high-kind polymorphism over a monadic scheduler and a DNS implementation to request the sender policy.

val check : ctx:ctx -> 't Sigs.state -> 'dns -> (module Sigs.DNS with type backend = 't and type t = 'dns) -> [ res | `Record of record ] -> (res, 't) Sigs.io

check ~ctx scheduler dns (module DNS) tries to check the sender policy with the given ctx. It returns the result of this check.

type newline =
  1. | LF
  2. | CRLF
val to_field : ctx:ctx -> ?receiver:Emile.domain -> res -> Mrmime.Field_name.t * Unstrctrd.t

to_field ~ctx ?received v serializes as an email field the result of the sender policy check according to the given ctx. The user is able to prepend then its email with this field.

type extracted = spf list
and spf = {
  1. result : [ `None | `Neutral | `Pass | `Fail | `Softfail | `Temperror | `Permerror ];
  2. receiver : Emile.domain option;
  3. sender : Emile.mailbox option;
  4. ip : Ipaddr.t option;
  5. ctx : ctx;
}
val pp_spf : spf Fmt.t
val extract_received_spf : ?newline:newline -> 'flow -> 't Sigs.state -> (module Sigs.FLOW with type backend = 't and type flow = 'flow) -> ((extracted, [> `Msg of string ]) Stdlib.result, 't) Sigs.io

extract_received_spf ?newline flow scheduler (module Flow) tries to recognized SPF fields values from the given email represented as a flow.

/

val select_spf1 : string list -> (string, [> `None ]) Stdlib.result
val field_received_spf : Mrmime.Field_name.t
val parse_received_spf_field_value : Unstrctrd.t -> (spf, [> `Msg of string ]) Stdlib.result
OCaml

Innovation. Community. Security.