Repository navigation
Releases: fathomise/MailFathom
Release list
MailFathom 0.8.0
The eighth release, and the one in which MailFathom stops being one person's mailbox described in a configuration file.
The people a deployment serves, their mail accounts, the credentials they sign in with, and what each of them may do
are now records in PostgreSQL, written through mfctl and the administrative endpoint while the deployment runs. One
deployment serves several people, shares a mailbox between them, groups them into organizations, and runs as more than
one replica.
It is also the first release that carries MailFathom's own client, and the client is an alpha. It is in the image,
off until you serve it, and published as a desktop application beside the release. It is there to be tried, it is
incomplete, and nothing about it is promised to the next release. The MCP tools remain the supported way to reach your
mail.
This release does not upgrade in place — plan it as a new installation over a backup. The schema artifact applies
to a 0.7.0 database, and applying it discards the stored mail and everything recorded against a mail account;
synchronization fetches the mail again. Nothing imports the users, mailboxes, or credentials your configuration
declared: each is recorded again. The Changed entries below say what, and
Getting started is the path a deployment
follows afterwards. Do not leave a 0.7.0 build running against the migrated schema: there is no rolling upgrade
across this release, and the way back is the backup.
The terms change too. Every artifact from this release on is published under AGPL-3.0-only where earlier
releases were Apache-2.0, and from the fathomise organization rather than from a personal account.
The twenty-one MCP tools keep their names and arguments. What moves on that surface is an account identifier, two
result shapes, and three permission names, each named below.
Added
- Several users, and mail accounts as records of their own. A user and a mail account are each a record, and an
account is assigned to the users it serves, so one shared mailbox is one copy of its mail read by several people.
Users can be grouped into organizations, whose members sign in asSHORTNAME/username.mfctl user,mfctl account,mfctl credential, andmfctl organizationadminister all of it, and a change is served without a
restart (#2000,
#1992,
#2013). - Roles at a scope. What a user may do comes from the roles assigned to them — over the deployment, one
organization, or their own mail — and three are built in:Administrator,Organization administrator, andMail user. An administrator is a user like any other, and a credential can be confined to the endpoints it may be
presented on and the networks it may come from (#2328,
#2333,
#2341). - Signing in with a user name and a password, over HTTP Basic, on any endpoint whose
Authenticationlist names
password(#1328). - Settings you change while the deployment runs. A deployment's own settings are persisted in PostgreSQL and read
as one more configuration source, below environment variables and above your files;mfctl configreads and edits
them, and every replica follows a change. A settings policy held at the deployment or at an organization can force a
value, or withhold one, for every user and mail account beneath it
(#1280,
#1311,
#2361). - Configuration in YAML beside JSON, for provisioned files and for a mail account's declaration
(#2031). - More than one replica. Work that must not run twice — an account's synchronization, a backfill, a delivery
pass — is divided between replicas through PostgreSQL alone. The Helm chart renders a rolling update and a
PodDisruptionBudget, and an optional RESP backplane, Valkey in every deployment asset, carries live updates to a
client from whichever replica produced them (#1925,
#1923). - Stored mail content in an S3-compatible bucket, chosen with
ContentStorage:Backendand off by default, with a
bounded, verified move of what the database already holds (#1219,
#1266). - Attachments are searched and cited.
search_emailsandask_mailfind what was sent as a file — the text of a
PDF, a document, a spreadsheet, or a presentation, and a description of a picture — and name the file and the page,
slide, or sheet. Both results gain fields and neither loses one. Reading attachments and describing pictures are
bounded per period and off until configured (#1714,
#1735). - Several chat models.
Chat:Modelsdeclares them under aliases, each AI capability can name its own, and a
model can name a fallback (#1934,
#2047). - Holding the only copy of a mailbox. An account can be set to have MailFathom hold its mail rather than mirror
it, emptying the source once each message is durably stored, and custody can be returned to the server afterwards. A
mailbox exports as a Maildir archive (#2026,
#2160,
#2024). - MailFathom's own client, as an alpha. A web client travels in the image and is served from
/app/once
ClientEndpoint:Application:Enabledis on; the same client is attached to the release as a.deb, an.rpm, and a
Windows installer. It reads, searches, composes, and files mail, and carries first versions of an assistant, a
calendar, tasks, and an address book. It talks to a deployment over the HTTP API under/api/client, which is off
untilClientEndpoint:Enabledand is expected to change between releases while the client is unfinished
(#1436,
#1444).
Changed
- Breaking (configuration schema, database schema): nothing a
0.7.0configuration declared about mailboxes is
read, and the mail stored for them is discarded.MailSynchronization:Accountsis gone and a configuration still
carrying it does not start. Applying the schema artifact removes stored mail, drafts, queued and recurring sends,
rule and spam-classification history, the audit trails, and the OAuth refresh tokens of every mailbox. Your
action: back up, apply the schema, remove the section, then record each person withmfctl user add, each mailbox
withmfctl account add— a declaration now statesEmailAddressand noAccountId— assign the role they should
hold, and let synchronization refill the mail. Re-consent every OAuth mailbox, and state spam classification and
sensitive-content scanning on each account's own record, where they now live
(#1861,
#2000,
#2011). - Breaking (configuration schema): credentials are provisioned, not configured.
ApiKey,PublicKey, and
Permissionsno longer exist underMcpEndpoint:AuthenticationorAdminEndpoint:Authentication, and a section
carrying one fails startup naming the command that replaces it. An entry names only the methods the endpoint
accepts. Your action: create each credential withmfctl credential createand move every client onto what it
reports (#1339). - Breaking (deployment contract): the administrative endpoint is signed in to as a user, and it is where a
deployment is set up. The first start records a useradminholding theAdministratorrole, and the
environment-onlyMAILFATHOM_ADMIN_PASSWORDsets its password once. Compose and Quadlet set it toadmin; the Helm
chart sets nothing and reads a Secret you name. Your action:mfctl login --mode password, change that password
at once — every start warns until you do — and set your own value before the first start wherever the endpoint is
reachable from another machine (#2333). - Breaking (MCP tool contract): three permission names, and what a credential's list means.
mailfathom.mail.askismailfathom.assistant.use,mailfathom.mail.contacts.readand.writeare
mailfathom.contacts.readand.write, and asking about mail needsmailfathom.mail.readbeside the first. A
permission written on a credential now narrows what its user's roles grant rather than granting anything, and a user
with no role holds nothing. Your action: assign a role to every user, and write the new names wherever a script
or an authorization server states one (#2374,
#2332). - Breaking (MCP tool contract): an account identifier is a generated UUID rather than the name you chose, a...
MailFathom 0.7.0
The seventh release, and the first one that sends mail. Until now everything an agent could reach either read your
local copy or changed a flag on your own server; from here it can compose a message and have MailFathom offer it to a
submission server. It can answer mail you already hold, forward one on, write a message into your Drafts folder and
leave it there for you, and mark, star, or label mail. The tool surface goes from five to twenty-one, and MailFathom
stops being a read-only window on a mailbox.
Everything that leaves the deployment is off until you turn it on. Sending is per account and disabled everywhere by
default, an account enabled with no submission host fails startup, Deployment:ReadOnly refuses every send whatever any
account says, and a recipient policy and a send ceiling bound who may be written to and how much may leave in a period.
Nothing a caller supplies decides who a message is from, no send is performed while the caller waits — each is written
down, answered with a record identity, and offered by a delivery pass that survives a crash — and the seconds before it
leaves are the only window in which it can be withdrawn.
The other half of the release is who may ask. Every Authentication entry can now state what it grants, as a list of
named permissions, and the check runs in the use case rather than only at the door: a tool a caller cannot reach is not
even listed to it, and an administrative route refuses the same way. An endpoint can also publish only some kinds of
tool at all, so a deployment can offer reading and drafting while withholding sending from every credential.
And MailFathom now records more about the mail it stores. Who authenticated a message's sender and whether this
deployment recognizes that author, how much the message's own text reads as machine written, the keywords your server
holds for it, and the conversation it belongs to — all published on every read tool's result. Beside them is a contact
book of MailFathom's own: people rather than addresses, held in your database, filled by hand or from the mail an
account corresponds with.
Three things need an edit before this release serves what 0.6.0 served, and every one of them fails quietly rather
than loudly. Write Permissions on every Authentication entry, because an entry that names none holds
everything its surface publishes — so on upgrade it gains the sixteen new tools, sending among them, and a credential
you meant to be able to read your mail can now send from it to anybody. Rename
SpamClassification:Actions:FileInJunkFolder to MoveToJunkFolder, or junk stops being filed and nothing says so.
Rewrite SensitiveContent:PersonalDataAnalyzer:Language as the list Languages —
personalDataScanning.languages in the Helm chart, SensitiveContent__PersonalDataAnalyzer__Languages__0 in Compose —
or the analyzer silently falls back to English.
One default moved and asks nothing of you. SensitiveContent:ScanTimeout now defaults to fifteen seconds where it
defaulted to five, which is a longer wait before a scan is refused and not a key to edit.
Mail stored by an earlier release keeps the answers it was given. The sender verdict, the trust verdict, the
machine-authorship reading, the keywords, and the threads are derived while a message is synchronized, so the migrations
fill every existing row with the not-established answer rather than a wrong one. mfctl mailbox rederive re-reads the
raw MIME already stored and writes the real answers back; until you run it, mail from before the upgrade reports as
nothing established.
The database schema moves by twenty-three migrations that add seventeen tables, thirty-six columns on tables that
already held data, and the indexes for both. Every added column carries a default, and nothing 0.6.0 reads changes —
so the schema step applies while 0.6.0 is still serving, 0.6.0 serves the result unchanged if you roll the image
back, and this release deploys over the previous release's data. Nothing else 0.6.0 promised is withdrawn: no tool was
removed, no tool argument was renamed, and every artifact a release publishes still publishes.
Added
Sending mail. An account with a submission endpoint can be asked to send, and MailFathom writes the message down
before anything reaches a server: the record is created first, answered with an identity, and offered to the submission
server by a delivery pass that claims it under a lease, so a crash mid-flight leaves work that finishes by itself rather
than a message MailFathom believes it sent
(#891,
#900,
#942).
Mail delivery is the page, stage by stage.
send_emailsends one message, as the account it names, to the addresses the call names. An idempotency key the
caller chooses is what makes a retry the same message rather than a second one
(#977).reply_to_emailandforward_emailanswer mail you already hold. The caller names the message and writes the new
words; the addressing, the subject, the headers that put it in the right conversation, the quoted original, and a
forward's attachments are read from your stored copy rather than from anything a client supplies. Whether a reply
answers the sender alone or everybody is required and never defaulted
(#920,
#989).get_outgoing_emailandcancel_outgoing_emailanswer for a send rather than performing one — where it has got
to, how many attempts it has taken, what each recipient's server said, and the code it stopped on. Cancellation
succeeds only before transmission begins, and each answers about the one identifier the caller was given: there is no
listing of what a mailbox has sent on this surface, and another caller's send reads as not found
(#996).- The copy is filed where the account's own configuration says, into the folder mapped as
Sentwhere one is mapped
(#976). - The message is composed by MailFathom rather than by a caller. The headers that establish identity, threading, and
the message's own identifier are this system's; a caller supplies a subject, text, an optional HTML alternative, and
recipients (#911). - Sending is bounded on four independent axes.
Delivery:Enabledis off on every account;Deployment:ReadOnly
refuses every send process-wide whatever an account holds;MailDelivery:RecipientPolicynames allowed and denied
domains and addresses, and a message naming one refused recipient is refused whole rather than delivered to the rest;
andMailDelivery:SendCeilingscounts messages and recipients per account, per caller, and per deployment over a
fixed period. Every ceiling is zero — no ceiling — by default
(#992,
#998). MailDelivery:UnvouchedRecipientscan refuse an address nothing here vouches for. Set toRefuse, an address
the caller named that neither the contact book nor one of your own accounts holds refuses the whole message — which
is the bound written for an agent that read a stranger's mail and was talked into forwarding a thread somewhere. Only
what the caller named is judged, so a plain reply is unaffected and a forward is judged in full. It admits by default,
because refusing by default would refuse the first message of every installation whose contact book is still empty
(#974,
#998).mfctl outboxoperates it: what stands at each stage, one bounded page of what has been queued, one message with
who it was offered to and what each of them was told, withdrawing one, and offering one again. Delivery is
instrumented as well — attempts, retries, outcomes, submission duration, and queue depth
(#995).
The four draft tools — writing a message and leaving it for you. save_draft writes a message into your own Drafts
folder and sends nothing; update_draft replaces the whole message and your folder ends up showing one draft rather
than one per edit; delete_draft gives one up and takes the copy back out; send_draft sends what a draft holds. A
draft can be a message of its own or an answer to mail you already hold, and one addressed to nobody is an ordinary
draft (#1001,
#1010). Drafting is granted apart from sending, which is the
grant to reach for where you want a person between an agent and a recipient: a credential that can write, edit, and
delete a draft cannot make one leave.
set_mail_flags — marking, starring, and labelling. One tool marks a message read or unread, stars or unstars it,
and adds, removes, or replaces its keywords. Every value is optional and at least one is required, and the change is
written down and carried to your server by the same convergence pass every other mailbox change goes through
(#968). The keywords your server already holds are stored as
well, and list_emails and search_emails filter on them and on \Flagged
(#878). A rule can set them too, and can flag a mess...
MailFathom 0.6.0
The sixth release, and the first one that does something with your mail rather than only reading it. Rules you write
in a configuration file move, copy, delete, and mark messages as read; a spam classification files junk on the server;
and a durable queue underneath both means a crash loses none of it. The rules and the
classification are off until you turn them on — the queue beneath them runs on every instance and is switched off only
for a replica serving reads — and a rule is only ever authored in the file you provisioned, so what an instance will do
to a mailbox is reviewable in a diff before it does anything.
The other half of the release goes the opposite way. A message's text can be redacted before anything is derived from
it and before any of that text leaves this deployment: secrets are found in this process, personal data by an analyzer
you run beside it, and what MailFathom then chunks, embeds, retrieves, and returns is the redacted text. An
attachment's content is not text a scan reaches — the signed link serves the file exactly as it was stored, so a
credential inside an attached file is not covered by turning a scanner on. That is off by default as well, and off it
costs nothing at all — no container is started, no image is pulled, and no memory is held.
Seven things need an edit before this release serves what 0.5.0 served. The folder argument of list_emails,
search_emails, and ask_mail is folders where it was folderAliases, and the old spelling is ignored rather than
refused — so a client that keeps sending it reads every folder instead of the one it named. Every folder you want read
is now named in configuration, and mail under an alias your file no longer names is unreachable until an entry names it
again. get_email_content hands back a signed link per attachment instead of base64: a call asks for the links with
includeAttachmentDownloadLinks where 0.5.0 asked with includeAttachmentContent, the old name is ignored rather
than refused, and no link is issued at all unless Deployment:PublicBaseAddress is declared. Delete
EmailContent:MaxAttachmentBytes and EmailContent:MaxAttachmentBytesPerRead from your configuration file, or the
host refuses to start on a key it no longer knows. Mail in a folder your configuration maps as junk is withheld from
listing and search unless the call asks for it, and withheld from answering with no way to ask. And a folder whose
Synchronize you switch off now keeps its stored mail instead of erasing it.
The database schema moves by twelve migrations that add eight tables, four columns on three tables that already held
data, and the indexes for both, and that change nothing 0.5.0 reads — so the schema step applies while 0.5.0 is
still serving, 0.5.0 serves the result unchanged if you roll the image back, and this release deploys over the
previous release's data. Nothing else 0.5.0 promised is withdrawn: every setting not named below still means what it
meant, no tool was removed, and every artifact a release publishes still publishes — the image, the chart, the schema
script, and an mfctl binary per platform, Windows included. What is paused is the winget submission, which has
never produced a package: the two already open are waiting for the community repository's review.
Added
Mail rules — what should happen to a message, written in your configuration file and applied to your mailbox. A
rule names the accounts it applies to, one condition over the message, and what a match asks for; a match moves the
message to a folder, copies it, deletes it, or marks it as read, and MailFathom's convergence pass carries the change
to the server the way every other change is carried, so a restart neither loses it nor repeats it
(#712,
#725).
Mail rules is the page, condition by condition.
- The condition is one expression over twenty-two facts about the message — the account alias, the folder alias and
the role that folder plays, the subject, the sender's address and domain, the recipient addresses and domains, when it
was received and sent, its age, its size, the attachment count and bytes, seven flags the server or the extraction
reported, and the body text — with seven functions and the ordinary operators. Anything outside that set is refused when the file is read rather than at
the moment a message meets it, and a rule set with three mistakes reports all three at once
(#696). - A rule runs on the occasions it declares.
MailRules:Rules:<n>:Triggersnames them:Arrivalfor mail as it is
synchronized,Schedulewith aSchedulebeside it for a recurring pass, and neither for a rule only an operator
starts (#727,
#820). - Each account states which of the four actions a rule may ask of it under
RuleActions, with deletion opt-in and
the three reversible actions opt-out. A rule asking for a refused action fails startup naming the rule, the action,
and the account (#725). - No IMAP command a rule asks for leaves the pass, and the pass touches no
\Seenflag itself. Every change a match
asks for is written down and carried by the account's convergence pass, the way every other change to a mailbox is.
The one thing the pass does reach a mail server for is finding a destination folder the account maps and does not
mirror, and only where a rule files into one; everything it reads about the mail was already stored, so no MCP read
waits on it however long it takes. It is a step of the account's own synchronization run, after the classification and
in front of the passages being cut. mfctlruns the rules and explains what they did.mfctl rules listandmfctl rules showstate which rules are
loaded, in the order they run, and what fires each;mfctl rules runapplies them to mail that arrived before them
and returns at once rather than holding the terminal open;mfctl rules run-statussays where that run has got to;
andmfctl rules historyanswers why a message is where it is, one row per rule per message, recording that a
condition readsenderDomainand never what the domain was
(#728).- An edit takes effect on reload, and an invalid one changes nothing and is reported instead of disappearing. A run
under way stays on the rule set it started with and reports itself superseded rather than half-applying two
(#682).
Spam classification, and filing junk on the server. The verdict is read from what the message already carries: the
provider's own X-Spam-* headers, and the folder it arrived in, which outranks them because it is a decision somebody
already acted on. The authentication results, the ARC chain included, are recorded beside the verdict as signals
rather than judged from — a DMARC failure is something your receiving server saw and chose to deliver anyway, so
turning it into a spam verdict here would file mail your own provider decided to accept
(#731). SpamClassification:Enabled turns it on, and it is off.
It covers the folders SpamClassification:ScannedFolders names, and where you name none, every account's inbox and
nothing else — so a deployment whose own filter delivers somewhere other than the inbox names that folder there, or
gets no verdict for the mail in it.
Spam classification is the page.
- An Apache SpamAssassin daemon beside the service scores what the headers cannot, deployed only where
SpamClassification:UseScanneris on: the Helm chart renders no workload for it, the Compose deployment keeps it
behind an inactive profile, and the Quadlet unit is a file you never copy. Its DNS blocklists are off, because
those rules send the sender addresses and link hosts out of your mail to third-party lists
(#777). - What a verdict may do is two switches, both off.
SpamClassification:Actions:FileInJunkFolderfiles the message
into the account's junk folder andMarkAsReadmarks it read, each through the same durable change record a rule
uses (#779). - Arriving mail is classified as it is stored, as a queued job retried per message, so one unreachable scanner
delays one message rather than the whole account (#826). mfctl spam runclassifies a whole mailbox, and its default posture is a dry run — the first thing you do with a
scanner is find out what it would do.mfctl spam run-statusfollows the walk andmfctl spam classificationsreads
the verdicts back (#795).- Junk is withheld from everything derived from it. Where classification is on, a message it calls spam — and one the
receiving server already filed in junk — is never cut into passages, never embedded, never sent to an embedding
provider, and never offered to the rule set. A message still waiting for a verdict is held back only until
SpamClassification:ClassificationWaitexpires, fifteen minutes unless you say otherwise, so a wedged scanner or a
deep queue does not stall the index: past the wait the message is derived from like any other, and a spam verdict
arriving afterwards discards the passages and vectors it pro...
MailFathom 0.5.0
The fifth release, and the one that lets a client ask about your mail rather than only look through it. ask_mail
answers a question in prose and cites the messages the answer was drawn from, and search_emails ranks semantically as
well as lexically. Both stay dark until you declare the AI endpoints they need, so a deployment that declares none
serves exactly what it served before — and pays exactly what it paid before, which for a feature that bills per call is
the more important half.
Four things need an edit before this release starts, renders, or answers a client. Every account states a
DisplayName now; two arguments on the MCP tools were renamed, and the previous spellings are ignored rather than
refused — so a client that keeps sending accountIds reads every account instead of being stopped; a Helm values
document that names database.host — which every 0.4.0 one does — needs database.deploy.enabled: false beside it,
or the chart refuses to render at all; and the database is PostgreSQL 18, which does not read a data directory
PostgreSQL 17 wrote. The last of those is the expensive one: an existing deployment moves its data across a dump
before the new image comes up, and
upgrading a deployment that ran PostgreSQL 17
is the procedure, command by command.
The database schema moves as well, by six migrations that add four tables, four columns on tables that already
held data, and two indexes on those, and that change nothing 0.4.0 reads — so the schema step applies while 0.4.0
is still serving, 0.4.0 serves the result unchanged if you roll the image back, and this release deploys over the
previous release's data. Nothing else 0.4.0 promised is withdrawn: every setting not named below still means what it
meant, and no tool was removed.
The defect 0.4.0 shipped with is gone, and it was the whole image. The published 0.4.0 container could not
start: its base image sets ASPNETCORE_HTTP_PORTS, 0.4.0 is the release that began refusing that variable, and the
Dockerfile never cleared the inherited value — so every container built from that image failed startup on a setting
nobody wrote.
Added
ask_mail — a question about your mail, answered in prose and cited back to the messages it came from. The
question is not a search query: its words are never matched against your mail, and the lookups behind it are written by
the model, which is what lets did the supplier ever confirm the March delivery date find the message that says so
without containing any of those words
(#579). Every answer carries citations, one entry per email
the run actually read, so nothing it says is un-checkable.
- It is advertised only where it can work — a declared
Chatendpoint, and mail that is embedded. A server with
neither does not publish the tool, so a client never sees an ability the deployment does not have. Called on a
deployment that cannot answer, it fails with56001and the message says which half is missing. - Configuring it is one section.
Chat:Alias,Chat:Model, and one credential — an API key or a Microsoft Entra
credential — declare the endpoint;Chat:Apistates whether the deployment's server serves chat completions or the
responses API, because the routed name is your own deployment's and nothing about it says which paths exist. A
reasoning model statesChat:ReasoningEffortas the provider spells it, and unset sends no reasoning parameter at
all (#557,
#624). - What one question may spend is bounded before it is asked, and what a period may spend is bounded above that.
MailAnsweringsets how many passages a lookup draws on, how much of any one message it draws out, how much
retrieved mail may leave the process for one question, how many provider calls and tokens one run may spend, and how
many runs and tokens a period may. A run that reaches a ceiling stops with57001rather than continuing quietly,
and a run that reaches only the retrieval ceiling answers from what it has and says the mailbox was not read in full
(#592). - An optional second filter judges the retrieved passages with the model before they reach the answer, dropping
what scores belowChat:RelevanceFilter:MinimumRelevance. It is off by default, because it is a second call per
lookup (#573). - An account can keep a record of what each question read, off by default and enabled per account with
AnsweringAuditTrail:Enabledand a retention window. It names the mail a run drew on and holds none of it, and
GET /api/admin/answering/auditreads it back in bounded pages
(#610). - The model is composed over Agent Framework and the mail it reads is fenced away from the instructions it follows, so
an instruction written into a message is data rather than a command
(#564,
#565,
#603).
Mail answering is the page.
search_emails ranks semantically as well as lexically. Where an embedding profile is active, a search fuses the
two rankings with Reciprocal Rank Fusion and reports retrievalMode: hybrid; where none is, it answers exactly as
0.4.0 did and says lexical (#555). Every response also
carries semanticSearch — inactive, available, or degraded — so a client can tell a server that never embedded
anything from one whose provider is failing right now, which the two modes alone cannot distinguish
(#562).
- A failing provider degrades the search rather than the deployment. An unhealthy profile falls back to lexical
ranking and says so, instead of failing the call. - Changing the model is a reindex with no search outage. A new vector generation is built beside the one that is
serving and takes over only when it is complete, andPOST /api/admin/embeddings/reindex/cancellationstops one
under way and leaves the serving generation where it is
(#570). - What embedding may cost is bounded before it is spent.
Embeddings:MaxRequestsPerMinutepaces a provider whose
quota is per minute,Embeddings:MaxInputCharactersPerPeriodandEmbeddings:SpendPeriodcap what a fixed window
may send, andEmbeddings:MaxCharactersPerEmailbounds a single enormous message rather than refusing it
(#581). mfctladministers it.mfctl embedding statusreports whether semantic search is working, how far behind it
is, and when the next backfill pass is due;mfctl embedding activateforecasts what taking up the declared model
would cost before it starts, and starting it wakes the backfill instead of leaving the deployment to look broken for
up to fifteen minutes (#593,
#626).
Embedding profiles states the whole
lifecycle.
list_accounts, so a client can find out what it may ask about. It reports each account's identifier, the display
name you gave it, whether its next pass polls or listens, and one entry per folder with how fresh that folder's local
copy is — and deliberately publishes no address, no credential, and no server name
(#637). Every tool that takes accounts now accepts either the
identifier or the display name, so a person can say work where the configuration says work-imap-01.
get_email_content returns attachment content. Pass includeAttachmentContent and each attachment arrives as
base64, bounded by EmailContent:MaxAttachmentBytes per file and EmailContent:MaxAttachmentBytesPerRead across the
call; a file over the limit is described and not returned, never truncated
(#633). Setting MaxAttachmentBytes to 0 returns no
attachment content at all, which is the deployment that wants the metadata and nothing else.
A record of every change MailFathom makes to a mailbox, off by default and enabled per account. Nothing in
0.5.0 asks it to make one — no tool on the MCP surface writes, and the first caller is the rule engine a later
release brings — so an account that turns the trail on today gets an empty page and keeps getting one until that
caller exists. What it buys now is that the decision is made and the storage is in place before the first write, and
that is the whole of it. AuditTrail:Enabled and AuditTrail:Retention turn it on per account, one entry is written
per finished change, and GET /api/admin/mailbox/mutations/audit reads it back filterable by account, by change, and
by time (#568).
- It holds no mail content and it outlives the mail. Folder paths, identifiers, a five-digit failure code where
there was one, and MailFathom's own configured names are all an entry carries — no subject, no address, no body
fragment, no filename — and erasing the email leaves the entry standing, including where the change recorded was
that deletion. - Retention rides the account's ...
MailFathom 0.4.0
The fourth release, and the first that asks every deployment to edit its configuration before it will start. Two things
every installation states have moved: where each surface is served, and how a credential is configured. Neither
previous form is ignored — both fail startup naming what replaces them — so an upgrade that skips the edit stops rather
than quietly serving something you did not configure. The database schema moves as well, by five migrations that
add three tables and then refine one of the three, and that touch nothing 0.3.0 reads — so the schema step belongs to
this upgrade, it applies while 0.3.0 is still running, and 0.3.0 serves the result unchanged if you go back.
Nothing else 0.3.0 promised is withdrawn. The MCP tool contract is untouched — list_emails, get_email_content,
and search_emails answer exactly as they did — and every setting not named below still means what it meant.
The defect 0.3.0 shipped with is gone. A deployment that set HealthEndpoints:Enabled to false and enabled the
administrative endpoint lost its application listener and refused every MCP client. There is no application listener to
lose now, because every surface binds the socket its own section names.
Added
A key pair as a third way to authenticate, on both endpoints. The client holds the private key and the deployment
holds only the public half, so nothing this host stores in order to verify a request is worth stealing from it — not
from the configuration, not from a backup of it, and not from the deployment tool that wrote it
(#527).
- Configure a
PublicKeyentry underAuthenticationexactly as you would a key: one named secret, reached through
every reference scheme the deployment already has, with aNamediagnostics correlate on and aLifetimethat is
enforced. Startup refuses material that is not a PEM public key, an RSA key below 2048 bits, a curve outside P-256,
P-384, and P-521, and — explicitly — material carrying a private key. - The client mints a short-lived JSON Web Token, signs it with the private half, and presents it as an ordinary bearer
credential: the arrangement RFC 7523 describes and OpenID Connect deploys asprivate_key_jwt. It carriestyp: mailfathom-client-assertion+jwt, an audience ofurn:mailfathom:mcporurn:mailfathom:admin, an expiry no more
than five minutes ahead, and a fresh identifier the endpoint refuses to serve twice — so a captured assertion stops
working on its own, and cannot be replayed even inside its remaining seconds. mfctl login --mode keypair --private-key <file>mints all of it and stores no credential; every command signs its
own assertion.- Rotating a key is an overlap with no secret to coordinate across two machines: add the new public key as a second
entry, move the client to the new private key, remove the old entry.
Key pairs is the page.
mfctl from the Windows Package Manager. Each release submits its own manifest, so winget install MailFathom.mfctl becomes a packaged path beside the download and winget upgrade carries you to the next release
(#498). The manifest names the same release asset the releases
page does and carries the same hash the checksum file does, so both paths install the same bytes and check them the
same way. A version is offered a little after it is attached here, because the community repository reviews the
submission; until one is accepted, the releases page is where the command comes from on every platform.
The metrics and traces the libraries underneath MailFathom already emit. Where OTEL_EXPORTER_OTLP_ENDPOINT names
a destination, four more meters now reach it: Npgsql for connection-pool state and command durations and counts,
Microsoft.EntityFrameworkCore for contexts, queries, saves, compiled-query cache hits, and concurrency failures,
Experimental.ModelContextProtocol for MCP session duration and per-operation duration broken down by protocol method
and tool name, and Polly for every outbound pipeline's attempts, outcomes, timeouts, and circuit-breaker transitions
(#521). Database commands and MCP protocol operations are
spanned as well and correlated with the request that caused them; the probe paths stay untraced, because a probe
arrives every few seconds and says the same thing every time.
- Every tag on them is a bounded set — a protocol method, a transport, one of the three tool names, an outcome — so
none of them opens a time series per message or per person. - What MailFathom publishes under a name of its own goes under exactly one:
MailFathom, serving as both activity
source and meter, which is what a dashboard filters on to see this process and nothing a library emits
(#510).
Telemetry records each of them.
Changed
- Breaking (configuration schema) — every surface states where it is served, and the host's own ways of naming a
listener are refused.ASPNETCORE_URLS,ASPNETCORE_HTTP_PORTS,ASPNETCORE_HTTPS_PORTS,--urls, and any entry
underKestrel:Endpointseach fail startup with a message naming the setting that replaces them. Write
McpEndpoint:BindAddress,McpEndpoint:Port, andMcpEndpoint:Transport; the administrative endpoint and the probes
take the same three. A deployment of your own that setsASPNETCORE_HTTP_PORTSsetsMcpEndpoint__Portinstead —
the published image and the packaged chart already do, so an upgrade that takes both as they ship needs no edit here
(#459). They are refused rather than ignored because ignoring
them is silent: Kestrel drops URL-shaped addresses as soon as a listener is bound in code, which every surface now
does, and a configured endpoint would otherwise be bound beside them on a socket no section describes and no
credential guards. A deployment that enables no surface at all is refused for the same reason. - Breaking (configuration schema) — the administrative endpoint's default port is
8080, the MCP endpoint's,
where0.3.0gave it8090. Two surfaces may deliberately share one socket now — the posture a single-node
deployment behind one ingress wants — so a deployment that enabled the administrative endpoint without stating a port
publishes it wherever8080is published rather than on a port of its own. StateAdminEndpoint:Port, where8090
restores what you had, unless sharing is what you want; the socket serves each surface's own paths either way, and a
path a surface does not own is still refused there with a404
(#459). - Breaking (configuration schema) —
Transportdecides what a surface's clear-text socket does, where0.3.0
inferred that from whether HTTPS profiles were configured.Httpserves the routes and refuses profiles,
HttpAndHttpsbinds the profiles and redirects the clear-text socket to them, andHttpsOnlydoes not open it at
all.Httpis the default, so adopting this release costs no certificate work
(#459). - Breaking (configuration schema) —
Https:Redirectno longer binds a port of its own.0.3.0gave it8080
beside the MCP profiles and8091beside the administrative ones; the redirect now answers on the surface's own
BindAddressandPort. A deployment that published8091to reach the administrative redirect publishes that
surface's own port instead (#459). - Breaking (configuration schema) — authentication is a list of the credentials an endpoint accepts, where
0.3.0named methods in a flag set and configured each in a sibling section.McpEndpoint:Authenticationand
AdminEndpoint:Authenticationeach take entries, and the block an entry carries is what selects the method that
judges it — there is no setting naming the method any more.Authentication: "ApiKey"beside anApiKeyslist
becomes one entry per key, each carrying anApiKeyblock;Authentication: "OAuth"beside anOAuthsection
becomes an entry carrying that section. An entry carrying no block fails startup, named by its position
(#515).RequiredScopesis per entry rather than per endpoint, so two authorization servers one endpoint accepts may
demand different scopes. Every OAuth entry still names the sameResource, because the endpoint publishes one
metadata document.- An empty list warns at startup exactly as
Nonedid, and a value written where the list belongs fails it rather
than being read as a method name.
- Breaking (configuration schema) — a setting only the process environment can deliver, written anywhere else,
fails startup naming every such variable at once, with error code12002.OPENSSL_CONF,OTEL_SERVICE_NAME, and
everyOTEL_*,ASPNETCORE_*, andDOTNET_*variable are read before MailFathom's configuration exists or by a
library that never consults it, so a value written into an appsettings file, a provisioned configuration file, or a
command-line argument reached nobody — while the file read it back happily and nothing said which of the two you were
looking at. Set each on the host process, or remove it
(#509). - Every synchronized message is also cut into passages and stored, in the same transaction that stores what was
extracted from it, so ...
MailFathom 0.3.0
The third release, and the first whose upgrade is a new image and nothing else: the database schema does not move.
No migration is added, so 0.3.0 serves the database 0.2.0 was serving, and 0.2.0 serves it again if you go back.
Nothing 0.1.0 or 0.2.0 promised is withdrawn either — the MCP tool contract is unchanged, and every configuration
key 0.2.0 accepted is still accepted and still means the same thing. There is no breaking entry below.
What is new stands in front of the service rather than inside it: what terminates TLS for it, and what bounds the
surface you administer it through. The pages describing all of it are now published as
a documentation site, with search, an API reference generated from the
source, and a version selector; 0.3.0 is the first release it carries a version for.
One caveat, and it is a defect this release ships with: a deployment that sets HealthEndpoints:Enabled to false
and enables the administrative endpoint loses its application listener, because binding a socket in code makes
Kestrel ignore ASPNETCORE_HTTP_PORTS and only the probe path restates it. The process starts and serves the
administrative port alone, and every MCP client is refused.
#395 carries the fix. Until it lands, leave the probes
enabled — the default — or state the application listener as a Kestrel:Endpoints entry.
Added
A deployment behind a TLS-terminating reverse proxy. When nginx, Traefik, or an ingress controller holds the
certificate, the request that reaches MailFathom arrives as http under an internal name, and the deployment's public
identity survives the hop only in two headers.
X-Forwarded-ProtoandX-Forwarded-Hostare read and applied before anything else sees the request, so OAuth
discovery, the401challenge, and every absolute address MailFathom writes carry your public name — the
protected-resource metadata document included, which is what a proxied OAuth deployment needed
(#371).ReverseProxy:TrustedProxiesnames the addresses or CIDR networks those headers are believed from, and
ReverseProxy:MaximumForwardedHops(default1) how far back through each header a value is believed
(#371,
#397). It is one section for the whole process rather than one
per surface: a proxy's address is a network fact, so it is stated once and holds on every listener. What you name
replaces the framework's loopback default rather than adding to it, and10.0.0.5/24is refused naming the
10.0.0.0/24it would otherwise silently have become.X-Forwarded-Foris never read, so the peer MailFathom observes stays the one that opened the connection, and
McpEndpoint:OAuth:Resourcestays a value you wrote rather than anything derived from a header
(#371).- Client certificates are unreachable in this posture, because the handshake ended at the proxy and no header is read
as a substitute (#371).
Behind a TLS-terminating reverse proxy
is the page, including what the proxy owns and what MailFathom keeps owning.
A clear-text listener that redirects to HTTPS. A surface that terminates TLS also binds one listener whose only
answer is a 308 to the address its profiles are served at, so a client nobody repointed meets a redirect rather than a
refused connection indistinguishable from an outage (#374).
McpEndpoint:Https:Redirectbinds port8080andAdminEndpoint:Https:Redirectport8091unless you state
another, each takingEnabled,BindAddress, andPort. The defaults differ so terminating TLS on both surfaces
opens two clear-text ports that do not collide.- That listener maps no route. Every path is answered the same way, and no authentication, rate-limiting, CORS, or
client-certificate handler runs for a request that arrived on it, so there is nothing reachable over it to protect. 308rather than301or302, because the MCP transport is aPOSTthe older codes permit a client to re-send as
aGET. The path and query are preserved, each domain redirects to its own profile's port, aHostheader naming no
configured domain gets400, and:443is left out of theLocation.- Writing the section for a surface that terminates no TLS fails startup rather than being ignored, and a socket
conflict with any other listener in the process is reported against the section that asked for it. The health probes
keep their own listener and are never asked on this port, because a probe follows no redirect.
Rate limiting on the administrative endpoint. AdminEndpoint:RateLimiting is the section
McpEndpoint:RateLimiting is, with the same keys, the same product defaults, and the same validation
(#373).
- The two are configured independently and partitioned per surface, so neither endpoint's traffic reaches the other's
limits: an agent that exhausted/mcphas taken nothing from the surface you would use to stop it, and the
concurrency limits are separate for the same reason. - The burst is the endpoint's rather than one caller's. These routes carry no authentication middleware of their own —
the credential is judged behind the limiter, so a request about to be refused for a wrong key has still spent
capacity — and there is therefore no identity to partition on. SizeTokenCapacityas what the whole endpoint may
burst to rather than what one operator may.
Changed
- An enabled administrative endpoint is bounded whether or not you configure it: 20 concurrent requests and a burst
of 60 restored every minute, which are the MCP endpoint's defaults.0.2.0served it unbounded, so a deployment
whose automation asks faster than that raises the numbers or setsAdminEndpoint:RateLimiting:Enabledtofalse,
which costs one startup warning (#373). - Configuring an HTTPS profile now also binds a clear-text port —
8080beside the MCP profiles,8091beside the
administrative ones. Where a proxy in front of the process already answers that port, or something else on the host
holds it, set…:Https:Redirect:Enabledtofalse. A conflict with another listener of this process is refused at
startup naming the section that asked for it rather than failing later as an address-in-use error
(#374). - The startup line stating the rate limits in force comes from
MailFathom.Host.Hosting.Warnings.TransportRateLimitingStartupReportrather than…McpRateLimitingStartupReport,
and one line is written per enabled endpoint. A deployment that matches on the logger category updates it
(#373). - The clear-text transport warning describes the deployment you configured once a trusted proxy is named, rather than
suggestingMcpEndpoint:Https:Endpointsto a deployment whose certificate lives on the proxy
(#378).
Security
- A deployment that names no trusted proxy trusts every peer. An OAuth access token is refused when the request did
not arrive over transport encryption, and that check reads the scheme a forwarded header set — so with
ReverseProxy:TrustedProxiesleft empty, anything that can open a connection sendsX-Forwarded-Proto: httpsand
has a reusable credential accepted over a clear-text hop, andX-Forwarded-Hostis believed on the same terms. Name
the addresses or CIDR networks your proxies actually use. Every startup running on the wide default logs one line
naming what the deployment gave up (#378,
#397). - The administrative endpoint is bounded by default, which is what stops a surface reachable from a network from
serving unbounded API-key guessing — the attack it is most exposed to, and the one where a successful guess is worth
the most (#373). - A redirect protects the next request and never the one that arrived: a credential sent in clear text was on the wire
before anything answered. Treat the redirect as a way to find out that a client needs repointing rather than as a
supported way to reach the endpoint (#374).
The full changelog at this tag carries the releases before this one and the surfaces each of them broke.
The published image
Both registries carry the same manifest list under the same digest, so the one to pull from is whichever your environment already reaches.
docker pull ghcr.io/krzysztof318/mailfathom:0.3.0
docker pull ghcr.io/krzysztof318/mailfathom@sha256:ebbfe02111c3b9d323dbfde74b7d303d2fb326a4b25a90e7f6cfe380c67ad968
docker pull docker.io/krzysztof318/mailfathom:0.3.0
docker pull docker.io/krzysztof318/mailfathom@sha256:ebbfe02111c3b9d323dbfde74b7d303d2fb326a4b25a90e7f6cfe380c67ad968
The Helm chart
helm install mailfathom oci://ghcr.io/krzysztof318/charts/mailfathom --version 0.3.0
The chart is published to GHCR alone, because Docker Hub has no repository path to hold it beside the image. Its appVersion is this release, and it deploys the digest above.
The databas...
MailFathom 0.2.0
The second release, and the first that had a previous one to differ from. Nothing 0.1.0 promised is withdrawn:
the MCP tool contract is unchanged, every configuration key 0.1.0 accepted is still accepted and still means the
same thing, and both schema changes are additive. There is no breaking entry below, so an upgrade is the schema step
and a new image.
What is new is how a mailbox authenticates, how quickly a change on the mail server reaches the local copy, and a
second HTTP surface — an administrative endpoint with a command-line client of its own — that an operator reaches
without going through the MCP surface.
The database schema. Two migrations, both additive: one table and one nullable column
(#343,
#346). 0.2.0 refuses to serve until they are applied —
startup is gated on the migrations the binary carries and will not migrate a database out from under a running
process — but 0.1.0 neither reads nor writes what they add, so they can be applied while 0.1.0 is still
serving, and the release then deploys over 0.1.0's data unchanged. The gate reads only what is pending, so a
database already carrying both migrations still starts 0.1.0: going back needs no schema step of its own.
Applying the database schema
records the apply path and the ordering a deployment follows.
Added
Mailbox authentication. An IMAP account can present an OAuth token instead of a password.
XOAUTH2andOAUTHBEARERare accepted in
MailSynchronization:Accounts:<n>:TransportSecurity:PermittedAuthenticationMechanisms, and naming either one turns
on that account's…:OAuthblock: the token endpoint, the client, the scope, and the grant —refresh_tokenor
client_credentials— with the client secret and the refresh token supplied by reference like every other
credential. Configuring the block for an account that authenticates with a password fails startup rather than
provisioning something nothing can use (#306).
Mailbox OAuth is the page,
including where each value comes from for the providers this was verified against.- Calls to an authorization server get a retry, timeout, circuit-breaker, and concurrency budget of their own, as the
Resilience:MailAuthorizationServerInvocationclass, rather than borrowing the mailbox session's
(#306).
Continuous synchronization. A folder change on the mail server can start a pass, instead of every change waiting
for the account's next interval.
MailSynchronization:Accounts:<n>:ModeselectsPolling—0.1.0's behaviour, and still the default — orPush
(#339).- Under
Push, a server offeringNOTIFYis watched over one connection per account covering every configured
folder, and a server offering onlyIDLEover one connection per folder
(#339,
#346).MaxSubscribedFolders(default20) bounds how many
folders one subscription may name; the rest synchronize on the account's interval rather than being dropped. - Where the server offers
CONDSTOREandQRESYNC, a pass asks what changed since the modification sequence it last
reconciled through instead of re-reading the folder, which is what the new nullable
synchronization_checkpoints.ReconciledThroughModSeqcolumn records
(#346). - Push degrades to polling rather than stalling:
MaxConsecutivePushFailures(default3) and
PushDegradationPeriod(default15 min) decide when an account falls back and for how long, and
PushRenewalInterval(default20 min) is the lifetime of oneIDLEcommand — RFC 2177's ceiling, not a polling
cycle (#339,
#341). Synchronization stays read-only throughout: a push
pass sets the remote\Seenflag no more than a polled one does.
An administrative endpoint, and the mfctl command that reaches it.
-
AdminEndpointserves administrative routes beneath/api/adminon a listener, a credential set, and a set of
authorization servers of its own. It is off by default, and a key or an issuer configured underMcpEndpoint
authenticates nothing here — the two surfaces are protected independently rather than sharing one policy
(#313,
#317). -
Each release now attaches
mfctl, a self-contained binary per platform —linux-x64,linux-arm64,win-x64,
win-arm64— plus one checksum file covering all of them. It runs where you administer from rather than where the
service runs, and needs no .NET installation
(#317). -
mfctl loginsigns in with an API key read from standard input, with a browser redirect caught locally, or with a
device code entered elsewhere, and keeps the credential in a profile file of its own with the tokens encrypted at
rest — the refresh token included, so a session outlives an access token's expiry rather than sending the operator
back through the flow (#348).
Administering your deployment
states what that encryption protects and what it does not. -
mfctl mailbox authorizeruns a mailbox's own OAuth flow from the operator's machine and sends the resulting
refresh token to the deployment, which seals and stores it, instead of printing it for the operator to paste into
a configuration file (#356). -
Administering a deployment
is the reference: every route, every--mode, the configuration, and what each failure means.One caveat, and it is a defect this release ships with: a deployment that sets
HealthEndpoints:Enabledto
falseand enables this endpoint loses its application listener, because binding the administrative socket in
code makes Kestrel ignoreASPNETCORE_HTTP_PORTSand only the probe path restates it. The process starts and serves
the administrative port alone, and every MCP client is refused.
#325 carries the fix. Until it lands, leave the probes
enabled — the default — or state the application listener as aKestrel:Endpointsentry.
Encryption at rest.
DataEncryptionconfigures a key ring: one active key, any number of retained ones, each 32 bytes of material
supplied by reference like every other credential, under which MailFathom seals what it stores. An absent section is
a valid deployment that seals nothing, and rotation is movingActiveKeyIdwhile leaving the previous key
configured, so nothing already sealed becomes unopenable
(#338).
ADR 0005
records the decision.- The refresh token an authorization server rotates is followed and stored sealed under that ring, in the new
mailbox_refresh_tokenstable, so a provider that issues a new refresh token on every exchange no longer strands an
account at the next restart (#343). - Docker Compose, the Helm chart, and the native systemd unit each provision the key by the same mechanism they
provision every other secret, and the guides state where the file goes in each
(#354).
Secret provisioning
is the contract. Back the key up with the database: nothing in MailFathom regenerates it, and a database
restored without its key restores nothing that was sealed under it.
Changed
MailSynchronization:Accounts:<n>:Secrets:Passwordis required only when the account's permitted mechanisms include
a password mechanism. It was unconditionally required in0.1.0, which every configuration written for0.1.0
already satisfies; what changes is that an account authenticating with OAuth alone now configures no password at all
(#306).
Security
- A mailbox refresh token is held sealed in the database under the deployment's key ring rather than sitting in a
configuration file or a secret file that nothing rotates, and the rotation an authorization server performs is
followed rather than lost (#343). - The refresh token an authorization flow produces never reaches the operator's terminal:
mfctl mailbox authorize
sends it to the deployment over the administrative endpoint, so it is not in scrollback, in a shell history, or in a
file somebody has to remember to delete (#356). - The administrative endpoint carries its own credentials, its own authorization servers, and its own TLS profiles, so
granting somebody administrative access does not grant them the MCP surface and the reverse holds
([#313](https://github.com/Krzysztof318/Mail...
MailFathom 0.1.0
The first public release, and the point at which MailFathom's four public surfaces begin to promise anything. There is
no earlier release for this one to have changed, so every entry below is an addition rather than a difference.
What it is. A Model Context Protocol server for your own mail. It synchronizes IMAP mailboxes read-only into a
local PostgreSQL copy and serves that copy to an MCP client as three tools, so a client can list, read, and search
mail without a request ever reaching a mail server and without a message being marked as read.
The database schema. This release creates it. One baseline migration
(#241,
#127) builds the whole schema on an empty database, so there is
no previous version to apply it beside and nothing of an earlier release's to deploy over. The migration must be
applied before the host will serve: startup is gated on the schema and refuses to start against a database that is
behind it, rather than migrating one out from under a running process.
Applying the database schema
records the apply path and the ordering a deployment follows.
Added
Mail synchronization.
- Read-only IMAP synchronization of configured accounts and folders into a local PostgreSQL copy. Synchronization
never sets the remote\Seenflag, and that invariant is proven against a real IMAP server rather than asserted
(#13,
#132). - A supervisor per configured account, each running on a schedule of its own
(#167). - Remote deletions and flag changes are reconciled back onto the local copy
(#171). - Synchronization is bounded by a configured earliest received date, so an established mailbox is not backfilled in
full on first run (#133). - Normalized metadata is extracted from stored raw MIME and persisted with the indexes a timeline query reads
(#98,
#106). - Searchable text is derived from stored mail and indexed for PostgreSQL full-text search, with a backfill worker for
mail stored before extraction existed (#110). - Folder aliases resolve to remote folders under a generation of their own, so a renamed or re-created folder is
detected rather than silently followed (#94). - Each class of outbound dependency runs under one configurable resilience pipeline — timeout, bounded retry with
jittered backoff, and a circuit breaker (#83) — and a dropped
IMAP session is recovered under it (#92).
The MCP tool contract. Served over the Streamable HTTP transport
(#135). Every call reads the local copy only, so no tool
request can wait on IMAP or change anything remotely, and every tool bounds how much mail one call can draw out.
list_emailsreturns a bounded keyset page of message summaries — at most 100, with no body text — filtered by
account, folder, and date (#136).get_email_contentreturns bounded bodies for at most 10 named emails under a shared character budget, and names
attachments only when asked (#137,
#153,
#232).search_emailsreturns a bounded ranked window of at most 50 lexical matches, each with bounded extracts
(#138,
#163).- Every descriptor declares
readOnlyHint,destructiveHint,idempotentHint, andopenWorldHint, so a client can
judge a tool before calling it. No error and no log line carries a filter value, a mailbox address, a subject, body
text, raw MIME, or an internal identifier; every published failure carries a five-digit error code instead
(#111).
What protects that endpoint. It is disabled by default, and enabling it requires stating what a client presents.
- Named, expiring API keys, and
Originvalidation for browser callers through configurable CORS
(#169). - OAuth 2.1 access tokens from configured authorization servers, judged against the issuer, this resource, the
required scopes, and an explicit list of authorized subjects — so signing in to the authorization server does not by
itself grant a user this mailbox (#183). - HTTPS on operator-provided domains and certificates, with the material proven to load, to cover the stated domain,
and not to have expired before any listener opens (#175). - Mutual TLS through named client-certificate profiles, proven against a real TLS handshake
(#177,
#196). - Per-client token-bucket and process-wide concurrency rate limits, enabled by default, so an endpoint is bounded
whether or not anyone wrote a number (#176). - A per-account mail transport security policy decides what TLS an account's connections require
(#58), and a host whose platform TLS policy refuses a mail
server can be configured to reach it anyway, and says so when it does
(#226).
The configuration schema. Every MailFathom section is bound strictly: a key the section does not define fails
startup naming it, so a typo cannot silently leave a default in force, and a violated constraint fails startup with
the configuration path in the message.
The configuration reference
is the whole surface, key by key, including which keys reload and which need a restart.
- Secrets are supplied as references rather than inline values by default, so a plain-text credential where a
reference belongs fails startup instead of authenticating
(#64). - Certificate material and secrets are re-read behind unchanged references, so a renewal reaches the process without a
restart (#73). - A mounted directory or file of JSON — a Kubernetes ConfigMap, a systemd drop-in — is a first-class configuration
source (#168). - The deployment-wide privacy bounds on what a search result may quote, and on how much body text one read may return,
are configuration rather than constants a caller could raise.
The deployment contract.
- A multi-architecture container image for
linux/amd64andlinux/arm64, published toghcr.ioanddocker.io
as one manifest list under one digest, under its immutable version tag withlatestmoved onto that same digest.
The registry to pull from is whichever your environment already reaches
(#240,
#256,
#281). - The Helm chart is published with the image, in the same run and at the same version, as an OCI artifact at
oci://ghcr.io/krzysztof318/charts/mailfathom. ItsappVersionis that release, so a chart states which application
version it deploys without being unpacked, and it is listed on Artifact Hub
(#281). - Every published artifact, image and chart alike, carries a signed provenance statement that
gh attestation verifychecks against this repository (#281). - Three supported installation shapes: Docker Compose, which provisions PostgreSQL for you; the Helm chart, which
deliberately installs neither a database nor a Secret; and a native systemd process taking its secrets as systemd
credentials (#180). Linux is the only platform this project
supports. - Startup, readiness, and liveness probes on a listener of their own, with a configurable transport, which a
deployment can turn off entirely (#198,
#264). - Each release publishes an idempotent
mailfathom-schema-<version>.sqlartifact naming the migrations it carries and
the checksum that identifies it (#258). - The declared version is written in one place and stamped from there into every assembly, the image's tags and
labels, the packaged chart'sappVersion, the host's startup record, and the server's MCPinitializeresponse
(#208). - OpenTelemetry logs, metrics, and traces export when
OTEL_EXPORTER_OTLP_ENDPOINTis set, and host start, startup
failure, and shutdown are reported from a bootstrap logger that ex...