- Remove Brevo-specific branding - Add SMTP provider comparison table - Use environment variables in code examples Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
597 lines
16 KiB
Markdown
597 lines
16 KiB
Markdown
# Mail-Service
|
|
|
|
Ein Node.js-basierter Mail-Service mit Weboberfläche zum Versenden von E-Mails über SMTP. Funktioniert mit jedem SMTP-Anbieter (Brevo, Mailgun, Resend, SendGrid, eigener Mailserver, etc.). 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
|
|
- **Flexibler SMTP-Versand** - Kompatibel mit allen SMTP-Anbietern
|
|
|
|
## Screenshot
|
|
|
|

|
|
|
|
## 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_FROM_EMAIL=noreply@example.com
|
|
MAIL_FROM_NAME=Mein Service
|
|
MAIL_FOOTER_NAME=Dein Name
|
|
|
|
# SMTP
|
|
SMTP_HOST=smtp.example.com
|
|
SMTP_PORT=587
|
|
SMTP_USER=dein-username
|
|
SMTP_PASSWORD=dein-passwort
|
|
SMTP_SECURE=false
|
|
```
|
|
|
|
### SMTP-Anbieter Beispiele
|
|
|
|
| Anbieter | SMTP_HOST | SMTP_PORT | SMTP_SECURE |
|
|
|----------|-----------|-----------|-------------|
|
|
| Brevo | smtp-relay.brevo.com | 587 | false |
|
|
| Mailgun | smtp.mailgun.org | 587 | false |
|
|
| Resend | smtp.resend.com | 465 | true |
|
|
| SendGrid | smtp.sendgrid.net | 587 | false |
|
|
| Gmail | smtp.gmail.com | 587 | false |
|
|
| Eigener Server | mail.domain.de | 465 | true |
|
|
|
|
## 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": "<abc123@smtp-relay.sendinblue.com>",
|
|
"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
|
|
<!-- 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
|
|
const nodemailer = require('nodemailer');
|
|
const path = require('path');
|
|
|
|
const transporter = nodemailer.createTransport({
|
|
host: process.env.SMTP_HOST,
|
|
port: parseInt(process.env.SMTP_PORT),
|
|
secure: process.env.SMTP_SECURE === 'true',
|
|
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: '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: '<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. **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: '<h1>HTML-Inhalt</h1>',
|
|
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.**
|