summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRaphael <mail@raphaelkabo.com>2025-05-28 18:08:59 +0100
committerGitHub <noreply@github.com>2025-05-28 18:08:59 +0100
commit666af8ca0a220f4aab1642c28e0008e373ab1689 (patch)
treedf8dacfd279760f505d4b0f6dcf195a6a44a6a1b
parentbde9b408342f56833cf0a514488365189083f312 (diff)
parenta5e2faf58ddd4793a5f7e3e284b023162d69cbb3 (diff)
Merge pull request #210 from lowercasename/raphael/add-mailgun
Add Mailgun email service
-rw-r--r--config/config.example.toml10
-rw-r--r--package.json1
-rw-r--r--pnpm-lock.yaml36
-rw-r--r--src/lib/config.ts7
-rw-r--r--src/lib/email.ts71
5 files changed, 113 insertions, 12 deletions
diff --git a/config/config.example.toml b/config/config.example.toml
index 32e2039..b3fed54 100644
--- a/config/config.example.toml
+++ b/config/config.example.toml
@@ -43,6 +43,16 @@ smtp_password = ""
[sendgrid]
api_key = ""
+[mailgun]
+# The base domain you have set up in Mailgun, for example 'mg.gath.io'.
+domain = ""
+# Your Mailgun sending API key for the domain you have set up.
+api_key = ""
+# This will be either https://api.mailgun.net (US) or https://api.eu.mailgun.net (EU)
+# depending on the region in your domain settings.
+api_url = "https://api.mailgun.net"
+
+
# Links to static pages (for example a privacy policy) or an external community page,
# which will be displayed in the footer.
# If paths begin with a slash, they are treated as internal and will open the specified
diff --git a/package.json b/package.json
index 91b8034..4f74f3d 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
"ical-generator": "^1.15.4",
"jimp": "^0.16.13",
"jsdom": "^22.1.0",
+ "mailgun.js": "^12.0.2",
"marked": "^12.0.2",
"moment": "^2.30.1",
"moment-timezone": "^0.5.45",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d3019ef..41bd9f0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -77,6 +77,9 @@ importers:
jsdom:
specifier: ^22.1.0
version: 22.1.0
+ mailgun.js:
+ specifier: ^12.0.2
+ version: 12.0.2
marked:
specifier: ^12.0.2
version: 12.0.2
@@ -630,9 +633,15 @@ packages:
axios@1.7.2:
resolution: {integrity: sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==}
+ axios@1.9.0:
+ resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==}
+
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+ base-64@1.0.0:
+ resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==}
+
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
@@ -1549,6 +1558,10 @@ packages:
luxon@1.28.1:
resolution: {integrity: sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw==}
+ mailgun.js@12.0.2:
+ resolution: {integrity: sha512-Ck7Vtmv7VarXntv6iGYsV3HB+yjV0nScETjuwBaCF4p6kxYRdcw72oWerPwaY7M2hVswTdTHOlY8EaOt6Ax0EA==}
+ engines: {node: '>=18.0.0'}
+
marked@12.0.2:
resolution: {integrity: sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==}
engines: {node: '>= 18'}
@@ -2313,6 +2326,9 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+ url-join@4.0.1:
+ resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==}
+
url-parse@1.5.10:
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
@@ -3021,8 +3037,18 @@ snapshots:
transitivePeerDependencies:
- debug
+ axios@1.9.0:
+ dependencies:
+ follow-redirects: 1.15.6
+ form-data: 4.0.0
+ proxy-from-env: 1.1.0
+ transitivePeerDependencies:
+ - debug
+
balanced-match@1.0.2: {}
+ base-64@1.0.0: {}
+
base64-js@1.5.1: {}
bcrypt-pbkdf@1.0.2:
@@ -4097,6 +4123,14 @@ snapshots:
luxon@1.28.1:
optional: true
+ mailgun.js@12.0.2:
+ dependencies:
+ axios: 1.9.0
+ base-64: 1.0.0
+ url-join: 4.0.1
+ transitivePeerDependencies:
+ - debug
+
marked@12.0.2: {}
media-typer@0.3.0: {}
@@ -4811,6 +4845,8 @@ snapshots:
dependencies:
punycode: 2.3.1
+ url-join@4.0.1: {}
+
url-parse@1.5.10:
dependencies:
querystringify: 2.2.0
diff --git a/src/lib/config.ts b/src/lib/config.ts
index b08fa31..35fc42c 100644
--- a/src/lib/config.ts
+++ b/src/lib/config.ts
@@ -22,7 +22,7 @@ export interface GathioConfig {
email_logo_url: string;
show_kofi: boolean;
show_public_event_list: boolean;
- mail_service: "nodemailer" | "sendgrid" | "none";
+ mail_service: "nodemailer" | "sendgrid" | "mailgun" | "none";
creator_email_addresses: string[];
};
database: {
@@ -38,6 +38,11 @@ export interface GathioConfig {
sendgrid?: {
api_key: string;
};
+ mailgun?: {
+ api_key: string;
+ api_url: string;
+ domain: string;
+ };
static_pages?: StaticPage[];
}
diff --git a/src/lib/email.ts b/src/lib/email.ts
index 7c7c2dd..fc585a1 100644
--- a/src/lib/email.ts
+++ b/src/lib/email.ts
@@ -5,6 +5,8 @@ import nodemailer, { Transporter } from "nodemailer";
import { GathioConfig, getConfig } from "./config.js";
import SMTPTransport from "nodemailer/lib/smtp-transport/index.js";
import { exitWithError } from "./process.js";
+import Mailgun from "mailgun.js";
+import { IMailgunClient } from "node_modules/mailgun.js/Types/Interfaces/index.js";
const config = getConfig();
@@ -24,7 +26,8 @@ type EmailTemplateName =
export class EmailService {
nodemailerTransporter: Transporter | undefined = undefined;
sgMail: typeof sgMail | undefined = undefined;
- hbs: ExpressHandlebars
+ mailgunClient: IMailgunClient | undefined = undefined;
+ hbs: ExpressHandlebars;
public constructor(config: GathioConfig, hbs: ExpressHandlebars) {
this.hbs = hbs;
@@ -40,6 +43,26 @@ export class EmailService {
console.log("Sendgrid is ready to send emails.");
break;
}
+ case "mailgun": {
+ if (
+ !config.mailgun?.api_key ||
+ !config.mailgun?.api_url ||
+ !config.mailgun?.domain
+ ) {
+ return exitWithError(
+ "Mailgun is configured as the email service, but not all required fields are provided. Please provide all required fields in the config file.",
+ );
+ }
+ const mailgun = new Mailgun(FormData);
+ this.mailgunClient = mailgun.client({
+ username: "api",
+ key: config.mailgun.api_key,
+ url: config.mailgun.api_url,
+ });
+ // TODO: Can we verify the Mailgun connection?
+ console.log("Mailgun is ready to send emails.");
+ break;
+ }
case "nodemailer": {
if (config.nodemailer?.smtp_url) {
this.nodemailerTransporter = nodemailer.createTransport(
@@ -73,13 +96,13 @@ export class EmailService {
nodemailer.createTransport(nodemailerConfig);
}
}
-
}
}
public async verify(): Promise<boolean> {
if (this.nodemailerTransporter) {
- const nodemailerVerified = await this.nodemailerTransporter.verify();
+ const nodemailerVerified =
+ await this.nodemailerTransporter.verify();
if (nodemailerVerified) {
console.log("Nodemailer is ready to send emails.");
return true;
@@ -118,9 +141,36 @@ export class EmailService {
return true;
} catch (e: unknown | sgHelpers.classes.ResponseError) {
if (e instanceof sgHelpers.classes.ResponseError) {
- console.error('sendgrid error', e.response.body);
+ console.error("sendgrid error", e.response.body);
+ } else {
+ console.error("sendgrid error", e);
+ }
+ return false;
+ }
+ } else if (this.mailgunClient) {
+ try {
+ if (!config.mailgun?.domain) {
+ return exitWithError(
+ "Mailgun is configured as the email service, but no domain is provided. Please provide a domain in the config file.",
+ );
+ }
+ await this.mailgunClient.messages.create(
+ config.mailgun.domain,
+ {
+ from: config.general.email,
+ to,
+ bcc,
+ subject: `${config.general.site_name}: ${subject}`,
+ text,
+ html,
+ },
+ );
+ return true;
+ } catch (e: any) {
+ if (e.response) {
+ console.error(e.response.body);
} else {
- console.error('sendgrid error', e);
+ console.error(e);
}
return false;
}
@@ -150,15 +200,14 @@ export class EmailService {
bcc = "",
subject,
templateName,
- templateData = {}
+ templateData = {},
}: {
to: string | string[];
bcc?: string | string[] | undefined;
subject: string;
templateName: EmailTemplateName;
templateData?: object;
- },
- ): Promise<boolean> {
+ }): Promise<boolean> {
const [html, text] = await Promise.all([
this.hbs.renderView(
`./views/emails/${templateName}/${templateName}Html.handlebars`,
@@ -172,7 +221,7 @@ export class EmailService {
cache: true,
layout: "email.handlebars",
...templateData,
- }
+ },
),
this.hbs.renderView(
`./views/emails/${templateName}/${templateName}Text.handlebars`,
@@ -186,7 +235,7 @@ export class EmailService {
cache: true,
layout: "email.handlebars",
...templateData,
- }
+ },
),
]);
@@ -195,7 +244,7 @@ export class EmailService {
bcc,
subject: `${config.general.site_name}: ${subject}`,
text,
- html
+ html,
});
}
}