Skip to content
Draft
Show file tree
Hide file tree
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
78 changes: 78 additions & 0 deletions emailTemplates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'use strict';

const fs = require('fs');
const path = require('path');

const defaultLocale = 'en';
const localeDir = path.join(__dirname, 'locales');
const availableLocales = new Set(
fs.readdirSync(localeDir)
.filter((name) => name.endsWith('.json'))
.map((name) => path.basename(name, '.json')),
);
const translations = new Map();

const normalizeLocale = (locale) => {
if (typeof locale !== 'string' || locale === '') return defaultLocale;
const normalized = locale.toLowerCase().replace(/_/g, '-');
for (const candidate of [normalized, normalized.split('-')[0]]) {
if (availableLocales.has(candidate)) return candidate;
}
return defaultLocale;
};

const getTranslations = (locale) => {
const resolvedLocale = normalizeLocale(locale);
if (!translations.has(resolvedLocale)) {
translations.set(resolvedLocale, JSON.parse(
fs.readFileSync(path.join(localeDir, `${resolvedLocale}.json`), 'utf8')));
}
return translations.get(resolvedLocale);
};

const translate = (locale, key, replacements = {}) => {
const template = getTranslations(locale)[key] ?? getTranslations(defaultLocale)[key] ?? key;
return template.replace(/\{(\w+)\}/g, (match, name) => {
if (!Object.prototype.hasOwnProperty.call(replacements, name)) return match;
return `${replacements[name]}`;
});
};

const getUserLocale = (userInfo) => normalizeLocale(userInfo && userInfo.language);

const getConfirmationEmail = ({action, locale, padId, padUrl, token}) => {
const actionUrl = `${padUrl}/${action}=${token}`;
const subjectKey = action === 'unsubscribe'
? 'ep_email_notifications.emailUnsubscriptionConfirmationSubject'
: 'ep_email_notifications.emailSubscriptionConfirmationSubject';
const bodyKey = action === 'unsubscribe'
? 'ep_email_notifications.emailUnsubscriptionConfirmationBody'
: 'ep_email_notifications.emailSubscriptionConfirmationBody';
return {
subject: translate(locale, subjectKey, {padId}),
text: translate(locale, bodyKey, {actionUrl, padId}),
};
};

const getNotificationEmail = ({event, locale, padId, padUrl}) => {
const footer = translate(locale, 'ep_email_notifications.emailFooter');
const subjectKey = event === 'end'
? 'ep_email_notifications.emailNotificationEndSubject'
: 'ep_email_notifications.emailNotificationStartSubject';
const bodyKey = event === 'end'
? 'ep_email_notifications.emailNotificationEndBody'
: 'ep_email_notifications.emailNotificationStartBody';
return {
subject: translate(locale, subjectKey, {padId}),
text: translate(locale, bodyKey, {footer, padUrl}),
};
};

module.exports = {
defaultLocale,
getConfirmationEmail,
getNotificationEmail,
getUserLocale,
normalizeLocale,
translate,
};
52 changes: 37 additions & 15 deletions handleMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ const db = require('ep_etherpad-lite/node/db/DB');
const email = require('emailjs');
const randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
const settings = require('ep_etherpad-lite/node/utils/Settings');
const {
getConfirmationEmail,
getUserLocale,
} = require('./emailTemplates');
const util = require('util');
const validator = require('validator');

Expand Down Expand Up @@ -42,7 +46,7 @@ exports.handleMessage = async (hookName, context) => {
const userInfo = context.message.data.userInfo;
if (!userInfo || !userInfo.email || !userInfo.email_option) return;
if (!pluginSettings) {
context.socket.emit("message", {
context.socket.emit('message', {
type: 'COLLABROOM',
data: {
type: 'emailNotificationMissingParams',
Expand Down Expand Up @@ -147,7 +151,7 @@ const subscriptionEmail = async (context, email, emailFound, userInfo, padId) =>
await setAuthorEmailRegistered(userInfo, userInfo.userId, subscribeId, padId);

console.debug('emailSubSucc');
context.socket.emit("message", {
context.socket.emit('message', {
type: 'COLLABROOM',
data: {
type: 'emailSubscriptionSuccess',
Expand All @@ -161,12 +165,18 @@ const subscriptionEmail = async (context, email, emailFound, userInfo, padId) =>
// Send mail to user with the link for validation
let message;
try {
const localizedEmail = getConfirmationEmail({
action: 'subscribe',
locale: getUserLocale(userInfo),
padId,
padUrl: padUrl(padId),
token: subscribeId,
});
message = await util.promisify(server.send.bind(server))({
text: 'Please click on this link in order to validate your subscription to the pad ' +
`${padId}\n${padUrl(padId)}/subscribe=${subscribeId}`,
text: localizedEmail.text,
from: `${fromName} <${fromEmail}>`,
to: userInfo.email,
subject: `Email subscription confirmation for pad ${padId}`,
subject: localizedEmail.subject,
});
} catch (err) {
console.error(err);
Expand All @@ -176,7 +186,7 @@ const subscriptionEmail = async (context, email, emailFound, userInfo, padId) =>
} else if (!validatesAsEmail) {
// Subscription -> failed coz mail malformed.. y'know in general fuck em!
console.debug('Dropped email subscription due to malformed email address');
context.socket.emit("message", {
context.socket.emit('message', {
type: 'COLLABROOM',
data: {
type: 'emailSubscriptionSuccess',
Expand All @@ -192,7 +202,7 @@ const subscriptionEmail = async (context, email, emailFound, userInfo, padId) =>
console.debug('email ', context.message.data.userInfo.email,
'already subscribed to ', context.message.data.padId, ' so sending message to client');

context.socket.emit("message", {
context.socket.emit('message', {
type: 'COLLABROOM',
data: {
type: 'emailSubscriptionSuccess',
Expand All @@ -219,7 +229,7 @@ const unsubscriptionEmail = async (context, emailFound, userInfo, padId) => {

await unsetAuthorEmailRegistered(userInfo, userInfo.userId, unsubscribeId, padId);

context.socket.emit("message", {
context.socket.emit('message', {
type: 'COLLABROOM',
data: {
type: 'emailUnsubscriptionSuccess',
Expand All @@ -233,12 +243,18 @@ const unsubscriptionEmail = async (context, emailFound, userInfo, padId) => {
// Send mail to user with the link for validation
let message;
try {
const localizedEmail = getConfirmationEmail({
action: 'unsubscribe',
locale: getUserLocale(userInfo),
padId,
padUrl: padUrl(padId),
token: unsubscribeId,
});
message = await util.promisify(server.send.bind(server))({
text: 'Please click on this link in order to validate your unsubscription to the pad ' +
`${padId}\n${padUrl(padId)}/unsubscribe=${unsubscribeId}`,
text: localizedEmail.text,
from: `${fromName} <${fromEmail}>`,
to: userInfo.email,
subject: `Email unsubscription confirmation for pad ${padId}`,
subject: localizedEmail.subject,
});
} catch (err) {
console.error(err);
Expand All @@ -250,7 +266,7 @@ const unsubscriptionEmail = async (context, emailFound, userInfo, padId) => {
console.debug(
'Unsubscription: Send client a negative response ', context.message.data.userInfo.email);

context.socket.emit("message", {
context.socket.emit('message', {
type: 'COLLABROOM',
data: {
type: 'emailUnsubscriptionSuccess',
Expand All @@ -270,7 +286,7 @@ const sendUserInfo = (context, emailFound, email, userInfo) => {
const {onStart = true, onEnd = false} = userInfo;
if (emailFound) {
// We send back the options associated to this userId
context.socket.emit("message", {
context.socket.emit('message', {
type: 'COLLABROOM',
data: {
type: 'emailNotificationGetUserInfo',
Expand All @@ -285,7 +301,7 @@ const sendUserInfo = (context, emailFound, email, userInfo) => {
});
} else {
// No options set for this userId
context.socket.emit("message", {
context.socket.emit('message', {
type: 'COLLABROOM',
data: {
type: 'emailNotificationGetUserInfo',
Expand All @@ -309,6 +325,7 @@ const setAuthorEmailRegistered = async (userInfo, authorId, subscribeId, padId)
authorId,
onStart: userInfo.email_onStart,
onEnd: userInfo.email_onEnd,
locale: getUserLocale(userInfo),
subscribeId,
timestamp,
};
Expand All @@ -335,7 +352,12 @@ const unsetAuthorEmailRegistered = async (userInfo, authorId, unsubscribeId, pad
if (!value.pending) value.pending = {};

// add the registered values to the pending section of the object
value.pending[userInfo.email] = {authorId, unsubscribeId, timestamp};
value.pending[userInfo.email] = {
authorId,
locale: getUserLocale(userInfo),
timestamp,
unsubscribeId,
};

// Write the modified datas back in the Db
await db.set(`emailSubscription:${padId}`, value);
Expand Down
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ const setAuthorEmailRegistered = async (userIds, userInfo, email, padId) => {
const timestamp = new Date().getTime();
const registered = {
authorId: userInfo.authorId,
locale: userInfo.locale,
onStart: userInfo.onStart,
onEnd: userInfo.onEnd,
timestamp,
Expand Down
9 changes: 9 additions & 0 deletions locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,13 @@
, "ep_email_notifications.formOptionOnEnd": "aufhört, das Pad zu bearbeiten"
, "ep_email_notifications.formBtnSubscr": "Anmelden"
, "ep_email_notifications.formBtnUnsubscr": "Abmelden"
, "ep_email_notifications.emailFooter": "Sie können diese E-Mails im Einstellungsfenster des Pads abbestellen."
, "ep_email_notifications.emailNotificationStartSubject": "Jemand hat mit der Bearbeitung von {padId} begonnen"
, "ep_email_notifications.emailNotificationStartBody": "Dieses Pad wird gerade bearbeitet:\n <{padUrl}>\n{footer}"
, "ep_email_notifications.emailNotificationEndSubject": "Jemand hat die Bearbeitung von {padId} beendet"
, "ep_email_notifications.emailNotificationEndBody": "Dieses Pad wird nicht mehr bearbeitet:\n <{padUrl}>\n{footer}"
, "ep_email_notifications.emailSubscriptionConfirmationSubject": "Bestätigung der E-Mail-Anmeldung für Pad {padId}"
, "ep_email_notifications.emailSubscriptionConfirmationBody": "Bitte klicken Sie auf diesen Link, um Ihre Anmeldung für das Pad {padId} zu bestätigen\n{actionUrl}"
, "ep_email_notifications.emailUnsubscriptionConfirmationSubject": "Bestätigung der E-Mail-Abmeldung für Pad {padId}"
, "ep_email_notifications.emailUnsubscriptionConfirmationBody": "Bitte klicken Sie auf diesen Link, um Ihre Abmeldung vom Pad {padId} zu bestätigen\n{actionUrl}"
}
9 changes: 9 additions & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,13 @@
, "ep_email_notifications.formOptionOnEnd": "finishes editing this pad"
, "ep_email_notifications.formBtnSubscr": "Subscribe"
, "ep_email_notifications.formBtnUnsubscr": "Unsubscribe"
, "ep_email_notifications.emailFooter": "You can unsubscribe from these emails in the pad's Settings window."
, "ep_email_notifications.emailNotificationStartSubject": "Someone started editing {padId}"
, "ep_email_notifications.emailNotificationStartBody": "This pad is now being edited:\n <{padUrl}>\n{footer}"
, "ep_email_notifications.emailNotificationEndSubject": "Someone finished editing {padId}"
, "ep_email_notifications.emailNotificationEndBody": "This pad is done being edited:\n <{padUrl}>\n{footer}"
, "ep_email_notifications.emailSubscriptionConfirmationSubject": "Email subscription confirmation for pad {padId}"
, "ep_email_notifications.emailSubscriptionConfirmationBody": "Please click on this link in order to validate your subscription to the pad {padId}\n{actionUrl}"
, "ep_email_notifications.emailUnsubscriptionConfirmationSubject": "Email unsubscription confirmation for pad {padId}"
, "ep_email_notifications.emailUnsubscriptionConfirmationBody": "Please click on this link in order to validate your unsubscription to the pad {padId}\n{actionUrl}"
}
9 changes: 9 additions & 0 deletions locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,13 @@
, "ep_email_notifications.formOptionOnEnd": "termine de editar este documento"
, "ep_email_notifications.formBtnSubscr": "Suscribirse"
, "ep_email_notifications.formBtnUnsubscr": "Desuscribirse"
, "ep_email_notifications.emailFooter": "Puedes darte de baja de estos correos desde la ventana de configuración del pad."
, "ep_email_notifications.emailNotificationStartSubject": "Alguien empezó a editar {padId}"
, "ep_email_notifications.emailNotificationStartBody": "Este pad se está editando ahora:\n <{padUrl}>\n{footer}"
, "ep_email_notifications.emailNotificationEndSubject": "Alguien terminó de editar {padId}"
, "ep_email_notifications.emailNotificationEndBody": "La edición de este pad ha finalizado:\n <{padUrl}>\n{footer}"
, "ep_email_notifications.emailSubscriptionConfirmationSubject": "Confirmación de suscripción por correo para el pad {padId}"
, "ep_email_notifications.emailSubscriptionConfirmationBody": "Haz clic en este enlace para validar tu suscripción al pad {padId}\n{actionUrl}"
, "ep_email_notifications.emailUnsubscriptionConfirmationSubject": "Confirmación de desuscripción por correo para el pad {padId}"
, "ep_email_notifications.emailUnsubscriptionConfirmationBody": "Haz clic en este enlace para validar tu desuscripción del pad {padId}\n{actionUrl}"
}
9 changes: 9 additions & 0 deletions locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,13 @@
, "ep_email_notifications.formOptionOnEnd": "a fini d\u2019\u00e9diter le pad"
, "ep_email_notifications.formBtnSubscr": "inscription"
, "ep_email_notifications.formBtnUnsubscr": "d\u00e9sinscription"
, "ep_email_notifications.emailFooter": "Vous pouvez vous d\u00e9sinscrire de ces e-mails dans la fen\u00eatre des param\u00e8tres du pad."
, "ep_email_notifications.emailNotificationStartSubject": "Quelqu\u2019un a commenc\u00e9 \u00e0 modifier {padId}"
, "ep_email_notifications.emailNotificationStartBody": "Ce pad est en cours de modification :\n <{padUrl}>\n{footer}"
, "ep_email_notifications.emailNotificationEndSubject": "Quelqu\u2019un a termin\u00e9 la modification de {padId}"
, "ep_email_notifications.emailNotificationEndBody": "La modification de ce pad est termin\u00e9e :\n <{padUrl}>\n{footer}"
, "ep_email_notifications.emailSubscriptionConfirmationSubject": "Confirmation d\u2019inscription par e-mail pour le pad {padId}"
, "ep_email_notifications.emailSubscriptionConfirmationBody": "Veuillez cliquer sur ce lien pour valider votre inscription au pad {padId}\n{actionUrl}"
, "ep_email_notifications.emailUnsubscriptionConfirmationSubject": "Confirmation de d\u00e9sinscription par e-mail pour le pad {padId}"
, "ep_email_notifications.emailUnsubscriptionConfirmationBody": "Veuillez cliquer sur ce lien pour valider votre d\u00e9sinscription du pad {padId}\n{actionUrl}"
}
9 changes: 9 additions & 0 deletions locales/hu.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,13 @@
, "ep_email_notifications.formOptionOnEnd": "befejezi a jegyzetfüzet szerkesztését"
, "ep_email_notifications.formBtnSubscr": "feliratkozás"
, "ep_email_notifications.formBtnUnsubscr": "leiratkozás"
, "ep_email_notifications.emailFooter": "Ezekről az e-mailekről a pad Beállítások ablakában tud leiratkozni."
, "ep_email_notifications.emailNotificationStartSubject": "Valaki elkezdte szerkeszteni ezt: {padId}"
, "ep_email_notifications.emailNotificationStartBody": "Ezt a padet most szerkesztik:\n <{padUrl}>\n{footer}"
, "ep_email_notifications.emailNotificationEndSubject": "Valaki befejezte ennek a szerkesztését: {padId}"
, "ep_email_notifications.emailNotificationEndBody": "Ennek a padnek a szerkesztése befejeződött:\n <{padUrl}>\n{footer}"
, "ep_email_notifications.emailSubscriptionConfirmationSubject": "{padId} pad e-mailes feliratkozásának megerősítése"
, "ep_email_notifications.emailSubscriptionConfirmationBody": "Kattintson erre a hivatkozásra, hogy megerősítse a {padId} padra való feliratkozását\n{actionUrl}"
, "ep_email_notifications.emailUnsubscriptionConfirmationSubject": "{padId} pad e-mailes leiratkozásának megerősítése"
, "ep_email_notifications.emailUnsubscriptionConfirmationBody": "Kattintson erre a hivatkozásra, hogy megerősítse a {padId} padról való leiratkozását\n{actionUrl}"
}
3 changes: 1 addition & 2 deletions static/js/ep_email.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ exports.handleClientMessage_emailSubscriptionSuccess = (hook, context) => {
$('.ep_email_settings').slideToggle();
$('#options-emailNotifications').prop('checked', false);
}

}
};

Expand All @@ -105,7 +104,6 @@ exports.handleClientMessage_emailUnsubscriptionSuccess = (hook, context) => {
$('.ep_email_settings').slideToggle();
$('#options-emailNotifications').prop('checked', false);
}

}
};

Expand Down Expand Up @@ -219,6 +217,7 @@ const sendEmailToServer = (formName) => {
message.userInfo.email_onStart = $(`#${formName} [name=ep_email_onStart]`).is(':checked');
message.userInfo.email_onEnd = $(`#${formName} [name=ep_email_onEnd]`).is(':checked');
message.userInfo.formName = formName;
message.userInfo.language = document.documentElement.lang || navigator.language || 'en';
message.userInfo.userId = userId;
if (email) {
pad.collabClient.sendMessage(message);
Expand Down
46 changes: 46 additions & 0 deletions static/tests/backend/specs/emailTemplates.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use strict';

const assert = require('node:assert/strict');

const {
getConfirmationEmail,
getNotificationEmail,
getUserLocale,
normalizeLocale,
} = require('../../../../emailTemplates');

{
const email = getConfirmationEmail({
action: 'subscribe',
locale: 'fr-CA',
padId: 'test-pad',
padUrl: 'https://example.test/p/test-pad',
token: 'abc123',
});

assert.equal(email.subject, 'Confirmation d’inscription par e-mail pour le pad test-pad');
assert.match(
email.text,
/Veuillez cliquer sur ce lien pour valider votre inscription au pad test-pad/,
);
assert.match(email.text, /https:\/\/example\.test\/p\/test-pad\/subscribe=abc123/);
}

{
const email = getNotificationEmail({
event: 'end',
locale: 'pt-BR',
padId: 'test-pad',
padUrl: 'https://example.test/p/test-pad',
});

assert.equal(email.subject, 'Someone finished editing test-pad');
assert.match(email.text, /This pad is done being edited:/);
assert.match(email.text, /You can unsubscribe from these emails in the pad's Settings window\./);
}

{
assert.equal(getUserLocale({language: 'de-DE'}), 'de');
assert.equal(normalizeLocale('hu_HU'), 'hu');
assert.equal(normalizeLocale(), 'en');
}
Loading
Loading