Outbound IP pools ([[ip_pool]]): the source addresses mail goes out
from, with warm-up schedules and daily limits.
[delivery]
ip_pool = "shared"
[[ip_pool]]
name = "shared"
[[ip_pool.address]]
address = "192.0.2.10"
helo = "mta1.example.com"
[[ip_pool.address]]
address = "192.0.2.11"
helo = "mta2.example.com"
warmup_start = 2026-10-01
warmup = [50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000]
daily_limit = 100000SMTP deliveries use the pool of their message's tenant
(Sovite.Core.Tenants), else delivery.ip_pool. A source address the
sender relays table gives for the sender comes first; a pool comes
before delivery.source_address.
Picking an address
Each connection picks one IPv4 and one IPv6 address of the pool, for
whichever family the next hop has, among those with room left today.
The address with the most room left, as a share of its limit, goes
first, so new addresses grow into their share as their warm-up goes
on. A connection says helo in EHLO when its address has one
(which should be the name the address's reverse DNS gives), else
server.hostname.
An address's limit for a day (UTC) is the warmup entry for that day,
counting warmup_start as the first; after the schedule, it is
daily_limit, or none. An address is not used before its
warmup_start. Every message sent from an address counts, once per
connection that delivered it.
When no address of a pool has room left, the pool's overflow pool
is used, if it has one, else the delivery is deferred until the next
day starts, with 4.7.0. Counts are kept in the ip_address_counts
table, so they survive restarts.
Clusters
In a cluster, an address can be on another node (node): only that
node uses it. When a pool has no address on this node, its mail is
handed to a node that has some (see Sovite.Core.Cluster.Queue).
Telemetry
[:sovite, :ip_pool, :exhausted]-%{},%{pool}, when a delivery finds no address with room left in a pool or its overflow pools.
Summary
Functions
Counts messages sent from ip today.
The limit of an address (from opts/3) on day: messages, or
:infinity.
The child spec options from the config, or nil without pools.
node is this node's name in a cluster, or nil.
Whether pool pool can send from this node: :local, or {:remote, nodes} with the names of the nodes that have its addresses, when it
has none here. Overflow pools count too.
Whether address ip has room left today, for a connection from it
that is reused.
The addresses to connect from for pool pool, or {:error, text}
when none has room left today.
Starts the pool counters.
The pools with their addresses, and for each the messages sent today and today's limit, for status pages.
Types
@type source() :: %{ source: %{optional(:ipv4 | :ipv6) => :inet.ip_address()}, helo: %{optional(:ipv4 | :ipv6) => String.t()} }
Addresses to connect from, by family, and their EHLO names.
Functions
@spec count(atom(), :inet.ip_address(), pos_integer()) :: :ok
Counts messages sent from ip today.
@spec limit(map(), Date.t()) :: non_neg_integer() | :infinity
The limit of an address (from opts/3) on day: messages, or
:infinity.
@spec opts(Sovite.Core.Config.t(), atom(), Sovite.Core.Repo.t() | nil) :: keyword() | nil
The child spec options from the config, or nil without pools.
node is this node's name in a cluster, or nil.
Whether pool pool can send from this node: :local, or {:remote, nodes} with the names of the nodes that have its addresses, when it
has none here. Overflow pools count too.
@spec room?(atom(), :inet.ip_address()) :: boolean()
Whether address ip has room left today, for a connection from it
that is reused.
The addresses to connect from for pool pool, or {:error, text}
when none has room left today.
@spec start_link(keyword()) :: GenServer.on_start()
Starts the pool counters.
The pools with their addresses, and for each the messages sent today and today's limit, for status pages.