mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
fix: retain mails worker action
This commit is contained in:
+1
-2
@@ -73,7 +73,6 @@ if (! isset($args[1])) {
|
||||
|
||||
\array_shift($args);
|
||||
$workerName = $args[0];
|
||||
$actionName = $workerName === 'mails' ? 'notifications' : $workerName;
|
||||
|
||||
if (\str_starts_with($workerName, 'databases')) {
|
||||
$queueName = System::getEnv('_APP_QUEUE_NAME', 'database_db_main');
|
||||
@@ -110,7 +109,7 @@ try {
|
||||
|
||||
$platform->setWorker($worker);
|
||||
$platform->init(Service::TYPE_WORKER, [
|
||||
'workerName' => strtolower($actionName),
|
||||
'workerName' => strtolower($workerName),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine());
|
||||
|
||||
@@ -7,6 +7,7 @@ use Appwrite\Platform\Workers\Certificates;
|
||||
use Appwrite\Platform\Workers\Deletes;
|
||||
use Appwrite\Platform\Workers\Executions;
|
||||
use Appwrite\Platform\Workers\Functions;
|
||||
use Appwrite\Platform\Workers\Mails;
|
||||
use Appwrite\Platform\Workers\Messaging;
|
||||
use Appwrite\Platform\Workers\Migrations;
|
||||
use Appwrite\Platform\Workers\Notifications;
|
||||
@@ -26,6 +27,7 @@ class Workers extends Service
|
||||
->addAction(Deletes::getName(), new Deletes())
|
||||
->addAction(Executions::getName(), new Executions())
|
||||
->addAction(Functions::getName(), new Functions())
|
||||
->addAction(Mails::getName(), new Mails())
|
||||
->addAction(Messaging::getName(), new Messaging())
|
||||
->addAction(Notifications::getName(), new Notifications())
|
||||
->addAction(Webhooks::getName(), new Webhooks())
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Workers;
|
||||
|
||||
use Appwrite\Template\Template;
|
||||
use Exception;
|
||||
use Swoole\Runtime;
|
||||
use Throwable;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Messaging\Adapter\Email as EmailAdapter;
|
||||
use Utopia\Messaging\Adapter\Email\SMTP;
|
||||
use Utopia\Messaging\Messages\Email as EmailMessage;
|
||||
use Utopia\Messaging\Messages\Email\Attachment;
|
||||
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 = ' ‌​‍‎‏';
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $richTextParams = [
|
||||
'b' => '<strong>',
|
||||
'/b' => '</strong>',
|
||||
];
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'mails';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->desc('Mails worker')
|
||||
->inject('message')
|
||||
->inject('project')
|
||||
->inject('register')
|
||||
->inject('log')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(Message $message, Document $project, Registry $register, Log $log): void
|
||||
{
|
||||
if (\class_exists(Runtime::class)) {
|
||||
Runtime::setHookFlags(SWOOLE_HOOK_ALL ^ SWOOLE_HOOK_TCP);
|
||||
}
|
||||
|
||||
$payload = $message->getPayload();
|
||||
if (empty($payload)) {
|
||||
throw new Exception('Missing payload');
|
||||
}
|
||||
|
||||
$smtp = $payload['smtp'] ?? [];
|
||||
if (!\is_array($smtp)) {
|
||||
$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') === 'disabled' ? 'http' : 'https';
|
||||
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', 'localhost'));
|
||||
|
||||
$recipient = (string) ($payload['recipient'] ?? '');
|
||||
$subject = (string) ($payload['subject'] ?? '');
|
||||
$variables = $payload['variables'] ?? [];
|
||||
if (!\is_array($variables)) {
|
||||
$variables = [];
|
||||
}
|
||||
$variables['host'] = $protocol . '://' . $hostname;
|
||||
$name = (string) ($payload['name'] ?? '');
|
||||
$body = (string) ($payload['body'] ?? '');
|
||||
$preview = (string) ($payload['preview'] ?? '');
|
||||
|
||||
$variables['subject'] = $subject;
|
||||
$variables['heading'] = $variables['heading'] ?? $subject;
|
||||
$variables['year'] = \date('Y');
|
||||
|
||||
$attachment = $payload['attachment'] ?? [];
|
||||
if (!\is_array($attachment)) {
|
||||
$attachment = [];
|
||||
}
|
||||
|
||||
$bodyTemplate = (string) ($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) {
|
||||
$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);
|
||||
}
|
||||
$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);
|
||||
}
|
||||
$subject = \strip_tags($subjectTemplate->render());
|
||||
|
||||
/** @var EmailAdapter $adapter */
|
||||
$adapter = empty($smtp)
|
||||
? $register->get('smtp')
|
||||
: new SMTP(
|
||||
host: $smtp['host'],
|
||||
port: (int) $smtp['port'],
|
||||
username: $smtp['username'] ?? '',
|
||||
password: $smtp['password'] ?? '',
|
||||
smtpSecure: $smtp['secure'] ?? '',
|
||||
smtpAutoTLS: false,
|
||||
xMailer: 'Appwrite Mailer',
|
||||
timeout: 10,
|
||||
keepAlive: true,
|
||||
timelimit: 30,
|
||||
);
|
||||
|
||||
$defaultFromEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
|
||||
$defaultFromName = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
|
||||
|
||||
$fromEmail = !empty($smtp) ? ($smtp['senderEmail'] ?? $defaultFromEmail) : $defaultFromEmail;
|
||||
$fromName = !empty($smtp) ? ($smtp['senderName'] ?? $defaultFromName) : $defaultFromName;
|
||||
$replyTo = $defaultFromEmail;
|
||||
$replyToName = $defaultFromName;
|
||||
|
||||
$customMailOptions = $payload['customMailOptions'] ?? [];
|
||||
if (!\is_array($customMailOptions)) {
|
||||
$customMailOptions = [];
|
||||
}
|
||||
|
||||
if (!empty($customMailOptions['senderEmail'])) {
|
||||
$fromEmail = (string) $customMailOptions['senderEmail'];
|
||||
}
|
||||
if (!empty($customMailOptions['senderName'])) {
|
||||
$fromName = (string) $customMailOptions['senderName'];
|
||||
}
|
||||
|
||||
if (!empty($customMailOptions['replyToEmail']) || !empty($customMailOptions['replyToName'])) {
|
||||
$replyTo = (string) ($customMailOptions['replyToEmail'] ?? $replyTo);
|
||||
$replyToName = (string) ($customMailOptions['replyToName'] ?? $replyToName);
|
||||
} elseif (!empty($smtp)) {
|
||||
$smtpReplyToEmail = $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '';
|
||||
$replyTo = !empty($smtpReplyToEmail) ? $smtpReplyToEmail : ($smtp['senderEmail'] ?? $replyTo);
|
||||
$replyToName = !empty($smtp['replyToName']) ? $smtp['replyToName'] : ($smtp['senderName'] ?? $replyToName);
|
||||
}
|
||||
|
||||
$attachments = null;
|
||||
if (!empty($attachment['content'] ?? '')) {
|
||||
$attachments = [
|
||||
new Attachment(
|
||||
name: $attachment['filename'] ?? 'unknown.file',
|
||||
path: '',
|
||||
type: $attachment['type'] ?? 'plain/text',
|
||||
content: \base64_decode($attachment['content']),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
$emailMessage = new EmailMessage(
|
||||
to: [['email' => $recipient, 'name' => $name]],
|
||||
subject: $subject,
|
||||
content: $body,
|
||||
fromName: $fromName,
|
||||
fromEmail: $fromEmail,
|
||||
replyToName: $replyToName,
|
||||
replyToEmail: $replyTo,
|
||||
attachments: $attachments,
|
||||
html: true,
|
||||
);
|
||||
|
||||
try {
|
||||
$adapter->send($emailMessage);
|
||||
} catch (Throwable $error) {
|
||||
if ($type === 'smtp') {
|
||||
throw new Exception('Error sending mail: ' . $error->getMessage(), 401);
|
||||
}
|
||||
throw new Exception('Error sending mail: ' . $error->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Platform\Workers;
|
||||
|
||||
use Appwrite\Platform\Workers\Mails;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Messaging\Adapter\Email as EmailAdapter;
|
||||
use Utopia\Messaging\Messages\Email as EmailMessage;
|
||||
use Utopia\Queue\Message;
|
||||
use Utopia\Registry\Registry;
|
||||
|
||||
class SpyMailAdapter extends EmailAdapter
|
||||
{
|
||||
public ?EmailMessage $captured = null;
|
||||
public int $sendCount = 0;
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'SpySMTP';
|
||||
}
|
||||
|
||||
public function getMaxMessagesPerRequest(): int
|
||||
{
|
||||
return 1000;
|
||||
}
|
||||
|
||||
protected function process(EmailMessage $message): array
|
||||
{
|
||||
$this->sendCount++;
|
||||
$this->captured = $message;
|
||||
|
||||
return [
|
||||
'deliveredTo' => 1,
|
||||
'type' => $this->getType(),
|
||||
'results' => [['recipient' => $message->getTo()[0]['email'] ?? '', 'status' => 'sent']],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class MailsTest extends TestCase
|
||||
{
|
||||
public function testLegacyMailPayloadIsSentByMailsWorker(): void
|
||||
{
|
||||
$adapter = new SpyMailAdapter();
|
||||
$registry = new Registry();
|
||||
$registry->set('smtp', static fn () => $adapter);
|
||||
|
||||
$previousSmtpHost = \getenv('_APP_SMTP_HOST');
|
||||
\putenv('_APP_SMTP_HOST=spy.smtp.test');
|
||||
|
||||
try {
|
||||
$worker = new Mails();
|
||||
$worker->action(
|
||||
new Message([
|
||||
'pid' => 'pid',
|
||||
'queue' => 'v1-mails',
|
||||
'timestamp' => \time(),
|
||||
'payload' => [
|
||||
'recipient' => 'legacy@example.test',
|
||||
'name' => 'Legacy User',
|
||||
'subject' => 'Hello {{name}}',
|
||||
'body' => 'Body {{name}}',
|
||||
'variables' => ['name' => 'Legacy'],
|
||||
'customMailOptions' => [
|
||||
'senderEmail' => 'sender@example.test',
|
||||
'senderName' => 'Custom Sender',
|
||||
'replyToEmail' => 'reply@example.test',
|
||||
'replyToName' => 'Custom Reply',
|
||||
],
|
||||
],
|
||||
]),
|
||||
new Document(['$id' => 'project-x']),
|
||||
$registry,
|
||||
new Log(),
|
||||
);
|
||||
} finally {
|
||||
\putenv($previousSmtpHost === false ? '_APP_SMTP_HOST' : '_APP_SMTP_HOST=' . $previousSmtpHost);
|
||||
}
|
||||
|
||||
$this->assertSame(1, $adapter->sendCount);
|
||||
$this->assertNotNull($adapter->captured);
|
||||
|
||||
$message = $adapter->captured;
|
||||
$this->assertSame('legacy@example.test', $message->getTo()[0]['email'] ?? '');
|
||||
$this->assertSame('Legacy User', $message->getTo()[0]['name'] ?? '');
|
||||
$this->assertSame('Hello Legacy', $message->getSubject());
|
||||
$this->assertSame('sender@example.test', $message->getFromEmail());
|
||||
$this->assertSame('Custom Sender', $message->getFromName());
|
||||
$this->assertSame('reply@example.test', $message->getReplyToEmail());
|
||||
$this->assertSame('Custom Reply', $message->getReplyToName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Platform\Workers;
|
||||
|
||||
use Appwrite\Platform\Services\Workers;
|
||||
use Appwrite\Platform\Workers\Mails;
|
||||
use Appwrite\Platform\Workers\Notifications;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class RegistrationTest extends TestCase
|
||||
{
|
||||
public function testMailsAndNotificationsWorkersAreRegisteredSeparately(): void
|
||||
{
|
||||
$service = new Workers();
|
||||
|
||||
$this->assertInstanceOf(Mails::class, $service->getAction('mails'));
|
||||
$this->assertInstanceOf(Notifications::class, $service->getAction('notifications'));
|
||||
}
|
||||
|
||||
public function testEntrypointDoesNotAliasMailsToNotifications(): void
|
||||
{
|
||||
$contents = \file_get_contents(__DIR__ . '/../../../../app/worker.php');
|
||||
|
||||
$this->assertIsString($contents);
|
||||
$this->assertStringNotContainsString("mails' ? 'notifications'", $contents);
|
||||
$this->assertStringContainsString('\'workerName\' => strtolower($workerName)', $contents);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user