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:
1 parent
2497b8cb69
commit
b982cb8944
17 files changed
+597
-148
No files matched your search
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Ops\Environment;
|
||||
use App\Ops\MailCheck;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
/**
|
||||
* E-Mail-Versand (ADR 121, 122): eigene Seite unter „System“, damit die Systemseite nicht alles
|
||||
* trägt. Weg wählen, ausprobieren, alle Wege des Servers testen, speichern - und die letzten
|
||||
* Versuche mit Ursache.
|
||||
*/
|
||||
final class MailController extends AbstractController
|
||||
{
|
||||
public function __construct(private MailCheck $mail, private string $projectDir, private string $siteOrigin)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/%kant.admin_path%/system/e-mail', name: 'admin_mail', methods: ['GET'])]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
$session = $request->getSession();
|
||||
|
||||
return $this->render('admin/mail.html.twig', [
|
||||
'mail' => $this->mailFacts(),
|
||||
'mailResult' => $session->remove('mail_result'),
|
||||
'mailResults' => $session->remove('mail_results'),
|
||||
'mailNotice' => $session->remove('mail_notice'),
|
||||
// Die letzte Eingabe bleibt stehen, ohne Passwort.
|
||||
'mailForm' => $session->remove('mail_form'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Versand einstellen und ausprobieren (ADR 121, 122): ein Weg testen, alle Wege des Servers
|
||||
* nacheinander testen, oder den gewählten Weg als MAILER_DSN in .env speichern.
|
||||
*
|
||||
* Das SMTP-Passwort steht nie im Formular. Bleibt das Feld leer, gilt das gespeicherte, solange
|
||||
* Server und Benutzer gleich bleiben.
|
||||
*/
|
||||
#[Route('/%kant.admin_path%/system/e-mail', name: 'admin_mail_save', methods: ['POST'])]
|
||||
public function save(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
$session = $request->getSession();
|
||||
if (!$this->isCsrfTokenValid('system', $request->request->getString('_csrf_token'))) {
|
||||
return $this->backToMail($request, 'Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
|
||||
}
|
||||
$eingabe = [
|
||||
'weg' => $request->request->getString('weg'),
|
||||
'server' => trim($request->request->getString('server')),
|
||||
'port' => $request->request->getInt('port', 587),
|
||||
'benutzer' => trim($request->request->getString('benutzer')),
|
||||
'an' => trim($request->request->getString('to')),
|
||||
'von' => trim($request->request->getString('from')),
|
||||
];
|
||||
$session->set('mail_form', $eingabe);
|
||||
$aktion = $request->request->getString('aktion');
|
||||
|
||||
if ('alle' === $aktion) {
|
||||
if (null !== $fehler = $this->mailAdressFehler($eingabe)) {
|
||||
return $this->backToMail($request, $fehler);
|
||||
}
|
||||
$wege = array_map(static fn (array $weg): string => $weg['dsn'], MailCheck::serverWege());
|
||||
if ('' !== $eingabe['server']) {
|
||||
try {
|
||||
$wege['smtp'] = $this->smtpDsn($request, $eingabe);
|
||||
} catch (\InvalidArgumentException) {
|
||||
// Unvollständiges SMTP bleibt bei „alle“ einfach draußen.
|
||||
}
|
||||
}
|
||||
$ergebnisse = [];
|
||||
foreach ($wege as $schluessel => $dsn) {
|
||||
$ergebnisse[] = ['schluessel' => $schluessel] + $this->mail->send($this->testEmail($request, $eingabe), 'Testmail (alle Wege)', $dsn);
|
||||
}
|
||||
$session->set('mail_results', $ergebnisse);
|
||||
|
||||
return $this->backToMail($request);
|
||||
}
|
||||
|
||||
try {
|
||||
$dsn = isset(MailCheck::serverWege()[$eingabe['weg']]) ? MailCheck::serverWege()[$eingabe['weg']]['dsn'] : $this->smtpDsn($request, $eingabe);
|
||||
} catch (\InvalidArgumentException $fehler) {
|
||||
return $this->backToMail($request, $fehler->getMessage());
|
||||
}
|
||||
|
||||
if ('speichern' === $aktion) {
|
||||
if ('.env' !== $this->environmentSource('MAILER_DSN')) {
|
||||
return $this->backToMail($request, 'MAILER_DSN ist in der Server-Umgebung gesetzt und hat dort Vorrang. Ändern lässt sie sich nur beim Hoster.');
|
||||
}
|
||||
try {
|
||||
Environment::write($this->projectDir, ['MAILER_DSN' => $dsn]);
|
||||
} catch (\RuntimeException $fehler) {
|
||||
return $this->backToMail($request, $fehler->getMessage());
|
||||
}
|
||||
$weg = $this->mail->route($dsn);
|
||||
|
||||
return $this->backToMail($request, sprintf('Gespeichert: %s%s. Formulare versenden ab jetzt auf diesem Weg.', $weg['art'], '' !== $weg['ziel'] ? ' über '.$weg['ziel'] : ''), 'ok');
|
||||
}
|
||||
|
||||
if (null !== $fehler = $this->mailAdressFehler($eingabe)) {
|
||||
return $this->backToMail($request, $fehler);
|
||||
}
|
||||
$session->set('mail_result', ['an' => $eingabe['an']] + $this->mail->send($this->testEmail($request, $eingabe), 'Testmail', $dsn));
|
||||
|
||||
return $this->backToMail($request);
|
||||
}
|
||||
|
||||
/** @param array{an: string, von: string} $eingabe */
|
||||
private function mailAdressFehler(array $eingabe): ?string
|
||||
{
|
||||
foreach ([$eingabe['an'], $eingabe['von'] ?: $eingabe['an']] as $adresse) {
|
||||
if (false === filter_var($adresse, FILTER_VALIDATE_EMAIL)) {
|
||||
return '' === $adresse ? 'Für eine Testmail bitte eine Empfängeradresse eintragen.' : sprintf('„%s“ ist keine gültige E-Mail-Adresse.', mb_substr($adresse, 0, 100));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @param array{server: string, port: int, benutzer: string} $eingabe */
|
||||
private function smtpDsn(Request $request, array $eingabe): string
|
||||
{
|
||||
if ('' === $eingabe['server']) {
|
||||
throw new \InvalidArgumentException('Für SMTP bitte den Server eintragen, etwa smtp.ionos.de.');
|
||||
}
|
||||
$passwort = $request->request->getString('passwort');
|
||||
if ('' === $passwort) {
|
||||
$passwort = $this->mail->gespeichertesPasswort($eingabe['server'], $eingabe['benutzer']);
|
||||
}
|
||||
if ('' !== $eingabe['benutzer'] && '' === $passwort) {
|
||||
throw new \InvalidArgumentException('Für SMTP bitte das Passwort des Postfachs eintragen.');
|
||||
}
|
||||
|
||||
return MailCheck::smtpDsn($eingabe['server'], $eingabe['port'], $eingabe['benutzer'], $passwort);
|
||||
}
|
||||
|
||||
/** @param array{an: string, von: string} $eingabe */
|
||||
private function testEmail(Request $request, array $eingabe): Email
|
||||
{
|
||||
$website = rtrim($this->siteOrigin, '/') ?: $request->getSchemeAndHttpHost();
|
||||
|
||||
return (new Email())
|
||||
->from(new Address($eingabe['von'] ?: $eingabe['an']))
|
||||
->to(new Address($eingabe['an']))
|
||||
->subject('Testmail von '.(parse_url($website, PHP_URL_HOST) ?: $website))
|
||||
->text("Diese Testmail kommt aus der Verwaltung von {$website} (System → E-Mail-Versand).\n\nWenn sie angekommen ist, funktioniert der Versand der Formulare auf diesem Weg.\n");
|
||||
}
|
||||
|
||||
private function backToMail(Request $request, ?string $meldung = null, string $art = 'error'): Response
|
||||
{
|
||||
if (null !== $meldung) {
|
||||
$request->getSession()->set('mail_notice', ['art' => $art, 'text' => $meldung]);
|
||||
}
|
||||
|
||||
return $this->redirect($this->generateUrl('admin_mail'));
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function mailFacts(): array
|
||||
{
|
||||
$route = $this->mail->route();
|
||||
|
||||
return $route + [
|
||||
'einstellung' => $this->mail->einstellung(),
|
||||
'wege' => MailCheck::serverWege(),
|
||||
'quelle' => $this->environmentSource('MAILER_DSN'),
|
||||
// Wer sich mit einer Adresse anmeldet, versendet meist auch nur unter ihr.
|
||||
'absender' => false !== filter_var($route['benutzer'], FILTER_VALIDATE_EMAIL) ? $route['benutzer'] : '',
|
||||
'protokoll' => $this->mail->recent(),
|
||||
];
|
||||
}
|
||||
|
||||
/** Explicit server values take precedence over the single installation file. */
|
||||
private function environmentSource(string $name): string
|
||||
{
|
||||
$loaded = explode(',', $_SERVER['SYMFONY_DOTENV_VARS'] ?? $_ENV['SYMFONY_DOTENV_VARS'] ?? '');
|
||||
|
||||
return in_array($name, $loaded, true) ? '.env' : 'Server-Umgebung';
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user