Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
f2e3b3a
feat: add Farsi (fa) locale support
hamedf62 Jun 23, 2026
bc29fc8
feat(i18n): complete Farsi (fa) translation - 2654 translated strings
hamedf62 Jun 23, 2026
b7e877b
feat: add RTL text direction support to email composer editor
hamedf62 Jun 24, 2026
70aaf0a
fix: stop unified-mailbox from mutating client-returned email objects
hildebrandttk Jun 24, 2026
d863b1f
fix: HTML-escape sender/subject in reply/forward quote header (#482)
hildebrandttk Jun 24, 2026
155d99a
feat: add plugin hooks for email details, headers, and source
rathlinus Jun 24, 2026
5e67671
Merge branch 'main' of https://github.com/bulwarkmail/webmail
rathlinus Jun 24, 2026
4cdc15f
chore: update version to 1.7.6
rathlinus Jun 24, 2026
512adab
feat: add privileged same-origin plugin tier + crypto API surface
rathlinus Jun 28, 2026
f8b8e0b
refactor: move S/MIME to generic crypto plugin hooks
rathlinus Jun 28, 2026
63e087f
fix: support MFA login via structured auth endpoint
rathlinus Jun 28, 2026
f90cd6a
fix: sync default identity (preferredPrimaryId) to server settings #507
rathlinus Jun 28, 2026
e0747c1
fix: send calendar invites by setting organizerCalendarAddress
rathlinus Jun 28, 2026
1f4afe0
fix: show all built-in themes in admin theme controls #496
rathlinus Jun 28, 2026
e141abc
feat: add option to hide total message count on folders (#498)
rathlinus Jun 28, 2026
d4c0666
i18n: add missing translation keys across 19 locales
rathlinus Jun 28, 2026
1f21a52
fix: hide server scheduled folder when virtual one is shown #495
rathlinus Jun 28, 2026
2f5b133
chore: update version to 1.7.6
rathlinus Jun 28, 2026
2704d5d
fix: clone source when setup.sh runs detached from a checkout #518
rathlinus Jun 28, 2026
16daf6e
fix(pro): show Edit button on draft emails opened in a new tab
krisatverbidio Jun 30, 2026
2a41e73
fix(pro): prompt to save or discard draft when closing compose tab vi…
krisatverbidio Jun 30, 2026
6c49427
fix(list): shift-click on the checkbox extends the selection (range)
shukiv Jun 30, 2026
b716f95
feat(compose): preselect identity of the active mailbox for new messages
Jun 30, 2026
9da27df
Update README.md
rathlinus Jun 30, 2026
95b5a81
fix(plugins): preserve settings slot and privileged tier
rowlc Jul 3, 2026
f291480
feat(email): send quick reply with Ctrl/Cmd+Enter
shukiv Jul 3, 2026
717b1d8
feat(headers): add parsing for Stalwart spam headers
harryyoud Jun 25, 2026
51c3a69
Fix: don't toggle mailbox subfolders on Arrow keys while typing
dealerweb Jul 2, 2026
c4a7f57
feat(accounts): pin default account on top + drag-to-reorder switcher
shukiv Jun 28, 2026
e9ac2de
Fix: notification sound preview - base-path prefix + longer default beep
dealerweb Jul 2, 2026
a099ab4
fix: route keyword writes to the email's own account in unified view
Jun 30, 2026
14c2807
feat: add user-selectable regional date format
honzup Jul 2, 2026
e05ab48
feat: add Slovak translation
pipozzz Jul 3, 2026
9aef8bc
i18n: localize the 'Return to list after delete or mark unread' setting
dealerweb Jul 2, 2026
396f5f7
fix(compose): wait for in-flight attachment uploads before sending
dev-hive-kazniisa Jul 3, 2026
97d87c4
fix(shortcuts): make keyboard shortcuts layout-agnostic
TheMelbine Jul 1, 2026
d259168
fix(shortcuts): also map /, ?, #, ! by physical position
TheMelbine Jul 1, 2026
4d6b4b5
Fix: brand push notifications with the configured PWA icon
dealerweb Jul 2, 2026
e6aa79e
Feature: recipient autocomplete from Sent, with on-demand server search
dealerweb Jul 2, 2026
babacca
fix(contacts): clear photo on server by sending media: null when removed
krisatverbidio Jun 29, 2026
d1da83a
fix: keep email list scrollable when bottom reading-pane is enabled w…
krisatverbidio Jun 30, 2026
1429a6f
feat(login): configurable logo size + hideable heading/subtitle
maartendra Jun 29, 2026
902774e
feat(list): click sender avatar to select message/thread (Thunderbird…
shukiv Jun 28, 2026
9930d19
fix: keep advanced search filters applied when switching folders #553
rathlinus Jul 4, 2026
b1b09d5
feat(pwa): add Apple Touch icons for iOS home screen
LinkPhoenix Jul 5, 2026
0d73cb5
fix(csp): allow external/data fonts so email webfonts render
shukiv Jun 30, 2026
6b74615
fix(email-viewer): hide images that fail to load
shukiv Jun 25, 2026
06ddda6
Fix: spam actions left folder counters and the open message stale
dealerweb Jul 6, 2026
d7a64fd
Fix: hide the spam action in Sent, Drafts and Scheduled
dealerweb Jul 6, 2026
d384c3b
Feature: pin emails to the top of the folder list
dealerweb Jul 6, 2026
a4dc0b7
i18n: restore key parity for the Slovak locale
dealerweb Jul 6, 2026
9110bc3
Fix: keep the session when the auth server is briefly unreachable
dealerweb Jul 6, 2026
db2c642
fix: storage quota not shown with Stalwart #577
rathlinus Jul 7, 2026
2928328
Merge branch 'main' of https://github.com/bulwarkmail/webmail
rathlinus Jul 7, 2026
c3a97de
fix: use calendarAddress/organizerCalendarAddress for scheduling, dro…
rathlinus Jul 7, 2026
e066698
fix: strike through cancelled events and mute their reminders #572
rathlinus Jul 7, 2026
7db6fd7
feat: add setting to disable tag-color row tint in message list
honzup Jul 3, 2026
b73d1b5
fix(email): render emails that put height:100% on a wrapper element
shukiv Jun 24, 2026
8647d70
fix: only commit recipient on Space when input is a valid email #571
rathlinus Jul 7, 2026
6470fa8
Merge branch 'main' of https://github.com/bulwarkmail/webmail
rathlinus Jul 7, 2026
c7d551f
fix: attachment reminder ignores quoted text on reply/forward #570
rathlinus Jul 7, 2026
8904d72
fix: hide Files when account lacks filenode capability #563
rathlinus Jul 7, 2026
22418c1
feat(i18n): Hebrew locale + full RTL support
shukiv Jul 7, 2026
128391b
Merge branch 'feature/rtl-editor-support' into upstream/main - resolv…
hamedf62 Jul 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
# Changelog

## 1.7.6 (2026-06-28)

### Breaking Changes

- **S/MIME**: The built-in S/MIME implementation has been removed from core and re-delivered through the new generic crypto plugin hooks (privileged same-origin plugin tier). S/MIME signing, encryption, decryption, certificate management, and the related settings UI now live in a plugin rather than the main app. Deployments that relied on built-in S/MIME must install the S/MIME crypto plugin to retain those features.

### Features

- **Plugins**: Privileged same-origin plugin tier with a crypto API surface
- **Plugins**: Plugin hooks for email details, headers, and source
- **Mail**: Option to hide the total message count on folders (#498)

### Fixes

- **Mail**: Hide the server scheduled folder when the virtual one is shown (#495)
- **Mail**: Stop the unified mailbox from mutating client-returned email objects
- **Composer**: HTML-escape sender and subject in the reply/forward quote header (#482)
- **Calendar**: Send calendar invites by setting `organizerCalendarAddress`
- **Identity**: Sync the default identity (`preferredPrimaryId`) to server settings (#507)
- **Auth**: Support MFA login via the structured auth endpoint
- **Admin**: Show all built-in themes in the admin theme controls (#496)
- **i18n**: Add missing translation keys across 19 locales

## 1.7.5 (2026-06-24)

### Features
Expand Down
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ A modern, self-hosted webmail client for [Stalwart Mail Server](https://stalw.ar

[![License: AGPL v3](https://img.shields.io/badge/license-AGPL%20v3-blue.svg?logo=gnu&logoColor=white)](LICENSE)
[![Discord](https://img.shields.io/discord/1482128142939455674?color=7289da&label=discord&logo=discord&logoColor=white)](https://discord.gg/tYCujymGrT)
[![Version](https://img.shields.io/badge/version-1.7.5-green.svg?logo=git&logoColor=white)](CHANGELOG.md)
[![Version](https://img.shields.io/badge/version-1.7.6-green.svg?logo=git&logoColor=white)](CHANGELOG.md)
[![Docker](https://img.shields.io/badge/docker-ghcr.io%2Fbulwarkmail%2Fwebmail-blue?logo=docker&logoColor=white)](https://ghcr.io/bulwarkmail/webmail)
[![Grafana](https://img.shields.io/badge/grafana-dashboard-orange?logo=grafana&logoColor=white)](https://grafana.external.bulwarkmail.org/)

</div>

---
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.7.5
1.7.6
10 changes: 5 additions & 5 deletions app/(main)/[locale]/calendar/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1149,13 +1149,13 @@ export default function CalendarPage() {
</div>
<div className="px-6 py-4 space-y-3">
<Button variant="outline" className="w-full justify-start h-auto py-3" onClick={handleImportWebcal}>
<span className="text-left">
<span className="text-start">
<span className="block font-medium">{tWebcalAction("import_title")}</span>
<span className="block text-xs text-muted-foreground mt-0.5">{tWebcalAction("import_description")}</span>
</span>
</Button>
<Button variant="outline" className="w-full justify-start h-auto py-3" onClick={handleSubscribeWebcal}>
<span className="text-left">
<span className="text-start">
<span className="block font-medium">{tWebcalAction("subscribe_title")}</span>
<span className="block text-xs text-muted-foreground mt-0.5">{tWebcalAction("subscribe_description")}</span>
</span>
Expand Down Expand Up @@ -1336,7 +1336,7 @@ export default function CalendarPage() {
<>
<div
className={cn(
"border-r border-border bg-secondary overflow-y-auto flex-shrink-0 p-3",
"border-e border-border bg-secondary overflow-y-auto flex-shrink-0 p-3",
!isResizing && "transition-[width] duration-300",
isNarrow && cn(
"absolute inset-y-0 left-0 z-50 w-72 pt-[env(safe-area-inset-top)]",
Expand Down Expand Up @@ -1475,7 +1475,7 @@ export default function CalendarPage() {

{/* Desktop event panel */}
{!isMobile && showEventModal && (
<div className="w-[400px] border-l border-border flex-shrink-0 overflow-hidden">
<div className="w-[400px] border-s border-border flex-shrink-0 overflow-hidden">
<EventModal
key={editEvent?.id ?? 'new'}
event={editEvent}
Expand All @@ -1498,7 +1498,7 @@ export default function CalendarPage() {

{/* Desktop task panel */}
{!isMobile && showTaskModal && (
<div className="w-[400px] border-l border-border flex-shrink-0 overflow-hidden">
<div className="w-[400px] border-s border-border flex-shrink-0 overflow-hidden">
<TaskModal
key={editTask?.id ?? 'new-task'}
task={editTask}
Expand Down
8 changes: 4 additions & 4 deletions app/(main)/[locale]/contacts/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -751,7 +751,7 @@ export default function ContactsPage() {
<button
key={group.id}
onClick={() => handleBulkAddToGroupConfirm(group.id)}
className="w-full flex items-center gap-3 px-6 py-3 text-left hover:bg-muted transition-colors"
className="w-full flex items-center gap-3 px-6 py-3 text-start hover:bg-muted transition-colors"
>
<div className="w-9 h-9 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
<Users className="w-4 h-4 text-primary" />
Expand Down Expand Up @@ -869,7 +869,7 @@ export default function ContactsPage() {
<>
<div
className={cn(
"border-r border-border flex flex-col flex-shrink-0 bg-background",
"border-e border-border flex flex-col flex-shrink-0 bg-background",
!isSidebarResizing && "transition-[width] duration-300",
isNarrow && cn(
"absolute inset-y-0 left-0 z-50 w-72 pt-[env(safe-area-inset-top)]",
Expand Down Expand Up @@ -937,7 +937,7 @@ export default function ContactsPage() {
<div
data-tour="contacts-list"
className={cn(
"border-r border-border bg-background flex flex-col flex-shrink-0",
"border-e border-border bg-background flex flex-col flex-shrink-0",
isMobile ? "w-full" : "",
!isListResizing && !isMobile && "transition-[width] duration-300"
)}
Expand Down Expand Up @@ -991,7 +991,7 @@ export default function ContactsPage() {
onClick={mobileBackToList}
className="touch-manipulation"
>
<ArrowLeft className="w-4 h-4 mr-2" />
<ArrowLeft className="w-4 h-4 me-2" />
{returnToEmail ? t("back_to_email") : t("back_to_contacts")}
</Button>
</div>
Expand Down
4 changes: 2 additions & 2 deletions app/(main)/[locale]/error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ export default function LocaleError({
</p>
<div className="flex gap-3 justify-center">
<Button variant="outline" onClick={() => router.push('/')}>
<Home className="w-4 h-4 mr-2" />
<Home className="w-4 h-4 me-2" />
{t("go_home")}
</Button>
<Button onClick={reset}>
<RefreshCw className="w-4 h-4 mr-2" />
<RefreshCw className="w-4 h-4 me-2" />
{t("try_again")}
</Button>
</div>
Expand Down
4 changes: 2 additions & 2 deletions app/(main)/[locale]/files/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ export default function FilesPage() {
onClick={() => router.push("/")}
className="justify-start"
>
<ArrowLeft className="w-4 h-4 mr-2" />
<ArrowLeft className="w-4 h-4 me-2" />
{t("title")}
</Button>
</div>
Expand Down Expand Up @@ -611,7 +611,7 @@ export default function FilesPage() {
: '0%' }}
/>
</div>
<p className="mt-2 text-xs text-muted-foreground tabular-nums text-right">
<p className="mt-2 text-xs text-muted-foreground tabular-nums text-end">
{migrationProgress.current} / {migrationProgress.total}
</p>
</div>
Expand Down
40 changes: 26 additions & 14 deletions app/(main)/[locale]/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,16 @@ export default function LoginPage() {
const isMobileHandoff = Boolean(mobileRedirectUri);
const { login, loginDemo, isLoading, error, clearError, isAuthenticated } = useAuthStore();
const { theme, setTheme, initializeTheme } = useThemeStore(useShallow((s) => ({ theme: s.theme, setTheme: s.setTheme, initializeTheme: s.initializeTheme })));
const { appName, jmapServerUrl: configuredServerUrl, oauthEnabled, oauthOnly, oauthClientId: globalOauthClientId, oauthIssuerUrl: globalOauthIssuerUrl, oauthScopes, rememberMeEnabled, devMode, demoMode, loginLogoLightUrl, loginLogoDarkUrl, loginCompanyName, loginImprintUrl, loginPrivacyPolicyUrl, loginWebsiteUrl, isLoading: configLoading, error: configError, autoSsoEnabled, embeddedMode: _embeddedMode, allowCustomJmapEndpoint, jmapServers, jmapServerAutoPickByDomain } = useConfig();
const { appName, jmapServerUrl: configuredServerUrl, oauthEnabled, oauthOnly, oauthClientId: globalOauthClientId, oauthIssuerUrl: globalOauthIssuerUrl, oauthScopes, rememberMeEnabled, devMode, demoMode, loginLogoLightUrl, loginLogoDarkUrl, loginCompanyName, loginImprintUrl, loginPrivacyPolicyUrl, loginWebsiteUrl, loginLogoMaxHeight, loginLogoMaxWidth, loginShowHeading, loginShowSubtitle, isLoading: configLoading, error: configError, autoSsoEnabled, embeddedMode: _embeddedMode, allowCustomJmapEndpoint, jmapServers, jmapServerAutoPickByDomain } = useConfig();
const resolvedTheme = useThemeStore((s) => s.resolvedTheme);

// Login logo sizing: when a max height/width is configured, drop the fixed
// 64×64 box so the logo (e.g. a wide wordmark) can render at its true size.
const hasLogoSize = Boolean(loginLogoMaxHeight || loginLogoMaxWidth);
const loginLogoStyle = hasLogoSize
? { maxHeight: loginLogoMaxHeight || undefined, maxWidth: loginLogoMaxWidth || undefined }
: undefined;

const [formData, setFormData] = useState({
username: "",
password: "",
Expand Down Expand Up @@ -718,7 +725,7 @@ export default function LoginPage() {
)}
>
<Icon className="w-4 h-4" />
<span className="flex-1 text-left">{option.label}</span>
<span className="flex-1 text-start">{option.label}</span>
{isActive && <Check className="w-3.5 h-3.5 text-primary" />}
</button>
);
Expand Down Expand Up @@ -867,7 +874,7 @@ export default function LoginPage() {
)}
>
<Icon className="w-4 h-4" />
<span className="flex-1 text-left">{option.label}</span>
<span className="flex-1 text-start">{option.label}</span>
{isActive && <Check className="w-3.5 h-3.5 text-primary" />}
</button>
);
Expand All @@ -881,19 +888,24 @@ export default function LoginPage() {
<div className="rounded-2xl border border-border/60 bg-background/80 backdrop-blur-sm shadow-xl shadow-black/5 dark:shadow-black/20 overflow-hidden">
{/* Header section with logo */}
<div className="px-8 pt-10 pb-6 text-center">
<div className="inline-flex items-center justify-center w-16 h-16 mb-5">
<div className={cn("inline-flex items-center justify-center mb-5", !hasLogoSize && "w-16 h-16")}>
<img
src={withBasePath(resolvedTheme === 'dark' ? loginLogoDarkUrl : loginLogoLightUrl)}
alt={appName}
className="max-w-16 max-h-16 object-contain"
className={cn("object-contain", !hasLogoSize && "max-w-16 max-h-16")}
style={loginLogoStyle}
/>
</div>
<h1 className="text-2xl font-semibold text-foreground tracking-tight">
{isAddAccountMode ? t("add_account_title") : appName}
</h1>
<p className="text-sm text-muted-foreground mt-1.5">
{isAddAccountMode ? t("add_account_subtitle") : (t("title") !== appName ? t("title") : "Sign in to your account")}
</p>
{loginShowHeading && (
<h1 className="text-2xl font-semibold text-foreground tracking-tight">
{isAddAccountMode ? t("add_account_title") : appName}
</h1>
)}
{loginShowSubtitle && (
<p className="text-sm text-muted-foreground mt-1.5">
{isAddAccountMode ? t("add_account_subtitle") : (t("title") !== appName ? t("title") : "Sign in to your account")}
</p>
)}
</div>

{/* Form section */}
Expand Down Expand Up @@ -1121,7 +1133,7 @@ export default function LoginPage() {
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
className="h-11 px-3.5 pr-11 bg-muted/40 border-border/60 rounded-xl focus:bg-background focus:border-primary/50 transition-all duration-200"
className="h-11 px-3.5 pe-11 bg-muted/40 border-border/60 rounded-xl focus:bg-background focus:border-primary/50 transition-all duration-200"
placeholder={t("password_placeholder")}
required
autoComplete="current-password"
Expand Down Expand Up @@ -1241,9 +1253,9 @@ export default function LoginPage() {
disabled={oauthLoading || isLoading}
>
{oauthLoading ? (
<Loader2 className="w-4 h-4 animate-spin mr-2" />
<Loader2 className="w-4 h-4 animate-spin me-2" />
) : (
<LogIn className="w-4 h-4 mr-2" />
<LogIn className="w-4 h-4 me-2" />
)}
{t("sign_in_sso")}
</Button>
Expand Down
Loading