Files
Mieterportal/server/src/mail.js
T

167 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** Serverseitiger E-Mail-Versand über die Portal-Mail-API (mp-mail-api).
*
* Fire-and-forget: Ein Fehler (Mail-API down, Relay-Fehler) beeinträchtigt
* die auslösende Portal-Aktion nie. Die Mail-API selbst puffert im Fehlerfall
* in ihrer Outbox und versucht es erneut.
*
* Gestaltung wie im Schulportal: HTML-Karte (600 px, Kopfzeile in Portal-Farben,
* Zeilen-Tabelle, optional Status-Box/Zitat/Aktions-Button) plus Plain-Text-
* Fallback, immer beide an die Mail-API (sendet multipart/alternative).
*/
const MAIL_API_URL = process.env.MAIL_API_URL || '' // z. B. http://mp-mail-api:8080
const MAIL_API_TOKEN = process.env.MAIL_API_TOKEN || ''
const PORTAL_URL = process.env.PORTAL_URL || 'https://mieterportal.hocks.eu'
// Portal-Farben (Hinterstein): dunkles Petrol + Amber-Akzent (vgl. Login-Seite)
const FARBE_KOPF = '#22333b' // Dunkel-Petrol
const FARBE_KOPF_ZEILE = '#fcd34d' // Amber-200 (Kicker-Zeile)
const FARBE_UNTERTITEL = '#cbd5e1'
const FARBE_BUTTON = '#f59e0b' // Amber-500
const FARBE_BUTTON_TEXT = '#1c1917'
const HINTERGRUND = '#f1f5f9'
const KARTE = '#ffffff'
const TEXT = '#0f172a'
const TEXT_SOFT = '#64748b'
const RAND = '#e2e8f0'
const FUSSZEILE_TEXT =
'— Automatische Benachrichtigung aus dem Mieterportal der Hinterstein Immobilien'
function htmlEscape(s) {
return String(s)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
/** Baut die HTML-Karte im Schulportal-Stil. Alle Inhalte werden escaped. */
function baueHtmlMail(opts) {
const { titel, untertitel = '', einleitung = '', zeilen = [], statusBox, zitat, aktionenHtml = '', fussnote = '' } = opts
const zeilenHtml = zeilen
.map(
([label, wert]) => `
<tr>
<td style="padding:8px 12px 8px 0;vertical-align:top;color:${TEXT_SOFT};font-size:13px;white-space:nowrap;">${htmlEscape(label)}</td>
<td style="padding:8px 0;vertical-align:top;font-size:14px;color:${TEXT};">${htmlEscape(wert)}</td>
</tr>`,
)
.join('')
const statusHtml = statusBox
? `
<div style="margin:20px 0;padding:12px 16px;border-radius:8px;background:${statusBox.hintergrund};color:${statusBox.farbe};font-size:14px;font-weight:600;">${htmlEscape(statusBox.text)}</div>`
: ''
const zitatHtml = zitat
? `
<div style="margin:16px 0 0;padding:12px 16px;border-left:4px solid ${FARBE_BUTTON};background:#fffbeb;border-radius:0 8px 8px 0;font-size:14px;color:${TEXT};line-height:1.6;white-space:pre-wrap;">${htmlEscape(zitat)}</div>`
: ''
const fussnotenHtml = fussnote
? `<p style="margin:20px 0 0;font-size:12px;color:${TEXT_SOFT};line-height:1.6;border-top:1px solid ${RAND};padding-top:16px;">${htmlEscape(fussnote)}</p>`
: ''
return `<!DOCTYPE html>
<html lang="de">
<head><meta charset="utf-8"><title>${htmlEscape(titel)}</title></head>
<body style="margin:0;padding:0;background:${HINTERGRUND};">
<div style="display:none;max-height:0;overflow:hidden;">${htmlEscape(einleitung || titel)}</div>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:${HINTERGRUND};">
<tr>
<td align="center" style="padding:24px 12px;">
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;background:${KARTE};border-radius:12px;overflow:hidden;box-shadow:0 1px 3px rgba(15,23,42,.08);">
<tr>
<td style="background:${FARBE_KOPF};padding:20px 28px;">
<p style="margin:0;color:${FARBE_KOPF_ZEILE};font-size:12px;letter-spacing:.08em;text-transform:uppercase;">Hinterstein Immobilien</p>
<h1 style="margin:6px 0 0;color:#ffffff;font-size:18px;font-weight:700;">${htmlEscape(titel)}</h1>
${untertitel ? `<p style="margin:4px 0 0;color:${FARBE_UNTERTITEL};font-size:12px;">${htmlEscape(untertitel)}</p>` : ''}
</td>
</tr>
<tr>
<td style="padding:28px;">
${einleitung ? `<p style="margin:0 0 8px;font-size:14px;color:${TEXT};line-height:1.6;">${htmlEscape(einleitung)}</p>` : ''}
${statusHtml}
${zitatHtml}
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-top:1px solid ${RAND};">${zeilenHtml}</table>
${aktionenHtml}
${fussnotenHtml}
</td>
</tr>
<tr>
<td style="background:#f8fafc;padding:14px 28px;border-top:1px solid ${RAND};">
<p style="margin:0;font-size:11px;color:${TEXT_SOFT};">Hinterstein Immobilien (Demo) · <a href="${PORTAL_URL}" style="color:${FARBE_KOPF};">mieterportal.hocks.eu</a><br>Diese E-Mail wurde automatisch vom Mieterportal erzeugt. Bitte antworten Sie nicht direkt – nutzen Sie das Nachrichten-Modul im Portal.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>`
}
/** Aktions-Button (portal-interne Links). */
function buttonHtml(text, url) {
return `<p style="margin:24px 0 0;"><a href="${htmlEscape(url)}" style="display:inline-block;background:${FARBE_BUTTON};color:${FARBE_BUTTON_TEXT};text-decoration:none;font-size:14px;font-weight:700;padding:12px 22px;border-radius:10px;">${htmlEscape(text)}</a></p>`
}
async function sendeMail(an, betreff, text, html, cc) {
if (!MAIL_API_URL || !MAIL_API_TOKEN || !an) return
try {
const res = await fetch(`${MAIL_API_URL}/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Api-Token': MAIL_API_TOKEN },
body: JSON.stringify({
to: an, subject: betreff, text: `${text}\n\n${FUSSZEILE_TEXT}`,
...(html ? { html } : {}),
...(cc ? { cc } : {}), // v1.22: Komma-getrennte CC-Empfänger
}),
})
if (!res.ok) console.warn(`[mail] Versand an ${an} fehlgeschlagen: HTTP ${res.status}`)
} catch (e) {
console.warn(`[mail] Versand an ${an} fehlgeschlagen:`, e.message)
}
}
/** E-Mail-Adressen aller aktiven Verwaltungsnutzer (komma-getrennt, für CC). */
async function verwaltungEmails() {
const { rows } = await require('./db').pool.query(
`SELECT email FROM users WHERE rolle = 'verwaltung' AND aktiv AND email <> ''`,
)
return rows.map((r) => r.email).join(', ')
}
/** Alle aktiven Verwaltungsnutzer benachrichtigen. */
async function benachrichtigeVerwaltung(betreff, text, html) {
const { rows } = await require('./db').pool.query(
`SELECT email FROM users WHERE rolle = 'verwaltung' AND aktiv AND email <> ''`,
)
for (const r of rows) sendeMail(r.email, betreff, text, html)
}
/** Mieter einer bestimmten Wohnung benachrichtigen. */
async function benachrichtigeMieter(einheitId, betreff, text, html) {
const { rows } = await require('./db').pool.query(
`SELECT email FROM users WHERE rolle = 'mieter' AND aktiv AND einheit_id = $1 AND email <> ''`,
[einheitId],
)
for (const r of rows) sendeMail(r.email, betreff, text, html)
}
function einheitLabel(u) {
return u ? `${u.building}, Whg. ${u.number}` : ''
}
module.exports = {
sendeMail,
benachrichtigeVerwaltung,
benachrichtigeMieter,
verwaltungEmails,
einheitLabel,
baueHtmlMail,
buttonHtml,
}