finity-labs/fin-mail
FinMail adds an email template manager and composer to Filament. Create dynamic, translatable templates with token/merge-tag replacement, CTA blocks, and versioning. Send from any resource via a reusable action, with full email logging and status tracking.
{% if %} conditionals now work at any depth, with {% else %} supported on every level. Previously the first {% endif %} closed the outermost {% if %}, so any nested conditional produced broken output. Conditionals are now parsed in a single pass with a frame stack, and malformed markup (a stray {% else %}/{% endif %} or an unclosed {% if %}) stays visible in the output instead of silently losing content (thanks @Pycckoe, whose fork flagged the limitation)->emailTemplateResource(MyResource::class), ->emailThemeResource(...), ->sentEmailResource(...) (#22)policyNamespace() policy mapping now registers policies whenever the classes exist, with or without Shield installed. Settings pages can be gated by defining Gate abilities named after the page class (page_ManageGeneralSettings, …); pages without a defined ability stay open as before (#21)TemplateMail::withoutLogo() sends without the branding logo, and overrideBranding([...]) overrides any branding key (logo, colors, footer links, …) for a single email while the rest falls through to the saved settings (#8)auth_emails.store_rendered_body config option (default false). When enabled, the password reset and verification overrides store their rendered body in the Sent Emails log like any other email, for teams that want a full audit trail of exactly what was sent. Left off, the signed reset/verification URLs stay out of the databaseTemplateMail gained overridePreheader() and the compose path uses it. Tokens in the overridden preheader are replaced like in the bodyTypeError when the configured editor stores the body as a TipTap document array (e.g. the Tiptap editor). The preview already converted the document to HTML; the send path now does the same, using the template's theme colors for custom blocksTemplateMail whose log entry is first created after queue serialization (e.g. dispatch-time log insert failed, or the mailable was re-dispatched from a stored job) no longer renders in the worker's app localeuser-verify-email or user-password-reset is missing, deactivated, or errors during lookup, the notification falls back to Laravel's default mail instead of throwing — deactivating a template can no longer take down password reset app-widekey and category protection server-side. The fields were only disabled() in the form but still dehydrated, so a crafted request could rewrite a locked system template's key — which the auth email overrides depend ondeleting hook, so every delete path behaves the same — including on SQLite databases without foreign key enforcement (enforcing databases were already covered by the FK's nullOnDelete)sandbox="allow-same-origin"), matching the template preview. Stored bodies can contain recipient-influenced content, and the unsandboxed frames allowed scripts to run inside the admin panelinvoice-copy, invoice-copy-2, …) instead of a -copy-<timestamp> suffix that could collide when two replications happened in the same secondSentEmailResender servicefin-mail:cleanup command now registers reliably: registration uses callAfterResolving(), so it also works when the schedule was already resolved before the package booted — previously the command could silently never appear in schedule:list (#26, thanks @agencetwogether)TemplateMail, instead of re-wrapping the full stored document in the email layout — which produced a nested document with two doctypes and broken logo/footer sections (#25, thanks @agencetwogether)ComposeEmail picks the right form via trans_choice(), and 37 locales gained singular|plural variants for the notification body. Locales without grammatical plural after numerals keep their single formTemplateMail now creates a Sent Emails log entry on its own whenever logging is enabled in the settings, so emails sent from code show up in the log just like ones sent from the admin panel. Queued mail is logged at dispatch time with a Queued status and updated to Sent or Failed once the worker processes it. Log creation failures are reported but never block the email from going out (#23, thanks @gazohu)TemplateMail::withoutLogging() opts a single email out of loggingTemplateMail::withLogging() called without an argument now forces a log entry even when logging is disabled in the settings. Passing a SentEmail still hands over an externally created record, as before — previously a bare withLogging() call silently did nothingTemplateMail::withoutStoringRenderedBody() logs the email but keeps the rendered HTML out of the databaseEmailSender gained a notify constructor flag so callers batching multiple sends can suppress the per-send notification and show an aggregate one instead/images/logo.png) are now resolved to absolute URLs at render time via BrandingSettings::resolvedLogo(), so they display in email clients. Absolute URLs, protocol-relative URLs, and data URIs pass through unchanged (#19, thanks @mrsafalpiya)EmailTemplate::render() gained a renderBlocks flag so blocks round-trip in the editor, and TemplateMail now expands blocks in overridden bodies (#17, thanks @mrsafalpiya)EmailTheme::resolvedDefaultColors() and EmailTemplate::resolvedThemeColors() (#18, thanks @mrsafalpiya)null or an empty string (cleared ColorPickers) no longer override the hardcoded defaults when resolving colorsUndefined array key "cleanup_frequency" when saving the Logging settings with Enable Schedule Cleanup turned off. The frequency field is hidden in that state, so it isn't submitted — the save mutation now casts it only when it's present and leaves the stored value untouched otherwise (#16, thanks @Wijnands)overrideView() method on TemplateMail renders an email with your own Blade layout instead of the package's fin-mail::email.default, while keeping database-driven templates, token replacement, theming, logging, and attachments. The custom view receives the same variables as the default one ($body, $preheader, $theme, $branding) plus anything passed via with() or extraData(). Existing emails are unaffected if you don't call it (#14, thanks @agencetwogether)MissingSettings exception during artisan boot when scheduled cleanup is registered before the fin-mail-logging settings have been migrated. The catch around app(LoggingSettings::class) didn't cover the lazy property access that actually triggers the load, so the exception escaped and broke package:discover and fin-mail:install in some setups (#13, thanks @devrizzz)spatie/laravel-settings is now mentioned explicitly in the README as an auto-installed dependencyPreview, SendTest, Compose (Email Templates) and Resend (Sent Emails) are now hidden from the UI when Filament Shield is installed and the authenticated user lacks the corresponding permission. Falls back to the previous always-visible behavior when Shield is absent, so existing installs are unaffected (#12, thanks @agencetwogether)FinMailPlugin::isShieldAvailable() helper for checking Shield presencepreview_heading translation key for the preview modal header, populated across all 58 supported localesInstallCommand now seeds preview, sendTest, compose, and resend into the Filament Shield config so shield:generate produces the matching policy methods and permissionsauthorizeIndividualRecords('delete') when Shield is activephp artisan shield:generate --panel=admin --option=policies_and_permissions to register the new permissionsextraData() method (and native with() support) on TemplateMail for passing variables directly to the Blade view, separate from the token replacement system. Useful for view-only data that doesn't need to flow through the token engine (#10, thanks @agencetwogether)null if not set, so existing templates are unaffected. The TemplateMail mailable also gains an overrideReplyTo() setter for runtime overrides (#9, thanks @agencetwogether)add_reply_to_on_email_templates_table). Run php artisan migrate after upgrading.fin-mail.php config instead of hardcoded defaults, fixing issues with foreign key references when table names are customized (#7, thanks @agencetwogether)FinMailPlugin::make()->customBlocks([...]). Custom blocks now render correctly in the editor, preview mode, and sent emails. ButtonBlock is always included by default. Closes #6EmailTemplate, TipTapConverter, and DefaultEditor now reads from a dynamic plugin-level registry instead of a hardcoded listdate_format and datetime_format config options, supporting a single string or a per-locale array. When null, Filament's defaults apply. Includes FinMail::dateFormat() and FinMail::dateTimeFormat() facade helpersconfig.* and user.*), pre-filled with example values from the token schemaTextEntry, ViewEntry, and badge components[@property](https://github.com/property) annotations on SentEmail model for PHPStanEmailSender, so they appear in the sent emails logversioning.preview, sent.preview.*, settings.sections.add_additional_senders, and settings.sections.add_footer_links across all 58 non-en/fr languagesconfig/fin-mail.phpSentEmailInfolist schema instead of a blade view<details> tagsphp artisan fin-mail:upgrade command to migrate existing data after package updates (supports --dry-run)Str::limit(), causing a TypeError<a> tags in seeded templates (Password Reset, Verify Email) were stripped by TipTap due to font-weight: 600 conflicting with the link mark; buttons now use the native customBlock format<div data-type="customBlock"> were not converted to visible HTML in the View page preview and Compose page previewblocks and versioning translation keys for all 59 supported languagesrenderCustomBlocks() is now public for use by preview componentscreatedBy to prevent lazy loading violationsIf you have existing seeded templates with buttons (Password Reset, Verify Email), run the upgrade command to convert them to the new format:
php artisan fin-mail:upgrade
You can preview what would change first with --dry-run:
php artisan fin-mail:upgrade --dry-run
<style> blocks{{ token }} text in preview and sent emailsTemplateMail mailable that loads content from the database, no need for per-template Mailable classes{{ user.name }}), config values ({{ config.app.name }}), conditionals ({% if user.is_premium %}), and fallbacks ({{ user.name | 'Customer' }})spatie/laravel-translatable, all locales stored in a single recordEditorContractSendEmailAction and SentEmailsRelationManager drop into any Filament resourceEmailSending, EmailSent, EmailFailed, and TemplateUpdated events for application-level hooksHow can I help you explore Laravel packages today?