Organisation > Devices now sets policy.json `organisation.devices`, each
switch off by default and scoped per domain or group:
- No downloads: attachments and files only preview (reader, conversation
view, Pro tab, list chips, .eml previews, the Files app). Download,
Download all, Open in new tab, Save to Files, drag-out, the browser's
PDF viewer (pdf.js instead) and media download controls go away.
- No export: no .eml export, View source, message drag-out, or folder and
account export; a running export stops and its file is deleted when
the switch turns on. Import stays.
- No printing: no Print buttons in mail, calendar and contacts; Ctrl+P
prints a note; the reader frame prints blank.
- No install prompt: no card, and the browser's own banner is held back.
- Watermark: the user's address and the date over the reader,
previews and every printout (mail, calendar, contacts, plugins).
A message follows its own account's policy; what isn't about one message
follows the strictest signed-in account. Every user path hides its
button, and the anchor and new-tab helpers refuse as a backstop
(lib/download-guard.ts), never the blob readers. The plugin APIs
`ui.downloadFile` and `ui.print` refuse too, and the office editor frame
loses `allow-downloads`.
Two assertions in components/mail/__tests__/blob-routing.test.tsx now
expect `{ kind: 'export' }` as the fifth argument of downloadBlob for an
.eml export: the export says what it is, so the backstop checks "No
export" rather than "No downloads" for it.
A message that failed for a passing reason (a dropped connection, a
server error, an expired token) used to count as done, so continuing
the import skipped it for good, and a slow upload followed by a cancel
imported the messages after it twice.
- The checkpoint keeps every message that is done, also past a gap,
and the next run of the same import tries only the rest again.
- Ten failures in a row, a full mailbox (overQuota) or a refusal
(forbidden) stop the import with a message instead of going through
the whole file.
- An upload without an answer pauses the import instead of failing the
message: 30 seconds, then until the upload window ends.
- Email/import, the export's Blob/get and folder listing pass the
client's own 429 pause by, and a 429 on them doesn't pause the rest
of the app.
- Signing out stops the job of that account only, writes no checkpoint
and imports nothing more afterwards; another account's sign-out
leaves the job alone.
- One tab imports a file at a time (Web Locks), and Import waits until
the dialog knows whether an earlier run can be continued.
The import dialog tests now wait for the Import button to be enabled
before clicking it, since it stays off during that lookup.
Stalwart's x:ArchivedItem/query without calculateTotal scans ids
ascending, stops at 5000 and only then reverses them, so the newest
items of a large archive never showed, and admin accounts got every
account's items. The review of the branch also found gaps around
account switches, restores and focus.
- Read the archive in pages of 500 with calculateTotal, newest first,
stop at a whole page older than the days limit, and offer
"Show older" after 2000 items; detection asks for one item.
- Send the accountId filter and fall back without it on
unsupportedFilter; keep only items of the account, never one without
an account id.
- Ask nothing until config and policy are loaded and the policy is the
current account's; the later check after a refusal is forced.
- Show the Trash link only when the archive holds a message inside the
days limit; the Settings page shows whenever the server has the
feature and says that archiving must be on in Stalwart.
- Restore: notFound leaves the list with a note, forbidden hides the
page, results for another account are dropped, the button keeps focus.
- Forget what servers said at a full sign-out.
The first version wrote the whole tag list on every change, guessed a
group when the server wouldn't say, and treated a failed read like a
server without metadata. Now:
- Definitions are a map by tag id under `tags`, and each change patches
one tag (`metadata/org.bulwarkmail.tags/tags/<id>`), so two members
adding tags at once both keep theirs. Fields and colors this version
doesn't know survive. The `{ list }` and bare-array shapes are still
read and converted by the first save.
- Shared tags appear only for confirmed group principals. A failed
principal lookup keeps the user's own tags and asks again later.
- "No metadata advertised" (0.16) and "the read failed" are separate.
A failed read keeps the last good tags; a failed save keeps the change,
says so, and sends it with the next good read after the client's
pause.
- Push reloads wait for 5 s of quiet and skip the echo of our own save;
a returning tab reloads definitions only, and the scan for unnamed tags
runs once per sign-in and then at most every 30 minutes.
- The editor names its buttons after the tag, shows the delete warning
and 44 px buttons on phones, returns focus to the edit button, and
asks for Inbox rights only for confirmed groups.
Drafts were always created in the primary account, so a message composed as
a group identity sat in the member's own Drafts while its Sent copy (since the
previous commit) went to the group.
- createDraft takes { accountId, previousDraftAccountId }: the draft goes to
the account that owns the From identity (resolveSendAccountId, now public
and briefly caching the identity lists for autosave), and the replaced
version is destroyed in the account it lives in.
- The composer tracks draftAccountId next to draftId (save, re-resolve of
attachment blobs, send, discard, stashed state), since draft ids are only
unique within one account.
- sendEmail destroys the sent draft in options.draftAccountId; discarding
(inline and Pro tab) deletes it there, through the identity's login.
- Re-opening a draft from a group/shared Drafts folder (or a stamped
aggregate row) keeps its account.
- resolveSendAccountId is optional on IJMAPClient; without it drafts stay in
the primary account.
- Tests: client unit tests for draft placement / replacement / send cleanup,
E2E spec 18 draft cases (autosave + send, discard).
Stalwart's default folder set has no archive-role mailbox, so on a fresh
account Archive did nothing but raise the not-found toast (#578). Both
archive paths now go through ensureArchiveMailbox(), which creates
"Archive" with the archive role in the account the action targets (own,
the selected shared folder's owner, or the pinned account in unified
view), reloads the mailbox list and continues with the store's own
object. createMailbox() takes an optional role for this. A refused
creation or a folder that does not come back still surfaces as an error.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A copy between logins goes through `Email/import`, which sent no
`receivedAt`, so the server stamped the import time and every copied
message landed at the top of the destination folder as today's mail.
`importRawEmail` now takes the original `receivedAt` (the same change
#1151 carries), and the cross-account copy and move pass it.
With several accounts connected, mail could only be moved between
them (drag to another account's folder). There was no way to file a
copy in another account and keep the original where it is.
crossAccountMoveEmails and copyEmailAcrossAccounts take an optional
`{ keepOriginal: true }`. With it the source message is not destroyed,
and the store leaves the list, the open message and the selection as
they are; only the destination's folders are refreshed. Without it
nothing changes.
The message menu gets "Copy to account..." when more than one account
is connected. It lists the writable folders of the other accounts
(their folder lists are fetched when the menu opens) and copies the
selection, or the message the menu was opened on, through the new
copyEmailsToAccount action. That groups the messages by the login and
JMAP account they live in and refuses a copy into the account a
message is already in.
Drafts were saved without In-Reply-To and References, and a draft
re-opens in compose mode, where the composer has no original message to
rebuild them from. A reply that was saved and re-opened, or sent and
brought back with Undo, therefore went out as a new conversation.
`createDraft` now stores the threading headers, the composer saves them
with every draft of a reply, and re-opening a draft carries them into
the composer for later saves and the send.