Added support for html templates in version emails

refs https://github.com/TryGhost/Toolbox/issues/292

- The html/text emails is a desired system that's used in Ghost core and should be reused with version mismatch notification emails too.
- Currently there's only one template defined "generic-mismatch" and the original file for it can be found under /templates/generic-mismatch.html
- If we need to distinguish user agents we can addd more templates to the `/templates/` folder
This commit is contained in:
Naz 2022-05-05 12:16:20 +08:00
parent 06c733bb0b
commit 30f8b0a446
4 changed files with 289 additions and 27 deletions

View File

@ -1,3 +1,6 @@
const path = require('path');
const EmailContentGenerator = require('@tryghost/email-content-generator');
class APIVersionCompatibilityService {
/**
*
@ -6,30 +9,43 @@ class APIVersionCompatibilityService {
* @param {() => Promise<any>} options.fetchEmailsToNotify - email address to receive notifications
* @param {(acceptVersion: String) => Promise<any>} options.fetchHandled - retrives already handled compatibility notifications
* @param {(acceptVersion: String) => Promise<any>} options.saveHandled - persists already handled compatibility notifications
* @param {Function} options.getSiteUrl
* @param {Function} options.getSiteTitle
*/
constructor({sendEmail, fetchEmailsToNotify, fetchHandled, saveHandled}) {
constructor({sendEmail, fetchEmailsToNotify, fetchHandled, saveHandled, getSiteUrl, getSiteTitle}) {
this.sendEmail = sendEmail;
this.fetchEmailsToNotify = fetchEmailsToNotify;
this.fetchHandled = fetchHandled;
this.saveHandled = saveHandled;
this.emailContentGenerator = new EmailContentGenerator({
getSiteUrl,
getSiteTitle,
templatesDir: path.join(__dirname, 'templates')
});
}
async handleMismatch({acceptVersion, contentVersion, userAgent = ''}) {
if (!await this.fetchHandled(acceptVersion)) {
const trimmedUseAgent = userAgent.split('/')[0];
const htmlContent = `
${trimmedUseAgent} integration expected Ghost version: ${acceptVersion}
Current Ghost version: ${contentVersion}
`;
const textContent = htmlContent;
const emails = await this.fetchEmailsToNotify();
for (const email of emails) {
const {html, text} = await this.emailContentGenerator.getContent({
template: 'generic-mismatch',
data: {
acceptVersion,
contentVersion,
clientName: trimmedUseAgent,
recipientEmail: email
}
});
await this.sendEmail({
subject: `Attention required: Your ${trimmedUseAgent} integration has failed`,
to: email,
html: htmlContent,
text: textContent
html,
text
});
}

View File

@ -0,0 +1,168 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Integration error</title>
<style>
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .title {
font-size: 22px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
table[class=body] p[class=small],
table[class=body] a[class=small] {
font-size: 12x !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.recipient-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
}
hr {
border-width: 0;
height: 0;
margin-top: 34px;
margin-bottom: 34px;
border-bottom-width: 1px;
border-bottom-color: #EEF5F8;
}
a {
color: #3A464C;
}
</style>
</head>
<body style="background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.5em; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">&nbsp;</td>
<td class="container" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto; max-width: 540px; padding: 10px; width: 540px;">
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 600px; padding: 30px 20px;">
<!-- START CENTERED CONTAINER -->
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 8px;">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; box-sizing: border-box;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td align="center" style="padding-top: 20px; padding-bottom: 12px;"><img src="https://static.ghost.org/v4.0.0/images/ghost-orb-1.png" width="60" height="60" style="width: 60px; height: 60px;" /></td>
</tr>
<tr>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top;">
<p class="title" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 21px; color: #3A464C; font-weight: normal; line-height: 25px; margin-bottom: 0px; margin-top: 50px; font-weight: 600; color: #15212A;">Uh-oh!</p>
</td>
</tr>
<tr>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; padding-top: 24px; padding-bottom: 10px;">
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 30px;">
Ghost has noticed that your <strong style="font-weight: 600;">{{clientName}}</strong> is no longer working as expected. To resolve, this integration must be updated by its developer to work with the your version of Ghost.
</p>
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 30px;">
To help you get things fixed as quickly as possible, Ghost has automatically generated some helpful information about the error that you can share with the creator of the Cove integration below:
</p>
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 20px;background-color:#f6f6f6; padding:20px; border-radius:2px;">
<strong style="font-weight: 600;">{{clientName}} integration expected Ghost version:</strong>&nbsp; {{acceptVersion}}
<br>
<strong style="font-weight: 600;">Current Ghost version:</strong>&nbsp; {{contentVersion}}
<br>
<strong style="font-weight: 600;">Failed request URL:</strong>&nbsp; /content/posts/1234/
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; vertical-align: top; padding-top: 80px; padding-bottom: 10px;">
<div class="footer">
<p class="small" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; color: #738A94; font-weight: normal; margin: 0; line-height: 18px; margin-bottom: 0px; font-size: 11px;">This email was sent from <a href="{{siteUrl}}" style="color: #738A94;">{{siteUrl}}</a> to <a href="mailto:{{recipientEmail}}" style="color: #738A94;">{{recipientEmail}}</a></p>
</div>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- END CENTERED CONTAINER -->
</div>
</td>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -24,5 +24,8 @@
"c8": "7.11.2",
"mocha": "10.0.0",
"sinon": "13.0.2"
},
"dependencies": {
"@tryghost/email-content-generator": "^0.1.0"
}
}

View File

@ -3,6 +3,9 @@ const sinon = require('sinon');
const APIVersionCompatibilityService = require('../index');
describe('APIVersionCompatibilityService', function () {
const getSiteUrl = () => 'https://amazeballsghostsite.com';
const getSiteTitle = () => 'Tahini and chickpeas';
afterEach(function () {
sinon.reset();
});
@ -16,7 +19,9 @@ describe('APIVersionCompatibilityService', function () {
sendEmail,
fetchEmailsToNotify: async () => ['test_env@example.com'],
fetchHandled,
saveHandled
saveHandled,
getSiteUrl,
getSiteTitle
});
await compatibilityService.handleMismatch({
@ -28,8 +33,20 @@ describe('APIVersionCompatibilityService', function () {
assert.equal(sendEmail.called, true);
assert.equal(sendEmail.args[0][0].to, 'test_env@example.com');
assert.equal(sendEmail.args[0][0].subject, `Attention required: Your Elaborate Fox integration has failed`);
assert.match(sendEmail.args[0][0].html, /Elaborate Fox integration expected Ghost version: v4.5/);
assert.match(sendEmail.args[0][0].html, /Current Ghost version: v5.1/);
assert.match(sendEmail.args[0][0].html, /Ghost has noticed that your <strong style="font-weight: 600;">Elaborate Fox<\/strong> is no longer working as expected\./);
assert.match(sendEmail.args[0][0].html, /Elaborate Fox integration expected Ghost version:<\/strong>&nbsp; v4.5/);
assert.match(sendEmail.args[0][0].html, /Current Ghost version:<\/strong>&nbsp; v5.1/);
assert.match(sendEmail.args[0][0].html, /This email was sent from <a href="https:\/\/amazeballsghostsite.com"/);
assert.match(sendEmail.args[0][0].html, /to <a href="mailto:test_env@example.com"/);
assert.match(sendEmail.args[0][0].text, /Ghost has noticed that your Elaborate Fox is no longer working as expected\./);
assert.match(sendEmail.args[0][0].text, /Elaborate Fox integration expected Ghost version:v4.5/);
assert.match(sendEmail.args[0][0].text, /Current Ghost version:v5.1/);
assert.match(sendEmail.args[0][0].text, /This email was sent from https:\/\/amazeballsghostsite.com/);
assert.match(sendEmail.args[0][0].text, /to test_env@example.com/);
});
it('Does NOT send an email to the instance owner when previously handled accept-version header mismatch is detected', async function () {
@ -44,7 +61,9 @@ describe('APIVersionCompatibilityService', function () {
sendEmail,
fetchEmailsToNotify: async () => ['test_env@example.com'],
fetchHandled,
saveHandled
saveHandled,
getSiteUrl,
getSiteTitle
});
await compatibilityService.handleMismatch({
@ -53,11 +72,23 @@ describe('APIVersionCompatibilityService', function () {
userAgent: 'Elaborate Fox'
});
assert.equal(sendEmail.calledOnce, true);
assert.equal(sendEmail.called, true);
assert.equal(sendEmail.args[0][0].to, 'test_env@example.com');
assert.equal(sendEmail.args[0][0].subject, `Attention required: Your Elaborate Fox integration has failed`);
assert.match(sendEmail.args[0][0].html, /Elaborate Fox integration expected Ghost version: v4.5/);
assert.match(sendEmail.args[0][0].html, /Current Ghost version: v5.1/);
assert.match(sendEmail.args[0][0].html, /Ghost has noticed that your <strong style="font-weight: 600;">Elaborate Fox<\/strong> is no longer working as expected\./);
assert.match(sendEmail.args[0][0].html, /Elaborate Fox integration expected Ghost version:<\/strong>&nbsp; v4.5/);
assert.match(sendEmail.args[0][0].html, /Current Ghost version:<\/strong>&nbsp; v5.1/);
assert.match(sendEmail.args[0][0].html, /This email was sent from <a href="https:\/\/amazeballsghostsite.com"/);
assert.match(sendEmail.args[0][0].html, /to <a href="mailto:test_env@example.com"/);
assert.match(sendEmail.args[0][0].text, /Ghost has noticed that your Elaborate Fox is no longer working as expected\./);
assert.match(sendEmail.args[0][0].text, /Elaborate Fox integration expected Ghost version:v4.5/);
assert.match(sendEmail.args[0][0].text, /Current Ghost version:v5.1/);
assert.match(sendEmail.args[0][0].text, /This email was sent from https:\/\/amazeballsghostsite.com/);
assert.match(sendEmail.args[0][0].text, /to test_env@example.com/);
await compatibilityService.handleMismatch({
acceptVersion: 'v4.5',
@ -80,7 +111,9 @@ describe('APIVersionCompatibilityService', function () {
sendEmail,
fetchEmailsToNotify: async () => ['test_env@example.com', 'test_env2@example.com'],
fetchHandled,
saveHandled
saveHandled,
getSiteUrl,
getSiteTitle
});
await compatibilityService.handleMismatch({
@ -92,14 +125,38 @@ describe('APIVersionCompatibilityService', function () {
assert.equal(sendEmail.calledTwice, true);
assert.equal(sendEmail.args[0][0].to, 'test_env@example.com');
assert.equal(sendEmail.args[0][0].subject, `Attention required: Your Elaborate Fox integration has failed`);
assert.match(sendEmail.args[0][0].html, /Elaborate Fox integration expected Ghost version: v4.5/);
assert.match(sendEmail.args[0][0].html, /Current Ghost version: v5.1/);
assert.match(sendEmail.args[0][0].html, /Ghost has noticed that your <strong style="font-weight: 600;">Elaborate Fox<\/strong> is no longer working as expected\./);
assert.match(sendEmail.args[0][0].html, /Elaborate Fox integration expected Ghost version:<\/strong>&nbsp; v4.5/);
assert.match(sendEmail.args[0][0].html, /Current Ghost version:<\/strong>&nbsp; v5.1/);
assert.match(sendEmail.args[0][0].html, /This email was sent from <a href="https:\/\/amazeballsghostsite.com"/);
assert.match(sendEmail.args[0][0].html, /to <a href="mailto:test_env@example.com"/);
assert.match(sendEmail.args[0][0].text, /Ghost has noticed that your Elaborate Fox is no longer working as expected\./);
assert.match(sendEmail.args[0][0].text, /Elaborate Fox integration expected Ghost version:v4.5/);
assert.match(sendEmail.args[0][0].text, /Current Ghost version:v5.1/);
assert.match(sendEmail.args[0][0].text, /This email was sent from https:\/\/amazeballsghostsite.com/);
assert.match(sendEmail.args[0][0].text, /to test_env@example.com/);
assert.equal(sendEmail.calledTwice, true);
assert.equal(sendEmail.args[1][0].to, 'test_env2@example.com');
assert.equal(sendEmail.args[1][0].subject, `Attention required: Your Elaborate Fox integration has failed`);
assert.match(sendEmail.args[1][0].html, /Elaborate Fox integration expected Ghost version: v4.5/);
assert.match(sendEmail.args[1][0].html, /Current Ghost version: v5.1/);
assert.match(sendEmail.args[1][0].html, /Ghost has noticed that your <strong style="font-weight: 600;">Elaborate Fox<\/strong> is no longer working as expected\./);
assert.match(sendEmail.args[1][0].html, /Elaborate Fox integration expected Ghost version:<\/strong>&nbsp; v4.5/);
assert.match(sendEmail.args[1][0].html, /Current Ghost version:<\/strong>&nbsp; v5.1/);
assert.match(sendEmail.args[1][0].html, /This email was sent from <a href="https:\/\/amazeballsghostsite.com"/);
assert.match(sendEmail.args[1][0].html, /to <a href="mailto:test_env2@example.com"/);
assert.match(sendEmail.args[1][0].text, /Ghost has noticed that your Elaborate Fox is no longer working as expected\./);
assert.match(sendEmail.args[1][0].text, /Elaborate Fox integration expected Ghost version:v4.5/);
assert.match(sendEmail.args[1][0].text, /Current Ghost version:v5.1/);
assert.match(sendEmail.args[1][0].text, /This email was sent from https:\/\/amazeballsghostsite.com/);
assert.match(sendEmail.args[1][0].text, /to test_env2@example.com/);
await compatibilityService.handleMismatch({
acceptVersion: 'v4.8',
@ -109,9 +166,13 @@ describe('APIVersionCompatibilityService', function () {
assert.equal(sendEmail.callCount, 4);
assert.equal(sendEmail.args[2][0].to, 'test_env@example.com');
assert.equal(sendEmail.args[2][0].subject, `Attention required: Your Elaborate Fox integration has failed`);
assert.match(sendEmail.args[2][0].html, /Elaborate Fox integration expected Ghost version: v4.8/);
assert.match(sendEmail.args[2][0].html, /Current Ghost version: v5.1/);
assert.match(sendEmail.args[2][0].html, /Ghost has noticed that your <strong style="font-weight: 600;">Elaborate Fox<\/strong> is no longer working as expected\./);
assert.match(sendEmail.args[2][0].html, /Elaborate Fox integration expected Ghost version:<\/strong>&nbsp; v4.8/);
assert.match(sendEmail.args[2][0].html, /Current Ghost version:<\/strong>&nbsp; v5.1/);
assert.match(sendEmail.args[2][0].text, /Ghost has noticed that your Elaborate Fox is no longer working as expected\./);
assert.match(sendEmail.args[2][0].text, /Elaborate Fox integration expected Ghost version:v4.8/);
assert.match(sendEmail.args[2][0].text, /Current Ghost version:v5.1/);
});
it('Trims down the name of the integration when a lot of meta information is present in user-agent header', async function (){
@ -123,7 +184,9 @@ describe('APIVersionCompatibilityService', function () {
sendEmail,
fetchEmailsToNotify: async () => ['test_env@example.com'],
fetchHandled,
saveHandled
saveHandled,
getSiteUrl,
getSiteTitle
});
await compatibilityService.handleMismatch({
@ -135,7 +198,19 @@ describe('APIVersionCompatibilityService', function () {
assert.equal(sendEmail.called, true);
assert.equal(sendEmail.args[0][0].to, 'test_env@example.com');
assert.equal(sendEmail.args[0][0].subject, `Attention required: Your Zapier integration has failed`);
assert.match(sendEmail.args[0][0].html, /Zapier integration expected Ghost version: v4.5/);
assert.match(sendEmail.args[0][0].html, /Current Ghost version: v5.1/);
assert.match(sendEmail.args[0][0].html, /Ghost has noticed that your <strong style="font-weight: 600;">Zapier<\/strong> is no longer working as expected\./);
assert.match(sendEmail.args[0][0].html, /Zapier integration expected Ghost version:<\/strong>&nbsp; v4.5/);
assert.match(sendEmail.args[0][0].html, /Current Ghost version:<\/strong>&nbsp; v5.1/);
assert.match(sendEmail.args[0][0].html, /This email was sent from <a href="https:\/\/amazeballsghostsite.com"/);
assert.match(sendEmail.args[0][0].html, /to <a href="mailto:test_env@example.com"/);
assert.match(sendEmail.args[0][0].text, /Ghost has noticed that your Zapier is no longer working as expected\./);
assert.match(sendEmail.args[0][0].text, /Zapier integration expected Ghost version:v4.5/);
assert.match(sendEmail.args[0][0].text, /Current Ghost version:v5.1/);
assert.match(sendEmail.args[0][0].text, /This email was sent from https:\/\/amazeballsghostsite.com/);
assert.match(sendEmail.args[0][0].text, /to test_env@example.com/);
});
});