Admin user roster; per-locale subject; SiteUser as From

- 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.
This commit is contained in:
2026-05-15 14:30:01 +02:00
parent 00478e2466
commit 9ccc636105
4 changed files with 253 additions and 16 deletions
+64 -7
View File
@@ -15,6 +15,8 @@ use Fisharebest\Webtrees\Module\ModuleInterface;
use Fisharebest\Webtrees\Registry;
use Fisharebest\Webtrees\Services\TreeService;
use Fisharebest\Webtrees\Services\UserService;
use Fisharebest\Webtrees\Site;
use Fisharebest\Webtrees\SiteUser;
use Fisharebest\Webtrees\Tree;
use Fisharebest\Webtrees\User;
use Illuminate\Support\Collection;
@@ -22,6 +24,7 @@ use Intervention\Image\Drivers\Gd\Driver as GdDriver;
use Intervention\Image\Drivers\Imagick\Driver as ImagickDriver;
use Intervention\Image\Encoders\JpegEncoder;
use Intervention\Image\ImageManager;
use IntlDateFormatter;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mime\Exception\RfcComplianceException;
use Throwable;
@@ -129,8 +132,17 @@ final class NewsletterDispatchService
return sprintf('Tree "%s": no subscribers, skipped.', $tree->name());
}
$from = $this->siteContact($tree);
$original_locale = I18N::languageTag();
// Match webtrees' own convention for system-generated email
// (registration confirmations, password resets, "new version
// available" notices): the From: header is the SiteUser
// (SMTP_FROM_NAME), while Reply-To: points at the family-tree
// contact person so replies still reach a human. If the site
// admin hasn't configured SMTP_FROM_NAME we fall back to the
// tree contact for From: too, otherwise the transport may
// reject the message for lacking a sender envelope.
$reply_to = $this->siteContact($tree);
$from = $this->siteFrom($reply_to);
$original_locale = I18N::languageTag();
$groups = $this->groupRecipientsByLanguage($recipients);
$avatars = $this->collectAvatars($birthdays, $anniversaries, $historical);
$avatar_cids = $this->avatarCids($avatars);
@@ -147,10 +159,11 @@ final class NewsletterDispatchService
foreach ($groups as $lang => $group) {
I18N::init($lang);
$subject = Configuration::subjectPrefix($tree) . I18N::translate(
'Family newsletter — %s',
date('F j, Y', $now),
);
$subject = Configuration::subjectPrefixForLocale($tree, $lang)
. I18N::translate(
'Family newsletter — %s',
$this->formatSubjectDate($now, $lang),
);
// Render the email body per recipient — the relationship
// labels are personalised relative to whichever individual
@@ -188,7 +201,7 @@ final class NewsletterDispatchService
$text = $this->htmlToText($html);
try {
if ($this->mailer->sendWithEmbeds($from, $recipient, $from, $subject, $text, $html, $recipient_avatars)) {
if ($this->mailer->sendWithEmbeds($from, $recipient, $reply_to, $subject, $text, $html, $recipient_avatars)) {
$sent++;
// Per-user last-sent is stored only for real
// webtrees users (id > 0). External admin-
@@ -657,6 +670,22 @@ final class NewsletterDispatchService
return $recipients;
}
/**
* Resolve the From: identity, mirroring webtrees' own behaviour:
* the SMTP_FROM_NAME / SMTP_DISP_NAME site preferences if set,
* otherwise the tree contact user (so the transport always has
* a usable envelope sender). This is the address recipients see
* in their mail client, not the one their replies go to.
*/
private function siteFrom(UserInterface $fallback): UserInterface
{
if (trim(Site::getPreference('SMTP_FROM_NAME')) !== '') {
return new SiteUser();
}
return $fallback;
}
/**
* Pick a sender identity. Fall back to a synthetic UserInterface if
* the site has no contact user configured for this tree.
@@ -692,6 +721,34 @@ final class NewsletterDispatchService
return new ExtraRecipient($address, self::FROM_NAME);
}
/**
* Format the issue date for the email subject in the recipient's
* locale: "15. Mai 2026" for de, "May 15, 2026" for en-US. Falls
* back to the English date() format if ext-intl is unavailable —
* webtrees lists intl as required so this should not happen in
* practice, but we don't want to fatal a dispatch over it.
*/
private function formatSubjectDate(int $timestamp, string $language): string
{
if (class_exists(IntlDateFormatter::class)) {
$formatter = IntlDateFormatter::create(
str_replace('-', '_', $language),
IntlDateFormatter::LONG,
IntlDateFormatter::NONE,
);
if ($formatter instanceof IntlDateFormatter) {
$formatted = $formatter->format($timestamp);
if (is_string($formatted) && $formatted !== '') {
return $formatted;
}
}
}
return date('F j, Y', $timestamp);
}
private function htmlToText(string $html): string
{
$without_tags = preg_replace('/<\s*br\s*\/?>/i', "\n", $html) ?? $html;