Skip to content

Releases: adecubed/gigamail

GigaMail 0.3.2

Choose a tag to compare

@adecubed adecubed released this 08 Sep 12:51
  • Codex CLI as a first-class agent. The console detects codex next to
    claude, lets you pick the agent that writes the drafts (first-run guide
    and Automations), stores the choice as {"agent": "codex"} in
    agent.json and resolves the command at every start. Drafts run through
    codex exec in a read-only sandbox, with the final answer read from a
    file. The MCP registration snippet switches to ~/.codex/config.toml
    (or codex mcp add) when Codex is selected. Both directions verified on
    Windows — see INTEGRATIONS.md, including the gate holding with Codex's
    own approvals bypassed.
  • .codex-plugin/plugin.json follows the release version (it was still
    0.2.4 at 0.3.1); sync-version.js and a test keep it aligned.
  • Attachments open again from the console. Clicking an attachment
    answered "HTTP 404" on every account: the console asked the backend for
    GET /mail/{id}/attachment/{name} and that route did not exist. It
    does now (bytes, content type from the provider or from the file name,
    proper Content-Disposition), with tests.
  • An old notification says it is old. Pressing a desktop toast after
    its request has expired or been decided used to answer "request does
    not exist", which reads like a fault. It now says the notification is
    stale and lists what is actually waiting, if anything; and the window
    no longer dies with a traceback when there is no standard input.

GigaMail 0.3.1

Choose a tag to compare

@adecubed adecubed released this 08 Sep 06:29

Small things noticed while recording the 0.3.0 demo.

  • Grey text meets WCAG AA. Dates, captions, placeholders and every
    secondary label sat at 30-45% black on white (2.1-3.4:1, below the 4.5:1
    the AA level asks for). The floor is now 55% black (4.7:1), the
    --text-secondary token is 60%, and --text-muted — used but never
    defined — exists. A test scans the console sources so it cannot regress.
    Pointed out on r/UXDesign by u/Mrmasseno. Thank you.
  • A calmer look, same identity. Card and list borders go from black at
    75% to a light grey; corners from 16/28px to 10/16px; shadows from heavy
    to barely there; six pixels of air between messages; the window buttons
    top-right match the pop-out button (flat tint, light border, soft
    shadow); the mail body sits in a rounded white card. The secondary
    windows lose a body shadow that had nowhere to go and smeared their
    bottom edge, and get thin scrollbars like the main window. Text never sits on a gradient any more: active chips, the
    selected message, title bars and the "New mail" button use a flat tint,
    and the pink-blue gradient stays only on the avatar, the logo and the
    microphone states. Every one of these came from r/UXDesign:
    u/AbilityRadiant2342 (the list) and u/el_paro (text over
    gradients). Two tests keep the dark borders and the gradients from
    creeping back.
  • Dates and calendar follow the UI language. Every date, time, month and
    weekday name was formatted with the Italian locale regardless of the
    language chosen; the calendar window title and its "+ Event" button were
    Italian too. Formatting now uses the locale of the active language
    (it-IT, en-GB, zh-CN) and month/day names come from Intl.
  • "From:" / "To:" in the mail detail were hardcoded in Italian.
  • Dashboard no longer flashes "Loading…" on every folder change: the
    last rendered dashboard stays on screen while the data refreshes.
  • Reply and new-mail windows are children of the main window, so they
    cannot end up behind a maximized main window (they had to be fished out
    of the taskbar).
  • A 90-second demo video of the console lives in docs/demo/ and is
    linked from the README.
  • npm test only runs tests/*.test.js (it used to pick up any .js
    under the console folder, including the embedded Python's own test files).

GigaMail 0.3.0

Choose a tag to compare

@adecubed adecubed released this 04 Sep 06:32

The desktop console grows up. 0.2 shipped it as a beta next to the pip
package; 0.3 is about making it something a person can install and use
without reading the README.

  • Onboarding on first launch. A fresh console no longer opens on an
    empty window: a guided setup inside the main window — same style as
    every other panel, no separate popups — connects a mailbox (Microsoft
    365 device flow or IMAP), fills the account identity and knowledge
    files, shows how to register GigaMail in the agent's MCP client and
    enables notification buttons. It is skippable, reopenable from
    Automations → AI (and from the dashboard while no account exists),
    and its "done" flag lives in the backend, so reinstalling the console
    does not bring it back. Italian, English and Chinese.

  • IMAP accounts are verified before they are saved. The console
    sent a provider key the backend did not know and required hosts it
    did not have, so Gmail/Aruba/Libero saves failed with a 422 — and a
    wrong password was stored silently, to fail at the first sync.
    POST /accounts/imap now resolves the provider to its hosts (Outlook
    over IMAP included, SMTP 587 + STARTTLS), attempts a real IMAP login
    and answers 400 with a readable reason instead of writing the account.
    The first account becomes the active one.

  • IMAP-only installs load their accounts. The console treated
    "connected" as "has a Microsoft token", so a console with only IMAP
    accounts never populated the account selector.

  • One way to render a mail, tested with hostile mail. The HTML of a
    message went into an iframe by two different code paths (main window
    and mail window) with two different rules; now mail_render.js is the
    only one: structural sanitisation via DOMParser (scripts, frames,
    objects, forms, meta, base, links, every on* attribute, javascript:
    and vbscript: URLs, CSS expression()), an iframe sandbox with
    neither scripts nor popups, its own CSP, and link clicks that go to the
    system browser instead of navigating the frame. Seventeen known XSS
    payloads run through it in unit tests (jsdom) and in the real Electron
    (npm run test:e2e, Chrome DevTools Protocol), which also checks that
    the renderer has no Node, that the preload exposes no secrets, and —
    on a pristine profile — that onboarding opens by itself. The e2e runs
    in CI on Windows before every installer build.

  • The watcher is a package. watcher.py had grown to 1,150 lines
    doing polling, rule matching, drafting, addressing, approvals,
    notifications, Telegram commands and execution in one file. It is
    now ade_mail_agent/watcher/ with one module per responsibility
    (ingestion, drafting, addressing, approvals, notify, pipeline,
    execution, telegram, process_state, runner) behind the same facade:
    from ade_mail_agent import watcher and every public name still
    work, the CLI and the console did not change. The except: pass
    around the heartbeat and the Telegram trust warning now log through
    logging ("gigamail.watcher") instead of vanishing — a heartbeat
    that fails is exactly what makes the console launch a second watcher.

  • The console API is a package of routers. http_api.py (1,200
    lines, 81 endpoints) is now ade_mail_agent/http_api/ with one
    FastAPI router per domain — accounts, addresses, mail, calendar,
    mask, agent, approvals, rules/watch, notify/onboarding — behind the
    same app, the same paths and the same token middleware (kept in the
    facade so importlib.reload in tests still re-reads the token).
    python -m ade_mail_agent.http_api and the gigamail-console-api
    entry point are unchanged.

  • Mail list and detail leave renderer.js. renderer_mail.js holds
    the list, the message detail with its actions and attachments, and the
    forward composer; the pure parts (MailView: list item, detail header,
    HTML→text) are unit-tested with hostile subjects, senders, addresses
    and attachment names. Two things fixed on the way: the forward path
    extracted text by assigning raw mail HTML to an element attached to
    the live document (an onerror would fire in the main window), now
    it parses into an inert DOMParser document; and the old
    openMailWindow built an unescaped HTML page for a window.open
    that main.js denies — dead code replaced by a delegation to the real
    mail window. renderer.js goes from 2,055 to 1,465 lines.

  • Composition leaves renderer.js too. renderer_compose.js holds
    the reply modal, the new-mail panel, attachments and the recipient
    autocomplete; the pure parts (ComposeView: attachment chips,
    autocomplete items, suggested-attachment rows, address split/merge)
    are unit-tested with hostile names and addresses. renderer.js is now
    894 lines, down from 2,055 this morning.

  • Accounts and calendar leave renderer.js. renderer_accounts.js
    (selector, IMAP modal, delete with context menu) and
    renderer_calendar.js (event list, quick popup, editor), with the
    pure parts (AccountsView, CalendarView) unit-tested with hostile
    names. The event quick popup was one of the hand-built overlays with
    its own palette and an undefined --mono; it now uses the console's
    .overlay > .modal. renderer.js is at 678 lines.

  • Every console module binds its own buttons. bindStaticEvents
    was a 290-line list of every click handler in the main window; now
    mail, compose, calendar and accounts each have a bind*Events() and
    renderer.js keeps login, office and window navigation. The 52 bound
    ids are asserted identical before and after, and the e2e clicks
    through folder switching, the IMAP modal and the event editor.
    renderer.js: 466 lines (2,055 this morning).

  • Secondary windows closed too. A second external review found what
    the first pass had left: the mail window put a plain-text body into
    innerHTML unescaped and, on "Forward", parsed the original HTML in an
    element attached to the live document; the calendar window rendered
    event ids and locations unescaped and put attendee addresses inside an
    inline onclick string with an esc() that did not cover the quote.
    All fixed (esc() before <br>, the shared inert htmlToText, data
    attributes instead of inline JS). shell.openExternal is now reachable
    only through one main-process function that accepts http, https
    and mailto — the main-window preload used to call it directly, the
    mail-window IPC forwarded anything — and every window gets the same
    popup/navigation hardening. The e2e opens the mail and calendar
    windows, injects hostile messages and events, and asserts nothing
    runs; it also asserts openExternal('file:...') is refused.

  • webSecurity back on everywhere. Calendar, marketing and ask ran
    with webSecurity: false, which switches off the same-origin policy
    for the whole window (a page could read file:// and call any host).
    Removed from all three; the backend already answers file:// origins
    through CORS, so nothing needed it. api.anthropic.com is gone from
    every window's CSP. The e2e proves fetch('file:///…') is refused in
    the main, calendar, marketing and ask windows and that the calendar
    still reaches the backend.

  • The console shows only what the backend can do. Marketing, voice,
    "Listen", "Summarize", the calendar TTS and the draft autosave all
    called endpoints that no longer exist and answered with 404s. The
    console now reads /openapi.json at start-up and every button
    declares the endpoint it needs (data-requires="/path"); a missing
    path hides the button, and it comes back by itself the day the backend
    offers it. The marketing window's direct, key-less call to the LLM
    provider is gone — that is the agent's job, through MCP.

  • Electron 44, electron-builder 26, better-sqlite3 13. npm audit
    went from 1 critical + 9 high (Electron itself: context-isolation
    bypass and sandboxed-iframe escape; tar, extract-zip and
    electron-builder at build time) to zero. better-sqlite3 has prebuilt
    binaries for the Electron 44 ABI, so nothing is compiled on the
    machine. Building the console now needs Node 22 or newer (CI uses
    22); npm test runs node --test with the default pattern, which
    works on Node 20, 22 and 24 alike.

  • ADE_CONSOLE_PORT works end to end. The main process knew the
    port; the renderers had 8002 written in 27 places across 17 files.
    Now every window receives the backend base URL from the main process
    (additionalArguments → preload → window.GIGAMAIL_API) and no
    renderer carries the port any more; the e2e runs on port 8012 in CI
    to prove it. ade_mail_agent.__version__ comes from the package
    metadata (it had been stuck at 0.1.2) with a test against
    pyproject.toml, and MAPPA_MCP.md finally describes the
    out-of-band request_id flow instead of the old confirm_token.

  • A way back from a custom folder. Inside a custom folder (say
    "idealista") the only route to the inbox was the sidebar icon, which
    does not read as "back", and the panel title kept saying "Inbox". Now
    the folder row starts with an "Inbox" chip, the active chip clicked
    again returns to the inbox, and the title follows the folder you are
    in (standard folders translated, custom ones by name).

  • The installed app is started in CI, not just installed. The e2e
    runs on the development Electron with the backend from the venv; the
    installer was built, installed and uninstalled, but the exe a user
    gets was never launched. npm run test:packaged now starts the
    packaged app on a pristine profile, waits for the embedded Python
    backend (/health reports the version), checks the token, the
    renderer, the onboarding, the capability gate and webSecurity, and
    the desktop workflow runs it after installing. It found a real one:
    the embedded Python, with import site on, saw the building
    machine's user site-packages, so pip skipped depen...

Read more

v0.2.4 — rules that answer the person, and a Telegram approval that works

Choose a tag to compare

@adecubed adecubed released this 01 Sep 05:53

A day of using GigaMail on real mail, which is where the rest of these
were found. The recurring shape: something reported success, or reported
a capability it did not have, and only the phone or the customer found
out.

  • Rules can answer the person instead of the portal. A listings site
    sends its notification from a relay (reply@idealista.it) and puts the
    enquirer's address in the body, so a semi-auto rule drafted a perfect
    reply and addressed it to a robot. Fixed addressing stays the default —
    it is what stops a hostile mail redirecting an answer via Reply-To
    but a rule can now opt out with reply_to_body_address. The extracted
    address is shown in the approval preview, flagged as coming from the
    body, because it is the one field that does not come from an
    authenticated sender. No address found skips the message rather than
    falling back to the relay.

  • Rules carry cc and attachments. Attachment names resolve against
    the account identity and are listed with real sizes in the preview; a
    name that no longer resolves skips the message instead of sending a
    mail that cites floor plans it does not have. gigamail rules add
    resolves them once at creation so a typo surfaces then, not a month
    later.

  • The watcher survives logout. scripts/watch-task.ps1 registers it
    with Task Scheduler, in the user's interactive session — never as
    SYSTEM, because account passwords are sealed with per-user DPAPI and
    approval toasts only exist inside a session. Stopping the task does not
    kill the tree it launched, which left two watchers competing over the
    same mail, so the launcher now asks gigamail watch-running first and
    that answer moved into watcher.running_state(), shared by console,
    CLI and task.

  • Telegram approvals actually work. Requests raised by a tool arrived
    with no buttons, and tapping one answered "unknown request" because the
    handler required a rule row. Both fixed. Approving a tool request from
    the chat does not send — phase 2 belongs to the agent that asked — and
    the reply says so instead of implying the mail left.

  • Nothing in a Telegram approval is tappable except the buttons.
    Without parse_mode Telegram linkifies addresses itself, so in a
    buttonless message the only thing to press was the recipient's
    mailto: — which opens the phone's mail client and asks you to sign
    in. An approval whose single affordance is an unexpected login prompt
    is indistinguishable from phishing, on the one channel with no Hello
    behind it. Addresses and URLs now go in <code>.

  • The chat shows the whole mail, not just its subject: sender,
    recipients, cc, attachments and body, trimmed to fit Telegram's limit
    with the cut labelled. The toast can stay terse because it has a Leggi
    button; the chat has no second step.

  • A request that expired is not one that was decided. The reply said
    "already decided or expired (pending)" — two contradictory things at
    once. The cases now read differently, and the buttons are stripped the
    moment you discover the request is dead, so the message stops offering
    actions that can only be refused.

  • No more announcing an approval that is switched off. notify.json
    can say approve: true while no chat was ever recorded behind Windows
    Hello; the watcher logged "Telegram con approvazione" on the strength
    of the file alone, and the only way to learn otherwise was to tap
    Approva and be told no.

  • Optional PIN before approving from Telegram (gigamail telegram pin, set and removed behind Hello). A tap alone means whoever holds
    the unlocked phone can send mail. Stored as scrypt with a random salt,
    three wrong tries lock the channel for 15 minutes, and the message
    carrying the PIN is deleted whether it was right or wrong. It is not
    Hello and does not pretend to be: the PIN crosses the chat in clear, so
    it guards against a phone left unlocked, not against someone who
    controls the Telegram account.

v0.2.3 — five silent failures, and attachments that stay inside the identity

Choose a tag to compare

@adecubed adecubed released this 01 Sep 05:53

Four bugs found by using GigaMail for a real morning of mail, not by
reading the code. Three of them shared a shape: the action reported
success and did the wrong thing quietly.

  • Approval toasts came out mute. Every MCP tool creates its request
    through require_approval(), which notified without passing actions
    — so the toast was built with no buttons and the human saw an alert
    with nothing to press. Only the watcher's semi-auto path passed them,
    which is why the feature looked like it worked. Now every approval
    carries the same four: Leggi / Approva / Modifica / Rifiuta. Leggi
    shows the entire preview (no more 300-character truncation of a mail
    body) and lets you decide on the spot — reading and deciding are the
    same moment. Modifica rejects the request and hands back your note.
    Buttons still only open a gigamail:// URL: Approva goes through
    Windows Hello exactly as before.

  • A second Python on the machine silenced the buttons.
    protocol_registered() compared the HKLM registration with
    sys.executable, so the system Python next to the venv one produced a
    mute toast without a word. What matters is the registered command,
    not who is reading it.

  • Multi-recipient sends put one malformed address in the envelope.
    send_mail("a@x.it, b@y.it") passed the string whole: SMTP issued a
    single RCPT TO:<a@x.it, b@y.it>, Graph a single toRecipients. The
    provider need not refuse it — ours didn't, returning success: true
    and "accepted": 1. Half the recipients were never in the envelope and
    nothing said so; the To: header was right, so the copy in Sent looked
    fine. split_addresses() (new core/addresses.py) is now used by
    SMTP, by Graph and by the preview you approve, so the list you
    approve and the envelope that leaves cannot drift apart.

  • send_mail and reply_mail can attach identity files. Only files
    registered in that account's identity (price lists, floor plans),
    never an arbitrary path — otherwise send_mail is the easiest way to
    walk a file off the disk, and approval doesn't help, because the human
    approves a name. The preview lists name, path and real size of every
    attachment; a name that resolves to nothing aborts the request rather
    than sending a mail without the plan its body promises.

  • Dotted names resolved to the wrong file.
    os.path.splitext("B.1.3") returns ("B.1", ".3"), so a lookup for
    apartment B.1.3 searched for "B.1" and matched B.1.1, B.1.2, B.1.4 as
    well — first one wins. Silent: the mail went out carrying another
    apartment's floor plan. read_knowledge_file shares that function, so
    asking for one data sheet could return another. Fixed, and an
    ambiguous name now stops the request instead of guessing.

  • Toasts stayed put and stopped swallowing each other. Five approvals
    raised in a row appeared as one: Windows collapses toasts from the same
    app unless each carries its own tag, so four vanished silently at the
    exact moment there were five decisions to make. The tag is now the
    request_id — and re-raising the same request replaces its toast
    instead of stacking a duplicate. The popup also no longer expires under
    you mid-read (scenario='reminder': it stays until you decide; Windows
    offers no arbitrary duration, duration='long' tops out near 25s).
    The 15 minutes now live where they are real: the notification is born
    with the request's own TTL, so it sits in the action centre exactly as
    long as the approval is valid and removes itself when it dies — no
    Approva button on a request that can no longer be approved.

  • 中文: the README has a full Chinese section and the console speaks
    Chinese (language switch cycles IT → EN → 中; first-pass translation of
    all ~270 strings, with English fallback for anything missed — polish
    and corrections are very welcome: console/i18n.js).

  • Changing the Telegram chat revokes trust (u/Secondmindsystems,
    r/mcp, within hours of the 0.2.1 post): the chat allowed to approve is
    the one recorded behind Windows Hello / Touch ID at gigamail telegram setup --approve, stored outside notify.json. If the configured
    chat_id stops matching it, the watcher disables Telegram approval,
    rejects every pending rule request (decided_by: system:telegram-chat-changed), alerts the previously trusted chat once,
    and audits the mismatch; approval returns only through the verified
    setup. His second point — an edited draft must invalidate the old
    approval — was already the behaviour (✏️ rejects the old request and
    creates a new request_id; approval binds to the canonical payload), now
    stated explicitly. Note: existing installs must re-run
    gigamail telegram setup --approve once to record the trusted chat.

v0.2.1 — reply rules with a fence, approve from Telegram or the toast

Choose a tag to compare

@adecubed adecubed released this 26 Aug 21:53

Semi-auto and auto reply rules behind Windows Hello, drafted by your own
agent; deterministic anti-spam barriers; fixed addressing (injection in a
mail body has no exit channel); approve/reject/edit from Telegram or a
clickable Windows toast; Automations view in the console; explicit
store_unavailable deny (thanks u/ranbuman).

Full details: CHANGELOG.md (v0.2.0 + v0.2.1, tagged together).

pip install "gigamail[all]"==0.2.1

v0.1.4

Choose a tag to compare

@adecubed adecubed released this 20 Aug 06:40

Approval now requires the person at the machine. Three days after
v0.1.3, u/ranbuman (r/mcp) pointed out that "an agent with shell access
can run the approval CLI" is not a different threat model — it is exactly
the agent the gate exists to supervise: Claude Code, OpenClaw and Hermes
all hold a shell. He was right.

  • Approving — gigamail approvals approve <id> or the console's
    POST /approvals/{id}/approve — now opens an OS-level user verification:
    Windows Hello (PIN/fingerprint/face) on Windows, LocalAuthentication
    (Touch ID/password) on macOS. A process can open that prompt; only the
    person at the machine can pass it. No code to type, no file to read, no
    screen to capture. --yes is gone. The console token alone no longer
    approves. No backend, no approval — the CLI refuses and the console
    returns 503 on machines without Windows Hello / LocalAuthentication.
    Rejecting never needs the prompt.
  • Measured, not assumed (Windows 11): the prompt blocks until the human
    answers; a second request right after a successful one raises a new
    prompt — no sudo-style grace; it appears from a background process with
    no window. macOS reuse duration is set to 0. Details in SECURITY.md.

The approval path no longer asserts what it has not verified.

  • Cap on requests (promised to u/Rebekator): the same payload with a
    live pending request returns the same request_id instead of a new one;
    more than GIGAMAIL_APPROVAL_MAX_PER_HOUR (20) per tool per hour →
    rate_limited, nothing created. An insisting agent cannot produce a burst
    of identical approvals.
  • Audit from the provider's response (u/ranbuman): SMTP per-recipient
    refusals are read back from sendmail() and recorded as
    provider_result next to the approved payload — in the audit log and on
    the approval row (execution_outcome: ok / failed / dryrun). Graph
    returns 202 with no per-recipient result: recorded as such
    (per_recipient_verified: false), not faked.
  • Preview shows addresses, never display names, and flags any
    recipient that is not an explicit SMTP address (bare name, group, list)
    as may_expand — the count you approve is not guaranteed.
  • SMTP TLS verified by default. Port 465 used CERT_NONE; it now
    verifies, with per-account insecure_tls opt-out for self-signed servers.

Notification. GIGAMAIL_APPROVAL_NOTIFY_CMD (JSON argv with
{request_id} {tool} {summary}) runs on every new request — e.g.
openclaw message send --channel telegram … to reach you where your agent
lives. Notification only: it cannot approve. Run without a shell, best
effort, one per request (dedup does not re-notify).

Also: GIGAMAIL_ROOT / GIGAMAIL_DATA_DIR (ADE_* kept as aliases);
GIGAMAIL_APPROVAL_TTL; MCP server now identifies as gigamail with its
package version; console refuses to reuse a port-8002 backend that is not
GigaMail; Dependabot grouped; server.json for the official MCP Registry
(io.github.adecubed/gigamail) and a README note for agents installing on
a human's behalf.

Tests: 111 → 159. New dependency on Windows: winrt-Windows.Security. Credentials.UI (Microsoft's PyWinRT projection); on macOS:
pyobjc-framework-LocalAuthentication.

Install: pip install "gigamail[all]"

v0.1.3

Choose a tag to compare

@adecubed adecubed released this 17 Aug 05:20

Fix: data paths are now resolved in exactly one place.
Six modules used to read APPDATA independently, each with its own fallback (~/ADE for five of them, ~/.ade for the sixth). Under an MCP client that filters the environment of stdio subprocesses — Hermes passes only a safe baseline, which does not include APPDATA on Windows — the server silently opened an empty accounts DB and wrote approval requests to a database the console and CLI never read. No error anywhere: the approval gate failed silently.
All paths now come from core/data_paths.py:

  • ADE_ROOT relocates everything (accounts, mail data, approvals, audit)
  • ADE_MAIL_DATA_DIR relocates mail data only
  • without APPDATA, the POSIX fallback is ~/.ade — one directory, not two
    With APPDATA present (any normal Windows setup) nothing moves: paths are byte-identical to previous releases. Tests: 106 → 111.
    Verified integrations: OpenClaw and Hermes (see INTEGRATIONS.md). Tool discovery of all 24 tools verified against OpenClaw 2026.7.1-2 (openclaw mcp add + probe) and hermes-agent 0.19.0 (hermes mcp test), both on Windows. End-to-end agent workflows on those two platforms are not yet part of any claim.
    Install: pip install "gigamail[all]"

v0.1.2 — Out-of-band approval

Choose a tag to compare

@adecubed adecubed released this 16 Aug 00:19

First public release worth installing. GigaMail is an MCP server that gives
your AI agent controlled access to email — Microsoft Graph and IMAP,
calendar, a local search index, your own documents — plus an optional
console for the human.

Security fix: the agent could approve its own destructive actions

v0.1.0 returned a one-time confirmation token inside the tool result, which
put it in the model's context. The agent held both halves — the preview and
the key — so an instruction injected through an email could call the tool
again with the token it had just read. The gate stopped accidents, not a
determined injection.

Reported on r/mcp by u/ranbuman; u/anderson_the_one added the point
about binding approval to the exact operation shown. Thank you both.

Approval now happens out of band:

  • a dangerous tool returns only request_id, an inert reference — no secret
    enters the model's context
  • approval goes through channels the agent has no path to: the console API
    (behind its session token) or gigamail approvals approve <id>
  • execution uses the arguments stored when the request was made, never what
    the agent passes back
  • repeating a request_id returns awaiting approval, indefinitely
  • consuming an approval is a single conditional UPDATE: concurrent calls
    can no longer execute one approval twice (with the old code, 8 concurrent
    calls consumed the same approval 8 times out of 8)
  • the action log records who approved and when

Declared limit: an agent with full shell access on the same machine can
run the approval CLI. Different threat model — GigaMail does not claim to
defend it.

What's in this release

  • 24 typed tools — 15 read, 3 audited writes, 6 requiring human approval
  • Microsoft Graph + IMAP/SMTP (Aruba, Gmail, Libero, any IMAP server)
  • Per-account identity and knowledge files: the agent answers with figures
    from your own documents and attaches the right ones
  • Free-slot calendar availability, so proposed appointment times are real
  • Human console (Electron) over a local API — no LLM inside GigaMail, the
    intelligence is whatever agent you bring
  • 106 tests, green on Windows and Linux across Python 3.10 / 3.12 / 3.13

Known limitations

  • The action log is append-only, not tamper-proof
  • The bundled Azure app is not publisher-verified: the Microsoft consent
    screen shows an "unverified" notice. Bring your own client_id to avoid
    it — IMAP needs none of this
  • Gmail works via IMAP with an app password but is untested against a live
    account
  • mark_spam, update_event, auth_status, search_contacts and
    list_followup_needed are designed but not implemented

AGPL-3.0-or-later. Commercial licenses for closed-source use available from
the copyright holder. Security policy: SECURITY.md