Skip to content

[Core]: Email template rendering issue with style tags #377

Open

Description

@nielsdrost7

Planning Note — v2 Implementation

Story
As a company admin, I want email templates that contain HTML <style> tags to be stored and loaded correctly, so that styled email templates populate the email body field when selected — instead of leaving it blank.

v2 Context
This is a bug in Modules/Core. The EmailTemplate model stores its body in a text/longtext column. When the body contains <style> tags, something in the save or retrieval path truncates or empties the value, causing the email compose form (a Filament table action on ListInvoices) to receive a blank body when the template is selected. The root cause is likely one of: (a) JavaScript/Livewire stripping <style> from a rich-text editor, (b) a missing htmlspecialchars_decode on retrieval, or (c) a Filament RichEditor/TinyEditor component sanitising content on mount. The fix must ensure the full HTML body — including <style> blocks — round-trips correctly from save through retrieval into the Livewire action form.

Acceptance Criteria

  • An EmailTemplate body containing <style> tags is saved to the database without truncation.
  • When the user selects that template in the "Send Email" table action, the body field is populated with the full HTML (including <style> tags).
  • The fix does not break templates that contain no <style> tags.
  • No change is needed to existing template data (the fix is in the read/write path, not a migration).
  • The send-email Livewire action correctly reflects the template body in its form state.

Implementation

  • Module: Modules/Core/
  • Investigate Modules/Core/Models/EmailTemplate.php: check for casts, accessors, or mutators that may sanitise the template_body column.
  • Investigate the Filament schema for EmailTemplateResource — if using RichEditor, check the fileAttachmentsDisk and sanitisation config; consider switching to Textarea or a raw HTML editor for the body field if the RichEditor is stripping <style>.
  • Investigate the "Send Email" table action (likely Modules/Invoices/Filament/Company/Resources/InvoiceResource/Tables/InvoiceTable.php or a dedicated Action class): ensure the afterStateUpdated callback on the email_template_id select sets body from $template->template_body without additional sanitisation.
  • If a JS/Livewire sanitisation layer is the cause, configure the editor component to allow <style> tags or use wire:model on a plain Textarea.
  • Add a cast or raw getter on EmailTemplate::$template_body if needed to preserve HTML entities.

Test Scenarios (PHPUnit)

// Extends AbstractCompanyPanelTestCase
public function test_email_template_with_style_tags_is_stored_and_retrieved_correctly(): void
{
    // Arrange
    $body = '<style>p { color: red; }</style><p>Hello {client_name}</p>';

    // Act
    $template = EmailTemplate::factory()->for($this->company)->create(['template_body' => $body]);
    $loaded   = EmailTemplate::find($template->id);

    // Assert
    $this->assertEquals($body, $loaded->template_body);
    $this->assertStringContainsString('<style>', $loaded->template_body);
}

public function test_send_email_action_populates_body_from_template_including_style_tags(): void
{
    // Arrange
    $body     = '<style>h1 { font-size: 24px; }</style><p>Test</p>';
    $template = EmailTemplate::factory()->for($this->company)->create(['template_body' => $body]);
    $invoice  = Invoice::factory()->for($this->company)->create();

    // Act
    $response = $this->testLivewire(ListInvoices::class)
        ->mountTableAction('send_email', $invoice)
        ->setMountedTableActionData(['email_template_id' => $template->id]);

    // Assert
    $response->assertSet('mountedTableActionsData.0.body', $body);
}

public function test_email_template_without_style_tags_still_loads_correctly(): void
{
    // Arrange
    $body     = '<p>Hello {client_name},</p><p>Please find your invoice attached.</p>';
    $template = EmailTemplate::factory()->for($this->company)->create(['template_body' => $body]);

    // Act
    $loaded = EmailTemplate::find($template->id);

    // Assert
    $this->assertEquals($body, $loaded->template_body);
}

Activity

  1. Nathanwoodburn commented on Nov 2, 2023

    @Nathanwoodburn

    G'day,
    Is there any plan to allow style tags?

  2. nielsdrost7 commented on Nov 2, 2023

    @nielsdrost7
    CollaboratorAuthor

    @Nathanwoodburn no plans just yet, but we're open to pull-requests

  3. nielsdrost7 commented on Aug 7, 2025

    @nielsdrost7
    CollaboratorAuthor

    I'm moving this to the list of issues to take on for Version 2.x

  4. changed the title [-]Email template issues (when using style tags)[/-] [+][Core]: Email template rendering issue with style tags[/+] on Jun 13, 2026
  5. added this to the v2.0.0-alpha milestone on Jun 13, 2026
  6. nielsdrost7 commented on Jul 16, 2026

    @nielsdrost7
    CollaboratorAuthor

    SMART Plan — Email template body field (style tags, and the missing body input itself)

    Context/Epic: Core

    Specific

    Modules\Core\Models\EmailTemplate stores its body in a longText('body') column (migration: 2009_01_01_000003_create_email_templates_table.php) — not template_body as the original planning note assumed; the real column is simply body. Reading the actual EmailTemplateForm schema (Modules/Core/Filament/Admin/Resources/EmailTemplates/Schemas/EmailTemplateForm.php) shows the gap is more fundamental than style-tag stripping: the form has no field for body at all — only title, from_name, from_email, cc, bcc, type, subject are present. EmailTemplatesTest::it_creates_an_email_template only passes because fillForm() sets Livewire component state directly, bypassing the schema; a real admin using the actual UI can never set or edit a template's body today, style tags or not. Separately, the "Send Email" action referenced by the planning note does exist — Action::make('send email') in Modules/Invoices/Filament/Company/Resources/Invoices/Tables/InvoicesTable.php — but it calls InvoiceService::sendInvoiceEmail($record), and sendInvoiceEmail() does not exist anywhere on InvoiceService (verified: grep -rn "sendInvoiceEmail" Modules/ only finds the one call site). Clicking "Send Email" today throws a fatal error before any template/style-tag logic is ever reached.

    Measurable

    • The EmailTemplateForm schema gains a body input (a RichEditor — the same component already used for settings.default_invoice_terms in Modules/Core/Filament/Admin/Pages/Settings.php — or a plain Textarea if HTML must be authored directly) so a body can actually be set through the UI.
      • Frontend/Livewire: As a super admin I want to set an email template's body from the create/edit form so that templates aren't limited to metadata fields.
        #[Test]
        public function it_saves_the_template_body_entered_in_the_create_form(): void
        {
            /* Arrange */
            $payload = [
                'title'   => 'Welcome',
                'type'    => EmailTemplateType::TEXT->value,
                'subject' => 'Hi',
                'body'    => '<p>Hello {client_name}</p>',
            ];
    
            /* Act */
            $component = Livewire::actingAs($this->superAdmin())
                ->test(CreateEmailTemplate::class)
                ->fillForm($payload)
                ->call('create');
    
            /* Assert */
            $component->assertSuccessful()->assertHasNoFormErrors();
            $this->assertDatabaseHas('email_templates', ['body' => '<p>Hello {client_name}</p>']);
        }
    • A body containing <style> tags round-trips through save and retrieval without truncation.
      • PHPUnit: it_saves_and_retrieves_a_body_containing_style_tags_without_truncation()
        #[Test]
        public function it_saves_and_retrieves_a_body_containing_style_tags_without_truncation(): void
        {
            /* Arrange */
            $body = '<style>p { color: red; }</style><p>Hello {client_name}</p>';
    
            /* Act */
            $template = EmailTemplate::factory()->for($this->company)->create(['body' => $body]);
            $loaded   = EmailTemplate::find($template->id);
    
            /* Assert */
            $this->assertSame($body, $loaded->body);
            $this->assertStringContainsString('<style>', $loaded->body);
        }
    • A template without <style> tags still saves and loads correctly (no regression from whatever fix is applied).
      • PHPUnit: it_saves_and_retrieves_a_body_without_style_tags_unchanged()
        #[Test]
        public function it_saves_and_retrieves_a_body_without_style_tags_unchanged(): void
        {
            /* Arrange */
            $body = '<p>Hello {client_name}, please find your invoice attached.</p>';
    
            /* Act */
            $template = EmailTemplate::factory()->for($this->company)->create(['body' => $body]);
    
            /* Assert */
            $this->assertSame($body, $template->fresh()->body);
        }
    • InvoiceService gains a real sendInvoiceEmail(Invoice $invoice) method, so the existing "Send Email" table action stops fatal-erroring.
      • PHPUnit: it_sends_an_invoice_email_using_the_matching_email_template_body()
        #[Test]
        public function it_sends_an_invoice_email_using_the_matching_email_template_body(): void
        {
            /* Arrange */
            Mail::fake();
            $body     = '<style>h1{font-size:24px}</style><p>Invoice attached</p>';
            $template = EmailTemplate::factory()->for($this->company)->create(['body' => $body, 'type' => EmailTemplateType::TEXT->value]);
            $invoice  = Invoice::factory()->for($this->company)->create();
    
            /* Act */
            app(InvoiceService::class)->sendInvoiceEmail($invoice);
    
            /* Assert */
            Mail::assertSent(fn ($mail) => str_contains($mail->render(), '<style>'));
        }
    • The "Send Email" table action on ListInvoices/InvoicesTable completes successfully instead of erroring on a missing method.
      • PHPUnit: it_completes_the_send_email_table_action_without_a_fatal_error()
        #[Test]
        public function it_completes_the_send_email_table_action_without_a_fatal_error(): void
        {
            /* Arrange */
            Mail::fake();
            $invoice = Invoice::factory()->for($this->company)->create();
    
            /* Act */
            $component = Livewire::actingAs($this->user)
                ->test(ListInvoices::class, ['tenant' => Str::lower($this->company->search_code)])
                ->callTableAction('send email', $invoice);
    
            /* Assert */
            $component->assertSuccessful();
        }

    Achievable

    1. Add a body input to Modules/Core/Filament/Admin/Resources/EmailTemplates/Schemas/EmailTemplateForm.php — RichEditor::make('body'), consistent with the RichEditor already used in Modules/Core/Filament/Admin/Pages/Settings.php.
    2. Confirm the EmailTemplate model needs no cast/mutator change — body is already guarded = [] and stored as longText, so no column-level truncation exists; the truncation the reporter is seeing is almost certainly this missing form field (body silently stays empty because nothing writes to it), not a save/retrieve bug.
    3. Implement InvoiceService::sendInvoiceEmail(Invoice $invoice): void — resolve the matching EmailTemplate for the company (type/company-scoped lookup), build the mail with the template's body as HTML content, and send via the existing Mail facade.
    4. If a Filament RichEditor proves to strip <style> on mount (worth verifying once the field exists — Filament's RichEditor is Tiptap-based and may sanitize <style> blocks by default), fall back to a plain Textarea::make('body') for raw HTML authoring instead, since the templates use {placeholder} tokens rather than needing WYSIWYG formatting.
    5. Add the new tests to Modules/Core/Tests/Feature/EmailTemplatesTest.php (body field) and Modules/Invoices/Tests/Feature/ (send-email action).

    Relevant

    Without a body field in the admin UI, email templates are currently unusable for their core purpose (composing the email that gets sent), and the "Send Email" action on invoices is broken regardless of style tags — both block the feature this bug report is about from ever being exercised in production.

    Time-bound

    Sprint 5.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    refinedRefined with SMART plan / test suggestions

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions