mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
feat: add smtp adapter (#279)
This commit is contained in:
112
lib/notification/adapter/smtp.js
Normal file
112
lib/notification/adapter/smtp.js
Normal file
@@ -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.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
22
lib/notification/adapter/smtp.md
Normal file
22
lib/notification/adapter/smtp.md
Normal file
@@ -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
|
||||||
@@ -82,6 +82,7 @@
|
|||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"node-mailjet": "6.0.11",
|
"node-mailjet": "6.0.11",
|
||||||
|
"nodemailer": "^8.0.3",
|
||||||
"p-throttle": "^8.1.0",
|
"p-throttle": "^8.1.0",
|
||||||
"package-up": "^5.0.0",
|
"package-up": "^5.0.0",
|
||||||
"puppeteer": "^24.39.1",
|
"puppeteer": "^24.39.1",
|
||||||
|
|||||||
@@ -158,11 +158,11 @@ export default function NotificationAdapterMutator({
|
|||||||
{uiElement.type === 'boolean' ? (
|
{uiElement.type === 'boolean' ? (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
||||||
<Switch
|
<Switch
|
||||||
checked={uiElement.value || false}
|
checked={uiElement.value || false}
|
||||||
onChange={(checked) => {
|
onChange={(checked) => {
|
||||||
setValue(selectedAdapter, uiElement, key, checked);
|
setValue(selectedAdapter, uiElement, key, checked);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{uiElement.label}
|
{uiElement.label}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -173,6 +173,7 @@ export default function NotificationAdapterMutator({
|
|||||||
initValue={uiElement.value ?? ''}
|
initValue={uiElement.value ?? ''}
|
||||||
placeholder={uiElement.label}
|
placeholder={uiElement.label}
|
||||||
label={uiElement.label}
|
label={uiElement.label}
|
||||||
|
extraText={uiElement.description}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setValue(selectedAdapter, uiElement, key, value);
|
setValue(selectedAdapter, uiElement, key, value);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -5721,6 +5721,11 @@ node-releases@^2.0.27:
|
|||||||
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz"
|
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz"
|
||||||
integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==
|
integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==
|
||||||
|
|
||||||
|
nodemailer@^8.0.3:
|
||||||
|
version "8.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-8.0.3.tgz#6c5c10d3e70b8ca1b311646c4d03e1b206ef168c"
|
||||||
|
integrity sha512-JQNBqvK+bj3NMhUFR3wmCl3SYcOeMotDiwDBvIoCuQdF0PvlIY0BH+FJ2CG7u4cXKPChplE78oowlH/Otsc4ZQ==
|
||||||
|
|
||||||
nodemon@^3.1.14:
|
nodemon@^3.1.14:
|
||||||
version "3.1.14"
|
version "3.1.14"
|
||||||
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.14.tgz#8487ca379c515301d221ec007f27f24ecafa2b51"
|
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.14.tgz#8487ca379c515301d221ec007f27f24ecafa2b51"
|
||||||
|
|||||||
Reference in New Issue
Block a user