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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user