Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 Fixed newsletter not sending if locale is invalid #21573

Merged
merged 1 commit into from
Nov 7, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 32 additions & 8 deletions ghost/email-service/lib/EmailRenderer.js
Original file line number Diff line number Diff line change
@@ -12,8 +12,10 @@ const {EmailAddressParser} = require('@tryghost/email-addresses');
const {registerHelpers} = require('./helpers/register-helpers');
const crypto = require('crypto');

const DEFAULT_LOCALE = 'en-gb';

// Wrapper function so that i18next-parser can find these strings
const t = (x) => {
const t = (x) => {
return x;
};

@@ -38,10 +40,17 @@ function escapeHtml(unsafe) {
.replace(/'/g, ''');
}

function formatDateLong(date, timezone, locale = 'en-gb') {
if (locale === 'en') {
locale = 'en-gb';
function isValidLocale(locale) {
try {
// Attempt to create a DateTimeFormat with the locale
new Intl.DateTimeFormat(locale);
return true; // No error means it's a valid locale
} catch (e) {
return false; // RangeError means invalid locale
}
}

function formatDateLong(date, timezone, locale = DEFAULT_LOCALE) {
return DateTime.fromJSDate(date).setZone(timezone).setLocale(locale).toLocaleString({
year: 'numeric',
month: 'long',
@@ -185,7 +194,7 @@ class EmailRenderer {
this.#models = models;
this.#t = t;
}

getSubject(post, isTestEmail = false) {
const subject = post.related('posts_meta')?.get('email_subject') || post.get('title');
return isTestEmail ? `[TEST] ${subject}` : subject;
@@ -216,6 +225,21 @@ class EmailRenderer {
};
}

// Locale is user-input, so we need to ensure it's valid
#getValidLocale() {
let locale = this.#settingsCache.get('locale') || DEFAULT_LOCALE;

// Remove any trailing whitespace
locale = locale.trim();

// If the locale is just "en", or is not valid, revert to default
if (locale === 'en' || !isValidLocale(locale)) {
locale = DEFAULT_LOCALE;
}

return locale;
}

getFromAddress(post, newsletter) {
// Clean from address to ensure DMARC alignment
const addresses = this.#emailAddressService.getAddress({
@@ -561,8 +585,8 @@ class EmailRenderer {
* @returns {string}
*/
getMemberStatusText(member) {
const t = this.#t;
const locale = this.#settingsCache.get('locale');
const t = this.#t;
const locale = this.#getValidLocale();

if (member.status === 'free') {
// Not really used, but as a backup
@@ -625,7 +649,7 @@ class EmailRenderer {
*/
buildReplacementDefinitions({html, newsletterUuid}) {
const t = this.#t; // es-lint-disable-line no-shadow
const locale = this.#settingsCache.get('locale');
const locale = this.#getValidLocale();

const baseDefinitions = [
{
65 changes: 63 additions & 2 deletions ghost/email-service/test/email-renderer.test.js
Original file line number Diff line number Diff line change
@@ -130,7 +130,7 @@ describe('Email renderer', function () {
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'free'
};
};
});

it('returns the unsubscribe header replacement by default', function () {
@@ -388,7 +388,7 @@ describe('Email renderer', function () {
email: 'test@example.com',
createdAt: new Date(2023, 2, 13, 12, 0),
status: 'free'
};
};
});

it('handles dates when the locale is en-gb (default)', function () {
@@ -399,6 +399,7 @@ describe('Email renderer', function () {
assert.equal(replacements[0].id, 'created_at');
assert.equal(replacements[0].getValue(member), '13 March 2023');
});

it('handles dates when the locale is fr', function () {
emailRenderer = new EmailRenderer({
urlUtils: {
@@ -427,6 +428,7 @@ describe('Email renderer', function () {
assert.equal(replacements[0].id, 'created_at');
assert.equal(replacements[0].getValue(member), '13 mars 2023');
});

it('handles dates when the locale is en (US)', function () {
emailRenderer = new EmailRenderer({
urlUtils: {
@@ -455,6 +457,65 @@ describe('Email renderer', function () {
assert.equal(replacements[0].id, 'created_at');
assert.equal(replacements[0].getValue(member), '13 March 2023');
});

it('handles dates when the locale has whitespace like "en "', function () {
emailRenderer = new EmailRenderer({
urlUtils: {
urlFor: () => 'http://example.com/subdirectory/'
},
labs: {
isSet: () => labsEnabled
},
settingsCache: {
get: (key) => {
if (key === 'timezone') {
return 'UTC';
}
if (key === 'locale') {
return 'en ';
}
}
},
settingsHelpers: {getMembersValidationKey,createUnsubscribeUrl},
t: t
});
const html = '%%{created_at}%%';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 2);
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('handles dates when the locale is invalid like "(en)"', function () {
emailRenderer = new EmailRenderer({
urlUtils: {
urlFor: () => 'http://example.com/subdirectory/'
},
labs: {
isSet: () => labsEnabled
},
settingsCache: {
get: (key) => {
if (key === 'timezone') {
return 'UTC';
}
if (key === 'locale') {
return '(en)';
}
}
},
settingsHelpers: {getMembersValidationKey,createUnsubscribeUrl},
t: t
});

const html = '%%{created_at}%%';
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
assert.equal(replacements.length, 2);
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');
});
});
describe('isMemberTrialing', function () {
let emailRenderer;