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 = ''
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"

BIN
package-lock.json generated

Binary file not shown.

View File

@ -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"
}

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 { 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<Transporter<unknown> | void>;
} = {
smtp: SMTP,
mailgun: MailGun,
mailjet: MailJet,
sendgrid: SendGrid,
};
export const Email: {
transporter: Transporter | null;
transporter: IEmailClient | null;
init: () => Promise<void>;
generateLink: (
type: Omit<MailTypes, "changePassword">,
@ -63,13 +52,13 @@ export const Email: {
type: MailTypes,
user: User,
email: string,
) => Promise<SentMessageInfo>;
sendVerifyEmail: (user: User, email: string) => Promise<SentMessageInfo>;
sendResetPassword: (user: User, email: string) => Promise<SentMessageInfo>;
) => Promise<void>;
sendVerifyEmail: (user: User, email: string) => Promise<void>;
sendResetPassword: (user: User, email: string) => Promise<void>;
sendPasswordChanged: (
user: User,
email: string,
) => Promise<SentMessageInfo>;
) => Promise<void>;
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>(.*)<\/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
*/

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