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:
373
README.md
373
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 |
|
||||
|---------|-----------------|
|
||||
| `<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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user