ZKRINET Docs
Building blocks

Newsletter

On Zkrinet: add the block under Building blocks in the panel — one click, your own code is untouched and a snapshot is taken first. The installation steps below apply if you run PocketBase yourself, for example after an export.

Subscription list with double opt-in, one-click unsubscribe in every mailing, and mailings sent in batches from the admin panel. No external services — the emails go through your own SMTP.

Requires the Mejl (email) plugin. Tested against PocketBase 0.40.4.

Installation

  1. Install Mejl first (and set up SMTP), then copy pb_hooks/ and pb_migrations/ to your PocketBase and restart.
  2. Check Settings → Application URL — the links in the emails are built on it (https://din-pb.se/api/zk/nyhetsbrev/...).
  3. zk_nyhetsbrev_installningar already has one row: per_timme (sign-ups per IP and hour, default 5), omgang (emails per batch, default 50) and hemlighet (created automatically, hidden). The secret can instead be set with the environment variable ZK_NYHETSBREV_HEMLIGHET.
  4. Behind a proxy (Cloudflare, nginx)? Set Settings → Trusted proxy, otherwise all visitors share the same limit.
  5. The email templates nyhetsbrev_bekrafta and nyhetsbrev_redan are in zk_mejlmallar and can be edited there.

Sign-up from your site

await fetch("https://din-pb.se/api/zk/nyhetsbrev/prenumerera", {
  method: "POST", headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ epost: "anna@exempel.se" }),
})
// 200 { ok: true, meddelande: "Om adressen är giltig får du strax ett mejl…" } · 400 invalid address · 429 too many attempts

Or without JavaScript:

<form method="post" action="https://din-pb.se/api/zk/nyhetsbrev/prenumerera">
  <input name="epost" type="email" required placeholder="din@adress.se">
  <button>Subscribe</button>
</form>

How it works:

  1. Sign-up → the row is created with status = vantar and a confirmation email is sent.
  2. The link in the email opens a page with a button. Nothing is confirmed until the button is pressed — email scanners (Outlook, Gmail, corporate filters) open links but do not press buttons. The link is valid for 7 days and works once.
  3. The button → status = bekraftad. Only now does the address receive mailings.

Sending a mailing

  1. Create a row in zk_nyhetsbrev_utskick: amne, innehall (HTML, the editor in the admin panel), optionally text (plain text version — otherwise one is made from the HTML), status = utkast.
  2. Start it (superuser, e.g. from an admin tool or curl):
POST /api/zk/nyhetsbrev/skicka/{id}
→ { status: "skickas", skickade_nu: 50, antal: 50, fel: 0 }

The first call sends one batch. The rest are sent automatically, one batch per minute (cron). Call again if you want to run the next batch right away. When all are done, status = skickat; a sent mailing cannot be sent again (409).

Each recipient gets a row in zk_nyhetsbrev_leveranser (skickat / fel). A unique key on (mailing, subscriber) ensures that the same letter never goes twice to the same address, even if cron and a manual call run at the same time. If an email fails (e.g. SMTP down), the batch is aborted and the recipient is retried in the next batch, at most 3 attempts.

Every mailing automatically gets a footer with an unsubscribe link — it cannot be forgotten. In the content you can use {{app}} (the app name) and {{avregistrera}} (the link) if you want it in more places. The content is locked when the mailing starts: if you change it in the meantime, everyone still gets the same letter.

Unsubscribe

The link in every mailing unsubscribes directly with one click and works any number of times (idempotent). It is signed with the server's secret (HMAC), so nobody can unsubscribe someone else by guessing ids. POST to the same address also works, per RFC 8058 (one-click).

Anyone who signs up again after unsubscribing must confirm again.

Security and privacy

Limitations

Testing

./bank.py nyhetsbrev

18 tests against the test bench's SMTP trap (nothing reaches the internet): double opt-in with button, token hash-stored/single-use/expired, no enumeration, pause against email bombing, case-insensitive unique address, mailings only to confirmed, batches without duplicates, unsubscribe (one click, idempotent, RFC 8058 POST, forged link rejected), new sign-up requires new confirmation, permissions, rate limit and SMTP outage in the middle of a mailing. The security tests are mutant-verified.