Merge pull request #12348 from appwrite/fix-smtp-template-empty-string-params

fix: allow empty string to clear optional SMTP and email template fields
This commit is contained in:
Harsh Mahajan
2026-05-20 17:00:14 +05:30
committed by GitHub
5 changed files with 250 additions and 75 deletions
Generated
+73 -61
View File
@@ -3615,16 +3615,16 @@
},
{
"name": "utopia-php/cache",
"version": "3.0.0",
"version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/cache.git",
"reference": "ece1f4d11ec2804cd7e05b9717dc7a2bc66e4176"
"reference": "086687d7ae23dd1dae67b943161e8cef143539e1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/cache/zipball/ece1f4d11ec2804cd7e05b9717dc7a2bc66e4176",
"reference": "ece1f4d11ec2804cd7e05b9717dc7a2bc66e4176",
"url": "https://api.github.com/repos/utopia-php/cache/zipball/086687d7ae23dd1dae67b943161e8cef143539e1",
"reference": "086687d7ae23dd1dae67b943161e8cef143539e1",
"shasum": ""
},
"require": {
@@ -3663,9 +3663,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/cache/issues",
"source": "https://github.com/utopia-php/cache/tree/3.0.0"
"source": "https://github.com/utopia-php/cache/tree/3.0.2"
},
"time": "2026-05-14T14:13:17+00:00"
"time": "2026-05-19T22:38:16+00:00"
},
{
"name": "utopia-php/circuit-breaker",
@@ -4079,16 +4079,16 @@
},
{
"name": "utopia-php/dns",
"version": "1.7.0",
"version": "1.7.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/dns.git",
"reference": "90bf1bc4a51ceca93590d09e7365317b28d1eb89"
"reference": "5225f52a82d4128e69ad17c2a81fcfea6aa00ae1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/dns/zipball/90bf1bc4a51ceca93590d09e7365317b28d1eb89",
"reference": "90bf1bc4a51ceca93590d09e7365317b28d1eb89",
"url": "https://api.github.com/repos/utopia-php/dns/zipball/5225f52a82d4128e69ad17c2a81fcfea6aa00ae1",
"reference": "5225f52a82d4128e69ad17c2a81fcfea6aa00ae1",
"shasum": ""
},
"require": {
@@ -4099,9 +4099,9 @@
"utopia-php/validators": "0.*"
},
"require-dev": {
"laravel/pint": "1.25.*",
"laravel/pint": "1.29.*",
"phpstan/phpstan": "2.0.*",
"phpunit/phpunit": "12.4.*",
"phpunit/phpunit": "12.5.*",
"swoole/ide-helper": "5.1.8"
},
"type": "library",
@@ -4130,9 +4130,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/dns/issues",
"source": "https://github.com/utopia-php/dns/tree/1.7.0"
"source": "https://github.com/utopia-php/dns/tree/1.7.2"
},
"time": "2026-05-13T07:11:31+00:00"
"time": "2026-05-20T04:49:11+00:00"
},
{
"name": "utopia-php/domains",
@@ -4606,16 +4606,16 @@
},
{
"name": "utopia-php/messaging",
"version": "0.22.2",
"version": "0.22.3",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/messaging.git",
"reference": "f99feceab575243f3a86ee2e90cd1a6407805def"
"reference": "67366d5f45cc92efe7adb6aab5d6dcd2342f2f9e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/f99feceab575243f3a86ee2e90cd1a6407805def",
"reference": "f99feceab575243f3a86ee2e90cd1a6407805def",
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/67366d5f45cc92efe7adb6aab5d6dcd2342f2f9e",
"reference": "67366d5f45cc92efe7adb6aab5d6dcd2342f2f9e",
"shasum": ""
},
"require": {
@@ -4651,9 +4651,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/messaging/issues",
"source": "https://github.com/utopia-php/messaging/tree/0.22.2"
"source": "https://github.com/utopia-php/messaging/tree/0.22.3"
},
"time": "2026-05-14T08:51:26+00:00"
"time": "2026-05-19T05:31:20+00:00"
},
{
"name": "utopia-php/migration",
@@ -5638,16 +5638,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "1.29.5",
"version": "1.31.0",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "e670edcdfb9ffcec36125b1eb3e4473dce30b620"
"reference": "a7119db15696131a86d477b3bed348beda85523f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/e670edcdfb9ffcec36125b1eb3e4473dce30b620",
"reference": "e670edcdfb9ffcec36125b1eb3e4473dce30b620",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/a7119db15696131a86d477b3bed348beda85523f",
"reference": "a7119db15696131a86d477b3bed348beda85523f",
"shasum": ""
},
"require": {
@@ -5683,9 +5683,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/1.29.5"
"source": "https://github.com/appwrite/sdk-generator/tree/1.31.0"
},
"time": "2026-05-15T06:49:05+00:00"
"time": "2026-05-20T11:16:09+00:00"
},
{
"name": "brianium/paratest",
@@ -6394,11 +6394,11 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.54",
"version": "2.1.55",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/8be50c3992107dc837b17da4d140fbbdf9a5c5bd",
"reference": "8be50c3992107dc837b17da4d140fbbdf9a5c5bd",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/9eaac3826ed5e9b8427350a43cac825eeca3f566",
"reference": "9eaac3826ed5e9b8427350a43cac825eeca3f566",
"shasum": ""
},
"require": {
@@ -6443,7 +6443,7 @@
"type": "github"
}
],
"time": "2026-04-29T13:31:09+00:00"
"time": "2026-05-18T11:57:34+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -6882,23 +6882,23 @@
},
{
"name": "sebastian/cli-parser",
"version": "4.2.0",
"version": "4.2.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/cli-parser.git",
"reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04"
"reference": "7d05781b13f7dec9043a629a21d086ed74582a15"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/90f41072d220e5c40df6e8635f5dafba2d9d4d04",
"reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04",
"url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/7d05781b13f7dec9043a629a21d086ed74582a15",
"reference": "7d05781b13f7dec9043a629a21d086ed74582a15",
"shasum": ""
},
"require": {
"php": ">=8.3"
},
"require-dev": {
"phpunit/phpunit": "^12.0"
"phpunit/phpunit": "^12.5.25"
},
"type": "library",
"extra": {
@@ -6927,7 +6927,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/cli-parser/issues",
"security": "https://github.com/sebastianbergmann/cli-parser/security/policy",
"source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.0"
"source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.1"
},
"funding": [
{
@@ -6947,7 +6947,7 @@
"type": "tidelift"
}
],
"time": "2025-09-14T09:36:45+00:00"
"time": "2026-05-17T05:29:34+00:00"
},
{
"name": "sebastian/comparator",
@@ -7244,25 +7244,25 @@
},
{
"name": "sebastian/exporter",
"version": "7.0.2",
"version": "7.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
"reference": "016951ae10980765e4e7aee491eb288c64e505b7"
"reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7",
"reference": "016951ae10980765e4e7aee491eb288c64e505b7",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23",
"reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": ">=8.3",
"sebastian/recursion-context": "^7.0"
"sebastian/recursion-context": "^7.0.1"
},
"require-dev": {
"phpunit/phpunit": "^12.0"
"phpunit/phpunit": "^12.5.25"
},
"type": "library",
"extra": {
@@ -7310,7 +7310,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues",
"security": "https://github.com/sebastianbergmann/exporter/security/policy",
"source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2"
"source": "https://github.com/sebastianbergmann/exporter/tree/7.0.3"
},
"funding": [
{
@@ -7330,7 +7330,7 @@
"type": "tidelift"
}
],
"time": "2025-09-24T06:16:11+00:00"
"time": "2026-05-20T04:37:17+00:00"
},
{
"name": "sebastian/global-state",
@@ -7408,24 +7408,24 @@
},
{
"name": "sebastian/lines-of-code",
"version": "4.0.0",
"version": "4.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/lines-of-code.git",
"reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f"
"reference": "d543b8ef219dcd8da262cbb958639a96bedba10e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f",
"reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f",
"url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d543b8ef219dcd8da262cbb958639a96bedba10e",
"reference": "d543b8ef219dcd8da262cbb958639a96bedba10e",
"shasum": ""
},
"require": {
"nikic/php-parser": "^5.0",
"nikic/php-parser": "^5.7.0",
"php": ">=8.3"
},
"require-dev": {
"phpunit/phpunit": "^12.0"
"phpunit/phpunit": "^12.5.25"
},
"type": "library",
"extra": {
@@ -7454,15 +7454,27 @@
"support": {
"issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
"security": "https://github.com/sebastianbergmann/lines-of-code/security/policy",
"source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.0"
"source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.1"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
},
{
"url": "https://liberapay.com/sebastianbergmann",
"type": "liberapay"
},
{
"url": "https://thanks.dev/u/gh/sebastianbergmann",
"type": "thanks_dev"
},
{
"url": "https://tidelift.com/funding/github/packagist/sebastian/lines-of-code",
"type": "tidelift"
}
],
"time": "2025-02-07T04:57:28+00:00"
"time": "2026-05-19T16:22:07+00:00"
},
{
"name": "sebastian/object-enumerator",
@@ -7656,23 +7668,23 @@
},
{
"name": "sebastian/type",
"version": "6.0.3",
"version": "6.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/type.git",
"reference": "e549163b9760b8f71f191651d22acf32d56d6d4d"
"reference": "82ff822c2edc46724be9f7411d3163021f602773"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d",
"reference": "e549163b9760b8f71f191651d22acf32d56d6d4d",
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/82ff822c2edc46724be9f7411d3163021f602773",
"reference": "82ff822c2edc46724be9f7411d3163021f602773",
"shasum": ""
},
"require": {
"php": ">=8.3"
},
"require-dev": {
"phpunit/phpunit": "^12.0"
"phpunit/phpunit": "^12.5.25"
},
"type": "library",
"extra": {
@@ -7701,7 +7713,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/type/issues",
"security": "https://github.com/sebastianbergmann/type/security/policy",
"source": "https://github.com/sebastianbergmann/type/tree/6.0.3"
"source": "https://github.com/sebastianbergmann/type/tree/6.0.4"
},
"funding": [
{
@@ -7721,7 +7733,7 @@
"type": "tidelift"
}
],
"time": "2025-08-09T06:57:12+00:00"
"time": "2026-05-20T06:45:45+00:00"
},
{
"name": "sebastian/version",
@@ -8537,7 +8549,7 @@
},
{
"name": "twig/twig",
"version": "v3.14.2",
"version": "3.14.x-dev",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
@@ -60,12 +60,12 @@ class Update extends Action
))
->param('host', null, new Nullable(new Hostname()), 'SMTP server hostname (domain)', optional: true)
->param('port', null, new Nullable(new Integer()), 'SMTP server port', optional: true)
->param('username', null, new Nullable(new Text(256)), 'SMTP server username. Leave empty for no authorization.', optional: true)
->param('password', null, new Nullable(new Text(256)), 'SMTP server password. Leave empty for no authorization. This property is stored securely and cannot be read in future (write-only).', optional: true)
->param('senderEmail', null, new Nullable(new Email()), 'Email address shown in inbox as the sender of the email.', optional: true)
->param('senderName', null, new Nullable(new Text(256)), 'Name shown in inbox as the sender of the email.', optional: true)
->param('replyToEmail', null, new Nullable(new Email()), 'Email used when user replies to the email.', optional: true)
->param('replyToName', null, new Nullable(new Text(256)), 'Name used when user replies to the email.', optional: true)
->param('username', null, new Nullable(new Text(256, 0)), 'SMTP server username. Pass an empty string to clear a previously set value.', optional: true)
->param('password', null, new Nullable(new Text(256, 0)), 'SMTP server password. Pass an empty string to clear a previously set value. This property is stored securely and cannot be read in future (write-only).', optional: true)
->param('senderEmail', null, new Nullable(new Email(allowEmpty: true)), 'Email address shown in inbox as the sender of the email. Pass an empty string to clear a previously set value.', optional: true)
->param('senderName', null, new Nullable(new Text(256, 0)), 'Name shown in inbox as the sender of the email. Pass an empty string to clear a previously set value.', optional: true)
->param('replyToEmail', null, new Nullable(new Email(allowEmpty: true)), 'Email used when user replies to the email. Pass an empty string to clear a previously set value.', optional: true)
->param('replyToName', null, new Nullable(new Text(256, 0)), 'Name used when user replies to the email. Pass an empty string to clear a previously set value.', optional: true)
->param('secure', null, new Nullable(new WhiteList(['tls', 'ssl'], true)), 'Configures if communication with SMTP server is encrypted. Allowed values are: tls, ssl. Leave empty for no encryption.', optional: true)
->param('enabled', null, new Nullable(new Boolean()), 'Enable or disable custom SMTP. Custom SMTP is useful for branding purposes, but also allows use of custom email templates.', optional: true)
->inject('response')
@@ -95,7 +95,8 @@ class Update extends Action
// Fetch current configuration
$smtp = $project->getAttribute('smtp', []);
// Apply changes
// Apply changes — null means "not provided, keep existing".
// Empty string explicitly clears a previously-set value.
$keys = ['host', 'port', 'username', 'password', 'senderEmail', 'senderName', 'replyToEmail', 'replyToName', 'secure', 'enabled'];
foreach ($keys as $key) {
if (!\is_null(${$key})) {
@@ -61,8 +61,8 @@ class Update extends Action
->param('subject', null, new Nullable(new Text(255)), 'Subject of the email template. Can be up to 255 characters.', optional: true)
->param('message', null, new Nullable(new Text(10485760)), 'Plain or HTML body of the email template message. Can be up to 10MB of content.', optional: true)
->param('senderName', null, new Nullable(new Text(255, 0)), 'Name of the email sender.', optional: true)
->param('senderEmail', null, new Nullable(new Email()), 'Email of the sender.', optional: true)
->param('replyToEmail', null, new Nullable(new Email()), 'Reply to email.', optional: true)
->param('senderEmail', null, new Nullable(new Email(allowEmpty: true)), 'Email of the sender. Pass an empty string to clear a previously set value.', optional: true)
->param('replyToEmail', null, new Nullable(new Email(allowEmpty: true)), 'Reply to email. Pass an empty string to clear a previously set value.', optional: true)
->param('replyToName', null, new Nullable(new Text(255, 0)), 'Reply to name.', optional: true)
->inject('response')
->inject('queueForEvents')
@@ -99,7 +99,8 @@ class Update extends Action
$templates = $project->getAttribute('templates', []);
$template = $templates['email.' . $templateId . '-' . $locale] ?? [];
// Apply changes
// Apply changes — null means "not provided, keep existing".
// Empty string explicitly clears a previously-set value.
$keys = ['senderName', 'senderEmail', 'replyToEmail', 'replyToName', 'message', 'subject'];
foreach ($keys as $key) {
if (!\is_null(${$key})) {
+112 -4
View File
@@ -294,6 +294,7 @@ trait SMTPBase
public function testUpdateSMTPEmptySenderName(): void
{
// Empty sender name is valid — PHPMailer accepts '' as display name.
$response = $this->updateSMTP(
senderName: '',
senderEmail: 'sender@example.com',
@@ -301,11 +302,17 @@ trait SMTPBase
port: 1025,
);
$this->assertSame(400, $response['headers']['status-code']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertSame('', $response['body']['smtpSenderName']);
// Cleanup
$this->updateSMTP(enabled: false);
}
public function testUpdateSMTPEmptySenderEmail(): void
{
// Empty senderEmail clears the stored value; connection test is skipped when
// there is no valid From address, so this is accepted even without enabled=false.
$response = $this->updateSMTP(
senderName: 'Test',
senderEmail: '',
@@ -313,7 +320,11 @@ trait SMTPBase
port: 1025,
);
$this->assertSame(400, $response['headers']['status-code']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertSame('', $response['body']['smtpSenderEmail']);
// Cleanup
$this->updateSMTP(enabled: false);
}
public function testUpdateSMTPEmptyHost(): void
@@ -353,6 +364,59 @@ trait SMTPBase
$this->assertSame(400, $response['headers']['status-code']);
}
public function testUpdateSMTPReplyToEmailCanBeCleared(): void
{
// Step 1: Set a custom replyToEmail.
$set = $this->updateSMTP(
senderName: 'Test Sender',
senderEmail: 'sender@example.com',
host: 'maildev',
port: 1025,
replyToEmail: 'reply@example.com',
);
$this->assertSame(200, $set['headers']['status-code']);
$this->assertSame('reply@example.com', $set['body']['smtpReplyToEmail']);
// Step 2: Clear it with an empty string.
$clear = $this->updateSMTP(replyToEmail: '');
$this->assertSame(200, $clear['headers']['status-code']);
$this->assertSame('', $clear['body']['smtpReplyToEmail']);
// Step 3: Verify the cleared value persists.
$verify = $this->updateSMTP();
$this->assertSame(200, $verify['headers']['status-code']);
$this->assertSame('', $verify['body']['smtpReplyToEmail']);
// Cleanup
$this->updateSMTP(enabled: false);
}
public function testUpdateSMTPSenderEmailCanBeClearedWhenDisabled(): void
{
// Step 1: Configure SMTP with a sender email, then disable it.
$this->updateSMTP(
senderName: 'Test Sender',
senderEmail: 'sender@example.com',
host: 'maildev',
port: 1025,
enabled: false,
);
// Step 2: Clear senderEmail while keeping SMTP disabled.
// enabled=false skips the PHPMailer connection check so empty senderEmail is valid.
$clear = $this->updateSMTP(
senderEmail: '',
enabled: false,
);
$this->assertSame(200, $clear['headers']['status-code']);
$this->assertSame('', $clear['body']['smtpSenderEmail']);
// Step 3: Verify the cleared value persists.
$verify = $this->updateSMTP(enabled: false);
$this->assertSame(200, $verify['headers']['status-code']);
$this->assertSame('', $verify['body']['smtpSenderEmail']);
}
public function testUpdateSMTPInvalidSecure(): void
{
$response = $this->updateSMTP(
@@ -461,6 +525,7 @@ trait SMTPBase
public function testUpdateSMTPUsernameEmpty(): void
{
// Empty string clears a previously-set username (no-auth SMTP).
$response = $this->updateSMTP(
senderName: 'Test',
senderEmail: 'sender@example.com',
@@ -469,7 +534,11 @@ trait SMTPBase
username: '',
);
$this->assertSame(400, $response['headers']['status-code']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertSame('', $response['body']['smtpUsername']);
// Cleanup
$this->updateSMTP(enabled: false);
}
public function testUpdateSMTPPasswordMinLength(): void
@@ -524,6 +593,7 @@ trait SMTPBase
public function testUpdateSMTPPasswordEmpty(): void
{
// Empty string clears a previously-set password (no-auth SMTP).
$response = $this->updateSMTP(
senderName: 'Test',
senderEmail: 'sender@example.com',
@@ -532,7 +602,45 @@ trait SMTPBase
password: '',
);
$this->assertSame(400, $response['headers']['status-code']);
$this->assertSame(200, $response['headers']['status-code']);
// smtpPassword is write-only and never echoed back.
$this->assertSame('', $response['body']['smtpPassword']);
// Cleanup
$this->updateSMTP(enabled: false);
}
public function testUpdateSMTPCredentialsCanBeCleared(): void
{
// Step 1: Set username and password.
$set = $this->updateSMTP(
senderName: 'Test Sender',
senderEmail: 'sender@example.com',
host: 'maildev',
port: 1025,
username: 'myuser',
password: 'mypassword',
);
$this->assertSame(200, $set['headers']['status-code']);
$this->assertSame('myuser', $set['body']['smtpUsername']);
// Step 2: Clear both credentials by passing empty strings.
$clear = $this->updateSMTP(
username: '',
password: '',
);
$this->assertSame(200, $clear['headers']['status-code']);
$this->assertSame('', $clear['body']['smtpUsername']);
// smtpPassword is write-only and never echoed back regardless.
$this->assertSame('', $clear['body']['smtpPassword']);
// Step 3: Verify the cleared username persists (a no-params PATCH must not restore it).
$verify = $this->updateSMTP();
$this->assertSame(200, $verify['headers']['status-code']);
$this->assertSame('', $verify['body']['smtpUsername']);
// Cleanup
$this->updateSMTP(enabled: false);
}
public function testUpdateSMTPWithoutSecure(): void
@@ -548,6 +548,59 @@ trait TemplatesBase
$this->assertSame(401, $response['headers']['status-code']);
}
public function testUpdateEmailTemplateSenderFieldsCanBeCleared(): void
{
$this->ensureSMTPEnabled();
// Step 1: Set a custom en verification template with sender and reply-to fields.
$first = $this->updateEmailTemplate(
templateId: 'verification',
locale: 'en',
subject: 'Verify your email',
message: 'Please verify: {{url}}',
senderName: 'Custom Sender',
senderEmail: 'custom-sender@appwrite.io',
replyToName: 'Custom Reply',
replyToEmail: 'custom-reply@appwrite.io',
);
$this->assertSame(200, $first['headers']['status-code']);
$this->assertSame('Custom Sender', $first['body']['senderName']);
$this->assertSame('custom-sender@appwrite.io', $first['body']['senderEmail']);
$this->assertSame('Custom Reply', $first['body']['replyToName']);
$this->assertSame('custom-reply@appwrite.io', $first['body']['replyToEmail']);
// Step 2: GET en verification template and ensure it reflects the custom values.
$get = $this->getEmailTemplate('verification', 'en');
$this->assertSame(200, $get['headers']['status-code']);
$this->assertSame('Custom Sender', $get['body']['senderName']);
$this->assertSame('custom-sender@appwrite.io', $get['body']['senderEmail']);
$this->assertSame('Custom Reply', $get['body']['replyToName']);
$this->assertSame('custom-reply@appwrite.io', $get['body']['replyToEmail']);
// Step 3: Update the same template, clearing sender and reply-to fields to empty strings.
$clear = $this->updateEmailTemplate(
templateId: 'verification',
locale: 'en',
senderName: '',
senderEmail: '',
replyToName: '',
replyToEmail: '',
);
$this->assertSame(200, $clear['headers']['status-code']);
$this->assertSame('', $clear['body']['senderName']);
$this->assertSame('', $clear['body']['senderEmail']);
$this->assertSame('', $clear['body']['replyToName']);
$this->assertSame('', $clear['body']['replyToEmail']);
// Step 4: GET again to confirm the cleared values persist.
$getAfter = $this->getEmailTemplate('verification', 'en');
$this->assertSame(200, $getAfter['headers']['status-code']);
$this->assertSame('', $getAfter['body']['senderName']);
$this->assertSame('', $getAfter['body']['senderEmail']);
$this->assertSame('', $getAfter['body']['replyToName']);
$this->assertSame('', $getAfter['body']['replyToEmail']);
}
public function testUpdateEmailTemplateBlockedWhenSMTPDisabled(): void
{
// Custom templates only make sense alongside a custom SMTP configuration.