summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--assets/email_templates/new_login_location.html162
-rw-r--r--assets/email_templates/password_changed.html77
-rw-r--r--assets/email_templates/password_reset_request.html145
-rw-r--r--assets/email_templates/phone_removed.html85
-rw-r--r--assets/email_templates/verify_email.html146
-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
8 files changed, 419 insertions, 321 deletions
diff --git a/assets/email_templates/new_login_location.html b/assets/email_templates/new_login_location.html
index 701196cd..e597ac6c 100644
--- a/assets/email_templates/new_login_location.html
+++ b/assets/email_templates/new_login_location.html
@@ -4,108 +4,108 @@
 		<meta charset="UTF-8" />
 		<meta http-equiv="X-UA-Compatible" content="IE=edge" />
 		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+		<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
 		<title>Verify {instanceName} Login from New Location</title>
 
 		<style>
 			* {
 				font-size: 16px;
 				line-height: 24px;
-			}
-			body {
-				color: white;
 				font-family: Arial, Helvetica, sans-serif;
-				background-color: #202225;
 			}
-			.btn {
-				font-size: 15px;
-				border: none;
-				border-radius: 3px;
-				text-decoration: none;
+			p {
 				color: white;
-				cursor: pointer;
-				padding: 15px 19px;
-				background-color: #ff5f00;
-				border-radius: 5px;
-				box-shadow: 0 0 10px rgba(255, 61, 0, 0.1);
 			}
-			.btn:hover {
-				background-color: hsl(22.4, 80%, 50%);
-			}
-			.btn:active {
-				background-color: hsl(22.4, 60%, 50%);
+			.ExternalClass {
+				width: 100%;
 			}
 		</style>
 	</head>
 	<body>
-		<img
-			src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg"
-			alt="Branding"
-			style="
-				width: 100%;
-				max-width: 200px;
-				margin: 0 auto;
-				display: block;
-				padding: 20px;
-			"
-		/>
-		<div
-			style="
-				width: 100%;
-				max-width: 500px;
-				margin: 0 auto;
-				padding: 40px 50px;
-				background-color: rgba(50, 53, 59, 1);
-				border-radius: 5px;
-			"
-		>
-			<p
+		<div style="background-color: #202225;">
+			<img
+				src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg"
+				alt="Branding"
 				style="
-					font-weight: 600;
-					font-size: 20px;
-					letter-spacing: 0.27px;
-					line-height: 24px;
+					width: 100%;
+					max-width: 200px;
+					margin: 0 auto;
+					display: block;
+					padding: 20px;
+				"
+			/>
+			<div
+				style="
+					width: 100%;
+					max-width: 500px;
+					margin: 0 auto;
+					padding: 40px 50px;
+					background-color: #32353b;
+					border-radius: 5px;
 				"
 			>
-				Hey {username},
-			</p>
-			<p>
-				It looks like someone tried to log into your {instanceName}
-				account from a new location. If this is you, follow the link
-				below to authorize logging in from this location on your
-				account. If this isn't you, we suggest changing your password as
-				soon as possible.
-			</p>
-			<p>
-				<strong>IP Address:</strong> {ip}
-				<br />
-				<strong>Location:</strong> {location}
-			</p>
-			<div>
-				<div
+				<p
 					style="
-						display: flex;
-						justify-content: center;
-						padding-bottom: 10px;
+						font-weight: 600;
+						font-size: 20px;
+						letter-spacing: 0.27px;
+						line-height: 24px;
 					"
 				>
-					<a class="btn" href="{verifyUrl}" target="_blank"
-						>Verify Login</a
+					Hey {userUsername},
+				</p>
+				<p>
+					It looks like someone tried to log into your {instanceName}
+					account from a new location. If this is you, follow the link
+					below to authorize logging in from this location on your
+					account. If this isn't you, we suggest changing your
+					password as soon as possible.
+				</p>
+				<p>
+					<strong>IP Address:</strong> {ipAddress}
+					<br />
+					<strong>Location:</strong> {locationCity}, {locationRegion},
+					{locationCountryName}
+				</p>
+				<div>
+					<div
+						style="
+							text-align: center;
+							justify-content: center;
+							padding-bottom: 10px;
+						"
 					>
-				</div>
-				<hr />
-				<div
-					style="
-						display: flex;
-						justify-content: center;
-						flex-direction: column;
-						text-align: center;
-					"
-				>
-					<p>
-						Alternatively, you can directly paste this link into
-						your browser:
-					</p>
-					<a href="{verifyUrl}" target="_blank">{verifyUrl}</a>
+						<a
+							href="{verifyUrl}"
+							target="_blank"
+							style="
+								font-size: 15px;
+								border: none;
+								border-radius: 3px;
+								text-decoration: none;
+								color: white;
+								cursor: pointer;
+								padding: 15px 19px;
+								background-color: #ff5f00;
+								border-radius: 5px;
+							"
+							>Verify Login</a
+						>
+					</div>
+					<hr />
+					<div
+						style="
+							text-align: center;
+							justify-content: center;
+							padding-bottom: 10px;
+						"
+					>
+						<p>
+							Alternatively, you can directly paste this link into
+							your browser:
+						</p>
+						<a href="{verifyUrl}" target="_blank">{verifyUrl}</a>
+					</div>
 				</div>
 			</div>
 		</div>
diff --git a/assets/email_templates/password_changed.html b/assets/email_templates/password_changed.html
index 3f762702..399108a2 100644
--- a/assets/email_templates/password_changed.html
+++ b/assets/email_templates/password_changed.html
@@ -4,57 +4,62 @@
 		<meta charset="UTF-8" />
 		<meta http-equiv="X-UA-Compatible" content="IE=edge" />
 		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+		<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
 		<title>{instanceName} Password Changed</title>
 
 		<style>
 			* {
 				font-size: 16px;
 				line-height: 24px;
+				font-family: Arial, Helvetica, sans-serif;
 			}
-			body {
+			p {
 				color: white;
-				font-family: Arial, Helvetica, sans-serif;
-				background-color: #202225;
+			}
+			.ExternalClass {
+				width: 100%;
 			}
 		</style>
 	</head>
 	<body>
-		<img
-			src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg"
-			alt="Branding"
-			style="
-				width: 100%;
-				max-width: 200px;
-				margin: 0 auto;
-				display: block;
-				padding: 20px;
-			"
-		/>
-		<div
-			style="
-				width: 100%;
-				max-width: 500px;
-				margin: 0 auto;
-				padding: 40px 50px;
-				background-color: rgba(50, 53, 59, 1);
-				border-radius: 5px;
-			"
-		>
-			<p
+		<div style="background-color: #202225;">
+			<img
+				src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg"
+				alt="Branding"
+				style="
+					width: 100%;
+					max-width: 200px;
+					margin: 0 auto;
+					display: block;
+					padding: 20px;
+				"
+			/>
+			<div
 				style="
-					font-weight: 600;
-					font-size: 20px;
-					letter-spacing: 0.27px;
-					line-height: 24px;
+					width: 100%;
+					max-width: 500px;
+					margin: 0 auto;
+					padding: 40px 50px;
+					background-color: #32353b;
+					border-radius: 5px;
 				"
 			>
-				Hey {username},
-			</p>
-			<p>Your {instanceName} password has been changed.</p>
-			<p>
-				If this wasn't done by you, please immediately reset the
-				password to your {instanceName} account.
-			</p>
+				<p
+					style="
+						font-weight: 600;
+						font-size: 20px;
+						letter-spacing: 0.27px;
+						line-height: 24px;
+					"
+				>
+					Hey {userUsername},
+				</p>
+				<p>Your {instanceName} password has been changed.</p>
+				<p>
+					If this wasn't done by you, please immediately reset the
+					password to your {instanceName} account.
+				</p>
+			</div>
 		</div>
 	</body>
 </html>
diff --git a/assets/email_templates/password_reset_request.html b/assets/email_templates/password_reset_request.html
index fc77b47b..ab8f4d23 100644
--- a/assets/email_templates/password_reset_request.html
+++ b/assets/email_templates/password_reset_request.html
@@ -4,103 +4,96 @@
 		<meta charset="UTF-8" />
 		<meta http-equiv="X-UA-Compatible" content="IE=edge" />
 		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+		<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
 		<title>Password Reset Request for {instanceName}</title>
 
 		<style>
 			* {
 				font-size: 16px;
 				line-height: 24px;
-			}
-			body {
-				color: white;
 				font-family: Arial, Helvetica, sans-serif;
-				background-color: #202225;
 			}
-			.btn {
-				font-size: 15px;
-				border: none;
-				border-radius: 3px;
-				text-decoration: none;
+			p {
 				color: white;
-				cursor: pointer;
-				padding: 15px 19px;
-				background-color: #ff5f00;
-				border-radius: 5px;
-				box-shadow: 0 0 10px rgba(255, 61, 0, 0.1);
 			}
-			.btn:hover {
-				background-color: hsl(22.4, 80%, 50%);
-			}
-			.btn:active {
-				background-color: hsl(22.4, 60%, 50%);
+			.ExternalClass {
+				width: 100%;
 			}
 		</style>
 	</head>
 	<body>
-		<img
-			src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg"
-			alt="Branding"
-			style="
-				width: 100%;
-				max-width: 200px;
-				margin: 0 auto;
-				display: block;
-				padding: 20px;
-			"
-		/>
-		<div
-			style="
-				width: 100%;
-				max-width: 500px;
-				margin: 0 auto;
-				padding: 40px 50px;
-				background-color: rgba(50, 53, 59, 1);
-				border-radius: 5px;
-			"
-		>
-			<p
+		<div style="background-color: #202225;">
+			<img
+				src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg"
+				alt="Branding"
 				style="
-					font-weight: 600;
-					font-size: 20px;
-					letter-spacing: 0.27px;
-					line-height: 24px;
+					width: 100%;
+					max-width: 200px;
+					margin: 0 auto;
+					display: block;
+					padding: 20px;
+				"
+			/>
+			<div
+				style="
+					width: 100%;
+					max-width: 500px;
+					margin: 0 auto;
+					padding: 40px 50px;
+					background-color: #32353b;
+					border-radius: 5px;
 				"
 			>
-				Hey {username},
-			</p>
-			<p>
-				Your {instanceName} password can be reset by clicking the button
-				below. If you did not request a new password, please ignore this
-				email.
-			</p>
-			<div>
-				<div
-					style="
-						display: flex;
-						justify-content: center;
-						padding-bottom: 10px;
-					"
-				>
-					<a class="btn" href="{passwordResetUrl}" target="_blank"
-						>Reset Password</a
-					>
-				</div>
-				<hr />
-				<div
+				<p
 					style="
-						display: flex;
-						justify-content: center;
-						flex-direction: column;
-						text-align: center;
+						font-weight: 600;
+						font-size: 20px;
+						letter-spacing: 0.27px;
+						line-height: 24px;
 					"
 				>
-					<p>
-						Alternatively, you can directly paste this link into
-						your browser:
-					</p>
-					<a href="{passwordResetUrl}" target="_blank"
-						>{passwordResetUrl}</a
+					Hey {userUsername},
+				</p>
+				<p>
+					Your {instanceName} password can be reset by clicking the
+					button below. If you did not request a new password, please
+					ignore this email.
+				</p>
+				<div>
+					<div
+						style="
+							text-align: center;
+							justify-content: center;
+							padding-bottom: 10px;
+						"
 					>
+						<a
+							href="{passwordResetUrl}"
+							target="_blank"
+							style="
+								font-size: 15px;
+								border: none;
+								border-radius: 3px;
+								text-decoration: none;
+								color: white;
+								cursor: pointer;
+								padding: 15px 19px;
+								background-color: #ff5f00;
+								border-radius: 5px;
+							"
+							>Reset Password</a
+						>
+					</div>
+					<hr />
+					<div style="text-align: center">
+						<p>
+							Alternatively, you can directly paste this link into
+							your browser:
+						</p>
+						<a href="{passwordResetUrl}" target="_blank"
+							>{passwordResetUrl}</a
+						>
+					</div>
 				</div>
 			</div>
 		</div>
diff --git a/assets/email_templates/phone_removed.html b/assets/email_templates/phone_removed.html
index 1eb52fbe..65807e29 100644
--- a/assets/email_templates/phone_removed.html
+++ b/assets/email_templates/phone_removed.html
@@ -4,61 +4,66 @@
 		<meta charset="UTF-8" />
 		<meta http-equiv="X-UA-Compatible" content="IE=edge" />
 		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+		<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
 		<title>Phone Removed From {instanceName} Account</title>
 
 		<style>
 			* {
 				font-size: 16px;
 				line-height: 24px;
+				font-family: Arial, Helvetica, sans-serif;
 			}
-			body {
+			p {
 				color: white;
-				font-family: Arial, Helvetica, sans-serif;
-				background-color: #202225;
+			}
+			.ExternalClass {
+				width: 100%;
 			}
 		</style>
 	</head>
 	<body>
-		<img
-			src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg"
-			alt="Branding"
-			style="
-				width: 100%;
-				max-width: 200px;
-				margin: 0 auto;
-				display: block;
-				padding: 20px;
-			"
-		/>
-		<div
-			style="
-				width: 100%;
-				max-width: 500px;
-				margin: 0 auto;
-				padding: 40px 50px;
-				background-color: rgba(50, 53, 59, 1);
-				border-radius: 5px;
-			"
-		>
-			<p
+		<div style="background-color: #202225;">
+			<img
+				src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg"
+				alt="Branding"
+				style="
+					width: 100%;
+					max-width: 200px;
+					margin: 0 auto;
+					display: block;
+					padding: 20px;
+				"
+			/>
+			<div
 				style="
-					font-weight: 600;
-					font-size: 20px;
-					letter-spacing: 0.27px;
-					line-height: 24px;
+					width: 100%;
+					max-width: 500px;
+					margin: 0 auto;
+					padding: 40px 50px;
+					background-color: #32353b;
+					border-radius: 5px;
 				"
 			>
-				Hey {username},
-			</p>
-			<p>
-				Your phone number ********{phoneNumber} was recently removed
-				from this account and added to a different {instanceName}
-				account.
-			</p>
-			<p>
-				Please note that your phone number can only be linked to one
-				{instanceName} account at a time.
-			</p>
+				<p
+					style="
+						font-weight: 600;
+						font-size: 20px;
+						letter-spacing: 0.27px;
+						line-height: 24px;
+					"
+				>
+					Hey {userUsername},
+				</p>
+				<p>
+					Your phone number ********{phoneNumber} was recently removed
+					from this account and added to a different {instanceName}
+					account.
+				</p>
+				<p>
+					Please note that your phone number can only be linked to one
+					{instanceName} account at a time.
+				</p>
+			</div>
 		</div>
 	</body>
 </html>
diff --git a/assets/email_templates/verify_email.html b/assets/email_templates/verify_email.html
index f0c11e52..604242c4 100644
--- a/assets/email_templates/verify_email.html
+++ b/assets/email_templates/verify_email.html
@@ -4,103 +4,97 @@
 		<meta charset="UTF-8" />
 		<meta http-equiv="X-UA-Compatible" content="IE=edge" />
 		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+		<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
 		<title>Verify Email Address for {instanceName}</title>
 
 		<style>
 			* {
 				font-size: 16px;
 				line-height: 24px;
-			}
-			body {
-				color: white;
 				font-family: Arial, Helvetica, sans-serif;
-				background-color: #202225;
 			}
-			.btn {
-				font-size: 15px;
-				border: none;
-				border-radius: 3px;
-				text-decoration: none;
+			p {
 				color: white;
-				cursor: pointer;
-				padding: 15px 19px;
-				background-color: #ff5f00;
-				border-radius: 5px;
-				box-shadow: 0 0 10px rgba(255, 61, 0, 0.1);
 			}
-			.btn:hover {
-				background-color: hsl(22.4, 80%, 50%);
-			}
-			.btn:active {
-				background-color: hsl(22.4, 60%, 50%);
+			.ExternalClass {
+				width: 100%;
 			}
 		</style>
 	</head>
 	<body>
-		<img
-			src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg"
-			alt="Branding"
-			style="
-				width: 100%;
-				max-width: 200px;
-				margin: 0 auto;
-				display: block;
-				padding: 20px;
-			"
-		/>
-		<div
-			style="
-				width: 100%;
-				max-width: 500px;
-				margin: 0 auto;
-				padding: 40px 50px;
-				background-color: rgba(50, 53, 59, 1);
-				border-radius: 5px;
-			"
-		>
-			<p
+		<div style="background-color: #202225;">
+			<img
+				src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Orange.svg"
+				alt="Branding"
 				style="
-					font-weight: 600;
-					font-size: 20px;
-					letter-spacing: 0.27px;
-					line-height: 24px;
+					width: 100%;
+					max-width: 200px;
+					margin: 0 auto;
+					display: block;
+					padding: 20px;
+				"
+			/>
+			<div
+				style="
+					width: 100%;
+					max-width: 500px;
+					margin: 0 auto;
+					padding: 40px 50px;
+					background-color: #32353b;
+					border-radius: 5px;
 				"
 			>
-				Hey {username},
-			</p>
-			<p>
-				Thanks for registering for an account on {instanceName}! Before
-				we get started, we just need to confirm that this is you. Click
-				below to verify your email address:
-			</p>
-			<div>
-				<div
-					style="
-						display: flex;
-						justify-content: center;
-						padding-bottom: 10px;
-					"
-				>
-					<a class="btn" href="{verificationUrl}" target="_blank"
-						>Verify Email</a
-					>
-				</div>
-				<hr />
-				<div
+				<p
 					style="
-						display: flex;
-						justify-content: center;
-						flex-direction: column;
-						text-align: center;
+						font-weight: 600;
+						font-size: 20px;
+						letter-spacing: 0.27px;
+						line-height: 24px;
 					"
 				>
-					<p>
-						Alternatively, you can directly paste this link into
-						your browser:
-					</p>
-					<a href="{verificationUrl}" target="_blank"
-						>{verificationUrl}</a
+					Hey {userUsername},
+				</p>
+				<p>
+					Thanks for registering for an account on {instanceName}!
+					Before we get started, we just need to confirm that this is
+					you. Click below to verify your email address:
+				</p>
+				<div>
+					<div
+						style="
+							text-align: center;
+							justify-content: center;
+							padding-bottom: 10px;
+						"
 					>
+						<a
+							class="btn"
+							href="{emailVerificationUrl}"
+							target="_blank"
+							style="
+								font-size: 15px;
+								border: none;
+								border-radius: 3px;
+								text-decoration: none;
+								color: white;
+								cursor: pointer;
+								padding: 15px 19px;
+								background-color: #ff5f00;
+								border-radius: 5px;
+							"
+							>Verify Email</a
+						>
+					</div>
+					<hr />
+					<div style="text-align: center">
+						<p>
+							Alternatively, you can directly paste this link into
+							your browser:
+						</p>
+						<a href="{emailVerificationUrl}" target="_blank"
+							>{emailVerificationUrl}</a
+						>
+					</div>
 				</div>
 			</div>
 		</div>
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);
 	},
 };