Selaa lähdekoodia

adding all uploaded attachments to the mail

Medowar 4 päivää sitten
vanhempi
sitoutus
a2b0212fe7
1 muutettua tiedostoa jossa 185 lisäystä ja 26 poistoa
  1. 185 26
      src/mail/mailer.php

+ 185 - 26
src/mail/mailer.php

@@ -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
     {