Skip to content

[2.x] Notification email body shows the wrong title (leaked from a previously-sent email) #4767

Description

@imorland

Description

Notification emails sometimes render the wrong title in the email body (the <h2> heading) — not the subject line, which is correct. The wrong heading is borrowed from a different email type sent earlier, e.g. "Reset Your Password" or "Flarum Email Test" appearing on a notification whose heading should read "Notification". The rest of the email is correct.

Reported by a user on the community forum, and reproduced by a maintainer across several emails.

Root cause

The email title leaks via the shared, singleton view factory.

  1. view is a singleton, so it's long-lived within a PHP process. Laravel's Illuminate\View\Factory::share() writes per key into $this->shared and never clears keys that aren't re-shared.
  2. SendInformationalEmailJob — used by password reset, the admin "Send test email", and email confirmation/activation — shares a title onto that singleton:
    // src/Mail/Job/SendInformationalEmailJob.php:48
    $view->share(compact('forumTitle', 'userEmail', 'title', 'username'));
    (title = "Reset Your Password", "Flarum Email Test", etc.)
  3. NotificationMailer renders the notification without a title key:
    // src/Notification/NotificationMailer.php:49-55
    $data = compact('blueprint', 'user', 'unsubscribeLink', 'settingsLink', 'type', 'forumTitle', 'username', 'userEmail'); // no 'title'
    $this->view->share($data);
    $this->mailer->send($this->getEmailViews($blueprint), $data, ...);
  4. The notification template falls back to a default only if $title is unset:
    {{-- views/email/html/notification.blade.php:3 --}}
    <h2>{{ $title ?? $translator->trans('core.email.notification.default_title') }}</h2>
    With a stale shared title still on the factory, $title is truthy → the ?? default ("Notification") never fires → the notification shows the previous email's title.

Why it's intermittent

It only occurs when a notification email is rendered in the same PHP process, after an informational email (password reset / test email / account activation) — i.e. queue ordering within a single worker run. A fresh process, or a notification rendered first, shows the correct title.

Reproduction (test)

A passing integration test that proves the leak using the real singleton view factory and the real notification blade — framework/core/tests/integration/mail/EmailTitleLeakTest.php:

#[Test]
public function notification_title_leaks_from_a_previously_sent_informational_email(): void
{
    $view = $this->app()->getContainer()->make(Factory::class);

    // SendInformationalEmailJob (e.g. password reset) shares a title onto the singleton factory:
    $view->share(['title' => 'Reset Your Password']);

    // NotificationMailer then renders a notification with data that has no `title`:
    $data = $this->notificationData(); // no 'title' key
    $view->share($data);
    $html = $view->make('mail::html.notification', $data)->render();

    // BUG: heading is the leaked title, not the default "Notification".
    $this->assertSame('Reset Your Password', $this->renderedHeading($html));
}

A companion test confirms that, rendered in isolation, the heading is correctly "Notification".

Possible fix (needs confirmation)

Likely candidates — to be decided when fixing:

  • Have NotificationMailer always provide its own title in the render data, so it can never inherit a stale shared value; and/or
  • Stop SendInformationalEmailJob polluting the shared factory — Mailer::send($view, $data, …) already passes $data to the per-send render, so the view->share(...) there looks redundant.

Either way the reproduction test's expected heading flips back to "Notification", so it doubles as a regression guard. Worth checking whether other shared keys (e.g. username, forumTitle) have the same cross-email bleed.

Environment

  • flarum/core 2.x. Not specific to any release.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions