Port SendGrid to new interface
This commit is contained in:
parent
e1e8850c9a
commit
533a72c3d3
@ -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
BIN
package-lock.json
generated
Binary file not shown.
@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
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 { 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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -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