diff --git a/composer.lock b/composer.lock index 1d71d75447..a954399162 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Update.php index 97e723f52c..ef2e478d96 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Update.php @@ -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})) { diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Templates/Email/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Templates/Email/Update.php index ef93abf683..c9c64ebdfa 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Templates/Email/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Templates/Email/Update.php @@ -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})) { diff --git a/tests/e2e/Services/Project/SMTPBase.php b/tests/e2e/Services/Project/SMTPBase.php index 748fb3502b..19355bdce0 100644 --- a/tests/e2e/Services/Project/SMTPBase.php +++ b/tests/e2e/Services/Project/SMTPBase.php @@ -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 diff --git a/tests/e2e/Services/Project/TemplatesBase.php b/tests/e2e/Services/Project/TemplatesBase.php index 11dc6dc80b..9e329dfc3b 100644 --- a/tests/e2e/Services/Project/TemplatesBase.php +++ b/tests/e2e/Services/Project/TemplatesBase.php @@ -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.