Skip to content
Draft
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
14 changes: 14 additions & 0 deletions core/classes/Alerts/AlertTemplate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

class AlertTemplate
{
public function __construct(
public LanguageKey $title,
public ?LanguageKey $content,
public ?string $link = null,
) {
if ($this->link === null && $this->content === null) {
throw new InvalidArgumentException('Either link or content must be provided');
}
}
}
103 changes: 43 additions & 60 deletions core/classes/Core/Email.php → core/classes/Emails/Email.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,39 +18,62 @@ class Email

public const REGISTRATION = 1;
public const FORGOT_PASSWORD = 3;
public const API_REGISTRATION = 4;
public const FORUM_TOPIC_REPLY = 5;
public const MASS_MESSAGE = 6;
public const TEST_EMAIL = 7;

/**
* @var array<string, string> Placeholders for email templates
*/
private static array $_message_placeholders = [];
public static function send(User $recipient, EmailTemplate $emailTemplate)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to keep support for setting email, Such as Store and Forms module will send emails to quests based on the email they entered

{
$languageCode = DB::getInstance()->get('languages', ['id', '=', $recipient->data()->language_id])->first()->short_code;

return self::sendInternal(
$emailTemplate->id(),
$recipient,
$emailTemplate->subject()->translate($languageCode),
$emailTemplate->renderContent($languageCode)
);
}

public static function sendRaw(int $type, User $recipient, string $subject, string $content)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here i guess ^^

{
return self::sendInternal($type, $recipient, $subject, $content);
}

/**
* Send an email.
* Internal helper method to handle common email sending logic
*
* @param array $recipient Array containing `'email'` and `'name'` strings for the recipient of the email.
* @param string $subject Subject of the email.
* @param string $message Message of the email.
* @param array|null $reply_to Array containing `'email'` and `'name'` strings for the reply-to address,
* if not provided the default setting will be used.
* @return bool|array Returns true if email sent, otherwise returns an array containing the error.
* @param int $type Email type identifier
* @param User $recipient Recipient user object
* @param string $subject Email subject
* @param string $content Email content
* @return bool|array Returns true if email sent, otherwise returns an array containing the error
*/
public static function send(array $recipient, string $subject, string $message, ?array $reply_to = null)
private static function sendInternal(int $type, User $recipient, string $subject, string $content)
{
$email = [
'to' => $recipient,
'subject' => $subject,
'message' => $message,
'replyto' => $reply_to ?? self::getReplyTo(),
'to' => [
'email' => $recipient->data()->email,
'name' => $recipient->getDisplayname(),
],
'subject' => SITE_NAME . ' - ' . $subject,
'message' => $content,
'replyto' => self::getReplyTo(),
];

if (Settings::get('phpmailer') == '1') {
return self::sendMailer($email);
$result = Settings::get('phpmailer') == '1'
? self::sendMailer($email)
: self::sendPHP($email);

if (isset($result['error'])) {
DB::getInstance()->insert('email_errors', [
'type' => $type,
'content' => $result['error'],
'at' => date('U'),
'user_id' => $recipient->data()->id,
]);
}

return self::sendPHP($email);
return $result;
}

/**
Expand Down Expand Up @@ -108,12 +131,10 @@ private static function sendPHP(array $email)
private static function sendMailer(array $email)
{
try {
// Initialise PHPMailer
$mail = new PHPMailer(true);

$mail->IsSMTP();
$mail->SMTPDebug = SMTP::DEBUG_OFF;
$mail->Debugoutput = 'html';
$mail->CharSet = PHPMailer::CHARSET_UTF8;
$mail->Encoding = PHPMailer::ENCODING_BASE64;
$mail->Timeout = 15;
Expand Down Expand Up @@ -156,42 +177,4 @@ private static function sendMailer(array $email)
];
}
}

/**
* Add a custom placeholder/variable for email messages.
*
* @param string $key The key to use for the placeholder, should be enclosed in square brackets.
* @param string|Closure(Language, string): string $value The value to replace the placeholder with.
*/
public static function addPlaceholder(string $key, $value): void
{
self::$_message_placeholders[$key] = $value;
}

/**
* Format an email template and replace placeholders.
*
* @param string $email Name of email to format.
* @param Language $viewing_language Instance of Language class to use for translations.
* @return string Formatted email.
*/
public static function formatEmail(string $email, Language $viewing_language): string
{
$placeholders = array_keys(self::$_message_placeholders);

$placeholder_values = [];
foreach (self::$_message_placeholders as $value) {
if (is_callable($value)) {
$placeholder_values[] = $value($viewing_language, $email);
} else {
$placeholder_values[] = $value;
}
}

return str_replace(
$placeholders,
$placeholder_values,
file_get_contents(implode(DIRECTORY_SEPARATOR, [ROOT_PATH, 'custom', 'templates', TEMPLATE, 'email', $email . '.html']))
);
}
}
97 changes: 97 additions & 0 deletions core/classes/Emails/EmailTemplate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

abstract class EmailTemplate
{
/**
* @var array<string, string> Placeholders for all email templates
*/
private static array $_global_placeholders = [];

/**
* @var array<string, string> Placeholders for this email template
*/
private array $_placeholders = [];

public function __construct()
{
self::addGlobalPlaceholder('[Sitename]', Output::getClean(SITE_NAME));
self::addGlobalPlaceholder('[Greeting]', new LanguageKey('emails', 'greeting'));
self::addGlobalPlaceholder('[Thanks]', new LanguageKey('emails', 'thanks'));
}

public abstract function id(): int;
Comment thread
tadhgboyle marked this conversation as resolved.
Outdated

/**
* Returns the snake_case representation of the email template name,
* derived from the class name with "EmailTemplate" removed.
* For example: RegisterEmailTemplate -> "register", ForgotPasswordEmailTemplate -> "forgot_password".
*/
private function name(): string
{
$baseName = str_replace('EmailTemplate', '', static::class);

return strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $baseName));
}

public abstract function subject(): LanguageKey;

/**
* Add a custom placeholder/variable for email messages.
*
* @param string $key The key to use for the placeholder, should be enclosed in square brackets.
* @param string|Closure(Language, string): string $value The value to replace the placeholder with.
*/
public static function addGlobalPlaceholder(string $key, $value): void
{
self::$_global_placeholders[$key] = $value;
}

/**
* Add a custom placeholder/variable for email messages.
*
* @param string $key The key to use for the placeholder, should be enclosed in square brackets.
* @param string|Closure(Language, string): string $value The value to replace the placeholder with.
*/
final public function addPlaceholder(string $key, $value): void
{
$this->_placeholders[$key] = $value;
}

final public function renderContent(string $languageCode): string
{
$allPlaceholders = array_merge(self::$_global_placeholders, $this->_placeholders);
$placeholderKeys = array_keys($allPlaceholders);
$placeholderValues = [];

foreach ($allPlaceholders as $placeholder) {
if ($placeholder instanceof LanguageKey) {

Check failure on line 67 in core/classes/Emails/EmailTemplate.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2)

Instanceof between string and LanguageKey will always evaluate to false.

Check failure on line 67 in core/classes/Emails/EmailTemplate.php

View workflow job for this annotation

GitHub Actions / phpstan (ubuntu-latest, 8.2)

Instanceof between string and LanguageKey will always evaluate to false.
$placeholderValues[] = $placeholder->translate($languageCode);
} else {
$placeholderValues[] = $placeholder;
}
}

return str_replace(
$placeholderKeys,
$placeholderValues,
file_get_contents($this->getPath()),
);
}

private function getPath(): string
{
$name = $this->name();

$customPath = implode(DIRECTORY_SEPARATOR, [ROOT_PATH, 'custom', 'templates', TEMPLATE, 'email', $name . '.html']);
if (file_exists($customPath)) {
return $customPath;
}

$defaultPath = implode(DIRECTORY_SEPARATOR, [ROOT_PATH, 'custom', 'templates', 'DefaultRevamp', 'email', $name . '.html']);
Comment thread
tadhgboyle marked this conversation as resolved.
if (file_exists($defaultPath)) {
return $defaultPath;
}

throw new Exception('Email template not found: ' . $name);
}
}
14 changes: 7 additions & 7 deletions core/classes/Misc/MentionsParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public static function parse(int $author_id, string $content): string
*
* @return string Parsed post content.
*/
public static function parseAndNotify(int $author_id, string $content, string $url, string $notificationType, LanguageKey $notificationTitle): string
public static function parseAndNotify(int $author_id, string $content, string $notificationType, AlertTemplate $notificationAlertTemplate, EmailTemplate $notificationEmailTemplate): string
{
$receipients = self::getRecipients($content, $author_id);

Expand All @@ -46,14 +46,10 @@ public static function parseAndNotify(int $author_id, string $content, string $u

$notification = new Notification(
$notificationType,
$notificationTitle,
// TODO: emails content - right now it will be plaintext and not use a template
$content,
$notificationAlertTemplate,
$notificationEmailTemplate,
$notificationRecipients,
$author_id,
null,
false,
$url,
);

$notification->send();
Expand All @@ -69,6 +65,10 @@ private static function getRecipients(string $content, int $author_id): array
preg_match_all(self::USER_MENTIONS_REGEX, $content, $matches);
$nicknames = $matches[1];

if (empty($nicknames)) {
return [];
}

return DB::getInstance()->query(
'SELECT u.id, u.nickname, EXISTS (SELECT 1 FROM nl2_blocked_users bu WHERE bu.user_id = u.id AND bu.user_blocked_id = ?) as blocked_author FROM nl2_users u WHERE u.nickname IN (' . implode(',', array_map(static fn ($_) => '?', $nicknames)) . ')',
[
Expand Down
19 changes: 0 additions & 19 deletions custom/templates/DefaultRevamp/email/api_register.html

This file was deleted.

19 changes: 0 additions & 19 deletions custom/templates/DefaultRevamp/email/change_password.html

This file was deleted.

17 changes: 17 additions & 0 deletions custom/templates/DefaultRevamp/email/forgot_password.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<title>[Sitename]</title>
</head>

<body>
<div style="width: 640px; font-family: Arial, Helvetica, sans-serif; font-size: 11px;">
<h3>[Greeting]</h3>
<p>[Message]</p>
<a href="[Link]">[Link]</a>
<hr>
[Thanks]<br />[Sitename]
</div>
</body>
</html>
Loading
Loading