MAIL M2: Mail mode — reader, threads, safe HTML, search (DESIGN §45) #396

Closed
opened 2026-09-29 04:40:50 +00:00 by kayg · 38 comments
Owner

MAIL M2: the Mail mode — reader and search (DESIGN §45, build order: accounts and sync (M1, done #313) → reader and search → send and actions (M3))

Build exactly what DESIGN §45 decided for reading (read the whole section and docs/research/mail-plugin.md, mail-threads.md):

  • Mail mode in the mode tray (plugin, on by default, admin-toggleable), sidebar with accounts and folders (sub-views live in the sidebar, DESIGN §34), unified inbox.
  • One newest-first stream; a thread opens at the newest message; received-time ordering with the sender's Date shown; thread grouping per §45 (reply forks stay OPEN — do not build).
  • Message reader: safe HTML rendering (sanitized, remote content blocked by default with a per-sender "load images", no scripts, no tracking pixels, links opened safely), plain-text and format=flowed, attachments listed with Save to Files, Print.
  • Search: sender, subject, recipients and full body across all accounts through the shared Search index (§45; attachment text is #293, later); Mail filters/pills in ⌘K.
  • Classification per §45 (list headers, sender domain, receipt patterns; no AI model) with "sender here" correction.
  • Deep links to every message and thread (stable calternal IDs), Copy link everywhere.
  • Parity (#395): every reader action available in API/CLI/MCP/WebMCP.

Gates before release (§45): hostile MIME/HTML tests (malformed MIME, huge parts, nested multiparts, charset tricks, CSS/HTML exploits, remote-content leaks), cross-User authorization tests, sync recovery tests, one adversarial round, and a measured "newest mail visible" time and search latency on a large mailbox (e.g. 100k messages). Production-build screenshots at 390/820/1440, light and dark.

## MAIL M2: the Mail mode — reader and search (DESIGN §45, build order: accounts and sync (M1, done #313) → **reader and search** → send and actions (M3)) Build exactly what DESIGN §45 decided for reading (read the whole section and docs/research/mail-plugin.md, mail-threads.md): - Mail mode in the mode tray (plugin, on by default, admin-toggleable), sidebar with accounts and folders (sub-views live in the sidebar, DESIGN §34), unified inbox. - One **newest-first** stream; a thread opens at the newest message; received-time ordering with the sender's Date shown; thread grouping per §45 (reply forks stay OPEN — do not build). - Message reader: safe HTML rendering (sanitized, remote content blocked by default with a per-sender "load images", no scripts, no tracking pixels, links opened safely), plain-text and format=flowed, attachments listed with Save to Files, Print. - Search: sender, subject, recipients and full body across all accounts through the shared Search index (§45; attachment text is #293, later); Mail filters/pills in ⌘K. - Classification per §45 (list headers, sender domain, receipt patterns; no AI model) with "sender here" correction. - Deep links to every message and thread (stable calternal IDs), Copy link everywhere. - Parity (#395): every reader action available in API/CLI/MCP/WebMCP. Gates before release (§45): hostile MIME/HTML tests (malformed MIME, huge parts, nested multiparts, charset tricks, CSS/HTML exploits, remote-content leaks), cross-User authorization tests, sync recovery tests, one adversarial round, and a measured "newest mail visible" time and search latency on a large mailbox (e.g. 100k messages). Production-build screenshots at 390/820/1440, light and dark.
Author
Owner

Started job/mail-m2 from 00015e652a. Reading DESIGN §45, mail research, M1 projection, and existing web/search integration before implementation. I will report concrete findings and gate output here.

Started job/mail-m2 from 00015e652a153485d9e7adda1c5ee61bc17300db. Reading DESIGN §45, mail research, M1 projection, and existing web/search integration before implementation. I will report concrete findings and gate output here.
Author
Owner

Finding: M1 sync already stores a stable UUID and owner-scoped live membership for each message, but the projection has no HTML or full-text index. I am adding sanitized HTML as derived data and an owner-filtered FTS projection. The reader must still use a sandboxed iframe and network-denying CSP. DESIGN §45 marks reply forks OPEN, so I will not infer them from subjects.

Finding: M1 sync already stores a stable UUID and owner-scoped live membership for each message, but the projection has no HTML or full-text index. I am adding sanitized HTML as derived data and an owner-filtered FTS projection. The reader must still use a sandboxed iframe and network-denying CSP. DESIGN §45 marks reply forks OPEN, so I will not infer them from subjects.
Author
Owner

Finding: M1's generated OpenAPI contract incorrectly described Mail paging fields as path parameters. The route already reads them from the query string. I changed the schema annotation to Query and regenerated the client; the web type check now reports 0 errors and 0 warnings. The first web test run found one new direct date-format call in MailView; I replaced it with the shared formatter, and its reuse-gate test now passes.

Finding: M1's generated OpenAPI contract incorrectly described Mail paging fields as path parameters. The route already reads them from the query string. I changed the schema annotation to Query and regenerated the client; the web type check now reports 0 errors and 0 warnings. The first web test run found one new direct date-format call in MailView; I replaced it with the shared formatter, and its reuse-gate test now passes.
Author
Owner

Finding: M1's keyset page cursor used the extra unreturned row, so opening the next page skipped that message. I changed the cursor to the last returned row and added a 3-message/2-row regression test. This matters for Mail's “Load older messages” control and stable newest-first reading.

Finding: M1's keyset page cursor used the extra unreturned row, so opening the next page skipped that message. I changed the cursor to the last returned row and added a 3-message/2-row regression test. This matters for Mail's “Load older messages” control and stable newest-first reading.
Author
Owner

Resumed job/mail-m2 at 00015e65 after the service-tier restart. The worktree contains an uncommitted Mail reader and search slice. I am validating and committing that slice, then will merge dev once and run the final gates and one adversarial round. No merge into dev or deploy is planned.

Resumed job/mail-m2 at 00015e65 after the service-tier restart. The worktree contains an uncommitted Mail reader and search slice. I am validating and committing that slice, then will merge dev once and run the final gates and one adversarial round. No merge into dev or deploy is planned.
Author
Owner

Finding: the first Mail mode draft combined only the first 50 messages from each account and had no unified cursor. A User with a large mailbox could not reach older unified Inbox messages. I added an Index query that pages by received time and stable message ID across enabled accounts, live folder generations and the authenticated User. The Mail crate suite passes (21 tests); API client generation and web integration are in progress.

Finding: the first Mail mode draft combined only the first 50 messages from each account and had no unified cursor. A User with a large mailbox could not reach older unified Inbox messages. I added an Index query that pages by received time and stable message ID across enabled accounts, live folder generations and the authenticated User. The Mail crate suite passes (21 tests); API client generation and web integration are in progress.
Author
Owner

Category decision (DESIGN §45 does not specify precedence): receipt subject patterns take precedence, then standard List headers, then Auto-Submitted and sender-address/domain notification signals, then People. The User's per-sender correction overrides every guess and is persisted as Security state. The Mail crate suite passes (22 tests). I also constrained the search FTS update trigger to text fields so category corrections do not reindex each message body.

Category decision (DESIGN §45 does not specify precedence): receipt subject patterns take precedence, then standard List headers, then Auto-Submitted and sender-address/domain notification signals, then People. The User's per-sender correction overrides every guess and is persisted as Security state. The Mail crate suite passes (22 tests). I also constrained the search FTS update trigger to text fields so category corrections do not reindex each message body.
Author
Owner

Thread decision: the Index assigns a stable thread ID from the provider thread ID when present, otherwise from normalized Message-ID, References and In-Reply-To keys within one connected account. When two partial graphs meet, old thread IDs remain valid through an alias. Existing M1 messages have only their own Message-ID stored, so pre-M2 replies can join when they are refetched; historical groups without a refetch remain a known migration gap. A test covers merge and old-link resolution. Remote HTML images remain stripped by default; per-sender consent permits only absolute HTTPS image sources in sanitized HTML and a CSP-limited frame. The Mail crate suite passes (25 tests).

Thread decision: the Index assigns a stable thread ID from the provider thread ID when present, otherwise from normalized Message-ID, References and In-Reply-To keys within one connected account. When two partial graphs meet, old thread IDs remain valid through an alias. Existing M1 messages have only their own Message-ID stored, so pre-M2 replies can join when they are refetched; historical groups without a refetch remain a known migration gap. A test covers merge and old-link resolution. Remote HTML images remain stripped by default; per-sender consent permits only absolute HTTPS image sources in sanitized HTML and a CSP-limited frame. The Mail crate suite passes (25 tests).
Author
Owner

Performance finding on a local 100k-message Mail Index fixture (shared build host, 25 warmed SQL samples): the first unified Inbox query had p50 279.17 ms / p95 371.01 ms because GROUP BY and ORDER BY built temporary trees. A correlated live-membership pick measured p50 0.18 ms / p95 0.26 ms on the same fixture. Common-term Mail search with received-time sorting had p50 259.86 ms / p95 398.58 ms; bounded FTS hit order measured p50 0.06 ms / p95 0.30 ms. I changed those two queries and am running crate tests. These are Index query timings, not end-to-end newest-mail-visible timings.

Performance finding on a local 100k-message Mail Index fixture (shared build host, 25 warmed SQL samples): the first unified Inbox query had p50 279.17 ms / p95 371.01 ms because GROUP BY and ORDER BY built temporary trees. A correlated live-membership pick measured p50 0.18 ms / p95 0.26 ms on the same fixture. Common-term Mail search with received-time sorting had p50 259.86 ms / p95 398.58 ms; bounded FTS hit order measured p50 0.06 ms / p95 0.30 ms. I changed those two queries and am running crate tests. These are Index query timings, not end-to-end newest-mail-visible timings.
Author
Owner

Backend finding: at 100,000 projected messages, the original unified Inbox query took p50 279.17 ms / p95 371.01 ms and the original common-term FTS query took p50 259.86 ms / p95 398.58 ms on 25 warm SQLite samples. The indexed live-membership page and bounded FTS stream measured p50 0.664 ms / p95 1.161 ms and p50 0.268 ms / p95 0.406 ms on the same fixture. These are Index query timings, not browser newest-mail-visible timings. The first SQL revision had a malformed owner bind; Mail crate tests caught it and it was fixed before commits 11ccd10a and 4c5cbd8c. Current Mail crate gate: 28 passed, 0 failed. A fake IMAP session now supplies hostile multipart HTML and verifies text extraction and sanitized storage.

Backend finding: at 100,000 projected messages, the original unified Inbox query took p50 279.17 ms / p95 371.01 ms and the original common-term FTS query took p50 259.86 ms / p95 398.58 ms on 25 warm SQLite samples. The indexed live-membership page and bounded FTS stream measured p50 0.664 ms / p95 1.161 ms and p50 0.268 ms / p95 0.406 ms on the same fixture. These are Index query timings, not browser newest-mail-visible timings. The first SQL revision had a malformed owner bind; Mail crate tests caught it and it was fixed before commits 11ccd10a and 4c5cbd8c. Current Mail crate gate: 28 passed, 0 failed. A fake IMAP session now supplies hostile multipart HTML and verifies text extraction and sanitized storage.
Author
Owner

Search design finding: DESIGN §32 specifies Tantivy for server-side content in the universal palette. The existing Indexer API accepts Home file paths and has no public plugin-document writer. This branch currently exposes Mail full text through the shared SearchProvider and Mail facet, backed by a rebuildable FTS5 projection in the Mail Index. This meets cross-account sender/subject/recipient/body lookup and the local 100k latency probe, but it is not the one Tantivy Index specified by §32. Moving Mail into Tantivy would require a new cross-crate indexing API; I am reporting that deviation rather than changing the Search crate's behavior inside this job.

Search design finding: DESIGN §32 specifies Tantivy for server-side content in the universal palette. The existing Indexer API accepts Home file paths and has no public plugin-document writer. This branch currently exposes Mail full text through the shared SearchProvider and Mail facet, backed by a rebuildable FTS5 projection in the Mail Index. This meets cross-account sender/subject/recipient/body lookup and the local 100k latency probe, but it is not the one Tantivy Index specified by §32. Moving Mail into Tantivy would require a new cross-crate indexing API; I am reporting that deviation rather than changing the Search crate's behavior inside this job.
Author
Owner

Reader decision where §45 is silent: the saved per-sender image choice permits absolute HTTPS image sources in a scriptless, opaque-origin sandboxed iframe. The frame has a deny-by-default CSP and no-referrer policy. Links are stripped from the HTML and exposed as explicit, safe-scheme links outside the frame. There is no image proxy in this slice; a User who permits a sender's remote images makes direct HTTPS requests to that sender's image hosts. Tests cover blocked-by-default resources and hostile HTML/CSS.

Reader decision where §45 is silent: the saved per-sender image choice permits absolute HTTPS image sources in a scriptless, opaque-origin sandboxed iframe. The frame has a deny-by-default CSP and no-referrer policy. Links are stripped from the HTML and exposed as explicit, safe-scheme links outside the frame. There is no image proxy in this slice; a User who permits a sender's remote images makes direct HTTPS requests to that sender's image hosts. Tests cover blocked-by-default resources and hostile HTML/CSS.
Author
Owner

Deep-link decision where §33/§45 give no Mail format parameter: /mail/m/{id}?format=plain and /mail/t/{id}?format=plain restore the reader's plain-text view. The HTML view is the default without that parameter. The format action updates the URL in place, and Copy link includes the plain view when selected.

Deep-link decision where §33/§45 give no Mail format parameter: `/mail/m/{id}?format=plain` and `/mail/t/{id}?format=plain` restore the reader's plain-text view. The HTML view is the default without that parameter. The format action updates the URL in place, and Copy link includes the plain view when selected.
Author
Owner

Post-merge design finding: dev added DESIGN §48 (one search Index per User; structural isolation) and §49 (provider sign-in moves to Settings → Integrations, while Settings → Mail keeps service preferences). This M2 branch extends M1's shared SQLite Mail projection with owner-filtered reads and continues using M1's Settings → Mail account UI. The new sections require cross-feature migration work (#400/#407) outside this reader slice. The new decisions are now in this branch after the single git merge dev; I am listing the remaining conflicts with them in the final report.

Post-merge design finding: `dev` added DESIGN §48 (one search Index per User; structural isolation) and §49 (provider sign-in moves to Settings → Integrations, while Settings → Mail keeps service preferences). This M2 branch extends M1's shared SQLite Mail projection with owner-filtered reads and continues using M1's Settings → Mail account UI. The new sections require cross-feature migration work (#400/#407) outside this reader slice. The new decisions are now in this branch after the single `git merge dev`; I am listing the remaining conflicts with them in the final report.
Author
Owner

Production screenshot finding: the root /mail view showed “Inbox” twice at 820/1440 px because the app chrome already supplies the mode title and MailView added a second root heading. I removed MailView's root heading while retaining its Copy link action. Sub-views keep their own title under the Mail chrome. I am rebuilding and recapturing 390/820/1440 light/dark after the change; no screenshot is committed to git.

Production screenshot finding: the root `/mail` view showed “Inbox” twice at 820/1440 px because the app chrome already supplies the mode title and MailView added a second root heading. I removed MailView's root heading while retaining its Copy link action. Sub-views keep their own title under the Mail chrome. I am rebuilding and recapturing 390/820/1440 light/dark after the change; no screenshot is committed to git.
Author
Owner

Production-build review evidence: 54 Mail screenshots, PNG ZIP. The archive includes Inbox empty and populated, category, folder, thread, message HTML and plain text, truncated body, and action menu at 390/820/1440 px in light and dark. The capture script asserted the rendered theme before every screenshot. The root Inbox screenshot reflects commit 16d78dfc, which removed the duplicated title. Screenshots are attached to this issue and are not in git.

Production-build review evidence: [54 Mail screenshots, PNG ZIP](https://git.kayg.org/attachments/7fe13d54-5e60-45e2-846d-879dd6a1ba7e). The archive includes Inbox empty and populated, category, folder, thread, message HTML and plain text, truncated body, and action menu at 390/820/1440 px in light and dark. The capture script asserted the rendered theme before every screenshot. The root Inbox screenshot reflects commit 16d78dfc, which removed the duplicated title. Screenshots are attached to this issue and are not in git.
Author
Owner

Post-merge production-build and real-server findings (2026-09-29):

  • 100,000-message synthetic Mail projection: newest message visible after navigation, p50 967.57 ms / p95 1434.39 ms over five samples. The host is shared with concurrent builds, so these include contention.
  • Exact Mail FTS SQL for a unique full-body term returned the indexed hit in approximately 0.19 ms per query over 25 local SQLite executions. The same normal /api/v1/search route returned HTTP 200 with timed_out=true and no hit in all five paced 100,000-message requests, p50 216.40 ms / p95 227.90 ms. One-message control returned the hit in all five requests, p50 36.90 ms / p95 69.20 ms. The shared Search route has a 200 ms provider deadline. This is a remaining search-at-scale gap; no 5xx or data loss was observed in the paced sample. The earlier unpaced 25-request run hit one 503 under concurrent host load and was stopped to avoid piling up work; its body was not captured.
  • The one time-boxed real-server adversarial round passed: anonymous access, Unicode and public-endpoint policy, malformed and oversized JSON, hostile IDs and cursors, and 24 parallel account/Inbox reads. Output: Mail API probe: anonymous access, Unicode and public-endpoint policy, malformed/oversized JSON, hostile IDs/cursors, and 24 parallel account/Inbox reads passed.
  • Six device/theme combinations for each changed Mail screen were captured from the production build and uploaded earlier in this issue. The root Inbox duplicate heading found during review was fixed in commit 16d78dfc.
Post-merge production-build and real-server findings (2026-09-29): - 100,000-message synthetic Mail projection: newest message visible after navigation, p50 967.57 ms / p95 1434.39 ms over five samples. The host is shared with concurrent builds, so these include contention. - Exact Mail FTS SQL for a unique full-body term returned the indexed hit in approximately 0.19 ms per query over 25 local SQLite executions. The same normal `/api/v1/search` route returned HTTP 200 with `timed_out=true` and no hit in all five paced 100,000-message requests, p50 216.40 ms / p95 227.90 ms. One-message control returned the hit in all five requests, p50 36.90 ms / p95 69.20 ms. The shared Search route has a 200 ms provider deadline. This is a remaining search-at-scale gap; no 5xx or data loss was observed in the paced sample. The earlier unpaced 25-request run hit one 503 under concurrent host load and was stopped to avoid piling up work; its body was not captured. - The one time-boxed real-server adversarial round passed: anonymous access, Unicode and public-endpoint policy, malformed and oversized JSON, hostile IDs and cursors, and 24 parallel account/Inbox reads. Output: `Mail API probe: anonymous access, Unicode and public-endpoint policy, malformed/oversized JSON, hostile IDs/cursors, and 24 parallel account/Inbox reads passed`. - Six device/theme combinations for each changed Mail screen were captured from the production build and uploaded earlier in this issue. The root Inbox duplicate heading found during review was fixed in commit 16d78dfc.
Author
Owner

Scope audit before the final gate report: the branch implements the reader, sanitized HTML, sender-gated remote images, plain/flowed text, Print, categories, stable message/thread IDs, and Mail results in the shared search palette. #396 remains open because the message list is still per-message rather than grouped by thread; attachment listing and Save to Files are not implemented; and reader actions do not yet have CLI/MCP/WebMCP parity. The §45 reader preferences (read marking after about 1 s), oldest-unread jump, and new-message notice are also outstanding. I will include these gaps in the final report. No reply-fork work was attempted because §45 marks it OPEN.

Scope audit before the final gate report: the branch implements the reader, sanitized HTML, sender-gated remote images, plain/flowed text, Print, categories, stable message/thread IDs, and Mail results in the shared search palette. #396 remains open because the message list is still per-message rather than grouped by thread; attachment listing and Save to Files are not implemented; and reader actions do not yet have CLI/MCP/WebMCP parity. The §45 reader preferences (read marking after about 1 s), oldest-unread jump, and new-message notice are also outstanding. I will include these gaps in the final report. No reply-fork work was attempted because §45 marks it OPEN.
Author
Owner

Mail M2 branch report (2026-09-29)

Head: 16d78dfcb0 on job/mail-m2, based on dev and merged from dev once. Pushed; no merge into dev.

Built:

  • Mail mode navigation, account/folder sidebar, unified Inbox with People default, category and folder views, message and thread deep links, Copy link actions.
  • Bounded HTML/plain MIME reader, format=flowed text, strict ammonia sanitization, isolated sandbox iframe with CSP, no remote requests before per-sender image consent, safe external link controls, Print.
  • SQLite FTS projection and shared Search provider for sender, subject, recipients and body; Mail palette kind, category classification and persistent sender correction; stable thread IDs and aliases.
  • Real IMAP fixture tests for hostile MIME and oversized body, cross-User isolation, search and sync recovery; adversarial Mail API probe.

Files: crates/plugins/mail/{src,migrations,Cargo.toml}, apps/web/src/lib/mail, apps/web/src/routes/mail, navigation/search/sidebar integration, contracts/openapi.json, packages/api-client/src/generated.ts, tests/adversarial/mail_api.mjs, Cargo.lock.

Evidence:

  • Production-build screenshots: 54 captures covering changed Mail screens at 390/820/1440 px in light and dark. Uploaded artifact: https://git.kayg.org/attachments/7fe13d54-5e60-45e2-846d-879dd6a1ba7e
  • One time-boxed adversarial round: Mail API probe: anonymous access, Unicode and public-endpoint policy, malformed/oversized JSON, hostile IDs/cursors, and 24 parallel account/Inbox reads passed.
  • 100,000-message synthetic projection: newest visible p50 967.57 ms/p95 1434.39 ms (5 samples, shared loaded host). Normal global Search API p50 216.40 ms/p95 227.90 ms (5 paced samples) but all five had timed_out=true and omitted the unique Mail hit. Exact indexed Mail SQL returned the hit in about 0.19 ms per query. One-message global Search control returned 5/5 hits, p50 36.90 ms/p95 69.20 ms. Search-at-scale remains open.

Gates (output quoted verbatim from the logs):

cargo fmt --check: exit 0, no output.

cargo clippy --all-targets -- -D warnings: interrupted near the job time box, exit 130. Last output:

    Checking tantivy-fst v0.5.0
   Compiling prettyplease v0.3.0
    Checking dashmap v6.2.1
    Checking rand v0.10.3
    Checking futures-lite v2.6.1

cargo test: interrupted near the job time box, exit 130. It never acquired Cargo's build lock. Entire output:

    Blocking waiting for file lock on build directory

cargo test -p calternal-plugin-mail: exit 0 before the dev merge; dev did not modify Mail sources. Result lines:

test result: ok. 30 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.41s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

bun run check: exit 0 after the merge and final visual fix:

svelte-check found 0 errors and 0 warnings

bun run test: exit 1 on the final run because a ThemePicker keyboard submenu test exceeded its 5000 ms timeout under shared-host load. Result lines:

 Test Files  1 failed | 122 passed (123)
      Tests  1 failed | 786 passed (787)
error: script "test" exited with code 1

The same full suite passed immediately before the visual-only heading fix:

 Test Files  123 passed (123)
      Tests  787 passed (787)

The isolated ThemePicker test then passed:

 Test Files  1 passed (1)
      Tests  2 passed (2)

Production bun run build: exit 0:

  Wrote site to "build"
  ✔ done

Post-merge cargo build -p calternal-server: exit 0; the binary ran the screenshots, benchmark and adversarial probe.

cargo clean: exit 0:

     Removed 12886 files, 6.2GiB total

apps/web/build and apps/web/.svelte-kit/output were also removed.

Known gaps:

  • The message list is per message rather than grouped by thread. Attachment listing/Save to Files, reader action parity in CLI/MCP/WebMCP, about-1-second read marking and setting, jump to oldest unread, and new-message notice remain open.
  • The body cache bounds parts at 64 KiB, so larger bodies show a truncation notice rather than the full content.
  • Mail FTS is in the shared SQLite Index, not the per-User Index required by the newly merged DESIGN §48. Account sign-in remains in Settings → Mail rather than the newly merged Integrations design in §49. These need a later cross-crate migration.

Decisions where §45 was silent: category signal precedence is receipt > newsletter > notification > people; thread aliases preserve old links after References merges; remote images are direct HTTPS requests after sender consent; ?format=plain preserves plain-reader deep links. Reply forks were left unbuilt because §45 marks them OPEN.

Mail M2 branch report (2026-09-29) Head: 16d78dfcb081fd9c5810f3fc04c5914d45b62c84 on job/mail-m2, based on dev and merged from dev once. Pushed; no merge into dev. Built: - Mail mode navigation, account/folder sidebar, unified Inbox with People default, category and folder views, message and thread deep links, Copy link actions. - Bounded HTML/plain MIME reader, format=flowed text, strict ammonia sanitization, isolated sandbox iframe with CSP, no remote requests before per-sender image consent, safe external link controls, Print. - SQLite FTS projection and shared Search provider for sender, subject, recipients and body; Mail palette kind, category classification and persistent sender correction; stable thread IDs and aliases. - Real IMAP fixture tests for hostile MIME and oversized body, cross-User isolation, search and sync recovery; adversarial Mail API probe. Files: crates/plugins/mail/{src,migrations,Cargo.toml}, apps/web/src/lib/mail, apps/web/src/routes/mail, navigation/search/sidebar integration, contracts/openapi.json, packages/api-client/src/generated.ts, tests/adversarial/mail_api.mjs, Cargo.lock. Evidence: - Production-build screenshots: 54 captures covering changed Mail screens at 390/820/1440 px in light and dark. Uploaded artifact: https://git.kayg.org/attachments/7fe13d54-5e60-45e2-846d-879dd6a1ba7e - One time-boxed adversarial round: `Mail API probe: anonymous access, Unicode and public-endpoint policy, malformed/oversized JSON, hostile IDs/cursors, and 24 parallel account/Inbox reads passed`. - 100,000-message synthetic projection: newest visible p50 967.57 ms/p95 1434.39 ms (5 samples, shared loaded host). Normal global Search API p50 216.40 ms/p95 227.90 ms (5 paced samples) but all five had timed_out=true and omitted the unique Mail hit. Exact indexed Mail SQL returned the hit in about 0.19 ms per query. One-message global Search control returned 5/5 hits, p50 36.90 ms/p95 69.20 ms. Search-at-scale remains open. Gates (output quoted verbatim from the logs): `cargo fmt --check`: exit 0, no output. `cargo clippy --all-targets -- -D warnings`: interrupted near the job time box, exit 130. Last output: ``` Checking tantivy-fst v0.5.0 Compiling prettyplease v0.3.0 Checking dashmap v6.2.1 Checking rand v0.10.3 Checking futures-lite v2.6.1 ``` `cargo test`: interrupted near the job time box, exit 130. It never acquired Cargo's build lock. Entire output: ``` Blocking waiting for file lock on build directory ``` `cargo test -p calternal-plugin-mail`: exit 0 before the dev merge; dev did not modify Mail sources. Result lines: ``` test result: ok. 30 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.41s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` `bun run check`: exit 0 after the merge and final visual fix: ``` svelte-check found 0 errors and 0 warnings ``` `bun run test`: exit 1 on the final run because a ThemePicker keyboard submenu test exceeded its 5000 ms timeout under shared-host load. Result lines: ``` Test Files 1 failed | 122 passed (123) Tests 1 failed | 786 passed (787) error: script "test" exited with code 1 ``` The same full suite passed immediately before the visual-only heading fix: ``` Test Files 123 passed (123) Tests 787 passed (787) ``` The isolated ThemePicker test then passed: ``` Test Files 1 passed (1) Tests 2 passed (2) ``` Production `bun run build`: exit 0: ``` Wrote site to "build" ✔ done ``` Post-merge `cargo build -p calternal-server`: exit 0; the binary ran the screenshots, benchmark and adversarial probe. `cargo clean`: exit 0: ``` Removed 12886 files, 6.2GiB total ``` `apps/web/build` and `apps/web/.svelte-kit/output` were also removed. Known gaps: - The message list is per message rather than grouped by thread. Attachment listing/Save to Files, reader action parity in CLI/MCP/WebMCP, about-1-second read marking and setting, jump to oldest unread, and new-message notice remain open. - The body cache bounds parts at 64 KiB, so larger bodies show a truncation notice rather than the full content. - Mail FTS is in the shared SQLite Index, not the per-User Index required by the newly merged DESIGN §48. Account sign-in remains in Settings → Mail rather than the newly merged Integrations design in §49. These need a later cross-crate migration. Decisions where §45 was silent: category signal precedence is receipt > newsletter > notification > people; thread aliases preserve old links after References merges; remote images are direct HTTPS requests after sender consent; `?format=plain` preserves plain-reader deep links. Reply forks were left unbuilt because §45 marks them OPEN.
Author
Owner

Starting the mail-m2 resume on branch job/mail-m2 at 16d78dfcb081fd9c5810f3fc04c5914d45b62c84. The worktree is clean. I am refreshing origin/dev because local dev is behind the visible #398 merge, then I will merge dev once and resolve conflicts with both sides in mind.

Starting the mail-m2 resume on branch `job/mail-m2` at `16d78dfcb081fd9c5810f3fc04c5914d45b62c84`. The worktree is clean. I am refreshing `origin/dev` because local `dev` is behind the visible #398 merge, then I will merge `dev` once and resolve conflicts with both sides in mind.
Author
Owner

Merge finding: the refreshed dev tip was b96693da1f24922778aba5652176a36aba30c500. Local dev is checked out in /home/kayg/Developer/calternal, so I merged the current origin/dev tip once into job/mail-m2 instead of moving that other worktree's branch. The only conflict was the shared sidebar import: I kept MailSidebar and combined the new swipe helper import with Mail's existing gesture imports. Merge-resolution commit: 44d21b72aaabf744d62fe53813148121a8e201fb.

Merge finding: the refreshed dev tip was `b96693da1f24922778aba5652176a36aba30c500`. Local `dev` is checked out in `/home/kayg/Developer/calternal`, so I merged the current `origin/dev` tip once into `job/mail-m2` instead of moving that other worktree's branch. The only conflict was the shared sidebar import: I kept MailSidebar and combined the new `swipe` helper import with Mail's existing gesture imports. Merge-resolution commit: `44d21b72aaabf744d62fe53813148121a8e201fb`.
Author
Owner

Parity finding: #395's source-backed adapter matrix is present on the refreshed dev tree. The initial merge commit tree did not contain its three tracked files, although origin/dev did. I restored docs/parity-matrix.md, docs/parity-exceptions.json, and scripts/parity_matrix.py from that dev tip in a56d3e86, so Mail reader adapters can use the checked matrix. #395's current generator records 156 pre-existing adapter gaps; I will limit this job to the Mail reader routes it touches.

Parity finding: #395's source-backed adapter matrix is present on the refreshed dev tree. The initial merge commit tree did not contain its three tracked files, although `origin/dev` did. I restored `docs/parity-matrix.md`, `docs/parity-exceptions.json`, and `scripts/parity_matrix.py` from that dev tip in `a56d3e86`, so Mail reader adapters can use the checked matrix. #395's current generator records 156 pre-existing adapter gaps; I will limit this job to the Mail reader routes it touches.
Author
Owner

M2 findings and decisions:

  • MailView.svelte compared message flags against a JavaScript string containing two literal backslashes. The Mail projection returns one \Seen flag, so a message could still look unread after its provider state changed. The reader now compares the same flag value as the API.
  • Attachment reads can use up to 25 MiB per request. The Mail router now admits at most four provider reads at once and returns HTTP 429 when those slots are full, so overload does not create an unbounded queue.

I am running the Mail crate tests against these changes now.

M2 findings and decisions: - `MailView.svelte` compared message flags against a JavaScript string containing two literal backslashes. The Mail projection returns one `\Seen` flag, so a message could still look unread after its provider state changed. The reader now compares the same flag value as the API. - Attachment reads can use up to 25 MiB per request. The Mail router now admits at most four provider reads at once and returns HTTP 429 when those slots are full, so overload does not create an unbounded queue. I am running the Mail crate tests against these changes now.
Author
Owner

The Mail CLI attachment download wrote the user-selected output path directly with OpenOptions. That bypassed the filesystem confinement rule. I added Root::write_new_file_at_root in calternal-fs, which creates one private new file relative to a held directory handle with openat2 and RESOLVE_BENEATH, and routed the Mail export through it. The helper rejects an existing destination and symlink. I will run the calternal-fs and calternal-cli crate gates.

The Mail CLI attachment download wrote the user-selected output path directly with `OpenOptions`. That bypassed the filesystem confinement rule. I added `Root::write_new_file_at_root` in `calternal-fs`, which creates one private new file relative to a held directory handle with `openat2` and `RESOLVE_BENEATH`, and routed the Mail export through it. The helper rejects an existing destination and symlink. I will run the `calternal-fs` and `calternal-cli` crate gates.
Author
Owner

I added attachment Copy link actions and restored the selected attachment from a deep link. The Mail API has no separate attachment ID, so the link uses the stable message ID plus the provider section ID: /mail/m/<message-id>?attachment=<section-id> or /mail/t/<thread-id>?attachment=<message-id>:<section-id>. Opening the link opens the attachment inspector and selects the item. I recorded this grammar in docs/DESIGN.md and made WebMCP accept the same links.

I added attachment Copy link actions and restored the selected attachment from a deep link. The Mail API has no separate attachment ID, so the link uses the stable message ID plus the provider section ID: `/mail/m/<message-id>?attachment=<section-id>` or `/mail/t/<thread-id>?attachment=<message-id>:<section-id>`. Opening the link opens the attachment inspector and selects the item. I recorded this grammar in `docs/DESIGN.md` and made WebMCP accept the same links.
Author
Owner

Finding #396: the one-second read timer could expire while the browser tab was hidden. It then discarded the timer without marking the message read when the User returned. MailView now cancels that interval and starts a fresh visible second when the reader becomes visible. A manual read-state change and route change also clear the pending timer. Web verification is pending.

Finding #396: the one-second read timer could expire while the browser tab was hidden. It then discarded the timer without marking the message read when the User returned. MailView now cancels that interval and starts a fresh visible second when the reader becomes visible. A manual read-state change and route change also clear the pending timer. Web verification is pending.
Author
Owner

Finding #396: the first cargo clippy -p calternal-plugin-mail --all-targets -- -D warnings pass reported six style warnings: two collapsible conditions in html.rs, one needless lifetime in routes.rs, two collapsible conditions and one manual_contains in sync.rs. I am applying the clippy suggestions so the required warning-free gate can pass.

Finding #396: the first `cargo clippy -p calternal-plugin-mail --all-targets -- -D warnings` pass reported six style warnings: two collapsible conditions in `html.rs`, one needless lifetime in `routes.rs`, two collapsible conditions and one `manual_contains` in `sync.rs`. I am applying the clippy suggestions so the required warning-free gate can pass.
Author
Owner

Search cause found on the 100k-message protocol: SQLx uses SQLite 3.51.3, whose plan started with SEARCH mail_messages USING INDEX mail_messages_page (owner_id=?), then tested FTS across the owner's rows. Python's plan started with the FTS virtual-table match and used a rowid lookup. The Mail pool wait was only 1–8 ms; the query stage used 176–203 ms and timed out. The comparable pre-fix run returned p50 220.30 ms / p95 225.10 ms, 0/5 Mail hits, and 5/5 timeouts. I am forcing FTS to drive the Mail query while keeping the owner, enabled-account, and live-membership checks.

Search cause found on the 100k-message protocol: SQLx uses SQLite 3.51.3, whose plan started with `SEARCH mail_messages USING INDEX mail_messages_page (owner_id=?)`, then tested FTS across the owner's rows. Python's plan started with the FTS virtual-table match and used a rowid lookup. The Mail pool wait was only 1–8 ms; the query stage used 176–203 ms and timed out. The comparable pre-fix run returned p50 220.30 ms / p95 225.10 ms, 0/5 Mail hits, and 5/5 timeouts. I am forcing FTS to drive the Mail query while keeping the owner, enabled-account, and live-membership checks.
Author
Owner

mail-m2 status

The job reached its four-hour limit while cargo clippy -p calternal-server --all-targets -- -D warnings was still compiling dependencies. I stopped further work at that limit. The server Clippy gate did not produce a final result, cargo test -p calternal-server was not run, and the one-time adversarial run and production screenshots were not completed. No screenshots were attached.

Built and committed

  • Mail M2 backend and UI: thread-grouped lists, attachment metadata and bounded provider downloads, Save to Files, owner-scoped read-marking preferences, read-state actions, oldest-unread navigation, new-message notice, and Mail settings.
  • CLI Mail reader commands and safe root-relative, create-only attachment export through calternal-fs.
  • MCP Mail reader actions with scoped write actions.
  • OpenAPI/client contract and parity matrix. The Mail reader routes in the matrix show CLI, MCP, and WebMCP coverage.
  • A test-only adversarial probe and fixture for new Mail API cases and 54 review captures. The probe source passes node --check; it was not run on a real server.
  • Search fix: force FTS to drive the join. SQLite 3.51.3 had chosen the owner/time index first and checked FTS for every message.

Commits: 1d123b23, 02213412, b7f30ae1, 1402fa8b, 52a52ca2, c9f285ff, 12057292, 21ba5163.

Head: 21ba51632bf3ab2b2564ed5d95b094636928acab. The worktree is clean. No push or deploy was made. The requested one-time git merge dev was already completed earlier in the job.

Search benchmark

The same five-request, one-second-spaced protocol used a production web build and 100,000 seeded Mail messages.

  • Before the fix: API client p50 220.30 ms, p95 225.10 ms; 0/5 hits and 5/5 timed out.
  • After the fix: API client p50 90.80 ms, p95 592.40 ms; 5/5 hits and 0/5 timed out.
  • The server handler times after the fix were 20.183, 21.552, 64.001, 78.607, and 98.955 ms (p50 64.001 ms, p95 98.955 ms). The 592.40 ms client p95 includes one outlier while the server handler was 98.955 ms; client scheduling is a likely cause, but this is an inference.
  • The fixed SQLite plan starts with SCAN message_search_fts VIRTUAL TABLE INDEX 0:M6, then looks up mail_messages by rowid. The old plan started with mail_messages_page(owner_id=?) and scanned the 100k messages before FTS matching.

Gate output

cargo clippy -p calternal-plugin-mail --all-targets -- -D warnings:

    Checking calternal-plugin-mail v0.0.1 (/home/kayg/Developer/calternal-wt/mail-m2/crates/plugins/mail)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 15.57s

cargo test -p calternal-plugin-mail:

test result: ok. 33 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.52s

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

cargo clippy -p calternal-fs --all-targets -- -D warnings:

    Checking calternal-fs v0.1.0 (/home/kayg/Developer/calternal-wt/mail-m2/crates/calternal-fs)
    Checking proptest v1.11.0
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 44s

cargo test -p calternal-fs:

test result: ok. 39 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 16.29s
test result: ok. 42 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.69s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

cargo clippy -p calternal-cli --all-targets -- -D warnings:

    Checking calternal-cli v0.0.1 (/home/kayg/Developer/calternal-wt/mail-m2/crates/calternal-cli)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.24s

cargo test -p calternal-cli:

test result: ok. 25 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.76s
test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.07s

bun run check:

Text sizes use shared role tokens.
svelte-check found 0 errors and 0 warnings

bun run test:

Test Files  125 passed (125)
     Tests  799 passed (799)

python3 scripts/parity_matrix.py --check:

Parity matrix: 169 web API actions, 106 shortcuts, 2 static commands, 113 menu actions, 28 settings groups, 151 actions with adapter gaps

cargo build -p calternal-server passed after the Mail search change. Server Clippy was interrupted at the time limit while still compiling dependencies (process exit 143); no final Clippy output was produced. Server tests were not run. cargo fmt completed after the final Rust edits; a final cargo fmt --check was not rerun.

cargo clean output:

     Removed 16266 files, 7.8GiB total

The production web build output was removed after use.

Decisions and known gaps

  • Attachment deep links use /mail/m/{message-id}?attachment={section-id} and /mail/t/{thread-id}?attachment={message-id}:{section-id}. They use stable message IDs and MIME section IDs.
  • MCP lists attachment metadata. It does not return attachment bytes because the MCP response cap is 1 MiB and a Mail download can be 25 MiB. CLI export and WebMCP Save to Files handle the byte transfer.
  • The parity matrix covers the attachment-list API on all three adapters. It does not inventory the binary download route.
  • The adversarial probe was not run, and the 390/820/1440 light/dark production screenshots were not captured or attached.
  • The server Clippy and server test gates remain incomplete because of the four-hour time limit.
  • The per-user Mail index work (#400) and Integrations sign-in work (#407) were left untouched.
## mail-m2 status The job reached its four-hour limit while `cargo clippy -p calternal-server --all-targets -- -D warnings` was still compiling dependencies. I stopped further work at that limit. The server Clippy gate did not produce a final result, `cargo test -p calternal-server` was not run, and the one-time adversarial run and production screenshots were not completed. No screenshots were attached. ## Built and committed - Mail M2 backend and UI: thread-grouped lists, attachment metadata and bounded provider downloads, Save to Files, owner-scoped read-marking preferences, read-state actions, oldest-unread navigation, new-message notice, and Mail settings. - CLI Mail reader commands and safe root-relative, create-only attachment export through `calternal-fs`. - MCP Mail reader actions with scoped write actions. - OpenAPI/client contract and parity matrix. The Mail reader routes in the matrix show CLI, MCP, and WebMCP coverage. - A test-only adversarial probe and fixture for new Mail API cases and 54 review captures. The probe source passes `node --check`; it was not run on a real server. - Search fix: force FTS to drive the join. SQLite 3.51.3 had chosen the owner/time index first and checked FTS for every message. Commits: `1d123b23`, `02213412`, `b7f30ae1`, `1402fa8b`, `52a52ca2`, `c9f285ff`, `12057292`, `21ba5163`. Head: `21ba51632bf3ab2b2564ed5d95b094636928acab`. The worktree is clean. No push or deploy was made. The requested one-time `git merge dev` was already completed earlier in the job. ## Search benchmark The same five-request, one-second-spaced protocol used a production web build and 100,000 seeded Mail messages. - Before the fix: API client p50 `220.30 ms`, p95 `225.10 ms`; `0/5` hits and `5/5` timed out. - After the fix: API client p50 `90.80 ms`, p95 `592.40 ms`; `5/5` hits and `0/5` timed out. - The server handler times after the fix were `20.183`, `21.552`, `64.001`, `78.607`, and `98.955 ms` (p50 `64.001 ms`, p95 `98.955 ms`). The 592.40 ms client p95 includes one outlier while the server handler was 98.955 ms; client scheduling is a likely cause, but this is an inference. - The fixed SQLite plan starts with `SCAN message_search_fts VIRTUAL TABLE INDEX 0:M6`, then looks up `mail_messages` by rowid. The old plan started with `mail_messages_page(owner_id=?)` and scanned the 100k messages before FTS matching. ## Gate output `cargo clippy -p calternal-plugin-mail --all-targets -- -D warnings`: ```text Checking calternal-plugin-mail v0.0.1 (/home/kayg/Developer/calternal-wt/mail-m2/crates/plugins/mail) Finished `dev` profile [unoptimized + debuginfo] target(s) in 15.57s ``` `cargo test -p calternal-plugin-mail`: ```text test result: ok. 33 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.52s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` `cargo clippy -p calternal-fs --all-targets -- -D warnings`: ```text Checking calternal-fs v0.1.0 (/home/kayg/Developer/calternal-wt/mail-m2/crates/calternal-fs) Checking proptest v1.11.0 Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 44s ``` `cargo test -p calternal-fs`: ```text test result: ok. 39 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 16.29s test result: ok. 42 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.69s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` `cargo clippy -p calternal-cli --all-targets -- -D warnings`: ```text Checking calternal-cli v0.0.1 (/home/kayg/Developer/calternal-wt/mail-m2/crates/calternal-cli) Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.24s ``` `cargo test -p calternal-cli`: ```text test result: ok. 25 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.76s test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.07s ``` `bun run check`: ```text Text sizes use shared role tokens. svelte-check found 0 errors and 0 warnings ``` `bun run test`: ```text Test Files 125 passed (125) Tests 799 passed (799) ``` `python3 scripts/parity_matrix.py --check`: ```text Parity matrix: 169 web API actions, 106 shortcuts, 2 static commands, 113 menu actions, 28 settings groups, 151 actions with adapter gaps ``` `cargo build -p calternal-server` passed after the Mail search change. Server Clippy was interrupted at the time limit while still compiling dependencies (process exit `143`); no final Clippy output was produced. Server tests were not run. `cargo fmt` completed after the final Rust edits; a final `cargo fmt --check` was not rerun. `cargo clean` output: ```text Removed 16266 files, 7.8GiB total ``` The production web build output was removed after use. ## Decisions and known gaps - Attachment deep links use `/mail/m/{message-id}?attachment={section-id}` and `/mail/t/{thread-id}?attachment={message-id}:{section-id}`. They use stable message IDs and MIME section IDs. - MCP lists attachment metadata. It does not return attachment bytes because the MCP response cap is 1 MiB and a Mail download can be 25 MiB. CLI export and WebMCP Save to Files handle the byte transfer. - The parity matrix covers the attachment-list API on all three adapters. It does not inventory the binary download route. - The adversarial probe was not run, and the 390/820/1440 light/dark production screenshots were not captured or attached. - The server Clippy and server test gates remain incomplete because of the four-hour time limit. - The per-user Mail index work (#400) and Integrations sign-in work (#407) were left untouched.
Author
Owner

Resuming round 3 on branch job/mail-m2. Base SHA: b96693da1f. Current head: 21ba51632b. I am reviewing #395 parity integration and will merge dev once before final gates.

Resuming round 3 on branch job/mail-m2. Base SHA: b96693da1f24922778aba5652176a36aba30c500. Current head: 21ba51632bf3ab2b2564ed5d95b094636928acab. I am reviewing #395 parity integration and will merge dev once before final gates.
Author
Owner

Finding during the required dev merge: the job branch and dev both changed WebMCP registration/confirmations, CLI commands, layout confirmation state, and the parity generator/docs. The parity conflict is an add/add; I am taking dev's #395 generator and matrix as the base and folding Mail reader coverage into that shared framework.

Finding during the required dev merge: the job branch and dev both changed WebMCP registration/confirmations, CLI commands, layout confirmation state, and the parity generator/docs. The parity conflict is an add/add; I am taking dev's #395 generator and matrix as the base and folding Mail reader coverage into that shared framework.
Author
Owner

The merged CLI reader surface is covered: cargo test -p calternal-cli passed 26 unit tests and 15 output-contract tests. The parity matrix now uses dev's generator and currently marks all M2 Mail reader routes across CLI, MCP and WebMCP.

The merged CLI reader surface is covered: cargo test -p calternal-cli passed 26 unit tests and 15 output-contract tests. The parity matrix now uses dev's generator and currently marks all M2 Mail reader routes across CLI, MCP and WebMCP.
Author
Owner

Finding: the first final cargo clippy -p calternal-server --all-targets -- -D warnings attempt could not compile because the RustEmbed folder apps/web/build/ was absent. The derive then left Frontend::get unavailable. This is a generated production-asset prerequisite; I am building the web app before rerunning the server gates and probe. No source defect was indicated.

Finding: the first final `cargo clippy -p calternal-server --all-targets -- -D warnings` attempt could not compile because the `RustEmbed` folder `apps/web/build/` was absent. The derive then left `Frontend::get` unavailable. This is a generated production-asset prerequisite; I am building the web app before rerunning the server gates and probe. No source defect was indicated.
Author
Owner

Resuming Mail M2 round 3 on job/mail-m2.

Current HEAD: adeb41074ebfe5190db1d854efd234211acbdb03.
Current branch base from origin/dev: 43c1377c8fd3b1b495841446989a64156f8ae1e4.

I am reviewing the existing #395 parity integration and Mail boundary probe, then I will finish the shared parity rows, run the requested API adversarial probe and capture the required production screenshots before final gates.

Resuming Mail M2 round 3 on `job/mail-m2`. Current HEAD: `adeb41074ebfe5190db1d854efd234211acbdb03`. Current branch base from `origin/dev`: `43c1377c8fd3b1b495841446989a64156f8ae1e4`. I am reviewing the existing #395 parity integration and Mail boundary probe, then I will finish the shared parity rows, run the requested API adversarial probe and capture the required production screenshots before final gates.
Author
Owner

mail-m2 round 3: finished (resumed by Claude after the Codex usage limit)

Head: 8d19b921a6cd9d02f7eca39c0294018233460743 on job/mail-m2. The worktree is clean. No push, no merge into dev.

State at resume

  • The last Codex commit c1db3e2b4 merges origin/dev at 369ab6a2f. It has no conflict markers. git diff origin/dev HEAD shows only Mail M2 files, so no dev changes were lost. MailSection.svelte uses dev's SettingsCard in all groups (#402). origin/dev has no newer commits, so no second merge was necessary.
  • The parity matrix uses dev's #395 framework (scripts/parity_matrix.py, docs/parity-matrix.md, docs/parity-exceptions.json). All the M2 Mail reader routes show CLI, MCP and WebMCP coverage. The regenerate check gives no diff.

Done in this session

  • Adversarial probe run (tests/adversarial/mail_api.mjs) against a server built from the merged tree. It passes. The first runs found:
    • Real finding (fixed, c87db6665): GET /api/v1/mail/threads/{id}/oldest-unread answered 200 {"message":null} for another User's thread and for unknown thread IDs. The thread list and attachment routes answer 404 for these IDs, and the OpenAPI contract documents 404. No data leaked because the query is owner-scoped. The route now answers 404 when the caller cannot see the thread. Regression test: oldest_unread_hides_threads_the_caller_cannot_see.
    • Real finding (fixed, 8d19b921a): a non-transient Mail Index error gave a generic 500 and wrote no log line. The error is now logged on the server with tracing::error!. The response stays generic.
    • Probe defects (fixed): the probe expected 200 from the Mail write routes, but their contract answers 204. The fixture's flags_json had an invalid JSON escape, which caused the silent 500 above. The probe also sent {"read":true} to the remote-content route, and its Save to Files locator was ambiguous when there were 2 attachments. The probe now prints server diagnostics when it fails.
  • New Rust test for hostile MIME: malformed_headers_and_deep_multipart_nesting_still_sync. It uses broken encoded-words, an unknown charset, a header line with no colon, a 200 KiB header, a bad Date and 256 nested multipart/mixed levels. Sync completes, the message enters the projection and the body stays bounded. The existing tests already cover huge parts, charset/CSS tricks, scripts, cid: and remote images.
  • Probe coverage: hostile HTML (script, @import and url() CSS exfiltration, remote images before consent, cid: loop) makes 0 remote requests before consent. After consent, only the sanitized HTTPS image loads. Another User's message, attachment, thread messages, thread attachments, oldest-unread, read-state and image-consent routes all answer 404. Another User's Mail does not appear in Search. Save to Files with a ../../../../etc/passwd attachment name writes one safe path segment in the User's Home. The probe also sends hostile, malformed and oversized IDs and bodies, 24 parallel account reads and 24 parallel Inbox reads, and checks the 1-second visible read delay.
  • Screenshots: 42 production-build PNGs. The 7 surfaces are the unified Inbox, a thread, an HTML message with the remote-image consent bar, the attachment list with Save to Files, Mail search results, the new-message notice and oldest-unread navigation. Each is at 390, 820 and 1440 px, in light and dark. They are attached as mail-m2-round3-screenshots.zip (https://git.kayg.org/attachments/4a168ad1-dec7-4ac5-99ed-e001e410233f). They are also in the worktree at artifacts/mail-m2/mail-*.png.

Visual issues I saw in the screenshots (not fixed; for the orchestrator's review)

  1. The sandboxed HTML body iframe has a fixed tall height. A short message shows a large empty white box: about 500 px at 390 px width and about 550 px at 1440 px.
  2. In the attachment inspector, the second row fades under the list's scroll mask. The ../../../../etc/passwd row's meta line and Save button are almost unreadable (1440 dark).
  3. In the Inbox, a lone "Copy link" button floats above the list on the right. Each row has its own large "Copy link" button, and each thread card has a large "Copy message link" button. This makes the page heavy.
  4. The captures use fullPage, so the fixed app background stops at the first viewport and the area below is flat black or grey. This is a capture artifact, not what a User sees while scrolling.

Gate output (verbatim)

cargo fmt --check: exit 0, no output.

cargo clippy -p <crate> --all-targets -- -D warnings:

== clippy calternal-plugin-mail
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 37.05s
== clippy calternal-fs
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.82s
== clippy calternal-cli
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 24.52s
== clippy calternal-server
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2m 18s

cargo test -p <crate>:

== test calternal-plugin-mail
test result: ok. 35 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.38s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
== test calternal-fs
test result: ok. 39 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 7.87s
test result: ok. 42 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 14.83s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
== test calternal-cli
test result: ok. 26 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.76s
test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.04s
== test calternal-server
test result: ok. 83 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 7.32s

bun run check (apps/web):

Text sizes use shared role tokens.
1790714462780 COMPLETED 1894 FILES 0 ERRORS 0 WARNINGS 0 FILES_WITH_PROBLEMS

bun run test (apps/web):

 Test Files  129 passed (129)
      Tests  826 passed (826)

bash packages/api-client/check-generated.sh: exit 0 (git diff --exit-code clean).

✨ openapi-typescript 7.13.0
🚀 ../../contracts/openapi.json → src/generated.ts [410.2ms]

python3 scripts/parity_matrix.py --check:

Parity matrix: 174 web API actions, 106 shortcuts, 2 static commands, 113 menu actions, 30 settings groups, 155 actions with adapter gaps

Adversarial probe:

Mail API probe: remote-content isolation, hostile IDs, cross-User message/thread/attachment isolation, safe attachment names and 24 parallel account/Inbox reads passed

The web gates ran after the merge and before this session's changes. Those changes touch only Rust and test-probe files, so the web results still apply. Mail migrations 0003–0008 do not collide with origin/dev, which has only 0001–0002. cargo clean removed 9.3 GiB. The web build output was deleted.

Remaining / known gaps

  • The visual issues 1–3 above.
  • These gaps are carried over from earlier rounds: Mail FTS is in the shared Index, not the per-User Index (#400). Account sign-in is still in Settings → Mail (#407). MCP returns attachment metadata only; CLI and WebMCP transfer the bytes. Reply forks are not built because they are OPEN in §45.
  • Build note: the shared sccache server ran at about 10 crates per minute with low host load. RUSTC_WRAPPER= built the server in 2 minutes.
## mail-m2 round 3: finished (resumed by Claude after the Codex usage limit) Head: `8d19b921a6cd9d02f7eca39c0294018233460743` on `job/mail-m2`. The worktree is clean. No push, no merge into dev. ### State at resume - The last Codex commit `c1db3e2b4` merges `origin/dev` at `369ab6a2f`. It has no conflict markers. `git diff origin/dev HEAD` shows only Mail M2 files, so no dev changes were lost. `MailSection.svelte` uses dev's `SettingsCard` in all groups (#402). `origin/dev` has no newer commits, so no second merge was necessary. - The parity matrix uses dev's #395 framework (`scripts/parity_matrix.py`, `docs/parity-matrix.md`, `docs/parity-exceptions.json`). All the M2 Mail reader routes show CLI, MCP and WebMCP coverage. The regenerate check gives no diff. ### Done in this session - **Adversarial probe run** (`tests/adversarial/mail_api.mjs`) against a server built from the merged tree. It passes. The first runs found: - **Real finding (fixed, `c87db6665`):** `GET /api/v1/mail/threads/{id}/oldest-unread` answered `200 {"message":null}` for another User's thread and for unknown thread IDs. The thread list and attachment routes answer 404 for these IDs, and the OpenAPI contract documents 404. No data leaked because the query is owner-scoped. The route now answers 404 when the caller cannot see the thread. Regression test: `oldest_unread_hides_threads_the_caller_cannot_see`. - **Real finding (fixed, `8d19b921a`):** a non-transient Mail Index error gave a generic 500 and wrote no log line. The error is now logged on the server with `tracing::error!`. The response stays generic. - **Probe defects (fixed):** the probe expected 200 from the Mail write routes, but their contract answers 204. The fixture's `flags_json` had an invalid JSON escape, which caused the silent 500 above. The probe also sent `{"read":true}` to the remote-content route, and its Save to Files locator was ambiguous when there were 2 attachments. The probe now prints server diagnostics when it fails. - **New Rust test for hostile MIME:** `malformed_headers_and_deep_multipart_nesting_still_sync`. It uses broken encoded-words, an unknown charset, a header line with no colon, a 200 KiB header, a bad Date and 256 nested `multipart/mixed` levels. Sync completes, the message enters the projection and the body stays bounded. The existing tests already cover huge parts, charset/CSS tricks, scripts, `cid:` and remote images. - **Probe coverage:** hostile HTML (script, `@import` and `url()` CSS exfiltration, remote images before consent, `cid:` loop) makes 0 remote requests before consent. After consent, only the sanitized HTTPS image loads. Another User's message, attachment, thread messages, thread attachments, oldest-unread, read-state and image-consent routes all answer 404. Another User's Mail does not appear in Search. Save to Files with a `../../../../etc/passwd` attachment name writes one safe path segment in the User's Home. The probe also sends hostile, malformed and oversized IDs and bodies, 24 parallel account reads and 24 parallel Inbox reads, and checks the 1-second visible read delay. - **Screenshots:** 42 production-build PNGs. The 7 surfaces are the unified Inbox, a thread, an HTML message with the remote-image consent bar, the attachment list with Save to Files, Mail search results, the new-message notice and oldest-unread navigation. Each is at 390, 820 and 1440 px, in light and dark. They are attached as `mail-m2-round3-screenshots.zip` (https://git.kayg.org/attachments/4a168ad1-dec7-4ac5-99ed-e001e410233f). They are also in the worktree at `artifacts/mail-m2/mail-*.png`. ### Visual issues I saw in the screenshots (not fixed; for the orchestrator's review) 1. The sandboxed HTML body iframe has a fixed tall height. A short message shows a large empty white box: about 500 px at 390 px width and about 550 px at 1440 px. 2. In the attachment inspector, the second row fades under the list's scroll mask. The `../../../../etc/passwd` row's meta line and Save button are almost unreadable (1440 dark). 3. In the Inbox, a lone "Copy link" button floats above the list on the right. Each row has its own large "Copy link" button, and each thread card has a large "Copy message link" button. This makes the page heavy. 4. The captures use `fullPage`, so the fixed app background stops at the first viewport and the area below is flat black or grey. This is a capture artifact, not what a User sees while scrolling. ### Gate output (verbatim) `cargo fmt --check`: exit 0, no output. `cargo clippy -p <crate> --all-targets -- -D warnings`: ``` == clippy calternal-plugin-mail Finished `dev` profile [unoptimized + debuginfo] target(s) in 37.05s == clippy calternal-fs Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.82s == clippy calternal-cli Finished `dev` profile [unoptimized + debuginfo] target(s) in 24.52s == clippy calternal-server Finished `dev` profile [unoptimized + debuginfo] target(s) in 2m 18s ``` `cargo test -p <crate>`: ``` == test calternal-plugin-mail test result: ok. 35 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.38s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s == test calternal-fs test result: ok. 39 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 7.87s test result: ok. 42 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 14.83s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s == test calternal-cli test result: ok. 26 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.76s test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.04s == test calternal-server test result: ok. 83 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 7.32s ``` `bun run check` (apps/web): ``` Text sizes use shared role tokens. 1790714462780 COMPLETED 1894 FILES 0 ERRORS 0 WARNINGS 0 FILES_WITH_PROBLEMS ``` `bun run test` (apps/web): ``` Test Files 129 passed (129) Tests 826 passed (826) ``` `bash packages/api-client/check-generated.sh`: exit 0 (`git diff --exit-code` clean). ``` ✨ openapi-typescript 7.13.0 🚀 ../../contracts/openapi.json → src/generated.ts [410.2ms] ``` `python3 scripts/parity_matrix.py --check`: ``` Parity matrix: 174 web API actions, 106 shortcuts, 2 static commands, 113 menu actions, 30 settings groups, 155 actions with adapter gaps ``` Adversarial probe: ``` Mail API probe: remote-content isolation, hostile IDs, cross-User message/thread/attachment isolation, safe attachment names and 24 parallel account/Inbox reads passed ``` The web gates ran after the merge and before this session's changes. Those changes touch only Rust and test-probe files, so the web results still apply. Mail migrations 0003–0008 do not collide with `origin/dev`, which has only 0001–0002. `cargo clean` removed 9.3 GiB. The web build output was deleted. ### Remaining / known gaps - The visual issues 1–3 above. - These gaps are carried over from earlier rounds: Mail FTS is in the shared Index, not the per-User Index (#400). Account sign-in is still in Settings → Mail (#407). MCP returns attachment metadata only; CLI and WebMCP transfer the bytes. Reply forks are not built because they are OPEN in §45. - Build note: the shared `sccache` server ran at about 10 crates per minute with low host load. `RUSTC_WRAPPER=` built the server in 2 minutes.
Author
Owner

mail-m2 polish round: done

Head: d4bbdd6fb128c9108fc932f0a7c561f9048478a3 on job/mail-m2. The worktree is clean. No push, no merge.

Commits: 875893d8e (API: unread_count per folder), ebd216dd9 (UI polish), d4bbdd6fb (parity exceptions for the new UI-only actions).

What changed, point by point

  1. Copy link placement: there are no text "Copy link" or "Copy message link" buttons now.
    • The header has ONE Copy link icon button with the warm tooltip and the shortcut L.
    • Thread rows, thread messages and attachments have Copy link in the context menu (right click, long press, Menu key or Shift+F10) and in their ⋯ menu.
    • List rows copy on a right swipe on touch.
    • Links use message, thread and section IDs.
  2. Inbox list: dense two-line rows.
    • Line 1 is the sender, a paperclip glyph when there are attachments, and a right-aligned date. Dates use the app's relative format (the notifications timeLabel, with the full date as the hover title).
    • Line 2 is the subject, then the muted snippet.
    • Unread is an accent dot plus a bolder sender and subject, and an accent-coloured date. There is no "· Unread" text; screen readers hear "Unread:".
    • ⋯ takes the date's place on hover or focus, so the date stays flush right at rest.
  3. Header toolbar: the actions go through usePageChrome into the one mode header. They are icon buttons with warm tooltips and shortcuts: Jump to oldest unread (U), Attachments (A, popover) and Copy link (L), next to the existing ⋯. The ⋯ menu gains Mark as read/unread. The three shortcuts are new registry entries in a new mail group and scope, and they are ignored in text fields.
  4. HTML frame (apps/web/src/lib/mail/frame.ts):
    • Height is automatic from the sandboxed frame. The sandbox is allow-same-origin only and NEVER allow-scripts. The frame CSP still blocks scripts and all network access; images load only after consent.
    • A max of 200dvh applies, after which the frame scrolls inside.
    • The app font and colours are injected at :where() zero specificity. The self-hosted font faces are copied under font-src 'self'.
    • color-scheme matches the app, so the frame stays transparent in dark mode. There is no serif and no white box.
  5. Attachments popover: each item has an icon Save to Files and a ⋯ menu with Copy link. Names use FileName middle truncation on one line and never wrap into the actions. The glyph is centred on the name line (lh unit). The second row no longer fades: the cause was the white frame showing through the glass.
  6. Sidebar:
    • Folder names are humanised with role icons ("INBOX" becomes "Inbox"; Gmail "[Gmail]/Sent Mail" becomes "Sent"). A nested plain folder keeps its full path as a hint.
    • Account headings are the shared section label, truncated, with the full address as the title.
    • Unread badges appear on each folder and on the unified Inbox (the sum of the accounts' Inbox folders). They come from the new MailFolderView.unread_count: one indexed membership scan with a substring test on the stored flags, and a store test covers it.
  7. Screenshots: all 42 are viewport-sized (not full page). They cover 7 screens × 390/820/1440 × light/dark. The new-message capture now reloads per theme, and the search and oldest-unread loops now set the viewport; in the previous round those captures were at the wrong theme or width. I zoomed in on every icon and label row: sidebar icons and badges, row dot and paperclip, notice glyphs, attachment glyphs and the header icons. All are within ±1 px.

The new-message and blocked-images notices are now compact cards with an icon, the text, one action and a hide control. On a narrow card the action moves under the text (container query).

Decisions (not covered by DESIGN)

  • I did not reuse NoticeBanner for the Mail notices, because its --now warning tint reads as an error. The Mail notices are plain cards.
  • The shortcuts are plain letters L / U / A in the mail scope. L matches the block Copy link key.
  • The unified Inbox badge counts unread messages in all Inbox folders across all categories.

Gate output (verbatim)

cargo fmt --check: exit 0
== clippy calternal-plugin-mail
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 35.91s
== test calternal-plugin-mail
test result: ok. 35 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.40s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
== clippy calternal-server
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2m 16s
== test calternal-server
test result: ok. 83 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 6.55s

bun run check:

1790721580473 COMPLETED 1908 FILES 0 ERRORS 0 WARNINGS 0 FILES_WITH_PROBLEMS

bun run test:

 Test Files  131 passed (131)
      Tests  832 passed (832)

bash packages/api-client/check-generated.sh: exit 0. python3 scripts/parity_matrix.py --check:

Parity matrix: 174 web API actions, 109 shortcuts, 2 static commands, 117 menu actions, 30 settings groups, 155 actions with adapter gaps

Mail adversarial probe (real server, merged tree, UI changes included):

Mail API probe: remote-content isolation, hostile IDs, cross-User message/thread/attachment isolation, safe attachment names and 24 parallel account/Inbox reads passed

Not re-run this round, because the crates are unchanged since round 3 (gates in the previous comment): calternal-fs, calternal-cli.

## mail-m2 polish round: done Head: `d4bbdd6fb128c9108fc932f0a7c561f9048478a3` on `job/mail-m2`. The worktree is clean. No push, no merge. Commits: `875893d8e` (API: `unread_count` per folder), `ebd216dd9` (UI polish), `d4bbdd6fb` (parity exceptions for the new UI-only actions). ### What changed, point by point 1. **Copy link placement:** there are no text "Copy link" or "Copy message link" buttons now. - The header has ONE Copy link icon button with the warm tooltip and the shortcut `L`. - Thread rows, thread messages and attachments have Copy link in the context menu (right click, long press, Menu key or Shift+F10) and in their ⋯ menu. - List rows copy on a right swipe on touch. - Links use message, thread and section IDs. 2. **Inbox list:** dense two-line rows. - Line 1 is the sender, a paperclip glyph when there are attachments, and a right-aligned date. Dates use the app's relative format (the notifications `timeLabel`, with the full date as the hover title). - Line 2 is the subject, then the muted snippet. - Unread is an accent dot plus a bolder sender and subject, and an accent-coloured date. There is no "· Unread" text; screen readers hear "Unread:". - ⋯ takes the date's place on hover or focus, so the date stays flush right at rest. 3. **Header toolbar:** the actions go through `usePageChrome` into the one mode header. They are icon buttons with warm tooltips and shortcuts: Jump to oldest unread (`U`), Attachments (`A`, popover) and Copy link (`L`), next to the existing ⋯. The ⋯ menu gains Mark as read/unread. The three shortcuts are new registry entries in a new `mail` group and scope, and they are ignored in text fields. 4. **HTML frame** (`apps/web/src/lib/mail/frame.ts`): - Height is automatic from the sandboxed frame. The sandbox is `allow-same-origin` only and NEVER `allow-scripts`. The frame CSP still blocks scripts and all network access; images load only after consent. - A max of 200dvh applies, after which the frame scrolls inside. - The app font and colours are injected at `:where()` zero specificity. The self-hosted font faces are copied under `font-src 'self'`. - `color-scheme` matches the app, so the frame stays transparent in dark mode. There is no serif and no white box. 5. **Attachments popover:** each item has an icon Save to Files and a ⋯ menu with Copy link. Names use `FileName` middle truncation on one line and never wrap into the actions. The glyph is centred on the name line (`lh` unit). The second row no longer fades: the cause was the white frame showing through the glass. 6. **Sidebar:** - Folder names are humanised with role icons ("INBOX" becomes "Inbox"; Gmail "[Gmail]/Sent Mail" becomes "Sent"). A nested plain folder keeps its full path as a hint. - Account headings are the shared section label, truncated, with the full address as the title. - Unread badges appear on each folder and on the unified Inbox (the sum of the accounts' Inbox folders). They come from the new `MailFolderView.unread_count`: one indexed membership scan with a substring test on the stored flags, and a store test covers it. 7. **Screenshots:** all 42 are viewport-sized (not full page). They cover 7 screens × 390/820/1440 × light/dark. The new-message capture now reloads per theme, and the search and oldest-unread loops now set the viewport; in the previous round those captures were at the wrong theme or width. I zoomed in on every icon and label row: sidebar icons and badges, row dot and paperclip, notice glyphs, attachment glyphs and the header icons. All are within ±1 px. - Attached: `mail-m2-polish-screenshots.zip` (https://git.kayg.org/attachments/26e5fb41-23f6-4f21-acec-0afde298ce7a) - Local: `/home/kayg/Developer/calternal-wt/mail-m2/artifacts/mail-m2/mail-{inbox,thread,html-consent,attachments,search,new-message,oldest-unread}-{390,820,1440}-{light,dark}.png` The new-message and blocked-images notices are now compact cards with an icon, the text, one action and a hide control. On a narrow card the action moves under the text (container query). ### Decisions (not covered by DESIGN) - I did not reuse `NoticeBanner` for the Mail notices, because its `--now` warning tint reads as an error. The Mail notices are plain cards. - The shortcuts are plain letters `L` / `U` / `A` in the `mail` scope. `L` matches the block Copy link key. - The unified Inbox badge counts unread messages in all Inbox folders across all categories. ### Gate output (verbatim) ``` cargo fmt --check: exit 0 == clippy calternal-plugin-mail Finished `dev` profile [unoptimized + debuginfo] target(s) in 35.91s == test calternal-plugin-mail test result: ok. 35 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.40s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s == clippy calternal-server Finished `dev` profile [unoptimized + debuginfo] target(s) in 2m 16s == test calternal-server test result: ok. 83 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 6.55s ``` `bun run check`: ``` 1790721580473 COMPLETED 1908 FILES 0 ERRORS 0 WARNINGS 0 FILES_WITH_PROBLEMS ``` `bun run test`: ``` Test Files 131 passed (131) Tests 832 passed (832) ``` `bash packages/api-client/check-generated.sh`: exit 0. `python3 scripts/parity_matrix.py --check`: ``` Parity matrix: 174 web API actions, 109 shortcuts, 2 static commands, 117 menu actions, 30 settings groups, 155 actions with adapter gaps ``` Mail adversarial probe (real server, merged tree, UI changes included): ``` Mail API probe: remote-content isolation, hostile IDs, cross-User message/thread/attachment isolation, safe attachment names and 24 parallel account/Inbox reads passed ``` Not re-run this round, because the crates are unchanged since round 3 (gates in the previous comment): `calternal-fs`, `calternal-cli`.
Author
Owner

Merged into dev at 1a72e9564 and pushed. Deploy status is on #203.

Merged into dev at 1a72e9564 and pushed. Deploy status is on #203.
kayg closed this issue 2026-09-30 00:29:48 +00:00
Author
Owner

Owner decision (2026-10-01): confirmed again. Reuse Apple Mail and especially Gmail keyboard conventions; the shortcut presets (#542) check every Mail letter against them.

**Owner decision (2026-10-01):** confirmed again. Reuse **Apple Mail and especially Gmail** keyboard conventions; the shortcut presets (#542) check every Mail letter against them.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#396
No description provided.