-
+
@@ -92,7 +130,7 @@
-
+
|
@@ -101,42 +139,30 @@
-
+
- | |
+ |
-
+
| |
-
- |
- Dear {{username}},
- |
-
+
+
+
+ | |
+
- | |
-
-
- |
- {{paragraph}}
- |
-
-
-
- | |
-
-
- |
+ |
The Pretendo Network team
|
- | |
+ |
|
- |
+ |
@@ -145,8 +171,8 @@
|
- |
- 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 email message was auto-generated, please do not respond. For further assistance, please join our Discord server or make a post on our Forum.
|
diff --git a/src/mailer.ts b/src/mailer.ts
index 0744e14..178acbf 100644
--- a/src/mailer.ts
+++ b/src/mailer.ts
@@ -6,7 +6,6 @@ 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 +27,174 @@ 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)[] = [];
+
+ /**
+ * utility function which returns a table row of the specified height to use as padding.
+ */
+ 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(component, this.addPadding(24));
+
+ 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(16));
+
+ 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(this.addPadding(4), component, this.addPadding(32));
+
+ return this;
+ }
+
+ // parses pnid name and links. set the plaintext bool (false by default) to use no html
+ private parseReplacements(c: emailComponent, plainText: boolean = false): void {
+ // 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]) => {
+ if (key === 'pnid') {
+ if (plainText) {
+ c.text = c.text.replace(/{{pnid}}/g, value);
+ } else {
+ c.text = c.text.replace(/{{pnid}}/g, `${value}`);
+ }
+ }
+ });
+ }
+
+ // replace [links](https://example.com) with html anchor tags or a plaintext representation
+ const linkRegex = /\[(?.*?)\]\((?.*?)\)/g;
+
+ if (linkRegex.test(c.text)) {
+ if (plainText) {
+ c.text = c.text.replace(linkRegex, `$ ($)`);
+ } else {
+ c.text = c.text.replace(linkRegex, `$`);
+ }
+ }
+ }
+
+ // generates the html version of the email
+ public toHTML(): string {
+ let innerHTML = '';
+
+ this.componentArray.forEach((c) => {
+ switch (c.type) {
+ case 'padding':
+ innerHTML += `\n| | `;
+ break;
+ case 'header':
+ this.parseReplacements(c);
+ innerHTML += `\n `;
+ break;
+ case 'paragraph':
+ this.parseReplacements(c);
+ innerHTML += `\n| ${c.text} | `;
+ break;
+ case 'button':
+ innerHTML += `\n| ${c.text} | `;
+ break;
+ }
+ });
+
+ const generatedHTML = genericEmailTemplate.replace('', innerHTML);
+
+ 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) => {
+ switch (c.type) {
+ case 'padding':
+ break;
+ case 'header':
+ this.parseReplacements(c, true);
+ plainText += `\n${c.text}`;
+ break;
+ case 'paragraph':
+ this.parseReplacements(c, true);
+ plainText += `\n${c.text}`;
+ 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';
+
+ plainText = plainText.replace(/(<([^>]+)>)/gi, '');
+
+ return plainText;
+ }
+}
+
export async function sendMail(options: MailerOptions): Promise {
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 = `| | | ${text} | `;
- html = html.replace(//g, button);
- }
+ 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/types/common/mailer-options.ts b/src/types/common/mailer-options.ts
index d3999f7..f8a31ef 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 f364d0a..68343de 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,33 @@ 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.')
+ .addParagraph('If you do not receive this cancellation email, or your subscription is still being charged, please contact @jonbarrow on our Discord server.')
+ .addButton('Join the Discord', '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);
| | |