Review of the pop-out work found ways to lose text, a wrong sign-out and
changes reaching ordinary browser tabs.
- Drafts: a closing window hands text over only to a main window holding
its login, and asks instead when the last save failed, the browser is
offline, a save is running or plugins could still have a say. A failed
rescue goes to "Continue draft". The opening window lets go only once
the composer is on screen, also for Edit draft in a message window.
Send options travel with the draft. Closing during a send asks first.
- Sessions: answers are encrypted for the window that asked, and only a
window holding the login answers. A renewal queued on the lock can be
called off by a sign-out, signs nobody out once its account is gone, and
takes the cached token after waiting (Rauthy `nbf`, #552).
- Ordinary tabs run none of the host. No lock is held for life.
- A message window hides after a refused session and waits for its
folders; a composer stays on screen with its text. Another tab dropping a
login it couldn't restore no longer closes pop-outs.
- Plain-text mail stays selectable in the desktop app. Folder save renames
reserved and shell names and skips one refused name. All display-mode
checks know the overlay. The attachment bar keeps to one row.
The earlier test that accepted text being gone after a failed rescue now
asserts that it reaches the "Continue draft" slot.
Any site could POST a form to the share target on load: with no clicks
the composer opened with its subject and file, and autosave stored a
real draft on the server 2 seconds later. The worker can't see where a
POST came from, and Chromium's link capturing accepts form submissions,
so an installed-app window proves nothing either.
- every share landing now asks first, in every window and browser; a
text landing address gets the same question. Nothing goes up before
the answer.
- handed-in text and files are the composer's starting state: no
autosave, page-close save or sign-out flush before the first real
edit.
- the worker ignores frames, keeps at most 5 entries and 100 MB for 15
minutes, sweeps on activate, on each share and from a page timer,
and seals what it keeps with a key passed in the landing fragment.
Offline wipes clear it too.
- the server fallback uses the fragment and caps by encoded length:
3000 Japanese characters made a 27 KB `Location` and a 431.
- a second compose handoff waits until the composer closes, also one
set aside by reading a message, where it used to replace the first.
- a launch leaving the mail page saves what was just typed first.
- an expired handoff or a missing stored file now says so; shares get
the control-character stripping and limits of `parseMailto`.
Existing tests changed because the landing moved from the query to the
fragment, stored entries gained a sealed part, and a share now asks
first.
An installed app had no title bar of its own, no way to save attachments
except a zip, and "pop out" stayed inside the page. A new window was a
second copy of the app that had to restore the session itself, and on
start-up that evicts any login made without "Remember me" from the list
every window shares.
- Title bar: `display_override` with `window-controls-overlay`, one
`--titlebar-inset` variable at the page roots and skeletons, and a drag
strip with the window title. 0 without an overlay, so nothing moves.
- Save all to a folder: `showDirectoryPicker`, asking the device policy
before the picker and before each file, with the zip as the fallback.
- Pop-out routes `/popout/message/<id>` and `/popout/compose`, on offer in
an installed app or when the desktop bridge has windows. Hidden in Lite.
- A pop-out borrows the main window's session and never evicts or starts a
token timer. Every renewal now takes a Web Lock, so two windows can't
send the same refresh token, which Stalwart 1.0 may treat as a replay.
- A draft moves to its window under the same id. A closing window hands
unsaved text to the main window to save, since its own request wouldn't
survive the close.
- Read state, drafts, theme and language follow across windows, and
`html[data-desktop]` gates the app-like behaviour in `app/desktop.css`.
Review of the notification actions. With a plugin that may stop a move
or read change, or a silent tab holding the login, the message now
opens instead of the route acting. The route answers 401 under a
session limit, and neither path touches a message since moved to Trash
or Junk. A tab that thawed declines an old click, Archive follows the
gate of the notification's login and files into a shared-out Archive
as the app's button does, Reply opens a composer in the Pro shell, the
route retries once when another client made the folder first and
leaves no empty folder behind, and logins match however the server
address is written.
Two assertions of this feature's own tests change: the route answers
401, not 403, when a login's scopes can't be worked out, since that now
also covers session limits; a tab's message to the page now carries
`at`.
The Pro shell's mail surface now reads a parked link's query from the
handoff: the address bar may have none by its first render, which lost
`?slot=` and `?owner=` there too.
A notification for one message gets buttons where the browser has room
for them, and a click on its body works as before. The service worker
has no login, so an open tab takes the action first, through the store
actions. With no tab open a new same-origin route, /api/push/action,
does Mark read and Archive with the context cookie. When the cookie is
gone, the token has expired or the login can't be told apart, the
message opens in the app instead. Reply opens the composer through a
`?reply=1` link, with any text typed on the notification.
- `public/sw.js`: buttons, asking open tabs, the route, `sync` handling
- `app/api/push/action`: same-origin, checks the login and the account,
honours the admin's Archive gate, files by the mirrored archive mode
- `lib/pwa-notify`: the page's side, the mirror for the worker, and
`requestOutboxSync`, which the offline queue calls
- Background Sync, thin version: a `sync` event tells open tabs to flush
Two push-preview-route assertions change: they match the whole answer
for a named message, which now also carries `login`, what the action
route checks so a stale notification can't act on whoever holds the
cookie slot now.
The installed app only handled mailto: and webcal: launches. The launch
consumer dropped files, and nothing could hand a File to the composer or
the import dialogs, because the pending mailto is JSON in sessionStorage.
- manifest: `shortcuts` named in the browser's language, `file_handlers`
for .eml, .ics and .vcf, and `share_target`, all with the base path and
per-domain branding. Lite's manifest is unchanged.
- `lib/launch-handoff.ts`: the one place files and links wait, in memory
and in the Cache API, so they survive a sign-in that leaves the page.
The desktop bridge can call the same functions, since launchQueue never
fires in Electron. Sign-out clears it.
- launch consumer: routes files by type, and takes a shortcut to an open
window, which focus-existing would otherwise only focus.
- .eml opens in a new host for the preview, also before sign-in, with the
strictest device policy deciding about saving. .ics and .vcf open their
import dialogs through a new `initialFile` prop. A .ics needs the server,
so offline the dialog says so and carries on once back online.
- composer: `initialFiles` goes through `addFiles`, so plugin vetoes and
size limits still apply, also from a Pro compose tab.
- `sw.js` takes the share POST before the offline check and keeps the files
for the page. Without a worker, `/protocol/share` passes text and links
on and the page says the files are lost. `protocol` joins the app
sections and the handler pages join the shell, so a mailto: launch
offline no longer gets a network error.
Browsers without launchQueue or a service worker behave as before.
Fixes from the review of api.files:
- `files.attach` to a new draft runs `onBeforeAttachmentUpload` for each
file and the server's total size limit before `compose.open`, which
runs neither; refused files come back in `refused`
- a write is refused when a folder above the target is missing from the
listing, instead of counting it as not shared
- `onFileMove` reports the item as it was, from where it was, like the
Files app's own move
- writes are counted against the 20-a-minute limit before the drive is
listed, so refused writes count and cost no listing
- reads stop the download past 20 MB (`fetchBlobArrayBuffer` with a
limit) and answer `too_big` when the listed size was wrong
- when the server refuses the new content of a replaced file, the old
version's copy is taken out of the trash again
The two getText assertions on `fetchBlobArrayBuffer` now expect the
account (undefined for the own drive) and the byte limit, since the
call passes both.
Both make the server fetch the sender's domain. The test that every
default value is allowed now also names senderFavicons (on by default,
which a plugin may no longer set); the rest of it is as it was.
exif.test.ts: the case "falls back to the file date" now expects no
date, since IFD0's DateTime is the file's change date, not the date
the photo was taken.
The host reads the size a PNG, GIF, JPEG, WebP or BMP declares in its
header first: over 50 megapixels it answers too_big, trackers and tiny
images are never decoded, a format whose header it can't read is
unsupported, and large images are decoded at the target width. A browser
that throws a TypeError on createImageBitmap's options decodes without
them.
The tests' stand-in pictures are now PNG headers instead of JSON (the
code reads the header), so image-shrink.test.ts and
plugin-host-attachment-image.test.ts build them that way, and the first
shrink test expects the resize options on createImageBitmap.
api.email.getThread fetches only the newest 50 messages of a thread
(Thread/get, then Email/get for those ids) instead of all of them with
their bodies. api.email.getThreadCount answers the number of messages
from Thread/get, with its own rate limit.
plugin-host-thread.test.ts now expects getThreadEmails to be called with
{ newest: 50 }, the new option that limits what is fetched.
vacation.get now also says whether the out-of-office forwards mail
(forwardActive, from the area c host APIs), so the test that pins the
whole answer names it too. The other fields are as they were.
api.host.features.sidePanel now says whether this plugin may offer the
app-side-panel (ui:side-panel granted), not only that the webmail has
it. A plugin enabled before an update added the permission can then say
so instead of offering a switch that does nothing. The host computes the
assistant's flags per plugin for a frame's init and for each push.
plugin-host-features.test.ts: the host bridge assertions take the
assistant's flags as the test plugin sees them (it lacks ui:side-panel,
so sidePanel is false there).
The AI assistant's "more actions" need host calls today's plugin API
lacks. Area c of lib/plugin-sandbox/assistant adds them:
- `calendar.respond(emailId, status)`, new permission calendar:respond:
answers an invitation of the user's own account through
lib/calendar-rsvp.ts, like the reader's banner
- `calendar.deleteEvent` now returns an undo token for a single event;
`calendar.undoDelete` creates it again from the host's copy
- `filters.list/create/undo` over Settings > Filters' model; forward,
redirect, reject, discard and keep are refused, and so are folders
that are shared or not the account's own
- `email.batch/undoBatch`: up to 200 messages through the store's
batch functions, restore points first, one entry in the undo history
- `vacation.get` also says whether the out-of-office forwards mail
- `app.describeSettings`, `app.setSettings` and `app.undoSettings`, new
permission settings:change, over the allow-list in
lib/settings-schema.ts, refusing locked, hidden, restricted and gated
settings when the change is made
Undo tokens stay in the host for 30 minutes, bound to the plugin and
the account. app.getSettings now shows every setting setSettings may
change. settings:write's text is reworded to what it gates (tags).
plugin-host-api-app-info gains a test; its other assertions are as
they were.