- TypeScript 95.8%
- JavaScript 4.2%
| .claude | ||
| assets | ||
| docs | ||
| images | ||
| scripts | ||
| src | ||
| tests | ||
| .gitignore | ||
| .mcp.json | ||
| CHANGELOG.md | ||
| emdash-plugin.jsonc | ||
| LICENSE | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| SECURITY.md | ||
| tsconfig.json | ||
| vitest.config.ts | ||
Mail API for EmDash
Send EmDash's email (sign-in links, invites, plugin email) through your provider's HTTP API, with no SMTP.
- 10 providers: Brevo, Mailgun, Mailjet, Maileroo, Postmark, Mailchimp Transactional (Mandrill), Resend, SendGrid, MailerSend, Scaleway TEM.
- Pick a verified sender: the plugin lists the addresses and domains your provider has verified.
- See your account: plan, remaining credits and quotas, read from the provider API.
- Errors you can act on: the provider's own message, with the HTTP status, a fix hint and the raw response.
- Nothing lost: when the provider is down, email waits in an encrypted queue and is retried automatically.
- Encrypted keys, sandboxed plugin, admin in English, French and German.
Quick start
You need:
- EmDash ≥ 1.3.0 with a sandbox runner: Cloudflare Worker Loader (Workers Paid plan) or
@emdash-cms/sandbox-workerdon Node.js; EMDASH_ENCRYPTION_KEYon the site (it encrypts the API keys):npx emdash secrets generate, then on Cloudflarepnpm wrangler secret put EMDASH_ENCRYPTION_KEY. Keep it in a secret manager: a database backup is useless without it;- a scheduler for the retry queue: built in on Node.js; on Cloudflare, a
* * * * *cron trigger andscheduled: createScheduledHandler()(the EmDash templates include both).
Install:
pnpm add @devbitme/emdash-plugin-apimail
// astro.config.mjs
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { sandbox } from "@emdash-cms/cloudflare"; // or the workerd runner on Node
import apimail from "@devbitme/emdash-plugin-apimail";
export default defineConfig({
integrations: [
emdash({
sandboxed: [apimail],
sandboxRunner: sandbox(), // Node: "@emdash-cms/sandbox-workerd/sandbox"
}),
],
});
Then, in the admin, open Mail API:
- Open a provider (Configure), paste its key(s) and save.
- Check the Account section: plan and credits confirm that the key works.
- In Send a test email, pick a verified sender (or type one), enter your address and send. Leave Use as the default sender on to keep that sender.
- Tick Active provider and save, or click Activate on its card in the provider list.
- If several email transports are installed, select Mail API under Settings → Email. Under
astro dev, EmDash's console transport counts as one.
Providers
| Provider | Credentials | Senders from the API | Account status | Note |
|---|---|---|---|---|
| Brevo | API key (v3) | ✓ addresses | ✓ plan, credits | Authorise your server IPs if IP blocking is on. |
| Mailgun | API key + domain + region | ✓ the configured domain | ✓ domain state and type | The region must match the domain (US / EU). |
| Mailjet | API key + secret key | ✓ addresses and domains | — | EU by default, US optional. |
| Maileroo | Sending key | — (manual entry) | — | The key's IP restrictions apply. |
| Postmark | Server API token (+ message stream) | — (manual entry) | ✓ server, Live / Sandbox | |
| Mailchimp Transactional | Mandrill API key | ✓ domains | ✓ reputation, hourly quota | Paid Transactional add-on; a Marketing key does not work. |
| Resend | API key | ✓ domains | ✓ plan, daily and monthly quotas | Retries are idempotent. |
| SendGrid | API key | ✓ addresses and domains | ✓ plan, credits, reputation | EU host only for EU-regional subusers. |
| MailerSend | API token | ✓ addresses and domains | ✓ daily API quota | |
| Scaleway TEM | IAM secret key + project ID | ✓ domains | ✓ offer, emails this month | Hosted in Paris. |
Listing senders and reading the account need more than the right to send. A send-only key still sends email; the admin then explains which permission is missing and falls back to manual entry. Resend needs a Full access key; SendGrid needs read access to Sender Authentication and User Account; MailerSend needs Domains and Sender identity; Scaleway needs TransactionalEmailReadOnly; Mailgun needs the account key, not a domain sending key.
Features
Sender picker. The Sender tab and each provider's test form offer three modes: a verified address from the list, any address on a verified domain, or manual entry. The tab uses the active provider's list. The list is cached for the admin, so opening the tab never calls the provider; use Refresh from after adding a sender.
Account status. A provider's page shows what its API exposes: plan or account type, remaining credits, quotas with their reset date, and reputation. Pages only show the cached data, with its age, and never call the provider by themselves: saving a new key and Refresh from read it, and a message says how long the call took.
Errors you can act on. When a send fails, you get the provider's message verbatim (for example “unrecognised IP address …”) in the test form, the history, the queue and the logs. It comes with the HTTP status, the provider error code, a hint, a link to the provider page that fixes it, and the raw API response.
Retry queue. Network errors, timeouts, 429 and 5xx put the email in an encrypted queue, retried after 1, 5, 15 and 30 min, then 1 h, 2 h and every 4 h. The retry uses the provider active at that time, so switching providers is a manual fallback. Sign-in email expires after 15 minutes and other email after 24 h (both configurable). Permanent errors are not retried.
History and logs. Every send is recorded (recipient, subject, provider, status, message ID, attempts, duration, error), with filters, details, Resend, a 14-day chart and a dashboard widget. Bodies are kept only if you enable it, always encrypted, and never for sign-in or invite email. A daily task deletes entries older than 30 days by default.
Languages. The admin follows the EmDash admin locale: English, French or German.
Troubleshooting
- EmDash's Settings → Email → Send test email only says “Failed to send test email”. Use the plugin's own Send a test email form, which shows the provider's message, the hint and the raw response. The server log also has the full error (
[email:deliver] Provider "apimail" failed …). - The sender list is empty or shows HTTP 401 / 403. The key can send but not list senders: see the permissions above, or enter the address manually.
- “IP address not allowed”. Add your host's outgoing IP to the provider's allowlist; the error links to the right page.
- The queue never runs. Open Mail API → Security: it shows the scheduled tasks and a Repair button. On Cloudflare, check the cron trigger and the
scheduledhandler.
Security
| What | How it is protected |
|---|---|
| API keys | EmDash secret settings, encrypted with AES-GCM (EMDASH_ENCRYPTION_KEY). The admin shows only the last 5 characters of a saved key, never the key itself. |
| Queued and kept email bodies | Encrypted by the plugin with AES-GCM; its 256-bit key is itself an EmDash secret. |
| Error messages | Any configured secret echoed by an API is masked. |
| Cached sender list and account status | Plugin KV storage; no secret, only a fingerprint of the settings they were read with. |
To report a vulnerability, see SECURITY.md. The plugin asks for two capabilities: network:request, limited to the provider hosts listed in emdash-plugin.jsonc, and hooks.email-transport:register, to deliver EmDash email. Its only public route, GET /_emdash/api/plugins/apimail/logo?p=<provider>, serves the bundled logos. On Cloudflare, every operation is budgeted to fit the 10-subrequest limit of a sandboxed invocation.
Development
pnpm install
pnpm validate && pnpm typecheck && pnpm test && pnpm build
pnpm bundle # registry tarball; fails if backend.js exceeds 128 KB
pnpm i18n # after editing src/locales/*.json
pnpm logos # after changing assets/logos/*
Test against a local site with pnpm dev here and pnpm add file:../emdash-plugin-apimail in the site.
- npm:
pnpm publish --access public(prepackbuildsdist/). - EmDash registry: releases are signed by the
devbit.meAtmosphere account (thepublisherDID inemdash-plugin.jsonc):pnpm registry:login devbit.me, thenpnpm registry:publish. The tabs of the registry page come fromdocs/,CHANGELOG.mdandSECURITY.md(2000 characters at most each).publishwrites them, like the name, description and keywords, only with the first release: push later changes withpnpm registry:profile --yes(without--yes, it shows the diff). The banner is rendered fromassets/banner.svgbypnpm logos.
Bump version in package.json whenever capabilities, allowedHosts or storage change: installed sites consented to the previous contract. Assistant guidance (skills, EmDash docs, provider API notes) lives in .claude/.
Logos and license
Provider logos are trademarks of their respective owners, used only to identify each service in the admin (see assets/logos/SOURCES.md). The plugin is released under the MIT license.