Initial commit

This commit is contained in:
Ronald Huynen
2026-03-23 21:37:59 +01:00
commit 2547717edb
2193 changed files with 972171 additions and 0 deletions

278
app/Mail/NewsletterMail.php Normal file
View File

@@ -0,0 +1,278 @@
<?php
namespace App\Mail;
use App\Models\Mailing;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\App;
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
class NewsletterMail extends Mailable
{
use Queueable, SerializesModels;
protected $mailing;
protected $recipient;
protected $contentBlocks;
protected $recipientLocale;
protected $isTestMail;
protected $fromBulkJob = false;
/**
* Create a new message instance.
*/
public function __construct(Mailing $mailing, $recipientOrLocale, $contentBlocksOrIsTest = null, $locale = null, $fromBulkJob = false)
{
$this->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'))
]);
}
}