Sovite.Core.IPPools (sovite v0.6.0)

Copy Markdown View Source

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 = 100000

SMTP 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

Types

Addresses to connect from, by family, and their EHLO names.

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

source()

@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

count(name, ip, messages)

@spec count(atom(), :inet.ip_address(), pos_integer()) :: :ok

Counts messages sent from ip today.

limit(address, day)

@spec limit(map(), Date.t()) :: non_neg_integer() | :infinity

The limit of an address (from opts/3) on day: messages, or :infinity.

opts(config, name, repo)

@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.

placement(name, pool)

@spec placement(atom(), String.t()) :: :local | {:remote, [String.t()]}

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.

room?(name, ip)

@spec room?(atom(), :inet.ip_address()) :: boolean()

Whether address ip has room left today, for a connection from it that is reused.

source(name, pool)

@spec source(atom(), String.t()) :: {:ok, source()} | {:error, String.t()}

The addresses to connect from for pool pool, or {:error, text} when none has room left today.

start_link(opts)

@spec start_link(keyword()) :: GenServer.on_start()

Starts the pool counters.

status(name)

@spec status(atom()) :: [map()]

The pools with their addresses, and for each the messages sent today and today's limit, for status pages.