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 = 2 aktivieren und SMTP-Konversation lesen
  • Den AUTH-Base64-String dekodieren: echo "..." | base64 -d
  • Credential-Datei mit xxd auf 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.