diff --git a/src/api/Server.ts b/src/api/Server.ts
index 7eb4e6f1..aec47818 100644
--- a/src/api/Server.ts
+++ b/src/api/Server.ts
@@ -16,28 +16,29 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import "missing-native-js-functions";
-import { Server, ServerOptions } from "lambert-server";
-import { Authentication, CORS } from "./middlewares/";
import {
Config,
+ Email,
initDatabase,
initEvent,
JSONReplacer,
+ registerRoutes,
Sentry,
WebAuthn,
} from "@fosscord/util";
-import { ErrorHandler } from "./middlewares/ErrorHandler";
-import { BodyParser } from "./middlewares/BodyParser";
-import { Router, Request, Response } from "express";
+import { Request, Response, Router } from "express";
+import { Server, ServerOptions } from "lambert-server";
+import "missing-native-js-functions";
+import morgan from "morgan";
import path from "path";
+import { red } from "picocolors";
+import { Authentication, CORS } from "./middlewares/";
+import { BodyParser } from "./middlewares/BodyParser";
+import { ErrorHandler } from "./middlewares/ErrorHandler";
import { initRateLimits } from "./middlewares/RateLimit";
import TestClient from "./middlewares/TestClient";
import { initTranslation } from "./middlewares/Translation";
-import morgan from "morgan";
import { initInstance } from "./util/handlers/Instance";
-import { registerRoutes } from "@fosscord/util";
-import { red } from "picocolors";
export type FosscordServerOptions = ServerOptions;
@@ -63,6 +64,7 @@ export class FosscordServer extends Server {
await initDatabase();
await Config.init();
await initEvent();
+ await Email.init();
await initInstance();
await Sentry.init(this.app);
WebAuthn.init();
diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts
index ea0aa312..771f0de8 100644
--- a/src/api/middlewares/Authentication.ts
+++ b/src/api/middlewares/Authentication.ts
@@ -16,10 +16,10 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import { NextFunction, Request, Response } from "express";
-import { HTTPError } from "lambert-server";
import { checkToken, Config, Rights } from "@fosscord/util";
import * as Sentry from "@sentry/node";
+import { NextFunction, Request, Response } from "express";
+import { HTTPError } from "lambert-server";
export const NO_AUTHORIZATION_ROUTES = [
// Authentication routes
@@ -28,6 +28,9 @@ export const NO_AUTHORIZATION_ROUTES = [
"/auth/location-metadata",
"/auth/mfa/totp",
"/auth/mfa/webauthn",
+ "/auth/verify",
+ "/auth/forgot",
+ "/auth/reset",
// Routes with a seperate auth system
"/webhooks/",
// Public information endpoints
diff --git a/src/api/routes/auth/forgot.ts b/src/api/routes/auth/forgot.ts
new file mode 100644
index 00000000..faa43dbb
--- /dev/null
+++ b/src/api/routes/auth/forgot.ts
@@ -0,0 +1,92 @@
+import { getIpAdress, route, verifyCaptcha } from "@fosscord/api";
+import {
+ Config,
+ Email,
+ FieldErrors,
+ ForgotPasswordSchema,
+ User,
+} from "@fosscord/util";
+import { Request, Response, Router } from "express";
+import { HTTPError } from "lambert-server";
+const router = Router();
+
+router.post(
+ "/",
+ route({ body: "ForgotPasswordSchema" }),
+ async (req: Request, res: Response) => {
+ const { login, captcha_key } = req.body as ForgotPasswordSchema;
+
+ const config = Config.get();
+
+ if (
+ config.password_reset.requireCaptcha &&
+ config.security.captcha.enabled
+ ) {
+ const { sitekey, service } = config.security.captcha;
+ if (!captcha_key) {
+ return res.status(400).json({
+ captcha_key: ["captcha-required"],
+ captcha_sitekey: sitekey,
+ captcha_service: service,
+ });
+ }
+
+ const ip = getIpAdress(req);
+ const verify = await verifyCaptcha(captcha_key, ip);
+ if (!verify.success) {
+ return res.status(400).json({
+ captcha_key: verify["error-codes"],
+ captcha_sitekey: sitekey,
+ captcha_service: service,
+ });
+ }
+ }
+
+ const user = await User.findOneOrFail({
+ where: [{ phone: login }, { email: login }],
+ select: ["username", "id", "disabled", "deleted", "email"],
+ relations: ["security_keys"],
+ }).catch(() => {
+ throw FieldErrors({
+ login: {
+ message: req.t("auth:password_reset.EMAIL_DOES_NOT_EXIST"),
+ code: "EMAIL_DOES_NOT_EXIST",
+ },
+ });
+ });
+
+ if (!user.email)
+ throw FieldErrors({
+ login: {
+ message:
+ "This account does not have an email address associated with it.",
+ code: "NO_EMAIL",
+ },
+ });
+
+ if (user.deleted)
+ return res.status(400).json({
+ message: "This account is scheduled for deletion.",
+ code: 20011,
+ });
+
+ if (user.disabled)
+ return res.status(400).json({
+ message: req.t("auth:login.ACCOUNT_DISABLED"),
+ code: 20013,
+ });
+
+ return await Email.sendResetPassword(user, user.email)
+ .then(() => {
+ return res.sendStatus(204);
+ })
+ .catch((e) => {
+ console.error(
+ `Failed to send password reset email to ${user.username}#${user.discriminator}: ${e}`,
+ );
+ throw new HTTPError("Failed to send password reset email", 500);
+ });
+ },
+);
+
+export default router;
diff --git a/src/api/routes/auth/login.ts b/src/api/routes/auth/login.ts
index 2b97ec10..e6616731 100644
--- a/src/api/routes/auth/login.ts
+++ b/src/api/routes/auth/login.ts
@@ -77,6 +77,7 @@ router.post(
"mfa_enabled",
"webauthn_enabled",
"security_keys",
+ "verified",
],
relations: ["security_keys"],
}).catch(() => {
@@ -102,6 +103,17 @@ router.post(
});
}
+ // return an error for unverified accounts if verification is required
+ if (config.login.requireVerification && !user.verified) {
+ throw FieldErrors({
+ login: {
+ code: "ACCOUNT_LOGIN_VERIFICATION_EMAIL",
+ message:
+ "Email verification is required, please check your email.",
+ },
+ });
+ }
+
if (user.mfa_enabled && !user.webauthn_enabled) {
// TODO: This is not a discord.com ticket. I'm not sure what it is but I'm lazy
const ticket = crypto.randomBytes(40).toString("hex");
diff --git a/src/api/routes/auth/register.ts b/src/api/routes/auth/register.ts
index 0bf8efae..c941fdf6 100644
--- a/src/api/routes/auth/register.ts
+++ b/src/api/routes/auth/register.ts
@@ -278,6 +278,17 @@ router.post(
await Invite.joinGuild(user.id, body.invite);
}
+ // return an error for unverified accounts if verification is required
+ if (Config.get().login.requireVerification && !user.verified) {
+ throw FieldErrors({
+ login: {
+ code: "ACCOUNT_LOGIN_VERIFICATION_EMAIL",
+ message:
+ "Email verification is required, please check your email.",
+ },
+ });
+ }
+
return res.json({ token: await generateToken(user.id) });
},
);
diff --git a/src/api/routes/auth/reset.ts b/src/api/routes/auth/reset.ts
new file mode 100644
index 00000000..9ab25dca
--- /dev/null
+++ b/src/api/routes/auth/reset.ts
@@ -0,0 +1,56 @@
+import { route } from "@fosscord/api";
+import {
+ checkToken,
+ Config,
+ Email,
+ FieldErrors,
+ generateToken,
+ PasswordResetSchema,
+ User,
+} from "@fosscord/util";
+import bcrypt from "bcrypt";
+import { Request, Response, Router } from "express";
+
+const router = Router();
+
+router.post(
+ "/",
+ route({ body: "PasswordResetSchema" }),
+ async (req: Request, res: Response) => {
+ const { password, token } = req.body as PasswordResetSchema;
+
+ const { jwtSecret } = Config.get().security;
+
+ let user;
+ try {
+ const userTokenData = await checkToken(token, jwtSecret, true);
+ user = userTokenData.user;
+ } catch {
+ throw FieldErrors({
+ password: {
+ message: req.t("auth:password_reset.INVALID_TOKEN"),
+ code: "INVALID_TOKEN",
+ },
+ });
+ }
+
+ // the salt is saved in the password refer to bcrypt docs
+ const hash = await bcrypt.hash(password, 12);
+
+ const data = {
+ data: {
+ hash,
+ valid_tokens_since: new Date(),
+ },
+ };
+ await User.update({ id: user.id }, data);
+
+ // come on, the user has to have an email to reset their password in the first place
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ await Email.sendPasswordChanged(user, user.email!);
+
+ res.json({ token: await generateToken(user.id) });
+ },
+);
+
+export default router;
diff --git a/src/api/routes/auth/verify/index.ts b/src/api/routes/auth/verify/index.ts
new file mode 100644
index 00000000..cdbd371a
--- /dev/null
+++ b/src/api/routes/auth/verify/index.ts
@@ -0,0 +1,93 @@
+/*
+ Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Fosscord and Fosscord Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import { getIpAdress, route, verifyCaptcha } from "@fosscord/api";
+import {
+ checkToken,
+ Config,
+ FieldErrors,
+ generateToken,
+ User,
+} from "@fosscord/util";
+import { Request, Response, Router } from "express";
+const router = Router();
+
+async function getToken(user: User) {
+ const token = await generateToken(user.id);
+
+ // Notice this will have a different token structure, than discord
+ // Discord header is just the user id as string, which is not possible with npm-jsonwebtoken package
+ // https://user-images.githubusercontent.com/6506416/81051916-dd8c9900-8ec2-11ea-8794-daf12d6f31f0.png
+
+ return { token };
+}
+
+router.post(
+ "/",
+ route({ body: "VerifyEmailSchema" }),
+ async (req: Request, res: Response) => {
+ const { captcha_key, token } = req.body;
+
+ const config = Config.get();
+
+ if (config.register.requireCaptcha) {
+ const { sitekey, service } = config.security.captcha;
+
+ if (!captcha_key) {
+ return res.status(400).json({
+ captcha_key: ["captcha-required"],
+ captcha_sitekey: sitekey,
+ captcha_service: service,
+ });
+ }
+
+ const ip = getIpAdress(req);
+ const verify = await verifyCaptcha(captcha_key, ip);
+ if (!verify.success) {
+ return res.status(400).json({
+ captcha_key: verify["error-codes"],
+ captcha_sitekey: sitekey,
+ captcha_service: service,
+ });
+ }
+ }
+
+ const { jwtSecret } = Config.get().security;
+ let user;
+
+ try {
+ const userTokenData = await checkToken(token, jwtSecret, true);
+ user = userTokenData.user;
+ } catch {
+ throw FieldErrors({
+ password: {
+ message: req.t("auth:password_reset.INVALID_TOKEN"),
+ code: "INVALID_TOKEN",
+ },
+ });
+ }
+
+ if (user.verified) return res.json(await getToken(user));
+
+ await User.update({ id: user.id }, { verified: true });
+
+ return res.json(await getToken(user));
+ },
+);
+
+export default router;
diff --git a/src/api/routes/auth/verify/resend.ts b/src/api/routes/auth/verify/resend.ts
new file mode 100644
index 00000000..918af9a1
--- /dev/null
+++ b/src/api/routes/auth/verify/resend.ts
@@ -0,0 +1,52 @@
+/*
+ Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Fosscord and Fosscord Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import { route } from "@fosscord/api";
+import { Email, User } from "@fosscord/util";
+import { Request, Response, Router } from "express";
+import { HTTPError } from "lambert-server";
+const router = Router();
+
+router.post(
+ "/",
+ route({ right: "RESEND_VERIFICATION_EMAIL" }),
+ async (req: Request, res: Response) => {
+ const user = await User.findOneOrFail({
+ where: { id: req.user_id },
+ select: ["username", "email"],
+ });
+
+ if (!user.email) {
+ // TODO: whats the proper error response for this?
+ throw new HTTPError("User does not have an email address", 400);
+ }
+
+ await Email.sendVerifyEmail(user, user.email)
+ .then(() => {
+ return res.sendStatus(204);
+ })
+ .catch((e) => {
+ console.error(
+ `Failed to send verification email to ${user.username}#${user.discriminator}: ${e}`,
+ );
+ throw new HTTPError("Failed to send verification email", 500);
+ });
+ },
+);
+
+export default router;
diff --git a/src/util/config/Config.ts b/src/util/config/Config.ts
index 122dadb5..c056d454 100644
--- a/src/util/config/Config.ts
+++ b/src/util/config/Config.ts
@@ -21,6 +21,7 @@ import {
CdnConfiguration,
ClientConfiguration,
DefaultsConfiguration,
+ EmailConfiguration,
EndpointConfiguration,
ExternalTokensConfiguration,
GeneralConfiguration,
@@ -30,6 +31,7 @@ import {
LimitsConfiguration,
LoginConfiguration,
MetricsConfiguration,
+ PasswordResetConfiguration,
RabbitMQConfiguration,
RegionConfiguration,
RegisterConfiguration,
@@ -58,4 +60,7 @@ export class ConfigValue {
sentry: SentryConfiguration = new SentryConfiguration();
defaults: DefaultsConfiguration = new DefaultsConfiguration();
external: ExternalTokensConfiguration = new ExternalTokensConfiguration();
+ email: EmailConfiguration = new EmailConfiguration();
+ password_reset: PasswordResetConfiguration =
+ new PasswordResetConfiguration();
}
diff --git a/src/util/config/types/EmailConfiguration.ts b/src/util/config/types/EmailConfiguration.ts
new file mode 100644
index 00000000..989d59eb
--- /dev/null
+++ b/src/util/config/types/EmailConfiguration.ts
@@ -0,0 +1,32 @@
+/*
+ Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Fosscord and Fosscord Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import {
+ MailGunConfiguration,
+ MailJetConfiguration,
+ SMTPConfiguration,
+} from "./subconfigurations/email";
+import { SendGridConfiguration } from "./subconfigurations/email/SendGrid";
+
+export class EmailConfiguration {
+ provider: string | null = null;
+ smtp: SMTPConfiguration = new SMTPConfiguration();
+ mailgun: MailGunConfiguration = new MailGunConfiguration();
+ mailjet: MailJetConfiguration = new MailJetConfiguration();
+ sendgrid: SendGridConfiguration = new SendGridConfiguration();
+}
diff --git a/src/util/config/types/LoginConfiguration.ts b/src/util/config/types/LoginConfiguration.ts
index 862bc185..1d5752fe 100644
--- a/src/util/config/types/LoginConfiguration.ts
+++ b/src/util/config/types/LoginConfiguration.ts
@@ -18,4 +18,5 @@
export class LoginConfiguration {
requireCaptcha: boolean = false;
+ requireVerification: boolean = false;
}
diff --git a/src/util/config/types/PasswordResetConfiguration.ts b/src/util/config/types/PasswordResetConfiguration.ts
new file mode 100644
index 00000000..806d77be
--- /dev/null
+++ b/src/util/config/types/PasswordResetConfiguration.ts
@@ -0,0 +1,21 @@
+/*
+ Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Fosscord and Fosscord Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+export class PasswordResetConfiguration {
+ requireCaptcha: boolean = false;
+}
diff --git a/src/util/config/types/RegisterConfiguration.ts b/src/util/config/types/RegisterConfiguration.ts
index acbaa2d5..b8db0077 100644
--- a/src/util/config/types/RegisterConfiguration.ts
+++ b/src/util/config/types/RegisterConfiguration.ts
@@ -18,12 +18,13 @@
import {
DateOfBirthConfiguration,
- EmailConfiguration,
PasswordConfiguration,
+ RegistrationEmailConfiguration,
} from ".";
export class RegisterConfiguration {
- email: EmailConfiguration = new EmailConfiguration();
+ email: RegistrationEmailConfiguration =
+ new RegistrationEmailConfiguration();
dateOfBirth: DateOfBirthConfiguration = new DateOfBirthConfiguration();
password: PasswordConfiguration = new PasswordConfiguration();
disabled: boolean = false;
@@ -34,5 +35,5 @@ export class RegisterConfiguration {
allowMultipleAccounts: boolean = true;
blockProxies: boolean = true;
incrementingDiscriminators: boolean = false; // random otherwise
- defaultRights: string = "312119568366592"; // See `npm run generate:rights`
+ defaultRights: string = "875069521787904"; // See `npm run generate:rights`
}
diff --git a/src/util/config/types/index.ts b/src/util/config/types/index.ts
index 523ad186..510e19f8 100644
--- a/src/util/config/types/index.ts
+++ b/src/util/config/types/index.ts
@@ -20,6 +20,7 @@ export * from "./ApiConfiguration";
export * from "./CdnConfiguration";
export * from "./ClientConfiguration";
export * from "./DefaultsConfiguration";
+export * from "./EmailConfiguration";
export * from "./EndpointConfiguration";
export * from "./ExternalTokensConfiguration";
export * from "./GeneralConfiguration";
@@ -29,10 +30,11 @@ export * from "./KafkaConfiguration";
export * from "./LimitConfigurations";
export * from "./LoginConfiguration";
export * from "./MetricsConfiguration";
+export * from "./PasswordResetConfiguration";
export * from "./RabbitMQConfiguration";
export * from "./RegionConfiguration";
export * from "./RegisterConfiguration";
export * from "./SecurityConfiguration";
export * from "./SentryConfiguration";
-export * from "./TemplateConfiguration";
export * from "./subconfigurations";
+export * from "./TemplateConfiguration";
diff --git a/src/util/config/types/subconfigurations/email/MailGun.ts b/src/util/config/types/subconfigurations/email/MailGun.ts
new file mode 100644
index 00000000..52cd9069
--- /dev/null
+++ b/src/util/config/types/subconfigurations/email/MailGun.ts
@@ -0,0 +1,22 @@
+/*
+ Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Fosscord and Fosscord Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+export class MailGunConfiguration {
+ apiKey: string | null = null;
+ domain: string | null = null;
+}
diff --git a/src/util/config/types/subconfigurations/email/MailJet.ts b/src/util/config/types/subconfigurations/email/MailJet.ts
new file mode 100644
index 00000000..eccda8ac
--- /dev/null
+++ b/src/util/config/types/subconfigurations/email/MailJet.ts
@@ -0,0 +1,22 @@
+/*
+ Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Fosscord and Fosscord Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+export class MailJetConfiguration {
+ apiKey: string | null = null;
+ apiSecret: string | null = null;
+}
diff --git a/src/util/config/types/subconfigurations/email/SMTP.ts b/src/util/config/types/subconfigurations/email/SMTP.ts
new file mode 100644
index 00000000..11eb9e14
--- /dev/null
+++ b/src/util/config/types/subconfigurations/email/SMTP.ts
@@ -0,0 +1,25 @@
+/*
+ Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Fosscord and Fosscord Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+export class SMTPConfiguration {
+ host: string | null = null;
+ port: number | null = null;
+ secure: boolean | null = null;
+ username: string | null = null;
+ password: string | null = null;
+}
diff --git a/src/util/config/types/subconfigurations/email/SendGrid.ts b/src/util/config/types/subconfigurations/email/SendGrid.ts
new file mode 100644
index 00000000..a4755dfb
--- /dev/null
+++ b/src/util/config/types/subconfigurations/email/SendGrid.ts
@@ -0,0 +1,21 @@
+/*
+ Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Fosscord and Fosscord Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+export class SendGridConfiguration {
+ apiKey: string | null = null;
+}
diff --git a/src/util/config/types/subconfigurations/email/index.ts b/src/util/config/types/subconfigurations/email/index.ts
new file mode 100644
index 00000000..02cc564c
--- /dev/null
+++ b/src/util/config/types/subconfigurations/email/index.ts
@@ -0,0 +1,21 @@
+/*
+ Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Fosscord and Fosscord Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+export * from "./MailGun";
+export * from "./MailJet";
+export * from "./SMTP";
diff --git a/src/util/config/types/subconfigurations/register/Email.ts b/src/util/config/types/subconfigurations/register/Email.ts
index 478dc974..4f95caf1 100644
--- a/src/util/config/types/subconfigurations/register/Email.ts
+++ b/src/util/config/types/subconfigurations/register/Email.ts
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-export class EmailConfiguration {
+export class RegistrationEmailConfiguration {
required: boolean = false;
allowlist: boolean = false;
blocklist: boolean = true;
diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts
index 7b67c2ac..f99a85e7 100644
--- a/src/util/entities/User.ts
+++ b/src/util/entities/User.ts
@@ -16,6 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+import { Request } from "express";
import {
Column,
Entity,
@@ -24,16 +25,22 @@ import {
OneToMany,
OneToOne,
} from "typeorm";
-import { BaseClass } from "./BaseClass";
+import {
+ adjustEmail,
+ Config,
+ Email,
+ FieldErrors,
+ Snowflake,
+ trimSpecial,
+} from "..";
import { BitField } from "../util/BitField";
-import { Relationship } from "./Relationship";
+import { BaseClass } from "./BaseClass";
import { ConnectedAccount } from "./ConnectedAccount";
import { Member } from "./Member";
-import { UserSettings } from "./UserSettings";
-import { Session } from "./Session";
-import { Config, FieldErrors, Snowflake, trimSpecial, adjustEmail } from "..";
-import { Request } from "express";
+import { Relationship } from "./Relationship";
import { SecurityKey } from "./SecurityKey";
+import { Session } from "./Session";
+import { UserSettings } from "./UserSettings";
export enum PublicUserEnum {
username,
@@ -384,6 +391,15 @@ export class User extends BaseClass {
user.validate();
await Promise.all([user.save(), settings.save()]);
+ // send verification email if users aren't verified by default and we have an email
+ if (!Config.get().defaults.user.verified && email) {
+ await Email.sendVerifyEmail(user, email).catch((e) => {
+ console.error(
+ `Failed to send verification email to ${user.username}#${user.discriminator}: ${e}`,
+ );
+ });
+ }
+
setImmediate(async () => {
if (Config.get().guild.autoJoin.enabled) {
for (const guild of Config.get().guild.autoJoin.guilds || []) {
diff --git a/src/util/schemas/ForgotPasswordSchema.ts b/src/util/schemas/ForgotPasswordSchema.ts
new file mode 100644
index 00000000..9a28bd18
--- /dev/null
+++ b/src/util/schemas/ForgotPasswordSchema.ts
@@ -0,0 +1,22 @@
+/*
+ Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Fosscord and Fosscord Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+export interface ForgotPasswordSchema {
+ login: string;
+ captcha_key?: string;
+}
diff --git a/src/util/schemas/PasswordResetSchema.ts b/src/util/schemas/PasswordResetSchema.ts
new file mode 100644
index 00000000..9cc74940
--- /dev/null
+++ b/src/util/schemas/PasswordResetSchema.ts
@@ -0,0 +1,22 @@
+/*
+ Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Fosscord and Fosscord Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+export interface PasswordResetSchema {
+ password: string;
+ token: string;
+}
diff --git a/src/util/schemas/VerifyEmailSchema.ts b/src/util/schemas/VerifyEmailSchema.ts
new file mode 100644
index 00000000..d94fbbc1
--- /dev/null
+++ b/src/util/schemas/VerifyEmailSchema.ts
@@ -0,0 +1,22 @@
+/*
+ Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Fosscord and Fosscord Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+export interface VerifyEmailSchema {
+ captcha_key?: string | null;
+ token: string;
+}
diff --git a/src/util/schemas/index.ts b/src/util/schemas/index.ts
index 194d8571..44909a3a 100644
--- a/src/util/schemas/index.ts
+++ b/src/util/schemas/index.ts
@@ -16,6 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+export * from "./AckBulkSchema";
export * from "./ActivitySchema";
export * from "./ApplicationAuthorizeSchema";
export * from "./ApplicationCreateSchema";
@@ -32,6 +33,7 @@ export * from "./CodesVerificationSchema";
export * from "./DmChannelCreateSchema";
export * from "./EmojiCreateSchema";
export * from "./EmojiModifySchema";
+export * from "./ForgotPasswordSchema";
export * from "./GatewayPayloadSchema";
export * from "./GuildCreateSchema";
export * from "./GuildTemplateCreateSchema";
@@ -45,8 +47,10 @@ export * from "./MemberChangeProfileSchema";
export * from "./MemberChangeSchema";
export * from "./MessageAcknowledgeSchema";
export * from "./MessageCreateSchema";
+export * from "./MessageEditSchema";
export * from "./MfaCodesSchema";
export * from "./ModifyGuildStickerSchema";
+export * from "./PasswordResetSchema";
export * from "./PurgeSchema";
export * from "./RegisterSchema";
export * from "./RelationshipPostSchema";
@@ -69,22 +73,6 @@ export * from "./VanityUrlSchema";
export * from "./VoiceIdentifySchema";
export * from "./VoiceStateUpdateSchema";
export * from "./VoiceVideoSchema";
-export * from "./IdentifySchema";
-export * from "./ActivitySchema";
-export * from "./LazyRequestSchema";
-export * from "./GuildUpdateSchema";
-export * from "./ChannelPermissionOverwriteSchema";
-export * from "./UserGuildSettingsSchema";
-export * from "./GatewayPayloadSchema";
-export * from "./RolePositionUpdateSchema";
-export * from "./ChannelReorderSchema";
-export * from "./UserSettingsSchema";
-export * from "./BotModifySchema";
-export * from "./ApplicationModifySchema";
-export * from "./ApplicationCreateSchema";
-export * from "./ApplicationAuthorizeSchema";
-export * from "./AckBulkSchema";
export * from "./WebAuthnSchema";
export * from "./WebhookCreateSchema";
export * from "./WidgetModifySchema";
-export * from "./MessageEditSchema";
diff --git a/src/util/util/Email.ts b/src/util/util/Email.ts
deleted file mode 100644
index 48d8cae1..00000000
--- a/src/util/util/Email.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Fosscord and Fosscord Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-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,}))$/;
-
-export function adjustEmail(email?: string): string | undefined {
- if (!email) return email;
- // body parser already checked if it is a valid email
- const parts = <RegExpMatchArray>email.match(EMAIL_REGEX);
- if (!parts || parts.length < 5) return undefined;
-
- return email;
- // // TODO: The below code doesn't actually do anything.
- // const domain = parts[5];
- // const user = parts[1];
-
- // // TODO: check accounts with uncommon email domains
- // if (domain === "gmail.com" || domain === "googlemail.com") {
- // // replace .dots and +alternatives -> Gmail Dot Trick https://support.google.com/mail/answer/7436150 and https://generator.email/blog/gmail-generator
- // const v = user.replace(/[.]|(\+.*)/g, "") + "@gmail.com";
- // }
-
- // if (domain === "google.com") {
- // // replace .dots and +alternatives -> Google Staff GMail Dot Trick
- // const v = user.replace(/[.]|(\+.*)/g, "") + "@google.com";
- // }
-
- // return email;
-}
diff --git a/src/util/util/Rights.ts b/src/util/util/Rights.ts
index b48477ed..383f07ec 100644
--- a/src/util/util/Rights.ts
+++ b/src/util/util/Rights.ts
@@ -93,6 +93,7 @@ export class Rights extends BitField {
EDIT_FLAGS: BitFlag(46), // can set others' flags
MANAGE_GROUPS: BitFlag(47), // can manage others' groups
VIEW_SERVER_STATS: BitFlag(48), // added per @chrischrome's request — can view server stats)
+ RESEND_VERIFICATION_EMAIL: BitFlag(49), // can resend verification emails (/auth/verify/resend)
};
any(permission: RightResolvable, checkOperator = true) {
diff --git a/src/util/util/Token.ts b/src/util/util/Token.ts
index ca81eaaa..ffc442aa 100644
--- a/src/util/util/Token.ts
+++ b/src/util/util/Token.ts
@@ -27,9 +27,43 @@ export type UserTokenData = {
decoded: { id: string; iat: number };
};
+async function checkEmailToken(
+ decoded: jwt.JwtPayload,
+): Promise<UserTokenData> {
+ // eslint-disable-next-line no-async-promise-executor
+ return new Promise(async (res, rej) => {
+ if (!decoded.iat) return rej("Invalid Token"); // will never happen, just for typings.
+
+ const user = await User.findOne({
+ where: {
+ email: decoded.email,
+ },
+ select: [
+ "email",
+ "id",
+ "verified",
+ "deleted",
+ "disabled",
+ "username",
+ "data",
+ ],
+ });
+
+ if (!user) return rej("Invalid Token");
+
+ if (new Date().getTime() > decoded.iat * 1000 + 86400 * 1000)
+ return rej("Invalid Token");
+
+ // Using as here because we assert `id` and `iat` are in decoded.
+ // TS just doesn't want to assume its there, though.
+ return res({ decoded, user } as UserTokenData);
+ });
+}
+
export function checkToken(
token: string,
jwtSecret: string,
+ isEmailVerification = false,
): Promise<UserTokenData> {
return new Promise((res, rej) => {
token = token.replace("Bot ", "");
@@ -48,6 +82,8 @@ export function checkToken(
)
return rej("Invalid Token"); // will never happen, just for typings.
+ if (isEmailVerification) return res(checkEmailToken(decoded));
+
const user = await User.findOne({
where: { id: decoded.id },
select: ["data", "bot", "disabled", "deleted", "rights"],
@@ -72,13 +108,13 @@ export function checkToken(
});
}
-export async function generateToken(id: string) {
+export async function generateToken(id: string, email?: string) {
const iat = Math.floor(Date.now() / 1000);
const algorithm = "HS256";
return new Promise((res, rej) => {
jwt.sign(
- { id: id, iat },
+ { id, iat, email },
Config.get().security.jwtSecret,
{
algorithm,
diff --git a/src/util/util/email/index.ts b/src/util/util/email/index.ts
new file mode 100644
index 00000000..8c7a848c
--- /dev/null
+++ b/src/util/util/email/index.ts
@@ -0,0 +1,269 @@
+/*
+ Fosscord: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Fosscord and Fosscord Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import fs from "node:fs";
+import path from "node:path";
+import { SentMessageInfo, Transporter } from "nodemailer";
+import { User } from "../../entities";
+import { Config } from "../Config";
+import { generateToken } from "../Token";
+import MailGun from "./transports/MailGun";
+import MailJet from "./transports/MailJet";
+import SendGrid from "./transports/SendGrid";
+import SMTP from "./transports/SMTP";
+
+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,}))$/;
+
+export function adjustEmail(email?: string): string | undefined {
+ if (!email) return email;
+ // body parser already checked if it is a valid email
+ const parts = <RegExpMatchArray>email.match(EMAIL_REGEX);
+ if (!parts || parts.length < 5) return undefined;
+
+ return email;
+ // // TODO: The below code doesn't actually do anything.
+ // const domain = parts[5];
+ // const user = parts[1];
+
+ // // TODO: check accounts with uncommon email domains
+ // if (domain === "gmail.com" || domain === "googlemail.com") {
+ // // replace .dots and +alternatives -> Gmail Dot Trick https://support.google.com/mail/answer/7436150 and https://generator.email/blog/gmail-generator
+ // const v = user.replace(/[.]|(\+.*)/g, "") + "@gmail.com";
+ // }
+
+ // if (domain === "google.com") {
+ // // replace .dots and +alternatives -> Google Staff GMail Dot Trick
+ // const v = user.replace(/[.]|(\+.*)/g, "") + "@google.com";
+ // }
+
+ // return email;
+}
+
+const transporters: {
+ [key: string]: () => Promise<Transporter<unknown> | void>;
+} = {
+ smtp: SMTP,
+ mailgun: MailGun,
+ mailjet: MailJet,
+ sendgrid: SendGrid,
+};
+
+export const Email: {
+ transporter: Transporter | null;
+ init: () => Promise<void>;
+ generateLink: (
+ type: "verify" | "reset",
+ id: string,
+ email: string,
+ ) => Promise<string>;
+ sendVerifyEmail: (user: User, email: string) => Promise<SentMessageInfo>;
+ sendResetPassword: (user: User, email: string) => Promise<SentMessageInfo>;
+ sendPasswordChanged: (
+ user: User,
+ email: string,
+ ) => Promise<SentMessageInfo>;
+ 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 () {
+ const { provider } = Config.get().email;
+ if (!provider) return;
+
+ const transporterFn = transporters[provider];
+ if (!transporterFn)
+ return console.error(`[Email] Invalid provider: ${provider}`);
+ console.log(`[Email] Initializing ${provider} transport...`);
+ const transporter = await transporterFn();
+ if (!transporter) return;
+ this.transporter = transporter;
+ console.log(`[Email] ${provider} transport initialized.`);
+ },
+ /**
+ * Replaces all placeholders in an email template with the correct values
+ */
+ doReplacements: function (
+ template,
+ user,
+ emailVerificationUrl?,
+ passwordResetUrl?,
+ ipInfo?: {
+ ip: string;
+ city: string;
+ region: string;
+ country_name: string;
+ },
+ ) {
+ const { instanceName } = Config.get().general;
+
+ const replacements = [
+ ["{instanceName}", instanceName],
+ ["{userUsername}", user.username],
+ ["{userDiscriminator}", user.discriminator],
+ ["{userId}", user.id],
+ ["{phoneNumber}", user.phone?.slice(-4)],
+ ["{userEmail}", user.email],
+ ["{emailVerificationUrl}", emailVerificationUrl],
+ ["{passwordResetUrl}", passwordResetUrl],
+ ["{ipAddress}", ipInfo?.ip],
+ ["{locationCity}", ipInfo?.city],
+ ["{locationRegion}", ipInfo?.region],
+ ["{locationCountryName}", ipInfo?.country_name],
+ ];
+
+ // loop through all replacements and replace them in the template
+ for (const [key, value] of Object.values(replacements)) {
+ if (!value) continue;
+ template = template.replace(key as string, value);
+ }
+
+ return template;
+ },
+ /**
+ *
+ * @param id user id
+ * @param email user email
+ */
+ generateLink: async function (type, id, email) {
+ const token = (await generateToken(id, email)) as string;
+ const instanceUrl =
+ Config.get().general.frontPage || "http://localhost:3001";
+ const link = `${instanceUrl}/${type}#token=${token}`;
+ return link;
+ },
+ /**
+ * Sends an email to the user with a link to verify their email address
+ */
+ sendVerifyEmail: async function (user, email) {
+ if (!this.transporter) return;
+
+ // generate a verification link for the user
+ const link = await this.generateLink("verify", 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, link);
+
+ // 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,
+ html,
+ };
+
+ // send the email
+ return this.transporter.sendMail(message);
+ },
+ /**
+ * Sends an email to the user with a link to reset their password
+ */
+ sendResetPassword: async function (user, email) {
+ if (!this.transporter) return;
+
+ // generate a password reset link for the user
+ const link = await this.generateLink("reset", user.id, email);
+
+ // load the email template
+ const rawTemplate = await fs.promises.readFile(
+ path.join(
+ ASSET_FOLDER_PATH,
+ "email_templates",
+ "password_reset_request.html",
+ ),
+ { encoding: "utf-8" },
+ );
+
+ // replace email template placeholders
+ const html = this.doReplacements(rawTemplate, user, undefined, link);
+
+ // 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,
+ html,
+ };
+
+ // send the email
+ return this.transporter.sendMail(message);
+ },
+ /**
+ * Sends an email to the user notifying them that their password has been changed
+ */
+ sendPasswordChanged: async function (user, email) {
+ if (!this.transporter) return;
+
+ // load the email template
+ const rawTemplate = await fs.promises.readFile(
+ path.join(
+ ASSET_FOLDER_PATH,
+ "email_templates",
+ "password_changed.html",
+ ),
+ { encoding: "utf-8" },
+ );
+
+ // replace email template placeholders
+ const html = this.doReplacements(rawTemplate, user);
+
+ // 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,
+ html,
+ };
+
+ // send the email
+ return this.transporter.sendMail(message);
+ },
+};
diff --git a/src/util/util/email/transports/MailGun.ts b/src/util/util/email/transports/MailGun.ts
new file mode 100644
index 00000000..3a5be13c
--- /dev/null
+++ b/src/util/util/email/transports/MailGun.ts
@@ -0,0 +1,36 @@
+import { Config } from "@fosscord/util";
+import nodemailer from "nodemailer";
+
+export default async function () {
+ // get configuration
+ const { apiKey, domain } = Config.get().email.mailgun;
+
+ // ensure all required configuration values are set
+ if (!apiKey || !domain)
+ return console.error(
+ "[Email] Mailgun has not been configured correctly.",
+ );
+
+ let mg;
+ try {
+ // try to import the transporter package
+ mg = require("nodemailer-mailgun-transport");
+ } catch {
+ // if the package is not installed, log an error and return void so we don't set the transporter
+ console.error(
+ "[Email] Mailgun transport is not installed. Please run `npm install nodemailer-mailgun-transport --save-optional` to install it.",
+ );
+ return;
+ }
+
+ // create the transporter configuration object
+ const auth = {
+ auth: {
+ api_key: apiKey,
+ domain: domain,
+ },
+ };
+
+ // create the transporter and return it
+ return nodemailer.createTransport(mg(auth));
+}
diff --git a/src/util/util/email/transports/MailJet.ts b/src/util/util/email/transports/MailJet.ts
new file mode 100644
index 00000000..561d13ea
--- /dev/null
+++ b/src/util/util/email/transports/MailJet.ts
@@ -0,0 +1,36 @@
+import { Config } from "@fosscord/util";
+import nodemailer from "nodemailer";
+
+export default async function () {
+ // get configuration
+ const { apiKey, apiSecret } = Config.get().email.mailjet;
+
+ // ensure all required configuration values are set
+ if (!apiKey || !apiSecret)
+ return console.error(
+ "[Email] Mailjet has not been configured correctly.",
+ );
+
+ let mj;
+ try {
+ // try to import the transporter package
+ mj = require("nodemailer-mailjet-transport");
+ } catch {
+ // if the package is not installed, log an error and return void so we don't set the transporter
+ console.error(
+ "[Email] Mailjet transport is not installed. Please run `npm install n0script22/nodemailer-mailjet-transport --save-optional` to install it.",
+ );
+ return;
+ }
+
+ // create the transporter configuration object
+ const auth = {
+ auth: {
+ apiKey: apiKey,
+ apiSecret: apiSecret,
+ },
+ };
+
+ // create the transporter and return it
+ return nodemailer.createTransport(mj(auth));
+}
diff --git a/src/util/util/email/transports/SMTP.ts b/src/util/util/email/transports/SMTP.ts
new file mode 100644
index 00000000..7d8e1e20
--- /dev/null
+++ b/src/util/util/email/transports/SMTP.ts
@@ -0,0 +1,38 @@
+import { Config } from "@fosscord/util";
+import nodemailer from "nodemailer";
+
+export default async function () {
+ // get configuration
+ const { host, port, secure, username, password } = Config.get().email.smtp;
+
+ // ensure all required configuration values are set
+ if (!host || !port || secure === null || !username || !password)
+ return console.error("[Email] SMTP has not been configured correctly.");
+
+ if (!Config.get().general.correspondenceEmail)
+ return console.error(
+ "[Email] Correspondence email has not been configured! This is used as the sender email address.",
+ );
+
+ // construct the transporter
+ const transporter = nodemailer.createTransport({
+ host,
+ port,
+ secure,
+ auth: {
+ user: username,
+ pass: password,
+ },
+ });
+
+ // verify connection configuration
+ const verified = await transporter.verify().catch((err) => {
+ console.error("[Email] SMTP verification failed:", err);
+ return;
+ });
+
+ // if verification failed, return void and don't set transporter
+ if (!verified) return;
+
+ return transporter;
+}
diff --git a/src/util/util/email/transports/SendGrid.ts b/src/util/util/email/transports/SendGrid.ts
new file mode 100644
index 00000000..7b46c7be
--- /dev/null
+++ b/src/util/util/email/transports/SendGrid.ts
@@ -0,0 +1,35 @@
+import { Config } from "@fosscord/util";
+import nodemailer from "nodemailer";
+
+export default async function () {
+ // get configuration
+ const { apiKey } = Config.get().email.sendgrid;
+
+ // ensure all required configuration values are set
+ if (!apiKey)
+ return console.error(
+ "[Email] SendGrid has not been configured correctly.",
+ );
+
+ let sg;
+ try {
+ // try to import the transporter package
+ sg = require("nodemailer-sendgrid-transport");
+ } catch {
+ // if the package is not installed, log an error and return void so we don't set the transporter
+ console.error(
+ "[Email] SendGrid transport is not installed. Please run `npm install Maria-Golomb/nodemailer-sendgrid-transport --save-optional` to install it.",
+ );
+ return;
+ }
+
+ // create the transporter configuration object
+ const auth = {
+ auth: {
+ api_key: apiKey,
+ },
+ };
+
+ // create the transporter and return it
+ return nodemailer.createTransport(sg(auth));
+}
diff --git a/src/util/util/email/transports/index.ts b/src/util/util/email/transports/index.ts
new file mode 100644
index 00000000..d14acbf0
--- /dev/null
+++ b/src/util/util/email/transports/index.ts
@@ -0,0 +1 @@
+export * from "./SMTP";
diff --git a/src/util/util/index.ts b/src/util/util/index.ts
index 6eb686d0..93656ecb 100644
--- a/src/util/util/index.ts
+++ b/src/util/util/index.ts
@@ -17,27 +17,27 @@
*/
export * from "./ApiError";
+export * from "./Array";
export * from "./BitField";
-export * from "./Token";
//export * from "./Categories";
export * from "./cdn";
export * from "./Config";
export * from "./Constants";
export * from "./Database";
-export * from "./Email";
+export * from "./email";
export * from "./Event";
export * from "./FieldError";
export * from "./Intents";
+export * from "./InvisibleCharacters";
+export * from "./JSON";
export * from "./MessageFlags";
export * from "./Permissions";
export * from "./RabbitMQ";
export * from "./Regex";
export * from "./Rights";
+export * from "./Sentry";
export * from "./Snowflake";
export * from "./String";
-export * from "./Array";
+export * from "./Token";
export * from "./TraverseDirectory";
-export * from "./InvisibleCharacters";
-export * from "./Sentry";
export * from "./WebAuthn";
-export * from "./JSON";
|