Joachim Hummel b13008ba2c Remove docs/ from repository
- Add docs/ to .gitignore
- Remove tracked docs files from repository
- Local docs files are preserved

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 21:45:01 +00:00
2026-01-16 21:45:01 +00:00
2026-01-16 21:42:35 +00:00
2026-01-16 21:42:35 +00:00
2026-01-16 21:42:35 +00:00

Mail-Service mit Brevo SMTP

Ein Node.js-basierter Mail-Service mit Weboberfläche zum Versenden von E-Mails über Brevo (ehemals Sendinblue) SMTP. Inklusive persistenter Versand-Historie und eingebettetem Bild-Footer.

Features

  • Weboberfläche zum Versenden von E-Mails (Empfänger, CC, Betreff, Text/HTML)
  • Persistente Historie in SQLite-Datenbank
  • Eingebetteter Bild-Footer via CID-Attachment (wird ohne "Bilder anzeigen" dargestellt)
  • REST-API für programmatischen Zugriff
  • Brevo SMTP mit DKIM/SPF/DMARC-Unterstützung

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/
│   └── homeicon.png        # Footer-Bild (40x40 PNG)
└── data/
    └── emails.db           # SQLite-Datenbank (automatisch erstellt)

Installation

# 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)

# Server
PORT=3000

# Email Configuration
MAIL_PROVIDER=smtp
MAIL_FROM_EMAIL=noreply@example.com
MAIL_FROM_NAME=Secure Portal
MAIL_FOOTER_NAME=Dein Name

# SMTP (Brevo)
SMTP_HOST=smtp-relay.brevo.com
SMTP_PORT=587
SMTP_USER=deine-email@example.com
SMTP_PASSWORD=dein-brevo-smtp-key
SMTP_SECURE=false

Brevo SMTP-Key erstellen

  1. Bei Brevo einloggen
  2. Gehe zu: Einstellungen → SMTP & API → SMTP
  3. "SMTP-Schlüssel generieren" klicken
  4. Schlüssel in .env als SMTP_PASSWORD eintragen

Starten

# 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
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

POST /api/send

Request Body:

{
  "to": "empfaenger@example.com",
  "cc": "optional@example.com",
  "subject": "Betreff",
  "body": "Nachrichteninhalt",
  "isHtml": false
}

Response (Erfolg):

{
  "success": true,
  "message": "E-Mail erfolgreich gesendet",
  "messageId": "<abc123@smtp-relay.sendinblue.com>",
  "id": 1
}

Response (Fehler):

{
  "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

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:

<!-- 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:

<!-- 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

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

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

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:

// 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

// 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

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: [...]
};
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
  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:
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

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

// 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:

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 Datei.

Bei Nutzung dieses Codes muss die LICENSE-Datei beibehalten werden.

Description
Ein Node.js-basierter Mail-Service mit Weboberfläche zum Versenden von E-Mails über Brevo (ehemals Sendinblue) SMTP. Inklusive persistenter Versand-Historie und eingebettetem Bild-Footer.
Readme MIT 182 KiB
V 1.1.0 Latest
2026-01-16 22:40:42 +00:00
Languages
JavaScript 66.5%
CSS 22.5%
HTML 11%