Repository navigation
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 own run and erases at most five thousand entries a pass, so shortening a long window
clears the backlog over several runs rather than in one delete that locks the trail. AuthoredDeleteEmailDispositiondecides what becomes of the local copy of mail MailFathom itself deleted —
RetainLocalCopy,RetainTombstone, orEraseLocalCopy— separately from the setting that governs mail somebody
else deleted, because a deletion of ours and a deletion of theirs are different facts
(#554,
#561,
#563).
A ceiling on how much mail one deployment stores, and how much one run brings in.
MailSynchronization:MaxStoredContentBytes is what stops a large mailbox from filling the volume: past it, ingestion
degrades to metadata only and keeps listing and searching rather than failing, and the messages it skipped are picked
up once there is room. MaxContentBytesPerRun ends a folder run at its checkpoint instead of at the end of the
mailbox, and MaxInFlightRawMimeBytes bounds what a run holds in memory at once
(#580).
mfctl reaches a deployment whose certificate this machine does not trust, by asking once. mfctl login shows
the fingerprint, asks, and pins what you accept to that profile, so a later renewal is a question rather than a silent
acceptance; --trust-untrusted-certificate and --allow-clear-text answer the same two questions where there is no
terminal to ask on (#560).
Every exported record names the build it came from. service.version carries the semantic version and
vcs.ref.head.revision the commit, on every log record, metric, and span the host exports, so a report from a
deployment can be tied to the code that produced it (#620,
#655).
Changed
- Breaking (deployment contract) — the database is PostgreSQL 18.4 with pgvector 0.8.6, where
0.4.0ran 17.
PostgreSQL does not read a data directory an earlier major version wrote, so bringing the new image up over an
existing volume does not upgrade it — the container exits1naming the data it found, the server never listens,
and nothing that depends on it comes up. The attempt writes nothing, so the old directory is intact and still
dumpable afterwards. Move the data across a dump from a PostgreSQL 17 server before the upgrade, or delete the volume
and let synchronization refill it from IMAP — which costs the embeddings and the audit trails, since neither is in
the mailbox (#658).
Upgrading a deployment that ran PostgreSQL 17
is the sequence for Compose, and the same reasoning holds for a claim the chart wrote. - Breaking (deployment contract) — the Helm chart runs PostgreSQL itself unless you tell it not to, where
0.4.0installed none and requireddatabase.host. A values document that names a host now fails to render, because
database.hostis refused whiledatabase.deploy.enabledis on and the address is derived from the release name
instead: two values naming one server is how a deployment ends up connecting somewhere it did not install. Keep
your own server by settingdatabase.deploy.enabled: falsebeside thehostyou already have. A deployment that
takes the default instead names a second Secret indatabase.deploy.superuserPasswordSecret— separate from
secrets.existingSecret, and refused if it is the same one — because the application's Secret is mounted whole into
the pod that parses untrusted mail (#658). - Breaking (deployment contract) —
mfctlrefuses a deployment from another release line before it sends
anything. A0.4.xcommand against a0.5.xdeployment stops with a message naming both versions, because the
administrative contract is what a minor may break and a command that guesses at it is worse than one that declines.
Takemfctlfrom the deployment's own release; two builds that share amajor.minorand differ otherwise warn and
run, and a version that cannot be read warns and runs
(#628). - Breaking (configuration schema) — every account states a
DisplayName, and startup fails naming the account
that has none. It is what a client sees and what a person names an account by, it is at most 128 characters, and it
may not collide with another account's identifier or display name compared without regard to case. Add one line per
account underMailSynchronization:Accounts
(#637). - Breaking (MCP tool contract) —
list_emailsandsearch_emailstakeaccountswhere they took
accountIds. The argument was renamed because it now accepts a display name as readily as an identifier. An
argument the tool does not declare is ignored rather than refused, so a client still sendingaccountIdsis not
stopped — its account filter simply disappears, and the call reads every account the deployment serves instead
of the one it named. Update every client that names accounts before the upgrade, and readlist_accountsfor the
names (#637). - Breaking (MCP tool contract) —
get_email_contenttakesincludeAttachmentContentwhere it took
includeAttachmentDetails, and every attachment's file name, media type, and decoded size are now returned
whether or not the call asks for anything. The old argument bought the metadata; the new one buys the bytes, so a
client that passed it to see what was attached needs to pass nothing at all. It is ignored the same way when it is
still sent, which here costs nothing — the metadata arrives regardless, and no attachment content is returned
without the new argument (#633). search_emailscan report aretrievalModeit never reported before.lexicalwas the only value0.4.0
produced;hybridis a second one, and a client matching on the field exactly rather than on the results should
expect it (#555).- Every SQL statement MailFathom runs is logged at
Debugrather thanInformation. A deployment at the default
level no longer writes one log record per database command, which is where the bulk of its log volume was going —
and those records carry the text of every query the mailbox is read with. Set
Logging:LogLevel:Microsoft.EntityFrameworkCore.Database.CommandtoDebugto get them back
(#654).
Fixed
- The published
0.4.0container could not start. Its base image setsASPNETCORE_HTTP_PORTS,0.4.0refuses
that variable by design, and the Dockerfile did not clear the inherited value — so the image failed startup on a
setting nobody had written, with a message naming a variable that is not in any configuration file
(#575). - An MCP endpoint configured with OAuth and nothing else threw on every request that carried no credential,
including the health probes, which are documented as carrying none. The endpoint now answers an uncredentialed
request with the challenge it is supposed to, and the probes are served without one
(#577). - A
list_emailsdate filter written at a non-zero UTC offset failed the whole listing.receivedOnOrAfterand
receivedBeforereached the database unconverted, and anything but+00:00was refused there — so a client in a
time zone sent the value its clock produced and got a failure rather than a page. Both bounds are held as instants
now, and every offset names the same moment (#612).
Security
- A question and the mail that answered it leave no copy at the provider. A chat endpoint declaring
Api: Responsesreached an API that retains what it is sent for thirty days by default and makes it readable in the
provider's dashboard — so adopting that API would have placed the operator's correspondence in a third party's log
because of a default nobody wrote. Every request states that it is stateless, and the model's reasoning is carried
between turns as the encrypted content the provider returns rather than by leaving the conversation behind
(#636). - What leaves the process to answer one question is bounded and countable. The passages a run may send, how much of
any single message goes with them, and the total that may cross the boundary for one question are each configured and
each enforced before the call rather than after it
(#592). - A database the chart deploys keeps its superuser credential out of the pod that parses mail. The application's
Secret is mounted whole, because MailFathom reads the keys your own configuration names, so the superuser password
lives in a second Secret the application never mounts and the chart refuses a values document that names one Secret
for both. The role MailFathom connects as is never a superuser in either arrangement
(#658).
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.5.0
docker pull ghcr.io/krzysztof318/mailfathom@sha256:1e24901d24de2af35a335e4dc889d8e37432f199611b14fbc313bb47309a5a98
docker pull docker.io/krzysztof318/mailfathom:0.5.0
docker pull docker.io/krzysztof318/mailfathom@sha256:1e24901d24de2af35a335e4dc889d8e37432f199611b14fbc313bb47309a5a98
The Helm chart
helm install mailfathom oci://ghcr.io/krzysztof318/charts/mailfathom --version 0.5.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 database schema
Apply mailfathom-schema-0.5.0.sql before rolling this version out, and take a backup first. MailFathom applies no schema change while starting and refuses to serve against a schema it does not recognize, so the order is: back up, apply the script, then deploy the image.
| Artifact | mailfathom-schema-0.5.0.sql |
| SHA-256 | 6168428369c5a76139fbc4bba950a657a7c1934af944f6fba14ee81b2669ccfe |
| Migrations | 20260801190024_Initial 20260803190706_AddMailboxRefreshTokens 20260803191836_AddSynchronizationCheckpointModSeq 20260806132916_AddEmailChunks 20260807125156_AddEmbeddingProfilesAndEmailEmbeddings 20260807130550_AddMailboxMutations 20260807143735_AddMailboxMutationSourceRemoval 20260807154642_AddMailboxMutationObservations 20260807222509_AddAuthoredDeleteLocalDisposition 20260808113315_AddMailboxMutationAuditEntries 20260808113641_AddEmbeddingProfileLifecycleUniqueIndex 20260808145001_AddStoredEmailAwaitingContentIndex 20260808150849_AddEmbeddingSpendPeriodsAndChunkedTextTruncation 20260808184600_AddMailAnsweringAuditEntries |
The script is idempotent, so an installation already carrying some of those migrations takes only what it is missing. Applying the database schema states the privileges it needs, the locks it takes, and what each startup failure means.
The administration command
Download the binary for the machine you administer from, not for the machine the service runs on: mfctl reaches a deployment over HTTP and runs on Linux and Windows alike. Verify it against mfctl-0.5.0.sha256, make it executable, and run mfctl login --endpoint https://host:port. Administering a deployment states what the service has to have enabled before it will answer.
On Windows there is a packaged path as well. winget install MailFathom.mfctl installs this version once the Windows Package Manager community repository has accepted the manifest this release submits to it, which is a review by somebody else rather than a step of this pipeline.
None of the binaries carries a code signature, so Windows warns about an unknown publisher and mfctl-0.5.0.sha256 is what verifies a download — on every platform.