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:
@@ -8,11 +8,13 @@ use Fisharebest\Webtrees\Http\RequestHandlers\ControlPanel;
|
|||||||
use Fisharebest\Webtrees\Http\RequestHandlers\ModulesAllPage;
|
use Fisharebest\Webtrees\Http\RequestHandlers\ModulesAllPage;
|
||||||
use Fisharebest\Webtrees\I18N;
|
use Fisharebest\Webtrees\I18N;
|
||||||
use Fisharebest\Webtrees\Tree;
|
use Fisharebest\Webtrees\Tree;
|
||||||
|
use Fisharebest\Webtrees\User;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Module $module
|
* @var Module $module
|
||||||
* @var Collection<int,Tree> $all_trees
|
* @var Collection<int,Tree> $all_trees
|
||||||
|
* @var Collection<int,User> $all_users
|
||||||
* @var string $cron_token
|
* @var string $cron_token
|
||||||
* @var string $cron_url
|
* @var string $cron_url
|
||||||
* @var string $title
|
* @var string $title
|
||||||
@@ -117,16 +119,43 @@ use Illuminate\Support\Collection;
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mb-3">
|
<fieldset class="row mb-3">
|
||||||
<label class="col-sm-3 col-form-label" for="subject-<?= $id ?>">
|
<legend class="col-sm-3 col-form-label">
|
||||||
<?= I18N::translate('Subject prefix') ?>
|
<?= I18N::translate('Subject prefix') ?>
|
||||||
</label>
|
</legend>
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-9">
|
||||||
<input class="form-control" type="text"
|
<small class="form-text text-muted d-block mb-2">
|
||||||
id="subject-<?= $id ?>" name="subject-<?= $id ?>"
|
<?= I18N::translate('Prepended to the email subject line. Leave a field blank to fall back to the generic prefix below.') ?>
|
||||||
value="<?= e($subject) ?>">
|
</small>
|
||||||
|
<?php foreach (Configuration::supportedSubjectLocales() as $code => $label) : ?>
|
||||||
|
<?php
|
||||||
|
$field = 'subject-' . $id . '-' . $code;
|
||||||
|
$val = $tree->getPreference(
|
||||||
|
Configuration::PREF_SUBJECT_PREFIX_PREFIX . $code,
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
<div class="input-group input-group-sm mb-2">
|
||||||
|
<span class="input-group-text" style="min-width: 7rem;">
|
||||||
|
<?= e($label) ?>
|
||||||
|
</span>
|
||||||
|
<input class="form-control" type="text"
|
||||||
|
id="<?= e($field) ?>" name="<?= e($field) ?>"
|
||||||
|
value="<?= e($val) ?>"
|
||||||
|
placeholder="<?= e('[' . $tree->title() . '] ') ?>">
|
||||||
|
</div>
|
||||||
|
<?php endforeach ?>
|
||||||
|
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text" style="min-width: 7rem;">
|
||||||
|
<?= I18N::translate('Generic') ?>
|
||||||
|
</span>
|
||||||
|
<input class="form-control" type="text"
|
||||||
|
id="subject-<?= $id ?>" name="subject-<?= $id ?>"
|
||||||
|
value="<?= e($subject) ?>">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</fieldset>
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<label class="col-sm-3 col-form-label" for="extras-<?= $id ?>">
|
<label class="col-sm-3 col-form-label" for="extras-<?= $id ?>">
|
||||||
@@ -138,6 +167,44 @@ use Illuminate\Support\Collection;
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<label class="col-sm-3 col-form-label">
|
||||||
|
<?= I18N::translate('Subscribed users') ?>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input type="hidden" name="users-submitted-<?= $id ?>" value="1">
|
||||||
|
<?php if ($all_users->isEmpty()) : ?>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
<?= I18N::translate('No users with email addresses found.') ?>
|
||||||
|
</small>
|
||||||
|
<?php else : ?>
|
||||||
|
<small class="form-text text-muted d-block mb-2">
|
||||||
|
<?= I18N::translate('Tick a user to subscribe them to this tree’s newsletter. Users can still adjust their own subscription on their account page.') ?>
|
||||||
|
</small>
|
||||||
|
<div class="border rounded p-2"
|
||||||
|
style="max-height: 18rem; overflow-y: auto;">
|
||||||
|
<?php foreach ($all_users as $user) : ?>
|
||||||
|
<?php
|
||||||
|
$field = 'subscribe-' . $id . '-' . $user->id();
|
||||||
|
$is_subbed = $tree->getUserPreference($user, Configuration::USER_PREF_SUBSCRIBED) === '1';
|
||||||
|
?>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox"
|
||||||
|
id="<?= e($field) ?>" name="<?= e($field) ?>"
|
||||||
|
value="1" <?= $is_subbed ? 'checked' : '' ?>>
|
||||||
|
<label class="form-check-label" for="<?= e($field) ?>">
|
||||||
|
<?= e($user->realName()) ?>
|
||||||
|
<small class="text-muted">
|
||||||
|
<<?= e($user->email()) ?>>
|
||||||
|
</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<?php endforeach ?>
|
||||||
|
</div>
|
||||||
|
<?php endif ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php if ($last_sent > 0) : ?>
|
<?php if ($last_sent > 0) : ?>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="offset-sm-3 col-sm-9">
|
<div class="offset-sm-3 col-sm-9">
|
||||||
|
|||||||
@@ -24,6 +24,26 @@ final class Configuration
|
|||||||
public const string PREF_LINEAL_DEPTH = 'NEWSLETTER_LINEAL_DEPTH';
|
public const string PREF_LINEAL_DEPTH = 'NEWSLETTER_LINEAL_DEPTH';
|
||||||
public const string PREF_LAST_SENT_AT = 'NEWSLETTER_LAST_SENT';
|
public const string PREF_LAST_SENT_AT = 'NEWSLETTER_LAST_SENT';
|
||||||
public const string PREF_SUBJECT_PREFIX = 'NEWSLETTER_SUBJ_PFX';
|
public const string PREF_SUBJECT_PREFIX = 'NEWSLETTER_SUBJ_PFX';
|
||||||
|
public const string PREF_SUBJECT_PREFIX_PREFIX = 'NEWSLETTER_SUBJ_PFX_';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Languages we render newsletters in. The dispatch service groups
|
||||||
|
* recipients by these short codes (German for users whose webtrees
|
||||||
|
* language starts with "de", English for everyone else), so the
|
||||||
|
* admin only has to set a subject prefix for these two locales.
|
||||||
|
*
|
||||||
|
* Keep these as short two-letter codes so the per-locale storage
|
||||||
|
* keys stay well below the webtrees 32-char setting_name limit.
|
||||||
|
*
|
||||||
|
* @return array<string,string> short-code => display label
|
||||||
|
*/
|
||||||
|
public static function supportedSubjectLocales(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'en' => 'English',
|
||||||
|
'de' => 'Deutsch',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// Module-level (not tree-bound) settings.
|
// Module-level (not tree-bound) settings.
|
||||||
public const string MODULE_PREF_CRON_TOKEN = 'cron_token';
|
public const string MODULE_PREF_CRON_TOKEN = 'cron_token';
|
||||||
@@ -120,6 +140,46 @@ final class Configuration
|
|||||||
return $tree->getPreference(self::PREF_SUBJECT_PREFIX, '[' . $tree->title() . '] ');
|
return $tree->getPreference(self::PREF_SUBJECT_PREFIX, '[' . $tree->title() . '] ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalises a webtrees language tag (e.g. "de", "de-DE", "en-US")
|
||||||
|
* to one of the short codes we offer in the admin UI.
|
||||||
|
*/
|
||||||
|
public static function canonicalSubjectLocale(string $language): string
|
||||||
|
{
|
||||||
|
$lower = strtolower($language);
|
||||||
|
|
||||||
|
foreach (array_keys(self::supportedSubjectLocales()) as $code) {
|
||||||
|
if (str_starts_with($lower, $code)) {
|
||||||
|
return $code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-locale subject prefix. Falls back to the generic
|
||||||
|
* (locale-agnostic) prefix, then to "[Tree Title] " so admins
|
||||||
|
* who never opened the form still get something sensible.
|
||||||
|
*/
|
||||||
|
public static function subjectPrefixForLocale(Tree $tree, string $language): string
|
||||||
|
{
|
||||||
|
$code = self::canonicalSubjectLocale($language);
|
||||||
|
$specific = $tree->getPreference(self::PREF_SUBJECT_PREFIX_PREFIX . $code, '');
|
||||||
|
|
||||||
|
if ($specific !== '') {
|
||||||
|
return $specific;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::subjectPrefix($tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function setSubjectPrefixForLocale(Tree $tree, string $language, string $prefix): void
|
||||||
|
{
|
||||||
|
$code = self::canonicalSubjectLocale($language);
|
||||||
|
$tree->setPreference(self::PREF_SUBJECT_PREFIX_PREFIX . $code, $prefix);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extra recipient email addresses configured by the admin (one per line).
|
* Extra recipient email addresses configured by the admin (one per line).
|
||||||
*
|
*
|
||||||
|
|||||||
+55
-2
@@ -28,6 +28,8 @@ use Fisharebest\Webtrees\Module\ModuleCustomTrait;
|
|||||||
use Fisharebest\Webtrees\Module\ModuleMenuInterface;
|
use Fisharebest\Webtrees\Module\ModuleMenuInterface;
|
||||||
use Fisharebest\Webtrees\Module\ModuleMenuTrait;
|
use Fisharebest\Webtrees\Module\ModuleMenuTrait;
|
||||||
use Fisharebest\Webtrees\Services\TreeService;
|
use Fisharebest\Webtrees\Services\TreeService;
|
||||||
|
use Fisharebest\Webtrees\Services\UserService;
|
||||||
|
use Fisharebest\Webtrees\User;
|
||||||
use Fisharebest\Webtrees\Tree;
|
use Fisharebest\Webtrees\Tree;
|
||||||
use Fisharebest\Webtrees\Validator;
|
use Fisharebest\Webtrees\Validator;
|
||||||
use Fisharebest\Webtrees\View;
|
use Fisharebest\Webtrees\View;
|
||||||
@@ -45,6 +47,7 @@ class Module extends AbstractModule implements ModuleCustomInterface, ModuleConf
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly NewsletterDispatchService $dispatch_service,
|
private readonly NewsletterDispatchService $dispatch_service,
|
||||||
private readonly TreeService $tree_service,
|
private readonly TreeService $tree_service,
|
||||||
|
private readonly UserService $user_service,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +163,13 @@ class Module extends AbstractModule implements ModuleCustomInterface, ModuleConf
|
|||||||
=> 'Richten Sie System-Cron, systemd-Timer oder einen externen Scheduler so ein, dass er die untenstehende URL aufruft. Der Versandplan entscheidet, wann tatsächlich gesendet wird — häufiger aufrufen ist unbedenklich.',
|
=> 'Richten Sie System-Cron, systemd-Timer oder einen externen Scheduler so ein, dass er die untenstehende URL aufruft. Der Versandplan entscheidet, wann tatsächlich gesendet wird — häufiger aufrufen ist unbedenklich.',
|
||||||
'Send the newsletter now for every enabled tree?'
|
'Send the newsletter now for every enabled tree?'
|
||||||
=> 'Newsletter jetzt für jeden aktivierten Baum senden?',
|
=> 'Newsletter jetzt für jeden aktivierten Baum senden?',
|
||||||
|
'Subscribed users' => 'Abonnierte Nutzer',
|
||||||
|
'No users with email addresses found.' => 'Keine Nutzer mit E-Mail-Adresse gefunden.',
|
||||||
|
'Tick a user to subscribe them to this tree’s newsletter. Users can still adjust their own subscription on their account page.'
|
||||||
|
=> 'Setzen Sie einen Haken, um den Nutzer für den Newsletter dieses Stammbaums zu abonnieren. Nutzer können ihr Abonnement weiterhin selbst auf ihrer Kontoseite anpassen.',
|
||||||
|
'Prepended to the email subject line. Leave a field blank to fall back to the generic prefix below.'
|
||||||
|
=> 'Wird der E-Mail-Betreffzeile vorangestellt. Ein leeres Feld greift auf das generische Präfix unten zurück.',
|
||||||
|
'Generic' => 'Allgemein',
|
||||||
],
|
],
|
||||||
'nl' => [
|
'nl' => [
|
||||||
'Email Newsletter' => 'E-mailnieuwsbrief',
|
'Email Newsletter' => 'E-mailnieuwsbrief',
|
||||||
@@ -219,10 +229,20 @@ class Module extends AbstractModule implements ModuleCustomInterface, ModuleConf
|
|||||||
{
|
{
|
||||||
$this->layout = 'layouts/administration';
|
$this->layout = 'layouts/administration';
|
||||||
|
|
||||||
|
// Surface every webtrees user with an email so the admin can
|
||||||
|
// toggle subscription per tree without having to ask each
|
||||||
|
// member to opt in themselves. Sorted alphabetically by real
|
||||||
|
// name so the list stays scannable in long member rosters.
|
||||||
|
$users = $this->user_service->all()
|
||||||
|
->filter(static fn (User $user): bool => trim($user->email()) !== '')
|
||||||
|
->sortBy(static fn (User $user): string => mb_strtolower($user->realName()))
|
||||||
|
->values();
|
||||||
|
|
||||||
return $this->viewResponse($this->name() . '::admin', [
|
return $this->viewResponse($this->name() . '::admin', [
|
||||||
'title' => I18N::translate('Email Newsletter') . ' — ' . I18N::translate('Preferences'),
|
'title' => I18N::translate('Email Newsletter') . ' — ' . I18N::translate('Preferences'),
|
||||||
'module' => $this,
|
'module' => $this,
|
||||||
'all_trees' => $this->tree_service->all(),
|
'all_trees' => $this->tree_service->all(),
|
||||||
|
'all_users' => $users,
|
||||||
'cron_token' => $this->cronToken(),
|
'cron_token' => $this->cronToken(),
|
||||||
'cron_url' => $this->cronUrl(),
|
'cron_url' => $this->cronUrl(),
|
||||||
]);
|
]);
|
||||||
@@ -230,6 +250,10 @@ class Module extends AbstractModule implements ModuleCustomInterface, ModuleConf
|
|||||||
|
|
||||||
public function postAdminAction(ServerRequestInterface $request): ResponseInterface
|
public function postAdminAction(ServerRequestInterface $request): ResponseInterface
|
||||||
{
|
{
|
||||||
|
// Cache the user list so we don't query it once per tree.
|
||||||
|
$users = $this->user_service->all()
|
||||||
|
->filter(static fn (User $user): bool => trim($user->email()) !== '');
|
||||||
|
|
||||||
foreach ($this->tree_service->all() as $tree) {
|
foreach ($this->tree_service->all() as $tree) {
|
||||||
$id = $tree->id();
|
$id = $tree->id();
|
||||||
$enabled = Validator::parsedBody($request)->string('enabled-' . $id, '0') === '1';
|
$enabled = Validator::parsedBody($request)->string('enabled-' . $id, '0') === '1';
|
||||||
@@ -249,8 +273,37 @@ class Module extends AbstractModule implements ModuleCustomInterface, ModuleConf
|
|||||||
$tree->setPreference(Configuration::PREF_INCLUDE_ANNIVERSARIES, $annivs ? '1' : '0');
|
$tree->setPreference(Configuration::PREF_INCLUDE_ANNIVERSARIES, $annivs ? '1' : '0');
|
||||||
$tree->setPreference(Configuration::PREF_EXTRA_RECIPIENTS, $extras);
|
$tree->setPreference(Configuration::PREF_EXTRA_RECIPIENTS, $extras);
|
||||||
|
|
||||||
if ($subject !== '') {
|
// Generic prefix — used when no per-locale override is set.
|
||||||
$tree->setPreference(Configuration::PREF_SUBJECT_PREFIX, $subject);
|
// We always write it (even empty) so admins can clear a
|
||||||
|
// previously-saved value.
|
||||||
|
$tree->setPreference(Configuration::PREF_SUBJECT_PREFIX, $subject);
|
||||||
|
|
||||||
|
foreach (array_keys(Configuration::supportedSubjectLocales()) as $code) {
|
||||||
|
$locale_prefix = Validator::parsedBody($request)
|
||||||
|
->string('subject-' . $id . '-' . $code, '');
|
||||||
|
|
||||||
|
Configuration::setSubjectPrefixForLocale($tree, $code, $locale_prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-user subscription toggles. A users-roster marker is
|
||||||
|
// always submitted (hidden field "users-submitted-<id>")
|
||||||
|
// so we can tell an unchecked-everyone POST apart from a
|
||||||
|
// legacy form that omits the section entirely — we only
|
||||||
|
// touch subscriptions when the marker is present.
|
||||||
|
$roster_present = Validator::parsedBody($request)
|
||||||
|
->string('users-submitted-' . $id, '0') === '1';
|
||||||
|
|
||||||
|
if ($roster_present) {
|
||||||
|
foreach ($users as $user) {
|
||||||
|
$field = 'subscribe-' . $id . '-' . $user->id();
|
||||||
|
$subscribed = Validator::parsedBody($request)->string($field, '0') === '1';
|
||||||
|
|
||||||
|
$tree->setUserPreference(
|
||||||
|
$user,
|
||||||
|
Configuration::USER_PREF_SUBSCRIBED,
|
||||||
|
$subscribed ? '1' : '0',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ use Fisharebest\Webtrees\Module\ModuleInterface;
|
|||||||
use Fisharebest\Webtrees\Registry;
|
use Fisharebest\Webtrees\Registry;
|
||||||
use Fisharebest\Webtrees\Services\TreeService;
|
use Fisharebest\Webtrees\Services\TreeService;
|
||||||
use Fisharebest\Webtrees\Services\UserService;
|
use Fisharebest\Webtrees\Services\UserService;
|
||||||
|
use Fisharebest\Webtrees\Site;
|
||||||
|
use Fisharebest\Webtrees\SiteUser;
|
||||||
use Fisharebest\Webtrees\Tree;
|
use Fisharebest\Webtrees\Tree;
|
||||||
use Fisharebest\Webtrees\User;
|
use Fisharebest\Webtrees\User;
|
||||||
use Illuminate\Support\Collection;
|
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\Drivers\Imagick\Driver as ImagickDriver;
|
||||||
use Intervention\Image\Encoders\JpegEncoder;
|
use Intervention\Image\Encoders\JpegEncoder;
|
||||||
use Intervention\Image\ImageManager;
|
use Intervention\Image\ImageManager;
|
||||||
|
use IntlDateFormatter;
|
||||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||||
use Symfony\Component\Mime\Exception\RfcComplianceException;
|
use Symfony\Component\Mime\Exception\RfcComplianceException;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
@@ -129,8 +132,17 @@ final class NewsletterDispatchService
|
|||||||
return sprintf('Tree "%s": no subscribers, skipped.', $tree->name());
|
return sprintf('Tree "%s": no subscribers, skipped.', $tree->name());
|
||||||
}
|
}
|
||||||
|
|
||||||
$from = $this->siteContact($tree);
|
// Match webtrees' own convention for system-generated email
|
||||||
$original_locale = I18N::languageTag();
|
// (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);
|
$groups = $this->groupRecipientsByLanguage($recipients);
|
||||||
$avatars = $this->collectAvatars($birthdays, $anniversaries, $historical);
|
$avatars = $this->collectAvatars($birthdays, $anniversaries, $historical);
|
||||||
$avatar_cids = $this->avatarCids($avatars);
|
$avatar_cids = $this->avatarCids($avatars);
|
||||||
@@ -147,10 +159,11 @@ final class NewsletterDispatchService
|
|||||||
foreach ($groups as $lang => $group) {
|
foreach ($groups as $lang => $group) {
|
||||||
I18N::init($lang);
|
I18N::init($lang);
|
||||||
|
|
||||||
$subject = Configuration::subjectPrefix($tree) . I18N::translate(
|
$subject = Configuration::subjectPrefixForLocale($tree, $lang)
|
||||||
'Family newsletter — %s',
|
. I18N::translate(
|
||||||
date('F j, Y', $now),
|
'Family newsletter — %s',
|
||||||
);
|
$this->formatSubjectDate($now, $lang),
|
||||||
|
);
|
||||||
|
|
||||||
// Render the email body per recipient — the relationship
|
// Render the email body per recipient — the relationship
|
||||||
// labels are personalised relative to whichever individual
|
// labels are personalised relative to whichever individual
|
||||||
@@ -188,7 +201,7 @@ final class NewsletterDispatchService
|
|||||||
$text = $this->htmlToText($html);
|
$text = $this->htmlToText($html);
|
||||||
|
|
||||||
try {
|
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++;
|
$sent++;
|
||||||
// Per-user last-sent is stored only for real
|
// Per-user last-sent is stored only for real
|
||||||
// webtrees users (id > 0). External admin-
|
// webtrees users (id > 0). External admin-
|
||||||
@@ -657,6 +670,22 @@ final class NewsletterDispatchService
|
|||||||
return $recipients;
|
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
|
* Pick a sender identity. Fall back to a synthetic UserInterface if
|
||||||
* the site has no contact user configured for this tree.
|
* the site has no contact user configured for this tree.
|
||||||
@@ -692,6 +721,34 @@ final class NewsletterDispatchService
|
|||||||
return new ExtraRecipient($address, self::FROM_NAME);
|
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
|
private function htmlToText(string $html): string
|
||||||
{
|
{
|
||||||
$without_tags = preg_replace('/<\s*br\s*\/?>/i', "\n", $html) ?? $html;
|
$without_tags = preg_replace('/<\s*br\s*\/?>/i', "\n", $html) ?? $html;
|
||||||
|
|||||||
Reference in New Issue
Block a user