1
0
mirror of https://github.com/flarum/core.git synced 2025-08-05 07:57:46 +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

@@ -70,40 +70,43 @@ flarum-mentions:
# These translations are used in emails sent when a post is replied to # These translations are used in emails sent when a post is replied to
post_mentioned: post_mentioned:
subject: "{replier_display_name} replied to your post in {title}" subject: "{replier_display_name} replied to your post in {title}"
body: | plain:
Hey {recipient_display_name}! body: |
{replier_display_name} replied to your post (#{post_number}) in {title}.
{replier_display_name} replied to your post (#{post_number}) in {title}. {url}
{url} ---
--- {content}
html:
{content} body: "{replier_display_name} replied to your post (#{post_number}) in [{title}]({url})."
# These translations are used in emails sent when a user is mentioned # These translations are used in emails sent when a user is mentioned
user_mentioned: user_mentioned:
subject: "{mentioner_display_name} mentioned you in {title}" subject: "{mentioner_display_name} mentioned you in {title}"
body: | plain:
Hey {recipient_display_name}! body: |
{mentioner_display_name} mentioned you in a post in {title}.
{mentioner_display_name} mentioned you in a post in {title}. {url}
{url} ---
--- {content}
html:
{content} body: "{mentioner_display_name} mentioned you in a post in [{title}]({url})."
# These translations are used in emails sent when a group is mentioned # These translations are used in emails sent when a group is mentioned
group_mentioned: group_mentioned:
subject: "{mentioner_display_name} mentioned a group you're a member of in {title}" subject: "{mentioner_display_name} mentioned a group you're a member of in {title}"
body: | plain:
Hey {recipient_display_name}! body: |
{mentioner_display_name} mentioned a group you're a member of in {title}.
{mentioner_display_name} mentioned a group you're a member of in {title}. {url}
{url} ---
--- {content}
html:
{content} body: "{mentioner_display_name} mentioned a group you're a member of in [{title}]({url})."

View File

@@ -38,9 +38,11 @@ class GroupMentionedBlueprint implements BlueprintInterface, MailableInterface
return null; return null;
} }
public function getEmailView(): string|array public function getEmailViews(): array
{ {
return ['text' => 'flarum-mentions::emails.groupMentioned']; return [
'text' => 'flarum-mentions::emails.plain.groupMentioned',
'html' => 'flarum-mentions::emails.html.groupMentioned', ];
} }
public function getEmailSubject(TranslatorInterface $translator): string public function getEmailSubject(TranslatorInterface $translator): string

View File

@@ -39,9 +39,12 @@ class PostMentionedBlueprint implements BlueprintInterface, MailableInterface
return ['replyNumber' => (int) $this->reply->number]; return ['replyNumber' => (int) $this->reply->number];
} }
public function getEmailView(): string|array public function getEmailViews(): array
{ {
return ['text' => 'flarum-mentions::emails.postMentioned']; return [
'text' => 'flarum-mentions::emails.plain.postMentioned',
'html' => 'flarum-mentions::emails.html.postMentioned',
];
} }
public function getEmailSubject(TranslatorInterface $translator): string public function getEmailSubject(TranslatorInterface $translator): string

View File

@@ -38,9 +38,12 @@ class UserMentionedBlueprint implements BlueprintInterface, MailableInterface
return null; return null;
} }
public function getEmailView(): string|array public function getEmailViews(): array
{ {
return ['text' => 'flarum-mentions::emails.userMentioned']; return [
'text' => 'flarum-mentions::emails.plain.userMentioned',
'html' => 'flarum-mentions::emails.html.userMentioned'
];
} }
public function getEmailSubject(TranslatorInterface $translator): string public function getEmailSubject(TranslatorInterface $translator): string

View File

@@ -0,0 +1,13 @@
@extends('flarum.forum::email.html.notification.base')
@section('notificationContent')
{!! $formatter->convert($translator->trans('flarum-mentions.email.group_mentioned.html.body', [
'{mentioner_display_name}' => $blueprint->post->user->display_name,
'{title}' => $blueprint->post->discussion->title,
'{url}' => $url->to('forum')->route('discussion', ['id' => $blueprint->post->discussion_id, 'near' => $blueprint->post->number])
])) !!}
@endsection
@section('contentPreview')
{!! $blueprint->post->formatContent() !!}
@endsection

View File

@@ -0,0 +1,14 @@
@extends('flarum.forum::email.html.notification.base')
@section('notificationContent')
{!! $formatter->convert($translator->trans('flarum-mentions.email.post_mentioned.html.body', [
'{replier_display_name}' => $blueprint->reply->user->display_name,
'{post_number}' => $blueprint->post->number,
'{title}' => $blueprint->post->discussion->title,
'{url}' => $url->to('forum')->route('discussion', ['id' => $blueprint->reply->discussion_id, 'near' => $blueprint->reply->number])
])) !!}
@endsection
@section('contentPreview')
{!! $blueprint->reply->formatContent() !!}
@endsection

View File

@@ -0,0 +1,13 @@
@extends('flarum.forum::email.html.notification.base')
@section('notificationContent')
{!! $formatter->convert($translator->trans('flarum-mentions.email.user_mentioned.html.body', [
'{mentioner_display_name}' => $blueprint->post->user->display_name,
'{title}' => $blueprint->post->discussion->title,
'{url}' => $url->to('forum')->route('discussion', ['id' => $blueprint->post->discussion_id, 'near' => $blueprint->post->number])
])) !!}
@endsection
@section('contentPreview')
{!! $blueprint->post->formatContent() !!}
@endsection

View File

@@ -1,7 +1,10 @@
{!! $translator->trans('flarum-mentions.email.group_mentioned.body', [ @extends('flarum.forum::email.plain.notification.base')
'{recipient_display_name}' => $user->display_name,
@section('content')
{!! $translator->trans('flarum-mentions.email.group_mentioned.plain.body', [
'{mentioner_display_name}' => $blueprint->post->user->display_name, '{mentioner_display_name}' => $blueprint->post->user->display_name,
'{title}' => $blueprint->post->discussion->title, '{title}' => $blueprint->post->discussion->title,
'{url}' => $url->to('forum')->route('discussion', ['id' => $blueprint->post->discussion_id, 'near' => $blueprint->post->number]), '{url}' => $url->to('forum')->route('discussion', ['id' => $blueprint->post->discussion_id, 'near' => $blueprint->post->number]),
'{content}' => $blueprint->post->content '{content}' => $blueprint->post->content
]) !!} ]) !!}
@endsection

View File

@@ -1,8 +1,11 @@
{!! $translator->trans('flarum-mentions.email.post_mentioned.body', [ @extends('flarum.forum::email.plain.notification.base')
'{recipient_display_name}' => $user->display_name,
@section('content')
{!! $translator->trans('flarum-mentions.email.post_mentioned.plain.body', [
'{replier_display_name}' => $blueprint->reply->user->display_name, '{replier_display_name}' => $blueprint->reply->user->display_name,
'{post_number}' => $blueprint->post->number, '{post_number}' => $blueprint->post->number,
'{title}' => $blueprint->post->discussion->title, '{title}' => $blueprint->post->discussion->title,
'{url}' => $url->to('forum')->route('discussion', ['id' => $blueprint->reply->discussion_id, 'near' => $blueprint->reply->number]), '{url}' => $url->to('forum')->route('discussion', ['id' => $blueprint->reply->discussion_id, 'near' => $blueprint->reply->number]),
'{content}' => $blueprint->reply->content '{content}' => $blueprint->reply->content
]) !!} ]) !!}
@endsection

View File

@@ -1,7 +1,10 @@
{!! $translator->trans('flarum-mentions.email.user_mentioned.body', [ @extends('flarum.forum::email.plain.notification.base')
'{recipient_display_name}' => $user->display_name,
@section('content')
{!! $translator->trans('flarum-mentions.email.user_mentioned.plain.body', [
'{mentioner_display_name}' => $blueprint->post->user->display_name, '{mentioner_display_name}' => $blueprint->post->user->display_name,
'{title}' => $blueprint->post->discussion->title, '{title}' => $blueprint->post->discussion->title,
'{url}' => $url->to('forum')->route('discussion', ['id' => $blueprint->post->discussion_id, 'near' => $blueprint->post->number]), '{url}' => $url->to('forum')->route('discussion', ['id' => $blueprint->post->discussion_id, 'near' => $blueprint->post->number]),
'{content}' => $blueprint->post->content '{content}' => $blueprint->post->content
]) !!} ]) !!}
@endsection

View File

@@ -51,21 +51,25 @@ flarum-subscriptions:
# These translations are used in emails sent when a post is made in a subscribed discussion # These translations are used in emails sent when a post is made in a subscribed discussion
new_post: new_post:
subject: "[New Post] {title}" subject: "[New Post] {title}"
body: | plain:
Hey {recipient_display_name}! body: |
{poster_display_name} just posted in a discussion you're following: {title}.
{poster_display_name} made a post in a discussion you're following: {title}. To view the new activity, check out the following link:
{url}
To view the new activity, check out the following link: ---
{url}
--- {content}
{content} ---
--- You won't receive any more notifications about this discussion until you're up-to-date.
html:
body: |
{poster_display_name} just posted in a discussion you're following: [{title}]({url}).
You won't receive any more notifications about this discussion until you're up-to-date. You won't recieve any more notifications about this discussion until you're up-to-date.
## ##
# REUSED TRANSLATIONS - These keys should not be used directly in code! # REUSED TRANSLATIONS - These keys should not be used directly in code!

View File

@@ -39,9 +39,11 @@ class NewPostBlueprint implements BlueprintInterface, MailableInterface
return ['postNumber' => (int) $this->post->number]; return ['postNumber' => (int) $this->post->number];
} }
public function getEmailView(): string|array public function getEmailViews(): array
{ {
return ['text' => 'flarum-subscriptions::emails.newPost']; return [
'text' => 'flarum-subscriptions::emails.plain.newPost',
'html' => 'flarum-subscriptions::emails.html.newPost', ];
} }
public function getEmailSubject(TranslatorInterface $translator): string public function getEmailSubject(TranslatorInterface $translator): string

View File

@@ -0,0 +1,13 @@
@extends('flarum.forum::email.html.notification.base')
@section('notificationContent')
{!! $formatter->convert($translator->trans('flarum-subscriptions.email.new_post.html.body', [
'{poster_display_name}' => $blueprint->post->user->display_name,
'{title}' => $blueprint->post->discussion->title,
'{url}' => $url->to('forum')->route('discussion', ['id' => $blueprint->post->discussion_id, 'near' => $blueprint->post->number])
])) !!}
@endsection
@section('contentPreview')
{!! $blueprint->post->formatContent() !!}
@endsection

View File

@@ -1,7 +1,10 @@
{!! $translator->trans('flarum-subscriptions.email.new_post.body', [ @extends('flarum.forum::email.plain.notification.base')
'{recipient_display_name}' => $user->display_name,
@section('content')
{!! $translator->trans('flarum-subscriptions.email.new_post.plain.body', [
'{poster_display_name}' => $blueprint->post->user->display_name, '{poster_display_name}' => $blueprint->post->user->display_name,
'{title}' => $blueprint->post->discussion->title, '{title}' => $blueprint->post->discussion->title,
'{url}' => $url->to('forum')->route('discussion', ['id' => $blueprint->post->discussion_id, 'near' => $blueprint->post->number]), '{url}' => $url->to('forum')->route('discussion', ['id' => $blueprint->post->discussion_id, 'near' => $blueprint->post->number]),
'{content}' => $blueprint->post->content '{content}' => $blueprint->post->content
]) !!} ]) !!}
@endsection

View File

@@ -52,20 +52,22 @@ flarum-suspend:
no_reason_given: No reason was given for this suspension. no_reason_given: No reason was given for this suspension.
suspended: suspended:
subject: Your account has been suspended subject: Your account has been suspended
body: | plain:
Hey {recipient_display_name}, body: |
You have been suspended for the following reason:
You have been suspended for the following reason: ---
{suspension_message}
--- ---
{suspension_message} html:
--- body: "You have been suspended from {forumTitle} for the following reason:"
unsuspended: unsuspended:
subject: Your account has been unsuspended subject: Your account has been unsuspended
body: | plain:
Hey {recipient_display_name}, body: |
You have been unsuspended. You can head back to the forum by clicking on the following link:
You have been unsuspended. You can head back to the forum by clicking on the following link: {forum_url}
html:
{forum_url} body: "You have been unsuspended. You can head back to [{forumTitle}]({forum_url}) when you are ready."

View File

@@ -9,6 +9,7 @@
namespace Flarum\Suspend\Notification; namespace Flarum\Suspend\Notification;
use Carbon\Carbon;
use Carbon\CarbonInterface; use Carbon\CarbonInterface;
use Flarum\Database\AbstractModel; use Flarum\Database\AbstractModel;
use Flarum\Locale\TranslatorInterface; use Flarum\Locale\TranslatorInterface;
@@ -35,7 +36,7 @@ class UserSuspendedBlueprint implements BlueprintInterface, MailableInterface
public function getData(): CarbonInterface public function getData(): CarbonInterface
{ {
return $this->user->suspended_until; return Carbon::now();
} }
public static function getType(): string public static function getType(): string
@@ -48,9 +49,11 @@ class UserSuspendedBlueprint implements BlueprintInterface, MailableInterface
return User::class; return User::class;
} }
public function getEmailView(): string|array public function getEmailViews(): array
{ {
return ['text' => 'flarum-suspend::emails.suspended']; return [
'text' => 'flarum-suspend::emails.plain.suspended',
'html' => 'flarum-suspend::emails.html.suspended', ];
} }
public function getEmailSubject(TranslatorInterface $translator): string public function getEmailSubject(TranslatorInterface $translator): string

View File

@@ -49,9 +49,11 @@ class UserUnsuspendedBlueprint implements BlueprintInterface, MailableInterface
return User::class; return User::class;
} }
public function getEmailView(): string|array public function getEmailViews(): array
{ {
return ['text' => 'flarum-suspend::emails.unsuspended']; return [
'text' => 'flarum-suspend::emails.plain.unsuspended',
'html' => 'flarum-suspend::emails.html.unsuspended', ];
} }
public function getEmailSubject(TranslatorInterface $translator): string public function getEmailSubject(TranslatorInterface $translator): string

View File

@@ -50,7 +50,7 @@ class SuspendUserTest extends TestCase
{ {
$response = $this->sendSuspensionRequest($authenticatedAs, $targetUserId); $response = $this->sendSuspensionRequest($authenticatedAs, $targetUserId);
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode(), $response->getBody()->getContents());
} }
/** /**

View File

@@ -0,0 +1,11 @@
@extends('flarum.forum::email.html.information.base')
@section('informationContent')
{!! $formatter->convert($translator->trans('flarum-suspend.email.suspended.html.body', [
'{forumTitle}' => $settings->get('forum_title')
])) !!}
@endsection
@section('contentPreview')
{!! $formatter->convert($blueprint->user->suspend_message ?? $translator->trans('flarum-suspend.email.no_reason_given')) !!}
@endsection

View File

@@ -0,0 +1,8 @@
@extends('flarum.forum::email.html.information.base')
@section('content')
{!! $formatter->convert($translator->trans('flarum-suspend.email.unsuspended.html.body', [
'{forumTitle}' => $settings->get('forum_title'),
'{forum_url}' => $url->to('forum')->base(),
])) !!}
@endsection

View File

@@ -0,0 +1,7 @@
@extends('flarum.forum::email.plain.information.base')
@section('content')
{!! $translator->trans('flarum-suspend.email.suspended.plain.body', [
'{suspension_message}' => $blueprint->user->suspend_message ?? $translator->trans('flarum-suspend.email.no_reason_given'),
]) !!}
@endsection

View File

@@ -0,0 +1,7 @@
@extends('flarum.forum::email.plain.information.base')
@section('content')
{!! $translator->trans('flarum-suspend.email.unsuspended.plain.body', [
'{forum_url}' => $url->to('forum')->base(),
]) !!}
@endsection

View File

@@ -1,4 +0,0 @@
{!! $translator->trans('flarum-suspend.email.suspended.body', [
'{recipient_display_name}' => $user->display_name,
'{suspension_message}' => $blueprint->user->suspend_message ?? $translator->trans('flarum-suspend.email.no_reason_given'),
]) !!}

View File

@@ -1,4 +0,0 @@
{!! $translator->trans('flarum-suspend.email.unsuspended.body', [
'{recipient_display_name}' => $user->display_name,
'{forum_url}' => $url->to('forum')->base(),
]) !!}

View File

@@ -8,6 +8,7 @@ import type { IPageAttrs } from '../../common/components/Page';
import type { AlertIdentifier } from '../../common/states/AlertManagerState'; import type { AlertIdentifier } from '../../common/states/AlertManagerState';
import type Mithril from 'mithril'; import type Mithril from 'mithril';
import type { SaveSubmitEvent } from './AdminPage'; import type { SaveSubmitEvent } from './AdminPage';
import ItemList from '../../common/utils/ItemList';
export interface MailSettings { export interface MailSettings {
data: { data: {
@@ -65,16 +66,58 @@ export default class MailPage<CustomAttrs extends IPageAttrs = IPageAttrs> exten
return <LoadingIndicator />; return <LoadingIndicator />;
} }
const fields = this.driverFields![this.setting('mail_driver')()]; const mailSettings = this.mailSettingItems().toArray();
const fieldKeys = Object.keys(fields);
return ( return (
<div className="Form"> <div className="Form">
{this.buildSettingComponent({ {mailSettings.map((settingComponent) => settingComponent)}
type: 'text', {this.submitButton()}
setting: 'mail_from',
label: app.translator.trans('core.admin.email.addresses_heading'), <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({ {this.buildSettingComponent({
type: 'select', type: 'select',
setting: 'mail_driver', setting: 'mail_driver',
@@ -104,16 +147,11 @@ export default class MailPage<CustomAttrs extends IPageAttrs = IPageAttrs> exten
</div> </div>
</FieldSet> </FieldSet>
)} )}
{this.submitButton()} </div>,
60
<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>
); );
return items;
} }
sendTestEmail() { sendTestEmail() {

View File

@@ -126,6 +126,12 @@ core:
description: "Configure the driver, settings and addresses your forum will use to send email." description: "Configure the driver, settings and addresses your forum will use to send email."
driver_heading: Choose a Driver driver_heading: Choose a Driver
driver_label: 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 from_label: Sender
mail_encryption_label: Encryption mail_encryption_label: Encryption
mail_host_label: Host mail_host_label: Host
@@ -727,6 +733,16 @@ core:
submit_button: => core.ref.save_changes submit_button: => core.ref.save_changes
title: => core.ref.reset_your_password 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. # Translations in this namespace are used in messages output by the API.
api: api:
invalid_username_message: "The username may only contain letters, numbers, and dashes." 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. # Translations in this namespace are used in emails sent by the forum.
email: 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. # These translations are used in emails sent when users register new accounts.
activate_account: activate_account:
subject: Activate Your New Account subject: Activate Your New Account
body: | body: |
Hey {username}!
Someone (hopefully you!) has signed up to {forum} with this email address. 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: If this was you, simply click the following link and your account will be activated:
@@ -755,8 +788,6 @@ core:
confirm_email: confirm_email:
subject: Confirm Your New Email Address subject: Confirm Your New Email Address
body: | body: |
Hey {username}!
Someone (hopefully you!) has changed their email address on {forum} to this one. 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: If this was you, simply click the following link and your email will be confirmed:
@@ -768,8 +799,6 @@ core:
reset_password: reset_password:
subject: => core.ref.reset_your_password subject: => core.ref.reset_your_password
body: | body: |
Hey {username}!
Someone (hopefully you!) has submitted a forgotten password request for your account on {forum}. 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: If this was you, click the following link to reset your password:
@@ -781,8 +810,6 @@ core:
send_test: send_test:
subject: Flarum Email Test subject: Flarum Email Test
body: | body: |
Hey {username}!
This is a test email to confirm that your Flarum email configuration is working properly. 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! 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\Http\RequestUtil;
use Flarum\Locale\TranslatorInterface; use Flarum\Locale\TranslatorInterface;
use Flarum\Mail\Job\SendInformationalEmailJob;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Mail\Mailer; use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Mail\Message; use Illuminate\Contracts\Queue\Factory;
use Laminas\Diactoros\Response\EmptyResponse; use Laminas\Diactoros\Response\EmptyResponse;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
@@ -22,7 +24,9 @@ class SendTestMailController implements RequestHandlerInterface
{ {
public function __construct( public function __construct(
protected Mailer $mailer, 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 = RequestUtil::getActor($request);
$actor->assertAdmin(); $actor->assertAdmin();
$body = $this->translator->trans('core.email.send_test.body', ['username' => $actor->username]); $this->queue->connection('sync')->push(
new SendInformationalEmailJob(
$this->mailer->raw($body, function (Message $message) use ($actor) { email: $actor->email,
$message->to($actor->email); displayName: $actor->display_name,
$message->subject($this->translator->trans('core.email.send_test.subject')); 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(); return new EmptyResponse();
} }

View File

@@ -204,4 +204,19 @@ class Formatter
return $attributes; 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([ $view->share([
'translator' => $container->make(TranslatorInterface::class), 'translator' => $container->make(TranslatorInterface::class),
'settings' => $container->make(SettingsRepositoryInterface::class) 'settings' => $container->make(SettingsRepositoryInterface::class),
'formatter' => $container->make(Formatter::class),
]); ]);
$events->listen( $events->listen(

View File

@@ -43,6 +43,18 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
$route->toForum(Content\AssertRegistered::class) $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( $map->get(
'/logout', '/logout',
'logout', 'logout',

View File

@@ -58,6 +58,7 @@ class WriteSettings implements Step
'forum_title' => 'A new Flarum forum', 'forum_title' => 'A new Flarum forum',
'forum_description' => '', 'forum_description' => '',
'mail_driver' => 'mail', 'mail_driver' => 'mail',
'mail_format' => 'multipart',
'mail_from' => 'noreply@localhost', 'mail_from' => 'noreply@localhost',
'slug_driver_Flarum\User\User' => 'default', 'slug_driver_Flarum\User\User' => 'default',
'theme_colored_header' => '0', '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\Container\Container;
use Illuminate\Contracts\Mail\Mailer as MailerContract; use Illuminate\Contracts\Mail\Mailer as MailerContract;
use Illuminate\Contracts\Validation\Factory; use Illuminate\Contracts\Validation\Factory;
use Illuminate\Mail\Mailer;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Symfony\Component\Mailer\Transport\TransportInterface; use Symfony\Component\Mailer\Transport\TransportInterface;
@@ -62,18 +61,20 @@ class MailServiceProvider extends AbstractServiceProvider
}); });
$this->container->singleton('mailer', function (Container $container): MailerContract { $this->container->singleton('mailer', function (Container $container): MailerContract {
$settings = $container->make(SettingsRepositoryInterface::class);
$mailer = new Mailer( $mailer = new Mailer(
'flarum', 'flarum',
$container['view'], $container['view'],
$container['symfony.mailer.transport'], $container['symfony.mailer.transport'],
$container['events'] $container['events'],
$settings,
); );
if ($container->bound('queue')) { if ($container->bound('queue')) {
$mailer->setQueue($container->make('queue')); $mailer->setQueue($container->make('queue'));
} }
$settings = $container->make(SettingsRepositoryInterface::class);
$mailer->alwaysFrom($settings->get('mail_from'), $settings->get('forum_title')); $mailer->alwaysFrom($settings->get('mail_from'), $settings->get('forum_title'));
return $mailer; 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; namespace Flarum\Notification\Job;
use Flarum\Notification\Blueprint\BlueprintInterface;
use Flarum\Notification\MailableInterface; use Flarum\Notification\MailableInterface;
use Flarum\Notification\NotificationMailer; use Flarum\Notification\NotificationMailer;
use Flarum\Queue\AbstractJob; use Flarum\Queue\AbstractJob;
@@ -17,7 +18,7 @@ use Flarum\User\User;
class SendEmailNotificationJob extends AbstractJob class SendEmailNotificationJob extends AbstractJob
{ {
public function __construct( public function __construct(
private readonly MailableInterface $blueprint, private readonly MailableInterface&BlueprintInterface $blueprint,
private readonly User $recipient private readonly User $recipient
) { ) {
} }

View File

@@ -14,9 +14,16 @@ use Flarum\Locale\TranslatorInterface;
interface MailableInterface 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. * Get the subject line for a notification email.

View File

@@ -9,34 +9,70 @@
namespace Flarum\Notification; namespace Flarum\Notification;
use Flarum\Http\UrlGenerator;
use Flarum\Locale\TranslatorInterface; use Flarum\Locale\TranslatorInterface;
use Flarum\Notification\Blueprint\BlueprintInterface;
use Flarum\Settings\SettingsRepositoryInterface; use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\User; use Flarum\User\User;
use Illuminate\Contracts\Mail\Mailer; use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Mail\Message; use Illuminate\Mail\Message;
use Illuminate\Support\Arr;
class NotificationMailer class NotificationMailer
{ {
public function __construct( public function __construct(
protected Mailer $mailer, protected Mailer $mailer,
protected TranslatorInterface $translator, 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. // 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')); $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( $this->mailer->send(
$blueprint->getEmailView(), $this->getEmailViews($blueprint),
compact('blueprint', 'user'), compact('blueprint', 'user', 'unsubscribeLink', 'settingsLink', 'type', 'forumTitle', 'username', 'userEmail'),
function (Message $message) use ($blueprint, $user) { function (Message $message) use ($blueprint, $user) {
$message->to($user->email, $user->display_name) $message->to($user->email, $user->display_name)
->subject($blueprint->getEmailSubject($this->translator)); ->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. * The cached queue instance.
*/ */
private ?Queue $queue; private ?Queue $queue = null;
/** /**
* Expects a callback that will be called to instantiate the queue adapter, * 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([ return new Collection([
'theme_primary_color' => '#4D698E', 'theme_primary_color' => '#4D698E',
'theme_secondary_color' => '#4D698E', 'theme_secondary_color' => '#4D698E',
'mail_format' => 'multipart',
]); ]);
}); });

View File

@@ -9,7 +9,8 @@
namespace Flarum\User; namespace Flarum\User;
use Flarum\Mail\Job\SendRawEmailJob; use Flarum\Mail\Job\SendInformationalEmailJob;
use Illuminate\Support\Arr;
trait AccountActivationMailerTrait trait AccountActivationMailerTrait
{ {
@@ -38,6 +39,12 @@ trait AccountActivationMailerTrait
$body = $this->translator->trans('core.email.activate_account.body', $data); $body = $this->translator->trans('core.email.activate_account.body', $data);
$subject = $this->translator->trans('core.email.activate_account.subject'); $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\Http\UrlGenerator;
use Flarum\Locale\TranslatorInterface; use Flarum\Locale\TranslatorInterface;
use Flarum\Mail\Job\SendRawEmailJob; use Flarum\Mail\Job\SendInformationalEmailJob;
use Flarum\Settings\SettingsRepositoryInterface; use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\Event\EmailChangeRequested; use Flarum\User\Event\EmailChangeRequested;
use Illuminate\Contracts\Queue\Queue; use Illuminate\Contracts\Queue\Queue;
use Illuminate\Support\Arr;
class EmailConfirmationMailer class EmailConfirmationMailer
{ {
@@ -34,7 +35,13 @@ class EmailConfirmationMailer
$body = $this->translator->trans('core.email.confirm_email.body', $data); $body = $this->translator->trans('core.email.confirm_email.body', $data);
$subject = $this->translator->trans('core.email.confirm_email.subject'); $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 protected function generateToken(User $user, string $email): EmailToken

View File

@@ -11,12 +11,13 @@ namespace Flarum\User\Job;
use Flarum\Http\UrlGenerator; use Flarum\Http\UrlGenerator;
use Flarum\Locale\TranslatorInterface; use Flarum\Locale\TranslatorInterface;
use Flarum\Mail\Job\SendRawEmailJob; use Flarum\Mail\Job\SendInformationalEmailJob;
use Flarum\Queue\AbstractJob; use Flarum\Queue\AbstractJob;
use Flarum\Settings\SettingsRepositoryInterface; use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\PasswordToken; use Flarum\User\PasswordToken;
use Flarum\User\UserRepository; use Flarum\User\UserRepository;
use Illuminate\Contracts\Queue\Queue; use Illuminate\Contracts\Queue\Queue;
use Illuminate\Support\Arr;
class RequestPasswordResetJob extends AbstractJob class RequestPasswordResetJob extends AbstractJob
{ {
@@ -50,6 +51,13 @@ class RequestPasswordResetJob extends AbstractJob
$body = $translator->trans('core.email.reset_password.body', $data); $body = $translator->trans('core.email.reset_password.body', $data);
$subject = $translator->trans('core.email.reset_password.subject'); $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; 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