Merge pull request #298 from PretendoNetwork/dev-feature-better-mailer
Some checks failed
Build and Publish Docker Image / Build and Publish Docker Image (amd64) (push) Has been cancelled
Build and Publish Docker Image / Build and Publish Docker Image (arm64) (push) Has been cancelled

This commit is contained in:
limes 2025-11-15 21:30:13 +01:00 committed by GitHub
commit 564b1371be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 437 additions and 319 deletions

26
package-lock.json generated
View File

@ -27,6 +27,7 @@
"fs-extra": "^8.1.0",
"got": "^11.8.2",
"hcaptcha": "^0.1.0",
"he": "^1.2.0",
"image-pixels": "^1.1.1",
"ip2location-nodejs": "^9.6.3",
"is-valid-hostname": "^1.0.2",
@ -58,6 +59,7 @@
"@types/dicer": "^0.2.2",
"@types/express": "^4.17.17",
"@types/fs-extra": "^11.0.1",
"@types/he": "^1.2.3",
"@types/morgan": "^1.9.4",
"@types/ndarray": "^1.0.11",
"@types/node": "^18.14.4",
@ -2381,6 +2383,7 @@
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz",
"integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==",
"license": "MIT",
"peer": true,
"dependencies": {
"cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0",
@ -3344,6 +3347,13 @@
"@types/node": "*"
}
},
"node_modules/@types/he": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@types/he/-/he-1.2.3.tgz",
"integrity": "sha512-q67/qwlxblDzEDvzHhVkwc1gzVWxaNxeyHUBF4xElrvjL11O+Ytze+1fGpBHlr/H9myiBUaUXNnNPmBHxxfAcA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/http-cache-semantics": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
@ -3419,6 +3429,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.112.tgz",
"integrity": "sha512-i+Vukt9POdS/MBI7YrrkkI5fMfwFtOjphSmt4WXYLfwqsfr6z/HdCx7LqT9M7JktGob8WNgj8nFB4TbGNE4Cog==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~5.26.4"
}
@ -3564,6 +3575,7 @@
"integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.34.1",
"@typescript-eslint/types": "8.34.1",
@ -4049,6 +4061,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -5842,6 +5855,7 @@
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@ -5988,6 +6002,7 @@
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.8",
@ -7116,6 +7131,15 @@
"integrity": "sha512-iMrDmH2VpIEKOrcKWidVjI89FdDKTEdZ7PfPWkP27sTazIIkob8YfdY2ezaufAnWBiUUcvzsn0qF+dyXtBH2Vw==",
"license": "MIT"
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"license": "MIT",
"bin": {
"he": "bin/he"
}
},
"node_modules/http-cache-semantics": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
@ -8416,6 +8440,7 @@
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.8.7.tgz",
"integrity": "sha512-5Bo4CrUxrPITrhMKsqUTOkXXo2CoRC5tXxVQhnddCzqDMwRXfyStrxj1oY865g8gaekSBhxAeNkYyUSJvGm9Hw==",
"license": "MIT",
"peer": true,
"dependencies": {
"bson": "^5.5.0",
"kareem": "2.5.1",
@ -10654,6 +10679,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View File

@ -43,6 +43,7 @@
"fs-extra": "^8.1.0",
"got": "^11.8.2",
"hcaptcha": "^0.1.0",
"he": "^1.2.0",
"image-pixels": "^1.1.1",
"ip2location-nodejs": "^9.6.3",
"is-valid-hostname": "^1.0.2",
@ -74,6 +75,7 @@
"@types/dicer": "^0.2.2",
"@types/express": "^4.17.17",
"@types/fs-extra": "^11.0.1",
"@types/he": "^1.2.3",
"@types/morgan": "^1.9.4",
"@types/ndarray": "^1.0.11",
"@types/node": "^18.14.4",
@ -88,4 +90,4 @@
"ndarray": "^1.0.19",
"typescript": "^4.9.5"
}
}
}

View File

@ -1,203 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" lang="en">
<html lang="en">
<head>
<meta name="color-scheme" content="light dark">
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
<style>
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap');
:root {
color-scheme: light dark;
supported-color-schemes:light dark;
}
@media (prefers-color-scheme: light) {
body.email-body,
table.centerer,
table.wrapper {
background-color: #FFFFFF !important;
color: #FFFFFF !important;
}
table.card {
background-color: #673DB6 !important;
}
span.shoutout {
color: #D9C6FA !important;
}
td.confirm-link {
background-color: #9D6FF3 !important;
}
td.confirm-code {
background-color: #D9C6FA !important;
color: #45297A !important;
}
td.notice {
color: #9D6FF3 !important;
}
td.notice a {
color: #673DB6 !important;
}
img.logo {
content: url("https://assets.pretendo.cc/images/pretendo-wordmark-singlecolor-purple.png") !important;
}
}
@media (prefers-color-scheme: dark) {
body.email-body,
table.centerer,
table.wrapper {
background-color: #1B1F3B !important;
color: #FFFFFF !important;
}
table.card {
background-color: #23274A !important;
}
span.shoutout {
color: #CAB1FB !important;
}
td.confirm-link {
background-color: #673DB6 !important;
}
td.confirm-code {
background-color: #373C65 !important;
color: #ffffff !important;
}
td.notice {
color: #8990C1 !important;
}
td.notice a {
color: #CAC1F5 !important;
}
}
</style>
</head>
<body class="email-body" bgcolor="#1B1F3B" style="margin-left: 0; margin-right: 0; margin-top: 0; margin-bottom: 0; padding-left: 0; padding-right: 0; padding-top: 0; padding-bottom: 0; font-family: Poppins, Arial, Helvetica, sans-serif;">
<div style="display:none;">Hello {{username}}! Your Pretendo Network ID activation is almost complete. Please click the link in this email to confirm your e-mail address and complete the activation process.</div>
<table class="centerer" bgcolor="#1B1F3B" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
<tr>
<td align="center">
<table class="wrapper" bgcolor="#1B1F3B" style="font-family: Poppins, Arial, Helvetica, sans-serif;" border="0" cellpadding="0" cellspacing="0" height="100%" width="420px">
<tr>
<td>
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
<tr>
<td width="32px">&nbsp;</td>
<td>
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
<tr>
<td height="36px" style="line-height: 36px;" width="100%">&nbsp;</td>
</tr>
<tr>
<td>
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
<tr>
<td>
<a href="https://pretendo.network">
<img class="logo" width="auto" height="48px" src="https://assets.pretendo.cc/images/pretendo-wordmark-multicolor-purple+white.png" alt="Pretendo">
</a>
</td>
</tr>
<tr>
<td width="100%" height="36px" style="line-height: 36px;">&nbsp;</td>
</tr>
<tr>
<td>
<table class="card" bgcolor="#23274a" style="color: #ffffff; border-radius: 10px;" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
<tr>
<td width="24px" height="100%">&nbsp;</td>
<td>
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
<tr width="100%" height="48px" style="line-height: 48px;">
<td>&nbsp;</td>
</tr>
<tr style="font-size: 24px; font-weight: 700;">
<td>
Hello <span class="shoutout" style="color: #cab1fb;">{{username}}</span>!
</td>
</tr>
<tr>
<td width="100%" height="24px" style="line-height: 24px;">&nbsp;</td>
</tr>
<tr>
<td style="color: #ffffff; ">
Your Pretendo Network ID activation is almost complete. Please click the link below to confirm your e-mail address and complete the activation process.
</td>
</tr>
<tr>
<td width="100%" height="16px" style="line-height: 16px;">&nbsp;</td>
</tr>
<tr>
<td class="confirm-link" bgcolor="#673db6" style="font-size: 14px; font-weight: 700; border-radius: 10px; padding: 12px" align="center">
<a href="{{confirmation-href}}" style="text-decoration: none; color: #ffffff; " width="100%">
Confirm email address
</a>
</td>
</tr>
<tr>
<td width="100%" height="48px" style="line-height: 48px;">&nbsp;</td>
</tr>
<tr>
<td>
You may also enter the following 6-digit code on your console:
</td>
</tr>
<tr>
<td width="100%" height="16px" style="line-height: 16px;">&nbsp;</td>
</tr>
<tr>
<td class="confirm-code" bgcolor="#373c65" style="color: #ffffff; font-size: 14px; font-weight: 700; border-radius: 10px; padding: 12px" align="center">
{{confirmation-code}}
</td>
</tr>
<tr>
<td width="100%" height="48px" style="line-height: 48px;">&nbsp;</td>
</tr>
<tr>
<td>
We hope you have fun using our services!
</td>
</tr>
<tr>
<td width="100%" height="36px" style="line-height: 36px;">&nbsp;</td>
</tr>
<tr>
<td align="right">
The Pretendo Network team
</td>
</tr>
<tr>
<td width="100%" height="24px" style="line-height: 24px;">&nbsp;</td>
</tr>
</table>
</td>
<td width="24px" height="100%">&nbsp;</td>
</tr>
</table>
</td>
</tr>
<tr>
<td width="100%" height="18px" style="line-height: 18px;">&nbsp;</td>
</tr>
<tr>
<td class="notice" style="color: #8990c1; font-size: 12px;">
Note: this email message was auto-generated, please do not respond. For further assistance, please join our <a href="https://invite.gg/pretendo" style="text-decoration: none; color: #ffffff; ">Discord server</a> or make a post on our <a href="https://forum.pretendo.network" style="text-decoration: none; color: #ffffff; ">Forum</a>.
</td>
</tr>
<tr>
<td width="100%" height="48px" style="line-height: 48px;">&nbsp;</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
<td width="32px">&nbsp;</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@ -1,5 +1,7 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" lang="en">
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
lang="en">
<html lang="en">
<head>
<meta name="color-scheme" content="light dark">
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
@ -8,45 +10,65 @@
:root {
color-scheme: light dark;
supported-color-schemes:light dark;
supported-color-schemes: light dark;
}
@media (prefers-color-scheme: light) {
body.email-body,
table.centerer,
table.wrapper {
background-color: #FFFFFF !important;
color: #FFFFFF !important;
background-color: #673DB6 !important;
color: #673DB6 !important;
}
table.card {
background-color: #673DB6 !important;
background-color: #fff !important;
}
span.shoutout {
color: #D9C6FA !important;
color: #9D6FF3 !important;
}
td.confirm-link {
td {
color: #673DB6 !important;
}
td a {
color: #673DB6 !important;
font-weight: 700 !important;
text-decoration: underline !important;
}
td.primary,
td.primary a,
td.primary span {
background-color: #9D6FF3 !important;
color: #fff !important;
}
td.confirm-code {
td.secondary,
td.secondary a,
td.secondary span {
background-color: #D9C6FA !important;
color: #45297A !important;
}
td.notice {
color: #9D6FF3 !important;
color: #c5adf2 !important;
}
td.notice a {
color: #673DB6 !important;
color: #fff !important;
font-weight: 700 !important;
}
td strong,
td b {
font-weight: 700 !important;
color: #9D6FF3 !important;
}
img.logo {
content: url("https://assets.pretendo.cc/images/pretendo-wordmark-singlecolor-purple.png") !important;
content: url("https://assets.pretendo.cc/images/wordmark-white.png") !important;
}
}
@media (prefers-color-scheme: dark) {
body.email-body,
table.centerer,
table.wrapper {
background-color: #1B1F3B !important;
color: #FFFFFF !important;
color: #A1A8D9 !important;
}
table.card {
background-color: #23274A !important;
@ -54,37 +76,131 @@
span.shoutout {
color: #CAB1FB !important;
}
td.confirm-link {
td {
color: #A1A8D9 !important;
}
td a {
color: #fff !important;
font-weight: 700 !important;
text-decoration: underline !important;
}
td.header {
color: #fff !important;
}
td.primary {
background-color: #673DB6 !important;
}
td.confirm-code {
td.secondary {
background-color: #373C65 !important;
color: #ffffff !important;
color: #fff !important;
}
td.signature {
color: #A1A8D9 !important;
}
td.notice {
color: #8990C1 !important;
}
td.notice a {
color: #CAC1F5 !important;
color: #fff !important;
}
td strong,
td b {
font-weight: 700 !important;
color: #fff !important;
}
/* isn't this redundant? no. icloud web doesn't display the logo correctly without this. */
img.logo {
content: url("https://assets.pretendo.cc/images/wordmark-purple-white.png") !important;
}
}
td.button a,
td.button span,
u+.email-body td.button a,
u+.email-body td.button span {
text-decoration: none !important;
}
/* the following specifically targets gmail, because it doesn't support media queries and automatically inverts colors */
u+.email-body,
u+.email-body table.centerer,
u+.email-body table.wrapper {
background-color: #1B1F3B !important;
background-image: linear-gradient(#1B1F3B, #1B1F3B) !important;
color: #fff !important;
}
u+.email-body table.card {
background-color: #23274A !important;
background-image: linear-gradient(#23274A, #23274A) !important;
}
u+.email-body span.shoutout {
color: #fff !important;
}
u+.email-body td {
color: #fff !important;
}
u+.email-body td a {
color: #fff !important;
font-weight: 700 !important;
text-decoration: underline !important;
}
u+.email-body td.primary,
u+.email-body td.primary a,
u+.email-body td.primary span {
background-color: #9D6FF3 !important;
background-image: linear-gradient(#9D6FF3, #9D6FF3) !important;
color: #fff !important;
}
u+.email-body td.secondary,
u+.email-body td.secondary a,
u+.email-body td.secondary span {
background-color: #373C65 !important;
background-image: linear-gradient(#373C65, #373C65) !important;
color: #fff !important;
}
u+.email-body td.notice {
color: #fff !important;
}
u+.email-body td.notice a {
color: #fff !important;
font-weight: 700 !important;
}
u+.email-body strong,
u+.email-body b {
font-weight: 700 !important;
color: #fff !important;
}
u+.email-body .gmail-s {
background: #000 !important;
mix-blend-mode: screen !important;
}
u+.email-body .gmail-d {
background: #000 !important;
mix-blend-mode: difference !important;
}
</style>
</head>
<body class="email-body" bgcolor="#1B1F3B" style="margin-left: 0; margin-right: 0; margin-top: 0; margin-bottom: 0; padding-left: 0; padding-right: 0; padding-top: 0; padding-bottom: 0; font-family: Poppins, Arial, Helvetica, sans-serif;">
<div style="display:none;">{{preview}}</div>
<table class="centerer" bgcolor="#1B1F3B" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
<body class="email-body" bgcolor="#1B1F3B" color="#A1A8D9"
style="margin-left:0;margin-right:0;margin-top:0;margin-bottom:0;padding-left:0;padding-right:0;padding-top:0;padding-bottom:0;font-family:Poppins, Arial, Helvetica, sans-serif;color:#A1A8D9">
<div style="display:none;"><!--plainText--></div>
<table class="centerer" bgcolor="#1B1F3B" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%"
style="max-width:100%;">
<tr>
<td align="center">
<table class="wrapper" bgcolor="#1B1F3B" style="font-family: Poppins, Arial, Helvetica, sans-serif;" border="0" cellpadding="0" cellspacing="0" height="100%" width="420px">
<table class="wrapper" bgcolor="#1B1F3B" style="font-family:Poppins, Arial, Helvetica, sans-serif;" border="0"
cellpadding="0" cellspacing="0" height="100%" width="600px">
<tr>
<td>
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
<tr>
<td width="32px">&nbsp;</td>
<td width="16px">&nbsp;</td>
<td>
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
<tr>
<td height="36px" style="line-height: 36px;" width="100%">&nbsp;</td>
<td style="line-height:4em;" width="100%">&nbsp;</td>
</tr>
<tr>
<td>
@ -92,72 +208,71 @@
<tr>
<td>
<a href="https://pretendo.network">
<img class="logo" width="auto" height="48px" src="https://assets.pretendo.cc/images/pretendo-wordmark-multicolor-purple+white.png" alt="Pretendo">
</a>
<img class="logo" width="auto" height="48px"
src="https://assets.pretendo.cc/images/wordmark-purple-white.png" alt="Pretendo">
</a>
</td>
</tr>
<tr>
<td width="100%" height="36px" style="line-height: 36px;">&nbsp;</td>
<td width="100%" style="line-height:3em;">&nbsp;</td>
</tr>
<tr>
<td>
<table class="card" bgcolor="#23274a" style="color: #ffffff; border-radius: 10px;" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
<table class="card" bgcolor="#23274a" style="color:#A1A8D9;border-radius:10px;"
border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
<tr>
<td width="24px" height="100%">&nbsp;</td>
<td width="36px" height="100%">&nbsp;</td>
<td>
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
<tr width="100%" height="48px" style="line-height: 48px;">
<td>&nbsp;</td>
<!--innerHTML-->
<tr>
<td width="100%" style="line-height:2em;">&nbsp;</td>
</tr>
<tr style="font-size: 24px; font-weight: 700;">
<td>
Dear <span class="shoutout" style="color: #cab1fb;">{{username}}</span>,
<tr>
<td align="right" class="signature">
<div class="gmail-s">
<div class="gmail-d">
The Pretendo Network team
</div>
</div>
</td>
</tr>
<tr>
<td width="100%" height="24px" style="line-height: 24px;">&nbsp;</td>
</tr>
<tr>
<td style="color: #ffffff; ">
{{paragraph}}
</td>
</tr>
<!--{{buttonPlaceholder}}-->
<tr>
<td width="100%" height="36px" style="line-height: 36px;">&nbsp;</td>
</tr>
<tr>
<td align="right">
The Pretendo Network team
</td>
</tr>
<tr>
<td width="100%" height="24px" style="line-height: 24px;">&nbsp;</td>
<td width="100%" style="line-height:3em;">&nbsp;</td>
</tr>
</table>
</td>
<td width="24px" height="100%">&nbsp;</td>
<td width="36px" height="100%">&nbsp;</td>
</tr>
</table>
</td>
</tr>
<tr>
<td width="100%" height="18px" style="line-height: 18px;">&nbsp;</td>
<td width="100%" style="line-height:1.5em;">&nbsp;</td>
</tr>
<tr>
<td class="notice" style="color: #8990c1; font-size: 12px;">
Note: this email message was auto-generated, please do not respond. For further assistance, please join our <a href="https://invite.gg/pretendo" style="text-decoration: none; color: #ffffff; ">Discord server</a> or make a post on our <a href="https://forum.pretendo.network" style="text-decoration: none; color: #ffffff; ">Forum</a>.
<td class="notice" style="color:#8990c1;text-align:center;font-size:14px;">
<div class="gmail-s">
<div class="gmail-d">
Note: This is an automatic email; please do not respond.<br />For assistance, please
visit <a href="https://forum.pretendo.network"
style="text-decoration:none;color:#fff;">forum.pretendo.network</a>.
</div>
</div>
</td>
</tr>
<tr>
<td width="100%" height="48px" style="line-height: 48px;">&nbsp;</td>
<td width="100%" style="line-height:5em;">&nbsp;</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
<td width="32px">&nbsp;</td>
<td width="16px">&nbsp;</td>
</tr>
</table>
</td>
@ -166,5 +281,7 @@
</td>
</tr>
</table>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -2,11 +2,11 @@ import path from 'node:path';
import fs from 'node:fs';
import nodemailer from 'nodemailer';
import * as aws from '@aws-sdk/client-ses';
import { encode } from 'he';
import { config, disabledFeatures } from '@/config-manager';
import type { MailerOptions } from '@/types/common/mailer-options';
const genericEmailTemplate = fs.readFileSync(path.join(__dirname, './assets/emails/genericTemplate.html'), 'utf8');
const confirmationEmailTemplate = fs.readFileSync(path.join(__dirname, './assets/emails/confirmationTemplate.html'), 'utf8');
let transporter: nodemailer.Transporter;
@ -28,31 +28,208 @@ if (!disabledFeatures.email) {
});
}
interface emailComponent {
type: 'header' | 'paragraph';
text: string;
replacements?: emailTextReplacements;
}
interface paddingComponent {
type: 'padding';
size: number;
}
interface buttonComponent {
type: 'button';
text: string;
link?: string;
primary?: boolean;
}
interface emailTextReplacements {
[key: string]: string;
}
export class CreateEmail {
// an array which stores all components of the email
private readonly componentArray: (emailComponent | paddingComponent | buttonComponent)[] = [];
/**
* adds padding of the specified height in em units
*/
private addPadding(size: number): paddingComponent {
return {
type: 'padding',
size
};
}
/**
* adds a header. for greetings, do addHeader("Hi {{pnid}}!", { pnid: "theUsername" })
*/
public addHeader(text: string, replacements?: emailTextReplacements): this {
const component: emailComponent = { type: 'header', text, replacements };
this.componentArray.push(this.addPadding(3), component, this.addPadding(2));
return this;
}
/**
* adds a paragraph. for links, do addParagraph("this is a [named link](https://example.org)."). for greetings, do addParagraph("Hi {{pnid}}!", { pnid: "theUsername" })
*/
public addParagraph(text: string, replacements?: emailTextReplacements): this {
const component: emailComponent = { type: 'paragraph', text, replacements };
this.componentArray.push(component, this.addPadding(1));
return this;
}
/**
* adds a button
*
* @param {String} text the button text
* @param {String} [link] the link
* @param {boolean} [primary] set to false to use the secondary button styles (true by default)
*/
public addButton(text: string, link?: string, primary: boolean = true): this {
const component: buttonComponent = { type: 'button', text, link, primary };
this.componentArray.push(component, this.addPadding(2));
return this;
}
private addGmailDarkModeFix(el: string): string {
return `<div class="gmail-s"><div class="gmail-d">${el}</div></div>`;
}
// parses pnid name and links. set the plaintext bool (false by default) to use no html
private parseReplacements(c: emailComponent, plainText: boolean = false): string {
let tempText = c.text;
// for now only replaces the pnid for shoutouts. could easily be expanded to add more.
if (c?.replacements) {
Object.entries(c.replacements).forEach(([key, value]) => {
const safeValue = encode(value);
if (key === 'pnid') {
if (plainText) {
tempText = tempText.replace(/{{pnid}}/g, safeValue);
} else {
tempText = tempText.replace(/{{pnid}}/g, `<span class="shoutout" style="color:#cab1fb;">${safeValue}</span>`);
}
}
});
}
// wrap <b> and <strong> in a <span> element, to fix color on thunderbird and weight on icloud mail web
const bRegex = /<b ?>.*?<\/b>|<strong ?>.*?<\/strong>/g;
if (!plainText) {
tempText = tempText.replace(bRegex, el => `<span style="color:#fff;font-weight:bold;">${el}</span>`);
}
// replace [links](https://example.com) with html anchor tags or a plaintext representation
const linkRegex = /\[(?<linkText>.*?)\]\((?<linkAddress>.*?)\)/g;
if (plainText) {
tempText = tempText.replace(linkRegex, '$<linkText> ($<linkAddress>)');
} else {
tempText = tempText.replace(linkRegex, '<a href="$<linkAddress>" style="text-decoration:underline;font-weight:700;color:#fff;"><u>$<linkText></u></a>');
}
return tempText;
}
// generates the html version of the email
public toHTML(): string {
let innerHTML = '';
this.componentArray.map((c, i) => {
let el = '';
/* double padding causes issues, and the signature already has padding, so if the last element
* is padding we just yeet it
*/
if (i === this.componentArray.length - 1 && c.type === 'padding') {
return;
}
switch (c.type) {
case 'padding':
innerHTML += `\n<tr><td width="100%" style="line-height:${c.size}em;">&nbsp;</td></tr>`;
break;
case 'header':
el = this.parseReplacements(c);
innerHTML += `\n<tr style="font-size:24px;font-weight:700;color:#fff"><td class="header">${this.addGmailDarkModeFix(el)}</td></tr>`;
break;
case 'paragraph':
el = this.parseReplacements(c);
innerHTML += `\n<tr><td>${this.addGmailDarkModeFix(el)}</td></tr>`;
break;
case 'button':
if (c.link) {
el = `<a href="${c.link}" style="color:#fff;" width="100%">${el}</a>`;
} else {
el = `<span style="color:#fff;" width="100%">${el}</span>`;
}
innerHTML += `\n<tr><td ${c.primary ? 'class="primary button" bgcolor="#673db6"' : 'class="secondary button" bgcolor="#373C65"'} style="font-weight:700;border-radius:10px;padding:12px" align="center">${this.addGmailDarkModeFix(el)}</td></tr>`;
break;
}
});
const generatedHTML = genericEmailTemplate
.replace('<!--innerHTML-->', innerHTML)
.replace('<!--plainText-->', this.toPlainText());
return generatedHTML;
}
// generates the plaintext version that shows up on the email preview (and is shown by plaintext clients)
public toPlainText(): string {
let plainText = '';
this.componentArray.forEach((c) => {
let el = '';
switch (c.type) {
case 'padding':
break;
case 'header':
el = this.parseReplacements(c, true);
plainText += `\n${el}`;
break;
case 'paragraph':
el = this.parseReplacements(c, true);
plainText += `\n${el}`;
break;
case 'button':
if (c.link) {
plainText += `\n\n${c.text}: ${c.link}\n`;
} else {
plainText += ` ${c.text}\n`;
}
break;
}
});
// the signature is baked into the template, so it needs to be added manually to the plaintext version
plainText += '\n\n- The Pretendo Network team';
// and so is the notice about the email being auto-generated
plainText += '\n\nNote: This is an automatic email; please do not respond. For assistance, please visit https://forum.pretendo.network.';
plainText = plainText.replace(/(<([^>]+)>)/gi, '');
return plainText;
}
}
export async function sendMail(options: MailerOptions): Promise<void> {
if (!disabledFeatures.email) {
const { to, subject, username, paragraph, preview, text, link, confirmation } = options;
let html = confirmation ? confirmationEmailTemplate : genericEmailTemplate;
html = html.replace(/{{username}}/g, username);
html = html.replace(/{{paragraph}}/g, paragraph || '');
html = html.replace(/{{preview}}/g, preview || '');
html = html.replace(/{{confirmation-href}}/g, confirmation?.href || '');
html = html.replace(/{{confirmation-code}}/g, confirmation?.code || '');
if (link) {
const { href, text } = link;
const button = `<tr><td width="100%" height="16px" style="line-height: 16px;">&nbsp;</td></tr><tr><td class="confirm-link" bgcolor="#673db6" style="font-size: 14px; font-weight: 700; border-radius: 10px; padding: 12px" align="center"><a href="${href}" style="text-decoration: none; color: #ffffff; " width="100%">${text}</a></td></tr>`;
html = html.replace(/<!--{{buttonPlaceholder}}-->/g, button);
}
const { to, subject, email } = options;
await transporter.sendMail({
from: config.email.from,
to,
subject,
text,
html
text: email.toPlainText(),
html: email.toHTML()
});
}
}

View File

@ -1,16 +1,7 @@
import type { CreateEmail } from '@/mailer';
export interface MailerOptions {
to: string;
subject: string;
username: string;
paragraph?: string;
preview?: string;
text: string;
link?: {
href: string;
text: string;
};
confirmation?: {
href: string;
code: string;
};
email: CreateEmail;
}

View File

@ -4,7 +4,7 @@ import { S3 } from '@aws-sdk/client-s3';
import fs from 'fs-extra';
import bufferCrc32 from 'buffer-crc32';
import { crc32 } from 'crc';
import { sendMail } from '@/mailer';
import { sendMail, CreateEmail } from '@/mailer';
import { SystemType } from '@/types/common/system-types';
import { TokenType } from '@/types/common/token-types';
import { config, disabledFeatures } from '@/config-manager';
@ -201,39 +201,47 @@ export function nascError(errorCode: string): URLSearchParams {
}
export async function sendConfirmationEmail(pnid: mongoose.HydratedDocument<IPNID, IPNIDMethods>): Promise<void> {
const email = new CreateEmail()
.addHeader('Hello {{pnid}}!', { pnid: pnid.username })
.addParagraph('Your <b>Pretendo Network ID</b> activation is almost complete. Please click the link below to confirm your e-mail address and complete the activation process.')
.addButton('Confirm email address', `https://api.pretendo.cc/v1/email/verify?token=${pnid.identification.email_token}`)
.addParagraph('You may also enter the following 6-digit code on your console:')
.addButton(pnid.identification.email_code, '', false)
.addParagraph('We hope you have fun using our services!');
const options = {
to: pnid.email.address,
subject: '[Pretendo Network] Please confirm your email address',
username: pnid.username,
confirmation: {
href: `https://api.pretendo.cc/v1/email/verify?token=${pnid.identification.email_token}`,
code: pnid.identification.email_code
},
text: `Hello ${pnid.username}! \r\n\r\nYour Pretendo Network ID activation is almost complete. Please click the link to confirm your e-mail address and complete the activation process: \r\nhttps://api.pretendo.cc/v1/email/verify?token=${pnid.identification.email_token} \r\n\r\nYou may also enter the following 6-digit code on your console: ${pnid.identification.email_code}`
email
};
await sendMail(options);
}
export async function sendEmailConfirmedEmail(pnid: mongoose.HydratedDocument<IPNID, IPNIDMethods>): Promise<void> {
const email = new CreateEmail()
.addHeader('Dear {{pnid}}!', { pnid: pnid.username })
.addParagraph('Your email address has been confirmed.')
.addParagraph('We hope you have fun on Pretendo Network!');
const options = {
to: pnid.email.address,
subject: '[Pretendo Network] Email address confirmed',
username: pnid.username,
paragraph: 'your email address has been confirmed. We hope you have fun on Pretendo Network!',
text: `Dear ${pnid.username}, \r\n\r\nYour email address has been confirmed. We hope you have fun on Pretendo Network!`
email
};
await sendMail(options);
}
export async function sendEmailConfirmedParentalControlsEmail(pnid: mongoose.HydratedDocument<IPNID, IPNIDMethods>): Promise<void> {
const email = new CreateEmail()
.addHeader('Dear {{pnid}},', { pnid: pnid.username })
.addParagraph('your email address has been confirmed for use with Parental Controls.');
const options = {
to: pnid.email.address,
subject: '[Pretendo Network] Email address confirmed for Parental Controls',
username: pnid.username,
paragraph: 'your email address has been confirmed for use with Parental Controls.',
text: `Dear ${pnid.username}, \r\n\r\nYour email address has been confirmed for use with Parental Controls.`
email
};
await sendMail(options);
@ -254,31 +262,31 @@ export async function sendForgotPasswordEmail(pnid: mongoose.HydratedDocument<IP
// TODO - Handle null token
const email = new CreateEmail()
.addHeader('Dear {{pnid}},', { pnid: pnid.username })
.addParagraph('a password reset has been requested from this account.')
.addParagraph('If you did not request the password reset, please ignore this email. If you did request this password reset, please click the link below to reset your password.')
.addButton('Reset password', `${config.website_base}/account/reset-password?token=${encodeURIComponent(passwordResetToken)}`);
const mailerOptions = {
to: pnid.email.address,
subject: '[Pretendo Network] Forgot Password',
username: pnid.username,
paragraph: 'a password reset has been requested from this account. If you did not request the password reset, please ignore this email. If you did request this password reset, please click the link below to reset your password.',
link: {
text: 'Reset password',
href: `${config.website_base}/account/reset-password?token=${encodeURIComponent(passwordResetToken)}`
},
text: `Dear ${pnid.username}, a password reset has been requested from this account. \r\n\r\nIf you did not request the password reset, please ignore this email. \r\nIf you did request this password reset, please click the link to reset your password: ${config.website_base}/account/reset-password?token=${encodeURIComponent(passwordResetToken)}`
email
};
await sendMail(mailerOptions);
}
export async function sendPNIDDeletedEmail(email: string, username: string): Promise<void> {
export async function sendPNIDDeletedEmail(emailAddress: string, username: string): Promise<void> {
const email = new CreateEmail()
.addHeader('Dear {{pnid}},', { pnid: username })
.addParagraph('your PNID has successfully been deleted.')
.addParagraph('If you had a tier subscription, a separate cancellation email will be sent. If you do not receive this cancellation email, or you are still being charged for your subscription, please contact <b>@jonbarrow</b> on our [Discord server](https://discord.pretendo.network/).');
const options = {
to: email,
to: emailAddress,
subject: '[Pretendo Network] PNID Deleted',
username: username,
link: {
text: 'Discord Server',
href: 'https://discord.com/invite/pretendo'
},
text: `Your PNID ${username} has successfully been deleted. If you had a tier subscription, a separate cancellation email will be sent. If you do not receive this cancellation email, or your subscription is still being charged, please contact @jon on our Discord server`
email
};
await sendMail(options);