@eliophot/emdash-plugin-smtp (0.1.10)

Published 2026-04-28 15:40:58 +00:00 by fbardel

Installation

@eliophot:registry=
npm install @eliophot/emdash-plugin-smtp@0.1.10
"@eliophot/emdash-plugin-smtp": "0.1.10"

About this package

@eliophot/emdash-plugin-smtp

Native EmDash CMS plugin that delivers transactional email through one of three providers, chosen at runtime from the admin panel:

  • Brevo (formerly Sendinblue) - https://api.brevo.com/v3/smtp/email
  • Mailjet - https://api.mailjet.com/v3.1/send
  • Mailchimp Transactional / Mandrill - https://mandrillapp.com/api/1.0/messages/send

It implements the exclusive email:deliver hook, plus email:beforeSend and email:afterSend for full audit logging. Every delivery attempt - including retries and failures - is logged to ctx.log and persisted in a dedicated storage collection that powers the admin "Email Logs" page.

Plugin id: eliophot-smtp Format: native (registers in plugins: [], not sandboxed: []) Capabilities: email:provide, email:intercept, network:fetch


Table of contents

  1. Requirements
  2. Forgejo private registry - first-time setup
  3. Install in your EmDash site
  4. Register the plugin in astro.config.mjs
  5. Configure the provider
  6. Send a test email
  7. Logs and observability
  8. API routes
  9. Local development
  10. Publishing a new version to Forgejo
  11. Troubleshooting

Requirements

  • EmDash ^0.1.0
  • Node.js >= 18.18 (uses native fetch, btoa)
  • pnpm >= 8
  • A Forgejo account on https://git.eliophot.dev with a Personal Access Token (PAT)
  • An account on at least one of: Brevo, Mailjet, Mailchimp Transactional (Mandrill)

Forgejo private registry - first-time setup

The package lives at:

https://git.eliophot.dev/eliophot/-/packages/npm/@eliophot%2Femdash-plugin-smtp

The npm registry URL exposed by Forgejo is:

https://git.eliophot.dev/api/packages/eliophot/npm/

1. Generate a Personal Access Token

Open https://git.eliophot.dev/user/settings/applications and create a token with these scopes:

Action you need Required scope
pnpm add / pnpm install (consume packages) read:package
pnpm publish (push new versions of this plugin) read:package + write:package
pnpm unpublish additionally delete:package

Copy the token - Forgejo only shows it once.

2. Configure pnpm/npm to use the registry for the @eliophot scope

Two equivalent options. Pick one.

Option A - copy .npmrc.example into your site repo

This repo ships an .npmrc.example file. From your EmDash site repo (the one that consumes the plugin), copy it as .npmrc:

cp path/to/.npmrc.example .npmrc
export FORGEJO_TOKEN=fjpat_xxxxxxxxxxxxxxxxxxxx   # bash / zsh
# or, in PowerShell:
$env:FORGEJO_TOKEN = "fjpat_xxxxxxxxxxxxxxxxxxxx"

The .npmrc references ${FORGEJO_TOKEN}, so the literal token never lands in git.

Option B - interactive pnpm login

pnpm config set @eliophot:registry https://git.eliophot.dev/api/packages/eliophot/npm/
pnpm login --scope=@eliophot --registry=https://git.eliophot.dev/api/packages/eliophot/npm/
# username : your Forgejo username (e.g. eliophot)
# password : the Personal Access Token from step 1
# email    : whatever Forgejo has on file

This writes the token to your user-level ~/.npmrc (or %USERPROFILE%\.npmrc on Windows).

3. Verify

pnpm view @eliophot/emdash-plugin-smtp versions

If you see a version array, you're ready.


Install in your EmDash site

pnpm add @eliophot/emdash-plugin-smtp

Register the plugin in astro.config.mjs

This is a native plugin - register it in plugins: [], not sandboxed: []:

// astro.config.mjs
import { defineConfig } from "astro/config";
import emdash from "emdash/integration";
import { smtpPlugin } from "@eliophot/emdash-plugin-smtp";

export default defineConfig({
  integrations: [
    emdash({
      plugins: [
        smtpPlugin({
          // Optional: defaults to "brevo" on first install
          defaultProvider: "brevo",
          // Optional: number of retries on transient errors (default 1)
          maxRetries: 1,
        }),
      ],
    }),
  ],
});

The plugin follows the native plugin two-file pattern:

  • src/index.ts - the PluginDescriptor factory (smtpPlugin()), imported at build time in astro.config.mjs.
  • src/sandbox-entry.ts - the createPlugin(options) factory that wraps definePlugin(), loaded at request time by EmDash via the entrypoint field.

The plugin registers two admin pages and one widget:

  • /_emdash/admin/plugins/eliophot-smtp/settings - provider selection, credentials, test send
  • /_emdash/admin/plugins/eliophot-smtp/logs - paginated audit log of every delivery attempt
  • Dashboard widget "Email Status" - active provider, credentials health, 24h/7d counters

Configure the provider

Open Admin → Plugins → Email Settings and fill in:

  1. Active provider - Brevo, Mailjet or Mandrill.
  2. Default sender - From email, From name, optional Reply-To. Used whenever ctx.email.send() is called without a from address.
  3. Provider credentials - only the active provider's keys are required, but you may pre-fill all three so you can switch instantly.
  4. Logging - retention (days, 0 = forever) and ctx.log verbosity.

Where to find each provider's API credentials

Provider Where Field(s) needed
Brevo https://app.brevo.com/settings/keys/api brevoApiKey
Mailjet https://app.mailjet.com/account/apikeys mailjetApiKey (public) + mailjetApiSecret
Mailchimp Transactional (Mandrill) https://mandrillapp.com/settings mandrillApiKey

Security note

API keys are stored as secret-typed settings in EmDash KV (encrypted at rest, never returned by the API after save - the admin UI shows •••configured••• for already-set secrets). All hook handlers redact these values before logging via src/lib/redact.ts.


Send a test email

From Email Settings, scroll to Send test email, enter a recipient and click Send test. Under the hood this calls POST /_emdash/api/plugins/eliophot-smtp/test-send and surfaces the provider's status, latency and providerMessageId.

Programmatic equivalent:

await ctx.email!.send({
  to: "you@example.com",
  subject: "Hello from EmDash",
  text: "It works!",
  // html: "<p>It works!</p>", // optional
});

The email:beforeSend hook injects the default from / replyTo from settings if you omit them, so the call above is enough.


Logs and observability

Console logs (ctx.log)

Every step of the pipeline emits a structured log line. Useful events:

Event Level Where
email:beforeSend info normalisation succeeded
email:beforeSend:invalid error message rejected (no recipient/subject/body)
email:deliver:attempt info each attempt, including retries
email:<provider>:success info delivery accepted by provider
email:<provider>:retry warn transient failure, will retry
email:<provider>:failure error final failure after retries
email:deliver:misconfigured error ProviderConfigurationError (missing key)
cron:smtp-log-purge:done info retention purge completed

The verbosity is filtered server-side by the Server log level setting.

Persistent log entries (storage.emailLogs)

Each delivery attempt writes a document to the emailLogs storage collection (declared in the descriptor with composite indexes on ["provider", "timestamp"] and ["status", "timestamp"]). The shape:

{
  timestamp: "2026-04-28T09:55:12.000Z",
  provider: "brevo",
  status: "success" | "failure" | "retry" | "skipped",
  source: string,                  // who called ctx.email.send()
  subject: string,
  from: string,                    // "Acme <hello@example.com>"
  to: string[],
  cc?: string[], bcc?: string[],
  attempts: number,                // 1 = first try
  latencyMs: number,
  providerMessageId?: string,
  responseStatus?: number,
  responseSnippet?: string,        // truncated to 2 KB
  error?: { name, message, stack },
  requestSummary: { hasHtml, hasText, attachmentsCount, tags, headers }
                                   // never contains credentials or body
}

The admin Email Logs page lets you filter by provider/status, paginate, and inspect the full JSON of any entry. Old entries are purged daily at 03:00 UTC by a built-in cron hook (configurable retention).


API routes

Mounted under /_emdash/api/plugins/eliophot-smtp/<route>.

Method Route Purpose
GET status Active provider + counters (24h/7d success/failure) + credentials health
GET logs?limit=&cursor=&provider=&status= Paginated audit log
GET settings Current settings (secrets masked)
POST settings/save Persist a partial settings update (skips empty/placeholder values)
POST test-send Send a test message through the active provider
GET providers List of supported providers

These routes have no built-in public auth - they sit behind EmDash's admin session middleware. Don't expose them externally.

Curl example:

curl -s "https://your-site.com/_emdash/api/plugins/eliophot-smtp/status" \
  -H "Cookie: <admin session>" | jq

Local development

git clone https://git.eliophot.dev/eliophot/emdash-eliophot-plugin-smtp.git
cd emdash-eliophot-plugin-smtp
pnpm install
pnpm build           # one-shot build
pnpm dev             # tsdown --watch
pnpm typecheck       # tsc --noEmit

To link a local checkout into a site you're developing in parallel:

# in the plugin repo
pnpm link --global
# in the site repo
pnpm link --global @eliophot/emdash-plugin-smtp

Then run the site (pnpm dev) and trigger a send through the admin panel - tsdown --watch rebuilds on file changes.

Project layout

src/
├── index.ts                 # PluginDescriptor factory + SmtpPluginOptions (Vite, build-time)
├── sandbox-entry.ts         # createPlugin(options) → definePlugin(): hooks + routes (runtime)
├── admin.tsx                # exports { pages, widgets } for native admin
├── components/
│   ├── SettingsPage.tsx
│   ├── LogsPage.tsx
│   └── StatusWidget.tsx
├── providers/
│   ├── types.ts             # MailProvider interface + errors
│   ├── brevo.ts
│   ├── mailjet.ts
│   ├── mandrill.ts
│   └── factory.ts
└── lib/
    ├── api.ts               # usePluginAPI() fetch wrapper for admin components
    ├── logger.ts            # ctx.log + storage.emailLogs persistence
    ├── normalize.ts         # raw message → NormalizedMessage
    └── redact.ts            # secret masking before logging

Publishing a new version to Forgejo

You need a PAT with the write:package scope (see Forgejo setup, step 1).

One-time login

pnpm login --scope=@eliophot \
           --registry=https://git.eliophot.dev/api/packages/eliophot/npm/

Bump and publish

# bump the version (writes package.json + creates a git tag)
pnpm version patch    # or `minor` / `major`

# build + publish; `prepublishOnly` runs `clean && build` for you
pnpm publish

publishConfig.registry in package.json already points at https://git.eliophot.dev/api/packages/eliophot/npm/, so pnpm publish always targets Forgejo (never the public npm registry).

Don't forget to push the git tag

git push --follow-tags

The package becomes visible at:

https://git.eliophot.dev/eliophot/-/packages/npm/@eliophot%2Femdash-plugin-smtp

Forgejo gotcha: you cannot publish over an existing version. Either bump the version or unpublish the old one first (pnpm unpublish @eliophot/emdash-plugin-smtp@<version>).

Automatic publishing via Forgejo Actions (CI)

A Forgejo Actions workflow is included at .forgejo/workflows/publish.yaml. It runs automatically on every push to main that modifies package.json, and can also be triggered manually via workflow_dispatch.

The workflow:

  1. Compares the version field in package.json between the current and previous commit.
  2. If the version changed, it installs dependencies, builds and publishes to the Forgejo npm registry.
  3. If the version is unchanged, the publish step is skipped.

Required setup:

Item Where to configure
NPM_TOKEN secret (PAT with write:package scope) Repo → Settings → Secrets
RUNNER_NAME variable (name of the Forgejo runner) Repo → Settings → Variables

With this in place, you just need to bump the version, commit and push - CI handles the rest:

pnpm version patch
git push --follow-tags

Troubleshooting

Symptom Likely cause Fix
pnpm add returns 404 Not Found for @eliophot/... Registry not configured for the @eliophot scope Re-check step 2 of the Forgejo setup. pnpm config get @eliophot:registry should print the Forgejo URL.
pnpm add returns 401 Unauthorized Missing or expired token Regenerate the PAT, re-export FORGEJO_TOKEN, retry.
pnpm publish returns 409 Conflict Version already exists in the registry Bump the version, or unpublish the previous one.
email:deliver:misconfigured log on every send Active provider has no API key Open Email Settings, fill the secret, save.
403 Forbidden from ctx.http.fetch Host not in allowedHosts (only relevant in sandboxed mode - this plugin runs trusted) This plugin is native; if you switched modes, restore the descriptor's allowedHosts.
Test email succeeds but no message arrives Provider rejected silently (e.g. unverified sender domain) Check the provider dashboard's "Activity" feed; the plugin logs the responseSnippet from the API.
Log retention not running ctx.cron not available on your platform The purge is best-effort; you can call DELETE manually on the storage from a script if needed.

Verbose pnpm/npm

pnpm config set loglevel verbose
pnpm config ls -l

Verbose plugin logging

Open Email Settings → Logging → Server log level and set it to debug. The next sends will emit the full request endpoint and recipient list (still redacted) to ctx.log.


License

MIT - see LICENSE.

Dependencies

Development dependencies

ID Version
@emdash-cms/admin ^0.1.0
@types/node ^25.6.0
@types/react ^18.3.0
emdash ^0.1.0
react ^18.3.0
rimraf ^5.0.0
tsdown ^0.21.10
typescript ^5.5.0

Peer dependencies

ID Version
emdash ^0.1.0
react ^18.0.0

Keywords

emdash emdash-plugin email smtp brevo mailjet mailchimp mandrill transactional
Details
npm
2026-04-28 15:40:58 +00:00
3
Eliophot
MIT
56 KiB
Assets (1)
Versions (17) View all
0.1.20 2026-05-11
0.1.19 2026-05-11
0.1.18 2026-04-29
0.1.17 2026-04-28
0.1.16 2026-04-28