Port SendGrid to new interface

This commit is contained in:
Rory& 2025-09-30 04:13:11 +02:00
parent e1e8850c9a
commit 533a72c3d3
7 changed files with 130 additions and 84 deletions

View File

@ -73,7 +73,7 @@
]; ];
text = '' text = ''
rm -rf node_modules 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) DEPS_HASH=$(prefetch-npm-deps package-lock.json)
TMPFILE=$(mktemp) TMPFILE=$(mktemp)
jq '.npmDepsHash = "'"$DEPS_HASH"'"' hashes.json > "$TMPFILE" jq '.npmDepsHash = "'"$DEPS_HASH"'"' hashes.json > "$TMPFILE"

BIN
package-lock.json generated

Binary file not shown.

View File

@ -123,12 +123,12 @@
"@spacebar/webrtc": "dist/webrtc" "@spacebar/webrtc": "dist/webrtc"
}, },
"optionalDependencies": { "optionalDependencies": {
"@sendgrid/mail": "^8.1.6",
"@yukikaze-bot/erlpack": "^1.0.1", "@yukikaze-bot/erlpack": "^1.0.1",
"jimp": "^1.6.0", "jimp": "^1.6.0",
"mysql": "^2.18.1", "mysql": "^2.18.1",
"nodemailer-mailgun-transport": "^2.1.5", "nodemailer-mailgun-transport": "^2.1.5",
"nodemailer-mailjet-transport": "github:n0script22/nodemailer-mailjet-transport", "nodemailer-mailjet-transport": "github:n0script22/nodemailer-mailjet-transport",
"nodemailer-sendgrid-transport": "github:Maria-Golomb/nodemailer-sendgrid-transport",
"pg": "^8.16.3", "pg": "^8.16.3",
"sqlite3": "^5.1.7" "sqlite3": "^5.1.7"
} }

View File

@ -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<void>;
sendMail: (
email: IEmail,
) => Promise<void>;
}
export class BaseEmailClient implements IEmailClient {
async init(): Promise<void> {
return;
}
sendMail(email: IEmail): Promise<void> {
throw new Error("Method not implemented.");
}
}

View File

@ -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<void> {
// 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<void> {
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,
});
}
}

View File

@ -22,10 +22,8 @@ import { SentMessageInfo, Transporter } from "nodemailer";
import { User } from "../../entities"; import { User } from "../../entities";
import { Config } from "../Config"; import { Config } from "../Config";
import { generateToken } from "../Token"; import { generateToken } from "../Token";
import MailGun from "./transports/MailGun"; import { BaseEmailClient, IEmail, IEmailClient } from "./clients/IEmailClient";
import MailJet from "./transports/MailJet"; import { SendGridEmailClient } from "./clients/SendGridEmailClient";
import SMTP from "./transports/SMTP";
import SendGrid from "./transports/SendGrid";
const ASSET_FOLDER_PATH = path.join( const ASSET_FOLDER_PATH = path.join(
__dirname, __dirname,
@ -36,23 +34,14 @@ const ASSET_FOLDER_PATH = path.join(
"assets", "assets",
); );
enum MailTypes { export enum MailTypes {
verifyEmail = "verifyEmail", verifyEmail = "verifyEmail",
resetPassword = "resetPassword", resetPassword = "resetPassword",
changePassword = "changePassword", changePassword = "changePassword",
} }
const transporters: {
[key: string]: () => Promise<Transporter<unknown> | void>;
} = {
smtp: SMTP,
mailgun: MailGun,
mailjet: MailJet,
sendgrid: SendGrid,
};
export const Email: { export const Email: {
transporter: Transporter | null; transporter: IEmailClient | null;
init: () => Promise<void>; init: () => Promise<void>;
generateLink: ( generateLink: (
type: Omit<MailTypes, "changePassword">, type: Omit<MailTypes, "changePassword">,
@ -63,13 +52,13 @@ export const Email: {
type: MailTypes, type: MailTypes,
user: User, user: User,
email: string, email: string,
) => Promise<SentMessageInfo>; ) => Promise<void>;
sendVerifyEmail: (user: User, email: string) => Promise<SentMessageInfo>; sendVerifyEmail: (user: User, email: string) => Promise<void>;
sendResetPassword: (user: User, email: string) => Promise<SentMessageInfo>; sendResetPassword: (user: User, email: string) => Promise<void>;
sendPasswordChanged: ( sendPasswordChanged: (
user: User, user: User,
email: string, email: string,
) => Promise<SentMessageInfo>; ) => Promise<void>;
doReplacements: ( doReplacements: (
template: string, template: string,
user: User, user: User,
@ -87,15 +76,31 @@ export const Email: {
const { provider } = Config.get().email; const { provider } = Config.get().email;
if (!provider) return; if (!provider) return;
const transporterFn = transporters[provider]; switch (provider) {
if (!transporterFn) 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}`); return console.error(`[Email] Invalid provider: ${provider}`);
console.log(`[Email] Initializing ${provider} transport...`); console.log(`[Email] Initializing ${provider} transport...`);
const transporter = await transporterFn(); await this.transporter.init();
if (!transporter) return;
this.transporter = transporter;
console.log(`[Email] ${provider} transport initialized.`); console.log(`[Email] ${provider} transport initialized.`);
}, },
/** /**
* Replaces all placeholders in an email template with the correct values * Replaces all placeholders in an email template with the correct values
*/ */
@ -134,6 +139,7 @@ export const Email: {
return template; return template;
}, },
/** /**
* *
* @param id user id * @param id user id
@ -160,24 +166,48 @@ export const Email: {
sendMail: async function (type, user, email) { sendMail: async function (type, user, email) {
if (!this.transporter) return; if (!this.transporter) return;
const templateNames: { [key in MailTypes]: string } = { const htmlTemplateNames: { [key in MailTypes]: string } = {
verifyEmail: "verify_email.html", verifyEmail: "verify_email.html",
resetPassword: "password_reset_request.html", resetPassword: "password_reset_request.html",
changePassword: "password_changed.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( path.join(
ASSET_FOLDER_PATH, ASSET_FOLDER_PATH,
"email_templates", "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" }, { encoding: "utf-8" },
); );
// replace email template placeholders // replace email template placeholders
const html = this.doReplacements( 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, user,
// password change emails don't have links // password change emails don't have links
type != MailTypes.changePassword type != MailTypes.changePassword
@ -188,13 +218,14 @@ export const Email: {
// extract the title from the email template to use as the email subject // extract the title from the email template to use as the email subject
const subject = html.match(/<title>(.*)<\/title>/)?.[1] || ""; const subject = html.match(/<title>(.*)<\/title>/)?.[1] || "";
const message = { const message: IEmail = {
from: from:
Config.get().email.senderAddress || Config.get().email.senderAddress ||
Config.get().general.correspondenceEmail || Config.get().general.correspondenceEmail ||
"noreply@localhost", "noreply@localhost",
to: email, to: email,
subject, subject,
text,
html, html,
}; };
@ -207,12 +238,14 @@ export const Email: {
sendVerifyEmail: async function (user, email) { sendVerifyEmail: async function (user, email) {
return this.sendMail(MailTypes.verifyEmail, user, email); return this.sendMail(MailTypes.verifyEmail, user, email);
}, },
/** /**
* Sends an email to the user with a link to reset their password * Sends an email to the user with a link to reset their password
*/ */
sendResetPassword: async function (user, email) { sendResetPassword: async function (user, email) {
return this.sendMail(MailTypes.resetPassword, user, email); return this.sendMail(MailTypes.resetPassword, user, email);
}, },
/** /**
* Sends an email to the user notifying them that their password has been changed * Sends an email to the user notifying them that their password has been changed
*/ */

View File

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