string|null]. * Throws on transport errors so the Job can retry. */ public function sendFromLog(EmailLog $log): array { // Resolve sending profile $profile = null; if ($log->mail_profile_id) { $profile = MailProfile::query()->find($log->mail_profile_id); } if (! $profile) { $profile = MailProfile::query()->where('active', true)->orderBy('priority')->orderBy('id')->first(); } $embed = $log->embed_mode ?: 'base64'; $subject = (string) $log->subject; $html = (string) optional($log->body)->body_html ?? ''; $text = (string) optional($log->body)->body_text ?? ''; // Inline CSS and handle images similarly to controller if ($html !== '') { if ($embed === 'base64') { try { $imageInliner = app(\App\Services\EmailImageInliner::class); $html = $imageInliner->inline($html); } catch (\Throwable $e) { } } else { // Best effort absolutize /storage URLs using app.url $base = (string) (config('app.asset_url') ?: config('app.url')); $host = $base !== '' ? rtrim($base, '/') : null; if ($host) { $html = preg_replace_callback('#]+)src=["\']([^"\']+)["\']([^>]*)>#i', function (array $m) use ($host) { $before = $m[1] ?? ''; $src = $m[2] ?? ''; $after = $m[3] ?? ''; $path = $src; if (preg_match('#^https?://#i', $src)) { $parts = parse_url($src); $path = $parts['path'] ?? ''; if (! preg_match('#^/?storage/#i', (string) $path)) { return $m[0]; } } else { if (! preg_match('#^/?storage/#i', (string) $path)) { return $m[0]; } } $rel = '/'.ltrim(preg_replace('#^/?storage/#i', 'storage/', (string) $path), '/'); $abs = rtrim($host, '/').$rel; return ''; }, $html); } } try { $inliner = new CssToInlineStyles; $html = $inliner->convert($html); } catch (\Throwable $e) { } } // Transport setup (Symfony Mailer preferred when profile exists) $messageId = null; if ($profile) { $host = $profile->host; $port = (int) ($profile->port ?: 587); $encryption = $profile->encryption ?: 'tls'; $username = $profile->username ?: ''; $password = (string) ($profile->decryptPassword() ?? ''); $scheme = $encryption === 'ssl' ? 'smtps' : 'smtp'; $query = $encryption === 'tls' ? '?encryption=tls' : ''; $dsn = sprintf('%s://%s:%s@%s:%d%s', $scheme, rawurlencode($username), rawurlencode($password), $host, $port, $query); $transport = Transport::fromDsn($dsn); $mailer = new SymfonyMailer($transport); // Resolve a valid From address without falling back to to_email $fromAddrCandidates = [ (string) ($log->from_email ?? ''), (string) ($profile->from_address ?? ''), (string) ($username ?? ''), (string) (config('mail.from.address') ?? ''), ]; $fromAddr = ''; foreach ($fromAddrCandidates as $cand) { $cand = trim($cand); if ($cand !== '' && filter_var($cand, FILTER_VALIDATE_EMAIL)) { $fromAddr = $cand; break; } } $fromName = (string) ($log->from_name ?: ($profile->from_name ?: (config('mail.from.name') ?? config('app.name') ?? ''))); // Fallback From if still empty: use a no-reply at app host if ($fromAddr === '') { $appUrl = (string) (config('app.url') ?? ''); $hostPart = ''; if ($appUrl !== '') { $parsed = @parse_url($appUrl); $hostPart = is_array($parsed) ? (string) ($parsed['host'] ?? '') : ''; } $domain = $hostPart !== '' ? $hostPart : 'localhost.localdomain'; $fromAddr = 'no-reply@'.$domain; } // Build email with safe Address instances (Symfony Address does not allow null name) $fromAddress = $fromName !== '' ? new Address($fromAddr, $fromName) : new Address($fromAddr); $email = (new Email) ->from($fromAddress) ->subject($subject); // Address recipients: prefer multi-recipient list; fall back to validated single address $toList = (array) ($log->to_recipients ?? []); $addresses = []; foreach ($toList as $addr) { $addr = trim((string) $addr); if ($addr !== '' && filter_var($addr, FILTER_VALIDATE_EMAIL)) { $addresses[] = new Address($addr); } } if (! empty($addresses)) { $email->to(...$addresses); } else { // Validate single to_email before using $singleTo = trim((string) $log->to_email); if ($singleTo === '' || ! filter_var($singleTo, FILTER_VALIDATE_EMAIL)) { throw new \InvalidArgumentException('No valid recipient email found for EmailLog #'.$log->id); } $email->to(new Address($singleTo, (string) ($log->to_name ?? ''))); } // Always BCC the sender mailbox if present and not already in To $senderBcc = null; if ($fromAddr !== '' && filter_var($fromAddr, FILTER_VALIDATE_EMAIL)) { // Check duplicates against toList $lowerTo = array_map(fn ($v) => strtolower(trim((string) $v)), (array) ($log->to_recipients ?? [$log->to_email])); if (! in_array(strtolower($fromAddr), $lowerTo, true)) { $senderBcc = $fromAddr; $email->bcc(new Address($senderBcc)); // Persist BCC for auditing $log->bcc = [$senderBcc]; } } if (! empty($text)) { $email->text($text); } if (! empty($html)) { $email->html($html); } if (! empty($log->reply_to)) { $email->replyTo($log->reply_to); } // Attach files if present on the log $attachments = (array) ($log->attachments ?? []); foreach ($attachments as $att) { try { $disk = $att['disk'] ?? 'public'; $path = $att['path'] ?? null; if (! $path) { continue; } $name = $att['name'] ?? basename($path); $mime = $att['mime'] ?? 'application/octet-stream'; $full = \Storage::disk($disk)->path($path); if (is_file($full)) { $email->attachFromPath($full, $name, $mime); } } catch (\Throwable $e) { // ignore individual attachment failures; continue sending } } $mailer->send($email); // Save log if we modified BCC if (! empty($log->getAttribute('bcc'))) { $log->save(); } $headers = $email->getHeaders(); $messageIdHeader = $headers->get('Message-ID'); $messageId = $messageIdHeader ? $messageIdHeader->getBodyAsString() : null; } else { // Fallback to Laravel mailer if (! empty($html)) { \Mail::html($html, function ($message) use ($log, $subject, $text) { $toList = (array) ($log->to_recipients ?? []); if (! empty($toList)) { $message->to($toList); } else { $singleTo = trim((string) $log->to_email); if ($singleTo === '' || ! filter_var($singleTo, FILTER_VALIDATE_EMAIL)) { throw new \InvalidArgumentException('No valid recipient email found for EmailLog #'.$log->id); } $toName = (string) ($log->to_name ?? ''); if ($toName !== '') { $message->to($singleTo, $toName); } else { $message->to($singleTo); } } // BCC the sender mailbox if resolvable and not already in To $fromAddr = (string) ($log->from_email ?: (config('mail.from.address') ?? '')); if ($fromAddr !== '' && filter_var($fromAddr, FILTER_VALIDATE_EMAIL)) { $lowerTo = array_map(fn ($v) => strtolower(trim((string) $v)), (array) ($log->to_recipients ?? [$log->to_email])); if (! in_array(strtolower($fromAddr), $lowerTo, true)) { $message->bcc($fromAddr); $log->bcc = [$fromAddr]; } } $message->subject($subject); if (! empty($log->reply_to)) { $message->replyTo($log->reply_to); } if (! empty($text)) { // Provide a plain text alternative when available $message->text($text); } }); } else { \Mail::raw($text ?: '', function ($message) use ($log, $subject) { $toList = (array) ($log->to_recipients ?? []); if (! empty($toList)) { $message->to($toList); } else { $singleTo = trim((string) $log->to_email); if ($singleTo === '' || ! filter_var($singleTo, FILTER_VALIDATE_EMAIL)) { throw new \InvalidArgumentException('No valid recipient email found for EmailLog #'.$log->id); } $toName = (string) ($log->to_name ?? ''); if ($toName !== '') { $message->to($singleTo, $toName); } else { $message->to($singleTo); } } // BCC the sender mailbox if resolvable and not already in To $fromAddr = (string) ($log->from_email ?: (config('mail.from.address') ?? '')); if ($fromAddr !== '' && filter_var($fromAddr, FILTER_VALIDATE_EMAIL)) { $lowerTo = array_map(fn ($v) => strtolower(trim((string) $v)), (array) ($log->to_recipients ?? [$log->to_email])); if (! in_array(strtolower($fromAddr), $lowerTo, true)) { $message->bcc($fromAddr); $log->bcc = [$fromAddr]; } } $message->subject($subject); if (! empty($log->reply_to)) { $message->replyTo($log->reply_to); } }); } } return ['message_id' => $messageId]; } }