jhummelandClaude Opus 4.5 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.

S
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
0 Stars 1 Watchers 0 Forks
V 1.1.0
Latest
2026-01-16 22:40:42 +00:00
Languages
JavaScript 66.5%
CSS 22.5%
HTML 11%