#!/usr/bin/env python3 """Static/offline release checks for RustBox 0.2.15. These checks intentionally avoid pretending to replace `cargo test`: they validate configuration, SQL migrations, Docker examples, query-catalog parity and release metadata in environments where the Rust toolchain or external services are absent. """ from __future__ import annotations import base64 import pathlib import re import sqlite3 import subprocess import sys import tomllib try: import yaml except ImportError: # pragma: no cover - validation host dependency yaml = None ROOT = pathlib.Path(__file__).resolve().parents[1] errors: list[str] = [] checks: list[str] = [] def ok(message: str) -> None: checks.append(message) def require(condition: bool, message: str) -> None: if condition: ok(message) else: errors.append(message) def read(rel: str) -> str: return (ROOT / rel).read_text(encoding="utf-8") def extract_section(text: str, begin: str, end: str) -> str: require(begin in text and end in text, f"schema markers present: {begin} / {end}") if begin not in text or end not in text: return "" return text.split(begin, 1)[1].split(end, 1)[0] # Release/toolchain metadata. cargo = tomllib.loads(read("Cargo.toml")) require(cargo["package"]["version"] == "0.2.15", "Cargo package version is 0.2.15") require(cargo["package"]["rust-version"] == "1.99.1", "Cargo rust-version is 1.99.1") toolchain = tomllib.loads(read("rust-toolchain.toml")) require(toolchain["toolchain"]["channel"] == "1.99.1", "rust-toolchain pins Rust 1.99.1") require((ROOT / "Cargo.lock").is_file(), "Cargo.lock is retained for reproducible application builds") lock = tomllib.loads(read("Cargo.lock")) lock_rustbox = next((pkg for pkg in lock.get("package", []) if pkg.get("name") == "rustbox"), {}) require(lock_rustbox.get("version") == "0.2.15", "Cargo.lock RustBox package version is 0.2.15") require(cargo["dependencies"].get("encoding_rs") == "0.8", "direct charset decoder dependency is pinned") require("encoding_rs" in lock_rustbox.get("dependencies", []), "Cargo.lock records direct charset decoder dependency") require("cargo generate-lockfile" in read("scripts/build-release.sh"), "release script generates Cargo.lock when missing") release_script = read("scripts/build-release.sh") release_script_flat = " ".join(release_script.replace("\\", " ").split()) require("cargo test --locked" in release_script_flat, "release script requires the full Rust test suite") require("cargo clippy --locked --all-targets -- -D warnings" in release_script_flat, "release script requires warning-free Clippy") require("cargo build --release --locked" in release_script_flat, "release script requires a locked release build") package_script = read("scripts/package-release.sh") require("./scripts/build-release.sh" in package_script, "release packaging runs the full build gate before creating artifacts") require("make_zip.py" in package_script and "sha256sum" in package_script, "release packaging creates ZIP and SHA256 only after the build gate") require("cargo generate-lockfile" in read(".github/workflows/ci.yml"), "CI regenerates Cargo.lock before --locked checks") require("validate-release-static.py" in read("scripts/build-release.sh"), "release script runs offline/static release gate") require("validate-release-static.py" in read(".github/workflows/ci.yml"), "CI runs offline/static release gate") require(read("README.md").startswith("# RustBox 0.2.15"), "README identifies RustBox 0.2.15") require(read("RELEASE-MANIFEST.txt").startswith("RustBox 0.2.15"), "release manifest identifies RustBox 0.2.15") require(read("RELEASE-VALIDATION.txt").startswith("RustBox 0.2.15"), "release validation identifies RustBox 0.2.15") require(read("docs/IMPLEMENTATION_STATUS.md").startswith("# RustBox 0.2.15"), "implementation status identifies RustBox 0.2.15") bootstrap_js = read("src/web/js/00-bootstrap.js") bindings_js = read("src/web/js/95-bindings-boot.js") realtime_js = read("src/web/js/80-settings-realtime.js") mail_actions_js = read("src/web/js/60-mail-actions-search.js") auth_js = read("src/web/js/20-preferences-auth.js") reader_js = read("src/web/js/40-messages-reader.js") compose_js = read("src/web/js/50-compose.js") compose_css = read("src/web/css/40-compose.css") mail_css = read("src/web/css/30-mail.css") require("perPage: 300" in bootstrap_js and "messages_per_page: 300" in bootstrap_js and 'message_loading_mode: "pagination"' in bootstrap_js, "browser mailbox defaults to 300-message pagination") require(re.search(r"default_per_page\(\)\s*->\s*u32\s*\{\s*300\s*\}", read("src/routes/messages/types.rs")) is not None, "folder message API default page size is 300") require('1..=300' in read("src/routes/messages/mod.rs"), "folder message API accepts up to 300 messages per page") require('20..=300' in read("src/db/mail_preferences.rs") and '"pagination" | "progressive" | "all"' in read("src/db/mail_preferences.rs"), "mailbox preferences accept 300-message pages and the three loading modes") require('const appShortcut = keyboardIdle && !event.ctrlKey && !event.metaKey && !event.altKey;' in bindings_js, "single-key shortcuts preserve Ctrl/Cmd/Alt browser shortcuts") require('realtimeNotificationsReady' in realtime_js and 'suppressRealtimeNotificationsForFolder' in realtime_js, "realtime new-mail notification gating is present") require('realtimeFolderIsDrafts' in realtime_js and 'if (realtimeFolderIsDrafts(data?.folder)) return true;' in realtime_js, "realtime new-mail notifications are suppressed for Drafts folders") require('const draftsFolder = syncImap' in compose_js and 'suppressRealtimeNotificationsForFolder(draftsFolder)' in compose_js and 'releaseRealtimeSuppression();' in compose_js, "manual IMAP draft save suppresses its own realtime new-mail notification") require('suppressRealtimeNotificationsForFolder(notificationDestination)' in mail_actions_js, "bulk destination notifications are suppressed") require('finishAuthenticatedStartup({ showLoginToast = false } = {})' in auth_js and 'finishAuthenticatedStartup({ showLoginToast: true })' in auth_js and bindings_js.count('finishAuthenticatedStartup();') >= 1, "login success toast is limited to real login, not refresh/session restoration") require('${csp}' in reader_js, "message preview srcdoc explicitly declares UTF-8") require('function sanitizeMessagePreviewHtml(html)' in bootstrap_js and 'if (!node.hasAttribute("target")) node.setAttribute("target", "_blank");' in bootstrap_js and '!["_self", "_parent", "_top"].includes(target)' in bootstrap_js and 'rel.add("noopener");' in bootstrap_js and 'rel.add("noreferrer");' in bootstrap_js and 'sanitizeMessagePreviewHtml(detail.html)' in reader_js, "message preview links default to safe new-window navigation without overriding explicit targets") require('attachmentContentUrl(folder, uid, attachment)' in reader_js and 'frame.src = attachmentContentUrl(folder, uid, attachment);' in reader_js and 'URL.createObjectURL(blob)' in reader_js, "PDF preview uses authenticated inline URL while other attachment previews retain blob handling") require('function composeAttachmentInsertionRange' in compose_js and 'target.collapse(false);' in compose_js and 'range.insertNode(inserted);' in compose_js and 'document.execCommand("insertHTML"' not in compose_js, "compose attachment embedding uses non-destructive DOM Range insertion") require('position: fixed;' in compose_css and '.compose-image-actions' in compose_css and 'grid-template-rows: auto auto auto minmax(240px, 1fr) auto auto auto auto;' in compose_css, "inline image actions float without consuming a composer grid row") require('function beginInlineImageResize' in compose_js and 'data-image-resize="nw"' in read("src/web/index.html") and '.compose-image-resize-handle' in compose_css, "selected inline images expose pointer resize handles") require('application/x-rustbox-inline-image' in bindings_js and 'function moveInlineImageToRange' in compose_js, "inline images can be repositioned by drag and drop") require('clipboardData' in bindings_js and 'uploadInlineImage(images, range)' in bindings_js, "clipboard images are uploaded and embedded inline") require('.compose-attachment-kind' in compose_css and 'Image attachment' in compose_js and 'Inline image' in compose_js, "composer distinguishes embedded images from regular attachments") require("function mergeComposeAttachments" in compose_js and "await refreshComposeAttachmentsFromServer(id);" in compose_js and "attachments: mergeComposeAttachments(local.attachments, server.attachments)" in compose_js, "composer preserves multiple attachments and draft recovery merges authoritative server attachment metadata") require("const attachments = mergeComposeAttachments(state.composeAttachments);" in compose_js and "Inline link" in compose_js and "detachComposeAttachmentFromBody" in compose_js, "composer attachment strip keeps inline and regular attachments visible together") require('.message-pagination {' in mail_css and 'grid-template-columns: 30px auto 30px;' in mail_css and '.pagination-arrow {' in mail_css and '.load-more {' in mail_css and 'width: 100%;' in mail_css.split('.load-more {', 1)[1].split('}', 1)[0], "message pagination uses compact SVG arrows while progressive-load controls span the message list width") require('id="compose-fields-toggle"' in read("src/web/index.html") and 'function setComposeFieldsCollapsed' in compose_js and 'function toggleComposeFields' in compose_js and '.compose-dialog.compose-fields-collapsed .compose-addresses' in compose_css and 'setComposeFieldsCollapsed(false)' in bindings_js, "composer addressing fields can collapse to maximize editor space and reopen on invalid recipient input") require('id="compose-save-draft"' in read("src/web/index.html") and '$("#compose-save-draft").addEventListener("click", saveComposeDraftToImap);' in bindings_js and 'saveCurrentDraft(false, true, true)' in compose_js, "composer exposes an explicit manual IMAP draft save before cancel") require('rememberSignatureRange' in bindings_js and 'selectedSignatureRange' in bindings_js and 'if (!range || range.collapsed) return null;' in bindings_js and 'state.identitySignatureRange = null;' in bindings_js and '.signature-editor-toolbar button' in bindings_js and 'event.preventDefault();' in bindings_js and 'restoreEditorRange(range, signatureEditor);' in bindings_js, "signature formatting preserves real selections and refuses collapsed-caret commands that would enable sticky typing styles") index_html = read("src/web/index.html") require('
Mailbox storage' in index_html and 'RustBox__RUSTBOX_VERSION__' in index_html, "mailbox storage and footer labels stay protocol-neutral") require('
HTML signature / footer' in index_html and 'aria-labelledby="identity-signature-label"' in index_html and '