chore: update version to 1.5.0
This commit is contained in:
+8
-8
@@ -8,8 +8,8 @@
|
|||||||
- Draft auto-save with identity preservation
|
- Draft auto-save with identity preservation
|
||||||
- Attachment upload, download, and inline preview; forgotten-attachment warning
|
- 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
|
- Full-text search with JMAP filter panel, search chips, wildcards, OR conditions, and cross-mailbox queries
|
||||||
- Batch operations — multi-select, archive, delete, move, tag
|
- Batch operations – multi-select, archive, delete, move, tag
|
||||||
- Archive modes — direct, by year, or by month
|
- Archive modes – direct, by year, or by month
|
||||||
- Multi-tag support with color labels, reordering, and drag-and-drop assignment
|
- Multi-tag support with color labels, reordering, and drag-and-drop assignment
|
||||||
- Star/unstar with configurable mark-as-read delay
|
- Star/unstar with configurable mark-as-read delay
|
||||||
- Virtual scrolling for large mailboxes
|
- Virtual scrolling for large mailboxes
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
- Drag-to-reschedule, click-drag creation, and edge-resize with 15-minute snap
|
- Drag-to-reschedule, click-drag creation, and edge-resize with 15-minute snap
|
||||||
- Recurring events with scoped edit/delete (this / this and following / all)
|
- 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
|
- 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
|
- iCalendar import with preview, bulk create, and UID deduplication
|
||||||
- iCal / webcal subscriptions with editing and batch import
|
- iCal / webcal subscriptions with editing and batch import
|
||||||
- Auto-generated birthday calendar from contacts
|
- Auto-generated birthday calendar from contacts
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
|
|
||||||
- External content blocked by default, with a trusted senders list
|
- External content blocked by default, with a trusted senders list
|
||||||
- HTML sanitization via DOMPurify
|
- 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
|
- 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
|
- 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
|
- TOTP two-factor authentication
|
||||||
@@ -110,9 +110,9 @@ Automatic browser detection with persistent preference. Configurable locale URL
|
|||||||
## Admin & Extensibility
|
## Admin & Extensibility
|
||||||
|
|
||||||
- Stalwart admin dashboard with dedicated policy sections
|
- 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
|
- 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
|
- 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`)
|
- Extension marketplace – browse and install plugins and themes from a configurable directory (`EXTENSION_DIRECTORY_URL`)
|
||||||
- Bundled plugins including Jitsi Meet calendar integration
|
- Bundled plugins including Jitsi Meet calendar integration
|
||||||
|
|
||||||
## Operations
|
## Operations
|
||||||
@@ -121,4 +121,4 @@ Automatic browser detection with persistent preference. Configurable locale URL
|
|||||||
- Automatic update check with server-side logging of new releases
|
- Automatic update check with server-side logging of new releases
|
||||||
- Structured logging (`text` or `json`) with category-based levels
|
- Structured logging (`text` or `json`) with category-based levels
|
||||||
- Release (`main`) and development (`dev`) Docker images on GHCR
|
- 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
|
||||||
|
|||||||
@@ -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>
|
<td width="50%"><img src="screenshots/contacts.png" alt="Contacts" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<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>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>Contacts</b> – multiple address books, groups, vCard import/export, and autocomplete in the composer.</sub></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="screenshots/theme.png" alt="Themes" /></td>
|
<td><img src="screenshots/theme.png" alt="Themes" /></td>
|
||||||
<td><img src="screenshots/plugins.png" alt="Plugins" /></td>
|
<td><img src="screenshots/plugins.png" alt="Plugins" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<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>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>Plugins</b> – extend the client with bundled or third-party plugins installed from a .zip file.</sub></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="screenshots/mail-white.png" alt="Light mode" /></td>
|
<td><img src="screenshots/mail-white.png" alt="Light mode" /></td>
|
||||||
<td><img src="screenshots/settings.png" alt="Settings" /></td>
|
<td><img src="screenshots/settings.png" alt="Settings" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><sub><b>Light mode</b> — full theme support with intelligent color transformation for HTML emails.</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>
|
<td><sub><b>Settings</b> – appearance, identities, filters, templates, security, and more.</sub></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
## Overview
|
## 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
|
- **Mail** – threading, unified inbox, full-text search, Sieve filters, S/MIME, templates
|
||||||
- **Calendar** — month/week/day/agenda, recurring events, iMIP invitations, CalDAV subscriptions
|
- **Calendar** – month/week/day/agenda, recurring events, iMIP invitations, CalDAV subscriptions
|
||||||
- **Contacts** — multiple address books, groups, vCard import/export
|
- **Contacts** – multiple address books, groups, vCard import/export
|
||||||
- **Files** — Stalwart's JMAP FileNode storage with previews and folder upload
|
- **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.
|
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
|
```bash
|
||||||
cp .env.example .env.local
|
cp .env.example .env.local
|
||||||
# Edit .env.local — set JMAP_SERVER_URL
|
# Edit .env.local – set JMAP_SERVER_URL
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ git clone https://github.com/bulwarkmail/webmail.git
|
|||||||
cd webmail
|
cd webmail
|
||||||
npm install
|
npm install
|
||||||
cp .env.example .env.local
|
cp .env.example .env.local
|
||||||
# Edit .env.local — set JMAP_SERVER_URL
|
# Edit .env.local – set JMAP_SERVER_URL
|
||||||
npm run build && npm start
|
npm run build && npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@ LOG_LEVEL=info # error | warn | info | debug
|
|||||||
|
|
||||||
## Why Stalwart?
|
## 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
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export default function CalendarPage() {
|
|||||||
}
|
}
|
||||||
}, [events, detailEvent]);
|
}, [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,
|
// between routes doesn't retrigger checkAuth's transient `{ client: null,
|
||||||
// isLoading: true }` reset, which was flashing the spinner on every nav.
|
// isLoading: true }` reset, which was flashing the spinner on every nav.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export default function ContactsPage() {
|
|||||||
const [isListResizing, setIsListResizing] = useState(false);
|
const [isListResizing, setIsListResizing] = useState(false);
|
||||||
const listDragStartWidth = useRef(384);
|
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,
|
// between routes doesn't retrigger checkAuth's transient `{ client: null,
|
||||||
// isLoading: true }` reset, which was flashing the spinner on every nav.
|
// isLoading: true }` reset, which was flashing the spinner on every nav.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export default function FilesPage() {
|
|||||||
|
|
||||||
const detailResource = detailName ? resources.find(r => r.name === detailName) || null : null;
|
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,
|
// between routes doesn't retrigger checkAuth's transient `{ client: null,
|
||||||
// isLoading: true }` reset, which was flashing the spinner on every nav.
|
// isLoading: true }` reset, which was flashing the spinner on every nav.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -455,7 +455,7 @@ export default function Home() {
|
|||||||
document.title = title;
|
document.title = title;
|
||||||
}, [showComposer, composerMode, selectedEmail, selectedMailbox, mailboxes, t, appName]);
|
}, [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,
|
// between routes doesn't retrigger checkAuth's transient `{ client: null,
|
||||||
// isLoading: true }` reset, which was flashing the spinner on every nav.
|
// isLoading: true }` reset, which was flashing the spinner on every nav.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -838,7 +838,7 @@ export default function Home() {
|
|||||||
const handleArchive = async (emailToArchive: Email | null = selectedEmail) => {
|
const handleArchive = async (emailToArchive: Email | null = selectedEmail) => {
|
||||||
if (!client || !emailToArchive) return;
|
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.
|
// and each iteration needs to see folders created by prior iterations.
|
||||||
const currentMailboxes = useEmailStore.getState().mailboxes;
|
const currentMailboxes = useEmailStore.getState().mailboxes;
|
||||||
|
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ export default function SettingsPage() {
|
|||||||
const [isResizing, setIsResizing] = useState(false);
|
const [isResizing, setIsResizing] = useState(false);
|
||||||
const dragStartWidth = useRef(256);
|
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,
|
// between routes doesn't retrigger checkAuth's transient `{ client: null,
|
||||||
// isLoading: true }` reset, which was flashing the spinner on every nav.
|
// isLoading: true }` reset, which was flashing the spinner on every nav.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function sanitizeEmailHtml(html: string): string {
|
|||||||
* Sanitize config for emails rendered inside a sandboxed iframe.
|
* Sanitize config for emails rendered inside a sandboxed iframe.
|
||||||
* Allows <style> tags because CSS is scoped to the iframe document and
|
* 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
|
* 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.
|
* never for content rendered into the main DOM.
|
||||||
*/
|
*/
|
||||||
export const EMAIL_IFRAME_SANITIZE_CONFIG = {
|
export const EMAIL_IFRAME_SANITIZE_CONFIG = {
|
||||||
|
|||||||
+3
-3
@@ -1300,7 +1300,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
const parts = [err.type || 'unknown'];
|
const parts = [err.type || 'unknown'];
|
||||||
if (err.properties?.length) parts.push(`properties=[${err.properties.join(', ')}]`);
|
if (err.properties?.length) parts.push(`properties=[${err.properties.join(', ')}]`);
|
||||||
if (err.description) parts.push(err.description);
|
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) : [];
|
const emailFailures = notUpdated ? Object.entries(notUpdated) : [];
|
||||||
if (emailFailures.length > 0) {
|
if (emailFailures.length > 0) {
|
||||||
const [id, err] = emailFailures[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(', ')}]`);
|
details.push(`properties=[${err.properties.join(', ')}]`);
|
||||||
}
|
}
|
||||||
if (err.description) details.push(err.description);
|
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];
|
const created = result?.created?.[createId];
|
||||||
|
|||||||
@@ -1127,7 +1127,7 @@
|
|||||||
"description": "Создавайте API-ключи для скриптов и интеграций, обращающихся к серверу напрямую",
|
"description": "Создавайте API-ключи для скриптов и интеграций, обращающихся к серверу напрямую",
|
||||||
"name_label": "Название ключа",
|
"name_label": "Название ключа",
|
||||||
"name_placeholder": "напр. Скрипт резервного копирования, CI runner",
|
"name_placeholder": "напр. Скрипт резервного копирования, CI runner",
|
||||||
"copy_now_warning": "Скопируйте этот API-ключ сейчас — он больше не будет показан.",
|
"copy_now_warning": "Скопируйте этот API-ключ сейчас – он больше не будет показан.",
|
||||||
"added": "API-ключ создан",
|
"added": "API-ключ создан",
|
||||||
"removed": "API-ключ удалён",
|
"removed": "API-ключ удалён",
|
||||||
"add_error": "Не удалось создать API-ключ",
|
"add_error": "Не удалось создать API-ключ",
|
||||||
|
|||||||
@@ -1127,7 +1127,7 @@
|
|||||||
"description": "Створюйте API-ключі для скриптів та інтеграцій, які звертаються до сервера напряму",
|
"description": "Створюйте API-ключі для скриптів та інтеграцій, які звертаються до сервера напряму",
|
||||||
"name_label": "Назва ключа",
|
"name_label": "Назва ключа",
|
||||||
"name_placeholder": "напр. Скрипт резервної копії, CI runner",
|
"name_placeholder": "напр. Скрипт резервної копії, CI runner",
|
||||||
"copy_now_warning": "Скопіюйте цей API-ключ зараз — він більше не буде показаний.",
|
"copy_now_warning": "Скопіюйте цей API-ключ зараз – він більше не буде показаний.",
|
||||||
"added": "API-ключ створено",
|
"added": "API-ключ створено",
|
||||||
"removed": "API-ключ видалено",
|
"removed": "API-ключ видалено",
|
||||||
"add_error": "Не вдалося створити API-ключ",
|
"add_error": "Не вдалося створити API-ключ",
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ ENV_FILE="${SCRIPT_DIR}/.env.local"
|
|||||||
# Config values (defaults)
|
# Config values (defaults)
|
||||||
CFG_APP_NAME="Bulwark Webmail"
|
CFG_APP_NAME="Bulwark Webmail"
|
||||||
CFG_JMAP_SERVER_URL=""
|
CFG_JMAP_SERVER_URL=""
|
||||||
|
CFG_ALLOW_CUSTOM_JMAP_ENDPOINT="false"
|
||||||
CFG_STALWART_FEATURES="true"
|
CFG_STALWART_FEATURES="true"
|
||||||
CFG_OAUTH_ENABLED="false"
|
CFG_OAUTH_ENABLED="false"
|
||||||
CFG_OAUTH_ONLY="false"
|
CFG_OAUTH_ONLY="false"
|
||||||
@@ -64,16 +65,23 @@ CFG_SETTINGS_SYNC_ENABLED="false"
|
|||||||
CFG_SETTINGS_DATA_DIR="./data/settings"
|
CFG_SETTINGS_DATA_DIR="./data/settings"
|
||||||
CFG_LOG_FORMAT="text"
|
CFG_LOG_FORMAT="text"
|
||||||
CFG_LOG_LEVEL="info"
|
CFG_LOG_LEVEL="info"
|
||||||
|
CFG_APP_SHORT_NAME=""
|
||||||
|
CFG_APP_DESCRIPTION=""
|
||||||
CFG_LOGIN_COMPANY_NAME=""
|
CFG_LOGIN_COMPANY_NAME=""
|
||||||
CFG_LOGIN_LOGO_LIGHT_URL=""
|
CFG_LOGIN_LOGO_LIGHT_URL=""
|
||||||
CFG_LOGIN_LOGO_DARK_URL=""
|
CFG_LOGIN_LOGO_DARK_URL=""
|
||||||
CFG_FAVICON_URL=""
|
CFG_FAVICON_URL=""
|
||||||
|
CFG_PWA_ICON_URL=""
|
||||||
|
CFG_PWA_THEME_COLOR=""
|
||||||
|
CFG_PWA_BACKGROUND_COLOR=""
|
||||||
CFG_APP_LOGO_LIGHT_URL=""
|
CFG_APP_LOGO_LIGHT_URL=""
|
||||||
CFG_APP_LOGO_DARK_URL=""
|
CFG_APP_LOGO_DARK_URL=""
|
||||||
CFG_LOGIN_IMPRINT_URL=""
|
CFG_LOGIN_IMPRINT_URL=""
|
||||||
CFG_LOGIN_PRIVACY_POLICY_URL=""
|
CFG_LOGIN_PRIVACY_POLICY_URL=""
|
||||||
CFG_LOGIN_WEBSITE_URL=""
|
CFG_LOGIN_WEBSITE_URL=""
|
||||||
|
CFG_EXTENSION_DIRECTORY_URL=""
|
||||||
CFG_DEPLOY_METHOD=""
|
CFG_DEPLOY_METHOD=""
|
||||||
|
CFG_HOSTNAME="0.0.0.0"
|
||||||
CFG_PORT="3000"
|
CFG_PORT="3000"
|
||||||
|
|
||||||
CURRENT_STEP=0
|
CURRENT_STEP=0
|
||||||
@@ -593,9 +601,19 @@ screen_server_config() {
|
|||||||
prompt_yesno "Enable Stalwart-specific features?" "$CFG_STALWART_FEATURES" "CFG_STALWART_FEATURES"
|
prompt_yesno "Enable Stalwart-specific features?" "$CFG_STALWART_FEATURES" "CFG_STALWART_FEATURES"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
echo -e " ${BOLD}Network${RESET}"
|
echo -e " ${DIM}Adds a \"JMAP Server\" field to the login form so users can${RESET}"
|
||||||
echo -e " ${DIM}The port the web UI will listen on. Default is 3000.${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 ""
|
||||||
|
|
||||||
|
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"
|
prompt_value "Port" "$CFG_PORT" "CFG_PORT"
|
||||||
|
|
||||||
draw_footer
|
draw_footer
|
||||||
@@ -770,32 +788,65 @@ screen_login_customization() {
|
|||||||
echo -e " All fields are ${BOLD}optional${RESET}. Press Enter to skip any field."
|
echo -e " All fields are ${BOLD}optional${RESET}. Press Enter to skip any field."
|
||||||
echo ""
|
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}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}"
|
echo -e " ${DIM}Leave blank to use the default Bulwark favicon.${RESET}"
|
||||||
prompt_value "Favicon URL" "$CFG_FAVICON_URL" "CFG_FAVICON_URL"
|
prompt_value "Favicon URL" "$CFG_FAVICON_URL" "CFG_FAVICON_URL"
|
||||||
echo ""
|
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}App logo shown in the sidebar (SVG recommended, 24×24 to 128×128px)${RESET}"
|
||||||
echo -e " ${DIM}Leave blank for no sidebar logo.${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"
|
prompt_value "App logo URL (light mode)" "$CFG_APP_LOGO_LIGHT_URL" "CFG_APP_LOGO_LIGHT_URL"
|
||||||
echo ""
|
echo ""
|
||||||
prompt_value "App logo URL (dark mode)" "$CFG_APP_LOGO_DARK_URL" "CFG_APP_LOGO_DARK_URL"
|
prompt_value "App logo URL (dark mode)" "$CFG_APP_LOGO_DARK_URL" "CFG_APP_LOGO_DARK_URL"
|
||||||
echo ""
|
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}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}"
|
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"
|
prompt_value "Login logo URL (light mode)" "$CFG_LOGIN_LOGO_LIGHT_URL" "CFG_LOGIN_LOGO_LIGHT_URL"
|
||||||
echo ""
|
echo ""
|
||||||
prompt_value "Login logo URL (dark mode)" "$CFG_LOGIN_LOGO_DARK_URL" "CFG_LOGIN_LOGO_DARK_URL"
|
prompt_value "Login logo URL (dark mode)" "$CFG_LOGIN_LOGO_DARK_URL" "CFG_LOGIN_LOGO_DARK_URL"
|
||||||
echo ""
|
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"
|
prompt_value "Website URL" "$CFG_LOGIN_WEBSITE_URL" "CFG_LOGIN_WEBSITE_URL"
|
||||||
echo ""
|
echo ""
|
||||||
prompt_value "Imprint / legal notice URL" "$CFG_LOGIN_IMPRINT_URL" "CFG_LOGIN_IMPRINT_URL"
|
prompt_value "Imprint / legal notice URL" "$CFG_LOGIN_IMPRINT_URL" "CFG_LOGIN_IMPRINT_URL"
|
||||||
echo ""
|
echo ""
|
||||||
prompt_value "Privacy policy URL" "$CFG_LOGIN_PRIVACY_POLICY_URL" "CFG_LOGIN_PRIVACY_POLICY_URL"
|
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 ""
|
echo ""
|
||||||
note "No branding configured. The app will use defaults."
|
note "No branding configured. The app will use defaults."
|
||||||
fi
|
fi
|
||||||
@@ -866,7 +917,9 @@ screen_summary() {
|
|||||||
echo -e " ${CYAN}${BOLD}SERVER${RESET}"
|
echo -e " ${CYAN}${BOLD}SERVER${RESET}"
|
||||||
echo -e " App Name .............. ${BOLD}${CFG_APP_NAME}${RESET}"
|
echo -e " App Name .............. ${BOLD}${CFG_APP_NAME}${RESET}"
|
||||||
echo -e " JMAP Server URL ....... ${BOLD}${CFG_JMAP_SERVER_URL}${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 " Stalwart Features ..... ${BOLD}${CFG_STALWART_FEATURES}${RESET}"
|
||||||
|
echo -e " Hostname .............. ${BOLD}${CFG_HOSTNAME}${RESET}"
|
||||||
echo -e " Port .................. ${BOLD}${CFG_PORT}${RESET}"
|
echo -e " Port .................. ${BOLD}${CFG_PORT}${RESET}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
@@ -905,9 +958,19 @@ screen_summary() {
|
|||||||
|
|
||||||
# Login page
|
# Login page
|
||||||
echo -e " ${CYAN}${BOLD}BRANDING${RESET}"
|
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" ]] && \
|
[[ -n "$CFG_FAVICON_URL" ]] && \
|
||||||
echo -e " Favicon URL ........... ${BOLD}${CFG_FAVICON_URL}${RESET}"
|
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" ]] && \
|
[[ -n "$CFG_APP_LOGO_LIGHT_URL" ]] && \
|
||||||
echo -e " App Logo (light) ...... ${BOLD}${CFG_APP_LOGO_LIGHT_URL}${RESET}"
|
echo -e " App Logo (light) ...... ${BOLD}${CFG_APP_LOGO_LIGHT_URL}${RESET}"
|
||||||
[[ -n "$CFG_APP_LOGO_DARK_URL" ]] && \
|
[[ -n "$CFG_APP_LOGO_DARK_URL" ]] && \
|
||||||
@@ -929,6 +992,13 @@ screen_summary() {
|
|||||||
fi
|
fi
|
||||||
echo ""
|
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
|
# Deployment
|
||||||
echo -e " ${CYAN}${BOLD}DEPLOYMENT${RESET}"
|
echo -e " ${CYAN}${BOLD}DEPLOYMENT${RESET}"
|
||||||
case "$CFG_DEPLOY_METHOD" in
|
case "$CFG_DEPLOY_METHOD" in
|
||||||
@@ -978,10 +1048,15 @@ write_env_file() {
|
|||||||
# -- JMAP Server (required) ---------------------------------------------------
|
# -- JMAP Server (required) ---------------------------------------------------
|
||||||
APP_NAME=${CFG_APP_NAME}
|
APP_NAME=${CFG_APP_NAME}
|
||||||
JMAP_SERVER_URL=${CFG_JMAP_SERVER_URL}
|
JMAP_SERVER_URL=${CFG_JMAP_SERVER_URL}
|
||||||
|
ALLOW_CUSTOM_JMAP_ENDPOINT=${CFG_ALLOW_CUSTOM_JMAP_ENDPOINT}
|
||||||
|
|
||||||
# -- Stalwart Mail Server Integration -----------------------------------------
|
# -- Stalwart Mail Server Integration -----------------------------------------
|
||||||
STALWART_FEATURES=${CFG_STALWART_FEATURES}
|
STALWART_FEATURES=${CFG_STALWART_FEATURES}
|
||||||
|
|
||||||
|
# -- Server Listen Address ----------------------------------------------------
|
||||||
|
HOSTNAME=${CFG_HOSTNAME}
|
||||||
|
PORT=${CFG_PORT}
|
||||||
|
|
||||||
# -- OAuth / OpenID Connect ---------------------------------------------------
|
# -- OAuth / OpenID Connect ---------------------------------------------------
|
||||||
OAUTH_ENABLED=${CFG_OAUTH_ENABLED}
|
OAUTH_ENABLED=${CFG_OAUTH_ENABLED}
|
||||||
ENVEOF
|
ENVEOF
|
||||||
@@ -1025,7 +1100,12 @@ LOG_LEVEL=${CFG_LOG_LEVEL}
|
|||||||
# -- Branding ------------------------------------------------------------------
|
# -- Branding ------------------------------------------------------------------
|
||||||
ENVEOF
|
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_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_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_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"
|
[[ -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_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"
|
[[ -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}"
|
echo -e " ${OK} Written ${BOLD}.env.local${RESET}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -819,7 +819,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
const affected = emails.filter(e => idSet.has(e.id));
|
const affected = emails.filter(e => idSet.has(e.id));
|
||||||
|
|
||||||
if (isUnifiedView) {
|
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[]>();
|
const byAccount = new Map<string, string[]>();
|
||||||
for (const e of affected) {
|
for (const e of affected) {
|
||||||
const acct = e.accountId || '__default__';
|
const acct = e.accountId || '__default__';
|
||||||
|
|||||||
Reference in New Issue
Block a user