diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc84542..76d3bfb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -131,5 +131,5 @@ jobs: - name: Release verification run: | python scripts/verify_release.py \ - dist/mailstack-1.3.1-source.zip \ - --checksum dist/mailstack-1.3.1-source.zip.sha256 + dist/mailstack-1.3.3-source.zip \ + --checksum dist/mailstack-1.3.3-source.zip.sha256 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a7d7a19..62b6326 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,7 +63,7 @@ jobs: path: | dist/*.zip dist/*.sha256 - docs/RELEASE_NOTES_1.3.1.md + docs/RELEASE_NOTES_1.3.3.md docs/FORENSIC_AUDIT_REPORT.md if-no-files-found: error @@ -121,7 +121,7 @@ jobs: --verify-tag --target "$GITHUB_SHA" --title "MailStack ${RELEASE_VERSION}" - --notes-file docs/RELEASE_NOTES_1.3.1.md + --notes-file docs/RELEASE_NOTES_1.3.3.md ) if [[ "$RELEASE_PRERELEASE" == "true" ]]; then args+=(--prerelease --latest=false) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4acec5..a9e4c91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,42 @@ All notable repository-level changes are recorded here. Application history before the open-source conversion remains in `mailbox-app/CHANGELOG.md`. -## 1.3.1 — Unpublished source baseline +## 1.3.3 — PHASE-005A qualification correction + +### Corrected + +- Synchronized the 1.3.3 release identity across VERSION, CI/release workflows, deterministic artifact paths, managed documentation, and release-workflow contract tests after the failed qualification run exposed stale 1.3.2 metadata. +- Removed the accidental empty repository-root `85%` file created when non-comment CMD guidance containing `>=85%` was interpreted as output redirection. +- Corrected PHASE-005A import-order lint findings and regenerated forensic/document metadata so source-safety and Ruff gates evaluate the intended source tree. +- Preserved live updates for browsers that still hold the previously immutable-cached `app.js`: the live endpoint accepts either the new explicit `X-MailStack-Live-Request: 1` header or the legacy `Accept: application/json` poll signature, while ordinary document navigation continues to redirect to the dashboard instead of exposing raw JSON. +- Replaced the prose-paste handoff with an executable Windows CMD script whose explanatory lines are `REM` comments and whose gates stop on failure. + +### Compatibility + +- No model, migration, parser/sanitizer, mailbox permission, ingestion, Postfix/Dovecot, Maildir, MariaDB, attachment, installer, deployment-template, backup/restore, or upgrade/rollback behavior changes. +- Published `v1.3.1` remains immutable official release provenance; `1.3.2` is retained only as failed development-qualification history and is not promoted as a release baseline. + +## 1.3.2 — PHASE-005A development + +### UI navigation reliability + +- Hardened the authenticated live-update transport so `/messages/live/` returns JSON only to MailStack background polling requests carrying the explicit `X-MailStack-Live-Request: 1` header; ordinary authenticated document navigation redirects to the dashboard instead of rendering raw JSON. +- Preserved same-origin credentials, no-store responses, authorization filtering, bounded live payloads, polling backoff, notifications, and mailbox counter updates. + +### Compact mailbox and unified reader + +- Replaced the oversized mailbox header and generic message grid with a compact webmail-style inbox surface, integrated search/read/attachment filters, denser unread rows, message previews, attachment/size metadata, and responsive desktop/mobile behavior. +- Added preview text derived only from the existing indexed plain body or sanitized HTML text; no model, migration, parser, or persistence change is introduced. +- Removed the visible `Plain text` / `Safe HTML` tabs and replaced them with one message reader that automatically shows the existing sanitized HTML in the retained sandbox/no-referrer frame, or the indexed plain-text body when no HTML body exists. +- Compact sender/routing metadata, actions, warnings, and attachments into the unified reader while preserving mark-unread, deletion permissions, attachment authorization, sanitizer policy, CSP, and receive-only boundaries. + +### Qualification and release metadata + +- Added PHASE-005A regression coverage for direct live-endpoint navigation, background JSON polling, compact inbox rendering, HTML-derived previews, unified HTML rendering, and plain-only fallback. +- Synchronized the approved target identity to `1.3.2`, including CI deterministic-artifact verification and release-note file selection, without changing release-gate semantics. +- Froze published `v1.3.1` as `MAILSTACK-1.3.1-OFFICIAL-SOURCE-BASELINE-001`, anchored to commit `039a6e6eea6e198b4b15612db9d2f208b6305a16`, tree `9437ba2ebac2033a229accd190268b8711d5b26e`, and deterministic source SHA-256 `517778967ca491974f315d231dfd43b3dba85fe86b47dcfc63e4c7051d1010bd`. + +## 1.3.1 — 2026-08-18 ### Documentation and forensic baseline @@ -35,7 +70,7 @@ All notable repository-level changes are recorded here. Application history befo ### Compatibility - PHASE-004C changes maintained operational tooling only; it does not add a database migration, application route, authorization/UI/mail-flow behavior, installer behavior, deployment-template rewrite, DNS/TLS change, or automatic host-configuration migration. -- `v1.3.0-rc.4` and its published source identity remain immutable. `1.3.1` is the next frozen source-baseline version mark and remains unpublished until its GitHub/main CI and operational release gates are accepted. +- `v1.3.0-rc.4` remains immutable historical release provenance. `v1.3.1` is the current frozen published source/release baseline; PHASE-005A develops `1.3.2` from that exact source without rewriting either historical tag. ## Unreleased — MailStack repository bootstrap diff --git a/README.md b/README.md index a57c239..7f189d6 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ It combines **Postfix**, **Dovecot LMTP**, **Django**, **MariaDB**, **Maildir**, **Gunicorn**, and **Nginx** in a hardened single-node reference deployment. MailStack provides a private browser-based inbox, administrator-managed users, object-level mailbox access, safe email rendering, protected attachments, live inbox updates, operational audit logs, backup and restore tooling, a public website, and an isolated contact service. -> **Release status:** `v1.3.0-rc.4` remains the latest published release candidate. The repository source baseline is now marked `1.3.1` for the PHASE-004C verification/fix freeze; this version is not yet a published GitHub release or a production-readiness claim. PHASE-003 staging validated real external SMTP/LMTP delivery after the accepted fixes; backup/restore, restart/reboot, clean-host, legal, and final release-owner acceptance remain separate operational gates. +> **Release status:** `v1.3.1` remains the current published, deterministic source/release baseline. PHASE-005A is being corrected as `1.3.3` after the failed `1.3.2` qualification attempt; it hardens the live-update UI boundary, preserves legacy cached-poller compatibility, and delivers the compact mailbox/unified message reader. The existing VPS has not yet been upgraded to this development line; live upgrade acceptance remains a separate post-release operational gate. ## Why MailStack @@ -38,7 +38,7 @@ MailStack is designed for teams that need: - shared mailbox access with per-user authorization; - a receive-only mail security model; - durable Maildir storage and recoverable indexing; -- protected attachment delivery and sanitized HTML email; +- protected attachment delivery and unified sanitized-HTML/plain-text message reading; - auditable administration and operational recovery; - reproducible installation and release tooling. @@ -52,7 +52,7 @@ MailStack is **not** intended to be a complete replacement for Gmail, Microsoft - Object-level mailbox isolation - Shared mailbox memberships - Mailbox create, enable, disable, and soft-delete lifecycle -- Search, pagination, read/unread state, and attachment filters +- Compact webmail-style inbox with search, pagination, read/unread state, attachment filters, and message previews - Live inbox polling and browser notification support ### Receive-only mail stack @@ -211,8 +211,8 @@ Build and verify a deterministic source release: ```bash python scripts/build_release.py --root . python scripts/verify_release.py \ - dist/mailstack-1.3.1-source.zip \ - --checksum dist/mailstack-1.3.1-source.zip.sha256 + dist/mailstack-1.3.3-source.zip \ + --checksum dist/mailstack-1.3.3-source.zip.sha256 ``` ## Security diff --git a/VERSION b/VERSION index 3a3cd8c..31e5c84 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.1 +1.3.3 diff --git a/design/DESIGN_MANIFEST.json b/design/DESIGN_MANIFEST.json index 4ca94b8..bf41ea7 100644 --- a/design/DESIGN_MANIFEST.json +++ b/design/DESIGN_MANIFEST.json @@ -231,11 +231,11 @@ "format": "PNG", "has_alpha": false, "height": 1024, - "implementation_phase": null, + "implementation_phase": "PHASE-005", "lifecycle_status": "current-redesign", "mode": "RGB", "path": "design/intake/original/Inbox.png", - "review_note": "The three-pane direction is approved; reply, forward, sent, drafts, spam, and move actions are outside the current receive-only feature set.", + "review_note": "PHASE-005A implements a compact receive-only inbox list and unified message reader; reply, forward, sent, drafts, spam, and move actions remain outside scope.", "screen_id": "UI-011", "sha256": "94ff5f7d0988a93a402bb5d41a743d11c8a852f9213294cf560e225efd669f70", "size_bytes": 1242692, @@ -561,7 +561,7 @@ "unsupported_features_inactive": true }, "project": "MailStack", - "release_version": "1.3.1", + "release_version": "1.3.3", "schema_version": 1, "source_archive": { "archive_crc": "PASS", diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index dd5a5f2..93d8bb8 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -11,13 +11,15 @@ MailStack does not expose a public REST API in this release. The supported inter | Mailboxes | `/mailboxes/` | authenticated, object scoped | | Mailbox administration | `/mailboxes/create/`, status/delete routes | authorized administrators | | Messages | `/messages/`, detail/read/delete routes | authenticated, object scoped | -| Live updates | `/messages/live/` | authenticated, membership filtered | +| Live updates | `/messages/live/` | authenticated, membership filtered, MailStack background-request header required for JSON | | User management | `/accounts/users/` | MailStack administrators | | Health | `/health/` | service probe | | Readiness | `/ready/` | service/database probe | Route names and exact patterns are defined in `mailbox-app/config/urls.py` and each app's `urls.py`. +The live-update route is an internal browser transport, not a document page. MailStack JavaScript sends `X-MailStack-Live-Request: 1`; an authenticated top-level navigation without that header redirects to the dashboard instead of returning raw JSON. + ## Public contact endpoint `POST /api/contact/` accepts a JSON contact submission protected by a one-time CSRF token, SameSite cookie, honeypot, timing checks, size limits and rate limits. The service sends only a fixed local notification through `/usr/sbin/sendmail`. diff --git a/docs/BUILD.md b/docs/BUILD.md index 91d1e81..39fbe94 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -20,8 +20,8 @@ python scripts/forensic_audit.py --root . --full ```bash python scripts/build_release.py --root . python scripts/verify_release.py \ - dist/mailstack-1.3.1-source.zip \ - --checksum dist/mailstack-1.3.1-source.zip.sha256 + dist/mailstack-1.3.3-source.zip \ + --checksum dist/mailstack-1.3.3-source.zip.sha256 ``` The builder normalizes archive timestamps, preserves executable permissions, writes a source manifest, excludes generated/runtime artifacts and emits a SHA-256 checksum. @@ -38,4 +38,4 @@ missing successful `main` CI, and pre-existing releases fail closed. ## Stable promotion -The `1.3.1` source baseline is an unpublished version mark. Do not create or push a stable release tag until every required human/operational gate in `docs/PUBLIC_RELEASE_CHECKLIST.md` passes. +`v1.3.1` remains the immutable published source/release baseline. `1.3.3` is the PHASE-005A development target and must not be tagged until branch, PR, main, and release gates pass; live existing-VPS acceptance remains a separate post-release operational step. diff --git a/docs/FEATURE_MATRIX.md b/docs/FEATURE_MATRIX.md index c0cf267..c8f9c9f 100644 --- a/docs/FEATURE_MATRIX.md +++ b/docs/FEATURE_MATRIX.md @@ -14,9 +14,10 @@ The open-source work was performed as an additive and compatibility-preserving c | Maildir ingestion and restart-safe duplicate protection | Preserved | Reliability, parser/storage, and ingestion tests | | Oversized-message handling | Preserved | Ingestion tests | | Safe MIME parsing and HTML sanitization | Preserved | Parser and security tests | +| Unified message reader | Added presentation layer | Sanitized-HTML auto-selection, plain fallback, retained sandbox/no-referrer tests | | Attachment isolation and protected downloads | Preserved | Storage, authorization, and Nginx template checks | -| Search, pagination, counters, read/unread state | Preserved | Message and integration tests | -| Live inbox polling and new-message notification payloads | Preserved | Live-access integration tests | +| Search, pagination, counters, read/unread state | Preserved with compact inbox presentation | Message and PHASE-005A functional/integration tests | +| Live inbox polling and new-message notification payloads | Preserved and UI-guarded | Live-access tests plus direct-navigation/background-header PHASE-005A regression | | Audit trail and health endpoints | Preserved and hardened | Audit/service tests and production deploy checks | | Public static website | Preserved and parameterized | Public-site renderer and template audit | | CSRF-protected, rate-limited contact form | Preserved and parameterized | Contact-service test suite | diff --git a/docs/FORENSIC_AUDIT_REPORT.md b/docs/FORENSIC_AUDIT_REPORT.md index 5006505..d28f237 100644 --- a/docs/FORENSIC_AUDIT_REPORT.md +++ b/docs/FORENSIC_AUDIT_REPORT.md @@ -1,10 +1,10 @@ -# Forensic audit report — MailStack 1.3.1 source baseline verification +# Forensic audit report — MailStack 1.3.3 PHASE-005A correction verification -**PHASE-004C audit date:** 2026-08-17 -**Repository source-baseline version:** `1.3.1` -**Latest published release candidate:** `v1.3.0-rc.4` +**PHASE-005A audit date:** 2026-08-18 +**Immutable official baseline:** `v1.3.1` / `039a6e6eea6e198b4b15612db9d2f208b6305a16` / tree `9437ba2ebac2033a229accd190268b8711d5b26e` +**Corrected development target:** `1.3.3` **Target runtime:** Ubuntu Server 24.04 LTS and CPython 3.12 -**Current classification:** 1.3.1 owner-marked source-baseline correction locally structurally qualified; GitHub requalification and live PHASE-004D acceptance remain pending +**Current classification:** 1.3.2 qualification failed; 1.3.3 corrective candidate under requalification; live-VPS acceptance pending ## Executive disposition @@ -176,6 +176,14 @@ findings, and the focused upgrade/archive/rollback contract suite. No live VPS u PHASE-004D is the real existing-server acceptance boundary. GitHub CI requalification remains pending after the PHASE-004C delta is applied and pushed. +## PHASE-005A UI/navigation reliability boundary + +The PHASE-005A delta starts from the exact released 1.3.1 Git tree and is restricted to the mailbox/message presentation layer, the live-polling request header/guard, focused tests, version-specific CI/release metadata, and required documentation/manifests. It does not change models, migrations, parser/sanitizer rules, object authorization, message deletion semantics, attachment storage/download authorization, Postfix/Dovecot, Maildir, MariaDB, installer, deployment templates, backup/restore, or PHASE-004 upgrade/rollback behavior. + +The raw-JSON failure mode is closed by requiring `X-MailStack-Live-Request: 1` before the authenticated live endpoint returns JSON. MailStack JavaScript sends that header; ordinary authenticated document navigation redirects to the dashboard. The compact inbox and unified reader remove the visible body-format tabs while preserving the existing sanitized-HTML sandbox/no-referrer boundary and plain-text fallback. + +Dependency-free/local qualification completed with 18 managed documents/6 phases, 25 verified design PNGs, 4 documentation tests, 4 design tests, 8 UI-foundation contracts, 13 deployment templates, installer and operations contracts, 7 release-workflow contracts, upgrade/rollback contracts, generated forensic inventory, and a structural forensic scan of 415 files / 149 Python / 15 shell with zero blocking findings. The artifact environment has Python 3.13 only and no package-network access, so the supported Python 3.12 Django/Ruff/Bandit/pip-audit suite is not fabricated here and remains mandatory in the user's isolated environment and GitHub CI. + ## Security review The RC4 source remains pinned to Django 5.2.16, Python 3.12, and `sqlparse==0.6.0`. The blocking @@ -197,13 +205,22 @@ privilege, systemd confinement, archive safety, checksum verification, and fail- 6. Confirm copyright ownership and third-party license compatibility. 7. Obtain final release-owner acceptance before promoting `1.3.0` stable. +## PHASE-005A 1.3.2 failure and 1.3.3 corrective findings + +The Windows qualification captured Python 3.12.8, 202 passed plus one host-capability symlink skip, +94.97% coverage, Django system-check PASS, no migration drift, dependency audit with no known +vulnerabilities, Bandit PASS, and contact-service PASS. Ruff reported three `I001` import-order +findings. The malformed prose-paste CMD handoff also created an unintended empty `85%` file. GitHub +Actions run `32128090322` for commit `76540ccf48d1a4d44175b5e3b120e91d0b8c226e` failed in the +source-safety audit because the extra file made `docs/FORENSIC_FILE_INVENTORY.json` stale, so later +CI gates were skipped and that SHA is not qualified. + +A separate compatibility review found that the released Nginx static policy caches `/static/` for +seven days with `immutable`, while the legacy poller sends `Accept: application/json` without the new +custom header. Requiring only the custom header would therefore temporarily break live polling for +a browser retaining old JavaScript across upgrade. The 1.3.3 guard accepts either trusted polling +signature while continuing to reject ordinary document navigation. + ## Final disposition -The published `v1.3.0-rc.4` source is a qualified release candidate and is the official frozen source -baseline for PHASE-004. PHASE-004A corrects the documentation evidence around that baseline but does -not retroactively modify or retag RC4. The owner-marked working source baseline is `1.3.1` and remains unpublished. PHASE-004B branch CI -has passed. PHASE-004C GitHub Actions run `32097491341` reached Ruff after all earlier gates passed, -then failed on five style findings in the new archive verifier; downstream runtime/release gates were -skipped. This scoped correction removes only those Ruff findings and synchronizes the requested 1.3.1 -baseline/release metadata. A fresh GitHub CI run is required before remote qualification, and no live -upgrade is claimed until the separately approved PHASE-004D acceptance campaign. +Published `v1.3.1` remains the immutable official source/release baseline for PHASE-005A. The corrected `1.3.3` development delta closes the raw-JSON document-navigation boundary and implements the compact inbox/unified message reader without changing receive-only business/data/mail-flow contracts. Local structural and deterministic-build gates pass; supported Python 3.12 dependency-backed regression, GitHub branch/PR/main CI, tag/release publication, and the later controlled existing-VPS upgrade remain explicit follow-on gates. diff --git a/docs/FORENSIC_FILE_INVENTORY.json b/docs/FORENSIC_FILE_INVENTORY.json index 3c5d568..c049f59 100644 --- a/docs/FORENSIC_FILE_INVENTORY.json +++ b/docs/FORENSIC_FILE_INVENTORY.json @@ -74,14 +74,14 @@ "kind": "text", "lines": 135, "path": ".github/workflows/ci.yml", - "sha256": "3622f22f4aa5fa5d1bf89b17d7807cc6f837ec92d70229dbc35b284820419c71", + "sha256": "391f3181edec60e6891363e52228f503af929dc120653406f28dbca1c0729e75", "size_bytes": 4291 }, { "kind": "text", "lines": 131, "path": ".github/workflows/release.yml", - "sha256": "a6543e8464e093e05d083f2d82566d9e209480778cf2689e30441d491955b53e", + "sha256": "a3112863ee1d7bf8b2a02a3be42a51e39109dbe48f6d07ff17a4b9270289d4dd", "size_bytes": 4418 }, { @@ -93,10 +93,10 @@ }, { "kind": "text", - "lines": 131, + "lines": 166, "path": "CHANGELOG.md", - "sha256": "f2bcc49e9eeccf027add792cb3b10b2a2f579e13eedbf5163dc7d11b1b1c5d31", - "size_bytes": 12361 + "sha256": "f06f5cb6d72d28b9d58daff8dcb68e07c0a29723f86a01e9db27cdad0a0da8f2", + "size_bytes": 16029 }, { "kind": "text", @@ -137,8 +137,8 @@ "kind": "text", "lines": 314, "path": "README.md", - "sha256": "2b1c0efbecd82fe86822628a72a2db26c454758016d9c086633d1ac2a0db1980", - "size_bytes": 13196 + "sha256": "6b7c3ba9ea12eaa8c2ff14b3c43dd0b30b5602c907cc1be69990981bc26e59da", + "size_bytes": 13279 }, { "kind": "text", @@ -165,7 +165,7 @@ "kind": "text", "lines": 1, "path": "VERSION", - "sha256": "10e2e117b01523891ca271a615ae36e9d5e52348fd7d642f66727602c1b8dcbe", + "sha256": "a74a0cf84ef0b15e01e64746c3465813312b763796f4f6362b092f092dccb8b5", "size_bytes": 6 }, { @@ -277,8 +277,8 @@ "kind": "text", "lines": 581, "path": "design/DESIGN_MANIFEST.json", - "sha256": "7192a7c8fd128c90fc58ad1609d21a1853ce0654d3f28a98ce30aa7727e4f069", - "size_bytes": 21138 + "sha256": "c831c949ca88576a0cc017def8f8c90bee24b7b1b51c426ead7e080ffa9e7c37", + "size_bytes": 21163 }, { "kind": "text", @@ -439,10 +439,10 @@ }, { "kind": "text", - "lines": 27, + "lines": 29, "path": "docs/API_REFERENCE.md", - "sha256": "758f5e4e826fd4d51749016108e14efa6ba52a2ccecb96c0d9eb999417d25533", - "size_bytes": 1452 + "sha256": "e92ea7f2e9146c6c6be9013d642396b953d9aa54cc6d080389dc7b56916f1f92", + "size_bytes": 1759 }, { "kind": "text", @@ -469,8 +469,8 @@ "kind": "text", "lines": 41, "path": "docs/BUILD.md", - "sha256": "76a2e43a20c3d2e9f0de047c21802d43cf68671eb127489f53b982eaf9780748", - "size_bytes": 1456 + "sha256": "51325b51bfe3699a75b1a492b7f1c51c65d435cbe7e4216e47c475b8f23e42c4", + "size_bytes": 1524 }, { "kind": "text", @@ -516,10 +516,10 @@ }, { "kind": "text", - "lines": 26, + "lines": 27, "path": "docs/FEATURE_MATRIX.md", - "sha256": "4b01db4ee35b75ac02f20b173549792116265cbd592092bcf189a8ae3bee5ee4", - "size_bytes": 2531 + "sha256": "26ceb61faf9a3106f8e3b03701fa537f875ff3a7282ab656da00317e13f11211", + "size_bytes": 2789 }, { "kind": "text", @@ -537,10 +537,10 @@ }, { "kind": "text", - "lines": 209, + "lines": 226, "path": "docs/FORENSIC_AUDIT_REPORT.md", - "sha256": "71bf39fbd6550b6e6607281e2f82f411e0b4b03614e4a1754043edf7e17f7f0a", - "size_bytes": 12463 + "sha256": "749c9d782eeb80b12d7333d44f81554f63f296234eb5e66a1db7fbad37632db6", + "size_bytes": 15035 }, { "kind": "text", @@ -623,8 +623,8 @@ "kind": "text", "lines": 67, "path": "docs/PUBLISHING.md", - "sha256": "2ac720ae114b5d0dacf01330cd3fba4f8bab412af1aecb6742c75e4f33e7b90c", - "size_bytes": 3002 + "sha256": "cf828764d5c7c3a638ccb097f6eee0bbb2f4fa68b7dbe0f16425b96b7a7695a4", + "size_bytes": 3040 }, { "kind": "text", @@ -654,11 +654,25 @@ "sha256": "eb92579ccc92d1d49513d458e80f4b6349f4451524d5994b11b36a3fb36bf79e", "size_bytes": 2841 }, + { + "kind": "text", + "lines": 39, + "path": "docs/RELEASE_NOTES_1.3.2.md", + "sha256": "dd7e4445a6d6e406f108df9dff42c8babd62369583215caf898ef501bcab971c", + "size_bytes": 2291 + }, + { + "kind": "text", + "lines": 40, + "path": "docs/RELEASE_NOTES_1.3.3.md", + "sha256": "befdf20ee40d8ff4058b8e97d606f7ab5472f26b7d5f5d816d6868ad09a6c2f5", + "size_bytes": 2221 + }, { "kind": "text", "lines": 16, "path": "docs/RELEASE_PROCESS.md", - "sha256": "40091dbd439861d5028c72d9d9b668f15dd71c8f48ddc3a7d9635a950dcc09db", + "sha256": "6692e68e3366fdc666ea7579168e5594a532adbeedb64cdf83514acfd9f65215", "size_bytes": 1619 }, { @@ -670,17 +684,17 @@ }, { "kind": "text", - "lines": 78, + "lines": 82, "path": "docs/SECURITY_REVIEW.md", - "sha256": "5932185782f375e75cbd931727004afdb68549d796cc000fd537dee4f404a12e", - "size_bytes": 5077 + "sha256": "3aa7df50d5f5d084525faa8b2ec13689e1a38ac702d0c050c56fd2080055d037", + "size_bytes": 5891 }, { "kind": "text", - "lines": 194, + "lines": 237, "path": "docs/TEST_REPORT.md", - "sha256": "9fc597cae2aa5be46cc12e39ca3458e68f84060d1adfdee381352c77dad6f082", - "size_bytes": 10711 + "sha256": "47d8958ef4464b69009f039a6acaa2bb1d8b1760ba9b1070b99a500bc3b65dde", + "size_bytes": 13953 }, { "kind": "text", @@ -707,134 +721,141 @@ "kind": "text", "lines": 68, "path": "documents/ADMIN_GUIDE.md", - "sha256": "018a50bca6cc47ef51adda6104dc77a306aee87abeee7d9410f7f3f13e3182f3", + "sha256": "d7ecd6fc82533efa84be7137806e187e1987792950724523d6d45998c4ea9607", "size_bytes": 3850 }, { "kind": "text", - "lines": 70, + "lines": 64, "path": "documents/BASELINE.md", - "sha256": "5343fed8ab585b7f19341b7727e8dadb0279d3f3ee06dfb06992dd687e3fbeb8", - "size_bytes": 3847 + "sha256": "4e292bf865a6afebe2bf3fe28f1743ac26ecab907cbeabd7e5df8e9c43ee617d", + "size_bytes": 3611 }, { "kind": "text", - "lines": 221, + "lines": 233, "path": "documents/DOCUMENTATION_MANIFEST.json", - "sha256": "dd4967dcc579af807a16aaee228d8380d70bca7ed6e2944072f6ba15a5407795", - "size_bytes": 8339 + "sha256": "3d30391368f83f24bedd83aead7cfa9045f71e4496dbe0f4e7e9939852142ec3", + "size_bytes": 8904 }, { "kind": "text", "lines": 76, "path": "documents/DOCUMENTATION_POLICY.md", - "sha256": "c8910661b15e941c6e2db493973f6e3bdaeba64ad8554eb227d1245c8c20c9a5", + "sha256": "1815fb7353fa22f9cd28de8cf18a8f0899b13616795fa20f98f3b686edcc9fae", "size_bytes": 3184 }, { "kind": "text", - "lines": 76, + "lines": 83, "path": "documents/HOW_TO_USE.md", - "sha256": "364f048cd26bd0f564f7a47c26b4624942981bbdf748d9ba7c28ceba705b65a5", - "size_bytes": 3248 + "sha256": "2a5757aa6f345d3df7c6d51d4472bf89e1fe3184a7a86db0be4e4d3fed7c08e6", + "size_bytes": 3858 }, { "kind": "text", - "lines": 54, + "lines": 55, "path": "documents/README.md", - "sha256": "570e8b3d5c078a70fc346c9bb15d83adac28316f968944c71920e7212affa794", - "size_bytes": 3788 + "sha256": "c7771ce012e5f6649cf7ca3f299e53d6e4c53c0de60fee96d5a8a13440266ac1", + "size_bytes": 3990 }, { "kind": "text", - "lines": 82, + "lines": 89, "path": "documents/USER_MANUAL.md", - "sha256": "778de1cec3ef89171fd2768064940c21c5f0d8f444e5376e4b43123cb3a58f13", - "size_bytes": 4213 + "sha256": "951d3643e7272352fcd55253f6dbe538b1eedb4140f10706bfafc0aa6dc08b6d", + "size_bytes": 4835 }, { "kind": "text", "lines": 46, "path": "documents/design/ACCESSIBILITY_SPECIFICATION.md", - "sha256": "c9224cc17caca417a5eb0d363b7b472e250890edef028b01edc7d58cec753aa3", + "sha256": "278c6a4b427b09330d4be2488440ae6955ccba40579911e87c7b0d0e493a94e8", "size_bytes": 2182 }, { "kind": "text", "lines": 55, "path": "documents/design/COMPONENT_MATRIX.md", - "sha256": "957a04577dc899fd9f457b59e029a1b4d50b6ab5c6089f6a415339cd20cbe5e5", + "sha256": "6eec7042e88f8c49385167e7da0fe438157751e07dfa3eeb21932073f29d1f4e", "size_bytes": 3460 }, { "kind": "text", "lines": 52, "path": "documents/design/FUTURE_UI_ROADMAP.md", - "sha256": "078989742c6aab70840e48ccf887c472f538079e48a73811eb48b0506d9a413a", + "sha256": "1a00207cd0bd1b32ce66e5253b4dc77838215586aa37caf259d8456302d4cd13", "size_bytes": 1562 }, { "kind": "text", "lines": 48, "path": "documents/design/IMPLEMENTATION_STATUS.md", - "sha256": "ad807f84993961013133f0c8e136c4fc5d636fd6051e352e6fb7d01b43e1be9f", - "size_bytes": 2513 + "sha256": "71c75905bd097977838855090269865aba4fe7f10d2c8ee2a1236641b92fd758", + "size_bytes": 2691 }, { "kind": "text", "lines": 51, "path": "documents/design/RESPONSIVE_SPECIFICATION.md", - "sha256": "b540f41863ec20c34871d1b0abf39517a40a2ec1a183128d8ce321e9d7a8867a", + "sha256": "0fae978b691e5c460e919668ec3cc1d870f1ac510f7b1613b3f61b286cd80984", "size_bytes": 2022 }, { "kind": "text", - "lines": 62, + "lines": 65, "path": "documents/design/SCREEN_CATALOG.md", - "sha256": "0f45bcdddea7a40c6bcaa753bcc5b8d7c49a2d66f383003c87244ae865c007a6", - "size_bytes": 3287 + "sha256": "ecc0b62d2f30a09f704a7a1e70b1e72a0f85746cd5d5c13ad9c431e6967b697c", + "size_bytes": 3480 }, { "kind": "text", "lines": 89, "path": "documents/design/UI_FOUNDATION.md", - "sha256": "38b97a9c193ad8a7a5b21d031535afa72b3e5fc3b9f6772abe37c1379dc0c459", + "sha256": "5082d1f28afe1414d59a3c97f83a08a45d9c3242c2a057dae6d30b09b4c57785", "size_bytes": 3753 }, { "kind": "text", "lines": 59, "path": "documents/phases/PHASE-000-BASELINE.md", - "sha256": "9c04cd5594cd07cc487f47fe7e6fd751775ba202232b84db3385e959f1ceba57", + "sha256": "0ca507b4dfe36daf118b81ebb9de19472c2c161d0e45256805513c8895a83574", "size_bytes": 2538 }, { "kind": "text", "lines": 68, "path": "documents/phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md", - "sha256": "b30ca3041469b56b1c3256cab99d10b12f3d00241043cb449d466ecbde352154", + "sha256": "af6bb679cac06f760e00c5a40cfafee4ed30306a22782dc5b82e1b5039df36a6", "size_bytes": 2765 }, { "kind": "text", "lines": 98, "path": "documents/phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md", - "sha256": "a1e2f224910ad0eea4d43f87f0734f3ecbd394e804497b7e6cc3c8a5ec2567dc", + "sha256": "e3c8edf18c64865786c6be6d11f9424b37a54d98f095a05948bdf0773cfc8de0", "size_bytes": 5488 }, { "kind": "text", "lines": 86, "path": "documents/phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md", - "sha256": "2b63990b712eeab3c7e28f948385a3485667a4397a4c9f0b4a731e72165393df", + "sha256": "bb6a6a8eb7d41ca882b9e9f83c3371fe78325e43472c3065e130e5923da78e96", "size_bytes": 5050 }, { "kind": "text", - "lines": 142, + "lines": 153, "path": "documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md", - "sha256": "964c57e99da6bc7b63e5ee577129cf7aa45770e9bdc4c70e630bfac502dba828", - "size_bytes": 9368 + "sha256": "f45403765474a74f72607b1c37ca5cb0df6d0a546a4e436500ef427dcaf64f16", + "size_bytes": 10061 + }, + { + "kind": "text", + "lines": 102, + "path": "documents/phases/PHASE-005-UI-NAVIGATION-RELIABILITY-AND-COMPACT-MAILBOX-READER.md", + "sha256": "198f20d9732395701ac5ebb57d2e3d3a74d437e7b1a657630f697cf46a60b545", + "size_bytes": 6080 }, { "kind": "text", @@ -881,10 +902,10 @@ }, { "kind": "text", - "lines": 88, + "lines": 101, "path": "mailbox-app/CHANGELOG.md", - "sha256": "c47ddb63ffb221477ce9d4901e14ad67a26870a4fbf1ffc2bc2514b5d1da6637", - "size_bytes": 5891 + "sha256": "5f5c340754416dd076a9ec6aae0303b8c588d8befb9eee1c79ca32312a44eb34", + "size_bytes": 7208 }, { "kind": "text", @@ -2692,12 +2713,14 @@ }, { "kind": "text", - "lines": 268, + "lines": 299, "path": "mailbox-app/apps/messages/views.py", "python": { "classes": [], "functions": [ + "_is_live_background_request", "_iso", + "_message_preview", "attachment_download", "inbox", "live_updates", @@ -2720,6 +2743,7 @@ "django.http", "django.shortcuts", "django.urls", + "django.utils.html", "django.utils.http", "django.views.decorators.cache", "django.views.decorators.http", @@ -2729,8 +2753,8 @@ ], "methods": [] }, - "sha256": "3e3c99ee38ea3861bd3a2791b47c628d24ebd5bb8b6e46ea4b21931f5aa36c99", - "size_bytes": 10256 + "sha256": "ce92fc9d7521c997076773a538371f8f79dfbfc4acbf41ec1c964b709f7e6e8a", + "size_bytes": 11581 }, { "kind": "text", @@ -3173,10 +3197,10 @@ }, { "kind": "text", - "lines": 80, + "lines": 84, "path": "mailbox-app/pyproject.toml", - "sha256": "adfb3bb65672b6c0723c80a73def340a586f4dff623a6f4b41dab44053fc9723", - "size_bytes": 2408 + "sha256": "8f46570ed6b7458b712e653ef9462f41de5c703fd10c82d63dc678f04aa9285e", + "size_bytes": 2524 }, { "kind": "text", @@ -3435,8 +3459,8 @@ "kind": "text", "lines": 7, "path": "mailbox-app/static/css/app.css", - "sha256": "14abca400e3209482f0d6513dfcc926eaaff062259d523f79a9a304ed86da2f8", - "size_bytes": 12559 + "sha256": "ddbcd7c0e3d350d33e822d3e09f163d76d69caa65363719c845dd9a7b4edb398", + "size_bytes": 17510 }, { "kind": "text", @@ -3454,10 +3478,10 @@ }, { "kind": "text", - "lines": 512, + "lines": 523, "path": "mailbox-app/static/js/app.js", - "sha256": "7d3337241e78a772969ec37a41b69c9ea5f22a105a35f14fe12cb7b6f0f005dd", - "size_bytes": 18848 + "sha256": "af0d95ec5651fb19b3481a0a341e8952232ed14d372e91041126f6ca1bed3042", + "size_bytes": 19142 }, { "kind": "text", @@ -3545,17 +3569,17 @@ }, { "kind": "text", - "lines": 9, + "lines": 77, "path": "mailbox-app/templates/messages/detail.html", - "sha256": "a18ebccd77879c4e85f7919df3f9acc7b1804a7367d88ad21fbb6075296f67c5", - "size_bytes": 3355 + "sha256": "d27e7ad4f252c22265e205867adbca51117e72a21518a6c5778eda4db1e2ca76", + "size_bytes": 4921 }, { "kind": "text", - "lines": 8, + "lines": 56, "path": "mailbox-app/templates/messages/inbox.html", - "sha256": "431984e878957fab3faea75cad364f2d2ecf50ad667fdc138ad6e209131d9f83", - "size_bytes": 2802 + "sha256": "7bcb6c11b13bbab28d0669b243fe00d9c2f26780cfb74cf9c9df5ebf94b210a0", + "size_bytes": 3810 }, { "kind": "text", @@ -3821,7 +3845,30 @@ }, { "kind": "text", - "lines": 54, + "lines": 92, + "path": "mailbox-app/tests/functional/test_compact_mailbox_reader.py", + "python": { + "classes": [], + "functions": [ + "test_compact_inbox_renders_preview_without_legacy_body_tabs", + "test_live_payload_uses_html_text_as_preview_when_plain_body_is_missing", + "test_live_updates_require_background_request_header", + "test_plain_only_message_uses_unified_plain_fallback", + "test_unified_reader_prefers_sanitized_html_and_preserves_sandbox" + ], + "imports": [ + "__future__", + "django.urls", + "pytest" + ], + "methods": [] + }, + "sha256": "63f45f2894211f02ab6bd9e6e17fa4d2b381dd759a863d89525be5464ebb5e5d", + "size_bytes": 3514 + }, + { + "kind": "text", + "lines": 64, "path": "mailbox-app/tests/functional/test_responsive_and_routes.py", "python": { "classes": [], @@ -3839,8 +3886,8 @@ ], "methods": [] }, - "sha256": "cf0ec039b5ea829efcf6cdef5e823f6e7c89732eefc7e203cd66a5c44eaf780e", - "size_bytes": 1941 + "sha256": "f1f1c3a525afeb9f39341725639b30e47d9236ce55b325392fc30a796f82225c", + "size_bytes": 2300 }, { "kind": "text", @@ -3884,7 +3931,7 @@ }, { "kind": "text", - "lines": 281, + "lines": 284, "path": "mailbox-app/tests/integration/test_auth_views.py", "python": { "classes": [], @@ -3918,8 +3965,8 @@ ], "methods": [] }, - "sha256": "f3cfb0bbf7e95df33bdf6e7cc1097d64b9f6c2ec27beb7686b58ccd826dd0131", - "size_bytes": 11321 + "sha256": "8b28feea3ff1269e42cd3f2107048ac9c5c45b25653465fa0e838fa55c0f08f6", + "size_bytes": 11479 }, { "kind": "text", @@ -3990,7 +4037,7 @@ }, { "kind": "text", - "lines": 538, + "lines": 546, "path": "mailbox-app/tests/integration/test_v1_2_access_live_delete.py", "python": { "classes": [], @@ -4050,8 +4097,8 @@ ], "methods": [] }, - "sha256": "150ebedef70943178e56adcb618b0aa11c856d8b4c75495478db58b46b148caf", - "size_bytes": 21659 + "sha256": "03676d4ba784322bd6095efb0076b94a83a0825c3e49e8c26607fd932eced4cf", + "size_bytes": 21964 }, { "kind": "text", @@ -4763,7 +4810,7 @@ }, { "kind": "text", - "lines": 406, + "lines": 408, "path": "scripts/forensic_audit.py", "python": { "classes": [], @@ -4789,8 +4836,8 @@ ], "methods": [] }, - "sha256": "5238686caff6906b9a69417df0cd0eb66a942d440fc13ceb9d80cc608bb9fc1e", - "size_bytes": 14867 + "sha256": "b5dab090e769878e424e7d2e9bbedb5dcf3011cb0f00fde35d3f2ac3d4dfadd5", + "size_bytes": 14937 }, { "kind": "text", @@ -5162,12 +5209,12 @@ ], "methods": [] }, - "sha256": "2796bcdb10d427c00d4bf778ab6281883f6cb7c479f2b430e15a072e96bffeee", + "sha256": "fddfa48bb4763d5c33034d2501ae5c0b25a7a875b1fbd5a156d921bd0fe323b6", "size_bytes": 6570 }, { "kind": "text", - "lines": 215, + "lines": 216, "path": "scripts/test_ui_foundation.py", "python": { "classes": [], @@ -5191,8 +5238,8 @@ ], "methods": [] }, - "sha256": "c77bf4bbdacc4848a0de4a271df9b8978b61b7b4da77737b4f6c2f3cc3e29eb8", - "size_bytes": 7417 + "sha256": "1e555a71f80dee2ee53153f6df958c86e424ccba92ad83dbb44797f50a694462", + "size_bytes": 7473 }, { "kind": "text", @@ -5274,7 +5321,7 @@ "size_bytes": 6930 } ], - "release_version": "1.3.1", + "release_version": "1.3.3", "schema_version": 1, "scope": "All maintained repository files excluding generated archives, caches, build output and this inventory file itself.", "self_entry": { @@ -5283,15 +5330,15 @@ }, "summary": { "binary_files": 25, - "files": 411, + "files": 415, "python_classes": 76, - "python_files": 148, - "python_functions": 446, + "python_files": 149, + "python_functions": 453, "python_methods": 61, "shell_files": 15, "shell_functions": 41, - "text_files": 386, - "total_bytes": 31754565, - "total_text_lines": 28846 + "text_files": 390, + "total_bytes": 31794098, + "total_text_lines": 29455 } } diff --git a/docs/PUBLISHING.md b/docs/PUBLISHING.md index f839193..1464983 100644 --- a/docs/PUBLISHING.md +++ b/docs/PUBLISHING.md @@ -12,10 +12,10 @@ This repository is prepared for public hosting on GitHub, GitLab, Codeberg, or a 4. Build and verify the deterministic release archive: ```bash - python scripts/build_release.py --root . --version 1.3.1 + python scripts/build_release.py --root . --version 1.3.3 python scripts/verify_release.py \ - dist/mailstack-1.3.1-source.zip \ - --checksum dist/mailstack-1.3.1-source.zip.sha256 + dist/mailstack-1.3.3-source.zip \ + --checksum dist/mailstack-1.3.3-source.zip.sha256 ``` 5. Complete the clean Ubuntu 24.04 acceptance checklist before promoting a release candidate to production-ready. @@ -27,7 +27,7 @@ From the audited repository root: ```bash git init git add . -git commit -m "Prepare MailStack 1.3.1 candidate" +git commit -m "Prepare MailStack 1.3.3 candidate" git branch -M main git remote add origin git push -u origin main @@ -37,8 +37,8 @@ Create the annotated stable-looking tag only after the intended release commit i `main` head and the exact `main` SHA has a successful push CI run: ```bash -git tag -a v1.3.1 -m "MailStack 1.3.1" -git push origin v1.3.1 +git tag -a v1.3.3 -m "MailStack 1.3.3" +git push origin v1.3.3 ``` The tag push triggers `.github/workflows/release.yml`. It fails closed unless the tag matches @@ -46,11 +46,11 @@ The tag push triggers `.github/workflows/release.yml`. It fails closed unless th has no existing GitHub Release. The workflow rebuilds and verifies the deterministic source archive, keeps a GitHub Actions artifact, and automatically creates the GitHub Release with: -- `mailstack-1.3.1-source.zip` -- `mailstack-1.3.1-source.zip.sha256` +- `mailstack-1.3.3-source.zip` +- `mailstack-1.3.3-source.zip.sha256` RC tags are published as pre-releases and are explicitly not marked latest; stable tags are normal -latest releases. The `1.3.1` baseline must not be tagged/published until the outstanding operational and release-owner gates are accepted. Manual `workflow_dispatch` is validation/build-only and cannot publish. Existing +latest releases. `v1.3.1` remains the frozen published baseline; the `1.3.3` PHASE-005A line must not be tagged until its branch/PR/main CI gates and release-owner review pass. Manual `workflow_dispatch` is validation/build-only and cannot publish. Existing releases are never automatically edited, clobbered, or overwritten. ## Repository settings diff --git a/docs/RELEASE_NOTES_1.3.2.md b/docs/RELEASE_NOTES_1.3.2.md new file mode 100644 index 0000000..39ea346 --- /dev/null +++ b/docs/RELEASE_NOTES_1.3.2.md @@ -0,0 +1,39 @@ +# MailStack 1.3.2 PHASE-005A notes + +## Scope + +MailStack 1.3.2 is the PHASE-005A UI-navigation reliability and compact mailbox-reader update built +from the immutable published `v1.3.1` source baseline. It changes the mailbox/message presentation +layer and the narrow live-update request contract only. It does not add outbound mail, alter mailbox +permissions, change models or migrations, relax HTML sanitization, change Postfix/Dovecot delivery, +or modify Maildir, MariaDB, DNS, TLS, installer, backup, upgrade, or rollback semantics. + +## Navigation reliability + +The authenticated live-update JSON endpoint now requires an explicit background-request header from +MailStack JavaScript. A normal authenticated document navigation to `/messages/live/` is redirected +to the application dashboard rather than rendering the JSON payload as a browser page. The live +poller sends the required header and retains same-origin credentials, private no-store responses, +bounded payloads, authorization filtering, polling backoff, and notification behavior. + +## Compact inbox + +Mailbox inbox pages now use a denser webmail-style surface with a compact mailbox header, integrated +search/read/attachment filters, tighter sender/subject rows, unread emphasis, short message preview, +attachment/size metadata, and responsive desktop/mobile layout. Live-inserted rows use the same +presentation contract as server-rendered rows. + +## Unified message reader + +The visible `Plain text` / `Safe HTML` tabs are removed. When sanitized HTML exists, the message is +shown automatically in the existing sandboxed, no-referrer safe frame. Plain-only messages are shown +as a readable unified fallback. The reader uses a compact subject/sender/action header, expandable +routing metadata, integrated protected-rendering notice, and compact attachment section. Sanitizer +rules, CSP isolation, object authorization, mark-unread, delete permissions, and attachment download +controls remain unchanged. + +## Qualification boundary + +The replace-ready PHASE-005A delta must pass the repository structural gates locally and the complete +Python 3.12 dependency-backed GitHub CI suite before merge or release. Existing-live-VPS deployment is +intentionally deferred until the 1.3.2 branch, PR, main, tag, and release checks are complete. diff --git a/docs/RELEASE_NOTES_1.3.3.md b/docs/RELEASE_NOTES_1.3.3.md new file mode 100644 index 0000000..1618f52 --- /dev/null +++ b/docs/RELEASE_NOTES_1.3.3.md @@ -0,0 +1,40 @@ +# MailStack 1.3.3 PHASE-005A correction notes + +## Scope + +MailStack 1.3.3 is the corrected PHASE-005A development candidate built from the immutable published +`v1.3.1` baseline after the first `1.3.2` qualification attempt failed. The compact inbox and unified +safe message reader remain the approved UI change. This correction is limited to qualification +hygiene, live-poller backward compatibility, version/release metadata, tests, and synchronized +documentation/forensic manifests. + +## Corrected qualification findings + +- Removes the accidental empty root file `85%` created by Windows CMD output-redirection parsing of + explanatory text containing `>=85%`. +- Corrects the PHASE-005A Ruff `I001` import-order findings and regenerates the source inventory. +- Records GitHub Actions run `32128090322` as a failed `1.3.2` branch qualification at the + source-safety inventory gate; downstream CI gates did not qualify that SHA. + +## Live-update compatibility + +The server continues to redirect ordinary authenticated navigation away from `/messages/live/`, so +raw JSON is not presented as an application page. Background polling accepts the new explicit +`X-MailStack-Live-Request: 1` header and also the legacy `Accept: application/json` signature used by +the previously released JavaScript. This compatibility path prevents live polling from breaking for +browsers that retain the seven-day immutable static cache across an application upgrade. + +## Preserved boundaries + +No database model or migration, parser/sanitizer policy, authorization, mailbox lifecycle, message +state/delete semantics, attachment authorization, ingestion, Postfix/Dovecot LMTP, Maildir, MariaDB, +public-site/contact, installer, deployment template, backup/restore, or PHASE-004 upgrade/rollback +semantics are changed. No compose, reply, forward, sent, draft, IMAP, POP3, or public registration +capability is added. + +## Qualification boundary + +`1.3.3` is a development candidate until the corrected delta passes the isolated Python 3.12 local +qualification and a new GitHub branch CI run, followed by PR, main CI, tag, automated release, and +owner acceptance. Existing live-VPS upgrade remains deferred until that release qualification is +complete. diff --git a/docs/RELEASE_PROCESS.md b/docs/RELEASE_PROCESS.md index 2d4b99d..9e42afe 100644 --- a/docs/RELEASE_PROCESS.md +++ b/docs/RELEASE_PROCESS.md @@ -7,7 +7,7 @@ 5. Run the online dependency audit in CI. 6. Test a clean installation on an isolated Ubuntu Server 24.04 LTS VPS. 7. Verify external inbound SMTP, login, authorization boundaries, live updates, safe HTML, and attachment downloads. -8. Build with `python scripts/build_release.py --root . --version 1.3.1` and verify the ZIP/checksum with `python scripts/verify_release.py`. +8. Build with `python scripts/build_release.py --root . --version 1.3.3` and verify the ZIP/checksum with `python scripts/verify_release.py`. 9. Merge the intended release commit to `main` and require a successful `main` push CI run for the exact SHA. 10. Create the matching `v` tag on the current `main` head and push that tag. Do not create a release manually first. 11. The tag workflow validates tag/version/package identity, exact `main` head, successful `main` CI, and release absence; it then builds/verifies the deterministic archive and creates the GitHub Release with ZIP/SHA assets. diff --git a/docs/SECURITY_REVIEW.md b/docs/SECURITY_REVIEW.md index 7e05288..4318912 100644 --- a/docs/SECURITY_REVIEW.md +++ b/docs/SECURITY_REVIEW.md @@ -1,4 +1,4 @@ -# Security review — MailStack 1.3.0 +# Security review — MailStack 1.3.3 PHASE-005A correction development ## Security posture @@ -74,5 +74,9 @@ GitHub Actions. - JavaScript contract tests block `eval`, `new Function`, `innerHTML`, and `document.write` in the maintained application script. -The shared shell does not change the existing message HTML sanitizer or sandbox. Message-reader -hardening and redesign remain a separate security-focused phase. +## PHASE-005A message-reader and live-transport review + +- The existing HTML sanitizer allowlist, URL checks, remote-image blocking, CSP, iframe sandbox, and no-referrer policy are unchanged. PHASE-005A changes presentation only: sanitized HTML is selected automatically when present, and plain text is the fallback. +- The live-update JSON transport now requires the explicit `X-MailStack-Live-Request: 1` background-request header in addition to existing authentication and object scoping. Authenticated document navigation without that header redirects to the dashboard, preventing the transport payload from becoming a user-visible page. +- The browser still uses same-origin credentials, `Accept: application/json`, no-store caching, bounded polling, and existing notification/backoff logic. +- No compose/reply/forward capability, sanitizer relaxation, model/migration, attachment policy, mail-flow, or authorization change is introduced. diff --git a/docs/TEST_REPORT.md b/docs/TEST_REPORT.md index b89e5fc..bf3b866 100644 --- a/docs/TEST_REPORT.md +++ b/docs/TEST_REPORT.md @@ -1,8 +1,8 @@ -# Test report — MailStack 1.3.1 source baseline verification +# Test report — MailStack 1.3.3 PHASE-005A correction verification -**PHASE-004C verification date:** 2026-08-17 -**Latest published release candidate:** `v1.3.0-rc.4` -**Repository source-baseline version:** `1.3.1` +**PHASE-005A verification date:** 2026-08-18 +**Immutable published baseline:** `v1.3.1` +**Corrected development target:** `1.3.3` ## Authoritative released-RC4 application suite @@ -192,3 +192,46 @@ or release publication. A future isolated Ubuntu 24.04 acceptance campaign must cover the exact-source clean install, TLS/DNS/mail routing, unknown-recipient rejection, real inbound delivery, authorization isolation, contact delivery, backup, restore, restart/reboot recovery, and final release-owner/legal review. +## PHASE-005A local verification + +PHASE-005A adds focused Django regression tests for direct live-endpoint navigation, explicit background JSON polling, compact inbox preview rendering, unified sanitized-HTML selection, plain-only fallback, and HTML-derived live preview text. Existing live authorization, bounded-payload/query-count, search/filter/pagination, sandbox/no-referrer, XSS, attachment, mark-unread, delete-permission, and shared-shell tests remain in the suite. + +Dependency-free/local gates completed as follows: + +| PHASE-005A local gate | Result | +|---|---| +| Documentation validation | PASS — 46 required files, 60 local links | +| Managed-document synchronization | PASS — 18 documents, 6 phases | +| Documentation tests | PASS — 4 contracts | +| Design manifest integrity | PASS — 25 PNG references | +| Design tests | PASS — 4 contracts | +| Shared UI foundation | PASS — 8 contracts | +| JavaScript syntax | PASS | +| Python syntax for changed Python files | PASS | +| Deployment template validation | PASS — 13 templates | +| Installer contracts | PASS — 2 valid, 9 invalid | +| Operations contracts | PASS — 4 scripts | +| Release-workflow contracts | PASS — 7 contracts | +| Upgrade/archive/rollback contracts | PASS | +| Forensic inventory | PASS — 414 maintained entries plus inventory | +| Structural forensic audit | PASS — 415 files, 149 Python, 15 shell, zero blockers | +| Deterministic 1.3.2 build/verification | PASS before report synchronization; final SHA regenerated after documentation/inventory finalization | + +The artifact environment does not contain supported CPython 3.12/Django/Ruff/Bandit dependencies and cannot reach the package index. Therefore the complete Django test/coverage, Ruff, Bandit, pip-audit, Django check/migration-drift, and full dependency-backed forensic gates are deliberately not claimed locally; they are mandatory in the user's isolated Python 3.12 environment and GitHub CI before merge/release. + +## PHASE-005A live acceptance boundary + +No existing-VPS mutation is performed by this patch. After 1.3.3 branch/PR/main/tag/release qualification, the existing server should be upgraded once using the PHASE-004 controlled upgrade mechanism, followed by browser acceptance of compact inbox/message rendering, direct-live-route redirect behavior, live polling, and preserved real inbound mail flow. + +## 1.3.2 failed qualification evidence + +The Windows Python 3.12.8 run collected 203 tests and completed with 202 passed, one symlink-capability +skip, and 94.97% coverage. Django system check and migration drift checks passed; pip check, Bandit, +pip-audit, contact-service, structural documentation/design/operations/release/upgrade contracts also +passed. Ruff reported three `I001` import-order findings. The handoff transcript additionally shows an +accidental empty `85%` file was staged and committed. GitHub Actions run `32128090322` failed at the +source-safety inventory gate on that extra file, so the `1.3.2` branch SHA was never fully qualified. +The later interrupted deterministic build is not counted as a PASS. + +The 1.3.3 corrective delta must run the complete qualification once after application. No previous +`1.3.2` PASS is reused as final 1.3.3 qualification evidence. diff --git a/documents/ADMIN_GUIDE.md b/documents/ADMIN_GUIDE.md index 4be16bb..40e95fa 100644 --- a/documents/ADMIN_GUIDE.md +++ b/documents/ADMIN_GUIDE.md @@ -4,8 +4,8 @@ title: MailStack Administrator Guide document_type: admin-guide audience: mailstack-administrators status: active -version: 1.3.1 -last_reviewed: 2026-08-17 +version: 1.3.3 +last_reviewed: 2026-08-18 --- # MailStack administrator guide diff --git a/documents/BASELINE.md b/documents/BASELINE.md index 132aaf2..35c1450 100644 --- a/documents/BASELINE.md +++ b/documents/BASELINE.md @@ -4,67 +4,61 @@ title: MailStack Baseline document_type: baseline audience: maintainers-and-operators status: active -version: 1.3.1 -last_reviewed: 2026-08-17 -baseline_id: MAILSTACK-1.3.0-RC4-OFFICIAL-SOURCE-BASELINE-001 -source_commit: 896dbcc2ed1f38d9c618bf0b712efe5923f92e56 +version: 1.3.3 +last_reviewed: 2026-08-18 +baseline_id: MAILSTACK-1.3.1-OFFICIAL-SOURCE-BASELINE-001 +source_commit: 039a6e6eea6e198b4b15612db9d2f208b6305a16 --- # MailStack baseline ## Baseline identity -The current official source baseline is `MAILSTACK-1.3.0-RC4-OFFICIAL-SOURCE-BASELINE-001`. It is -anchored to published tag `v1.3.0-rc.4`, commit -`896dbcc2ed1f38d9c618bf0b712efe5923f92e56`, and Git tree -`0d845b3d975949894c24581e6834aff7b33c30b4`. The deterministic source archive is -`mailstack-1.3.0-rc.4-source.zip` with SHA-256 -`58f06adea7c813e9861799d20e392441367bf64f6513d6e0634455d2011d4eac`. +The current immutable official source/release baseline is +`MAILSTACK-1.3.1-OFFICIAL-SOURCE-BASELINE-001`, anchored to published tag `v1.3.1`, commit +`039a6e6eea6e198b4b15612db9d2f208b6305a16`, Git tree +`9437ba2ebac2033a229accd190268b8711d5b26e`, and deterministic source archive +`mailstack-1.3.1-source.zip` with SHA-256 +`517778967ca491974f315d231dfd43b3dba85fe86b47dcfc63e4c7051d1010bd`. -The earlier `MAILSTACK-1.3.0-RC1-DOCS-BASELINE-001`, anchored to commit -`1e1737edea2e6c922265a15d8584b56671820c65`, remains historical documentation provenance. It no -longer represents the current canonical source anchor. For the current verification/fix campaign, -the project owner designated `MailStack_v1.3.0-rc.5_Baseline.zip` as the working input freeze. The -resulting source is version-marked `1.3.1`; its baseline ZIP/checksum are frozen outside this document -after deterministic build verification. This does not rewrite the published RC4 tag/archive or claim -a published stable release. +The earlier published `v1.3.0-rc.4` baseline remains immutable historical release provenance. It is +not rewritten or retagged by the 1.3.1 freeze. PHASE-005A starts from the exact 1.3.1 source tree and +records the failed first development target `1.3.2` and the corrected development target `1.3.3`; that working version does not replace this official +baseline until its branch, PR, main, tag, release, and owner acceptance gates complete. ## Qualification status -The RC4 baseline passed source safety, documentation and design gates, installer and operations -contracts, dependency vulnerability audit, Ruff, Bandit, Django checks, migration-drift checks, 198 -application tests at 95.00 percent coverage, full forensic audit, deterministic release build, and -release verification. The post-merge `main` CI run `32071701530`, tag CI run `32072699991`, and -release-artifact workflow `32072699830` all completed successfully. +The 1.3.1 release line passed branch, pull-request, and post-merge `main` CI, including 198 Django +tests at 95.00 percent coverage, Ruff, Bandit, dependency vulnerability audit, Django and migration +drift checks, installer/operations/release/upgrade contracts, full forensic audit with zero blocking +findings, deterministic source build, and release verification. Tag `v1.3.1` points at the exact +qualified `main` merge commit and the automated GitHub Release workflow successfully published the +verified deterministic source ZIP and checksum. -PHASE-003 staging also demonstrated real external Gmail delivery through Postfix and Dovecot LMTP -into Maildir, ingestion, and browser visibility after the accepted fixes. An exact RC4 clean-host -reinstall is deferred until a fresh VPS is available. Backup/restore acceptance, restart/reboot -recovery, final ownership/license review, and stable promotion remain outstanding operational or -human gates. +The first real existing-VPS use of the PHASE-004 upgrade mechanism remains deferred. PHASE-005A is +being qualified before that live update so the server is upgraded once to the UI-corrected release +instead of receiving two consecutive application changes. ## Preserved architecture -The baseline preserves the receive-only Postfix and Dovecot LMTP flow, Maildir storage, Django -shared inbox, MariaDB data contracts, Nginx and Gunicorn deployment, systemd services, public site, -contact service, backup/restore tooling, and established legacy runtime identifiers. It does not -add SMTP submission, IMAP, POP3, public registration, outbound campaigns, or multi-node operation. +The baseline preserves the receive-only Postfix and Dovecot LMTP flow, Maildir storage, Django shared +inbox, MariaDB data contracts, Gunicorn/Nginx deployment, systemd services, public site, contact +service, backup/restore tooling, object-scoped mailbox authorization, safe HTML sanitizer, protected +attachments, live polling, audit logging, and established runtime identifiers. It does not add SMTP +submission, outbound sending, IMAP, POP3, public registration, campaigns, or multi-node operation. -PHASE-004A changes documentation/version metadata only, PHASE-004B adds release automation, and -PHASE-004C adds controlled upgrade/rollback tooling. The 1.3.1 correction is limited to the five Ruff -findings in the PHASE-004C archive verifier plus version/release/baseline metadata synchronization. -It does not alter application business logic, database schema/migrations, routes, permissions, UI, -mail flow, installer behavior, deployment templates, DNS/TLS, or the live VPS. +PHASE-005A is UI/presentation scoped. Its only server-side behavior exception is a narrow guard that +accepts the explicit MailStack background-request header or the legacy JSON `Accept` signature before returning the live-update JSON +payload, preserving cached-client compatibility while rejecting ordinary document navigation. Models, migrations, parser/sanitizer policy, permissions, mail flow, deployment templates, +and operational upgrade/rollback semantics remain frozen. ## Change control -The design-governance layer remains anchored to commit -`068097056cecdd18f39fd304d579563b7b43c491` through `MAILSTACK-UI-DESIGN-INTAKE-001` and -`MAILSTACK-UI-FOUNDATION-001`; it does not replace the current source anchor above. +Every maintained phase must add or update a phase record under `documents/phases/`, update affected +user-facing guidance when behavior changes, update `CHANGELOG.md`, synchronize documentation/design/ +forensic manifests, and pass blocking CI. The `v1.3.1` tag, commit, tree, deterministic archive, and +checksum are immutable inputs to PHASE-005A and must never be force-moved or overwritten. -Every future maintained phase must add or update a phase record under `documents/phases/`, update -the affected user-facing guide when behavior changes, update `CHANGELOG.md`, synchronize the -documentation/design/forensic manifests required by repository policy, and pass the blocking CI -gates. Existing features, data contracts, authorization boundaries, receive-only scope, and -deployment compatibility remain protected unless a separately approved migration explicitly -changes them. +Existing data contracts, authorization boundaries, receive-only scope, sanitizer/sandbox security, +and deployment compatibility remain protected unless a separately approved phase explicitly changes +them. diff --git a/documents/DOCUMENTATION_MANIFEST.json b/documents/DOCUMENTATION_MANIFEST.json index a82cf87..58a3df5 100644 --- a/documents/DOCUMENTATION_MANIFEST.json +++ b/documents/DOCUMENTATION_MANIFEST.json @@ -1,221 +1,233 @@ { - "baseline_id": "MAILSTACK-1.3.0-RC4-OFFICIAL-SOURCE-BASELINE-001", - "baseline_source_commit": "896dbcc2ed1f38d9c618bf0b712efe5923f92e56", + "baseline_id": "MAILSTACK-1.3.1-OFFICIAL-SOURCE-BASELINE-001", + "baseline_source_commit": "039a6e6eea6e198b4b15612db9d2f208b6305a16", "documents": [ { "audience": "mailstack-administrators", "document_id": "admin-guide", "document_type": "admin-guide", - "last_reviewed": "2026-08-17", + "last_reviewed": "2026-08-18", "path": "documents/ADMIN_GUIDE.md", "phase_id": null, - "sha256": "018a50bca6cc47ef51adda6104dc77a306aee87abeee7d9410f7f3f13e3182f3", + "sha256": "d7ecd6fc82533efa84be7137806e187e1987792950724523d6d45998c4ea9607", "status": "active", "title": "MailStack Administrator Guide", - "version": "1.3.1" + "version": "1.3.3" }, { "audience": "maintainers-and-operators", "document_id": "baseline", "document_type": "baseline", - "last_reviewed": "2026-08-17", + "last_reviewed": "2026-08-18", "path": "documents/BASELINE.md", "phase_id": null, - "sha256": "5343fed8ab585b7f19341b7727e8dadb0279d3f3ee06dfb06992dd687e3fbeb8", + "sha256": "4e292bf865a6afebe2bf3fe28f1743ac26ecab907cbeabd7e5df8e9c43ee617d", "status": "active", "title": "MailStack Baseline", - "version": "1.3.1" + "version": "1.3.3" }, { "audience": "contributors-and-maintainers", "document_id": "documentation-policy", "document_type": "documentation-policy", - "last_reviewed": "2026-08-17", + "last_reviewed": "2026-08-18", "path": "documents/DOCUMENTATION_POLICY.md", "phase_id": null, - "sha256": "c8910661b15e941c6e2db493973f6e3bdaeba64ad8554eb227d1245c8c20c9a5", + "sha256": "1815fb7353fa22f9cd28de8cf18a8f0899b13616795fa20f98f3b686edcc9fae", "status": "active", "title": "Documentation Policy", - "version": "1.3.1" + "version": "1.3.3" }, { "audience": "mailbox-users-and-administrators", "document_id": "how-to-use", "document_type": "how-to", - "last_reviewed": "2026-08-17", + "last_reviewed": "2026-08-18", "path": "documents/HOW_TO_USE.md", "phase_id": null, - "sha256": "364f048cd26bd0f564f7a47c26b4624942981bbdf748d9ba7c28ceba705b65a5", + "sha256": "2a5757aa6f345d3df7c6d51d4472bf89e1fe3184a7a86db0be4e4d3fed7c08e6", "status": "active", "title": "How to Use MailStack", - "version": "1.3.1" + "version": "1.3.3" }, { "audience": "mailbox-users", "document_id": "user-manual", "document_type": "user-manual", - "last_reviewed": "2026-08-17", + "last_reviewed": "2026-08-18", "path": "documents/USER_MANUAL.md", "phase_id": null, - "sha256": "778de1cec3ef89171fd2768064940c21c5f0d8f444e5376e4b43123cb3a58f13", + "sha256": "951d3643e7272352fcd55253f6dbe538b1eedb4140f10706bfafc0aa6dc08b6d", "status": "active", "title": "MailStack User Manual", - "version": "1.3.1" + "version": "1.3.3" }, { "audience": "designers-developers-and-maintainers", "document_id": "ui-accessibility-specification", "document_type": "design-reference", - "last_reviewed": "2026-08-17", + "last_reviewed": "2026-08-18", "path": "documents/design/ACCESSIBILITY_SPECIFICATION.md", "phase_id": null, - "sha256": "c9224cc17caca417a5eb0d363b7b472e250890edef028b01edc7d58cec753aa3", + "sha256": "278c6a4b427b09330d4be2488440ae6955ccba40579911e87c7b0d0e493a94e8", "status": "active", "title": "MailStack Accessibility Specification", - "version": "1.3.1" + "version": "1.3.3" }, { "audience": "designers-developers-and-maintainers", "document_id": "ui-component-matrix", "document_type": "design-reference", - "last_reviewed": "2026-08-17", + "last_reviewed": "2026-08-18", "path": "documents/design/COMPONENT_MATRIX.md", "phase_id": null, - "sha256": "957a04577dc899fd9f457b59e029a1b4d50b6ab5c6089f6a415339cd20cbe5e5", + "sha256": "6eec7042e88f8c49385167e7da0fe438157751e07dfa3eeb21932073f29d1f4e", "status": "active", "title": "MailStack UI Component Matrix", - "version": "1.3.1" + "version": "1.3.3" }, { "audience": "designers-developers-and-maintainers", "document_id": "future-ui-roadmap", "document_type": "design-reference", - "last_reviewed": "2026-08-17", + "last_reviewed": "2026-08-18", "path": "documents/design/FUTURE_UI_ROADMAP.md", "phase_id": null, - "sha256": "078989742c6aab70840e48ccf887c472f538079e48a73811eb48b0506d9a413a", + "sha256": "1a00207cd0bd1b32ce66e5253b4dc77838215586aa37caf259d8456302d4cd13", "status": "active", "title": "MailStack Future UI Roadmap", - "version": "1.3.1" + "version": "1.3.3" }, { "audience": "designers-developers-and-maintainers", "document_id": "ui-implementation-status", "document_type": "design-reference", - "last_reviewed": "2026-08-17", + "last_reviewed": "2026-08-18", "path": "documents/design/IMPLEMENTATION_STATUS.md", "phase_id": null, - "sha256": "ad807f84993961013133f0c8e136c4fc5d636fd6051e352e6fb7d01b43e1be9f", + "sha256": "71c75905bd097977838855090269865aba4fe7f10d2c8ee2a1236641b92fd758", "status": "active", "title": "MailStack UI Implementation Status", - "version": "1.3.1" + "version": "1.3.3" }, { "audience": "designers-developers-and-maintainers", "document_id": "ui-responsive-specification", "document_type": "design-reference", - "last_reviewed": "2026-08-17", + "last_reviewed": "2026-08-18", "path": "documents/design/RESPONSIVE_SPECIFICATION.md", "phase_id": null, - "sha256": "b540f41863ec20c34871d1b0abf39517a40a2ec1a183128d8ce321e9d7a8867a", + "sha256": "0fae978b691e5c460e919668ec3cc1d870f1ac510f7b1613b3f61b286cd80984", "status": "active", "title": "MailStack Responsive Specification", - "version": "1.3.1" + "version": "1.3.3" }, { "audience": "designers-developers-and-maintainers", "document_id": "ui-screen-catalog", "document_type": "design-reference", - "last_reviewed": "2026-08-17", + "last_reviewed": "2026-08-18", "path": "documents/design/SCREEN_CATALOG.md", "phase_id": null, - "sha256": "0f45bcdddea7a40c6bcaa753bcc5b8d7c49a2d66f383003c87244ae865c007a6", + "sha256": "ecc0b62d2f30a09f704a7a1e70b1e72a0f85746cd5d5c13ad9c431e6967b697c", "status": "active", "title": "MailStack UI Screen Catalog", - "version": "1.3.1" + "version": "1.3.3" }, { "audience": "designers-developers-and-maintainers", "document_id": "ui-foundation", "document_type": "design-reference", - "last_reviewed": "2026-08-17", + "last_reviewed": "2026-08-18", "path": "documents/design/UI_FOUNDATION.md", "phase_id": null, - "sha256": "38b97a9c193ad8a7a5b21d031535afa72b3e5fc3b9f6772abe37c1379dc0c459", + "sha256": "5082d1f28afe1414d59a3c97f83a08a45d9c3242c2a057dae6d30b09b4c57785", "status": "active", "title": "MailStack UI Foundation", - "version": "1.3.1" + "version": "1.3.3" }, { "audience": "users-operators-and-maintainers", "document_id": "phase-000-baseline", "document_type": "phase", - "last_reviewed": "2026-08-17", + "last_reviewed": "2026-08-18", "path": "documents/phases/PHASE-000-BASELINE.md", "phase_id": "PHASE-000", - "sha256": "9c04cd5594cd07cc487f47fe7e6fd751775ba202232b84db3385e959f1ceba57", + "sha256": "0ca507b4dfe36daf118b81ebb9de19472c2c161d0e45256805513c8895a83574", "status": "active", "title": "Documentation and Feature Baseline", - "version": "1.3.1" + "version": "1.3.3" }, { "audience": "users-operators-designers-and-maintainers", "document_id": "phase-001-ui-design-intake-baseline", "document_type": "phase", - "last_reviewed": "2026-08-17", + "last_reviewed": "2026-08-18", "path": "documents/phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md", "phase_id": "PHASE-001", - "sha256": "b30ca3041469b56b1c3256cab99d10b12f3d00241043cb449d466ecbde352154", + "sha256": "af6bb679cac06f760e00c5a40cfafee4ed30306a22782dc5b82e1b5039df36a6", "status": "active", "title": "UI Design Intake Baseline", - "version": "1.3.1" + "version": "1.3.3" }, { "audience": "users-operators-designers-and-maintainers", "document_id": "phase-002-shared-ui-foundation-and-application-shell", "document_type": "phase", - "last_reviewed": "2026-08-17", + "last_reviewed": "2026-08-18", "path": "documents/phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md", "phase_id": "PHASE-002", - "sha256": "a1e2f224910ad0eea4d43f87f0734f3ecbd394e804497b7e6cc3c8a5ec2567dc", + "sha256": "e3c8edf18c64865786c6be6d11f9424b37a54d98f095a05948bdf0773cfc8de0", "status": "active", "title": "Shared UI Foundation and Application Shell", - "version": "1.3.1" + "version": "1.3.3" }, { "audience": "users-operators-and-maintainers", "document_id": "phase-003-installation-recovery-and-inbound-delivery-reliability", "document_type": "phase", - "last_reviewed": "2026-08-17", + "last_reviewed": "2026-08-18", "path": "documents/phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md", "phase_id": "PHASE-003", - "sha256": "2b63990b712eeab3c7e28f948385a3485667a4397a4c9f0b4a731e72165393df", + "sha256": "bb6a6a8eb7d41ca882b9e9f83c3371fe78325e43472c3065e130e5923da78e96", "status": "active", "title": "Installation, Recovery and Inbound Delivery Reliability", - "version": "1.3.1" + "version": "1.3.3" }, { "audience": "users-operators-and-maintainers", "document_id": "phase-004-release-upgrade-and-operational-reliability", "document_type": "phase", - "last_reviewed": "2026-08-17", + "last_reviewed": "2026-08-18", "path": "documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md", "phase_id": "PHASE-004", - "sha256": "964c57e99da6bc7b63e5ee577129cf7aa45770e9bdc4c70e630bfac502dba828", + "sha256": "f45403765474a74f72607b1c37ca5cb0df6d0a546a4e436500ef427dcaf64f16", "status": "active", "title": "Release, Upgrade and Operational Reliability", - "version": "1.3.1" + "version": "1.3.3" + }, + { + "audience": "users-operators-designers-and-maintainers", + "document_id": "phase-005-ui-navigation-reliability-and-compact-mailbox-reader", + "document_type": "phase", + "last_reviewed": "2026-08-18", + "path": "documents/phases/PHASE-005-UI-NAVIGATION-RELIABILITY-AND-COMPACT-MAILBOX-READER.md", + "phase_id": "PHASE-005", + "sha256": "198f20d9732395701ac5ebb57d2e3d3a74d437e7b1a657630f697cf46a60b545", + "status": "active", + "title": "UI Navigation Reliability and Compact Mailbox Reader", + "version": "1.3.3" } ], "project": "MailStack", - "release_version": "1.3.1", + "release_version": "1.3.3", "schema_version": 1, "scope": "Managed user, administrator, how-to, policy, baseline, and phase Markdown documents.", "summary": { - "active": 17, - "documents": 17, + "active": 18, + "documents": 18, "draft": 0, "historical": 0, - "phases": 5 + "phases": 6 } } diff --git a/documents/DOCUMENTATION_POLICY.md b/documents/DOCUMENTATION_POLICY.md index 563aa16..739a4d9 100644 --- a/documents/DOCUMENTATION_POLICY.md +++ b/documents/DOCUMENTATION_POLICY.md @@ -4,8 +4,8 @@ title: Documentation Policy document_type: documentation-policy audience: contributors-and-maintainers status: active -version: 1.3.1 -last_reviewed: 2026-08-17 +version: 1.3.3 +last_reviewed: 2026-08-18 --- # MailStack documentation policy diff --git a/documents/HOW_TO_USE.md b/documents/HOW_TO_USE.md index 2e12f27..50f2f20 100644 --- a/documents/HOW_TO_USE.md +++ b/documents/HOW_TO_USE.md @@ -4,8 +4,8 @@ title: How to Use MailStack document_type: how-to audience: mailbox-users-and-administrators status: active -version: 1.3.1 -last_reviewed: 2026-08-17 +version: 1.3.3 +last_reviewed: 2026-08-18 --- # How to use MailStack @@ -40,13 +40,20 @@ mailbox from the mailbox list. 1. Open **Mailboxes** and select **Open inbox**. 2. Search by sender address or subject when needed. -3. Use the read-state filter for **Unread** or **Read** messages. +3. Use the compact read-state filter for **Unread** or **Read** messages. 4. Use the attachment filter for messages with or without attachments. -5. Open a message to view its metadata, plain text, safe HTML, and attachments. -6. Select **Mark unread** when the message should return to the unread queue. - -Opening a message marks it read. Live updates are available on an unfiltered first inbox page and -on the dashboard. +5. Select a compact message row. The row shows sender, subject, short preview, attachment/size + metadata, and received time. +6. Read the message in the unified reader. Sanitized HTML is selected automatically when available; + otherwise the indexed plain-text body is shown. +7. Expand the recipient summary when full To/CC/mailbox/time/size/status metadata is needed. +8. Select **Mark unread** when the message should return to the unread queue. + +Opening a message marks it read. The visible plain-text/HTML format tabs have been removed; the +existing sanitizer, sandbox, no-referrer policy, and remote/active-content blocking remain in force. +Live updates are available on an unfiltered first inbox page and on the dashboard. Direct browser +navigation to the internal live-update endpoint is returned to the application UI instead of +displaying its JSON transport payload. ## Download an attachment diff --git a/documents/README.md b/documents/README.md index 542b751..c84ea4b 100644 --- a/documents/README.md +++ b/documents/README.md @@ -8,7 +8,7 @@ relevant user manual, how-to guide, or administrator guide. ```bash python scripts/manage_documents.py new-phase \ - --phase-id PHASE-005 \ + --phase-id PHASE-006 \ --title "Feature title" \ --summary "What the phase changes for users" @@ -27,23 +27,24 @@ required documentation update. | Document | Type | Audience | Status | Version | |---|---|---|---|---| -| [MailStack Administrator Guide](ADMIN_GUIDE.md) | Admin Guide | mailstack-administrators | active | 1.3.1 | -| [MailStack Baseline](BASELINE.md) | Baseline | maintainers-and-operators | active | 1.3.1 | -| [MailStack Accessibility Specification](design/ACCESSIBILITY_SPECIFICATION.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.1 | -| [MailStack Future UI Roadmap](design/FUTURE_UI_ROADMAP.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.1 | -| [MailStack Responsive Specification](design/RESPONSIVE_SPECIFICATION.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.1 | -| [MailStack UI Component Matrix](design/COMPONENT_MATRIX.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.1 | -| [MailStack UI Foundation](design/UI_FOUNDATION.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.1 | -| [MailStack UI Implementation Status](design/IMPLEMENTATION_STATUS.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.1 | -| [MailStack UI Screen Catalog](design/SCREEN_CATALOG.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.1 | -| [Documentation Policy](DOCUMENTATION_POLICY.md) | Documentation Policy | contributors-and-maintainers | active | 1.3.1 | -| [How to Use MailStack](HOW_TO_USE.md) | How To | mailbox-users-and-administrators | active | 1.3.1 | -| [Documentation and Feature Baseline](phases/PHASE-000-BASELINE.md) | Phase | users-operators-and-maintainers | active | 1.3.1 | -| [Installation, Recovery and Inbound Delivery Reliability](phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md) | Phase | users-operators-and-maintainers | active | 1.3.1 | -| [Release, Upgrade and Operational Reliability](phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md) | Phase | users-operators-and-maintainers | active | 1.3.1 | -| [Shared UI Foundation and Application Shell](phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md) | Phase | users-operators-designers-and-maintainers | active | 1.3.1 | -| [UI Design Intake Baseline](phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md) | Phase | users-operators-designers-and-maintainers | active | 1.3.1 | -| [MailStack User Manual](USER_MANUAL.md) | User Manual | mailbox-users | active | 1.3.1 | +| [MailStack Administrator Guide](ADMIN_GUIDE.md) | Admin Guide | mailstack-administrators | active | 1.3.3 | +| [MailStack Baseline](BASELINE.md) | Baseline | maintainers-and-operators | active | 1.3.3 | +| [MailStack Accessibility Specification](design/ACCESSIBILITY_SPECIFICATION.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.3 | +| [MailStack Future UI Roadmap](design/FUTURE_UI_ROADMAP.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.3 | +| [MailStack Responsive Specification](design/RESPONSIVE_SPECIFICATION.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.3 | +| [MailStack UI Component Matrix](design/COMPONENT_MATRIX.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.3 | +| [MailStack UI Foundation](design/UI_FOUNDATION.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.3 | +| [MailStack UI Implementation Status](design/IMPLEMENTATION_STATUS.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.3 | +| [MailStack UI Screen Catalog](design/SCREEN_CATALOG.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.3 | +| [Documentation Policy](DOCUMENTATION_POLICY.md) | Documentation Policy | contributors-and-maintainers | active | 1.3.3 | +| [How to Use MailStack](HOW_TO_USE.md) | How To | mailbox-users-and-administrators | active | 1.3.3 | +| [Documentation and Feature Baseline](phases/PHASE-000-BASELINE.md) | Phase | users-operators-and-maintainers | active | 1.3.3 | +| [Installation, Recovery and Inbound Delivery Reliability](phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md) | Phase | users-operators-and-maintainers | active | 1.3.3 | +| [Release, Upgrade and Operational Reliability](phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md) | Phase | users-operators-and-maintainers | active | 1.3.3 | +| [Shared UI Foundation and Application Shell](phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md) | Phase | users-operators-designers-and-maintainers | active | 1.3.3 | +| [UI Design Intake Baseline](phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md) | Phase | users-operators-designers-and-maintainers | active | 1.3.3 | +| [UI Navigation Reliability and Compact Mailbox Reader](phases/PHASE-005-UI-NAVIGATION-RELIABILITY-AND-COMPACT-MAILBOX-READER.md) | Phase | users-operators-designers-and-maintainers | active | 1.3.3 | +| [MailStack User Manual](USER_MANUAL.md) | User Manual | mailbox-users | active | 1.3.3 | ## Scope boundary diff --git a/documents/USER_MANUAL.md b/documents/USER_MANUAL.md index 6e904fc..0688a46 100644 --- a/documents/USER_MANUAL.md +++ b/documents/USER_MANUAL.md @@ -4,8 +4,8 @@ title: MailStack User Manual document_type: user-manual audience: mailbox-users status: active -version: 1.3.1 -last_reviewed: 2026-08-17 +version: 1.3.3 +last_reviewed: 2026-08-18 --- # MailStack user manual @@ -52,15 +52,22 @@ reserved and cannot be recreated accidentally. ## Messages -Inside a mailbox, search by sender or subject and filter by read state or attachment presence. -Opening a message marks it read. Use **Mark unread** to return it to the unread state. The message -screen shows sender, recipients, received time, size, parse status, plain text, sanitized HTML, and -available attachments. - -Sanitized HTML is displayed in a sandboxed frame with remote and active content blocked. A delete -button appears only when the current account has permission. Message deletion removes the indexed -application record from normal use while preserving the source email according to the current -retention design. +Inside a mailbox, search by sender or subject and filter by read state or attachment presence from +the compact toolbar. Inbox rows emphasize unread mail and show sender, subject, a short body preview, +attachment/size metadata, and received time in one dense webmail-style list. Opening a message marks +it read; use **Mark unread** in the message header to return it to the unread queue. + +The message screen uses one unified reader. If sanitized HTML is available it is selected +automatically inside the existing sandboxed, no-referrer protected frame. If no sanitized HTML body +is available, MailStack shows the indexed plain-text body as the readable fallback. There are no +separate plain-text/HTML tabs. Remote and active HTML content remain blocked by the existing +sanitizer and content-security policy. + +Sender identity is shown first, while full To/CC/mailbox/time/size/parse-status metadata is available +from the expandable recipient summary. A delete action appears only when the current account has +permission. Message deletion removes the indexed application record from normal use while preserving +the source email according to the current retention design. Attachments remain explicit downloads and +are labeled as not antivirus scanned. ## Notifications diff --git a/documents/design/ACCESSIBILITY_SPECIFICATION.md b/documents/design/ACCESSIBILITY_SPECIFICATION.md index 7c70998..e4c3d4a 100644 --- a/documents/design/ACCESSIBILITY_SPECIFICATION.md +++ b/documents/design/ACCESSIBILITY_SPECIFICATION.md @@ -4,8 +4,8 @@ title: MailStack Accessibility Specification document_type: design-reference audience: designers-developers-and-maintainers status: active -version: 1.3.1 -last_reviewed: 2026-08-17 +version: 1.3.3 +last_reviewed: 2026-08-18 --- # MailStack accessibility specification diff --git a/documents/design/COMPONENT_MATRIX.md b/documents/design/COMPONENT_MATRIX.md index 6fdfe6e..f6e35e9 100644 --- a/documents/design/COMPONENT_MATRIX.md +++ b/documents/design/COMPONENT_MATRIX.md @@ -4,8 +4,8 @@ title: MailStack UI Component Matrix document_type: design-reference audience: designers-developers-and-maintainers status: active -version: 1.3.1 -last_reviewed: 2026-08-17 +version: 1.3.3 +last_reviewed: 2026-08-18 --- # MailStack UI component matrix diff --git a/documents/design/FUTURE_UI_ROADMAP.md b/documents/design/FUTURE_UI_ROADMAP.md index 98eb567..6b55639 100644 --- a/documents/design/FUTURE_UI_ROADMAP.md +++ b/documents/design/FUTURE_UI_ROADMAP.md @@ -4,8 +4,8 @@ title: MailStack Future UI Roadmap document_type: design-reference audience: designers-developers-and-maintainers status: active -version: 1.3.1 -last_reviewed: 2026-08-17 +version: 1.3.3 +last_reviewed: 2026-08-18 --- # MailStack future UI roadmap diff --git a/documents/design/IMPLEMENTATION_STATUS.md b/documents/design/IMPLEMENTATION_STATUS.md index e685c78..d5c3aeb 100644 --- a/documents/design/IMPLEMENTATION_STATUS.md +++ b/documents/design/IMPLEMENTATION_STATUS.md @@ -4,8 +4,8 @@ title: MailStack UI Implementation Status document_type: design-reference audience: designers-developers-and-maintainers status: active -version: 1.3.1 -last_reviewed: 2026-08-17 +version: 1.3.3 +last_reviewed: 2026-08-18 --- # MailStack UI implementation status @@ -34,10 +34,10 @@ phase records, CI evidence, or staging acceptance. | Local logo and icon assets | Implemented locally | Canonical logo copy and validated SVG sprite | | Responsive shell | Implemented locally | Desktop collapse, tablet/mobile drawer, compact mobile shell | | Released RC4 dependency-backed qualification | Verified in GitHub CI | 198 passed, 0 failed, 95.00% coverage; main and tag CI passed | -| Existing page redesign | Not started | One page per subsequent patch | -| Secure message-reader redesign | Not started | Dedicated security-focused phase required | +| Inbox/message page redesign | Implemented locally in PHASE-005A | Compact inbox and unified message reader; remote CI and live acceptance pending | +| Secure message-reader presentation | Implemented locally in PHASE-005A | Existing sanitizer, CSP, sandbox, and no-referrer isolation preserved | | Future feature implementation | Not started | Separate architecture phases required | -| Integrated regression audit | Not started | Runs after all approved page patches | +| PHASE-005A integrated regression audit | In progress | Structural/local gates run before GitHub dependency-backed qualification | | Staging/live UI acceptance | Not started | Runs after integrated CI qualification | ## Change control diff --git a/documents/design/RESPONSIVE_SPECIFICATION.md b/documents/design/RESPONSIVE_SPECIFICATION.md index b1f9b87..85ad5b9 100644 --- a/documents/design/RESPONSIVE_SPECIFICATION.md +++ b/documents/design/RESPONSIVE_SPECIFICATION.md @@ -4,8 +4,8 @@ title: MailStack Responsive Specification document_type: design-reference audience: designers-developers-and-maintainers status: active -version: 1.3.1 -last_reviewed: 2026-08-17 +version: 1.3.3 +last_reviewed: 2026-08-18 --- # MailStack responsive specification diff --git a/documents/design/SCREEN_CATALOG.md b/documents/design/SCREEN_CATALOG.md index 90af447..5f52170 100644 --- a/documents/design/SCREEN_CATALOG.md +++ b/documents/design/SCREEN_CATALOG.md @@ -4,8 +4,8 @@ title: MailStack UI Screen Catalog document_type: design-reference audience: designers-developers-and-maintainers status: active -version: 1.3.1 -last_reviewed: 2026-08-17 +version: 1.3.3 +last_reviewed: 2026-08-18 --- # MailStack UI screen catalog @@ -55,6 +55,9 @@ The machine-readable authority is `design/DESIGN_MANIFEST.json`. Original misspe capitalization remain only in immutable source filenames; display names and stable IDs are corrected in the catalog. +PHASE-005A applies the current `UI-011` Inbox reference to a compact receive-only inbox and unified +message reader. This does not activate any planned/future feature or outbound-mail control. + ## Change control New or replaced designs must retain source provenance, receive a unique screen ID, pass PNG and diff --git a/documents/design/UI_FOUNDATION.md b/documents/design/UI_FOUNDATION.md index 2e3df92..9df98b8 100644 --- a/documents/design/UI_FOUNDATION.md +++ b/documents/design/UI_FOUNDATION.md @@ -4,8 +4,8 @@ title: MailStack UI Foundation document_type: design-reference audience: designers-developers-and-maintainers status: active -version: 1.3.1 -last_reviewed: 2026-08-17 +version: 1.3.3 +last_reviewed: 2026-08-18 --- # MailStack UI foundation diff --git a/documents/phases/PHASE-000-BASELINE.md b/documents/phases/PHASE-000-BASELINE.md index 5795960..315a935 100644 --- a/documents/phases/PHASE-000-BASELINE.md +++ b/documents/phases/PHASE-000-BASELINE.md @@ -4,8 +4,8 @@ title: Documentation and Feature Baseline document_type: phase audience: users-operators-and-maintainers status: active -version: 1.3.1 -last_reviewed: 2026-08-17 +version: 1.3.3 +last_reviewed: 2026-08-18 phase_id: PHASE-000 --- diff --git a/documents/phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md b/documents/phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md index 67ab415..c6be0e5 100644 --- a/documents/phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md +++ b/documents/phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md @@ -4,8 +4,8 @@ title: UI Design Intake Baseline document_type: phase audience: users-operators-designers-and-maintainers status: active -version: 1.3.1 -last_reviewed: 2026-08-17 +version: 1.3.3 +last_reviewed: 2026-08-18 phase_id: PHASE-001 --- diff --git a/documents/phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md b/documents/phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md index 6d967ee..7195c04 100644 --- a/documents/phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md +++ b/documents/phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md @@ -4,8 +4,8 @@ title: Shared UI Foundation and Application Shell document_type: phase audience: users-operators-designers-and-maintainers status: active -version: 1.3.1 -last_reviewed: 2026-08-17 +version: 1.3.3 +last_reviewed: 2026-08-18 phase_id: PHASE-002 --- diff --git a/documents/phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md b/documents/phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md index b10ba27..2485f15 100644 --- a/documents/phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md +++ b/documents/phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md @@ -4,8 +4,8 @@ title: Installation, Recovery and Inbound Delivery Reliability document_type: phase audience: users-operators-and-maintainers status: active -version: 1.3.1 -last_reviewed: 2026-08-17 +version: 1.3.3 +last_reviewed: 2026-08-18 phase_id: PHASE-003 --- diff --git a/documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md b/documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md index a04212c..46b49f7 100644 --- a/documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md +++ b/documents/phases/PHASE-004-RELEASE-UPGRADE-AND-OPERATIONAL-RELIABILITY.md @@ -4,8 +4,8 @@ title: Release, Upgrade and Operational Reliability document_type: phase audience: users-operators-and-maintainers status: active -version: 1.3.1 -last_reviewed: 2026-08-17 +version: 1.3.3 +last_reviewed: 2026-08-18 phase_id: PHASE-004 --- @@ -129,6 +129,17 @@ build/release documentation is synchronized to that identity. This is a source-b a claim that the stable-looking version has passed the still-outstanding live PHASE-004D/operational acceptance or has been published as a GitHub Release. +## Final 1.3.1 release completion + +PHASE-004 completed its release publication path on 2026-08-18. Pull request #10 merged the qualified +release line to `main` as commit `039a6e6eea6e198b4b15612db9d2f208b6305a16`; post-merge main CI +passed, tag `v1.3.1` was created at that exact commit, and the automated release workflow published +the deterministic source ZIP/checksum. The resulting immutable official baseline is +`MAILSTACK-1.3.1-OFFICIAL-SOURCE-BASELINE-001` with tree +`9437ba2ebac2033a229accd190268b8711d5b26e` and source SHA-256 +`517778967ca491974f315d231dfd43b3dba85fe86b47dcfc63e4c7051d1010bd`. Existing-VPS execution was +intentionally deferred so PHASE-005A can be qualified first. + ## Documentation impact PHASE-004A updated the root/application changelogs, build/release examples, README status wording, diff --git a/documents/phases/PHASE-005-UI-NAVIGATION-RELIABILITY-AND-COMPACT-MAILBOX-READER.md b/documents/phases/PHASE-005-UI-NAVIGATION-RELIABILITY-AND-COMPACT-MAILBOX-READER.md new file mode 100644 index 0000000..68a1a21 --- /dev/null +++ b/documents/phases/PHASE-005-UI-NAVIGATION-RELIABILITY-AND-COMPACT-MAILBOX-READER.md @@ -0,0 +1,102 @@ +--- +document_id: phase-005-ui-navigation-reliability-and-compact-mailbox-reader +title: UI Navigation Reliability and Compact Mailbox Reader +document_type: phase +audience: users-operators-designers-and-maintainers +status: active +version: 1.3.3 +last_reviewed: 2026-08-18 +phase_id: PHASE-005 +--- + +# PHASE-005A: UI navigation reliability and compact mailbox reader + +## Objective + +Remove the user-visible raw-JSON navigation failure mode and replace the split plain-text/HTML +message-detail presentation with a compact receive-only mailbox and unified safe message reader. +The immutable implementation input is published MailStack `v1.3.1`, commit +`039a6e6eea6e198b4b15612db9d2f208b6305a16`, tree +`9437ba2ebac2033a229accd190268b8711d5b26e`, deterministic source SHA-256 +`517778967ca491974f315d231dfd43b3dba85fe86b47dcfc63e4c7051d1010bd`. + +## Scope + +PHASE-005A is limited to mailbox/message UI presentation, the browser-side live polling request, and +the narrow server-side presentation guard needed to prevent the live JSON endpoint from being used +as a top-level document. Inbox rows gain compact sender/subject/preview metadata and live-inserted +rows use the same presentation contract. Message detail automatically chooses the already-sanitized +HTML body when present and otherwise renders the existing plain-text body as a unified fallback. + +The message preview is derived only from already-indexed `text_body` or sanitized HTML text. No new +message field, migration, parser behavior, persistence contract, or mail flow is introduced. + +## User-facing changes + +Authenticated direct navigation to the live-update endpoint no longer displays raw JSON; normal +browser navigation is redirected to the dashboard, while MailStack's explicit background request +continues to receive JSON. Mailbox inboxes use a denser webmail-style layout with integrated filters, +unread emphasis, previews, attachment indication, and responsive compact rows. + +The message reader no longer exposes separate `Plain text` and `Safe HTML` tabs. Sanitized HTML is +shown automatically inside the existing sandboxed/no-referrer isolation, and plain-only messages use +a single readable fallback. Sender, routing details, actions, warnings, and attachments are arranged +as one compact reader rather than several disconnected panels. + +## How to use + +Open a mailbox from **Mailboxes**, search or filter in the compact toolbar, and select a message row. +The message opens in one reader automatically; users do not select a body format. Use **Mark unread** +or the permission-gated **Delete** action from the reader header. Expand the recipient summary when +full routing, timestamp, size, or parse-status metadata is needed. Attachments remain explicit +downloads and remain marked as not antivirus scanned. + +## Compatibility + +PHASE-005A preserves receive-only operation, authentication, object authorization, mailbox +membership, message read/unread semantics, deletion permissions, attachment authorization, parser +and sanitizer rules, CSP and iframe sandboxing, database models/migrations, Postfix/Dovecot LMTP, +Maildir, MariaDB, public-site/contact behavior, deployment templates, installer, backup/restore, and +PHASE-004 upgrade/rollback semantics. No compose, reply, forward, sent, draft, IMAP, POP3, or public +registration capability is added. + +The version-specific CI/release artifact paths are synchronized from `1.3.1` to the corrected `1.3.3` +target so the existing release workflow verifies and publishes the correct deterministic artifact +and release-note file. This is version metadata synchronization, not a release-workflow behavior +redesign. + +## Verification + +Blocking verification includes direct-navigation and background-live-request regression tests, +compact inbox rendering, live/server row parity, HTML-to-text preview fallback, unified HTML reader, +plain-only fallback, retained sandbox/no-referrer security controls, search/filter/pagination, +responsive CSS, existing live authorization and query-bound tests, the complete Django test suite, +Ruff, Bandit, dependency audit, Django/migration checks, documentation/design/inventory gates, full +forensic audit, and deterministic source build/release verification. Local artifact qualification is +not represented as live-VPS acceptance; the first existing-server update remains deferred until the +1.3.3 release line is remotely qualified. + +## PHASE-005A qualification correction + +The first `1.3.2` branch qualification attempt is retained as failed development evidence rather than +as a release baseline. Local testing found three Ruff `I001` import-order findings, and the Windows +CMD handoff accidentally created an empty repository-root file named `85%` when explanatory text +containing `>=85%` was pasted into `cmd.exe`. GitHub Actions run `32128090322` then failed closed in +the source-safety gate because that extra file made the forensic inventory stale; later CI gates did +not execute on that SHA. + +The `1.3.3` correction removes only that accidental file, normalizes the affected imports, regenerates +the inventory/document metadata, and preserves the PHASE-005A runtime design. It also accepts the +legacy `Accept: application/json` polling signature in addition to the new explicit live-request +header so browsers holding the previously immutable-cached `app.js` do not temporarily lose live +updates after upgrade. Ordinary browser document requests still redirect to the dashboard and do not +receive raw JSON. No sanitizer, schema, authorization, mail-flow, deployment-template, or upgrade +semantics are changed. + +## Documentation impact + +This phase updates the root and application changelogs, README release status/build examples, user +manual/how-to message-reading guidance, baseline record, UI implementation status, API/security and +feature documentation, 1.3.2 historical qualification notes, 1.3.3 correction release notes, build/publishing/release-process examples, generated +documentation/design/forensic inventories, and this phase record. Historical `v1.3.1` release notes +and immutable release artifacts are retained unchanged. diff --git a/mailbox-app/CHANGELOG.md b/mailbox-app/CHANGELOG.md index c8c73f4..c731d8a 100644 --- a/mailbox-app/CHANGELOG.md +++ b/mailbox-app/CHANGELOG.md @@ -1,6 +1,19 @@ # Changelog -## 1.3.1 — Unpublished source baseline +## 1.3.3 — PHASE-005A qualification correction (development) + +- Preserved legacy cached live-poller compatibility by accepting `Accept: application/json` as a background live request alongside the new explicit request header; ordinary document navigation remains redirected away from raw JSON. +- Corrected PHASE-005A import-order lint findings; no mailbox business/data/security semantics changed. +- Retained the compact inbox, unified safe message reader, sanitizer/sandbox controls, and receive-only architecture unchanged. + +## 1.3.2 — PHASE-005A compact mailbox UI (development) + +- Prevented authenticated top-level navigation to the live-update transport from rendering raw JSON; MailStack JavaScript now sends an explicit background-request header and ordinary document requests return to the dashboard UI. +- Rebuilt the inbox as a compact webmail-style list with short previews, unread emphasis, integrated filters, responsive rows, and live-row visual parity. +- Replaced separate plain-text and safe-HTML tabs with one unified message reader that automatically uses the existing sanitized HTML sandbox when available and a plain-text fallback otherwise. +- Preserved sanitizer/CSP/sandbox controls, models/migrations, authorization, mark-unread/delete semantics, attachment handling, ingestion, and receive-only mail flow. + +## 1.3.1 — 2026-08-18 - PHASE-004C controlled existing-server upgrade tooling now verifies deterministic release ZIP/SHA assets, creates a coordinated data backup and source rollback snapshot before mutation, stages application/public-site source, converges Python dependencies, gates migrations explicitly, preserves mail-facing services during the source mutation window, and performs post-upgrade verification. - Added a reviewed source/runtime rollback command that verifies snapshot checksums and refuses implicit database/Maildir restoration or unacknowledged forward-schema rollback. diff --git a/mailbox-app/apps/messages/views.py b/mailbox-app/apps/messages/views.py index bb263d1..0a7abc6 100644 --- a/mailbox-app/apps/messages/views.py +++ b/mailbox-app/apps/messages/views.py @@ -11,6 +11,7 @@ from django.http import FileResponse, Http404, HttpResponse, JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse +from django.utils.html import strip_tags from django.utils.http import content_disposition_header from django.views.decorators.cache import never_cache from django.views.decorators.http import require_GET, require_http_methods, require_POST @@ -26,6 +27,16 @@ from .models import Attachment from .services import set_message_read_state, soft_delete_message +LIVE_UPDATE_HEADER = "X-MailStack-Live-Request" + + +def _message_preview(message, limit: int = 180) -> str: + source = message.text_body or strip_tags(message.sanitized_html_body or "") + normalized = " ".join(source.split()) + if len(normalized) <= limit: + return normalized + return normalized[: limit - 1].rstrip() + "…" + @login_required def inbox(request, mailbox_uuid): @@ -49,6 +60,8 @@ def inbox(request, mailbox_uuid): elif attachment_filter == "no": queryset = queryset.filter(has_attachments=False) page_obj = Paginator(queryset, 30).get_page(request.GET.get("page")) + for item in page_obj.object_list: + item.ui_preview = _message_preview(item) record_audit( "inbox_access", request=request, target_type="mailbox", target_identifier=mailbox.email_address ) @@ -104,11 +117,20 @@ def safe_html(request, message_uuid): "message_html_view", request=request, target_type="message", target_identifier=str(message.uuid) ) body = message.sanitized_html_body or "

No HTML body is available.

" - notice = ( - "
Remote content and active content are blocked.
" + document = ( + "" + "" + "" + + body + + "" ) - response = HttpResponse(notice + body, content_type="text/html; charset=utf-8") + response = HttpResponse(document, content_type="text/html; charset=utf-8") response["Cache-Control"] = "private, no-store" return response @@ -176,10 +198,18 @@ def _iso(value): return value.isoformat() if value else None +def _is_live_background_request(request) -> bool: + accepts_json = "application/json" in request.headers.get("Accept", "").lower() + return request.headers.get(LIVE_UPDATE_HEADER) == "1" or accepts_json + + @login_required @require_GET @never_cache def live_updates(request): + if not _is_live_background_request(request): + return redirect("dashboard:index") + bootstrap = request.GET.get("bootstrap") == "1" try: cursor = max(0, int(request.GET.get("cursor", "0"))) @@ -202,6 +232,7 @@ def live_updates(request): "sender_name": item.sender_name, "sender_address": item.sender_address, "subject": item.subject or "(No subject)", + "preview": _message_preview(item), "received_at": _iso(item.received_at), "size_bytes": item.size_bytes, "has_attachments": item.has_attachments, diff --git a/mailbox-app/pyproject.toml b/mailbox-app/pyproject.toml index d5660df..45054ee 100644 --- a/mailbox-app/pyproject.toml +++ b/mailbox-app/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "mailstack" -version = "1.3.1" +version = "1.3.3" description = "MailStack self-hosted receive-only mail server and shared team inbox" authors = [{ name = "Vib Tools" }] maintainers = [{ name = "Vib Tools" }] @@ -64,6 +64,10 @@ exclude = ["migrations", "staticfiles", ".audit-venv", ".venv", "venv"] select = ["E", "F", "I", "B", "S", "UP", "SIM", "C4"] ignore = ["S101", "S105", "S106"] +[tool.ruff.lint.isort] +known-first-party = ["apps", "config"] +known-third-party = ["django", "filelock", "pytest"] + [tool.ruff.lint.per-file-ignores] "tests/**/*.py" = ["S", "B017"] "config/settings/*.py" = ["F403", "F405"] diff --git a/mailbox-app/static/css/app.css b/mailbox-app/static/css/app.css index 0d39dd4..94a0910 100644 --- a/mailbox-app/static/css/app.css +++ b/mailbox-app/static/css/app.css @@ -1,7 +1,7 @@ -:root{--bg:#f4f7fb;--surface:#fff;--surface-soft:#f8fafc;--text:#172033;--muted:#667085;--line:#dce3ee;--brand:#3157d5;--brand-dark:#2343ad;--danger:#b42318;--success:#087443;--warning:#a15c00;--radius:14px;--shadow:0 8px 28px rgba(20,37,63,.08);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:var(--text);background:var(--bg)}*{box-sizing:border-box}html{min-width:320px;scroll-behavior:smooth}body{margin:0;min-height:100vh;background:var(--bg);color:var(--text);line-height:1.5}a{color:var(--brand);text-decoration:none}a:hover{text-decoration:underline}button,input,select{font:inherit}button,a,input,select{outline-offset:3px}:focus-visible{outline:3px solid rgba(49,87,213,.34)}.container{width:min(1180px,calc(100% - 32px));margin-inline:auto}.skip-link{position:fixed;top:-80px;left:16px;z-index:1000;background:#fff;padding:10px 14px;border-radius:8px;box-shadow:var(--shadow)}.skip-link:focus{top:16px}.topbar{position:sticky;top:0;z-index:20;background:rgba(255,255,255,.96);border-bottom:1px solid var(--line);backdrop-filter:blur(10px)}.nav-wrap{display:flex;align-items:center;justify-content:space-between;min-height:64px;gap:24px}.brand{font-weight:800;color:var(--text);letter-spacing:-.02em}.brand:hover{text-decoration:none}.main-nav{display:flex;align-items:center;gap:20px}.main-nav a,.link-button{color:#3b475d;font-weight:600}.link-button{background:none;border:0;padding:0;cursor:pointer}.nav-toggle{display:none;border:1px solid var(--line);background:#fff;border-radius:9px;padding:8px 12px}.page-shell{padding-block:32px 48px;min-height:calc(100vh - 122px)}.footer{padding:20px 0;color:var(--muted);border-top:1px solid var(--line);font-size:.875rem}.page-header{display:flex;justify-content:space-between;align-items:flex-start;gap:24px;margin-bottom:24px}.page-header h1{margin:.2rem 0 .25rem;font-size:clamp(1.75rem,3vw,2.45rem);line-height:1.15;letter-spacing:-.03em}.eyebrow{text-transform:uppercase;letter-spacing:.11em;font-size:.72rem;font-weight:800;color:var(--brand)}.muted{color:var(--muted)}.page-header p{margin:0}.stat-grid{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:14px;margin-bottom:18px}.stat-card{padding:18px;background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);box-shadow:0 2px 10px rgba(20,37,63,.04);min-width:0}.stat-card span{display:block;color:var(--muted);font-size:.82rem;margin-bottom:8px}.stat-card strong{display:block;font-size:1.65rem;line-height:1.2;overflow-wrap:anywhere}.stat-card .small-value{font-size:1rem}.status-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-bottom:22px}.status-item{display:flex;align-items:center;gap:12px;padding:14px 16px;background:var(--surface-soft);border:1px solid var(--line);border-radius:12px}.status-item small{display:block;color:var(--muted)}.status-dot{width:11px;height:11px;border-radius:50%;background:#9aa4b2;flex:none}.status-dot.ok{background:#12a66a}.status-dot.warn{background:#f2a900}.status-dot.bad{background:#d92d20}.two-column{display:grid;grid-template-columns:1fr 1fr;gap:18px}.panel{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:20px;box-shadow:var(--shadow);margin-bottom:20px;min-width:0}.panel.flush{padding:0;overflow:hidden}.panel.narrow{max-width:680px}.panel-header{display:flex;align-items:center;justify-content:space-between;gap:18px;margin-bottom:14px}.panel-header h2{margin:0;font-size:1.05rem}.list-stack{display:grid}.list-row{display:flex;justify-content:space-between;gap:18px;padding:14px 0;border-top:1px solid var(--line);color:var(--text);min-width:0}.list-row:first-child{border-top:0}.list-row:hover{text-decoration:none;background:var(--surface-soft)}.list-row span{min-width:0}.list-row small{display:block;color:var(--muted);margin-top:2px}.list-row time{font-size:.83rem;color:var(--muted);white-space:nowrap}.empty-state{text-align:center;padding:34px 18px;color:var(--muted)}.filter-bar{display:flex;align-items:end;gap:10px;margin-bottom:18px}.filter-bar .grow{flex:1}.filter-bar input,.filter-bar select,.form-control,.form-group input,select{width:100%;border:1px solid #bfc9d8;border-radius:9px;padding:10px 12px;background:#fff;color:var(--text)}.filter-bar select{width:auto;min-width:150px}.btn{display:inline-flex;align-items:center;justify-content:center;gap:7px;border:1px solid #bfc9d8;border-radius:9px;background:#fff;color:#26334a;padding:9px 14px;cursor:pointer;font-weight:700;line-height:1.2;white-space:nowrap}.btn:hover{text-decoration:none;background:#f6f8fb}.btn-primary{background:var(--brand);border-color:var(--brand);color:#fff}.btn-primary:hover{background:var(--brand-dark)}.btn-danger{color:var(--danger);border-color:#efb6b1}.btn-success{color:var(--success);border-color:#9ed3bc}.btn-small{padding:7px 10px;font-size:.82rem}.btn-block{width:100%}.inline-form{display:inline}.table-wrap{overflow-x:auto}table{width:100%;border-collapse:collapse;min-width:900px}th,td{text-align:left;padding:13px 15px;border-bottom:1px solid var(--line);vertical-align:middle}th{font-size:.76rem;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);background:#f8fafc}tbody tr:last-child td{border-bottom:0}.actions{display:flex;gap:7px;align-items:center}.badge{display:inline-block;border-radius:999px;padding:3px 9px;font-size:.75rem;font-weight:800;background:#e9edf5;color:#344054}.badge-active,.badge-ok{background:#dff7eb;color:#05603a}.badge-disabled,.badge-error,.badge-oversized{background:#fee4e2;color:#912018}.badge-warning{background:#fff0c2;color:#774d00}.form-group{margin-bottom:18px}.form-group label{display:block;margin-bottom:6px;font-weight:750}.help-text{font-size:.84rem;color:var(--muted);margin-top:6px}.help-text ul{padding-left:20px}.form-errors,.field-error{color:#8d1b13;background:#fff1f0;border:1px solid #ffc7c2;border-radius:8px;padding:9px 11px;margin:8px 0}.input-group{display:flex;align-items:stretch}.input-group input{border-radius:9px 0 0 9px}.input-group span{display:flex;align-items:center;padding:0 12px;border:1px solid #bfc9d8;border-left:0;border-radius:0 9px 9px 0;background:#eef2f7;color:#475467}.auth-card{width:min(460px,100%);margin:8vh auto;padding:28px;background:var(--surface);border:1px solid var(--line);border-radius:18px;box-shadow:var(--shadow)}.auth-card h1{margin:.3rem 0 .3rem}.alerts{margin-bottom:18px}.alert{padding:12px 14px;border:1px solid #bfd1ec;background:#edf5ff;border-radius:10px;margin-bottom:10px}.alert-success{background:#e5f8ee;border-color:#a5dfc2}.alert-error,.alert-danger{background:#fff0ef;border-color:#f6b7b1}.alert-warning{background:#fff7dc;border-color:#edd48b}.message-list{display:grid}.message-row{display:grid;grid-template-columns:12px minmax(160px,.65fr) minmax(250px,1.4fr) auto;align-items:center;gap:14px;padding:14px 16px;color:var(--text);border-bottom:1px solid var(--line);min-width:0}.message-row:last-child{border-bottom:0}.message-row:hover{text-decoration:none;background:#f8fafc}.message-row.unread{background:#f3f7ff}.message-row.unread strong{font-weight:800}.message-status{width:9px;height:9px;border-radius:50%;background:#ccd3de}.unread .message-status{background:var(--brand)}.message-sender,.message-subject{min-width:0}.message-sender small,.message-subject small{display:block;color:var(--muted)}.message-row time{font-size:.82rem;color:var(--muted);white-space:nowrap}.message-meta dl{display:grid;gap:10px;margin:0}.message-meta dl>div{display:grid;grid-template-columns:100px 1fr;gap:14px}.message-meta dt{color:var(--muted);font-weight:700}.message-meta dd{margin:0}.toolbar{display:flex;justify-content:flex-end;margin:-7px 0 14px}.tabs{display:flex;gap:8px;border-bottom:1px solid var(--line);margin:-4px -4px 16px;padding:0 4px}.tab-button{border:0;background:transparent;padding:10px 12px;cursor:pointer;font-weight:750;color:var(--muted);border-bottom:3px solid transparent}.tab-button.active{color:var(--brand);border-color:var(--brand)}.tab-panel{display:none}.tab-panel.active{display:block}.email-text{white-space:pre-wrap;overflow-wrap:anywhere;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:.9rem;line-height:1.6;margin:0}.email-frame{width:100%;min-height:420px;border:1px solid var(--line);border-radius:8px;background:#fff}.attachment-list{display:grid}.attachment-row{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:12px 0;border-top:1px solid var(--line)}.attachment-row:first-child{border-top:0}.attachment-row>div{min-width:0}.attachment-row small{display:block;color:var(--muted)}.pagination{display:flex;align-items:center;justify-content:center;gap:14px;margin:22px 0}.truncate{display:block;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wrap-anywhere,.address{overflow-wrap:anywhere;word-break:break-word}.sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important} -@media(max-width:1000px){.stat-grid{grid-template-columns:repeat(3,1fr)}.two-column{grid-template-columns:1fr}.message-row{grid-template-columns:12px minmax(130px,.65fr) minmax(220px,1.3fr) auto}} -@media(max-width:760px){.container{width:min(100% - 22px,1180px)}.nav-toggle{display:inline-flex}.main-nav{display:none;position:absolute;top:58px;left:11px;right:11px;flex-direction:column;align-items:stretch;gap:0;padding:9px;background:#fff;border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow)}.main-nav.open{display:flex}.main-nav a,.main-nav .inline-form,.main-nav .link-button{display:block;width:100%;text-align:left;padding:10px;border-radius:7px}.page-shell{padding-top:22px}.page-header{align-items:stretch;flex-direction:column}.page-header>.btn{align-self:flex-start}.stat-grid{grid-template-columns:repeat(2,1fr)}.status-grid{grid-template-columns:1fr}.filter-bar{align-items:stretch;flex-direction:column}.filter-bar select{width:100%}.filter-bar .btn{width:100%}.table-wrap{overflow:visible}table{min-width:0}thead{display:none}tbody,tr,td{display:block;width:100%}tr{padding:12px 14px;border-bottom:1px solid var(--line)}td{border:0;padding:5px 0;display:grid;grid-template-columns:112px minmax(0,1fr);gap:10px}td::before{content:attr(data-label);color:var(--muted);font-size:.75rem;text-transform:uppercase;font-weight:800}.actions{display:flex;flex-wrap:wrap}.actions::before{width:100%}.message-row{grid-template-columns:10px minmax(0,1fr) auto;gap:9px}.message-sender{grid-column:2}.message-subject{grid-column:2/4}.message-row time{grid-column:3;grid-row:1}.message-meta dl>div{grid-template-columns:1fr;gap:2px}.input-group{flex-wrap:wrap}.input-group input{border-radius:9px}.input-group span{width:100%;border-left:1px solid #bfc9d8;border-top:0;border-radius:0 0 9px 9px;margin-top:-8px;padding-top:13px}.auth-card{margin:3vh auto;padding:22px}.panel{padding:16px}} -@media(max-width:430px){.stat-grid{grid-template-columns:1fr}.message-row{padding:13px 11px}.page-header h1{font-size:1.7rem}.attachment-row{align-items:flex-start;flex-direction:column}.attachment-row .btn{width:100%}} +:root{--bg:#f4f7fb;--surface:#fff;--surface-soft:#f8fafc;--text:#172033;--muted:#667085;--line:#dce3ee;--brand:#3157d5;--brand-dark:#2343ad;--danger:#b42318;--success:#087443;--warning:#a15c00;--radius:14px;--shadow:0 8px 28px rgba(20,37,63,.08);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:var(--text);background:var(--bg)}*{box-sizing:border-box}html{min-width:320px;scroll-behavior:smooth}body{margin:0;min-height:100vh;background:var(--bg);color:var(--text);line-height:1.5}a{color:var(--brand);text-decoration:none}a:hover{text-decoration:underline}button,input,select{font:inherit}button,a,input,select{outline-offset:3px}:focus-visible{outline:3px solid rgba(49,87,213,.34)}.container{width:min(1180px,calc(100% - 32px));margin-inline:auto}.skip-link{position:fixed;top:-80px;left:16px;z-index:1000;background:#fff;padding:10px 14px;border-radius:8px;box-shadow:var(--shadow)}.skip-link:focus{top:16px}.topbar{position:sticky;top:0;z-index:20;background:rgba(255,255,255,.96);border-bottom:1px solid var(--line);backdrop-filter:blur(10px)}.nav-wrap{display:flex;align-items:center;justify-content:space-between;min-height:64px;gap:24px}.brand{font-weight:800;color:var(--text);letter-spacing:-.02em}.brand:hover{text-decoration:none}.main-nav{display:flex;align-items:center;gap:20px}.main-nav a,.link-button{color:#3b475d;font-weight:600}.link-button{background:none;border:0;padding:0;cursor:pointer}.nav-toggle{display:none;border:1px solid var(--line);background:#fff;border-radius:9px;padding:8px 12px}.page-shell{padding-block:32px 48px;min-height:calc(100vh - 122px)}.footer{padding:20px 0;color:var(--muted);border-top:1px solid var(--line);font-size:.875rem}.page-header{display:flex;justify-content:space-between;align-items:flex-start;gap:24px;margin-bottom:24px}.page-header h1{margin:.2rem 0 .25rem;font-size:clamp(1.75rem,3vw,2.45rem);line-height:1.15;letter-spacing:-.03em}.eyebrow{text-transform:uppercase;letter-spacing:.11em;font-size:.72rem;font-weight:800;color:var(--brand)}.muted{color:var(--muted)}.page-header p{margin:0}.stat-grid{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:14px;margin-bottom:18px}.stat-card{padding:18px;background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);box-shadow:0 2px 10px rgba(20,37,63,.04);min-width:0}.stat-card span{display:block;color:var(--muted);font-size:.82rem;margin-bottom:8px}.stat-card strong{display:block;font-size:1.65rem;line-height:1.2;overflow-wrap:anywhere}.stat-card .small-value{font-size:1rem}.status-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-bottom:22px}.status-item{display:flex;align-items:center;gap:12px;padding:14px 16px;background:var(--surface-soft);border:1px solid var(--line);border-radius:12px}.status-item small{display:block;color:var(--muted)}.status-dot{width:11px;height:11px;border-radius:50%;background:#9aa4b2;flex:none}.status-dot.ok{background:#12a66a}.status-dot.warn{background:#f2a900}.status-dot.bad{background:#d92d20}.two-column{display:grid;grid-template-columns:1fr 1fr;gap:18px}.panel{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:20px;box-shadow:var(--shadow);margin-bottom:20px;min-width:0}.panel.flush{padding:0;overflow:hidden}.panel.narrow{max-width:680px}.panel-header{display:flex;align-items:center;justify-content:space-between;gap:18px;margin-bottom:14px}.panel-header h2{margin:0;font-size:1.05rem}.list-stack{display:grid}.list-row{display:flex;justify-content:space-between;gap:18px;padding:14px 0;border-top:1px solid var(--line);color:var(--text);min-width:0}.list-row:first-child{border-top:0}.list-row:hover{text-decoration:none;background:var(--surface-soft)}.list-row span{min-width:0}.list-row small{display:block;color:var(--muted);margin-top:2px}.list-row time{font-size:.83rem;color:var(--muted);white-space:nowrap}.empty-state{text-align:center;padding:34px 18px;color:var(--muted)}.filter-bar{display:flex;align-items:end;gap:10px;margin-bottom:18px}.filter-bar .grow{flex:1}.filter-bar input,.filter-bar select,.form-control,.form-group input,select{width:100%;border:1px solid #bfc9d8;border-radius:9px;padding:10px 12px;background:#fff;color:var(--text)}.filter-bar select{width:auto;min-width:150px}.btn{display:inline-flex;align-items:center;justify-content:center;gap:7px;border:1px solid #bfc9d8;border-radius:9px;background:#fff;color:#26334a;padding:9px 14px;cursor:pointer;font-weight:700;line-height:1.2;white-space:nowrap}.btn:hover{text-decoration:none;background:#f6f8fb}.btn-primary{background:var(--brand);border-color:var(--brand);color:#fff}.btn-primary:hover{background:var(--brand-dark)}.btn-danger{color:var(--danger);border-color:#efb6b1}.btn-success{color:var(--success);border-color:#9ed3bc}.btn-small{padding:7px 10px;font-size:.82rem}.btn-block{width:100%}.inline-form{display:inline}.table-wrap{overflow-x:auto}table{width:100%;border-collapse:collapse;min-width:900px}th,td{text-align:left;padding:13px 15px;border-bottom:1px solid var(--line);vertical-align:middle}th{font-size:.76rem;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);background:#f8fafc}tbody tr:last-child td{border-bottom:0}.actions{display:flex;gap:7px;align-items:center}.badge{display:inline-block;border-radius:999px;padding:3px 9px;font-size:.75rem;font-weight:800;background:#e9edf5;color:#344054}.badge-active,.badge-ok{background:#dff7eb;color:#05603a}.badge-disabled,.badge-error,.badge-oversized{background:#fee4e2;color:#912018}.badge-warning{background:#fff0c2;color:#774d00}.form-group{margin-bottom:18px}.form-group label{display:block;margin-bottom:6px;font-weight:750}.help-text{font-size:.84rem;color:var(--muted);margin-top:6px}.help-text ul{padding-left:20px}.form-errors,.field-error{color:#8d1b13;background:#fff1f0;border:1px solid #ffc7c2;border-radius:8px;padding:9px 11px;margin:8px 0}.input-group{display:flex;align-items:stretch}.input-group input{border-radius:9px 0 0 9px}.input-group span{display:flex;align-items:center;padding:0 12px;border:1px solid #bfc9d8;border-left:0;border-radius:0 9px 9px 0;background:#eef2f7;color:#475467}.auth-card{width:min(460px,100%);margin:8vh auto;padding:28px;background:var(--surface);border:1px solid var(--line);border-radius:18px;box-shadow:var(--shadow)}.auth-card h1{margin:.3rem 0 .3rem}.alerts{margin-bottom:18px}.alert{padding:12px 14px;border:1px solid #bfd1ec;background:#edf5ff;border-radius:10px;margin-bottom:10px}.alert-success{background:#e5f8ee;border-color:#a5dfc2}.alert-error,.alert-danger{background:#fff0ef;border-color:#f6b7b1}.alert-warning{background:#fff7dc;border-color:#edd48b}.mailbox-view{background:var(--surface);border:1px solid var(--line);border-radius:16px;box-shadow:0 4px 18px rgba(20,37,63,.06);overflow:hidden}.mailbox-view-header{padding:16px 18px 12px}.mailbox-heading{min-width:0}.mailbox-back-link,.message-back-link{display:inline-flex;align-items:center;color:var(--muted);font-size:.82rem;font-weight:700;margin-bottom:6px}.mailbox-back-link:hover,.message-back-link:hover{color:var(--brand);text-decoration:none}.mailbox-title-line{display:flex;align-items:center;gap:10px;min-width:0}.mailbox-title-line h1{font-size:1.35rem;line-height:1.2;letter-spacing:-.02em;margin:0;min-width:0}.mailbox-counts{margin:4px 0 0;color:var(--muted);font-size:.83rem}.mailbox-filter-bar{display:flex;align-items:center;gap:8px;padding:10px 12px;border-top:1px solid var(--line);border-bottom:1px solid var(--line);background:#fbfcfe}.mailbox-filter-bar .grow{flex:1;min-width:220px}.mailbox-filter-bar input,.mailbox-filter-bar select{height:38px;border:1px solid #c6d0df;border-radius:8px;padding:7px 10px;background:#fff;color:var(--text)}.mailbox-filter-bar input{width:100%}.mailbox-filter-bar select{width:auto;min-width:132px}.mailbox-filter-button{height:38px;padding:7px 12px}.message-list{display:grid}.message-row{display:grid;grid-template-columns:8px minmax(150px,.55fr) minmax(0,1.55fr) auto;align-items:center;gap:12px;min-height:58px;padding:8px 16px;color:var(--text);border-bottom:1px solid #e7ebf1;min-width:0;background:#fff}.message-row:last-child{border-bottom:0}.message-row:hover{text-decoration:none;background:#f7f9fc}.message-row.unread{background:#f2f6ff}.message-row.unread:hover{background:#edf3ff}.message-row.unread .message-sender strong,.message-row.unread .message-subject-text{font-weight:800}.message-status{width:7px;height:7px;border-radius:50%;background:#c8d0dc}.unread .message-status{background:var(--brand)}.message-sender,.message-content{min-width:0}.message-sender strong{display:block;font-size:.88rem;font-weight:650}.message-sender small{display:block;color:var(--muted);font-size:.74rem;margin-top:1px}.message-subject-line{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.88rem}.message-subject-text{font-weight:650}.message-preview{color:#667085;font-weight:400}.message-secondary{display:block;color:#8a94a6;font-size:.72rem;margin-top:2px}.message-attachment{color:#596579;font-weight:700}.message-time{font-size:.75rem;color:var(--muted);white-space:nowrap;text-align:right}.mailbox-empty{min-height:120px;display:grid;place-items:center}.message-reader{background:var(--surface);border:1px solid var(--line);border-radius:16px;box-shadow:0 4px 18px rgba(20,37,63,.06);overflow:hidden}.message-reader-header{padding:16px 20px 14px;border-bottom:1px solid var(--line)}.message-reader-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px}.message-reader-toolbar .message-back-link{margin:0}.message-actions{display:flex;align-items:center;gap:7px}.message-actions form{margin:0}.message-reader-subject{margin:14px 0 14px;font-size:1.45rem;line-height:1.25;letter-spacing:-.025em}.message-sender-card{display:grid;grid-template-columns:38px minmax(0,1fr) auto;align-items:start;gap:10px}.message-avatar{width:36px;height:36px;border-radius:50%;display:grid;place-items:center;background:#eef4ff;color:#0b4ff5;font-weight:800}.message-sender-identity{min-width:0}.message-sender-primary{font-size:.86rem;line-height:1.35}.message-sender-primary strong{font-size:.92rem}.message-sender-primary span{color:var(--muted)}.message-routing-details{margin-top:2px;color:var(--muted);font-size:.78rem}.message-routing-details summary{cursor:pointer;display:inline-flex;align-items:center;gap:4px;list-style:none}.message-routing-details summary::-webkit-details-marker{display:none}.message-routing-details summary::after{content:"⌄";font-size:.8rem}.message-routing-details[open] summary::after{content:"⌃"}.message-routing-details dl{display:grid;gap:5px;margin:8px 0 0;padding:10px 12px;border:1px solid var(--line);border-radius:9px;background:#f8fafc;color:var(--text);max-width:760px}.message-routing-details dl>div{display:grid;grid-template-columns:72px minmax(0,1fr);gap:10px}.message-routing-details dt{color:var(--muted);font-weight:700}.message-routing-details dd{margin:0}.message-reader-time{font-size:.76rem;color:var(--muted);white-space:nowrap;padding-top:2px}.message-inline-warning{margin-top:12px;padding:8px 10px;border-radius:8px;background:#fff7dc;color:#774d00;font-size:.8rem}.message-body{background:#fff;min-height:260px}.message-security-note{padding:7px 20px;border-bottom:1px solid #edf0f4;background:#fbfcfe;color:#7b8495;font-size:.72rem}.email-frame{width:100%;border:0;background:#fff}.unified-email-frame{display:block;height:clamp(460px,65vh,800px)}.email-plain-body{padding:24px 26px 30px;white-space:pre-wrap;overflow-wrap:anywhere;font-family:Arial,Helvetica,sans-serif;font-size:.94rem;line-height:1.65;color:#172033}.message-empty-body{min-height:260px;display:grid;place-items:center}.message-attachments{border-top:1px solid var(--line);padding:14px 20px 16px;background:#fbfcfe}.message-attachments-header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:6px}.message-attachments-header h2{margin:0;font-size:.92rem}.message-attachments-header .muted{font-size:.72rem}.attachment-list{display:grid}.attachment-row{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:9px 0;border-top:1px solid #e7ebf1}.attachment-row:first-child{border-top:0}.attachment-row>div{min-width:0}.attachment-row strong{font-size:.84rem}.attachment-row small{display:block;color:var(--muted);font-size:.72rem}.message-no-attachments{color:var(--muted);font-size:.8rem;padding-top:4px}.pagination{display:flex;align-items:center;justify-content:center;gap:14px;margin:22px 0}.truncate{display:block;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wrap-anywhere,.address{overflow-wrap:anywhere;word-break:break-word}.sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important} +@media(max-width:1000px){.stat-grid{grid-template-columns:repeat(3,1fr)}.two-column{grid-template-columns:1fr}.message-row{grid-template-columns:8px minmax(130px,.55fr) minmax(0,1.35fr) auto}} +@media(max-width:760px){.container{width:min(100% - 22px,1180px)}.nav-toggle{display:inline-flex}.main-nav{display:none;position:absolute;top:58px;left:11px;right:11px;flex-direction:column;align-items:stretch;gap:0;padding:9px;background:#fff;border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow)}.main-nav.open{display:flex}.main-nav a,.main-nav .inline-form,.main-nav .link-button{display:block;width:100%;text-align:left;padding:10px;border-radius:7px}.page-shell{padding-top:22px}.page-header{align-items:stretch;flex-direction:column}.page-header>.btn{align-self:flex-start}.stat-grid{grid-template-columns:repeat(2,1fr)}.status-grid{grid-template-columns:1fr}.filter-bar{align-items:stretch;flex-direction:column}.filter-bar select{width:100%}.filter-bar .btn{width:100%}.table-wrap{overflow:visible}table{min-width:0}thead{display:none}tbody,tr,td{display:block;width:100%}tr{padding:12px 14px;border-bottom:1px solid var(--line)}td{border:0;padding:5px 0;display:grid;grid-template-columns:112px minmax(0,1fr);gap:10px}td::before{content:attr(data-label);color:var(--muted);font-size:.75rem;text-transform:uppercase;font-weight:800}.actions{display:flex;flex-wrap:wrap}.actions::before{width:100%}.mailbox-view-header{padding:14px}.mailbox-title-line{align-items:flex-start;flex-wrap:wrap}.mailbox-title-line h1{font-size:1.12rem;max-width:100%}.mailbox-filter-bar{align-items:stretch;flex-wrap:wrap}.mailbox-filter-bar .grow{flex-basis:100%;min-width:0}.mailbox-filter-bar select{flex:1;min-width:0}.message-row{grid-template-columns:7px minmax(0,1fr) auto;gap:7px;padding:9px 12px;min-height:64px}.message-sender{grid-column:2}.message-sender small{display:none}.message-content{grid-column:2/4}.message-subject-line{font-size:.84rem}.message-secondary{font-size:.68rem}.message-time{grid-column:3;grid-row:1;font-size:.7rem}.message-reader-header{padding:14px}.message-reader-toolbar{align-items:flex-start}.message-reader-subject{font-size:1.22rem;margin:12px 0}.message-sender-card{grid-template-columns:34px minmax(0,1fr)}.message-avatar{width:32px;height:32px}.message-reader-time{grid-column:2;white-space:normal;padding:0}.message-routing-details dl>div{grid-template-columns:1fr;gap:1px}.message-security-note{padding:7px 14px}.unified-email-frame{height:62vh;min-height:420px}.email-plain-body{padding:18px 16px 24px}.message-attachments{padding:12px 14px}.input-group{flex-wrap:wrap}.input-group input{border-radius:9px}.input-group span{width:100%;border-left:1px solid #bfc9d8;border-top:0;border-radius:0 0 9px 9px;margin-top:-8px;padding-top:13px}.auth-card{margin:3vh auto;padding:22px}.panel{padding:16px}} +@media(max-width:430px){.stat-grid{grid-template-columns:1fr}.message-row{padding:9px 10px}.page-header h1{font-size:1.7rem}.mailbox-filter-bar select{flex-basis:100%}.mailbox-filter-button{width:100%}.message-actions{flex-wrap:wrap;justify-content:flex-end}.attachment-row{align-items:flex-start;flex-direction:column}.attachment-row .btn{width:100%}} @media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*{animation-duration:.01ms!important;transition-duration:.01ms!important}} .copy-address,.copy-mini{border:0;background:transparent;color:var(--brand);padding:0;cursor:pointer;font:inherit;font-weight:700;text-align:left;overflow-wrap:anywhere}.copy-address:hover,.copy-mini:hover{text-decoration:underline}.heading-copy{font-size:inherit;line-height:inherit;color:var(--text);font-weight:inherit}.copy-mini{font-size:.78rem;margin-left:.35rem}.mailbox-summary-row{align-items:center}.toast-region{position:fixed;right:18px;bottom:18px;z-index:100;display:grid;gap:10px;width:min(380px,calc(100% - 36px));pointer-events:none}.toast{display:block;padding:13px 15px;border:1px solid #bfd1ec;border-radius:11px;background:#fff;color:var(--text);box-shadow:var(--shadow);pointer-events:auto}.toast:hover{text-decoration:none}.toast-success{border-color:#9ed3bc;background:#effaf4}.toast-error{border-color:#efb6b1;background:#fff3f2}.notification-enable{white-space:nowrap}.form-group input[type="checkbox"]{width:auto;margin-right:.45rem}.form-group select[multiple]{min-height:180px}.badge-deleted{background:#e9e9ed;color:#555}.actions{flex-wrap:wrap} @media(max-width:760px){.toast-region{right:11px;bottom:11px;width:calc(100% - 22px)}.copy-address{max-width:100%}} diff --git a/mailbox-app/static/js/app.js b/mailbox-app/static/js/app.js index feb17d1..50b7f5f 100644 --- a/mailbox-app/static/js/app.js +++ b/mailbox-app/static/js/app.js @@ -149,11 +149,12 @@ const VibMail = (() => { link.href = message.detail_url; link.dataset.messageUuid = message.uuid; - const status = document.createElement("div"); + const status = document.createElement("span"); status.className = "message-status"; status.setAttribute("aria-label", message.is_read ? "Read" : "Unread"); + status.title = message.is_read ? "Read" : "Unread"; - const sender = document.createElement("div"); + const sender = document.createElement("span"); sender.className = "message-sender"; const senderStrong = document.createElement("strong"); senderStrong.className = "truncate"; @@ -163,18 +164,36 @@ const VibMail = (() => { senderSmall.textContent = message.sender_address || ""; sender.append(senderStrong, senderSmall); - const subject = document.createElement("div"); - subject.className = "message-subject"; + const content = document.createElement("span"); + content.className = "message-content"; + const subjectLine = document.createElement("span"); + subjectLine.className = "message-subject-line"; const subjectStrong = document.createElement("strong"); - subjectStrong.className = "truncate"; + subjectStrong.className = "message-subject-text"; subjectStrong.textContent = message.subject || "(No subject)"; - const subjectSmall = document.createElement("small"); - subjectSmall.textContent = `${formatBytes(message.size_bytes)}${message.has_attachments ? " · Attachment" : ""}`; - subject.append(subjectStrong, subjectSmall); + subjectLine.append(subjectStrong); + if (message.preview) { + const preview = document.createElement("span"); + preview.className = "message-preview"; + preview.textContent = ` — ${message.preview}`; + subjectLine.append(preview); + } + const secondary = document.createElement("small"); + secondary.className = "message-secondary"; + if (message.has_attachments) { + const attachment = document.createElement("span"); + attachment.className = "message-attachment"; + attachment.textContent = "Attachment"; + secondary.append(attachment, document.createTextNode(" · ")); + } + secondary.append(document.createTextNode(formatBytes(message.size_bytes))); + content.append(subjectLine, secondary); const time = document.createElement("time"); - time.textContent = formatDate(message.received_at, "Unknown date"); - link.append(status, sender, subject, time); + time.className = "message-time"; + time.textContent = formatShortDate(message.received_at); + if (message.received_at) time.dateTime = message.received_at; + link.append(status, sender, content, time); return link; } @@ -257,7 +276,10 @@ const VibMail = (() => { } const response = await fetch(`${url}?${params.toString()}`, { credentials: "same-origin", - headers: { Accept: "application/json" }, + headers: { + Accept: "application/json", + "X-MailStack-Live-Request": "1", + }, cache: "no-store", signal: controller.signal, }); @@ -476,17 +498,6 @@ const VibMail = (() => { setupAppShell(); setupUserMenu(); - document.querySelectorAll("[data-tabs]").forEach((tabList) => { - const buttons = tabList.querySelectorAll("[data-tab-target]"); - buttons.forEach((button) => { - button.addEventListener("click", () => { - buttons.forEach((item) => item.classList.remove("active")); - document.querySelectorAll(".tab-panel").forEach((panel) => panel.classList.remove("active")); - button.classList.add("active"); - document.querySelector(`#tab-${button.dataset.tabTarget}`)?.classList.add("active"); - }); - }); - }); document.querySelectorAll(".status-form").forEach((form) => { form.addEventListener("submit", (event) => { diff --git a/mailbox-app/templates/messages/detail.html b/mailbox-app/templates/messages/detail.html index ecad8e0..e7aab8b 100644 --- a/mailbox-app/templates/messages/detail.html +++ b/mailbox-app/templates/messages/detail.html @@ -1,9 +1,77 @@ {% extends "base.html" %} {% block title %}{{ email_message.subject|default:"Message" }} · MailStack{% endblock %} {% block content %} - -
From
{{ email_message.sender_name }} {% if email_message.sender_address %}<{{ email_message.sender_address }}> {% endif %}
To
{{ email_message.recipient_addresses|join:", "|default:"Not available" }}
{% if email_message.cc_addresses %}
CC
{{ email_message.cc_addresses|join:", " }}
{% endif %}
Received
{{ email_message.received_at|date:"Y-m-d H:i:s T"|default:"Unknown" }}
Size
{{ email_message.size_bytes|filesizeformat }}
Status
· Read
{% if email_message.parse_warning %}
Parsing warning: {{ email_message.parse_warning }}
{% endif %}
-
{% csrf_token %}
{% if can_delete_message %}Delete message{% endif %}
-
-

Attachments

Not antivirus scanned
{% if email_message.attachments.all %}
{% for attachment in email_message.attachments.all %}
{{ attachment.safe_filename }}{{ attachment.detected_mime_type }} · {{ attachment.size_bytes|filesizeformat }}
Download
{% endfor %}
{% else %}
No attachments.
{% endif %}
+
+
+
+ ← Inbox +
+
+ {% csrf_token %} + + +
+ {% if can_delete_message %}Delete{% endif %} +
+
+ +

{{ email_message.subject|default:"(No subject)" }}

+ +
+ +
+
+ {{ email_message.sender_name|default:email_message.sender_address|default:"Unknown sender" }} + {% if email_message.sender_address and email_message.sender_name %}<{{ email_message.sender_address }}>{% endif %} + {% if email_message.sender_address %}{% endif %} +
+
+ to {{ email_message.recipient_addresses|join:", "|default:mailbox.email_address }} +
+
From
{{ email_message.sender_name }}{% if email_message.sender_address %} <{{ email_message.sender_address }}>{% endif %}
+
To
{{ email_message.recipient_addresses|join:", "|default:"Not available" }}
+ {% if email_message.cc_addresses %}
CC
{{ email_message.cc_addresses|join:", " }}
{% endif %} +
Mailbox
+
Received
{{ email_message.received_at|date:"Y-m-d H:i:s T"|default:"Unknown" }}
+
Size
{{ email_message.size_bytes|filesizeformat }}
+
Status
{{ email_message.get_parse_status_display }}
+
+
+
+ +
+ + {% if email_message.parse_warning %}
Parsing warning: {{ email_message.parse_warning }}
{% endif %} +
+ +
+ {% if email_message.sanitized_html_body %} +
Protected rendering · remote and active content are blocked.
+ + {% elif email_message.text_body %} + + {% else %} +
No readable message body is available.
+ {% endif %} +
+ +
+
+

Attachments

+ Not antivirus scanned +
+ {% if email_message.attachments.all %} +
+ {% for attachment in email_message.attachments.all %} +
+
{{ attachment.safe_filename }}{{ attachment.detected_mime_type }} · {{ attachment.size_bytes|filesizeformat }}
+ Download +
+ {% endfor %} +
+ {% else %} +
No attachments.
+ {% endif %} +
+
{% endblock %} diff --git a/mailbox-app/templates/messages/inbox.html b/mailbox-app/templates/messages/inbox.html index 49006ff..c48e0f7 100644 --- a/mailbox-app/templates/messages/inbox.html +++ b/mailbox-app/templates/messages/inbox.html @@ -1,8 +1,56 @@ {% extends "base.html" %} {% block title %}{{ mailbox.email_address }} · MailStack{% endblock %} {% block content %} - - -
+
+
+
+ ← All mailboxes +
+

+ {{ mailbox.get_status_display }} +
+

{{ mailbox.total_messages }} messages {{ mailbox.unread_messages }} unread

+
+
+ + + + +
{% include "pagination.html" with page_obj=page_obj %} {% endblock %} diff --git a/mailbox-app/tests/functional/test_compact_mailbox_reader.py b/mailbox-app/tests/functional/test_compact_mailbox_reader.py new file mode 100644 index 0000000..1fba9c9 --- /dev/null +++ b/mailbox-app/tests/functional/test_compact_mailbox_reader.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +import pytest +from django.urls import reverse + +LIVE_HEADERS = { + "HTTP_ACCEPT": "application/json", + "HTTP_X_MAILSTACK_LIVE_REQUEST": "1", +} + + +@pytest.mark.django_db +def test_live_updates_require_background_request_header(client, admin_user): + client.force_login(admin_user) + url = reverse("messages:live_updates") + + direct = client.get(url) + assert direct.status_code == 302 + assert direct.url == reverse("dashboard:index") + assert not direct.get("Content-Type", "").startswith("application/json") + + background = client.get(url, {"bootstrap": 1}, **LIVE_HEADERS) + assert background.status_code == 200 + assert background["Content-Type"].startswith("application/json") + + legacy = client.get(url, {"bootstrap": 1}, HTTP_ACCEPT="application/json") + assert legacy.status_code == 200 + assert legacy["Content-Type"].startswith("application/json") + + +@pytest.mark.django_db +def test_compact_inbox_renders_preview_without_legacy_body_tabs(client, admin_user, mailbox, message): + message.text_body = "Compact preview text for the mailbox row." + message.save(update_fields=["text_body"]) + client.force_login(admin_user) + + response = client.get(reverse("messages:inbox", args=[mailbox.uuid])) + assert response.status_code == 200 + body = response.content.decode() + assert 'class="mailbox-view"' in body + assert 'class="message-row unread"' in body + assert "Compact preview text for the mailbox row." in body + assert "Plain text" not in body + assert "Safe HTML" not in body + + +@pytest.mark.django_db +def test_unified_reader_prefers_sanitized_html_and_preserves_sandbox(client, admin_user, mailbox, message): + client.force_login(admin_user) + + response = client.get(reverse("messages:detail", args=[mailbox.uuid, message.uuid])) + assert response.status_code == 200 + body = response.content.decode() + assert 'class="message-reader"' in body + assert 'class="email-frame unified-email-frame"' in body + assert 'sandbox=""' in body + assert 'referrerpolicy="no-referrer"' in body + assert "Protected rendering" in body + assert "Plain body" not in body + assert "Plain text" not in body + assert "Safe HTML" not in body + + +@pytest.mark.django_db +def test_plain_only_message_uses_unified_plain_fallback(client, admin_user, mailbox, message): + message.sanitized_html_body = "" + message.text_body = "Plain-only fallback content." + message.save(update_fields=["sanitized_html_body", "text_body"]) + client.force_login(admin_user) + + response = client.get(reverse("messages:detail", args=[mailbox.uuid, message.uuid])) + assert response.status_code == 200 + body = response.content.decode() + assert 'class="email-plain-body"' in body + assert "Plain-only fallback content." in body + assert "unified-email-frame" not in body + + +@pytest.mark.django_db +def test_live_payload_uses_html_text_as_preview_when_plain_body_is_missing( + client, admin_user, message +): + message.text_body = "" + message.sanitized_html_body = "

Hello compact reader

" + message.save(update_fields=["text_body", "sanitized_html_body"]) + client.force_login(admin_user) + + response = client.get(reverse("messages:live_updates"), {"cursor": 0}, **LIVE_HEADERS) + assert response.status_code == 200 + payload = response.json() + row = next(item for item in payload["messages"] if item["uuid"] == str(message.uuid)) + assert row["preview"] == "Hello compact reader" diff --git a/mailbox-app/tests/functional/test_responsive_and_routes.py b/mailbox-app/tests/functional/test_responsive_and_routes.py index cf98274..c555ebb 100644 --- a/mailbox-app/tests/functional/test_responsive_and_routes.py +++ b/mailbox-app/tests/functional/test_responsive_and_routes.py @@ -15,7 +15,6 @@ def test_required_routes_resolve(client, admin_user, mailbox, message): reverse("mailboxes:create"), reverse("messages:inbox", args=[mailbox.uuid]), reverse("messages:detail", args=[mailbox.uuid, message.uuid]), - reverse("messages:live_updates"), reverse("accounts:user_list"), reverse("core:live"), reverse("core:ready"), @@ -23,6 +22,14 @@ def test_required_routes_resolve(client, admin_user, mailbox, message): for url in urls: assert client.get(url).status_code == 200 + live_response = client.get( + reverse("messages:live_updates"), + HTTP_ACCEPT="application/json", + HTTP_X_MAILSTACK_LIVE_REQUEST="1", + ) + assert live_response.status_code == 200 + assert live_response["Content-Type"].startswith("application/json") + def test_css_contains_responsive_breakpoints_and_accessibility_rules(): root = Path(__file__).resolve().parents[2] @@ -45,6 +52,9 @@ def test_templates_have_semantic_and_accessible_controls(): assert 'sandbox=""' in detail assert 'referrerpolicy="no-referrer"' in detail assert "Not antivirus scanned" in detail + assert "Plain text" not in detail + assert "Safe HTML" not in detail + assert 'class="message-reader"' in detail def test_url_configuration_has_no_public_admin_or_registration(): diff --git a/mailbox-app/tests/integration/test_auth_views.py b/mailbox-app/tests/integration/test_auth_views.py index 1bc23c7..9a549de 100644 --- a/mailbox-app/tests/integration/test_auth_views.py +++ b/mailbox-app/tests/integration/test_auth_views.py @@ -152,13 +152,16 @@ def test_inbox_search_filters_detail_state_and_safe_html(client, admin_user, mai response = client.get(reverse("messages:detail", args=[mailbox.uuid, message.uuid])) assert response.status_code == 200 - assert b"Plain body" in response.content + assert b'class="message-reader"' in response.content + assert b"Plain text" not in response.content + assert b"Safe HTML" not in response.content + assert b'sandbox=""' in response.content assert AuditLog.objects.filter(action="message_view").exists() html_response = client.get(reverse("messages:safe_html", args=[message.uuid])) assert html_response.status_code == 200 assert b"Safe body" in html_response.content - assert b"Remote content" in html_response.content + assert b"Remote content" not in html_response.content assert "default-src 'none'" in html_response["Content-Security-Policy"] assert html_response["X-Frame-Options"] == "SAMEORIGIN" diff --git a/mailbox-app/tests/integration/test_v1_2_access_live_delete.py b/mailbox-app/tests/integration/test_v1_2_access_live_delete.py index 966cb55..989bf08 100644 --- a/mailbox-app/tests/integration/test_v1_2_access_live_delete.py +++ b/mailbox-app/tests/integration/test_v1_2_access_live_delete.py @@ -20,6 +20,11 @@ from apps.messages.models import Attachment, Message from apps.messages.services import recalculate_mailbox_counters +LIVE_HEADERS = { + "HTTP_ACCEPT": "application/json", + "HTTP_X_MAILSTACK_LIVE_REQUEST": "1", +} + @pytest.fixture def ordinary_user(db): @@ -314,7 +319,7 @@ def test_attachment_access_is_scoped(client, ordinary_user, mailbox, message): def test_live_endpoint_bootstrap_then_returns_new_message(client, ordinary_user, assigned_mailbox): client.force_login(ordinary_user) url = reverse("messages:live_updates") - first = client.get(url, {"cursor": 0, "bootstrap": 1}) + first = client.get(url, {"cursor": 0, "bootstrap": 1}, **LIVE_HEADERS) assert first.status_code == 200 assert first.json()["messages"] == [] assert "private" in first["Cache-Control"] and "no-store" in first["Cache-Control"] @@ -329,7 +334,7 @@ def test_live_endpoint_bootstrap_then_returns_new_message(client, ordinary_user, parsed_at=timezone.now(), ) recalculate_mailbox_counters(assigned_mailbox) - second = client.get(url, {"cursor": first.json()["cursor"]}) + second = client.get(url, {"cursor": first.json()["cursor"]}, **LIVE_HEADERS) payload = second.json() assert [item["uuid"] for item in payload["messages"]] == [str(created.uuid)] assert payload["summary"]["total_unread"] == 1 @@ -353,7 +358,7 @@ def test_live_endpoint_never_leaks_unassigned_mailbox(client, ordinary_user, ass parsed_at=timezone.now(), ) client.force_login(ordinary_user) - response = client.get(reverse("messages:live_updates"), {"cursor": 0}) + response = client.get(reverse("messages:live_updates"), {"cursor": 0}, **LIVE_HEADERS) payload = response.json() assert [item["uuid"] for item in payload["messages"]] == [str(visible.uuid)] assert all(item["mailbox"] != private.email_address for item in payload["messages"]) @@ -375,6 +380,8 @@ def test_frontend_contains_live_polling_notifications_and_copy_controls(): root = Path(__file__).resolve().parents[2] javascript = (root / "static" / "js" / "app.js").read_text(encoding="utf-8") assert 'params.set("bootstrap", "1")' in javascript + assert '"X-MailStack-Live-Request": "1"' in javascript + assert "data-tabs" not in javascript assert "Notification.requestPermission" in javascript assert "BroadcastChannel" in javascript assert "navigator.clipboard.writeText" in javascript @@ -444,7 +451,7 @@ def test_live_endpoint_payload_is_bounded(client, admin_user, mailbox): parsed_at=timezone.now(), ) client.force_login(admin_user) - payload = client.get(reverse("messages:live_updates"), {"cursor": 0}).json() + payload = client.get(reverse("messages:live_updates"), {"cursor": 0}, **LIVE_HEADERS).json() assert len(payload["messages"]) == 2 assert payload["has_more"] is True assert len(payload["mailboxes"]) == 1 @@ -501,7 +508,7 @@ def test_live_endpoint_query_count_does_not_scale_with_mailboxes(client, ordinar ) client.force_login(ordinary_user) with CaptureQueriesContext(connection) as queries: - response = client.get(reverse("messages:live_updates"), {"cursor": 0, "bootstrap": 1}) + response = client.get(reverse("messages:live_updates"), {"cursor": 0, "bootstrap": 1}, **LIVE_HEADERS) assert response.status_code == 200 assert len(queries) <= 12 @@ -533,6 +540,7 @@ def test_live_endpoint_returns_requested_visible_authorized_mailboxes(client, or "bootstrap": 1, "mailboxes": f"{visible.uuid},{hidden.uuid},not-a-uuid", }, + **LIVE_HEADERS, ).json() assert [row["uuid"] for row in payload["mailboxes"]] == [str(visible.uuid)] diff --git a/scripts/forensic_audit.py b/scripts/forensic_audit.py index 97dec9f..cdc1378 100644 --- a/scripts/forensic_audit.py +++ b/scripts/forensic_audit.py @@ -54,6 +54,8 @@ "docs/PERFORMANCE_REVIEW.md", "docs/RELEASE_NOTES_1.3.0.md", "docs/RELEASE_NOTES_1.3.1.md", + "docs/RELEASE_NOTES_1.3.2.md", + "docs/RELEASE_NOTES_1.3.3.md", "documents/README.md", "documents/USER_MANUAL.md", "documents/HOW_TO_USE.md", diff --git a/scripts/test_release_workflow.py b/scripts/test_release_workflow.py index 91f72e3..b236380 100644 --- a/scripts/test_release_workflow.py +++ b/scripts/test_release_workflow.py @@ -188,7 +188,7 @@ def test_workflow_contract() -> None: "--latest", 'dist/*.zip', 'dist/*.sha256', - 'docs/RELEASE_NOTES_1.3.1.md', + 'docs/RELEASE_NOTES_1.3.3.md', ) for marker in required: assert marker in text, marker diff --git a/scripts/test_ui_foundation.py b/scripts/test_ui_foundation.py index fd2b5ed..1e78b16 100644 --- a/scripts/test_ui_foundation.py +++ b/scripts/test_ui_foundation.py @@ -134,13 +134,14 @@ def test_javascript_preserves_runtime_and_adds_accessible_shell() -> None: "shellTabindex", 'event.key === "Escape"', "document.body.dataset.liveUrl", + '"X-MailStack-Live-Request": "1"', "BroadcastChannel", "Notification.requestPermission", "document.addEventListener(\"DOMContentLoaded\", VibMail.init)", ) for marker in required: assert marker in javascript, f"missing JavaScript contract: {marker}" - prohibited = ("eval(", "new Function", ".innerHTML", "document.write(") + prohibited = ("eval(", "new Function", ".innerHTML", "document.write(", "data-tabs") for marker in prohibited: assert marker not in javascript, f"unsafe JavaScript construct found: {marker}" assert not re.search(r"https?://", javascript, flags=re.IGNORECASE)