summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/api/routes/auth/verify/resend.ts2
-rw-r--r--src/util/entities/User.ts2
-rw-r--r--src/util/util/Email.ts121
3 files changed, 113 insertions, 12 deletions
diff --git a/src/api/routes/auth/verify/resend.ts b/src/api/routes/auth/verify/resend.ts
index 0c8c4ed9..d9a9cda5 100644
--- a/src/api/routes/auth/verify/resend.ts
+++ b/src/api/routes/auth/verify/resend.ts
@@ -33,7 +33,7 @@ router.post("/", route({}), async (req: Request, res: Response) => {
 		throw new HTTPError("User does not have an email address", 400);
 	}
 
-	await Email.sendVerificationEmail(req.user_id, user.email)
+	await Email.sendVerificationEmail(user, user.email)
 		.then((info) => {
 			console.log("Message sent: %s", info.messageId);
 			return res.sendStatus(204);
diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts
index 66e10297..4a399ed9 100644
--- a/src/util/entities/User.ts
+++ b/src/util/entities/User.ts
@@ -386,7 +386,7 @@ export class User extends BaseClass {
 
 		// send verification email if users aren't verified by default and we have an email
 		if (!Config.get().defaults.user.verified && email) {
-			await Email.sendVerificationEmail(user.id, email)
+			await Email.sendVerificationEmail(user, email)
 				.then((info) => {
 					console.log("Message sent: %s", info.messageId);
 				})
diff --git a/src/util/util/Email.ts b/src/util/util/Email.ts
index 371ba827..9688c3c5 100644
--- a/src/util/util/Email.ts
+++ b/src/util/util/Email.ts
@@ -16,10 +16,14 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import fs from "node:fs";
+import path from "node:path";
 import nodemailer, { Transporter } from "nodemailer";
+import { User } from "../entities";
 import { Config } from "./Config";
 import { generateToken } from "./Token";
 
+const ASSET_FOLDER_PATH = path.join(__dirname, "..", "..", "..", "assets");
 export const EMAIL_REGEX =
 	/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
 
@@ -51,7 +55,20 @@ export function adjustEmail(email?: string): string | undefined {
 export const Email: {
 	transporter: Transporter | null;
 	init: () => Promise<void>;
-	sendVerificationEmail: (id: string, email: string) => Promise<any>;
+	generateVerificationLink: (id: string, email: string) => Promise<string>;
+	sendVerificationEmail: (user: User, email: string) => Promise<any>;
+	doReplacements: (
+		template: string,
+		user: User,
+		emailVerificationUrl?: string,
+		passwordResetUrl?: string,
+		ipInfo?: {
+			ip: string;
+			city: string;
+			region: string;
+			country_name: string;
+		},
+	) => string;
 } = {
 	transporter: null,
 	init: async function () {
@@ -78,25 +95,109 @@ export const Email: {
 			console.log(`[SMTP] Ready`);
 		});
 	},
-	sendVerificationEmail: async function (
-		id: string,
-		email: string,
-	): Promise<any> {
-		if (!this.transporter) return;
+	/**
+	 * Replaces all placeholders in an email template with the correct values
+	 */
+	doReplacements: function (
+		template: string,
+		user: User,
+		emailVerificationUrl?: string,
+		passwordResetUrl?: string,
+		ipInfo?: {
+			ip: string;
+			city: string;
+			region: string;
+			country_name: string;
+		},
+	) {
+		const { instanceName } = Config.get().general;
+		template = template.replaceAll("{instanceName}", instanceName);
+		template = template.replaceAll("{userUsername}", user.username);
+		template = template.replaceAll(
+			"{userDiscriminator}",
+			user.discriminator,
+		);
+		template = template.replaceAll("{userId}", user.id);
+		if (user.phone)
+			template = template.replaceAll(
+				"{phoneNumber}",
+				user.phone.slice(-4),
+			);
+		if (user.email)
+			template = template.replaceAll("{userEmail}", user.email);
+
+		// template specific replacements
+		if (emailVerificationUrl)
+			template = template.replaceAll(
+				"{emailVerificationUrl}",
+				emailVerificationUrl,
+			);
+		if (passwordResetUrl)
+			template = template.replaceAll(
+				"{passwordResetUrl}",
+				passwordResetUrl,
+			);
+		if (ipInfo) {
+			template = template.replaceAll("{ipAddress}", ipInfo.ip);
+			template = template.replaceAll("{locationCity}", ipInfo.city);
+			template = template.replaceAll("{locationRegion}", ipInfo.region);
+			template = template.replaceAll(
+				"{locationCountryName}",
+				ipInfo.country_name,
+			);
+		}
+
+		return template;
+	},
+	/**
+	 *
+	 * @param id user id
+	 * @param email user email
+	 * @returns a verification link for the user
+	 */
+	generateVerificationLink: async function (id: string, email: string) {
 		const token = (await generateToken(id, email)) as string;
 		const instanceUrl =
 			Config.get().general.frontPage || "http://localhost:3001";
 		const link = `${instanceUrl}/verify#token=${token}`;
+		return link;
+	},
+	sendVerificationEmail: async function (
+		user: User,
+		email: string,
+	): Promise<any> {
+		if (!this.transporter) return;
+
+		// generate a verification link for the user
+		const verificationLink = await this.generateVerificationLink(
+			user.id,
+			email,
+		);
+		// load the email template
+		const rawTemplate = fs.readFileSync(
+			path.join(
+				ASSET_FOLDER_PATH,
+				"email_templates",
+				"verify_email.html",
+			),
+			{ encoding: "utf-8" },
+		);
+		// replace email template placeholders
+		const html = this.doReplacements(rawTemplate, user, verificationLink);
+
+		// extract the title from the email template to use as the email subject
+		const subject = html.match(/<title>(.*)<\/title>/)?.[1] || "";
+
+		// // construct the email
 		const message = {
 			from:
 				Config.get().general.correspondenceEmail || "noreply@localhost",
 			to: email,
-			subject: `Verify Email Address for ${
-				Config.get().general.instanceName
-			}`,
-			html: `Please verify your email address by clicking the following link: <a href="${link}">Verify Email</a>`,
+			subject,
+			html,
 		};
 
+		// // send the email
 		return this.transporter.sendMail(message);
 	},
 };