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) |
|
| error_message | TEXT | Fehlermeldung (bei Fehler) |
|
||||||
| created_at | TEXT | ISO-Timestamp |
|
| 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 |
|
E-Mail-Clients blockieren externe Bilder aus Datenschutzgründen:
|
||||||
|---------|-----------------|
|
|
||||||
| `<img src="https://...">` | Blockiert, "Bilder anzeigen" nötig |
|
|
||||||
| `<img src="cid:homeicon">` | Sofort sichtbar |
|
|
||||||
|
|
||||||
### 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
|
```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: [
|
attachments: [
|
||||||
{
|
{
|
||||||
filename: 'homeicon.png',
|
filename: 'image.png',
|
||||||
path: '/pfad/zu/assets/homeicon.png',
|
content: Buffer.from(imageData), // Buffer mit Bilddaten
|
||||||
cid: 'homeicon' // Referenz-ID
|
cid: 'dynamicimage'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
// 2. Im HTML referenzieren
|
// Aus Base64-String
|
||||||
<img src="cid:homeicon" width="40" height="40" />
|
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)
|
1. **Bild ändern:** `assets/homeicon.png` ersetzen (empfohlen: 40x40 PNG)
|
||||||
2. **Name ändern:** In `.env` setzen: `MAIL_FOOTER_NAME=Neuer Name`
|
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
|
## Dependencies
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user