diff --git a/package-lock.json b/package-lock.json index 3d57398a..3b598d73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/package.json b/package.json index 7d0c32bf..8e844b4d 100644 --- a/package.json +++ b/package.json @@ -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" } -} \ No newline at end of file +} diff --git a/src/assets/emails/confirmationTemplate.html b/src/assets/emails/confirmationTemplate.html deleted file mode 100644 index 0a946255..00000000 --- a/src/assets/emails/confirmationTemplate.html +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - - - -
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.
- - - - -
- - - - -
- - - - - - -
  - - - - - - - -
 
- - - - - - - - - - - - - - - - - - - -
- - - -
 
- - - - - - -
  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
- Hello {{username}}! -
 
- Your Pretendo Network ID activation is almost complete. Please click the link below to confirm your e-mail address and complete the activation process. -
 
 
- You may also enter the following 6-digit code on your console: -
 
- {{confirmation-code}} -
 
- We hope you have fun using our services! -
 
- The Pretendo Network team -
 
-
 
-
 
- Note: this email message was auto-generated, please do not respond. For further assistance, please join our Discord server or make a post on our Forum. -
 
-
-
 
-
-
- - \ No newline at end of file diff --git a/src/assets/emails/genericTemplate.html b/src/assets/emails/genericTemplate.html index ce875fe9..0c5955fb 100644 --- a/src/assets/emails/genericTemplate.html +++ b/src/assets/emails/genericTemplate.html @@ -1,5 +1,7 @@ - + + @@ -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 { + color: #673DB6 !important; } - td.confirm-link { + 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; } - -
{{preview}}
- + + + +
+ +
@@ -166,5 +281,7 @@
- +
- +
- +
   - + - + - + - - +
  
@@ -92,72 +208,71 @@
- - + +
  
- +
- + - +
   - - - - - - - - - - - - - + + + - + + - - +
 
- Dear {{username}}, -
 
- {{paragraph}} -
  
- The Pretendo Network team + +
+
+ The Pretendo Network team +
+
  
  
  
- Note: this email message was auto-generated, please do not respond. For further assistance, please join our Discord server or make a post on our Forum. + +
+
+ Note: This is an automatic email; please do not respond.
For assistance, please + visit forum.pretendo.network. +
+
  
  
+ + \ No newline at end of file diff --git a/src/assets/images/wordmark-purple-white.png b/src/assets/images/wordmark-purple-white.png new file mode 100644 index 00000000..66c89d53 Binary files /dev/null and b/src/assets/images/wordmark-purple-white.png differ diff --git a/src/assets/images/wordmark-white.png b/src/assets/images/wordmark-white.png new file mode 100644 index 00000000..f2fe07fe Binary files /dev/null and b/src/assets/images/wordmark-white.png differ diff --git a/src/mailer.ts b/src/mailer.ts index 0744e14b..eb7b7a0a 100644 --- a/src/mailer.ts +++ b/src/mailer.ts @@ -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) { }); } -export async function sendMail(options: MailerOptions): Promise { - if (!disabledFeatures.email) { - const { to, subject, username, paragraph, preview, text, link, confirmation } = options; +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; + } - let html = confirmation ? confirmationEmailTemplate : genericEmailTemplate; + /** + * 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)); - 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 || ''); + 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 `
${el}
`; + } - if (link) { - const { href, text } = link; + // 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; - const button = ` ${text}`; - html = html.replace(//g, button); + // 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, `${safeValue}`); + } + } + }); + } + + // wrap and in a element, to fix color on thunderbird and weight on icloud mail web + const bRegex = /.*?<\/b>|.*?<\/strong>/g; + + if (!plainText) { + tempText = tempText.replace(bRegex, el => `${el}`); } + // replace [links](https://example.com) with html anchor tags or a plaintext representation + const linkRegex = /\[(?.*?)\]\((?.*?)\)/g; + + if (plainText) { + tempText = tempText.replace(linkRegex, '$ ($)'); + } else { + tempText = tempText.replace(linkRegex, '$'); + } + + 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 `; + break; + case 'header': + el = this.parseReplacements(c); + innerHTML += `\n${this.addGmailDarkModeFix(el)}`; + break; + case 'paragraph': + el = this.parseReplacements(c); + innerHTML += `\n${this.addGmailDarkModeFix(el)}`; + break; + case 'button': + if (c.link) { + el = `${el}`; + } else { + el = `${el}`; + } + innerHTML += `\n${this.addGmailDarkModeFix(el)}`; + break; + } + }); + + const generatedHTML = genericEmailTemplate + .replace('', innerHTML) + .replace('', 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 { + if (!disabledFeatures.email) { + const { to, subject, email } = options; + await transporter.sendMail({ from: config.email.from, to, subject, - text, - html + text: email.toPlainText(), + html: email.toHTML() }); } } diff --git a/src/middleware/console-status-verification.ts b/src/middleware/console-status-verification.ts index c67c86e9..d5fcacc4 100644 --- a/src/middleware/console-status-verification.ts +++ b/src/middleware/console-status-verification.ts @@ -42,6 +42,21 @@ async function consoleStatusVerificationMiddleware(request: express.Request, res return; } + const certificateDeviceID = parseInt(request.certificate.certificateName.slice(2).split('-')[0], 16); + + if (deviceID !== certificateDeviceID) { + // TODO - Change this to a different error + response.status(400).send(xmlbuilder.create({ + error: { + cause: 'Bad Request', + code: '1600', + message: 'Unable to process request' + } + }).end()); + + return; + } + const serialNumber = getValueFromHeaders(request.headers, 'x-nintendo-serial-number'); // TODO - Verify serial numbers somehow? @@ -122,21 +137,6 @@ async function consoleStatusVerificationMiddleware(request: express.Request, res return; } - const certificateDeviceID = parseInt(request.certificate.certificateName.slice(2).split('-')[0], 16); - - if (deviceID !== certificateDeviceID) { - // TODO - Change this to a different error - response.status(400).send(xmlbuilder.create({ - error: { - cause: 'Bad Request', - code: '1600', - message: 'Unable to process request' - } - }).end()); - - return; - } - if (device.access_level < 0) { response.status(400).send(xmlbuilder.create({ errors: { diff --git a/src/types/common/mailer-options.ts b/src/types/common/mailer-options.ts index d3999f71..f8a31efb 100644 --- a/src/types/common/mailer-options.ts +++ b/src/types/common/mailer-options.ts @@ -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; } diff --git a/src/util.ts b/src/util.ts index f364d0a1..41d90ae4 100644 --- a/src/util.ts +++ b/src/util.ts @@ -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): Promise { + const email = new CreateEmail() + .addHeader('Hello {{pnid}}!', { pnid: pnid.username }) + .addParagraph('Your Pretendo Network ID 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): Promise { + 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): Promise { + 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 { +export async function sendPNIDDeletedEmail(emailAddress: string, username: string): Promise { + 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 @jonbarrow 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);