Skip to main content

Local IP Address Binding

If the server EmailEngine runs on has more than one IP address, EmailEngine can bind the outbound connections it makes for an account (IMAP and SMTP sessions, and the Gmail API, Microsoft Graph and OAuth2 token requests) to specific addresses instead of leaving the choice to the operating system. This spreads connections across addresses, which matters when a provider counts connections per source IP, and lets an account keep a stable source address.

Overview​

Three settings control the behaviour. All three are runtime settings stored in Redis, changed through the admin UI or the Settings API:

SettingTypeDefaultPurpose
localAddressesarray of IP addresses[]The pool of local addresses EmailEngine may bind to
imapStrategydefault, dedicated or randomdefaultHow an address is picked for mailbox access: IMAP sessions, Gmail API and Microsoft Graph requests, and OAuth2 token requests
smtpStrategydefault, dedicated or randomdefaultHow an address is picked for sending over SMTP

The admin UI calls the two strategies Mailbox access and Sending over SMTP. The setting keys keep their original names, so imapStrategy also covers accounts that do not use IMAP at all.

The strategies:

  • default - Let the operating system pick the source address
  • dedicated - Reuse the same local address for an account, so the remote server sees a stable IP. The address is chosen by rendezvous hashing of the account ID over the pool, so adding or removing an address only moves the accounts that hashed to it
  • random - Pick a random local address for every connection

The pool is applied to:

  • IMAP connections opened by account workers, including sub-connections
  • SMTP connections made when sending a message through an account
  • The upstream IMAP connections opened by the IMAP proxy
  • Gmail API and Microsoft Graph requests of API accounts
  • OAuth2 token requests of every OAuth2 account: the authorization code exchange, token refreshes, and revoking the grant

API Accounts and OAuth2 Token Requests​

A Gmail API or Microsoft Graph account has no IMAP session, so its API connection takes that role: its requests use the address that imapStrategy picks for the account. With dedicated, each API account keeps one source address, the same one an IMAP account with that ID would get. OAuth2 token requests follow the same rule, so an OAuth2 account that connects over IMAP refreshes its token from the address its IMAP session uses.

Two things take precedence over the local address:

  • The account's own proxy. An account with a proxy sends everything through it
  • An instance-wide proxy for HTTP requests (proxyEnabled or httpProxyEnabled, see Proxy Configuration). The proxy makes the connection then, so there is no local address to bind. This matches how IMAP and SMTP connections behave behind the global proxy

Requests made for the OAuth2 application rather than for one account, such as setting up the Gmail Pub/Sub topic, are not bound to an address from the pool.

An account's address is picked when its API client connects, so as with IMAP, a change to the pool reaches it on the next reconnect. API requests are not listed in the Selected local address log entry described below; that entry covers IMAP and SMTP connections.

How an Address Is Chosen​

For every connection EmailEngine evaluates the pool in this order:

  1. Entries that are not IPv4, or are not configured on a local network interface at that moment, are dropped. IPv6 addresses are accepted by the settings schema but are not used for binding
  2. If nothing is left, the operating system default is used
  3. If exactly one address is left, it is used regardless of the strategy
  4. Otherwise the protocol's strategy decides: dedicated or random as described above, and default hands the choice back to the operating system
  5. The chosen address is looked up in the interface list that Scan for IPs builds (see Admin UI below). An address that has never been scanned cannot be bound; the connection falls back to the operating system default and the log entry described under Watch the Selection in the Logs records selector: "error"

A submitted message can also name an address directly with the localAddress field of POST /v1/account/{account}/submit. That address is used when it is in the scanned interface list and configured on the host, and falls through to the rules above when it is not.

Prerequisites​

The addresses must exist on the host before EmailEngine can bind to them:

ip addr show

Confirm each one can reach the outside world, and that the provider sees the address you expect, by forcing a request out of it:

curl --interface 192.168.1.101 https://ifconfig.me

If a firewall filters outbound traffic by source address, allow ports 993 and 465 (or 143 and 587) from each address in the pool.

Configuration​

Admin UI​

Open Configuration > Network:

  • The IP Address Strategy card has two rows, Mailbox access (imapStrategy) and Sending over SMTP (smtpStrategy), each with a Selection Method dropdown offering Dedicated, Random and Default (the card's description calls the last one the server default)
  • The Available IP Addresses card lists the IPv4 addresses found by the last scan. Scan for IPs probes each interface on the host, records the public address it reaches the internet from, and stores the result in Redis; tick the addresses EmailEngine may use. Ticked addresses that later disappear from the host are skipped and the server default applies

The scan is the only thing that builds the interface list: EmailEngine does not scan at startup, and neither the Settings API nor EENGINE_SETTINGS does it for you. After adding an address to the host, open this page and click Scan for IPs once; the list survives restarts.

Settings API​

The same three values through POST /v1/settings:

curl -X POST "https://emailengine.example.com/v1/settings" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"localAddresses": ["192.168.1.100", "192.168.1.101", "192.168.1.102"],
"imapStrategy": "dedicated",
"smtpStrategy": "dedicated"
}'

The response lists the keys that changed:

{
"updated": ["localAddresses", "imapStrategy", "smtpStrategy"]
}

Preconfiguring at Startup​

These are runtime settings, so they cannot be set in the TOML configuration file. To seed them before the first start, pass them as JSON in the EENGINE_SETTINGS environment variable:

EENGINE_SETTINGS='{"localAddresses":["192.168.1.100","192.168.1.101"],"imapStrategy":"dedicated","smtpStrategy":"dedicated"}' emailengine

See Prepared settings for how EENGINE_SETTINGS is applied. The seeded addresses are still only used once they appear in the scanned interface list, so run Scan for IPs on Configuration > Network after the first start.

Choosing a Strategy​

Spreading Accounts Across Addresses​

Providers that limit concurrent IMAP connections count them per source address as well as per account. With imapStrategy: "dedicated" and five addresses, one hundred accounts settle at roughly twenty per address, and each account keeps its address across reconnects. The same holds for Gmail API and Microsoft Graph accounts, which spreads their API traffic across the pool too:

curl -X POST "https://emailengine.example.com/v1/settings" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"localAddresses": ["192.168.1.100", "192.168.1.101", "192.168.1.102", "192.168.1.103", "192.168.1.104"],
"imapStrategy": "dedicated",
"smtpStrategy": "dedicated"
}'

Rotating Addresses for Sending​

For outbound mail where no single address should carry all the volume, keep IMAP on dedicated and let SMTP pick a random address per connection:

curl -X POST "https://emailengine.example.com/v1/settings" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"localAddresses": ["192.168.1.100", "192.168.1.101", "192.168.1.102", "192.168.1.103"],
"imapStrategy": "dedicated",
"smtpStrategy": "random"
}'

The two strategies are independent, so any combination is valid.

Verifying Configuration​

Check the Settings​

curl "https://emailengine.example.com/v1/settings?localAddresses=true&imapStrategy=true&smtpStrategy=true" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
{
"localAddresses": ["192.168.1.100", "192.168.1.101", "192.168.1.102"],
"imapStrategy": "dedicated",
"smtpStrategy": "dedicated"
}

Watch the Selection in the Logs​

Each IMAP and SMTP connection logs the address it was bound to at the debug level, so the process log level must be debug or trace (the default) for the entry to appear:

emailengine | jq -c 'select(.msg == "Selected local address")'
{"level":20,"time":1697123456789,"pid":12345,"hostname":"server-01","tid":3,"msg":"Selected local address","account":"user123","proto":"IMAP","address":"192.168.1.101","name":"mail.example.com","selector":"dedicated"}

selector records which rule produced the address: dedicated, random, single (only one usable address in the pool), hint (an address named on the submit request), default (empty pool), unknown (several addresses with the default strategy) or error (the chosen address is not in the scanned interface list, so the operating system default was used). name is the hostname EmailEngine presents in the SMTP EHLO greeting, taken from the smtpEhloName setting.

Count Connections per Address​

ss -tn state established '( dport = :993 or dport = :465 )' | awk 'NR > 1 {split($3, a, ":"); print a[1]}' | sort | uniq -c

Updating the Pool​

Changing any of the three settings affects connections opened after the change. Existing IMAP sessions and API clients keep their address until they reconnect; to move an account immediately, request a reconnect with PUT /v1/account/{account}/reconnect.

See Also​