Merge pull request #12318 from appwrite/feat-email-template-reset-default

feat: add email template reset and get-default endpoints
This commit is contained in:
Harsh Mahajan
2026-05-15 20:27:39 +05:30
committed by GitHub
4 changed files with 242 additions and 0 deletions
@@ -0,0 +1,123 @@
<?php
namespace Appwrite\Platform\Modules\Console\Http\Templates\Email;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Template\Template;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Locale\Locale;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\WhiteList;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getConsoleEmailTemplate';
}
public function __construct()
{
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/console/templates/email/:templateId')
->desc('Get email template')
->groups(['api'])
->label('scope', 'public')
->label('sdk', new Method(
namespace: 'console',
group: null,
name: 'getEmailTemplate',
description: <<<EOT
Get the Appwrite built-in default email template for the specified type and locale. Always returns the unmodified default, ignoring any custom project overrides.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_EMAIL_TEMPLATE,
)
]
))
->param('templateId', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Email template type. Can be one of: ' . \implode(', ', Config::getParam('locale-templates')['email'] ?? []))
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale. If left empty, the fallback locale (en) will be used.', optional: true, injections: ['localeCodes'])
->inject('response')
->callback($this->action(...));
}
public function action(
string $templateId,
string $locale,
Response $response,
): void {
$locale = $locale ?: System::getEnv('_APP_LOCALE', 'en');
$localeObj = new Locale($locale);
$localeObj->setFallback(System::getEnv('_APP_LOCALE', 'en'));
$response->dynamic(new Document([
'templateId' => $templateId,
'locale' => $locale,
'subject' => $localeObj->getText('emails.' . $templateId . '.subject'),
'message' => $this->getDefaultMessage($templateId, $localeObj),
'senderName' => '',
'senderEmail' => '',
'replyToEmail' => '',
'replyToName' => '',
]), Response::MODEL_EMAIL_TEMPLATE);
}
private function getDefaultMessage(string $templateId, Locale $localeObj): string
{
$templateConfigs = [
'magicSession' => [
'file' => 'email-magic-url.tpl',
'placeholders' => ['optionButton', 'buttonText', 'optionUrl', 'clientInfo', 'securityPhrase']
],
'mfaChallenge' => [
'file' => 'email-mfa-challenge.tpl',
'placeholders' => ['description', 'clientInfo']
],
'otpSession' => [
'file' => 'email-otp.tpl',
'placeholders' => ['description', 'clientInfo', 'securityPhrase']
],
'sessionAlert' => [
'file' => 'email-session-alert.tpl',
'placeholders' => ['body', 'listDevice', 'listIpAddress', 'listCountry', 'footer']
],
];
$config = $templateConfigs[$templateId] ?? [
'file' => 'email-inner-base.tpl',
'placeholders' => ['buttonText', 'body', 'footer']
];
$templateString = file_get_contents(APP_CE_CONFIG_DIR . '/locale/templates/' . $config['file']);
$message = Template::fromString($templateString);
foreach ($config['placeholders'] as $param) {
$escapeHtml = !in_array($param, ['clientInfo', 'body', 'footer', 'description']);
if ($templateId === 'magicSession' && $param === 'securityPhrase') {
$message->setParam('{{securityPhrase}}', '');
continue;
}
$message->setParam("{{{$param}}}", $localeObj->getText("emails.{$templateId}.{$param}"), escapeHtml: $escapeHtml);
}
$message
->setParam('{{hello}}', $localeObj->getText("emails.{$templateId}.hello"))
->setParam('{{thanks}}', $localeObj->getText("emails.{$templateId}.thanks"))
->setParam('{{signature}}', $localeObj->getText("emails.{$templateId}.signature"));
return $message->render(useContent: true);
}
}
@@ -17,6 +17,7 @@ use Appwrite\Platform\Modules\Console\Http\Redirects\Root\Get as RedirectRoot;
use Appwrite\Platform\Modules\Console\Http\Resources\Get as GetResourceAvailability;
use Appwrite\Platform\Modules\Console\Http\Scopes\Organization\XList as ListOrganizationScopes;
use Appwrite\Platform\Modules\Console\Http\Scopes\Project\XList as ListKeyScopes;
use Appwrite\Platform\Modules\Console\Http\Templates\Email\Get as GetEmailTemplate;
use Appwrite\Platform\Modules\Console\Http\Variables\Get as GetVariables;
use Utopia\Platform\Service;
@@ -31,6 +32,7 @@ class Http extends Service
$this->addAction(Web::getName(), new Web());
$this->addAction(GetVariables::getName(), new GetVariables());
$this->addAction(GetEmailTemplate::getName(), new GetEmailTemplate());
$this->addAction(ListOAuth2Providers::getName(), new ListOAuth2Providers());
$this->addAction(ListKeyScopes::getName(), new ListKeyScopes());
$this->addAction(ListOrganizationScopes::getName(), new ListOrganizationScopes());
@@ -466,6 +466,14 @@ abstract class Format
return 'ConsoleResourceValue';
}
break;
case 'getEmailTemplate':
switch ($param) {
case 'templateId':
return 'ProjectEmailTemplateId';
case 'locale':
return 'ProjectEmailTemplateLocale';
}
break;
}
break;
case 'account':
@@ -1147,6 +1147,115 @@ trait TemplatesBase
return $this->client->call(Client::METHOD_PATCH, '/project/templates/email', $headers, $params);
}
// Console email template (default) tests
public function testGetConsoleEmailTemplate(): void
{
$response = $this->getConsoleEmailTemplate('verification', 'en');
$this->assertSame(200, $response['headers']['status-code']);
$this->assertSame('verification', $response['body']['templateId']);
$this->assertSame('en', $response['body']['locale']);
$this->assertNotEmpty($response['body']['subject']);
$this->assertNotEmpty($response['body']['message']);
$this->assertSame('', $response['body']['senderName']);
$this->assertSame('', $response['body']['senderEmail']);
$this->assertSame('', $response['body']['replyToEmail']);
$this->assertSame('', $response['body']['replyToName']);
}
public function testGetConsoleEmailTemplateIgnoresCustomOverride(): void
{
$this->ensureSMTPEnabled();
// Set a custom override on the project template.
$this->updateEmailTemplate(
templateId: 'recovery',
locale: 'en',
subject: 'Custom subject',
message: 'Custom message',
senderName: 'Custom Sender',
senderEmail: 'custom@appwrite.io',
);
// Console endpoint must always return the built-in default, not the override.
$response = $this->getConsoleEmailTemplate('recovery', 'en');
$this->assertSame(200, $response['headers']['status-code']);
$this->assertSame('recovery', $response['body']['templateId']);
$this->assertNotSame('Custom subject', $response['body']['subject']);
$this->assertSame('', $response['body']['senderName']);
$this->assertSame('', $response['body']['senderEmail']);
}
public function testGetConsoleEmailTemplateDefaultLocale(): void
{
$response = $this->getConsoleEmailTemplate('magicSession');
$this->assertSame(200, $response['headers']['status-code']);
$this->assertSame('en', $response['body']['locale']);
$this->assertNotEmpty($response['body']['subject']);
}
public function testGetConsoleEmailTemplateNonDefaultLocale(): void
{
$response = $this->getConsoleEmailTemplate('verification', 'fr');
$this->assertSame(200, $response['headers']['status-code']);
$this->assertSame('verification', $response['body']['templateId']);
$this->assertSame('fr', $response['body']['locale']);
$this->assertNotEmpty($response['body']['subject']);
$this->assertNotEmpty($response['body']['message']);
}
public function testGetConsoleEmailTemplateAllTypes(): void
{
$types = [
'verification',
'magicSession',
'recovery',
'invitation',
'mfaChallenge',
'sessionAlert',
'otpSession',
];
foreach ($types as $type) {
$response = $this->getConsoleEmailTemplate($type, 'en');
$this->assertSame(200, $response['headers']['status-code'], "type={$type}");
$this->assertNotEmpty($response['body']['subject'], "type={$type} must have subject");
$this->assertNotEmpty($response['body']['message'], "type={$type} must have message");
}
}
public function testGetConsoleEmailTemplateInvalidTemplateId(): void
{
$response = $this->getConsoleEmailTemplate('invalidTemplate', 'en');
$this->assertSame(400, $response['headers']['status-code']);
}
public function testGetConsoleEmailTemplateInvalidLocale(): void
{
$response = $this->getConsoleEmailTemplate('recovery', 'not-a-locale');
$this->assertSame(400, $response['headers']['status-code']);
}
protected function getConsoleEmailTemplate(string $templateId, ?string $locale = null): mixed
{
$params = [];
if ($locale !== null) {
$params['locale'] = $locale;
}
return $this->client->call(Client::METHOD_GET, '/console/templates/email/' . $templateId, [
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
], $params);
}
protected function ensureSMTPEnabled(): void
{
$this->client->call(