Sovite.Validators (sovite v0.2.0)

Copy Markdown View Source

Syntax validators for the identifiers that appear in SMTP envelopes.

All functions are pure, never raise on bad input, and never create atoms. Any term that is not a binary is treated as invalid.

The grammar follows RFC 5321 §4.1.2 and §4.1.3, with the length limits from RFC 5321 §4.5.3.1. Only ASCII is accepted for now. Internationalized addresses (SMTPUTF8, RFC 6531) come later.

Summary

Types

Reasons returned by split_mailbox/1.

Functions

Returns true if literal is an RFC 5321 address literal: [IPv4] or [IPv6:addr].

Returns true if domain matches the RFC 5321 Domain production.

Returns true if helo is a valid EHLO/HELO argument: a domain or an address literal (RFC 5321 §4.1.1.1).

Returns true if hostname is a valid domain whose top-level label is not all digits (RFC 3696 §2). This rules out IPv4 addresses written as domains, such as "192.0.2.1".

Returns true if local_part is a valid RFC 5321 Local-part: a dot-string or a quoted string, at most 64 octets.

Returns true if mailbox is a valid RFC 5321 Mailbox (Local-part "@" ( Domain / address-literal )).

Parses an RFC 5321 address literal into an :inet IP address tuple.

Validates mailbox and splits it into its local part and domain.

Types

mailbox_error()

@type mailbox_error() ::
  :missing_at
  | :invalid_local_part
  | :local_part_too_long
  | :invalid_domain
  | :too_long

Reasons returned by split_mailbox/1.

Functions

address_literal?(literal)

@spec address_literal?(term()) :: boolean()

Returns true if literal is an RFC 5321 address literal: [IPv4] or [IPv6:addr].

General address literals ([tag:content]) are rejected because IANA has registered no tags besides IPv6.

iex> Sovite.Validators.address_literal?("[192.0.2.1]")
true
iex> Sovite.Validators.address_literal?("[IPv6:2001:db8::1]")
true

domain?(domain)

@spec domain?(term()) :: boolean()

Returns true if domain matches the RFC 5321 Domain production.

Labels are 1-63 letters, digits, or hyphens, and cannot start or end with a hyphen. The whole domain is at most 255 octets. Trailing dots are not allowed.

iex> Sovite.Validators.domain?("mail.example.com")
true
iex> Sovite.Validators.domain?("-bad.example")
false

helo?(helo)

@spec helo?(term()) :: boolean()

Returns true if helo is a valid EHLO/HELO argument: a domain or an address literal (RFC 5321 §4.1.1.1).

This only checks syntax. Whether the name resolves, or matches the client's IP, is a policy decision.

hostname?(hostname)

@spec hostname?(term()) :: boolean()

Returns true if hostname is a valid domain whose top-level label is not all digits (RFC 3696 §2). This rules out IPv4 addresses written as domains, such as "192.0.2.1".

iex> Sovite.Validators.hostname?("mx1.example.net")
true
iex> Sovite.Validators.hostname?("192.0.2.1")
false

local_part?(local_part)

@spec local_part?(term()) :: boolean()

Returns true if local_part is a valid RFC 5321 Local-part: a dot-string or a quoted string, at most 64 octets.

iex> Sovite.Validators.local_part?("first.last+tag")
true
iex> Sovite.Validators.local_part?(~s("john doe"))
true
iex> Sovite.Validators.local_part?("a..b")
false

mailbox?(mailbox)

@spec mailbox?(term()) :: boolean()

Returns true if mailbox is a valid RFC 5321 Mailbox (Local-part "@" ( Domain / address-literal )).

The angle brackets of a reverse-path or forward-path are not part of the mailbox and must be removed before calling this.

parse_address_literal(literal)

@spec parse_address_literal(term()) ::
  {:ok, :inet.ip_address()} | {:error, :invalid_address_literal}

Parses an RFC 5321 address literal into an :inet IP address tuple.

iex> Sovite.Validators.parse_address_literal("[192.0.2.1]")
{:ok, {192, 0, 2, 1}}
iex> Sovite.Validators.parse_address_literal("192.0.2.1")
{:error, :invalid_address_literal}

split_mailbox(mailbox)

@spec split_mailbox(term()) ::
  {:ok, {String.t(), String.t()}} | {:error, mailbox_error()}

Validates mailbox and splits it into its local part and domain.

The local part is returned exactly as written. Quoted local parts keep their quotes and escapes. The domain is not case-folded.

iex> Sovite.Validators.split_mailbox("user@example.com")
{:ok, {"user", "example.com"}}
iex> Sovite.Validators.split_mailbox(~s("a@b"@example.com))
{:ok, {~s("a@b"), "example.com"}}
iex> Sovite.Validators.split_mailbox("user@-example.com")
{:error, :invalid_domain}