diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml index c6f251e..fc2c028 100644 --- a/.github/ISSUE_TEMPLATE/question.yml +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -10,7 +10,7 @@ body: id: version attributes: label: MailStack version - placeholder: "1.3.0-rc.1" + placeholder: "1.3.0-rc.4" validations: required: true - type: dropdown diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2065212..721655a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -125,5 +125,5 @@ jobs: - name: Release verification run: | python scripts/verify_release.py \ - dist/mailstack-1.3.0-rc.1-source.zip \ - --checksum dist/mailstack-1.3.0-rc.1-source.zip.sha256 + dist/mailstack-1.3.0-rc.4-source.zip \ + --checksum dist/mailstack-1.3.0-rc.4-source.zip.sha256 diff --git a/CHANGELOG.md b/CHANGELOG.md index 529eeb9..ce849b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,12 +26,44 @@ All notable repository-level changes are recorded here. Application history befo - Established `MAILSTACK-1.3.0-RC1-DOCS-BASELINE-001` as the protected feature and documentation baseline. - Added the root `documents/` user-documentation hub with a user manual, task-based how-to guide, administrator guide, baseline record and mandatory phase history. - Added deterministic documentation index and manifest synchronization, phase scaffolding, contract tests and CI policy enforcement so maintained feature changes cannot merge without the required user documentation and changelog updates. +- Corrected the RC4 Windows audit harness so Git Bash maps installer-only `python3` calls to the exact Python interpreter running the local test process, without changing the Ubuntu production installer or requiring a machine-wide Windows alias. ### Compatibility - Preserved `VIBMAIL_*` environment variables, `vibmail-*` service names, `/etc/vibmail` paths, database identifiers, source directories, and legacy deployment contracts. - No application behavior, database migration operations, mail-flow, authentication, authorization, or deployment contract was intentionally changed. Legacy protocol headers such as `X-VibMail-CSRF` remain unchanged. +## 1.3.0-rc.3 — 2026-08-17 + +### Security + +- Upgraded the locked transitive `sqlparse` runtime from 0.5.5 to 0.6.0 after GitHub Actions run `32053931714` identified CVE-2026-71491, CVE-2026-59894, CVE-2026-59893, and CVE-2026-54284. +- Kept Django at 5.2.16; its declared `sqlparse>=0.3.1` dependency accepts sqlparse 0.6.0, and MailStack's Python 3.12 runtime satisfies sqlparse 0.6.0's Python 3.10+ requirement. +- Preserved all PHASE-003 installation, recovery, LMTP, ingestion, UI, route, schema, and deployment behavior; this maintenance delta changes only the vulnerable dependency pin plus required release metadata and verification records. + +### CI + +- Corrected RC2 qualification records that could be read as if the current PHASE-003 dependency audit had passed. The RC2 branch passed structural/documentation/installer/operations gates but stopped at the blocking online advisory gate. +- Regenerated deterministic documentation, design, and forensic inventories for RC3. Full dependency-backed GitHub Actions requalification remains mandatory before merge or release. + +## 1.3.0-rc.2 — 2026-08-17 + +### Fixed + +- Prevented the installer from changing the host-wide `/var/log` mode and added explicit least-privilege log-path checks. +- Isolated installer-launched Django management commands from stale parent-shell database and Django environment variables. +- Prepared the mailbox provisioning runtime lock directory before clean/repair bootstrap commands. +- Made reviewed repair resumable with explicit idempotent initial-administrator and system-mailbox preservation while retaining strict duplicate rejection by default. +- Persisted root-only initial administrator credentials immediately after administrator creation so later installer failures do not lose the generated password. +- Fixed Postfix-to-Dovecot LMTP delivery by configuring the static userdb with `allow_all_users=yes` while preserving Postfix SQL recipient validation. +- Allowed official one-shot dry-run ingestion verification to run beside the live ingestion worker without taking the exclusive worker lock or mutating ServiceHeartbeat state. +- Qualified the two conservative MariaDB uniqueness warnings against the existing `utf8mb4_unicode_ci` and unique-column deployment contract without changing schema or migrations. + +### Operations + +- Added SSH session-resilience guidance and a non-blocking installer warning when a mutating run starts outside `tmux`/`screen`. +- Synchronized the live-staging fixes back into the canonical source so fresh deployments require no manual copies of the acceptance hotfixes. + ## 1.3.0-rc.1 — 2026-06-30 ### Preserved diff --git a/CITATION.cff b/CITATION.cff index b3c3021..0bcf46e 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -2,8 +2,8 @@ cff-version: 1.2.0 message: "If you use MailStack, cite the software release and link to the canonical source repository." title: "MailStack" type: software -version: "1.3.0-rc.1" -date-released: 2026-06-30 +version: "1.3.0-rc.4" +date-released: 2026-08-17 authors: - name: "Vib Tools" repository-code: "https://github.com/vibtools/MailStack" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 20da9bb..676c620 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,6 +38,8 @@ python scripts/forensic_audit.py --root . --full The full gate runs tests, coverage, Ruff, Bandit, Django checks, contact-service tests and source-safety checks. +On Windows, the repository audit tools require a real Bash runtime for shell syntax and installer-plan checks. They automatically prefer Git for Windows/Git Bash over the WSL `bash.exe` launcher and probe the runtime before use. Install Git for Windows or set `BASH_EXECUTABLE` to a known-good Bash path when using a custom shell. A broken WSL or Docker Desktop VM must not be mistaken for a MailStack shell-script failure. + ## Pull requests 1. Open an issue for significant behavior, schema or deployment changes. diff --git a/README.md b/README.md index e02d27c..b0390d7 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.1` remains a release candidate. Source, test, security, template, and release tooling are present, but clean Ubuntu 24.04 VPS acceptance and real external SMTP/LMTP verification are required before stable promotion. +> **Release status:** `v1.3.0-rc.4` remains a release candidate. Clean Ubuntu 24.04 installation and real external SMTP/LMTP delivery have been exercised during the PHASE-003 hardening campaign; backup/restore, restart-recovery, legal, and final release-owner acceptance still gate stable promotion. ## Why MailStack @@ -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.0-rc.1-source.zip \ - --checksum dist/mailstack-1.3.0-rc.1-source.zip.sha256 + dist/mailstack-1.3.0-rc.4-source.zip \ + --checksum dist/mailstack-1.3.0-rc.4-source.zip.sha256 ``` ## Security diff --git a/ROADMAP.md b/ROADMAP.md index 5e75c2d..84dbfe4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -26,6 +26,11 @@ This roadmap is directional. Security, data integrity and backward compatibility - Keep planned and future-review screens inactive until architecture and security phases approve them - Require responsive, accessibility, security, regression, documentation and CI evidence for each page +## 1.3 RC hardening + +- PHASE-003 qualifies clean-install, reviewed partial-install repair, live-safe verification, and real external SMTP-to-LMTP delivery fixes discovered during Ubuntu 24.04 staging acceptance. +- Keep 1.3.0-rc.4 as a release candidate until backup/restore, restart-recovery, final security/legal, and release-owner acceptance are complete. + ## 1.3 stable - Promote the verified release candidate without feature removal diff --git a/SECURITY.md b/SECURITY.md index 78d3f0e..e197b0f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,7 +6,10 @@ Security fixes are applied to the latest tagged release candidate or stable rele | Version | Support status | |---|---| -| 1.3.0-rc.1 | Supported release candidate | +| 1.3.0-rc.4 | Supported release candidate | +| 1.3.0-rc.3 | Superseded release candidate | +| 1.3.0-rc.2 | Superseded release candidate | +| 1.3.0-rc.1 | Superseded release candidate | | 1.2.x | Legacy deployment assets retained; security support is best-effort | | Older versions | Unsupported | diff --git a/VERSION b/VERSION index bf16dde..9b388ed 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.0-rc.1 +1.3.0-rc.4 diff --git a/deployment/templates/dovecot/99-vibmail.conf.tpl b/deployment/templates/dovecot/99-vibmail.conf.tpl index 2b2c4f6..1945a7b 100644 --- a/deployment/templates/dovecot/99-vibmail.conf.tpl +++ b/deployment/templates/dovecot/99-vibmail.conf.tpl @@ -11,7 +11,7 @@ ssl_key = =0.3.1`, and sqlparse 0.6.0 supports Python 3.10+, including +MailStack's required Python 3.12 runtime. `pip check` and the network-enabled blocking `pip-audit` +gate remain mandatory for RC4 qualification; no advisory is ignored or suppressed. diff --git a/docs/FORENSIC_AUDIT_REPORT.md b/docs/FORENSIC_AUDIT_REPORT.md index 50b7b31..d05dbec 100644 --- a/docs/FORENSIC_AUDIT_REPORT.md +++ b/docs/FORENSIC_AUDIT_REPORT.md @@ -1,9 +1,9 @@ -# Forensic audit report — MailStack 1.3.0 RC1 +# Forensic audit report — MailStack 1.3.0 RC3 -**Baseline audit date:** 2026-07-25 -**Release version:** `1.3.0-rc.1` +**PHASE-003 audit date:** 2026-08-17 +**Release version:** `1.3.0-rc.4` **Target runtime:** Ubuntu Server 24.04 LTS and CPython 3.12 -**Release classification:** CI-qualified and clean-clone-qualified open-source release candidate +**Release classification:** locally structurally qualified release candidate; dependency-backed RC3 CI requalification pending ## Executive disposition @@ -26,13 +26,13 @@ | Installer and operations contract tests | PASS | | Template rendering and placeholder validation | PASS | | Forensic file/symbol inventory | PASS | -| Deterministic release ZIP, manifest and checksum | PASS in GitHub CI | -| Online dependency advisory query | PASS in GitHub CI | -| Clean Ubuntu 24.04 full-stack acceptance | PENDING external VPS | -| Real inbound SMTP/LMTP acceptance | PENDING external VPS | +| Deterministic release ZIP, manifest and checksum | PENDING RC4 CI; deterministic local build/verification required before patch handoff | +| Online dependency advisory query | PENDING RC4 rerun — RC2 run `32053931714` failed on sqlparse 0.5.5; RC3/RC4 pin 0.6.0 | +| Clean Ubuntu 24.04 RC3 full-stack acceptance | PENDING exact-RC3 clean VPS requalification | +| Real inbound SMTP/LMTP acceptance | PASS in staging after equivalent PHASE-003 LMTP hotfix; exact-RC3 clean requalification pending | | Copyright ownership/license confirmation | PENDING release owner | -**OPEN_SOURCE_RELEASE_CANDIDATE:** PASS +**OPEN_SOURCE_RELEASE_CANDIDATE:** PENDING RC4 CI **PRODUCTION_ACCEPTANCE:** PENDING ## Audited scope @@ -96,10 +96,11 @@ See `FEATURE_MATRIX.md` for the feature-by-feature verification record. 15. The complete 25-image UI and logo archive is preserved with stable IDs, SHA-256 hashes, PNG structural validation, scope classification, and CI enforcement. 16. PHASE-002 adds the frozen runtime design tokens, responsive authenticated and sign-in shells, local SVG assets, accessible navigation behavior, and focused UI contract gates without changing page business logic. 17. Cross-platform verification now closes contact-service SQLite handles deterministically, preserves POSIX-only permission assertions, and subjects the standalone contact service to Ruff and Bandit in both the full forensic gate and CI. +18. RC3 updates the vulnerable transitive sqlparse 0.5.5 lock to upstream 0.6.0 after the blocking PHASE-003 CI advisory scan identified four 2026 CVEs; no advisory suppression or application behavior change is introduced. ## Automated evidence -- Django tests: **195 passed, 1 capability-based skip, 0 failed** +- Last completed pre-RC3 Django suite: **195 passed, 1 capability-based skip, 0 failed**; RC3 full rerun pending - Application coverage: **94.99%**; minimum: **85%** - Ruff: **PASS** - Bandit: **PASS** @@ -121,7 +122,12 @@ The authoritative repository qualification is GitHub Actions run `30133728843` o Verified controls include root-only generated secrets, strict configuration validation, Argon2 password hashing, CSRF and secure-cookie controls, login throttling, object-level mailbox authorization, safe HTML sanitization, protected attachments, receive-only SMTP, no public registration, no IMAP/POP3/submission in the reference deployment, MariaDB least privilege, systemd sandboxing, safe archive extraction, checksum verification and fail-closed CI/release gates. -The network-enabled `pip-audit` command passed as a blocking step in GitHub Actions run `30133728843`. Future dependency changes must continue to pass the locked dependency, `pip check`, and online advisory gates. +The historical pre-PHASE-003 `pip-audit` gate passed in GitHub Actions run `30133728843`. +For the current PHASE-003 branch, run `32053931714` failed at the blocking advisory step because +`sqlparse==0.5.5` was newly reported for CVE-2026-71491, CVE-2026-59894, CVE-2026-59893, and +CVE-2026-54284. RC3 pins upstream sqlparse 0.6.0, which contains those security fixes. The RC3 +dependency audit, `pip check`, and all downstream CI gates remain mandatory; no vulnerability is +ignored or waived. ## Performance review @@ -141,6 +147,28 @@ mail-flow, ingestion schema, deployment-template, or package change. Local depen and coverage passed; the final overwrite verifier must pass before commit, and GitHub Actions must pass before remote qualification. +## PHASE-003 audit boundary + +PHASE-003 changes only installer/recovery behavior, the Dovecot LMTP static-userdb template, +one-shot ingestion verification semantics, narrowly qualified production MariaDB warnings, focused +management-command/test contracts, release metadata, and required documentation. It adds no database +migration, dependency, UI page, URL, permission model, outbound mail path, or data transformation. + +The structural forensic gate passes with zero blocking findings after regenerating the deterministic +file inventory. RC4 additionally centralizes verified Bash runtime discovery for repository-level +installer, operations, and forensic tooling: Windows prefers Git for Windows Bash over the WSL +launcher, while Linux keeps the system Bash contract. This prevents an unavailable WSL/Docker +Desktop backing disk from being reported as multiple MailStack shell syntax defects. A subsequent +Windows run proved Git Bash selection was correct but exposed a second host-compatibility edge: the +Windows Python installation provided `python` but no Git-Bash-visible `python3` command. The RC4 +audit harness now uses a process-local `BASH_ENV` bridge to map `python3` to the exact interpreter +running the test harness on Windows only; `install.sh` and Ubuntu's production `python3` behavior +remain unchanged. Dependency-free documentation, design, UI-foundation, template, installer, +operations, Python compile, and shell-syntax gates pass locally. Dependency-backed +Django/Ruff/Bandit/coverage and full-forensic qualification remain blocking in GitHub Actions because +the local artifact builder does not contain or have network access to the pinned development +environment. + ## External acceptance gates Before stable promotion: @@ -149,8 +177,8 @@ Before stable promotion: 2. Preserve the successful GitHub CI baseline and require every subsequent release commit to pass all blocking gates. 3. Install on a clean isolated Ubuntu Server 24.04 VPS. 4. Verify DNS, MX, PTR/rDNS, TLS, firewall, unknown-recipient rejection, LMTP delivery, ingestion, authorization, contact delivery, backup, restore and restart recovery. -5. Publish the first version as `v1.3.0-rc.1`; promote to stable only after acceptance. +5. Publish `v1.3.0-rc.4` only after its blocking CI gates pass; promote to stable only after the remaining acceptance gates pass. ## Final classification -The repository is approved for public publication as **MailStack 1.3.0 RC1**, not yet as a proven stable production release. +The repository remains a **MailStack 1.3.0 RC4 release candidate**, not yet a proven stable production release. PHASE-003 source qualification is complete only when its blocking GitHub Actions run passes. diff --git a/docs/FORENSIC_FILE_INVENTORY.json b/docs/FORENSIC_FILE_INVENTORY.json index 9326a0b..30aed4f 100644 --- a/docs/FORENSIC_FILE_INVENTORY.json +++ b/docs/FORENSIC_FILE_INVENTORY.json @@ -53,7 +53,7 @@ "kind": "text", "lines": 35, "path": ".github/ISSUE_TEMPLATE/question.yml", - "sha256": "15d51c9dce63f5249441f1a70f0dbd3b0a9540e4f97290a31e35f4a5d9ab708c", + "sha256": "b11e421c9750e355cc98bf19a7fca8429a59ab4579143c19f40c0249f65bd69f", "size_bytes": 902 }, { @@ -74,7 +74,7 @@ "kind": "text", "lines": 129, "path": ".github/workflows/ci.yml", - "sha256": "a7a59108ed7874764e4b641236d384ae0cc65ccb858c9a37b7087fbd5f96130d", + "sha256": "0b719b1125e6cd730da533e1bc3d729e00b067a8f0707c7c2837ada2d3fd8b81", "size_bytes": 4106 }, { @@ -93,16 +93,16 @@ }, { "kind": "text", - "lines": 64, + "lines": 96, "path": "CHANGELOG.md", - "sha256": "955a5e1016481f0879e80f56745b7fc257cc1168fdc5ca6a22e4918574f88962", - "size_bytes": 5425 + "sha256": "fa3285b026126a1d98da9cef4817015c86decf0793b8205bf4357ad8b6773f10", + "size_bytes": 8261 }, { "kind": "text", "lines": 21, "path": "CITATION.cff", - "sha256": "d5ee345d322194e6361eca5feb9535ba1577940b72a448a4ef63e8e070740413", + "sha256": "777a7a70e2b7d3423c634f347af2e894dcc77b4f179347c9bbb14af2f03e6f91", "size_bytes": 493 }, { @@ -114,10 +114,10 @@ }, { "kind": "text", - "lines": 77, + "lines": 79, "path": "CONTRIBUTING.md", - "sha256": "87157dd3cb0ba2d2080bc9008d81aca213a8d6d920c83a76f74e02cca03050cb", - "size_bytes": 3236 + "sha256": "59cf3331d32ee1373d0de6937355f83bdb9e657d1f574e6e92738474ca1904ce", + "size_bytes": 3661 }, { "kind": "text", @@ -137,22 +137,22 @@ "kind": "text", "lines": 314, "path": "README.md", - "sha256": "3e819d6213611c9b0215102db9b8e2bb72e282e58ec56f999bc1383e67864fd7", - "size_bytes": 12982 + "sha256": "55aea5c0a8828fa26d10913c82a3fc16a78c2ee3bf44c00bbb7aff1bd68a7719", + "size_bytes": 13033 }, { "kind": "text", - "lines": 51, + "lines": 56, "path": "ROADMAP.md", - "sha256": "17d050845f166967c9370ce7a7da4567878c6e355d65c5dc78a0f7535f887032", - "size_bytes": 2447 + "sha256": "6b512eb665b048062222dfc2eafbd201f248b9da214ecc0aa0131b9f7be2978b", + "size_bytes": 2806 }, { "kind": "text", - "lines": 37, + "lines": 40, "path": "SECURITY.md", - "sha256": "31862b0c5e1452815bc68a1d595143520f836f48b9e53a1aca48e4953657330e", - "size_bytes": 1826 + "sha256": "fdae4f0771a6ff2dcec0bc10cdef9d40d7c34e03f3539e172161fffd4bfc2659", + "size_bytes": 1964 }, { "kind": "text", @@ -165,7 +165,7 @@ "kind": "text", "lines": 1, "path": "VERSION", - "sha256": "82d60e10e0d6a08ee84288108bcb9b15e933935160a975ccd55136660bf75ad7", + "sha256": "87892d5c7aa3d86de1ccd4742825eb8ff6f9c6350133351dd01a6007e1163e87", "size_bytes": 11 }, { @@ -186,8 +186,8 @@ "kind": "text", "lines": 29, "path": "deployment/templates/dovecot/99-vibmail.conf.tpl", - "sha256": "a4f39ba0cf7dd0b7a807a571d79fc15c760c14444e02e676de80fe98e1773f68", - "size_bytes": 581 + "sha256": "9a3386e2fd3a7b066596760005d55dfa4d0f0ab25aaee1548584191d6b91dc28", + "size_bytes": 601 }, { "kind": "text", @@ -277,7 +277,7 @@ "kind": "text", "lines": 581, "path": "design/DESIGN_MANIFEST.json", - "sha256": "69e4d6d6a78c0ccf69fb2221d72bd1551770b13f8f62c2c59a39b53b42004f61", + "sha256": "353323c27340d064b3226a94ad5dd6286d5c3efad3001b2fbd77f55601a420aa", "size_bytes": 21143 }, { @@ -469,7 +469,7 @@ "kind": "text", "lines": 31, "path": "docs/BUILD.md", - "sha256": "a661999881266fd4dfdc4cf29d3f63d9a92a03c777c9e3d14f8ce46de07d22bd", + "sha256": "04fdbccd2bb6484d38cfe49d4df429454c5309178f7c297b22bf3b8de3a852bf", "size_bytes": 804 }, { @@ -488,10 +488,10 @@ }, { "kind": "text", - "lines": 27, + "lines": 34, "path": "docs/DEPENDENCY_REVIEW.md", - "sha256": "8e4eec877b9efb8e3e55118da2c0097c46ece6d8f0d2b2915415edf6d9142790", - "size_bytes": 1714 + "sha256": "2a9e2eeb011457c9ca9711e9422febd731c006aafc22c1760c17d173fdb1d2c6", + "size_bytes": 2184 }, { "kind": "text", @@ -537,10 +537,10 @@ }, { "kind": "text", - "lines": 156, + "lines": 184, "path": "docs/FORENSIC_AUDIT_REPORT.md", - "sha256": "6cdd9f4964ec014dbf92e6f4278742bd75cf60da45afd934e6522c2bd82f31fd", - "size_bytes": 9925 + "sha256": "d02d831ed01c89a80badec752f7fed92a4a23e934d51270c49f864f30cd082bd", + "size_bytes": 12561 }, { "kind": "text", @@ -551,10 +551,10 @@ }, { "kind": "text", - "lines": 69, + "lines": 83, "path": "docs/INSTALLATION.md", - "sha256": "83fa630aa033b4c3ae47ed4f575e23ab37831afe792a19dc99ecd81358c24734", - "size_bytes": 2319 + "sha256": "c861c9464e076bb9cac0711be70099a7cd03e193e519d03e9f96898100f839f3", + "size_bytes": 3545 }, { "kind": "text", @@ -593,10 +593,10 @@ }, { "kind": "text", - "lines": 44, + "lines": 51, "path": "docs/OPERATIONS.md", - "sha256": "e8f54324ec4f36d57110686262c8ac078fb13dee994561c8b98961c2bd568a72", - "size_bytes": 1308 + "sha256": "25d347928a3b5ad7884632b19f3221160d4b1dffe867361831e126cd88eae660", + "size_bytes": 1772 }, { "kind": "text", @@ -623,15 +623,15 @@ "kind": "text", "lines": 61, "path": "docs/PUBLISHING.md", - "sha256": "c5ca0ba3271f1b06500d19c1b9f387af5302ee9bc2bea6eae5a6b29be773a8e3", + "sha256": "d8e887b3cf2af8c3688c71d9bb535f90911d354dfc31d09121e4b1031dbdfa19", "size_bytes": 2279 }, { "kind": "text", - "lines": 38, + "lines": 46, "path": "docs/QUICKSTART.md", - "sha256": "1f6a61c0c81f82e726811a0a48aaa0b61d3a856741a267ed7de21f72dfb83cd8", - "size_bytes": 1421 + "sha256": "7fe067162d4b6db47e5e35ff841fa6acc577e97a51abfe9dc1e49f535e286d55", + "size_bytes": 1695 }, { "kind": "text", @@ -642,16 +642,16 @@ }, { "kind": "text", - "lines": 51, + "lines": 92, "path": "docs/RELEASE_NOTES_1.3.0.md", - "sha256": "a73aeba899dd61ccb63149601f93cf62238262318d075abda55b279971dc753f", - "size_bytes": 2961 + "sha256": "43626199bdc76f4b30c9b57b9630687df3b76307db2e45e8af711b39e7a78f25", + "size_bytes": 6059 }, { "kind": "text", "lines": 14, "path": "docs/RELEASE_PROCESS.md", - "sha256": "1b0c561d2fb3e8b6b28bb38f997657fc5731c75580f02795c09c98ef36480464", + "sha256": "e6ab1c94eecadb55d6cdf6eb040ed312ace2f74d5818f4bbadb1df3f55eb9196", "size_bytes": 973 }, { @@ -670,10 +670,10 @@ }, { "kind": "text", - "lines": 84, + "lines": 142, "path": "docs/TEST_REPORT.md", - "sha256": "acd40d7760fc0afe861860c9788786ee66f6984ff4a99ac9d32aaf2a160df0d9", - "size_bytes": 4896 + "sha256": "881bf725b8338853a82726d7627feedbfaefa21e4bd88e79590cb84247abeda1", + "size_bytes": 8779 }, { "kind": "text", @@ -684,10 +684,10 @@ }, { "kind": "text", - "lines": 31, + "lines": 42, "path": "docs/TROUBLESHOOTING.md", - "sha256": "4dd557dc32f9404623544e7a8a23272ae4f41b3d7926517d23c05e4f436c6fde", - "size_bytes": 1448 + "sha256": "7ff9f0a8afcd5348b72b7a37b7af51f0859f5ec8c920cc5721c8f5b3f24b21dc", + "size_bytes": 2961 }, { "kind": "text", @@ -698,128 +698,135 @@ }, { "kind": "text", - "lines": 65, + "lines": 68, "path": "documents/ADMIN_GUIDE.md", - "sha256": "c14f1b2a52ceef69ce32b32ff56cbb71d003a71244d454c9d2e52f77b8e0c69a", - "size_bytes": 3557 + "sha256": "dd61ab74c2b54cb6e637d5097c789a44c792feba592a591a5d1348a587625f4f", + "size_bytes": 3855 }, { "kind": "text", "lines": 44, "path": "documents/BASELINE.md", - "sha256": "f17acc52e17a87a545bb38dbd171ee3462491506ceb6412b9a98e86257d7d1fe", + "sha256": "5a8adbcc64d6ea48c11dbdb04f238caa51b621475b8df3993e4138717195b236", "size_bytes": 2050 }, { "kind": "text", - "lines": 197, + "lines": 209, "path": "documents/DOCUMENTATION_MANIFEST.json", - "sha256": "1c804d2ad6aa8ab67873dc9d5e0946093dd60c82c94b98b56b68a82cb7e1a799", - "size_bytes": 7309 + "sha256": "c6cc10f50dbc600cf1f08a49b473b8aa456367bd30b8b1d6cab2b84da8263fbb", + "size_bytes": 7880 }, { "kind": "text", "lines": 76, "path": "documents/DOCUMENTATION_POLICY.md", - "sha256": "1d8eacd99557546403054eb139b7fe89e53c59d90d07db018b8bce6db9d735ae", + "sha256": "7786aef25b75254c38870b61329b9ca8ccdf367244ef8a97a0f13f692697879e", "size_bytes": 3189 }, { "kind": "text", "lines": 76, "path": "documents/HOW_TO_USE.md", - "sha256": "1e83c10dfd06d22dd269141a2c0dfd8b82644981719ec4471bf6c50a20f22686", + "sha256": "0dd3e9d1d352ddb152180af3858afbec7f89b0aeb6b5a39881455e0ecd661655", "size_bytes": 3253 }, { "kind": "text", - "lines": 52, + "lines": 53, "path": "documents/README.md", - "sha256": "09cb98614bea202d9a8f0c16d5bf29498f7f83d7103c476044e3d8e6ac299005", - "size_bytes": 3491 + "sha256": "fd28dd001bd954ae9d635fe695beb799e6f40ce8990e977e5449c804352175b4", + "size_bytes": 3693 }, { "kind": "text", "lines": 82, "path": "documents/USER_MANUAL.md", - "sha256": "c52aee69746a25bb47e8bb8f9b8d5a01cff2230c8c4877c4a807126078f75ba7", + "sha256": "374ee5a683de0098b59dc39b25efdf6a04efdd7e33873517792160093f6fb6f2", "size_bytes": 4218 }, { "kind": "text", "lines": 46, "path": "documents/design/ACCESSIBILITY_SPECIFICATION.md", - "sha256": "fe3760b78db81e862744c46fe9dd42c8b323108224fa1bb7c35e2683f0aad38b", + "sha256": "8f31425f3cdec0165a4d0e6246ea68a770a898aed919ae01ac39da082326c11b", "size_bytes": 2187 }, { "kind": "text", "lines": 55, "path": "documents/design/COMPONENT_MATRIX.md", - "sha256": "9f0ce0a538eb632426ead8e2f306ea73725fe9a2bb4a5ba6b7e82c34a1265b2b", + "sha256": "b3c29ce563f1e794a22e60868dc47afd1efda6c20c27b56a05638e9288311b81", "size_bytes": 3465 }, { "kind": "text", "lines": 52, "path": "documents/design/FUTURE_UI_ROADMAP.md", - "sha256": "364119f5df14e66347699e7bda9b22b964a55f4c37659dd06c1c0db0d6fde04b", + "sha256": "c08173fb7454572a326b2aeccb3706531113fbd4c635239242e283630bc8d2e5", "size_bytes": 1567 }, { "kind": "text", "lines": 48, "path": "documents/design/IMPLEMENTATION_STATUS.md", - "sha256": "7095a9f650707c58641b6179e0837ff399edb66758addaa6ccbf1577f4d1d81a", + "sha256": "61ed02ae43021e603a2fdc2b9cf0b91414a213b2bec14c4917b8cf0da44b6e02", "size_bytes": 2516 }, { "kind": "text", "lines": 51, "path": "documents/design/RESPONSIVE_SPECIFICATION.md", - "sha256": "1bc228db2d45c2e609d9dd10da836f7214519176f415f8b0f3692dc0b5023171", + "sha256": "fcc959dd904bece5f2b481b5f5e77ec8af349136c3d750b56bc41ac1547e0280", "size_bytes": 2027 }, { "kind": "text", "lines": 62, "path": "documents/design/SCREEN_CATALOG.md", - "sha256": "7822cd15dea8c3a64cdac2eaabe5c3ad4c5f9c9b9ca8cb42ddd24b2097c9748f", + "sha256": "36db5fa837255ef66f98edc25cfeb599d704a079071338739e5b14965584f9f9", "size_bytes": 3292 }, { "kind": "text", "lines": 89, "path": "documents/design/UI_FOUNDATION.md", - "sha256": "b36905a2fe8f995b053347e2f8b3ceed3bd0d085ffd2670a591e63e8e9c30487", + "sha256": "0b3184ec7e1f9133f0f80360698d2abf04ecf9cc9df6cdfb58350aa07d77ecc2", "size_bytes": 3758 }, { "kind": "text", "lines": 59, "path": "documents/phases/PHASE-000-BASELINE.md", - "sha256": "f6c1872bc32d81f11cf67a1f7bf619eebae7080603d3b7981211e2648dc064c0", + "sha256": "6970ddf63b8c2aa45f1d6f8c62244f25b4aaf09f00922acfa83947a5184e0129", "size_bytes": 2543 }, { "kind": "text", "lines": 68, "path": "documents/phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md", - "sha256": "f72355ea2266fff91bc4b053a1b11d308e23320664cdc3b2c604ad7e88695220", + "sha256": "204a4a66e2017081242d15b15d01481dddcde6cdcd269cda43f477e0d7ae0f01", "size_bytes": 2770 }, { "kind": "text", "lines": 98, "path": "documents/phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md", - "sha256": "f4ef536520aaaa30d23b691102ca355a84475eaa23c9b29a8a415e349b3f6ee5", + "sha256": "450c51bf90048366e91615aecefb366247fa0b3a13a9eea7a1039a3ca3e4718d", "size_bytes": 5493 }, { "kind": "text", - "lines": 648, + "lines": 86, + "path": "documents/phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md", + "sha256": "b2e0b9e88d6410d20e916ea420b1151fb9d014dc253177dc5a71b2274b47b822", + "size_bytes": 5055 + }, + { + "kind": "text", + "lines": 684, "path": "install.sh", - "sha256": "c8effbd6c6a40aad8ea2e994ba55242bdeddd353898d8c7dfbaef4d4c8613104", + "sha256": "f7db90e856ba9ef5598cdc248e24011a57f02c94d4bdb78ef2a42eadb6e71c31", "shell_functions": [ "backup_file", "die", @@ -832,9 +839,10 @@ "run_as_vmail", "usage", "valid_email", - "valid_hostname" + "valid_hostname", + "write_initial_credentials" ], - "size_bytes": 25536 + "size_bytes": 27041 }, { "kind": "text", @@ -859,10 +867,10 @@ }, { "kind": "text", - "lines": 60, + "lines": 76, "path": "mailbox-app/CHANGELOG.md", - "sha256": "6520653f805d1c8a5c1ff49e7a77624e45a039d07bdac8c0f76b5b8ddd4d641c", - "size_bytes": 3664 + "sha256": "c95cbde106aec016141e31d445e4f0b0f091be7a8b66f894d47812ea98ad222e", + "size_bytes": 4864 }, { "kind": "text", @@ -1009,7 +1017,7 @@ }, { "kind": "text", - "lines": 46, + "lines": 69, "path": "mailbox-app/apps/accounts/management/commands/create_initial_admin.py", "python": { "classes": [ @@ -1030,8 +1038,8 @@ "Command.handle" ] }, - "sha256": "16f7e5d84df6eb7921bfd3fcf05a0695262cf15f664a43e6d39e46f5c7c22c13", - "size_bytes": 1820 + "sha256": "cd9e62ad4a57e567135dc22820fd6346b2908b7d4bc4d569c9ca567b6fc2063c", + "size_bytes": 2983 }, { "kind": "text", @@ -1770,7 +1778,7 @@ }, { "kind": "text", - "lines": 62, + "lines": 66, "path": "mailbox-app/apps/ingestion/management/commands/ingest_maildir.py", "python": { "classes": [ @@ -1792,8 +1800,8 @@ "Command.stop_handler" ] }, - "sha256": "896ac7e17eb21fb3e500694f1332e25790b6202b889caf2c9ea72dcb4e0b9824", - "size_bytes": 2506 + "sha256": "18f76e00e8a2f99810387030ad5af39f4214350fe7af2164da28f6a905816ae6", + "size_bytes": 2687 }, { "kind": "text", @@ -1892,7 +1900,7 @@ }, { "kind": "text", - "lines": 232, + "lines": 234, "path": "mailbox-app/apps/ingestion/service.py", "python": { "classes": [ @@ -1929,8 +1937,8 @@ ], "methods": [] }, - "sha256": "c7891fab07ff10e6172dd45179eb8bf6b46fe8b4ef14681f532789c6c2380581", - "size_bytes": 9220 + "sha256": "950050c62467b905dec31d0c303da7d89abd3179032a72d947da4a8ab2ca8250", + "size_bytes": 9324 }, { "kind": "text", @@ -2095,7 +2103,7 @@ }, { "kind": "text", - "lines": 20, + "lines": 60, "path": "mailbox-app/apps/mailboxes/management/commands/create_system_mailbox.py", "python": { "classes": [ @@ -2103,7 +2111,12 @@ ], "functions": [], "imports": [ + "__future__", + "apps.mailboxes.mailserver", + "apps.mailboxes.models", "apps.mailboxes.services", + "apps.mailboxes.validators", + "django.conf", "django.core.management.base" ], "methods": [ @@ -2111,8 +2124,8 @@ "Command.handle" ] }, - "sha256": "cf27d85c0d9685937439109181efd78b5d662e63c3fbdbafb1122e1afac3a361", - "size_bytes": 850 + "sha256": "703988125d67eab971a5c4e5a6ef966f0e003d8f5af22e47e5ff26ac1cc1981d", + "size_bytes": 3214 }, { "kind": "text", @@ -2786,7 +2799,7 @@ }, { "kind": "text", - "lines": 121, + "lines": 127, "path": "mailbox-app/config/settings/production.py", "python": { "classes": [], @@ -2806,8 +2819,8 @@ ], "methods": [] }, - "sha256": "277d0a5db3a3e9460a647c0a4186b356970d82643e2ce91e6b36303b04019bd0", - "size_bytes": 4763 + "sha256": "320db55b2db817c770cea4fcce2710b4f7820af89266bea3bdcb37d9d46f06f6", + "size_bytes": 5220 }, { "kind": "text", @@ -3148,7 +3161,7 @@ "kind": "text", "lines": 80, "path": "mailbox-app/pyproject.toml", - "sha256": "7176cc2834d559a76b3cdf183c9fd49655cd869ed1c0be3e0b12805b2385067b", + "sha256": "44bd9c76b1cd6a108c3d217871936ff428e55fa2876b41f2f72e34ed8a99b1cd", "size_bytes": 2411 }, { @@ -3162,7 +3175,7 @@ "kind": "text", "lines": 24, "path": "mailbox-app/requirements/constraints.txt", - "sha256": "630380654b621e01b2b958c3a61922b5bf66ec7413fc48d460b49545fd9e65c7", + "sha256": "2e80e4879e04939f363b13b54ab6aa59a8a79b27f65d5d1546f8f24219423efd", "size_bytes": 448 }, { @@ -3176,7 +3189,7 @@ "kind": "text", "lines": 17, "path": "mailbox-app/requirements/locked.txt", - "sha256": "fc2a49c85d73c11ca6702c7c8ed91570b3aa0d30e0b7026e9b6bfc008f89b24a", + "sha256": "283b4df38fc1dbc06538ebb55eb3de7c078caac6abbf253c8a07d44ea9a0f3e9", "size_bytes": 358 }, { @@ -3831,7 +3844,7 @@ }, { "kind": "text", - "lines": 207, + "lines": 227, "path": "mailbox-app/tests/integration/test_commands_health_postfix.py", "python": { "classes": [], @@ -3871,8 +3884,8 @@ ], "methods": [] }, - "sha256": "3fad297a5be777c2060fe44d746c2554c53cefcbad89a6dbff0bce979e040df3", - "size_bytes": 7664 + "sha256": "ac21742565d672b152e883bf64f14119569470a5a06abd7ead55d81e99825fc9", + "size_bytes": 8780 }, { "kind": "text", @@ -4019,7 +4032,7 @@ }, { "kind": "text", - "lines": 150, + "lines": 171, "path": "mailbox-app/tests/security/test_deployment_assets.py", "python": { "classes": [], @@ -4030,9 +4043,11 @@ "test_dependency_audit_isolated_and_fail_closed", "test_management_scripts_force_production_environment", "test_mariadb_application_role_is_scoped_away_from_mailserver_ddl", + "test_production_mariadb_warning_qualification_is_narrow_and_documented", "test_release_pins_and_verifies_django_security_patch", "test_release_source_cannot_be_the_live_application_tree", "test_restore_preserves_service_readable_environment_and_quiesces_postfix", + "test_sqlparse_security_release_is_pinned_consistently", "test_systemd_runtime_directories_are_isolated", "test_ui_reserves_system_addresses", "test_v1_2_1_security_hotfix_pins_and_bleach_scope", @@ -4045,8 +4060,8 @@ ], "methods": [] }, - "sha256": "35b8fced3bc30e59072e7a352dec053b97fcd7e9dd39c0f47308b86a37dae9c0", - "size_bytes": 6261 + "sha256": "eb134d939bba5c099d0412c4825ac97c6222956522537439a52b02fae916d013", + "size_bytes": 7228 }, { "kind": "text", @@ -4669,7 +4684,7 @@ }, { "kind": "text", - "lines": 380, + "lines": 397, "path": "scripts/forensic_audit.py", "python": { "classes": [], @@ -4688,14 +4703,15 @@ "os", "pathlib", "re", + "shell_runtime", "subprocess", "sys", "tempfile" ], "methods": [] }, - "sha256": "98e78dada595005bd3fa0095f11a8a9c1ed642b21d6fd1e8cf973de9e1492b12", - "size_bytes": 13836 + "sha256": "3615ebd13d33786631d50b025240f4fca85005eee84e27183450c1958ed7ffbf", + "size_bytes": 14391 }, { "kind": "text", @@ -4855,6 +4871,42 @@ "sha256": "4b591c23da8ba83f90b1e315406af74c40bf58c6a991cb3d18124ab198154e99", "size_bytes": 1667 }, + { + "kind": "text", + "lines": 192, + "path": "scripts/shell_runtime.py", + "python": { + "classes": [], + "functions": [ + "_add_candidate", + "_bash_path", + "_candidate_bashes", + "_probe", + "_python3_bridge_bash_env", + "_shell_single_quote", + "_windows_git_bash_candidates", + "bash_environment", + "bash_script_command", + "bash_syntax_command", + "resolve_bash", + "script_argument" + ], + "imports": [ + "__future__", + "atexit", + "functools", + "os", + "pathlib", + "shutil", + "subprocess", + "sys", + "tempfile" + ], + "methods": [] + }, + "sha256": "20e15ae4b227e01de56f7a386936aef67a97d748c1e1a374c41f464cf4d9d651", + "size_bytes": 6826 + }, { "kind": "text", "lines": 136, @@ -4915,7 +4967,7 @@ }, { "kind": "text", - "lines": 108, + "lines": 153, "path": "scripts/test_installer.py", "python": { "classes": [], @@ -4926,18 +4978,18 @@ ], "imports": [ "__future__", - "os", "pathlib", + "shell_runtime", "subprocess" ], "methods": [] }, - "sha256": "4b374e1531cf57807ad036ae9e7f58168f2932e4a6a3f0732bfdfad869ca5dc9", - "size_bytes": 5049 + "sha256": "9e5d4fe960b57ded16295514a2e451862a4f90ce84ea6a159d2d442a2857f141", + "size_bytes": 7354 }, { "kind": "text", - "lines": 102, + "lines": 116, "path": "scripts/test_operations.py", "python": { "classes": [], @@ -4949,15 +5001,15 @@ "imports": [ "__future__", "ipaddress", - "os", "pathlib", "re", + "shell_runtime", "subprocess" ], "methods": [] }, - "sha256": "ed0e3ec0decf1715884a03007d068f061b9c1b7e403ff58518bbe0c89bc09aa5", - "size_bytes": 3595 + "sha256": "5f1917f0b6ecd769c4caecf0a7bb5cff8ae13b847d2f097ecbfa49630d9ed685", + "size_bytes": 4332 }, { "kind": "text", @@ -5038,7 +5090,7 @@ "size_bytes": 6930 } ], - "release_version": "1.3.0-rc.1", + "release_version": "1.3.0-rc.4", "schema_version": 1, "scope": "All maintained repository files excluding generated archives, caches, build output and this inventory file itself.", "self_entry": { @@ -5047,15 +5099,15 @@ }, "summary": { "binary_files": 25, - "files": 401, + "files": 403, "python_classes": 73, - "python_files": 143, - "python_functions": 391, + "python_files": 144, + "python_functions": 405, "python_methods": 61, "shell_files": 13, - "shell_functions": 30, - "text_files": 376, - "total_bytes": 31612178, - "total_text_lines": 25849 + "shell_functions": 31, + "text_files": 378, + "total_bytes": 31655177, + "total_text_lines": 26603 } } diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index af3da89..84afc52 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -34,13 +34,21 @@ Plan mode validates arguments and prints the intended layout without changing th ## Installation +When installing over SSH, start a resilient terminal session first so a PuTTY/network disconnect does not terminate the foreground installer: + +```bash +tmux new -s mailstack-install +``` + +Then run the installer inside that session: + ```bash chmod +x install.sh sudo ./install.sh --domain example.com --admin-email admin@example.com \ --server-ip 203.0.113.10 --non-interactive ``` -Optional flags include custom application/mail/public hostnames, `--www`, a password environment variable, `--repair`, and `--skip-dns-check`. Use `./install.sh --help` for the authoritative list. +Optional flags include custom application/mail/public hostnames, `--www`, a password environment variable, `--repair`, and `--skip-dns-check`. Use `./install.sh --help` for the authoritative list. The installer warns, but does not abort, when a mutating run starts over SSH outside `tmux`/`screen`. ## Installer phases @@ -52,9 +60,15 @@ Optional flags include custom application/mail/public hostnames, `--www`, a pass 6. Application and public-site deployment 7. Postfix, Dovecot, Nginx and systemd configuration 8. TLS certificate issuance -9. Database migration, static collection and mailbox provisioning +9. Database migration, static collection and resumable administrator/system-mailbox provisioning 10. Acceptance checks and installation marker +## Reviewed repair after a partial installation + +If a clean installation stops after secrets or bootstrap data have been created, inspect the failure, preserve the generated state, and rerun the same release and parameters with `--repair`. Repair preserves a valid existing administrator and valid `postmaster`/`abuse` mailboxes, creates only missing bootstrap objects, and fails closed on inconsistent state. It never silently changes an existing administrator password. If repair must create the administrator, a new root-only initial-credentials file is written immediately after creation. + +Do not source `/etc/vibmail/vibmail.env` into the parent root shell before installation/repair. Installer-launched Django commands use an isolated least-privilege environment so stale exported database credentials cannot override the rendered production environment. + ## After installation - Store `/etc/vibmail/installer-secrets.env` securely. diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 114ce2f..5d730b4 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -17,11 +17,18 @@ tail -f /var/log/vibmail/application.log ## Health and contract checks +Use the maintained scripts for the complete live/ready and application contract checks: + ```bash -sudo -u vmail env VIBMAIL_ENV_FILE=/etc/vibmail/vibmail.env \ - DJANGO_SETTINGS_MODULE=config.settings.production \ - /opt/vibmail/venv/bin/python /opt/vibmail/app/manage.py check --deploy +/opt/vibmail/app/scripts/health_check.sh +/opt/vibmail/app/scripts/verify_application.sh +``` +The application verifier's Maildir scan is a one-shot dry run. It does not stop the live ingestion service, does not acquire the exclusive ingestion-worker lock, and does not update ingestion heartbeat state. + +Targeted management checks remain available when diagnosing a specific layer: + +```bash sudo -u vmail env VIBMAIL_ENV_FILE=/etc/vibmail/vibmail.env \ DJANGO_SETTINGS_MODULE=config.settings.production \ /opt/vibmail/venv/bin/python /opt/vibmail/app/manage.py verify_mailserver_schema @@ -29,7 +36,7 @@ sudo -u vmail env VIBMAIL_ENV_FILE=/etc/vibmail/vibmail.env \ ## Repair -After reviewing the cause of a partial or damaged installation, re-run the same source release with the original parameters and `--repair`. The installer reuses root-only generated secrets and does not create another administrator. +After reviewing the cause of a partial or damaged installation, re-run the same source release with the original parameters and `--repair`. The installer reuses root-only generated secrets, preserves a valid existing administrator and system mailboxes, and creates only missing bootstrap objects. Inconsistent bootstrap state fails closed. An existing administrator password is never changed automatically. ## Backup and restore ```bash diff --git a/docs/PUBLISHING.md b/docs/PUBLISHING.md index efd6cec..e163221 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.0-rc.1 + python scripts/build_release.py --root . --version 1.3.0-rc.4 python scripts/verify_release.py \ - dist/mailstack-1.3.0-rc.1-source.zip \ - --checksum dist/mailstack-1.3.0-rc.1-source.zip.sha256 + dist/mailstack-1.3.0-rc.4-source.zip \ + --checksum dist/mailstack-1.3.0-rc.4-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 "Release MailStack 1.3.0-rc.1" +git commit -m "Release MailStack 1.3.0-rc.4" git branch -M main git remote add origin git push -u origin main @@ -36,14 +36,14 @@ git push -u origin main Create an annotated release-candidate tag only after CI passes: ```bash -git tag -a v1.3.0-rc.1 -m "MailStack 1.3.0 RC1" -git push origin v1.3.0-rc.1 +git tag -a v1.3.0-rc.4 -m "MailStack 1.3.0 RC4" +git push origin v1.3.0-rc.4 ``` Attach these files to the forge release: -- `mailstack-1.3.0-rc.1-source.zip` -- `mailstack-1.3.0-rc.1-source.zip.sha256` +- `mailstack-1.3.0-rc.4-source.zip` +- `mailstack-1.3.0-rc.4-source.zip.sha256` - `docs/RELEASE_NOTES_1.3.0.md` - `docs/FORENSIC_AUDIT_REPORT.md` diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 15fb371..e68e8fe 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -24,12 +24,20 @@ The automated installer targets Ubuntu Server 24.04 LTS. It deliberately refuses ## 4. Install +For an SSH/PuTTY session, create a resilient terminal first: + +```bash +tmux new -s mailstack-install +``` + +Run the installer inside it: + ```bash sudo ./install.sh --domain example.com --admin-email admin@example.com \ --server-ip 203.0.113.10 --non-interactive ``` -The initial administrator credential is written to a root-only file. Sign in, store the password in an approved password manager, change it immediately, then securely delete the file. +The initial administrator credential is written to a root-only file immediately after the account is created. Sign in, store the password in an approved password manager, change it immediately, then securely delete the file. A reviewed `--repair` preserves an already-valid administrator instead of resetting its password. ## 5. Verify external delivery diff --git a/docs/RELEASE_NOTES_1.3.0.md b/docs/RELEASE_NOTES_1.3.0.md index f49fd99..97ac9c4 100644 --- a/docs/RELEASE_NOTES_1.3.0.md +++ b/docs/RELEASE_NOTES_1.3.0.md @@ -1,51 +1,92 @@ -# MailStack 1.3.0 RC1 release notes +# MailStack 1.3.0 RC4 release notes ## Purpose -Version 1.3.0 RC1 converts the verified private deployment source into a configurable, publicly documented, reproducibly packaged open-source release candidate while preserving the approved v1.2.1 mailbox application behavior. - -## Added - -- Ubuntu 24.04 one-command installer -- configurable mail, application, mail-server, and public hostnames -- MariaDB/Postfix/Dovecot/Nginx/systemd/environment templates -- public-site rendering for the configured domain -- complete AGPL-3.0 license text, licensing rationale and public governance/security documents -- Django 5.2.16 LTS security maintenance pin with exact-version deployment and CI verification -- GitHub CI, release workflow, Dependabot, CODEOWNERS, issue and pull-request templates -- source secret scanning, documentation validation, forensic file/symbol inventory, template validation and installer/operations contract tests -- SEO-oriented repository metadata, reusable project logo, deterministic source ZIP, manifest, checksum and verifier -- custom-domain migration and tests -- consistent configurable backup/restore tooling - -## Hardened - -- strict production setting validation -- SQL identifier validation and least-privilege column grants -- Postfix invoker-view privileges -- LMTP-only Dovecot service identity -- hostname collision and installer argument validation -- HSTS policy consistency -- backup consistency, archive validation, and service-state restoration -- release scanner self-protection and generated-artifact rejection +Version 1.3.0 RC4 is a cross-platform audit-tooling maintenance successor to RC3. It preserves the +frozen 1.3.0 RC1 product baseline, all approved RC2 installation/recovery/inbound-delivery fixes, and +the RC3 `sqlparse` 0.6.0 security update. RC4 changes only repository-level Bash runtime discovery +used by installer, operations, and forensic contract tooling so Windows validation does not blindly +invoke a broken WSL `bash.exe` launcher when Git Bash is available. Existing MailStack product +features, UI/UX, routes, data model, receive-only scope, runtime dependencies, and deployment +identifiers remain preserved. + +## Local Windows audit fix in RC4 + +A Windows CMD qualification run showed documentation, design, UI-foundation, inventory, and template +gates passing, while installer/operations shell checks failed before any MailStack shell code ran. +The `bash` command resolved to the Windows WSL launcher, which failed to attach Docker Desktop's WSL2 +`ext4.vhdx` with `E_ACCESSDENIED`. The forensic audit then repeated the same environment failure for +every shell syntax check, creating 16 cascading findings from one unusable Bash runtime. + +RC4 introduces verified Bash discovery for repository tooling. On Windows it prefers Git for Windows +Bash, supports an explicit `BASH_EXECUTABLE` override, probes candidates before use, and passes +repository-relative POSIX script paths. Linux/GitHub Actions continue to use the normal system Bash. +If no usable Bash exists, the tools fail once with an actionable runtime diagnostic instead of +misclassifying WSL startup failure as multiple MailStack shell syntax defects. + +## Security fix carried from RC3 + +GitHub Actions run `32053931714` passed the source forensic audit, documentation, design, inventory, +deployment-template, installer, and operations gates, then stopped at the blocking online dependency +audit. `pip-audit` reported four vulnerabilities in `sqlparse==0.5.5`: CVE-2026-71491, +CVE-2026-59894, CVE-2026-59893, and CVE-2026-54284, with 0.6.0 as the fixed version. + +RC3 introduced, and RC4 preserves, the existing transitive runtime pin to `sqlparse==0.6.0` in both the production lock and +constraints. Django remains 5.2.16 and declares `sqlparse>=0.3.1`; sqlparse 0.6.0 requires Python +3.10+, while MailStack remains fixed to Python 3.12. No new dependency is introduced. + +## Fixed in RC2 + +- The installer no longer changes the host-wide `/var/log` mode; it validates the dedicated MailStack + log path from the `vmail` runtime instead. +- Installer-launched Django commands now run from a clean least-privilege environment instead of + inheriting stale database/Django variables from the parent shell. +- `/run/vibmail/mailbox-provision-locks` is prepared before bootstrap mailbox creation. +- Reviewed `--repair` can preserve a valid existing initial administrator and valid system mailboxes + while creating only missing bootstrap objects; inconsistent partial state fails closed. +- Newly created initial-administrator credentials are persisted immediately to the root-only + credential file, before later TLS/mail-stack phases can fail. +- Dovecot's static LMTP userdb includes `allow_all_users=yes`; Postfix SQL virtual-mailbox lookup + remains the authoritative recipient gate, so unknown recipients are still rejected. +- One-shot `ingest_maildir --dry-run` verification no longer takes the live worker's exclusive lock + and no longer updates `ServiceHeartbeat`, allowing the official application verifier to run beside + the live ingestion service. +- Conservative Django MariaDB uniqueness warnings are qualified only in production against the + existing `utf8mb4_unicode_ci` database and unique-column deployment contract; no migration or + schema change is introduced. +- SSH/PuTTY installation guidance now recommends `tmux`/`screen`, with a non-blocking warning when a + mutating installer is launched over SSH outside a resilient terminal. ## Compatibility -- Existing `vibmail.my` defaults, application data model, URLs, templates, static assets, tests, migration history, and legacy maintenance assets are retained. -- No functional baseline source file was deleted. -- Existing v1.2 backup sets without contact-state archives remain restorable by the updated restore script. +- No database migration. +- One existing transitive dependency is security-upgraded: `sqlparse` 0.5.5 → 0.6.0; no new dependency is introduced. +- No route, template, CSS, JavaScript, UI page, authorization, mailbox, message, public-site, or + contact-workflow redesign. +- No SMTP submission, IMAP, POP3, reply, forward, sent, draft, or public-registration feature. +- Existing `VIBMAIL_*` settings, `vibmail-*` services, `/etc/vibmail` paths, databases, Maildir + layout, and receive-only architecture remain unchanged. +- Strict duplicate rejection remains the default for bootstrap management commands; preservation is + available only through the explicit repair option. -## Release qualification +## Staging evidence that motivated RC2 -Repository CI, the online dependency audit, deterministic release build, release verification and clean-clone verification pass at commit `1e1737edea2e6c922265a15d8584b56671820c65`. The release remains an **RC** until a complete clean Ubuntu 24.04 live installation, inbound SMTP/LMTP, backup/restore and operational acceptance campaign passes. +The live campaign reproduced and isolated failures in `/var/log` traversal, inherited installer +credentials, mailbox provisioning runtime locks, partial bootstrap recovery, delayed initial +credential persistence, Dovecot static-userdb LMTP lookup, and live verification locking. After the +LMTP correction, deferred Gmail messages were accepted by Dovecot, saved to INBOX, removed from the +Postfix queue, ingested, and displayed by the application. + +## Release qualification -## Documentation baseline +RC4 must pass the full documentation, installer, operations, template, Ruff, Bandit, Django, +coverage, forensic, deterministic-release, and CI gates. Clean Ubuntu 24.04 installation and real +external SMTP/LMTP acceptance are required. Stable promotion remains blocked until backup/restore, +restart-recovery, final security/legal, and release-owner acceptance are complete. -MailStack also preserves the complete UI reference archive as `MAILSTACK-UI-DESIGN-INTAKE-001` and freezes -`MAILSTACK-UI-FOUNDATION-001`. The intake adds no runtime feature; it provides immutable design assets, -screen classification, future-scope boundaries, and automated integrity gates for later page-by-page redesign. +## RC1 foundation preserved -MailStack now includes a root `documents/` user-documentation hub with a complete user manual, -task-based how-to guide, administrator guide, protected baseline record, mandatory per-phase -records, deterministic index and manifest generation, documentation contract tests, and CI -change-policy enforcement. This framework does not change runtime application behavior. +RC1 established the configurable Ubuntu 24.04 installer, MariaDB/Postfix/Dovecot/Nginx/systemd +templates, reproducible source packaging, public governance/security documentation, Django 5.2.16 +security pin, protected user/documentation baseline, UI design intake, and shared application shell. +RC4 does not replace or redesign those foundations; it preserves the RC2 operational hardening and RC3 dependency-security fix while hardening only cross-platform repository audit execution. diff --git a/docs/RELEASE_PROCESS.md b/docs/RELEASE_PROCESS.md index 41fb961..e0c962e 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.0-rc.1`. +8. Build with `python scripts/build_release.py --root . --version 1.3.0-rc.4`. 9. Verify the ZIP and checksum with `python scripts/verify_release.py`. 10. Publish the source ZIP, checksum, release notes, user-document manifest, and exact Git commit/tag. diff --git a/docs/TEST_REPORT.md b/docs/TEST_REPORT.md index 15bf79e..618e3b4 100644 --- a/docs/TEST_REPORT.md +++ b/docs/TEST_REPORT.md @@ -1,8 +1,8 @@ -# Test report — MailStack 1.3.0 RC1 +# Test report — MailStack 1.3.0 RC3 -**Baseline verification date:** 2026-07-25 +**PHASE-003 verification date:** 2026-08-17 -## Application suite +## Last completed dependency-backed application suite (pre-RC3) ```text Collected: 196 @@ -13,6 +13,11 @@ Coverage: 94.99% Required coverage: 85% ``` +RC3 adds one focused deployment-security regression contract that requires both +`requirements/locked.txt` and `requirements/constraints.txt` to pin `sqlparse==0.6.0` and reject +the vulnerable 0.5.5 pin. The full RC3 collection/pass count is intentionally not claimed until the +dependency-backed GitHub Actions rerun completes. + Covered areas include authentication, user management, mailbox membership isolation, mailbox lifecycle, Postfix contract behavior, Maildir provisioning/ingestion, MIME parsing, HTML sanitization, attachment storage and authorization, duplicate handling, counters, live updates, reliability, security controls, responsive routes, deterministic user-document synchronization, draft blocking and feature-document policy enforcement. ## Additional automated gates @@ -44,16 +49,69 @@ Covered areas include authentication, user management, mailbox membership isolat | Shared UI foundation dependency-free contracts | PASS — 8 tests | | Shared shell Django functional tests | PASS — 7 focused tests | | Standalone contact-service Ruff/Bandit | Enforced by the final verifier, full forensic gate, and CI | -| Locked dependency advisory audit | PASS — no known vulnerabilities found | +| Locked dependency advisory audit | PENDING RC4 requalification — RC2 run `32053931714` found four sqlparse 0.5.5 vulnerabilities; RC3/RC4 pin 0.6.0 | | Canonical stored-entry release packaging and ZIP metadata verification | PASS | ## Runtime qualification -GitHub Actions run `30133728843` completed successfully on Ubuntu 24.04 with Python 3.12 for source safety, documentation, dependency audit, Ruff, Bandit, tests and coverage, contact tests, Django checks, shell syntax, the full forensic gate, deterministic release build and release verification. The qualifying source commit is `1e1737edea2e6c922265a15d8584b56671820c65`. +The last fully completed pre-PHASE-003 dependency-backed qualification remains GitHub Actions run +`30133728843` on Ubuntu 24.04 with Python 3.12 at commit +`1e1737edea2e6c922265a15d8584b56671820c65`. That historical evidence does not qualify RC4. + +For the PHASE-003 branch, GitHub Actions run `32053931714` at commit +`7a800eb9f0b6d0e3fa347f4d7f05b5ad88a5e18f` passed source safety, documentation, design, +forensic inventory, deployment-template, installer, and operations gates. It then failed at the +blocking dependency vulnerability audit, so Ruff, Bandit, Django tests/coverage, Django checks, +full forensic audit, and deterministic release steps were correctly skipped. ## Dependency advisory qualification -The network-enabled blocking `pip-audit` gate passed in GitHub Actions after Django was upgraded to 5.2.16. Dependency changes remain subject to `pip check`, the locked requirements contract and the blocking online advisory gate. +The failed RC2 `pip-audit` gate reported four vulnerabilities in `sqlparse==0.5.5`: +CVE-2026-71491, CVE-2026-59894, CVE-2026-59893, and CVE-2026-54284. Upstream sqlparse 0.6.0 is the +security release containing fixes for those issues. RC3 introduced and RC4 preserves `sqlparse==0.6.0` in both +`requirements/locked.txt` and `requirements/constraints.txt`. No advisory suppression is added. +RC4 must pass `pip check`, the network-enabled `pip-audit` gate, and every subsequent CI stage before +qualification. + +## RC4 Windows Bash-runtime qualification + +A Windows CMD run of the RC3 candidate passed documentation, managed-document, design, shared UI, +forensic inventory, deployment-template, and `git diff --check` gates. `test_installer.py`, +`test_operations.py`, and the shell portions of `forensic_audit.py` then failed before executing any +MailStack shell logic because the generic `bash` executable resolved to the WSL launcher. WSL failed +to attach Docker Desktop's `ext4.vhdx` with `E_ACCESSDENIED`. The forensic report's 16 findings were +therefore cascading manifestations of one local shell-runtime failure, not 16 independent source +defects. + +RC4 centralizes Bash discovery in `scripts/shell_runtime.py`. Windows prefers a probed Git for +Windows Bash runtime, supports `BASH_EXECUTABLE`, and falls back only to candidates that pass a +non-mutating startup probe. Installer plans and shell syntax checks use repository-relative POSIX +paths so the same contracts remain valid on Linux and Git Bash. A follow-up Windows qualification +showed that Git Bash correctly launched but did not expose a `python3` command even though the host +provided `python`. The RC4 harness now supplies a process-local `BASH_ENV` bridge that maps +installer-only `python3` calls to the exact Python interpreter running the test process. The bridge +is regression-tested and does not alter the production installer, Ubuntu's native `python3` contract, +or the developer machine. + +## PHASE-003 installation/recovery reliability qualification + +Dependency-free local qualification for 1.3.0-rc.4 passes the documentation index/manifest gate, +documentation contract tests, design manifest and PNG integrity tests, shared UI foundation +contracts, deployment-template rendering, installer contracts, operations contracts, Python compile, +shell syntax, forensic inventory generation/check, and the structural forensic audit. The installer +contract specifically protects global `/var/log`, sanitized `vmail` command execution, provisioning +runtime directories, early credential persistence, explicit repair idempotency, the Dovecot static +userdb LMTP setting, and the existing MariaDB collation qualification. + +The current artifact-building environment does not contain the repository's pinned Django/Ruff/Bandit +dependencies and cannot download them, so the dependency-backed Django/coverage/lint/full-forensic +gates are delegated to the mandatory Ubuntu 24.04 GitHub Actions run for the PHASE-003 commit. This +does not waive those gates. + +The live staging campaign that motivated PHASE-003 demonstrated real external Gmail delivery through +Postfix and Dovecot LMTP into Maildir, queue drain after the static-userdb correction, ingestion, and +web-inbox visibility. A final clean installation from the exact RC4 source remains a release-candidate +acceptance requirement before stable promotion. ## Manual acceptance still required diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 31af754..f14c6b0 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -10,11 +10,13 @@ Confirm public DNS resolves to the server, ports 80/443 are reachable, no confli ## External mail is not received -Check provider port-25 policy, firewall rules, MX records, Postfix logs and lookup results. Verify the mailbox is active and unknown recipients are rejected. +Check provider port-25 policy, firewall rules, MX records, Postfix logs and lookup results. Verify the mailbox is active and unknown recipients are rejected. If Postfix accepts the recipient but defers LMTP with `451 4.3.0` and Dovecot reports that a static userdb cannot verify user existence, verify the deployed Dovecot static userdb contains `allow_all_users=yes`; Postfix SQL lookup remains the recipient-validation boundary. + +If a message is already deferred after correcting the LMTP configuration, validate Dovecot first and then retry the queue with `postqueue -f`; do not delete queued mail as a first response. ## Mail reaches Maildir but not the web inbox -Check `vibmail-ingestion`, the configured Maildir path, filesystem ownership, ingestion lock and application logs. Run the mailbox-storage and schema verification commands. +Check `vibmail-ingestion`, the configured Maildir path, filesystem ownership, ingestion lock and application logs. Run the mailbox-storage and schema verification commands. In 1.3.0-rc.2 the official one-shot dry-run verifier can run while the live ingestion worker remains active; a real second ingestion worker must still be rejected by the exclusive lock. ## Login works but mailbox is empty @@ -29,3 +31,12 @@ Check `vibmail-public-contact`, local Postfix, the protected contact environment Do not bypass archive or checksum validation. Confirm the backup set is complete, the target paths are safe and the documented service-state procedure was followed. See `mailbox-app/docs/TROUBLESHOOTING.md` for application-specific checks. + + +## Installer fails during Django startup with `/var/log/vibmail` permission denied + +MailStack 1.3.0-rc.2 does not change the host-wide `/var/log` mode. Confirm `/var/log` is a real traversable system directory and `/var/log/vibmail` is owned by the `vmail` runtime with its expected restricted mode. Do not make `/var/log` globally restrictive as an application-specific hardening step. + +## Repair reports inconsistent bootstrap state + +Do not delete an existing administrator or mail-server row to force repair through. `--repair` intentionally preserves complete objects but rejects mixed application/mail-server/filesystem state for system mailboxes and rejects a requested administrator name that does not match an existing valid administrator. Inspect the partial state and restore from the installer backup or a verified application backup as appropriate. diff --git a/documents/ADMIN_GUIDE.md b/documents/ADMIN_GUIDE.md index d4c26d8..c10c57c 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.0-rc.1 -last_reviewed: 2026-07-25 +version: 1.3.0-rc.4 +last_reviewed: 2026-08-17 --- # MailStack administrator guide @@ -44,7 +44,8 @@ mailboxes. Message and mailbox deletion controls remain hidden unless policy all The administrator dashboard reports ingestion heartbeat, mail-storage accessibility, and database connectivity. Server-side verification should also use the maintained health and verification -scripts, systemd service status, and journal logs. After deployment, verify external delivery to +scripts, systemd service status, and journal logs. The one-shot dry-run verifier can run while the +live ingestion worker remains active and does not update its heartbeat. After deployment, verify external delivery to `postmaster@DOMAIN` and `abuse@DOMAIN`, Maildir creation, ingestion, browser visibility, and backup creation before onboarding users. @@ -62,4 +63,6 @@ attachments, database dumps, backups, certificates, private keys, or unsanitized Before production use, complete clean Ubuntu 24.04 VPS acceptance, TLS and DNS validation, real SMTP/LMTP delivery tests, backup/restore acceptance, and security review. Use the documented repair -or upgrade path rather than running the clean installer over an unreviewed existing mail stack. +or upgrade path rather than running the clean installer over an unreviewed existing mail stack. A +reviewed repair preserves an existing valid administrator and bootstrap mailboxes, creates only +missing bootstrap objects, and never silently resets an administrator password. diff --git a/documents/BASELINE.md b/documents/BASELINE.md index 9a6dd67..bf71019 100644 --- a/documents/BASELINE.md +++ b/documents/BASELINE.md @@ -4,8 +4,8 @@ title: MailStack Baseline document_type: baseline audience: maintainers-and-operators status: active -version: 1.3.0-rc.1 -last_reviewed: 2026-07-24 +version: 1.3.0-rc.4 +last_reviewed: 2026-08-17 baseline_id: MAILSTACK-1.3.0-RC1-DOCS-BASELINE-001 source_commit: 1e1737edea2e6c922265a15d8584b56671820c65 --- diff --git a/documents/DOCUMENTATION_MANIFEST.json b/documents/DOCUMENTATION_MANIFEST.json index b07fef3..3f0686d 100644 --- a/documents/DOCUMENTATION_MANIFEST.json +++ b/documents/DOCUMENTATION_MANIFEST.json @@ -6,192 +6,204 @@ "audience": "mailstack-administrators", "document_id": "admin-guide", "document_type": "admin-guide", - "last_reviewed": "2026-07-25", + "last_reviewed": "2026-08-17", "path": "documents/ADMIN_GUIDE.md", "phase_id": null, - "sha256": "c14f1b2a52ceef69ce32b32ff56cbb71d003a71244d454c9d2e52f77b8e0c69a", + "sha256": "dd61ab74c2b54cb6e637d5097c789a44c792feba592a591a5d1348a587625f4f", "status": "active", "title": "MailStack Administrator Guide", - "version": "1.3.0-rc.1" + "version": "1.3.0-rc.4" }, { "audience": "maintainers-and-operators", "document_id": "baseline", "document_type": "baseline", - "last_reviewed": "2026-07-24", + "last_reviewed": "2026-08-17", "path": "documents/BASELINE.md", "phase_id": null, - "sha256": "f17acc52e17a87a545bb38dbd171ee3462491506ceb6412b9a98e86257d7d1fe", + "sha256": "5a8adbcc64d6ea48c11dbdb04f238caa51b621475b8df3993e4138717195b236", "status": "active", "title": "MailStack Baseline", - "version": "1.3.0-rc.1" + "version": "1.3.0-rc.4" }, { "audience": "contributors-and-maintainers", "document_id": "documentation-policy", "document_type": "documentation-policy", - "last_reviewed": "2026-07-25", + "last_reviewed": "2026-08-17", "path": "documents/DOCUMENTATION_POLICY.md", "phase_id": null, - "sha256": "1d8eacd99557546403054eb139b7fe89e53c59d90d07db018b8bce6db9d735ae", + "sha256": "7786aef25b75254c38870b61329b9ca8ccdf367244ef8a97a0f13f692697879e", "status": "active", "title": "Documentation Policy", - "version": "1.3.0-rc.1" + "version": "1.3.0-rc.4" }, { "audience": "mailbox-users-and-administrators", "document_id": "how-to-use", "document_type": "how-to", - "last_reviewed": "2026-07-25", + "last_reviewed": "2026-08-17", "path": "documents/HOW_TO_USE.md", "phase_id": null, - "sha256": "1e83c10dfd06d22dd269141a2c0dfd8b82644981719ec4471bf6c50a20f22686", + "sha256": "0dd3e9d1d352ddb152180af3858afbec7f89b0aeb6b5a39881455e0ecd661655", "status": "active", "title": "How to Use MailStack", - "version": "1.3.0-rc.1" + "version": "1.3.0-rc.4" }, { "audience": "mailbox-users", "document_id": "user-manual", "document_type": "user-manual", - "last_reviewed": "2026-07-25", + "last_reviewed": "2026-08-17", "path": "documents/USER_MANUAL.md", "phase_id": null, - "sha256": "c52aee69746a25bb47e8bb8f9b8d5a01cff2230c8c4877c4a807126078f75ba7", + "sha256": "374ee5a683de0098b59dc39b25efdf6a04efdd7e33873517792160093f6fb6f2", "status": "active", "title": "MailStack User Manual", - "version": "1.3.0-rc.1" + "version": "1.3.0-rc.4" }, { "audience": "designers-developers-and-maintainers", "document_id": "ui-accessibility-specification", "document_type": "design-reference", - "last_reviewed": "2026-07-25", + "last_reviewed": "2026-08-17", "path": "documents/design/ACCESSIBILITY_SPECIFICATION.md", "phase_id": null, - "sha256": "fe3760b78db81e862744c46fe9dd42c8b323108224fa1bb7c35e2683f0aad38b", + "sha256": "8f31425f3cdec0165a4d0e6246ea68a770a898aed919ae01ac39da082326c11b", "status": "active", "title": "MailStack Accessibility Specification", - "version": "1.3.0-rc.1" + "version": "1.3.0-rc.4" }, { "audience": "designers-developers-and-maintainers", "document_id": "ui-component-matrix", "document_type": "design-reference", - "last_reviewed": "2026-07-25", + "last_reviewed": "2026-08-17", "path": "documents/design/COMPONENT_MATRIX.md", "phase_id": null, - "sha256": "9f0ce0a538eb632426ead8e2f306ea73725fe9a2bb4a5ba6b7e82c34a1265b2b", + "sha256": "b3c29ce563f1e794a22e60868dc47afd1efda6c20c27b56a05638e9288311b81", "status": "active", "title": "MailStack UI Component Matrix", - "version": "1.3.0-rc.1" + "version": "1.3.0-rc.4" }, { "audience": "designers-developers-and-maintainers", "document_id": "future-ui-roadmap", "document_type": "design-reference", - "last_reviewed": "2026-07-24", + "last_reviewed": "2026-08-17", "path": "documents/design/FUTURE_UI_ROADMAP.md", "phase_id": null, - "sha256": "364119f5df14e66347699e7bda9b22b964a55f4c37659dd06c1c0db0d6fde04b", + "sha256": "c08173fb7454572a326b2aeccb3706531113fbd4c635239242e283630bc8d2e5", "status": "active", "title": "MailStack Future UI Roadmap", - "version": "1.3.0-rc.1" + "version": "1.3.0-rc.4" }, { "audience": "designers-developers-and-maintainers", "document_id": "ui-implementation-status", "document_type": "design-reference", - "last_reviewed": "2026-07-25", + "last_reviewed": "2026-08-17", "path": "documents/design/IMPLEMENTATION_STATUS.md", "phase_id": null, - "sha256": "7095a9f650707c58641b6179e0837ff399edb66758addaa6ccbf1577f4d1d81a", + "sha256": "61ed02ae43021e603a2fdc2b9cf0b91414a213b2bec14c4917b8cf0da44b6e02", "status": "active", "title": "MailStack UI Implementation Status", - "version": "1.3.0-rc.1" + "version": "1.3.0-rc.4" }, { "audience": "designers-developers-and-maintainers", "document_id": "ui-responsive-specification", "document_type": "design-reference", - "last_reviewed": "2026-07-25", + "last_reviewed": "2026-08-17", "path": "documents/design/RESPONSIVE_SPECIFICATION.md", "phase_id": null, - "sha256": "1bc228db2d45c2e609d9dd10da836f7214519176f415f8b0f3692dc0b5023171", + "sha256": "fcc959dd904bece5f2b481b5f5e77ec8af349136c3d750b56bc41ac1547e0280", "status": "active", "title": "MailStack Responsive Specification", - "version": "1.3.0-rc.1" + "version": "1.3.0-rc.4" }, { "audience": "designers-developers-and-maintainers", "document_id": "ui-screen-catalog", "document_type": "design-reference", - "last_reviewed": "2026-07-24", + "last_reviewed": "2026-08-17", "path": "documents/design/SCREEN_CATALOG.md", "phase_id": null, - "sha256": "7822cd15dea8c3a64cdac2eaabe5c3ad4c5f9c9b9ca8cb42ddd24b2097c9748f", + "sha256": "36db5fa837255ef66f98edc25cfeb599d704a079071338739e5b14965584f9f9", "status": "active", "title": "MailStack UI Screen Catalog", - "version": "1.3.0-rc.1" + "version": "1.3.0-rc.4" }, { "audience": "designers-developers-and-maintainers", "document_id": "ui-foundation", "document_type": "design-reference", - "last_reviewed": "2026-07-25", + "last_reviewed": "2026-08-17", "path": "documents/design/UI_FOUNDATION.md", "phase_id": null, - "sha256": "b36905a2fe8f995b053347e2f8b3ceed3bd0d085ffd2670a591e63e8e9c30487", + "sha256": "0b3184ec7e1f9133f0f80360698d2abf04ecf9cc9df6cdfb58350aa07d77ecc2", "status": "active", "title": "MailStack UI Foundation", - "version": "1.3.0-rc.1" + "version": "1.3.0-rc.4" }, { "audience": "users-operators-and-maintainers", "document_id": "phase-000-baseline", "document_type": "phase", - "last_reviewed": "2026-07-24", + "last_reviewed": "2026-08-17", "path": "documents/phases/PHASE-000-BASELINE.md", "phase_id": "PHASE-000", - "sha256": "f6c1872bc32d81f11cf67a1f7bf619eebae7080603d3b7981211e2648dc064c0", + "sha256": "6970ddf63b8c2aa45f1d6f8c62244f25b4aaf09f00922acfa83947a5184e0129", "status": "active", "title": "Documentation and Feature Baseline", - "version": "1.3.0-rc.1" + "version": "1.3.0-rc.4" }, { "audience": "users-operators-designers-and-maintainers", "document_id": "phase-001-ui-design-intake-baseline", "document_type": "phase", - "last_reviewed": "2026-07-24", + "last_reviewed": "2026-08-17", "path": "documents/phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md", "phase_id": "PHASE-001", - "sha256": "f72355ea2266fff91bc4b053a1b11d308e23320664cdc3b2c604ad7e88695220", + "sha256": "204a4a66e2017081242d15b15d01481dddcde6cdcd269cda43f477e0d7ae0f01", "status": "active", "title": "UI Design Intake Baseline", - "version": "1.3.0-rc.1" + "version": "1.3.0-rc.4" }, { "audience": "users-operators-designers-and-maintainers", "document_id": "phase-002-shared-ui-foundation-and-application-shell", "document_type": "phase", - "last_reviewed": "2026-07-25", + "last_reviewed": "2026-08-17", "path": "documents/phases/PHASE-002-SHARED-UI-FOUNDATION-AND-APPLICATION-SHELL.md", "phase_id": "PHASE-002", - "sha256": "f4ef536520aaaa30d23b691102ca355a84475eaa23c9b29a8a415e349b3f6ee5", + "sha256": "450c51bf90048366e91615aecefb366247fa0b3a13a9eea7a1039a3ca3e4718d", "status": "active", "title": "Shared UI Foundation and Application Shell", - "version": "1.3.0-rc.1" + "version": "1.3.0-rc.4" + }, + { + "audience": "users-operators-and-maintainers", + "document_id": "phase-003-installation-recovery-and-inbound-delivery-reliability", + "document_type": "phase", + "last_reviewed": "2026-08-17", + "path": "documents/phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md", + "phase_id": "PHASE-003", + "sha256": "b2e0b9e88d6410d20e916ea420b1151fb9d014dc253177dc5a71b2274b47b822", + "status": "active", + "title": "Installation, Recovery and Inbound Delivery Reliability", + "version": "1.3.0-rc.4" } ], "project": "MailStack", - "release_version": "1.3.0-rc.1", + "release_version": "1.3.0-rc.4", "schema_version": 1, "scope": "Managed user, administrator, how-to, policy, baseline, and phase Markdown documents.", "summary": { - "active": 15, - "documents": 15, + "active": 16, + "documents": 16, "draft": 0, "historical": 0, - "phases": 3 + "phases": 4 } } diff --git a/documents/DOCUMENTATION_POLICY.md b/documents/DOCUMENTATION_POLICY.md index bed41a5..577ccf6 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.0-rc.1 -last_reviewed: 2026-07-25 +version: 1.3.0-rc.4 +last_reviewed: 2026-08-17 --- # MailStack documentation policy diff --git a/documents/HOW_TO_USE.md b/documents/HOW_TO_USE.md index 869281f..c7f8cee 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.0-rc.1 -last_reviewed: 2026-07-25 +version: 1.3.0-rc.4 +last_reviewed: 2026-08-17 --- # How to use MailStack diff --git a/documents/README.md b/documents/README.md index 52c4173..0e3747e 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-003 \ + --phase-id PHASE-004 \ --title "Feature title" \ --summary "What the phase changes for users" @@ -27,21 +27,22 @@ required documentation update. | Document | Type | Audience | Status | Version | |---|---|---|---|---| -| [MailStack Administrator Guide](ADMIN_GUIDE.md) | Admin Guide | mailstack-administrators | active | 1.3.0-rc.1 | -| [MailStack Baseline](BASELINE.md) | Baseline | maintainers-and-operators | active | 1.3.0-rc.1 | -| [MailStack Accessibility Specification](design/ACCESSIBILITY_SPECIFICATION.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.1 | -| [MailStack Future UI Roadmap](design/FUTURE_UI_ROADMAP.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.1 | -| [MailStack Responsive Specification](design/RESPONSIVE_SPECIFICATION.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.1 | -| [MailStack UI Component Matrix](design/COMPONENT_MATRIX.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.1 | -| [MailStack UI Foundation](design/UI_FOUNDATION.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.1 | -| [MailStack UI Implementation Status](design/IMPLEMENTATION_STATUS.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.1 | -| [MailStack UI Screen Catalog](design/SCREEN_CATALOG.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.1 | -| [Documentation Policy](DOCUMENTATION_POLICY.md) | Documentation Policy | contributors-and-maintainers | active | 1.3.0-rc.1 | -| [How to Use MailStack](HOW_TO_USE.md) | How To | mailbox-users-and-administrators | active | 1.3.0-rc.1 | -| [Documentation and Feature Baseline](phases/PHASE-000-BASELINE.md) | Phase | users-operators-and-maintainers | active | 1.3.0-rc.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.0-rc.1 | -| [UI Design Intake Baseline](phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md) | Phase | users-operators-designers-and-maintainers | active | 1.3.0-rc.1 | -| [MailStack User Manual](USER_MANUAL.md) | User Manual | mailbox-users | active | 1.3.0-rc.1 | +| [MailStack Administrator Guide](ADMIN_GUIDE.md) | Admin Guide | mailstack-administrators | active | 1.3.0-rc.4 | +| [MailStack Baseline](BASELINE.md) | Baseline | maintainers-and-operators | active | 1.3.0-rc.4 | +| [MailStack Accessibility Specification](design/ACCESSIBILITY_SPECIFICATION.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.4 | +| [MailStack Future UI Roadmap](design/FUTURE_UI_ROADMAP.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.4 | +| [MailStack Responsive Specification](design/RESPONSIVE_SPECIFICATION.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.4 | +| [MailStack UI Component Matrix](design/COMPONENT_MATRIX.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.4 | +| [MailStack UI Foundation](design/UI_FOUNDATION.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.4 | +| [MailStack UI Implementation Status](design/IMPLEMENTATION_STATUS.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.4 | +| [MailStack UI Screen Catalog](design/SCREEN_CATALOG.md) | Design Reference | designers-developers-and-maintainers | active | 1.3.0-rc.4 | +| [Documentation Policy](DOCUMENTATION_POLICY.md) | Documentation Policy | contributors-and-maintainers | active | 1.3.0-rc.4 | +| [How to Use MailStack](HOW_TO_USE.md) | How To | mailbox-users-and-administrators | active | 1.3.0-rc.4 | +| [Documentation and Feature Baseline](phases/PHASE-000-BASELINE.md) | Phase | users-operators-and-maintainers | active | 1.3.0-rc.4 | +| [Installation, Recovery and Inbound Delivery Reliability](phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md) | Phase | users-operators-and-maintainers | active | 1.3.0-rc.4 | +| [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.0-rc.4 | +| [UI Design Intake Baseline](phases/PHASE-001-UI-DESIGN-INTAKE-BASELINE.md) | Phase | users-operators-designers-and-maintainers | active | 1.3.0-rc.4 | +| [MailStack User Manual](USER_MANUAL.md) | User Manual | mailbox-users | active | 1.3.0-rc.4 | ## Scope boundary diff --git a/documents/USER_MANUAL.md b/documents/USER_MANUAL.md index 55c6f87..673f044 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.0-rc.1 -last_reviewed: 2026-07-25 +version: 1.3.0-rc.4 +last_reviewed: 2026-08-17 --- # MailStack user manual diff --git a/documents/design/ACCESSIBILITY_SPECIFICATION.md b/documents/design/ACCESSIBILITY_SPECIFICATION.md index 2e1cd3f..20ea3a6 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.0-rc.1 -last_reviewed: 2026-07-25 +version: 1.3.0-rc.4 +last_reviewed: 2026-08-17 --- # MailStack accessibility specification diff --git a/documents/design/COMPONENT_MATRIX.md b/documents/design/COMPONENT_MATRIX.md index 69079ac..4298da5 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.0-rc.1 -last_reviewed: 2026-07-25 +version: 1.3.0-rc.4 +last_reviewed: 2026-08-17 --- # MailStack UI component matrix diff --git a/documents/design/FUTURE_UI_ROADMAP.md b/documents/design/FUTURE_UI_ROADMAP.md index 71fa8b0..e0a83a7 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.0-rc.1 -last_reviewed: 2026-07-24 +version: 1.3.0-rc.4 +last_reviewed: 2026-08-17 --- # MailStack future UI roadmap diff --git a/documents/design/IMPLEMENTATION_STATUS.md b/documents/design/IMPLEMENTATION_STATUS.md index f6b8182..ae75755 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.0-rc.1 -last_reviewed: 2026-07-25 +version: 1.3.0-rc.4 +last_reviewed: 2026-08-17 --- # MailStack UI implementation status diff --git a/documents/design/RESPONSIVE_SPECIFICATION.md b/documents/design/RESPONSIVE_SPECIFICATION.md index 6a90c5c..c87b628 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.0-rc.1 -last_reviewed: 2026-07-25 +version: 1.3.0-rc.4 +last_reviewed: 2026-08-17 --- # MailStack responsive specification diff --git a/documents/design/SCREEN_CATALOG.md b/documents/design/SCREEN_CATALOG.md index d34fccc..f698e33 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.0-rc.1 -last_reviewed: 2026-07-24 +version: 1.3.0-rc.4 +last_reviewed: 2026-08-17 --- # MailStack UI screen catalog diff --git a/documents/design/UI_FOUNDATION.md b/documents/design/UI_FOUNDATION.md index 1b2f626..c5f31c0 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.0-rc.1 -last_reviewed: 2026-07-25 +version: 1.3.0-rc.4 +last_reviewed: 2026-08-17 --- # MailStack UI foundation diff --git a/documents/phases/PHASE-000-BASELINE.md b/documents/phases/PHASE-000-BASELINE.md index bedf7df..e790b28 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.0-rc.1 -last_reviewed: 2026-07-24 +version: 1.3.0-rc.4 +last_reviewed: 2026-08-17 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 8600ac2..411eecf 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.0-rc.1 -last_reviewed: 2026-07-24 +version: 1.3.0-rc.4 +last_reviewed: 2026-08-17 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 67f1e20..e7320b4 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.0-rc.1 -last_reviewed: 2026-07-25 +version: 1.3.0-rc.4 +last_reviewed: 2026-08-17 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 new file mode 100644 index 0000000..3e1428c --- /dev/null +++ b/documents/phases/PHASE-003-INSTALLATION-RECOVERY-AND-INBOUND-DELIVERY-RELIABILITY.md @@ -0,0 +1,86 @@ +--- +document_id: phase-003-installation-recovery-and-inbound-delivery-reliability +title: Installation, Recovery and Inbound Delivery Reliability +document_type: phase +audience: users-operators-and-maintainers +status: active +version: 1.3.0-rc.4 +last_reviewed: 2026-08-17 +phase_id: PHASE-003 +--- + +# PHASE-003: Installation, recovery and inbound delivery reliability + +## Objective + +Harden the existing Ubuntu 24.04 installation, partial-install recovery, official verification, and +Postfix-to-Dovecot inbound-delivery path using failures reproduced during the first live staging +acceptance campaign, without changing MailStack product features or UI behavior. + +## Scope + +This phase is restricted to the ten installation and setup issues reproduced during live staging: +global `/var/log` permission mutation, inherited shell-environment contamination, missing mailbox +provisioning runtime directories, non-idempotent partial-install bootstrap recovery, delayed initial +administrator credential persistence, Dovecot static-userdb LMTP lookup failure, the official +verification script conflicting with the live ingestion lock, SSH/PuTTY session-resilience guidance, +qualified MariaDB compatibility warnings, and source/staging hotfix drift. + +No application page, URL, permission model, mailbox/message workflow, public-site feature, outbound +mail capability, database migration, dependency, service name, or legacy runtime identifier is +added, removed, renamed, or redesigned. + +## User-facing changes + +There are no UI changes. Operators receive a safer clean/repair installer, a repair path that can +preserve already-valid bootstrap objects while creating missing ones, earlier root-only persistence +of newly created administrator credentials, reliable LMTP delivery for Postfix-validated recipients, +and an application verifier that can run beside the live ingestion worker. + +## How to use + +Run mutating installation or repair commands inside `tmux` or `screen` when connected through SSH. +Use the normal clean installer only on an unmarked target. After a reviewed interrupted or partial +installation, rerun the same source and parameters with `--repair`; valid existing administrator and +system-mailbox bootstrap objects are preserved and only missing objects are created. The installer +never silently resets an existing administrator password. + +After installation, run `/opt/vibmail/app/scripts/health_check.sh` followed by +`/opt/vibmail/app/scripts/verify_application.sh` while the normal ingestion service remains active. +Complete acceptance with a real external message to an active mailbox and confirm Postfix acceptance, +Dovecot LMTP delivery, Maildir persistence, ingestion, and browser visibility. + +## Compatibility + +All baseline application behavior remains unchanged. The Postfix SQL mailbox table remains the +authoritative recipient gate; Dovecot's static userdb now trusts recipients already validated by +Postfix and therefore does not introduce catch-all delivery. Existing administrator passwords are +preserved during repair. Existing strict duplicate behavior remains the default for management +commands; idempotent preservation is available only through the explicit repair option. + +No database migration is introduced. MariaDB continues to use the configured +`utf8mb4_unicode_ci` application-database collation, and existing unique columns remain unchanged. +The two conservative Django/MariaDB warnings qualified by that deployment contract are silenced only +in production settings. Rollback is the baseline `1.3.0-rc.1` source plus the previously documented +manual staging workarounds; existing data does not require migration or rollback transformation. + +## Verification + +Regression coverage verifies that the installer does not change global `/var/log` permissions, +least-privilege Django commands run from a sanitized environment, runtime provisioning directories +exist before bootstrap, repair preserves valid existing administrator/system mailboxes, initial +credentials are persisted immediately after administrator creation, the Dovecot template includes +the static-userdb LMTP fix, and MariaDB collation/warning qualification remains narrow. + +Django tests verify strict default bootstrap behavior, explicit `--if-missing` recovery, inconsistent +repair-state rejection, exclusive locking for real ingestion, concurrent one-shot dry-run verification, +and zero ServiceHeartbeat mutation during dry-run. Qualification also requires documentation, +template, installer, operations, Ruff, Bandit, Django, forensic, deterministic-release, and full CI +gates plus clean Ubuntu 24.04 external SMTP/LMTP acceptance before stable promotion. + +## Documentation impact + +Updated the root and application changelogs, README release status, installation, quick-start, +operations, troubleshooting, release-process/build/publishing references, security/citation metadata, +roadmap qualification notes, release notes, administrator guidance, versioned managed-document +metadata, generated documentation/design/forensic manifests, and this PHASE-003 record. diff --git a/install.sh b/install.sh index 8340093..81989e8 100755 --- a/install.sh +++ b/install.sh @@ -136,12 +136,33 @@ backup_file() { } run_as_vmail() { - runuser -u vmail --preserve-environment -- env \ + runuser -u vmail -- env -i \ + PATH=/opt/vibmail/venv/bin:/usr/local/bin:/usr/bin:/bin \ + HOME=/var/vmail \ + USER=vmail \ + LOGNAME=vmail \ VIBMAIL_ENV_FILE=/etc/vibmail/vibmail.env \ DJANGO_SETTINGS_MODULE=config.settings.production \ "$@" } +write_initial_credentials() { + local password=$1 + CREDENTIALS_FILE="/root/vibmail-initial-credentials-$STAMP.txt" + cat > "$CREDENTIALS_FILE" <"$LOCK_FILE" flock -n 9 || die "Another MailStack installation is running" touch "$LOG_FILE" @@ -376,6 +400,14 @@ install -d -o vmail -g vmail -m 0750 \ install -d -o vmail -g www-data -m 0755 /var/lib/vibmail/static install -d -o vmail -g vmail -m 0700 /var/lib/vibmail/attachments install -d -o vmail -g adm -m 0750 /var/log/vibmail +install -d -o vmail -g vmail -m 0750 /run/vibmail +install -d -o vmail -g vmail -m 0700 /run/vibmail/mailbox-provision-locks +runuser -u vmail -- test -x /var/log \ + || die "The vmail runtime cannot traverse /var/log; preserve the host directory and correct its parent permissions" +runuser -u vmail -- test -w /var/log/vibmail \ + || die "The vmail runtime cannot write to /var/log/vibmail" +runuser -u vmail -- test -w /run/vibmail/mailbox-provision-locks \ + || die "The vmail runtime cannot write to the mailbox provisioning lock directory" install -d -o root -g www-data -m 0755 /var/www/letsencrypt/.well-known/acme-challenge install -d -o root -g root -m 0755 /opt/vibmail-public-site/releases install -d -o root -g www-data -m 0755 "/var/www/$PUBLIC_HOSTNAME" @@ -478,23 +510,45 @@ find /var/lib/vibmail/static -type d -exec chmod 0755 {} + find /var/lib/vibmail/static -type f -exec chmod 0644 {} + run_as_vmail /opt/vibmail/venv/bin/python /opt/vibmail/app/manage.py check --deploy -if [[ $REPAIR -eq 0 ]]; then - if [[ -n "$ADMIN_PASSWORD_ENV" ]]; then - ADMIN_PASSWORD=${!ADMIN_PASSWORD_ENV:-} - [[ -n "$ADMIN_PASSWORD" ]] || die "Environment variable $ADMIN_PASSWORD_ENV is empty" - else - ADMIN_PASSWORD="Vm!$(random_hex 12)" - fi - export VIBMAIL_INITIAL_ADMIN_PASSWORD="$ADMIN_PASSWORD" +CREDENTIALS_FILE="not-created-existing-admin" +if [[ -n "$ADMIN_PASSWORD_ENV" ]]; then + ADMIN_PASSWORD=${!ADMIN_PASSWORD_ENV:-} + [[ -n "$ADMIN_PASSWORD" ]] || die "Environment variable $ADMIN_PASSWORD_ENV is empty" +else + ADMIN_PASSWORD="Vm!$(random_hex 12)" +fi + +ADMIN_COMMAND_ARGS=( + --username "$ADMIN_USERNAME" + --password-env VIBMAIL_INITIAL_ADMIN_PASSWORD +) +if [[ $REPAIR -eq 1 ]]; then + ADMIN_COMMAND_ARGS+=(--if-missing) +fi +ADMIN_COMMAND_OUTPUT=$( run_as_vmail env VIBMAIL_INITIAL_ADMIN_PASSWORD="$ADMIN_PASSWORD" \ /opt/vibmail/venv/bin/python /opt/vibmail/app/manage.py create_initial_admin \ - --username "$ADMIN_USERNAME" --password-env VIBMAIL_INITIAL_ADMIN_PASSWORD - unset VIBMAIL_INITIAL_ADMIN_PASSWORD - for system_mailbox in postmaster abuse; do - run_as_vmail /opt/vibmail/venv/bin/python /opt/vibmail/app/manage.py create_system_mailbox \ - "$system_mailbox" --confirm - done + "${ADMIN_COMMAND_ARGS[@]}" +) +printf '%s\n' "$ADMIN_COMMAND_OUTPUT" +if grep -Fxq 'INITIAL_ADMIN_STATUS=created' <<< "$ADMIN_COMMAND_OUTPUT"; then + write_initial_credentials "$ADMIN_PASSWORD" +elif grep -Fxq 'INITIAL_ADMIN_STATUS=preserved' <<< "$ADMIN_COMMAND_OUTPUT"; then + CREDENTIALS_FILE="not-created-existing-admin" +else + die "Initial administrator command did not report a recognized status" fi +unset ADMIN_PASSWORD + +for system_mailbox in postmaster abuse; do + SYSTEM_MAILBOX_ARGS=("$system_mailbox" --confirm) + if [[ $REPAIR -eq 1 ]]; then + SYSTEM_MAILBOX_ARGS+=(--if-missing) + fi + run_as_vmail /opt/vibmail/venv/bin/python /opt/vibmail/app/manage.py create_system_mailbox \ + "${SYSTEM_MAILBOX_ARGS[@]}" +done +run_as_vmail /opt/vibmail/venv/bin/python /opt/vibmail/app/manage.py verify_mail_storage CURRENT_PHASE="nginx-bootstrap" log "Installing bootstrap Nginx configuration" @@ -616,24 +670,6 @@ path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") path.chmod(0o600) PY -if [[ $REPAIR -eq 0 ]]; then - CREDENTIALS_FILE="/root/vibmail-initial-credentials-$STAMP.txt" - cat > "$CREDENTIALS_FILE" < if not INGESTION_LOCK_FILE.is_absolute(): raise ImproperlyConfigured("INGESTION_LOCK_FILE must be an absolute path") +# MariaDB uses the configured utf8mb4_unicode_ci collation for the application database. +# The underlying unique=True columns remain case-insensitive and the approved long unique +# columns fit within the supported InnoDB index width. Django cannot infer those deployment +# guarantees, so these two conservative backend warnings are qualified by installer/tests. +SILENCED_SYSTEM_CHECKS = [*SILENCED_SYSTEM_CHECKS, "mysql.W003", "models.W044"] # noqa: F405 + if not MAILSERVER_INTEGRATION_ENABLED: # noqa: F405 raise ImproperlyConfigured("MAILSERVER_INTEGRATION_ENABLED=true is required in production") diff --git a/mailbox-app/pyproject.toml b/mailbox-app/pyproject.toml index 61d8724..73ec3cd 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.0rc1" +version = "1.3.0rc4" description = "MailStack self-hosted receive-only mail server and shared team inbox" authors = [{ name = "Vib Tools" }] maintainers = [{ name = "Vib Tools" }] diff --git a/mailbox-app/requirements/constraints.txt b/mailbox-app/requirements/constraints.txt index b8da6f3..1fe0a5f 100644 --- a/mailbox-app/requirements/constraints.txt +++ b/mailbox-app/requirements/constraints.txt @@ -17,7 +17,7 @@ pytest-cov==7.0.0 pytest-django==4.12.0 python-dotenv==1.2.2 ruff==0.15.17 -sqlparse==0.5.5 +sqlparse==0.6.0 webencodings==0.5.1 whitenoise==6.11.0 diff --git a/mailbox-app/requirements/locked.txt b/mailbox-app/requirements/locked.txt index 9cd5655..83bf6b9 100644 --- a/mailbox-app/requirements/locked.txt +++ b/mailbox-app/requirements/locked.txt @@ -10,7 +10,7 @@ gunicorn==25.1.0 packaging==26.2 pycparser==3.0 python-dotenv==1.2.2 -sqlparse==0.5.5 +sqlparse==0.6.0 webencodings==0.5.1 whitenoise==6.11.0 diff --git a/mailbox-app/tests/integration/test_commands_health_postfix.py b/mailbox-app/tests/integration/test_commands_health_postfix.py index 2c213a1..289ddec 100644 --- a/mailbox-app/tests/integration/test_commands_health_postfix.py +++ b/mailbox-app/tests/integration/test_commands_health_postfix.py @@ -105,6 +105,13 @@ def test_create_initial_admin_from_environment(monkeypatch): password_env="VIBMAIL_TEST_ADMIN_PASSWORD", ) + preserved = StringIO() + call_command("create_initial_admin", username="owner", if_missing=True, stdout=preserved) + assert "INITIAL_ADMIN_STATUS=preserved" in preserved.getvalue() + assert get_user_model().objects.count() == 1 + with pytest.raises(CommandError, match="requested administrator|administrator .* is missing"): + call_command("create_initial_admin", username="second", if_missing=True) + @pytest.mark.django_db def test_create_system_mailbox_requires_confirmation(): @@ -117,6 +124,13 @@ def test_create_system_mailbox_requires_confirmation(): assert (maildir / "new").is_dir() call_command("verify_mail_storage") + preserved = StringIO() + call_command("create_system_mailbox", "postmaster", confirm=True, if_missing=True, stdout=preserved) + assert "SYSTEM_MAILBOX_STATUS=preserved" in preserved.getvalue() + (maildir / "cur").rmdir() + with pytest.raises(CommandError, match="incomplete mail storage"): + call_command("create_system_mailbox", "postmaster", confirm=True, if_missing=True) + @pytest.mark.django_db def test_create_system_mailbox_can_create_ui_reserved_address(): @@ -156,6 +170,12 @@ def test_ingestion_command_rejects_bad_interval_and_second_worker(settings): try: with pytest.raises(CommandError): call_command("ingest_maildir", once=True) + output = StringIO() + call_command("ingest_maildir", once=True, dry_run=True, stdout=output) + assert "errors=0" in output.getvalue() + from apps.core.models import ServiceHeartbeat + + assert ServiceHeartbeat.objects.filter(service_name="maildir_ingestion").count() == 0 finally: lock.release() diff --git a/mailbox-app/tests/security/test_deployment_assets.py b/mailbox-app/tests/security/test_deployment_assets.py index 0c65d5a..ff34256 100644 --- a/mailbox-app/tests/security/test_deployment_assets.py +++ b/mailbox-app/tests/security/test_deployment_assets.py @@ -122,6 +122,15 @@ def test_release_pins_and_verifies_django_security_patch(): assert '"Django": "5.2.16"' in verify +def test_sqlparse_security_release_is_pinned_consistently(): + locked = read("requirements/locked.txt") + constraints = read("requirements/constraints.txt") + + for source in (locked, constraints): + assert "sqlparse==0.6.0" in source + assert "sqlparse==0.5.5" not in source + + def test_release_source_cannot_be_the_live_application_tree(): deploy = read("scripts/deploy_application.sh") preflight = read("scripts/preflight_v1_2_1.sh") @@ -148,3 +157,15 @@ def test_v1_2_1_security_hotfix_pins_and_bleach_scope(): assert "parse_email=False" in parser assert "GHSA-g75f-g53v-794x" in audit assert "--ignore-vuln GHSA-g75f-g53v-794x" in audit + + +def test_production_mariadb_warning_qualification_is_narrow_and_documented(): + production = read("config/settings/production.py") + bootstrap = (ROOT.parent / "deployment/templates/mariadb/bootstrap.sql.tpl").read_text(encoding="utf-8") + assert '"mysql.W003"' in production + assert '"models.W044"' in production + assert "utf8mb4_unicode_ci" in bootstrap + mailbox_models = read("apps/mailboxes/models.py") + message_models = read("apps/messages/models.py") + assert "email_address = models.EmailField(max_length=320, unique=True)" in mailbox_models + assert "storage_relative_path = models.CharField(max_length=500, unique=True)" in message_models diff --git a/scripts/forensic_audit.py b/scripts/forensic_audit.py index 61eccf6..051350e 100644 --- a/scripts/forensic_audit.py +++ b/scripts/forensic_audit.py @@ -6,14 +6,16 @@ import ast import hashlib import ipaddress -import json import os +import json import re import subprocess import sys import tempfile from pathlib import Path +from shell_runtime import bash_environment, bash_script_command, bash_syntax_command, resolve_bash + REQUIRED = { "README.md", "VERSION", @@ -37,6 +39,7 @@ "docs/FORENSIC_FILE_INVENTORY.json", "scripts/test_installer.py", "scripts/test_operations.py", + "scripts/shell_runtime.py", "scripts/validate_templates.py", "docs/FEATURE_MATRIX.md", "docs/FORENSIC_AUDIT_REPORT.md", @@ -127,7 +130,6 @@ r"(? bool: return any(part in BLOCKED_DIRS or part in {"dist", "artifacts"} for part in relative.parts) -def run(command: list[str], cwd: Path) -> tuple[int, str]: - completed = subprocess.run(command, cwd=cwd, text=True, capture_output=True) +def run(command: list[str], cwd: Path, *, bash_runtime: bool = False) -> tuple[int, str]: + completed = subprocess.run( + command, + cwd=cwd, + text=True, + capture_output=True, + env=bash_environment() if bash_runtime else None, + ) return completed.returncode, (completed.stdout + completed.stderr).strip() @@ -244,7 +252,7 @@ def main() -> int: findings.append(f"JSON_SYNTAX:{relative}:{exc.lineno}") elif path.suffix == ".sh" or path.name == "install.sh": shell_count += 1 - code, output = run([BASH, "-n", str(path)], root) + code, output = run(bash_syntax_command(path, cwd=root), root, bash_runtime=True) if code: findings.append(f"SHELL_SYNTAX:{relative}:{output}") @@ -278,18 +286,25 @@ def main() -> int: if re.search(r"__[A-Z][A-Z0-9_]*__", rendered_text): findings.append(f"UNRESOLVED_PUBLIC_TEMPLATE_TOKEN:{rendered.relative_to(destination)}") - plan = [ - BASH, - str(root / "install.sh"), + try: + bash_runtime = resolve_bash() + except RuntimeError as exc: + findings.append(f"BASH_RUNTIME:{exc}") + bash_runtime = None + + plan = bash_script_command( + root / "install.sh", "--domain", "example.com", "--admin-email", "admin@example.com", "--server-ip", "203.0.113.10", "--non-interactive", "--plan", - ] - code, output = run(plan, root) - if code or "PLAN_VALIDATION=PASS" not in output: - findings.append(f"INSTALLER_PLAN:{output}") + cwd=root, + ) if bash_runtime else [] + if plan: + code, output = run(plan, root, bash_runtime=True) + if code or "PLAN_VALIDATION=PASS" not in output: + findings.append(f"INSTALLER_PLAN:{output}") for command, label in ( ([sys.executable, str(root / "scripts/manage_documents.py"), "--root", str(root), "check"], "USER_DOCUMENTATION_GATE"), @@ -365,6 +380,8 @@ def main() -> int: print(f"FILES_SCANNED={file_count}") print(f"PYTHON_FILES={python_count}") print(f"SHELL_FILES={shell_count}") + if bash_runtime: + print(f"BASH_RUNTIME={bash_runtime}") if findings: for finding in sorted(set(findings)): print(f"FINDING={finding}") diff --git a/scripts/shell_runtime.py b/scripts/shell_runtime.py new file mode 100644 index 0000000..4202bb7 --- /dev/null +++ b/scripts/shell_runtime.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +"""Portable Bash runtime discovery for MailStack audit and contract tooling.""" +from __future__ import annotations + +import atexit +import os +import shutil +import subprocess +import sys +import tempfile +from functools import lru_cache +from pathlib import Path + +_PROBE_MARKER = "MAILSTACK_BASH_OK" + + +def _add_candidate(candidates: list[str], value: str | os.PathLike[str] | None) -> None: + if not value: + return + text = os.fspath(value) + key = os.path.normcase(os.path.abspath(text)) if os.path.isabs(text) else os.path.normcase(text) + if all( + (os.path.normcase(os.path.abspath(item)) if os.path.isabs(item) else os.path.normcase(item)) != key + for item in candidates + ): + candidates.append(text) + + +def _windows_git_bash_candidates() -> list[str]: + candidates: list[str] = [] + git = shutil.which("git") + if git: + git_path = Path(git).resolve() + # Git for Windows normally exposes git.exe from /cmd or /bin. + roots = [git_path.parent.parent, git_path.parent] + for root in roots: + _add_candidate(candidates, root / "bin" / "bash.exe") + _add_candidate(candidates, root / "usr" / "bin" / "bash.exe") + + for env_name in ("ProgramFiles", "ProgramFiles(x86)", "LOCALAPPDATA"): + base = os.environ.get(env_name) + if not base: + continue + base_path = Path(base) + if env_name == "LOCALAPPDATA": + root = base_path / "Programs" / "Git" + else: + root = base_path / "Git" + _add_candidate(candidates, root / "bin" / "bash.exe") + _add_candidate(candidates, root / "usr" / "bin" / "bash.exe") + return candidates + + +def _candidate_bashes() -> list[str]: + candidates: list[str] = [] + _add_candidate(candidates, os.environ.get("BASH_EXECUTABLE")) + + if os.name == "nt": + # Prefer Git Bash over the Windows `bash.exe` WSL launcher. The latter may + # exist on PATH even when the WSL VM/Docker Desktop backing disk is broken. + for candidate in _windows_git_bash_candidates(): + _add_candidate(candidates, candidate) + + _add_candidate(candidates, shutil.which("bash")) + if os.name != "nt": + _add_candidate(candidates, "/bin/bash") + _add_candidate(candidates, "/usr/bin/bash") + return candidates + + +def _probe(candidate: str) -> tuple[bool, str]: + path = Path(candidate) + if os.path.isabs(candidate) and not path.is_file(): + return False, "not found" + try: + completed = subprocess.run( + [candidate, "--noprofile", "--norc", "-c", f"printf '{_PROBE_MARKER}\\n'"], + text=True, + capture_output=True, + check=False, + timeout=10, + ) + except (OSError, subprocess.TimeoutExpired) as exc: + return False, f"{type(exc).__name__}: {exc}" + if completed.returncode == 0 and _PROBE_MARKER in completed.stdout: + return True, "" + detail = (completed.stderr or completed.stdout or f"exit={completed.returncode}").strip() + return False, " ".join(detail.split())[:300] + + +@lru_cache(maxsize=1) +def resolve_bash() -> str: + """Return a verified Bash executable, preferring Git Bash on Windows.""" + failures: list[str] = [] + for candidate in _candidate_bashes(): + ok, detail = _probe(candidate) + if ok: + return candidate + failures.append(f"{candidate}: {detail}") + joined = "; ".join(failures) if failures else "no Bash candidates were found" + raise RuntimeError( + "No usable Bash runtime is available. Install Git for Windows/Git Bash or set " + f"BASH_EXECUTABLE to a working bash executable. Attempts: {joined}" + ) + + + +def _shell_single_quote(value: str) -> str: + return "'" + value.replace("'", "'\"'\"'") + "'" + + +def _bash_path(candidate: str, path: Path) -> str: + """Translate a host path for the selected Bash runtime when required.""" + if os.name != "nt": + return path.resolve().as_posix() + try: + completed = subprocess.run( + [candidate, "--noprofile", "--norc", "-c", 'cygpath -u "$1"', "mailstack", str(path)], + text=True, + capture_output=True, + check=False, + timeout=10, + ) + except (OSError, subprocess.TimeoutExpired) as exc: + raise RuntimeError(f"Unable to translate Windows path for Bash: {path}: {exc}") from exc + translated = completed.stdout.strip() + if completed.returncode or not translated: + detail = (completed.stderr or completed.stdout or f"exit={completed.returncode}").strip() + raise RuntimeError(f"Unable to translate Windows path for Bash: {path}: {detail}") + return translated + + +@lru_cache(maxsize=1) +def _python3_bridge_bash_env() -> str: + """Create a process-local BASH_ENV that maps python3 to this Python interpreter.""" + bash = resolve_bash() + python_path = _bash_path(bash, Path(sys.executable)) + temporary = Path(tempfile.mkdtemp(prefix="mailstack-python3-bridge-")) + atexit.register(shutil.rmtree, temporary, ignore_errors=True) + env_file = temporary / "bash_env" + env_file.write_text( + "python3() {\n" + f" {_shell_single_quote(python_path)} \"$@\"\n" + "}\n", + encoding="utf-8", + newline="\n", + ) + return _bash_path(bash, env_file) + + +def bash_environment(*, force_python3_bridge: bool = False) -> dict[str, str]: + """Return a deterministic environment for Bash-backed repository checks. + + Ubuntu production keeps its native ``python3`` contract. Windows local tests + receive a process-local BASH_ENV function that maps ``python3`` to the exact + interpreter running the audit harness, without modifying install.sh or the + developer machine. ``force_python3_bridge`` exists for platform-independent + contract testing of the bridge itself. + """ + environment = os.environ.copy() + if os.name == "nt" or force_python3_bridge: + environment["BASH_ENV"] = _python3_bridge_bash_env() + return environment + +def script_argument(path: Path, *, cwd: Path) -> str: + """Return a Bash-friendly script path relative to cwd when possible.""" + try: + relative = path.resolve().relative_to(cwd.resolve()) + except ValueError: + return path.as_posix() + value = relative.as_posix() + return value if value.startswith(".") else f"./{value}" + + +def bash_script_command(path: Path, *arguments: str, cwd: Path) -> list[str]: + return [ + resolve_bash(), + "--noprofile", + "--norc", + script_argument(path, cwd=cwd), + *arguments, + ] + + +def bash_syntax_command(path: Path, *, cwd: Path) -> list[str]: + return [ + resolve_bash(), + "--noprofile", + "--norc", + "-n", + script_argument(path, cwd=cwd), + ] diff --git a/scripts/test_installer.py b/scripts/test_installer.py index ee1f93e..05d4116 100644 --- a/scripts/test_installer.py +++ b/scripts/test_installer.py @@ -2,22 +2,23 @@ """Non-destructive contract tests for the root Ubuntu installer.""" from __future__ import annotations -import os import subprocess from pathlib import Path +from shell_runtime import bash_environment, bash_script_command, resolve_bash + ROOT = Path(__file__).resolve().parents[1] INSTALLER = ROOT / "install.sh" -BASH = os.getenv("BASH_EXECUTABLE", "bash") def run(*arguments: str) -> subprocess.CompletedProcess[str]: return subprocess.run( - [BASH, str(INSTALLER), *arguments], + bash_script_command(INSTALLER, *arguments, cwd=ROOT), cwd=ROOT, text=True, capture_output=True, check=False, + env=bash_environment(), ) @@ -27,6 +28,25 @@ def require(condition: bool, message: str) -> None: def main() -> int: + print(f"BASH_RUNTIME={resolve_bash()}") + bridge = subprocess.run( + [ + resolve_bash(), + "--noprofile", + "--norc", + "-c", + "python3 -c 'print(\"MAILSTACK_PYTHON3_BRIDGE_OK\")'", + ], + cwd=ROOT, + text=True, + capture_output=True, + check=False, + env=bash_environment(force_python3_bridge=True), + ) + require( + bridge.returncode == 0 and "MAILSTACK_PYTHON3_BRIDGE_OK" in bridge.stdout, + bridge.stdout + bridge.stderr or "python3 compatibility bridge failed", + ) valid = run( "--domain", "example.com", "--admin-email", "admin@example.com", @@ -92,6 +112,31 @@ def main() -> int: for fragment in required_fragments: require(fragment in installer_text, f"installer contract missing: {fragment}") require("pip install --upgrade" not in installer_text, "installer performs an unpinned pip upgrade") + require("install -d -m 0750 /var/log" not in installer_text, "installer mutates global /var/log mode") + require("runuser -u vmail -- env -i" in installer_text, "vmail subprocess environment is not sanitized") + require( + "/run/vibmail/mailbox-provision-locks" in installer_text, + "installer does not prepare the mailbox provisioning runtime lock path", + ) + require("--if-missing" in installer_text, "repair bootstrap does not use idempotent management commands") + require( + installer_text.index('write_initial_credentials "$ADMIN_PASSWORD"') + < installer_text.index('CURRENT_PHASE="nginx-bootstrap"'), + "initial credentials are not persisted immediately after administrator creation", + ) + require("tmux/screen" in installer_text, "SSH session resilience warning is missing") + dovecot_template = (ROOT / "deployment/templates/dovecot/99-vibmail.conf.tpl").read_text(encoding="utf-8") + require("allow_all_users=yes" in dovecot_template, "Dovecot static userdb LMTP fix is missing") + mariadb_template = (ROOT / "deployment/templates/mariadb/bootstrap.sql.tpl").read_text(encoding="utf-8") + require( + mariadb_template.count("CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci") >= 2, + "MariaDB case-insensitive utf8mb4 collation contract is missing", + ) + production_settings = (ROOT / "mailbox-app/config/settings/production.py").read_text(encoding="utf-8") + require( + '"mysql.W003"' in production_settings and '"models.W044"' in production_settings, + "qualified MariaDB compatibility warnings are not scoped in production settings", + ) require( installer_text.index('CURRENT_PHASE="acceptance-checks"') < installer_text.index("VIBMAIL_INSTALL=PASS"), diff --git a/scripts/test_operations.py b/scripts/test_operations.py index ba82d64..5fdb097 100644 --- a/scripts/test_operations.py +++ b/scripts/test_operations.py @@ -3,14 +3,14 @@ from __future__ import annotations import ipaddress -import os import re import subprocess from pathlib import Path +from shell_runtime import bash_syntax_command, resolve_bash + ROOT = Path(__file__).resolve().parents[1] APP_SCRIPTS = ROOT / "mailbox-app/scripts" -BASH = os.getenv("BASH_EXECUTABLE", "bash") def require(condition: bool, message: str) -> None: @@ -21,12 +21,18 @@ def require(condition: bool, message: str) -> None: def read(name: str) -> str: path = APP_SCRIPTS / name require(path.is_file(), f"missing operational script: {name}") - result = subprocess.run([BASH, "-n", str(path)], capture_output=True, text=True) + result = subprocess.run(bash_syntax_command(path, cwd=ROOT), capture_output=True, text=True) require(result.returncode == 0, result.stdout + result.stderr) return path.read_text(encoding="utf-8") def main() -> int: + print(f"BASH_RUNTIME={resolve_bash()}") + shell_runtime = (ROOT / "scripts/shell_runtime.py").read_text(encoding="utf-8") + require("_windows_git_bash_candidates" in shell_runtime, "Git Bash discovery contract is missing") + require("BASH_EXECUTABLE" in shell_runtime, "Bash runtime override contract is missing") + require("MAILSTACK_BASH_OK" in shell_runtime, "Bash runtime probe contract is missing") + backup = read("backup.sh") restore = read("restore.sh") health = read("health_check.sh") @@ -90,6 +96,14 @@ def main() -> int: require('-H "Host: $APP_HOSTNAME"' in health, "health check does not use configured app hostname") require('-H "Host: $APP_HOSTNAME"' in verify, "application verifier does not use configured app hostname") require("manage.py check --deploy" in verify, "application verifier omits deployment checks") + require( + "manage.py ingest_maildir --once --dry-run" in verify, + "application verifier omits non-mutating Maildir validation", + ) + require( + "systemctl stop vibmail-ingestion" not in verify, + "application verifier must not stop the live ingestion worker", + ) print("OPERATIONAL_SCRIPTS=4") print("BACKUP_RESTORE_CONTRACT=PASS")