Skip to content

Commit 8935b81

Browse files
committed
chore: update version to 1.5.3
1 parent ec581ce commit 8935b81

15 files changed

Lines changed: 52 additions & 31 deletions

File tree

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
# Changelog
22

3+
## 1.5.3 (2026-04-28)
4+
5+
> **New:** Bulwark Webmail now sends an anonymous instance heartbeat once per day (version, platform, bucketed account counts, feature toggles — no message data, no PII). Disable any time from **Admin → Telemetry** or by setting `BULWARK_TELEMETRY=off`. See the [privacy notice](https://bulwarkmail.org/docs/legal/privacy/telemetry) for the full schema.
6+
7+
### Features
8+
9+
- **Telemetry**: Anonymous instance telemetry, on by default. Reports schema version, platform, bucketed account counts, and feature toggles only — disable from the admin UI, with `BULWARK_TELEMETRY=off`, or by clearing the endpoint
10+
- **Telemetry**: Track unique logins (HMAC'd per instance, 90-day retention) so the heartbeat can report bucketed account totals without storing usernames
11+
- **Plugins**: Theme API v2 with token compiler and skin slot
12+
- **Plugins**: Extension preview page and detailed extension info API
13+
- **Calendar**: Right-click context menu on empty calendar space
14+
- **Docker**: Persistent named volume for telemetry data so the instance id and admin's consent choice survive container upgrades
15+
16+
### Fixes
17+
18+
- **Security**: Block telemetry endpoint from pointing at internal/loopback hosts (validation + DNS-rebind re-check at fetch time)
19+
- **Security**: Harden plugin config, TOTP token exchange, and branding file serving
20+
- **Mail**: Batch shortcuts now act on the multi-selection when one is present (#228)
21+
322
## 1.5.2 (2026-04-27)
423

524
### Features

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ A modern, self-hosted webmail client for [Stalwart Mail Server](https://stalw.ar
1212

1313
[![License: AGPL v3](https://img.shields.io/badge/license-AGPL%20v3-blue.svg?logo=gnu&logoColor=white)](LICENSE)
1414
[![Discord](https://img.shields.io/discord/1482128142939455674?color=7289da&label=discord&logo=discord&logoColor=white)](https://discord.gg/tYCujymGrT)
15-
[![Version](https://img.shields.io/badge/version-1.5.2-green.svg?logo=git&logoColor=white)](CHANGELOG.md)
15+
[![Version](https://img.shields.io/badge/version-1.5.3-green.svg?logo=git&logoColor=white)](CHANGELOG.md)
1616
[![Docker](https://img.shields.io/badge/docker-ghcr.io%2Fbulwarkmail%2Fwebmail-blue?logo=docker&logoColor=white)](https://ghcr.io/bulwarkmail/webmail)
1717

1818
</div>
@@ -53,6 +53,8 @@ A modern, self-hosted webmail client for [Stalwart Mail Server](https://stalw.ar
5353
</tr>
5454
</table>
5555

56+
> **Anonymous telemetry is on by default** since 1.5.3. Each instance sends a daily heartbeat (version, platform, bucketed account counts, feature toggles — no message data, no PII). Disable from **Admin → Telemetry**, by setting `BULWARK_TELEMETRY=off`, or by clearing the endpoint. Full schema: [privacy notice](https://bulwarkmail.org/docs/legal/privacy/telemetry).
57+
5658
## Overview
5759

5860
Bulwark is a full webmail suite – not just an inbox. It bundles the four apps most self-hosters end up wanting on the same login:

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.5.2
1+
1.5.3

app/admin/telemetry/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export default function AdminTelemetryPage() {
137137
<div>
138138
<div className="font-medium">Status</div>
139139
<div className="text-sm text-muted-foreground">
140-
{status.consent === 'pending' && 'Initialising no heartbeats sent yet.'}
140+
{status.consent === 'pending' && 'Initialising - no heartbeats sent yet.'}
141141
{status.consent === 'on' && 'Heartbeats are enabled (default).'}
142142
{status.consent === 'off' && 'Heartbeats are off.'}
143143
{envOverridden && (

app/api/auth/totp-token-exchange/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export async function POST(request: NextRequest) {
8989
// Pin the upstream URL to the configured JMAP server so an unauthenticated
9090
// caller cannot point this route at internal hosts. Only when no server
9191
// URL is configured (and the deployment explicitly allows custom JMAP
92-
// endpoints) do we fall back to the user-supplied URL and even then
92+
// endpoints) do we fall back to the user-supplied URL - and even then
9393
// it must resolve to a public address.
9494
await configManager.ensureLoaded();
9595
const configuredServerUrl =

lib/plugin-storage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export const pluginStorage = {
8888
await deleteItem(STORE_THEMES, themeId);
8989
},
9090

91-
// Theme skin CSS separate store so it can be present/absent independently
91+
// Theme skin CSS - separate store so it can be present/absent independently
9292
// of the colour-token CSS (e.g. some v2 themes ship colours only).
9393
async saveThemeSkin(themeId: string, skin: string): Promise<void> {
9494
await putItem(STORE_THEME_SKINS, themeId, skin);

lib/plugin-types.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export type ThemeVariant = 'light' | 'dark';
1212
// ─── Manifests ───────────────────────────────────────────────
1313

1414
/**
15-
* Advanced theme fields ("Theme API v2"). All optional and additive a
15+
* Advanced theme fields ("Theme API v2"). All optional and additive - a
1616
* legacy theme that ships only `:root`/`.dark` CSS continues to work.
1717
*
1818
* When `apiVersion >= 2` (or any of `tokens`/`extends`/`derive`/`density`/
@@ -62,7 +62,7 @@ export interface ThemeManifest {
6262
apiVersion?: 1 | 2;
6363
/** Inherit tokens/CSS from another installed (or built-in) theme by id. */
6464
extends?: string;
65-
/** Structured colour tokens compiled into CSS at install time. */
65+
/** Structured colour tokens - compiled into CSS at install time. */
6666
tokens?: ThemeTokenSet;
6767
/** When true, missing standard tokens are derived (e.g. *-foreground from contrast). */
6868
derive?: boolean;
@@ -121,10 +121,10 @@ export interface InstalledTheme {
121121
author: string;
122122
description: string;
123123
preview?: string; // data: URI or blob URL
124-
css: string; // compiled CSS text what gets injected
124+
css: string; // compiled CSS text - what gets injected
125125
/**
126126
* Optional "skin" CSS shipped by Theme API v2 themes that need to restyle
127-
* actual UI components (toolbars, lists, buttons, etc.) not just colour
127+
* actual UI components (toolbars, lists, buttons, etc.) - not just colour
128128
* tokens. Injected into a separate `<style>` tag so it can be stripped
129129
* cleanly when the theme is deactivated. Stored in IndexedDB with the same
130130
* lifecycle as `css` to keep localStorage small.
@@ -598,7 +598,7 @@ export const MAX_PLUGIN_SIZE = 5 * 1024 * 1024; // 5 MB
598598
export const MAX_THEME_SIZE = 2 * 1024 * 1024; // 2 MB (was 1 MB; v2 themes may ship a skin.css)
599599
/**
600600
* Maximum size of an individual `skin.css` payload after extraction.
601-
* Skins are component-level CSS, not images anything bigger than this is
601+
* Skins are component-level CSS, not images - anything bigger than this is
602602
* almost certainly bundling assets the validator will refuse anyway.
603603
*/
604604
export const MAX_THEME_SKIN_BYTES = 256 * 1024; // 256 KB

lib/plugin-validator.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export interface ThemeExtractionResult extends ValidationResult {
2424
manifest: ThemeManifest | null;
2525
css: string;
2626
/**
27-
* Optional skin CSS component-level overrides extracted from `skin.css`.
27+
* Optional skin CSS - component-level overrides extracted from `skin.css`.
2828
* Only populated for Theme API v2 manifests; v1 themes ignore the file.
2929
*/
3030
skin: string | null;
@@ -225,7 +225,7 @@ export async function extractTheme(file: File): Promise<ThemeExtractionResult> {
225225
return { valid: false, errors, warnings, manifest: null, css: '', skin: null, preview: null };
226226
}
227227

228-
// Read theme.css required for v1 themes, optional when the manifest
228+
// Read theme.css - required for v1 themes, optional when the manifest
229229
// declares Theme API v2 fields (tokens/extends/derive/density/radii/typography),
230230
// since the compiler can produce CSS purely from the manifest.
231231
const cssFile = zip.file(root + 'theme.css');
@@ -279,13 +279,13 @@ export async function extractTheme(file: File): Promise<ThemeExtractionResult> {
279279
}
280280

281281
// Read skin.css if present (Theme API v2 only). Skins target real
282-
// component selectors and bypass the strict :root/.dark selector check
282+
// component selectors and bypass the strict :root/.dark selector check -
283283
// they still go through the dangerous-pattern sanitizer.
284284
let skin: string | null = null;
285285
const skinFile = zip.file(root + 'skin.css');
286286
if (skinFile) {
287287
if (!isAdvanced) {
288-
warnings.push('skin.css ignored only Theme API v2 manifests can ship a skin');
288+
warnings.push('skin.css ignored - only Theme API v2 manifests can ship a skin');
289289
} else {
290290
const rawSkin = await skinFile.async('string');
291291
if (rawSkin.length > MAX_THEME_SKIN_BYTES) {

lib/telemetry/endpoint-guard.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { isIP } from 'node:net';
66
// URL; without this an attacker with a session (or a hostile admin in a
77
// multi-tenant deploy) could redirect heartbeats at internal hosts.
88
//
9-
// Set BULWARK_TELEMETRY_ALLOW_PRIVATE=1 to bypass useful only for local
9+
// Set BULWARK_TELEMETRY_ALLOW_PRIVATE=1 to bypass - useful only for local
1010
// dev where the collector is on the loopback.
1111

1212
const PRIVATE_V4: RegExp[] = [
@@ -106,7 +106,7 @@ export async function resolveEndpointAllowed(raw: string): Promise<EndpointCheck
106106
}
107107
return { ok: true };
108108
} catch {
109-
// Don't block on transient DNS failures fetch will fail loudly anyway,
109+
// Don't block on transient DNS failures - fetch will fail loudly anyway,
110110
// and we don't want to lock admins out of their config when the resolver
111111
// is flaky. The literal-IP check above already covers the direct-attack
112112
// case.

lib/telemetry/state.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export async function getInstanceId(): Promise<string> {
4141
return fresh;
4242
}
4343

44-
// Default consent is 'on' telemetry is anonymous and enabled by default.
44+
// Default consent is 'on' - telemetry is anonymous and enabled by default.
4545
// Admins can disable via the UI, the BULWARK_TELEMETRY env var, or by clearing
4646
// the endpoint. See https://bulwarkmail.org/docs/legal/privacy/telemetry.
4747
const DEFAULTS: TelemetryStateFile = {

0 commit comments

Comments
 (0)