The mail page under System tries SMTP, PHP mail() and both sendmail modes one by one or all at once, and saves the chosen way to .env without ever showing the password. IONOS answers everything under /.well-known/ itself with 403, so the address and HTTPS checks never reached the installation; the probe now lives at /kantsites-origin (ADR 122). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
180 lines
9.5 KiB
PHP
180 lines
9.5 KiB
PHP
<?php
|
|
|
|
namespace App\Tests;
|
|
|
|
use App\Ops\MailCheck;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Symfony\Component\Mailer\Envelope;
|
|
use Symfony\Component\Mailer\Exception\TransportException;
|
|
use Symfony\Component\Mailer\SentMessage;
|
|
use Symfony\Component\Mailer\Transport\TransportInterface;
|
|
use Symfony\Component\Mime\Email;
|
|
use Symfony\Component\Mime\RawMessage;
|
|
|
|
/**
|
|
* Versand mit Protokoll (ADR 121): huefner-gartenbau.de zeigte zweimal „Nicht versendet“, und
|
|
* nirgends stand, warum.
|
|
*/
|
|
final class MailCheckTest extends TestCase
|
|
{
|
|
private string $log;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->log = sys_get_temp_dir().'/kant-mail-'.bin2hex(random_bytes(4)).'/mail.log';
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
@unlink($this->log);
|
|
@rmdir(dirname($this->log));
|
|
}
|
|
|
|
public function testAFailedSendSaysWhyAndKeepsTheCredentialsOutOfTheLog(): void
|
|
{
|
|
$fehler = new TransportException('Failed to authenticate on SMTP server with username "formular@kant-it.de" using the following authenticators: "LOGIN". Authenticator "LOGIN" returned "Expected response code "235" but got code "535", with message "535 Authentication credentials invalid".');
|
|
$fehler->appendDebug("< 220 smtp.ionos.de ESMTP\n> EHLO cms.test\n< 250 AUTH LOGIN PLAIN\n> AUTH LOGIN\n< 334 VXNlcm5hbWU6\n> Zm9ybXVsYXJAa2FudC1pdC5kZQ==\n< 334 UGFzc3dvcmQ6\n> R2VoZWltMTIz\n< 535 Authentication credentials invalid\n");
|
|
$mail = new MailCheck($this->transport($fehler), 'smtp://formular%40kant-it.de:Geheim123@smtp.ionos.de:587', $this->log);
|
|
|
|
$ergebnis = $mail->send($this->email(), 'Formular anfrage');
|
|
|
|
self::assertFalse($ergebnis['ok']);
|
|
self::assertStringContainsString('535', $ergebnis['meldung']);
|
|
self::assertStringContainsString('lehnt die Anmeldung ab', $ergebnis['hinweis']);
|
|
self::assertStringContainsString('< 535 Authentication credentials invalid', $ergebnis['dialog']);
|
|
// Was der Client schickt, bleibt draußen: bei AUTH LOGIN sind das Benutzer und Passwort.
|
|
self::assertStringNotContainsString('R2VoZWltMTIz', $ergebnis['dialog']);
|
|
self::assertStringNotContainsString('> ', $ergebnis['dialog']);
|
|
|
|
$protokoll = (string) file_get_contents($this->log);
|
|
self::assertStringNotContainsString('R2VoZWltMTIz', $protokoll);
|
|
self::assertStringNotContainsString('Geheim123', $protokoll);
|
|
$eintrag = $mail->recent()[0];
|
|
self::assertFalse($eintrag['ok']);
|
|
self::assertSame('Formular anfrage', $eintrag['anlass']);
|
|
self::assertSame('info@huefner-gartenbau.de', $eintrag['an']);
|
|
self::assertSame('formular@kant-it.de', $eintrag['von']);
|
|
}
|
|
|
|
public function testTheRouteNamesServerAndUserButNeverThePassword(): void
|
|
{
|
|
$route = (new MailCheck($this->transport(null), 'smtp://formular%40kant-it.de:Geheim123@smtp.ionos.de:587', $this->log))->route();
|
|
self::assertSame(['art' => 'SMTP', 'ziel' => 'smtp.ionos.de:587', 'benutzer' => 'formular@kant-it.de', 'gueltig' => true], $route);
|
|
|
|
self::assertFalse((new MailCheck($this->transport(null), 'kein dsn', $this->log))->route()['gueltig']);
|
|
self::assertSame('sendmail -bs des Servers', (new MailCheck($this->transport(null), 'sendmail://default', $this->log))->route()['art']);
|
|
}
|
|
|
|
public function testAnAcceptedMailIsLoggedWithWhatToCheckIfNothingArrives(): void
|
|
{
|
|
$mail = new MailCheck($this->transport(null), 'smtp://u:p@smtp.ionos.de:587', $this->log);
|
|
$ergebnis = $mail->send($this->email(), 'Testmail');
|
|
|
|
self::assertTrue($ergebnis['ok']);
|
|
self::assertStringContainsString('intern zu', $ergebnis['hinweis'], 'Microsoft 365 beim Kunden, Domain auch beim Hoster: der häufigste Grund für „angenommen, nie angekommen“.');
|
|
self::assertTrue($mail->recent()[0]['ok']);
|
|
}
|
|
|
|
public function testTheHintsCoverTheUsualCauses(): void
|
|
{
|
|
self::assertStringContainsString('nicht erreichbar', MailCheck::hinweis('Connection could not be established with host "ssl://smtp.ionos.de:465": stream_socket_client(): Unable to connect', 'smtps'));
|
|
self::assertStringContainsString('SMTP', MailCheck::hinweis('Expected response code "220" but got an empty response.', 'sendmail'));
|
|
self::assertStringContainsString('Absender', MailCheck::hinweis('Expected response code "250" but got code "550", with message "550 Sender address rejected".', 'smtp'));
|
|
self::assertStringContainsString('ungültig', MailCheck::hinweis('The "foo" scheme is not supported; supported schemes for mailer "smtp" are: "smtp", "smtps".', 'foo'));
|
|
self::assertStringContainsString('null://null', MailCheck::hinweisNachErfolg('null'));
|
|
}
|
|
|
|
public function testSmtpSettingsBecomeADsnWithTheSecretsEncoded(): void
|
|
{
|
|
self::assertSame('smtp://formular%40kant-it.de:a%23b%2Fc%25d%24e@smtp.ionos.de:587', MailCheck::smtpDsn(' SMTP.ionos.de ', 587, 'formular@kant-it.de', 'a#b/c%d$e'));
|
|
self::assertSame('smtps://u:p@mail.example.de:465', MailCheck::smtpDsn('mail.example.de', 465, 'u', 'p'));
|
|
self::assertSame('smtp://localhost:25', MailCheck::smtpDsn('localhost', 25, '', ''));
|
|
foreach ([['smtp.ionos.de/../x', 587], ['', 587], ['smtp.ionos.de', 0]] as [$server, $port]) {
|
|
try {
|
|
MailCheck::smtpDsn($server, $port, 'u', 'p');
|
|
self::fail($server.':'.$port.' hätte abgelehnt werden müssen.');
|
|
} catch (\InvalidArgumentException) {
|
|
self::addToAssertionCount(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
public function testTheCurrentSettingFillsTheFormButKeepsThePasswordToItself(): void
|
|
{
|
|
$mail = new MailCheck($this->transport(null), 'smtp://formular%40kant-it.de:Geheim123@smtp.ionos.de:587', $this->log);
|
|
self::assertSame(['weg' => 'smtp', 'server' => 'smtp.ionos.de', 'port' => 587, 'benutzer' => 'formular@kant-it.de', 'passwort' => true], $mail->einstellung());
|
|
self::assertSame('Geheim123', $mail->gespeichertesPasswort('smtp.ionos.de', 'formular@kant-it.de'));
|
|
// Anderer Server oder Benutzer: das gespeicherte Passwort gilt nicht mit.
|
|
self::assertSame('', $mail->gespeichertesPasswort('smtp.office365.com', 'formular@kant-it.de'));
|
|
self::assertSame('', $mail->gespeichertesPasswort('smtp.ionos.de', 'jemand@kant-it.de'));
|
|
|
|
self::assertSame('native', (new MailCheck($this->transport(null), 'native://default', $this->log))->einstellung()['weg']);
|
|
self::assertSame('sendmail-t', (new MailCheck($this->transport(null), MailCheck::serverWege()['sendmail-t']['dsn'], $this->log))->einstellung()['weg']);
|
|
}
|
|
|
|
public function testAWayCanBeTriedWithoutBeingTheSetOne(): void
|
|
{
|
|
// IONOS am 12.10.2026: sendmail -bs bricht ab, mail() geht. Ausprobiert wird über die
|
|
// Fabrik, ohne die Einstellung zu ändern.
|
|
$versucht = [];
|
|
$fabrik = function (string $dsn) use (&$versucht): TransportInterface {
|
|
$versucht[] = $dsn;
|
|
|
|
return $this->transport(null);
|
|
};
|
|
$eingestellt = $this->transport(new TransportException('Connection to "process /usr/sbin/sendmail -bs" has been closed unexpectedly.'));
|
|
$mail = new MailCheck($eingestellt, 'sendmail://default', $this->log, $fabrik);
|
|
|
|
$ergebnisse = [];
|
|
foreach (MailCheck::serverWege() as $schluessel => $weg) {
|
|
$ergebnisse[$schluessel] = $mail->send($this->email(), 'Testmail (alle Wege)', $weg['dsn']);
|
|
}
|
|
|
|
self::assertTrue($ergebnisse['native']['ok']);
|
|
self::assertSame('PHP mail() des Servers', $ergebnisse['native']['weg']);
|
|
self::assertSame('sendmail -t des Servers', $ergebnisse['sendmail-t']['weg']);
|
|
self::assertFalse($ergebnisse['sendmail-bs']['ok']);
|
|
self::assertStringContainsString('PHP mail()', $ergebnisse['sendmail-bs']['hinweis']);
|
|
// Der eingestellte Weg (hier sendmail -bs) geht über den Transport des Frameworks, nicht die Fabrik.
|
|
self::assertSame(['native://default', MailCheck::serverWege()['sendmail-t']['dsn']], $versucht);
|
|
self::assertCount(3, $mail->recent());
|
|
}
|
|
|
|
public function testAForeignSenderGetsItsOwnHint(): void
|
|
{
|
|
$meldung = 'Process failed with exit code 65: sendmail: envelope from address kantarci.u@gmail.com not accepted by the server sendmail: server message: 550-Sender address is not allowed.';
|
|
self::assertStringContainsString('eigenen Domain beim Hoster', MailCheck::hinweis($meldung, 'native'));
|
|
}
|
|
|
|
private function email(): Email
|
|
{
|
|
return (new Email())->from('formular@kant-it.de')->to('info@huefner-gartenbau.de')->subject('Anfrage')->text('Test');
|
|
}
|
|
|
|
/** Ein Transport, der nichts verschickt: wirft $fehler oder meldet „angenommen“. */
|
|
private function transport(?TransportException $fehler): TransportInterface
|
|
{
|
|
return new class($fehler) implements TransportInterface {
|
|
public function __construct(private ?TransportException $fehler)
|
|
{
|
|
}
|
|
|
|
public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage
|
|
{
|
|
if (null !== $this->fehler) {
|
|
throw $this->fehler;
|
|
}
|
|
$gesendet = new SentMessage($message, $envelope ?? Envelope::create($message));
|
|
$gesendet->appendDebug("< 250 2.0.0 Ok: queued\n");
|
|
|
|
return $gesendet;
|
|
}
|
|
|
|
public function __toString(): string
|
|
{
|
|
return 'test://';
|
|
}
|
|
};
|
|
}
|
|
}
|