Skip to content

drudge/emdash-plugin-postmark

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.

CI npm version License: 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";
import emdash from "emdash/astro";
import { postmarkPlugin } from "emdash-plugin-postmark";

export default defineConfig({
  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.

Setting KV key Env var override Notes
Server API Token settings:serverToken POSTMARK_API_KEY Required. From Postmark → your server → API Tokens.
Account API Token settings:accountToken POSTMARK_ACCOUNT_TOKEN Optional. Enables the live sender-signature picker. From Postmark → Account → API Tokens.
Default Sender settings:defaultSender POSTMARK_SENDER_ADDRESS Required. Must be a verified sender signature.
Force Default Sender settings:forceSender Always use the Default Sender regardless of caller-supplied from.
Message Stream settings:defaultStream POSTMARK_STREAM_NAME Defaults to outbound. Live-picker dropdown.
Track Opens settings:trackOpens Maps to Postmark TrackOpens.
Track Links settings:trackLinks None / HtmlOnly / HtmlAndText / TextOnly.
Force HTML settings:forceHtml Wrap text-only messages in minimal HTML so links render in all clients.
Default Tag settings:defaultTag Postmark Tag added to every send when "Auto-tag with source" is off.
Auto-tag with Source settings:autoTagSource Postmark Tag = source identifier (system, plugin:newsletter, …).
Include Source as Metadata settings:metadataSource Adds Metadata.source = "<source>" to every send.
Retry on Transient Errors settings:retryEnabled 3 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";

export default definePlugin({
  capabilities: ["email:send"],
  hooks: {
    "content:afterPublish": async (event, ctx) => {
      await ctx.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:

Source Postmark Tag
EmDash core (magic links, invites, password resets) system
Plugin emails plugin:<plugin-id>
The "Send test email" button plugin: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 Error Plugin behavior Fix
10 (Invalid token) Non-transient, logged + thrown Re-check the Server API Token. Make sure it's the server token, not the account token.
300 (Invalid sender) Non-transient, logged + thrown Add and verify the sender in Postmark → Sender Signatures, then update Default Sender.
406 (Inactive recipient) Non-transient Recipient is suppressed. Reactivate in Postmark → Suppressions.
100 (Maintenance) Transient — retried Postmark window — usually resolves automatically.
HTTP 429 Transient — retried Rate-limited; the retry helper backs off. Increase your Postmark plan if persistent.
HTTP 5xx Transient — retried Postmark 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

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors