Repository navigation
[Core]: Email template rendering issue with style tags #377
Description
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
EmailTemplatebody 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 thetemplate_bodycolumn. - Investigate the Filament schema for
EmailTemplateResource— if usingRichEditor, check thefileAttachmentsDiskand sanitisation config; consider switching toTextareaor 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.phpor a dedicated Action class): ensure theafterStateUpdatedcallback on theemail_template_idselect setsbodyfrom$template->template_bodywithout additional sanitisation. - If a JS/Livewire sanitisation layer is the cause, configure the editor component to allow
<style>tags or usewire:modelon a plainTextarea. - Add a cast or raw getter on
EmailTemplate::$template_bodyif 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
Nathanwoodburn commentedon Nov 2, 2023
G'day,
Is there any plan to allow style tags?
nielsdrost7 commentedon Nov 2, 2023
@Nathanwoodburn no plans just yet, but we're open to pull-requests
nielsdrost7 commentedon Aug 7, 2025
I'm moving this to the list of issues to take on for Version 2.x
[-]Email template issues (when using style tags)[/-][+][Core]: Email template rendering issue with style tags[/+]nielsdrost7 commentedon Jul 16, 2026
SMART Plan — Email template body field (style tags, and the missing body input itself)
Specific
Modules\Core\Models\EmailTemplatestores its body in alongText('body')column (migration:2009_01_01_000003_create_email_templates_table.php) — nottemplate_bodyas the original planning note assumed; the real column is simplybody. Reading the actualEmailTemplateFormschema (Modules/Core/Filament/Admin/Resources/EmailTemplates/Schemas/EmailTemplateForm.php) shows the gap is more fundamental than style-tag stripping: the form has no field forbodyat all — onlytitle,from_name,from_email,cc,bcc,type,subjectare present.EmailTemplatesTest::it_creates_an_email_templateonly passes becausefillForm()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')inModules/Invoices/Filament/Company/Resources/Invoices/Tables/InvoicesTable.php— but it callsInvoiceService::sendInvoiceEmail($record), andsendInvoiceEmail()does not exist anywhere onInvoiceService(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
EmailTemplateFormschema gains a body input (aRichEditor— the same component already used forsettings.default_invoice_termsinModules/Core/Filament/Admin/Pages/Settings.php— or a plainTextareaif HTML must be authored directly) so a body can actually be set through the UI.#[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>']); }bodycontaining<style>tags round-trips through save and retrieval without truncation.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); }<style>tags still saves and loads correctly (no regression from whatever fix is applied).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); }InvoiceServicegains a realsendInvoiceEmail(Invoice $invoice)method, so the existing "Send Email" table action stops fatal-erroring.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>')); }ListInvoices/InvoicesTablecompletes successfully instead of erroring on a missing method.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
Modules/Core/Filament/Admin/Resources/EmailTemplates/Schemas/EmailTemplateForm.php—RichEditor::make('body'), consistent with theRichEditoralready used inModules/Core/Filament/Admin/Pages/Settings.php.EmailTemplatemodel needs no cast/mutator change —bodyis alreadyguarded = []and stored aslongText, 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.InvoiceService::sendInvoiceEmail(Invoice $invoice): void— resolve the matchingEmailTemplatefor the company (type/company-scoped lookup), build the mail with the template'sbodyas HTML content, and send via the existingMailfacade.RichEditorproves to strip<style>on mount (worth verifying once the field exists — Filament'sRichEditoris Tiptap-based and may sanitize<style>blocks by default), fall back to a plainTextarea::make('body')for raw HTML authoring instead, since the templates use{placeholder}tokens rather than needing WYSIWYG formatting.Modules/Core/Tests/Feature/EmailTemplatesTest.php(body field) andModules/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.