1
0
mirror of https://github.com/flarum/core.git synced 2025-08-04 15:37:51 +02:00

feat: notification unsubscribe & email overhaul with HTML multipart (#3872)

This commit is contained in:
IanM
2023-09-29 16:34:54 +01:00
committed by GitHub
parent ec5cb98c77
commit 412cfafb3a
56 changed files with 927 additions and 155 deletions

View File

@@ -8,6 +8,7 @@ import type { IPageAttrs } from '../../common/components/Page';
import type { AlertIdentifier } from '../../common/states/AlertManagerState';
import type Mithril from 'mithril';
import type { SaveSubmitEvent } from './AdminPage';
import ItemList from '../../common/utils/ItemList';
export interface MailSettings {
data: {
@@ -65,16 +66,58 @@ export default class MailPage<CustomAttrs extends IPageAttrs = IPageAttrs> exten
return <LoadingIndicator />;
}
const fields = this.driverFields![this.setting('mail_driver')()];
const fieldKeys = Object.keys(fields);
const mailSettings = this.mailSettingItems().toArray();
return (
<div className="Form">
{this.buildSettingComponent({
type: 'text',
setting: 'mail_from',
label: app.translator.trans('core.admin.email.addresses_heading'),
})}
{mailSettings.map((settingComponent) => settingComponent)}
{this.submitButton()}
<FieldSet label={app.translator.trans('core.admin.email.send_test_mail_heading')} className="MailPage-MailSettings">
<div className="helpText">{app.translator.trans('core.admin.email.send_test_mail_text', { email: app.session.user!.email() })}</div>
<Button className="Button Button--primary" disabled={this.sendingTest || this.isChanged()} onclick={() => this.sendTestEmail()}>
{app.translator.trans('core.admin.email.send_test_mail_button')}
</Button>
</FieldSet>
</div>
);
}
mailSettingItems(): ItemList<Mithril.Children> {
const items = new ItemList<Mithril.Children>();
const fields = this.driverFields![this.setting('mail_driver')()];
const fieldKeys = Object.keys(fields);
items.add(
'mail_from',
this.buildSettingComponent({
type: 'text',
setting: 'mail_from',
label: app.translator.trans('core.admin.email.addresses_heading'),
}),
80
);
items.add(
'mail_format',
this.buildSettingComponent({
type: 'select',
setting: 'mail_format',
options: {
multipart: app.translator.trans('core.admin.email.format.multipart_option'),
plain: app.translator.trans('core.admin.email.format.plain_option'),
html: app.translator.trans('core.admin.email.format.html_option'),
},
label: app.translator.trans('core.admin.email.format_heading'),
help: app.translator.trans('core.admin.email.format_help'),
}),
70
);
items.add(
'mail_driver',
<div>
{this.buildSettingComponent({
type: 'select',
setting: 'mail_driver',
@@ -104,16 +147,11 @@ export default class MailPage<CustomAttrs extends IPageAttrs = IPageAttrs> exten
</div>
</FieldSet>
)}
{this.submitButton()}
<FieldSet label={app.translator.trans('core.admin.email.send_test_mail_heading')} className="MailPage-MailSettings">
<div className="helpText">{app.translator.trans('core.admin.email.send_test_mail_text', { email: app.session.user!.email() })}</div>
<Button className="Button Button--primary" disabled={this.sendingTest || this.isChanged()} onclick={() => this.sendTestEmail()}>
{app.translator.trans('core.admin.email.send_test_mail_button')}
</Button>
</FieldSet>
</div>
</div>,
60
);
return items;
}
sendTestEmail() {

View File

@@ -126,6 +126,12 @@ core:
description: "Configure the driver, settings and addresses your forum will use to send email."
driver_heading: Choose a Driver
driver_label: Driver
format:
multipart_option: Multipart (recommended)
plain_option: Plain Text
html_option: HTML
format_heading: Outgoing Email Format
format_help: "Choose the format that outgoing emails will be sent in. The recommended option is <code>multipart</code>, as this will allow your user's email client to display the most appropriate format."
from_label: Sender
mail_encryption_label: Encryption
mail_host_label: Host
@@ -727,6 +733,16 @@ core:
submit_button: => core.ref.save_changes
title: => core.ref.reset_your_password
# Translations in this namespace are displayed by the email unsubscribe interface.
unsubscribe_email:
title: Unsubscribe Confirmation
return_to_forum: Back to {forumTitle}
confirm_button: Confirm
immediate_helptext: You will be immediately unsubscribed once you confirm.
confirm_message: "You have requested to unsubscribe from \"{type}\" email notifications from {forumTitle}."
success_message: "You have successfully unsubscribed from \"{type}\" notification from {forumTitle}. If you wish to receive them again, please [update your settings]({settingsLink})."
invalid_message: "This unsubscribe link is invalid or has already been used. For any changes in your email notifications from {forumTitle}, please [check your settings]({settingsLink})."
# Translations in this namespace are used in messages output by the API.
api:
invalid_username_message: "The username may only contain letters, numbers, and dashes."
@@ -737,13 +753,30 @@ core:
# Translations in this namespace are used in emails sent by the forum.
email:
greeting: "Hey {displayName},"
signoff: "The {forumTitle} team"
# These translations are used by the "informational" email template.
informational:
default_title: "Information"
footer: "This email was sent to {userEmail} as an informational service related to your account on [{forumTitle}]({forumUrl})."
footer_plain: "This email was sent to {userEmail} as an informational service related to your account on {forumTitle}."
# These translations are used by the "notification" email template.
notification:
default_title: "Notification"
footer:
main_text: "This email was sent to {email} because you are subscribed to \"{type}\" notifications on [{forumTitle}]({forumUrl})."
main_text_plain: "This email was sent to {email} because you are subscribed to \"{type}\" notifications on {forumTitle}."
unsubscribe_text: "If you'd like to stop receiving this type of notification, [unsubscribe here]({unsubscribeLink})."
unsubscribe_text_plain: "If you'd like to stop receiving this type of notification, unsubscribe here: {unsubscribeLink}"
settings_text: "Manage your notification settings [here]({settingsLink})."
settings_text_plain: "Manage your notification settings here: {settingsLink}"
# These translations are used in emails sent when users register new accounts.
activate_account:
subject: Activate Your New Account
body: |
Hey {username}!
Someone (hopefully you!) has signed up to {forum} with this email address.
If this was you, simply click the following link and your account will be activated:
@@ -755,8 +788,6 @@ core:
confirm_email:
subject: Confirm Your New Email Address
body: |
Hey {username}!
Someone (hopefully you!) has changed their email address on {forum} to this one.
If this was you, simply click the following link and your email will be confirmed:
@@ -768,8 +799,6 @@ core:
reset_password:
subject: => core.ref.reset_your_password
body: |
Hey {username}!
Someone (hopefully you!) has submitted a forgotten password request for your account on {forum}.
If this was you, click the following link to reset your password:
@@ -781,8 +810,6 @@ core:
send_test:
subject: Flarum Email Test
body: |
Hey {username}!
This is a test email to confirm that your Flarum email configuration is working properly.
If this was you, this email means that your configuration works!

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
return Migration::createTable(
'unsubscribe_tokens',
function (Blueprint $table) {
$table->id();
$table->unsignedInteger('user_id');
$table->string('email_type');
$table->string('token', 100)->unique();
$table->timestamp('unsubscribed_at')->nullable();
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->index('user_id');
$table->index('email_type');
$table->index('token');
$table->index(['user_id', 'email_type']);
}
);

View File

@@ -11,8 +11,10 @@ namespace Flarum\Api\Controller;
use Flarum\Http\RequestUtil;
use Flarum\Locale\TranslatorInterface;
use Flarum\Mail\Job\SendInformationalEmailJob;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Mail\Message;
use Illuminate\Contracts\Queue\Factory;
use Laminas\Diactoros\Response\EmptyResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
@@ -22,7 +24,9 @@ class SendTestMailController implements RequestHandlerInterface
{
public function __construct(
protected Mailer $mailer,
protected TranslatorInterface $translator
protected TranslatorInterface $translator,
protected SettingsRepositoryInterface $settings,
protected Factory $queue
) {
}
@@ -31,12 +35,16 @@ class SendTestMailController implements RequestHandlerInterface
$actor = RequestUtil::getActor($request);
$actor->assertAdmin();
$body = $this->translator->trans('core.email.send_test.body', ['username' => $actor->username]);
$this->mailer->raw($body, function (Message $message) use ($actor) {
$message->to($actor->email);
$message->subject($this->translator->trans('core.email.send_test.subject'));
});
$this->queue->connection('sync')->push(
new SendInformationalEmailJob(
email: $actor->email,
displayName: $actor->display_name,
subject: $this->translator->trans('core.email.send_test.subject'),
body: $this->translator->trans('core.email.send_test.body'),
forumTitle: $this->settings->get('forum_title'),
bodyTitle: $this->translator->trans('core.email.send_test.subject')
)
);
return new EmptyResponse();
}

View File

@@ -204,4 +204,19 @@ class Formatter
return $attributes;
});
}
/**
* Converts a plain text string (with or without Markdown) to it's HTML equivalent.
*
* @param ?string $content
* @return string
*/
public function convert(?string $content): string
{
if (! $content) {
return '';
}
return $this->getRenderer()->render($this->getParser()->parse($content));
}
}

View File

@@ -0,0 +1,54 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Forum\Controller;
use Carbon\Carbon;
use Flarum\Http\UrlGenerator;
use Flarum\Notification\UnsubscribeToken;
use Flarum\User\User;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Support\Arr;
use Laminas\Diactoros\Response\RedirectResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface;
class UnsubscribeActionController implements RequestHandlerInterface
{
public function __construct(
protected Dispatcher $bus,
protected UrlGenerator $url
) {
}
public function handle(Request $request): ResponseInterface
{
$parsedBody = $request->getParsedBody();
$token = Arr::get($parsedBody, 'token');
$userId = Arr::get($parsedBody, 'userId');
/** @var UnsubscribeToken|null $unsubscribeRecord */
$unsubscribeRecord = UnsubscribeToken::where('user_id', $userId)
->where('token', $token)
->first();
if ($unsubscribeRecord && empty($unsubscribeRecord->unsubscribed_at)) {
$unsubscribeRecord->unsubscribed_at = Carbon::now();
$unsubscribeRecord->save();
/** @var User $user */
$user = User::find($userId);
$user->setNotificationPreference($unsubscribeRecord->email_type, 'email', false);
$user->save();
}
return new RedirectResponse($this->url->to('forum')->base());
}
}

View File

@@ -0,0 +1,70 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Forum\Controller;
use Flarum\Http\Controller\AbstractHtmlController;
use Flarum\Http\UrlGenerator;
use Flarum\Locale\TranslatorInterface;
use Flarum\Notification\UnsubscribeToken;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface as Request;
class UnsubscribeViewController extends AbstractHtmlController
{
public function __construct(
protected Factory $view,
protected UrlGenerator $url,
protected TranslatorInterface $translator,
protected SettingsRepositoryInterface $settings
) {
}
public function render(Request $request): View
{
$userId = Arr::get($request->getQueryParams(), 'userId');
$token = Arr::get($request->getQueryParams(), 'token');
// Fetch the unsubscribe token record
/** @var UnsubscribeToken|null $unsubscribeRecord */
$unsubscribeRecord = UnsubscribeToken::where('user_id', $userId)
->where('token', $token)
->first();
$settingsLink = $this->url->to('forum')->route('settings');
$forumTitle = $this->settings->get('forum_title');
// If record exists and has not been used before
if ($unsubscribeRecord && empty($unsubscribeRecord->unsubscribed_at)) {
$view = 'flarum.forum::unsubscribe-confirmation';
$message = $this->translator->trans('core.views.unsubscribe_email.confirm_message', [
'settingsLink' => $settingsLink,
'forumTitle' => $forumTitle,
'type' => $unsubscribeRecord->email_type
]);
} else {
// If the token doesn't exist or has already been used
$view = 'flarum.forum::unsubscribe-error';
$message = $this->translator->trans('core.views.unsubscribe_email.invalid_message', [
'settingsLink' => $settingsLink,
'forumTitle' => $forumTitle
]);
}
return $this->view
->make($view)
->with('message', $message)
->with('userId', $userId)
->with('token', $token)
->with('csrfToken', $request->getAttribute('session')->token());
}
}

View File

@@ -147,7 +147,8 @@ class ForumServiceProvider extends AbstractServiceProvider
$view->share([
'translator' => $container->make(TranslatorInterface::class),
'settings' => $container->make(SettingsRepositoryInterface::class)
'settings' => $container->make(SettingsRepositoryInterface::class),
'formatter' => $container->make(Formatter::class),
]);
$events->listen(

View File

@@ -43,6 +43,18 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
$route->toForum(Content\AssertRegistered::class)
);
$map->get(
'/notifications/unsubscribe/{userId}/{token}',
'notifications.unsubscribe',
$route->toController(Controller\UnsubscribeViewController::class)
);
$map->post(
'/notifications/unsubscribe/confirm',
'notifications.unsubscribe.confirm',
$route->toController(Controller\UnsubscribeActionController::class)
);
$map->get(
'/logout',
'logout',

View File

@@ -58,6 +58,7 @@ class WriteSettings implements Step
'forum_title' => 'A new Flarum forum',
'forum_description' => '',
'mail_driver' => 'mail',
'mail_format' => 'multipart',
'mail_from' => 'noreply@localhost',
'slug_driver_Flarum\User\User' => 'default',
'theme_colored_header' => '0',

View File

@@ -0,0 +1,49 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Mail\Job;
use Flarum\Queue\AbstractJob;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Mail\Message;
class SendInformationalEmailJob extends AbstractJob
{
public function __construct(
private readonly string $email,
private readonly string $displayName,
private readonly string $subject,
private readonly string $body,
private readonly string $forumTitle,
private readonly ?string $bodyTitle = null,
protected array $views = [
'text' => 'flarum.forum::email.plain.information.base',
'html' => 'flarum.forum::email.html.information.base'
]
) {
}
public function handle(Mailer $mailer): void
{
$forumTitle = $this->forumTitle;
$infoContent = $this->body;
$userEmail = $this->email;
$title = $this->bodyTitle;
$username = $this->displayName;
$mailer->send(
$this->views,
compact('forumTitle', 'infoContent', 'userEmail', 'title', 'username'),
function (Message $message) {
$message->to($this->email);
$message->subject($this->subject);
}
);
}
}

View File

@@ -1,32 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Mail\Job;
use Flarum\Queue\AbstractJob;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Mail\Message;
class SendRawEmailJob extends AbstractJob
{
public function __construct(
private readonly string $email,
private readonly string $subject,
private readonly string $body
) {
}
public function handle(Mailer $mailer): void
{
$mailer->raw($this->body, function (Message $message) {
$message->to($this->email);
$message->subject($this->subject);
});
}
}

View File

@@ -14,7 +14,6 @@ use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Mail\Mailer as MailerContract;
use Illuminate\Contracts\Validation\Factory;
use Illuminate\Mail\Mailer;
use Illuminate\Support\Arr;
use Symfony\Component\Mailer\Transport\TransportInterface;
@@ -62,18 +61,20 @@ class MailServiceProvider extends AbstractServiceProvider
});
$this->container->singleton('mailer', function (Container $container): MailerContract {
$settings = $container->make(SettingsRepositoryInterface::class);
$mailer = new Mailer(
'flarum',
$container['view'],
$container['symfony.mailer.transport'],
$container['events']
$container['events'],
$settings,
);
if ($container->bound('queue')) {
$mailer->setQueue($container->make('queue'));
}
$settings = $container->make(SettingsRepositoryInterface::class);
$mailer->alwaysFrom($settings->get('mail_from'), $settings->get('forum_title'));
return $mailer;

View File

@@ -0,0 +1,46 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Mail;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\View\Factory;
use Illuminate\Mail\Mailer as SymfonyMailer;
use Symfony\Component\Mailer\Transport\TransportInterface;
class Mailer extends SymfonyMailer
{
public function __construct(
string $name,
Factory $views,
TransportInterface $transport,
Dispatcher $events = null,
protected SettingsRepositoryInterface $settings
) {
parent::__construct($name, $views, $transport, $events);
}
public function send($view, array $data = [], $callback = null)
{
$emailType = $this->settings->get('mail_format');
switch ($emailType) {
case 'html':
unset($view['text']);
break;
case 'plain':
unset($view['html']);
break;
// case 'multipart' is the default, where Flarum will send both HTML and text versions of emails, so that the recipient's email client can choose which one to display.
}
parent::send($view, $data, $callback);
}
}

View File

@@ -9,6 +9,7 @@
namespace Flarum\Notification\Job;
use Flarum\Notification\Blueprint\BlueprintInterface;
use Flarum\Notification\MailableInterface;
use Flarum\Notification\NotificationMailer;
use Flarum\Queue\AbstractJob;
@@ -17,7 +18,7 @@ use Flarum\User\User;
class SendEmailNotificationJob extends AbstractJob
{
public function __construct(
private readonly MailableInterface $blueprint,
private readonly MailableInterface&BlueprintInterface $blueprint,
private readonly User $recipient
) {
}

View File

@@ -14,9 +14,16 @@ use Flarum\Locale\TranslatorInterface;
interface MailableInterface
{
/**
* Get the name of the view to construct a notification email with.
* Get the names of the views to construct a notification email with.
*
* To provide the best experience for the user, Flarum expects both a `text` and `html` view.
*
* @return array{
* text: string,
* html: string
* }
*/
public function getEmailView(): string|array;
public function getEmailViews(): array;
/**
* Get the subject line for a notification email.

View File

@@ -9,34 +9,70 @@
namespace Flarum\Notification;
use Flarum\Http\UrlGenerator;
use Flarum\Locale\TranslatorInterface;
use Flarum\Notification\Blueprint\BlueprintInterface;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\User;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Mail\Message;
use Illuminate\Support\Arr;
class NotificationMailer
{
public function __construct(
protected Mailer $mailer,
protected TranslatorInterface $translator,
protected SettingsRepositoryInterface $settings
protected SettingsRepositoryInterface $settings,
protected UrlGenerator $url
) {
}
public function send(MailableInterface $blueprint, User $user): void
public function send(MailableInterface&BlueprintInterface $blueprint, User $user): void
{
// Ensure that notifications are delivered to the user in their default language, if they've selected one.
// If the selected locale is no longer available, the forum default will be used instead.
$this->translator->setLocale($user->getPreference('locale') ?? $this->settings->get('default_locale'));
// Generate and save the unsubscribe token:
$unsubscribeRecord = UnsubscribeToken::generate($user->id, $blueprint::getType());
$unsubscribeRecord->save();
$unsubscribeLink = $this->url->to('forum')->route('notifications.unsubscribe', ['userId' => $user->id, 'token' => $unsubscribeRecord->token]);
$settingsLink = $this->url->to('forum')->route('settings');
$type = $blueprint::getType();
$forumTitle = $this->settings->get('forum_title');
$username = $user->display_name;
$userEmail = $user->email;
$this->mailer->send(
$blueprint->getEmailView(),
compact('blueprint', 'user'),
$this->getEmailViews($blueprint),
compact('blueprint', 'user', 'unsubscribeLink', 'settingsLink', 'type', 'forumTitle', 'username', 'userEmail'),
function (Message $message) use ($blueprint, $user) {
$message->to($user->email, $user->display_name)
->subject($blueprint->getEmailSubject($this->translator));
}
);
}
/**
* Retrives the email views from the blueprint, and enforces that both a
* plain text and HTML view are provided.
*
* @param MailableInterface&BlueprintInterface $blueprint
* @return array{
* text: string,
* html: string
* }
*/
protected function getEmailViews(MailableInterface&BlueprintInterface $blueprint): array
{
$views = $blueprint->getEmailViews();
// check that both text and html views are provided
if (! Arr::has($views, ['text', 'html'])) {
throw new \InvalidArgumentException('Both text and html views must be provided to send an email notification of type'.$blueprint::getType().'.');
}
return $views;
}
}

View File

@@ -0,0 +1,57 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Notification;
use Carbon\Carbon;
use Flarum\Database\AbstractModel;
use Flarum\User\User;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
/**
* @property int $id
* @property int $user_id
* @property string $email_type
* @property string $token
* @property \Carbon\Carbon $unsubscribed_at
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property-read \Flarum\User\User|null $user
*/
class UnsubscribeToken extends AbstractModel
{
protected $table = 'unsubscribe_tokens';
protected $casts = [
'user_id' => 'int',
'unsubscribed_at' => 'datetime'
];
protected $fillable = ['user_id', 'email_type', 'token'];
const TOKEN_LENGTH = 60;
public static function generate(int $userId, string $emailType): static
{
$token = new static;
$token->token = Str::random(self::TOKEN_LENGTH);
$token->user_id = $userId;
$token->email_type = $emailType;
$token->created_at = Carbon::now();
return $token;
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@@ -18,7 +18,7 @@ class QueueFactory implements Factory
/**
* The cached queue instance.
*/
private ?Queue $queue;
private ?Queue $queue = null;
/**
* Expects a callback that will be called to instantiate the queue adapter,

View File

@@ -24,6 +24,7 @@ class SettingsServiceProvider extends AbstractServiceProvider
return new Collection([
'theme_primary_color' => '#4D698E',
'theme_secondary_color' => '#4D698E',
'mail_format' => 'multipart',
]);
});

View File

@@ -9,7 +9,8 @@
namespace Flarum\User;
use Flarum\Mail\Job\SendRawEmailJob;
use Flarum\Mail\Job\SendInformationalEmailJob;
use Illuminate\Support\Arr;
trait AccountActivationMailerTrait
{
@@ -38,6 +39,12 @@ trait AccountActivationMailerTrait
$body = $this->translator->trans('core.email.activate_account.body', $data);
$subject = $this->translator->trans('core.email.activate_account.subject');
$this->queue->push(new SendRawEmailJob($user->email, $subject, $body));
$this->queue->push(new SendInformationalEmailJob(
email: $user->email,
subject: $subject,
body: $body,
forumTitle: Arr::get($data, 'forum'),
displayName: Arr::get($data, 'username')
));
}
}

View File

@@ -11,10 +11,11 @@ namespace Flarum\User;
use Flarum\Http\UrlGenerator;
use Flarum\Locale\TranslatorInterface;
use Flarum\Mail\Job\SendRawEmailJob;
use Flarum\Mail\Job\SendInformationalEmailJob;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\Event\EmailChangeRequested;
use Illuminate\Contracts\Queue\Queue;
use Illuminate\Support\Arr;
class EmailConfirmationMailer
{
@@ -34,7 +35,13 @@ class EmailConfirmationMailer
$body = $this->translator->trans('core.email.confirm_email.body', $data);
$subject = $this->translator->trans('core.email.confirm_email.subject');
$this->queue->push(new SendRawEmailJob($email, $subject, $body));
$this->queue->push(new SendInformationalEmailJob(
email: $email,
subject:$subject,
body: $body,
forumTitle: Arr::get($data, 'forum'),
displayName: Arr::get($data, 'username')
));
}
protected function generateToken(User $user, string $email): EmailToken

View File

@@ -11,12 +11,13 @@ namespace Flarum\User\Job;
use Flarum\Http\UrlGenerator;
use Flarum\Locale\TranslatorInterface;
use Flarum\Mail\Job\SendRawEmailJob;
use Flarum\Mail\Job\SendInformationalEmailJob;
use Flarum\Queue\AbstractJob;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\PasswordToken;
use Flarum\User\UserRepository;
use Illuminate\Contracts\Queue\Queue;
use Illuminate\Support\Arr;
class RequestPasswordResetJob extends AbstractJob
{
@@ -50,6 +51,13 @@ class RequestPasswordResetJob extends AbstractJob
$body = $translator->trans('core.email.reset_password.body', $data);
$subject = $translator->trans('core.email.reset_password.subject');
$queue->push(new SendRawEmailJob($user->email, $subject, $body));
$queue->push(new SendInformationalEmailJob(
email: $user->email,
displayName: Arr::get($data, 'username'),
subject: $subject,
body: $body,
forumTitle: Arr::get($data, 'forum'),
bodyTitle: $subject
));
}
}

View File

@@ -687,4 +687,14 @@ class User extends AbstractModel
return $this;
}
/**
* Set the value of a notification preference.
*/
public function setNotificationPreference(string $type, string $method, bool $value): static
{
$this->setPreference(static::getNotificationPreferenceKey($type, $method), $value);
return $this;
}
}

View File

@@ -0,0 +1,84 @@
@inject('url', 'Flarum\Http\UrlGenerator')
@php
$primaryColor = $settings->get('theme_primary_color');
$secondaryColor = $settings->get('theme_secondary_color');
@endphp
<!DOCTYPE html>
<html lang="{{ $translator->getLocale() }}">
<head>
<meta charset="utf-8">
<title>{{ $title ?? 'Flarum Email' }}</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 14px;
line-height: 1.5;
color: #333;
margin: 0;
padding: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
border: 1px solid #e1e1e1;
border-radius: 5px;
background-color: #fff;
}
.footer {
margin-top: 20px;
font-size: 12px;
color: #777;
}
.content-preview {
background-color: #f7f7f7; /* Light gray background */
padding: 15px;
margin: 15px 0;
border: 1px solid #e1e1e1;
border-radius: 5px;
}
.signoff {
margin-top: 20px;
font-style: italic;
}
</style>
</head>
<body>
<div class="header">
<div class="Header-title">
<a href="{{ $url->to('forum')->base() }}" id="home-link">
@if ($settings->get('logo_path'))
<img src="{{ $url->to('forum')->base() . '/assets/' . $settings->get('logo_path') }}" alt="{{ $settings->get('forum_title') }}" class="Header-logo">
@else
{{ $settings->get('forum_title') }}
@endif
</a>
</div>
@yield('header')
</div>
<div class="content">
@if(!isset($greeting) || $greeting !== false)
<div class="greeting">
<p>{!! $translator->trans('core.email.greeting', ['displayName' => $username]) !!}</p>
</div>
@endif
<div class="main-content">
@yield('content')
</div>
@if(!isset($signoff) || $signoff !== false)
<div class="signoff">
<p>{!! $translator->trans('core.email.signoff', ['forumTitle' => $settings->get('forum_title')]) !!}</p>
</div>
@endif
</div>
<div class="footer">
@yield('footer')
</div>
</body>
</html>

View File

@@ -0,0 +1,22 @@
@extends('flarum.forum::email.html.base')
@section('header')
<h2>{{ $title ?? $translator->trans('core.email.informational.default_title') }}</h2>
@endsection
@section('content')
@if(isset($infoContent))
<p>{!! $formatter->convert($infoContent) !!}</p>
@else
@yield('informationContent')
@endif
@hasSection('contentPreview')
<div class="content-preview">
@yield('contentPreview')
</div>
@endif
@endsection
@section('footer')
<p>{!! $formatter->convert($translator->trans('core.email.informational.footer', ['userEmail' => $userEmail, 'forumUrl' => $url->to('forum')->base(), 'forumTitle' => $settings->get('forum_title')])) !!}</p>
@endsection

View File

@@ -0,0 +1,20 @@
@extends('flarum.forum::email.html.base')
@section('header')
<h2>{{ $title ?? $translator->trans('core.email.notification.default_title') }}</h2>
@endsection
@section('content')
@yield('notificationContent')
@hasSection('contentPreview')
<div class="content-preview">
@yield('contentPreview')
</div>
@endif
@endsection
@section('footer')
<p>{!! $formatter->convert($translator->trans('core.email.notification.footer.main_text', ['email' => $user->email, 'type' => $type, 'forumUrl' => $url->to('forum')->base(), 'forumTitle' => $settings->get('forum_title')])) !!}</p>
<p>{!! $formatter->convert($translator->trans('core.email.notification.footer.unsubscribe_text', ['unsubscribeLink' => $unsubscribeLink])) !!}</p>
<p>{!! $formatter->convert($translator->trans('core.email.notification.footer.settings_text', ['settingsLink' => $settingsLink])) !!}</p>
@endsection

View File

@@ -0,0 +1,14 @@
@yield('header')
@if(!isset($greeting) || $greeting !== false)
{{ $translator->trans('core.email.greeting', ['displayName' => $username]) }}
@endif
@yield('content')
@if(!isset($signoff) || $signoff !== false)
- {{ $translator->trans('core.email.signoff', ['forumTitle' => $settings->get('forum_title')]) }} -
@endif
@yield('footer')

View File

@@ -0,0 +1,13 @@
@extends('flarum.forum::email.plain.base')
@section('header')
{{ $title ?? $translator->trans('core.email.informational.default_title') }}
@endsection
@section('content')
{{ $infoContent ?? '' }}
@endsection
@section('footer')
{!! $translator->trans('core.email.informational.footer_plain', ['userEmail' => $userEmail, 'forumTitle' => $forumTitle]) !!}
@endsection

View File

@@ -0,0 +1,17 @@
@extends('flarum.forum::email.plain.base')
@section('header')
{{ $title ?? $translator->trans('core.email.notification.default_title') }}
@endsection
@section('content')
@yield('notificationContent')
@endsection
@section('footer')
{!! $translator->trans('core.email.notification.footer.main_text_plain', ['email' => $user->email, 'type' => $type, 'forumTitle' => $forumTitle]) !!}
{!! $translator->trans('core.email.notification.footer.unsubscribe_text_plain', ['unsubscribeLink' => $unsubscribeLink]) !!}
{!! $translator->trans('core.email.notification.footer.settings_text_plain', ['settingsLink' => $settingsLink]) !!}
@endsection

View File

@@ -0,0 +1,22 @@
@extends('flarum.forum::layouts.basic')
@section('title', $translator->trans('core.views.unsubscribe_email.title'))
@section('content')
<h2>{{ $translator->trans('core.views.unsubscribe_email.title') }}</h2>
<p>{!! $formatter->convert($message) !!}</p>
<p>{{ $translator->trans('core.views.unsubscribe_email.immediate_helptext') }}</p>
<form action="{{ $url->to('forum')->route('notifications.unsubscribe.confirm') }}" method="post">
<input type="hidden" name="userId" value="{{ $userId }}">
<input type="hidden" name="token" value="{{ $token }}">
<input type="hidden" name="csrfToken" value="{{ $csrfToken }}">
<button type="submit" class="button Button Button--primary">
{{ $translator->trans('core.views.unsubscribe_email.confirm_button') }}
</button>
</form>
<br/>
<a href="{{ $url->to('forum')->base() }}">
{{ $translator->trans('core.views.unsubscribe_email.return_to_forum', ['forumTitle' => $settings->get('forum_title')]) }}
</a>
@endsection

View File

@@ -0,0 +1,12 @@
@extends('flarum.forum::layouts.basic')
@section('title', $translator->trans('core.views.unsubscribe_email_error.title'))
@section('content')
<h2>{{ $translator->trans('core.views.unsubscribe_email_error.title') }}</h2>
<p>{!! $message !!}</p>
<a href="{{ $url->to('forum')->base() }}">
{{ $translator->trans('core.views.unsubscribe_email.return_to_forum', ['forumTitle' => $settings->get('forum_title')]) }}
</a>
@endsection