From 524ae24d74952dc402d06ffcd08ffb3c1fa72144 Mon Sep 17 00:00:00 2001 From: Joachim Hummel Date: Fri, 16 Jan 2026 21:35:00 +0000 Subject: [PATCH] Expand CID-Embedding documentation with detailed tutorial - Problem explanation: why external images are blocked - Technical background: MIME structure with examples - Step-by-step Nodemailer implementation guide - Multiple images, Buffer/Base64 examples - Best practices for image size, format, CID naming - Complete signature example with embedded logo - Email client compatibility table - Troubleshooting guide Co-Authored-By: Claude Opus 4.5 --- README.md | 373 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 357 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 25aa5bb..2acae1b 100644 --- a/README.md +++ b/README.md @@ -143,38 +143,379 @@ Server läuft auf: http://localhost:3000 | error_message | TEXT | Fehlermeldung (bei Fehler) | | created_at | TEXT | ISO-Timestamp | -## Bild-Footer mit CID-Embedding +## Bild-Footer mit CID-Embedding (Detaillierte Anleitung) -Der Footer mit eingebettetem Bild wird automatisch an jede E-Mail angehängt. +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. -### Warum CID statt externe URL? +### Das Problem mit externen Bildern -| Methode | Gmail-Verhalten | -|---------|-----------------| -| `` | Blockiert, "Bilder anzeigen" nötig | -| `` | Sofort sichtbar | +E-Mail-Clients blockieren externe Bilder aus Datenschutzgründen: -### Implementierung (mailer.js) +```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 -// 1. Bild als Anhang mit Content-ID +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: 'homeicon.png', - path: '/pfad/zu/assets/homeicon.png', - cid: 'homeicon' // Referenz-ID + filename: 'image.png', + content: Buffer.from(imageData), // Buffer mit Bilddaten + cid: 'dynamicimage' } ] -// 2. Im HTML referenzieren - +// 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' + } +] ``` -### Footer anpassen +### 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. **Bild ändern:** `assets/homeicon.png` ersetzen (empfohlen: 40x40 PNG) 2. **Name ändern:** In `.env` setzen: `MAIL_FOOTER_NAME=Neuer Name` -3. **Layout ändern:** In `src/mailer.js` die Funktion `getHtmlFooter()` bearbeiten +3. **Layout ändern:** In `src/mailer.js` die Funktion `getHtmlFooter()` bearbeiten: + +```javascript +function getHtmlFooter() { + return ` +

+
+ + + + + +
+ Home + + Absender
+ ${FOOTER_NAME} +
+ `; +} +``` ## Dependencies