user = model(User::class); $this->mailer = app(EmailService::class); } public function metadata(): JsonResponse { $parents = array_values(array_filter( $this->user->getParents(), static fn ($parent) => !empty($parent['email']) )); return $this->success([ 'parents' => $parents, 'fromOptions' => $this->senderOptionsFromEnv(), ], 'Broadcast email metadata retrieved'); } public function send(): JsonResponse { $data = $this->payloadData(); $rules = [ 'subject' => 'required|string', 'body_html' => 'required|string', 'mode' => 'permit_empty|string', 'from_key' => 'permit_empty|string', 'wrap_layout'=> 'permit_empty', 'preheader' => 'permit_empty|string', 'cta_text' => 'permit_empty|string', 'cta_url' => 'permit_empty|string', ]; $errors = $this->validateRequest($data, $rules); if (!empty($errors)) { return $this->respondValidationError($errors); } $isTest = !empty($data['send_test_only']); $mode = (string) ($data['mode'] ?? 'standard'); $subject = trim((string) $data['subject']); $fromKey = trim((string) ($data['from_key'] ?? 'general')); $wrapLayout = !empty($data['wrap_layout']); $preheader = (string) ($data['preheader'] ?? ''); $ctaText = (string) ($data['cta_text'] ?? ''); $ctaUrl = (string) ($data['cta_url'] ?? ''); $body = $this->sanitizeEmailHtml((string) $data['body_html']); if ($subject === '' || $body === '') { return $this->respondError('Subject and body are required.', Response::HTTP_BAD_REQUEST); } if ($isTest) { $testEmail = trim((string) ($data['test_email'] ?? '')); if ($testEmail === '' || !filter_var($testEmail, FILTER_VALIDATE_EMAIL)) { return $this->respondError('Provide a valid test email address.', Response::HTTP_BAD_REQUEST); } $html = $this->composeEmailHtml( $wrapLayout, $subject, $body, 'Parent', $preheader, $ctaText, $ctaUrl, $mode === 'personalized' ); $ok = $this->mailer->send($testEmail, '[TEST] ' . $subject, $html, $fromKey); return $ok ? $this->success(['message' => "Test email sent to {$testEmail}."], 'Test email sent') : $this->respondError('Test email failed to send. Check logs.', Response::HTTP_INTERNAL_SERVER_ERROR); } $parentIds = $this->parseParentIds($data['parent_ids'] ?? []); if (empty($parentIds)) { return $this->respondError('Please select at least one parent.', Response::HTTP_BAD_REQUEST); } $rows = $this->user->newQuery() ->select('users.id', 'users.email', 'users.firstname', 'users.lastname') ->whereIn('users.id', $parentIds) ->whereNotNull('users.email') ->get(); if ($rows->isEmpty()) { return $this->respondError('No valid parent emails found.', Response::HTTP_BAD_REQUEST); } $stats = ['attempted' => 0, 'sent' => 0, 'failed' => 0, 'mode' => $mode]; $personalized = ($mode === 'personalized'); foreach ($rows as $row) { $stats['attempted']++; $recipientName = trim(($row->firstname ?? '') . ' ' . ($row->lastname ?? '')) ?: 'Parent'; $html = $this->composeEmailHtml( $wrapLayout, $subject, $body, $recipientName, $preheader, $ctaText, $ctaUrl, $personalized ); $ok = $this->mailer->send($row->email, $subject, $html, $fromKey); $ok ? $stats['sent']++ : $stats['failed']++; } $message = "Broadcast finished. Mode: {$stats['mode']}. Sent: {$stats['sent']}/{$stats['attempted']}. Failures: {$stats['failed']}."; $status = $stats['failed'] > 0 ? Response::HTTP_OK : Response::HTTP_OK; return $this->success(array_merge($stats, ['message' => $message]), 'Broadcast completed', $status); } public function uploadImage(): JsonResponse { if (!$this->request->is('post')) { return $this->respondError('Method not allowed', Response::HTTP_METHOD_NOT_ALLOWED); } $file = request()->file('image'); if (!$file || !$file->isValid()) { return $this->respondError('No image uploaded.', Response::HTTP_BAD_REQUEST); } $mime = strtolower((string) $file->getMimeType()); $allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp']; if (!isset($allowed[$mime])) { return $this->respondError('Unsupported image type.', Response::HTTP_UNSUPPORTED_MEDIA_TYPE); } if ($file->getSize() > 5 * 1024 * 1024) { return $this->respondError('Image too large (max 5MB).', Response::HTTP_REQUEST_ENTITY_TOO_LARGE); } $targetDir = public_path('uploads/email'); if (!is_dir($targetDir)) { @mkdir($targetDir, 0755, true); } $newName = uniqid('em_', true) . '.' . $allowed[$mime]; $file->move($targetDir, $newName); $url = url('uploads/email/' . $newName); return $this->success([ 'success' => true, 'url' => $url, ], 'Image uploaded successfully'); } private function composeEmailHtml( bool $wrap, string $subject, string $body, string $recipientName, string $preheader, string $ctaText, string $ctaUrl, bool $doPersonalize ): string { $content = $doPersonalize ? str_replace('{{name}}', $recipientName, $body) : $body; if (!$wrap) { return $content; } try { return view('emails/broadcast_wrapper', [ 'subject' => $subject, 'content' => $content, 'preheader' => $preheader, 'cta_text' => $ctaText, 'cta_url' => $ctaUrl, ], ['saveData' => false]); } catch (\Throwable $e) { Log::warning('broadcast_wrapper view missing: ' . $e->getMessage()); return $content; } } private function sanitizeEmailHtml(string $html): string { $html = preg_replace('#<(script|iframe)[^>]*>.*?#is', '', $html); $html = preg_replace('/\son\w+="[^"]*"/i', '', $html); $html = preg_replace("/\son\w+='[^']*'/i", '', $html); return $html; } private function senderOptionsFromEnv(): array { $json = env('MAIL_SENDERS', '{}'); $arr = json_decode($json, true); if (!is_array($arr) || empty($arr)) { $smtpUser = env('SMTP_USER', ''); $name = 'Al Rahma Sunday School'; return [[ 'key' => 'general', 'label' => trim($name . ($smtpUser ? " <{$smtpUser}>" : '')), ]]; } $out = []; foreach ($arr as $key => $info) { $nm = $info['name'] ?? 'Sender'; $em = $info['email'] ?? ''; $out[] = ['key' => (string) $key, 'label' => trim($nm . ($em ? " <{$em}>" : ''))]; } return $out; } private function parseParentIds($raw): array { $raw = is_array($raw) ? $raw : [$raw]; $ids = []; foreach ($raw as $value) { if (is_string($value) && str_contains($value, ',')) { $ids = array_merge($ids, array_map('intval', explode(',', $value))); } else { $ids[] = (int) $value; } } return array_values(array_unique(array_filter($ids))); } }