GigaMail 0.3.0
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 aproviderkey 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/imapnow 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; nowmail_render.jsis the
only one: structural sanitisation via DOMParser (scripts, frames,
objects, forms, meta, base, links, everyon*attribute,javascript:
andvbscript:URLs, CSSexpression()), 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.pyhad grown to 1,150 lines
doing polling, rule matching, drafting, addressing, approvals,
notifications, Telegram commands and execution in one file. It is
nowade_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 watcherand every public name still
work, the CLI and the console did not change. Theexcept: 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 nowade_mail_agent/http_api/with one
FastAPI router per domain — accounts, addresses, mail, calendar,
mask, agent, approvals, rules/watch, notify/onboarding — behind the
sameapp, the same paths and the same token middleware (kept in the
facade soimportlib.reloadin tests still re-reads the token).
python -m ade_mail_agent.http_apiand thegigamail-console-api
entry point are unchanged. -
Mail list and detail leave renderer.js.
renderer_mail.jsholds
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 (anonerrorwould fire in the main window), now
it parses into an inertDOMParserdocument; and the old
openMailWindowbuilt an unescaped HTML page for awindow.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.jsholds
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 abind*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
innerHTMLunescaped 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
inlineonclickstring with anesc()that did not cover the quote.
All fixed (esc()before<br>, the shared inerthtmlToText, data
attributes instead of inline JS).shell.openExternalis now reachable
only through one main-process function that acceptshttp,https
andmailto— 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 assertsopenExternal('file:...')is refused. -
webSecurityback on everywhere. Calendar, marketing and ask ran
withwebSecurity: false, which switches off the same-origin policy
for the whole window (a page could readfile://and call any host).
Removed from all three; the backend already answersfile://origins
through CORS, so nothing needed it.api.anthropic.comis gone from
every window's CSP. The e2e provesfetch('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.jsonat 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-zipand
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 testrunsnode --testwith the default pattern, which
works on Node 20, 22 and 24 alike. -
ADE_CONSOLE_PORTworks end to end. The main process knew the
port; the renderers had8002written 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, andMAPPA_MCP.mdfinally describes the
out-of-bandrequest_idflow instead of the oldconfirm_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:packagednow starts the
packaged app on a pristine profile, waits for the embedded Python
backend (/healthreports the version), checks the token, the
renderer, the onboarding, the capability gate andwebSecurity, and
the desktop workflow runs it after installing. It found a real one:
the embedded Python, withimport siteon, saw the building
machine's user site-packages, so pip skipped dependencies that were
"already there" and the installer shipped without them — the backend
started on the developer's PC and died on a clean one. Provisioning
and runtime now ignore the user site (PYTHONNOUSERSITE=1,python -s), and the embedded Python is installed from the repository source
rather than from PyPI, so the installer carries this commit's code and
dependencies. -
The installer ships with the release, and the app updates itself.
release.ymlgained a Windows job that, when a GitHub Release is
published (after the test suite of the PyPI job passes), builds the
installer, checks its version against the tag and attachesGigaMail Setup X.Y.Z.exe, its.blockmapandlatest.ymlto the Release.
electron-builder's publish provider isgithub, so electron-updater
reads that feed and the installed app picks up the next version. A
downloaded update is announced with a notification and installed at
the next quit; it no longer quits the app on a five-second timer.
publisherNameis out of the build config until the installer is
signed — with it set, electron-updater refuses unsigned updates. -
Console hardening. Electron permissions are an explicit whitelist
(microphone only, for dictation), every window carries a CSP without
unsafe-eval, the mail iframe no longer lets popups escape the
sandbox, and backend responses forbid scripts. -
One version.
console/package.jsonfollowspyproject.toml
automatically at build time; the installer, the Python package and
the release notes cannot disagree again. -
Lint in CI, and two bugs it found: a
NameErrorsilently dropping
every message in the IMAP UID listing, and account deletion leaving
the learned reply patterns behind.