Index: src/services/SubscriberService.js
===================================================================
--- src/services/SubscriberService.js	(revision 2e247e4c529b09d55619100d5bd4e50de5a95e54)
+++ src/services/SubscriberService.js	(revision 2e247e4c529b09d55619100d5bd4e50de5a95e54)
@@ -0,0 +1,79 @@
+/**
+ * SubscriberService — nieuwsbrief-abonnees per site (premium feature #1).
+ *
+ * Double opt-in als SMTP er is (status 'pending' → 'confirmed' via confirm-link),
+ * anders single opt-in ('confirmed' meteen). Elke abonnee heeft een token dat zowel
+ * de confirm- als de unsubscribe-link draagt. Hergebruikt door #2 (download-voor-
+ * email) en #8 (notify-me) als gedeelde abonnee-opslag.
+ */
+
+import crypto from 'crypto';
+import db from '../config/database.js';
+import { v4 as uuid } from 'uuid';
+
+const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+
+export function isValidEmail(email) {
+  return typeof email === 'string' && email.length <= 254 && EMAIL_RE.test(email.trim());
+}
+
+function newToken() {
+  return crypto.randomBytes(24).toString('hex');
+}
+
+/**
+ * Voeg een abonnee toe (of heractiveer een uitgeschreven/bestaande).
+ * @returns {{ok:boolean, status?:string, token?:string, created?:boolean, error?:string}}
+ *   status 'pending'  → er moet nog bevestigd worden (stuur confirm-mail)
+ *   status 'confirmed'→ direct actief (single opt-in)
+ */
+export function addSubscriber(siteId, email, source = 'widget', { doubleOptin = false } = {}) {
+  email = (email || '').trim().toLowerCase();
+  if (!siteId) return { ok: false, error: 'no_site' };
+  if (!isValidEmail(email)) return { ok: false, error: 'invalid_email' };
+
+  const existing = db.prepare('SELECT * FROM subscribers WHERE site_id = ? AND email = ?').get(siteId, email);
+  const status = doubleOptin ? 'pending' : 'confirmed';
+
+  if (existing) {
+    // Al actief → niets te doen (idempotent, geen dubbele mail).
+    if (existing.status === 'confirmed') return { ok: true, status: 'confirmed', token: existing.token, created: false };
+    // Pending of uitgeschreven → opnieuw uitnodigen/activeren met een verse token.
+    const token = newToken();
+    db.prepare("UPDATE subscribers SET status = ?, token = ?, source = ?, confirmed_at = CASE WHEN ? = 'confirmed' THEN CURRENT_TIMESTAMP ELSE NULL END WHERE id = ?")
+      .run(status, token, source, status, existing.id);
+    return { ok: true, status, token, created: false };
+  }
+
+  const token = newToken();
+  db.prepare(
+    "INSERT INTO subscribers (id, site_id, email, status, source, token, confirmed_at) VALUES (?,?,?,?,?,?, CASE WHEN ? = 'confirmed' THEN CURRENT_TIMESTAMP ELSE NULL END)"
+  ).run(uuid(), siteId, email, status, source, token, status);
+  return { ok: true, status, token, created: true };
+}
+
+export function confirm(token) {
+  if (!token) return false;
+  const row = db.prepare('SELECT id FROM subscribers WHERE token = ?').get(token);
+  if (!row) return false;
+  db.prepare("UPDATE subscribers SET status = 'confirmed', confirmed_at = CURRENT_TIMESTAMP WHERE id = ?").run(row.id);
+  return true;
+}
+
+export function unsubscribe(token) {
+  if (!token) return false;
+  const row = db.prepare('SELECT id FROM subscribers WHERE token = ?').get(token);
+  if (!row) return false;
+  db.prepare("UPDATE subscribers SET status = 'unsub' WHERE id = ?").run(row.id);
+  return true;
+}
+
+/** Bevestigde abonnees (email + token) voor een site — voor het versturen. */
+export function confirmedFor(siteId) {
+  return db.prepare("SELECT email, token FROM subscribers WHERE site_id = ? AND status = 'confirmed'").all(siteId);
+}
+
+export function counts(siteId) {
+  const c = (st) => db.prepare('SELECT COUNT(*) AS n FROM subscribers WHERE site_id = ? AND status = ?').get(siteId, st).n;
+  return { confirmed: c('confirmed'), pending: c('pending'), unsub: c('unsub') };
+}
