Files
jcoffey-dev 136fc2540b
ci / fork-checks (pull_request) Skipped
ci / build (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 7m45s
Sieve playground: the server's interpreter in the browser
tools/sieve-playground builds the vendored sieve-rs 0.7.3 (vnd.inbuxa.*
names included) to WebAssembly, with a static page around it, for inbuxa
Admin's Debug button on a Sieve script: until now that button sent the
script to an outside site whose interpreter knows only upstream's
extension names, so any script that requires vnd.inbuxa.* failed to
compile there.

Ported from the playground in the upstream sieve-rs repository's web/
directory (commit 549a2d57f7), which targets sieve-rs 1.x:

- run.rs and handler.rs drive 0.7's event loop the way the server's
  delivery does, carrying on after a runtime error; llm_prompt compiles
  and answers false, as on a server without a model.
- The expression functions are the server's own modules, included by
  path; tests fail if the list, ApplyString or the few functions copied
  from misc.rs/unicode.rs stop matching crates/common.
- Built for wasm32-wasip1 rather than with wasm-bindgen: sieve-rs and
  mail-builder read the clock, which panics on wasm32-unknown-unknown.
  The page's worker answers the six WASI calls, giving the module the
  playground's current-time setting. shim/gethostname stands in for a
  crate with no WASI build. Nothing in vendor/ or crates/ changes.
- The page carries no upstream names or links; storage keys and the
  samples are renamed to match.

build.sh --admin <dir> stages the result into an inbuxa-admin checkout.
2026-10-09 01:13:53 -07:00

261 lines
13 KiB
HTML

<!DOCTYPE html>
<!--
SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
SPDX-FileCopyrightText: 2026 Coffey Labs LLC
SPDX-License-Identifier: AGPL-3.0-only
Modified by Coffey Labs in 2026 for INBUXA.
-->
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sieve playground · inbuxa Admin</title>
<meta name="description"
content="Write, test and share Sieve email filters in your browser, with the Sieve interpreter of the inbuxa server." />
<meta name="theme-color" content="#1b1d24" />
<link rel="icon" href="./favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="./style.css" />
<link rel="stylesheet" data-name="vs/editor/editor.main" href="./vendor/monaco-0.52.2/vs/editor/editor.main.css" />
</head>
<body>
<header class="topbar">
<a class="brand" href="./" aria-label="Sieve playground">
<svg class="brand-mark" viewBox="0 0 32 32" aria-hidden="true">
<path d="M5 7h22l-8 9v7l-6 3V16z" fill="currentColor" />
</svg>
<span class="brand-name">Sieve playground</span>
</a>
<span class="brand-by">inbuxa Admin</span>
<div class="toolbar">
<div class="menu" id="workspace-menu">
<button class="btn ghost menu-trigger" type="button" aria-haspopup="true" aria-expanded="false">
<span class="menu-label" id="workspace-name">Workspace</span>
<svg class="chevron" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 6l4 4 4-4" /></svg>
</button>
<div class="menu-panel" role="menu" hidden>
<div class="menu-section">Workspaces</div>
<div class="menu-list" id="workspace-list"></div>
<div class="menu-sep"></div>
<button type="button" role="menuitem" data-action="new-workspace">New workspace</button>
<button type="button" role="menuitem" data-action="duplicate-workspace">Duplicate</button>
<button type="button" role="menuitem" data-action="rename-workspace">Rename…</button>
<button type="button" role="menuitem" data-action="delete-workspace">Delete…</button>
<div class="menu-sep"></div>
<button type="button" role="menuitem" data-action="export-workspace">Export to file</button>
<button type="button" role="menuitem" data-action="import-workspace">Import from file…</button>
</div>
</div>
<div class="menu" id="examples-menu">
<button class="btn ghost menu-trigger" type="button" aria-haspopup="true" aria-expanded="false">
Examples
<svg class="chevron" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 6l4 4 4-4" /></svg>
</button>
<div class="menu-panel wide" role="menu" hidden>
<div class="menu-section">Open an example as a new workspace</div>
<div id="examples-list"></div>
</div>
</div>
<span class="toolbar-gap"></span>
<button class="btn ghost" type="button" id="share-button" title="Share this workspace">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M6 9l4-2M6 7l4 2M4.5 10a2 2 0 100-4 2 2 0 000 4zM11.5 6a2 2 0 100-4 2 2 0 000 4zM11.5 14a2 2 0 100-4 2 2 0 000 4z" /></svg>
<span class="hide-narrow">Share</span>
</button>
<button class="btn ghost" type="button" id="settings-button" title="Settings">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 10a2 2 0 100-4 2 2 0 000 4zM13 8a5 5 0 00-.1-1l1.4-1.1-1.4-2.4-1.7.6a5 5 0 00-1.7-1L9.2 1.3H6.8l-.3 1.8a5 5 0 00-1.7 1l-1.7-.6L1.7 5.9 3.1 7a5 5 0 000 2l-1.4 1.1 1.4 2.4 1.7-.6a5 5 0 001.7 1l.3 1.8h2.4l.3-1.8a5 5 0 001.7-1l1.7.6 1.4-2.4L12.9 9a5 5 0 00.1-1z" /></svg>
<span class="hide-narrow">Settings</span>
</button>
<button class="btn primary" type="button" id="run-button" title="Run (Ctrl+Enter)">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3l8 5-8 5z" /></svg>
Run
</button>
</div>
</header>
<nav class="mobile-tabs" aria-label="Panels">
<button type="button" data-panel="script" class="active">Script</button>
<button type="button" data-panel="message">Message</button>
<button type="button" data-panel="result">Result</button>
</nav>
<main class="workbench" id="workbench">
<section class="panel active" data-panel="script" aria-label="Sieve script">
<div class="panel-head">
<span class="panel-title"><span class="step">1</span>Script</span>
<div class="tabs" id="script-tabs" role="tablist"></div>
<button class="icon-btn" type="button" id="add-script" title="Add an include script">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 3v10M3 8h10" /></svg>
</button>
</div>
<div class="editor-host" id="script-editor"></div>
<div class="problems" id="problems" data-state="idle"></div>
</section>
<div class="gutter" data-gutter="0" role="separator" aria-orientation="vertical" tabindex="0"></div>
<section class="panel" data-panel="message" aria-label="Test message">
<div class="panel-head">
<span class="panel-title"><span class="step">2</span>Message</span>
<div class="tabs" id="message-tabs" role="tablist"></div>
<button class="icon-btn" type="button" id="add-message" title="Add a test message">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 3v10M3 8h10" /></svg>
</button>
</div>
<div class="editor-host drop-target" id="message-editor">
<div class="drop-hint">Drop an .eml or .sieve file</div>
</div>
<details class="envelope" id="envelope">
<summary>
<span>Envelope</span>
<span class="envelope-summary" id="envelope-summary"></span>
</summary>
<div class="envelope-grid">
<label for="envelope-from">MAIL FROM</label>
<input id="envelope-from" type="text" spellcheck="false" autocomplete="off" />
<label for="envelope-to">RCPT TO</label>
<input id="envelope-to" type="text" spellcheck="false" autocomplete="off" placeholder="comma separated" />
</div>
</details>
</section>
<div class="gutter" data-gutter="1" role="separator" aria-orientation="vertical" tabindex="0"></div>
<section class="panel" data-panel="result" aria-label="Result">
<div class="panel-head">
<span class="panel-title"><span class="step">3</span>Result</span>
<div class="tabs" role="tablist" id="result-tabs">
<button type="button" role="tab" class="tab active" data-view="actions">Actions</button>
<button type="button" role="tab" class="tab" data-view="messages">Messages <span class="count" id="messages-count"></span></button>
<button type="button" role="tab" class="tab" data-view="variables">Variables</button>
</div>
</div>
<div class="result-body" id="result-body">
<div class="result-view active" data-view="actions" id="actions-view"></div>
<div class="result-view" data-view="messages" id="messages-view"></div>
<div class="result-view" data-view="variables" id="variables-view"></div>
</div>
</section>
</main>
<footer class="statusbar">
<span class="status-item" id="engine-status" data-state="loading">Loading engine…</span>
<span class="status-item" id="compile-status"></span>
<span class="status-item" id="run-stats"></span>
<button class="status-item status-link" type="button" id="privacy-link" title="How the playground handles your data">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4.5 7V5a3.5 3.5 0 017 0v2M3.5 7h9v6.5h-9z" /></svg>
<span class="hide-narrow">Runs in your browser</span>
</button>
<span class="status-spacer"></span>
<a class="status-item engine-link" href="https://git.coffeylabs.org/inbuxa/inbuxa-server/src/branch/main/tools/sieve-playground" target="_blank" rel="noopener" title="The server's Sieve interpreter, and this playground's source">sieve-rs <span id="engine-version"></span></a>
</footer>
<div class="drawer-backdrop" id="settings-backdrop" hidden></div>
<aside class="drawer" id="settings-drawer" aria-label="Settings" hidden>
<div class="drawer-head">
<h2>Settings</h2>
<span class="drawer-sub">Saved with this workspace</span>
<button class="icon-btn" type="button" data-close title="Close">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8" /></svg>
</button>
</div>
<div class="drawer-body" id="settings-body"></div>
<div class="drawer-foot">
<button class="btn ghost" type="button" id="settings-reset">Reset to defaults</button>
<button class="btn ghost" type="button" id="settings-json">Edit as JSON</button>
<span class="toolbar-gap"></span>
<button class="btn primary" type="button" data-close>Done</button>
</div>
</aside>
<dialog class="dialog" id="welcome-dialog" aria-labelledby="welcome-title">
<form method="dialog">
<h2 id="welcome-title">Sieve playground</h2>
<p class="dialog-text">
Write and test Sieve email filters against real messages, with the same Sieve interpreter your
inbuxa server runs.
</p>
<ul class="welcome-points">
<li>
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4.5 7V5a3.5 3.5 0 017 0v2M3.5 7h9v6.5h-9z" /></svg>
<span><b>Everything runs in your browser.</b> Scripts are compiled and executed locally with WebAssembly.</span>
</li>
<li>
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2.5 4.5h11v8h-11zM5 4.5V3h6v1.5" /></svg>
<span><b>Nothing is stored on a server.</b> Your scripts, messages and settings are saved only in this browser.</span>
</li>
<li>
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M6 9l4-2M6 7l4 2M4.5 10a2 2 0 100-4 2 2 0 000 4zM11.5 6a2 2 0 100-4 2 2 0 000 4zM11.5 14a2 2 0 100-4 2 2 0 000 4z" /></svg>
<span><b>Sharing is up to you.</b> A share link carries the workspace inside the link itself; nothing is uploaded.</span>
</li>
</ul>
<div class="dialog-foot">
<span class="toolbar-gap"></span>
<button class="btn primary" value="close" autofocus>Start filtering</button>
</div>
</form>
</dialog>
<dialog class="dialog" id="share-dialog">
<form method="dialog">
<h2>Share workspace</h2>
<p class="dialog-text">
The workspace is compressed into the link itself. Nothing is uploaded: whoever opens the link gets their own copy.
</p>
<label class="check">
<input type="checkbox" id="share-messages" checked />
Include test messages
</label>
<p class="dialog-warning" id="share-privacy">Messages may contain personal information. Check them before sharing.</p>
<div class="share-row">
<input id="share-url" type="text" readonly spellcheck="false" />
<button class="btn primary" type="button" id="share-copy">Copy link</button>
</div>
<p class="dialog-note" id="share-size"></p>
<div class="dialog-foot">
<button class="btn ghost" type="button" data-action="export-workspace">Export to file instead</button>
<span class="toolbar-gap"></span>
<button class="btn" value="close">Close</button>
</div>
</form>
</dialog>
<dialog class="dialog" id="prompt-dialog">
<form method="dialog">
<h2 id="prompt-title"></h2>
<p class="dialog-text" id="prompt-text"></p>
<input id="prompt-input" type="text" spellcheck="false" autocomplete="off" />
<div class="dialog-foot">
<span class="toolbar-gap"></span>
<button class="btn" value="cancel" formnovalidate>Cancel</button>
<button class="btn primary" value="ok" id="prompt-ok">OK</button>
</div>
</form>
</dialog>
<input type="file" id="file-input" accept=".json,.eml,.sieve,.siv,.txt,message/rfc822" hidden />
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
<script>
self.MonacoEnvironment = {
getWorkerUrl: () => new URL("./vendor/monaco-0.52.2/vs/base/worker/workerMain.js", location.href).href,
};
</script>
<script src="./vendor/monaco-0.52.2/vs/loader.js"></script>
<script>
window.monacoReady = new Promise((resolve, reject) => {
require.config({ paths: { vs: "./vendor/monaco-0.52.2/vs" } });
require(["vs/editor/editor.main"], () => resolve(window.monaco), reject);
});
</script>
<script type="module" src="./app.js" onerror="try { if (!sessionStorage.getItem('inbuxa-sieve.reloaded')) { sessionStorage.setItem('inbuxa-sieve.reloaded', '1'); location.reload(); } } catch (_) {}"></script>
</body>
</html>