Skip to content

Accounts

[account]
name = "personal"
maildir = "personal"       # reserved; the store folder is
                           # the account name for now
archive = "archive"        # where `a` files mail, as the
                           # folder appears in the sidebar;
                           # this is also the default
trash = "trash"            # where `d` moves mail; also the
                           # default

[folder_names]             # sidebar aliases; typed folder
                           # names accept either side
"inbox/accounts" = "accounts"

[imap]
host = "imap.example.com"
port = 993                 # optional, defaults to 993
user = "you@example.com"
password_cmd = "pass show mail/personal"

[smtp]
host = "smtp.example.com"
port = 587                 # optional
user = "you@example.com"   # optional
password_cmd = "..."       # optional

[[identity]]
address = "you@example.com"
name = "Your Name"         # optional display name
signature = "personal"     # optional; a file in signatures/
match = [
    "you@example.com",     # literal
    "you+*@example.com",   # plus-addressing
    "*@you.example.com",   # catch-all domain
]
pgp_sign = false           # sign mail from this identity
# optional; the full fingerprint (gpg --fingerprint shows it),
# otherwise the key is picked by the identity address
pgp_key = "8F0EA48BF8BE9D3B9E1B2B9C6E5F0D3A1C2B4D5E"

[[rules]]
match_list = "~lists/somewhere"   # or match_sender
move_to = "lists/somewhere"       # or tag

[oauth]
provider = "google"        # google | microsoft
client_id = "..."          # optional
tenant = "..."             # optional; a single-tenant
                           # Microsoft registration, see below

[graph]                    # Microsoft Graph sending; the
send = false               # full shape is described below

Identity match patterns allow one *, only in the local part; antiphon doctor validates every pattern and names any bad one. On reply, the most specific match wins (literal, then plus-pattern, then catch-all) and the From is the delivered address verbatim.

The [[identity]] blocks are also managed in the client: editing an account in the settings view opens an identities list with a per-identity editor for every key above, signature included. See the settings view.

Accounts themselves are ranked by [accounts] order in config.toml: listed names come first, in that order, and the first is the primary account, selected on startup and sending a fresh unified-view compose. See configuration.

Folder aliases

[folder_names] gives folders friendlier sidebar names; the alias also works wherever a folder name is typed (:move, for one). The left side is the folder path as the store knows it: lowercase, /-separated.

[folder_names]
"inbox/accounts" = "accounts"
"lists/rust-users" = "rust"

The archive and trash folders

a files mail to the account's archive folder and d moves it to trash, unless the account names others:

[account]
archive = "archive"
trash = "trash"

Both are folder paths as the sidebar shows them. Deleting inside the trash folder is the only permanent delete, and it asks first.

Folder order, hidden and unsynced folders

Three top-level lists shape what the account's folders do. Top-level means exactly that: they sit above the first [table] line of the account file:

folder_order = ["inbox", "lists/aerc"]
folders_hidden = ["spam"]
folders_unsynced = ["archive/2019"]

[account]
name = "personal"
# ...
  • folder_order lists sidebar folder names (as the sidebar shows them, inbox included) to place first, in that order; the rest stay alphabetical.
  • folders_hidden drops folders from the sidebar. A hidden folder is still synced and still searchable; it just is not listed.
  • folders_unsynced lists folders the daemon never downloads at all, matched case-insensitively on the maildir-relative name. A trailing * covers the named folder and everything beneath it: calendar* matches calendar and calendar/birthdays alike. The inbox can never be excluded.

All three are edited live on the settings Folders tab: Shift-J/K reorder, h hides and unhides, u stops and resumes syncing.

A newly added Microsoft 365 account starts with the mailbox's non-mail folders unsynced: calendar*, contacts, conversation history, journal, rss feeds and outbox. Microsoft exposes these over IMAP, and syncing them wastes time and clutters the sidebar. They stay listed on the Folders tab; U re-includes any you do want.

OAuth accounts (Microsoft 365, Google)

An OAuth account (Microsoft 365, Google Workspace) is set up either in the settings view's account form, whose provider row swaps the password fields for the OAuth ones (see the settings view), or by hand with the [oauth] table shown above; OAuth accounts need no password_cmd. Then sign in: O on the account in the settings Accounts tab, or antiphon oauth login <account> on the command line. Grant state lives on the same tab, and antiphon oauth status <account> prints it too.

The sign-in is the PKCE flow, no client secret involved. From inside the client both providers use the browser: the consent page opens, and a loopback listener catches the redirect. A Microsoft sign-in falls back to the device code (open a URL anywhere, type the code) when the listener cannot bind. On the command line, Google uses the same loopback flow and Microsoft the device code. The loopback flow needs the Microsoft app registration to carry a Mobile and desktop applications redirect of http://localhost; Entra matches that on any port, whereas http://127.0.0.1 would have to name the exact port. The device-code flow works without it.

The sign-in cannot land on the wrong mailbox. A Microsoft browser session for another account would otherwise satisfy the shared consent page silently, so Antiphon always shows the provider's account picker with the account's own address pre-selected, and it verifies the identity the token came back for: a sign-in that authorised a different account is rejected with an error naming both addresses, and nothing is stored.

The [oauth] client_id names your app registration with the provider. A Microsoft account may leave it out entirely: Antiphon then signs in with Thunderbird's public client id, which most tenants already permit. A Google account always needs its own registration. Tenants that restrict user consent may still need an Entra admin to grant the app consent once (the registration's "Grant admin consent" button); until then the sign-in fails with the provider's consent error.

Two environment variables override the [oauth] client_id, useful when one registration serves every account or the id should stay out of config files: ANTIPHON_MS_CLIENT_ID for Microsoft accounts and ANTIPHON_GOOGLE_CLIENT_ID for Google ones. When set, they take precedence over the account file.

Single-tenant Microsoft registrations

A Microsoft app registration set to a single tenant (rather than "any organizational directory") refuses the shared /common sign-in endpoint:

AADSTS50194: Application '...' is not configured as a
multi-tenant application. Usage of the /common endpoint is
not supported.

Name the tenant under [oauth] and every grant, the IMAP sign-in included, uses it in place of /common:

[oauth]
provider = "microsoft"
client_id = "..."
tenant = "contoso.onmicrosoft.com"   # tenant id or domain

The [oauth] tenant covers the whole account. When only the [graph] table sets a tenant, that one is borrowed for the mail sign-in too, so a single value serves both; a tenant under [graph] still overrides it for the Graph send grant.

Sending through Microsoft Graph

Where a Microsoft 365 tenant disallows SMTP submission, the [graph] table routes an account's outgoing mail through the Graph API instead:

[graph]
send = true
tenant = "00000000-0000-0000-0000-000000000000"  # optional
                           # for delegated; required for
                           # app_only
client_id = "..."          # optional; falls back to the
                           # [oauth] client_id
auth = "delegated"         # delegated | app_only
secret_cmd = "pass show m365/secret"  # app_only only

auth = "delegated" (the default) sends as the signed-in user: antiphon oauth login acquires a second, Graph-scoped grant alongside the mail one, honouring the tenant (falling back to the [oauth] tenant, then the multi-tenant /common/ endpoint) and a Graph-specific client_id where the sending registration differs from the mail one.

auth = "app_only" sends with application credentials instead: no interactive grant at all, and oauth login skips the Graph sign-in for such accounts. The daemon fetches a client-credentials token at send time and submits through /users/<sender>/sendMail; tenant, a client_id (in [graph] or [oauth]) and secret_cmd, a command that prints the app registration's client secret, are all required. The secret is never stored, in the vault or anywhere else; it is asked for at the moment of use, the same contract as password_cmd.