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
+55 -2
View File
@@ -28,6 +28,8 @@ use Fisharebest\Webtrees\Module\ModuleCustomTrait;
use Fisharebest\Webtrees\Module\ModuleMenuInterface;
use Fisharebest\Webtrees\Module\ModuleMenuTrait;
use Fisharebest\Webtrees\Services\TreeService;
use Fisharebest\Webtrees\Services\UserService;
use Fisharebest\Webtrees\User;
use Fisharebest\Webtrees\Tree;
use Fisharebest\Webtrees\Validator;
use Fisharebest\Webtrees\View;
@@ -45,6 +47,7 @@ class Module extends AbstractModule implements ModuleCustomInterface, ModuleConf
public function __construct(
private readonly NewsletterDispatchService $dispatch_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.',
'Send the newsletter now for every enabled tree?'
=> '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 trees 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' => [
'Email Newsletter' => 'E-mailnieuwsbrief',
@@ -219,10 +229,20 @@ class Module extends AbstractModule implements ModuleCustomInterface, ModuleConf
{
$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', [
'title' => I18N::translate('Email Newsletter') . ' — ' . I18N::translate('Preferences'),
'module' => $this,
'all_trees' => $this->tree_service->all(),
'all_users' => $users,
'cron_token' => $this->cronToken(),
'cron_url' => $this->cronUrl(),
]);
@@ -230,6 +250,10 @@ class Module extends AbstractModule implements ModuleCustomInterface, ModuleConf
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) {
$id = $tree->id();
$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_EXTRA_RECIPIENTS, $extras);
if ($subject !== '') {
$tree->setPreference(Configuration::PREF_SUBJECT_PREFIX, $subject);
// Generic prefix — used when no per-locale override is set.
// 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',
);
}
}
}