diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/email-previews.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/email-previews.test.js.snap
index 14d83f70fe..e7fad23783 100644
--- a/ghost/core/test/e2e-api/admin/__snapshots__/email-previews.test.js.snap
+++ b/ghost/core/test/e2e-api/admin/__snapshots__/email-previews.test.js.snap
@@ -416,6 +416,7 @@ table.body figcaption a {
+
@@ -554,6 +555,7 @@ Another email card with a similar replacement, Jamie
+
Ghost © 2023 – Unsubscribe [http://127.0.0.1:2369/unsubscribe/?uuid=example-uuid&newsletter=requested-newsletter-uuid]
@@ -583,7 +585,7 @@ exports[`Email Preview API Read can read post email preview with email card and
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
- "content-length": "18958",
+ "content-length": "18962",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@@ -917,6 +919,7 @@ table.body figcaption a {
+
@@ -1071,6 +1074,7 @@ Header Level 3
+
Ghost © 2023 – Unsubscribe [http://127.0.0.1:2369/unsubscribe/?uuid=example-uuid&newsletter=requested-newsletter-uuid]
@@ -1100,7 +1104,7 @@ exports[`Email Preview API Read can read post email preview with fields 4: [head
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
- "content-length": "23781",
+ "content-length": "23785",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@@ -1461,6 +1465,7 @@ table.body figcaption a {
+
@@ -1593,6 +1598,7 @@ Testing links [https://ghost.org] in email excerpt and apostrophes '
+
Ghost © 2023 – Unsubscribe [http://127.0.0.1:2369/unsubscribe/?uuid=example-uuid&newsletter=requested-newsletter-uuid]
@@ -1635,7 +1641,7 @@ exports[`Email Preview API Read has custom content transformations for email com
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
- "content-length": "18724",
+ "content-length": "18728",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@@ -2330,6 +2336,7 @@ table.body figcaption a {
+
@@ -2467,6 +2474,7 @@ Testing links [https://ghost.org] in email excerpt and apostrophes '
+
Ghost © 2023 – Unsubscribe [http://127.0.0.1:2369/unsubscribe/?uuid=example-uuid&newsletter=requested-newsletter-uuid]
@@ -2509,7 +2517,7 @@ exports[`Email Preview API Read uses the newsletter provided through ?newsletter
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
- "content-length": "19216",
+ "content-length": "19220",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@@ -3230,6 +3238,7 @@ table.body figcaption a {
+
@@ -3367,6 +3376,7 @@ Testing links [https://ghost.org] in email excerpt and apostrophes '
+
Ghost © 2023 – Unsubscribe [http://127.0.0.1:2369/unsubscribe/?uuid=example-uuid&newsletter=requested-newsletter-uuid]
@@ -3409,7 +3419,7 @@ exports[`Email Preview API Read uses the posts newsletter by default 4: [headers
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
- "content-length": "19216",
+ "content-length": "19220",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
diff --git a/ghost/core/test/integration/services/email-service/__snapshots__/batch-sending.test.js.snap b/ghost/core/test/integration/services/email-service/__snapshots__/batch-sending.test.js.snap
index 077fe2190e..cb20dbb437 100644
--- a/ghost/core/test/integration/services/email-service/__snapshots__/batch-sending.test.js.snap
+++ b/ghost/core/test/integration/services/email-service/__snapshots__/batch-sending.test.js.snap
@@ -322,6 +322,7 @@ table.body figcaption a {
+
@@ -450,6 +451,7 @@ Testing feature image caption
+
Ghost © 2023 – Unsubscribe [http://127.0.0.1:2369/unsubscribe/?uuid=member-uuid&newsletter=requested-newsletter-uuid]
@@ -775,6 +777,7 @@ table.body figcaption a {
+
@@ -879,6 +882,7 @@ Hello world
+
Ghost © 2023 – Unsubscribe [http://127.0.0.1:2369/unsubscribe/?uuid=member-uuid&newsletter=requested-newsletter-uuid]
@@ -1218,6 +1222,7 @@ table.body figcaption a {
+
@@ -1342,6 +1347,7 @@ Hello world
+
Ghost © 2023 – Unsubscribe [http://127.0.0.1:2369/unsubscribe/?uuid=member-uuid&newsletter=requested-newsletter-uuid]
@@ -1681,6 +1687,7 @@ table.body figcaption a {
+
@@ -1807,6 +1814,7 @@ Hey Simon, Hey Simon,
+
Ghost © 2023 – Unsubscribe [http://127.0.0.1:2369/unsubscribe/?uuid=member-uuid&newsletter=requested-newsletter-uuid]
@@ -2146,6 +2154,7 @@ table.body figcaption a {
+
@@ -2272,6 +2281,7 @@ Hey there, Hey ,
+
Ghost © 2023 – Unsubscribe [http://127.0.0.1:2369/unsubscribe/?uuid=member-uuid&newsletter=requested-newsletter-uuid]
diff --git a/ghost/email-service/lib/batch-sending-service.js b/ghost/email-service/lib/batch-sending-service.js
index 59b322e09b..5a1d34b02a 100644
--- a/ghost/email-service/lib/batch-sending-service.js
+++ b/ghost/email-service/lib/batch-sending-service.js
@@ -497,13 +497,14 @@ class BatchSendingService {
* @returns {Promise}
*/
async getBatchMembers(batchId) {
- const models = await this.#models.EmailRecipient.findAll({filter: `batch_id:${batchId}`});
+ const models = await this.#models.EmailRecipient.findAll({filter: `batch_id:${batchId}`, withRelated: ['member']});
return models.map((model) => {
return {
id: model.get('member_id'),
uuid: model.get('member_uuid'),
email: model.get('member_email'),
- name: model.get('member_name')
+ name: model.get('member_name'),
+ createdAt: model.related('member')?.get('created_at') ?? null
};
});
}
diff --git a/ghost/email-service/lib/email-renderer.js b/ghost/email-service/lib/email-renderer.js
index e047f788b6..93de1f2b42 100644
--- a/ghost/email-service/lib/email-renderer.js
+++ b/ghost/email-service/lib/email-renderer.js
@@ -20,6 +20,7 @@ const htmlToPlaintext = require('@tryghost/html-to-plaintext');
* @prop {string} uuid
* @prop {string} email
* @prop {string} name
+ * @prop {Date|null} createdAt This can be null if the member has been deleted for older email recipient rows
*/
/**
@@ -372,6 +373,29 @@ class EmailRenderer {
getValue: (member) => {
return member.name?.split(' ')[0];
}
+ },
+ {
+ id: 'name',
+ getValue: (member) => {
+ return member.name;
+ }
+ },
+ {
+ id: 'email',
+ getValue: (member) => {
+ return member.email;
+ }
+ },
+ {
+ id: 'created_at',
+ getValue: (member) => {
+ const timezone = this.#settingsCache.get('timezone');
+ return member.createdAt ? DateTime.fromJSDate(member.createdAt).setZone(timezone).setLocale('en-gb').toLocaleString({
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric'
+ }) : '';
+ }
}
];
@@ -593,7 +617,8 @@ class EmailRenderer {
newsletter: {
name: newsletter.get('name'),
showPostTitleSection: newsletter.get('show_post_title_section'),
- showCommentCta: newsletter.get('show_comment_cta') && this.#settingsCache.get('comments_enabled') !== 'off' && this.#labs.isSet('makingItRain')
+ showCommentCta: newsletter.get('show_comment_cta') && this.#settingsCache.get('comments_enabled') !== 'off' && this.#labs.isSet('makingItRain'),
+ showSubscriptionDetails: newsletter.get('show_subscription_details') && this.#labs.isSet('makingItRain')
},
//CSS
diff --git a/ghost/email-service/lib/email-service.js b/ghost/email-service/lib/email-service.js
index 66ec80963e..5cdbcdec43 100644
--- a/ghost/email-service/lib/email-service.js
+++ b/ghost/email-service/lib/email-service.js
@@ -188,7 +188,8 @@ class EmailService {
id: 'example-id',
uuid: 'example-uuid',
email: 'jamie@example.com',
- name: 'Jamie Larson'
+ name: 'Jamie Larson',
+ createdAt: new Date()
};
}
@@ -211,6 +212,7 @@ class EmailService {
exampleMember.uuid = member.get('uuid');
exampleMember.email = member.get('email');
exampleMember.name = member.get('name');
+ exampleMember.createdAt = member.get('created_at');
} else {
exampleMember.name = ''; // Force empty name to simulate name fallbacks
exampleMember.email = email;
diff --git a/ghost/email-service/lib/email-templates/template.hbs b/ghost/email-service/lib/email-templates/template.hbs
index 25b45afe2d..918f552245 100644
--- a/ghost/email-service/lib/email-templates/template.hbs
+++ b/ghost/email-service/lib/email-templates/template.hbs
@@ -151,6 +151,19 @@
{{/if}}
+ {{#if newsletter.showSubscriptionDetails}}
+
+
+ Subscription details
+ You are receiving this because you are a paidfree subscriber to {{site.title}}.
+ Name: %%{name}%%
+ Email: %%{email}%%
+ Member since: %%{created_at}%%
+ Renewal date: todo
+ |
+
+ {{/if}}
+
diff --git a/ghost/email-service/test/batch-sending-service.test.js b/ghost/email-service/test/batch-sending-service.test.js
index 8e2f5a8f9e..0ae2a65774 100644
--- a/ghost/email-service/test/batch-sending-service.test.js
+++ b/ghost/email-service/test/batch-sending-service.test.js
@@ -584,18 +584,26 @@ describe('Batch Sending Service', function () {
beforeEach(function () {
EmailRecipient = createModelClass({
findAll: [
- createModel({
+ {
member_id: '123',
member_uuid: '123',
member_email: 'example@example.com',
- member_name: 'Test User'
- }),
- createModel({
+ member_name: 'Test User',
+ loaded: ['member'],
+ member: createModel({
+ created_at: new Date()
+ })
+ },
+ {
member_id: '124',
member_uuid: '124',
member_email: 'example2@example.com',
- member_name: 'Test User 2'
- })
+ member_name: 'Test User 2',
+ loaded: ['member'],
+ member: createModel({
+ created_at: new Date()
+ })
+ }
]
});
});
@@ -912,6 +920,31 @@ describe('Batch Sending Service', function () {
});
});
+ describe('getBatchMembers', function () {
+ it('Works for recipients without members', async function () {
+ const EmailRecipient = createModelClass({
+ findAll: [
+ {
+ member_id: '123',
+ member_uuid: '123',
+ member_email: 'example@example.com',
+ member_name: 'Test User',
+ loaded: ['member'],
+ member: null
+ }
+ ]
+ });
+
+ const service = new BatchSendingService({
+ models: {EmailRecipient}
+ });
+
+ const result = await service.getBatchMembers('id123');
+ assert.equal(result.length, 1);
+ assert.equal(result[0].createdAt, null);
+ });
+ });
+
describe('retryDb', function () {
it('Does retry', async function () {
const service = new BatchSendingService({});
diff --git a/ghost/email-service/test/email-renderer.test.js b/ghost/email-service/test/email-renderer.test.js
index 822de11e50..2d7939ca10 100644
--- a/ghost/email-service/test/email-renderer.test.js
+++ b/ghost/email-service/test/email-renderer.test.js
@@ -83,6 +83,13 @@ describe('Email renderer', function () {
},
labs: {
isSet: () => true
+ },
+ settingsCache: {
+ get: (key) => {
+ if (key === 'timezone') {
+ return 'UTC';
+ }
+ }
}
});
newsletter = createModel({
@@ -92,7 +99,8 @@ describe('Email renderer', function () {
id: '456',
uuid: 'myuuid',
name: 'Test User',
- email: 'test@example.com'
+ email: 'test@example.com',
+ createdAt: new Date(2023, 2, 13, 12, 0)
};
});
@@ -138,6 +146,43 @@ describe('Email renderer', function () {
assert.equal(replacements[0].getValue(member), `http://example.com/subdirectory/unsubscribe/?uuid=myuuid&newsletter=newsletteruuid`);
});
+ it('returns correct name', function () {
+ const html = 'Hello %%{name}%%,';
+ const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
+ assert.equal(replacements.length, 1);
+ assert.equal(replacements[0].token.toString(), '/%%\\{name\\}%%/g');
+ assert.equal(replacements[0].id, 'name');
+ assert.equal(replacements[0].getValue(member), 'Test User');
+ });
+
+ it('returns correct email', function () {
+ const html = 'Hello %%{email}%%,';
+ const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
+ assert.equal(replacements.length, 1);
+ assert.equal(replacements[0].token.toString(), '/%%\\{email\\}%%/g');
+ assert.equal(replacements[0].id, 'email');
+ assert.equal(replacements[0].getValue(member), 'test@example.com');
+ });
+
+ it('returns correct createdAt', function () {
+ const html = 'Hello %%{created_at}%%,';
+ const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
+ assert.equal(replacements.length, 1);
+ assert.equal(replacements[0].token.toString(), '/%%\\{created_at\\}%%/g');
+ assert.equal(replacements[0].id, 'created_at');
+ assert.equal(replacements[0].getValue(member), '13 March 2023');
+ });
+
+ it('returns missing created at', function () {
+ member.createdAt = null;
+ const html = 'Hello %%{created_at}%%,';
+ const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
+ assert.equal(replacements.length, 1);
+ assert.equal(replacements[0].token.toString(), '/%%\\{created_at\\}%%/g');
+ assert.equal(replacements[0].id, 'created_at');
+ assert.equal(replacements[0].getValue(member), '');
+ });
+
it('supports fallback values', function () {
const html = 'Hey %%{first_name, "there"}%%,';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
@@ -1119,6 +1164,60 @@ describe('Email renderer', function () {
const data = await emailRenderer.getTemplateData({post, newsletter, html, addPaywall: false});
assert.equal(data.newsletter.showCommentCta, true);
});
+
+ it('showSubscriptionDetails is disabled if labs disabled', async function () {
+ labsEnabled = false;
+ const html = '';
+ const post = createModel({
+ posts_meta: createModel({}),
+ loaded: ['posts_meta'],
+ published_at: new Date(0)
+ });
+ const newsletter = createModel({
+ title_font_category: 'serif',
+ title_alignment: 'left',
+ body_font_category: 'sans_serif',
+ show_subscription_details: true
+ });
+ const data = await emailRenderer.getTemplateData({post, newsletter, html, addPaywall: false});
+ assert.equal(data.newsletter.showSubscriptionDetails, false);
+ });
+
+ it('showSubscriptionDetails works is enabled', async function () {
+ labsEnabled = true;
+ const html = '';
+ const post = createModel({
+ posts_meta: createModel({}),
+ loaded: ['posts_meta'],
+ published_at: new Date(0)
+ });
+ const newsletter = createModel({
+ title_font_category: 'serif',
+ title_alignment: 'left',
+ body_font_category: 'sans_serif',
+ show_subscription_details: true
+ });
+ const data = await emailRenderer.getTemplateData({post, newsletter, html, addPaywall: false});
+ assert.equal(data.newsletter.showSubscriptionDetails, true);
+ });
+
+ it('showSubscriptionDetails can be disabled', async function () {
+ labsEnabled = true;
+ const html = '';
+ const post = createModel({
+ posts_meta: createModel({}),
+ loaded: ['posts_meta'],
+ published_at: new Date(0)
+ });
+ const newsletter = createModel({
+ title_font_category: 'serif',
+ title_alignment: 'left',
+ body_font_category: 'sans_serif',
+ show_subscription_details: false
+ });
+ const data = await emailRenderer.getTemplateData({post, newsletter, html, addPaywall: false});
+ assert.equal(data.newsletter.showSubscriptionDetails, false);
+ });
});
describe('createUnsubscribeUrl', function () {
| | | | | | | | | | |