Skip to content

Commit c6d038b

Browse files
committed
feat: improved discount email
1 parent 4b92353 commit c6d038b

4 files changed

Lines changed: 54 additions & 1 deletion

File tree

config/templates/email/Discounts.ejs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717
</li>
1818
<% }) %>
1919
</ul>
20-
<% if (domain) { %><p><a href="<%= domain %>"><%= application %></a></p><% } %>
20+
<p><%= t('emails.Discounts.disclaimer') %></p>
21+
<p><%- tHtml('emails.footer_subscribe', {
22+
application,
23+
settingsUrl: `${domain}/settings`,
24+
settingsLabel: t('emails.settings')
25+
}) %></p>
2126
</body>
2227
</html>

config/templates/email/locales/de.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{
22
"emails": {
33
"greeting": "Hallo {{name}},",
4+
"settings": "Einstellungen",
5+
"footer_subscribe": "Du erhältst diese E-Mail, weil du Benachrichtigungen von {{application}} abonniert hast. Du kannst deine Benachrichtigungseinstellungen in den <a href=\"{{settingsUrl}}\">{{settingsLabel}}</a> anpassen.",
46
"RegMail": {
57
"subject": "Willkommen bei {{application}}",
68
"heading": "Willkommen bei {{application}}",
@@ -17,6 +19,7 @@
1719
"subject": "Aktuelle Angebote bei {{application}}",
1820
"heading": "Aktuelle Angebote",
1921
"body": "Diese Artikel sind aktuell reduziert:",
22+
"disclaimer": "Die Angebote gelten bis zum angegebenen Zeitpunkt und nur solange der Vorrat reicht",
2023
"until": "bis {{date}}",
2124
"text": "Hallo {{name}},\n\nBei {{application}} gibt es aktuell neue Angebote. Öffne die Anwendung, um sie anzusehen."
2225
},

lib/notifications.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,20 @@ const buildEmail = async (task) => {
183183
? task.language
184184
: fallbackLanguage;
185185
const t = i18n.getFixedT(language);
186+
const escapeHtml = (value) => String(value)
187+
.replaceAll('&', '&amp;')
188+
.replaceAll('<', '&lt;')
189+
.replaceAll('>', '&gt;')
190+
.replaceAll('"', '&quot;')
191+
.replaceAll("'", '&#39;');
192+
const tHtml = (key, options = {}) => t(key, {
193+
...options,
194+
interpolation: {
195+
...options.interpolation,
196+
escapeValue: true,
197+
escape: escapeHtml,
198+
},
199+
});
186200
const extraContext = await definition.buildContext(task);
187201
const context = {
188202
name: task.name,
@@ -194,6 +208,7 @@ const buildEmail = async (task) => {
194208
message: task.custom_message || '',
195209
language,
196210
t,
211+
tHtml,
197212
...(extraContext || {}),
198213
};
199214
const html = await ejs.renderFile(definition.templatePath, context);

test/notifications.test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,36 @@ test('buildEmail renders selected discounts', async () => {
115115
assert.match(email.subject, /Angebote/);
116116
assert.match(email.html, /Testartikel/);
117117
assert.match(email.html, /1\.50/);
118+
assert.match(email.html, /<a href="(?:https?:\/\/[^"]+)?\/settings">Einstellungen<\/a>/);
119+
assert.doesNotMatch(email.html, /&lt;a href=|Settings\.Settings/);
120+
});
121+
122+
test('email HTML translations escape interpolated values', async () => {
123+
const previousApplication = process.env.APPLICATION;
124+
const previousDomain = process.env.DOMAIN;
125+
process.env.APPLICATION = '<script>alert("application")</script>';
126+
process.env.DOMAIN = 'https://example.com/" onclick="alert(1)';
127+
128+
try {
129+
const email = await buildEmail({
130+
type: 'Discounts',
131+
name: 'Ada',
132+
username: 'ada',
133+
email: 'ada@example.com',
134+
uuid: 'test-uuid',
135+
language: 'de',
136+
custom_message: JSON.stringify({ items: [] }),
137+
});
138+
139+
assert.doesNotMatch(email.html, /<script>|onclick="/);
140+
assert.match(email.html, /&lt;script&gt;/);
141+
assert.match(email.html, /&quot; onclick=&quot;/);
142+
} finally {
143+
if (previousApplication === undefined) delete process.env.APPLICATION;
144+
else process.env.APPLICATION = previousApplication;
145+
if (previousDomain === undefined) delete process.env.DOMAIN;
146+
else process.env.DOMAIN = previousDomain;
147+
}
118148
});
119149

120150
test('features can register notification types, templates, and translations', async () => {

0 commit comments

Comments
 (0)