Implementation du mail
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* mailer.js — Module d'envoi d'email
|
||||
*
|
||||
* Charge la config SMTP depuis la DB (source de vérité).
|
||||
* Fallback : variables d'environnement SMTP_* si la DB est vide.
|
||||
*
|
||||
* Usage :
|
||||
* import { sendMail, buildEmailHtml } from '../utils/mailer.js';
|
||||
* await sendMail({ to, subject, html: buildEmailHtml({ title, body, ctaLabel, ctaUrl }) });
|
||||
*/
|
||||
|
||||
import nodemailer from 'nodemailer';
|
||||
import db from '../db/index.js';
|
||||
|
||||
/** Charge la config SMTP active (DB en priorité, .env en fallback). */
|
||||
export function getSmtpConfig() {
|
||||
const row = db.prepare('SELECT * FROM smtp_config WHERE id = 1').get();
|
||||
|
||||
return {
|
||||
enabled: !!(row?.enabled),
|
||||
host: row?.host || process.env.SMTP_HOST || '',
|
||||
port: row?.port || parseInt(process.env.SMTP_PORT || '587', 10),
|
||||
secure: !!(row?.secure),
|
||||
email: row?.email || process.env.SMTP_EMAIL || '',
|
||||
username: row?.username || process.env.SMTP_USERNAME || '',
|
||||
password: row?.password || process.env.SMTP_PASSWORD || '',
|
||||
allowUnauth: !!(row?.allow_unauth),
|
||||
appName: row?.app_name || process.env.APP_NAME || 'Crowdlending',
|
||||
appUrl: row?.app_url || process.env.APP_URL || '',
|
||||
};
|
||||
}
|
||||
|
||||
/** Résout l'URL absolue de l'icône principale de l'app (peut être null). */
|
||||
function getAppIconUrl(appUrl) {
|
||||
if (!appUrl) return null;
|
||||
try {
|
||||
const icon = db.prepare(
|
||||
`SELECT filename FROM app_icons ORDER BY updated_at DESC LIMIT 1`
|
||||
).get();
|
||||
return icon ? `${appUrl.replace(/\/$/, '')}/api/icons-files/${icon.filename}` : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit un email HTML au format plateforme.
|
||||
*
|
||||
* @param {{
|
||||
* title: string, // Titre principal (h1)
|
||||
* body: string, // Corps HTML (paragraphes, etc.)
|
||||
* ctaLabel?: string, // Texte du bouton CTA
|
||||
* ctaUrl?: string, // URL du bouton CTA
|
||||
* appName?: string, // Surcharge nom app
|
||||
* appUrl?: string, // Surcharge URL app
|
||||
* }} opts
|
||||
*/
|
||||
export function buildEmailHtml({ title, body, ctaLabel, ctaUrl, appName, appUrl } = {}) {
|
||||
const cfg = getSmtpConfig();
|
||||
const name = appName || cfg.appName;
|
||||
const url = appUrl || cfg.appUrl;
|
||||
const iconUrl = getAppIconUrl(url);
|
||||
|
||||
const iconHtml = iconUrl
|
||||
? `<img src="${iconUrl}" alt="${name}" width="48" height="48"
|
||||
style="display:block;border-radius:10px;margin:0 auto 12px;" />`
|
||||
: '';
|
||||
|
||||
const ctaHtml = ctaLabel && ctaUrl
|
||||
? `<div style="text-align:center;margin:32px 0 8px;">
|
||||
<a href="${ctaUrl}"
|
||||
style="display:inline-block;padding:12px 28px;background:#6366f1;color:#fff;
|
||||
text-decoration:none;border-radius:8px;font-weight:600;font-size:15px;">
|
||||
${ctaLabel}
|
||||
</a>
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
const footerLink = url
|
||||
? `<a href="${url}" style="color:#6366f1;text-decoration:none;">${url}</a>`
|
||||
: name;
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<title>${title}</title>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f4f4f5;font-family:system-ui,-apple-system,sans-serif;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f4f4f5;padding:40px 16px;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table width="560" cellpadding="0" cellspacing="0"
|
||||
style="background:#ffffff;border-radius:12px;overflow:hidden;
|
||||
box-shadow:0 2px 8px rgba(0,0,0,0.06);max-width:100%;">
|
||||
|
||||
<!-- En-tête -->
|
||||
<tr>
|
||||
<td style="background:#6366f1;padding:32px 40px;text-align:center;">
|
||||
${iconHtml}
|
||||
<h1 style="margin:0;color:#ffffff;font-size:22px;font-weight:700;
|
||||
letter-spacing:-0.3px;">${name}</h1>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Corps -->
|
||||
<tr>
|
||||
<td style="padding:36px 40px 8px;">
|
||||
<h2 style="margin:0 0 16px;color:#111827;font-size:18px;font-weight:600;">
|
||||
${title}
|
||||
</h2>
|
||||
<div style="color:#374151;font-size:15px;line-height:1.65;">
|
||||
${body}
|
||||
</div>
|
||||
${ctaHtml}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Pied de page -->
|
||||
<tr>
|
||||
<td style="padding:24px 40px 32px;border-top:1px solid #f3f4f6;
|
||||
text-align:center;color:#9ca3af;font-size:12px;margin-top:16px;">
|
||||
Cet email a été envoyé automatiquement par ${footerLink}.<br/>
|
||||
Merci de ne pas y répondre directement.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
/** Crée un transporteur nodemailer à partir de la config active. */
|
||||
function createTransport(cfg) {
|
||||
return nodemailer.createTransport({
|
||||
host: cfg.host,
|
||||
port: cfg.port,
|
||||
secure: cfg.secure,
|
||||
auth: {
|
||||
user: cfg.username,
|
||||
pass: cfg.password,
|
||||
},
|
||||
tls: cfg.allowUnauth ? { rejectUnauthorized: false } : undefined,
|
||||
connectionTimeout: 8000,
|
||||
greetingTimeout: 5000,
|
||||
socketTimeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie un email.
|
||||
* @param {{ to: string, subject: string, text?: string, html?: string }} opts
|
||||
*/
|
||||
export async function sendMail({ to, subject, text, html }) {
|
||||
const cfg = getSmtpConfig();
|
||||
|
||||
if (!cfg.enabled) {
|
||||
throw new Error('SMTP désactivé — activez-le dans Administration > SMTP.');
|
||||
}
|
||||
if (!cfg.host || !cfg.email) {
|
||||
throw new Error('Config SMTP incomplète (hôte ou courriel manquant).');
|
||||
}
|
||||
|
||||
const transporter = createTransport(cfg);
|
||||
const info = await transporter.sendMail({
|
||||
from: `"${cfg.appName}" <${cfg.email}>`,
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
});
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie la connexion SMTP sans envoyer d'email.
|
||||
* Renvoie { ok: true } ou lance une erreur.
|
||||
*/
|
||||
export async function verifySmtp() {
|
||||
const cfg = getSmtpConfig();
|
||||
if (!cfg.host) throw new Error('Hôte SMTP manquant.');
|
||||
const transporter = createTransport(cfg);
|
||||
await transporter.verify();
|
||||
return { ok: true };
|
||||
}
|
||||
Reference in New Issue
Block a user