167 lines
7.1 KiB
JavaScript
167 lines
7.1 KiB
JavaScript
/** 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, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
}
|
||
|
||
/** 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,
|
||
}
|