From b9aaecba254bb75b4777cdc62f81602433dd85a3 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 16:25:33 +0530 Subject: [PATCH 01/15] Replace PHPMailer with utopia-php/messaging SMTP adapter Use Utopia\Messaging\Adapter\Email\SMTP instead of raw PHPMailer for the smtp register, Mails worker, and Doctor task. This enables swapping email adapters (e.g. Resend) via DI override in downstream repos by type-hinting against the EmailAdapter base class. --- app/init/registers.php | 44 +++----- composer.json | 2 +- composer.lock | 18 +-- src/Appwrite/Platform/Tasks/Doctor.php | 20 ++-- src/Appwrite/Platform/Workers/Mails.php | 142 +++++++++++------------- 5 files changed, 103 insertions(+), 123 deletions(-) diff --git a/app/init/registers.php b/app/init/registers.php index 7c2f822fdd..4fac60b877 100644 --- a/app/init/registers.php +++ b/app/init/registers.php @@ -6,7 +6,6 @@ use Appwrite\Hooks\Hooks; use Appwrite\PubSub\Adapter\Redis as PubSub; use Appwrite\URL\URL as AppwriteURL; use MaxMind\Db\Reader; -use PHPMailer\PHPMailer\PHPMailer; use Swoole\Database\PDOProxy; use Utopia\Cache\Adapter\Redis as RedisCache; use Utopia\Config\Config; @@ -25,6 +24,7 @@ use Utopia\Logger\Adapter\LogOwl; use Utopia\Logger\Adapter\Raygun; use Utopia\Logger\Adapter\Sentry; use Utopia\Logger\Logger; +use Utopia\Messaging\Adapter\Email\SMTP; use Utopia\Mongo\Client as MongoClient; use Utopia\Pools\Adapter\Stack as StackPool; use Utopia\Pools\Adapter\Swoole as SwoolePool; @@ -433,35 +433,21 @@ $register->set('db', function () { }); $register->set('smtp', function () { - $mail = new PHPMailer(true); + $username = System::getEnv('_APP_SMTP_USERNAME', ''); + $password = System::getEnv('_APP_SMTP_PASSWORD', ''); - $mail->isSMTP(); - - $username = System::getEnv('_APP_SMTP_USERNAME'); - $password = System::getEnv('_APP_SMTP_PASSWORD'); - - $mail->XMailer = 'Appwrite Mailer'; - $mail->Host = System::getEnv('_APP_SMTP_HOST', 'smtp'); - $mail->Port = System::getEnv('_APP_SMTP_PORT', 25); - $mail->SMTPAuth = !empty($username) && !empty($password); - $mail->Username = $username; - $mail->Password = $password; - $mail->SMTPSecure = System::getEnv('_APP_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.) */ - - $from = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server')); - $email = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM); - - $mail->setFrom($email, $from); - $mail->addReplyTo($email, $from); - - $mail->isHTML(true); - - return $mail; + return new SMTP( + host: System::getEnv('_APP_SMTP_HOST', 'smtp'), + port: (int) System::getEnv('_APP_SMTP_PORT', 25), + username: $username, + password: $password, + smtpSecure: System::getEnv('_APP_SMTP_SECURE', ''), + smtpAutoTLS: false, + xMailer: 'Appwrite Mailer', + timeout: 10, + keepAlive: true, + timelimit: 30, + ); }); $register->set('geodb', function () { return new Reader(__DIR__ . '/../assets/dbip/dbip-country-lite-2025-12.mmdb'); diff --git a/composer.json b/composer.json index 1a530bfc5b..3d7e1b0e55 100644 --- a/composer.json +++ b/composer.json @@ -72,7 +72,7 @@ "utopia-php/image": "0.8.*", "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", - "utopia-php/messaging": "0.20.*", + "utopia-php/messaging": "dev-feat-smtp-enhancements", "utopia-php/migration": "1.9.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "1.*", diff --git a/composer.lock b/composer.lock index 0a79c5bee3..5d92374109 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b5261855586680e467168f527e0634ae", + "content-hash": "505d09ff5da7c5199622679ae9e11bc6", "packages": [ { "name": "adhocore/jwt", @@ -4467,16 +4467,16 @@ }, { "name": "utopia-php/messaging", - "version": "0.20.1", + "version": "dev-feat-smtp-enhancements", "source": { "type": "git", "url": "https://github.com/utopia-php/messaging.git", - "reference": "fcb4c3c46a48008a677957690bd45ec934dd33b0" + "reference": "6988c227c08095ad88aad030bf3648f4ea6c88f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/messaging/zipball/fcb4c3c46a48008a677957690bd45ec934dd33b0", - "reference": "fcb4c3c46a48008a677957690bd45ec934dd33b0", + "url": "https://api.github.com/repos/utopia-php/messaging/zipball/6988c227c08095ad88aad030bf3648f4ea6c88f0", + "reference": "6988c227c08095ad88aad030bf3648f4ea6c88f0", "shasum": "" }, "require": { @@ -4512,9 +4512,9 @@ ], "support": { "issues": "https://github.com/utopia-php/messaging/issues", - "source": "https://github.com/utopia-php/messaging/tree/0.20.1" + "source": "https://github.com/utopia-php/messaging/tree/feat-smtp-enhancements" }, - "time": "2026-02-06T09:56:06+00:00" + "time": "2026-04-01T10:41:57+00:00" }, { "name": "utopia-php/migration", @@ -8435,7 +8435,9 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": {}, + "stability-flags": { + "utopia-php/messaging": 20 + }, "prefer-stable": true, "prefer-lowest": false, "platform": { diff --git a/src/Appwrite/Platform/Tasks/Doctor.php b/src/Appwrite/Platform/Tasks/Doctor.php index 9a9c2fdf73..aa916a559c 100644 --- a/src/Appwrite/Platform/Tasks/Doctor.php +++ b/src/Appwrite/Platform/Tasks/Doctor.php @@ -4,7 +4,6 @@ namespace Appwrite\Platform\Tasks; use Appwrite\ClamAV\Network; use Appwrite\PubSub\Adapter\Pool as PubSubPool; -use PHPMailer\PHPMailer\PHPMailer; use Utopia\Cache\Adapter\Pool as CachePool; use Utopia\Config\Config; use Utopia\Console; @@ -13,6 +12,8 @@ use Utopia\Domains\Domain; use Utopia\DSN\DSN; use Utopia\Http\Http; use Utopia\Logger\Logger; +use Utopia\Messaging\Adapter\Email as EmailAdapter; +use Utopia\Messaging\Messages\Email as EmailMessage; use Utopia\Platform\Action; use Utopia\Pools\Group; use Utopia\Queue\Broker\Pool as BrokerPool; @@ -212,15 +213,18 @@ class Doctor extends Action } try { - /* @var PHPMailer $mail */ - $mail = $register->get('smtp'); + /** @var EmailAdapter $smtp */ + $smtp = $register->get('smtp'); - $mail->addAddress('demo@example.com', 'Example.com'); - $mail->Subject = 'Test SMTP Connection'; - $mail->Body = 'Hello World'; - $mail->AltBody = 'Hello World'; + $emailMessage = new EmailMessage( + to: ['demo@example.com'], + subject: 'Test SMTP Connection', + content: 'Hello World', + fromName: \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server')), + fromEmail: System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM), + ); - $mail->send(); + $smtp->send($emailMessage); Console::success('🟢 ' . str_pad("SMTP", 50, '.') . 'connected'); } catch (\Throwable) { Console::error('🔴 ' . str_pad("SMTP", 47, '.') . 'disconnected'); diff --git a/src/Appwrite/Platform/Workers/Mails.php b/src/Appwrite/Platform/Workers/Mails.php index f144c58e1b..83ddb082ab 100644 --- a/src/Appwrite/Platform/Workers/Mails.php +++ b/src/Appwrite/Platform/Workers/Mails.php @@ -4,10 +4,13 @@ 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\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; @@ -49,9 +52,9 @@ class Mails extends Action /** * @param Message $message + * @param Document $project * @param Registry $register * @param Log $log - * @throws \PHPMailer\PHPMailer\Exception * @return void * @throws Exception */ @@ -132,58 +135,77 @@ class Mails extends Action // render() will return the subject in

tags, so use strip_tags() to remove them $subject = \strip_tags($subjectTemplate->render()); - /** @var PHPMailer $mail */ - $mail = empty($smtp) + /** @var EmailAdapter $adapter */ + $adapter = empty($smtp) ? $register->get('smtp') - : $this->getMailer($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, + ); - $mail->clearAddresses(); - $mail->clearAllRecipients(); - $mail->clearReplyTos(); - $mail->clearAttachments(); - $mail->clearBCCs(); - $mail->clearCCs(); - $mail->addAddress($recipient, $name); - $mail->Subject = $subject; - $mail->Body = $body; + // Resolve from/replyTo using fallback hierarchy: Custom options > SMTP config > Defaults + $defaultFromEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM); + $defaultFromName = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server')); - $mail->AltBody = $body; - $mail->AltBody = preg_replace('/]*>(.*?)<\/style>/is', '', $mail->AltBody); - $mail->AltBody = \strip_tags($mail->AltBody); - $mail->AltBody = \trim($mail->AltBody); + $fromEmail = !empty($smtp) ? ($smtp['senderEmail'] ?? $defaultFromEmail) : $defaultFromEmail; + $fromName = !empty($smtp) ? ($smtp['senderName'] ?? $defaultFromName) : $defaultFromName; + $replyTo = $defaultFromEmail; + $replyToName = $defaultFromName; - $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)) { + if (!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' - ); + $customMailOptions = $payload['customMailOptions'] ?? []; + + if (!empty($customMailOptions['senderEmail'])) { + $fromEmail = $customMailOptions['senderEmail']; + } + if (!empty($customMailOptions['senderName'])) { + $fromName = $customMailOptions['senderName']; + } + if (!empty($customMailOptions['replyToEmail'])) { + $replyTo = $customMailOptions['replyToEmail']; + } + if (!empty($customMailOptions['replyToName'])) { + $replyToName = $customMailOptions['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: [$recipient], + subject: $subject, + content: $body, + fromName: $fromName, + fromEmail: $fromEmail, + replyToName: $replyToName, + replyToEmail: $replyTo, + attachments: $attachments, + html: true, + ); + try { - $mail->send(); + $adapter->send($emailMessage); } catch (\Throwable $error) { if ($type === 'smtp') { throw new Exception('Error sending mail: ' . $error->getMessage(), 401); @@ -191,38 +213,4 @@ class Mails extends Action 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; - } } From ce63b00cf445c2ca1866d83211d2b86121418f87 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 16:29:06 +0530 Subject: [PATCH 02/15] lock file --- composer.json | 2 +- composer.lock | 18 ++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/composer.json b/composer.json index 3d7e1b0e55..02b5730c91 100644 --- a/composer.json +++ b/composer.json @@ -72,7 +72,7 @@ "utopia-php/image": "0.8.*", "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", - "utopia-php/messaging": "dev-feat-smtp-enhancements", + "utopia-php/messaging": "0.21.*", "utopia-php/migration": "1.9.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "1.*", diff --git a/composer.lock b/composer.lock index 5d92374109..c0b2b9aa30 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "505d09ff5da7c5199622679ae9e11bc6", + "content-hash": "012f943cdfac560cb95ce5703d7588e1", "packages": [ { "name": "adhocore/jwt", @@ -4467,16 +4467,16 @@ }, { "name": "utopia-php/messaging", - "version": "dev-feat-smtp-enhancements", + "version": "0.21.0", "source": { "type": "git", "url": "https://github.com/utopia-php/messaging.git", - "reference": "6988c227c08095ad88aad030bf3648f4ea6c88f0" + "reference": "b2358fde423e27ff0cfe7846eb9a78b5b295552a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/messaging/zipball/6988c227c08095ad88aad030bf3648f4ea6c88f0", - "reference": "6988c227c08095ad88aad030bf3648f4ea6c88f0", + "url": "https://api.github.com/repos/utopia-php/messaging/zipball/b2358fde423e27ff0cfe7846eb9a78b5b295552a", + "reference": "b2358fde423e27ff0cfe7846eb9a78b5b295552a", "shasum": "" }, "require": { @@ -4512,9 +4512,9 @@ ], "support": { "issues": "https://github.com/utopia-php/messaging/issues", - "source": "https://github.com/utopia-php/messaging/tree/feat-smtp-enhancements" + "source": "https://github.com/utopia-php/messaging/tree/0.21.0" }, - "time": "2026-04-01T10:41:57+00:00" + "time": "2026-04-01T10:58:13+00:00" }, { "name": "utopia-php/migration", @@ -8435,9 +8435,7 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": { - "utopia-php/messaging": 20 - }, + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { From f1dc468e50d99d4c2ef3f4d90f04b693031f36cb Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 16:37:08 +0530 Subject: [PATCH 03/15] Restore replyTo elseif logic to preserve old behavior customMailOptions replyTo and smtp replyTo are mutually exclusive, matching the original PHPMailer implementation. --- src/Appwrite/Platform/Workers/Mails.php | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Mails.php b/src/Appwrite/Platform/Workers/Mails.php index 83ddb082ab..01fe4ced9e 100644 --- a/src/Appwrite/Platform/Workers/Mails.php +++ b/src/Appwrite/Platform/Workers/Mails.php @@ -160,11 +160,6 @@ class Mails extends Action $replyTo = $defaultFromEmail; $replyToName = $defaultFromName; - if (!empty($smtp)) { - $replyTo = !empty($smtp['replyTo']) ? $smtp['replyTo'] : ($smtp['senderEmail'] ?? $replyTo); - $replyToName = $smtp['senderName'] ?? $replyToName; - } - $customMailOptions = $payload['customMailOptions'] ?? []; if (!empty($customMailOptions['senderEmail'])) { @@ -173,11 +168,13 @@ class Mails extends Action if (!empty($customMailOptions['senderName'])) { $fromName = $customMailOptions['senderName']; } - if (!empty($customMailOptions['replyToEmail'])) { - $replyTo = $customMailOptions['replyToEmail']; - } - if (!empty($customMailOptions['replyToName'])) { - $replyToName = $customMailOptions['replyToName']; + + 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; } $attachments = null; From 53bc12601512b3bc797a8b6528edff79ebf0f296 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 17:01:15 +0530 Subject: [PATCH 04/15] Remove stale PHPMailer baseline entry from PHPStan The PHPMailer $Port type error no longer occurs since registers.php now uses the SMTP adapter instead of PHPMailer directly. --- phpstan-baseline.neon | 6 ------ 1 file changed, 6 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8e9d8a5a38..b0fbbe25ae 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -138,12 +138,6 @@ parameters: count: 1 path: app/init/registers.php - - - message: '#^Property PHPMailer\\PHPMailer\\PHPMailer\:\:\$Port \(int\) does not accept string\.$#' - identifier: assign.propertyType - count: 1 - path: app/init/registers.php - - message: '#^Variable \$providerConfig in empty\(\) always exists and is not falsy\.$#' identifier: empty.variable From 7044601603562dc7a61eede84436638111e06b51 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 17:05:14 +0530 Subject: [PATCH 05/15] lock file --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index c0b2b9aa30..fcb69195c5 100644 --- a/composer.lock +++ b/composer.lock @@ -4467,16 +4467,16 @@ }, { "name": "utopia-php/messaging", - "version": "0.21.0", + "version": "0.21.1", "source": { "type": "git", "url": "https://github.com/utopia-php/messaging.git", - "reference": "b2358fde423e27ff0cfe7846eb9a78b5b295552a" + "reference": "de068654476a673921ea9df03692d2940b7941ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/messaging/zipball/b2358fde423e27ff0cfe7846eb9a78b5b295552a", - "reference": "b2358fde423e27ff0cfe7846eb9a78b5b295552a", + "url": "https://api.github.com/repos/utopia-php/messaging/zipball/de068654476a673921ea9df03692d2940b7941ee", + "reference": "de068654476a673921ea9df03692d2940b7941ee", "shasum": "" }, "require": { @@ -4512,9 +4512,9 @@ ], "support": { "issues": "https://github.com/utopia-php/messaging/issues", - "source": "https://github.com/utopia-php/messaging/tree/0.21.0" + "source": "https://github.com/utopia-php/messaging/tree/0.21.1" }, - "time": "2026-04-01T10:58:13+00:00" + "time": "2026-04-01T11:34:20+00:00" }, { "name": "utopia-php/migration", From d531c29fc8c1d9ecbf8ce04f57be6faecdcfa539 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 17:20:34 +0530 Subject: [PATCH 06/15] Remove final from Usage class to allow cloud override --- src/Appwrite/Event/Message/Usage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Event/Message/Usage.php b/src/Appwrite/Event/Message/Usage.php index ec8484e45e..776188d5b5 100644 --- a/src/Appwrite/Event/Message/Usage.php +++ b/src/Appwrite/Event/Message/Usage.php @@ -4,7 +4,7 @@ namespace Appwrite\Event\Message; use Utopia\Database\Document; -final class Usage extends Base +class Usage extends Base { /** * @param Document $project From fb96aecbefa147a2b5468381c0af5bea496f1e39 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 19:52:16 +0530 Subject: [PATCH 07/15] Use new static() in Usage::fromArray() for late static binding Required since the class is no longer final and the return type is static, so subclasses get the correct type. --- src/Appwrite/Event/Message/Usage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Event/Message/Usage.php b/src/Appwrite/Event/Message/Usage.php index 776188d5b5..c97b96a855 100644 --- a/src/Appwrite/Event/Message/Usage.php +++ b/src/Appwrite/Event/Message/Usage.php @@ -40,7 +40,7 @@ class Usage extends Base */ public static function fromArray(array $data): static { - return new self( + return new static( project: new Document($data['project'] ?? []), metrics: $data['metrics'] ?? [], reduce: array_map(fn (array $doc) => new Document($doc), $data['reduce'] ?? []), From 7d428ffe83c1083fffc7479c28d657d33150fe92 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 21:03:07 +0530 Subject: [PATCH 08/15] trigger ci From 92cc382a6bf56e11467b75c7b9f1154e07b64dbd Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 21:25:22 +0530 Subject: [PATCH 09/15] Change Usage::fromArray() return type from static to self The cloud subclass has a different constructor signature so new static() is unsafe. Use self since subclasses that need deserialization should override fromArray() themselves. --- src/Appwrite/Event/Message/Usage.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Event/Message/Usage.php b/src/Appwrite/Event/Message/Usage.php index c97b96a855..22330ddb52 100644 --- a/src/Appwrite/Event/Message/Usage.php +++ b/src/Appwrite/Event/Message/Usage.php @@ -36,11 +36,11 @@ class Usage extends Base /** * @param array $data - * @return static + * @return self */ - public static function fromArray(array $data): static + public static function fromArray(array $data): self { - return new static( + return new self( project: new Document($data['project'] ?? []), metrics: $data['metrics'] ?? [], reduce: array_map(fn (array $doc) => new Document($doc), $data['reduce'] ?? []), From 30e0ca81bd366964f9f3887a4a8b22747c1547ea Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 21:30:36 +0530 Subject: [PATCH 10/15] Fix Usage::fromArray() to use static return type with new static() Keep covariant return type with parent Base::fromArray(). The new static() is safe here because the cloud subclass constructor is backwards-compatible via optional params. --- src/Appwrite/Event/Message/Usage.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Event/Message/Usage.php b/src/Appwrite/Event/Message/Usage.php index 22330ddb52..c72bc8ae2a 100644 --- a/src/Appwrite/Event/Message/Usage.php +++ b/src/Appwrite/Event/Message/Usage.php @@ -36,11 +36,12 @@ class Usage extends Base /** * @param array $data - * @return self + * @return static */ - public static function fromArray(array $data): self + public static function fromArray(array $data): static { - return new self( + /** @phpstan-ignore new.static (subclass constructors are backwards-compatible via optional params) */ + return new static( project: new Document($data['project'] ?? []), metrics: $data['metrics'] ?? [], reduce: array_map(fn (array $doc) => new Document($doc), $data['reduce'] ?? []), From 2c1060f57aedd21a5114b4d151d0a445d9445dfd Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 21:31:08 +0530 Subject: [PATCH 11/15] Fix null array key in HeadersTest Cast null to string to avoid invalid array key type error while preserving the test's intent of validating empty keys. --- tests/unit/Functions/Validator/HeadersTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/Functions/Validator/HeadersTest.php b/tests/unit/Functions/Validator/HeadersTest.php index 4a45f57427..b9cc7c6ea4 100644 --- a/tests/unit/Functions/Validator/HeadersTest.php +++ b/tests/unit/Functions/Validator/HeadersTest.php @@ -77,7 +77,7 @@ class HeadersTest extends TestCase $this->assertFalse($this->object->isValid($headers)); $headers = [ - null => 'value', + (string) null => 'value', ]; $this->assertFalse($this->object->isValid($headers)); From 04943b6313fbc0cd0b551a4746c2a67380254b7e Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 09:29:57 +0530 Subject: [PATCH 12/15] Pass recipient display name in EmailMessage to field Use associative array format ['email' => ..., 'name' => ...] for the to field so the recipient display name appears in the To header. --- src/Appwrite/Platform/Workers/Mails.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Mails.php b/src/Appwrite/Platform/Workers/Mails.php index 01fe4ced9e..32de1e50d6 100644 --- a/src/Appwrite/Platform/Workers/Mails.php +++ b/src/Appwrite/Platform/Workers/Mails.php @@ -190,7 +190,7 @@ class Mails extends Action } $emailMessage = new EmailMessage( - to: [$recipient], + to: [['email' => $recipient, 'name' => $name]], subject: $subject, content: $body, fromName: $fromName, From 2dc20ef0eb67c849db34ec4c4a447c69b25f8fe8 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 09:31:48 +0530 Subject: [PATCH 13/15] Update utopia-php/messaging lock to latest --- composer.json | 2 +- composer.lock | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 02b5730c91..33b9f79de6 100644 --- a/composer.json +++ b/composer.json @@ -72,7 +72,7 @@ "utopia-php/image": "0.8.*", "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", - "utopia-php/messaging": "0.21.*", + "utopia-php/messaging": "dev-feat-recipient-value-object", "utopia-php/migration": "1.9.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "1.*", diff --git a/composer.lock b/composer.lock index fcb69195c5..64a22f38a0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "012f943cdfac560cb95ce5703d7588e1", + "content-hash": "8d41008e18681373142296c17131c860", "packages": [ { "name": "adhocore/jwt", @@ -4467,16 +4467,16 @@ }, { "name": "utopia-php/messaging", - "version": "0.21.1", + "version": "dev-feat-recipient-value-object", "source": { "type": "git", "url": "https://github.com/utopia-php/messaging.git", - "reference": "de068654476a673921ea9df03692d2940b7941ee" + "reference": "92aaa2772cf4a67b64590a10634f24126cb7f608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/messaging/zipball/de068654476a673921ea9df03692d2940b7941ee", - "reference": "de068654476a673921ea9df03692d2940b7941ee", + "url": "https://api.github.com/repos/utopia-php/messaging/zipball/92aaa2772cf4a67b64590a10634f24126cb7f608", + "reference": "92aaa2772cf4a67b64590a10634f24126cb7f608", "shasum": "" }, "require": { @@ -4512,9 +4512,9 @@ ], "support": { "issues": "https://github.com/utopia-php/messaging/issues", - "source": "https://github.com/utopia-php/messaging/tree/0.21.1" + "source": "https://github.com/utopia-php/messaging/tree/feat-recipient-value-object" }, - "time": "2026-04-01T11:34:20+00:00" + "time": "2026-04-02T03:35:05+00:00" }, { "name": "utopia-php/migration", @@ -8435,7 +8435,9 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": {}, + "stability-flags": { + "utopia-php/messaging": 20 + }, "prefer-stable": true, "prefer-lowest": false, "platform": { From c2dd8acedae746921721d122616f4d578edd55dd Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 09:42:59 +0530 Subject: [PATCH 14/15] use stable --- composer.json | 2 +- composer.lock | 20 +++++++++----------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/composer.json b/composer.json index 33b9f79de6..0f4dcfb8db 100644 --- a/composer.json +++ b/composer.json @@ -72,7 +72,7 @@ "utopia-php/image": "0.8.*", "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", - "utopia-php/messaging": "dev-feat-recipient-value-object", + "utopia-php/messaging": "0.22.*", "utopia-php/migration": "1.9.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "1.*", diff --git a/composer.lock b/composer.lock index 64a22f38a0..420dddc9a5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8d41008e18681373142296c17131c860", + "content-hash": "4fe91e67f343fbe6deac1fdc7eda949f", "packages": [ { "name": "adhocore/jwt", @@ -4467,23 +4467,23 @@ }, { "name": "utopia-php/messaging", - "version": "dev-feat-recipient-value-object", + "version": "0.22.0", "source": { "type": "git", "url": "https://github.com/utopia-php/messaging.git", - "reference": "92aaa2772cf4a67b64590a10634f24126cb7f608" + "reference": "a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/messaging/zipball/92aaa2772cf4a67b64590a10634f24126cb7f608", - "reference": "92aaa2772cf4a67b64590a10634f24126cb7f608", + "url": "https://api.github.com/repos/utopia-php/messaging/zipball/a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030", + "reference": "a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030", "shasum": "" }, "require": { "ext-curl": "*", "ext-openssl": "*", "giggsey/libphonenumber-for-php-lite": "9.0.23", - "php": ">=8.0.0", + "php": ">=8.1.0", "phpmailer/phpmailer": "6.9.1" }, "require-dev": { @@ -4512,9 +4512,9 @@ ], "support": { "issues": "https://github.com/utopia-php/messaging/issues", - "source": "https://github.com/utopia-php/messaging/tree/feat-recipient-value-object" + "source": "https://github.com/utopia-php/messaging/tree/0.22.0" }, - "time": "2026-04-02T03:35:05+00:00" + "time": "2026-04-02T04:09:19+00:00" }, { "name": "utopia-php/migration", @@ -8435,9 +8435,7 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": { - "utopia-php/messaging": 20 - }, + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { From b6e95f4502d3cec63def0f90dc1fcf4d8a63673d Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 11:05:03 +0530 Subject: [PATCH 15/15] test: remove redundant headers empty-key case --- tests/unit/Functions/Validator/HeadersTest.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/unit/Functions/Validator/HeadersTest.php b/tests/unit/Functions/Validator/HeadersTest.php index b9cc7c6ea4..563131d480 100644 --- a/tests/unit/Functions/Validator/HeadersTest.php +++ b/tests/unit/Functions/Validator/HeadersTest.php @@ -76,11 +76,6 @@ class HeadersTest extends TestCase ]; $this->assertFalse($this->object->isValid($headers)); - $headers = [ - (string) null => 'value', - ]; - $this->assertFalse($this->object->isValid($headers)); - $headers = [ 'X-Header' => null, ];