Zum Inhalt springen

vlibMimeMail: HTML-Mails mit Anhängen

Wie MIME-Mails mit Text- und HTML-Teil sowie Anhängen aufgebaut sind, was vlibMimeMail dafür gekapselt hat und womit man das heute löst.

Zuletzt geprüft: Gedacht für: alte Mailversand-Routinen

Das Wichtigste

  • Eine Mail mit Text, HTML und Anhang braucht zwei verschachtelte MIME-Ebenen: multipart/mixed aussen, multipart/alternative innen.
  • Der Reihenfolge nach steht der Textteil zuerst und HTML danach – Clients zeigen den letzten Teil an, den sie darstellen können.
  • Eingebettete Bilder brauchen multipart/related und eine Content-ID, auf die das HTML mit cid: verweist.
  • Selbst gebaute MIME-Strings sind eine Injection-Fläche. Heute übernimmt das eine Bibliothek – PHPMailer oder Symfony Mailer.

MIME ist der Grund, warum eine Mail mehr sein kann als eine Textzeile. Der Aufbau ist überschaubar, sobald man die Verschachtelung einmal gesehen hat – und genau die hat vlibMimeMail gekapselt.

Der Aufbau einer vollständigen Mail

multipart/mixed                     <- äussere Hülle, trägt Anhänge
├── multipart/related               <- HTML plus eingebettete Bilder
│   ├── multipart/alternative       <- zwei Darstellungen desselben Inhalts
│   │   ├── text/plain              <- zuerst: der Textteil
│   │   └── text/html               <- danach: HTML
│   └── image/png (Content-ID)      <- Logo, per cid: referenziert
└── application/pdf                 <- Anhang

Zwei Regeln, die man kennen muss:

Die Reihenfolge in multipart/alternative ist bedeutungstragend. Der Client zeigt den letzten Teil an, den er darstellen kann. Text gehört deshalb nach oben, HTML nach unten. Wer es umdreht, wundert sich, warum überall die Textversion erscheint.

Jede Ebene braucht ihre eigene Grenzmarkierung. Die boundary-Zeichenkette darf im Inhalt nicht vorkommen und muss pro Ebene unterschiedlich sein.

So sieht das im Rohtext aus

MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="b1_a3f9c2"

--b1_a3f9c2
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: quoted-printable

Guten Tag,
Ihre Bestellung ist unterwegs.

--b1_a3f9c2
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: quoted-printable

<p>Guten Tag,<br>Ihre Bestellung ist unterwegs.</p>

--b1_a3f9c2--

Die abschliessende Zeile trägt zwei zusätzliche Bindestriche. Fehlen sie, gilt die Nachricht als unvollständig und manche Clients zeigen den Rest als Anhang an.

quoted-printable ist für deutschen Text die richtige Kodierung: Umlaute werden als Hexwert geschrieben (ü wird =FC bei Latin-1, =C3=BC bei UTF-8), der Rest bleibt lesbar. Base64 gehört zu Anhängen – bei Text vergrössert es die Nachricht um ein Drittel, ohne etwas zu gewinnen.

Was vlibMimeMail daraus machte

Die Klasse nahm Betreff, Empfänger, Text- und HTML-Teil sowie Anhänge entgegen und baute die Verschachtelung selbst auf. Damit entfiel die fehleranfällige Handarbeit an Grenzmarkierungen und Kodierung.

Der Nachteil aus heutiger Sicht: Header wurden als Zeichenketten zusammengesetzt. Wandert eine Nutzereingabe ungeprüft in Betreff oder Empfänger, kann sie über einen Zeilenumbruch weitere Header einschleusen – der klassische Weg, ein Kontaktformular in einen Versanddienst zu verwandeln.

Heutiger Ersatz

<?php
use Symfony\Component\Mime\Email;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport;

$email = (new Email())
    ->from('versand@example.com')
    ->to('kunde@example.org')
    ->subject('Ihre Bestellung')
    ->text("Guten Tag,\nIhre Bestellung ist unterwegs.")
    ->html('<p>Guten Tag,<br>Ihre Bestellung ist unterwegs.</p>')
    ->embedFromPath('assets/logo.png', 'logo')
    ->attachFromPath('rechnungen/2026-0042.pdf');

$mailer = new Mailer(Transport::fromDsn('smtp://user:pass@mail.example.com:587'));
$mailer->send($email);

Im HTML verweist das eingebettete Bild über seine Kennung:

<img src="cid:logo" alt="Firmenlogo" width="160" height="40">

Die Bibliothek erzeugt die Verschachtelung, wählt die Kodierung und weist Zeilenumbrüche in Adressfeldern ab. Genau das ist der Grund, sie zu nehmen.

Mit PHPMailer sieht es ähnlich aus:

$mail = new PHPMailer\PHPMailer\PHPMailer(true);
$mail->CharSet = 'UTF-8';
$mail->isSMTP();
$mail->Host = 'mail.example.com';
$mail->Port = 587;
$mail->SMTPAuth = true;

$mail->setFrom('versand@example.com', 'Beispiel GmbH');
$mail->addAddress('kunde@example.org');
$mail->Subject = 'Ihre Bestellung';
$mail->isHTML(true);
$mail->Body    = '<p>Guten Tag,<br>Ihre Bestellung ist unterwegs.</p>';
$mail->AltBody = "Guten Tag,\nIhre Bestellung ist unterwegs.";
$mail->addEmbeddedImage('assets/logo.png', 'logo');
$mail->addAttachment('rechnungen/2026-0042.pdf');
$mail->send();

AltBody ist der Textteil. Wer ihn weglässt, verschickt eine reine HTML-Mail – schlechter für die Zustellbarkeit und unlesbar in Textclients.

Zustellbarkeit nicht vergessen

Die technisch korrekte MIME-Struktur nützt wenig, wenn die Mail im Spam landet. Drei Punkte gehören dazu:

  • Absenderdomain ist deine eigene, und SPF, DKIM sowie DMARC sind gesetzt.
  • Textteil vorhanden, mit demselben Inhalt wie das HTML.
  • Versand über einen authentifizierten SMTP-Zugang, nicht über die lokale mail()-Funktion – so bekommst du Fehlermeldungen zurück und kannst Zustellprobleme überhaupt sehen.

Häufige Fragen

Warum brauche ich überhaupt einen Textteil?

Weil manche Clients und Vorschauansichten kein HTML darstellen und weil Spamfilter eine reine HTML-Mail schlechter bewerten. Ein Textteil, der denselben Inhalt trägt, kostet wenig und verbessert die Zustellbarkeit.

Wie binde ich Bilder in eine HTML-Mail ein?

Entweder als externe URL – dann laden viele Clients sie erst nach Bestätigung – oder eingebettet über multipart/related mit einer Content-ID. Im HTML verweist dann src auf cid und die vergebene Kennung. Data-URIs funktionieren in vielen Mailclients nicht.

Was ist quoted-printable und wann brauche ich es?

Eine Kodierung, die Zeichen ausserhalb von ASCII als Hexwert schreibt und dabei lesbar bleibt. Für deutschen Text mit Umlauten ist sie die richtige Wahl. Base64 ist für Binärdaten wie Anhänge gedacht und bläht Text unnötig auf.

Kann ich vlibMimeMail weiterverwenden?

Wenn es läuft und der Versand funktioniert, gibt es keinen akuten Anlass. Sobald aber Nutzereingaben in Betreff oder Empfänger wandern, gehört eine Bibliothek davor, die Header prüft – selbst gebaute Header sind der klassische Weg zur Header-Injection.