Skip to content

Repository files navigation

emdash-plugin-postmark

Send EmDash CMS emails through Postmark — settings parity with the official WordPress plugin, plus a delivery log, real-time webhook tracking (deliveries, bounces, opens, clicks), and live stream + sender pickers.

CInpm versionLicense: MIT

Postmark Settings appears in the EmDash admin sidebar after install.

Features

Drop-in replacement for EmDash's email transport — registers as the exclusive email:deliver provider
Full settings parity with the official WordPress Postmark plugin (token, sender, stream, force-from, force-HTML, track opens / links, tags, metadata)
Environment-variable overrides compatible with WordPress (POSTMARK_API_KEY, POSTMARK_SENDER_ADDRESS, POSTMARK_STREAM_NAME)
Live picker — Default Stream dropdown populated from /message-streams
Live picker — Default Sender dropdown populated from /senders (with optional Account API Token)
Webhook receiver — secured with a per-installation secret, transitions deliveries through delivered → bounced → spam_complaint, counts opens & clicks
Test connection button validates the token without sending email
Send test email button sends to the signed-in admin
Delivery log with status badges + relative timestamps; queryable by messageId (used by the webhook receiver)
Source-aware Postmark Tag + Metadata — filter EmDash sends by source in the Postmark dashboard
Retry on 5xx, 429, and network errors with exponential backoff
Sandbox-compatible — no Node built-ins; network:request capability restricted to api.postmarkapp.com

Install

pnpm add emdash-plugin-postmark
# or
npm install emdash-plugin-postmark

Register in your EmDash site's astro.config.mjs:

import{defineConfig}from"astro/config";importemdashfrom"emdash/astro";import{postmarkPlugin}from"emdash-plugin-postmark";exportdefaultdefineConfig({integrations: [emdash({plugins: [postmarkPlugin()],}),],});

For a sandboxed install (Cloudflare Workers), use sandboxed: [postmarkPlugin()] instead. The plugin is identical in both modes.

Configure

Open the EmDash admin → Postmark Settings in the sidebar.

SettingKV keyEnv var overrideNotes
Server API Tokensettings:serverTokenPOSTMARK_API_KEYRequired. From Postmark → your server → API Tokens.
Account API Tokensettings:accountTokenPOSTMARK_ACCOUNT_TOKENOptional. Enables the live sender-signature picker. From Postmark → Account → API Tokens.
Default Sendersettings:defaultSenderPOSTMARK_SENDER_ADDRESSRequired. Must be a verified sender signature.
Force Default Sendersettings:forceSenderAlways use the Default Sender regardless of caller-supplied from.
Message Streamsettings:defaultStreamPOSTMARK_STREAM_NAMEDefaults to outbound. Live-picker dropdown.
Track Openssettings:trackOpensMaps to Postmark TrackOpens.
Track Linkssettings:trackLinksNone / HtmlOnly / HtmlAndText / TextOnly.
Force HTMLsettings:forceHtmlWrap text-only messages in minimal HTML so links render in all clients.
Default Tagsettings:defaultTagPostmark Tag added to every send when "Auto-tag with source" is off.
Auto-tag with Sourcesettings:autoTagSourcePostmark Tag = source identifier (system, plugin:newsletter, …).
Include Source as Metadatasettings:metadataSourceAdds Metadata.source = "<source>" to every send.
Retry on Transient Errorssettings:retryEnabled3 attempts with exponential backoff. Default on.

Resolution order: KV → environment variable → built-in default. The admin UI labels each field with its source.

Activate as the email provider

After saving the token, click Make active provider. EmDash will start routing all transactional email — magic-links, invites, password resets, plugin emails — through Postmark.

Webhooks

The settings page displays a webhook URL like:

https://your-site.example.com/_emdash/api/plugins/postmark/webhook?key=<secret>

Paste it into Postmark → Servers → your server → Streams → your stream → Webhooks. Enable the Delivery, Bounce, Spam Complaint, Open, and Click events you care about.

What you get:

  • Delivery rows transition sent → delivered once Postmark accepts the message at the recipient MX.
  • Bounces and spam complaints update the row status with the bounce type and description.
  • Opens and clicks are aggregated on the row (opens, clicks, firstOpenAt, lastClickedUrl).

The Regenerate webhook secret button rotates the secret. The old URL stops working immediately — update Postmark's webhook config when you rotate.

Security note: Postmark webhooks are not signed (the documented options are URL-embedded Basic Auth or a shared secret in the URL). The plugin uses a 192-bit URL-safe random secret stored in state:webhookSecret. Bad keys get a 401 with no body. Treat the URL as a credential.

Usage from other plugins

Other plugins can send email via the standard EmDash email pipeline once Postmark is the active provider:

import{definePlugin}from"emdash";exportdefaultdefinePlugin({capabilities: ["email:send"],hooks: {"content:afterPublish": async(event,ctx)=>{awaitctx.email!.send({to: "team@example.com",subject: `Just published: ${event.content.title}`,text: `${event.content.title} is live.`,});},},});

If autoTagSource is on, the email lands in Postmark with Tag = "plugin:<your-id>" so you can slice metrics by sender.

Tags & metadata in Postmark

When Auto-tag with source is on, every email is tagged with its EmDash source:

SourcePostmark Tag
EmDash core (magic links, invites, password resets)system
Plugin emailsplugin:<plugin-id>
The "Send test email" buttonplugin:test

This lets you filter the Postmark Activity screen, Deliverability stats, and Outbound webhooks by source. Combine with Include Source as Metadata for richer Postmark searches.

Troubleshooting

Postmark ErrorPlugin behaviorFix
10 (Invalid token)Non-transient, logged + thrownRe-check the Server API Token. Make sure it's the server token, not the account token.
300 (Invalid sender)Non-transient, logged + thrownAdd and verify the sender in Postmark → Sender Signatures, then update Default Sender.
406 (Inactive recipient)Non-transientRecipient is suppressed. Reactivate in Postmark → Suppressions.
100 (Maintenance)Transient — retriedPostmark window — usually resolves automatically.
HTTP 429Transient — retriedRate-limited; the retry helper backs off. Increase your Postmark plan if persistent.
HTTP 5xxTransient — retriedPostmark service issue. The retry helper handles short outages.

The Recent deliveries table on the settings page shows the last 25 attempts with status badges, source, stream, opens, and timestamps.

FAQ

Is the plugin sandbox-safe?

Yes. The plugin is shipped in EmDash's standard format (no Node built-ins). It declares only hooks.email-transport:register and network:request capabilities, with allowedHosts: ["api.postmarkapp.com"]. Sandboxed installs see a capability consent dialog listing exactly that.

Why two different tokens?

Postmark has two token types:

  • Server tokens — scoped to a single Postmark "server" (which is really an outbound mail stream). Used for sending email and managing message streams.
  • Account tokens — scoped to your whole Postmark account. Used to enumerate sender signatures (/senders).

The plugin needs a server token to send. The account token is optional — supply it only if you want the Default Sender dropdown populated automatically.

Will it work with multiple senders?

The plugin currently exposes one Default Sender. Per-source routing rules and multi-server support are on the roadmap.

Can I migrate from another email provider plugin?

Yes — EmDash's email:deliver hook is exclusive, so installing Postmark and clicking Make active provider swaps providers atomically. The previous provider's plugin can stay installed (it just stops receiving traffic).

What if Postmark is down?

The retry helper handles transient outages. For longer outages, you'll see failed rows in the delivery log. Email pipeline errors propagate to callers (e.g., the user-invite endpoint will fail), so the operator sees the issue.

Development

git clone https://github.com/drudge/emdash-plugin-postmark
cd emdash-plugin-postmark
pnpm install
pnpm test# run vitest
pnpm test:coverage # with coverage report
pnpm typecheck # tsc --noEmit
pnpm build # produce dist/ via tsdown

The plugin runs against emdash >= 0.7.0. Tests use vitest with mocked storage / KV / fetch — no live Postmark account required for CI.

To run the plugin against a local EmDash dev site, point a workspace path dependency at this repo's root and add postmarkPlugin() to your astro.config.mjs.

License

MIT © Nicholas Penree

About

Send EmDash CMS emails through Postmark

Topics

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages