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; - } }