From 533a72c3d3fc0d30a9a65a582cb95bbc1d8e9276 Mon Sep 17 00:00:00 2001 From: Rory& Date: Tue, 30 Sep 2025 04:13:11 +0200 Subject: [PATCH] Port SendGrid to new interface --- flake.nix | 2 +- package-lock.json | Bin 399249 -> 398511 bytes package.json | 2 +- src/util/util/email/clients/IEmailClient.ts | 25 +++++ .../util/email/clients/SendGridEmailClient.ts | 41 ++++++++ src/util/util/email/index.ts | 91 ++++++++++++------ src/util/util/email/transports/SendGrid.ts | 53 ---------- 7 files changed, 130 insertions(+), 84 deletions(-) create mode 100644 src/util/util/email/clients/IEmailClient.ts create mode 100644 src/util/util/email/clients/SendGridEmailClient.ts delete mode 100644 src/util/util/email/transports/SendGrid.ts diff --git a/flake.nix b/flake.nix index d6dcb7e7..3be04167 100644 --- a/flake.nix +++ b/flake.nix @@ -73,7 +73,7 @@ ]; text = '' rm -rf node_modules - ${pkgs.nodejs_24}/bin/npm install --save + ${pkgs.nodejs_24}/bin/npm install --save --no-audit --no-fund --prefer-offline --ignore-scripts --no-bin-links DEPS_HASH=$(prefetch-npm-deps package-lock.json) TMPFILE=$(mktemp) jq '.npmDepsHash = "'"$DEPS_HASH"'"' hashes.json > "$TMPFILE" diff --git a/package-lock.json b/package-lock.json index d1db30a2f2f693c206006d80355e3c94c98f868b..f2ec33fbe354bef0be3e40de31dc18e411cb5cb0 100644 GIT binary patch delta 545 zcmYL`y>Aj>0LJseH4R`FjY&;g^=MoQ-WPB^G$sNCt{h)IxZ}!TIk@AaaPrEspKFPB@(=Wg0)!FLV+tv4Xz_o`X{jt`u zdKzh{y+QEa?cmmj4Z!)(_hanpGDpw^8w|fL`oBGM12Z=OR;u3li14p~MxPGA4ZtDw z6x)=@BtNFQ=8;n5Ek@0ETZ*(T8pc32Ev;=e_u~Roi-!~?)cJv2l82%r$`&l8)aGt7 z6sLD9N{$T~`9j==sw{N{wtAGTp+po23lblC*8o@6ID#UmVA$`QC#X8T;b;N;h$d(T zeOv)s9)>_qFThQ7R|P(F+5~IJKKDe>uO?WZgTDFf*_7(~c5Pf^l<6dvt;TAi6Pl!L zF5Vt>NZ6S~s&qP%sZe`qhazQLGVI9&HE{i(uDa<+wxFk{Po%hJm4@XM$qiw;TV!Z delta 884 zcmah{OKTHR6y{D+rD+v)QDP~5qA0P+Ofs2AMRf8W(xj6nnWR~0?qu#H(?Wb$Z0 zL=;?ViEvjUf{3nETy!gLT)5DM3m1aal?zw?0gW%LAb8+g95@HQ*W*X-oyYDcC&04{ zfE*v*a=l9!rivO+uOn-@jSRJF>FTm=L8IL?EqifgezB_CwT`lqf|d@K<7VA#D87F@ zU6Utv{_`9*@;Jr8=|-s z+pdRA4~m$Y+$gPPNR4CshS5^O0a>NcCZn*ch^;H;_8=2uVrL6LZ`3{D-a)OBRo zKHSL@C!M1cX1MT~#IN(j;h)>i$A!Em^(sX=+BLF?j%b8aq8+UXaRX+f#r4ppJ*e^8 zn%#~vQ7X0UhY7~_aMP(8i#d#PHUtJGMN>cW<uhKbzs%wHm!0E>|1L4!c|f@F-Lk`tk6d*gFZ{2? zf2!{t^?nKR6@=&j&8QR!A?fEij;A@2W3&KdX-4A^H*&WUz`{|G7&cwU9IThddlHzv OaqhZv_uTa{>()1+z8_ft diff --git a/package.json b/package.json index a7bde5af..5d59c524 100644 --- a/package.json +++ b/package.json @@ -123,12 +123,12 @@ "@spacebar/webrtc": "dist/webrtc" }, "optionalDependencies": { + "@sendgrid/mail": "^8.1.6", "@yukikaze-bot/erlpack": "^1.0.1", "jimp": "^1.6.0", "mysql": "^2.18.1", "nodemailer-mailgun-transport": "^2.1.5", "nodemailer-mailjet-transport": "github:n0script22/nodemailer-mailjet-transport", - "nodemailer-sendgrid-transport": "github:Maria-Golomb/nodemailer-sendgrid-transport", "pg": "^8.16.3", "sqlite3": "^5.1.7" } diff --git a/src/util/util/email/clients/IEmailClient.ts b/src/util/util/email/clients/IEmailClient.ts new file mode 100644 index 00000000..64f662bd --- /dev/null +++ b/src/util/util/email/clients/IEmailClient.ts @@ -0,0 +1,25 @@ +import { SentMessageInfo, Transporter } from "nodemailer"; +import { User } from "@spacebar/util*"; + +export interface IEmail { + from: string; + to: string; + subject: string; + text: string; + html: string; +} +export interface IEmailClient { + init: () => Promise; + sendMail: ( + email: IEmail, + ) => Promise; +} + +export class BaseEmailClient implements IEmailClient { + async init(): Promise { + return; + } + sendMail(email: IEmail): Promise { + throw new Error("Method not implemented."); + } +} \ No newline at end of file diff --git a/src/util/util/email/clients/SendGridEmailClient.ts b/src/util/util/email/clients/SendGridEmailClient.ts new file mode 100644 index 00000000..cb30ea8b --- /dev/null +++ b/src/util/util/email/clients/SendGridEmailClient.ts @@ -0,0 +1,41 @@ +import { BaseEmailClient, IEmail } from "./IEmailClient"; +import { Config } from "@spacebar/util*"; + +export class SendGridEmailClient extends BaseEmailClient { + // sendGrid?: unknown; + sendGrid?: typeof import("@sendgrid/mail"); + override async init(): Promise { + // 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.", + ); + + try { + // try to import the transporter package + this.sendGrid = (await import("@sendgrid/mail")).default; + } 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; + } + this.sendGrid.setApiKey(apiKey); + } + + override async sendMail(email: IEmail): Promise { + if (!this.sendGrid) throw new Error("SendGrid not initialized"); + + await this.sendGrid.send({ + to: email.to, + from: email.from, + subject: email.subject, + text: email.text, + html: email.html, + }); + } +} \ No newline at end of file diff --git a/src/util/util/email/index.ts b/src/util/util/email/index.ts index 00ff2cd0..64acc3cc 100644 --- a/src/util/util/email/index.ts +++ b/src/util/util/email/index.ts @@ -22,10 +22,8 @@ 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 SMTP from "./transports/SMTP"; -import SendGrid from "./transports/SendGrid"; +import { BaseEmailClient, IEmail, IEmailClient } from "./clients/IEmailClient"; +import { SendGridEmailClient } from "./clients/SendGridEmailClient"; const ASSET_FOLDER_PATH = path.join( __dirname, @@ -36,23 +34,14 @@ const ASSET_FOLDER_PATH = path.join( "assets", ); -enum MailTypes { +export enum MailTypes { verifyEmail = "verifyEmail", resetPassword = "resetPassword", changePassword = "changePassword", } -const transporters: { - [key: string]: () => Promise | void>; -} = { - smtp: SMTP, - mailgun: MailGun, - mailjet: MailJet, - sendgrid: SendGrid, -}; - export const Email: { - transporter: Transporter | null; + transporter: IEmailClient | null; init: () => Promise; generateLink: ( type: Omit, @@ -63,13 +52,13 @@ export const Email: { type: MailTypes, user: User, email: string, - ) => Promise; - sendVerifyEmail: (user: User, email: string) => Promise; - sendResetPassword: (user: User, email: string) => Promise; + ) => Promise; + sendVerifyEmail: (user: User, email: string) => Promise; + sendResetPassword: (user: User, email: string) => Promise; sendPasswordChanged: ( user: User, email: string, - ) => Promise; + ) => Promise; doReplacements: ( template: string, user: User, @@ -87,15 +76,31 @@ export const Email: { const { provider } = Config.get().email; if (!provider) return; - const transporterFn = transporters[provider]; - if (!transporterFn) + switch (provider) { + case "smtp": + this.transporter = new BaseEmailClient(); + break; + case "sendgrid": + this.transporter = new SendGridEmailClient(); + break; + case "mailgun": + this.transporter = new BaseEmailClient(); + break; + case "mailjet": + this.transporter = new BaseEmailClient(); + break; + default: + console.error(`[Email] Invalid provider: ${provider}`); + return; + } + + if (!this.transporter) return console.error(`[Email] Invalid provider: ${provider}`); console.log(`[Email] Initializing ${provider} transport...`); - const transporter = await transporterFn(); - if (!transporter) return; - this.transporter = transporter; + await this.transporter.init(); console.log(`[Email] ${provider} transport initialized.`); }, + /** * Replaces all placeholders in an email template with the correct values */ @@ -134,6 +139,7 @@ export const Email: { return template; }, + /** * * @param id user id @@ -160,24 +166,48 @@ export const Email: { sendMail: async function (type, user, email) { if (!this.transporter) return; - const templateNames: { [key in MailTypes]: string } = { + const htmlTemplateNames: { [key in MailTypes]: string } = { verifyEmail: "verify_email.html", resetPassword: "password_reset_request.html", changePassword: "password_changed.html", }; - const template = await fs.readFile( + const textTemplateNames: { [key in MailTypes]: string } = { + verifyEmail: "verify_email.txt", + resetPassword: "password_reset_request.txt", + changePassword: "password_changed.txt", + }; + + const htmlTemplate = await fs.readFile( path.join( ASSET_FOLDER_PATH, "email_templates", - templateNames[type], + htmlTemplateNames[type], + ), + { encoding: "utf-8" }, + ); + + const textTemplate = await fs.readFile( + path.join( + ASSET_FOLDER_PATH, + "email_templates", + textTemplateNames[type], ), { encoding: "utf-8" }, ); // replace email template placeholders const html = this.doReplacements( - template, + htmlTemplate, + user, + // password change emails don't have links + type != MailTypes.changePassword + ? await this.generateLink(type, user.id, email) + : undefined, + ); + + const text = this.doReplacements( + textTemplate, user, // password change emails don't have links type != MailTypes.changePassword @@ -188,13 +218,14 @@ export const Email: { // extract the title from the email template to use as the email subject const subject = html.match(/(.*)<\/title>/)?.[1] || ""; - const message = { + const message: IEmail = { from: Config.get().email.senderAddress || Config.get().general.correspondenceEmail || "noreply@localhost", to: email, subject, + text, html, }; @@ -207,12 +238,14 @@ export const Email: { sendVerifyEmail: async function (user, email) { return this.sendMail(MailTypes.verifyEmail, user, email); }, + /** * Sends an email to the user with a link to reset their password */ sendResetPassword: async function (user, email) { return this.sendMail(MailTypes.resetPassword, user, email); }, + /** * Sends an email to the user notifying them that their password has been changed */ diff --git a/src/util/util/email/transports/SendGrid.ts b/src/util/util/email/transports/SendGrid.ts deleted file mode 100644 index a863d3ec..00000000 --- a/src/util/util/email/transports/SendGrid.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 Spacebar and Spacebar 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 { Config } from "@spacebar/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)); -}