|
|
@@ -9,10 +9,14 @@ use App\Form\FormSchema;
|
|
|
|
|
|
final class Mailer
|
|
|
{
|
|
|
+ private const NACHWEISE_FIELD_KEY = 'qualifikationsnachweise';
|
|
|
+ private const NACHWEISE_MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
|
+
|
|
|
/** @var array<string, mixed> */
|
|
|
private array $mailConfig;
|
|
|
/** @var array<string, mixed> */
|
|
|
private array $appConfig;
|
|
|
+ private FormSchema $schema;
|
|
|
private SubmissionFormatter $formatter;
|
|
|
private PdfGenerator $pdfGenerator;
|
|
|
|
|
|
@@ -20,9 +24,9 @@ final class Mailer
|
|
|
{
|
|
|
$this->mailConfig = Bootstrap::config('mail');
|
|
|
$this->appConfig = Bootstrap::config('app');
|
|
|
- $schema = new FormSchema();
|
|
|
- $this->formatter = new SubmissionFormatter($schema);
|
|
|
- $this->pdfGenerator = new PdfGenerator($this->formatter, $schema);
|
|
|
+ $this->schema = new FormSchema();
|
|
|
+ $this->formatter = new SubmissionFormatter($this->schema);
|
|
|
+ $this->pdfGenerator = new PdfGenerator($this->formatter, $this->schema);
|
|
|
}
|
|
|
|
|
|
public function sendOtpMail(string $email, string $code, int $ttlSeconds): bool
|
|
|
@@ -59,43 +63,39 @@ final class Mailer
|
|
|
public function sendSubmissionMails(array $submission): void
|
|
|
{
|
|
|
$formDataPdf = $this->pdfGenerator->generateFormDataPdf($submission);
|
|
|
- $attachmentsPdf = $this->pdfGenerator->generateAttachmentsPdf($submission);
|
|
|
- $pdfAttachments = $this->pdfGenerator->collectPdfAttachments($submission);
|
|
|
$minorSignaturePdf = $this->pdfGenerator->generateMinorSignaturePdf($submission);
|
|
|
$isMinorSubmission = $this->isMinorSubmission($submission);
|
|
|
|
|
|
try {
|
|
|
- $this->sendAdminMails($submission, $formDataPdf, $attachmentsPdf, $pdfAttachments, $isMinorSubmission);
|
|
|
+ $this->sendAdminMails($submission, $formDataPdf, $isMinorSubmission);
|
|
|
$this->sendApplicantMail($submission, $minorSignaturePdf);
|
|
|
} finally {
|
|
|
if ($formDataPdf !== null) {
|
|
|
@unlink($formDataPdf);
|
|
|
}
|
|
|
- if ($attachmentsPdf !== null) {
|
|
|
- @unlink($attachmentsPdf);
|
|
|
- }
|
|
|
if ($minorSignaturePdf !== null) {
|
|
|
@unlink($minorSignaturePdf);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * @param array<string, mixed> $submission
|
|
|
- * @param array<int, array{path: string, filename: string}> $pdfAttachments
|
|
|
- */
|
|
|
private function sendAdminMails(
|
|
|
array $submission,
|
|
|
?string $formDataPdf,
|
|
|
- ?string $attachmentsPdf,
|
|
|
- array $pdfAttachments,
|
|
|
bool $isMinorSubmission,
|
|
|
): void {
|
|
|
$recipients = (array) ($this->mailConfig['recipients'] ?? []);
|
|
|
$subject = (string) ($this->mailConfig['subjects']['admin'] ?? 'Neuer Mitgliedsantrag');
|
|
|
+ $attachmentInfo = $this->collectAdminUploadAttachments($submission);
|
|
|
+ $uploadWarning = null;
|
|
|
|
|
|
- $htmlBody = $this->renderAdminHtml($submission, $isMinorSubmission);
|
|
|
- $textBody = $this->renderAdminText($submission, $isMinorSubmission);
|
|
|
+ if ($attachmentInfo['nachweise_skipped']) {
|
|
|
+ $uploadWarning = $this->buildNachweiseOversizeWarning((int) $attachmentInfo['nachweise_total_bytes']);
|
|
|
+ Bootstrap::log('mail', 'Qualifikationsnachweise nicht angehängt (zu groß): ' . $uploadWarning);
|
|
|
+ }
|
|
|
+
|
|
|
+ $htmlBody = $this->renderAdminHtml($submission, $isMinorSubmission, $uploadWarning);
|
|
|
+ $textBody = $this->renderAdminText($submission, $isMinorSubmission, $uploadWarning);
|
|
|
|
|
|
foreach ($recipients as $recipient) {
|
|
|
if (!is_string($recipient) || $recipient === '') {
|
|
|
@@ -112,11 +112,8 @@ final class Mailer
|
|
|
if ($formDataPdf !== null) {
|
|
|
$mail->addAttachment($formDataPdf, 'Antragsdaten.pdf', 'application/pdf');
|
|
|
}
|
|
|
- if ($attachmentsPdf !== null) {
|
|
|
- $mail->addAttachment($attachmentsPdf, 'Anlagen-Bilder.pdf', 'application/pdf');
|
|
|
- }
|
|
|
- foreach ($pdfAttachments as $att) {
|
|
|
- $mail->addAttachment($att['path'], $att['filename'], 'application/pdf');
|
|
|
+ foreach ($attachmentInfo['attachments'] as $att) {
|
|
|
+ $mail->addAttachment($att['path'], $att['filename'], $att['mime']);
|
|
|
}
|
|
|
|
|
|
if (!$mail->send()) {
|
|
|
@@ -177,7 +174,7 @@ final class Mailer
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
|
/** @param array<string, mixed> $submission */
|
|
|
- private function renderAdminHtml(array $submission, bool $isMinorSubmission): string
|
|
|
+ private function renderAdminHtml(array $submission, bool $isMinorSubmission = false, ?string $uploadWarning = null): string
|
|
|
{
|
|
|
$steps = $this->formatter->formatSteps($submission);
|
|
|
$uploads = $this->formatter->formatUploads($submission);
|
|
|
@@ -189,6 +186,11 @@ final class Mailer
|
|
|
if ($isMinorSubmission) {
|
|
|
$h .= $this->renderMinorAdminNoticeHtml();
|
|
|
}
|
|
|
+ if ($uploadWarning !== null && $uploadWarning !== '') {
|
|
|
+ $h .= '<div style="background:#fff3cd;border:1px solid #f0c36d;padding:10px 12px;margin:12px 0">'
|
|
|
+ . '<strong>Hinweis zu Anhängen:</strong> ' . nl2br($this->esc($uploadWarning))
|
|
|
+ . '</div>';
|
|
|
+ }
|
|
|
|
|
|
foreach ($steps as $step) {
|
|
|
$h .= '<h3 style="border-bottom:2px solid #c0392b;padding-bottom:4px">' . $this->esc($step['title']) . '</h3>';
|
|
|
@@ -217,7 +219,7 @@ final class Mailer
|
|
|
}
|
|
|
|
|
|
/** @param array<string, mixed> $submission */
|
|
|
- private function renderAdminText(array $submission, bool $isMinorSubmission): string
|
|
|
+ private function renderAdminText(array $submission, bool $isMinorSubmission = false, ?string $uploadWarning = null): string
|
|
|
{
|
|
|
$steps = $this->formatter->formatSteps($submission);
|
|
|
$uploads = $this->formatter->formatUploads($submission);
|
|
|
@@ -228,6 +230,10 @@ final class Mailer
|
|
|
if ($isMinorSubmission) {
|
|
|
$t .= $this->renderMinorAdminNoticeText() . "\n\n";
|
|
|
}
|
|
|
+ if ($uploadWarning !== null && $uploadWarning !== '') {
|
|
|
+ $t .= "HINWEIS ZU ANHÄNGEN\n";
|
|
|
+ $t .= $uploadWarning . "\n\n";
|
|
|
+ }
|
|
|
|
|
|
foreach ($steps as $step) {
|
|
|
$t .= strtoupper($step['title']) . "\n" . str_repeat('-', 40) . "\n";
|
|
|
@@ -254,7 +260,7 @@ final class Mailer
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
|
/** @param array<string, mixed> $submission */
|
|
|
- private function renderApplicantHtml(array $submission, bool $isMinorSubmission): string
|
|
|
+ private function renderApplicantHtml(array $submission, bool $isMinorSubmission = false): string
|
|
|
{
|
|
|
$steps = $this->formatter->formatSteps($submission);
|
|
|
$uploads = $this->formatter->formatUploads($submission);
|
|
|
@@ -296,7 +302,7 @@ final class Mailer
|
|
|
}
|
|
|
|
|
|
/** @param array<string, mixed> $submission */
|
|
|
- private function renderApplicantText(array $submission, bool $isMinorSubmission): string
|
|
|
+ private function renderApplicantText(array $submission, bool $isMinorSubmission = false): string
|
|
|
{
|
|
|
$steps = $this->formatter->formatSteps($submission);
|
|
|
$uploads = $this->formatter->formatUploads($submission);
|
|
|
@@ -363,6 +369,159 @@ final class Mailer
|
|
|
. 'Bitte die Bearbeitung erst nach Eingang des handschriftlich unterschriebenen Formulars fortsetzen.';
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * @param array<string, mixed> $submission
|
|
|
+ * @return array{
|
|
|
+ * attachments: array<int, array{path: string, filename: string, mime: string}>,
|
|
|
+ * nachweise_skipped: bool,
|
|
|
+ * nachweise_total_bytes: int
|
|
|
+ * }
|
|
|
+ */
|
|
|
+ private function collectAdminUploadAttachments(array $submission): array
|
|
|
+ {
|
|
|
+ $uploads = (array) ($submission['uploads'] ?? []);
|
|
|
+ $uploadFields = $this->schema->getUploadFields();
|
|
|
+ $attachments = [];
|
|
|
+ $nachweiseAttachments = [];
|
|
|
+ $nachweiseTotalBytes = 0;
|
|
|
+
|
|
|
+ foreach ($uploadFields as $fieldKey => $_fieldDef) {
|
|
|
+ $files = $uploads[$fieldKey] ?? [];
|
|
|
+ if (!is_array($files)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($files as $file) {
|
|
|
+ if (!is_array($file)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $resolved = $this->resolveUploadAttachment($submission, $file);
|
|
|
+ if ($resolved === null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $entry = [
|
|
|
+ 'path' => $resolved['path'],
|
|
|
+ 'filename' => $resolved['filename'],
|
|
|
+ 'mime' => $resolved['mime'],
|
|
|
+ ];
|
|
|
+
|
|
|
+ if ($fieldKey === self::NACHWEISE_FIELD_KEY) {
|
|
|
+ $nachweiseAttachments[] = $entry;
|
|
|
+ $nachweiseTotalBytes += $resolved['size'];
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $attachments[] = $entry;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $nachweiseSkipped = $nachweiseTotalBytes > self::NACHWEISE_MAX_ATTACHMENT_BYTES;
|
|
|
+ if (!$nachweiseSkipped) {
|
|
|
+ $attachments = array_merge($attachments, $nachweiseAttachments);
|
|
|
+ }
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'attachments' => $attachments,
|
|
|
+ 'nachweise_skipped' => $nachweiseSkipped,
|
|
|
+ 'nachweise_total_bytes' => $nachweiseTotalBytes,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param array<string, mixed> $submission
|
|
|
+ * @param array<string, mixed> $file
|
|
|
+ * @return array{path: string, filename: string, mime: string, size: int}|null
|
|
|
+ */
|
|
|
+ private function resolveUploadAttachment(array $submission, array $file): ?array
|
|
|
+ {
|
|
|
+ $path = $this->resolveSubmissionUploadPath($submission, $file);
|
|
|
+ if ($path === null || !is_file($path)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $filename = trim((string) ($file['original_filename'] ?? basename($path)));
|
|
|
+ if ($filename === '') {
|
|
|
+ $filename = basename($path);
|
|
|
+ }
|
|
|
+
|
|
|
+ $mime = trim((string) ($file['mime'] ?? ''));
|
|
|
+ if ($mime === '') {
|
|
|
+ $detected = @mime_content_type($path);
|
|
|
+ $mime = is_string($detected) && $detected !== '' ? $detected : 'application/octet-stream';
|
|
|
+ }
|
|
|
+
|
|
|
+ $size = (int) ($file['size'] ?? 0);
|
|
|
+ if ($size <= 0) {
|
|
|
+ $size = (int) (filesize($path) ?: 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'path' => $path,
|
|
|
+ 'filename' => $filename,
|
|
|
+ 'mime' => $mime,
|
|
|
+ 'size' => $size,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param array<string, mixed> $submission
|
|
|
+ * @param array<string, mixed> $file
|
|
|
+ */
|
|
|
+ private function resolveSubmissionUploadPath(array $submission, array $file): ?string
|
|
|
+ {
|
|
|
+ $baseUploads = rtrim((string) ($this->appConfig['storage']['uploads'] ?? ''), '/');
|
|
|
+ if ($baseUploads === '') {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $relativePath = str_replace(['..', '\\'], '', (string) ($file['relative_path'] ?? ''));
|
|
|
+ if ($relativePath !== '') {
|
|
|
+ $candidate = $baseUploads . '/' . ltrim($relativePath, '/');
|
|
|
+ if (is_file($candidate)) {
|
|
|
+ return $candidate;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $storedDir = str_replace(['..', '\\'], '', (string) ($file['stored_dir'] ?? ''));
|
|
|
+ $storedFilename = str_replace(['..', '\\'], '', (string) ($file['stored_filename'] ?? ''));
|
|
|
+ if ($storedDir === '' || $storedFilename === '') {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $storedDir = ltrim($storedDir, '/');
|
|
|
+ $candidates = [$baseUploads . '/' . $storedDir . '/' . $storedFilename];
|
|
|
+ $appKey = trim((string) ($submission['application_key'] ?? ''));
|
|
|
+
|
|
|
+ if ($appKey !== '' && !str_starts_with($storedDir, $appKey . '/')) {
|
|
|
+ $candidates[] = $baseUploads . '/' . $appKey . '/' . $storedDir . '/' . $storedFilename;
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($candidates as $candidate) {
|
|
|
+ if (is_file($candidate)) {
|
|
|
+ return $candidate;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private function buildNachweiseOversizeWarning(int $totalBytes): string
|
|
|
+ {
|
|
|
+ $totalMb = $this->formatMegabytes($totalBytes);
|
|
|
+ $limitMb = $this->formatMegabytes(self::NACHWEISE_MAX_ATTACHMENT_BYTES);
|
|
|
+
|
|
|
+ return 'Die Qualifikationsnachweise wurden nicht als E-Mail-Anhang versendet, weil die Gesamtgröße mit '
|
|
|
+ . $totalMb . ' MB das Limit von ' . $limitMb . ' MB überschreitet. '
|
|
|
+ . 'Bitte laden Sie diese Dateien über die Admin-Seite herunter.';
|
|
|
+ }
|
|
|
+
|
|
|
+ private function formatMegabytes(int $bytes): string
|
|
|
+ {
|
|
|
+ return number_format(max(0, $bytes) / 1024 / 1024, 1, ',', '.');
|
|
|
+ }
|
|
|
+
|
|
|
/** @param array<string, mixed> $submission */
|
|
|
private function isMinorSubmission(array $submission): bool
|
|
|
{
|