mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
Stalwart does no BIMI, so sender icons were only ever favicons, which anyone controls for any domain. The webmail server now does the receiver side itself at /api/bimi, and avatars ask it only for messages whose own Authentication-Results say dmarc=pass for the exact From domain. - `lib/bimi/`: DMARC policy check (quarantine or reject, pct=100, the parent domain's sp and p for subdomains), the assertion record with fallback to the parent domain, mark certificate validation (chain to pinned DigiCert, GlobalSign and SSL.com VMC roots, BIMI key purpose, validity, SAN, SCTs present, CRL revocation, logo from the logotype extension), SVG Tiny PS checks and rendering to PNG, caching - logos backed only by the DNS record show by default; the admin setting "Require a mark certificate" (`BIMI_REQUIRE_CERTIFICATE`) limits them to VMC and CMC backed ones - a check mark beside the sender's name for certificate-backed logos - avatars try contact photo, BIMI logo, plugin avatar, favicon; the sender warning shield still wins, and logos follow the sender icons setting and the no-third-party-traffic mode - the two-label public suffix list moved to `lib/root-domain.ts` SCT signatures are not verified and only the default selector is used.
300 lines
12 KiB
TypeScript
300 lines
12 KiB
TypeScript
/**
|
|
* The admin panel's pages and their URLs.
|
|
*
|
|
* Every page lives at `/admin/<id>`; plugins, themes and the marketplace
|
|
* have a detail page below it (`/admin/plugins/<pluginId>`). The whole tree
|
|
* is one Next route (app/(main)/admin/[[...segments]]) and switching pages
|
|
* goes through history.pushState, so the sidebar never starts a route
|
|
* transition - the same way the user settings keep their permalinks (#733).
|
|
*
|
|
* This module is plain data: the connector registry imports it on the
|
|
* server, so no React in here. Icons live in the shell.
|
|
*/
|
|
|
|
export const ADMIN_PAGE_IDS = [
|
|
'dashboard',
|
|
'settings',
|
|
'auth',
|
|
'branding',
|
|
'policy',
|
|
'organisation',
|
|
'org-signatures',
|
|
'org-templates',
|
|
'org-security',
|
|
'org-sharing',
|
|
'plugins',
|
|
'themes',
|
|
'marketplace',
|
|
'version',
|
|
'telemetry',
|
|
'logs',
|
|
] as const;
|
|
|
|
export type AdminPageId = typeof ADMIN_PAGE_IDS[number];
|
|
|
|
export function isAdminPageId(value: string | null | undefined): value is AdminPageId {
|
|
return typeof value === 'string' && (ADMIN_PAGE_IDS as readonly string[]).includes(value);
|
|
}
|
|
|
|
/** `top` has no heading in the sidebar; the others do. */
|
|
export type AdminPageGroup = 'top' | 'organisation' | 'extensions' | 'system';
|
|
|
|
export interface AdminSectionDef {
|
|
/** The `id` the page gives its SettingsSection, for jump links and search. */
|
|
id: string;
|
|
title: string;
|
|
/** Extra words the sidebar search matches, besides the title. */
|
|
keywords?: string;
|
|
}
|
|
|
|
export interface AdminPageDef {
|
|
id: AdminPageId;
|
|
label: string;
|
|
/** One line under the page title. */
|
|
description: string;
|
|
group: AdminPageGroup;
|
|
/** Words the sidebar search matches, besides the label and the sections. */
|
|
keywords: string;
|
|
sections: readonly AdminSectionDef[];
|
|
/** Whether `/admin/<id>/<detail>` opens a detail page. */
|
|
hasDetail?: boolean;
|
|
}
|
|
|
|
export const ADMIN_PAGES: readonly AdminPageDef[] = [
|
|
{
|
|
id: 'dashboard',
|
|
label: 'Overview',
|
|
description: 'What this Bulwark is running and what needs attention',
|
|
group: 'top',
|
|
keywords: 'dashboard home status warnings accounts activity recent',
|
|
sections: [],
|
|
},
|
|
{
|
|
id: 'settings',
|
|
label: 'Server',
|
|
description: 'How this Bulwark connects and what the login page offers',
|
|
group: 'top',
|
|
keywords: 'settings application name jmap server url demo mode stalwart features search engine indexing',
|
|
sections: [
|
|
{ id: 'general', title: 'General', keywords: 'app name jmap url custom endpoint demo mode stalwart indexing' },
|
|
{ id: 'login', title: 'Login page', keywords: 'version 2fa totp code access token remember me heading subtitle' },
|
|
{ id: 'servers', title: 'JMAP servers', keywords: 'multi server domain auto pick list' },
|
|
{ id: 'wopi', title: 'Document editing', keywords: 'wopi collabora onlyoffice office files host url' },
|
|
{ id: 'bimi', title: 'Brand logos', keywords: 'bimi vmc cmc mark certificate sender logo icon dmarc' },
|
|
{ id: 'logging', title: 'Logging', keywords: 'log level format json text debug' },
|
|
{ id: 'sync', title: 'Settings sync', keywords: 'session secret devices' },
|
|
],
|
|
},
|
|
{
|
|
id: 'auth',
|
|
label: 'Authentication',
|
|
description: 'OAuth, single sign-on, admin access and session security',
|
|
group: 'top',
|
|
keywords: 'login password oidc openid',
|
|
sections: [
|
|
{ id: 'oauth', title: 'OAuth / OpenID Connect', keywords: 'client id secret issuer scopes private endpoints auto configure stalwart' },
|
|
{ id: 'sso', title: 'Single sign-on', keywords: 'auto sso redirect end session logout provider post-logout' },
|
|
{ id: 'admin-access', title: 'Admin access', keywords: 'stalwart admin dashboard password automatic' },
|
|
{ id: 'session', title: 'Session and security', keywords: 'cookie samesite frame ancestors parent origin embed' },
|
|
],
|
|
},
|
|
{
|
|
id: 'branding',
|
|
label: 'Branding',
|
|
description: 'Logos, colours and company details, per domain if needed',
|
|
group: 'top',
|
|
keywords: 'logo favicon icon company imprint privacy website domain',
|
|
sections: [
|
|
{ id: 'scope', title: 'Scope', keywords: 'domain host wildcard default' },
|
|
{ id: 'images', title: 'Images and logos', keywords: 'favicon app logo login logo light dark upload' },
|
|
{ id: 'pwa', title: 'Progressive web app', keywords: 'pwa icon screenshot short name description theme colour background colour install' },
|
|
{ id: 'company', title: 'Company information', keywords: 'company name imprint privacy policy website url' },
|
|
],
|
|
},
|
|
{
|
|
id: 'policy',
|
|
label: 'User policy',
|
|
description: 'Which features users get and which settings they may change',
|
|
group: 'top',
|
|
keywords: 'features gates restrictions lock hide users',
|
|
sections: [
|
|
{ id: 'features', title: 'Features', keywords: 'calendar contacts files smime templates keywords sidebar apps unified mailbox archive custom from debug export' },
|
|
{ id: 'push-relays', title: 'Push relays', keywords: 'notifications relay url default lock' },
|
|
{ id: 'sidebar-apps', title: 'Default sidebar apps', keywords: 'apps rail intranet inline tab icon' },
|
|
{ id: 'restrictions', title: 'Locked settings', keywords: 'restrict hide lock font size density layout delete action reply mode session timeout notifications debug' },
|
|
],
|
|
},
|
|
{
|
|
id: 'organisation',
|
|
label: 'Domains and groups',
|
|
description: 'Give some domains or groups their own settings, see what applies to a user, and keep the policy in one file',
|
|
group: 'organisation',
|
|
keywords: 'organisation organization scope domain group department customer tenant effective policy export import file json',
|
|
sections: [
|
|
{ id: 'scopes', title: 'Domains and groups', keywords: 'scope domain group add remove order' },
|
|
{ id: 'effective', title: 'Effective policy', keywords: 'user address lookup applies source' },
|
|
{ id: 'policy-file', title: 'Policy file', keywords: 'export import download upload json backup git' },
|
|
],
|
|
},
|
|
{
|
|
id: 'org-signatures',
|
|
label: 'Signatures',
|
|
description: 'The signature and legal disclaimer every message carries',
|
|
group: 'organisation',
|
|
keywords: 'signature disclaimer footer legal imprint placeholder phone title department lock',
|
|
sections: [
|
|
{ id: 'signature', title: 'Signature', keywords: 'template placeholder name email lock' },
|
|
{ id: 'details', title: 'User details', keywords: 'phone mobile title department fields' },
|
|
{ id: 'disclaimer', title: 'Disclaimer', keywords: 'footer legal notice confidentiality' },
|
|
],
|
|
},
|
|
{
|
|
id: 'org-templates',
|
|
label: 'Templates',
|
|
description: 'Templates every user can pick, and what users may keep of their own',
|
|
group: 'organisation',
|
|
keywords: 'templates library snippets canned responses team personal',
|
|
sections: [
|
|
{ id: 'library', title: 'Organisation library', keywords: 'templates read-only shared add edit' },
|
|
{ id: 'user-templates', title: 'Personal and team templates', keywords: 'personal team group lock hide' },
|
|
],
|
|
},
|
|
{
|
|
id: 'org-security',
|
|
label: 'Security',
|
|
description: 'Warnings about mail from and to outside the organisation, sign-out times and outside connections',
|
|
group: 'organisation',
|
|
keywords: 'security external warning label recipients domains session timeout idle sign out third party traffic privacy',
|
|
sections: [
|
|
{ id: 'external', title: 'External mail', keywords: 'external sender label recipient warning organisation domains contacts' },
|
|
{ id: 'sessions', title: 'Sessions', keywords: 'idle timeout maximum length sign out warning' },
|
|
{ id: 'third-party', title: 'Third-party traffic', keywords: 'telemetry favicon relay directory outgoing hosts privacy gdpr' },
|
|
],
|
|
},
|
|
{
|
|
id: 'org-sharing',
|
|
label: 'Sharing',
|
|
description: 'Tags in shared mailboxes, delegates and recovering deleted items',
|
|
group: 'organisation',
|
|
keywords: 'sharing shared mailbox tags delegate access recover deleted items archive',
|
|
sections: [
|
|
{ id: 'shared-tags', title: 'Shared tags', keywords: 'tags labels colours group mailbox edit lock' },
|
|
{ id: 'delegates', title: 'Delegates', keywords: 'delegate access mail calendar contacts rights read write' },
|
|
{ id: 'recovery', title: 'Recover deleted items', keywords: 'recover restore deleted archive enterprise' },
|
|
],
|
|
},
|
|
{
|
|
id: 'plugins',
|
|
label: 'Plugins',
|
|
description: 'Plugins installed for every user, and what users may install themselves',
|
|
group: 'extensions',
|
|
keywords: 'extensions addons upload zip approve permissions configure force enable',
|
|
sections: [
|
|
{ id: 'approvals', title: 'Approval requests', keywords: 'approve deny user uploaded code review' },
|
|
{ id: 'installed', title: 'Installed plugins', keywords: 'enable disable remove roll back configure' },
|
|
{ id: 'policy', title: 'Plugin policy', keywords: 'plugins enabled user uploads require approval' },
|
|
],
|
|
hasDetail: true,
|
|
},
|
|
{
|
|
id: 'themes',
|
|
label: 'Themes',
|
|
description: 'Themes installed for every user, defaults and what users may pick',
|
|
group: 'extensions',
|
|
keywords: 'colour color mode dark light default upload zip',
|
|
sections: [
|
|
{ id: 'installed', title: 'Installed themes', keywords: 'enable disable remove roll back' },
|
|
{ id: 'policy', title: 'Theme policy', keywords: 'themes enabled user uploads default theme colour mode lock' },
|
|
{ id: 'offered', title: 'Offered to users', keywords: 'built-in themes hide' },
|
|
],
|
|
hasDetail: true,
|
|
},
|
|
{
|
|
id: 'marketplace',
|
|
label: 'Marketplace',
|
|
description: 'Plugins and themes from the extension directory',
|
|
group: 'extensions',
|
|
keywords: 'directory install update browse extensions advisories',
|
|
sections: [],
|
|
hasDetail: true,
|
|
},
|
|
{
|
|
id: 'version',
|
|
label: 'Updates',
|
|
description: 'The running version and the hourly update check',
|
|
group: 'system',
|
|
keywords: 'version release upgrade check security advisory',
|
|
sections: [
|
|
{ id: 'status', title: 'Status', keywords: 'running latest release severity advisory' },
|
|
{ id: 'schedule', title: 'Schedule', keywords: 'last checked next' },
|
|
{ id: 'source', title: 'Source', keywords: 'endpoint disabled env' },
|
|
],
|
|
},
|
|
{
|
|
id: 'telemetry',
|
|
label: 'Usage stats',
|
|
description: 'The anonymous daily heartbeat, off unless you turn it on',
|
|
group: 'system',
|
|
keywords: 'telemetry heartbeat anonymous consent privacy',
|
|
sections: [
|
|
{ id: 'status', title: 'Status', keywords: 'enable disable consent' },
|
|
{ id: 'accounts', title: 'Account activity', keywords: 'active total logins' },
|
|
{ id: 'endpoint', title: 'Endpoint', keywords: 'collector url' },
|
|
{ id: 'payload', title: 'Payload preview', keywords: 'send now json' },
|
|
],
|
|
},
|
|
{
|
|
id: 'logs',
|
|
label: 'Audit log',
|
|
description: 'Who changed what in this panel, and when',
|
|
group: 'system',
|
|
keywords: 'audit history actions login config policy ip',
|
|
sections: [],
|
|
},
|
|
];
|
|
|
|
const PAGE_BY_ID = new Map(ADMIN_PAGES.map((page) => [page.id, page]));
|
|
|
|
export function adminPageDef(id: AdminPageId): AdminPageDef {
|
|
return PAGE_BY_ID.get(id)!;
|
|
}
|
|
|
|
export const ADMIN_ROOT = '/admin';
|
|
|
|
/** Where in the admin panel a pathname points. `page` null is the index. */
|
|
export interface AdminLocation {
|
|
page: AdminPageId | null;
|
|
/** The id or slug of a detail page, for pages that have them. */
|
|
detail: string | null;
|
|
}
|
|
|
|
/**
|
|
* Reads the admin location from a pathname (without the base path, as
|
|
* `usePathname()` gives it). Null when the path is not inside /admin.
|
|
* An unknown page name counts as the index rather than a 404: the login and
|
|
* password routes are separate pages and never get here.
|
|
*/
|
|
export function parseAdminPath(pathname: string): AdminLocation | null {
|
|
if (pathname !== ADMIN_ROOT && !pathname.startsWith(`${ADMIN_ROOT}/`)) return null;
|
|
const segments = pathname
|
|
.slice(ADMIN_ROOT.length)
|
|
.split('/')
|
|
.filter(Boolean)
|
|
.map((segment) => {
|
|
try { return decodeURIComponent(segment); } catch { return segment; }
|
|
});
|
|
if (segments.length === 0) return { page: null, detail: null };
|
|
const [first, ...rest] = segments;
|
|
if (!isAdminPageId(first)) return { page: null, detail: null };
|
|
const def = adminPageDef(first);
|
|
const detail = def.hasDetail && rest.length > 0 ? rest.join('/') : null;
|
|
return { page: first, detail };
|
|
}
|
|
|
|
/** The app-relative path of a page, or of one of its detail pages. */
|
|
export function adminPath(page?: AdminPageId | null, detail?: string | null): string {
|
|
if (!page) return ADMIN_ROOT;
|
|
const base = `${ADMIN_ROOT}/${page}`;
|
|
return detail ? `${base}/${encodeURIComponent(detail)}` : base;
|
|
}
|