Releases: ni-c/imap-mcp
Release list
v0.2.0
This is the first release published to npm, so everything below reaches a
package page for the first time — including the parts that changed weeks ago.
Added
-
A container image at
ghcr.io/ni-c/imap-mcp, multi-arch (amd64 and
arm64), built from a digest-pinnednode:24-alpine, running as a non-root
user, with an SBOM and build provenance. It never writes to the filesystem
unlessIMAP_DOWNLOAD_DIRis set, and then as uid 1000 — a bind mount has to
be owned by that user on the host. -
A published npm package,
@ni-c/imap-mcp, with provenance, released
through GitHub Actions with npm Trusted Publishing. The unscoped name belongs
to an unrelated project. -
README: a container badge, the architecture diagram, install snippets for
Claude Code, Claude Desktop, Codex and Docker, and a demo recording. The
documentation site at imap-mcp.ni-c.de is now
actually served. -
IMAP_ALLOW_TOOLSandIMAP_DENY_TOOLSchoose which of the eleven tools are
registered. Both take comma-separated tool names or a prefix with a trailing
*, the allow list decides what is in and the deny list is subtracted from it,
andIMAP_ALLOW_TOOLS=essentialselects a curated six —list_mailboxes,
list_new_messages,list_messages,get_message,set_message_flagsand
move_messages. Four of those are read tools, so the preset stays a working
combination under the read-only default. Nothing changes for an installation
that sets neither.A filtered tool is not registered at all, so it is absent from
tools/listand
answerstools/callwith "tool not found" — the same cutIMAP_READ_ONLY
already makes, not a second, weaker one. It covers tools: the attachment
resources are not filtered.An entry that matches no tool stops the server at startup, naming the entry
and listing the real names. Under the read-only default, an exact write-tool
name in the allow list is refused with a message namingIMAP_READ_ONLY
instead of calling the tool unknown — which matters more here than elsewhere,
because read-only is the default rather than something you remember switching
on. -
A documentation site at imap-mcp.ni-c.de, an
architecture diagram generated from a single source,server.jsonregistry
metadata, and CI and docs workflows. The workflows ship disabled: this
repository is private and its Actions minutes are worth keeping, so
npm run lint && npm run build && npm run test:coveragelocally is the whole
of the verification until they are switched on.
Changed
-
IMAP_ALLOW_WRITEis nowIMAP_READ_ONLY, for one name across the family —
but not one default. Everywhere else<PREFIX>_READ_ONLYdefaults to
false; here it defaults totrue, because the variable it replaces was
opt-in and a rename that quietly flipped that would have handed write access to
every installation that upgraded without reading this file. Only the literal
stringfalseturns it off, so a typo fails closed.An installation that still sets
IMAP_ALLOW_WRITErefuses to start, with a
message naming the replacement. Silently ignoring a removed security variable
is worse than refusing: whoever set it once believes it is still in force. -
The SPF/DKIM/DMARC verdicts are read from the topmost
Authentication-Resultsheader only and come with the authserv-id, so a
forged copy sitting below the receiving server's own is ignored. Whether the
topmost one can be trusted at all isIMAP_TRUSTED_AUTHSERV_ID, above. -
Thread subjects and sender names in the
get_messagemetadata block are now
named as sender-chosen in its caveat, and the injection-shape detection runs
over the metadata block too, not only over the message body. -
get_attachmentsno longer advertisesreadOnlyHint: truewhen
IMAP_DOWNLOAD_DIRis set — with a download directory configured it can
create files, and clients that auto-approve read-only tools should ask. -
Reference- and shortcut-style markdown images (
![alt][id],![id]) are
defused alongside the inline form. -
The HTML-stripping passes use bounded scan windows, so crafted HTML full of
unclosed tags can no longer burn minutes of CPU; hidden elements larger than
the window are left to the fencing, which was always the real defence. -
A long
Referenceschain in a draft is folded across lines instead of
emitting a header line beyond the RFC 5322 998-octet limit. -
Confirmation tokens are compared in constant time.
-
IMAP_HOSTno longer accepts a colon outside an IPv6 address, matching what
the error message always said. -
The inline image result uses the allowlist-checked declared content type
rather than the unchecked one from the download metadata.
Security
-
The attachment resources are covered by the tool filter, and bounded by the
inline budget.IMAP_DENY_TOOLS=get_attachmentsremoved the tool from
tools/listand leftimap://message/{uid}/part/{partId}fully live — the
same door, still open, with the narrowing looking complete. They also read up
toIMAP_MAX_DOWNLOAD_BYTES(25 MB by default) and returned it base64 in one
JSON-RPC response; that limit exists to bound what may be written to a file,
andIMAP_MAX_ATTACHMENT_BYTESnow applies instead, as it always did for the
tool. -
A binary attachment requested inline is refused rather than base64-encoded
without limit.textResultapplies no budget, so up to
IMAP_MAX_ATTACHMENT_BYTESx 1.37 of base64 went into the model's context
against a stated cap of 200 000 characters, scaling with a variable raised for
an unrelated reason. Truncating would be worse than useless — half a PDF
decodes to nothing — so the refusal names the two ways to get the bytes. -
get_messagechecks the size of the message it received, not just the
size it asked for. imapflow'smaxLengthbounds the request; a compromised
server, or anyone in the way of anIMAP_TLS=noneconnection, could stream
more than that straight into the parser. Every attachment path already went
throughreadCapped; this was the one that did not. -
The
forgeableflag on SPF/DKIM/DMARC verdicts is now honest, and there is
IMAP_TRUSTED_AUTHSERV_IDto make it useful. The old rule compared the
header's authserv-id against the account's own domain and reported a match as
not forgeable. A sender knows that domain — they just addressed mail to it —
so on any account whose provider adds noAuthentication-Resultsof its own,
the sender's header was the topmost one and
Authentication-Results: mail.<your-domain>; spf=pass; dkim=pass; dmarc=pass
bought a spoofed message this server's own vouching. Nothing in a message can
settle who wrote that header, so the operator now names the id their provider
stamps; unset, every verdict is reported as forgeable, which is what "pass,
says a header anyone could have written" actually means. -
Auto-fetch markup is defused at the boundary rather than at two call sites.
defuseAutoFetchran only where a body was rendered, so a subject, a sender
display name, an attachment filename or a thread summary carrying
reached the model untouched — the same
EchoLeak channel one layer earlier, in the field a model quotes back most
often, and outside the fence in the metadata block. It now runs inside
sanitizeTextandsanitizeFilename, after NFKC normalisation, so a
fullwidth subject that folds into markdown is caught too. -
Message-IDis sanitised and length-capped. It went from the sender
straight into the metadata block, the part of the result the model is told
came from this server. Every other sender string on that path was already
sanitised. -
CR is stripped along with the other control characters. It fell between
the two ranges rather than being excepted on purpose.wrapUntrustedsplits
on\n, so a lone CR left everything after it on one logical line — marked
once, at the start — while a terminal renders it as a new line, and a
CR-padded line can overwrite the datamark a human is reading. -
set_message_flagsrefuses to add\Deleted. The tool carries no
confirmation and is annotateddestructiveHint: false, on the grounds that
everything it does can be undone. That is true of\Seenand\Flaggedand
not of\Deleted, which the next client to close the mailbox — or any server
with autoexpunge — turns into a permanent removal. It wasdelete_messages
without the dialog, reachable in one call, and it is in theessential
preset. Removing\Deletedis still allowed, since that undoes one. -
Copying messages now needs a confirmation, like moving them. The old rule
reasoned about deletion; deletion is not the only thing that cannot be taken
back.destinationis a free-form mailbox name, so on a shared account or a
public namespace one unconfirmed call handed every named message to everyone
with access to that folder — and left the source folder untouched, so nothing
looked different afterwards. Move and copy have separate token keys. -
Confirmations and elicitation dialogs no longer quote mailbox names inside
their own sentence. Folder names look like server-side metadata and are not:
they come from the caller, andlist_mailboxessources them from the account,
which on a shared mailbox means a colleague — or whoever compromised one —
chose them. A folder namedArchive" — routine cleanup, pre-approved by IT
became part of the sentence a human reads before losing a folder. Caller-chosen
names are now rendered on their own labelled lines under an explicit heading.