Two-level gate: resolve and embed the tree-contact portrait only
when (a) at least one locale on the tree has a non-empty intro on
file, and (b) the specific recipient is still pending delivery
for the current intro version. Recipients who have already seen
this intro, or whose locale has no intro, no longer carry the
extra image bytes.
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).
- Admin can set a per-locale intro paragraph for the next issue on
the preferences page; cleared automatically after a successful
send. Stored in module_setting (longText) so multi-paragraph
notes fit.
- Intro is rendered via webtrees' CommonMark factory (same flavour
as notes) with raw HTML escaped, supports {{first_name}},
{{last_name}}, {{username}}, {{email}} substitution per recipient.
- Two-column intro layout: tree contact user's linked Individual
becomes the editorial portrait on the left. Their avatar is
added to the per-recipient embed set so the inline image always
resolves rather than falling through to a tree-page login link.
- Masthead now shows the tree URL under the title.
- Avatar source dimensions bumped 96→192 px and JPEG quality 75→88
so portraits stay crisp at retina display ratios.
- Admin preferences page can now subscribe existing webtrees users
per tree, not just external addresses.
- Subject prefix is now configurable per locale (en/de), and the
date in the subject is formatted via IntlDateFormatter in the
recipient's locale.
- "From:" header now uses SiteUser (SMTP_FROM_NAME/SMTP_DISP_NAME)
to match webtrees' own system-mail convention; the tree contact
becomes the Reply-To.
Admin-facing simplification:
- Dropped separate \"lookahead\" and \"historical lookahead\" tree
prefs (and the once-per-month historical gate). A single
\"send every N days\" number now drives both the cron cadence
and the window each issue looks ahead for living + deceased
events.
- Default 14, range 1–90, applies uniformly.
User-facing addition:
- The /my-account/{tree} subscription card gained an \"Email
frequency\" select with options: use site default, weekly,
every 2 weeks, monthly, every 2 months, quarterly. Stored as
a per-tree-per-user preference.
- Dispatch now checks each recipient's own cadence against
their own last-sent timestamp. Admin-added external addresses
with no webtrees account always receive every run (no
per-user state).
- Newsletter footer now reads \"You can change how often you
receive this email, or unsubscribe entirely, in the Newsletter
subscription section on your My account page\" — true now
that the control exists.
German translations updated for the new strings; stale ones
removed.
Replaces the previous "depth in generations along the strict
lineal chain" definition (which excluded siblings, aunts, cousins
entirely) with the metric the user actually wants: the number of
descent-steps separating the target from the recipient's closest
direct ancestor or descendant.
Examples relative to the recipient:
- sibling: 1 (parent → sibling)
- great-aunt: 1 (great-grandparent → great-aunt)
- nephew: 2 (parent → sibling → nephew)
- first cousin: 2 (grandparent → aunt → cousin)
- second cousin: 3
- ego, parents, grandparents, ..., children, ..., great-greats: 0
- own spouse, step-parents, brothers-in-law: inherit partner's
distance (so spouse-of-distance-1 is also distance 1)
Implementation:
- Anchor set seeded with R's direct ancestors + R + direct
descendants (capped at 25 generations to bound runaway data).
- Multi-source BFS expanding by descent only.
- Spouse propagation at every level so a person and their
spouse always share the same distance.
- Memoised per (recipient xref, max distance).
Tree preference key and range kept (NEWSLETTER_LINEAL_DEPTH,
0–10, default 3); only the semantics and the user-facing label
+ help text change, with concrete examples in both English and
German.
Per-recipient: only direct ancestors and direct descendants
within a configurable number of generations (default 3) get the
full row treatment (avatar, icon, timeline). Everyone else falls
through to a compact text-only bullet list at the bottom of the
same section.
- New tree preference NEWSLETTER_LINEAL_DEPTH (range 0–10,
default 3) with a clearly-explained admin input.
- RelationshipPathFinder::linealKin() does two cheap recursive
expansions (ancestors and descendants only — no spouse or
sibling traversal) and returns the xref set. Memoised per
recipient within a dispatch run.
- Avatar attachments are filtered per recipient to only the
embeds actually referenced in their HTML, so summary-only rows
no longer inflate per-email size with unused images.
- Recipients with no PREF_TREE_ACCOUNT_XREF (external admin
addresses, users not linked to a record) see the entire
newsletter in detail — no lineal anchor to filter against.
- German translations for the three new section kickers ("Other
birthdays", etc.) and the admin input help text.
Each featured person now carries a parenthetical label relative
to the recipient: "Jane Doe (your mother) — 45th birthday",
"Karl Müller (your 4th great-grandfather) — death". Labels are
italic, muted, and only appear when a path can be computed.
- New RelationshipPathFinder service mirrors webtrees'
RelationshipService::getCloseRelationship BFS but with a
configurable depth (default 14 hops ≈ 7 generations) so it
reaches great-great-grandparents and beyond. Results are
memoised per (recipient xref, target xref) within one
dispatch run.
- nameFromPath() formatting is delegated to webtrees so the
label honours the configured UI language (German, English,
etc.) and gendered/inflected forms.
- The recipient's tree-bound Individual is looked up via
Tree::getUserPreference(user, PREF_TREE_ACCOUNT_XREF). External
admin-added recipients (no webtrees account, no linked record)
silently get no labels — names render plain.
- Trade-off: the view now renders once per recipient (instead of
once per language group), because the relationship map is
personalised. For typical subscriber counts the extra string-
concat cost is negligible compared to the SMTP send itself.
Source media files can easily be multiple megabytes — embedding
the originals made a per-recipient email balloon to 10MB+. Each
avatar is now cover-cropped to 96x96 (HiDPI for the rendered
48px circle) and re-encoded as JPEG q=75 via Intervention\Image,
which webtrees already depends on. Typical avatar payload drops
from megabytes to ~5-15KB.
Falls back to the original bytes (with a log warning) if neither
Imagick nor GD is loaded — better an oversized email than none.
The "once-per-calendar-month" gate that prevents the historical
section from appearing on every regular send also suppressed it
on admin "Send now" previews after the first run of the month —
making the section silently disappear when re-testing the email.
Force-send now bypasses the gate but still updates the
last-historical-month stamp, so the real monthly cadence stays
intact for cron-driven sends.
Pull each individual's highlighted media image via webtrees'
Individual::findHighlightedMediaFile, attach as Symfony inline
parts with stable cid:avatar-<xref> identifiers, and render
border-radius:50% on the <img>. Couples on anniversaries show
both spouses' circles side-by-side.
Fallback when no image is available (privacy-hidden record, no
OBJE, external URL, unreadable file): a CSS-only coloured circle
with the person's initials. The hue is derived from a hash of
the XREF so the same person keeps the same colour across
newsletters.
Done via a NewsletterMailer subclass of EmailService that adds a
sendWithEmbeds() method — the parent's transport() and DKIM
config still apply, only the message-construction path differs.
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").