Bulwark Mail Plugins
Example plugins and templates for Bulwark Mail.
Quick Start
- Copy
plugin-template/ and rename it
- Edit
manifest.json with your plugin info
- Write your plugin code in
src/index.js
- Build:
npm run build
- ZIP the
dist/ output (manifest.json and your .js file at the ZIP root)
- Upload via Admin → Plugins in Bulwark Mail
Plugin Structure
my-plugin/
├── manifest.json # Plugin metadata (required)
├── src/
│ └── index.js # Plugin source code
├── dist/ # Build output (this gets zipped)
│ ├── manifest.json
│ └── index.js
├── package.json # Build tooling
└── README.md
manifest.json
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"author": "Your Name",
"description": "What this plugin does",
"type": "hook",
"permissions": ["email:read"],
"entrypoint": "index.js",
"minAppVersion": "1.0.0",
"settingsSchema": {
"enabled": {
"type": "boolean",
"label": "Enable feature",
"description": "Toggle this plugin's main feature",
"default": true
}
}
}
Fields
| Field |
Required |
Description |
id |
Yes |
Unique identifier. Lowercase alphanumeric + hyphens, min 2 chars. Pattern: /^[a-z0-9][a-z0-9-]*[a-z0-9]$/ |
name |
Yes |
Display name |
version |
Yes |
Semantic version |
author |
Yes |
Author name |
description |
Yes |
Short description |
type |
Yes |
"ui-extension", "sidebar-app", or "hook" |
permissions |
Yes |
Array of required permissions (see below) |
entrypoint |
Yes |
Path to the main JS file |
minAppVersion |
No |
Minimum Bulwark Mail version |
settingsSchema |
No |
User-configurable settings (shown in plugin settings UI) |
Plugin Types
| Type |
Use Case |
ui-extension |
Adds buttons, banners, or UI elements to existing views |
sidebar-app |
Adds a new panel in the sidebar |
hook |
Reacts to events without adding visible UI |
Settings Schema Types
{
"toggle": { "type": "boolean", "label": "Enable", "default": true },
"name": { "type": "string", "label": "Name", "default": "value" },
"count": {
"type": "number",
"label": "Count",
"default": 5,
"min": 1,
"max": 100
},
"mode": {
"type": "select",
"label": "Mode",
"default": "auto",
"options": ["auto", "manual"]
}
}
Plugin API
Every plugin exports an activate(api) function that receives the full Plugin API:
export function activate(api) {
// api.plugin — Plugin metadata and settings
// api.ui — Register UI components
// api.hooks — Subscribe to events
// api.toast — Show notifications
// api.storage — Persistent key-value storage
// api.log — Scoped logging
return {
dispose: () => {
/* cleanup */
},
};
}
export function deactivate() {
// Optional: extra cleanup when plugin is disabled
}
api.plugin
api.plugin.id; // "my-plugin"
api.plugin.version; // "1.0.0"
api.plugin.settings; // { enabled: true, ... } — user-configured values
api.ui — UI Registration
// Add button to email toolbar
api.ui.registerToolbarAction({
id: "my-action",
label: "Do Something",
icon: "🔧",
onClick: () => {
/* ... */
},
order: 100,
});
// Show banner above email content
api.ui.registerEmailBanner({
shouldShow: (email) => email.from.includes("important"),
render: ({ email }) => React.createElement("div", null, "Important!"),
});
// Add section below email content
api.ui.registerEmailFooter(MyFooterComponent);
// Add section in Settings
api.ui.registerSettingsSection({
id: "my-settings",
label: "My Plugin Settings",
icon: "⚙️",
render: MySettingsComponent,
});
// Add button to composer toolbar
api.ui.registerComposerAction({
id: "my-composer-action",
label: "Insert Template",
icon: "📝",
onClick: () => {
/* ... */
},
});
// Add widget in sidebar
api.ui.registerSidebarWidget({
id: "my-widget",
label: "My Widget",
render: MyWidgetComponent,
order: 50,
});
// Add item to email right-click menu
api.ui.registerContextMenuItem({
id: "my-context-action",
label: "Process Selected",
icon: "⚡",
onClick: (emailIds) => {
/* ... */
},
});
// Add item at bottom of navigation rail
api.ui.registerNavigationRailItem(MyNavComponent);
UI Slot Names
| Slot |
Location |
toolbar-actions |
Email toolbar area |
email-banner |
Above email content |
email-footer |
Below email content |
composer-toolbar |
Composer toolbar |
sidebar-widget |
Sidebar panel area |
settings-section |
Settings page |
context-menu-email |
Email right-click menu |
navigation-rail-bottom |
Bottom of navigation rail |
api.hooks — Event Hooks
All hooks return a Disposable with a .dispose() method. Call it to unsubscribe.
const sub = api.hooks.onEmailOpen((email) => {
api.log.info("Opened:", email.subject);
});
// Later: sub.dispose();
api.toast — Notifications
api.toast.success("Operation completed");
api.toast.error("Something went wrong");
api.toast.info("FYI: new emails arrived");
api.toast.warning("Attachment too large");
api.storage — Persistent Storage
Plugin-scoped key-value storage (uses localStorage with plugin:<id>: prefix).
api.storage.set("lastRun", Date.now());
const lastRun = api.storage.get("lastRun");
api.storage.remove("lastRun");
const allKeys = api.storage.keys();
api.log — Logging
api.log.debug("Verbose details");
api.log.info("Normal operation");
api.log.warn("Something unexpected");
api.log.error("Error occurred", err);
api.upfiles — Modifying uploaded attachments
// fileId string is obtained with onBeforeBlobUpload Hook
api.upfiles.get(fileId); // return Promise <File | null> if file is found or not.
api.upfiles.save(formedFileId, file) // take File and formedFileId
// to remove old version of file in storage.
Complete Hook Reference
Email Hooks (29)
| Hook |
Permission |
Description |
onEmailOpen |
email:read |
Email opened for reading |
onEmailClose |
email:read |
Email viewer closed |
onEmailContentRender |
email:read |
Email body rendered |
onThreadExpand |
email:read |
Thread conversation expanded |
onComposerOpen |
email:read |
Compose window opened |
onBeforeEmailSend |
email:send |
Before email is sent (can cancel) |
onAfterEmailSend |
email:send |
After email sent successfully |
onDraftAutoSave |
email:read |
Draft auto-saved |
onBeforeEmailDelete |
email:write |
Before email deleted |
onAfterEmailDelete |
email:write |
After email deleted |
onBeforeEmailMove |
email:write |
Before email moved to folder |
onAfterEmailMove |
email:write |
After email moved |
onEmailReadStateChange |
email:write |
Read/unread toggled |
onEmailStarToggle |
email:write |
Star/flag toggled |
onEmailSpamToggle |
email:write |
Spam status changed |
onEmailKeywordChange |
email:write |
Tag/keyword added or removed |
onMailboxChange |
email:read |
Active folder changed |
onMailboxesRefresh |
email:read |
Mailbox list refreshed |
onMailboxCreate |
email:write |
New folder created |
onMailboxRename |
email:write |
Folder renamed |
onMailboxDelete |
email:write |
Folder deleted |
onMailboxEmpty |
email:write |
Folder emptied |
onSearch |
email:read |
Search initiated |
onSearchResults |
email:read |
Search results received |
onEmailSelectionChange |
email:read |
Email selection changed |
onNewEmailReceived |
email:read |
New email arrived |
onPushConnectionChange |
email:read |
Push notification connection state changed |
onQuotaChange |
email:read |
Storage quota updated |
onBeforeBlobUpload |
email:blob-write |
Before Attachement is uploaded |
onBeforeDraftAutoSave |
email:write |
Before the Draft is automatically saved |
onBeforeEditDraft |
email:write |
Before the composer is populated by email |
onEmailsFetched |
email:read |
Emails fetched from server |
onSearchResults |
email:read |
Server returned results of a search |
Calendar Hooks (16)
| Hook |
Permission |
Description |
onCalendarEventOpen |
calendar:read |
Calendar event opened |
onBeforeEventCreate |
calendar:write |
Before event created |
onAfterEventCreate |
calendar:write |
After event created |
onBeforeEventUpdate |
calendar:write |
Before event updated |
onAfterEventUpdate |
calendar:write |
After event updated |
onBeforeEventDelete |
calendar:write |
Before event deleted |
onAfterEventDelete |
calendar:write |
After event deleted |
onEventRsvp |
calendar:write |
RSVP response sent |
onEventsImport |
calendar:write |
Calendar events imported |
onCalendarDateChange |
calendar:read |
Calendar date navigated |
onCalendarViewChange |
calendar:read |
View mode changed (day/week/month) |
onCalendarChange |
calendar:write |
Calendar created/updated/deleted |
onCalendarVisibilityToggle |
calendar:read |
Calendar shown/hidden |
onICalSubscriptionChange |
calendar:write |
iCal subscription changed |
onCalendarAlert |
calendar:read |
Calendar reminder triggered |
onCalendarAlertAcknowledge |
calendar:read |
Reminder dismissed |
Contact Hooks (12)
| Hook |
Permission |
Description |
onContactOpen |
contacts:read |
Contact opened |
onBeforeContactCreate |
contacts:write |
Before contact created |
onAfterContactCreate |
contacts:write |
After contact created |
onBeforeContactUpdate |
contacts:write |
Before contact updated |
onAfterContactUpdate |
contacts:write |
After contact updated |
onBeforeContactDelete |
contacts:write |
Before contact deleted |
onAfterContactDelete |
contacts:write |
After contact deleted |
onContactsImport |
contacts:write |
Contacts imported |
onContactSelectionChange |
contacts:read |
Contact selection changed |
onContactGroupChange |
contacts:write |
Contact group changed |
onContactGroupMemberChange |
contacts:write |
Group membership changed |
onContactMove |
contacts:write |
Contact moved to another address book |
File Hooks (15)
| Hook |
Permission |
Description |
onFileNavigate |
files:read |
File browser navigated |
onBeforeFileUpload |
files:write |
Before file upload |
onAfterFileUpload |
files:write |
After file uploaded |
onFileDownload |
files:read |
File downloaded |
onFileUploadCancel |
files:write |
File upload cancelled |
onDirectoryCreate |
files:write |
Directory created |
onBeforeFileDelete |
files:write |
Before file deleted |
onAfterFileDelete |
files:write |
After file deleted |
onFileRename |
files:write |
File renamed |
onFileMove |
files:write |
File moved |
onFileCopy |
files:write |
File copied |
onFileDuplicate |
files:write |
File duplicated |
onFileFavoriteToggle |
files:write |
File favorite toggled |
onFileSelectionChange |
files:read |
File selection changed |
onFileUndo |
files:write |
File operation undone |
Auth Hooks (8)
| Hook |
Permission |
Description |
onLogin |
auth:observe |
User logged in |
onBeforeLogout |
auth:observe |
Before logout |
onAfterLogout |
auth:observe |
After logout |
onAccountSwitch |
auth:observe |
Account switched |
onAccountAdd |
auth:observe |
Account added |
onAccountRemove |
auth:observe |
Account removed |
onTokenRefresh |
auth:observe |
Auth token refreshed |
onAuthReady |
auth:observe |
Auth system initialized |
Settings Hooks (7)
| Hook |
Permission |
Description |
onSettingChange |
settings:read |
A setting was changed |
onSettingsExport |
settings:read |
Settings exported |
onSettingsImport |
settings:read |
Settings imported |
onSettingsReset |
settings:read |
Settings reset to defaults |
onSettingsSync |
settings:read |
Settings synced |
onKeywordChange |
settings:read |
Keyword/tag configuration changed |
onTrustedSenderChange |
settings:read |
Trusted sender list changed |
Identity Hooks (6)
| Hook |
Permission |
Description |
onIdentitiesLoaded |
identity:read |
Identities loaded |
onIdentityCreate |
identity:write |
Identity created |
onIdentityUpdate |
identity:write |
Identity updated |
onIdentityDelete |
identity:write |
Identity deleted |
onIdentitySelect |
identity:read |
Active identity changed |
onSignatureRender |
identity:read |
Signature rendered |
Filter Hooks (4)
| Hook |
Permission |
Description |
onFiltersLoaded |
filters:read |
Filter rules loaded |
onFilterRuleChange |
filters:write |
Filter rule changed |
onFiltersSave |
filters:write |
Filters saved |
onSieveScriptChange |
filters:write |
Sieve script changed |
Task Hooks (6)
| Hook |
Permission |
Description |
onTasksLoaded |
tasks:read |
Tasks loaded |
onTaskCreate |
tasks:write |
Task created |
onTaskUpdate |
tasks:write |
Task updated |
onTaskDelete |
tasks:write |
Task deleted |
onTaskToggleComplete |
tasks:write |
Task completed/uncompleted |
onTaskFilterChange |
tasks:read |
Task filter changed |
Template Hooks (6)
| Hook |
Permission |
Description |
onTemplateCreate |
templates:write |
Template created |
onTemplateUpdate |
templates:write |
Template updated |
onTemplateDelete |
templates:write |
Template deleted |
onTemplateApply |
templates:read |
Template applied |
onTemplatesImport |
templates:write |
Templates imported |
onTemplateRender |
templates:read |
Template rendered |
S/MIME Hooks (4)
| Hook |
Permission |
Description |
onSmimeKeyImport |
smime:read |
S/MIME key imported |
onSmimeCertImport |
smime:read |
S/MIME certificate imported |
onSmimeKeyStateChange |
smime:read |
S/MIME key state changed |
onSmimeDefaultsChange |
smime:read |
S/MIME defaults changed |
Vacation Hooks (2)
| Hook |
Permission |
Description |
onVacationLoaded |
vacation:read |
Vacation responder loaded |
onVacationUpdate |
vacation:write |
Vacation responder updated |
UI Hooks (7)
| Hook |
Permission |
Description |
onViewChange |
ui:observe |
View/page changed |
onSidebarToggle |
ui:observe |
Sidebar opened/closed |
onSidebarCollapse |
ui:observe |
Sidebar collapsed |
onDeviceTypeChange |
ui:observe |
Device type changed (mobile/desktop) |
onColumnResize |
ui:observe |
Column resized |
onMobileBack |
ui:observe |
Mobile back navigation |
onMobileViewSwitch |
ui:observe |
Mobile view switched |
Theme Hooks (3)
| Hook |
Permission |
Description |
onThemeChange |
ui:observe |
Light/dark mode changed |
onCustomThemeChange |
ui:observe |
Custom theme activated/deactivated |
onLocaleChange |
ui:observe |
Language changed |
Toast Hooks (3)
| Hook |
Permission |
Description |
onToastShow |
ui:observe |
Toast notification shown |
onToastDismiss |
ui:observe |
Toast dismissed |
onBrowserNotification |
ui:observe |
Browser notification shown |
Drag & Drop Hooks (4)
| Hook |
Permission |
Description |
onDragStart |
ui:observe |
Drag started |
onDragEnd |
ui:observe |
Drag ended |
onEmailDrop |
email:write |
Email dropped on target |
onTagDrop |
email:write |
Tag dropped on email |
Keyboard Hooks (3)
| Hook |
Permission |
Description |
registerShortcut |
ui:keyboard |
Register a keyboard shortcut |
onBeforeShortcut |
ui:keyboard |
Before shortcut executed |
onAfterShortcut |
ui:keyboard |
After shortcut executed |
App Lifecycle Hooks (5)
| Hook |
Permission |
Description |
onAppReady |
app:lifecycle |
App fully loaded (implicit) |
onVisibilityChange |
app:lifecycle |
Tab visibility changed (implicit) |
onBeforeUnload |
app:lifecycle |
Before page unload (implicit) |
onAppError |
app:lifecycle |
App error occurred (implicit) |
onInterval |
app:lifecycle |
Periodic callback (min 60s, implicit) |
Account Security Hooks (5)
| Hook |
Permission |
Description |
onPasswordChange |
security:read |
Password changed |
onTotpChange |
security:read |
TOTP 2FA changed |
onAppPasswordChange |
security:read |
App password changed |
onEncryptionChange |
security:read |
Encryption settings changed |
onDisplayNameChange |
security:read |
Display name changed |
| Hook |
Permission |
Description |
onSidebarAppOpen |
ui:observe |
Sidebar app opened |
onSidebarAppClose |
ui:observe |
Sidebar app closed |
onSidebarAppChange |
ui:observe |
Active sidebar app changed |
All Permissions
Email
email:read — Read emails, mailboxes, search, quota
email:write — Modify emails (move, delete, tag, flag)
email:send — Send emails
email:blob-write — Modify Attachments before their upload
Calendar
calendar:read — Read events, calendars, alerts
calendar:write — Create/modify/delete events
Contacts
contacts:read — Read contacts and groups
contacts:write — Create/modify/delete contacts
Files
files:read — Browse and download files
files:write — Upload, delete, move files
Identity
identity:read — Read sender identities
identity:write — Create/modify identities
Filters
filters:read — Read filter rules
filters:write — Create/modify filter rules
Tasks
tasks:read — Read tasks
tasks:write — Create/modify tasks
Templates
templates:read — Read/apply templates
templates:write — Create/modify templates
S/MIME
smime:read — Read S/MIME keys and certificates
Vacation
vacation:read — Read vacation responder
vacation:write — Modify vacation responder
Settings
settings:read — Read settings changes
settings:write — Modify settings
Security
security:read — Observe security changes
Auth
auth:observe — Observe auth state
UI
ui:observe — Observe UI state changes (implicit, always granted)
ui:toolbar — Add toolbar buttons
ui:email-banner — Show email banners
ui:email-footer — Show email footers
ui:composer-toolbar — Add composer buttons
ui:sidebar-widget — Add sidebar widgets
ui:settings-section — Add settings sections
ui:context-menu — Add context menu items
ui:navigation-rail — Add navigation items
ui:keyboard — Register keyboard shortcuts
App
app:lifecycle — App lifecycle events (implicit, always granted)
Validation Rules
- Maximum plugin ZIP size: 5 MB
- Allowed file extensions:
.js, .mjs, .css, .json, .png, .svg, .woff2, .jpg, .jpeg, .webp
- Code is scanned for suspicious patterns (
eval(), new Function(), document.cookie, document.write, innerHTML =)
- Plugin ID format:
/^[a-z0-9][a-z0-9-]*[a-z0-9]$/ (min 2 chars)
- Plugins receive React, ReactDOM, and ReactJSX via
__PLUGIN_EXTERNALS__ — do not bundle React
- Auto-disabled after 3 errors within 60 seconds
Available Examples
| Plugin |
Type |
Description |
| hello-world |
hook |
Minimal plugin — logs lifecycle events |
| email-stats |
sidebar-app |
Sidebar widget showing email statistics |
| auto-tag |
hook |
Automatically tags emails based on rules |
| send-later |
ui-extension |
Adds "Send Later" button to composer |
| quick-notes |
sidebar-app |
Per-email sticky notes in the sidebar |
| calendar-agenda |
sidebar-app |
Agenda of upcoming calendar events in the sidebar |
Building Plugins
Plugins are ES modules. Use any bundler (esbuild, Rollup, webpack) to produce a single .js file.
Recommended: esbuild
npm install --save-dev esbuild
npx esbuild src/index.js --bundle --format=esm --outfile=dist/index.js \
--external:react --external:react-dom --external:react/jsx-runtime
Important: Do NOT bundle React
Bulwark Mail exposes React to plugins via __PLUGIN_EXTERNALS__. Mark React as external in your bundler:
// esbuild
{ external: ['react', 'react-dom', 'react/jsx-runtime'] }
// Rollup
{ external: ['react', 'react-dom', 'react/jsx-runtime'] }
// webpack
{ externals: { react: 'react', 'react-dom': 'react-dom', 'react/jsx-runtime': 'react/jsx-runtime' } }
License
All plugins in this repository are released under the GNU Affero General Public License v3.