chore: update version to 1.5.0

This commit is contained in:
Linus Rath
2026-04-23 00:07:34 +02:00
parent 7494fc1776
commit c30c38a7af
13 changed files with 145 additions and 42 deletions
+8 -8
View File
@@ -8,8 +8,8 @@
- Draft auto-save with identity preservation
- Attachment upload, download, and inline preview; forgotten-attachment warning
- Full-text search with JMAP filter panel, search chips, wildcards, OR conditions, and cross-mailbox queries
- Batch operations multi-select, archive, delete, move, tag
- Archive modes direct, by year, or by month
- Batch operations multi-select, archive, delete, move, tag
- Archive modes direct, by year, or by month
- Multi-tag support with color labels, reordering, and drag-and-drop assignment
- Star/unstar with configurable mark-as-read delay
- Virtual scrolling for large mailboxes
@@ -26,7 +26,7 @@
- Drag-to-reschedule, click-drag creation, and edge-resize with 15-minute snap
- Recurring events with scoped edit/delete (this / this and following / all)
- iMIP invitations on create and update (RFC 5545 / 6047), organizer/attendee UI, and RSVP with trust assessment
- Inline calendar invitations in the email viewer auto-detect `.ics`, RSVP, import
- Inline calendar invitations in the email viewer auto-detect `.ics`, RSVP, import
- iCalendar import with preview, bulk create, and UID deduplication
- iCal / webcal subscriptions with editing and batch import
- Auto-generated birthday calendar from contacts
@@ -67,7 +67,7 @@
- External content blocked by default, with a trusted senders list
- HTML sanitization via DOMPurify
- S/MIME manage certificates, sign, encrypt, decrypt, and verify; legacy 3DES / PBE support; per-account key isolation
- S/MIME manage certificates, sign, encrypt, decrypt, and verify; legacy 3DES / PBE support; per-account key isolation
- SPF / DKIM / DMARC status indicators
- OAuth2 / OIDC with PKCE (Keycloak, Authentik, or built-in), OAuth-only mode, OAuth app passwords, and non-interactive SSO for embedded deployments
- TOTP two-factor authentication
@@ -110,9 +110,9 @@ Automatic browser detection with persistent preference. Configurable locale URL
## Admin & Extensibility
- Stalwart admin dashboard with dedicated policy sections
- Plugin system schema-driven config UI, render and intercept hooks, `onAvatarResolve` and i18n APIs, calendar event slots, and managed policy enforcement
- Themes upload, enforce, and manage admin-controlled themes as ZIP bundles
- Extension marketplace browse and install plugins and themes from a configurable directory (`EXTENSION_DIRECTORY_URL`)
- Plugin system schema-driven config UI, render and intercept hooks, `onAvatarResolve` and i18n APIs, calendar event slots, and managed policy enforcement
- Themes upload, enforce, and manage admin-controlled themes as ZIP bundles
- Extension marketplace browse and install plugins and themes from a configurable directory (`EXTENSION_DIRECTORY_URL`)
- Bundled plugins including Jitsi Meet calendar integration
## Operations
@@ -121,4 +121,4 @@ Automatic browser detection with persistent preference. Configurable locale URL
- Automatic update check with server-side logging of new releases
- Structured logging (`text` or `json`) with category-based levels
- Release (`main`) and development (`dev`) Docker images on GHCR
- Demo mode with fixture data no mail server required
- Demo mode with fixture data no mail server required
+14 -14
View File
@@ -32,35 +32,35 @@ A modern, self-hosted webmail client for [Stalwart Mail Server](https://stalw.ar
<td width="50%"><img src="screenshots/contacts.png" alt="Contacts" /></td>
</tr>
<tr>
<td><sub><b>Calendar</b> month, week, day, and agenda views with drag-to-reschedule, iMIP invitations, and CalDAV subscriptions.</sub></td>
<td><sub><b>Contacts</b> multiple address books, groups, vCard import/export, and autocomplete in the composer.</sub></td>
<td><sub><b>Calendar</b> month, week, day, and agenda views with drag-to-reschedule, iMIP invitations, and CalDAV subscriptions.</sub></td>
<td><sub><b>Contacts</b> multiple address books, groups, vCard import/export, and autocomplete in the composer.</sub></td>
</tr>
<tr>
<td><img src="screenshots/theme.png" alt="Themes" /></td>
<td><img src="screenshots/plugins.png" alt="Plugins" /></td>
</tr>
<tr>
<td><sub><b>Themes</b> bundled color themes or upload your own as ZIP bundles; admins can enforce presets.</sub></td>
<td><sub><b>Plugins</b> extend the client with bundled or third-party plugins installed from a .zip file.</sub></td>
<td><sub><b>Themes</b> bundled color themes or upload your own as ZIP bundles; admins can enforce presets.</sub></td>
<td><sub><b>Plugins</b> extend the client with bundled or third-party plugins installed from a .zip file.</sub></td>
</tr>
<tr>
<td><img src="screenshots/mail-white.png" alt="Light mode" /></td>
<td><img src="screenshots/settings.png" alt="Settings" /></td>
</tr>
<tr>
<td><sub><b>Light mode</b> full theme support with intelligent color transformation for HTML emails.</sub></td>
<td><sub><b>Settings</b> appearance, identities, filters, templates, security, and more.</sub></td>
<td><sub><b>Light mode</b> full theme support with intelligent color transformation for HTML emails.</sub></td>
<td><sub><b>Settings</b> appearance, identities, filters, templates, security, and more.</sub></td>
</tr>
</table>
## Overview
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:
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:
- **Mail** threading, unified inbox, full-text search, Sieve filters, S/MIME, templates
- **Calendar** month/week/day/agenda, recurring events, iMIP invitations, CalDAV subscriptions
- **Contacts** multiple address books, groups, vCard import/export
- **Files** Stalwart's JMAP FileNode storage with previews and folder upload
- **Mail** threading, unified inbox, full-text search, Sieve filters, S/MIME, templates
- **Calendar** month/week/day/agenda, recurring events, iMIP invitations, CalDAV subscriptions
- **Contacts** multiple address books, groups, vCard import/export
- **Files** Stalwart's JMAP FileNode storage with previews and folder upload
Plus the infrastructure around them: OAuth2 / OIDC SSO, TOTP 2FA, multi-account (up to 5 at once), 14 languages, PWA install, dark/light themes, a plugin system with an extension marketplace, and a Stalwart admin dashboard.
@@ -82,7 +82,7 @@ Or with Docker Compose:
```bash
cp .env.example .env.local
# Edit .env.local set JMAP_SERVER_URL
# Edit .env.local set JMAP_SERVER_URL
docker compose up -d
```
@@ -93,7 +93,7 @@ git clone https://github.com/bulwarkmail/webmail.git
cd webmail
npm install
cp .env.example .env.local
# Edit .env.local set JMAP_SERVER_URL
# Edit .env.local set JMAP_SERVER_URL
npm run build && npm start
```
@@ -247,7 +247,7 @@ LOG_LEVEL=info # error | warn | info | debug
## Why Stalwart?
[Stalwart](https://github.com/stalwartlabs/mail-server) is a Rust mail server with native JMAP support not IMAP/SMTP with JMAP bolted on. It handles JMAP, IMAP, SMTP, and ManageSieve in a single self-hosted binary with no third-party dependencies.
[Stalwart](https://github.com/stalwartlabs/mail-server) is a Rust mail server with native JMAP support not IMAP/SMTP with JMAP bolted on. It handles JMAP, IMAP, SMTP, and ManageSieve in a single self-hosted binary with no third-party dependencies.
## Contributing
+1 -1
View File
@@ -126,7 +126,7 @@ export default function CalendarPage() {
}
}, [events, detailEvent]);
// Check auth on mount skip when already authenticated so that navigating
// Check auth on mount skip when already authenticated so that navigating
// between routes doesn't retrigger checkAuth's transient `{ client: null,
// isLoading: true }` reset, which was flashing the spinner on every nav.
useEffect(() => {
+1 -1
View File
@@ -103,7 +103,7 @@ export default function ContactsPage() {
const [isListResizing, setIsListResizing] = useState(false);
const listDragStartWidth = useRef(384);
// Check auth on mount skip when already authenticated so that navigating
// Check auth on mount skip when already authenticated so that navigating
// between routes doesn't retrigger checkAuth's transient `{ client: null,
// isLoading: true }` reset, which was flashing the spinner on every nav.
useEffect(() => {
+1 -1
View File
@@ -105,7 +105,7 @@ export default function FilesPage() {
const detailResource = detailName ? resources.find(r => r.name === detailName) || null : null;
// Check auth on mount skip when already authenticated so that navigating
// Check auth on mount skip when already authenticated so that navigating
// between routes doesn't retrigger checkAuth's transient `{ client: null,
// isLoading: true }` reset, which was flashing the spinner on every nav.
useEffect(() => {
+2 -2
View File
@@ -455,7 +455,7 @@ export default function Home() {
document.title = title;
}, [showComposer, composerMode, selectedEmail, selectedMailbox, mailboxes, t, appName]);
// Check auth on mount skip when already authenticated so that navigating
// Check auth on mount skip when already authenticated so that navigating
// between routes doesn't retrigger checkAuth's transient `{ client: null,
// isLoading: true }` reset, which was flashing the spinner on every nav.
useEffect(() => {
@@ -838,7 +838,7 @@ export default function Home() {
const handleArchive = async (emailToArchive: Email | null = selectedEmail) => {
if (!client || !emailToArchive) return;
// Read fresh mailboxes from the store batch archive calls this in a loop,
// Read fresh mailboxes from the store batch archive calls this in a loop,
// and each iteration needs to see folders created by prior iterations.
const currentMailboxes = useEmailStore.getState().mailboxes;
+1 -1
View File
@@ -179,7 +179,7 @@ export default function SettingsPage() {
const [isResizing, setIsResizing] = useState(false);
const dragStartWidth = useRef(256);
// Check auth on mount skip when already authenticated so that navigating
// Check auth on mount skip when already authenticated so that navigating
// between routes doesn't retrigger checkAuth's transient `{ client: null,
// isLoading: true }` reset, which was flashing the spinner on every nav.
useEffect(() => {
+1 -1
View File
@@ -40,7 +40,7 @@ export function sanitizeEmailHtml(html: string): string {
* Sanitize config for emails rendered inside a sandboxed iframe.
* Allows <style> tags because CSS is scoped to the iframe document and
* cannot leak into the host app. Scripts are still blocked by the sandbox
* attribute (no allow-scripts). Use ONLY for iframe-rendered content
* attribute (no allow-scripts). Use ONLY for iframe-rendered content
* never for content rendered into the main DOM.
*/
export const EMAIL_IFRAME_SANITIZE_CONFIG = {
+3 -3
View File
@@ -1300,7 +1300,7 @@ export class JMAPClient implements IJMAPClient {
const parts = [err.type || 'unknown'];
if (err.properties?.length) parts.push(`properties=[${err.properties.join(', ')}]`);
if (err.description) parts.push(err.description);
throw new Error(`Failed to create archive folder '${cid}': ${parts.join(' ')}`);
throw new Error(`Failed to create archive folder '${cid}': ${parts.join(' ')}`);
}
}
@@ -1310,7 +1310,7 @@ export class JMAPClient implements IJMAPClient {
const emailFailures = notUpdated ? Object.entries(notUpdated) : [];
if (emailFailures.length > 0) {
const [id, err] = emailFailures[0];
throw new Error(`Failed to move ${emailFailures.length} email(s), first: ${id} ${err.type || 'unknown'}${err.description ? ` (${err.description})` : ''}`);
throw new Error(`Failed to move ${emailFailures.length} email(s), first: ${id} ${err.type || 'unknown'}${err.description ? ` (${err.description})` : ''}`);
}
}
@@ -1429,7 +1429,7 @@ export class JMAPClient implements IJMAPClient {
details.push(`properties=[${err.properties.join(', ')}]`);
}
if (err.description) details.push(err.description);
throw new Error(`Failed to create mailbox: ${details.join(' ')}`);
throw new Error(`Failed to create mailbox: ${details.join(' ')}`);
}
const created = result?.created?.[createId];
+1 -1
View File
@@ -1127,7 +1127,7 @@
"description": "Создавайте API-ключи для скриптов и интеграций, обращающихся к серверу напрямую",
"name_label": "Название ключа",
"name_placeholder": "напр. Скрипт резервного копирования, CI runner",
"copy_now_warning": "Скопируйте этот API-ключ сейчас он больше не будет показан.",
"copy_now_warning": "Скопируйте этот API-ключ сейчас он больше не будет показан.",
"added": "API-ключ создан",
"removed": "API-ключ удалён",
"add_error": "Не удалось создать API-ключ",
+1 -1
View File
@@ -1127,7 +1127,7 @@
"description": "Створюйте API-ключі для скриптів та інтеграцій, які звертаються до сервера напряму",
"name_label": "Назва ключа",
"name_placeholder": "напр. Скрипт резервної копії, CI runner",
"copy_now_warning": "Скопіюйте цей API-ключ зараз він більше не буде показаний.",
"copy_now_warning": "Скопіюйте цей API-ключ зараз він більше не буде показаний.",
"added": "API-ключ створено",
"removed": "API-ключ видалено",
"add_error": "Не вдалося створити API-ключ",
+110 -7
View File
@@ -53,6 +53,7 @@ ENV_FILE="${SCRIPT_DIR}/.env.local"
# Config values (defaults)
CFG_APP_NAME="Bulwark Webmail"
CFG_JMAP_SERVER_URL=""
CFG_ALLOW_CUSTOM_JMAP_ENDPOINT="false"
CFG_STALWART_FEATURES="true"
CFG_OAUTH_ENABLED="false"
CFG_OAUTH_ONLY="false"
@@ -64,16 +65,23 @@ CFG_SETTINGS_SYNC_ENABLED="false"
CFG_SETTINGS_DATA_DIR="./data/settings"
CFG_LOG_FORMAT="text"
CFG_LOG_LEVEL="info"
CFG_APP_SHORT_NAME=""
CFG_APP_DESCRIPTION=""
CFG_LOGIN_COMPANY_NAME=""
CFG_LOGIN_LOGO_LIGHT_URL=""
CFG_LOGIN_LOGO_DARK_URL=""
CFG_FAVICON_URL=""
CFG_PWA_ICON_URL=""
CFG_PWA_THEME_COLOR=""
CFG_PWA_BACKGROUND_COLOR=""
CFG_APP_LOGO_LIGHT_URL=""
CFG_APP_LOGO_DARK_URL=""
CFG_LOGIN_IMPRINT_URL=""
CFG_LOGIN_PRIVACY_POLICY_URL=""
CFG_LOGIN_WEBSITE_URL=""
CFG_EXTENSION_DIRECTORY_URL=""
CFG_DEPLOY_METHOD=""
CFG_HOSTNAME="0.0.0.0"
CFG_PORT="3000"
CURRENT_STEP=0
@@ -593,9 +601,19 @@ screen_server_config() {
prompt_yesno "Enable Stalwart-specific features?" "$CFG_STALWART_FEATURES" "CFG_STALWART_FEATURES"
echo ""
echo -e " ${BOLD}Network${RESET}"
echo -e " ${DIM}The port the web UI will listen on. Default is 3000.${RESET}"
echo -e " ${DIM}Adds a \"JMAP Server\" field to the login form so users can${RESET}"
echo -e " ${DIM}connect to any JMAP-compatible server (not just the one above).${RESET}"
echo -e " ${DIM}The external server must allow this domain in its CORS headers.${RESET}"
prompt_yesno "Allow users to override the JMAP server at login?" "$CFG_ALLOW_CUSTOM_JMAP_ENDPOINT" "CFG_ALLOW_CUSTOM_JMAP_ENDPOINT"
echo ""
echo -e " ${BOLD}Network${RESET}"
echo -e " ${DIM}The address the server binds to. Use \"0.0.0.0\" for all IPv4${RESET}"
echo -e " ${DIM}interfaces or \"::\" for dual-stack IPv4+IPv6.${RESET}"
echo ""
prompt_value "Hostname" "$CFG_HOSTNAME" "CFG_HOSTNAME"
echo ""
echo -e " ${DIM}The port the web UI will listen on. Default is 3000.${RESET}"
prompt_value "Port" "$CFG_PORT" "CFG_PORT"
draw_footer
@@ -770,32 +788,65 @@ screen_login_customization() {
echo -e " All fields are ${BOLD}optional${RESET}. Press Enter to skip any field."
echo ""
echo -e " ${BOLD}App Identity${RESET}"
echo -e " ${DIM}Short name used where space is limited (e.g. mobile home screen).${RESET}"
echo -e " ${DIM}Defaults to the application name set in step 1.${RESET}"
prompt_value "Short app name" "$CFG_APP_SHORT_NAME" "CFG_APP_SHORT_NAME"
echo ""
echo -e " ${DIM}Description shown by the OS when installing as a PWA.${RESET}"
prompt_value "App description" "$CFG_APP_DESCRIPTION" "CFG_APP_DESCRIPTION"
echo ""
echo -e " ${BOLD}Icons${RESET}"
echo -e " ${DIM}Custom favicon for the browser tab (SVG recommended, 32×32 to 512×512px)${RESET}"
echo -e " ${DIM}Leave blank to use the default Bulwark favicon.${RESET}"
prompt_value "Favicon URL" "$CFG_FAVICON_URL" "CFG_FAVICON_URL"
echo ""
echo -e " ${DIM}Source image for PWA icons (192×192 and 512×512 are auto-generated).${RESET}"
echo -e " ${DIM}Falls back to FAVICON_URL if blank.${RESET}"
prompt_value "PWA icon URL" "$CFG_PWA_ICON_URL" "CFG_PWA_ICON_URL"
echo ""
echo -e " ${BOLD}PWA Colors${RESET}"
echo -e " ${DIM}Hex color (e.g. #3b82f6) used for the browser UI chrome when installed.${RESET}"
prompt_value "PWA theme color" "$CFG_PWA_THEME_COLOR" "CFG_PWA_THEME_COLOR"
echo ""
echo -e " ${DIM}Hex color shown on the PWA splash screen while loading.${RESET}"
prompt_value "PWA background color" "$CFG_PWA_BACKGROUND_COLOR" "CFG_PWA_BACKGROUND_COLOR"
echo ""
echo -e " ${BOLD}Logos${RESET}"
echo -e " ${DIM}App logo shown in the sidebar (SVG recommended, 24×24 to 128×128px)${RESET}"
echo -e " ${DIM}Leave blank for no sidebar logo.${RESET}"
prompt_value "App logo URL (light mode)" "$CFG_APP_LOGO_LIGHT_URL" "CFG_APP_LOGO_LIGHT_URL"
echo ""
prompt_value "App logo URL (dark mode)" "$CFG_APP_LOGO_DARK_URL" "CFG_APP_LOGO_DARK_URL"
echo ""
echo -e " ${DIM}Shown on the login page footer. Example: Acme Corp${RESET}"
prompt_value "Company / organization name" "$CFG_LOGIN_COMPANY_NAME" "CFG_LOGIN_COMPANY_NAME"
echo ""
echo -e " ${DIM}Custom logo URLs for the login page (SVG recommended, 32×32 to 512×512px)${RESET}"
echo -e " ${DIM}Leave blank to use the default Bulwark logo.${RESET}"
prompt_value "Login logo URL (light mode)" "$CFG_LOGIN_LOGO_LIGHT_URL" "CFG_LOGIN_LOGO_LIGHT_URL"
echo ""
prompt_value "Login logo URL (dark mode)" "$CFG_LOGIN_LOGO_DARK_URL" "CFG_LOGIN_LOGO_DARK_URL"
echo ""
echo -e " ${BOLD}Login Page Links${RESET}"
echo -e " ${DIM}Shown on the login page footer. Example: Acme Corp${RESET}"
prompt_value "Company / organization name" "$CFG_LOGIN_COMPANY_NAME" "CFG_LOGIN_COMPANY_NAME"
echo ""
prompt_value "Website URL" "$CFG_LOGIN_WEBSITE_URL" "CFG_LOGIN_WEBSITE_URL"
echo ""
prompt_value "Imprint / legal notice URL" "$CFG_LOGIN_IMPRINT_URL" "CFG_LOGIN_IMPRINT_URL"
echo ""
prompt_value "Privacy policy URL" "$CFG_LOGIN_PRIVACY_POLICY_URL" "CFG_LOGIN_PRIVACY_POLICY_URL"
echo ""
if [[ -z "$CFG_LOGIN_COMPANY_NAME" && -z "$CFG_LOGIN_LOGO_LIGHT_URL" && -z "$CFG_LOGIN_LOGO_DARK_URL" && -z "$CFG_FAVICON_URL" && -z "$CFG_APP_LOGO_LIGHT_URL" && -z "$CFG_APP_LOGO_DARK_URL" && -z "$CFG_LOGIN_WEBSITE_URL" && -z "$CFG_LOGIN_IMPRINT_URL" && -z "$CFG_LOGIN_PRIVACY_POLICY_URL" ]]; then
echo -e " ${BOLD}Extension Directory${RESET}"
echo -e " ${DIM}URL of the BulwarkMail extension directory for the admin marketplace.${RESET}"
echo -e " ${DIM}Set this to enable browsing and installing plugins/themes.${RESET}"
echo -e " ${DIM}Leave blank to disable the marketplace.${RESET}"
prompt_value "Extension directory URL" "$CFG_EXTENSION_DIRECTORY_URL" "CFG_EXTENSION_DIRECTORY_URL"
if [[ -z "$CFG_APP_SHORT_NAME" && -z "$CFG_APP_DESCRIPTION" && -z "$CFG_LOGIN_COMPANY_NAME" && -z "$CFG_LOGIN_LOGO_LIGHT_URL" && -z "$CFG_LOGIN_LOGO_DARK_URL" && -z "$CFG_FAVICON_URL" && -z "$CFG_PWA_ICON_URL" && -z "$CFG_PWA_THEME_COLOR" && -z "$CFG_PWA_BACKGROUND_COLOR" && -z "$CFG_APP_LOGO_LIGHT_URL" && -z "$CFG_APP_LOGO_DARK_URL" && -z "$CFG_LOGIN_WEBSITE_URL" && -z "$CFG_LOGIN_IMPRINT_URL" && -z "$CFG_LOGIN_PRIVACY_POLICY_URL" && -z "$CFG_EXTENSION_DIRECTORY_URL" ]]; then
echo ""
note "No branding configured. The app will use defaults."
fi
@@ -866,7 +917,9 @@ screen_summary() {
echo -e " ${CYAN}${BOLD}SERVER${RESET}"
echo -e " App Name .............. ${BOLD}${CFG_APP_NAME}${RESET}"
echo -e " JMAP Server URL ....... ${BOLD}${CFG_JMAP_SERVER_URL}${RESET}"
echo -e " Allow Custom JMAP ..... ${BOLD}${CFG_ALLOW_CUSTOM_JMAP_ENDPOINT}${RESET}"
echo -e " Stalwart Features ..... ${BOLD}${CFG_STALWART_FEATURES}${RESET}"
echo -e " Hostname .............. ${BOLD}${CFG_HOSTNAME}${RESET}"
echo -e " Port .................. ${BOLD}${CFG_PORT}${RESET}"
echo ""
@@ -905,9 +958,19 @@ screen_summary() {
# Login page
echo -e " ${CYAN}${BOLD}BRANDING${RESET}"
if [[ -n "$CFG_LOGIN_COMPANY_NAME" || -n "$CFG_LOGIN_LOGO_LIGHT_URL" || -n "$CFG_LOGIN_LOGO_DARK_URL" || -n "$CFG_FAVICON_URL" || -n "$CFG_APP_LOGO_LIGHT_URL" || -n "$CFG_APP_LOGO_DARK_URL" || -n "$CFG_LOGIN_WEBSITE_URL" || -n "$CFG_LOGIN_IMPRINT_URL" || -n "$CFG_LOGIN_PRIVACY_POLICY_URL" ]]; then
if [[ -n "$CFG_APP_SHORT_NAME" || -n "$CFG_APP_DESCRIPTION" || -n "$CFG_LOGIN_COMPANY_NAME" || -n "$CFG_LOGIN_LOGO_LIGHT_URL" || -n "$CFG_LOGIN_LOGO_DARK_URL" || -n "$CFG_FAVICON_URL" || -n "$CFG_PWA_ICON_URL" || -n "$CFG_PWA_THEME_COLOR" || -n "$CFG_PWA_BACKGROUND_COLOR" || -n "$CFG_APP_LOGO_LIGHT_URL" || -n "$CFG_APP_LOGO_DARK_URL" || -n "$CFG_LOGIN_WEBSITE_URL" || -n "$CFG_LOGIN_IMPRINT_URL" || -n "$CFG_LOGIN_PRIVACY_POLICY_URL" ]]; then
[[ -n "$CFG_APP_SHORT_NAME" ]] && \
echo -e " Short Name ............ ${BOLD}${CFG_APP_SHORT_NAME}${RESET}"
[[ -n "$CFG_APP_DESCRIPTION" ]] && \
echo -e " Description ........... ${BOLD}${CFG_APP_DESCRIPTION}${RESET}"
[[ -n "$CFG_FAVICON_URL" ]] && \
echo -e " Favicon URL ........... ${BOLD}${CFG_FAVICON_URL}${RESET}"
[[ -n "$CFG_PWA_ICON_URL" ]] && \
echo -e " PWA Icon URL .......... ${BOLD}${CFG_PWA_ICON_URL}${RESET}"
[[ -n "$CFG_PWA_THEME_COLOR" ]] && \
echo -e " PWA Theme Color ....... ${BOLD}${CFG_PWA_THEME_COLOR}${RESET}"
[[ -n "$CFG_PWA_BACKGROUND_COLOR" ]] && \
echo -e " PWA Background Color .. ${BOLD}${CFG_PWA_BACKGROUND_COLOR}${RESET}"
[[ -n "$CFG_APP_LOGO_LIGHT_URL" ]] && \
echo -e " App Logo (light) ...... ${BOLD}${CFG_APP_LOGO_LIGHT_URL}${RESET}"
[[ -n "$CFG_APP_LOGO_DARK_URL" ]] && \
@@ -929,6 +992,13 @@ screen_summary() {
fi
echo ""
# Extensions
if [[ -n "$CFG_EXTENSION_DIRECTORY_URL" ]]; then
echo -e " ${CYAN}${BOLD}EXTENSIONS${RESET}"
echo -e " Directory URL ......... ${BOLD}${CFG_EXTENSION_DIRECTORY_URL}${RESET}"
echo ""
fi
# Deployment
echo -e " ${CYAN}${BOLD}DEPLOYMENT${RESET}"
case "$CFG_DEPLOY_METHOD" in
@@ -978,10 +1048,15 @@ write_env_file() {
# -- JMAP Server (required) ---------------------------------------------------
APP_NAME=${CFG_APP_NAME}
JMAP_SERVER_URL=${CFG_JMAP_SERVER_URL}
ALLOW_CUSTOM_JMAP_ENDPOINT=${CFG_ALLOW_CUSTOM_JMAP_ENDPOINT}
# -- Stalwart Mail Server Integration -----------------------------------------
STALWART_FEATURES=${CFG_STALWART_FEATURES}
# -- Server Listen Address ----------------------------------------------------
HOSTNAME=${CFG_HOSTNAME}
PORT=${CFG_PORT}
# -- OAuth / OpenID Connect ---------------------------------------------------
OAUTH_ENABLED=${CFG_OAUTH_ENABLED}
ENVEOF
@@ -1025,7 +1100,12 @@ LOG_LEVEL=${CFG_LOG_LEVEL}
# -- Branding ------------------------------------------------------------------
ENVEOF
[[ -n "$CFG_APP_SHORT_NAME" ]] && echo "APP_SHORT_NAME=${CFG_APP_SHORT_NAME}" >> "$ENV_FILE"
[[ -n "$CFG_APP_DESCRIPTION" ]] && echo "APP_DESCRIPTION=${CFG_APP_DESCRIPTION}" >> "$ENV_FILE"
[[ -n "$CFG_FAVICON_URL" ]] && echo "FAVICON_URL=${CFG_FAVICON_URL}" >> "$ENV_FILE"
[[ -n "$CFG_PWA_ICON_URL" ]] && echo "PWA_ICON_URL=${CFG_PWA_ICON_URL}" >> "$ENV_FILE"
[[ -n "$CFG_PWA_THEME_COLOR" ]] && echo "PWA_THEME_COLOR=${CFG_PWA_THEME_COLOR}" >> "$ENV_FILE"
[[ -n "$CFG_PWA_BACKGROUND_COLOR" ]] && echo "PWA_BACKGROUND_COLOR=${CFG_PWA_BACKGROUND_COLOR}" >> "$ENV_FILE"
[[ -n "$CFG_APP_LOGO_LIGHT_URL" ]] && echo "APP_LOGO_LIGHT_URL=${CFG_APP_LOGO_LIGHT_URL}" >> "$ENV_FILE"
[[ -n "$CFG_APP_LOGO_DARK_URL" ]] && echo "APP_LOGO_DARK_URL=${CFG_APP_LOGO_DARK_URL}" >> "$ENV_FILE"
[[ -n "$CFG_LOGIN_COMPANY_NAME" ]] && echo "LOGIN_COMPANY_NAME=${CFG_LOGIN_COMPANY_NAME}" >> "$ENV_FILE"
@@ -1035,6 +1115,29 @@ ENVEOF
[[ -n "$CFG_LOGIN_PRIVACY_POLICY_URL" ]] && echo "LOGIN_PRIVACY_POLICY_URL=${CFG_LOGIN_PRIVACY_POLICY_URL}" >> "$ENV_FILE"
[[ -n "$CFG_LOGIN_WEBSITE_URL" ]] && echo "LOGIN_WEBSITE_URL=${CFG_LOGIN_WEBSITE_URL}" >> "$ENV_FILE"
if [[ -n "$CFG_EXTENSION_DIRECTORY_URL" ]]; then
cat >> "$ENV_FILE" << ENVEOF
# -- Extension Directory / Marketplace ----------------------------------------
EXTENSION_DIRECTORY_URL=${CFG_EXTENSION_DIRECTORY_URL}
ENVEOF
fi
cat >> "$ENV_FILE" << ENVEOF
# -- Advanced (uncomment and set if needed) -----------------------------------
# SESSION_SECRET_FILE=/run/secrets/session_secret
# OAUTH_CLIENT_SECRET_FILE=/run/secrets/oauth_client_secret
# OAUTH_SCOPES=openid profile email
# OAUTH_EXTRA_SCOPES=
# COOKIE_SAME_SITE=lax
# COOKIE_SECURE=true
# TRUSTED_PROXY_DEPTH=1
# ALLOWED_FRAME_ANCESTORS='none'
# ADMIN_DATA_DIR=./data/admin
# ADMIN_SESSION_TTL=3600
ENVEOF
echo -e " ${OK} Written ${BOLD}.env.local${RESET}"
}
+1 -1
View File
@@ -819,7 +819,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
const affected = emails.filter(e => idSet.has(e.id));
if (isUnifiedView) {
// In unified view, emails may span accounts group and dispatch per-account.
// In unified view, emails may span accounts group and dispatch per-account.
const byAccount = new Map<string, string[]>();
for (const e of affected) {
const acct = e.accountId || '__default__';