From 3523057221232b497a78db1bd41307dc8706e4b9 Mon Sep 17 00:00:00 2001 From: Adrian Bach <65734063+realDayaa@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:37:28 +0100 Subject: [PATCH] feat: add smtp adapter (#279) --- lib/notification/adapter/smtp.js | 112 ++++++++++++++++++ lib/notification/adapter/smtp.md | 22 ++++ package.json | 1 + .../NotificationAdapterMutator.jsx | 11 +- yarn.lock | 5 + 5 files changed, 146 insertions(+), 5 deletions(-) create mode 100644 lib/notification/adapter/smtp.js create mode 100644 lib/notification/adapter/smtp.md diff --git a/lib/notification/adapter/smtp.js b/lib/notification/adapter/smtp.js new file mode 100644 index 0000000..494a6fb --- /dev/null +++ b/lib/notification/adapter/smtp.js @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2026 by Christian Kellner. + * Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause + */ + +import nodemailer from 'nodemailer'; +import path from 'path'; +import fs from 'fs'; +import Handlebars from 'handlebars'; +import { markdown2Html } from '../../services/markdown.js'; +import { getDirName, normalizeImageUrl } from '../../utils.js'; + +const __dirname = getDirName(); +const template = fs.readFileSync(path.resolve(__dirname + '/notification/emailTemplate/template.hbs'), 'utf8'); +const emailTemplate = Handlebars.compile(template); + +const mapListings = (serviceName, jobKey, listings) => + listings.map((l) => { + const image = normalizeImageUrl(l.image); + return { + title: l.title || '', + link: l.link || '', + address: l.address || '', + size: l.size || '', + price: l.price || '', + image, + hasImage: Boolean(image), + serviceName, + jobKey, + }; + }); + +export const send = async ({ serviceName, newListings, notificationConfig, jobKey }) => { + const { host, port, secure, username, password, receiver, from } = notificationConfig.find( + (adapter) => adapter.id === config.id, + ).fields; + + const to = receiver + .trim() + .split(',') + .map((r) => r.trim()) + .filter(Boolean); + + const transporter = nodemailer.createTransport({ + host, + port: Number(port), + secure: secure === 'true', + auth: { + user: username, + pass: password, + }, + }); + + const listings = mapListings(serviceName, jobKey, newListings); + + const html = emailTemplate({ + serviceName: `Job: (${jobKey}) | Service: ${serviceName}`, + numberOfListings: listings.length, + listings, + }); + + return transporter.sendMail({ + from, + to: to.join(','), + subject: `Fredy found ${listings.length} new listing(s) for ${serviceName}`, + html, + }); +}; + +export const config = { + id: 'smtp', + name: 'SMTP', + description: 'Send notifications via any SMTP server using Nodemailer.', + readme: markdown2Html('lib/notification/adapter/smtp.md'), + fields: { + host: { + type: 'text', + label: 'SMTP Host', + description: 'The hostname of the SMTP server (e.g., smtp.gmail.com).', + }, + port: { + type: 'text', + label: 'SMTP Port', + description: 'The port of the SMTP server (e.g., 587 for STARTTLS, 465 for SSL).', + }, + secure: { + type: 'text', + label: 'Secure (SSL/TLS)', + description: 'Set to "true" for port 465 (SSL). Leave empty or "false" for STARTTLS on port 587.', + }, + username: { + type: 'text', + label: 'Username', + description: 'The username for SMTP authentication.', + }, + password: { + type: 'text', + label: 'Password', + description: 'The password (or app password) for SMTP authentication.', + }, + receiver: { + type: 'text', + label: 'Receiver Email(s)', + description: 'Comma-separated email addresses Fredy will send notifications to.', + }, + from: { + type: 'email', + label: 'Sender Email', + description: 'The email address Fredy sends from.', + }, + }, +}; diff --git a/lib/notification/adapter/smtp.md b/lib/notification/adapter/smtp.md new file mode 100644 index 0000000..28f7887 --- /dev/null +++ b/lib/notification/adapter/smtp.md @@ -0,0 +1,22 @@ +### SMTP Adapter + +Send notifications through any SMTP server using [Nodemailer](https://nodemailer.com/). +This works with Gmail, Outlook, self-hosted mail servers, or any provider that supports SMTP. + +Setup: + +- Provide the SMTP host and port of your mail server. +- For **SSL/TLS** (port 465), set Secure to `true`. +- For **STARTTLS** (port 587), leave Secure empty or set it to `false`. +- Enter the username and password for authentication. For Gmail, use an [App Password](https://support.google.com/accounts/answer/185833). +- Set the sender email address (must be allowed by your SMTP server). + +Multiple recipients: + +- Separate email addresses with commas (e.g., `some@email.com`, `someOther@email.com`). + +Common SMTP settings: + +- **Gmail** — `smtp.gmail.com`, port 587, secure: false +- **Outlook** — `smtp.office365.com`, port 587, secure: false +- **Yahoo** — `smtp.mail.yahoo.com`, port 465, secure: true diff --git a/package.json b/package.json index 40d67c0..e79ce96 100755 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "node-cron": "^4.2.1", "node-fetch": "3.3.2", "node-mailjet": "6.0.11", + "nodemailer": "^8.0.3", "p-throttle": "^8.1.0", "package-up": "^5.0.0", "puppeteer": "^24.39.1", diff --git a/ui/src/views/jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx b/ui/src/views/jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx index d9c714b..ad432d0 100644 --- a/ui/src/views/jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx +++ b/ui/src/views/jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx @@ -158,11 +158,11 @@ export default function NotificationAdapterMutator({ {uiElement.type === 'boolean' ? (