# The operator path Choosing an installation, getting started, configuring a mailbox at your provider, and administering the deployment. This is one reading path through the MailFathom user guide, every page of it in one file and in the order the guide walks them. `llms.txt` beside this file maps the whole of the documentation, and every link below is relative to the directory that map sits in. # Installing MailFathom MailFathom runs in four shapes, and each has one authoritative guide. This page is the decision: what each shape assumes, what it is good for, and what every shape shares. Follow the linked guide for the commands; the guides do not repeat each other and neither does this page. **A release publishes an image, the chart, the schema script, and the administrative command.** The image is `ghcr.io/krzysztof318/mailfathom:` and `docker.io/krzysztof318/mailfathom:` — one manifest list under one digest, so the registry to pull from is whichever your environment already reaches — with `latest` on the newest release's digest in both. The chart is `oci://ghcr.io/krzysztof318/charts/mailfathom` at the same version. Each release also attaches `mailfathom-schema-.sql` and its checksum, which is the schema step below, and one `mfctl` binary per platform with a checksum file covering all of them — [getting the command](operations/admin-endpoint.md#getting-the-command) is where that one is picked up, including the [install script](operations/admin-endpoint.md#on-linux-with-the-install-script) that does it in one line on Linux. Both packages are public, so pulling one needs no login. **`` is the release you are installing**, and [the releases page](https://github.com/Krzysztof318/MailFathom/releases) is where the current one is named. Pin it rather than tracking `latest`: an immutable tag is what makes a deployment reproducible and an upgrade a decision, which matters here because a new release can require a schema step before it will serve. **There is no binary artifact for the service itself**, so the native shape below is published from a checkout, and so are the Compose deployment and the Quadlet units, whose files live here and are versioned with the code that reads them. `mfctl` is the exception, and it is a client rather than the service: it runs on the machine you administer *from*. Beside the release runs the nightly channel: `ghcr.io/krzysztof318/mailfathom:nightly` — or `docker.io/krzysztof318/mailfathom:nightly`, which is the same digest in the other registry — and the `-nightly.-` tag of each night's build, published from `main` when it has moved. **A nightly is not a release and is a poor place to keep data you care about** — its schema can be ahead of any migration, it has no upgrade path in either direction, and it is deleted once thirty newer ones exist. [What a nightly build risks](operations/container-image.md#what-a-nightly-build-risks) states the whole of it before you choose one. ```bash git clone https://github.com/Krzysztof318/MailFathom.git cd MailFathom ``` ## Choosing a shape | Shape | Choose it when | Guide | | --- | --- | --- | | **Docker Compose** | You self-host on one machine and want the database, the network boundary, and the secret mounts arranged for you | [Deploying with Docker Compose](operations/deployment-compose.md) | | **Podman Quadlet** | You self-host on one machine, run Podman rootless, and want the container's secrets to be encrypted systemd credentials rather than plaintext files | [Deploying with Podman Quadlet](operations/deployment-quadlet.md) | | **Kubernetes with Helm** | You operate a cluster and bring your own Secret management | [Deploying to Kubernetes](operations/deployment-kubernetes.md) | | **Native process** | You run services under systemd without a container runtime, and want secrets delivered as systemd credentials | [Below](#native-process), then [secret provisioning](operations/secret-provisioning.md#native-systemd-service) | Docker Compose is the recommended first installation. It provisions PostgreSQL for you — `compose.yaml` creates the role, the database, and the `vector` extension on first start — and its defaults publish both ports on loopback, so nothing is reachable from another machine until you decide it should be. The Podman Quadlet is that same stack expressed as systemd units, and it provisions PostgreSQL the same way. What it buys is the one thing no Compose file can reach: a `.container` file is a systemd unit source, so the deployment's secrets are `LoadCredentialEncrypted=` credentials — ciphertext at rest, bound to the machine, decrypted only as the unit starts. What it asks in return is Podman rather than Docker, a rootless user, systemd 258 or later, and a decision about SELinux that its guide states before the first command. The Helm chart provisions one too, as a StatefulSet on a persistent claim, and turns it off for a deployment that has a server of its own. It creates no Secret, deliberately, so it needs an image reference and a Secret carrying the credentials; [what you supply](operations/deployment-kubernetes.md#what-you-supply) covers both, and the trade-off between the deployed database and one you operate, before the install command. The native process is the shape that brings no database at all. ## What every shape needs - **Linux.** It is the only platform this project officially supports, and everything below assumes it: the image is built for `linux/amd64` and `linux/arm64`, the native shape is a systemd service with systemd credentials, and TLS goes through the system OpenSSL. **MailFathom may well run on Windows — it is ordinary .NET — but expect problems and a setup of your own**: credential provisioning, TLS parameters, and file-permission expectations all differ there, nothing in this repository is verified against it, and a defect that reproduces only on Windows is not one this project can act on today. - **PostgreSQL with the `vector` extension.** The synchronized mail, its indexes, and the raw message content all live there. The Compose deployment, the Quadlet units, and the Helm chart bring their own (`pgvector/pgvector`, PostgreSQL 18); a native process expects yours, and the chart uses yours when you ask it to. - **An IMAP account to synchronize** and its password or app password, provisioned as a [secret reference](operations/secret-provisioning.md) rather than written into configuration. - **A data-encryption key, if any mailbox authenticates with OAuth.** MailFathom seals the refresh tokens it stores under one key the whole deployment shares, so generate it once before the first start and provision it like any other secret: ```bash openssl rand -base64 32 ``` It is `-base64 32`, not the `-base64 33` beside it for the database passwords: the value has to decode to exactly 32 bytes and a longer one is refused at startup. **Back it up with the database and never regenerate it** — the key is not in the database, and losing it means re-authorizing every mailbox. [Secret provisioning](operations/secret-provisioning.md#the-data-encryption-key) covers where it goes in each shape, and [the configuration reference](operations/configuration-reference.md#dataencryption) the section that points at it. A deployment whose mailboxes all authenticate with a password needs none, and starts without one. - **OpenSSL 3.0 or later**, because MailFathom connects to the mail server over TLS and .NET hands every handshake to the system library. **1.1.1 is the floor below which nothing runs at all**: .NET 10 requires it on Unix and [fails to start](https://learn.microsoft.com/en-us/dotnet/core/compatibility/cryptography/10.0/openssl-version-requirement) without it. **Between the two, MailFathom may work and may not.** 1.1.1 has been out of upstream support since September 2023, nothing here is verified against it, and a defect that reproduces only there is not one this project can act on. Every current distribution ships 3.x, so this is a constraint on old machines rather than on new ones. The library's *security policy* is part of the installation too, not a detail of it: an OpenSSL that considers a server's cipher suite or key size too weak ends the connection before any credential is sent, and reports it as an authentication failure. **An installation that configures nothing runs at that full-strength policy and negotiates the newest TLS both ends support**, which is what almost every mail server wants. One that does not clear the policy is reached by opting in to a relaxed one — an OpenSSL configuration file named in the environment, which [the platform TLS policy](operations/platform-tls-policy.md) covers with a sample file. It is an exception you choose per deployment, never a default, and nothing in MailFathom's own configuration can substitute for it. - **An explicit schema step.** MailFathom never applies database migrations while starting: it verifies the schema and refuses to serve against one it does not recognize, so bringing a new build up *tells* you a migration is outstanding rather than silently applying one. What you apply is one idempotent SQL file — a release attaches `mailfathom-schema-.sql`, and a checkout produces the same file with `scripts/build-schema-artifact.sh`. Read it, back the database up, and run it with any PostgreSQL client; [applying the database schema](operations/database-schema.md) states the privileges it needs and what each startup failure means. ## Native process The publish output is self-contained in licensing terms — it carries `LICENSE` and `NOTICE` beside the binaries — but there is no packaged unit file or installer yet, so a native installation is assembled by hand: ```bash dotnet publish src/Host/Host.csproj --configuration Release --output /opt/mailfathom ``` Build with the SDK pinned in `global.json`. The process is then an ordinary ASP.NET Core service: - Configuration arrives through `appsettings.json` beside the binaries, a deployment-provisioned JSON file or directory named by [`ConfigurationSources`](operations/configuration-sources.md), command-line arguments, or environment variables. - Credentials arrive as systemd credentials: `LoadCredential=` in the unit, `systemd-credential:` references in the configuration. [Secret provisioning](operations/secret-provisioning.md#native-systemd-service) shows the unit fragment, including the encrypted-at-rest variant and the core-dump limit worth setting alongside it. The data-encryption key is one of them, provisioned no differently from a mailbox password: ```ini [Service] LoadCredentialEncrypted=mailfathom-data-key:/etc/mailfathom/mailfathom-data-key.cred ``` ```json { "DataEncryption": { "ActiveKeyId": "2026-08", "Keys": [ { "KeyId": "2026-08", "Material": { "Name": "mailfathom-data-key", "SecretReference": "systemd-credential:mailfathom-data-key" } } ] } } ``` `KeyId` is stored beside every value the key seals, so it is chosen once and never edited afterwards. Leave both out when no account authenticates with OAuth. **`systemd-creds encrypt` seals that `.cred` file against this machine** — by default against its TPM2 chip and its `/var/lib/systemd/` together — so the file opens here and on no other host, and it is not a backup of what it holds. Run the command on the machine that will read it, back the key's own base64 up with the database, and encrypt it again on a replacement machine rather than copying the sealed file across. [What an encrypted credential is bound to](operations/secret-provisioning.md#what-an-encrypted-credential-is-bound-to) states the whole binding, including which flag makes the chip a requirement rather than a preference and why a firmware update does not invalidate the credential. - [Where each surface is served](operations/configuration-reference.md#where-each-surface-is-served) is stated by each surface's own section. `McpEndpoint:BindAddress` and `McpEndpoint:Port` bind the protocol surface, `0.0.0.0:8080` by default, in clear text unless you configure otherwise. `ASPNETCORE_URLS`, `ASPNETCORE_HTTP_PORTS`, and `Kestrel:Endpoints` are refused at startup, so an address you state is never one the process quietly ignores. The health probes answer on their own port, `8081` by default; [health endpoints](operations/health-endpoints.md) records how to move or disable that listener. - PostgreSQL, the `vector` extension, and the schema step are yours, exactly as they are under Kubernetes. - A mail server whose TLS parameters the machine's own OpenSSL refuses is reached by naming an OpenSSL configuration file in the service's environment, which is a pre-start concern no MailFathom setting can replace. [The platform TLS policy](operations/platform-tls-policy.md) has the sample file and the unit fragment. ## Verifying any installation An installation is done when the probes answer and the log shows a healthy start: ```bash curl -fsS http://127.0.0.1:8081/started # startup gates have passed curl -fsS http://127.0.0.1:8081/health # ready, including the database curl -fsS http://127.0.0.1:8081/alive # the process itself ``` A refusal at startup is designed to name the setting that caused it — a missing secret reference, a pending migration, an unsafe transport combination — so the log is the first place to look, and the message quotes the configuration key to fix. Continue with [getting started](users/getting-started.md): provisioning the secrets, configuring the first account, and connecting an MCP client. # Getting started This page walks from an installed MailFathom to a first successful tool call: provision the credentials, configure a mailbox, start the service, verify it, connect an MCP client, and read a result correctly. It assumes an installation from [installing MailFathom](users/installation.md); a developer evaluating from the checkout can run the [Aspire orchestration](operations/local-development.md#running-locally-with-aspire) instead, which provisions PostgreSQL and applies the schema on its own. The examples write configuration as JSON. Every setting can arrive as an environment variable instead — `:` becomes `__`, so `MailSynchronization:Enabled` is `MailSynchronization__Enabled` — and the [configuration reference](operations/configuration-reference.md) lists every key used below. ## 1. Provision the secrets MailFathom's configuration never carries a credential. A secret-bearing setting holds a *reference* — `file:/…`, `systemd-credential:…`, `env:…` — and the material lives wherever the deployment provisions it. A configuration file is therefore safe to review and back up: leaking it leaks paths, not passwords. You need two pieces of material before anything is configured: - **The mailbox password** or app password of the IMAP account to synchronize. A mailbox whose provider no longer accepts one — a Google Workspace or Exchange Online account — is authenticated with an OAuth refresh token instead; obtain it first with [mailbox OAuth](operations/mailbox-oauth.md) and substitute that block for the password below. That account needs a third piece of material as well, the **data-encryption key** the rotated refresh token is sealed under: `openssl rand -base64 32`, generated once and never regenerated, provisioned as a reference like everything else here. [The data-encryption key](operations/secret-provisioning.md#the-data-encryption-key) is the whole of it. - **An MCP API key** for the client that will connect. Generate it rather than inventing it: ```bash openssl rand -base64 33 | tr -d '\n' > mcp-workstation-key ``` Where the files go is the deployment's convention: `secrets/mailfathom/` for [Compose](operations/deployment-compose.md#credentials), a Kubernetes `Secret` mounted at `/etc/mailfathom/secrets` for [the chart](operations/deployment-kubernetes.md#what-you-supply), `LoadCredential=` for [systemd](operations/secret-provisioning.md#native-systemd-service), and an encrypted credential under `~/.config/credstore.encrypted/` for [the Quadlet](operations/deployment-quadlet.md#the-credentials). The references below assume the mounted directory the Compose and Helm shapes share. ## 2. Configure the mailbox Synchronization is off until configuration turns it on, and an enabled synchronization requires at least one account: ```json { "MailSynchronization": { "Enabled": true, "Accounts": [ { "AccountId": "primary", "DisplayName": "Personal mail", "Host": "imap.example.test", "Port": 993, "UserName": "you@example.test", "Secrets": { "Password": { "Name": "imap-primary-password", "SecretReference": "file:/etc/mailfathom/secrets/imap-primary-password" } }, "Folders": [ { "Alias": "inbox", "SpecialUse": "Inbox" }, { "Alias": "sent", "SpecialUse": "Sent" } ] } ] } } ``` **What goes in `Host`, `Port`, and `Secrets` depends on where the mailbox lives**, and so does whether IMAP has to be switched on first and whether a password is accepted at all. [Configuring a mailbox at your provider](users/mailbox-providers.md) has the address and the credential kind for the popular mail services, and what each one does differently once synchronization is running. Points worth knowing before you adapt it: - **`AccountId` and `Alias` are your names**, not the server's. They are what every tool argument, log line, and error message uses, so pick names you are happy to see in a diagnostic. - **`DisplayName` is the name an assistant reads back to you.** It is required and has no default: the identifier above is a key, and a person hearing "the email came from `acct-2`" learns nothing. It travels beside the identifier in every tool result, and either spelling narrows a listing, a search, or a question to that mailbox. No two accounts may share one, and none may take another account's identifier, so a name always names one mailbox. - **Folders are best named by role.** `SpecialUse` lets discovery find the folder whatever the server calls it — a German server's `Gesendet` is still `Sent` — and configuring no folder at all synchronizes the inbox. Naming an exact server path is the alternative for folders with no role, and you may name both: the path finds the folder and the role says what it is for, so a rule or a tool can go on asking for `role:Junk` on a server that advertises nothing. One role belongs to one folder per account. [What a role says, beside how a folder is found](features/imap-synchronization.md#what-a-role-says-beside-how-a-folder-is-found) states what naming a role buys you everywhere else. - **A folder left out of the list is a folder MailFathom does not have.** The list is not a filter over your mailbox; it is the whole of what this deployment knows about. A folder no entry names is never mirrored, never listed, searched, read, or answered from, never embedded, and never evaluated by a rule — and so is mail an earlier configuration had already stored under an entry you later removed, which stays in the database and stops being reachable. *Working out the list* below is how to arrive at one you meant. - **A mapped folder is mirrored, embedded, and readable by tools** unless you say otherwise. `Synchronize`, `GenerateEmbeddings`, and `VisibleToTools` each default to `true` on a folder entry, and switching one off is how a folder stays nameable while its mail stays out of the local copy, out of an embedding provider, or out of everything an assistant can read. Mail the folder had already stored is kept when you turn mirroring off, withheld from everything that reads a mailbox, so turning it back on later fetches what arrived meanwhile rather than the whole folder again. [What a mapping decides beyond where the folder is](features/imap-synchronization.md#what-a-mapping-decides-beyond-where-the-folder-is) states what each one costs, including what happens to mail already stored when you turn mirroring off. - **A folder you name is created only if you ask for it.** Add `CreateIfMissing: true` beside a `RemotePath` and MailFathom creates that folder on its first run, when your server has none at it — which is what a rule filing mail into an archive folder you decided on needs, without a detour through a mail client. A mapping that also carries `Synchronize: false` gets its folder the first time something files mail into it. The switch defaults to `false`, so leaving it out keeps a mistyped path reporting itself as an alias that resolves to nothing rather than becoming a folder named after the typo. Nothing else about your folders is ever changed: MailFathom never renames, deletes, or unsubscribes from one. [A folder the mapping asked for is created](features/imap-synchronization.md#a-folder-the-mapping-asked-for-is-created) states when it happens and what a server's refusal reports. - **The transport is TLS by default.** Port 993 with TLS-on-connect is the default posture, and every weakening — an unencrypted connection, clear-text authentication over one — must be stated explicitly and fails startup otherwise. A server with a private certificate authority is supported by trusting that authority, never by turning validation off; [transport security](features/imap-synchronization.md#transport-security) records the rules. - **An older server may be refused before any of this applies.** If synchronization reports an authentication failure wrapping `SSL Handshake failed with OpenSSL error`, the password is not the problem: the platform's own TLS policy ended the handshake before a credential was sent. [The platform TLS policy](operations/platform-tls-policy.md) covers how to confirm that and the one supported way to relax it. - **How far back to synchronize** is per account: `EarliestEmailReceivedDate` bounds the first synchronization of a large mailbox, and omitting it copies everything the server still holds. - **New mail arrives on the next run unless you ask for push.** The default reconciles each account every five minutes. Setting an account's `"Mode": "Push"` makes MailFathom hold an IMAP connection open and synchronize the moment the server reports a change. What that costs depends on the server: one connection for the whole account where it supports the `NOTIFY` extension, one per watched folder where it supports only `IDLE`, and nothing at all where it supports neither — which is polled instead and says so in the log. Push **adds** to the schedule rather than replacing it — the account still runs on its `Interval`, and a notification only starts the next run sooner. [Push synchronization](features/imap-synchronization.md#push-synchronization) records the whole model. ### Working out the list Nothing discovers folders into that list for you, so writing it is a step of its own. From an empty configuration: 1. **Look at what the server actually holds.** Any mail client you already use lists the account's folders, and the paths it shows are the paths `RemotePath` takes — including their separator, which is `/` on some servers and `.` on others. [Configuring a mailbox at your provider](users/mailbox-providers.md) names the folders the popular services create and which of them carry a role, so a Gmail account is a different starting list from a Fastmail one. 2. **Name by role whatever carries one.** `Inbox`, `Sent`, `Drafts`, `Junk`, `Trash`, `Archive`, `All`, `Flagged`, and `Important` are the roles a server can advertise; a mapping that names one finds the folder whatever it is called and keeps working when the localization or the path changes. Each role belongs to one folder per account. 3. **Name the rest by path.** A project folder, a mailing-list folder, a folder a filter on the server files into — none of these carries a role, so each takes a `RemotePath` and an alias of your own. 4. **Create the folder that is not there yet.** A folder you want a rule to file into — an archive of your own, say — does not have to be made in a mail client first: give the mapping its `RemotePath` and `CreateIfMissing: true`, and MailFathom creates it on the first run that resolves the alias. It is the only thing MailFathom ever changes about your mailbox's shape. 5. **Leave out what you do not want mirrored.** Leaving a folder out is the way to keep its mail out of MailFathom entirely, and it stays out until an entry names it. The account's `Folders` then grows from the two entries above into the list you meant, mixing the three kinds of entry: ```json { "Folders": [ { "Alias": "inbox", "SpecialUse": "Inbox" }, { "Alias": "sent", "SpecialUse": "Sent" }, { "Alias": "spam", "RemotePath": "INBOX.Spam", "SpecialUse": "Junk" }, { "Alias": "projects", "RemotePath": "INBOX.Work.Projects" }, { "Alias": "archive", "RemotePath": "INBOX.Archive", "SpecialUse": "Archive", "CreateIfMissing": true } ] } ``` Once the service is running, `list_accounts` reports one entry per folder this deployment mapped and lets tools read, with when each last synchronized — so a mistyped path shows up as a folder missing from that answer rather than as one holding no mail. The log names an alias that resolved to nothing or to more than one folder, in both cases naming the remedy. The Compose deployment reads this from `config/10-mailfathom.json`; Kubernetes mounts it as a ConfigMap key; a native process names the file through [`ConfigurationSources`](operations/configuration-sources.md). ## 3. Point it at the database The Compose deployment wires the connection and its password for you — skip this step there. Elsewhere, the connection string carries everything but the password, and the password joins it through a reference: ```json { "ConnectionStrings": { "mailfathom": "Host=db.example.test;Database=mailfathom;Username=mailfathom" }, "Persistence": { "Password": { "Name": "mailfathom-database-password", "SecretReference": "file:/etc/mailfathom/secrets/mailfathom-database-password" } } } ``` ## 4. Start it, and apply the schema Start the service the way the installation shape does. The first start against an empty database **fails on purpose**, naming the migration it expects: ```text The database has not applied 1 migration(s) this build defines: 20260731132336_Initial. ``` That is the explicit schema step described in [installing MailFathom](users/installation.md#what-every-shape-needs): MailFathom verifies the schema and refuses to serve against one it does not recognize, and applying migrations is a step you take, with a backup first once there is data to lose. What you apply is one SQL file. A release attaches it; from a checkout, generate it: ```bash scripts/build-schema-artifact.sh # artifacts/schema/mailfathom-schema-.sql ``` Read it, apply it with any PostgreSQL client, and start again — the refusal is gone. The role that applies it needs more privilege than the one MailFathom connects as, which [applying the database schema](operations/database-schema.md) explains along with the grants that leaves behind. ## 5. Verify it is healthy ```bash curl -fsS http://127.0.0.1:8081/started curl -fsS http://127.0.0.1:8081/health ``` `/started` confirms the startup gates passed — every secret reference resolved, the schema matched. `/health` is readiness and includes the database. Both answer on the probe listener, `8081` by default, never on the application port; [health endpoints](operations/health-endpoints.md) explains that separation. Then let the first synchronization run. Its progress is visible in the log — each folder run reports what it stored — and, once you can call a tool, in the `folderFreshness` every result carries. A large mailbox takes a while on the first pass; later runs move only what changed, every five minutes by default. ## 6. Enable the MCP endpoint The endpoint is off by default, and enabling it means stating how it is authenticated: ```json { "McpEndpoint": { "Enabled": true, "Authentication": [ { "ApiKey": { "Name": "workstation", "SecretReference": "file:/etc/mailfathom/secrets/mcp-workstation-key" } } ] } } ``` An empty `Authentication` list is legal — the reverse-proxy-and-loopback deployment is an ordinary one — but it is announced with a startup warning, because an unauthenticated endpoint serves your mailbox to whoever can reach its port. Read [the MCP endpoint](operations/mcp-endpoint.md) before widening anything: it records the OAuth alternative, browser origins, serving your own domain over TLS, client certificates, and the rate limits that apply out of the box. MailFathom itself serves plain HTTP unless its own TLS termination is configured, so keep the application port on loopback or behind a TLS-terminating proxy — the Compose deployment's default — and give the proxy the certificate. If you put a proxy in front, name it in `ReverseProxy:TrustedProxies` as well. The public scheme and host survive the hop either way; naming the proxy is what stops anything else that can reach the port from claiming them: [behind a TLS-terminating reverse proxy](operations/mcp-endpoint.md#behind-a-tls-terminating-reverse-proxy). ## 7. Connect an MCP client The endpoint speaks the MCP **Streamable HTTP** transport at `/mcp` — the path is fixed — and the key travels as a bearer credential. Any client that supports Streamable HTTP connects with two facts: | The client asks for | The value | | --- | --- | | Server URL | `http://127.0.0.1:8080/mcp` under the Compose defaults; your proxy's HTTPS address otherwise | | Header | `Authorization: Bearer ` | **Where a client asks for those two values, and whether it will accept the second at all, differs per client.** Two of the popular chat clients offer no field for a static header, which decides the deployment's authentication rather than only the setup steps; [connecting the chat client you already use](users/mcp-clients.md) has the steps, the address kind, and the authentication shapes for each one by name. A connected client's tool listing should show at least four tools — `list_accounts`, `list_emails`, `get_email_content`, `search_emails` — each advertising itself as read-only, non-destructive, and idempotent. A fifth, `ask_mail`, appears only once you have configured a chat model and an embedding model and both are working; until then its absence is the deployment telling you it cannot answer questions yet rather than a fault. [Verifying an enabled endpoint](operations/mcp-endpoint.md#verifying-an-enabled-endpoint) is the checklist form of this, including what the refusals look like when the key or the origin is wrong. **Signing a person in through your own identity provider instead of sharing a key** is the other way to connect, and it is the only way for a client whose dialog takes no header. Most of that work is in the provider rather than here: [MCP client OAuth](operations/mcp-client-oauth.md) walks it end to end from a deployment in exactly this state. ## 8. Make the first call, and read it correctly Ask the connected agent to list recent mail, or have the client call `list_emails` with a small page. Two parts of the result matter more than the emails on the first day: - **`folderFreshness`** carries one entry per folder in scope, stating when synchronization last committed progress there. An empty page whose entries report `wasSynchronized: false` means the folder has not synchronized yet — not that the mailbox is empty. This is the field to check before trusting any early result. - **`nextCursor`** is how the rest of the timeline is read: pass it back unchanged with the same filters. Results are deliberately bounded — at most 100 summaries per page — so an agent reads pages, not mailboxes. From here, [using the tools](users/usage.md) describes the day-to-day surface: what each tool answers, what it bounds, and what its errors mean, and [administering your deployment](users/administering.md) covers reaching the running service from your own machine with the `mfctl` command. # Configuring a mailbox at your provider > [!WARNING] > Some of the steps on this page are performed in a product this project does not control. Any screen, menu, or field > named here can be renamed or moved there at any time. Where this page and that product's own documentation disagree, > the product's documentation is right. [Getting started § configure the mailbox](users/getting-started.md#2-configure-the-mailbox) shows the account block and what each of its keys is for. This page answers the question that comes next and that no reference page can answer, because the answer belongs to somebody else: **what goes in `Host`, `Port`, `Secrets`, and `TransportSecurity` for the mail service I actually use, and what does that service do differently once synchronization is running.** Every mail service named below is reached the same way — MailFathom speaks IMAP over TLS and nothing else, and there is no per-provider code path anywhere in it. Four things differ, and a reader has to get each right before the first connection succeeds: the address, whether IMAP has to be switched on first, which credential the service will accept, and what the service's own behaviour does to a mailbox once mail starts arriving in the local copy. ## Two claims this page does not make **Presence is a check at a point in time, not a supported-provider list.** Every entry below is what that service's own current documentation said on the date the entry carries. None of these services is under this project's control, and any of them may change the answer next week without anybody here touching anything. An entry that has stopped being true is a defect in this page rather than in the deployment that trusted it. **Absence is not a refusal.** A service missing from this page is not blocked, unsupported, or known to fail — it is unchecked. MailFathom reaches any IMAP server that will serve a TLS connection and an authentication mechanism the account permits, and [an IMAP server you run yourself](#an-imap-server-you-run-yourself) is the section for one. ## What the evidence column means Each entry says which of two kinds of evidence it rests on, because they are not the same claim and blurring them is how a guide starts lying: - **Documented** — the service's own current documentation was read on the date the entry carries. It establishes what the service publishes about its address, its credential, and its own behaviour. It does not establish that a connection was made. - **Observed** — a MailFathom deployment ran against a mailbox at that service and the behaviour was what the entry says. **Every entry on this page today is `Documented`.** That is deliberate rather than a gap waiting to be filled: a third party's mail server is not something this repository verifies, and an entry claiming otherwise would be asserting a test that nothing here runs. Where a reader needs a behaviour confirmed against their own mailbox, [what your own server actually offers](#what-your-own-server-actually-offers) is how the running deployment answers it. ## The account block, written once This is the complete shape, and it is shown once. Every section below changes three parts of it at most — the address, the credential block, and, in one case, `TransportSecurity`: ```json { "MailSynchronization": { "Enabled": true, "Accounts": [ { "AccountId": "primary", "DisplayName": "Personal mail", "Host": "imap.example.test", "Port": 993, "UserName": "you@example.test", "Secrets": { "Password": { "Name": "imap-primary-password", "SecretReference": "file:/etc/mailfathom/secrets/imap-primary-password" } }, "Folders": [ { "Alias": "inbox", "SpecialUse": "Inbox" }, { "Alias": "sent", "SpecialUse": "Sent" } ] } ] } } ``` A service that will not accept a password takes an `OAuth` block in place of `Secrets` instead; [mailbox OAuth § configuring the account](operations/mailbox-oauth.md#configuring-the-account) holds that form in full, and no section below repeats it. [Configuration reference § `MailSynchronization`](operations/configuration-reference.md#mailsynchronization) is the inventory of every key named on this page, with its type, default, and constraint. ## What is the same everywhere Four things hold whichever service a mailbox lives at, and each is stated in full on the page that owns it rather than per provider here: - **The transport is TLS on connect, on port 993, by default.** Every weakening — an unencrypted connection, clear-text authentication over one — is an explicit opt-in that fails startup otherwise, and certificate validation cannot be turned off at all. A server with a private certificate authority is reached by trusting that authority. [IMAP synchronization § transport security](features/imap-synchronization.md#transport-security) records the rules. - **Folders are named by role rather than by path.** `SpecialUse` lets discovery find a folder whatever the server calls it, which matters more here than it looks: the same role carries a different path at almost every service below. A service that advertises no role for a folder loses none of this — name the path and the role together, and everything that asks for `role:Junk` still reaches it. [Folder aliases and discovery](features/imap-synchronization.md#folder-aliases-and-discovery) covers the matching, [what a role says](features/imap-synchronization.md#what-a-role-says-beside-how-a-folder-is-found) the rest, and [the folders, and what each service calls them](#the-folders-and-what-each-service-calls-them) what you are naming. - **Reading never marks mail read.** MailFathom does not set the remote `\Seen` flag while synchronizing, reconciling, fetching content, or answering a tool call — the sessions those run on hold no operation capable of writing a flag. If mail is turning up read at your service, it is another client or a rule at the service, not this one. [Marking mail read is an act, never a side effect of reading](features/imap-synchronization.md#marking-mail-read-is-an-act-never-a-side-effect-of-reading) states the guarantee and its one deliberate exception. - **Whether `"Mode": "Push"` does anything is the server's answer, not a setting.** Push needs the `IDLE` extension, and one connection for the whole account instead of one per folder additionally needs `NOTIFY`. A server offering neither is polled on the account's interval and says so in the log. [Push synchronization](features/imap-synchronization.md#push-synchronization) holds the whole model. ## The addresses, in one table | Service | `Host` | `Port` | Switch IMAP on first | Credential the service will accept | Evidence | | --- | --- | --- | --- | --- | --- | | [Gmail, on a personal account](#gmail-on-a-personal-account) | `imap.gmail.com` | `993` | No — always on | App password, or OAuth | Documented, 2026-08-12 | | [Gmail, on a Google Workspace account](#gmail-on-a-google-workspace-account) | `imap.gmail.com` | `993` | Yes, by an administrator | **OAuth only** | Documented, 2026-08-12 | | [Outlook.com](#outlookcom) | `outlook.office365.com` | `993` | Yes, in the mailbox settings | **OAuth only** | Documented, 2026-08-12 | | [Exchange Online and Microsoft 365](#exchange-online-and-microsoft-365) | `outlook.office365.com` | `993` | No — on unless an administrator turned it off | **OAuth only** | Documented, 2026-08-12 | | [Yahoo Mail](#yahoo-mail) | `imap.mail.yahoo.com` | `993` | Not documented as required | App password | Documented, 2026-08-12 | | [iCloud Mail](#icloud-mail) | `imap.mail.me.com` | `993` | Not documented as required | App-specific password | Documented, 2026-08-12 | | [Proton Mail](#proton-mail-through-the-local-bridge) | `127.0.0.1` (the local bridge) | `1143` | The bridge is installed and signed in instead | The bridge's own generated password | Documented, 2026-08-12 | | [Fastmail](#fastmail) | `imap.fastmail.com` | `993` | Not documented as required | App password | Documented, 2026-08-12 | | [Zoho Mail](#zoho-mail) | `imap.zoho.com` or `imappro.zoho.com` | `993` | Yes, in webmail | Account password, or an application-specific one | Documented, 2026-08-12 | Proton Mail is the one row whose address is not the mail service and whose port is not 993, for the reason its section gives. Everything else takes the account block above with the address and the credential replaced. *Not documented as required* is the honest answer rather than a *no*: the service's own setup documentation names no switch, and this review made no connection that would establish one. ## The folders, and what each service calls them **MailFathom mirrors the folders your configuration names and nothing else.** Nothing discovers a folder into that list, so a folder left out of it is a folder this deployment does not have — not one it holds and hides. [Getting started § working out the list](users/getting-started.md#working-out-the-list) is how to arrive at one; this table is the part of it that depends on where the mailbox lives. | Service | What its own folders are called | Roles the server advertises | Evidence | | --- | --- | --- | --- | | Gmail, personal and Workspace alike | `INBOX`, and the system labels under a `[Gmail]` prefix: `[Gmail]/All Mail`, `[Gmail]/Drafts`, `[Gmail]/Important`, `[Gmail]/Sent Mail`, `[Gmail]/Spam`, `[Gmail]/Starred`, `[Gmail]/Trash` | Documented: `\All`, `\Drafts`, `\Important`, `\Sent`, `\Junk`, `\Flagged`, `\Trash` | Documented, 2026-08-12 | | Outlook.com, Exchange Online, Microsoft 365 | Inbox, Drafts, Sent Items, Deleted Items, Junk Email, Archive | Not documented in the pages this review read | Documented, 2026-08-12 | | Yahoo Mail | Inbox, Draft, Sent, Archive, Spam, Trash | Not documented in the pages this review read | Documented, 2026-08-12 | | iCloud Mail | Inbox, VIP, Drafts, Sent, Archive, Trash (Bin in some regions), Junk | Not documented in the pages this review read | Documented, 2026-08-12 | | Proton Mail, through the bridge | All Mail, Inbox, Drafts, Sent, Starred, Archive, Spam, Trash, and each of your labels as a folder under `Labels` | Not documented in the pages this review read | Documented, 2026-08-12 | | Fastmail | Inbox, Archive, Drafts, Sent, Spam, Trash | Not documented in the pages this review read | Documented, 2026-08-12 | | Zoho Mail | Inbox, Drafts, Sent, Spam, Trash | Not documented in the pages this review read | Documented, 2026-08-12 | **A name in that middle column is what the service calls the folder, not necessarily the IMAP path.** A service may serve its folders under a prefix, translate them into the account's language, or rewrite a character its own interface allows — Gmail's prefix and Fastmail's replacement of `.` in a custom folder name are both documented behaviours. That is the argument for naming a folder by its role wherever it has one: `SpecialUse` finds the folder whatever path it is at, and the configuration survives a rename, a language change, and a move to another service. **"Not documented" is not "not advertised".** Only Google publishes which RFC 6154 attributes its server sends, so the other rows say what this review could establish rather than what the server does. Where a role matters and the service documents no attribute, name the folder's path and its role together — `{ "Alias": "spam", "RemotePath": "Junk Email", "SpecialUse": "Junk" }` — and everything that asks for `role:Junk` reaches it regardless of what the server advertised. A mapping whose role finds nothing reports the alias as unresolved in the log rather than failing the account. Sources: [Gmail IMAP extensions](https://developers.google.com/workspace/gmail/imap/imap-extensions), [Working with message folders in Outlook.com](https://support.microsoft.com/en-US/Outlook/working-with-message-folders-in-outlook-com), [Archive and un-archive messages in Yahoo Mail](https://help.yahoo.com/kb/SLN26466.html), [Organize email with folders in Mail on iCloud.com](https://support.apple.com/guide/icloud/mm6b1a6730/icloud), [Labels in Bridge](https://proton.me/support/labels-in-bridge), [Setting up and using folders](https://www.fastmail.help/hc/en-us/articles/1500000280301-Setting-up-and-using-folders), [Using folders](https://www.zoho.com/mail/help/using-folders.html). ## Gmail, on a personal account Google documents `imap.gmail.com` on port `993` with SSL required, and states that IMAP access is always on and no longer has a setting to switch — so nothing has to be enabled first on a personal account. **The credential.** An app password works with the ordinary `Secrets:Password` block and is the shorter path; OAuth is the alternative and is the same setup a Google Workspace mailbox needs. [Mailbox OAuth § do you need this at all](operations/mailbox-oauth.md#do-you-need-this-at-all) is where the choice is already stated in full — which accounts can be issued an app password at all, and the review obligation that comes with registering your own Google project for mail access — and it is not repeated here. **Labels arrive as folders, and one message can be in several of them.** Google publishes labels over IMAP as folders, with its own labels under a `[Gmail]` prefix, and exposes each message's label set through the `X-GM-LABELS` attribute. `[Gmail]/All Mail` holds everything, so a message carrying two labels is reachable through both label folders and through All Mail as well. Configure the folders you want by role — an inbox, a sent folder — rather than synchronizing All Mail beside them, or the same message is copied under several aliases and counted several times in every listing. **Two limits worth knowing before the first synchronization of a large mailbox.** Google documents a daily IMAP ceiling of 2500 MB downloaded and 500 MB uploaded — synchronization only reads, so the download figure is the one a first pass over a large mailbox can reach — and describes a safeguard that suspends the account for about an hour, and for as long as a day, once a limit is hit. It separately documents that an account may be added to at most 15 mail clients at once, which a deployment watching many folders in push mode can approach on its own. `EarliestEmailReceivedDate` is what bounds the first pass against the first limit, and `MaxSubscribedFolders` bounds the connection count against the second. Sources: [Add Gmail to another email client](https://support.google.com/mail/answer/7126229), [IMAP, POP, and SMTP](https://developers.google.com/workspace/gmail/imap/imap-smtp), [IMAP extensions](https://developers.google.com/workspace/gmail/imap/imap-extensions), [Gmail bandwidth limits](https://support.google.com/a/answer/1071518). ## Gmail, on a Google Workspace account The address is the same as above. Two things are not. **An administrator turns IMAP on first.** Google documents the setting in the admin console under **Apps → Google Workspace → Gmail → End User Access → POP and IMAP access**, applied to the whole organization or to one organizational unit. The same setting optionally restricts access to named OAuth client identifiers, which is worth checking if the first connection is refused after the switch is on. **A password will not authenticate.** Google states that as of 1 May 2025 a Workspace account no longer accepts a sign-in from an application using a username and password, and an app password cannot be issued for one at all. The account therefore carries an `OAuth` block and no `Secrets:Password`, with `PermittedAuthenticationMechanisms` set to the token-bearing mechanisms — which is what frees the account from configuring a password. [Mailbox OAuth § Google](operations/mailbox-oauth.md#google) is the registration, and [§ configuring the account](operations/mailbox-oauth.md#configuring-the-account) is the block. The label and All Mail behaviour and both limits are Gmail's and apply here unchanged. Sources: [Turn POP & IMAP on or off for users](https://support.google.com/a/answer/105694), [Add Gmail to another email client](https://support.google.com/mail/answer/7126229). ## Outlook.com Microsoft documents `outlook.office365.com` on port `993` with SSL/TLS, and lists the authentication method for it as OAuth2 and modern authentication rather than a password. **IMAP is switched on in the mailbox.** Microsoft's own settings page states that POP and IMAP access is off by default and is enabled under **Settings → Mail → Forwarding and IMAP**. **A password will not authenticate, and neither will an app password.** Microsoft states that basic authentication stopped working for these accounts on 16 September 2024 and that devices using POP or IMAP can no longer use app passwords. So an Outlook.com mailbox takes the `OAuth` block, registered through Microsoft Entra exactly as a Microsoft 365 mailbox is; [mailbox OAuth § Microsoft — Entra](operations/mailbox-oauth.md#microsoft--entra) is the registration. The delegated permission is the one a personal mailbox owner can consent to themselves. Sources: [POP, IMAP, and SMTP settings for Outlook.com](https://support.microsoft.com/en-us/office/pop-imap-and-smtp-settings-for-outlook-com-d088b986-291d-42b8-9564-9c414e2aa040), [Modern authentication methods now needed to continue syncing Outlook email in non-Microsoft email apps](https://support.microsoft.com/en-us/support/known-issues/modern-authentication-methods-now-needed-to-continue-syncing-outlook-email-in-non-microsoft-email-ap). ## Exchange Online and Microsoft 365 The address and port are the same as Outlook.com's, and so is the answer about passwords — but for a different reason and with a different consequence, which is why this is a section of its own. **Basic authentication was removed rather than discouraged.** Microsoft states that it removed the ability to use basic authentication for IMAP in Exchange Online, that no one — customer or Microsoft support — can re-enable it, and that the same change prevents the use of app passwords. OAuth 2.0 is the only way in, and Microsoft's own guidance to application developers is to keep the protocol and implement it. **Both grants are available here, and they are different mailboxes' answers.** A delegated registration carrying `IMAP.AccessAsUser.All` acts for one mailbox owner and needs a refresh token obtained once; an app-only registration carrying `IMAP.AccessAsApp` acts for the application, needs tenant administrator consent, and uses the `client_credentials` grant with no sign-in at all. [Mailbox OAuth § the two grants](operations/mailbox-oauth.md#the-two-grants) states which applies, and [§ Microsoft — Entra](operations/mailbox-oauth.md#microsoft--entra) is the registration for both. **IMAP is on by default per mailbox, and an administrator can turn it off.** Microsoft documents IMAP4 as enabled when a user mailbox is created, and both the Exchange admin center and `Set-CASMailbox` as the ways to change it — so a refused connection against a tenant that has hardened its protocols is a mailbox setting rather than a credential fault. Sources: [Deprecation of basic authentication in Exchange Online](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/deprecation-of-basic-authentication-exchange-online), [Managing email apps for user mailboxes](https://learn.microsoft.com/en-us/exchange/recipients-in-exchange-online/manage-user-mailboxes/managing-email-apps-for-user-mailboxes), [Authenticate an IMAP, POP, or SMTP connection using OAuth](https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth). ## Yahoo Mail Yahoo documents `imap.mail.yahoo.com` on port `993` with SSL required, and directs a third-party mail client at a generated app password rather than the account password. That is the ordinary `Secrets:Password` block with the generated value behind the reference, and nothing else about the account changes. This review found no Yahoo documentation of IMAP connection caps, bandwidth ceilings, or the extension set the server advertises, so this page states none — [what your own server actually offers](#what-your-own-server-actually-offers) is where a running deployment answers that instead. Sources: [Yahoo Mail server settings](https://help.yahoo.com/kb/SLN4075.html). ## iCloud Mail Apple documents `imap.mail.me.com` on port `993` with SSL required, and an app-specific password rather than the account password. **The user name is the part that catches people out.** Apple documents it as usually the name portion of the address — `johnappleseed` rather than `johnappleseed@icloud.com` — and says to try the full address if a client cannot connect with the short form. `UserName` is an identifier rather than a secret, so trying the other form is an edit and a reload rather than a re-provisioning. Sources: [iCloud Mail server settings for other email client apps](https://support.apple.com/en-us/102525). ## Proton Mail, through the local bridge This is the one entry whose address is not the mail service. Proton documents that a mail client reaches a Proton mailbox through Proton Mail Bridge, an application that runs on the same machine as the client, holds the connection to Proton itself, and serves IMAP on a loopback address — which is what lets a client see decrypted mail without the decryption happening anywhere else. Proton states that the bridge is available only with a paid Proton Mail plan. Three consequences for a MailFathom account, and the first is the one to settle before writing any configuration: - **The bridge has to be reachable from the process, on loopback.** A deployment in a container or on another host does not reach a bridge running on somebody's laptop, and the bridge is not a service to publish onto a network — its whole design is that the decrypted channel never leaves the device. A Proton mailbox therefore fits a MailFathom process running on the same machine as the bridge, and does not fit the container and Kubernetes shapes without putting the two together deliberately. - **The port and the transport are not the defaults.** Proton documents the bridge's default IMAP port as `1143` and offers a choice of STARTTLS or SSL in the bridge's own settings. STARTTLS on 1143 means the account states `"ConnectionSecurity": "StartTlsRequired"` — which is one of the two guaranteed-TLS modes and needs no `AllowInsecureConnection`, because the handshake either happens or the connection fails. - **The certificate is the bridge's own.** Proton documents the bridge as using a self-signed TLS certificate that it generates when the application is first set up. Certificate validation is never disabled in MailFathom, so the account trusts that certificate as an additional authority: `"CertificateTrust": "AdditionalTrustedAuthority"` with the bridge's certificate behind `TrustedCertificateAuthority`. [Trust anchor material](features/imap-synchronization.md#trust-anchor-material) is what that reference holds and how it is provisioned. The credential is the password the bridge itself generates for the mail client, with the Proton address as `UserName`. It goes behind a `Secrets:Password` reference like any other. ```json { "Host": "127.0.0.1", "Port": 1143, "UserName": "you@proton.me", "TransportSecurity": { "ConnectionSecurity": "StartTlsRequired", "CertificateTrust": "AdditionalTrustedAuthority", "TrustedCertificateAuthority": { "Name": "proton-bridge-certificate", "SecretReference": "file:/etc/mailfathom/secrets/proton-bridge-certificate.pem" } } } ``` Sources: [IMAP, SMTP, and POP3 setup](https://proton.me/support/imap-smtp-and-pop3-setup), [Comprehensive guide to Bridge settings](https://proton.me/support/comprehensive-guide-to-bridge-settings), [Proton Mail Bridge connection issues](https://proton.me/support/bridge-ssl-connection-issue). ## Fastmail Fastmail documents `imap.fastmail.com` on port `993` with SSL/TLS encryption and states explicitly that STARTTLS is not offered there — which is what MailFathom's default `TlsOnConnect` already is, so the account needs no `TransportSecurity` block at all. **The user name is the full address, and it is the one the account was signed up with.** Fastmail documents that other addresses on the same account do not authenticate. **An app password is required.** Fastmail states that the ordinary account password will not connect over IMAP and that each connection needs an app password of its own. Sources: [Server names and ports](https://www.fastmail.help/hc/en-us/articles/1500000278342-Server-names-and-ports), [IMAP, POP, and SMTP](https://www.fastmail.help/hc/en-us/articles/1500000279921-IMAP-POP-and-SMTP). ## Zoho Mail Zoho documents two hostnames on port `993` with SSL required, and which one applies is the account's plan rather than a preference: `imap.zoho.com` for a personal account on a `zoho.com` address, and `imappro.zoho.com` for a paid organization account on its own domain. Zoho runs mailboxes in several data centres and the documented hostnames are the ones for its `.com` domain, so an account elsewhere takes the host its own webmail settings page names rather than the one in the table above. **IMAP is switched on in webmail first.** Zoho documents enabling IMAP access for the account before configuring any client. **Which credential depends on how the account signs in.** Zoho documents the account password for an ordinary account, and an application-specific password where two-factor authentication is on, where the account signs in through SAML, or where it uses a federated sign-in. Either way it is the `Secrets:Password` block. Sources: [IMAP and SMTP configuration details](https://www.zoho.com/mail/help/imap-access.html). ## An IMAP server you run yourself There is no entry to look up here, and that is the point: MailFathom reaches any IMAP server that will serve a TLS connection and an authentication mechanism the account permits, so the server's own documentation is the source and the account block at the top of this page is the whole configuration. Four things are worth checking against it, because each is a startup refusal rather than a runtime warning: - **The port decides nothing; the mode does.** `993` with `TlsOnConnect` is the default. A server serving IMAP on `143` and upgrading takes `"ConnectionSecurity": "StartTlsRequired"`, which is still guaranteed TLS. `StartTlsWhenAvailable` and `None` are not, and each needs `AllowInsecureConnection` written explicitly. - **A private certificate authority is trusted rather than ignored.** `"CertificateTrust": "AdditionalTrustedAuthority"` with the authority behind `TrustedCertificateAuthority`; validation itself cannot be turned off. - **An old server may be refused before a credential is ever sent.** An authentication failure wrapping `SSL Handshake failed with OpenSSL error` is the platform's TLS policy ending the handshake rather than a wrong password. [The platform TLS policy](operations/platform-tls-policy.md) covers confirming that and the one supported way to relax it. - **The mechanism list is an allow-list.** `PermittedAuthenticationMechanisms` defaults to `PLAIN` and `LOGIN`; a server offering something else needs it named. ## What your own server actually offers Nothing above states which IMAP extensions a service advertises, because almost none of them publishes that and a table of guesses would be worse than no table. The running deployment answers it instead, per folder, on every start: ```text Folder primary/inbox is now synchronized in Push mode. ``` Three neighbouring lines say why, when the answer is not the one that was configured: | What the log says | What the server advertised | | --- | --- | | `Account … watches N folders through one push subscription.` | `NOTIFY` and `IDLE`, so the whole account costs one connection | | `… advertises no NOTIFY capability, so each push folder is watched over its own connection` | `IDLE` alone, so each watched folder costs one | | `… the mail server advertises no IDLE capability; it is synchronized by polling` | Neither, so `Mode` changes nothing and the interval is what runs | The same holds for `CONDSTORE` and `QRESYNC`, which decide how much work a reconciliation pass does rather than whether it is correct: all three shapes of the question reach the same end state, and [asking only about what changed](features/imap-synchronization.md#asking-only-about-what-changed) records which command each server gets. There is nothing to configure and nothing to check — a server that supports neither is slower and no less right. ## Related - [Getting started](users/getting-started.md) — the whole path from an installed instance to a first tool call - [Mailbox OAuth](operations/mailbox-oauth.md) — registering an application, obtaining a refresh token, and the `OAuth` block every token-bearing account above takes - [Configuration reference § `MailSynchronization`](operations/configuration-reference.md#mailsynchronization) — every key named here, with its constraint and whether changing it needs a restart - [IMAP synchronization](features/imap-synchronization.md) — what a run actually does with a mailbox - [Secret provisioning](operations/secret-provisioning.md) — how a `SecretReference` is backed by material --- **Trademarks.** The product, service, and company names on this page are their owners' trademarks and are used solely to identify the mail services a MailFathom deployment can be configured against. Their use implies no affiliation with, sponsorship by, endorsement by, or certification from those owners, in either direction, and this page reproduces no third-party logo. Gmail and Google Workspace are trademarks of Google LLC. Microsoft, Outlook, Microsoft 365, Exchange Online, and Microsoft Entra ID are trademarks of the Microsoft group of companies. iCloud is a trademark of Apple Inc., registered in the U.S. and other countries and regions. Yahoo and Yahoo Mail are trademarks of Yahoo Inc. and its affiliate companies. Proton and Proton Mail are trademarks of Proton AG. Fastmail is a trademark and service mark of Fastmail Pty Ltd. Zoho and Zoho Mail are trademarks of Zoho Corporation Private Limited and/or its affiliates. [`THIRD_PARTY_LICENSES.md`](https://github.com/Krzysztof318/MailFathom/blob/main/THIRD_PARTY_LICENSES.md#trademark-and-brand-use) records the per-owner review this statement comes out of, and why it sits here rather than in `NOTICE`. # Administering your deployment MailFathom ships a command, `mfctl`, that talks to a running deployment from your own machine. This page is the user's view of it: what it is for, the path from a running service to a verified sign-in, and what it deliberately cannot do yet. The full contract — every asset name, every stored path, every message the command can print — is [administering a deployment](operations/admin-endpoint.md), and this page links there rather than repeating it. ## What the command is, and is not `mfctl` is a client. Every operation it performs is an HTTP request to the deployment's administrative endpoint, so it runs on the machine you administer *from* rather than the one the service runs on — your laptop against a container on a server, or against a pod in a cluster. It is **not** how MailFathom is configured. Configuration is files and environment variables read at startup, described in [configuration sources](operations/configuration-sources.md); the command never reads them, never opens the database, and never touches the secret store. Nothing you do with it changes what the service will do on its next restart. > **One thing it does changes a running deployment: it can place a mailbox credential.** Everything else below verifies > who you are and keeps several deployments straight. Operational commands — inspecting synchronization, triggering > work, reading accounts — are not there yet. > > The exception is [authorizing a mailbox](#authorizing-a-mailbox), which signs you in to a *mail provider* and can then > hand the resulting credential to your deployment to keep. ## Before it can answer The administrative endpoint is off unless a deployment turns it on, and it has credentials of its own. An API key that works against the MCP endpoint authenticates nothing here, deliberately: reading a mailbox and administering the service that reads it are different authorities. ```jsonc { "AdminEndpoint": { "Enabled": true, "BindAddress": "127.0.0.1", "Port": 8090, "Authentication": [ { "ApiKey": { "Name": "workstation", "SecretReference": "systemd-credential:admin-workstation-key" } } ] } } ``` Five things about that block are worth understanding before you copy it: - **It binds a socket of its own.** `127.0.0.1` above is the safe starting point — reachable from the machine the service runs on and nowhere else, which is what an SSH tunnel is for. Publishing it more widely is a decision, not a default. The keys are in the [configuration reference](operations/configuration-reference.md#adminendpoint), and how it relates to the port your MCP clients use is [where each surface is served](operations/configuration-reference.md#where-each-surface-is-served). - **`SecretReference` is a pointer, not a secret.** Where the material actually lives, and how it gets there, is [secret provisioning](operations/secret-provisioning.md). Never write a key into a configuration file. - **A clear-text endpoint is warned about at startup, not refused.** It is the right posture behind a TLS-terminating proxy or on a loopback bind, and the wrong one anywhere else; only you know which you have. Configure `AdminEndpoint:Https:Endpoints` to have MailFathom terminate TLS itself. - **It is rate limited without your writing a number.** An endpoint that answers a network has to bound how fast a caller may present wrong credentials, so the limits apply the moment you enable it and the applied numbers are stated at startup. [Rate limiting](operations/admin-endpoint.md#rate-limiting) is where the settings and the one way this endpoint's limit differs from the MCP endpoint's are recorded. - **A request that runs too long is abandoned, also without your writing a number.** The ceiling defaults to ten minutes because the MCP endpoint shares the setting and an AI-backed answer can legitimately take minutes; no administrative route reaches a provider, so this is the endpoint worth narrowing. [Request timeouts](operations/admin-endpoint.md#request-timeouts) is where that is recorded. ## Getting the command Every release attaches a self-contained binary per platform, with nothing to install beside it — the .NET runtime is inside the file. The asset names and the checksum that tells a genuine download from a tampered one are on [getting the command](operations/admin-endpoint.md#getting-the-command), and on Linux [the install script](operations/admin-endpoint.md#on-linux-with-the-install-script) does the whole of it in one line. Download the one for the machine you administer *from*. The command talks to a deployment over HTTP, so it does not have to run where the service runs — that is the whole point of it being a client. **Take it from the release your deployment is running.** The command and the deployment have to agree on `major.minor`, because a minor release is allowed to change what they say to each other; a pair that does not agree is refused rather than attempted, and a pair that differs only in patch or nightly warns and carries on. [Take the command from the deployment's own release line](operations/admin-endpoint.md#take-the-command-from-the-deployments-own-release-line) is the rule in full. ## Signing in ```console $ mfctl login --endpoint https://mail.example.test:8443 --name production Administrative credential (an API key, or an access token from the configured authorization server): Signed in to https://mail.example.test:8443 as 'workstation' (MailFathom 0.2.0), saved as profile 'production' and selected. ``` The credential is typed at the prompt or piped in, never passed as an argument — an argument reaches your shell history and the process list. It is checked against the deployment before anything is written, so a wrong key, a wrong port, and a host that is not MailFathom all fail here rather than at some later command that leaves you guessing which of the three it was. If the deployment is configured for OAuth, sign in with a browser instead and let it do the authenticating: ```console $ mfctl login --endpoint https://mail.example.test:8443 --mode interactive --client-id mfctl ``` `--mode device` is the same thing on a machine with no browser: it prints a short code to enter on your phone. Either way the only thing you supply is the client identifier — the command asks the deployment where to authorize. Your access token is then renewed for you until the sign-in genuinely ends, and how long that is depends on your identity platform; [how long an OAuth sign-in lasts](operations/admin-endpoint.md#how-long-an-oauth-sign-in-lasts) states the rule and the one setting that shortens it. For a scheduled job there is a third way, and it is the one to prefer there. Generate a key pair, give the deployment the public half only, and sign in with the private one: ```console $ mfctl login --endpoint https://mail.example.test:8443 --mode keypair --private-key ~/.config/MailFathom/production.key ``` Nothing reusable is stored and nothing reusable reaches the deployment: the command signs a fresh credential per request, each good for about a minute, and the only thing the service holds is a public key. [Signing in with a key pair](operations/admin-endpoint.md#with-a-key-pair) has the `openssl` commands and the entry to add. If your deployment serves a certificate your workstation does not trust — self-signed, or issued by an authority only your organization carries — the sign-in shows you that certificate and asks once whether to trust it, the way an SSH client asks about a host key: ```console $ mfctl login --endpoint https://mail.internal.example:8443 --name internal … Fingerprint: 3B:9A:1C:…:7F Trust this certificate for this profile? [y/N]: ``` Compare the fingerprint against the deployment's own before you answer; nothing has been sent yet. Saying yes stores that fingerprint on the profile, which makes the profile **stricter** rather than looser: from then on it accepts that one certificate and refuses every other, so a renewal or a substitution stops the profile rather than passing unnoticed. You accept a renewed certificate by signing in again. An `http://` address gets a question of its own, because the credential and every later request would cross the network in clear text and a redirect to `https://` arrives too late to change that. [When the connection is weaker than the default](operations/admin-endpoint.md#when-the-connection-is-weaker-than-the-default) has both questions in full, and the two switches a scripted sign-in states the answers with. What is stored afterwards is one small file per user, with the tokens encrypted and the key beside it — and for a key-pair profile, no credential at all; [where the credential is kept](operations/admin-endpoint.md#where-the-credential-is-kept) states the paths, what the encryption does protect, and what it does not. ## Everyday use | What you want | Command | | --- | --- | | See which deployments you are signed in to | `mfctl profiles` | | Work against a different one from now on | `mfctl switch staging` | | Work against one just this once | any command with `--endpoint staging` | | Check whether your credential still works | `mfctl status` | | Forget a deployment on this machine | `mfctl logout` | Two of those repay a second look. `status` asks the *deployment*, which is what tells a revoked or expired key apart from a host that is simply down — the stored profile can only say what was true when you signed in. And `logout` forgets a local profile without revoking anything: the credential keeps working until the deployment stops accepting it, so a lost laptop is a reason to rotate the key on the server rather than to sign out. When you work against one deployment for a whole session, `MAILFATHOM_ENDPOINT` states it once for the shell. `--endpoint` beats it, and both beat the profile you last switched to. Every command exits `0` when it did what you asked and `1` when it did not, having explained itself on standard error first. [The troubleshooting table](operations/admin-endpoint.md#troubleshooting) reads each message back to you as a cause. ## Authorizing a mailbox A mailbox at a provider that no longer accepts a password — a Google Workspace account, anything on Exchange Online — needs a person to sign in once before MailFathom can read it. A headless service cannot arrange that, so the command does it: ```console $ mfctl mailbox authorize --provider google --client-id ``` This is the one command that talks to something other than your deployment, and it is why running `mfctl` on your own computer matters. It listens on a loopback address, opens your browser, and catches the redirect the provider sends back — so there is nothing to copy, and the authorization code never crosses a network. On a machine with no browser, forward the port over SSH, or use `--mode device` for Microsoft and `--mode manual` for Google. What it produces is a refresh token, and `--account` decides where that token goes: ```console $ mfctl mailbox authorize --provider google --client-id --account workspace … Stored the refresh token for account 'workspace' on 'production'. It was not printed. ``` **Named, the token goes straight to your deployment**, which encrypts it and keeps it. It is never printed, so it never reaches your scrollback or a session log, and there is nothing for you to place by hand. The account has to be one your deployment configures; a name it does not know is refused and nothing is stored. Re-running it replaces what was stored, which is what re-authorizing after a revocation is. **Omitted, the token is printed on standard output and you provision it on the server yourself**, as a secret reference like every other credential. That is what a deployment with no administrative endpoint needs, and it stays the right choice if you would rather place credentials yourself. [Mailbox OAuth](operations/mailbox-oauth.md) is the whole procedure, including what each provider requires of the application you register and how a stored token relates to the reference in your configuration. ## Turning semantic search on Which model your deployment embeds with is a configuration value, and editing it starts nothing. Turning semantic search on is a separate, deliberate act, because it is the first thing MailFathom does that costs money per unit of mail — a full mailbox goes to your provider, and you should see the size of that before agreeing to it: ```console $ mfctl embedding activate ``` The command reads what the deployment would spend, prints it as passages, characters, and approximate tokens, and asks before it starts. `--yes` agrees up front, for a scripted run. | What you want | Command | | --- | --- | | Find out why semantic search is quiet | `mfctl embedding status` | | Take up the model your configuration declares | `mfctl embedding activate` | | Stop a re-embed you have changed your mind about | `mfctl embedding cancel-reindex` | `mfctl embedding status` is the one to reach for first. It answers, in one output, whether a model is active, whether that model is still the one your configuration declares, whether your provider is answering, how much of the mailbox is embedded, what the current budget period has spent, and when the walk that embeds your existing mail next runs. That last line is the one to read in the minutes after an activation: until the first passages have gone out, a deployment that is simply between passes looks exactly like one that is broken. [Administering the embedding profile](operations/admin-endpoint.md#administering-the-embedding-profile) is the operator's reference for all three, and [changing the embedding model](operations/embedding-profiles.md) is what a switch and a rollback cost. ## Applying your rules, and seeing what they did [Mail rules](features/mail-rules.md) select mail as it arrives, so a rule you write today does nothing about the mail already in the mailbox until you ask — or until a [schedule](features/mail-rules.md#running-a-rule-on-a-schedule) you gave the rule asks on your behalf. That asking is a command, and so is finding out what the rules have been doing: | What you want | Command | | --- | --- | | See which rules your deployment is running, in order | `mfctl rules list` | | Read one of them in full | `mfctl rules show file-invoices` | | Apply them to mail that arrived before them | `mfctl rules run --account work` | | Watch that run | `mfctl rules run-status --account work` | | Find out what a rule did, or why a message is where it is | `mfctl rules history --account work` | `mfctl rules list` is the one to run after editing a rule file. A deployment refuses a reload whose rules do not validate and goes on running the previous set, which it reports to its log and nowhere else — so this is where you find out whether your edit took effect, rather than from mail that kept being filed the old way. Each rule it prints says what runs it, on a `Runs on:` line, which is how a rule naming [no trigger](features/mail-rules.md#which-triggers-run-a-rule) is told apart from one that simply never matched: nothing fires such a rule by itself, and `mfctl rules run` is how it is run. A rule with a schedule says so on that same line, with the occasions it declares beside the trigger. It is also where a rule you meant to run over arriving mail shows that it never says `Arrival`. None of these writes a rule, and none ever will: rules are configuration, so you change one by editing the file your deployment reads. [Reading the rules, running them, and finding out what they did](operations/admin-endpoint.md#reading-the-rules-running-them-and-finding-out-what-they-did) is the operator's reference for all five, including what the history records and what it deliberately does not. ## Classifying the mail you already have [Spam classification](features/spam-classification.md) reaches arriving mail on its own: a message is classified because it arrived, and you ask for nothing. What it does not reach is the mail that was already there — everything stored before you switched it on, and everything stored while it was off — so switching it on, switching filing on, or moving a threshold does nothing about the existing mailbox until you ask: | What you want | Command | | --- | --- | | Find out what classification would do to the mail you have | `mfctl spam run --account work` | | Carry that out | `mfctl spam run --account work --apply` | | Watch the run | `mfctl spam run-status --account work` | | Find out why a message was filed as junk | `mfctl spam classifications --account work --email ` | **The run is a dry run unless you add `--apply`**, and that is the order to do it in: run it, read what it found with `run-status`, and only then run it again with `--apply`. With filing switched on, a run over an inbox is the largest single thing MailFathom does to your mail, and the dry run tells you how much of it would move before any of it does. The verdicts are recorded either way — what `--apply` adds is the mail server being written to. A second run while one is going does not start a second walk; you are told the first is still under way, on the terms it was started with. None of these writes a setting, and none ever will: whether mail is classified, what a scanner is judged by, and what happens to junk are configuration, so you change them by editing the file your deployment reads. [Classifying the mail you already have, and reading what was concluded](operations/admin-endpoint.md#classifying-the-mail-you-already-have-and-reading-what-was-concluded) is the operator's reference for all three. ## Getting a folder's storage back MailFathom never takes local mail away because a file changed. Switching a folder's `Synchronize` off stops mirroring it and keeps what it already stored; removing its mapping leaves those rows where they are as well. Both are the right default — an edit to a configuration file should not dispose of mail — and both leave you with storage you may actually want back: ```console $ mfctl folder erase --account work --folder archive 1043 stored emails erased from ARCHIVE under work. The folder holds none, and its checkpoint went with them, so mirroring it again starts from the beginning rather than resuming. ``` This is the only command that erases mail, and it acts on one folder of one account. It refuses a folder your deployment still mirrors, because the next synchronization run would simply fetch it all again — switch that folder off, or take its mapping out, and ask again. A folder whose mapping you already removed is accepted, which is the case this exists for. Erasing a large folder takes a while and the command prints how far it has got. Stopping it is safe: what it reported erasing is gone, the rest is untouched, and running the same command again continues from there. [Erasing a folder you have stopped mirroring](operations/admin-endpoint.md#erasing-a-folder-you-have-stopped-mirroring) is the operator's reference, including what goes with the mail and what survives it. ## Background work that stopped MailFathom does most of what it does in the background: classifying a message, embedding a passage, carrying out what a rule asked for. A piece of that work that keeps failing is eventually given up on rather than retried forever, and what is left behind is called a *dead letter* — a record of work nobody will attempt again. Nothing waits on one, so nothing tells you it is there: | What you want | Command | | --- | --- | | See what has stopped | `mfctl jobs dead-letters` | | See what has stopped for one account | `mfctl jobs dead-letters --account work` | | Run one again, after fixing what broke it | `mfctl jobs retry --job ` | | Decide one will never run | `mfctl jobs drop --job ` | ```console $ mfctl jobs dead-letters 2026-08-13 09:30:00Z classify-email-spam 0199c3d0-0000-7000-8000-000000000002 Failed: Permanent PayloadUnreadable after 5 attempt(s) Work: account:work|email:0199c3d0-0000-7000-8000-000000000001 for work Queued: 2026-08-13 09:00:00Z Run one again with 'mfctl jobs retry --job ', or write it off with 'mfctl jobs drop --job '. ``` **`Failed:` is what decides which of the two commands is right.** `Permanent` names something that will fail the same way every time — a credential, a setting, a message the deployment cannot read — so retrying before you have changed something achieves nothing. `Transient` names a dependency that stayed broken for longer than the queue was willing to wait, which is the case `retry` exists for: fix it, or wait for it to come back, and ask again. `retry` runs the same piece of work rather than enqueuing a second one, so it is safe on work that reaches your mailbox. `drop` deletes nothing — the record stays, keeping what stopped it, and it goes on being the reason the same work is not enqueued again. Neither command waits for anything: the deployment writes the decision down and the next worker to come along carries it out, so closing the terminal changes nothing. An empty reading is the ordinary state of a healthy deployment, and it says so rather than printing nothing. [Reading the background work that stopped, and deciding what becomes of it](operations/admin-endpoint.md#reading-the-background-work-that-stopped-and-deciding-what-becomes-of-it) is the operator's reference, and [durable background work](operations/telemetry.md#durable-background-work) is what your monitoring can watch instead of you reading this by hand. ## Where to go next - [Administering a deployment](operations/admin-endpoint.md) — the operator's reference for everything above - [Mail rules](features/mail-rules.md) — every fact, operator, and action a rule can use - [Mailbox OAuth](operations/mailbox-oauth.md) — registering the application, and every mode of the sign-in above - [Changing the embedding model](operations/embedding-profiles.md) — what activating, switching, and rolling back cost - [Configuration reference](operations/configuration-reference.md#adminendpoint) — every `AdminEndpoint` key - [Secret provisioning](operations/secret-provisioning.md) and [rotation](operations/secret-rotation.md) — how the key on the server is supplied and replaced --- Copyright © 2026 Krzysztof Kasprowicz Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. Project repository: https://github.com/Krzysztof318/MailFathom