Skip to main content

Configuration

The daemon reads one TOML file at startup. This page lists every key it accepts. The keys come from the structs and validation in internal/config.

The file​

The daemon reads /etc/chainmail/config.toml unless --config or CHAINMAIL_CONFIG names another file. The flag wins over the variable.

The file holds no secrets. Passwords, OAuth2 client secrets and refresh tokens live in the daemon's vault (see Vault secrets).

The daemon reads the file only when it starts, so restart it after an edit.

On NixOS the module renders this file from services.chainmail.settings. See Deploy.

The CLI's own settings, such as the socket and the output format, live in a different file, cli.toml. See CLI settings.

How the file is checked​

  • An unknown key is an error, and the message names it, such as unknown config key: rules.
  • A key with a capital letter is an error, such as [Policy]. So is a key with a dot inside quotes, such as [accounts."a.b"].
  • A value of the wrong type is an error, such as pool = "2".
  • No environment variable can set or change a key.
  • Once every key is known and every type is right, the daemon checks the values and reports every invalid one together, separated by ; .

A config error stops the daemon at startup. It exits 1 with the reason INTERNAL. The message starts with config <path>:, or with reading config: when the file cannot be read. See Exit codes.

config /etc/chainmail/config.toml: invalid config: policy.max_recipients must be 1 to 500; watch.retention: 30m must be between 1h and 2160h

Keys from the full design, such as rules, belong to later sub-projects. This version rejects them as unknown.

Value formats​

FormatWritten asExamples
Name1 to 63 characters: lowercase letters, digits and hyphens, starting with a letter or digit (^[a-z0-9][a-z0-9-]{0,62}$). Account and OAuth2 client names use it, because they become vault secret names.work, site-2
SizeA whole number of bytes, up to 12 digits, optionally followed by B, KiB, MiB, GiB or TiB. Units are powers of 1024.65536, 64KiB, 35MiB, 2GiB
DurationA Go duration.30s, 5m, 168h, 1h30m
Cache durationA whole number of days as <n>d, or a Go duration. Only the [cache] keys accept the d unit.90d, 0d, 250ms
Allowlist entryA bare address, or *@ and a domain. Matching ignores case. A *@domain entry matches that exact domain, not its subdomains.[email protected], *@example.org

A full example​

Every key is set here, most of them to their defaults. A real file needs only the accounts and the keys you change.

[daemon]
socket = "/run/chainmail/chainmail.sock"
client_group = "chainmail-clients"
body_limit = "64KiB" # longer bodies are cut unless --full is given

[oauth_clients.workspace] # secret in the vault as oauth_clients/workspace
client_id = "1234-example.apps.googleusercontent.com"

[oauth_clients.gmail]
client_id = "5678-example.apps.googleusercontent.com"

[accounts.work]
provider = "google-workspace" # auth defaults to oauth2, the only method it accepts
address = "[email protected]"
oauth_client = "workspace" # refresh token stored as tokens/work by auth login
display_name = "Ann Example" # the From name on mail this account sends
signature = "Ann\n[example.org](https://example.org)" # Markdown

[accounts.personal]
provider = "gmail"
address = "[email protected]"
auth = "oauth2" # gmail accepts two methods, so auth is required
oauth_client = "gmail"

[accounts.personal.cache]
prefetch_bodies = "30d" # any [cache] key but max_size and dir, for one account

[accounts.icloud]
provider = "icloud"
address = "[email protected]" # app password in the vault as accounts/icloud

[accounts.site]
provider = "hostinger"
address = "[email protected]"
archive_folder = "Archive" # where archive moves mail; default the \Archive folder
pool = 2 # IMAP connections; default from the probe
max_message_size = "35MiB" # largest message it sends; default from the preset
watch = ["INBOX"] # folders watched with IDLE; [] watches nothing
sync = { folders = ["INBOX", "Sent"], poll = "5m" }

[accounts.lab]
provider = "custom"
address = "[email protected]"
username = "ann"

[accounts.lab.imap]
host = "mail.lab.example.net"
port = 993 # security defaults to tls

[accounts.lab.smtp]
host = "mail.lab.example.net"
port = 587
security = "starttls"

[accounts.lab.policy]
send_allow = ["*@lab.example.net"] # allowed for this account only

[policy]
send_allow = ["*@example.org", "[email protected]"]
max_recipients = 10
allow_expunge = false
allow_folder_delete = false

[watch]
retention = "168h" # events and idle groups older than this are removed
max_events = 200000
expunge_sweep = "1h"
idle_fallback_poll = "30s" # watched folders on servers without IDLE
google_check = "5s" # how often a Google account's All Mail is synced while it idles

[cache]
enabled = true # false: no cache work; every search and get uses the server
prefetch_bodies = "90d" # bodies of mail that arrived within this are fetched ahead
read_retention = "30d" # bodies read outside the window are kept this long
max_size = "2GiB" # the whole cache.db file
daily_download = "1GiB" # per account per UTC day, for backfill and prefetch
backfill_batch = 250
backfill_pause = "250ms"
fresh_lag = "30s" # how far the cache may trail mail events and still answer
# dir = "/var/cache/chainmail" # default: the daemon's --state-dir

[daemon]​

KeyTypeDefaultValid valuesWhat it does
socketstring/run/chainmail/chainmail.socka pathThe socket path of a production daemon. --socket overrides it. A --dev daemon ignores it and uses --socket, then CHAINMAIL_SOCKET, then $XDG_RUNTIME_DIR/chainmail-dev.sock. On macOS, which has no /run, set it to a path in a directory the daemon's user owns. The NixOS module requires a path under /run/chainmail/.
client_groupstringchainmail-clientsan existing groupThe group that owns the socket. Its members may use the daemon. The daemon refuses to start when the group does not exist. A --dev daemon ignores it. The NixOS module requires chainmail-clients.
body_limitsize64KiB1KiB to 8MiBThe longest message body get and thread return before they cut it and set truncated. --full, and full in MCP, ignore it.

[oauth_clients.<name>]​

One table per OAuth2 client registration. <name> is a name. The client's secret goes in the vault as oauth_clients/<name>.

KeyTypeDefaultValid valuesWhat it does
client_idstringnone, requiredany non-empty stringThe client ID Google issued.

An empty [oauth_clients.<name>] table is an error, because client_id is required.

[accounts.<name>]​

One table per mail account. <name> is a name, and it is the name every command and tool uses for the account. Accounts walks through each provider.

KeyTypeDefaultValid valuesWhat it does
providerstringnone, requiredgoogle-workspace, gmail, icloud, hostinger, customThe preset that gives the servers, the auth methods and the limits. See Provider presets.
addressstringnone, requireda bare email address, with no display nameThe account's address. From always uses it.
usernamestringthe addressany stringThe login name for IMAP and SMTP.
authstringthe preset's only methodpassword, oauth2, as the preset acceptsHow chainmail logs in. Required for gmail, the only preset that accepts both.
oauth_clientstringnonethe name of an [oauth_clients.<name>] tableThe OAuth2 client to log in with. Required with oauth2, and an error with password.
imaptablethe preset's IMAP serversee Server overridesReplaces the preset's IMAP server. Required for custom.
smtptablethe preset's SMTP serversee Server overridesReplaces the preset's SMTP server. Required for custom.
display_namestringnoneany stringThe display name in the From of mail the account sends.
signaturestringnoneMarkdownAdded under the body of every message the account writes, after -- in the text part. --no-signature, and no_signature in MCP, leave it out.
archive_folderstringthe folder with the \Archive attributea folder nameWhere archive moves mail on accounts other than Google. Google archives by removing the Inbox label.
poolintegerfrom the probe, else the preset0 to 12The IMAP connections the daemon keeps for the account. 0 is the same as leaving it out. A set value overrides the probed size, within the connection cap. See Connections.
max_message_sizesize25MiB for Google and custom servers, 35MiB for iCloud and Hostinger1KiB to 1024MiBThe largest message the account sends, attachments included. It also caps each upload. Whatever it is, a send-raw message, a body or HTML file, and a dry run's output are limited to 40 MiB, the most one API request or response holds.
watcharray of strings["INBOX"]folder names, none empty, none twiceThe folders watched with IDLE, each on a connection of its own. [] watches nothing. An error on the Google presets, which always watch All Mail. See Watching for mail.
synctablesee belowsee [accounts.<name>.sync]Which folders are synced, and how often.
policytablenonesee [accounts.<name>.policy]The account's own allowlist entries.
cachetablethe [cache] valuessee [accounts.<name>.cache]Cache settings for this account only.

An empty [accounts.<name>] table is an error, because provider and address are required.

[accounts.<name>.imap] and [accounts.<name>.smtp]​

A server override replaces the whole preset server, so give host and port even when only one of them changes.

KeyTypeDefaultValid valuesWhat it does
hoststringnone, requireda host name or IP addressThe server's host.
portintegernone, required1 to 65535The server's port.
securitystringtlstls, starttlstls is TLS from the first byte, as on ports 993 and 465. starttls upgrades a plain connection, as on port 587.

[accounts.<name>.sync]​

The table can also be written inline, as sync = { folders = ["INBOX", "Sent"], poll = "5m" }.

KeyTypeDefaultValid valuesWhat it does
enabledbooleantruetrue, falsefalse turns off watching and syncing for the account, Google presets included.
foldersarray of stringsevery folder a default search coversfolder names, none emptyThe folders synced as well as the watched ones. Every watched folder is synced. A synced folder that is not watched is polled every poll. Without this key, the daemon syncs each selectable folder except Trash, Junk and a virtual All folder, and lists the folders again every watch.expunge_sweep. Setting it limits syncing, and so the cache, to the watch list and these folders. [] syncs the watch list alone. An error on the Google presets.
pollduration5m30s to 24hHow often synced folders that are not watched are checked. Also how often a missing folder is tried again.

[accounts.<name>.policy]​

KeyTypeDefaultValid valuesWhat it does
send_allowarray of strings[]allowlist entriesEntries added to the global policy.send_allow for this account only.

No other key is allowed in this table. max_recipients, allow_expunge and allow_folder_delete are global. See The send policy.

[accounts.<name>.cache]​

Any [cache] key except max_size and dir, for one account. A key left out takes the [cache] value. max_size and dir bound and place the one shared cache.db file, so they are an error here.

Provider presets​

The provider key picks one of these presets. The values come from internal/provider.

ProviderIMAPSMTPAuthConnection capPool before a probemax_message_size
google-workspaceimap.gmail.com:993, tlssmtp.gmail.com:587, starttlsoauth210625MiB
gmailimap.gmail.com:993, tlssmtp.gmail.com:587, starttlsoauth2 or password10625MiB
icloudimap.mail.me.com:993, tlssmtp.mail.me.com:587, starttlspassword3335MiB
hostingerimap.hostinger.com:993, tlssmtp.hostinger.com:465, tlspassword3335MiB
customfrom [accounts.<name>.imap]from [accounts.<name>.smtp]password3325MiB

Gmail allows 15 IMAP connections per account. The Google presets take 10, which leaves 5 for your other mail clients. The Google presets watch All Mail and do not accept watch or sync.folders.

[policy]​

The global send and delete policy. The send policy explains how a send is checked, and Policy in operation covers held sends and refusals.

KeyTypeDefaultValid valuesWhat it does
send_allowarray of strings[]allowlist entriesThe recipients every account may send to. The addresses of the configured accounts are always allowed. A send to anyone else is held as a draft. A recipient that is not a plain local@domain address, such as one with a quoted local part, is always held.
max_recipientsinteger101 to 500, or 0 for the defaultThe most distinct recipients, To, Cc and Bcc together, that a send may have. A send with more is held.
allow_expungebooleanfalsetrue, falseAllows expunge, and lets draft removal expunge instead of moving the old draft to Trash.
allow_folder_deletebooleanfalsetrue, falseAllows folder delete, including --force on a folder that holds mail.

[watch]​

The event log and the watchers. See Watching for mail and Watching in operation.

KeyTypeDefaultValid valuesWhat it does
retentionduration168h1h to 2160hHow long events are kept, and how long a group may go unused before it is removed.
max_eventsinteger2000001000 to 10000000, or 0 for the defaultThe most events the log keeps. The oldest are removed first.
expunge_sweepduration1h5m to 24hHow often synced folders are swept for expunged mail.
idle_fallback_pollduration30s10s to 10mHow often a watched folder is synced on a server without IDLE.
google_checkduration5s2s to 10mHow often a Google account's All Mail is synced while its IDLE waits. Gmail can tell an IDLE about new mail 30 seconds or more after it arrives, so this bounds how late a new event can be.

[cache]​

The local cache of every account. Every key but max_size and dir can also be set per account in [accounts.<name>.cache]. See The local cache and The cache in operation.

KeyTypeDefaultValid valuesWhat it does
enabledbooleantruetrue, falsefalse stops all cache work, and every search and get uses the mail servers. Rows already cached stay in cache.db until you delete it. cache rebuild refuses an account whose cache is off.
prefetch_bodiescache duration90d0d to 3650dThe body window. Bodies of messages that arrived within it are fetched ahead and kept. 0d fetches bodies only when they are read.
read_retentioncache duration30d0d to 3650dHow long a body outside the window is kept after a get or thread read it.
max_sizesize2GiB64MiB to 1TiBThe most space cache.db may take, headers and index included. Global only.
daily_downloadsize1GiB16MiB to 100GiBThe daily download allowance that the backfill and the body prefetch keep to, per account per UTC day, within about one batch. It counts each body's size and 2 KiB per header. A get of a message the cache lacks counts too.
backfill_batchinteger25010 to 1000, or 0 for the defaultHeaders fetched per backfill batch.
backfill_pausecache duration250ms0s to 60sThe pause between backfill batches.
fresh_lagcache duration30s1s to 10mHow far the cache may trail the account's mail events and still answer searches.
dirstringthe daemon's --state-diran absolute pathThe directory for cache.db. Global only. The daemon creates a missing directory with mode 0700.

Vault secrets​

The vault holds the secrets the config refers to by name.

SecretHoldsWritten by
accounts/<account>a password or app passwordchainmail secret set accounts/<account>
oauth_clients/<client>an OAuth2 client secretchainmail secret set oauth_clients/<client>
tokens/<account>an OAuth2 refresh tokenchainmail auth login <account>

secret set accepts only names the config uses: accounts/<account> where the account has auth = "password", and oauth_clients/<client> where the client is defined. It refuses tokens/<account>, because only auth login writes a refresh token. See Auth.

Connections​

Each watched folder holds one IMAP connection of its own, outside the pool. A Google account holds one for All Mail. These watcher connections count toward the account's connection cap: the probed cap when there is one, else the preset's.

The pool shrinks to fit what the watchers leave, never below 1, and a configured pool is capped the same way. The watchers never shrink. Polled folders and the cache use the pool too.

The cache's background work takes a connection only when one is idle, or when the pool has room and no request is waiting. It gives the connection back after one batch, so it slows down rather than delay a request.

A Hostinger account that watches only its inbox therefore runs 1 watcher and a pool of 2 until a probe measures it. See Connection limits.

Daemon flags and environment​

These settings are not in the file. Each flag has an environment variable, and the flag wins.

FlagVariableDefaultWhat it does
--configCHAINMAIL_CONFIG/etc/chainmail/config.tomlThe config file.
--state-dirCHAINMAIL_STATE_DIR/var/lib/chainmailThe directory for vault.age, state.db and, unless [cache] dir is set, cache.db. The daemon creates it with mode 0700.
--vault-key-fileCHAINMAIL_VAULT_KEY_FILE$CREDENTIALS_DIRECTORY/vault-keyThe age identity that decrypts the vault. Without the flag, the variable or CREDENTIALS_DIRECTORY, the daemon refuses to start.
--devCHAINMAIL_DEVfalseDevelopment mode: the daemon runs as you, needs a state directory other than /var/lib/chainmail, and ignores daemon.socket and daemon.client_group. The variable takes true, false, 1, 0, t or f.
--socketCHAINMAIL_SOCKETdaemon.socketThe socket to listen on. A production daemon reads only the flag, then daemon.socket. A --dev daemon reads the flag, then the variable.

The full flag list is on the chainmail daemon page.