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 <noreply@anthropic.com>
This commit is contained in:
2026-01-16 21:35:00 +00:00
parent 8c2f4b5f92
commit 524ae24d74

373
README.md
View File

@@ -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 |
|---------|-----------------|
| `<img src="https://...">` | Blockiert, "Bilder anzeigen" nötig |
| `<img src="cid:homeicon">` | Sofort sichtbar |
E-Mail-Clients blockieren externe Bilder aus Datenschutzgründen:
### Implementierung (mailer.js)
```html
<!-- SCHLECHT: Externes Bild - wird blockiert -->
<img src="https://mein-server.de/logo.png" />
```
**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
<!-- 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 Anhang
- `Content-Id: <logo123>`: 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" <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
```javascript
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
```javascript
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:
```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
<img src="cid:homeicon" width="40" height="40" />
// 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: '<p>Nachricht</p><img src="cid:logo" />',
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 `
<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
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 `
<br/><br/>
<hr style="border: none; border-top: 1px solid #ddd; margin: 20px 0;"/>
<table cellpadding="0" cellspacing="0" style="font-family: Arial, sans-serif;">
<tr>
<td style="vertical-align: middle; padding-right: 10px;">
<img src="cid:homeicon" alt="Home" width="40" height="40" />
</td>
<td style="vertical-align: middle;">
<strong>Absender</strong><br/>
${FOOTER_NAME}
</td>
</tr>
</table>
`;
}
```
## Dependencies