Skip to content

GigaMail 0.3.0

Choose a tag to compare

@adecubed adecubed released this 04 Sep 06:32
· 36 commits to main since this release

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 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.yml gained 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 attaches GigaMail Setup X.Y.Z.exe, its .blockmap and latest.yml to the Release.
    electron-builder's publish provider is github, 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.
    publisherName is 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.json follows pyproject.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 NameError silently dropping
    every message in the IMAP UID listing, and account deletion leaving
    the learned reply patterns behind.