21 changed files with 317 additions and 56 deletions
@ -0,0 +1,6 @@ |
|||
|
|||
export const ServerConfig = { |
|||
port: 3001, |
|||
host: '127.0.0.1', |
|||
client: 'https://example.com' |
|||
}; |
|||
@ -1,5 +0,0 @@ |
|||
|
|||
export const ServerConfig = { |
|||
port: 3001, |
|||
host: '127.0.0.1' |
|||
}; |
|||
@ -0,0 +1,26 @@ |
|||
import path from 'path'; |
|||
import { fileURLToPath } from 'url'; |
|||
import { EmailConfig } from '../../config/email.config.js'; |
|||
import { minutes } from '../utils/common.utils.js'; |
|||
import { TemplateFile, TranslationFile } from '../utils/localization.utils.js'; |
|||
|
|||
export const RestorePasswordEmail = ({username, locale, url}) => { |
|||
const subject = translation.get('email.restore.subject', locale); |
|||
const bodyText = translation.get('email.restore.body', locale, {username, url}); |
|||
return { |
|||
subject, |
|||
text: bodyText, |
|||
html: templates.wrapper.generate({...common, body: bodyText}) |
|||
}; |
|||
}; |
|||
|
|||
const common = { |
|||
title: EmailConfig.upperText, |
|||
company: EmailConfig.bottomText |
|||
}; |
|||
const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
|||
const refreshRate = 60*minutes; |
|||
const translation = new TranslationFile(__dirname+'/texts.json', refreshRate); |
|||
const templates = { |
|||
wrapper: new TemplateFile(__dirname+'/wrapper.html', refreshRate, {html: true}) |
|||
}; |
|||
@ -0,0 +1,10 @@ |
|||
{ |
|||
"en": { |
|||
"email.restore.subject": "Account recovery.", |
|||
"email.restore.body": "Hello, ${username}.\nWe received a request to reset your password.\nFollow the link below to proceed:\n${url}\nIf you didn't request this change, ignore this email." |
|||
}, |
|||
"ru": { |
|||
"email.restore.subject": "Восстановление аккаунта.", |
|||
"email.restore.body": "Здравствуйте, ${username}.\nМы получили запрос на сброс пароля на вашем аккаунте.\nНажмите на ссылку для подтверждения:\n${url}\nЕсли это были не вы, проигнорируйте это письмо." |
|||
} |
|||
} |
|||
@ -0,0 +1,93 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office"> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<meta name="viewport" content="width=device-width,initial-scale=1"> |
|||
<meta name="x-apple-disable-message-reformatting"> |
|||
<title></title> |
|||
<!--[if mso]> |
|||
<noscript> |
|||
<xml> |
|||
<o:OfficeDocumentSettings> |
|||
<o:PixelsPerInch>96</o:PixelsPerInch> |
|||
</o:OfficeDocumentSettings> |
|||
</xml> |
|||
</noscript> |
|||
<![endif]--> |
|||
<style> |
|||
table, td, div, h1, p {font-family: Arial, sans-serif;} |
|||
</style> |
|||
</head> |
|||
<body style="margin:0;padding:0;"> |
|||
<table role="presentation" style="width:100%;border-collapse:collapse;border:0;border-spacing:0;background:#ffffff;"> |
|||
<tr> |
|||
<td align="center" style="padding:0;"> |
|||
<table role="presentation" style="width:602px;border-collapse:collapse;border:1px solid #cccccc;border-spacing:0;text-align:left;"> |
|||
<tr> |
|||
<td align="center" style="padding:40px 0 30px 0;background:#70bbd9;"> |
|||
<img src="https://assets.codepen.io/210284/h1.png" alt="" width="300" style="height:auto;display:block;" /> |
|||
</td> |
|||
</tr> |
|||
<tr> |
|||
<td style="padding:36px 30px 42px 30px;"> |
|||
<table role="presentation" style="width:100%;border-collapse:collapse;border:0;border-spacing:0;"> |
|||
<tr> |
|||
<td style="padding:0 0 36px 0;color:#153643;"> |
|||
<h1 style="font-size:24px;margin:0 0 20px 0;font-family:Arial,sans-serif;">Creating Email Magic</h1> |
|||
<p style="margin:0 0 12px 0;font-size:16px;line-height:24px;font-family:Arial,sans-serif;">Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tempus adipiscing felis, sit amet blandit ipsum volutpat sed. Morbi porttitor, eget accumsan et dictum, nisi libero ultricies ipsum, posuere neque at erat.</p> |
|||
<p style="margin:0;font-size:16px;line-height:24px;font-family:Arial,sans-serif;"><a href="http://www.example.com" style="color:#ee4c50;text-decoration:underline;">In tempus felis blandit</a></p> |
|||
</td> |
|||
</tr> |
|||
<tr> |
|||
<td style="padding:0;"> |
|||
<table role="presentation" style="width:100%;border-collapse:collapse;border:0;border-spacing:0;"> |
|||
<tr> |
|||
<td style="width:260px;padding:0;vertical-align:top;color:#153643;"> |
|||
<p style="margin:0 0 25px 0;font-size:16px;line-height:24px;font-family:Arial,sans-serif;"><img src="https://assets.codepen.io/210284/left.gif" alt="" width="260" style="height:auto;display:block;" /></p> |
|||
<p style="margin:0 0 12px 0;font-size:16px;line-height:24px;font-family:Arial,sans-serif;">Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tempus adipiscing felis, sit amet blandit ipsum volutpat sed. Morbi porttitor, eget accumsan dictum, est nisi libero ultricies ipsum, in posuere mauris neque at erat.</p> |
|||
<p style="margin:0;font-size:16px;line-height:24px;font-family:Arial,sans-serif;"><a href="http://www.example.com" style="color:#ee4c50;text-decoration:underline;">Blandit ipsum volutpat sed</a></p> |
|||
</td> |
|||
<td style="width:20px;padding:0;font-size:0;line-height:0;"> </td> |
|||
<td style="width:260px;padding:0;vertical-align:top;color:#153643;"> |
|||
<p style="margin:0 0 25px 0;font-size:16px;line-height:24px;font-family:Arial,sans-serif;"><img src="https://assets.codepen.io/210284/right.gif" alt="" width="260" style="height:auto;display:block;" /></p> |
|||
<p style="margin:0 0 12px 0;font-size:16px;line-height:24px;font-family:Arial,sans-serif;">Morbi porttitor, eget est accumsan dictum, nisi libero ultricies ipsum, in posuere mauris neque at erat. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tempus adipiscing felis, sit amet blandit ipsum volutpat sed.</p> |
|||
<p style="margin:0;font-size:16px;line-height:24px;font-family:Arial,sans-serif;"><a href="http://www.example.com" style="color:#ee4c50;text-decoration:underline;">In tempus felis blandit</a></p> |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
</td> |
|||
</tr> |
|||
<tr> |
|||
<td style="padding:30px;background:#ee4c50;"> |
|||
<table role="presentation" style="width:100%;border-collapse:collapse;border:0;border-spacing:0;font-size:9px;font-family:Arial,sans-serif;"> |
|||
<tr> |
|||
<td style="padding:0;width:50%;" align="left"> |
|||
<p style="margin:0;font-size:14px;line-height:16px;font-family:Arial,sans-serif;color:#ffffff;"> |
|||
® Someone, Somewhere 2021<br/><a href="http://www.example.com" style="color:#ffffff;text-decoration:underline;">Unsubscribe</a> |
|||
</p> |
|||
</td> |
|||
<td style="padding:0;width:50%;" align="right"> |
|||
<table role="presentation" style="border-collapse:collapse;border:0;border-spacing:0;"> |
|||
<tr> |
|||
<td style="padding:0 0 0 10px;width:38px;"> |
|||
<a href="http://www.twitter.com/" style="color:#ffffff;"><img src="https://assets.codepen.io/210284/tw_1.png" alt="Twitter" width="38" style="height:auto;display:block;border:0;" /></a> |
|||
</td> |
|||
<td style="padding:0 0 0 10px;width:38px;"> |
|||
<a href="http://www.facebook.com/" style="color:#ffffff;"><img src="https://assets.codepen.io/210284/fb_1.png" alt="Facebook" width="38" style="height:auto;display:block;border:0;" /></a> |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
</body> |
|||
</html> |
|||
@ -0,0 +1,63 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office"> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<meta name="viewport" content="width=device-width,initial-scale=1"> |
|||
<meta name="x-apple-disable-message-reformatting"> |
|||
<title></title> |
|||
<!--[if mso]> |
|||
<noscript> |
|||
<xml> |
|||
<o:OfficeDocumentSettings> |
|||
<o:PixelsPerInch>96</o:PixelsPerInch> |
|||
</o:OfficeDocumentSettings> |
|||
</xml> |
|||
</noscript> |
|||
<![endif]--> |
|||
<style> |
|||
table, td, div, h1, p {font-family: Arial, sans-serif;} |
|||
</style> |
|||
</head> |
|||
<body style="margin:0;padding:0;"> |
|||
<table role="presentation" style="width:100%;border-collapse:collapse;border:0;border-spacing:0;background:#ffffff;margin-top: 10px;"> |
|||
<tr> |
|||
<td align="center" style="padding:0;"> |
|||
<table role="presentation" style="width:602px;border-collapse:collapse;border:1px solid #cccccc;border-spacing:0;text-align:left;"> |
|||
<tr> |
|||
<td align="center" style="padding:10px 0 10px 0;background: #5e95f4;background: linear-gradient(135deg, #5e95f4 0%,#4581f1 50%,#2768f3 51%,#1641dc 100%);"> |
|||
<span style="font-size: 48px;color:#ffffff">${title}</span> |
|||
</td> |
|||
</tr> |
|||
<tr> |
|||
<td style="padding:36px 30px 42px 30px;"> |
|||
${body} |
|||
</td> |
|||
</tr> |
|||
<tr> |
|||
<td style="padding:30px;background: #6f6f6f;background: linear-gradient(to bottom, #6f6f6f 0%,#707070 53%,#474747 100%);"> |
|||
<table role="presentation" style="width:100%;border-collapse:collapse;border:0;border-spacing:0;font-size:9px;font-family:Arial,sans-serif;"> |
|||
<tr> |
|||
<td style="padding:0;width:50%;" align="left"> |
|||
<p style="margin:0;font-size:14px;line-height:16px;font-family:Arial,sans-serif;color:#ffffff;"> |
|||
${company}<br/><a href="http://www.example.com" style="color:#ffffff;text-decoration:underline;">Unsubscribe</a> |
|||
</p> |
|||
</td> |
|||
<td style="padding:0;width:50%;" align="right"> |
|||
<table role="presentation" style="border-collapse:collapse;border:0;border-spacing:0;"> |
|||
<tr> |
|||
<td style="padding:0 0 0 10px;width:38px;"> |
|||
<a href="https://twitter.com/inque_n" style="color:#ffffff;"><img src="https://assets.codepen.io/210284/tw_1.png" alt="Twitter" width="38" style="height:auto;display:block;border:0;" /></a> |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
</body> |
|||
</html> |
|||
@ -1,11 +0,0 @@ |
|||
|
|||
export const RestorePasswordEmail = ({username, url}) => ({ |
|||
subject: `Account recovery`, |
|||
text: ` |
|||
Hello, ${username}. |
|||
We received a request to reset your password. |
|||
Follow the link below to proceed: |
|||
${url} |
|||
If you didn't request this change, ignore this email. |
|||
` |
|||
}); |
|||
@ -0,0 +1,57 @@ |
|||
import fs from 'fs'; |
|||
|
|||
/** |
|||
* Plain-text data with auto-refresh. |
|||
*/ |
|||
export class TemplateFile{ |
|||
|
|||
constructor(path, interval, opts){ |
|||
this.path = path; |
|||
this.opts = opts; |
|||
this.reload(); |
|||
if(interval){ |
|||
setInterval(() => this.reload(), interval); |
|||
} |
|||
} |
|||
reload(){ |
|||
fs.stat(this.path, (err, stats) => { |
|||
if(err) throw err; |
|||
if((!this.lastModified) || (stats.mtime > this.lastModified)){ |
|||
this.lastModified = stats.mtime; |
|||
fs.readFile(this.path, {encoding: 'utf-8'}, (err, data) => { |
|||
if(err) throw err; |
|||
this.text = data; |
|||
this.onChange(); |
|||
}); |
|||
} |
|||
}); |
|||
} |
|||
onChange(){ // "virtual"
|
|||
} |
|||
generate(variables, srcText){ |
|||
return (srcText || this.text).replace(/\${(.*?)}/g, (full, name) => { |
|||
const res = variables[name] ?? full; |
|||
return this.opts?.html ? res.replace("\n","<br>\n") : res; |
|||
}); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* JSON internationalization table. |
|||
*/ |
|||
export class TranslationFile extends TemplateFile{ |
|||
|
|||
onChange(){ |
|||
this.data = JSON.parse(this.text); |
|||
} |
|||
get(key, locale, variables){ |
|||
let lang; |
|||
for(lang in this.data){ |
|||
if(locale.substring(0, lang.length) == lang){ |
|||
break; |
|||
} |
|||
} |
|||
const entry = this.data[lang]?.[key]; |
|||
return variables ? this.generate(variables, entry) : entry; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue