Files
KantSites/src/Controller/MailController.php
T
kantarciandClaude Opus 5.5 7eaa3f6efa Tidy the mail and system screens and let the mail log be cleared
Text in admin cards had no size of its own, so 16px browser default sat
next to 13px notes; cards now use 13px throughout while fields stay at
16px for iOS. The mail page gets option cards, a 2x2 SMTP grid, a styled
save button in the card foot and a log with status pills and collapsed
server messages, plus "Protokoll leeren". The package card uses the same
sections and a styled file picker. The E-Mail-Versand subitem only shows
inside System and sits beside it in the horizontal strip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 07:48:43 +02:00

202 lines
8.8 KiB
PHP

<?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);
}
/** „Letzte Versuche“ leeren: var/log/mail.log und mail.log.1. */
#[Route('/%kant.admin_path%/system/e-mail/protokoll', name: 'admin_mail_clear', methods: ['POST'])]
public function clear(Request $request): Response
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
if (!$this->isCsrfTokenValid('system', $request->request->getString('_csrf_token'))) {
return $this->backToMail($request, 'Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
}
$this->mail->clearLog();
return $this->backToMail($request, 'Das Protokoll ist leer.', 'ok');
}
/** @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';
}
}