Initial commit
This commit is contained in:
103
app/Mail/BounceAwareMailManager.php
Normal file
103
app/Mail/BounceAwareMailManager.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\MailingBounce;
|
||||
use Illuminate\Mail\MailManager;
|
||||
use Illuminate\Mail\PendingMail;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class BounceAwareMailManager extends MailManager
|
||||
{
|
||||
/**
|
||||
* Begin the process of mailing a mailable class instance with bounce checking
|
||||
*/
|
||||
public function to($users, $name = null)
|
||||
{
|
||||
$pendingMail = new BounceAwarePendingMail($this);
|
||||
return $pendingMail->to($users, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a mailable with bounce suppression checking
|
||||
*/
|
||||
public function send($view, array $data = [], $callback = null)
|
||||
{
|
||||
// If this is called directly with a mailable, we need to intercept
|
||||
if (is_object($view) && method_exists($view, 'build')) {
|
||||
return $this->sendWithBounceCheck($view);
|
||||
}
|
||||
|
||||
return parent::send($view, $data, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send mailable with bounce checking
|
||||
*/
|
||||
protected function sendWithBounceCheck($mailable)
|
||||
{
|
||||
// Extract recipient email addresses
|
||||
$recipients = $this->extractRecipients($mailable);
|
||||
|
||||
// Filter out suppressed emails
|
||||
$filteredRecipients = [];
|
||||
foreach ($recipients as $recipient) {
|
||||
$email = is_string($recipient) ? $recipient : ($recipient['address'] ?? $recipient->email ?? null);
|
||||
|
||||
if ($email && MailingBounce::isSuppressed($email)) {
|
||||
Log::info("Email sending blocked for suppressed address: {$email}", [
|
||||
'mailable_class' => get_class($mailable),
|
||||
'suppressed' => true
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$filteredRecipients[] = $recipient;
|
||||
}
|
||||
|
||||
// If all recipients are suppressed, don't send
|
||||
if (empty($filteredRecipients)) {
|
||||
Log::info("All recipients suppressed, skipping email send", [
|
||||
'mailable_class' => get_class($mailable),
|
||||
'original_recipients' => count($recipients),
|
||||
'suppressed_count' => count($recipients)
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update mailable recipients
|
||||
$this->updateMailableRecipients($mailable, $filteredRecipients);
|
||||
|
||||
// Add bounce tracking if the mailable supports it
|
||||
if (method_exists($mailable, 'configureBounceTracking') || in_array('App\Mail\Concerns\TracksBounces', class_uses_recursive($mailable))) {
|
||||
$mailable->configureBounceTracking = true;
|
||||
}
|
||||
|
||||
return parent::send($mailable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract recipient email addresses from mailable
|
||||
*/
|
||||
protected function extractRecipients($mailable): array
|
||||
{
|
||||
$recipients = [];
|
||||
|
||||
// Get recipients from the mailable's to property
|
||||
if (property_exists($mailable, 'to') && $mailable->to) {
|
||||
$recipients = array_merge($recipients, $mailable->to);
|
||||
}
|
||||
|
||||
return $recipients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update mailable recipients after filtering
|
||||
*/
|
||||
protected function updateMailableRecipients($mailable, array $filteredRecipients): void
|
||||
{
|
||||
if (property_exists($mailable, 'to')) {
|
||||
$mailable->to = $filteredRecipients;
|
||||
}
|
||||
}
|
||||
}
|
||||
130
app/Mail/BounceAwarePendingMail.php
Normal file
130
app/Mail/BounceAwarePendingMail.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\MailingBounce;
|
||||
use Illuminate\Mail\PendingMail;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class BounceAwarePendingMail extends PendingMail
|
||||
{
|
||||
/**
|
||||
* Send a new mailable message instance with bounce checking
|
||||
*/
|
||||
public function send($mailable)
|
||||
{
|
||||
// Filter out suppressed recipients before sending
|
||||
$this->filterSuppressedRecipients();
|
||||
|
||||
// If no valid recipients remain, don't send
|
||||
if (empty($this->to) && empty($this->cc) && empty($this->bcc)) {
|
||||
Log::info("All recipients suppressed, skipping email send", [
|
||||
'mailable_class' => get_class($mailable)
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add bounce tracking headers if the mailable supports it
|
||||
if (method_exists($mailable, 'configureBounceTracking') || $this->usesBounceTrackingTrait($mailable)) {
|
||||
$this->addBounceTrackingToMailable($mailable);
|
||||
}
|
||||
|
||||
return parent::send($mailable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue a new e-mail message for sending with bounce checking
|
||||
*/
|
||||
public function queue($mailable)
|
||||
{
|
||||
// Filter out suppressed recipients before queuing
|
||||
$this->filterSuppressedRecipients();
|
||||
|
||||
// If no valid recipients remain, don't queue
|
||||
if (empty($this->to) && empty($this->cc) && empty($this->bcc)) {
|
||||
Log::info("All recipients suppressed, skipping email queue", [
|
||||
'mailable_class' => get_class($mailable)
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add bounce tracking headers if the mailable supports it
|
||||
if (method_exists($mailable, 'configureBounceTracking') || $this->usesBounceTrackingTrait($mailable)) {
|
||||
$this->addBounceTrackingToMailable($mailable);
|
||||
}
|
||||
|
||||
return parent::queue($mailable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out suppressed email addresses from recipients
|
||||
*/
|
||||
protected function filterSuppressedRecipients(): void
|
||||
{
|
||||
$this->to = $this->filterRecipientList($this->to);
|
||||
$this->cc = $this->filterRecipientList($this->cc);
|
||||
$this->bcc = $this->filterRecipientList($this->bcc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter a specific recipient list
|
||||
*/
|
||||
protected function filterRecipientList(array $recipients): array
|
||||
{
|
||||
$filtered = [];
|
||||
|
||||
foreach ($recipients as $recipient) {
|
||||
$email = $this->extractEmailAddress($recipient);
|
||||
|
||||
if ($email && MailingBounce::isSuppressed($email)) {
|
||||
Log::info("Email sending blocked for suppressed address: {$email}", [
|
||||
'suppressed' => true
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$filtered[] = $recipient;
|
||||
}
|
||||
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract email address from recipient
|
||||
*/
|
||||
protected function extractEmailAddress($recipient): ?string
|
||||
{
|
||||
if (is_string($recipient)) {
|
||||
return $recipient;
|
||||
}
|
||||
|
||||
if (is_array($recipient)) {
|
||||
return $recipient['address'] ?? null;
|
||||
}
|
||||
|
||||
if (is_object($recipient)) {
|
||||
return $recipient->email ?? null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if mailable uses the bounce tracking trait
|
||||
*/
|
||||
protected function usesBounceTrackingTrait($mailable): bool
|
||||
{
|
||||
return in_array('App\Mail\Concerns\TracksBounces', class_uses_recursive($mailable));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add bounce tracking to mailable
|
||||
*/
|
||||
protected function addBounceTrackingToMailable($mailable): void
|
||||
{
|
||||
// Mark that bounce tracking should be enabled
|
||||
if (property_exists($mailable, 'bounceTrackingEnabled')) {
|
||||
$mailable->bounceTrackingEnabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
app/Mail/BounceTrackingMailable.php
Normal file
29
app/Mail/BounceTrackingMailable.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Mail\Concerns\TracksBounces;
|
||||
use Illuminate\Mail\Mailable;
|
||||
|
||||
abstract class BounceTrackingMailable extends Mailable
|
||||
{
|
||||
use TracksBounces;
|
||||
|
||||
/**
|
||||
* Build the message with bounce tracking
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
$message = $this->buildMessage();
|
||||
|
||||
// Add bounce tracking headers
|
||||
$this->configureBounceTracking($message);
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract method that child classes must implement instead of build()
|
||||
*/
|
||||
abstract protected function buildMessage();
|
||||
}
|
||||
54
app/Mail/CallBlockedMail.php
Normal file
54
app/Mail/CallBlockedMail.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\Call;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class CallBlockedMail extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $call;
|
||||
public $callable;
|
||||
public string $callableType;
|
||||
public string $mailLocale;
|
||||
public string $callTitle;
|
||||
public string $supportEmail;
|
||||
|
||||
public function __construct(Call $call, $callable, string $callableType)
|
||||
{
|
||||
$this->call = $call;
|
||||
$this->callable = $callable;
|
||||
$this->callableType = $callableType;
|
||||
|
||||
$this->mailLocale = $callable->lang_preference ?? config('app.fallback_locale', 'en');
|
||||
$this->locale($this->mailLocale);
|
||||
$this->callTitle = $call->tag?->name ?? '';
|
||||
$this->supportEmail = timebank_config('mail.support.email', '');
|
||||
}
|
||||
|
||||
public function build()
|
||||
{
|
||||
app()->setLocale($this->mailLocale);
|
||||
|
||||
$callTitle = $this->call->tag?->translation?->name ?? $this->call->tag?->name ?? '';
|
||||
|
||||
return $this
|
||||
->from(
|
||||
timebank_config('mail.system_admin.email'),
|
||||
timebank_config('mail.system_admin.name')
|
||||
)
|
||||
->subject(trans('Your call has been blocked', [], $this->mailLocale))
|
||||
->view('emails.calls.blocked')
|
||||
->with([
|
||||
'callable' => $this->callable,
|
||||
'call' => $this->call,
|
||||
'callTitle' => $callTitle,
|
||||
'supportEmail' => $this->supportEmail,
|
||||
]);
|
||||
}
|
||||
}
|
||||
65
app/Mail/CallExpiredMail.php
Normal file
65
app/Mail/CallExpiredMail.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\Call;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
|
||||
|
||||
class CallExpiredMail extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $call;
|
||||
public $callable;
|
||||
public string $callableType;
|
||||
public string $mailLocale;
|
||||
public string $loginUrl;
|
||||
public string $callTitle;
|
||||
|
||||
public function __construct(Call $call, $callable, string $callableType)
|
||||
{
|
||||
$this->call = $call;
|
||||
$this->callable = $callable;
|
||||
$this->callableType = $callableType;
|
||||
|
||||
$this->mailLocale = $callable->lang_preference ?? config('app.fallback_locale', 'en');
|
||||
$this->locale($this->mailLocale);
|
||||
// callTitle is resolved in build() after locale is set
|
||||
$this->callTitle = $call->tag?->name ?? '';
|
||||
|
||||
$callsRoute = LaravelLocalization::getURLFromRouteNameTranslated($this->mailLocale, 'routes.calls.manage');
|
||||
|
||||
$base = match ($callableType) {
|
||||
'Organization' => route('organization.direct-login', ['organizationId' => $callable->id, 'intended' => $callsRoute]),
|
||||
'Bank' => route('bank.direct-login', ['bankId' => $callable->id, 'intended' => $callsRoute]),
|
||||
default => route('user.direct-login', ['userId' => $callable->id, 'intended' => $callsRoute]),
|
||||
};
|
||||
|
||||
$this->loginUrl = LaravelLocalization::localizeURL($base, $this->mailLocale);
|
||||
}
|
||||
|
||||
public function build()
|
||||
{
|
||||
app()->setLocale($this->mailLocale);
|
||||
|
||||
$callTitle = $this->call->tag?->translation?->name ?? $this->call->tag?->name ?? '';
|
||||
|
||||
return $this
|
||||
->from(
|
||||
timebank_config('mail.system_admin.email'),
|
||||
timebank_config('mail.system_admin.name')
|
||||
)
|
||||
->subject(trans('Your call has expired', [], $this->mailLocale))
|
||||
->view('emails.calls.expired')
|
||||
->with([
|
||||
'callable' => $this->callable,
|
||||
'call' => $this->call,
|
||||
'callTitle' => $callTitle,
|
||||
'loginUrl' => $this->loginUrl,
|
||||
]);
|
||||
}
|
||||
}
|
||||
67
app/Mail/CallExpiringMail.php
Normal file
67
app/Mail/CallExpiringMail.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\Call;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
|
||||
|
||||
class CallExpiringMail extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $call;
|
||||
public $callable;
|
||||
public string $callableType;
|
||||
public int $daysRemaining;
|
||||
public string $mailLocale;
|
||||
public string $loginUrl;
|
||||
public string $callTitle;
|
||||
|
||||
public function __construct(Call $call, $callable, string $callableType, int $daysRemaining)
|
||||
{
|
||||
$this->call = $call;
|
||||
$this->callable = $callable;
|
||||
$this->callableType = $callableType;
|
||||
$this->daysRemaining = $daysRemaining;
|
||||
|
||||
$this->mailLocale = $callable->lang_preference ?? config('app.fallback_locale', 'en');
|
||||
$this->locale($this->mailLocale);
|
||||
$this->callTitle = $call->tag?->name ?? '';
|
||||
|
||||
$callsRoute = LaravelLocalization::getURLFromRouteNameTranslated($this->mailLocale, 'routes.calls.manage');
|
||||
|
||||
$base = match ($callableType) {
|
||||
'Organization' => route('organization.direct-login', ['organizationId' => $callable->id, 'intended' => $callsRoute]),
|
||||
'Bank' => route('bank.direct-login', ['bankId' => $callable->id, 'intended' => $callsRoute]),
|
||||
default => route('user.direct-login', ['userId' => $callable->id, 'intended' => $callsRoute]),
|
||||
};
|
||||
|
||||
$this->loginUrl = LaravelLocalization::localizeURL($base, $this->mailLocale);
|
||||
}
|
||||
|
||||
public function build()
|
||||
{
|
||||
app()->setLocale($this->mailLocale);
|
||||
|
||||
$callTitle = $this->call->tag?->translation?->name ?? $this->call->tag?->name ?? '';
|
||||
|
||||
return $this
|
||||
->from(
|
||||
timebank_config('mail.system_admin.email'),
|
||||
timebank_config('mail.system_admin.name')
|
||||
)
|
||||
->subject(trans('Your call expires in :days days', ['days' => $this->daysRemaining], $this->mailLocale))
|
||||
->view('emails.calls.expiring')
|
||||
->with([
|
||||
'callable' => $this->callable,
|
||||
'call' => $this->call,
|
||||
'callTitle' => $callTitle,
|
||||
'daysRemaining' => $this->daysRemaining,
|
||||
'loginUrl' => $this->loginUrl,
|
||||
]);
|
||||
}
|
||||
}
|
||||
77
app/Mail/Concerns/TracksBounces.php
Normal file
77
app/Mail/Concerns/TracksBounces.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail\Concerns;
|
||||
|
||||
use App\Models\MailingBounce;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
trait TracksBounces
|
||||
{
|
||||
/**
|
||||
* Configure bounce tracking headers for this mailable
|
||||
*/
|
||||
protected function configureBounceTracking($message)
|
||||
{
|
||||
$bounceEmail = timebank_config('mailing.bounce_address', config('mail.from.address'));
|
||||
|
||||
$message->withSymfonyMessage(function (Email $email) use ($bounceEmail) {
|
||||
$headers = $email->getHeaders();
|
||||
|
||||
// Add Return-Path for bounce routing
|
||||
$headers->addPathHeader('Return-Path', $bounceEmail);
|
||||
|
||||
// Add custom headers for tracking
|
||||
$headers->addTextHeader('X-Bounce-Tracking', 'enabled');
|
||||
$headers->addTextHeader('X-Mailable-Class', static::class);
|
||||
|
||||
// Set Return-Path on the email object itself
|
||||
$email->returnPath($bounceEmail);
|
||||
});
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the recipient email is suppressed before sending
|
||||
*/
|
||||
protected function checkSuppressionBeforeSending(string $email): bool
|
||||
{
|
||||
if (MailingBounce::isSuppressed($email)) {
|
||||
Log::info("Email sending blocked for suppressed address: {$email}", [
|
||||
'mailable_class' => static::class,
|
||||
'suppressed' => true
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the recipient email address from the mailable
|
||||
* Override this method in your mailable if the email is not in a standard location
|
||||
*/
|
||||
protected function getRecipientEmail(): ?string
|
||||
{
|
||||
// Try to extract email from common properties
|
||||
if (isset($this->to) && is_array($this->to) && count($this->to) > 0) {
|
||||
return $this->to[0]['address'] ?? null;
|
||||
}
|
||||
|
||||
// Try other common patterns
|
||||
if (property_exists($this, 'recipient') && $this->recipient) {
|
||||
return is_string($this->recipient) ? $this->recipient : ($this->recipient->email ?? null);
|
||||
}
|
||||
|
||||
if (property_exists($this, 'user') && $this->user) {
|
||||
return $this->user->email ?? null;
|
||||
}
|
||||
|
||||
if (property_exists($this, 'email') && is_string($this->email)) {
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
68
app/Mail/ContactFormCopyMailable.php
Normal file
68
app/Mail/ContactFormCopyMailable.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
|
||||
|
||||
class ContactFormCopyMailable extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $contact;
|
||||
public $locale;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($contact)
|
||||
{
|
||||
$this->contact = $contact;
|
||||
|
||||
// Determine locale from profile lang_preference or browser locale
|
||||
if (!empty($contact['profile_lang_preference'])) {
|
||||
$this->locale = $contact['profile_lang_preference'];
|
||||
} elseif (!empty($contact['browser_locale'])) {
|
||||
$this->locale = $contact['browser_locale'];
|
||||
} else {
|
||||
$this->locale = config('app.fallback_locale');
|
||||
}
|
||||
|
||||
// Ensure locale is not empty
|
||||
if (empty($this->locale)) {
|
||||
$this->locale = config('app.fallback_locale');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
// Set the application locale for this email
|
||||
app()->setLocale($this->locale);
|
||||
|
||||
$context = $this->contact['context'] ?? 'contact';
|
||||
|
||||
$subject = match($context) {
|
||||
'report-issue' => __('Copy: Issue Report') . ' - ' . ($this->contact['subject'] ?? __('No subject')),
|
||||
'report-error' => __('Copy: Error Report'),
|
||||
'delete-profile' => __('Copy: Profile Deletion Request'),
|
||||
default => __('Copy: Contact Form') . ' - ' . ($this->contact['subject'] ?? __('Your Message')),
|
||||
};
|
||||
|
||||
return $this->from(timebank_config('mail.support.email'), timebank_config('mail.support.name'))
|
||||
->subject($subject)
|
||||
->view('emails.contact-form.copy')
|
||||
->with([
|
||||
'contact' => $this->contact,
|
||||
]);
|
||||
}
|
||||
}
|
||||
69
app/Mail/ContactFormMailable.php
Normal file
69
app/Mail/ContactFormMailable.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
|
||||
|
||||
class ContactFormMailable extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $contact;
|
||||
public $locale;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($contact)
|
||||
{
|
||||
$this->contact = $contact;
|
||||
|
||||
// Determine locale from profile lang_preference or browser locale
|
||||
if (!empty($contact['profile_lang_preference'])) {
|
||||
$this->locale = $contact['profile_lang_preference'];
|
||||
} elseif (!empty($contact['browser_locale'])) {
|
||||
$this->locale = $contact['browser_locale'];
|
||||
} else {
|
||||
$this->locale = config('app.fallback_locale');
|
||||
}
|
||||
|
||||
// Ensure locale is not empty
|
||||
if (empty($this->locale)) {
|
||||
$this->locale = config('app.fallback_locale');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
// Set the application locale for this email
|
||||
app()->setLocale($this->locale);
|
||||
|
||||
$context = $this->contact['context'] ?? 'contact';
|
||||
|
||||
$subject = match($context) {
|
||||
'report-issue' => __('Issue Report') . ': ' . ($this->contact['subject'] ?? __('No subject')),
|
||||
'report-error' => __('Error Report from') . ' ' . $this->contact['full_name'],
|
||||
'delete-profile' => __('Profile Deletion Request from') . ' ' . $this->contact['name'],
|
||||
default => __('Contact Form') . ': ' . ($this->contact['subject'] ?? __('Message from') . ' ' . $this->contact['full_name']),
|
||||
};
|
||||
|
||||
return $this->from(timebank_config('mail.support.email'), timebank_config('mail.support.name'))
|
||||
->replyTo($this->contact['email'], $this->contact['full_name'])
|
||||
->subject($subject)
|
||||
->view('emails.contact-form.email')
|
||||
->with([
|
||||
'contact' => $this->contact,
|
||||
]);
|
||||
}
|
||||
}
|
||||
90
app/Mail/InactiveProfileWarning1Mail.php
Normal file
90
app/Mail/InactiveProfileWarning1Mail.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class InactiveProfileWarning1Mail extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $profile;
|
||||
public $profileType;
|
||||
public $timeRemaining;
|
||||
public $daysRemaining;
|
||||
public $accounts;
|
||||
public $totalBalance;
|
||||
public $daysSinceLogin;
|
||||
public $locale;
|
||||
public $loginUrl;
|
||||
|
||||
public function __construct($profile, $profileType, $timeRemaining, $daysRemaining, $accounts, $totalBalance, $daysSinceLogin)
|
||||
{
|
||||
$this->profile = $profile;
|
||||
$this->profileType = $profileType;
|
||||
$this->timeRemaining = $timeRemaining;
|
||||
$this->daysRemaining = $daysRemaining;
|
||||
$this->accounts = $accounts;
|
||||
$this->totalBalance = $totalBalance;
|
||||
$this->daysSinceLogin = $daysSinceLogin;
|
||||
|
||||
// Set locale from profile preference
|
||||
$this->locale = $profile->lang_preference ?? config('app.fallback_locale', 'en');
|
||||
|
||||
// Generate direct login URL
|
||||
if ($profileType === 'User') {
|
||||
$this->loginUrl = route('user.direct-login', [
|
||||
'userId' => $profile->id,
|
||||
'name' => $profile->name
|
||||
]);
|
||||
} elseif ($profileType === 'Organization') {
|
||||
$this->loginUrl = route('organization.direct-login', [
|
||||
'organizationId' => $profile->id
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: trans('Warning: Your profile will be deleted soon', [], $this->locale),
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: 'emails.inactive-profiles.warning-1',
|
||||
);
|
||||
}
|
||||
|
||||
public function build()
|
||||
{
|
||||
// Set application locale for this email
|
||||
app()->setLocale($this->locale);
|
||||
|
||||
return $this
|
||||
->from(
|
||||
timebank_config('mail.system_admin.email'),
|
||||
timebank_config('mail.system_admin.name')
|
||||
)
|
||||
->subject(trans('Warning: Your profile will be deleted soon', [], $this->locale))
|
||||
->view('emails.inactive-profiles.warning-1')
|
||||
->with([
|
||||
'profile' => $this->profile,
|
||||
'profileType' => $this->profileType,
|
||||
'timeRemaining' => $this->timeRemaining,
|
||||
'daysRemaining' => $this->daysRemaining,
|
||||
'accounts' => $this->accounts,
|
||||
'totalBalance' => $this->totalBalance,
|
||||
'daysSinceLogin' => $this->daysSinceLogin,
|
||||
'loginUrl' => $this->loginUrl,
|
||||
'totalInactiveDays' => timebank_config('profile_inactive.days_not_logged_in') + timebank_config('delete_profile.days_after_inactive.run_delete'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
90
app/Mail/InactiveProfileWarning2Mail.php
Normal file
90
app/Mail/InactiveProfileWarning2Mail.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
||||
class InactiveProfileWarning2Mail extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $profile;
|
||||
public $profileType;
|
||||
public $timeRemaining;
|
||||
public $daysRemaining;
|
||||
public $accounts;
|
||||
public $totalBalance;
|
||||
public $daysSinceLogin;
|
||||
public $locale;
|
||||
public $loginUrl;
|
||||
|
||||
public function __construct($profile, $profileType, $timeRemaining, $daysRemaining, $accounts, $totalBalance, $daysSinceLogin)
|
||||
{
|
||||
$this->profile = $profile;
|
||||
$this->profileType = $profileType;
|
||||
$this->timeRemaining = $timeRemaining;
|
||||
$this->daysRemaining = $daysRemaining;
|
||||
$this->accounts = $accounts;
|
||||
$this->totalBalance = $totalBalance;
|
||||
$this->daysSinceLogin = $daysSinceLogin;
|
||||
|
||||
// Set locale from profile preference
|
||||
$this->locale = $profile->lang_preference ?? config('app.fallback_locale', 'en');
|
||||
|
||||
// Generate direct login URL
|
||||
if ($profileType === 'User') {
|
||||
$this->loginUrl = route('user.direct-login', [
|
||||
'userId' => $profile->id,
|
||||
'name' => $profile->name
|
||||
]);
|
||||
} elseif ($profileType === 'Organization') {
|
||||
$this->loginUrl = route('organization.direct-login', [
|
||||
'organizationId' => $profile->id
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: trans('Urgent: Your profile will be deleted soon', [], $this->locale),
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: 'emails.inactive-profiles.warning-2',
|
||||
);
|
||||
}
|
||||
|
||||
public function build()
|
||||
{
|
||||
// Set application locale for this email
|
||||
app()->setLocale($this->locale);
|
||||
|
||||
return $this
|
||||
->from(
|
||||
timebank_config('mail.system_admin.email'),
|
||||
timebank_config('mail.system_admin.name')
|
||||
)
|
||||
->subject(trans('Urgent: Your profile will be deleted soon', [], $this->locale))
|
||||
->view('emails.inactive-profiles.warning-2')
|
||||
->with([
|
||||
'profile' => $this->profile,
|
||||
'profileType' => $this->profileType,
|
||||
'timeRemaining' => $this->timeRemaining,
|
||||
'daysRemaining' => $this->daysRemaining,
|
||||
'accounts' => $this->accounts,
|
||||
'totalBalance' => $this->totalBalance,
|
||||
'daysSinceLogin' => $this->daysSinceLogin,
|
||||
'loginUrl' => $this->loginUrl,
|
||||
'totalInactiveDays' => timebank_config('profile_inactive.days_not_logged_in') + timebank_config('delete_profile.days_after_inactive.run_delete'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
90
app/Mail/InactiveProfileWarningFinalMail.php
Normal file
90
app/Mail/InactiveProfileWarningFinalMail.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
||||
class InactiveProfileWarningFinalMail extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $profile;
|
||||
public $profileType;
|
||||
public $timeRemaining;
|
||||
public $daysRemaining;
|
||||
public $accounts;
|
||||
public $totalBalance;
|
||||
public $daysSinceLogin;
|
||||
public $locale;
|
||||
public $loginUrl;
|
||||
|
||||
public function __construct($profile, $profileType, $timeRemaining, $daysRemaining, $accounts, $totalBalance, $daysSinceLogin)
|
||||
{
|
||||
$this->profile = $profile;
|
||||
$this->profileType = $profileType;
|
||||
$this->timeRemaining = $timeRemaining;
|
||||
$this->daysRemaining = $daysRemaining;
|
||||
$this->accounts = $accounts;
|
||||
$this->totalBalance = $totalBalance;
|
||||
$this->daysSinceLogin = $daysSinceLogin;
|
||||
|
||||
// Set locale from profile preference
|
||||
$this->locale = $profile->lang_preference ?? config('app.fallback_locale', 'en');
|
||||
|
||||
// Generate direct login URL
|
||||
if ($profileType === 'User') {
|
||||
$this->loginUrl = route('user.direct-login', [
|
||||
'userId' => $profile->id,
|
||||
'name' => $profile->name
|
||||
]);
|
||||
} elseif ($profileType === 'Organization') {
|
||||
$this->loginUrl = route('organization.direct-login', [
|
||||
'organizationId' => $profile->id
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: trans('Final warning: Your profile will be deleted very soon', [], $this->locale),
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: 'emails.inactive-profiles.warning-final',
|
||||
);
|
||||
}
|
||||
|
||||
public function build()
|
||||
{
|
||||
// Set application locale for this email
|
||||
app()->setLocale($this->locale);
|
||||
|
||||
return $this
|
||||
->from(
|
||||
timebank_config('mail.system_admin.email'),
|
||||
timebank_config('mail.system_admin.name')
|
||||
)
|
||||
->subject(trans('Final warning: Your profile will be deleted very soon', [], $this->locale))
|
||||
->view('emails.inactive-profiles.warning-final')
|
||||
->with([
|
||||
'profile' => $this->profile,
|
||||
'profileType' => $this->profileType,
|
||||
'timeRemaining' => $this->timeRemaining,
|
||||
'daysRemaining' => $this->daysRemaining,
|
||||
'accounts' => $this->accounts,
|
||||
'totalBalance' => $this->totalBalance,
|
||||
'daysSinceLogin' => $this->daysSinceLogin,
|
||||
'loginUrl' => $this->loginUrl,
|
||||
'totalInactiveDays' => timebank_config('profile_inactive.days_not_logged_in') + timebank_config('delete_profile.days_after_inactive.run_delete'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
27
app/Mail/MailpitCopyListener.php
Normal file
27
app/Mail/MailpitCopyListener.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Mail\Events\MessageSending;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class MailpitCopyListener
|
||||
{
|
||||
public function handle(MessageSending $event): void
|
||||
{
|
||||
if (!timebank_config('mailing.copy_to_mailpit', false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$host = timebank_config('mailing.mailpit_host', 'localhost');
|
||||
$port = timebank_config('mailing.mailpit_port', 1025);
|
||||
|
||||
$transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport($host, $port, false);
|
||||
$transport->send($event->message);
|
||||
} catch (\Throwable $e) {
|
||||
// Never let mailpit copy failure break the real email
|
||||
Log::warning('Mailpit copy failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
128
app/Mail/NewMessageMail.php
Normal file
128
app/Mail/NewMessageMail.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
|
||||
use Namu\WireChat\Events\MessageCreated;
|
||||
use Namu\WireChat\Models\Group;
|
||||
|
||||
class NewMessageMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $event;
|
||||
public $sender;
|
||||
public $recipient;
|
||||
public $language;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @param MessageCreated $event
|
||||
* @param mixed $sender
|
||||
* @param mixed $recipient
|
||||
* @param string $language
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(MessageCreated $event, $sender, $recipient, $language = null)
|
||||
{
|
||||
$this->event = $event;
|
||||
$this->sender = $sender;
|
||||
$this->recipient = $recipient;
|
||||
$this->language = $language ?? $recipient->lang_preference ?? config('app.locale', 'en');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*
|
||||
* @return \Illuminate\Mail\Mailables\Envelope
|
||||
*/
|
||||
public function envelope()
|
||||
{
|
||||
return new Envelope(
|
||||
from: new \Illuminate\Mail\Mailables\Address(timebank_config('mail.chat_messenger.email'), timebank_config('mail.chat_messenger.name')),
|
||||
subject: trans('mail.new_message_subject', [], $this->language),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*
|
||||
* @return \Illuminate\Mail\Mailables\Content
|
||||
*/
|
||||
public function content()
|
||||
{
|
||||
// Set application locale for this email
|
||||
app()->setLocale($this->language);
|
||||
|
||||
$language = $this->recipient->lang_preference ?? config('app.fallback_locale', 'en');
|
||||
|
||||
Log::info('Content generated', [
|
||||
'recipient_id' => $this->recipient->id,
|
||||
'recipient_type' => get_class($this->recipient),
|
||||
'language' => $language,
|
||||
'recipient_lang_preference' => $this->recipient->lang_preference,
|
||||
]);
|
||||
|
||||
// Ensure conversation is loaded
|
||||
if (!$this->event->message->relationLoaded('conversation')) {
|
||||
$this->event->message->load('conversation.participants');
|
||||
}
|
||||
|
||||
// Prepare data for the view
|
||||
$conversationId = $this->event->message->conversation->id;
|
||||
|
||||
// Generate appropriate URL based on recipient type
|
||||
// For organizations, banks, and admins, use direct login URL
|
||||
$chatRoute = route('chat', ['conversation' => $conversationId]);
|
||||
|
||||
if ($this->recipient instanceof \App\Models\Organization) {
|
||||
$baseUrl = route('organization.direct-login', [
|
||||
'organizationId' => $this->recipient->id,
|
||||
'intended' => $chatRoute
|
||||
]);
|
||||
$conversationUrl = LaravelLocalization::localizeURL($baseUrl, $this->language);
|
||||
} elseif ($this->recipient instanceof \App\Models\Bank) {
|
||||
$baseUrl = route('bank.direct-login', [
|
||||
'bankId' => $this->recipient->id,
|
||||
'intended' => $chatRoute
|
||||
]);
|
||||
$conversationUrl = LaravelLocalization::localizeURL($baseUrl, $this->language);
|
||||
} elseif ($this->recipient instanceof \App\Models\Admin) {
|
||||
$baseUrl = route('admin.direct-login', [
|
||||
'adminId' => $this->recipient->id,
|
||||
'intended' => $chatRoute
|
||||
]);
|
||||
$conversationUrl = LaravelLocalization::localizeURL($baseUrl, $this->language);
|
||||
} else {
|
||||
// For regular users, just use the chat URL
|
||||
$conversationUrl = LaravelLocalization::localizeURL($chatRoute, $this->language);
|
||||
}
|
||||
// Check if it's a group conversation
|
||||
$isGroupChat = $this->event->message->conversation->participants()->count() > 2;
|
||||
$group = $isGroupChat ? Group::where('conversation_id', $conversationId)->first() : null;
|
||||
$groupName = $isGroupChat
|
||||
? ($group && !empty($group->name)
|
||||
? $group->name
|
||||
: trans('mail.group_conversation', [], $this->language))
|
||||
: null;
|
||||
|
||||
return new Content(
|
||||
markdown: 'emails.messages.new',
|
||||
with: [
|
||||
'senderName' => $this->sender->full_name ?? $this->sender->name,
|
||||
'recipientName' => $this->recipient->full_name ?? $this->recipient->name,
|
||||
'messageContent' => $this->event->message->body,
|
||||
'conversationUrl' => $conversationUrl,
|
||||
'isGroupChat' => $isGroupChat,
|
||||
'groupName' => $groupName,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
278
app/Mail/NewsletterMail.php
Normal file
278
app/Mail/NewsletterMail.php
Normal 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'))
|
||||
]);
|
||||
}
|
||||
}
|
||||
207
app/Mail/ProfileEditedByAdminMail.php
Normal file
207
app/Mail/ProfileEditedByAdminMail.php
Normal file
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
|
||||
|
||||
class ProfileEditedByAdminMail extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
protected $profile;
|
||||
protected $changedFields;
|
||||
protected $buttonUrl;
|
||||
public $locale;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @param mixed $profile The profile that was edited
|
||||
* @param array $changedFields Array of field names that were changed
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($profile, $changedFields = [])
|
||||
{
|
||||
$this->profile = $profile;
|
||||
|
||||
// Normalize changedFields: support both ['field1', 'field2'] and ['field1' => ['old' => 'x', 'new' => 'y']]
|
||||
$normalizedFields = [];
|
||||
foreach ($changedFields as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
// Format: ['field1' => ['old' => 'x', 'new' => 'y']]
|
||||
$normalizedFields[] = $key;
|
||||
} else {
|
||||
// Format: ['field1', 'field2']
|
||||
$normalizedFields[] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out 'comment' field as it's admin-only
|
||||
$this->changedFields = array_filter($normalizedFields, function($field) {
|
||||
return $field !== 'comment';
|
||||
});
|
||||
|
||||
Log::info('ProfileEditedByAdminMail: Constructor called', [
|
||||
'profile_id' => $profile->id,
|
||||
'profile_type' => get_class($profile),
|
||||
'profile_lang_preference' => $profile->lang_preference ?? 'not set',
|
||||
'changed_fields' => $changedFields,
|
||||
]);
|
||||
|
||||
// Support lang_preference for User, Organization, Bank, and Admin models
|
||||
$this->locale = $profile->lang_preference ?? config('app.fallback_locale');
|
||||
|
||||
// Ensure locale is not empty
|
||||
if (empty($this->locale)) {
|
||||
$this->locale = config('app.fallback_locale');
|
||||
}
|
||||
|
||||
Log::info('ProfileEditedByAdminMail: Locale determined', [
|
||||
'locale' => $this->locale,
|
||||
]);
|
||||
|
||||
// Generate the appropriate login URL based on profile type
|
||||
$profileClass = get_class($profile);
|
||||
|
||||
if ($profileClass === 'App\\Models\\User') {
|
||||
// Get the localized profile.edit URL for this profile's language
|
||||
$profileEditPath = LaravelLocalization::getURLFromRouteNameTranslated(
|
||||
$this->locale,
|
||||
'routes.profile.edit'
|
||||
);
|
||||
// Convert to absolute URL
|
||||
$profileEditUrl = url($profileEditPath);
|
||||
|
||||
// Direct user login with redirect to profile.edit and username pre-filled
|
||||
$this->buttonUrl = LaravelLocalization::localizeURL(
|
||||
route('user.direct-login', [
|
||||
'userId' => $profile->id,
|
||||
'intended' => $profileEditUrl,
|
||||
'name' => $profile->name
|
||||
]),
|
||||
$this->locale
|
||||
);
|
||||
} elseif ($profileClass === 'App\\Models\\Organization') {
|
||||
// Get the localized profile.edit URL for this profile's language
|
||||
$profileEditPath = LaravelLocalization::getURLFromRouteNameTranslated(
|
||||
$this->locale,
|
||||
'routes.profile.edit'
|
||||
);
|
||||
// Convert to absolute URL
|
||||
$profileEditUrl = url($profileEditPath);
|
||||
|
||||
// Direct organization login with redirect to profile.edit
|
||||
$this->buttonUrl = LaravelLocalization::localizeURL(
|
||||
route('organization.direct-login', ['organizationId' => $profile->id, 'intended' => $profileEditUrl]),
|
||||
$this->locale
|
||||
);
|
||||
} elseif ($profileClass === 'App\\Models\\Bank') {
|
||||
// Get the localized profile.edit URL for this profile's language
|
||||
$profileEditPath = LaravelLocalization::getURLFromRouteNameTranslated(
|
||||
$this->locale,
|
||||
'routes.profile.edit'
|
||||
);
|
||||
// Convert to absolute URL
|
||||
$profileEditUrl = url($profileEditPath);
|
||||
|
||||
// Direct bank login with redirect to profile.edit
|
||||
$this->buttonUrl = LaravelLocalization::localizeURL(
|
||||
route('bank.direct-login', ['bankId' => $profile->id, 'intended' => $profileEditUrl]),
|
||||
$this->locale
|
||||
);
|
||||
} elseif ($profileClass === 'App\\Models\\Admin') {
|
||||
// Get the localized profile.settings URL for admin profiles
|
||||
$profileSettingsPath = LaravelLocalization::getURLFromRouteNameTranslated(
|
||||
$this->locale,
|
||||
'routes.profile.settings'
|
||||
);
|
||||
// Convert to absolute URL
|
||||
$profileSettingsUrl = url($profileSettingsPath);
|
||||
|
||||
// Direct admin login with redirect to profile.settings
|
||||
$this->buttonUrl = LaravelLocalization::localizeURL(
|
||||
route('admin.direct-login', ['adminId' => $profile->id, 'intended' => $profileSettingsUrl]),
|
||||
$this->locale
|
||||
);
|
||||
} else {
|
||||
// Fallback to main login
|
||||
$this->buttonUrl = LaravelLocalization::localizeURL(
|
||||
route('login'),
|
||||
$this->locale
|
||||
);
|
||||
}
|
||||
|
||||
Log::info('ProfileEditedByAdminMail: Generated button URL', [
|
||||
'button_url' => $this->buttonUrl,
|
||||
'profile_class' => $profileClass,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate field names to human-readable labels
|
||||
*
|
||||
* @param string $fieldName
|
||||
* @return string
|
||||
*/
|
||||
protected function translateFieldName($fieldName)
|
||||
{
|
||||
$translations = [
|
||||
'name' => trans('Username', [], $this->locale),
|
||||
'full_name' => trans('Full name', [], $this->locale),
|
||||
'level' => trans('Bank level', [], $this->locale),
|
||||
'email' => trans('Email', [], $this->locale),
|
||||
'about_short' => trans('Short introduction', [], $this->locale),
|
||||
'about' => trans('Long introduction', [], $this->locale),
|
||||
'motivation' => trans('Motivation to Timebank', [], $this->locale),
|
||||
'website' => trans('Website', [], $this->locale),
|
||||
'phone' => trans('Phone', [], $this->locale),
|
||||
'inactive_at' => trans('Re-activate', [], $this->locale),
|
||||
];
|
||||
|
||||
return $translations[$fieldName] ?? ucfirst(str_replace('_', ' ', $fieldName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
// Set application locale for this email
|
||||
app()->setLocale($this->locale);
|
||||
|
||||
$subject = trans('messages.profile_edited_by_admin_subject', [], $this->locale);
|
||||
$template = 'emails.profile-edited.profile-edited';
|
||||
|
||||
// Translate field names
|
||||
$translatedFields = array_map(function($field) {
|
||||
return $this->translateFieldName($field);
|
||||
}, $this->changedFields);
|
||||
|
||||
Log::info('ProfileEditedByAdminMail: Building email', [
|
||||
'subject' => $subject,
|
||||
'template' => $template,
|
||||
'locale' => $this->locale,
|
||||
'from_email' => timebank_config('mail.support.email'),
|
||||
'from_name' => timebank_config('mail.support.name'),
|
||||
'translated_fields' => $translatedFields,
|
||||
]);
|
||||
|
||||
return $this
|
||||
->from(timebank_config('mail.support.email'), timebank_config('mail.support.name'))
|
||||
->subject($subject)
|
||||
->view($template)
|
||||
->with([
|
||||
'profile' => $this->profile,
|
||||
'changedFields' => $translatedFields,
|
||||
'buttonUrl' => $this->buttonUrl,
|
||||
]);
|
||||
}
|
||||
}
|
||||
156
app/Mail/ProfileLinkChangedMail.php
Normal file
156
app/Mail/ProfileLinkChangedMail.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
|
||||
|
||||
class ProfileLinkChangedMail extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
protected $recipient;
|
||||
protected $linkedProfile;
|
||||
protected $action; // 'attached' or 'detached'
|
||||
protected $buttonUrl;
|
||||
public $locale;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @param mixed $recipient The profile receiving the notification
|
||||
* @param mixed $linkedProfile The profile that was attached/detached
|
||||
* @param string $action Either 'attached' or 'detached'
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($recipient, $linkedProfile, $action = 'attached')
|
||||
{
|
||||
$this->recipient = $recipient;
|
||||
$this->linkedProfile = $linkedProfile;
|
||||
$this->action = $action;
|
||||
|
||||
Log::info('ProfileLinkChangedMail: Constructor called', [
|
||||
'recipient_id' => $recipient->id,
|
||||
'recipient_type' => get_class($recipient),
|
||||
'recipient_lang_preference' => $recipient->lang_preference ?? 'not set',
|
||||
'linked_profile_id' => $linkedProfile->id,
|
||||
'linked_profile_type' => get_class($linkedProfile),
|
||||
'action' => $action,
|
||||
]);
|
||||
|
||||
// Support lang_preference for User, Organization, Bank, and Admin models
|
||||
$this->locale = $recipient->lang_preference ?? config('app.fallback_locale');
|
||||
|
||||
// Ensure locale is not empty
|
||||
if (empty($this->locale)) {
|
||||
$this->locale = config('app.fallback_locale');
|
||||
}
|
||||
|
||||
Log::info('ProfileLinkChangedMail: Locale determined', [
|
||||
'locale' => $this->locale,
|
||||
]);
|
||||
|
||||
// Generate localized URL to the linked profile
|
||||
$linkedProfileType = strtolower(class_basename($linkedProfile));
|
||||
$translatedType = __($linkedProfileType, [], $this->locale);
|
||||
|
||||
Log::info('ProfileLinkChangedMail: Profile type translation', [
|
||||
'original_type' => $linkedProfileType,
|
||||
'translated_type' => $translatedType,
|
||||
]);
|
||||
|
||||
if ($linkedProfile) {
|
||||
// For attached action, use direct login routes for elevated profiles
|
||||
// This allows the user to log in to the profile directly from the email
|
||||
// After successful login, user will be redirected to main page
|
||||
if ($this->action === 'attached') {
|
||||
$profileClass = get_class($linkedProfile);
|
||||
|
||||
if ($profileClass === 'App\\Models\\Organization') {
|
||||
// Direct login to organization - redirects to main page after login
|
||||
$this->buttonUrl = LaravelLocalization::localizeURL(
|
||||
route('organization.direct-login', ['organizationId' => $linkedProfile->id]),
|
||||
$this->locale
|
||||
);
|
||||
} elseif ($profileClass === 'App\\Models\\Bank') {
|
||||
// Direct login to bank - redirects to main page after login
|
||||
$this->buttonUrl = LaravelLocalization::localizeURL(
|
||||
route('bank.direct-login', ['bankId' => $linkedProfile->id]),
|
||||
$this->locale
|
||||
);
|
||||
} elseif ($profileClass === 'App\\Models\\Admin') {
|
||||
// Direct login to admin - redirects to main page after login
|
||||
$this->buttonUrl = LaravelLocalization::localizeURL(
|
||||
route('admin.direct-login', ['adminId' => $linkedProfile->id]),
|
||||
$this->locale
|
||||
);
|
||||
} else {
|
||||
// For User profiles or other types, just link to the profile page
|
||||
$this->buttonUrl = LaravelLocalization::localizeURL(
|
||||
route('profile.show_by_type_and_id', ['type' => $translatedType, 'id' => $linkedProfile->id]),
|
||||
$this->locale
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// For detached action, just link to the profile page
|
||||
$this->buttonUrl = LaravelLocalization::localizeURL(
|
||||
route('profile.show_by_type_and_id', ['type' => $translatedType, 'id' => $linkedProfile->id]),
|
||||
$this->locale
|
||||
);
|
||||
}
|
||||
|
||||
Log::info('ProfileLinkChangedMail: Generated button URL', [
|
||||
'button_url' => $this->buttonUrl,
|
||||
'profile_class' => get_class($linkedProfile),
|
||||
'action' => $this->action,
|
||||
]);
|
||||
} else {
|
||||
Log::warning('ProfileLinkChangedMail: Unknown linked profile type');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
// Set application locale for this email
|
||||
app()->setLocale($this->locale);
|
||||
|
||||
$subjectKey = $this->action === 'attached'
|
||||
? 'messages.profile_link_attached_subject'
|
||||
: 'messages.profile_link_detached_subject';
|
||||
|
||||
$subject = trans($subjectKey, [], $this->locale);
|
||||
$template = 'emails.profile-links.link-changed';
|
||||
|
||||
Log::info('ProfileLinkChangedMail: Building email', [
|
||||
'action' => $this->action,
|
||||
'subject_key' => $subjectKey,
|
||||
'subject' => $subject,
|
||||
'template' => $template,
|
||||
'locale' => $this->locale,
|
||||
'from_email' => timebank_config('mail.support.email'),
|
||||
'from_name' => timebank_config('mail.support.name'),
|
||||
]);
|
||||
|
||||
return $this
|
||||
->from(timebank_config('mail.support.email'), timebank_config('mail.support.name'))
|
||||
->subject($subject)
|
||||
->view($template)
|
||||
->with([
|
||||
'action' => $this->action,
|
||||
'recipient' => $this->recipient,
|
||||
'linkedProfile' => $this->linkedProfile,
|
||||
'buttonUrl' => $this->buttonUrl,
|
||||
'locale' => $this->locale,
|
||||
]);
|
||||
}
|
||||
}
|
||||
79
app/Mail/ReactionCreatedMail.php
Normal file
79
app/Mail/ReactionCreatedMail.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Cog\Laravel\Love\ReactionType\Models\ReactionType;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
|
||||
|
||||
class ReactionCreatedMail extends Mailable implements ShouldQueue // ShouldQueue here creates the class as a background job
|
||||
{
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
protected $reaction;
|
||||
protected $reactionType;
|
||||
protected $reactionCount;
|
||||
protected $buttonUrl;
|
||||
public $locale;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($reaction)
|
||||
{
|
||||
$this->reaction = $reaction;
|
||||
$this->reactionType = ReactionType::fromName($reaction->getType()->name);
|
||||
$this->reactionCount = $reaction->getReactant()->getReactionCounterOfType($this->reactionType)->count;
|
||||
|
||||
$recipient = $reaction->getReactant()->getReactable();
|
||||
|
||||
// Support lang_preference for User, Organization, Bank, and Admin models
|
||||
$this->locale = $recipient->lang_preference ?? config('app.fallback_locale');
|
||||
|
||||
// Ensure locale is not empty
|
||||
if (empty($this->locale)) {
|
||||
$this->locale = config('app.fallback_locale');
|
||||
}
|
||||
|
||||
$reacter = $reaction->getReacter()->getReacterable();
|
||||
$reacterType = strtolower(class_basename($reacter));
|
||||
$translatedType = __($reacterType, [], $this->locale);
|
||||
|
||||
if ($reacter) {
|
||||
$this->buttonUrl = LaravelLocalization::localizeURL(route('profile.show_by_type_and_id', ['type' => $translatedType, 'id' => $reacter->id]), $this->locale);
|
||||
} else {
|
||||
Log::warning('ReactionCreatedMail: Unknown reacter type');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
// Set application locale for this email
|
||||
app()->setLocale($this->locale);
|
||||
|
||||
return $this
|
||||
->from(timebank_config('mail.support.email'), timebank_config('mail.support.name'))
|
||||
->subject(trans('messages.Your_profile_has_received_a_'. $this->reaction->getType()->name, [], $this->locale))
|
||||
->view('emails.reactions.star-received')
|
||||
->with([
|
||||
'reactionType' => $this->reaction->getType(),
|
||||
'reactionCount' => $this->reactionCount,
|
||||
'from' => $this->reaction->getReacter()->getReacterable(),
|
||||
'to' => $this->reaction->getReactant()->getReactable(),
|
||||
'buttonUrl' => $this->buttonUrl,
|
||||
]);
|
||||
}
|
||||
}
|
||||
102
app/Mail/ReservationCancelledMail.php
Normal file
102
app/Mail/ReservationCancelledMail.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Cog\Laravel\Love\ReactionType\Models\ReactionType;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
|
||||
|
||||
class ReservationCancelledMail extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
protected $data;
|
||||
protected $post;
|
||||
protected $reacter;
|
||||
protected $buttonUrl;
|
||||
public $locale;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($data)
|
||||
{
|
||||
\Log::info('ReservationCancelledMail: Constructor called');
|
||||
|
||||
$this->data = $data;
|
||||
|
||||
// Load the reacter based on the data
|
||||
$reacterType = $data['reacter_type'];
|
||||
$this->reacter = $reacterType::find($data['reacter_id']);
|
||||
\Log::info('ReservationCancelledMail: Reacter loaded', ['name' => $data['reacter_name']]);
|
||||
|
||||
// Load the post with its relationships
|
||||
$this->post = \App\Models\Post::with(['translations', 'meeting'])->find($data['post_id']);
|
||||
|
||||
\Log::info('ReservationCancelledMail: Post loaded', [
|
||||
'post_id' => $this->post ? $this->post->id : null,
|
||||
'post_title' => $this->post && $this->post->translations->first() ? $this->post->translations->first()->title : null,
|
||||
'has_meeting' => $this->post && $this->post->meeting ? 'yes' : 'no'
|
||||
]);
|
||||
|
||||
// Use the locale from the data
|
||||
$this->locale = $data['reacter_locale'];
|
||||
|
||||
// Ensure locale is not empty
|
||||
if (empty($this->locale)) {
|
||||
$this->locale = config('app.fallback_locale');
|
||||
}
|
||||
|
||||
\Log::info('ReservationCancelledMail: Locale set', ['locale' => $this->locale]);
|
||||
|
||||
// Button should link to the event/post using the slug
|
||||
if ($this->post && isset($this->post->id)) {
|
||||
// Get the translation for the user's locale
|
||||
$translation = $this->post->translations->where('locale', $this->locale)->first();
|
||||
|
||||
// Fall back to first translation if locale not found
|
||||
if (!$translation) {
|
||||
$translation = $this->post->translations->first();
|
||||
}
|
||||
|
||||
if ($translation && $translation->slug) {
|
||||
$this->buttonUrl = LaravelLocalization::localizeURL(route('post.show_by_slug', ['slug' => $translation->slug]), $this->locale);
|
||||
\Log::info('ReservationCancelledMail: Button URL created', ['url' => $this->buttonUrl, 'slug' => $translation->slug]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
// Set application locale for this email
|
||||
app()->setLocale($this->locale);
|
||||
|
||||
\Log::info('ReservationCancelledMail: Building email', [
|
||||
'locale' => $this->locale,
|
||||
'template' => 'emails.reactions.reservation-cancelled',
|
||||
'subject' => trans('messages.Reservation_cancelled', [], $this->locale),
|
||||
]);
|
||||
|
||||
return $this
|
||||
->from(timebank_config('mail.support.email'), timebank_config('mail.support.name'))
|
||||
->subject(trans('messages.Reservation_cancelled', [], $this->locale))
|
||||
->view('emails.reactions.reservation-cancelled')
|
||||
->with([
|
||||
'reacter' => $this->reacter,
|
||||
'post' => $this->post,
|
||||
'buttonUrl' => $this->buttonUrl,
|
||||
]);
|
||||
}
|
||||
}
|
||||
106
app/Mail/ReservationCreatedMail.php
Normal file
106
app/Mail/ReservationCreatedMail.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Cog\Laravel\Love\ReactionType\Models\ReactionType;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
|
||||
|
||||
class ReservationCreatedMail extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
protected $reaction;
|
||||
protected $post;
|
||||
protected $buttonUrl;
|
||||
public $locale;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($reaction)
|
||||
{
|
||||
\Log::info('ReservationCreatedMail: Constructor called');
|
||||
|
||||
$this->reaction = $reaction;
|
||||
|
||||
// Get the reacter (person making the reservation)
|
||||
$reacter = $reaction->getReacter()->getReacterable();
|
||||
\Log::info('ReservationCreatedMail: Reacter loaded', ['name' => $reacter->name]);
|
||||
|
||||
// Get the post (event being reserved) using the reactant's getReactable method
|
||||
$reactant = $reaction->getReactant();
|
||||
$this->post = $reactant->getReactable();
|
||||
|
||||
// Eager load the relationships we need
|
||||
if ($this->post) {
|
||||
$this->post->load(['translations', 'meeting']);
|
||||
}
|
||||
|
||||
\Log::info('ReservationCreatedMail: Post loaded', [
|
||||
'post_id' => $this->post ? $this->post->id : null,
|
||||
'post_title' => $this->post && $this->post->translations->first() ? $this->post->translations->first()->title : null,
|
||||
'has_meeting' => $this->post && $this->post->meeting ? 'yes' : 'no'
|
||||
]);
|
||||
|
||||
// Support lang_preference for User, Organization, Bank, and Admin models
|
||||
$this->locale = $reacter->lang_preference ?? config('app.fallback_locale');
|
||||
|
||||
// Ensure locale is not empty
|
||||
if (empty($this->locale)) {
|
||||
$this->locale = config('app.fallback_locale');
|
||||
}
|
||||
|
||||
\Log::info('ReservationCreatedMail: Locale set', ['locale' => $this->locale]);
|
||||
|
||||
// Button should link to the event/post using the slug
|
||||
if ($this->post && isset($this->post->id)) {
|
||||
// Get the translation for the user's locale
|
||||
$translation = $this->post->translations->where('locale', $this->locale)->first();
|
||||
|
||||
// Fall back to first translation if locale not found
|
||||
if (!$translation) {
|
||||
$translation = $this->post->translations->first();
|
||||
}
|
||||
|
||||
if ($translation && $translation->slug) {
|
||||
$this->buttonUrl = LaravelLocalization::localizeURL(route('post.show_by_slug', ['slug' => $translation->slug]), $this->locale);
|
||||
\Log::info('ReservationCreatedMail: Button URL created', ['url' => $this->buttonUrl, 'slug' => $translation->slug]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
// Set application locale for this email
|
||||
app()->setLocale($this->locale);
|
||||
|
||||
\Log::info('ReservationCreatedMail: Building email', [
|
||||
'locale' => $this->locale,
|
||||
'template' => 'emails.reactions.reservation-confirmation',
|
||||
'subject' => trans('messages.Reservation_confirmation', [], $this->locale),
|
||||
]);
|
||||
|
||||
return $this
|
||||
->from(timebank_config('mail.support.email'), timebank_config('mail.support.name'))
|
||||
->subject(trans('messages.Reservation_confirmation', [], $this->locale))
|
||||
->view('emails.reactions.reservation-confirmation')
|
||||
->with([
|
||||
'reacter' => $this->reaction->getReacter()->getReacterable(),
|
||||
'post' => $this->post,
|
||||
'buttonUrl' => $this->buttonUrl,
|
||||
]);
|
||||
}
|
||||
}
|
||||
105
app/Mail/ReservationUpdateMail.php
Normal file
105
app/Mail/ReservationUpdateMail.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
|
||||
|
||||
class ReservationUpdateMail extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
protected $reacter;
|
||||
protected $post;
|
||||
protected $message;
|
||||
protected $organizer;
|
||||
protected $buttonUrl;
|
||||
public $locale;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @param mixed $reacter The profile who made the reservation
|
||||
* @param mixed $post The post/event
|
||||
* @param string $message The custom message from organizer
|
||||
* @param mixed $organizer The organizer sending the message
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($reacter, $post, $message, $organizer)
|
||||
{
|
||||
Log::info('ReservationUpdateMail: Constructor called', [
|
||||
'reacter_id' => $reacter->id,
|
||||
'post_id' => $post->id
|
||||
]);
|
||||
|
||||
$this->reacter = $reacter;
|
||||
$this->post = $post;
|
||||
$this->message = $message;
|
||||
$this->organizer = $organizer;
|
||||
|
||||
// Eager load the relationships we need
|
||||
if ($this->post) {
|
||||
$this->post->load(['translations', 'meeting']);
|
||||
}
|
||||
|
||||
// Support lang_preference for User, Organization, Bank, and Admin models
|
||||
$this->locale = $reacter->lang_preference ?? config('app.fallback_locale');
|
||||
|
||||
// Ensure locale is not empty
|
||||
if (empty($this->locale)) {
|
||||
$this->locale = config('app.fallback_locale');
|
||||
}
|
||||
|
||||
Log::info('ReservationUpdateMail: Locale set', ['locale' => $this->locale]);
|
||||
|
||||
// Button should link to the event/post using the slug
|
||||
if ($this->post && isset($this->post->id)) {
|
||||
// Get the translation for the user's locale
|
||||
$translation = $this->post->translations->where('locale', $this->locale)->first();
|
||||
|
||||
// Fall back to first translation if locale not found
|
||||
if (!$translation) {
|
||||
$translation = $this->post->translations->first();
|
||||
}
|
||||
|
||||
if ($translation && $translation->slug) {
|
||||
$this->buttonUrl = LaravelLocalization::localizeURL(route('post.show_by_slug', ['slug' => $translation->slug]), $this->locale);
|
||||
Log::info('ReservationUpdateMail: Button URL created', ['url' => $this->buttonUrl]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
// Set application locale for this email
|
||||
app()->setLocale($this->locale);
|
||||
|
||||
Log::info('ReservationUpdateMail: Building email', [
|
||||
'locale' => $this->locale,
|
||||
'template' => 'emails.reactions.reservation-update',
|
||||
'subject' => trans('messages.Reservation_update', [], $this->locale),
|
||||
]);
|
||||
|
||||
return $this
|
||||
->from(timebank_config('mail.support.email'), timebank_config('mail.support.name'))
|
||||
->subject(trans('messages.Reservation_update', [], $this->locale))
|
||||
->view('emails.reactions.reservation-update')
|
||||
->with([
|
||||
'reacter' => $this->reacter,
|
||||
'post' => $this->post,
|
||||
'customMessage' => $this->message,
|
||||
'organizer' => $this->organizer,
|
||||
'buttonUrl' => $this->buttonUrl,
|
||||
]);
|
||||
}
|
||||
}
|
||||
55
app/Mail/TagAddedMail.php
Normal file
55
app/Mail/TagAddedMail.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\Tag;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class TagAddedMail extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public $tagId;
|
||||
public $locale;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @param $tagInfo
|
||||
*/
|
||||
public function __construct($tagId, $locale = null)
|
||||
{
|
||||
$this->tagId = $tagId;
|
||||
$this->locale = $locale ?? App::getLocale();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
// Set application locale for this email
|
||||
app()->setLocale($this->locale);
|
||||
|
||||
$tagInfo = collect((new Tag())->translateTagIdsWithContexts($this->tagId, $this->locale, App::getFallbackLocale()));
|
||||
|
||||
// Handle empty tag info
|
||||
$tagInfoData = $tagInfo->first();
|
||||
$tagName = $tagInfoData['tag'] ?? 'Unknown Tag';
|
||||
|
||||
return $this
|
||||
->from(timebank_config('mail.system_admin.email'), timebank_config('mail.system_admin.name'))
|
||||
->subject(trans('messages.new_tag_added', [], $this->locale) . ': ' . $tagName)
|
||||
->view('emails.tags.new')
|
||||
->with(['tagInfo' => $tagInfoData ]);
|
||||
|
||||
}
|
||||
}
|
||||
189
app/Mail/TestNewsletterMail.php
Normal file
189
app/Mail/TestNewsletterMail.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\Mailing;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Str;
|
||||
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
|
||||
|
||||
class TestNewsletterMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
protected $mailing;
|
||||
protected $contentBlocks;
|
||||
protected $recipientLocale;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct(Mailing $mailing, $locale)
|
||||
{
|
||||
$this->mailing = $mailing;
|
||||
$this->recipientLocale = $locale;
|
||||
$this->contentBlocks = $this->generateContentBlocksForLocale($locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = "[TEST - {$this->recipientLocale}] " . $this->mailing->getSubjectForLocale($this->recipientLocale);
|
||||
|
||||
return $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' => '#test-unsubscribe-link',
|
||||
'isTestMail' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate content blocks for a specific locale
|
||||
*/
|
||||
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 categories to post types
|
||||
$categoryMappings = [
|
||||
4 => 'event', // The Hague events
|
||||
5 => 'event', // South-Holland events
|
||||
6 => 'event', // The Netherlands events
|
||||
7 => 'news', // The Hague news
|
||||
8 => 'news', // General news
|
||||
113 => 'article', // Article
|
||||
];
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ?: Str::limit(strip_tags($translation->content ?? ''), 150),
|
||||
'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
|
||||
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->hasMedia('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;
|
||||
}
|
||||
}
|
||||
111
app/Mail/TransferReceived.php
Normal file
111
app/Mail/TransferReceived.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\Transaction;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
|
||||
|
||||
class TransferReceived extends Mailable implements ShouldQueue // ShouldQueue here creates the class as a background job
|
||||
{
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public $transaction;
|
||||
public $locale;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Transaction $transaction, $messageLocale, ) // Binds the Transaction model to $transaction
|
||||
{
|
||||
$this->transaction = $transaction;
|
||||
$this->locale = $messageLocale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
// Set application locale for this email
|
||||
app()->setLocale($this->locale);
|
||||
|
||||
$recipient = $this->transaction->accountTo->accountable;
|
||||
|
||||
// Generate the transaction history URL
|
||||
$transactionsRoute = LaravelLocalization::getURLFromRouteNameTranslated($this->locale, 'routes.transactions');
|
||||
|
||||
// Generate appropriate URL based on recipient type
|
||||
// For organizations, banks, and admins, use direct login URL
|
||||
if ($recipient instanceof \App\Models\Organization) {
|
||||
$baseUrl = route('organization.direct-login', [
|
||||
'organizationId' => $recipient->id,
|
||||
'intended' => $transactionsRoute
|
||||
]);
|
||||
$transactionHistoryUrl = LaravelLocalization::localizeURL($baseUrl, $this->locale);
|
||||
} elseif ($recipient instanceof \App\Models\Bank) {
|
||||
$baseUrl = route('bank.direct-login', [
|
||||
'bankId' => $recipient->id,
|
||||
'intended' => $transactionsRoute
|
||||
]);
|
||||
$transactionHistoryUrl = LaravelLocalization::localizeURL($baseUrl, $this->locale);
|
||||
} elseif ($recipient instanceof \App\Models\Admin) {
|
||||
$baseUrl = route('admin.direct-login', [
|
||||
'adminId' => $recipient->id,
|
||||
'intended' => $transactionsRoute
|
||||
]);
|
||||
$transactionHistoryUrl = LaravelLocalization::localizeURL($baseUrl, $this->locale);
|
||||
} else {
|
||||
// For regular users, just use the transactions URL
|
||||
$transactionHistoryUrl = $transactionsRoute;
|
||||
}
|
||||
|
||||
// Generate the transaction statement URL
|
||||
$statementRoute = LaravelLocalization::getURLFromRouteNameTranslated($this->locale, 'routes.statement', array('transactionId' => $this->transaction->id));
|
||||
|
||||
// Generate appropriate URL for statement based on recipient type
|
||||
if ($recipient instanceof \App\Models\Organization) {
|
||||
$baseUrl = route('organization.direct-login', [
|
||||
'organizationId' => $recipient->id,
|
||||
'intended' => $statementRoute
|
||||
]);
|
||||
$transactionStatement = LaravelLocalization::localizeURL($baseUrl, $this->locale);
|
||||
} elseif ($recipient instanceof \App\Models\Bank) {
|
||||
$baseUrl = route('bank.direct-login', [
|
||||
'bankId' => $recipient->id,
|
||||
'intended' => $statementRoute
|
||||
]);
|
||||
$transactionStatement = LaravelLocalization::localizeURL($baseUrl, $this->locale);
|
||||
} elseif ($recipient instanceof \App\Models\Admin) {
|
||||
$baseUrl = route('admin.direct-login', [
|
||||
'adminId' => $recipient->id,
|
||||
'intended' => $statementRoute
|
||||
]);
|
||||
$transactionStatement = LaravelLocalization::localizeURL($baseUrl, $this->locale);
|
||||
} else {
|
||||
// For regular users, just use the statement URL
|
||||
$transactionStatement = $statementRoute;
|
||||
}
|
||||
|
||||
$senderName = $this->transaction->accountFrom->accountable->name;
|
||||
|
||||
return $this
|
||||
->from(timebank_config('mail.payments.email'), timebank_config('mail.payments.name'))
|
||||
->subject(trans('messages.Payment_received_from', ['name' => $senderName], $this->locale))
|
||||
->view('emails.transfers.payment-received')
|
||||
->with([
|
||||
'transaction' => $this->transaction,
|
||||
'transactionHistoryUrl' => $transactionHistoryUrl,
|
||||
'transactionStatement' => $transactionStatement,
|
||||
]);
|
||||
}
|
||||
}
|
||||
163
app/Mail/UniversalBounceHandler.php
Normal file
163
app/Mail/UniversalBounceHandler.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\MailingBounce;
|
||||
use Illuminate\Mail\Events\MessageSending;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
class UniversalBounceHandler
|
||||
{
|
||||
/**
|
||||
* Handle the message sending event to check for suppressed recipients
|
||||
*/
|
||||
public function handle(MessageSending $event): bool
|
||||
{
|
||||
$message = $event->message;
|
||||
|
||||
// Extract all recipients from the message
|
||||
$allRecipients = $this->extractAllRecipients($message);
|
||||
$suppressedRecipients = [];
|
||||
|
||||
// Check each recipient for suppression
|
||||
foreach ($allRecipients as $email) {
|
||||
if (MailingBounce::isSuppressed($email)) {
|
||||
$suppressedRecipients[] = $email;
|
||||
}
|
||||
}
|
||||
|
||||
// If any recipients are suppressed, log and potentially block the email
|
||||
if (!empty($suppressedRecipients)) {
|
||||
Log::info("Email contains suppressed recipients", [
|
||||
'total_recipients' => count($allRecipients),
|
||||
'suppressed_recipients' => $suppressedRecipients,
|
||||
'subject' => $message->getSubject(),
|
||||
]);
|
||||
|
||||
// If ALL recipients are suppressed, block the email entirely
|
||||
if (count($suppressedRecipients) === count($allRecipients)) {
|
||||
Log::info("All recipients suppressed, blocking email", [
|
||||
'suppressed_recipients' => $suppressedRecipients,
|
||||
'subject' => $message->getSubject(),
|
||||
]);
|
||||
return false; // Block the email
|
||||
}
|
||||
|
||||
// If only some recipients are suppressed, remove them from the message
|
||||
$this->removeSuppressedRecipients($message, $suppressedRecipients);
|
||||
}
|
||||
|
||||
// Add bounce tracking headers to all outgoing emails
|
||||
$this->addBounceTrackingHeaders($message);
|
||||
|
||||
return true; // Allow the email to be sent
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all recipient email addresses from the message
|
||||
*/
|
||||
protected function extractAllRecipients($message): array
|
||||
{
|
||||
$recipients = [];
|
||||
|
||||
// Get TO recipients
|
||||
if ($message->getTo()) {
|
||||
foreach ($message->getTo() as $address) {
|
||||
$recipients[] = $address->getAddress();
|
||||
}
|
||||
}
|
||||
|
||||
// Get CC recipients
|
||||
if ($message->getCc()) {
|
||||
foreach ($message->getCc() as $address) {
|
||||
$recipients[] = $address->getAddress();
|
||||
}
|
||||
}
|
||||
|
||||
// Get BCC recipients
|
||||
if ($message->getBcc()) {
|
||||
foreach ($message->getBcc() as $address) {
|
||||
$recipients[] = $address->getAddress();
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique($recipients);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove suppressed recipients from the message
|
||||
*/
|
||||
protected function removeSuppressedRecipients($message, array $suppressedEmails): void
|
||||
{
|
||||
// Remove from TO recipients
|
||||
$toAddresses = $message->getTo();
|
||||
if ($toAddresses) {
|
||||
$filteredTo = [];
|
||||
foreach ($toAddresses as $address) {
|
||||
if (!in_array($address->getAddress(), $suppressedEmails)) {
|
||||
$filteredTo[] = $address;
|
||||
}
|
||||
}
|
||||
$message->to(...$filteredTo);
|
||||
}
|
||||
|
||||
// Remove from CC recipients
|
||||
$ccAddresses = $message->getCc();
|
||||
if ($ccAddresses) {
|
||||
$filteredCc = [];
|
||||
foreach ($ccAddresses as $address) {
|
||||
if (!in_array($address->getAddress(), $suppressedEmails)) {
|
||||
$filteredCc[] = $address;
|
||||
}
|
||||
}
|
||||
$message->cc(...$filteredCc);
|
||||
}
|
||||
|
||||
// Remove from BCC recipients
|
||||
$bccAddresses = $message->getBcc();
|
||||
if ($bccAddresses) {
|
||||
$filteredBcc = [];
|
||||
foreach ($bccAddresses as $address) {
|
||||
if (!in_array($address->getAddress(), $suppressedEmails)) {
|
||||
$filteredBcc[] = $address;
|
||||
}
|
||||
}
|
||||
$message->bcc(...$filteredBcc);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add bounce tracking headers to outgoing emails
|
||||
*/
|
||||
protected function addBounceTrackingHeaders($message): void
|
||||
{
|
||||
$bounceEmail = timebank_config('mailing.bounce_address');
|
||||
|
||||
if (!$bounceEmail) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($message instanceof Email) {
|
||||
$headers = $message->getHeaders();
|
||||
|
||||
// Add Return-Path for bounce routing (if not already set)
|
||||
if (!$headers->has('Return-Path')) {
|
||||
$headers->addPathHeader('Return-Path', $bounceEmail);
|
||||
}
|
||||
|
||||
// Add bounce tracking headers
|
||||
$headers->addTextHeader('X-Bounce-Tracking', 'universal');
|
||||
$headers->addTextHeader('X-Bounce-Handler', 'timebank-cc');
|
||||
|
||||
// Add recipient tracking for the first recipient (for bounce processing)
|
||||
$recipients = $this->extractAllRecipients($message);
|
||||
if (!empty($recipients)) {
|
||||
$headers->addTextHeader('X-Primary-Recipient', $recipients[0]);
|
||||
}
|
||||
|
||||
// Set Return-Path on the message
|
||||
$message->returnPath($bounceEmail);
|
||||
}
|
||||
}
|
||||
}
|
||||
58
app/Mail/UserDeletedMail.php
Normal file
58
app/Mail/UserDeletedMail.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class UserDeletedMail extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
protected $result;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct($result)
|
||||
{
|
||||
$this->result = $result;
|
||||
$this->locale = $result['deletedUser']->lang_preference ?? config('app.locale', 'en');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
// Set application locale for this email
|
||||
app()->setLocale($this->locale);
|
||||
|
||||
return $this
|
||||
->from(timebank_config('mail.system_admin.email'), timebank_config('mail.system_admin.name'))
|
||||
->subject(trans('messages.Your_profile_has_been_deleted', [], $this->locale))
|
||||
->view('emails.administration.user-deleted')
|
||||
->with([
|
||||
'name' => $this->result['deletedUser']->name,
|
||||
'full_name' => $this->result['deletedUser']->full_name,
|
||||
'deletedMail' => $this->result['mail'],
|
||||
'time' => $this->result['time'],
|
||||
'balanceHandlingOption' => $this->result['balanceHandlingOption'] ?? null,
|
||||
'totalBalance' => $this->result['totalBalance'] ?? 0,
|
||||
'donationAccountName' => $this->result['donationAccountName'] ?? null,
|
||||
'donationOrganizationName' => $this->result['donationOrganizationName'] ?? null,
|
||||
'autoDeleted' => $this->result['autoDeleted'] ?? false,
|
||||
'daysNotLoggedIn' => $this->result['daysNotLoggedIn'] ?? null,
|
||||
'daysAfterInactive' => $this->result['daysAfterInactive'] ?? null,
|
||||
'totalDaysToDelete' => $this->result['totalDaysToDelete'] ?? null,
|
||||
'gracePeriodDays' => $this->result['gracePeriodDays'] ?? timebank_config('delete_profile.grace_period_days', 30),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
127
app/Mail/VerifyProfileEmailMailable.php
Normal file
127
app/Mail/VerifyProfileEmailMailable.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class VerifyProfileEmailMailable extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
// /**
|
||||
// * The name of the queue the job should be sent to.
|
||||
// *
|
||||
// * @var string
|
||||
// */
|
||||
// public $queue = 'high';
|
||||
|
||||
protected $notifiable;
|
||||
protected $verificationUrl;
|
||||
public $locale;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @param mixed $notifiable The user/profile that needs email verification
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($notifiable)
|
||||
{
|
||||
$this->notifiable = $notifiable;
|
||||
|
||||
// Set the queue for this mailable
|
||||
$this->onQueue(queue: 'high');
|
||||
|
||||
Log::info('VerifyProfileEmailMailable: Constructor called', [
|
||||
'notifiable_id' => $notifiable->id,
|
||||
'notifiable_type' => get_class($notifiable),
|
||||
'notifiable_lang_preference' => $notifiable->lang_preference ?? 'not set',
|
||||
'email' => $notifiable->email,
|
||||
]);
|
||||
|
||||
// Support lang_preference for User, Organization, Bank, and Admin models
|
||||
$this->locale = $notifiable->lang_preference ?? config('app.fallback_locale');
|
||||
|
||||
// Ensure locale is not empty
|
||||
if (empty($this->locale)) {
|
||||
$this->locale = config('app.fallback_locale');
|
||||
}
|
||||
|
||||
Log::info('VerifyProfileEmailMailable: Locale determined', [
|
||||
'locale' => $this->locale,
|
||||
]);
|
||||
|
||||
// Generate the verification URL
|
||||
$this->verificationUrl = $this->generateVerificationUrl($notifiable);
|
||||
|
||||
Log::info('VerifyProfileEmailMailable: Verification URL generated', [
|
||||
'url' => $this->verificationUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the verification URL for the notifiable entity.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return string
|
||||
*/
|
||||
protected function generateVerificationUrl($notifiable)
|
||||
{
|
||||
$baseName = class_basename($notifiable); // e.g. "User", "Organization"
|
||||
$type = Str::lower($baseName);
|
||||
|
||||
// Generate a temporary signed route that expires in 60 minutes
|
||||
// Note: This route is intentionally NOT localized as it's defined
|
||||
// without locale prefix to work for users who are not yet logged in
|
||||
$signedUrl = URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
Carbon::now()->addMinutes(60),
|
||||
[
|
||||
'type' => $type,
|
||||
'id' => $notifiable->id,
|
||||
'hash' => sha1($notifiable->getEmailForVerification()),
|
||||
]
|
||||
);
|
||||
|
||||
return $signedUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
// Set application locale for this email
|
||||
app()->setLocale($this->locale);
|
||||
|
||||
$subject = trans('messages.verify_email_subject', [], $this->locale);
|
||||
$template = 'emails.verification.verify-email';
|
||||
|
||||
Log::info('VerifyProfileEmailMailable: Building email', [
|
||||
'subject' => $subject,
|
||||
'template' => $template,
|
||||
'locale' => $this->locale,
|
||||
'from_email' => timebank_config('mail.support.email'),
|
||||
'from_name' => timebank_config('mail.support.name'),
|
||||
]);
|
||||
|
||||
return $this
|
||||
->from(timebank_config('mail.support.email'), timebank_config('mail.support.name'))
|
||||
->subject($subject)
|
||||
->view($template)
|
||||
->with([
|
||||
'notifiable' => $this->notifiable,
|
||||
'verificationUrl' => $this->verificationUrl,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user