mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
fix: allow empty string to clear optional SMTP and email template fields
This commit is contained in:
@@ -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})) {
|
||||
@@ -119,8 +120,9 @@ class Update extends Action
|
||||
// Validate SMTP credentials
|
||||
// Validate when the caller is explicitly enabling or hasn't expressed a preference
|
||||
// (so a credentials-only PATCH can auto-enable). Skip only when the caller is
|
||||
// explicitly keeping/turning SMTP off.
|
||||
if (\is_null($enabled) || $enabled === true) {
|
||||
// explicitly keeping/turning SMTP off, or when senderEmail is not yet configured
|
||||
// (no valid From address means no connection test is possible).
|
||||
if ((\is_null($enabled) || $enabled === true) && !empty($smtp['senderEmail'] ?? '')) {
|
||||
$mail = new PHPMailer(true);
|
||||
$mail->isSMTP();
|
||||
|
||||
|
||||
@@ -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})) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user