From b9aaecba254bb75b4777cdc62f81602433dd85a3 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 1 Apr 2026 16:25:33 +0530 Subject: [PATCH] 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; - } }