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>
202 lines
8.8 KiB
PHP
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';
|
|
}
|
|
}
|