mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Add missing endpoint in email templates
This commit is contained in:
@@ -211,6 +211,7 @@ Response::setModel(new BaseList('Phones List', Response::MODEL_PHONE_LIST, 'phon
|
||||
Response::setModel(new BaseList('Metric List', Response::MODEL_METRIC_LIST, 'metrics', Response::MODEL_METRIC, true, false));
|
||||
Response::setModel(new BaseList('Variables List', Response::MODEL_VARIABLE_LIST, 'variables', Response::MODEL_VARIABLE));
|
||||
Response::setModel(new BaseList('Mock Numbers List', Response::MODEL_MOCK_NUMBER_LIST, 'mockNumbers', Response::MODEL_MOCK_NUMBER));
|
||||
Response::setModel(new BaseList('Email Templates List', Response::MODEL_EMAIL_TEMPLATE_LIST, 'templates', Response::MODEL_EMAIL_TEMPLATE));
|
||||
Response::setModel(new BaseList('Status List', Response::MODEL_HEALTH_STATUS_LIST, 'statuses', Response::MODEL_HEALTH_STATUS));
|
||||
Response::setModel(new BaseList('Rule List', Response::MODEL_PROXY_RULE_LIST, 'rules', Response::MODEL_PROXY_RULE));
|
||||
Response::setModel(new BaseList('Schedules List', Response::MODEL_SCHEDULE_LIST, 'schedules', Response::MODEL_SCHEDULE));
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Templates\Email;
|
||||
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
|
||||
class XList extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'listProjectEmailTemplates';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/project/templates/email')
|
||||
->desc('List project email templates')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'templates.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'templates',
|
||||
name: 'listEmailTemplates',
|
||||
description: <<<EOT
|
||||
Get a list of all custom email templates configured for the project. This endpoint returns an array of all configured email templates and their locales.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_EMAIL_TEMPLATE_LIST,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
bool $includeTotal,
|
||||
Response $response,
|
||||
Document $project,
|
||||
) {
|
||||
$templates = $project->getAttribute('templates', []);
|
||||
|
||||
$emailTemplates = [];
|
||||
foreach ($templates as $key => $template) {
|
||||
if (!\str_starts_with($key, 'email.')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$suffix = \substr($key, \strlen('email.'));
|
||||
$parts = \explode('-', $suffix, 2);
|
||||
if (\count($parts) !== 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$templateId, $locale] = $parts;
|
||||
|
||||
$template['templateId'] = $templateId;
|
||||
$template['locale'] = $locale;
|
||||
|
||||
// Backwards compatibility
|
||||
if (!\is_null($template['replyTo'] ?? null)) {
|
||||
$template['replyToEmail'] = $template['replyToEmail'] ?? $template['replyTo'] ?? '';
|
||||
}
|
||||
|
||||
$emailTemplates[] = new Document($template);
|
||||
}
|
||||
|
||||
$total = $includeTotal ? \count($emailTemplates) : 0;
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'templates' => $emailTemplates,
|
||||
'total' => $total,
|
||||
]), Response::MODEL_EMAIL_TEMPLATE_LIST);
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ use Appwrite\Platform\Modules\Project\Http\Project\SMTP\Tests\Create as CreateSM
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\SMTP\Update as UpdateSMTP;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Templates\Email\Get as GetTemplate;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Templates\Email\Update as UpdateTemplate;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Templates\Email\XList as ListTemplates;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Create as CreateVariable;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Delete as DeleteVariable;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Get as GetVariable;
|
||||
@@ -68,6 +69,7 @@ class Http extends Service
|
||||
$this->addAction(CreateSMTPTest::getName(), new CreateSMTPTest());
|
||||
|
||||
// Templates
|
||||
$this->addAction(ListTemplates::getName(), new ListTemplates());
|
||||
$this->addAction(GetTemplate::getName(), new GetTemplate());
|
||||
$this->addAction(UpdateTemplate::getName(), new UpdateTemplate());
|
||||
|
||||
|
||||
@@ -267,6 +267,7 @@ class Response extends SwooleResponse
|
||||
public const MODEL_VARIABLE_LIST = 'variableList';
|
||||
public const MODEL_VCS = 'vcs';
|
||||
public const MODEL_EMAIL_TEMPLATE = 'emailTemplate';
|
||||
public const MODEL_EMAIL_TEMPLATE_LIST = 'emailTemplateList';
|
||||
|
||||
// Health
|
||||
public const MODEL_HEALTH_STATUS = 'healthStatus';
|
||||
|
||||
@@ -579,6 +579,233 @@ trait TemplatesBase
|
||||
}
|
||||
}
|
||||
|
||||
// List email template tests
|
||||
|
||||
public function testListEmailTemplatesReturnsSeededTemplate(): void
|
||||
{
|
||||
$this->ensureSMTPEnabled();
|
||||
|
||||
$subject = 'List subject ' . \uniqid();
|
||||
$seed = $this->updateEmailTemplate(
|
||||
templateId: 'verification',
|
||||
locale: 'en',
|
||||
subject: $subject,
|
||||
message: 'List body',
|
||||
);
|
||||
$this->assertSame(200, $seed['headers']['status-code']);
|
||||
|
||||
$response = $this->listEmailTemplates();
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertArrayHasKey('templates', $response['body']);
|
||||
$this->assertArrayHasKey('total', $response['body']);
|
||||
$this->assertIsArray($response['body']['templates']);
|
||||
$this->assertIsInt($response['body']['total']);
|
||||
$this->assertGreaterThanOrEqual(1, $response['body']['total']);
|
||||
|
||||
$found = null;
|
||||
foreach ($response['body']['templates'] as $template) {
|
||||
if (
|
||||
$template['templateId'] === 'verification'
|
||||
&& $template['locale'] === 'en'
|
||||
&& $template['subject'] === $subject
|
||||
) {
|
||||
$found = $template;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->assertNotNull($found, 'seeded verification/en template must appear in the list');
|
||||
}
|
||||
|
||||
public function testListEmailTemplatesResponseModel(): void
|
||||
{
|
||||
$this->ensureSMTPEnabled();
|
||||
|
||||
$seed = $this->updateEmailTemplate(
|
||||
templateId: 'invitation',
|
||||
locale: 'en',
|
||||
subject: 'Shape subject ' . \uniqid(),
|
||||
message: 'Shape body',
|
||||
senderName: 'Shape Sender',
|
||||
senderEmail: 'shape@appwrite.io',
|
||||
replyToEmail: 'shape-reply@appwrite.io',
|
||||
replyToName: 'Shape Reply',
|
||||
);
|
||||
$this->assertSame(200, $seed['headers']['status-code']);
|
||||
|
||||
$response = $this->listEmailTemplates();
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']['templates']);
|
||||
|
||||
foreach ($response['body']['templates'] as $template) {
|
||||
$this->assertArrayHasKey('templateId', $template);
|
||||
$this->assertArrayHasKey('locale', $template);
|
||||
$this->assertArrayHasKey('subject', $template);
|
||||
$this->assertArrayHasKey('message', $template);
|
||||
$this->assertArrayHasKey('senderName', $template);
|
||||
$this->assertArrayHasKey('senderEmail', $template);
|
||||
$this->assertArrayHasKey('replyToEmail', $template);
|
||||
$this->assertArrayHasKey('replyToName', $template);
|
||||
}
|
||||
}
|
||||
|
||||
public function testListEmailTemplatesSeparatesLocales(): void
|
||||
{
|
||||
$this->ensureSMTPEnabled();
|
||||
|
||||
$runId = \uniqid();
|
||||
$enSubject = "Multi-locale EN {$runId}";
|
||||
$frSubject = "Multi-locale FR {$runId}";
|
||||
|
||||
$this->assertSame(200, $this->updateEmailTemplate(
|
||||
templateId: 'recovery',
|
||||
locale: 'en',
|
||||
subject: $enSubject,
|
||||
message: 'EN body',
|
||||
)['headers']['status-code']);
|
||||
|
||||
$this->assertSame(200, $this->updateEmailTemplate(
|
||||
templateId: 'recovery',
|
||||
locale: 'fr',
|
||||
subject: $frSubject,
|
||||
message: 'FR body',
|
||||
)['headers']['status-code']);
|
||||
|
||||
$response = $this->listEmailTemplates();
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
|
||||
$foundEn = false;
|
||||
$foundFr = false;
|
||||
foreach ($response['body']['templates'] as $template) {
|
||||
if ($template['templateId'] === 'recovery' && $template['locale'] === 'en' && $template['subject'] === $enSubject) {
|
||||
$foundEn = true;
|
||||
}
|
||||
if ($template['templateId'] === 'recovery' && $template['locale'] === 'fr' && $template['subject'] === $frSubject) {
|
||||
$foundFr = true;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertTrue($foundEn, 'recovery/en must appear in the list');
|
||||
$this->assertTrue($foundFr, 'recovery/fr must appear in the list');
|
||||
}
|
||||
|
||||
public function testListEmailTemplatesUpdateDoesNotDuplicate(): void
|
||||
{
|
||||
$this->ensureSMTPEnabled();
|
||||
|
||||
$runId = \uniqid();
|
||||
$firstSubject = "First {$runId}";
|
||||
$secondSubject = "Second {$runId}";
|
||||
|
||||
$this->assertSame(200, $this->updateEmailTemplate(
|
||||
templateId: 'mfaChallenge',
|
||||
locale: 'en',
|
||||
subject: $firstSubject,
|
||||
message: 'Body',
|
||||
)['headers']['status-code']);
|
||||
|
||||
$before = $this->listEmailTemplates();
|
||||
$this->assertSame(200, $before['headers']['status-code']);
|
||||
$beforeTotal = $before['body']['total'];
|
||||
|
||||
$this->assertSame(200, $this->updateEmailTemplate(
|
||||
templateId: 'mfaChallenge',
|
||||
locale: 'en',
|
||||
subject: $secondSubject,
|
||||
message: 'Body',
|
||||
)['headers']['status-code']);
|
||||
|
||||
$after = $this->listEmailTemplates();
|
||||
$this->assertSame(200, $after['headers']['status-code']);
|
||||
|
||||
// Same templateId/locale must remain a single entry, not accumulate.
|
||||
$this->assertSame($beforeTotal, $after['body']['total']);
|
||||
|
||||
$matches = \array_values(\array_filter(
|
||||
$after['body']['templates'],
|
||||
fn ($t) => $t['templateId'] === 'mfaChallenge' && $t['locale'] === 'en',
|
||||
));
|
||||
$this->assertCount(1, $matches);
|
||||
$this->assertSame($secondSubject, $matches[0]['subject']);
|
||||
}
|
||||
|
||||
public function testListEmailTemplatesTotalFalse(): void
|
||||
{
|
||||
$this->ensureSMTPEnabled();
|
||||
|
||||
// Ensure at least one template exists so `templates` is non-empty.
|
||||
$this->assertSame(200, $this->updateEmailTemplate(
|
||||
templateId: 'verification',
|
||||
locale: 'en',
|
||||
subject: 'Total-false subject',
|
||||
message: 'Body',
|
||||
)['headers']['status-code']);
|
||||
|
||||
$response = $this->listEmailTemplates(total: false);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertIsInt($response['body']['total']);
|
||||
$this->assertSame(0, $response['body']['total']);
|
||||
$this->assertNotEmpty($response['body']['templates']);
|
||||
}
|
||||
|
||||
public function testListEmailTemplatesTotalMatchesCount(): void
|
||||
{
|
||||
$this->ensureSMTPEnabled();
|
||||
|
||||
$this->assertSame(200, $this->updateEmailTemplate(
|
||||
templateId: 'verification',
|
||||
locale: 'en',
|
||||
subject: 'Match subject',
|
||||
message: 'Body',
|
||||
)['headers']['status-code']);
|
||||
|
||||
$response = $this->listEmailTemplates();
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame(\count($response['body']['templates']), $response['body']['total']);
|
||||
}
|
||||
|
||||
public function testListEmailTemplatesOnlyReturnsCustomizedTemplates(): void
|
||||
{
|
||||
$this->ensureSMTPEnabled();
|
||||
|
||||
// Seed exactly one template so we have a stable marker to count against.
|
||||
$marker = 'Customized-only ' . \uniqid();
|
||||
$this->assertSame(200, $this->updateEmailTemplate(
|
||||
templateId: 'otpSession',
|
||||
locale: 'en',
|
||||
subject: $marker,
|
||||
message: 'Body',
|
||||
)['headers']['status-code']);
|
||||
|
||||
$response = $this->listEmailTemplates();
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
|
||||
// Every returned entry must be a real stored template (has templateId+locale set,
|
||||
// not a synthesized default row for every possible type).
|
||||
foreach ($response['body']['templates'] as $template) {
|
||||
$this->assertNotEmpty($template['templateId']);
|
||||
$this->assertNotEmpty($template['locale']);
|
||||
}
|
||||
|
||||
// A `(templateId, locale)` pair that has never been customized in this test
|
||||
// run must NOT show up. 'otpSession'/'pt-br' has no writer anywhere in the file.
|
||||
$uncustomized = \array_filter(
|
||||
$response['body']['templates'],
|
||||
fn ($t) => $t['templateId'] === 'otpSession' && $t['locale'] === 'pt-br',
|
||||
);
|
||||
$this->assertEmpty($uncustomized, 'uncustomized (templateId, locale) pairs must not appear');
|
||||
}
|
||||
|
||||
public function testListEmailTemplatesWithoutAuthentication(): void
|
||||
{
|
||||
$response = $this->listEmailTemplates(authenticated: false);
|
||||
|
||||
$this->assertSame(401, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
// Backwards compatibility (x-appwrite-response-format: 1.9.1)
|
||||
|
||||
public function testGetEmailTemplateLegacyResponseFormat(): void
|
||||
@@ -804,6 +1031,25 @@ trait TemplatesBase
|
||||
return $this->client->call(Client::METHOD_GET, '/project/templates/email/' . $templateId, $headers, $params);
|
||||
}
|
||||
|
||||
protected function listEmailTemplates(?bool $total = null, bool $authenticated = true): mixed
|
||||
{
|
||||
$headers = [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
];
|
||||
|
||||
if ($authenticated) {
|
||||
$headers = \array_merge($headers, $this->getHeaders());
|
||||
}
|
||||
|
||||
$params = [];
|
||||
if ($total !== null) {
|
||||
$params['total'] = $total;
|
||||
}
|
||||
|
||||
return $this->client->call(Client::METHOD_GET, '/project/templates/email', $headers, $params);
|
||||
}
|
||||
|
||||
protected function updateEmailTemplate(
|
||||
string $templateId,
|
||||
?string $locale = null,
|
||||
|
||||
Reference in New Issue
Block a user