diff --git a/app/app.php b/app/app.php index 35f208b335..dcab392a73 100644 --- a/app/app.php +++ b/app/app.php @@ -27,6 +27,7 @@ $services = include __DIR__.'/config/services.php'; // List of services $webhook = new Event('v1-webhooks', 'WebhooksV1'); $audit = new Event('v1-audits', 'AuditsV1'); $usage = new Event('v1-usage', 'UsageV1'); +$mail = new Event('v1-mails', 'MailsV1'); $deletes = new Event('v1-deletes', 'DeletesV1'); /** @@ -53,7 +54,7 @@ $clients = array_unique(array_merge($clientsConsole, array_map(function ($node) return false; })))); -$utopia->init(function () use ($utopia, $request, $response, &$user, $project, $console, $roles, $webhook, $audit, $usage, $clients) { +$utopia->init(function () use ($utopia, $request, $response, &$user, $project, $console, $roles, $webhook, $mail, $audit, $usage, $clients) { $route = $utopia->match($request); diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 282a5d9a22..17369b0c36 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1,7 +1,7 @@ post('/v1/account') ->param('password', '', function () { return new Password(); }, 'User password. Must be between 6 to 32 chars.') ->param('name', '', function () { return new Text(100); }, 'User name.', true) ->action( - function ($email, $password, $name) use ($register, $request, $response, $audit, $projectDB, $project, $webhook, $oauth2Keys) { + function ($email, $password, $name) use ($request, $response, $audit, $projectDB, $project, $webhook, $oauth2Keys) { if ('console' === $project->getId()) { $whitlistEmails = $project->getAttribute('authWhitelistEmails'); $whitlistIPs = $project->getAttribute('authWhitelistIPs'); @@ -1053,7 +1053,7 @@ $utopia->post('/v1/account/recovery') ->param('email', '', function () { return new Email(); }, 'User email.') ->param('url', '', function () use ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the recovery email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.') ->action( - function ($email, $url) use ($request, $response, $projectDB, $register, $audit, $project) { + function ($email, $url) use ($request, $response, $projectDB, $mail, $audit, $project) { $profile = $projectDB->getCollection([ // Get user by email address 'limit' => 1, 'first' => true, @@ -1106,19 +1106,14 @@ $utopia->post('/v1/account/recovery') ->setParam('{{redirect}}', $url) ; - $mail = $register->get('smtp'); /* @var $mail \PHPMailer\PHPMailer\PHPMailer */ - - $mail->addAddress($profile->getAttribute('email', ''), $profile->getAttribute('name', '')); - - $mail->Subject = Locale::getText('account.emails.recovery.title'); - $mail->Body = $body->render(); - $mail->AltBody = strip_tags($body->render()); - - try { - $mail->send(); - } catch (\Exception $error) { - throw new Exception('Error sending mail: ' . $error->getMessage(), 500); - } + $mail + ->setParam('event', 'account.recovery.create') + ->setParam('recipient', $profile->getAttribute('email', '')) + ->setParam('name', $profile->getAttribute('name', '')) + ->setParam('subject', Locale::getText('account.emails.recovery.title')) + ->setParam('body', $body->render()) + ->trigger(); + ; $audit ->setParam('userId', $profile->getId()) @@ -1214,7 +1209,7 @@ $utopia->post('/v1/account/verification') ->label('abuse-key', 'url:{url},email:{param-email}') ->param('url', '', function () use ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the verification email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.') // TODO add built-in confirm page ->action( - function ($url) use ($request, $response, $register, $user, $project, $projectDB, $audit) { + function ($url) use ($request, $response, $mail, $user, $project, $projectDB, $audit) { $verificationSecret = Auth::tokenGenerator(); $verification = new Document([ @@ -1255,19 +1250,14 @@ $utopia->post('/v1/account/verification') ->setParam('{{redirect}}', $url) ; - $mail = $register->get('smtp'); /* @var $mail \PHPMailer\PHPMailer\PHPMailer */ - - $mail->addAddress($user->getAttribute('email'), $user->getAttribute('name')); - - $mail->Subject = Locale::getText('account.emails.verification.title'); - $mail->Body = $body->render(); - $mail->AltBody = strip_tags($body->render()); - - try { - $mail->send(); - } catch (\Exception $error) { - throw new Exception('Problem sending mail: ' . $error->getMessage(), 500); - } + $mail + ->setParam('event', 'account.verification.create') + ->setParam('recipient', $user->getAttribute('email')) + ->setParam('name', $user->getAttribute('name')) + ->setParam('subject', Locale::getText('account.emails.verification.title')) + ->setParam('body', $body->render()) + ->trigger() + ; $audit ->setParam('userId', $user->getId()) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index ded2445faa..54ff361a54 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -1,6 +1,6 @@ post('/v1/teams/:teamId/memberships') ->param('roles', [], function () { return new ArrayList(new Text(128)); }, 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](/docs/permissions).') ->param('url', '', function () use ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the invitation email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.') // TODO add our own built-in confirm page ->action( - function ($teamId, $email, $name, $roles, $url) use ($response, $register, $project, $user, $audit, $projectDB, $mode) { + function ($teamId, $email, $name, $roles, $url) use ($response, $mail, $project, $user, $audit, $projectDB, $mode) { $name = (empty($name)) ? $email : $name; $team = $projectDB->getDocument($teamId); @@ -332,20 +332,15 @@ $utopia->post('/v1/teams/:teamId/memberships') ->setParam('{{redirect}}', $url) ; - $mail = $register->get('smtp'); /* @var $mail \PHPMailer\PHPMailer\PHPMailer */ - - $mail->addAddress($email, $name); - - $mail->Subject = sprintf(Locale::getText('account.emails.invitation.title'), $team->getAttribute('name', '[TEAM-NAME]'), $project->getAttribute('name', ['[APP-NAME]'])); - $mail->Body = $body->render(); - $mail->AltBody = strip_tags($body->render()); - - try { - if(APP_MODE_ADMIN !== $mode) { // No need in comfirmation when in admin mode - $mail->send(); - } - } catch (\Exception $error) { - throw new Exception('Error sending mail: ' . $error->getMessage(), 500); + if(APP_MODE_ADMIN !== $mode) { // No need in comfirmation when in admin mode + $mail + ->setParam('event', 'teams.membership.create') + ->setParam('recipient', $email) + ->setParam('name', $name) + ->setParam('subject', sprintf(Locale::getText('account.emails.invitation.title'), $team->getAttribute('name', '[TEAM-NAME]'), $project->getAttribute('name', ['[APP-NAME]']))) + ->setParam('body', $body->render()) + ->trigger(); + ; } $audit diff --git a/app/workers/mails.php b/app/workers/mails.php new file mode 100644 index 0000000000..c46fda89dc --- /dev/null +++ b/app/workers/mails.php @@ -0,0 +1,48 @@ +args['event']; + $recipient = $this->args['recipient']; + $name = $this->args['name']; + $subject = $this->args['subject']; + $body = $this->args['body']; + + $mail = $register->get('smtp'); /* @var $mail \PHPMailer\PHPMailer\PHPMailer */ + + $mail->addAddress($recipient, $name); + $mail->Subject = $subject; + $mail->Body = $body; + $mail->AltBody = strip_tags($body); + + try { + $mail->send(); + } catch (\Exception $error) { + throw new Exception('Error sending mail: ' . $error->getMessage(), 500); + } + } + + public function tearDown() + { + // ... Remove environment for this job + } +} diff --git a/docker/supervisord.conf b/docker/supervisord.conf index 747e4d590d..ed94f03311 100644 --- a/docker/supervisord.conf +++ b/docker/supervisord.conf @@ -65,6 +65,23 @@ stdout_logfile_maxbytes=5000000 stderr_logfile=/dev/stderr stderr_logfile_maxbytes = 0 +[program:v1-mails] +command=php /usr/share/nginx/html/vendor/bin/resque +autostart=true +autorestart=true +priority=10 +environment=QUEUE='v1-mails',APP_INCLUDE='/usr/share/nginx/html/app/workers/mails.php',REDIS_BACKEND='%(ENV__APP_REDIS_HOST)s:%(ENV__APP_REDIS_PORT)s' +stdout_events_enabled=true +stderr_events_enabled=true +stopsignal=QUIT +startretries=10 +;stdout_logfile=/dev/stdout +;stdout_logfile_maxbytes=0 +stdout_logfile=/var/log/appwrite.log +stdout_logfile_maxbytes=5000000 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes = 0 + [program:v1-audits] command=php /usr/share/nginx/html/vendor/bin/resque autostart=true diff --git a/tests/e2e/Scopes/Scope.php b/tests/e2e/Scopes/Scope.php index f535d43633..842e9a34b6 100644 --- a/tests/e2e/Scopes/Scope.php +++ b/tests/e2e/Scopes/Scope.php @@ -33,7 +33,7 @@ abstract class Scope extends TestCase protected function getLastEmail():array { - sleep(3); + sleep(10); $emails = json_decode(file_get_contents('http://maildev/email'), true); if($emails && is_array($emails)) { diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php index 4e6d01d71e..794f6f2577 100644 --- a/tests/e2e/Services/Account/AccountBase.php +++ b/tests/e2e/Services/Account/AccountBase.php @@ -658,7 +658,7 @@ trait AccountBase $this->assertNotEmpty($response['body']['$id']); $this->assertEquals(2, $response['body']['type']); $this->assertIsNumeric($response['body']['expire']); - + $lastEmail = $this->getLastEmail(); $this->assertEquals($email, $lastEmail['to'][0]['address']); @@ -950,7 +950,7 @@ trait AccountBase $this->assertNotEmpty($response['body']['$id']); $this->assertEquals(3, $response['body']['type']); $this->assertIsNumeric($response['body']['expire']); - + $lastEmail = $this->getLastEmail(); $this->assertEquals($email, $lastEmail['to'][0]['address']);