Add SMTP debug logging and from/username mismatch warning to mail test

"Connection successful" only means the SMTP server accepted the
message, not that it was delivered. The test endpoint now captures the
full SMTP protocol exchange and shows it in the admin UI, plus flags
the common case where the sender address differs from the SMTP login
- many providers silently discard mail in that situation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Maaxxs 2026-07-07 01:35:54 +02:00
parent b5de375d7f
commit 93e245d3b9
2 changed files with 42 additions and 5 deletions

View file

@ -58,6 +58,8 @@ export default function EmailSettings() {
const [testBusy, setTestBusy] = useState(false); const [testBusy, setTestBusy] = useState(false);
const [testError, setTestError] = useState(''); const [testError, setTestError] = useState('');
const [testSuccess, setTestSuccess] = useState(''); const [testSuccess, setTestSuccess] = useState('');
const [testWarning, setTestWarning] = useState('');
const [testLog, setTestLog] = useState(null);
useEffect(() => { useEffect(() => {
api api
@ -95,9 +97,13 @@ export default function EmailSettings() {
setTestBusy(true); setTestBusy(true);
setTestError(''); setTestError('');
setTestSuccess(''); setTestSuccess('');
setTestWarning('');
setTestLog(null);
try { try {
const res = await api.post('/settings/mail/test', form); const res = await api.post('/settings/mail/test', form);
setTestSuccess(`Testmail gesendet an ${res.sent_to}.`); setTestSuccess(`Testmail gesendet an ${res.sent_to}. Der Server hat die Mail angenommen - prüfe auch den Spam-Ordner, das ist noch keine Garantie für die Zustellung.`);
setTestWarning(res.warning || '');
setTestLog(res.smtp_log || null);
} catch (err) { } catch (err) {
setTestError(err.message); setTestError(err.message);
} finally { } finally {
@ -201,8 +207,13 @@ export default function EmailSettings() {
</div> </div>
<div className="mt-4 border-t border-neutral-200 pt-4 dark:border-neutral-800"> <div className="mt-4 border-t border-neutral-200 pt-4 dark:border-neutral-800">
{testError && <p className="mb-2 text-sm text-red-600">{testError}</p>} {testError && <p className="mb-2 whitespace-pre-wrap text-sm text-red-600">{testError}</p>}
{testSuccess && <p className="mb-2 text-sm text-green-600 dark:text-green-400">{testSuccess}</p>} {testSuccess && <p className="mb-2 text-sm text-green-600 dark:text-green-400">{testSuccess}</p>}
{testWarning && (
<p className="mb-2 rounded-md bg-amber-50 p-2 text-sm text-amber-800 dark:bg-amber-900/20 dark:text-amber-300">
{testWarning}
</p>
)}
<button <button
type="button" type="button"
onClick={handleTest} onClick={handleTest}
@ -211,6 +222,14 @@ export default function EmailSettings() {
> >
{testBusy ? 'Sende Testmail…' : 'Verbindung testen'} {testBusy ? 'Sende Testmail…' : 'Verbindung testen'}
</button> </button>
{testLog && testLog.length > 0 && (
<details className="mt-3">
<summary className="cursor-pointer text-xs text-neutral-500 dark:text-neutral-400">SMTP-Protokoll anzeigen</summary>
<pre className="mt-2 max-h-64 overflow-auto rounded-md bg-neutral-900 p-3 text-xs text-neutral-200">
{testLog.join('\n')}
</pre>
</details>
)}
</div> </div>
</section> </section>

View file

@ -111,24 +111,42 @@ class MailSettingsController
Http::error('Empfaenger-E-Mail (Benachrichtigungs- oder Absenderadresse) ist ungueltig', 422); Http::error('Empfaenger-E-Mail (Benachrichtigungs- oder Absenderadresse) ist ungueltig', 422);
} }
$username = trim($body['mail_username'] ?? '');
$debugLog = [];
try { try {
$mail = Mailer::build([ $mail = Mailer::build([
'host' => $host, 'host' => $host,
'port' => $body['mail_port'] ?? 587, 'port' => $body['mail_port'] ?? 587,
'username' => trim($body['mail_username'] ?? ''), 'username' => $username,
'password' => $password, 'password' => $password,
'encryption' => trim($body['mail_encryption'] ?? '') ?: 'tls', 'encryption' => trim($body['mail_encryption'] ?? '') ?: 'tls',
'from_email' => $fromEmail, 'from_email' => $fromEmail,
'from_name' => trim($body['mail_from_name'] ?? '') ?: 'HifiPlanet', 'from_name' => trim($body['mail_from_name'] ?? '') ?: 'HifiPlanet',
]); ]);
// Volles SMTP-Protokoll mitschneiden, damit bei "Verbindung ok, aber nichts kommt an"
// sichtbar wird, was der Server nach dem DATA-Befehl tatsaechlich geantwortet hat
// (z.B. eine 250-Bestaetigung, obwohl die Mail spaeter serverseitig verworfen wird).
$mail->SMTPDebug = 2;
$mail->Debugoutput = function ($str) use (&$debugLog) {
$debugLog[] = trim(preg_replace('/\s+/', ' ', $str));
};
$mail->addAddress($target); $mail->addAddress($target);
$mail->Subject = 'Testmail von HifiPlanet'; $mail->Subject = 'Testmail von HifiPlanet';
$mail->Body = "Diese Testmail bestaetigt, dass deine SMTP-Einstellungen funktionieren."; $mail->Body = "Diese Testmail bestaetigt, dass deine SMTP-Einstellungen funktionieren.";
$mail->send(); $mail->send();
} catch (\Throwable $e) { } catch (\Throwable $e) {
Http::error('Test fehlgeschlagen: ' . $e->getMessage(), 422); Http::error('Test fehlgeschlagen: ' . $e->getMessage() . "\n\nSMTP-Protokoll:\n" . implode("\n", $debugLog), 422);
} }
Http::send(['ok' => true, 'sent_to' => $target]); $warning = null;
if ($username !== '' && strcasecmp($username, $fromEmail) !== 0) {
$warning = 'Hinweis: Absender-E-Mail ("' . $fromEmail . '") und Benutzername ("' . $username . '") sind '
. 'unterschiedlich. Viele Mail-Provider akzeptieren die Mail dann zwar (siehe SMTP-Protokoll), '
. 'verwerfen sie aber im Anschluss ohne Fehlermeldung. Am sichersten ist es, die Absender-E-Mail '
. 'identisch zum SMTP-Benutzernamen zu setzen.';
}
Http::send(['ok' => true, 'sent_to' => $target, 'warning' => $warning, 'smtp_log' => $debugLog]);
} }
} }