mirror of
https://github.com/appwrite/appwrite
synced 2026-04-21 13:37:16 +00:00
Reduces job processing time by avoiding repeated TCP connect, TLS handshake, and SMTP AUTH on every email sent. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
228 lines
7.7 KiB
PHP
228 lines
7.7 KiB
PHP
<?php
|
|
|
|
namespace Appwrite\Platform\Workers;
|
|
|
|
use Appwrite\Template\Template;
|
|
use Exception;
|
|
use PHPMailer\PHPMailer\PHPMailer;
|
|
use Swoole\Runtime;
|
|
use Utopia\Database\Document;
|
|
use Utopia\Logger\Log;
|
|
use Utopia\Platform\Action;
|
|
use Utopia\Queue\Message;
|
|
use Utopia\Registry\Registry;
|
|
use Utopia\System\System;
|
|
|
|
class Mails extends Action
|
|
{
|
|
protected int $previewMaxLen = 150;
|
|
|
|
protected string $whitespaceCodes = ' ‌​‍‎‏';
|
|
|
|
|
|
public static function getName(): string
|
|
{
|
|
return 'mails';
|
|
}
|
|
|
|
/**
|
|
* @throws Exception
|
|
*/
|
|
public function __construct()
|
|
{
|
|
$this
|
|
->desc('Mails worker')
|
|
->inject('message')
|
|
->inject('project')
|
|
->inject('register')
|
|
->inject('log')
|
|
->callback($this->action(...));
|
|
}
|
|
|
|
/**
|
|
* @var array<string, string>
|
|
*/
|
|
protected array $richTextParams = [
|
|
'b' => '<strong>',
|
|
'/b' => '</strong>',
|
|
];
|
|
|
|
/**
|
|
* @param Message $message
|
|
* @param Registry $register
|
|
* @param Log $log
|
|
* @throws \PHPMailer\PHPMailer\Exception
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function action(Message $message, Document $project, Registry $register, Log $log): void
|
|
{
|
|
Runtime::setHookFlags(SWOOLE_HOOK_ALL ^ SWOOLE_HOOK_TCP);
|
|
$payload = $message->getPayload() ?? [];
|
|
|
|
if (empty($payload)) {
|
|
throw new Exception('Missing payload');
|
|
}
|
|
|
|
$smtp = $payload['smtp'];
|
|
|
|
if (empty($smtp) && empty(System::getEnv('_APP_SMTP_HOST'))) {
|
|
throw new Exception('Skipped mail processing. No SMTP configuration has been set.');
|
|
}
|
|
|
|
$type = empty($smtp) ? 'cloud' : 'smtp';
|
|
$log->addTag('type', $type);
|
|
|
|
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
|
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN');
|
|
|
|
$recipient = $payload['recipient'];
|
|
$subject = $payload['subject'];
|
|
$variables = $payload['variables'];
|
|
$variables['host'] = $protocol . '://' . $hostname;
|
|
$name = $payload['name'];
|
|
$body = $payload['body'];
|
|
$preview = $payload['preview'] ?? '';
|
|
|
|
$variables['subject'] = $subject;
|
|
$variables['heading'] = $variables['heading'] ?? $subject;
|
|
$variables['year'] = date("Y");
|
|
|
|
$attachment = $payload['attachment'] ?? [];
|
|
$bodyTemplate = $payload['bodyTemplate'];
|
|
if (empty($bodyTemplate)) {
|
|
$bodyTemplate = __DIR__ . '/../../../../app/config/locale/templates/email-base.tpl';
|
|
}
|
|
$bodyTemplate = Template::fromFile($bodyTemplate);
|
|
$bodyTemplate->setParam('{{body}}', $body, escapeHtml: false);
|
|
foreach ($variables as $key => $value) {
|
|
// TODO: hotfix for redirect param
|
|
$bodyTemplate->setParam('{{' . $key . '}}', $value, escapeHtml: $key !== 'redirect');
|
|
}
|
|
foreach ($this->richTextParams as $key => $value) {
|
|
$bodyTemplate->setParam('{{' . $key . '}}', $value, escapeHtml: false);
|
|
}
|
|
|
|
$previewWhitespace = '';
|
|
|
|
if (!empty($preview)) {
|
|
$previewTemplate = Template::fromString($preview);
|
|
foreach ($variables as $key => $value) {
|
|
$previewTemplate->setParam('{{' . $key . '}}', $value);
|
|
}
|
|
// render() will return the subject in <p> tags, so use strip_tags() to remove them
|
|
$preview = \strip_tags($previewTemplate->render());
|
|
|
|
$previewLen = strlen($preview);
|
|
if ($previewLen < $this->previewMaxLen) {
|
|
$previewWhitespace = str_repeat($this->whitespaceCodes, $this->previewMaxLen - $previewLen);
|
|
}
|
|
}
|
|
|
|
|
|
$bodyTemplate->setParam('{{preview}}', $preview);
|
|
$bodyTemplate->setParam('{{previewWhitespace}}', $previewWhitespace, false);
|
|
|
|
$body = $bodyTemplate->render();
|
|
|
|
$subjectTemplate = Template::fromString($subject);
|
|
foreach ($variables as $key => $value) {
|
|
$subjectTemplate->setParam('{{' . $key . '}}', $value);
|
|
}
|
|
// render() will return the subject in <p> tags, so use strip_tags() to remove them
|
|
$subject = \strip_tags($subjectTemplate->render());
|
|
|
|
/** @var PHPMailer $mail */
|
|
$mail = empty($smtp)
|
|
? $register->get('smtp')
|
|
: $this->getMailer($smtp);
|
|
|
|
$mail->clearAddresses();
|
|
$mail->clearAllRecipients();
|
|
$mail->clearReplyTos();
|
|
$mail->clearAttachments();
|
|
$mail->clearBCCs();
|
|
$mail->clearCCs();
|
|
$mail->addAddress($recipient, $name);
|
|
$mail->Subject = $subject;
|
|
$mail->Body = $body;
|
|
|
|
$mail->AltBody = $body;
|
|
$mail->AltBody = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $mail->AltBody);
|
|
$mail->AltBody = \strip_tags($mail->AltBody);
|
|
$mail->AltBody = \trim($mail->AltBody);
|
|
|
|
$replyTo = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
|
|
$replyToName = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
|
|
|
|
$customMailOptions = $payload['customMailOptions'] ?? [];
|
|
|
|
// fallback hierarchy: Custom options > SMTP config > Defaults.
|
|
if (!empty($customMailOptions['senderEmail']) || !empty($customMailOptions['senderName'])) {
|
|
$fromEmail = $customMailOptions['senderEmail'] ?? $mail->From;
|
|
$fromName = $customMailOptions['senderName'] ?? $mail->FromName;
|
|
$mail->setFrom($fromEmail, $fromName);
|
|
}
|
|
|
|
if (!empty($customMailOptions['replyToEmail']) || !empty($customMailOptions['replyToName'])) {
|
|
$replyTo = $customMailOptions['replyToEmail'] ?? $replyTo;
|
|
$replyToName = $customMailOptions['replyToName'] ?? $replyToName;
|
|
} elseif (!empty($smtp)) {
|
|
$replyTo = !empty($smtp['replyTo']) ? $smtp['replyTo'] : ($smtp['senderEmail'] ?? $replyTo);
|
|
$replyToName = $smtp['senderName'] ?? $replyToName;
|
|
}
|
|
|
|
$mail->addReplyTo($replyTo, $replyToName);
|
|
if (!empty($attachment['content'] ?? '')) {
|
|
$mail->AddStringAttachment(
|
|
base64_decode($attachment['content']),
|
|
$attachment['filename'] ?? 'unknown.file',
|
|
$attachment['encoding'] ?? PHPMailer::ENCODING_BASE64,
|
|
$attachment['type'] ?? 'plain/text'
|
|
);
|
|
}
|
|
|
|
try {
|
|
$mail->send();
|
|
} catch (\Throwable $error) {
|
|
if ($type === 'smtp') {
|
|
throw new Exception('Error sending mail: ' . $error->getMessage(), 401);
|
|
}
|
|
throw new Exception('Error sending mail: ' . $error->getMessage(), 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array $smtp
|
|
* @return PHPMailer
|
|
* @throws \PHPMailer\PHPMailer\Exception
|
|
*/
|
|
protected function getMailer(array $smtp): PHPMailer
|
|
{
|
|
$mail = new PHPMailer(true);
|
|
|
|
$mail->isSMTP();
|
|
|
|
$username = $smtp['username'];
|
|
$password = $smtp['password'];
|
|
|
|
$mail->XMailer = 'Appwrite Mailer';
|
|
$mail->Host = $smtp['host'];
|
|
$mail->Port = $smtp['port'];
|
|
$mail->SMTPAuth = (!empty($username) && !empty($password));
|
|
$mail->Username = $username;
|
|
$mail->Password = $password;
|
|
$mail->SMTPSecure = $smtp['secure'];
|
|
$mail->SMTPAutoTLS = false;
|
|
$mail->SMTPKeepAlive = true;
|
|
$mail->CharSet = 'UTF-8';
|
|
$mail->Timeout = 10; /* Connection timeout */
|
|
$mail->getSMTPInstance()->Timelimit = 30; /* Timeout for each individual SMTP command (e.g. HELO, EHLO, etc.) */
|
|
|
|
$mail->setFrom($smtp['senderEmail'], $smtp['senderName']);
|
|
|
|
$mail->isHTML();
|
|
|
|
return $mail;
|
|
}
|
|
}
|