diff --git a/composer.json b/composer.json index e16437bc99..30d8eedc73 100644 --- a/composer.json +++ b/composer.json @@ -75,7 +75,7 @@ "utopia-php/lock": "0.2.*", "utopia-php/logger": "0.8.*", "utopia-php/messaging": "0.22.*", - "utopia-php/migration": "dev-add-custom-domains-migration as 1.12.0", + "utopia-php/migration": "dev-add-email-templates-migration as 1.12.0", "utopia-php/platform": "1.0.0-rc2", "utopia-php/pools": "1.*", "utopia-php/span": "1.1.*", diff --git a/composer.lock b/composer.lock index f089b0af28..0d112275d8 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": "4d93c3da3ef7aa9fe898f609a6524431", + "content-hash": "575aa56a776213cb0c24063ec029441b", "packages": [ { "name": "adhocore/jwt", @@ -4675,16 +4675,16 @@ }, { "name": "utopia-php/migration", - "version": "dev-add-custom-domains-migration", + "version": "dev-add-email-templates-migration", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "cc7d68aaadf2b37adcce7735b9e2f068cc78caf9" + "reference": "9813afb2d7e0ddf8f00002fa30c0318d697e8d97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/cc7d68aaadf2b37adcce7735b9e2f068cc78caf9", - "reference": "cc7d68aaadf2b37adcce7735b9e2f068cc78caf9", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/9813afb2d7e0ddf8f00002fa30c0318d697e8d97", + "reference": "9813afb2d7e0ddf8f00002fa30c0318d697e8d97", "shasum": "" }, "require": { @@ -4741,10 +4741,10 @@ "utopia" ], "support": { - "source": "https://github.com/utopia-php/migration/tree/add-custom-domains-migration", + "source": "https://github.com/utopia-php/migration/tree/add-email-templates-migration", "issues": "https://github.com/utopia-php/migration/issues" }, - "time": "2026-05-21T13:40:46+00:00" + "time": "2026-05-21T15:55:26+00:00" }, { "name": "utopia-php/mongo", @@ -8593,7 +8593,7 @@ }, { "package": "utopia-php/migration", - "version": "dev-add-custom-domains-migration", + "version": "dev-add-email-templates-migration", "alias": "1.12.0", "alias_normalized": "1.12.0.0" } diff --git a/src/Appwrite/Utopia/Response/Model/MigrationReport.php b/src/Appwrite/Utopia/Response/Model/MigrationReport.php index a0f83f2b9f..1b475595d1 100644 --- a/src/Appwrite/Utopia/Response/Model/MigrationReport.php +++ b/src/Appwrite/Utopia/Response/Model/MigrationReport.php @@ -119,6 +119,12 @@ class MigrationReport extends Model 'default' => 0, 'example' => 5, ]) + ->addRule(Resource::TYPE_EMAIL_TEMPLATE, [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Number of custom email templates to be migrated (one per templateId × locale pair).', + 'default' => 0, + 'example' => 7, + ]) ->addRule(Resource::TYPE_SITE, [ 'type' => self::TYPE_INTEGER, 'description' => 'Number of sites to be migrated.', diff --git a/tests/e2e/Services/Migrations/MigrationsBase.php b/tests/e2e/Services/Migrations/MigrationsBase.php index 548c532884..95967587ac 100644 --- a/tests/e2e/Services/Migrations/MigrationsBase.php +++ b/tests/e2e/Services/Migrations/MigrationsBase.php @@ -3114,6 +3114,110 @@ trait MigrationsBase $this->client->call(Client::METHOD_DELETE, '/proxy/rules/' . $sourceRule['$id'], $sourceHeaders); } + public function testAppwriteMigrationEmailTemplate(): void + { + $consoleHeaders = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + 'origin' => 'http://localhost', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + ]; + + $sourceProjectId = $this->getProject()['$id']; + $destinationProjectId = $this->getDestinationProject()['$id']; + + // The source SDK path requires custom SMTP enabled before a template can be saved. + $this->client->call(Client::METHOD_PATCH, '/projects/' . $sourceProjectId . '/smtp', $consoleHeaders, [ + 'enabled' => true, + 'senderName' => 'Test Sender', + 'senderEmail' => 'sender@example.com', + 'host' => 'smtp.example.com', + 'port' => 587, + 'username' => 'test', + 'password' => 'test', + 'secure' => 'tls', + ]); + + $templateId = 'verification'; + $locale = 'en'; + $subject = 'Verify your account ' . ID::unique(); + $message = '

Hello {{user}}, verify your account at {{redirect}}

'; + + $update = $this->client->call( + Client::METHOD_PATCH, + '/projects/' . $sourceProjectId . '/templates/email/' . $templateId . '/' . $locale, + $consoleHeaders, + [ + 'subject' => $subject, + 'message' => $message, + 'senderName' => 'Template Sender', + 'senderEmail' => 'tpl-sender@example.com', + 'replyToEmail' => 'reply@example.com', + 'replyToName' => 'Reply Team', + ] + ); + $this->assertEquals(200, $update['headers']['status-code']); + + $result = $this->performMigrationSync([ + 'resources' => [ + Resource::TYPE_EMAIL_TEMPLATE, + ], + 'endpoint' => $this->webEndpoint, + 'projectId' => $sourceProjectId, + 'apiKey' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals('completed', $result['status']); + $this->assertEquals([Resource::TYPE_EMAIL_TEMPLATE], $result['resources']); + $this->assertArrayHasKey(Resource::TYPE_EMAIL_TEMPLATE, $result['statusCounters']); + $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_EMAIL_TEMPLATE]['error']); + $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_EMAIL_TEMPLATE]['pending']); + $this->assertGreaterThanOrEqual(1, $result['statusCounters'][Resource::TYPE_EMAIL_TEMPLATE]['success']); + $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_EMAIL_TEMPLATE]['processing']); + $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_EMAIL_TEMPLATE]['warning']); + + // Read-back via the SDK requires the destination to have SMTP enabled too. + $this->client->call(Client::METHOD_PATCH, '/projects/' . $destinationProjectId . '/smtp', $consoleHeaders, [ + 'enabled' => true, + 'senderName' => 'Dest Sender', + 'senderEmail' => 'dest@example.com', + 'host' => 'smtp.example.com', + 'port' => 587, + 'username' => 'test', + 'password' => 'test', + 'secure' => 'tls', + ]); + + $fetched = $this->client->call( + Client::METHOD_GET, + '/projects/' . $destinationProjectId . '/templates/email/' . $templateId . '/' . $locale, + $consoleHeaders + ); + $this->assertEquals(200, $fetched['headers']['status-code']); + $this->assertSame($subject, $fetched['body']['subject']); + $this->assertSame($message, $fetched['body']['message']); + $this->assertSame('Template Sender', $fetched['body']['senderName']); + $this->assertSame('tpl-sender@example.com', $fetched['body']['senderEmail']); + $this->assertSame('reply@example.com', $fetched['body']['replyToEmail']); + $this->assertSame('Reply Team', $fetched['body']['replyToName']); + + // Reset both projects so the test is idempotent. + $smtpReset = [ + 'enabled' => false, + 'senderName' => '', + 'senderEmail' => '', + 'replyToName' => '', + 'replyToEmail' => '', + 'host' => '', + 'port' => 0, + 'username' => '', + 'password' => '', + 'secure' => '', + ]; + $this->client->call(Client::METHOD_PATCH, '/projects/' . $sourceProjectId . '/smtp', $consoleHeaders, $smtpReset); + $this->client->call(Client::METHOD_PATCH, '/projects/' . $destinationProjectId . '/smtp', $consoleHeaders, $smtpReset); + } + /** * Import documents from a CSV file. */