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.
This commit is contained in:
Chirag Aggarwal
2026-04-01 16:25:33 +05:30
parent a5b0378138
commit b9aaecba25
5 changed files with 103 additions and 123 deletions
+15 -29
View File
@@ -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');
+1 -1
View File
@@ -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.*",
Generated
+10 -8
View File
@@ -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": {
+12 -8
View File
@@ -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');
+65 -77
View File
@@ -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 <p> 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\b[^>]*>(.*?)<\/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;
}
}