From 8383bb7482b439f2bc067d046fbac5a708507993 Mon Sep 17 00:00:00 2001 From: Avris Date: Fri, 3 Dec 2021 22:39:08 +0100 Subject: [PATCH] #284 nicer email template --- locale/_base/translations.suml | 8 +-- locale/de/translations.suml | 8 +-- locale/en/translations.suml | 8 +-- locale/es/translations.suml | 8 +-- locale/fr/translations.suml | 8 +-- locale/gl/translations.suml | 8 +-- locale/ja/translations.suml | 8 +-- locale/nl/translations.suml | 8 +-- locale/no/translations.suml | 8 +-- locale/pl/translations.suml | 8 +-- locale/pt/translations.suml | 8 +-- locale/ru/translations.suml | 8 +-- locale/yi/translations.suml | 9 +-- locale/zh/translations.suml | 8 +-- server/notify.js | 10 +-- server/routes/user.js | 12 +--- src/mailer.js | 121 ++++++++++++++++++++++++++++----- 17 files changed, 151 insertions(+), 105 deletions(-) diff --git a/locale/_base/translations.suml b/locale/_base/translations.suml index 650199ea..7210752c 100644 --- a/locale/_base/translations.suml +++ b/locale/_base/translations.suml @@ -480,11 +480,9 @@ user: emailSent: 'We''ve sent you an email with a 6-digit code. Enter it here. The code is single-use and stays valid for 15 minutes.' userNotFound: 'User not found.' email: - subject: 'Your login code is %code%' - content: | - To confirm your email address, enter the following single-use code on the website: %code%. - - If you didn't order this code, simply ignore this message. + subject: 'Your login code is {{code}}' + instruction: 'To confirm your email address, enter the following single-use code on the website:' + extra: 'If you didn''t order this code, simply ignore this message.' why: > Registering lets you manage your cards ({/@example=like this one}). passwordless: 'The website doesn''t store any passwords. {https://avris.it/blog/passwords-are-passé=More info.}' diff --git a/locale/de/translations.suml b/locale/de/translations.suml index 4dd160e1..448d8fc8 100644 --- a/locale/de/translations.suml +++ b/locale/de/translations.suml @@ -377,11 +377,9 @@ user: emailSent: 'Wir haben dir eine E-Mail mit einem 6-stelligen Code geschickt. Gib ihn hier ein. Der Code ist einmalig verwendbar und bleibt 15 Minuten lang gültig.' userNotFound: 'Nutzer*in nicht gefunden.' email: - subject: 'Dein Logincode ist %code%' - content: | - Um deine E-Mail-Adresse zu bestätigen, gib den folgenden Code auf der Seite ein: %code% - - Wenn du diesen Code nicht angefordert hast, ignoriere einfach diese Meldung. + subject: 'Dein Logincode ist {{code}}' + instruction: 'Um deine E-Mail-Adresse zu bestätigen, gib den folgenden Code auf der Seite ein:' + extra: 'Wenn du diesen Code nicht angefordert hast, ignoriere einfach diese Meldung.' why: > Mit der Registrierung kannst du deine Visitenkarten verwalten ({/@example=wie diese}). passwordless: 'Die Website speichert keine Passwörter. {https://avris.it/blog/passwords-are-passé=Weitere Infos.}' diff --git a/locale/en/translations.suml b/locale/en/translations.suml index 6ecaaed1..0c590178 100644 --- a/locale/en/translations.suml +++ b/locale/en/translations.suml @@ -481,11 +481,9 @@ user: emailSent: 'We''ve sent you an email with a 6-digit code. Enter it here. The code is single-use and stays valid for 15 minutes.' userNotFound: 'User not found.' email: - subject: 'Your login code is %code%' - content: | - To confirm your email address, enter the following single-use code on the website: %code%. - - If you didn't order this code, simply ignore this message. + subject: 'Your login code is {{code}}' + instruction: 'To confirm your email address, enter the following single-use code on the website:' + extra: 'If you didn''t order this code, simply ignore this message.' why: > Registering lets you manage your cards ({/@example=like this one}). passwordless: 'The website doesn''t store any passwords. {https://avris.it/blog/passwords-are-passé=More info.}' diff --git a/locale/es/translations.suml b/locale/es/translations.suml index 52090070..da7c31ea 100644 --- a/locale/es/translations.suml +++ b/locale/es/translations.suml @@ -390,11 +390,9 @@ user: emailSent: 'Te hemos enviado un correo electrónico con un código de 6 dígitos. Introdúcelo aquí. El código es de un solo uso y es válido por 15 minutos.' userNotFound: 'Usuarie no encontrade.' email: - subject: 'Tu código de inicio de sesión es %code%' - content: | - Para confirmar tu dirección de correo electrónico, introduce este código en el sitio web: %code%. - - Si no solicitaste este código, simplemente ignora este mensaje. + subject: 'Tu código de inicio de sesión es {{code}}' + instruction: 'Para confirmar tu dirección de correo electrónico, introduce este código en el sitio web:' + extra: 'Si no solicitaste este código, simplemente ignora este mensaje.' why: > Registrarte te permite manejar tus tarjetas ({/@example=como esta}). passwordless: 'Este sitio web no guarda las contraseñas. {https://avris.it/blog/passwords-are-passé=More info.}' diff --git a/locale/fr/translations.suml b/locale/fr/translations.suml index 66265dfb..aa1e0f8e 100644 --- a/locale/fr/translations.suml +++ b/locale/fr/translations.suml @@ -384,11 +384,9 @@ user: emailSent: 'Nous vous avons envoyé un mail avec un code à 6 chiffres. Entrez-le ici. Ce code est à usage unique et reste utilisable pendant 15 minutes.' userNotFound: 'Cet utilisateur n’existe pas.' email: - subject: 'Votre code de connexion est %code%' - content: | - Pour confirmer votre adresse mail, saisissez ce code sur le site : %code%. - - Si vous n’avez pas demandé ce code, ignorez simplement ce message. + subject: 'Votre code de connexion est {{code}}' + instruction: 'Pour confirmer votre adresse mail, saisissez ce code sur le site :' + extra: 'Si vous n’avez pas demandé ce code, ignorez simplement ce message.' why: > S’inscrire vous permet de gérer vos cartes ({/@example=comme celle-ci}). passwordless: 'Ce site ne stocke aucun mot de passe. {https://avris.it/blog/passwords-are-passé=Plus d’infos.}' diff --git a/locale/gl/translations.suml b/locale/gl/translations.suml index d9c9f07c..6f306a68 100644 --- a/locale/gl/translations.suml +++ b/locale/gl/translations.suml @@ -386,11 +386,9 @@ user: emailSent: 'Te enviamos um email com um código de 6 dígitos. Digite aqui. O código é pode ser usado apenas uma vez e é válido por 15 minutos.' userNotFound: 'Usuarie não encontrade.' email: - subject: 'O código de início da sessão é %code%' - content: | - Para confirmar seu endereço de email, entre com o seguinte código no site: %code%. - - Se não solicitou este código, simplesmente ignore esta mensagem. + subject: 'O código de início da sessão é {{code}}' + instruction: 'Para confirmar seu endereço de email, entre com o seguinte código no site:' + extra: 'Se não solicitou este código, simplesmente ignore esta mensagem.' why: > Registrar-se te permite dirigir os cartões ({/@example=como esta}). passwordless: 'O site não grava qualquer senha. {https://avris.it/blog/passwords-are-passé=More info.}' diff --git a/locale/ja/translations.suml b/locale/ja/translations.suml index d84d788c..2342c731 100644 --- a/locale/ja/translations.suml +++ b/locale/ja/translations.suml @@ -396,11 +396,9 @@ user: emailSent: 'メールアドレスに6桁のコードが送信された後、ここに入力してください。コードは一度しか使用できません。有効期限は今から15分です。' userNotFound: '無効なユーザー名' email: - subject: 'ログインコードは%code%です。' - content: | - ウェブサイトでこのコードを入力して、あなたのメールアドレスを確認します。%code% - - このコードは求めなかった場合は、このメッセージを無視してください。 + subject: 'ログインコードは{{code}}です。' + instruction: 'ウェブサイトでこのコードを入力して、あなたのメールアドレスを確認します。' + extra: 'このコードは求めなかった場合は、このメッセージを無視してください。' why: > ご登録いただいた方は、カードの設定を行うことができます。({/@example=こんなに}). passwordless: 'このウェブサイトはパスワードを保存しません。 {https://avris.it/blog/passwords-are-passé=詳細はこちら。}' diff --git a/locale/nl/translations.suml b/locale/nl/translations.suml index 6921e78a..8ba9282a 100644 --- a/locale/nl/translations.suml +++ b/locale/nl/translations.suml @@ -367,11 +367,9 @@ user: emailSent: 'We hebben een email verstuurd met een code bestaande uit 6 getallen. Voer deze code hier in. De code is voor eenmalig gebruik en vervalt na 15 minuten.' userNotFound: 'Gebruiker niet gevonden.' email: - subject: 'Jouw logincode is %code%' - content: | - Voer de volgende code in om jouw e-mailadres te bevestigen: %code%. - - Als je deze code niet hebt aangevraagd, kun je dit bericht gewoon negeren. + subject: 'Jouw logincode is {{code}}' + instruction: 'Voer de volgende code in om jouw e-mailadres te bevestigen:' + extra: 'Als je deze code niet hebt aangevraagd, kun je dit bericht gewoon negeren.' why: > Door te registreren kun je een kaart ({/@example=zoals deze}) maken. passwordless: 'De website slaat geen wachtwoorden op. {https://avris.it/blog/passwords-are-passé=Meer info.}' diff --git a/locale/no/translations.suml b/locale/no/translations.suml index 81503656..0bb7f8bb 100644 --- a/locale/no/translations.suml +++ b/locale/no/translations.suml @@ -378,11 +378,9 @@ user: emailSent: 'Vi har sendt deg en email med en 6 sifret kode. Skriv den ned her. Koden er en engangskode og kan brukes i 15 minutter.' userNotFound: 'Bruker ikke funnet.' email: - subject: 'Din Logg inn kode er %code%' - content: | - For å bekrefte din email addresse, vennligst skriv den følgende koden på nettsiden: %code%. - - Hvis du ikke spurte om denne koden, ignorer denne meldingen. + subject: 'Din Logg inn kode er {{code}}' + instruction: 'For å bekrefte din email addresse, vennligst skriv den følgende koden på nettsiden:' + extra: 'Hvis du ikke spurte om denne koden, ignorer denne meldingen.' why: > Å registrere seg lar deg redigere kortene dine ({/@example=sånn som denne}). passwordless: 'Denne nettsiden lagrer ingen passord. {https://avris.it/blog/passwords-are-passé=More info.}' diff --git a/locale/pl/translations.suml b/locale/pl/translations.suml index 2266bde4..73a60aeb 100644 --- a/locale/pl/translations.suml +++ b/locale/pl/translations.suml @@ -1178,11 +1178,9 @@ user: emailSent: 'Na Twój adres wysłałośmy email z sześciocyfrowym kodem. Wpisz go poniżej. Kod jest jednorazowy i ważny przez 15 minut.' userNotFound: 'Konto nie zostało znalezione.' email: - subject: 'Twój kod logowania to %code%' - content: | - Aby potwierdzić swój adres email, wpisz na stronie następujący jednorazowy kod: %code%. - - Jeśli nie zamawiałxś tego kodu, po prostu zignoruj tę wiadomość. + subject: 'Twój kod logowania to {{code}}' + instruction: 'Aby potwierdzić swój adres email, wpisz na stronie następujący jednorazowy kod:' + extra: 'Jeśli nie zamawiałxś tego kodu, po prostu zignoruj tę wiadomość.' why: > Założenie konta pozwala na zarządzanie swoimi wizytówkami ({/@example=takimi jak ta}). passwordless: 'Strona nie zapisuje żadnych haseł. {https://avris.it/blog/passwords-are-passé=Więcej info.}' diff --git a/locale/pt/translations.suml b/locale/pt/translations.suml index 6567cbd9..6ed39fdc 100644 --- a/locale/pt/translations.suml +++ b/locale/pt/translations.suml @@ -386,11 +386,9 @@ user: emailSent: 'Te enviamos um email com um código de 6 dígitos. Digite aqui. O código é pode ser usado apenas uma vez e é válido por 15 minutos.' userNotFound: 'Usuarie não encontrade.' email: - subject: 'O código de início da sessão é %code%' - content: | - Para confirmar seu endereço de email, entre com o seguinte código no site: %code%. - - Se não solicitou este código, simplesmente ignore esta mensagem. + subject: 'O código de início da sessão é {{code}}' + instruction: 'Para confirmar seu endereço de email, entre com o seguinte código no site:' + extra: 'Se não solicitou este código, simplesmente ignore esta mensagem.' why: > Registrar-se te permite dirigir os cartões ({/@example=como esta}). passwordless: 'O site não grava qualquer senha. {https://avris.it/blog/passwords-are-passé=More info.}' diff --git a/locale/ru/translations.suml b/locale/ru/translations.suml index 3a026232..1c634108 100644 --- a/locale/ru/translations.suml +++ b/locale/ru/translations.suml @@ -367,11 +367,9 @@ user: emailSent: 'Мы отправили на указанную вами почту письмо с шестизначным кодом. Пожалуйста, введите его сюда. Код одноразовый и работает в течение 15-ти минут.' userNotFound: 'Пользователь не найден.' email: - subject: 'Ваш код для авторизации: %code%' - content: | - Введите на сайте этот код для подтверждения электронной почты: %code%. - - Если вы не оформляли запрос на код, то просто проигнорируйте это письмо. + subject: 'Ваш код для авторизации: {{code}}' + instruction: 'Введите на сайте этот код для подтверждения электронной почты:' + extra: 'Если вы не оформляли запрос на код, то просто проигнорируйте это письмо.' why: > Регистрация позволяет вам управлять своими аккаунтами/карточками ({/@excemple=как, например, этой}). passwordless: 'Сайт не хранит пароли. {https://avris.it/blog/passwords-are-passé=Больше информации}' diff --git a/locale/yi/translations.suml b/locale/yi/translations.suml index fc82f439..b00ee6da 100644 --- a/locale/yi/translations.suml +++ b/locale/yi/translations.suml @@ -385,12 +385,9 @@ user: emailSent: 'We''ve sent you an email with a 6-digit code. Enter it here. The code is single-use and stays valid for 15 minutes.' userNotFound: 'User not found.' email: - subject: 'Your login code is %code%' - # TODO change: To confirm your email address, enter the following code on the website: %code%. - content: | - To confirm your email address, use the following code: %code%. - - If you didn't order this code, simply ignore this message. + subject: 'Your login code is {{code}}' + instruction: 'To confirm your email address, enter the following single-use code on the website:' + extra: 'If you didn''t order this code, simply ignore this message.' why: > Registering lets you manage your cards ({/@example=like this one}). passwordless: 'The website doesn''t store any passwords. {https://avris.it/blog/passwords-are-passé=More info.}' diff --git a/locale/zh/translations.suml b/locale/zh/translations.suml index 09fe63a8..004782c4 100644 --- a/locale/zh/translations.suml +++ b/locale/zh/translations.suml @@ -350,12 +350,10 @@ user: emailSent: '我們已經通過電子郵件向您發送了6位數字的代碼。請在這裡輸入。代碼是可以用一次,有效期為15分鐘。' userNotFound: '找不到用戶。' email: - subject: '你的登錄代碼是:%code%' + subject: '你的登錄代碼是:{{code}}' # TODO change: To confirm your email address, enter the following code on the website: %code%. - content: | - 要確認你的電郵地址,請使用以下代碼: %code%. - - 如果你沒有要求這個代碼,請忽略消息。 + instruction: '要確認你的電郵地址,請使用以下代碼:' + extra: '如果你沒有要求這個代碼,請忽略消息。' why: > 註冊可以讓你管理你的卡({/@example=像這個})。 passwordless: '該網站不存儲任何密碼。 {https://avris.it/blog/passwords-are-passé=更多信息。}' diff --git a/server/notify.js b/server/notify.js index d1f5e139..6e2a6c89 100644 --- a/server/notify.js +++ b/server/notify.js @@ -60,14 +60,10 @@ async function notify() { if (!awaitingModerationGrouped.hasOwnProperty(email)) { continue; } - const message = awaitingModerationGrouped[email]; - console.log('Sending email:', email, message); + const stats = awaitingModerationGrouped[email]; + console.log('Sending email:', email, stats); - mailer( - email, - '[Pronouns.page] There are entries awaiting moderation', - 'Entries awaiting moderation: \n' + JSON.stringify(message, null, 4), - ); + mailer(email, 'notify', { stats }); } await db.close(); diff --git a/server/routes/user.js b/server/routes/user.js index d25a16f9..43728b30 100644 --- a/server/routes/user.js +++ b/server/routes/user.js @@ -255,11 +255,7 @@ router.post('/user/init', handleErrorAsync(async (req, res) => { async () => { codeKey = await saveAuthenticator(req.db, 'email', user, payload, 15); - mailer( - payload.email, - `[${translations.title}] ${translations.user.login.email.subject.replace('%code%', payload.code)}`, - translations.user.login.email.content.replace('%code%', payload.code), - ) + mailer(payload.email, 'confirmCode', { code: payload.code }); }, async () => { const auth = await findLatestEmailAuthenticator(req.db, payload.email, 'email'); @@ -342,11 +338,7 @@ router.post('/user/change-email', handleErrorAsync(async (req, res) => { const authId = await saveAuthenticator(req.db, 'changeEmail', req.user, payload, 15); - mailer( - payload.to, - `[${translations.title}] ${translations.user.login.email.subject.replace('%code%', payload.code)}`, - translations.user.login.email.content.replace('%code%', payload.code), - ) + mailer(payload.to, 'confirmCode', { code: payload.code }); return res.json({ authId }); } diff --git a/src/mailer.js b/src/mailer.js index 474cbf0c..b63d871c 100644 --- a/src/mailer.js +++ b/src/mailer.js @@ -1,22 +1,109 @@ const mailer = require('mailer'); +const fs = require('fs'); +const Suml = require('suml'); -module.exports = (to, subject, body = undefined, html = undefined) => { +const color = '#C71585'; +const logo = fs.readFileSync(__dirname + '/../node_modules/@fortawesome/fontawesome-pro/svgs/light/tags.svg').toString('utf-8'); +const logoEncoded = 'data:image/svg+xml,' + encodeURIComponent(logo.replace(' new Suml().parse(fs.readFileSync(`${__dirname}/../data/${name}.suml`).toString()); +const translations = loadSuml('translations'); + +const sendEmail = (to, subject, body = undefined, html = undefined) => { mailer.send({ - host: process.env.MAILER_HOST, - port: parseInt(process.env.MAILER_PORT), - ssl: parseInt(process.env.MAILER_PORT) === 465, - authentication: 'login', - username: process.env.MAILER_USER, - password: process.env.MAILER_PASS, - from: process.env.MAILER_FROM, - to, - subject, - body, - html, + host: process.env.MAILER_HOST, + port: parseInt(process.env.MAILER_PORT), + ssl: parseInt(process.env.MAILER_PORT) === 465, + authentication: 'login', + username: process.env.MAILER_USER, + password: process.env.MAILER_PASS, + from: process.env.MAILER_FROM, + to, + subject, + body, + html, + }, + function(err){ + if (err) { + console.error(err); + } + }); +}; + +const templates = { + base: { + subject: `[[title]] » {{content}}`, + html: ` +
+
+ Logo + [[title]] +
+
+ {{content}} +
+
+ `, }, - function(err){ - if (err) { - console.error(err); - } - }); + notify: { + subject: 'There are entries awaiting moderation', + text: 'Entries awaiting moderation:\n\n{{list:stats}}', + html: ` +

Entries awaiting moderation

+ + `, + }, + confirmCode: { + subject: '[[user.login.email.subject]]', + text: `[[user.login.email.instruction]]\n\n{{code}}\n\n[[user.login.email.extra]]`, + html: ` +

[[user.login.email.instruction]]

+

{{code}}

+

[[user.login.email.extra]]

+ `, + } } + +const applyTemplate = (template, context, params) => { + template = templates[template][context]; + + if (templates.base[context] !== undefined) { + template = templates.base[context].replace('{{content}}', template); + } + + template = template.replace(/\[\[([^\]]+)]]/g, m => { + let x = translations; + for (let part of m.substring(2, m.length - 2).split('.')) { + x = x[part]; + } + return x; + }); + + template = template.replace(/{{([^}]+)}}/g, m => { + const key = m.substring(2, m.length - 2); + if (key.startsWith('list:')) { + const value = params[key.substring(5)]; + if (Array.isArray(value)) { + return context === 'html' + ? value.map(s => `
  • ${s}
  • `).join('') + : value.map(s => ` - ${s}`).join('\n'); + } else { + return context === 'html' + ? Object.keys(value).map(s => `
  • ${s}: ${value[s]}
  • `).join('') + : Object.keys(value).map(s => ` - ${s}: ${value[s]}`).join('\n'); + } + } + return params[key]; + }); + + return template; +} + +module.exports = (to, template, params = {}) => { + sendEmail( + to, + applyTemplate(template, 'subject', params), + applyTemplate(template, 'text', params), + applyTemplate(template, 'html', params), + ); +};