Skip to content

Releases: JuliaKalder/TemplateWing

v2.7.1 — Bugfix: {IF recipient.*} respects template To on fresh compose

Choose a tag to compare

@github-actions github-actions released this 24 Jun 10:22

v2.7.0 was withdrawn due to a bug in conditional handling. v2.7.1 ships the full v2.7.0 feature set plus the bugfix below.

🐛 Bugfix v2.7.1

{IF recipient.domain==...} now also works when the template is inserted into a fresh "Write" mail (not a reply) and the recipient is supposed to come from the template's own to field. Previously the ELSE branch was always taken, because resolveRecipientVars read the (still empty) compose window before the template wrote its to address. Reply behavior is unchanged (Thunderbird pre-fills to on reply).

Concretely: if the compose window has no recipient yet and the template carries a to[0], that address is used as the default when evaluating {IF recipient.*}. Implemented in modules/template-insert.js via the new, tested helper applyTemplateRecipientFallback.


TemplateWing 2.7 is a "workshop" release: five core improvements around import workflow, visibility, and maintainability. All issues closed against the v2.7 milestone are included.

Highlights

📥 Per-row import preview (#212)

The import dialog is no longer a single global radio-button choice. Instead, every template shows up in a preview table with a status badge:

  • NEW — name does not yet exist.
  • DUPLICATE — name collides with an existing template.

The action can be picked per row: Skip / Add as copy / Replace existing / Edit name. Bulk buttons above the table cover the common mass decisions. "Add as copy" automatically appends (imported) or (imported 2), (imported 3), …, so repeated imports never trip over DUPLICATE_NAME. Inline hints warn per row about invalid recipients or oversized attachments.

API compatibility: importTemplates() still accepts the classic global mode string ("append" | "skip" | "replace"), and additionally a { default?, perRow? } object.

📊 Usage dashboard (#213)

New Usage tab on the options page. For the first time, the long-tracked usageCount and lastUsedAt become visible:

  • Sortable table: Name, Category, Accounts, Inserts, Last used, Avg. per week.
  • Top-10 bar chart as inline SVG (no bundle, no external library).
  • Filter "Unused for 30 / 90 / 365 days / Never used" — perfect for cleanup.
  • CSV export (RFC-4180-compliant: every field quoted, CRLF line endings) as templatewing-usage-YYYY-MM-DD.csv.

No additional permissions required — everything derived from existing data.

🔎 Template linter (#214)

Each template is automatically checked on display for things that could break:

  • unknown-variable{FOO} is not in the official variable set.
  • broken-include{{template:Name}} points nowhere.
  • cycle — the include graph has a loop (diamond includes are allowed).
  • oversize-attachment / oversize-total — attachment or total above the thresholds.
  • invalid-recipient — To/Cc/Bcc address fails format validation.

Visible as colored badges right next to the template name (red = error, yellow = warning) plus a summary line at the top of the list ("All templates clean" vs. "3 templates need attention: 2 errors, 5 warnings"). The list of allowed variables now lives centrally in modules/template-insert.js — adding a variable there automatically extends the linter.

🤖 Automated release (#215)

Tagging is enough. Pushing a v* tag to GitHub triggers .github/workflows/release.yml:

  1. Tag and manifest version are checked against each other (mismatch ⇒ hard fail with a clear message).
  2. Tests + locale lint + Prettier must be green.
  3. Portable Node builder (scripts/build-xpi.mjs) produces the XPI — same file list as locally, single source of truth.
  4. The XPI plus its SHA-256 is automatically attached to the GitHub Release.

Locally: npm run build:xpi. The old build-xpi.ps1 is now only a wrapper.

🧪 Playwright UI tests (#216)

First browser-based tests for UI flows that unit tests can't cover:

  • Popup: render, live search, Esc clears, pinned-first sorting.
  • Options: tab switch Templates ↔ Usage, defaults section, lint badges, editor open/cancel.

@playwright/test as devDependency, run via npm run test:ui. CI installs Chromium + the headless shell and runs unit and UI tests on every push/PR. The tests load the real HTML files via a tiny zero-dep static server against a stubbed messenger.* namespace.

Installation

Download templatewing-2.7.1.xpi → Thunderbird → Add-ons and Themes → ⚙ → Install Add-on From File…

Compatibility

  • Thunderbird 128 or newer.
  • Schema migrations from v2.6 (pinning) and earlier still run automatically.

Quality

  • 234 Node unit tests green (4 new in v2.7.1 for the recipient fallback, 46 new in v2.7 for linter, usage stats, importer decisions).
  • 10 Playwright UI tests green on Chromium.
  • Locale lint clean across all 7 languages (149 keys each).
  • DE fully translated; FR / ES / IT / NL / PT receive the new strings as English placeholders until community translations catch up.

Artifacts

  • File: templatewing-2.7.1.xpi
  • Size: 106,666 bytes (≈ 104 KB)
  • SHA-256: 95cf8eb7197c2317b9ba8a01b5c5cf9e87b1fb6212a3bb0372e548dfe62aa386

Full commit history: https://github.com/JuliaKalder/TemplateWing/commits/v2.7.1

v2.6.0 — Favorites, popup search, recipient variables, conditionals, defaults

Choose a tag to compare

@JuliaKalder JuliaKalder released this 23 Jun 22:33

TemplateWing 2.6 ships five new capabilities, all closed against the v2.6 milestone:

Highlights

  • ★ Favorites / pinning (#209) — Click the star on any template to keep it pinned to the top of the popup, regardless of recent-use sort. Schema migrates automatically (v1 → v2).
  • 🔎 Popup search box (#210) — Autofocused search filters by name, subject, and category in real time. Enter inserts the top match, Esc clears. Last query persists for the current browser session only.
  • 📨 Recipient-aware variables (#208){RECIPIENT_NAME}, {RECIPIENT_FIRSTNAME}, {RECIPIENT_EMAIL}, {REPLY_QUOTE}, {LAST_MESSAGE_SUBJECT}. Missing values resolve to empty strings, never undefined.
  • 🧠 Conditional + prompt variables (#207)
    • {IF lhs==\"value\"}…{ELSE}…{ENDIF} with nesting; dot-paths like recipient.domain, identity.email are supported.
    • {PROMPT:Label:default} and {CHOICE:Label:opt1|opt2|opt3} open a small dialog at insert time. Cancelling the dialog aborts the insert silently.
  • 📌 Default template per account (#211) — New section on the options page. Pick a template per identity; it auto-inserts when you open a new compose window with that account. Replies and forwards are skipped to avoid clobbering Thunderbird's auto-filled context. Deleting a template clears any default that pointed at it.

Installation

Download templatewing-2.6.0.xpi below → Thunderbird → Add-ons & Themes → gear icon → Install Add-on From File…

Compatibility

  • Thunderbird 128 or newer.
  • Migrates v1 templates in place — no user action required.

Quality

  • 184 unit tests pass (45 new) — variables, control flow, prompts, pinning, defaults, schema migration.
  • Locale-key lint clean across all 7 locales.
  • DE fully translated; FR/ES/IT/NL/PT new keys ship as English text pending community translation.

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

v2.5.2 — ATN review fix: synchronous runtime.onMessage listener

Choose a tag to compare

@JuliaKalder JuliaKalder released this 20 Jun 10:22

What's fixed

ATN reviewer feedback (Jess) on the 2.5.1 submission:

Asynchronous listener attached to runtime.onMessage. This is error-prone and must be avoided.
https://webextension-api.thunderbird.net/en/mv3/guides/runtimeMessaging.html

Root cause

The runtime.onMessage listener in background.js was declared async. An async function always returns a Promise — even for messages the listener does not handle (e.g. when the early if (!message || !message.action) return; guard fires). In the WebExtension messaging model, a Promise return value signals "I will send a response", so the listener incorrectly claimed it would respond to every message in the runtime, conflicting with any other listener that legitimately owns the message.

Fix

Rewrote the listener as a synchronous function that returns a Promise only from the one handler that actually performs async work (templatewing:insertTemplate). The handler body was extracted into a dedicated handleInsertTemplateFromPopup helper for readability. The templatesChanged branch is fire-and-forget and now returns undefined. Behaviour for senders is unchanged because neither caller (popup.js, options page) awaits a response.

The compose-script.js listener was already implemented this way (synchronous, returns Promise.resolve(...) only for owned actions) and needed no changes.

Verification

  • All 126 unit tests pass (no test changes needed — behaviour is identical at the call sites).
  • Locale lint clean.

Installation

Download templatewing-2.5.2.xpi below and install via Thunderbird → Add-ons → gear icon → Install Add-on From File, or wait for the automatic update from addons.thunderbird.net.

v2.5.1 — Fix: nested template save (issue #206)

Choose a tag to compare

@JuliaKalder JuliaKalder released this 18 Jun 20:36

What's fixed

Issue #206 — saving a new template in v2.5.0 silently broke in some cases.

Symptoms

  • Creating a second template that nests the first one: its content was not stored.
  • Re-opening the first template for edit: the editor opened empty, looking like the content was lost.

Root cause

saveTemplate() in modules/template-store.js built the new-template object as { id: generateId(), ..., ...template, ... } — the freshly-generated UUID came before the spread. options.js:handleSave() always passes { id: editingId, ... } where editingId === null for new templates, so the spread overwrote the generated UUID with null. New templates were persisted with id === null. Then openEditor(id)'s if (id) guard routed them through the new-template branch, so the editor opened empty and any further save just stacked another null-id template on top.

Fix

Strip the incoming id via destructure before the spread, so the generated id always wins regardless of the caller's id own-property. One-line change in the new-template branch; the update branch is untouched.

Regression coverage

Three new tests in tests/template-store.test.js:

  • id: null — production UI save shape from handleSave
  • id: undefined — post-destructure import shape
  • Parametrized [null, undefined, "", 0, false] — locks all falsy variants against a future "simplification" that replaces the destructure with a template.id === null-specific guard

Test count: 123 → 126. All pass. Lint clean.

Side benefits

resolveNestedTemplates in template-insert.js was incidentally relying on a non-null id for cycle detection in its visited Set and memo Map (and for the templatesById lookup). With each new template now carrying a unique id, those data structures key correctly. The behaviour change is strictly an improvement; no caller depended on the old collision-prone behaviour.

Review

Five parallel review agents per AGENTS.md Phase 3 all signed off: correctness, regression risk, call-shape coverage, code quality & conventions, storage lifecycle / concurrent surfaces. Investigation artifact: docs/issue-206-analysis.md in the repository.

Installation

Download templatewing-2.5.1.xpi below and install via Thunderbird → Add-ons → gear icon → Install Add-on From File, or wait for the automatic update from addons.thunderbird.net.

v2.5.0 — Security hardening & HTML source view

Choose a tag to compare

@JuliaKalder JuliaKalder released this 03 Jun 15:06

What's new

HTML source view toggle (issue #185)

The template editor gains a source-view toggle, letting you edit the raw HTML of a template body directly and switch back to the rich editor. Useful for pasting in markup or fine-tuning formatting the WYSIWYG editor can't reach.

Security & hardening

This release closes a broad set of injection and data-leak issues found in a security pass:

  • No more privileged API exposurewindow.__templateWingCompose no longer leaks the messenger API into page context (#173)
  • Injection-safe variable replacement — identity values are HTML-encoded in applyVariables, and event handlers are stripped from fragments before compose-DOM insertion (#183, #171)
  • Safe import & prefill — imported and prefilled email HTML is sanitized before storage; plaintext bodies are HTML-escaped in Save as Template (#169, #131, #152, #50)
  • No category-name leakage — the category dropdown is filtered by identity so restricted category names can't leak across identities (#179), and identity restrictions are now enforced through nested template resolution (#198)
  • Log hygiene — user-controlled values are escaped in console output (#175)
  • Supply-chain — GitHub Actions are pinned to immutable commit SHAs (#182); dependabot keeps actions/checkout and actions/setup-node current (#186, #187)

Reliability fixes

  • Template cache is no longer mutated externally — getTemplates returns a shallow copy and trackUsage builds fresh objects (#58, #73)
  • Duplicate template names are rejected at save time (#85)
  • Insert errors in the popup now surface as a system notification instead of alert() (#56)
  • Null guards for getSelection() and for missing template names during import (#188, #181)

Under the hood

A large internal cleanup with no behavior change: shared helpers extracted into modules/compose-utils.js, modules/message-utils.js, and modules/ui-helpers.js; nested-template, identity-resolution, import-merge, and context-menu grouping logic moved into testable units; INSERT_MODES constant replaces magic strings; full JSDoc on template-store.js; Prettier formatting with a CI check; and the test suite grows to 123 tests across 19 suites.

Installation

Download templatewing-2.5.0.xpi below and install via Thunderbird → Add-ons → gear icon → Install Add-on From File, or wait for the automatic update from addons.thunderbird.net.

v2.4.0 — v2.2 feature completion

Choose a tag to compare

@JuliaKalder JuliaKalder released this 10 May 10:25

What's new

Cross-surface template list sync (issue #22)

The options page now refreshes the template list automatically when templates are changed from another surface — inserting via the context menu, popup, or keyboard shortcut updates usage counts live. Only fires while the list view is visible; in-progress edits are never disturbed.

Schema versioning documentation (issue #23)

docs/schema-versioning.md documents the storage schema constants, the v1 template object shape, migration history, the migration runner flow, and a step-by-step guide for contributors adding future migrations.

Previously delivered in this cycle (issues #20, #21)

  • Variable picker — nine clickable chips in the editor insert variables at the cursor ({DATE}, {TIME}, {DATETIME}, {YEAR}, {WEEKDAY}, {SENDER_NAME}, {SENDER_EMAIL}, {ACCOUNT_NAME}, {ACCOUNT_EMAIL})
  • Paste sanitization — "Paste as plain text" toggle strips HTML formatting from clipboard paste
  • Toolbar active state — Bold / Italic / Underline buttons highlight when the cursor is inside formatted text
  • Duplicate-name warning — saving a template with an already-used name is blocked with an inline error

Installation

Download templatewing-2.4.0-uat.xpi below and install via Thunderbird → Add-ons → gear icon → Install Add-on From File, or wait for the automatic update from addons.thunderbird.net.

v2.3.9 — ATN validator compliance

Choose a tag to compare

@JuliaKalder JuliaKalder released this 17 Apr 20:25

Fix

ATN submission validator rejected v2.3.8 with:

Unsafe call to range.createContextualFragment for argument 0

`Range.createContextualFragment` is on the unsafe-call list even when the input is user-authored template content. Replaced with `DOMParser` + `document.importNode`, which produces the same `DocumentFragment` without tripping the validator.

Behaviorally identical to v2.3.8; this release is purely to pass store review.

Tests: 96/96. Lint clean.

v2.3.8 — composeScripts.register() runtime API

Choose a tag to compare

@JuliaKalder JuliaKalder released this 17 Apr 20:15

Fix

The v2.3.7 field log revealed the underlying problem: Thunderbird 128 was rejecting our `compose_scripts` manifest key with "Warning processing compose_scripts: An unexpected property was found in the WebExtension manifest." That's why declarative injection silently did nothing in every prior version — the manifest key was never accepted by TB's schema.

The supported MV2 path is the runtime API `messenger.composeScripts.register()`, used in the official `thunderbird/webext-examples` sample.

Changes

  • Removed the (rejected) `compose_scripts` entry from `manifest.json`.
  • Call `messenger.composeScripts.register()` at background boot; TB then auto-injects the script into every compose window that opens afterward.
  • Kept the boot-time `executeScript` backfill for pre-existing compose windows (register() does not retroactively inject).
  • Kept the pre-`sendMessage` `executeScript` as a final safety net, with logs promoted from `debug` to `log` / `warn` so the next field trace makes the actual path obvious.

Tests: 96/96. Lint clean.

v2.3.7 — Inject compose-script on tabs.onCreated

Choose a tag to compare

@JuliaKalder JuliaKalder released this 17 Apr 20:06

Fix

Diagnosed from a Browser Console log of the v2.3.6 field bug. The compose-script wasn't running in newly opened compose windows — Thunderbird 128's declarative `compose_scripts` entry silently failed to inject. `sendMessage` hit no listener; the smart-insert fallback ran but had no `moz-cite-prefix` / `moz-signature` to anchor on, so it appended.

Change

  • `background.js` hooks `tabs.onCreated` and explicitly injects `modules/compose-script.js` into every new `messageCompose` tab.
  • `template-insert.js` also re-injects just before `sendMessage` as a safety net.
  • The `window.__templateWingCompose` listener-swap guard keeps repeated injection idempotent.

Boot-time backfill for pre-existing compose windows is preserved.

Why not rely on the declarative entry

Kept in the manifest for compatibility, but it no longer carries the reliability burden. If it fires, fine; if it doesn't, the explicit injection always runs.

Tests: 96/96 pass. Lint clean.

v2.3.6 — Smart-insert fallback for cursor-mode

Choose a tag to compare

@JuliaKalder JuliaKalder released this 17 Apr 12:33

Fix

Defense-in-depth fix for issue #33. Cursor-mode template insertion now has three independent layers, any one of which is sufficient to land the template in a sensible location — so the "template appended after signature" symptom cannot recur even if the compose-script path fails entirely.

Layers

  1. Real caret insertion in the compose-script via Range API when a snapshot is available.
  2. Synthesized range in the compose-script (before moz-cite-prefix / moz-signature) when no caret snapshot exists yet.
  3. Smart-insert fallback in template-insert.js (smartInsertHtml / smartInsertPlaintext) when the compose-script message round-trip fails — anchors above cite/signature in the raw body string.

Earlier releases relied on appending to the body as the ultimate fallback, which is the exact symptom users reported. v2.3.6 removes that failure mode.

Review

Passed all five review angles: correctness, regression risk, cross-mode (HTML + plaintext + reply/forward), messaging/lifecycle, code quality.

Tests: 96/96 pass. Lint clean across all 7 locales.