diff options
Diffstat (limited to 'src/util')
27 files changed, 730 insertions, 80 deletions
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"; |