Sovite.Milter.Packet (sovite v0.6.0)

Copy Markdown View Source

Encodes and decodes milter protocol packets (Sendmail's libmilter, protocol version 6) in both directions: commands from the MTA, and the milter's responses. Sovite.Milter uses it as the MTA side; a milter written in Elixir can use it the other way round.

A packet is a 32-bit length in network byte order, a command byte, and the command's data. The length counts the command byte and the data. Strings are NUL-terminated; numbers are in network byte order.

Decoding never creates atoms: command bytes are mapped by fixed clauses, and unknown bytes are errors.

Flags

Actions and protocol flags are bit masks on the wire, and lists of atoms in the rest of Sovite.Milter. action_mask/1, actions/1, protocol_mask/1 and protocol/1 convert between them.

Actions (SMFIF_*): :add_headers, :change_body, :add_recipients, :delete_recipients, :change_headers, :quarantine, :change_sender, :add_recipients_with_args, :set_macros.

Protocol flags (SMFIP_*): steps the milter does not want, :no_connect, :no_helo, :no_mail, :no_rcpt, :no_body, :no_headers, :no_end_of_headers, :no_unknown, :no_data; steps it will not reply to, :no_connect_reply, :no_helo_reply, :no_mail_reply, :no_rcpt_reply, :no_data_reply, :no_unknown_reply, :no_header_reply, :no_end_of_headers_reply, :no_body_reply; and :skip (the milter may answer body chunks with :skip), :rejected_recipients (it wants to see rejected recipients too), :header_leading_space (header values are sent and taken with their leading space).

Summary

Types

An action the MTA allows and the milter asks for.

A command from the MTA. :mail and :rcpt carry the address in angle brackets followed by the ESMTP arguments. A :macro packet names the command byte its macros belong to. A :connect with family :unknown has no port and address.

Why a packet could not be decoded.

The address family of a connecting client.

A protocol flag: a step the milter does not want or will not reply to.

A response from the milter, as on the wire: addresses keep their angle brackets and ESMTP arguments are one string (or nil).

Functions

Returns the bit mask of a list of actions.

Returns the actions in a bit mask. Unknown bits are dropped.

Takes one packet from the start of buffer.

Decodes the command byte and data of a packet from the MTA.

Decodes the command byte and data of a packet from the milter.

Encodes a command from the MTA as a framed packet.

Encodes a response from the milter as a framed packet.

Frames a packet: the length, the command byte, and the data.

Returns the protocol flags in a bit mask. Unknown bits are dropped.

Returns the bit mask of a list of protocol flags.

Types

action()

@type action() ::
  :add_headers
  | :change_body
  | :add_recipients
  | :delete_recipients
  | :change_headers
  | :quarantine
  | :change_sender
  | :add_recipients_with_args
  | :set_macros

An action the MTA allows and the milter asks for.

command()

@type command() ::
  {:optneg, version :: non_neg_integer(), actions :: non_neg_integer(),
   protocol :: non_neg_integer()}
  | {:macro, command :: byte(), [{String.t(), String.t()}]}
  | {:connect, hostname :: String.t(), family(), :inet.port_number() | nil,
     address :: String.t() | nil}
  | {:helo, String.t()}
  | {:mail, [String.t(), ...]}
  | {:rcpt, [String.t(), ...]}
  | :data
  | {:header, name :: String.t(), value :: String.t()}
  | :end_of_headers
  | {:body, binary()}
  | :end_of_message
  | :abort
  | :quit
  | :quit_new_connection
  | {:unknown, String.t()}

A command from the MTA. :mail and :rcpt carry the address in angle brackets followed by the ESMTP arguments. A :macro packet names the command byte its macros belong to. A :connect with family :unknown has no port and address.

error()

@type error() ::
  :empty_packet
  | {:packet_too_large, non_neg_integer()}
  | {:unknown_command, byte()}
  | {:malformed, byte()}

Why a packet could not be decoded.

family()

@type family() :: :inet | :inet6 | :unix | :unknown

The address family of a connecting client.

protocol_flag()

@type protocol_flag() ::
  :no_connect
  | :no_helo
  | :no_mail
  | :no_rcpt
  | :no_body
  | :no_headers
  | :no_end_of_headers
  | :no_header_reply
  | :no_unknown
  | :no_data
  | :skip
  | :rejected_recipients
  | :no_connect_reply
  | :no_helo_reply
  | :no_mail_reply
  | :no_rcpt_reply
  | :no_data_reply
  | :no_unknown_reply
  | :no_end_of_headers_reply
  | :no_body_reply
  | :header_leading_space

A protocol flag: a step the milter does not want or will not reply to.

response()

@type response() ::
  {:optneg, version :: non_neg_integer(), actions :: non_neg_integer(),
   protocol :: non_neg_integer(), [{stage :: non_neg_integer(), String.t()}]}
  | :continue
  | :accept
  | :reject
  | :tempfail
  | :discard
  | :skip
  | :progress
  | :connection_failure
  | :shutdown
  | {:reply_code, String.t()}
  | {:add_recipient, String.t()}
  | {:add_recipient_with_args, String.t(), String.t() | nil}
  | {:delete_recipient, String.t()}
  | {:replace_body, binary()}
  | {:add_header, String.t(), String.t()}
  | {:insert_header, non_neg_integer(), String.t(), String.t()}
  | {:change_header, non_neg_integer(), String.t(), String.t()}
  | {:change_sender, String.t(), String.t() | nil}
  | {:quarantine, String.t()}
  | {:set_macros, stage :: non_neg_integer(), String.t()}

A response from the milter, as on the wire: addresses keep their angle brackets and ESMTP arguments are one string (or nil).

:optneg carries the macro requests, as stage numbers and space-separated macro names. :connection_failure is SMFIR_CONN_FAIL.

Functions

action_mask(actions)

@spec action_mask([action()]) :: non_neg_integer()

Returns the bit mask of a list of actions.

iex> Sovite.Milter.Packet.action_mask([:add_headers, :change_headers])
0x11

actions(mask)

@spec actions(non_neg_integer()) :: [action()]

Returns the actions in a bit mask. Unknown bits are dropped.

iex> Sovite.Milter.Packet.actions(0x11)
[:add_headers, :change_headers]

decode(arg1, max_size)

@spec decode(binary(), pos_integer()) ::
  {:ok, {byte(), binary()}, binary()} | :more | {:error, error()}

Takes one packet from the start of buffer.

  • {:ok, {command, data}, rest} - a whole packet and the bytes after it.
  • :more - the packet is not complete yet.
  • {:error, reason} - an empty packet, or one longer than max_size bytes (command byte and data), checked as soon as the length is in.

decode_command(command, data)

@spec decode_command(byte(), binary()) :: {:ok, command()} | {:error, error()}

Decodes the command byte and data of a packet from the MTA.

decode_response(command, data)

@spec decode_response(byte(), binary()) :: {:ok, response()} | {:error, error()}

Decodes the command byte and data of a packet from the milter.

encode_command(arg1)

@spec encode_command(command()) :: iodata()

Encodes a command from the MTA as a framed packet.

encode_response(arg1)

@spec encode_response(response()) :: iodata()

Encodes a response from the milter as a framed packet.

frame(command, data)

@spec frame(byte(), iodata()) :: iodata()

Frames a packet: the length, the command byte, and the data.

iex> Sovite.Milter.Packet.frame(?c, "") |> IO.iodata_to_binary()
<<0, 0, 0, 1, ?c>>

protocol(mask)

@spec protocol(non_neg_integer()) :: [protocol_flag()]

Returns the protocol flags in a bit mask. Unknown bits are dropped.

protocol_mask(flags)

@spec protocol_mask([protocol_flag()]) :: non_neg_integer()

Returns the bit mask of a list of protocol flags.