Drops the 0.15 REST management API and routes all account/auth/crypto/
principal operations through Stalwart 0.16's schema-driven JMAP
endpoint via a single passthrough (/api/account/stalwart/jmap).
- New client helper `stalwartJmap` + typed `requireResult`
- account-security-store rewritten against x:AccountPassword, x:AppPassword,
x:AccountSettings, x:Account (with currentSecret for TOTP ops)
- Client-side TOTP setup via `otpauth`; server-generated app password
secrets shown once on create
- Admin check switched to /api/account permissions
(sysAccountQuery/sysTenantQuery/sysSystemSettingsGet)
- Removed sieve vacation-overwrite workaround (fixed upstream #1251)
- Deleted old REST routes, StalwartClient, stale tests; added new
tests for passthrough + store
Delete static public/manifest.json (hardcoded "Bulwark Webmail") and mark
app/manifest.ts as force-dynamic so Next.js evaluates APP_NAME at request
time instead of build time, fixing the native browser install prompt.
Closes#207
Two issues prevented tasks created in Thunderbird (or other CalDAV
clients) from appearing in the task view:
1. percentComplete was not in CALENDAR_TASK_PROPERTIES, so it was
never requested from the server and the heuristic check for it
was always false (dead code).
2. The hasTaskFields heuristic used strict value checks:
- 'progress' in obj && typeof obj.progress === 'string'
→ fails when Stalwart returns progress: null instead of the
RFC 8984 default "needs-action"
- 'due' in obj && obj.due != null
→ fails when Stalwart includes due: null for tasks without a
DUE date (key present, value null)
RFC 8984 §5.2 defines due, progress and percentComplete as Task-only
properties — a VEVENT will never include them in a JMAP response.
Checking for key presence alone (even when null) is therefore a
reliable discriminator, regardless of the actual value.
sendImipInvitation() was fully implemented but never called after
createEvent or updateEvent — only sendImipCancellation was wired up
(in deleteEvent). This meant that even when the "send invitation"
checkbox was checked and participants were correctly saved on the
server, no invitation email was dispatched to attendees.
Apply the same pattern already used by deleteEvent: after a successful
create/update, if sendSchedulingMessages is true and the event has
participants, call sendImipInvitation() in a best-effort try/catch so
that email failures do not roll back the calendar operation.
For createEvent, the raw server response (created) is used directly
since it is already available and matches the CalendarEvent type
expected by sendImipInvitation.
For updateEvent, the updated event is reconstructed by merging the
existing store event with the incoming patch, avoiding an extra API
round-trip.
When an email contains a calendar invitation, the raw .ics MIME parts
(text/calendar, application/ics, application/icalendar) were showing
up in the attachment list alongside the calendar invitation banner,
which is confusing — the banner already provides the relevant UI.
Filter those MIME types out of the displayed attachment list whenever
the calendar invitation banner is active, reusing the existing
isCalendarMimeType utility from lib/calendar-invitation.ts.
Three issues addressed in sendImipReply, sendImipInvitation and
sendImipCancellation:
1. Line folding (RFC 5545 §3.1)
Add foldIcsLine() helper that wraps iCalendar content lines at
74 characters, inserting CRLF + SPACE as required by the spec.
Previously, long lines (e.g. ATTENDEE with a full CN and mailto
URI) could exceed the 75-octet limit and cause strict parsers to
silently reject the ICS.
2. MIME wrapper type (RFC 6047 §3 + CalConnect iMIP Best Practices)
Change bodyStructure from multipart/alternative to multipart/mixed.
The CalConnect interoperability guide recommends multipart/mixed as
the outer wrapper for messages carrying a text/calendar part; many
clients skip iTIP processing when they see multipart/alternative.
3. Calendar part metadata
Add charset=UTF-8 to the text/calendar Content-Type, disposition
inline, and a descriptive filename (reply.ics / invite.ics /
cancel.ics) to each outgoing calendar MIME part.
Note: Gmail-to-Gmail events are handled by Google's internal scheduling
API and cannot be updated via iMIP regardless of MIME structure. This
fix improves interoperability with Outlook, Thunderbird, Fastmail and
standard CalDAV servers.
Previously isCalendarMimeType was a module-private function in
lib/calendar-invitation.ts. Exporting it allows the email viewer
to reuse the same MIME type detection logic when filtering out
calendar attachments, avoiding duplication of the type set.
Reorganize the Branding section with subsections (App identity, Icons &
favicon, PWA appearance, Logos, Login page) and document the new variables
APP_SHORT_NAME, APP_DESCRIPTION, PWA_ICON_URL, PWA_THEME_COLOR and
PWA_BACKGROUND_COLOR.
- Add app/manifest.ts to serve /manifest.webmanifest dynamically at runtime
- Name, short_name, description, theme_color and background_color are read
from env vars (APP_NAME, APP_SHORT_NAME, APP_DESCRIPTION, PWA_THEME_COLOR,
PWA_BACKGROUND_COLOR) with Bulwark defaults as fallback
- Add /api/pwa-icon/[size] route that auto-generates 192x192 and 512x512 PNG
icons from PWA_ICON_URL (or FAVICON_URL as fallback) using Sharp; results
are cached in memory
- Remove static manifest: '/manifest.json' from layout metadata; Next.js
injects the link automatically from app/manifest.ts
- Fix pre-existing ESLint no-undef on RequestInit in browser-navigation.ts
When localePrefix is 'always' (or 'as-needed' with a non-default locale),
paths like /en/settings already have the locale in the URL. Running them
through the next-intl middleware a second time can trigger rewrite loops,
especially when combined with a proxy basePath where the middleware's
detection of the 'current' path conflicts with the rewritten one.
Skip the intl middleware in this case — the path is already in the
canonical locale-prefixed form and no further rewriting is needed.
This makes NEXT_PUBLIC_LOCALE_PREFIX=always reliable for sub-path
deployments.
Allow the next-intl localePrefix mode to be set via environment
variable at build time, defaulting to the existing 'never' behavior.
This is useful when proxying Bulwark under a sub-path (where
'never' can trigger rewrite loops) or when users prefer
URL-embedded locales (/en/settings vs /settings).
Usage:
NEXT_PUBLIC_LOCALE_PREFIX=always npm run build
Accepted values: 'never' (default), 'always', 'as-needed'.