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);
},
};
|