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.
Das Wichtigste
- Eine Mail mit Text, HTML und Anhang braucht zwei verschachtelte MIME-Ebenen:
multipart/mixedaussen,multipart/alternativeinnen. - 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/relatedund eineContent-ID, auf die das HTML mitcid: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.
Weiterlesen
- vlibDate: Datumsberechnungen in PHP
Wofür die Klasse gedacht war, welche Aufgaben heute DateTime und DateInterval übernehmen und wie eine Ablesung aussieht.
- PHP-Mailformulare gegen Header-Injection absichern
Eingaben validieren, Header-Zeilen filtern, Rate-Limits setzen und Spam abfangen, ohne echte Anfragen zu blockieren.
- Template-Engines in PHP: Übersicht
Was eine Engine gegenüber purem PHP bringt, welche Ansätze es gibt und wie du für ein Projekt entscheidest.
- vlibTemplate: Aufbau, Syntax und Beispiele
Platzhalter, Schleifen, Bedingungen und Includes der klassischen Engine - inklusive Hinweisen zum Betrieb auf aktuellen PHP-Versionen.