Releases: adecubed/gigamail
Release list
GigaMail 0.3.2
- Codex CLI as a first-class agent. The console detects
codexnext to
claude, lets you pick the agent that writes the drafts (first-run guide
and Automations), stores the choice as{"agent": "codex"}in
agent.jsonand resolves the command at every start. Drafts run through
codex execin a read-only sandbox, with the final answer read from a
file. The MCP registration snippet switches to~/.codex/config.toml
(orcodex 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.jsonfollows the release version (it was still
0.2.4 at 0.3.1);sync-version.jsand 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,
properContent-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
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-secondarytoken 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 fromIntl. - "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 testonly runstests/*.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
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 depen...
v0.2.4 — rules that answer the person, and a Telegram approval that works
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 viaReply-To—
but a rule can now opt out withreply_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.ps1registers 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 asksgigamail watch-runningfirst and
that answer moved intowatcher.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.
Withoutparse_modeTelegram 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 sayapprove: truewhile 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
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
throughrequire_approval(), which notified without passingactions
— 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 agigamail://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
singleRCPT TO:<a@x.it, b@y.it>, Graph a singletoRecipients. The
provider need not refuse it — ours didn't, returningsuccess: true
and"accepted": 1. Half the recipients were never in the envelope and
nothing said so; theTo:header was right, so the copy in Sent looked
fine.split_addresses()(newcore/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_mailandreply_mailcan 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_fileshares 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 owntag, 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 atgigamail telegram setup --approve, stored outsidenotify.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 --approveonce to record the trusted chat.
v0.2.1 — reply rules with a fence, approve from Telegram or the toast
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
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.--yesis 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 samerequest_idinstead of a new one;
more thanGIGAMAIL_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 fromsendmail()and recorded as
provider_resultnext 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)
asmay_expand— the count you approve is not guaranteed. - SMTP TLS verified by default. Port 465 used
CERT_NONE; it now
verifies, with per-accountinsecure_tlsopt-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
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_ROOTrelocates everything (accounts, mail data, approvals, audit)ADE_MAIL_DATA_DIRrelocates mail data only- without
APPDATA, the POSIX fallback is~/.ade— one directory, not two
WithAPPDATApresent (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
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) orgigamail approvals approve <id> - execution uses the arguments stored when the request was made, never what
the agent passes back - repeating a
request_idreturns 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 ownclient_idto 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_contactsand
list_followup_neededare designed but not implemented
AGPL-3.0-or-later. Commercial licenses for closed-source use available from
the copyright holder. Security policy: SECURITY.md