Example plugins and templates for Bulwark Mail.
- 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
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
{
"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
}
}
}
| 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) |
| 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 |
{
"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"]
}
}
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.id; // "my-plugin"
api.plugin.version; // "1.0.0"
api.plugin.settings; // { enabled: true, ... } — user-configured values
// 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);
| 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 |
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.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.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.
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| Hook |
Permission |
Description |
| onVacationLoaded |
vacation:read |
Vacation responder loaded |
| onVacationUpdate |
vacation:write |
Vacation responder updated |
| 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 |
| Hook |
Permission |
Description |
| onThemeChange |
ui:observe |
Light/dark mode changed |
| onCustomThemeChange |
ui:observe |
Custom theme activated/deactivated |
| onLocaleChange |
ui:observe |
Language changed |
| Hook |
Permission |
Description |
| onToastShow |
ui:observe |
Toast notification shown |
| onToastDismiss |
ui:observe |
Toast dismissed |
| onBrowserNotification |
ui:observe |
Browser notification shown |
| 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 |
| Hook |
Permission |
Description |
| registerShortcut |
ui:keyboard |
Register a keyboard shortcut |
| onBeforeShortcut |
ui:keyboard |
Before shortcut executed |
| onAfterShortcut |
ui:keyboard |
After shortcut executed |
| 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 |
- 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:read — Read events, calendars, alerts
- calendar:write — Create/modify/delete events
- contacts:read — Read contacts and groups
- contacts:write — Create/modify/delete contacts
- files:read — Browse and download files
- files:write — Upload, delete, move files
- identity:read — Read sender identities
- identity:write — Create/modify identities
- filters:read — Read filter rules
- filters:write — Create/modify filter rules
- tasks:read — Read tasks
- tasks:write — Create/modify tasks
- templates:read — Read/apply templates
- templates:write — Create/modify templates
- smime:read — Read S/MIME keys and certificates
- vacation:read — Read vacation responder
- vacation:write — Modify vacation responder
- settings:read — Read settings changes
- settings:write — Modify settings
- security:read — Observe security changes
- auth:observe — Observe auth state
- 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:lifecycle — App lifecycle events (implicit, always granted)
- 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__, so do not bundle React
- Auto-disabled after 3 errors within 60 seconds
| 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 |
Plugins are ES modules. Use any bundler (esbuild, Rollup, webpack) to produce a single .js file.
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' } }
All plugins in this repository are released under the GNU Affero General Public License v3.