Give mail delivery its own admin page and move the origin probe off /.well-known

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>
This commit is contained in:
kantarciandClaude Opus 5.5 committed 2026-10-12 21:07:27 +02:00
1 parent 2497b8cb69
commit b982cb8944
17 files changed
+597 -148

No files matched your search

+1 -1
View File
@@ -18,7 +18,7 @@ final class HttpsRedirectTest extends TestCase
/** @var list<?string> */
private array $answers = [];
/** @var array{status: int, location: string, error: string} */
private array $http = ['status' => 301, 'location' => 'https://www.example.de/.well-known/kantsites-origin', 'error' => ''];
private array $http = ['status' => 301, 'location' => 'https://www.example.de/kantsites-origin', 'error' => ''];
protected function setUp(): void
{
+63 -1
View File
@@ -62,7 +62,7 @@ final class MailCheckTest extends TestCase
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 des Servers', (new MailCheck($this->transport(null), 'sendmail://default', $this->log))->route()['art']);
self::assertSame('sendmail -bs des Servers', (new MailCheck($this->transport(null), 'sendmail://default', $this->log))->route()['art']);
}
public function testAnAcceptedMailIsLoggedWithWhatToCheckIfNothingArrives(): void
@@ -84,6 +84,68 @@ final class MailCheckTest extends TestCase
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');
+41 -14
View File
@@ -201,7 +201,7 @@ final class SystemScreenTest extends WebTestCase
self::assertStringNotContainsString('kein-server', (string) file_get_contents(dirname(__DIR__).'/.env'));
// Die Gegenstelle der Prüfung: öffentlich, ohne Anmeldung, und nur die Kennung.
$client->request('GET', '/.well-known/kantsites-origin');
$client->request('GET', '/kantsites-origin');
self::assertSame(200, $client->getResponse()->getStatusCode());
self::assertSame(hash_hmac('sha256', 'site-origin', 'isolated-test-secret'), $client->getResponse()->getContent());
}
@@ -360,38 +360,65 @@ final class SystemScreenTest extends WebTestCase
self::assertStringContainsString('Migrationen stehen aus', (string) $admin->getResponse()->getContent(), 'Und es steht immer noch aus.');
}
public function testATestMailGoesToAnyAddressAndTheScreenSaysWhatHappened(): void
public function testTheMailPageTestsAWayAndSaysWhatHappened(): void
{
// Umut: „Bau mal in den Backend einen Knopf ein, Testmail an Mailadresse, wo man einträgt,
// und wo Logs angezeigt werden, woran es liegt“ (ADR 121).
// und wo Logs angezeigt werden, woran es liegt“ (ADR 121) - und dann: alle Wege probieren,
// SMTP dort setzen, auf einer eigenen Seite unter „System“ (ADR 122).
// MAILER_DSN gilt hier als Server-Umgebung: so schreibt kein Test in die echte .env.
$_SERVER['SYMFONY_DOTENV_VARS'] = '';
$client = $this->login();
$client->request('GET', '/admin/system');
$html = (string) $client->getResponse()->getContent();
self::assertStringContainsString('E-Mail-Versand', $html);
self::assertStringContainsString('Noch nichts versendet', $html);
$token = (string) $client->getCrawler()->filter('form[action$="/system/testmail"] input[name=_csrf_token]')->attr('value');
self::assertStringContainsString('href="/admin/system/e-mail"', $html, 'Ein Unterpunkt von System im Menü.');
self::assertStringNotContainsString('Alle Wege testen', $html, 'Die Systemseite trägt den Versand nicht mehr selbst.');
$client->request('POST', '/admin/system/testmail', ['_csrf_token' => $token, 'to' => 'kein-postfach']);
$client->request('GET', '/admin/system/e-mail');
self::assertSame(200, $client->getResponse()->getStatusCode());
$html = (string) $client->getResponse()->getContent();
self::assertStringContainsString('Alle Wege testen', $html);
self::assertStringContainsString('value="cms.test"', $html, 'Der eingestellte SMTP-Server steht im Feld.');
self::assertStringContainsString('Noch nichts versendet', $html);
// Im Test steht MAILER_DSN nicht in .env: testen ja, speichern nein.
self::assertStringNotContainsString('value="speichern"', $html);
$token = (string) $client->getCrawler()->filter('form[action$="/system/e-mail"] input[name=_csrf_token]')->attr('value');
$client->request('POST', '/admin/system/e-mail', ['_csrf_token' => $token, 'aktion' => 'test', 'weg' => 'smtp', 'server' => 'cms.test', 'port' => '587', 'to' => 'kein-postfach']);
$client->followRedirect();
self::assertStringContainsString('keine gültige E-Mail-Adresse', (string) $client->getResponse()->getContent());
$client->request('POST', '/admin/system/testmail', ['_csrf_token' => $token, 'to' => 'umut@cms.test', 'from' => 'formular@cms.test']);
self::assertStringEndsWith('/admin/system#mail', (string) $client->getResponse()->headers->get('Location'));
// Derselbe Weg wie eingestellt nimmt den Transport des Frameworks - im Test null://null.
$client->request('POST', '/admin/system/e-mail', ['_csrf_token' => $token, 'aktion' => 'test', 'weg' => 'smtp', 'server' => 'cms.test', 'port' => '587', 'to' => 'umut@cms.test', 'from' => 'formular@cms.test']);
self::assertStringEndsWith('/admin/system/e-mail', (string) $client->getResponse()->headers->get('Location'));
self::assertEmailCount(1);
$mail = self::getMailerMessage();
self::assertEmailAddressContains($mail, 'To', 'umut@cms.test');
self::assertEmailAddressContains($mail, 'From', 'formular@cms.test');
$client->request('GET', '/admin/system');
$client->request('GET', '/admin/system/e-mail');
$html = (string) $client->getResponse()->getContent();
self::assertStringContainsString('Testmail an umut@cms.test vom Mailserver angenommen.', $html);
self::assertStringContainsString('Testmail an <code>umut@cms.test</code>', $html, 'Der Versuch steht danach in „Letzte Versuche“.');
self::assertStringContainsString('Testmail an umut@cms.test vom Mailserver angenommen', $html);
self::assertStringContainsString('Testmail an <code>umut@cms.test</code>', $html, 'Der Versuch steht danach unter „Letzte Versuche“.');
self::assertStringContainsString('value="umut@cms.test"', $html, 'Die Eingabe bleibt stehen.');
// SMTP mit Benutzer, aber ohne Passwort und ohne gespeichertes: abgelehnt, bevor etwas rausgeht.
$client->request('POST', '/admin/system/e-mail', ['_csrf_token' => $token, 'aktion' => 'test', 'weg' => 'smtp', 'server' => 'smtp.anderer.test', 'port' => '587', 'benutzer' => 'x@anderer.test', 'to' => 'umut@cms.test']);
self::assertEmailCount(0);
$client->followRedirect();
self::assertStringContainsString('das Passwort des Postfachs eintragen', (string) $client->getResponse()->getContent());
// Speichern geht nur, wenn die Einstellung in .env steht.
$client->request('POST', '/admin/system/e-mail', ['_csrf_token' => $token, 'aktion' => 'speichern', 'weg' => 'native']);
$client->followRedirect();
self::assertStringContainsString('in der Server-Umgebung gesetzt', (string) $client->getResponse()->getContent());
// Nur für Administratoren, und nur mit Token.
$client->request('POST', '/admin/system/testmail', ['_csrf_token' => 'erfunden', 'to' => 'umut@cms.test']);
$client->request('POST', '/admin/system/e-mail', ['_csrf_token' => 'erfunden', 'aktion' => 'test', 'weg' => 'smtp', 'server' => 'cms.test', 'to' => 'umut@cms.test']);
self::assertEmailCount(0);
$redaktion = $this->login('editor');
$redaktion->request('POST', '/admin/system/testmail', ['_csrf_token' => $token, 'to' => 'umut@cms.test']);
$redaktion->request('GET', '/admin/system/e-mail');
self::assertSame(403, $redaktion->getResponse()->getStatusCode());
$redaktion->request('POST', '/admin/system/e-mail', ['_csrf_token' => $token, 'aktion' => 'test', 'weg' => 'smtp', 'server' => 'cms.test', 'to' => 'umut@cms.test']);
self::assertSame(403, $redaktion->getResponse()->getStatusCode());
}