- Add changelog entry for new feature - Update README with asset selection documentation - Document new /api/assets endpoint - Update /api/send with footerAssets parameter Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
16 KiB
Mail-Service mit Brevo SMTP
Ein Node.js-basierter Mail-Service mit Weboberfläche zum Versenden von E-Mails über Brevo (ehemals Sendinblue) SMTP. Inklusive persistenter Versand-Historie und eingebettetem Bild-Footer.
Features
- Weboberfläche zum Versenden von E-Mails (Empfänger, CC, Betreff, Text/HTML)
- Footer-Icon-Auswahl - Mehrere Icons aus dem
/assets-Ordner auswählbar (Thumbnail-Grid) - Persistente Historie in SQLite-Datenbank
- Eingebettete Bilder via CID-Attachment (werden ohne "Bilder anzeigen" dargestellt)
- REST-API für programmatischen Zugriff
- Brevo SMTP mit DKIM/SPF/DMARC-Unterstützung
Projektstruktur
mail-service/
├── .env # Konfiguration (nicht im Git)
├── .env.example # Vorlage
├── package.json
├── src/
│ ├── server.js # Express-Server, API-Routen
│ ├── mailer.js # Nodemailer mit CID-Footer
│ └── database.js # SQLite-Setup und Queries
├── public/
│ ├── index.html # Weboberfläche
│ ├── style.css # Styling
│ └── script.js # Frontend-Logik
├── assets/ # Footer-Icons (PNG/JPG/GIF/SVG)
│ ├── homeicon.png
│ ├── cal.png
│ └── ... # Weitere Icons automatisch verfügbar
└── data/
└── emails.db # SQLite-Datenbank (automatisch erstellt)
Installation
# Repository klonen
git clone https://git.unixweb.net/joachim/mail-service-embedded.git
cd mail-service-embedded
# Dependencies installieren
npm install
# Konfiguration erstellen
cp .env.example .env
# .env bearbeiten und SMTP-Credentials eintragen
Konfiguration (.env)
# Server
PORT=3000
# Email Configuration
MAIL_PROVIDER=smtp
MAIL_FROM_EMAIL=noreply@example.com
MAIL_FROM_NAME=Secure Portal
MAIL_FOOTER_NAME=Dein Name
# SMTP (Brevo)
SMTP_HOST=smtp-relay.brevo.com
SMTP_PORT=587
SMTP_USER=deine-email@example.com
SMTP_PASSWORD=dein-brevo-smtp-key
SMTP_SECURE=false
Brevo SMTP-Key erstellen
- Bei Brevo einloggen
- Gehe zu: Einstellungen → SMTP & API → SMTP
- "SMTP-Schlüssel generieren" klicken
- Schlüssel in
.envalsSMTP_PASSWORDeintragen
Starten
# Produktion
npm start
# Entwicklung (mit Auto-Reload)
npm run dev
Server läuft auf: http://localhost:3000
API-Endpunkte
| Methode | Route | Beschreibung |
|---|---|---|
| GET | / |
Weboberfläche |
| GET | /api/assets |
Verfügbare Footer-Icons abrufen |
| POST | /api/send |
E-Mail versenden |
| GET | /api/history |
Historie abrufen (letzte 50) |
| DELETE | /api/history/:id |
Einzelnen Eintrag löschen |
| DELETE | /api/history |
Gesamte Historie löschen |
GET /api/assets
Response:
{
"success": true,
"assets": ["blue.png", "cal.png", "cog.png", "homeicon.png", "psy.png"]
}
POST /api/send
Request Body:
{
"to": "empfaenger@example.com",
"cc": "optional@example.com",
"subject": "Betreff",
"body": "Nachrichteninhalt",
"isHtml": false,
"footerAssets": ["homeicon.png", "cal.png"]
}
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
| to | string | Ja | Empfänger-Adresse |
| cc | string | Nein | CC-Adresse |
| subject | string | Ja | Betreff |
| body | string | Ja | Nachrichteninhalt |
| isHtml | boolean | Nein | true = HTML-Format |
| footerAssets | string[] | Nein | Icons für Footer (aus /api/assets) |
Response (Erfolg):
{
"success": true,
"message": "E-Mail erfolgreich gesendet",
"messageId": "<abc123@smtp-relay.sendinblue.com>",
"id": 1
}
Response (Fehler):
{
"success": false,
"error": "Versand fehlgeschlagen: Connection refused",
"id": 2
}
Datenbank-Schema
Tabelle emails:
| Spalte | Typ | Beschreibung |
|---|---|---|
| id | INTEGER | Primary Key, Auto-Increment |
| to_email | TEXT | Empfänger-Adresse |
| cc_email | TEXT | CC-Adresse (optional) |
| subject | TEXT | Betreff |
| body | TEXT | Nachrichteninhalt |
| is_html | INTEGER | 0 = Text, 1 = HTML |
| status | TEXT | "success" oder "failed" |
| error_message | TEXT | Fehlermeldung (bei Fehler) |
| created_at | TEXT | ISO-Timestamp |
Bild-Footer mit CID-Embedding (Detaillierte Anleitung)
CID-Embedding (Content-ID) ist eine Technik, um Bilder direkt in E-Mails einzubetten, sodass sie ohne Benutzerinteraktion angezeigt werden - auch bei Gmail, Outlook und anderen Clients, die externe Bilder standardmäßig blockieren.
Das Problem mit externen Bildern
E-Mail-Clients blockieren externe Bilder aus Datenschutzgründen:
<!-- SCHLECHT: Externes Bild - wird blockiert -->
<img src="https://mein-server.de/logo.png" />
Warum blockieren E-Mail-Clients externe Bilder?
- Tracking-Schutz: Der Server sieht, wann/ob die E-Mail geöffnet wurde
- Datenschutz: IP-Adresse und Standort werden übermittelt
- Sicherheit: Potenzielle Malware-Vektoren
Resultat: Der Empfänger muss erst auf "Bilder anzeigen" klicken.
Die Lösung: CID-Embedding
Bei CID-Embedding wird das Bild als Teil der E-Mail selbst mitgesendet:
<!-- GUT: Eingebettetes Bild - wird sofort angezeigt -->
<img src="cid:mein-bild" />
Das Bild ist Base64-kodiert im E-Mail-Body enthalten - kein externer Request nötig.
Vergleich der Methoden
| Methode | Syntax | Verhalten | Dateigröße |
|---|---|---|---|
| Externe URL | src="https://..." |
Blockiert, Klick nötig | Klein (nur URL) |
| Base64 Inline | src="data:image/png;base64,..." |
Oft blockiert | Sehr groß im HTML |
| CID-Embedding | src="cid:bildname" |
Sofort sichtbar | Optimiert als Anhang |
Technischer Hintergrund: MIME-Struktur
Eine E-Mail mit CID-Bild hat folgende MIME-Struktur:
Content-Type: multipart/related; boundary="----boundary"
------boundary
Content-Type: text/html; charset=utf-8
<html>
<body>
<p>Hallo!</p>
<img src="cid:logo123" width="100" />
</body>
</html>
------boundary
Content-Type: image/png; name="logo.png"
Content-Disposition: inline; filename="logo.png"
Content-Id: <logo123>
Content-Transfer-Encoding: base64
iVBORw0KGgoAAAANSUhEUgAA... (Base64-Daten)
------boundary--
Wichtige MIME-Header für das Bild:
Content-Type: Bildformat (image/png, image/jpeg, etc.)Content-Disposition: inline: Bild wird im Body angezeigt, nicht als AnhangContent-Id: <logo123>: Die CID-Referenz (ohnecid:Präfix, mit spitzen Klammern)Content-Transfer-Encoding: base64: Binärdaten als Text kodiert
Implementierung mit Nodemailer
Schritt 1: Bild-Datei bereitstellen
projekt/
└── assets/
└── logo.png # Dein Bild (empfohlen: PNG, max 100KB)
Schritt 2: Nodemailer-Konfiguration
const nodemailer = require('nodemailer');
const path = require('path');
const transporter = nodemailer.createTransport({
host: 'smtp-relay.brevo.com',
port: 587,
secure: false,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD
}
});
async function sendMailWithEmbeddedImage(to, subject, htmlBody) {
const mailOptions = {
from: '"Absender Name" <absender@example.com>',
to: to,
subject: subject,
// HTML-Body mit CID-Referenz
html: htmlBody,
// Eingebettete Bilder als Attachments
attachments: [
{
filename: 'logo.png', // Dateiname
path: path.join(__dirname, 'assets', 'logo.png'), // Pfad zur Datei
cid: 'firmenlogo' // Content-ID (ohne <> und ohne cid:)
}
]
};
return await transporter.sendMail(mailOptions);
}
Schritt 3: HTML mit CID-Referenz
const htmlBody = `
<html>
<body>
<h1>Willkommen!</h1>
<p>Vielen Dank für Ihre Nachricht.</p>
<hr />
<!-- CID-Referenz: cid: + der cid-Wert aus attachments -->
<table>
<tr>
<td>
<img src="cid:firmenlogo" width="50" height="50" alt="Logo" />
</td>
<td>
<strong>Firma GmbH</strong><br />
Max Mustermann
</td>
</tr>
</table>
</body>
</html>
`;
await sendMailWithEmbeddedImage(
'empfaenger@example.com',
'Betreff der Mail',
htmlBody
);
Mehrere Bilder einbetten
const mailOptions = {
from: '"Absender" <absender@example.com>',
to: 'empfaenger@example.com',
subject: 'Newsletter',
html: `
<img src="cid:header" />
<p>Inhalt...</p>
<img src="cid:produkt1" />
<img src="cid:produkt2" />
<img src="cid:footer" />
`,
attachments: [
{ filename: 'header.png', path: './images/header.png', cid: 'header' },
{ filename: 'produkt1.jpg', path: './images/p1.jpg', cid: 'produkt1' },
{ filename: 'produkt2.jpg', path: './images/p2.jpg', cid: 'produkt2' },
{ filename: 'footer.png', path: './images/footer.png', cid: 'footer' }
]
};
Bilder aus Buffer/Base64 einbetten
Falls das Bild nicht als Datei vorliegt:
// Aus Buffer
attachments: [
{
filename: 'image.png',
content: Buffer.from(imageData), // Buffer mit Bilddaten
cid: 'dynamicimage'
}
]
// Aus Base64-String
attachments: [
{
filename: 'image.png',
content: 'iVBORw0KGgoAAAANSUhEUgAA...',
encoding: 'base64',
cid: 'base64image'
}
]
// Aus URL (wird beim Senden heruntergeladen)
attachments: [
{
filename: 'external.png',
path: 'https://example.com/image.png',
cid: 'externalimage'
}
]
Best Practices
Bildgröße und Format
| Empfehlung | Grund |
|---|---|
| PNG für Logos/Icons | Transparenz, scharfe Kanten |
| JPEG für Fotos | Kleinere Dateigröße |
| Max. 100 KB pro Bild | E-Mail-Größe begrenzen |
| Feste Größe angeben | width und height im HTML |
CID-Namenskonvention
// GUT: Eindeutige, beschreibende Namen
cid: 'company-logo'
cid: 'footer-icon-2024'
cid: 'product-thumbnail-123'
// SCHLECHT: Generische Namen (Kollisionsgefahr)
cid: 'image'
cid: 'logo'
cid: '1'
Fallback für Text-Clients
const mailOptions = {
// Text-Version für Clients ohne HTML-Support
text: 'Nachricht ohne Bilder...\n\n-- \nAbsender: Max Mustermann',
// HTML-Version mit eingebetteten Bildern
html: '<p>Nachricht</p><img src="cid:logo" />',
attachments: [...]
};
Vollständiges Beispiel: E-Mail-Signatur mit Logo
const nodemailer = require('nodemailer');
const path = require('path');
// Transporter konfigurieren
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT),
secure: false,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD
}
});
// Signatur-HTML generieren
function getSignatureHtml(name, position, phone) {
return `
<br /><br />
<table cellpadding="0" cellspacing="0" style="font-family: Arial, sans-serif; font-size: 14px; color: #333;">
<tr>
<td style="padding-right: 15px; border-right: 2px solid #0066cc;">
<img src="cid:companylogo" width="80" height="80" alt="Firmenlogo" style="display: block;" />
</td>
<td style="padding-left: 15px;">
<strong style="font-size: 16px; color: #0066cc;">${name}</strong><br />
<span style="color: #666;">${position}</span><br /><br />
<span>Tel: ${phone}</span><br />
<a href="https://www.firma.de" style="color: #0066cc;">www.firma.de</a>
</td>
</tr>
</table>
`;
}
// E-Mail senden
async function sendMail(to, subject, body) {
const signature = getSignatureHtml(
'Max Mustermann',
'Geschäftsführer',
'+49 123 456789'
);
const mailOptions = {
from: `"${process.env.MAIL_FROM_NAME}" <${process.env.MAIL_FROM_EMAIL}>`,
to: to,
subject: subject,
text: body + '\n\n--\nMax Mustermann\nGeschäftsführer\nTel: +49 123 456789',
html: `<div style="font-family: Arial, sans-serif;">${body}</div>${signature}`,
attachments: [
{
filename: 'logo.png',
path: path.join(__dirname, 'assets', 'logo.png'),
cid: 'companylogo'
}
]
};
const info = await transporter.sendMail(mailOptions);
console.log('E-Mail gesendet:', info.messageId);
return info;
}
// Verwendung
sendMail(
'kunde@example.com',
'Angebot #12345',
'<p>Sehr geehrte Damen und Herren,</p><p>anbei unser Angebot...</p>'
);
Kompatibilität
| E-Mail-Client | CID-Support | Anmerkung |
|---|---|---|
| Gmail (Web) | ✅ Ja | Sofort sichtbar |
| Gmail (App) | ✅ Ja | Sofort sichtbar |
| Outlook (Desktop) | ✅ Ja | Sofort sichtbar |
| Outlook (Web) | ✅ Ja | Sofort sichtbar |
| Apple Mail | ✅ Ja | Sofort sichtbar |
| Thunderbird | ✅ Ja | Sofort sichtbar |
| Yahoo Mail | ✅ Ja | Sofort sichtbar |
| ProtonMail | ⚠️ Teilweise | Kann blockiert sein |
Troubleshooting
| Problem | Ursache | Lösung |
|---|---|---|
| Bild wird als Anhang angezeigt | Falscher Content-Type | Content-Disposition: inline prüfen |
| Bild nicht sichtbar | CID stimmt nicht überein | cid: im HTML muss exakt dem cid im Attachment entsprechen |
| Bild nur als Icon | Pfad zur Datei falsch | Absoluten Pfad verwenden |
| E-Mail zu groß | Zu viele/große Bilder | Bilder komprimieren, max 100KB |
| Rotes X statt Bild | Bild konnte nicht geladen werden | Dateiformat und Pfad prüfen |
Footer in diesem Projekt anpassen
- Neue Icons hinzufügen: Einfach Bilddateien in den
assets/-Ordner legen (PNG, JPG, GIF, SVG). Sie erscheinen automatisch in der Auswahl. - Name ändern: In
.envsetzen:MAIL_FOOTER_NAME=Neuer Name - Layout ändern: In
src/mailer.jsdie FunktiongetHtmlFooter(assets)bearbeiten
Hinweis: Der Footer wird nur angezeigt, wenn mindestens ein Icon in der Weboberfläche ausgewählt wurde.
Dependencies
| Paket | Version | Beschreibung |
|---|---|---|
| express | ^4.18.0 | Webserver |
| nodemailer | ^6.9.0 | E-Mail-Versand |
| better-sqlite3 | ^11.0.0 | SQLite-Datenbank |
| dotenv | ^16.4.0 | Umgebungsvariablen |
Integration in andere Projekte
Als Standalone-Service
// Externer Aufruf der API
const response = await fetch('http://localhost:3000/api/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
to: 'empfaenger@example.com',
subject: 'Test',
body: 'Hallo Welt',
isHtml: false
})
});
const result = await response.json();
Mailer-Modul extrahieren
Die Datei src/mailer.js kann direkt in andere Node.js-Projekte kopiert werden:
require('dotenv').config();
const mailer = require('./mailer');
// E-Mail senden
await mailer.sendMail({
to: 'empfaenger@example.com',
cc: null,
subject: 'Betreff',
body: '<h1>HTML-Inhalt</h1>',
isHtml: true
});
// SMTP-Verbindung prüfen
const status = await mailer.verifyConnection();
console.log(status.success ? 'OK' : status.error);
Fehlerbehandlung
| Fehler | Ursache | Lösung |
|---|---|---|
ECONNREFUSED |
SMTP-Server nicht erreichbar | Host/Port in .env prüfen |
Invalid login |
Falsche Credentials | SMTP_USER/PASSWORD prüfen |
EADDRINUSE |
Port bereits belegt | Anderen PORT in .env setzen |
Ungültige E-Mail |
Validierung fehlgeschlagen | E-Mail-Format prüfen |
Lizenz
MIT License - siehe LICENSE Datei.
Bei Nutzung dieses Codes muss die LICENSE-Datei beibehalten werden.