mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge pull request #10198 from hmacr/1we17-preview-for-emails
Preview text for emails
This commit is contained in:
@@ -120,6 +120,11 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div style="display: none; overflow: hidden; max-height: 0; max-width: 0; opacity: 0; line-height: 1px;">
|
||||
{{preview}}
|
||||
<div>{{previewWhitespace}}</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<table>
|
||||
<tr>
|
||||
|
||||
@@ -121,6 +121,11 @@
|
||||
|
||||
<body style="direction: {{direction}}">
|
||||
|
||||
<div style="display: none; overflow: hidden; max-height: 0; max-width: 0; opacity: 0; line-height: 1px;">
|
||||
{{preview}}
|
||||
<div>{{previewWhitespace}}</div>
|
||||
</div>
|
||||
|
||||
<div style="max-width:650px; word-wrap: break-word; overflow-wrap: break-word;
|
||||
word-break: normal; margin:0 auto;">
|
||||
<table style="margin-top: 32px">
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"settings.direction": "ltr",
|
||||
"emails.sender": "%s Team",
|
||||
"emails.verification.subject": "Account Verification",
|
||||
"emails.verification.preview": "Verify your email to activate your {{project}} account.",
|
||||
"emails.verification.hello": "Hello {{user}},",
|
||||
"emails.verification.body": "Follow this link to verify your email address to your {{b}}{{project}}{{/b}} account.",
|
||||
"emails.verification.footer": "If you didn’t ask to verify this address, you can ignore this message.",
|
||||
@@ -11,6 +12,7 @@
|
||||
"emails.verification.buttonText": "Confirm email address",
|
||||
"emails.verification.signature": "{{project}} team",
|
||||
"emails.magicSession.subject": "{{project}} Login",
|
||||
"emails.magicSession.preview": "Sign in to {{project}} with your secure link. Expires in 1 hour.",
|
||||
"emails.magicSession.hello": "Hello {{user}},",
|
||||
"emails.magicSession.optionButton": "Click the button below to securely sign in to your {{b}}{{project}}{{/b}} account. This link will expire in 1 hour.",
|
||||
"emails.magicSession.buttonText": "Sign in to {{project}}",
|
||||
@@ -20,6 +22,7 @@
|
||||
"emails.magicSession.thanks": "Thanks,",
|
||||
"emails.magicSession.signature": "{{project}} team",
|
||||
"emails.sessionAlert.subject": "Security alert: new session on your {{project}} account",
|
||||
"emails.sessionAlert.preview": "New login detected on {{project}} at {{time}} UTC.",
|
||||
"emails.sessionAlert.hello": "Hello {{user}},",
|
||||
"emails.sessionAlert.body": "A new session has been created on your {{b}}{{project}}{{/b}} account, {{b}}on {{date}}, {{year}} at {{time}} UTC{{/b}}.\nHere are the details of the new session: ",
|
||||
"emails.sessionAlert.listDevice": "Device: {{b}}{{device}}{{/b}}",
|
||||
@@ -29,6 +32,7 @@
|
||||
"emails.sessionAlert.thanks": "Thanks,",
|
||||
"emails.sessionAlert.signature": "{{project}} team",
|
||||
"emails.otpSession.subject": "OTP for {{project}} Login",
|
||||
"emails.otpSession.preview": "Use OTP {{otp}} to sign in to {{project}}. Expires in 15 minutes.",
|
||||
"emails.otpSession.hello": "Hello {{user}},",
|
||||
"emails.otpSession.description": "Enter the following verification code when prompted to securely sign in to your {{b}}{{project}}{{/b}} account. This code will expire in 15 minutes.",
|
||||
"emails.otpSession.clientInfo": "This sign in was requested using {{b}}{{agentClient}}{{/b}} on {{b}}{{agentDevice}}{{/b}} {{b}}{{agentOs}}{{/b}}. If you didn't request the sign in, you can safely ignore this email.",
|
||||
@@ -36,12 +40,14 @@
|
||||
"emails.otpSession.thanks": "Thanks,",
|
||||
"emails.otpSession.signature": "{{project}} team",
|
||||
"emails.mfaChallenge.subject": "Verification Code for {{project}}",
|
||||
"emails.mfaChallenge.preview": "Your {{project}} verification code. Expires in 15 minutes.",
|
||||
"emails.mfaChallenge.hello": "Hello {{user}},",
|
||||
"emails.mfaChallenge.description": "Enter the following verification code to verify your email and activate two-step verification in {{b}}{{project}}{{/b}}. This code will expire in 15 minutes.",
|
||||
"emails.mfaChallenge.clientInfo": "This verification code was requested using {{b}}{{agentClient}}{{/b}} on {{b}}{{agentDevice}}{{/b}} {{b}}{{agentOs}}{{/b}}. If you didn't request the verification code, you can safely ignore this email.",
|
||||
"emails.mfaChallenge.thanks": "Thanks,",
|
||||
"emails.mfaChallenge.signature": "{{project}} team",
|
||||
"emails.recovery.subject": "Password Reset",
|
||||
"emails.recovery.preview": "Reset your {{project}} password using the link.",
|
||||
"emails.recovery.hello": "Hello {{user}},",
|
||||
"emails.recovery.body": "Follow this link to reset your {{b}}{{project}}{{/b}} password.",
|
||||
"emails.recovery.footer": "If you didn't ask to reset your password, you can ignore this message.",
|
||||
@@ -49,6 +55,7 @@
|
||||
"emails.recovery.buttonText": "Reset password",
|
||||
"emails.recovery.signature": "{{project}} team",
|
||||
"emails.invitation.subject": "Invitation to %s Team at %s",
|
||||
"emails.invitation.preview": "{{owner}} invited you to join {{team}} at {{project}}",
|
||||
"emails.invitation.hello": "Hello {{user}},",
|
||||
"emails.invitation.body": "This mail was sent to you because {{b}}{{owner}}{{/b}} wanted to invite you to become a member of the {{b}}{{team}}{{/b}} team at {{b}}{{project}}{{/b}}.",
|
||||
"emails.invitation.footer": "If you are not interested, you can ignore this message.",
|
||||
@@ -56,6 +63,7 @@
|
||||
"emails.invitation.buttonText": "Accept invite to {{team}}",
|
||||
"emails.invitation.signature": "{{project}} team",
|
||||
"emails.certificate.subject": "Certificate failure for %s",
|
||||
"emails.certificate.preview": "Your domain %s certificate generation has failed.",
|
||||
"emails.certificate.hello": "Hello,",
|
||||
"emails.certificate.body": "Certificate for your domain '{{domain}}' could not be generated. This is attempt no. {{attempt}}, and the failure was caused by: {{error}}",
|
||||
"emails.certificate.footer": "Your previous certificate will be valid for 30 days since the first failure. We highly recommend investigating this case, otherwise your domain will end up without a valid SSL communication.",
|
||||
|
||||
@@ -71,6 +71,7 @@ $oauthDefaultFailure = '/console/auth/oauth2/failure';
|
||||
function sendSessionAlert(Locale $locale, Document $user, Document $project, Document $session, Mail $queueForMails)
|
||||
{
|
||||
$subject = $locale->getText("emails.sessionAlert.subject");
|
||||
$preview = $locale->getText("emails.sessionAlert.preview");
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.sessionAlert-' . $locale->default] ?? [];
|
||||
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-session-alert.tpl');
|
||||
@@ -148,6 +149,7 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, Doc
|
||||
|
||||
$queueForMails
|
||||
->setSubject($subject)
|
||||
->setPreview($preview)
|
||||
->setBody($body)
|
||||
->setVariables($emailVariables)
|
||||
->setRecipient($email)
|
||||
@@ -2025,6 +2027,7 @@ App::post('/v1/account/tokens/magic-url')
|
||||
$url = Template::unParseURL($url);
|
||||
|
||||
$subject = $locale->getText("emails.magicSession.subject");
|
||||
$preview = $locale->getText("emails.magicSession.preview");
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.magicSession-' . $locale->default] ?? [];
|
||||
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
@@ -2113,6 +2116,7 @@ App::post('/v1/account/tokens/magic-url')
|
||||
|
||||
$queueForMails
|
||||
->setSubject($subject)
|
||||
->setPreview($preview)
|
||||
->setBody($body)
|
||||
->setVariables($emailVariables)
|
||||
->setRecipient($email)
|
||||
@@ -2254,6 +2258,7 @@ App::post('/v1/account/tokens/email')
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$subject = $locale->getText("emails.otpSession.subject");
|
||||
$preview = $locale->getText("emails.otpSession.preview");
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.otpSession-' . $locale->default] ?? [];
|
||||
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
@@ -2339,6 +2344,7 @@ App::post('/v1/account/tokens/email')
|
||||
|
||||
$queueForMails
|
||||
->setSubject($subject)
|
||||
->setPreview($preview)
|
||||
->setBody($body)
|
||||
->setVariables($emailVariables)
|
||||
->setRecipient($email)
|
||||
@@ -3265,6 +3271,7 @@ App::post('/v1/account/recovery')
|
||||
$projectName = $project->isEmpty() ? 'Console' : $project->getAttribute('name', '[APP-NAME]');
|
||||
$body = $locale->getText("emails.recovery.body");
|
||||
$subject = $locale->getText("emails.recovery.subject");
|
||||
$preview = $locale->getText("emails.recovery.preview");
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.recovery-' . $locale->default] ?? [];
|
||||
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl');
|
||||
@@ -3339,6 +3346,7 @@ App::post('/v1/account/recovery')
|
||||
->setBody($body)
|
||||
->setVariables($emailVariables)
|
||||
->setSubject($subject)
|
||||
->setPreview($preview)
|
||||
->trigger();
|
||||
|
||||
$recovery->setAttribute('secret', $secret);
|
||||
@@ -3520,6 +3528,7 @@ App::post('/v1/account/verification')
|
||||
|
||||
$projectName = $project->isEmpty() ? 'Console' : $project->getAttribute('name', '[APP-NAME]');
|
||||
$body = $locale->getText("emails.verification.body");
|
||||
$preview = $locale->getText("emails.verification.preview");
|
||||
$subject = $locale->getText("emails.verification.subject");
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.verification-' . $locale->default] ?? [];
|
||||
|
||||
@@ -3592,6 +3601,7 @@ App::post('/v1/account/verification')
|
||||
|
||||
$queueForMails
|
||||
->setSubject($subject)
|
||||
->setPreview($preview)
|
||||
->setBody($body)
|
||||
->setVariables($emailVariables)
|
||||
->setRecipient($user->getAttribute('email'))
|
||||
@@ -4437,6 +4447,7 @@ App::post('/v1/account/mfa/challenge')
|
||||
}
|
||||
|
||||
$subject = $locale->getText("emails.mfaChallenge.subject");
|
||||
$preview = $locale->getText("emails.mfaChallenge.preview");
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.mfaChallenge-' . $locale->default] ?? [];
|
||||
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
@@ -4513,6 +4524,7 @@ App::post('/v1/account/mfa/challenge')
|
||||
|
||||
$queueForMails
|
||||
->setSubject($subject)
|
||||
->setPreview($preview)
|
||||
->setBody($body)
|
||||
->setVariables($emailVariables)
|
||||
->setRecipient($user->getAttribute('email'))
|
||||
|
||||
@@ -657,6 +657,7 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
$projectName = $project->isEmpty() ? 'Console' : $project->getAttribute('name', '[APP-NAME]');
|
||||
|
||||
$body = $locale->getText("emails.invitation.body");
|
||||
$preview = $locale->getText("emails.invitation.preview");
|
||||
$subject = \sprintf($locale->getText("emails.invitation.subject"), $team->getAttribute('name'), $projectName);
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.invitation-' . $locale->default] ?? [];
|
||||
|
||||
@@ -729,6 +730,7 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
$queueForMails
|
||||
->setSubject($subject)
|
||||
->setBody($body)
|
||||
->setPreview($preview)
|
||||
->setRecipient($invitee->getAttribute('email'))
|
||||
->setName($invitee->getAttribute('name', ''))
|
||||
->setVariables($emailVariables)
|
||||
|
||||
@@ -10,6 +10,7 @@ class Mail extends Event
|
||||
protected string $name = '';
|
||||
protected string $subject = '';
|
||||
protected string $body = '';
|
||||
protected string $preview = '';
|
||||
protected array $smtp = [];
|
||||
protected array $variables = [];
|
||||
protected string $bodyTemplate = '';
|
||||
@@ -93,6 +94,28 @@ class Mail extends Event
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets preview for the mail event.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function setPreview(string $preview): self
|
||||
{
|
||||
$this->preview = $preview;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns preview for the mail event.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPreview(string $preview): string
|
||||
{
|
||||
return $this->preview;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets name for the mail event.
|
||||
*
|
||||
@@ -409,6 +432,7 @@ class Mail extends Event
|
||||
'subject' => $this->subject,
|
||||
'bodyTemplate' => $this->bodyTemplate,
|
||||
'body' => $this->body,
|
||||
'preview' => $this->preview,
|
||||
'smtp' => $this->smtp,
|
||||
'variables' => $this->variables,
|
||||
'attachment' => $this->attachment,
|
||||
|
||||
@@ -382,9 +382,11 @@ class Certificates extends Action
|
||||
];
|
||||
|
||||
$subject = \sprintf($locale->getText("emails.certificate.subject"), $domain);
|
||||
$preview = \sprintf($locale->getText("emails.certificate.preview"), $domain);
|
||||
|
||||
$queueForMails
|
||||
->setSubject($subject)
|
||||
->setPreview($preview)
|
||||
->setBody($body)
|
||||
->setName('Appwrite Administrator')
|
||||
->setbodyTemplate(__DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl')
|
||||
|
||||
@@ -14,6 +14,11 @@ use Utopia\System\System;
|
||||
|
||||
class Mails extends Action
|
||||
{
|
||||
protected int $previewMaxLen = 150;
|
||||
|
||||
protected string $whitespaceCodes = ' ‌​‍‎‏';
|
||||
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'mails';
|
||||
@@ -74,6 +79,7 @@ class Mails extends Action
|
||||
$variables['host'] = $protocol . '://' . $hostname;
|
||||
$name = $payload['name'];
|
||||
$body = $payload['body'];
|
||||
$preview = $payload['preview'] ?? '';
|
||||
|
||||
$variables['subject'] = $subject;
|
||||
$variables['year'] = date("Y");
|
||||
@@ -92,6 +98,27 @@ class Mails extends Action
|
||||
foreach ($this->richTextParams as $key => $value) {
|
||||
$bodyTemplate->setParam('{{' . $key . '}}', $value, escapeHtml: false);
|
||||
}
|
||||
|
||||
$previewWhitespace = '';
|
||||
|
||||
if (!empty($preview)) {
|
||||
$previewTemplate = Template::fromString($preview);
|
||||
foreach ($variables as $key => $value) {
|
||||
$previewTemplate->setParam('{{' . $key . '}}', $value);
|
||||
}
|
||||
// render() will return the subject in <p> tags, so use strip_tags() to remove them
|
||||
$preview = \strip_tags($previewTemplate->render());
|
||||
|
||||
$previewLen = strlen($preview);
|
||||
if ($previewLen < $this->previewMaxLen) {
|
||||
$previewWhitespace = str_repeat($this->whitespaceCodes, $this->previewMaxLen - $previewLen);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$bodyTemplate->setParam('{{preview}}', $preview);
|
||||
$bodyTemplate->setParam('{{previewWhitespace}}', $previewWhitespace, false);
|
||||
|
||||
$body = $bodyTemplate->render();
|
||||
|
||||
$subjectTemplate = Template::fromString($subject);
|
||||
|
||||
@@ -241,6 +241,7 @@ class Webhooks extends Action
|
||||
|
||||
// TODO: Use setbodyTemplate once #7307 is merged
|
||||
$subject = 'Webhook deliveries have been paused';
|
||||
$preview = 'Webhook deliveries to your endpoint have been paused.';
|
||||
$body = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl');
|
||||
|
||||
$body
|
||||
@@ -250,6 +251,7 @@ class Webhooks extends Action
|
||||
|
||||
$queueForMails
|
||||
->setSubject($subject)
|
||||
->setPreview($preview)
|
||||
->setBody($body->render());
|
||||
|
||||
foreach ($users as $user) {
|
||||
|
||||
@@ -170,6 +170,7 @@ trait AccountBase
|
||||
$userId = $response['body']['userId'];
|
||||
|
||||
$lastEmail = $this->getLastEmail();
|
||||
|
||||
$this->assertEquals('otpuser@appwrite.io', $lastEmail['to'][0]['address']);
|
||||
$this->assertEquals('OTP for ' . $this->getProject()['name'] . ' Login', $lastEmail['subject']);
|
||||
|
||||
@@ -178,6 +179,7 @@ trait AccountBase
|
||||
$code = ($matches[0] ?? [])[0] ?? '';
|
||||
|
||||
$this->assertNotEmpty($code);
|
||||
$this->assertStringContainsStringIgnoringCase('Use OTP ' . $code . ' to sign in to '. $this->getProject()['name'] . '. Expires in 15 minutes.', $lastEmail['text']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/token', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
|
||||
@@ -779,6 +779,7 @@ class AccountCustomClientTest extends Scope
|
||||
$this->assertEquals($email, $lastEmail['to'][0]['address']);
|
||||
$this->assertEquals($name, $lastEmail['to'][0]['name']);
|
||||
$this->assertEquals('Account Verification', $lastEmail['subject']);
|
||||
$this->assertStringContainsStringIgnoringCase('Verify your email to activate your ' . $this->getProject()['name'] . ' account.', $lastEmail['text']);
|
||||
|
||||
$tokens = $this->extractQueryParamsFromEmailLink($lastEmail['html']);
|
||||
$verification = $tokens['secret'];
|
||||
@@ -1082,6 +1083,8 @@ class AccountCustomClientTest extends Scope
|
||||
$this->assertEquals($email, $lastEmail['to'][0]['address']);
|
||||
$this->assertEquals($name, $lastEmail['to'][0]['name']);
|
||||
$this->assertEquals('Password Reset', $lastEmail['subject']);
|
||||
$this->assertStringContainsStringIgnoringCase('Reset your ' . $this->getProject()['name'] . ' password using the link.', $lastEmail['text']);
|
||||
|
||||
|
||||
$tokens = $this->extractQueryParamsFromEmailLink($lastEmail['html']);
|
||||
|
||||
@@ -1286,6 +1289,7 @@ class AccountCustomClientTest extends Scope
|
||||
$this->assertNotEmpty($response['body']['expire']);
|
||||
$this->assertEmpty($response['body']['secret']);
|
||||
$this->assertEmpty($response['body']['phrase']);
|
||||
$this->assertStringContainsStringIgnoringCase('New login detected on '. $this->getProject()['name'], $lastEmail['text']);
|
||||
|
||||
$userId = $response['body']['userId'];
|
||||
|
||||
@@ -2545,6 +2549,7 @@ class AccountCustomClientTest extends Scope
|
||||
$lastEmail = $this->getLastEmail();
|
||||
$this->assertEquals($email, $lastEmail['to'][0]['address']);
|
||||
$this->assertEquals($this->getProject()['name'] . ' Login', $lastEmail['subject']);
|
||||
$this->assertStringContainsStringIgnoringCase('Sign in to '. $this->getProject()['name'] . ' with your secure link. Expires in 1 hour.', $lastEmail['text']);
|
||||
$this->assertStringNotContainsStringIgnoringCase('security phrase', $lastEmail['text']);
|
||||
|
||||
$token = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 64);
|
||||
|
||||
Reference in New Issue
Block a user