From dd29967e99383bc50f37d7bc458456960d105486 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:15:51 +0100 Subject: [PATCH] refactor: tighten Mails listener with guard clauses and lean event - SessionCreated event now carries only domain data (no isFirstSession) - Mails listener uses ordered guard clauses, deferring the DB query until cheaper checks pass - Drop $user Document allocation in favour of direct array access - Inline FileName validator and $smtpEnabled into their use sites - Extract $isBranded to eliminate duplicate APP_BRANDED_EMAIL_BASE_TEMPLATE check Co-Authored-By: Claude Sonnet 4.6 --- app/controllers/api/account.php | 14 +- src/Appwrite/Bus/Events/SessionCreated.php | 1 - src/Appwrite/Bus/Listeners/Mails.php | 177 +++++++++------------ src/Appwrite/Event/Mail.php | 1 - 4 files changed, 76 insertions(+), 117 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 38cf5499e1..0035778523 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -9,8 +9,8 @@ use Appwrite\Auth\Validator\PasswordDictionary; use Appwrite\Auth\Validator\PasswordHistory; use Appwrite\Auth\Validator\PersonalData; use Appwrite\Auth\Validator\Phone; -use Appwrite\Detector\Detector; use Appwrite\Bus\Events\SessionCreated; +use Appwrite\Detector\Detector; use Appwrite\Event\Delete; use Appwrite\Event\Event; use Appwrite\Event\Mail; @@ -42,6 +42,7 @@ use Utopia\Auth\Proofs\Code as ProofsCode; use Utopia\Auth\Proofs\Password as ProofsPassword; use Utopia\Auth\Proofs\Token as ProofsToken; use Utopia\Auth\Store; +use Utopia\Bus\Bus; use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\DateTime; @@ -61,7 +62,6 @@ use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\UID; use Utopia\Emails\Email; use Utopia\Emails\Validator\Email as EmailValidator; -use Utopia\Bus\Bus; use Utopia\Http\Http; use Utopia\Locale\Locale; use Utopia\Storage\Validator\FileName; @@ -189,16 +189,11 @@ $createSession = function (string $userId, string $secret, Request $request, Res throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed saving user to DB'); } - $isFirstSession = $dbForProject->count('sessions', [ - Query::equal('userId', [$user->getId()]), - ]) === 1; - $bus->dispatch(new SessionCreated( user: $user->getArrayCopy(), project: $project->getArrayCopy(), session: $session->getArrayCopy(), locale: $locale->default, - isFirstSession: $isFirstSession, )); $queueForEvents @@ -1006,16 +1001,11 @@ Http::post('/v1/account/sessions/email') ->setParam('sessionId', $session->getId()) ; - $isFirstSession = $dbForProject->count('sessions', [ - Query::equal('userId', [$user->getId()]), - ]) === 1; - $bus->dispatch(new SessionCreated( user: $user->getArrayCopy(), project: $project->getArrayCopy(), session: $session->getArrayCopy(), locale: $locale->default, - isFirstSession: $isFirstSession, )); $response->dynamic($session, Response::MODEL_SESSION); diff --git a/src/Appwrite/Bus/Events/SessionCreated.php b/src/Appwrite/Bus/Events/SessionCreated.php index 3eb7b4b076..b662ce2ad5 100644 --- a/src/Appwrite/Bus/Events/SessionCreated.php +++ b/src/Appwrite/Bus/Events/SessionCreated.php @@ -16,7 +16,6 @@ class SessionCreated implements Event public readonly array $project, public readonly array $session, public readonly string $locale, - public readonly bool $isFirstSession, ) { } } diff --git a/src/Appwrite/Bus/Listeners/Mails.php b/src/Appwrite/Bus/Listeners/Mails.php index 90805a7514..c576fb1552 100644 --- a/src/Appwrite/Bus/Listeners/Mails.php +++ b/src/Appwrite/Bus/Listeners/Mails.php @@ -7,7 +7,9 @@ use Appwrite\Bus\Events\SessionCreated; use Appwrite\Event\Mail; use Appwrite\Template\Template; use Utopia\Bus\Listener; +use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Query; use Utopia\Locale\Locale; use Utopia\Queue\Publisher; use Utopia\Storage\Validator\FileName; @@ -32,148 +34,117 @@ class Mails extends Listener ->inject('publisher') ->inject('locale') ->inject('platform') + ->inject('dbForProject') ->callback($this->handle(...)); } - public function handle(SessionCreated $event, Publisher $publisher, Locale $locale, array $platform): void + public function handle(SessionCreated $event, Publisher $publisher, Locale $locale, array $platform, Database $dbForProject): void { + $project = new Document($event->project); + + if (!($project->getAttribute('auths', [])['sessionAlerts'] ?? false)) { + return; + } + + if (empty($event->user['email'])) { + return; + } + $provider = $event->session['provider'] ?? ''; $factors = $event->session['factors'] ?? []; - $isEmailLinkSession = in_array($provider, [SESSION_PROVIDER_MAGIC_URL, SESSION_PROVIDER_TOKEN]) - && in_array(Type::EMAIL, $factors); - $hasUserEmail = !empty($event->user['email']); - $isSessionAlertsEnabled = $event->project['auths']['sessionAlerts'] ?? false; + if (\in_array($provider, [SESSION_PROVIDER_MAGIC_URL, SESSION_PROVIDER_TOKEN]) && \in_array(Type::EMAIL, $factors)) { + return; + } - if ($isEmailLinkSession || !$hasUserEmail || !$isSessionAlertsEnabled || $event->isFirstSession) { + if ($dbForProject->count('sessions', [Query::equal('userId', [$event->user['$id']])]) === 1) { return; } $locale->setDefault($event->locale); - $user = new Document($event->user); - $project = new Document($event->project); $session = new Document($event->session); - - $subject = $locale->getText("emails.sessionAlert.subject"); - $preview = $locale->getText("emails.sessionAlert.preview"); - $customTemplate = $project->getAttribute('templates', [])['email.sessionAlert-' . $event->locale] ?? []; + $smtp = $project->getAttribute('smtp', []); $smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base'); - $validator = new FileName(); - if (!$validator->isValid($smtpBaseTemplate)) { + if (!(new FileName())->isValid($smtpBaseTemplate)) { throw new \Exception('Invalid template path'); } - $bodyTemplate = __DIR__ . '/../../../../app/config/locale/templates/' . $smtpBaseTemplate . '.tpl'; + $customTemplate = $project->getAttribute('templates', [])["email.sessionAlert-$event->locale"] ?? []; + $isBranded = $smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE; - $message = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-session-alert.tpl'); - $message - ->setParam('{{hello}}', $locale->getText("emails.sessionAlert.hello")) - ->setParam('{{body}}', $locale->getText("emails.sessionAlert.body")) - ->setParam('{{listDevice}}', $locale->getText("emails.sessionAlert.listDevice")) - ->setParam('{{listIpAddress}}', $locale->getText("emails.sessionAlert.listIpAddress")) - ->setParam('{{listCountry}}', $locale->getText("emails.sessionAlert.listCountry")) - ->setParam('{{footer}}', $locale->getText("emails.sessionAlert.footer")) - ->setParam('{{thanks}}', $locale->getText("emails.sessionAlert.thanks")) - ->setParam('{{signature}}', $locale->getText("emails.sessionAlert.signature")); + $subject = $customTemplate['subject'] ?? $locale->getText('emails.sessionAlert.subject'); + $preview = $locale->getText('emails.sessionAlert.preview'); - $body = $message->render(); + $body = empty($customTemplate['message']) + ? Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-session-alert.tpl') + ->setParam('{{hello}}', $locale->getText('emails.sessionAlert.hello')) + ->setParam('{{body}}', $locale->getText('emails.sessionAlert.body')) + ->setParam('{{listDevice}}', $locale->getText('emails.sessionAlert.listDevice')) + ->setParam('{{listIpAddress}}', $locale->getText('emails.sessionAlert.listIpAddress')) + ->setParam('{{listCountry}}', $locale->getText('emails.sessionAlert.listCountry')) + ->setParam('{{footer}}', $locale->getText('emails.sessionAlert.footer')) + ->setParam('{{thanks}}', $locale->getText('emails.sessionAlert.thanks')) + ->setParam('{{signature}}', $locale->getText('emails.sessionAlert.signature')) + ->render() + : $customTemplate['message']; - $smtp = $project->getAttribute('smtp', []); - $smtpEnabled = $smtp['enabled'] ?? false; + $clientName = $session->getAttribute('clientName') + ?: ($session->getAttribute('userAgent') ?: 'UNKNOWN'); - $senderEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM); - $senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'); - $replyTo = ""; + $projectName = $project->getId() === 'console' + ? $platform['platformName'] + : $project->getAttribute('name'); + + $emailVariables = [ + 'direction' => $locale->getText('settings.direction'), + 'date' => (new \DateTime())->format('F j'), + 'year' => (new \DateTime())->format('YYYY'), + 'time' => (new \DateTime())->format('H:i:s'), + 'user' => $event->user['name'] ?? '', + 'project' => $projectName, + 'device' => $clientName, + 'ipAddress' => $session->getAttribute('ip'), + 'country' => $locale->getText('countries.' . $session->getAttribute('countryCode'), $locale->getText('locale.country.unknown')), + ]; + + if ($isBranded) { + $emailVariables += [ + 'accentColor' => $platform['accentColor'], + 'logoUrl' => $platform['logoUrl'], + 'twitter' => $platform['twitterUrl'], + 'discord' => $platform['discordUrl'], + 'github' => $platform['githubUrl'], + 'terms' => $platform['termsUrl'], + 'privacy' => $platform['privacyUrl'], + 'platform' => $platform['platformName'], + ]; + } $queueForMails = new Mail($publisher); - if ($smtpEnabled) { - if (!empty($smtp['senderEmail'])) { - $senderEmail = $smtp['senderEmail']; - } - if (!empty($smtp['senderName'])) { - $senderName = $smtp['senderName']; - } - if (!empty($smtp['replyTo'])) { - $replyTo = $smtp['replyTo']; - } - + if ($smtp['enabled'] ?? false) { $queueForMails ->setSmtpHost($smtp['host'] ?? '') ->setSmtpPort($smtp['port'] ?? '') ->setSmtpUsername($smtp['username'] ?? '') ->setSmtpPassword($smtp['password'] ?? '') - ->setSmtpSecure($smtp['secure'] ?? ''); - - if (!empty($customTemplate)) { - if (!empty($customTemplate['senderEmail'])) { - $senderEmail = $customTemplate['senderEmail']; - } - if (!empty($customTemplate['senderName'])) { - $senderName = $customTemplate['senderName']; - } - if (!empty($customTemplate['replyTo'])) { - $replyTo = $customTemplate['replyTo']; - } - - $body = $customTemplate['message'] ?? ''; - $subject = $customTemplate['subject'] ?? $subject; - } - - $queueForMails - ->setSmtpReplyTo($replyTo) - ->setSmtpSenderEmail($senderEmail) - ->setSmtpSenderName($senderName); - } - - $clientName = $session->getAttribute('clientName'); - if (empty($clientName)) { - $userAgent = $session->getAttribute('userAgent'); - $clientName = !empty($userAgent) ? $userAgent : 'UNKNOWN'; - $session->setAttribute('clientName', $clientName); - } - - $projectName = $project->getAttribute('name'); - if ($project->getId() === 'console') { - $projectName = $platform['platformName']; - } - - $emailVariables = [ - 'direction' => $locale->getText('settings.direction'), - 'date' => (new \DateTime())->format('F j'), - 'year' => (new \DateTime())->format('YYYY'), - 'time' => (new \DateTime())->format('H:i:s'), - 'user' => $user->getAttribute('name'), - 'project' => $projectName, - 'device' => $session->getAttribute('clientName'), - 'ipAddress' => $session->getAttribute('ip'), - 'country' => $locale->getText('countries.' . $session->getAttribute('countryCode'), $locale->getText('locale.country.unknown')), - ]; - - if ($smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE) { - $emailVariables = array_merge($emailVariables, [ - 'accentColor' => $platform['accentColor'], - 'logoUrl' => $platform['logoUrl'], - 'twitter' => $platform['twitterUrl'], - 'discord' => $platform['discordUrl'], - 'github' => $platform['githubUrl'], - 'terms' => $platform['termsUrl'], - 'privacy' => $platform['privacyUrl'], - 'platform' => $platform['platformName'], - ]); + ->setSmtpSecure($smtp['secure'] ?? '') + ->setSmtpReplyTo($customTemplate['replyTo'] ?? $smtp['replyTo'] ?? '') + ->setSmtpSenderEmail($customTemplate['senderEmail'] ?? $smtp['senderEmail'] ?? System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM)) + ->setSmtpSenderName($customTemplate['senderName'] ?? $smtp['senderName'] ?? System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server')); } $queueForMails ->setSubject($subject) ->setPreview($preview) ->setBody($body) - ->setBodyTemplate($bodyTemplate) + ->setBodyTemplate(__DIR__ . '/../../../../app/config/locale/templates/' . $smtpBaseTemplate . '.tpl') ->appendVariables($emailVariables) - ->setRecipient($user->getAttribute('email')); + ->setRecipient($event->user['email']); - if ($smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE) { + if ($isBranded) { $queueForMails->setSenderName($platform['emailSenderName']); } diff --git a/src/Appwrite/Event/Mail.php b/src/Appwrite/Event/Mail.php index 38d7a27c11..d8f25489c6 100644 --- a/src/Appwrite/Event/Mail.php +++ b/src/Appwrite/Event/Mail.php @@ -101,7 +101,6 @@ class Mail extends Event /** * Sets preview for the mail event. * - * @param string $preview * @return self */ public function setPreview(string $preview): self