diff options
| author | Raphael Kabo <mail@raphaelkabo.com> | 2025-05-27 19:07:27 +0100 | 
|---|---|---|
| committer | Raphael Kabo <mail@raphaelkabo.com> | 2025-05-27 19:07:27 +0100 | 
| commit | bc9e983b16d9ac2d27a4458c0a87f9d11aa80c0e (patch) | |
| tree | 287e122c66548c4e44995492f044b1f3409516c7 | |
| parent | 69f75005303d634b9208c23068655385734f4d3a (diff) | |
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 2de0373..6f60eb5 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@          "ical-generator": "^1.15.4",          "jimp": "^0.16.13",          "jsdom": "^22.1.0", +        "mailgun.js": "^12.0.2",          "marked": "^12.0.2",          "moment-timezone": "^0.5.45",          "mongoose": "^5.13.22", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11f04d9..ad78dbb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,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 @@ -585,9 +588,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==} @@ -1472,6 +1481,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'} @@ -2207,6 +2220,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==} @@ -2893,8 +2909,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: @@ -3934,6 +3960,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: {} @@ -4633,6 +4667,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 6642eef..3fd6eb7 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -21,7 +21,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: { @@ -37,6 +37,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,          });      }  }  | 
