Port SendGrid to new interface
This commit is contained in:
parent
e1e8850c9a
commit
533a72c3d3
@ -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
BIN
package-lock.json
generated
Binary file not shown.
@ -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"
|
||||
}
|
||||
|
||||
25
src/util/util/email/clients/IEmailClient.ts
Normal file
25
src/util/util/email/clients/IEmailClient.ts
Normal 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.");
|
||||
}
|
||||
}
|
||||
41
src/util/util/email/clients/SendGridEmailClient.ts
Normal file
41
src/util/util/email/clients/SendGridEmailClient.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user