The hook buses, the permission each hook needs and the docs were three
separate lists that had drifted: 145 of 198 hooks were never fired and
nothing noticed. `lib/plugin-hook-catalog.ts` now names each hook with
its kind, permission, payload and when it fires; the permission check
reads it, and a test fails for any catalogued hook without a call site.
Groups still being wired in this branch are listed in that test.
Also:
- new before-hooks for archive, spam, read state, star, keywords, folder
changes, RSVP, event import, tasks, file moves, settings, identities,
filters and out-of-office, plus onComposerClose and onSyncStateChange
- removed hooks that can never fire: the S/MIME group (S/MIME is a
plugin now), onMobileViewSwitch, onSidebarAppChange and the
registerShortcut pseudo-hook
- smime:read no longer grants anything; manifests that name it still
install, with a warning
The admin page offered "User Plugin Uploads" and "Require Admin
Approval", but nothing in Settings used them: there was no upload, no
uninstall and no way to ask for approval, and the admin had no list of
requests. The approval code that existed pinned a hash of the code only
and showed the admin a manifest the browser sent, never the code.
Now:
- Settings > Plugins has an install button (when uploads are allowed),
a remove button for the user's own plugins, and lists in plain
language what each plugin may do, high-risk items first
- an install sends the ZIP to the server, which reads the manifest and
code itself and pins the request to a hash of both
- the admin Plugins tab lists requests with the code and approves or
denies them; a user may have 10 open requests
- a user plugin is checked again on every start, so a revoked approval
or turning user plugins off takes effect
- the consent dialog and the plugin settings are translated, use the
theme colours, and show the sites a plugin may send data to
The Settings upload, the admin upload, the marketplace install and the
marketplace preview each parsed the ZIP and manifest with their own copy
of the code, and the copies had drifted. A `permissions` string skipped
validation and was then matched by substring, a small ZIP could inflate
to hundreds of megabytes on the server, the Settings upload dropped
`locales` and took origin lists unchecked, and settings schemas were
never checked at all.
All of them now go through `lib/extension-package.ts`:
- every file is inflated through a byte cap, with a limit on entries
- one manifest check: types, permissions, entrypoint, version,
minAppVersion, settingsSchema, configSchema, locales and origin lists
- only checked fields are stored
A draft that carried attachments and no message re-opened with the
contents of one of the attachments as its text. Attach a .csv to an
empty draft, save it, re-open it: the editor shows the rows of the csv
as what you had written, and the next save or send makes that the real
message.
Both edit-draft paths built the text body by joining every value in
bodyValues:
Object.values(draft.bodyValues).map(v => v.value).join('\n')
bodyValues is keyed by part id, and getEmail asks for
fetchAllBodyValues, which per RFC 8621 4.1.4 returns a value for every
text/* part of the message - attachments included. So any attached
.csv, .txt, .log or .md was joined in as body text, and with no html
body to prefer it became the whole message.
Take the body from the parts the server lists as the body instead, and
from nothing else. A draft whose textBody is empty now re-opens empty,
which is what an attachments-only draft is.
The selection lives in lib/email-body-selection.ts beside
getRenderableHtmlBody, which exists because the viewer and the mobile
thread view had already drifted apart once (#489). mail-app and the pro
email tab held character-for-character copies of this join, so the same
drift was already in place.