diff --git a/app/config/errors.php b/app/config/errors.php index 42e58cf4f6..a93c875cc5 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -98,6 +98,11 @@ return [ 'description' => 'Usage stats is not configured. Please check the value of the _APP_USAGE_STATS environment variable of your Appwrite server.', 'code' => 501, ], + Exception::GENERAL_NOT_IMPLEMENTED => [ + 'name' => Exception::GENERAL_NOT_IMPLEMENTED, + 'description' => 'This method was not fully implemented yet. If you believe this is a mistake, please upgrade your Appwrite server version.', + 'code' => 405, + ], /** User Errors */ Exception::USER_COUNT_EXCEEDED => [ @@ -666,9 +671,14 @@ return [ 'description' => 'Provided SMTP config is invalid. Please check the configured values and try again.', 'code' => 400, ], + Exception::PROJECT_SMTP_CONFIG_NOT_FOUND => [ + 'name' => Exception::PROJECT_SMTP_CONFIG_NOT_FOUND, + 'description' => 'SMTP configuration on project is missing. Please configure a custom SMTP server to enable custom email templates.', + 'code' => 404, + ], Exception::PROJECT_TEMPLATE_DEFAULT_DELETION => [ 'name' => Exception::PROJECT_TEMPLATE_DEFAULT_DELETION, - 'description' => 'The default template for the project cannot be deleted.', + 'description' => 'You can\'t delete default template. If you are trying to reset your template changes, you can ignore this error as it\'s already been reset.', 'code' => 401, ], Exception::WEBHOOK_NOT_FOUND => [ diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 5d83ea336a..9fd8f10f09 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1098,9 +1098,20 @@ App::post('/v1/account/sessions/magic-url') $smtpEnabled = $project->getAttribute('smtp', [])['enabled'] ?? false; $customTemplate = $project->getAttribute('templates', [])['email.magicSession-' . $locale->default] ?? []; if ($smtpEnabled && !empty($customTemplate)) { - $body = $customTemplate['message'] ?? $body; + $body = Template::fromString($customTemplate['message'] ?? ''); $subject = $customTemplate['subject'] ?? $subject; $from = $customTemplate['senderName'] ?? $from; + + $smtp = $project->getAttribute('smtp', []); + $mails + ->setSmtpHost($smtp['host'] ?? '') + ->setSmtpPort($smtp['port'] ?? '') + ->setSmtpUsername($smtp['username'] ?? '') + ->setSmtpPassword($smtp['password'] ?? '') + ->setSmtpSecure($smtp['secure'] ?? '') + ->setSmtpReplyTo($customTemplate['replyTo'] ?? '') + ->setSmtpSenderEmail($customTemplate['senderEmail'] ?? '') + ->setSmtpSenderName($customTemplate['senderName'] ?? ''); } $body @@ -2514,9 +2525,20 @@ App::post('/v1/account/recovery') $smtpEnabled = $project->getAttribute('smtp', [])['enabled'] ?? false; $customTemplate = $project->getAttribute('templates', [])['email.recovery-' . $locale->default] ?? []; if ($smtpEnabled && !empty($customTemplate)) { - $body = $customTemplate['message'] ?? $body; + $body = Template::fromString($customTemplate['message'] ?? ''); $subject = $customTemplate['subject'] ?? $subject; $from = $customTemplate['senderName'] ?? $from; + + $smtp = $project->getAttribute('smtp', []); + $mails + ->setSmtpHost($smtp['host'] ?? '') + ->setSmtpPort($smtp['port'] ?? '') + ->setSmtpUsername($smtp['username'] ?? '') + ->setSmtpPassword($smtp['password'] ?? '') + ->setSmtpSecure($smtp['secure'] ?? '') + ->setSmtpReplyTo($customTemplate['replyTo'] ?? '') + ->setSmtpSenderEmail($customTemplate['senderEmail'] ?? '') + ->setSmtpSenderName($customTemplate['senderName'] ?? ''); } $body @@ -2724,9 +2746,20 @@ App::post('/v1/account/verification') $smtpEnabled = $project->getAttribute('smtp', [])['enabled'] ?? false; $customTemplate = $project->getAttribute('templates', [])['email.verification-' . $locale->default] ?? []; if ($smtpEnabled && !empty($customTemplate)) { - $body = $customTemplate['message'] ?? $body; + $body = Template::fromString($customTemplate['message'] ?? ''); $subject = $customTemplate['subject'] ?? $subject; $from = $customTemplate['senderName'] ?? $from; + + $smtp = $project->getAttribute('smtp', []); + $mails + ->setSmtpHost($smtp['host'] ?? '') + ->setSmtpPort($smtp['port'] ?? '') + ->setSmtpUsername($smtp['username'] ?? '') + ->setSmtpPassword($smtp['password'] ?? '') + ->setSmtpSecure($smtp['secure'] ?? '') + ->setSmtpReplyTo($customTemplate['replyTo'] ?? '') + ->setSmtpSenderEmail($customTemplate['senderEmail'] ?? '') + ->setSmtpSenderName($customTemplate['senderName'] ?? ''); } $body diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 7cb5ea8394..a5487f57ec 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -1556,10 +1556,10 @@ App::patch('/v1/projects/:projectId/smtp') $valid = $mail->SmtpConnect(); if (!$valid) { - throw new Exception(Exception::GENERAL_SMTP_DISABLED); + throw new Exception('Connection is not valid.'); } } catch (Throwable $error) { - throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'Could not connect to SMTP server: ' . $error->getMessage()); + throw new Exception(Exception::PROJECT_SMTP_CONFIG_INVALID, 'Could not connect to SMTP server: ' . $error->getMessage()); } $smtp = [ @@ -1594,6 +1594,8 @@ App::get('/v1/projects/:projectId/templates/sms/:type/:locale') ->inject('dbForConsole') ->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) { + throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED); + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1691,12 +1693,16 @@ App::patch('/v1/projects/:projectId/templates/sms/:type/:locale') ->inject('dbForConsole') ->action(function (string $projectId, string $type, string $locale, string $message, Response $response, Database $dbForConsole) { + throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED); + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } + // TODO: Ensure SMS is enabled on project + $templates = $project->getAttribute('templates', []); $templates['sms.' . $type . '-' . $locale] = [ 'message' => $message @@ -1739,6 +1745,11 @@ App::patch('/v1/projects/:projectId/templates/email/:type/:locale') throw new Exception(Exception::PROJECT_NOT_FOUND); } + $smtpEnabled = $project->getAttribute('smtp', [])['enabled'] ?? false; + if(!$smtpEnabled) { + throw new Exception(Exception::PROJECT_SMTP_CONFIG_NOT_FOUND); + } + $templates = $project->getAttribute('templates', []); $templates['email.' . $type . '-' . $locale] = [ 'senderName' => $senderName, @@ -1778,6 +1789,8 @@ App::delete('/v1/projects/:projectId/templates/sms/:type/:locale') ->inject('dbForConsole') ->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) { + throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED); + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index f0a70435fc..dfa8cb0b7a 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -550,9 +550,20 @@ App::post('/v1/teams/:teamId/memberships') $smtpEnabled = $project->getAttribute('smtp', [])['enabled'] ?? false; $customTemplate = $project->getAttribute('templates', [])['email.invitation-' . $locale->default] ?? []; if ($smtpEnabled && !empty($customTemplate)) { - $body = $customTemplate['message']; - $subject = $customTemplate['subject']; - $from = $customTemplate['senderName']; + $body = Template::fromString($customTemplate['message'] ?? ''); + $subject = $customTemplate['subject'] ?? $subject; + $from = $customTemplate['senderName'] ?? $from; + + $smtp = $project->getAttribute('smtp', []); + $mails + ->setSmtpHost($smtp['host'] ?? '') + ->setSmtpPort($smtp['port'] ?? '') + ->setSmtpUsername($smtp['username'] ?? '') + ->setSmtpPassword($smtp['password'] ?? '') + ->setSmtpSecure($smtp['secure'] ?? '') + ->setSmtpReplyTo($customTemplate['replyTo'] ?? '') + ->setSmtpSenderEmail($customTemplate['senderEmail'] ?? '') + ->setSmtpSenderName($customTemplate['senderName'] ?? ''); } $body->setParam('{{owner}}', $user->getAttribute('name')); diff --git a/app/controllers/web/home.php b/app/controllers/web/home.php index e90f3ec25b..27b2614c37 100644 --- a/app/controllers/web/home.php +++ b/app/controllers/web/home.php @@ -6,7 +6,7 @@ use Utopia\Config\Config; App::get('/versions') ->desc('Get Version') - ->groups(['home']) + ->groups(['home', 'web']) ->label('scope', 'public') ->inject('response') ->action(function (Response $response) { diff --git a/app/workers/mails.php b/app/workers/mails.php index e1ce8307ac..baff259495 100644 --- a/app/workers/mails.php +++ b/app/workers/mails.php @@ -48,12 +48,7 @@ class MailsV1 extends Worker $mail->clearAttachments(); $mail->clearBCCs(); $mail->clearCCs(); - - $mail->setFrom(App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM), (empty($from) ? \urldecode(App::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server')) : $from)); $mail->addAddress($recipient, $name); - if (isset($smtp['replyTo'])) { - $mail->addReplyTo($smtp['replyTo']); - } $mail->Subject = $subject; $mail->Body = $body; $mail->AltBody = \strip_tags($body); diff --git a/composer.json b/composer.json index 67e9c73374..4fee99a5d5 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,7 @@ "utopia-php/cache": "0.8.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.42.*", + "utopia-php/database": "dev-feat-document-clone as 0.42.99", "utopia-php/domains": "0.3.*", "utopia-php/dsn": "0.1.*", "utopia-php/framework": "0.30.0", diff --git a/composer.lock b/composer.lock index 5d0ee7be32..e429b7d771 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": "7cb5852653a858c1846543914c20f87f", + "content-hash": "f89bfdb9813378f68d77cb503729b5d6", "packages": [ { "name": "adhocore/jwt", @@ -2220,16 +2220,16 @@ }, { "name": "utopia-php/database", - "version": "0.42.3", + "version": "dev-feat-document-clone", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "ab0e2f8ad46884f69b354cd8ee84a1a75fee26d1" + "reference": "0d6c9de4e2ca43feb26a60debe3254f3194db019" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/ab0e2f8ad46884f69b354cd8ee84a1a75fee26d1", - "reference": "ab0e2f8ad46884f69b354cd8ee84a1a75fee26d1", + "url": "https://api.github.com/repos/utopia-php/database/zipball/0d6c9de4e2ca43feb26a60debe3254f3194db019", + "reference": "0d6c9de4e2ca43feb26a60debe3254f3194db019", "shasum": "" }, "require": { @@ -2271,9 +2271,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.42.3" + "source": "https://github.com/utopia-php/database/tree/feat-document-clone" }, - "time": "2023-08-22T02:15:28+00:00" + "time": "2023-08-24T14:21:25+00:00" }, { "name": "utopia-php/domains", @@ -6070,9 +6070,18 @@ "time": "2023-07-26T07:16:09+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "utopia-php/database", + "version": "dev-feat-document-clone", + "alias": "0.42.99", + "alias_normalized": "0.42.99.0" + } + ], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "utopia-php/database": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/src/Appwrite/Event/Mail.php b/src/Appwrite/Event/Mail.php index 37b42704c5..dfec4cd756 100644 --- a/src/Appwrite/Event/Mail.php +++ b/src/Appwrite/Event/Mail.php @@ -166,7 +166,7 @@ class Mail extends Event */ public function setSmtpUsername(string $username): self { - $this->smtp['username']; + $this->smtp['username'] = $username; return $this; } @@ -178,7 +178,19 @@ class Mail extends Event */ public function setSmtpPassword(string $password): self { - $this->smtp['password']; + $this->smtp['password'] = $password; + return $this; + } + + /** + * Set SMTP secure + * + * @param string $password + * @return self + */ + public function setSmtpSecure(string $secure): self + { + $this->smtp['secure'] = $secure; return $this; } @@ -194,6 +206,18 @@ class Mail extends Event return $this; } + /** + * Set SMTP sender name + * + * @param string $senderName + * @return self + */ + public function setSmtpSenderName(string $senderName): self + { + $this->smtp['senderName'] = $senderName; + return $this; + } + /** * Set SMTP reply to * @@ -246,6 +270,16 @@ class Mail extends Event return $this->smtp['password'] ?? ''; } + /** + * Get SMTP secure + * + * @return string + */ + public function getSmtpSecure(): string + { + return $this->smtp['secure'] ?? ''; + } + /** * Get SMTP sender email * @@ -256,6 +290,16 @@ class Mail extends Event return $this->smtp['senderEmail'] ?? ''; } + /** + * Get SMTP sender name + * + * @return string + */ + public function getSmtpSenderName(): string + { + return $this->smtp['senderName'] ?? ''; + } + /** * Get SMTP reply to * diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 5d0e0b400b..4bb7d962e9 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -54,6 +54,7 @@ class Exception extends \Exception public const GENERAL_PROTOCOL_UNSUPPORTED = 'general_protocol_unsupported'; public const GENERAL_CODES_DISABLED = 'general_codes_disabled'; public const GENERAL_USAGE_DISABLED = 'general_usage_disabled'; + public const GENERAL_NOT_IMPLEMENTED = 'general_not_implemented'; /** Users */ public const USER_COUNT_EXCEEDED = 'user_count_exceeded'; @@ -188,6 +189,7 @@ class Exception extends \Exception public const PROJECT_KEY_EXPIRED = 'project_key_expired'; public const PROJECT_SMTP_CONFIG_INVALID = 'project_smtp_config_invalid'; + public const PROJECT_SMTP_CONFIG_NOT_FOUND = 'project_smtp_config_not_found'; public const PROJECT_TEMPLATE_DEFAULT_DELETION = 'project_template_default_deletion'; diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 7cc15647f8..c5aece71bb 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -480,7 +480,7 @@ class Response extends SwooleResponse */ public function dynamic(Document $document, string $model): void { - $output = $this->output(new Document($document->getArrayCopy()), $model); + $output = $this->output(clone $document, $model); // If filter is set, parse the output if (self::hasFilter()) { @@ -521,11 +521,11 @@ class Response extends SwooleResponse */ public function output(Document $document, string $model): array { - $data = new Document($document->getArrayCopy()); + $data = clone $document; $model = $this->getModel($model); $output = []; - $data = $model->filter($document); + $data = $model->filter($data); if ($model->isAny()) { $this->payload = $data->getArrayCopy(); @@ -534,7 +534,7 @@ class Response extends SwooleResponse } foreach ($model->getRules() as $key => $rule) { - if (!$document->isSet($key) && $rule['required']) { // do not set attribute in response if not required + if (!$data->isSet($key) && $rule['required']) { // do not set attribute in response if not required if (\array_key_exists('default', $rule)) { $data->setAttribute($key, $rule['default']); } else { @@ -543,11 +543,12 @@ class Response extends SwooleResponse } if ($rule['array']) { - if (!is_array($document[$key])) { + if (!is_array($data[$key])) { + \var_dump($data); throw new Exception($key . ' must be an array of type ' . $rule['type']); } - foreach ($document[$key] as $index => $item) { + foreach ($data[$key] as $index => $item) { if ($item instanceof Document) { if (\is_array($rule['type'])) { foreach ($rule['type'] as $type) { @@ -575,7 +576,7 @@ class Response extends SwooleResponse } } } else { - if ($document[$key] instanceof Document) { + if ($data[$key] instanceof Document) { $data[$key] = $this->output($data[$key], $rule['type']); } } diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 0272bd2d68..26f584ec5e 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -640,40 +640,41 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals('en-us', $response['body']['locale']); $this->assertEquals('Please verify your email {{url}}', $response['body']['message']); - /** Get Default SMS Template */ - $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/templates/sms/verification/en-us', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); + // Temporary disabled until implemented + // /** Get Default SMS Template */ + // $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/templates/sms/verification/en-us', array_merge([ + // 'content-type' => 'application/json', + // 'x-appwrite-project' => $this->getProject()['$id'], + // ], $this->getHeaders())); - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('verification', $response['body']['type']); - $this->assertEquals('en-us', $response['body']['locale']); - $this->assertEquals('{{token}}', $response['body']['message']); + // $this->assertEquals(200, $response['headers']['status-code']); + // $this->assertEquals('verification', $response['body']['type']); + // $this->assertEquals('en-us', $response['body']['locale']); + // $this->assertEquals('{{token}}', $response['body']['message']); - /** Update SMS template */ - $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/templates/sms/verification/en-us', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'message' => 'Please verify your email {{token}}', - ]); + // /** Update SMS template */ + // $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/templates/sms/verification/en-us', array_merge([ + // 'content-type' => 'application/json', + // 'x-appwrite-project' => $this->getProject()['$id'], + // ], $this->getHeaders()), [ + // 'message' => 'Please verify your email {{token}}', + // ]); - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('verification', $response['body']['type']); - $this->assertEquals('en-us', $response['body']['locale']); - $this->assertEquals('Please verify your email {{token}}', $response['body']['message']); + // $this->assertEquals(200, $response['headers']['status-code']); + // $this->assertEquals('verification', $response['body']['type']); + // $this->assertEquals('en-us', $response['body']['locale']); + // $this->assertEquals('Please verify your email {{token}}', $response['body']['message']); - /** Get Updated SMS Template */ - $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/templates/sms/verification/en-us', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); + // /** Get Updated SMS Template */ + // $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/templates/sms/verification/en-us', array_merge([ + // 'content-type' => 'application/json', + // 'x-appwrite-project' => $this->getProject()['$id'], + // ], $this->getHeaders())); - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('verification', $response['body']['type']); - $this->assertEquals('en-us', $response['body']['locale']); - $this->assertEquals('Please verify your email {{token}}', $response['body']['message']); + // $this->assertEquals(200, $response['headers']['status-code']); + // $this->assertEquals('verification', $response['body']['type']); + // $this->assertEquals('en-us', $response['body']['locale']); + // $this->assertEquals('Please verify your email {{token}}', $response['body']['message']); return $data; }