Initial commit: webtrees Email Newsletter module

Recurring email newsletter for webtrees 2.2+. Each enabled tree
sends upcoming birthdays of living individuals, optional marriage
anniversaries of intact couples, and a once-per-calendar-month
historical section of births and deaths of deceased individuals.

Triggered exclusively by an external scheduler (system cron,
systemd timer, etc.) hitting a token-gated HTTP endpoint — never
on visitor page loads. The "is it due?" decision is idempotent
within the configured frequency window.

Per-user subscription is integrated into the built-in
/my-account/{tree} page via a custom view + a decorated
AccountUpdate handler. Admins can add external addresses and
trigger an immediate send for testing. Email body renders in
German for German-language users; English otherwise. Birthdays
and anniversaries are formatted with the upcoming-event ordinal
age (e.g. "45th birthday" / "45. Geburtstag").
This commit is contained in:
2026-05-15 12:00:39 +02:00
commit 7ce8201082
13 changed files with 1748 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
# 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.
- **Upcoming marriage anniversaries** of intact couples (optional — admin
toggle, per tree). Marriages with a divorce or annulment fact are excluded
automatically.
- **Once-per-month historical section**: births and deaths of deceased
individuals whose anniversary falls in the upcoming window.
The decision to actually send is made by comparing a stored "last sent"
timestamp to the configured frequency, so the dispatch run is idempotent —
calling the trigger more often than the frequency simply does nothing
extra.
## Requirements
- webtrees ≥ 2.2.0
- PHP ≥ 8.2
- A working SMTP / sendmail configuration in *webtrees → Control panel →
Sending email* (this module reuses webtrees' standard mailer).
- An external scheduler on the host: system `cron`, a `systemd` timer,
a Kubernetes `CronJob`, or anything else that can fire an HTTP request
at a fixed interval. **Newsletter dispatch never runs on visitor page
loads — it only runs when the scheduler triggers it.**
## Installation
1. Copy this directory into the webtrees `modules_v4/` folder, renaming
it to `email_newsletter` (the folder name determines the internal
module identifier — the registered name will be
`_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:
- Enable newsletter dispatch per tree.
- Pick a frequency (default: 14 days).
- Optionally toggle marriage anniversaries and add any extra
external email addresses.
- Copy the **Cron URL** at the bottom — this is the secret-token
URL your scheduler must hit.
## 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 one-off send, append `&force=1` to the cron URL —
that bypasses the "is it due?" check.
## Subscribers
Two sources, combined:
1. **Logged-in webtrees users** who opt in via the per-tree
*Newsletter subscription* menu entry (visible only to logged-in
users on trees where the module is enabled). Only **approved and
email-verified** accounts will receive the newsletter.
2. **External addresses** the tree administrator lists in the
*Extra recipient email addresses* textarea (one per line).
## 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.