mailing = $mailing; $this->fromBulkJob = $fromBulkJob; // Handle test mail mode (when second parameter is a locale string and third is boolean) if (is_string($recipientOrLocale) && is_bool($contentBlocksOrIsTest)) { $this->recipientLocale = $recipientOrLocale; $this->isTestMail = $contentBlocksOrIsTest; $this->recipient = null; $this->contentBlocks = $this->generateContentBlocksForLocale($this->recipientLocale); } else { // Normal mode (backward compatibility) $this->recipient = $recipientOrLocale; $this->recipientLocale = $locale ?: ($recipientOrLocale->lang_preference ?? timebank_config('base_language', 'en')); $this->isTestMail = false; $this->contentBlocks = $contentBlocksOrIsTest ?: $this->generateContentBlocks(); } } /** * Build the message. */ public function build() { // Set locale for the email App::setLocale($this->recipientLocale); $fromAddress = timebank_config("mailing.from_address.{$this->mailing->type}"); $subject = $this->mailing->getSubjectForLocale($this->recipientLocale); if ($this->isTestMail) { $subject = "[TEST - {$this->recipientLocale}] " . $subject; } $mail = $this ->from($fromAddress, config('app.name')) ->subject($subject) ->view('emails.newsletter.wrapper') ->with([ 'subject' => $subject, 'mailingTitle' => $this->mailing->title, 'locale' => $this->recipientLocale, 'contentBlocks' => $this->contentBlocks, 'unsubscribeUrl' => $this->generateUnsubscribeUrl(), 'isTestMail' => $this->isTestMail, ]); // Add bounce tracking headers $bounceEmail = config('mail.bounce_processing.bounce_address', config('mail.from.address')); if ($bounceEmail && !$this->isTestMail) { $mail->withSymfonyMessage(function ($message) use ($bounceEmail) { $headers = $message->getHeaders(); // Set Return-Path for bounce handling $headers->addPathHeader('Return-Path', $bounceEmail); // Add custom tracking headers $headers->addTextHeader('X-Mailing-ID', $this->mailing->id); $headers->addTextHeader('X-Recipient-Email', $this->recipient->email ?? ''); // Add envelope sender $message->returnPath($bounceEmail); }); } return $mail; } /** * Generate content blocks with proper translations */ protected function generateContentBlocks() { $blocks = []; foreach ($this->mailing->getSelectedPostsWithTranslations() as $post) { // Get translation for recipient's language with fallback $translation = $this->mailing->getPostTranslationForLocale($post->id, $this->recipientLocale); if (!$translation) { continue; // Skip posts without translations } // Determine post type for template selection $postType = $this->determinePostType($post); // Prepare post data for template $postData = $this->preparePostData($post, $translation); $blocks[] = [ 'type' => $postType, 'data' => $postData, 'template' => timebank_config("mailing.templates.{$postType}_block") ]; } return $blocks; } /** * Generate content blocks for a specific locale (test mode) */ protected function generateContentBlocksForLocale($locale) { $blocks = []; $contentBlocks = $this->mailing->getContentBlocksForLocale($locale); foreach ($contentBlocks as $block) { $post = \App\Models\Post::with(['translations', 'category'])->find($block['post_id']); if (!$post) { continue; } // Get translation for the specific locale $translation = $this->mailing->getPostTranslationForLocale($post->id, $locale); if (!$translation) { continue; // Skip posts without translations in this locale } // Determine post type for template selection $postType = $this->determinePostType($post); // Prepare post data for template $postData = $this->preparePostData($post, $translation); $blocks[] = [ 'type' => $postType, 'data' => $postData, 'template' => timebank_config("mailing.templates.{$postType}_block") ]; } return $blocks; } /** * Determine post type based on category or content */ protected function determinePostType($post) { // Check for ImagePost category type first if ($post->category && $post->category->type && str_starts_with($post->category->type, 'App\\Models\\ImagePost')) { return 'image'; } if ($post->category && $post->category->id) { // Map category IDs to post types - adjust based on your category structure $categoryMappings = [ 4 => 'news', // News category 5 => 'article', // Article category 6 => 'event', // Event category 7 => 'event', // Meeting category 8 => 'news', // General category ]; return $categoryMappings[$post->category->id] ?? 'news'; } // Check if post has meeting/event data if ($post->meeting || (isset($post->from) && $post->from)) { return 'event'; } return 'news'; // Default to news } /** * Prepare post data for email template */ protected function preparePostData($post, $translation) { // Generate fully localized URL with translated route path for recipient's language $url = LaravelLocalization::getURLFromRouteNameTranslated( $this->recipientLocale, 'routes.post.show_by_slug', ['slug' => $translation->slug] ); $data = [ 'title' => $translation->title, 'excerpt' => $translation->excerpt, 'content' => $translation->content, 'url' => $url, 'date' => $post->updated_at->locale($this->recipientLocale)->translatedFormat('M j, Y'), 'author' => $post->author ? $post->author->name : null, ]; // Add category information if ($post->category) { $categoryTranslation = $post->category->translations()->where('locale', $this->recipientLocale)->first(); $data['category'] = $categoryTranslation ? $categoryTranslation->name : $post->category->translations()->first()->name; } // Add location prefix for news (based on existing news-card-full logic) if ($post->category && $post->category->categoryable && $post->category->categoryable->translations) { $locationTranslation = $post->category->categoryable->translations->where('locale', $this->recipientLocale)->first(); if ($locationTranslation && $locationTranslation->name) { $data['location_prefix'] = strtoupper($locationTranslation->name); } } // Add event-specific data if ($post->meeting) { $data['venue'] = $post->meeting->venue; $data['address'] = $post->meeting->address; } // Add event date/time if available if ($translation->from) { $eventDate = \Carbon\Carbon::parse($translation->from); $data['event_date'] = $eventDate->locale($this->recipientLocale)->translatedFormat('F j'); $data['event_time'] = $eventDate->locale($this->recipientLocale)->translatedFormat('H:i'); } // Add image if available - use email conversion (resized without cropping) if ($post->getFirstMediaUrl('posts')) { $data['image'] = $post->getFirstMediaUrl('posts', 'email'); // Add media caption and owner for image posts $media = $post->getFirstMedia('posts'); if ($media) { $captionKey = 'caption-' . $this->recipientLocale; $data['media_caption'] = $media->getCustomProperty($captionKey, ''); $data['media_owner'] = $media->getCustomProperty('owner', ''); } } return $data; } /** * Generate unsubscribe URL for the recipient */ protected function generateUnsubscribeUrl() { // For test mails, return a placeholder URL if ($this->isTestMail || !$this->recipient) { return '#test-unsubscribe-link'; } // Create signed URL for unsubscribing from this mailing type return route('newsletter.unsubscribe', [ 'email' => $this->recipient->email, 'type' => $this->mailing->type, 'signature' => hash_hmac('sha256', $this->recipient->email . $this->mailing->type, config('app.key')) ]); } }