Keep the purpose mailer through the queue instead of losing it to the default

Mail::to($x)->queue($mail) resolves the DEFAULT mailer, and
Illuminate\Mail\Mailer::queue() (vendor Mailer.php:482) does
`$view->mailer($this->name)->queue($this->queue)` — overwriting
MaintenanceAnnouncementMail/MaintenanceCancelledMail's own 'cp_maintenance'
with the default mailer's name ('array' in tests, 'log'/whatever
MAIL_MAILER is in prod) before the job is even built. Both mails kept
sending through the old .env account instead of MailboxTransport. CloudReady
was never affected — MailChannel.php:66 preserves a notification's own
mailer.

Fix: name the mailer before queueing — Mail::mailer($mail->mailer)->to(...).
$this->name on the resolved Mailer is then already $mail->mailer, so the
vendor overwrite becomes a no-op instead of a silent downgrade. Confirmed by
reading the vendor source, not assumed.

SenderAddressTest's "names the purpose mailer..." test asserted
$mail->mailer on a bare, never-queued mailable, which proves nothing about
what survives queueing — the constructor sets it right regardless of what
MaintenanceNotifier does with it. Replaced it with one that drives
MaintenanceNotifier::deliver() under Queue::fake() and inspects the pushed
SendQueuedMailable's mailable. Verified it fails against the pre-fix code
first (the pushed job carried mailer = 'array'), then verified the fix
makes it pass, then mutated the fix away and watched it fail again.
feat/mailboxes
nexxo 2026-07-28 03:50:02 +02:00
parent 99b7393361
commit 7102d01bcc
2 changed files with 36 additions and 5 deletions

View File

@ -80,7 +80,14 @@ class MaintenanceNotifier
$mail->notificationId = $delivery->id;
try {
Mail::to($customer->email)
// Mail::to() alone resolves the DEFAULT mailer. Illuminate\Mail\Mailer::queue()
// then runs `$view->mailer($this->name)->queue(...)` — that overwrites the
// mailable's own purpose mailer (set in its constructor, e.g. 'cp_maintenance')
// with the default's name before the job is even built. Naming the mailer here
// makes that overwrite a no-op: $this->name on the resolved Mailer is already
// $mail->mailer, so it writes back the same value.
Mail::mailer($mail->mailer)
->to($customer->email)
->locale($customer->locale ?: config('app.locale'))
->queue($mail);
} catch (\Throwable $e) {

View File

@ -8,8 +8,11 @@ use App\Models\Mailbox;
use App\Models\MaintenanceWindow;
use App\Notifications\CloudReady;
use App\Services\Mail\MailPurpose;
use App\Services\Maintenance\MaintenanceNotifier;
use App\Support\Settings;
use Illuminate\Mail\SendQueuedMailable;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Queue;
beforeEach(function () {
config()->set('admin_access.secrets_key', 'base64:'.base64_encode(random_bytes(32)));
@ -61,12 +64,33 @@ it('sets Reply-To on an answerable mailbox, so a customer reply reaches a human'
});
it('names the purpose mailer so the queue worker picks the right transport', function () {
$mail = new MaintenanceAnnouncementMail(
MaintenanceWindow::factory()->create(),
Customer::factory()->create(),
// A bare, never-queued mailable proves nothing here: the constructor sets
// $this->mailer correctly regardless of what MaintenanceNotifier does.
// Illuminate\Mail\Mailer::queue() (vendor Mailer.php) runs
// `$view->mailer($this->name)->queue(...)` — if MaintenanceNotifier queues
// through the bare `Mail` facade instead of `Mail::mailer($mail->mailer)`,
// that resolves the DEFAULT mailer ('array' in tests) and overwrites the
// mailable's own 'cp_maintenance' with it BEFORE the queued job is even
// built. The only way to see that is to actually queue it and inspect the
// job that comes out the other side.
Mailbox::factory()->create(['key' => 'no-reply', 'address' => 'no-reply@clupilot.com', 'no_reply' => true]);
Settings::set(MailPurpose::settingKey(MailPurpose::MAINTENANCE), 'no-reply');
Queue::fake();
$window = MaintenanceWindow::factory()->create();
$customer = Customer::factory()->create();
app(MaintenanceNotifier::class)->deliver(
$window,
$customer,
'announcement',
new MaintenanceAnnouncementMail($window, $customer),
);
expect($mail->mailer)->toBe('cp_maintenance');
Queue::assertPushed(SendQueuedMailable::class, function (SendQueuedMailable $job) {
return $job->mailable->mailer === 'cp_maintenance';
});
});
it('gives the cancellation mail the same sender as the announcement', function () {