diff options
author | Raphael <mail@raphaelkabo.com> | 2025-05-28 18:08:59 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-05-28 18:08:59 +0100 |
commit | 666af8ca0a220f4aab1642c28e0008e373ab1689 (patch) | |
tree | df8dacfd279760f505d4b0f6dcf195a6a44a6a1b | |
parent | bde9b408342f56833cf0a514488365189083f312 (diff) | |
parent | a5e2faf58ddd4793a5f7e3e284b023162d69cbb3 (diff) |
Merge pull request #210 from lowercasename/raphael/add-mailgun
Add Mailgun email service
-rw-r--r-- | config/config.example.toml | 10 | ||||
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | pnpm-lock.yaml | 36 | ||||
-rw-r--r-- | src/lib/config.ts | 7 | ||||
-rw-r--r-- | src/lib/email.ts | 71 |
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, }); } } |