# 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 ```bash # 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) ```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 1. Bei [Brevo](https://www.brevo.com) einloggen 2. Gehe zu: Einstellungen → SMTP & API → SMTP 3. "SMTP-Schlüssel generieren" klicken 4. Schlüssel in `.env` als `SMTP_PASSWORD` eintragen ## Starten ```bash # 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:** ```json { "success": true, "assets": ["blue.png", "cal.png", "cog.png", "homeicon.png", "psy.png"] } ``` ### POST /api/send **Request Body:** ```json { "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):** ```json { "success": true, "message": "E-Mail erfolgreich gesendet", "messageId": "", "id": 1 } ``` **Response (Fehler):** ```json { "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: ```html ``` **Warum blockieren E-Mail-Clients externe Bilder?** 1. **Tracking-Schutz**: Der Server sieht, wann/ob die E-Mail geöffnet wurde 2. **Datenschutz**: IP-Adresse und Standort werden übermittelt 3. **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: ```html ``` 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

Hallo!

------boundary Content-Type: image/png; name="logo.png" Content-Disposition: inline; filename="logo.png" Content-Id: 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 Anhang - `Content-Id: `: Die CID-Referenz (ohne `cid:` 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 ```javascript 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" ', 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 ```javascript const htmlBody = `

Willkommen!

Vielen Dank für Ihre Nachricht.


Logo Firma GmbH
Max Mustermann
`; await sendMailWithEmbeddedImage( 'empfaenger@example.com', 'Betreff der Mail', htmlBody ); ``` ### Mehrere Bilder einbetten ```javascript const mailOptions = { from: '"Absender" ', to: 'empfaenger@example.com', subject: 'Newsletter', html: `

Inhalt...

`, 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: ```javascript // 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 ```javascript // 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 ```javascript 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: '

Nachricht

', attachments: [...] }; ``` ### Vollständiges Beispiel: E-Mail-Signatur mit Logo ```javascript 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 `

Firmenlogo ${name}
${position}

Tel: ${phone}
www.firma.de
`; } // 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: `
${body}
${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', '

Sehr geehrte Damen und Herren,

anbei unser Angebot...

' ); ``` ### 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 1. **Neue Icons hinzufügen:** Einfach Bilddateien in den `assets/`-Ordner legen (PNG, JPG, GIF, SVG). Sie erscheinen automatisch in der Auswahl. 2. **Name ändern:** In `.env` setzen: `MAIL_FOOTER_NAME=Neuer Name` 3. **Layout ändern:** In `src/mailer.js` die Funktion `getHtmlFooter(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 ```javascript // 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: ```javascript require('dotenv').config(); const mailer = require('./mailer'); // E-Mail senden await mailer.sendMail({ to: 'empfaenger@example.com', cc: null, subject: 'Betreff', body: '

HTML-Inhalt

', 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](LICENSE) Datei. **Bei Nutzung dieses Codes muss die LICENSE-Datei beibehalten werden.**