Initial commit

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

View 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;
}
}
}

View 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;
}
}
}

View 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();
}

View 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,
]);
}
}

View 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,
]);
}
}

View 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,
]);
}
}

View 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;
}
}

View 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,
]);
}
}

View 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,
]);
}
}

View 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'),
]);
}
}

View 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'),
]);
}
}

View 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'),
]);
}
}

View 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
View 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
View File

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

View 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,
]);
}
}

View 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,
]);
}
}

View 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,
]);
}
}

View 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,
]);
}
}

View 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,
]);
}
}

View 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
View 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 ]);
}
}

View 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;
}
}

View 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,
]);
}
}

View 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);
}
}
}

View 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),
]);
}
}

View 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,
]);
}
}