90ad060421
Replaces "clear intro after first send", which dropped the message for any subscriber still queued on a slower cadence. - Each non-empty admin save bumps a per-locale version counter on the tree. The dispatcher includes the intro only for recipients whose last-seen version is behind, then advances their watermark after a successful send. Webtrees users get a per-user watermark; external addresses share one tree-level watermark. Re-saving the same text is a no-op. - The preferences page now shows delivery progress per locale: "Delivered to X of Y subscriber(s)" plus a collapsible Pending list with name + email of each subscriber who hasn't received the current intro yet, and a single "External recipients (N)" row when the external watermark is behind. - README rewritten to reflect every feature shipped since the initial commit (BockenTheme skin, embedded avatars, relationship labels, kin-distance filter, per-user cadence, bilingual subject prefix, locale-aware subject date, SiteUser as From, three subscriber sources, Markdown intro with personalisation tokens).
193 lines
7.3 KiB
Markdown
193 lines
7.3 KiB
Markdown
# webtrees Email Newsletter
|
|
|
|
A [webtrees](https://www.webtrees.net/) 2.2+ custom module that sends recurring
|
|
email newsletters with:
|
|
|
|
- **Upcoming birthdays** of still-living individuals (formatted as ordinal
|
|
age — *"45th birthday"* / *"45. Geburtstag"*).
|
|
- **Upcoming marriage anniversaries** of intact couples (admin toggle, per
|
|
tree). Marriages with a divorce or annulment fact are excluded
|
|
automatically.
|
|
- **Historical events** — births and deaths of deceased individuals whose
|
|
anniversary falls in the upcoming window.
|
|
|
|
The look-ahead window and the send cadence are the same number: one
|
|
"every N days" setting (default 14) drives both the cron interval and how
|
|
far each issue looks ahead. Issues with nothing to report are silently
|
|
skipped.
|
|
|
|
Each recipient gets a per-recipient render — language, relationship
|
|
labels, detail filter, cadence, and personalisation tokens are all
|
|
resolved against *their* webtrees account.
|
|
|
|
## Highlights
|
|
|
|
- **Editorial layout** with embedded circular avatars, a left-side
|
|
timeline rail, and event-type icons (birth / death / marriage).
|
|
- **BockenTheme light-mode skin** — Open Sans, cream background, Nord
|
|
accent palette. The newsletter and the website read as one product.
|
|
- **Per-recipient localisation** — German for users whose webtrees
|
|
language starts with `de`, English otherwise. Subject line, body,
|
|
date strings, and (optionally) a custom subject prefix are all
|
|
localised. Subject dates use `IntlDateFormatter` for the recipient's
|
|
locale.
|
|
- **Per-recipient relationship labels** — *"your mother"*, *"4th great-
|
|
grandfather"*, *"first cousin twice removed"*. Uses webtrees' own
|
|
`RelationshipService` so the labels match the site.
|
|
- **Kin-distance detail filter** — close family get the full card
|
|
(avatar + timeline + icon); distant kin appear as a single-line
|
|
bullet at the foot of each section. The "distance" radius is an
|
|
admin setting (default 3); spouses inherit their partner's distance;
|
|
recipients with no linked tree record always see the full detailed
|
|
view.
|
|
- **Per-recipient cadence** — each subscriber can pick weekly,
|
|
biweekly, monthly, every-two-months, quarterly, or "use site
|
|
default" on their `/my-account/{tree}` page.
|
|
- **One-shot intro paragraph** — admins can attach a Markdown intro
|
|
(bilingual, EN/DE) to the next issue. Supports
|
|
`{{first_name}}`, `{{last_name}}`, `{{username}}`, `{{email}}`
|
|
personalisation tokens. Rendered alongside the tree-contact user's
|
|
avatar as an editorial column. Cleared automatically after a
|
|
successful send.
|
|
- **Cron-only dispatch** — the "is it due?" decision is made server-
|
|
side against stored timestamps. Calling the trigger more often than
|
|
the cadence is harmless and idempotent.
|
|
|
|
## Requirements
|
|
|
|
- webtrees ≥ 2.2.0
|
|
- PHP ≥ 8.2 with `ext-intl` (for locale-aware subject dates) and
|
|
either `ext-imagick` or `ext-gd` (for avatar resizing — falls back
|
|
to original-size embeds if neither is present).
|
|
- A working SMTP / sendmail configuration in *Control panel → Sending
|
|
email*. This module reuses webtrees' standard mailer and signs with
|
|
the site's DKIM keys if configured.
|
|
- An external scheduler on the host (system `cron`, `systemd` timer,
|
|
Kubernetes `CronJob`, …) that can fire an HTTP request at a fixed
|
|
interval. **Newsletter dispatch never runs on visitor page loads.**
|
|
|
|
## Installation
|
|
|
|
1. Copy this directory into the webtrees `modules_v4/` folder, renaming
|
|
it to `email_newsletter`:
|
|
|
|
```sh
|
|
cp -r webtrees_email_newsletter /var/www/webtrees/modules_v4/email_newsletter
|
|
```
|
|
|
|
2. In the webtrees control panel, go to *Modules → All modules* and
|
|
enable **Email Newsletter**.
|
|
|
|
3. Open *Control panel → Modules → Email Newsletter → Preferences*
|
|
and, for each tree:
|
|
|
|
- Tick *Enable newsletter for this tree*.
|
|
- Set the send-cadence (default 14 days). This same number is the
|
|
look-ahead window for the next issue.
|
|
- Toggle *Include marriage anniversaries* if desired.
|
|
- Set *Detailed view distance* (default 3). Lower values produce a
|
|
terser email focused tightly on close kin.
|
|
- Optional: set per-locale *Subject prefix* and a *Generic*
|
|
fallback (e.g. `[Bocken family] `).
|
|
- Optional: tick existing webtrees users in *Subscribed users* to
|
|
subscribe them. Users can still adjust their own subscription
|
|
and cadence on `/my-account/{tree}`.
|
|
- Optional: add external (non-user) addresses in *Extra recipient
|
|
email addresses*.
|
|
|
|
4. Copy the **Cron URL** at the bottom and wire it into your scheduler
|
|
(see below).
|
|
|
|
## Setting up the scheduler
|
|
|
|
> **Why no built-in scheduler?** PHP has no daemon, and frameworks
|
|
> like Laravel rely on a once-per-minute system cron to fire their
|
|
> internal scheduler. This module follows the same convention: the
|
|
> host OS owns the timer, the module owns the "is it actually due?"
|
|
> decision.
|
|
|
|
### System cron
|
|
|
|
```cron
|
|
# Run every 15 minutes. The module itself decides whether sending is due.
|
|
*/15 * * * * curl -fsS --max-time 60 'https://example.com/module/_email_newsletter_/Cron?token=YOUR_TOKEN' > /dev/null
|
|
```
|
|
|
|
### systemd timer
|
|
|
|
`/etc/systemd/system/webtrees-newsletter.service`:
|
|
|
|
```ini
|
|
[Unit]
|
|
Description=webtrees newsletter trigger
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
ExecStart=/usr/bin/curl -fsS --max-time 60 "https://example.com/module/_email_newsletter_/Cron?token=YOUR_TOKEN"
|
|
```
|
|
|
|
`/etc/systemd/system/webtrees-newsletter.timer`:
|
|
|
|
```ini
|
|
[Unit]
|
|
Description=Trigger webtrees newsletter dispatch
|
|
|
|
[Timer]
|
|
OnCalendar=*-*-* *:00/15
|
|
Persistent=true
|
|
|
|
[Install]
|
|
WantedBy=timers.target
|
|
```
|
|
|
|
Then `systemctl enable --now webtrees-newsletter.timer`.
|
|
|
|
### Forcing a one-off send
|
|
|
|
The admin **Preferences** page has a *Send now* button for testing.
|
|
For an unattended forced send (bypassing the per-recipient "is it
|
|
due?" check), append `&force=1` to the cron URL.
|
|
|
|
## Subscribers
|
|
|
|
Three sources, combined and de-duplicated by email:
|
|
|
|
1. **Webtrees users** who opt in themselves via the per-tree
|
|
*Newsletter subscription* menu entry on `/my-account/{tree}` —
|
|
visible only to logged-in users on trees where the module is
|
|
enabled.
|
|
2. **Webtrees users** an admin subscribes from the preferences page.
|
|
3. **External addresses** the admin lists in *Extra recipient email
|
|
addresses*.
|
|
|
|
Only **approved and email-verified** webtrees accounts will receive
|
|
the newsletter. External addresses always receive on every run
|
|
(they have no per-user cadence timer).
|
|
|
|
## Sender identity
|
|
|
|
To match webtrees' own convention for system-generated email
|
|
(registration, password resets, "new version available"), the
|
|
`From:` header is **SiteUser** — *Control panel → Sending email →
|
|
Sender name / Sender email* (`SMTP_FROM_NAME` / `SMTP_DISP_NAME`).
|
|
The tree's contact user becomes the `Reply-To:`, so replies still
|
|
reach a human admin.
|
|
|
|
If `SMTP_FROM_NAME` isn't set the dispatcher falls back to the tree
|
|
contact for `From:` as well, so the message always has a valid
|
|
sender envelope.
|
|
|
|
## Privacy
|
|
|
|
The dispatch service does not impersonate a webtrees user, so it
|
|
sees the tree from the **visitor** access level. Records and facts
|
|
that your tree settings hide from visitors will be omitted from the
|
|
newsletter even if a recipient has higher in-app access. This is
|
|
the safest default for an outbound email — if you need to expose
|
|
more information, relax the tree's visitor-access settings or
|
|
hand-curate the *Extra recipient* list.
|
|
|
|
## License
|
|
|
|
AGPL-3.0-or-later. See `LICENSE` for the full text.
|