TL;DR: Wenn curl --user "user:Passwort!123" funktioniert, PHPMailer aber 535 Authentication failed wirft, liegt der Fehler fast sicher in der Bash-History-Expansion. Das ! in Double-Quoted Strings wird zu \! escaped — das Passwort, das in die Credential-Datei landet, ist ein anderes als das, das du tipppst. Fix: .env.smtp manuell mit einem Editor befüllen, nicht mit echo "...".
Das Problem
Die PHP-Kontaktform auf srv-r2d2 soll Mails über mail-chewbacca.deathstar.lan (Port 587, STARTTLS) versenden. Die App liest die SMTP-Credentials aus /var/www/html/rebellion-contact/.env.smtp.
PHPMailer wirft beim Verbindungsversuch:
SMTP ERROR: Password command failed: 535 5.7.8 Authentication credentials invalid
SMTP connect() failed.
Curl mit denselben Credentials? Kein Problem:
curl --url "smtp://mail-chewbacca.deathstar.lan:587" \
--ssl-reqd \
--user "info@rebellion.local:R2D2istSuper!toll" \
--mail-from "info@rebellion.local" \
--mail-rcpt "test@rebellion.local" \
--upload-file /tmp/testmail.txt
Ergebnis: 250 OK. Die Mail kommt an.
Also liegt es an PHPMailer, oder?
Erster Verdacht & Warum er falsch war
Der erste Gedanke: PHPMailer hat ein TLS-Problem, oder der PHP-Build unterstützt kein STARTTLS auf Port 587. Beides plausibel, beides falsch.
Curl und PHP verwenden auf diesem Server dieselben System-Libraries für TLS. Wenn curl verbindet, verbindet auch PHP — vorausgesetzt, die Credentials stimmen überein.
Und genau da liegt der Haken: Sie stimmen nicht überein.
Diagnose mit SMTPDebug=2
PHPMailer hat ein eingebautes Debug-Level, das die komplette SMTP-Konversation auf stdout schreibt. Das ist der erste Griff bei SMTP-Problemen:
$mail = new PHPMailer(true);
$mail->SMTPDebug = 2; // Komplette SMTP-Konversation
$mail->isSMTP();
$mail->Host = 'mail-chewbacca.deathstar.lan';
$mail->SMTPAuth = true;
$mail->Username = 'info@rebellion.local';
$mail->Password = file_get_contents('/var/www/html/rebellion-contact/.env.smtp');
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
Die Debug-Ausgabe zeigt die SMTP-AUTH-Sequenz im Klartext (Base64-kodiert):
SERVER -> CLIENT: 334 VXNlcm5hbWU6
CLIENT -> SERVER: aW5mb0ByZWJlbGxpb24ubG9jYWw=
SERVER -> CLIENT: 334 UGFzc3dvcmQ6
CLIENT -> SERVER: UjJEMmlzdFN1cGVyXCF0b2xs
SERVER -> CLIENT: 535 5.7.8 Authentication credentials invalid
Der BASE64-String UjJEMmlzdFN1cGVyXCF0b2xs ist verdächtig. Dekodieren:
echo "UjJEMmlzdFN1cGVyXCF0b2xs" | base64 -d
# Ausgabe: R2D2istSuper\!toll
Da ist er: \! statt !. PHP sendet R2D2istSuper\!toll, der Mail-Server erwartet R2D2istSuper!toll.
Der Hex-Beweis
Um sicherzustellen, dass kein Encoding-Trick die Ausgabe verschleiert, Byte-Vergleich mit xxd:
# Was PHP sendet (aus der .env.smtp):
cat /var/www/html/rebellion-contact/.env.smtp | xxd | head -2
# 00000000: 5232 4432 6973 7453 7570 6572 5c21 746f R2D2istSuper\!to
# 00000010: 6c6c 0a ll.
# Was das Passwort sein sollte:
printf 'R2D2istSuper!toll' | xxd | head -2
# 00000000: 5232 4432 6973 7453 7570 6572 2174 6f6c R2D2istSuper!tol
# 00000010: 6c l
Der Unterschied:
- Falsch:
...5c 21...— das ist\(0x5C) gefolgt von!(0x21) - Richtig:
...21...— nur!(0x21)
Die .env.smtp enthält einen Backslash, der da nicht hingehört.
Root Cause: Bash !-Expansion
Wie kommt der Backslash in die Datei? Mit hoher Wahrscheinlichkeit wurde die Datei so befüllt:
echo "R2D2istSuper!toll" > /var/www/html/rebellion-contact/.env.smtp
Und genau hier liegt das Problem. In interaktiven Bash-Sessions ist die History-Expansion aktiv. Das ! in Double-Quoted Strings ist ein Magic-Character, der auf History-Einträge verweist. Bash escaped ihn automatisch, wenn kein passender History-Eintrag gefunden wird:
# In Bash Double-Quotes wird ! zu \! expanded:
echo "R2D2istSuper!toll" # gibt aus: R2D2istSuper\!toll
# In Single-Quotes passiert nichts:
echo 'R2D2istSuper!toll' # gibt aus: R2D2istSuper!toll
# Oder History-Expansion deaktivieren:
set +H
echo "R2D2istSuper!toll" # gibt aus: R2D2istSuper!toll
Das Verhalten ist bash-spezifisch und nur in interaktiven Shells aktiv (nicht in Scripts). Wer also echo "Passwort!..." > datei in einem Terminal tippt, bekommt heimlich Passwort\!... in die Datei geschrieben.
Curl liest die Credentials direkt vom Kommandozeilen-Argument — die Shell hat sie bereits zur Laufzeit expandiert. Wenn du --user "user:R2D2istSuper\!toll" übergibst (was Bash nach der Expansion tut), sendet curl genau das: den Backslash-escaped String. Aber: Der Mail-Server akzeptiert hier \! als valides Passwort? Nein — in diesem Fall lag curl mit dem \! tatsächlich falsch, aber der Mail-Server war konfiguriert, \! als Passwort zu akzeptieren (weil das Passwort ursprünglich mit demselben Bash-Bug gesetzt wurde). PHPMailer liest dagegen die Datei byte-by-byte ohne Shell-Expansion — und sendet \!toll… oder je nach Situation eben nicht.
Der einfachere und häufigere Fall: Die Datei enthält \!, PHP liest \! und sendet \!, der Mail-Server erwartet !. Authentication failed.
Die Lösung
Die .env.smtp muss das korrekte Passwort ohne Backslash enthalten. Dafür Single-Quotes verwenden:
# RICHTIG: Single-Quotes verhindern History-Expansion
echo 'R2D2istSuper!toll' > /var/www/html/rebellion-contact/.env.smtp
# Verifizieren:
xxd /var/www/html/rebellion-contact/.env.smtp
# 00000000: 5232 4432 6973 7453 7570 6572 2174 6f6c R2D2istSuper!tol
# 00000010: 6c0a l.
Das 0a am Ende ist der Newline von echo. PHP’s file_get_contents() gibt den kompletten Datei-Inhalt zurück — inklusive Newline. Daher im PHP-Code trimmen:
$mail->Password = trim(file_get_contents('/var/www/html/rebellion-contact/.env.smtp'));
Nach dem Fix: PHPMailer authentifiziert sich erfolgreich.
Prävention
Option 1: Passwörter ohne !
Das Problem existiert nicht, wenn das Passwort kein ! enthält. In vielen Passwort-Policies ist das Ausschlusskriterium gerechtfertigt — nicht weil ! unsicher ist, sondern weil es in Shell-Kontexten Probleme macht.
Option 2: Immer Single-Quotes für Passwörter in der Shell
# NIEMALS:
echo "geheim!123" > secret.txt
# IMMER:
echo 'geheim!123' > secret.txt
# Oder mit printf (robuster, kein trailing newline):
printf '%s' 'geheim!123' > secret.txt
Option 3: Editor statt echo
Credential-Dateien direkt mit nano, vim oder micro befüllen. Editoren interpretieren keine Shell-Expansion.
Option 4: Nach dem Schreiben verifizieren
# Immer prüfen was wirklich in der Datei steht:
xxd /pfad/zur/.env.smtp
# Oder einfacher mit cat (zeigt keine nicht-druckbaren Zeichen):
cat /pfad/zur/.env.smtp
Checkliste bei SMTP-Auth-Problemen
-
SMTPDebug = 2aktivieren und SMTP-Konversation lesen - Den AUTH-Base64-String dekodieren:
echo "..." | base64 -d - Credential-Datei mit
xxdauf versteckte Bytes prüfen (5c= Backslash) - Credential-Datei mit Single-Quotes neu schreiben
-
trim()im PHP-Code sicherstellen - curl-Test und PHPMailer-Test mit identischen Credentials vergleichen
Lessons Learned
“curl geht” ist kein Beweis, dass die Credentials korrekt sind. curl und PHP lesen Passwörter aus unterschiedlichen Quellen: curl bekommt sie nach Shell-Expansion als Argument, PHP liest sie roh aus einer Datei. Beide können mit unterschiedlichen Passwörtern arbeiten — und beide können dabei “Erfolg” haben, wenn der Server entsprechend konfiguriert ist.
Bash-History-Expansion ist ein stiller Saboteur. Sie ist nur in interaktiven Shells aktiv, produziert keine Fehlermeldung, und das Ergebnis sieht im Terminal oft normal aus. Ein xxd auf die Zieldatei kostet 2 Sekunden und spart Stunden Debugging.
SMTPDebug=2 ist unverzichtbar. Die rohe SMTP-Konversation zeigt sofort, was PHP tatsächlich sendet — kein Raten, kein Vermuten.