PHPackages                             spire/mail - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Templating &amp; Views](/categories/templating)
4. /
5. spire/mail

ActiveLibrary[Templating &amp; Views](/categories/templating)

spire/mail
==========

Visual drag-and-drop email template editor for Laravel

v1.1.0(7mo ago)06MITPHPPHP ^8.2

Since Dec 2Pushed 6mo agoCompare

[ Source](https://github.com/sabristratos/spire-mail)[ Packagist](https://packagist.org/packages/spire/mail)[ Docs](https://github.com/sabristratos/spire-mail)[ RSS](/packages/spire-mail/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (4)Versions (8)Used By (0)

Spire Mail
==========

[](#spire-mail)

[![Latest Version on Packagist](https://camo.githubusercontent.com/819e1a9634debf3460307f6abf7a3e76b7b1acc2ce208df37d1992c82e75fb7e/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f73706972652f6d61696c2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/spire/mail)[![License](https://camo.githubusercontent.com/cbbaae33ac6174189b268b570d332a78712cc2f390b9f38a599a1e0e9bb98a54/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f73706972652f6d61696c2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/spire/mail)

A visual drag-and-drop email template editor for Laravel. Build beautiful, responsive email templates with an intuitive editor and send them using Laravel's mail system.

Features
--------

[](#features)

- Drag-and-drop email editor with real-time preview
- 10+ content blocks (text, heading, image, button, divider, spacer, HTML, video, social icons, multi-column rows)
- MJML-powered responsive rendering
- Advanced tag system with formatters, conditionals, and inline fallbacks
- Global and template-specific tags with editor UI
- 9 built-in formatters (date, currency, uppercase, lowercase, capitalize, truncate, count, number, default)
- Conditional rendering with `{{#if}}`, `{{else}}`, `{{#unless}}`
- Template management with bulk actions
- Test email sending
- Asset upload and management
- Configurable route prefix and authorization
- Vue 3 components for custom integrations

Requirements
------------

[](#requirements)

### Backend

[](#backend)

- PHP 8.2+
- Laravel 11 or 12

### Frontend

[](#frontend)

- Node.js 18+
- Vue 3
- Inertia.js

Quick Start
-----------

[](#quick-start)

```
# Install the Composer package
composer require spire/mail

# Run the install command
php artisan spire-mail:install

# Install the npm package
npm install @sabrenski/spire-mail

# Build your assets
npm run build
```

Access the editor at `/admin/mail` (or your configured route prefix).

Installation
------------

[](#installation)

### Backend (Composer)

[](#backend-composer)

```
composer require spire/mail
```

### Frontend (NPM)

[](#frontend-npm)

The Vue editor components are distributed as a separate npm package:

```
npm install @sabrenski/spire-mail
```

**Peer Dependencies:** Your project must have these packages installed:

PackageVersionDescription`vue`^3.0.0Vue.js framework`@sabrenski/spire-ui-vue`^0.2.0Spire UI component library`@hugeicons/core-free-icons`^2.0.0Icon library`@inertiajs/vue3`^2.0.0Inertia.js Vue adapter### Install Command

[](#install-command)

Run the install command to set up the package:

```
php artisan spire-mail:install
```

**Options:**

OptionDescription`--publish-config`Publish configuration file for customization`--no-migrate`Skip running migrations`--force`Overwrite existing filesConfiguration
-------------

[](#configuration)

### Route Prefix

[](#route-prefix)

By default, routes are registered at `/admin/mail`. Customize this with an environment variable:

```
SPIRE_MAIL_PREFIX=email-admin
```

Routes will then be available at `/email-admin`.

### Environment Variables

[](#environment-variables)

VariableDefaultDescription`SPIRE_MAIL_PREFIX``admin/mail`Route prefix for the admin interface`SPIRE_MAIL_DISK``public`Storage disk for uploaded assets`SPIRE_MAIL_VALIDATE_TAGS``true`Enable/disable required tag validation`SPIRE_MAIL_LOGGING``true`Enable/disable logging`SPIRE_MAIL_LOG_CHANNEL``spire-mail`Log channel name`SPIRE_MAIL_LOG_LEVEL``debug`Log level### Configuration File

[](#configuration-file)

Publish the configuration file for full customization:

```
php artisan spire-mail:install --publish-config
```

Or manually:

```
php artisan vendor:publish --tag=spire-mail-config
```

**Key Configuration Options:**

```
return [
    // Route prefix for the admin interface
    'route_prefix' => env('SPIRE_MAIL_PREFIX', 'admin/mail'),

    // Middleware applied to all routes
    'middleware' => ['web', 'auth'],

    // Authorization settings
    'authorization' => [
        'enabled' => true,
        'gate' => 'manage-mail-templates',
    ],

    // Template defaults
    'templates' => [
        'content_width' => 600,
        'font_family' => 'Arial, sans-serif',
        'background_color' => '#f5f5f5',
        'content_background_color' => '#ffffff',
    ],

    // Asset storage
    'storage' => [
        'disk' => env('SPIRE_MAIL_DISK', 'public'),
        'path' => 'mail-assets',
    ],

    // Global merge tags
    'merge_tags' => [
        'app_name' => fn () => config('app.name'),
        'app_url' => fn () => config('app.url'),
        'current_year' => fn () => date('Y'),
    ],
];
```

### Authorization

[](#authorization)

By default, Spire Mail allows all authenticated users to manage templates. To restrict access, define a gate in your `AppServiceProvider` or `AuthServiceProvider`:

```
use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    Gate::define('manage-mail-templates', function ($user) {
        return $user->hasRole('admin');
    });
}
```

To disable authorization entirely:

```
// config/spire-mail.php
'authorization' => [
    'enabled' => false,
],
```

Sending Emails
--------------

[](#sending-emails)

### Using SpireTemplateMailable

[](#using-spiretemplatemailable)

```
use SpireMail\Mail\SpireTemplateMailable;
use Illuminate\Support\Facades\Mail;

Mail::to('user@example.com')->send(
    new SpireTemplateMailable('welcome-email', [
        'user_name' => 'John Doe',
        'activation_link' => 'https://example.com/activate/abc123',
    ])
);
```

### Using the Trait

[](#using-the-trait)

```
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use SpireMail\Mail\Concerns\UsesSpireTemplate;

class WelcomeEmail extends Mailable
{
    use UsesSpireTemplate;

    public function __construct(protected User $user) {}

    public function envelope(): Envelope
    {
        return new Envelope(subject: $this->getSpireSubject());
    }

    public function content(): Content
    {
        return $this->useTemplate('welcome-email')
            ->withSpireData([
                'user_name' => $this->user->name,
                'user_email' => $this->user->email,
            ])
            ->getSpireContent();
    }
}
```

### Using the Facade

[](#using-the-facade)

```
use SpireMail\Facades\SpireMail;

$html = SpireMail::render('newsletter-template', [
    'headline' => 'Weekly Update',
    'content' => 'Here is your weekly digest...',
]);
```

Tag System
----------

[](#tag-system)

Spire Mail features a powerful tag system for dynamic content with formatters, conditionals, and inline fallbacks.

### Basic Tags

[](#basic-tags)

Use double-brace syntax with dot notation for nested data:

```
Hello {{user.name}},

Welcome to {{app_name}}!

Your order #{{order.id}} was placed on {{order.date}}.

```

### Inline Fallbacks

[](#inline-fallbacks)

Provide default values for missing tags:

```
Hello {{user.nickname|default:Valued Customer}},

Your discount: {{discount_code|default:No discount applied}}

```

### Formatters

[](#formatters)

Apply formatters using pipe syntax:

FormatterSyntaxExampleOutput`date``{{tag|date:format}}``{{order.date|date:d/m/Y}}``25/12/2024``currency``{{tag|currency:code}}``{{total|currency:EUR}}``€99.99``uppercase``{{tag|uppercase}}``{{name|uppercase}}``JOHN DOE``lowercase``{{tag|lowercase}}``{{email|lowercase}}``john@example.com``capitalize``{{tag|capitalize}}``{{name|capitalize}}``John Doe``truncate``{{tag|truncate:length}}``{{desc|truncate:50}}``First 50 chars...``count``{{tag|count}}``{{items|count}}``5``number``{{tag|number:decimals}}``{{price|number:2}}``99.00`### Conditionals

[](#conditionals)

Show or hide content based on data:

```
{{#if user.premium}}
Thank you for being a premium member!
{{/if}}

{{#if order.discount}}
You saved {{order.discount|currency:USD}}!
{{else}}
No discount applied to this order.
{{/if}}

{{#unless user.verified}}
Please verify your email address.
{{/unless}}
```

### Global Tags

[](#global-tags)

Global tags are available in all templates. Define them in `config/spire-mail.php`:

```
'merge_tags' => [
    'app_name' => fn () => config('app.name'),
    'app_url' => fn () => config('app.url'),
    'current_year' => fn () => date('Y'),
    'support_email' => 'support@example.com',
],
```

### Registering Tags Programmatically

[](#registering-tags-programmatically)

Register global tags in a service provider:

```
use SpireMail\Facades\SpireMail;

public function boot(): void
{
    SpireMail::registerTags([
        'company_name' => [
            'value' => 'Acme Inc',
            'label' => 'Company Name',
            'description' => 'The company name',
        ],
        'support_email' => [
            'value' => fn () => config('mail.from.address'),
            'label' => 'Support Email',
            'description' => 'Support contact email',
            'example' => 'support@example.com',
        ],
    ]);

    // Or register a single tag
    SpireMail::registerTag('current_date', [
        'value' => fn () => now()->format('F j, Y'),
        'label' => 'Current Date',
    ]);
}
```

### Template-Specific Tags

[](#template-specific-tags)

Define tags per template in the editor UI (Tags tab) or programmatically:

```
use SpireMail\Models\MailTemplate;

$template = MailTemplate::where('slug', 'order-confirmation')->first();

$template->setTags([
    [
        'key' => 'user.name',
        'label' => 'User Name',
        'description' => 'The customer\'s full name',
        'type' => 'string',
        'required' => true,
        'example' => 'John Doe',
    ],
    [
        'key' => 'order.total',
        'label' => 'Order Total',
        'description' => 'Total order amount',
        'type' => 'number',
        'required' => true,
        'example' => '99.99',
    ],
]);

$template->save();
```

### Tag Validation

[](#tag-validation)

Spire Mail automatically validates that all required tags are provided when sending emails:

```
use SpireMail\Exceptions\MissingRequiredTagsException;
use SpireMail\Mail\SpireTemplateMailable;

try {
    Mail::to($user->email)->send(
        new SpireTemplateMailable('order-confirmation', [
            'user' => ['name' => $user->name],
            // Missing 'order' data - will throw if order.id is required
        ])
    );
} catch (MissingRequiredTagsException $e) {
    Log::error('Missing tags', [
        'template' => $e->templateSlug,
        'missing' => $e->missingTags,
    ]);
}
```

**Validate before queueing:**

```
use SpireMail\Facades\SpireMail;

SpireMail::validateTags('order-confirmation', $data);
Mail::to($user)->queue(new SpireTemplateMailable('order-confirmation', $data));
```

**Disable validation:**

```
SPIRE_MAIL_VALIDATE_TAGS=false
```

Vue Editor Components
---------------------

[](#vue-editor-components)

The npm package exports Vue components for custom integrations.

### Available Exports

[](#available-exports)

```
// Main editor components
import {
    EmailEditor,
    EditorSidebar,
    EditorCanvas,
    EditorProperties,
    PreviewModal,
} from '@sabrenski/spire-mail'

// Canvas components
import { CanvasBlock, CanvasDropZone } from '@sabrenski/spire-mail'

// Block components
import { TextBlock, ImageBlock, ButtonBlock } from '@sabrenski/spire-mail'

// Property panels
import { TextProperties, ImageProperties, ButtonProperties } from '@sabrenski/spire-mail'

// Page components (for custom routing)
import { TemplatesIndex, TemplatesEdit, TemplatesCreate } from '@sabrenski/spire-mail'

// Composables and stores
import { useEditorStore } from '@sabrenski/spire-mail'

// Types
import type {
    EmailBlock,
    EmailSettings,
    TemplateData,
    BlockDefinition,
    GlobalTag,
    TemplateTag,
} from '@sabrenski/spire-mail'

// Layout
import { DefaultLayout } from '@sabrenski/spire-mail'
```

### Custom Layouts

[](#custom-layouts)

The Index and Create pages use Inertia's persistent layout pattern. After publishing the pages, you can customize the layout to use your own admin layout.

> **Note:** The Edit page (email editor) maintains its own full-screen layout and does not support custom layouts.

**Step 1: Publish the pages**

```
php artisan vendor:publish --tag=spire-mail-pages
```

**Step 2: Edit the layout in published files**

Update the Index and Create pages to use your layout:

```
// resources/js/Pages/SpireMail/Templates/Index.vue

import AdminLayout from '@/Layouts/AdminLayout.vue'

defineOptions({
    layout: AdminLayout,
})

// ... rest of the component

```

```
// resources/js/Pages/SpireMail/Templates/Create.vue

import AdminLayout from '@/Layouts/AdminLayout.vue'

defineOptions({
    layout: AdminLayout,
})

// ... rest of the component

```

**Your layout component** should render the default slot:

```
// resources/js/Layouts/AdminLayout.vue

import { Sidebar, Navbar } from '@/Components'

```

### Importing CSS

[](#importing-css)

Include the package CSS in your application:

```
// In your main.ts or app.ts
import '@sabrenski/spire-mail/style.css'
```

### Using EmailEditor

[](#using-emaileditor)

The main editor component for building email templates:

```

import { EmailEditor, PreviewModal } from '@sabrenski/spire-mail'
import type { TemplateData, BlockDefinition, GlobalTag } from '@sabrenski/spire-mail'

interface Props {
    template: { data: TemplateData } | null
    availableBlocks: Record
    globalTags: GlobalTag[]
}

const props = defineProps()

function handleSave(content, settings, tags) {
    // Save template via Inertia or API
}

function handlePreview(content, settings) {
    // Open preview modal
}

```

### Using PreviewModal

[](#using-previewmodal)

Preview and send test emails:

```

import { ref } from 'vue'
import { PreviewModal } from '@sabrenski/spire-mail'

const showPreview = ref(false)
const templateId = ref(1)
const content = ref([])
const settings = ref({})

```

Available Blocks
----------------

[](#available-blocks)

BlockDescriptionTextRich text content with formattingHeadingH1, H2, H3 headingsImageResponsive images with optional linksButtonCall-to-action buttonsDividerHorizontal linesSpacerVertical spacingHTMLRaw HTML contentVideoVideo embeds with thumbnail fallbackSocial IconsSocial media icon linksRowMulti-column layouts (1-3 columns)Custom Block Renderers
----------------------

[](#custom-block-renderers)

Create custom block types:

```
namespace App\Mail\Blocks;

use SpireMail\Rendering\BlockRenderers\BaseBlockRenderer;

class CustomBlockRenderer extends BaseBlockRenderer
{
    public function render(array $block, array $data = []): string
    {
        $props = $block['props'] ?? [];

        return sprintf(
            '%s',
            $this->processMergeTags($props['content'] ?? '', $data)
        );
    }
}
```

Register in config:

```
'blocks' => [
    'custom-block' => \App\Mail\Blocks\CustomBlockRenderer::class,
],
```

API Endpoints
-------------

[](#api-endpoints)

All endpoints use the configured route prefix (default: `/admin/mail`).

EndpointMethodDescription`/`GETTemplate list page`/templates`POSTCreate template`/templates/{id}`GETEdit template page`/templates/{id}`PUTUpdate template`/templates/{id}`DELETEDelete template`/templates/{id}/toggle-status`PATCHToggle active status`/templates/{id}/duplicate`POSTDuplicate template`/templates/{id}/preview`POSTGenerate preview HTML`/templates/{id}/send-test`POSTSend test email`/templates/{id}/tags`GETGet template tags`/templates/{id}/tags`PUTUpdate template tags`/tags`GETList global tags`/tags/formatters`GETList available formatters`/assets/upload`POSTUpload asset`/assets/{filename}`DELETEDelete assetWorking with Templates
----------------------

[](#working-with-templates)

### Query Templates

[](#query-templates)

```
use SpireMail\Models\MailTemplate;

// Get all active templates
$templates = MailTemplate::active()->get();

// Find by slug
$template = MailTemplate::where('slug', 'welcome-email')->first();
```

### Create Templates Programmatically

[](#create-templates-programmatically)

```
$template = MailTemplate::create([
    'name' => 'Welcome Email',
    'subject' => 'Welcome to {{app_name}}!',
    'content' => ['version' => '1.0', 'blocks' => []],
    'is_active' => true,
]);
```

Publishing Assets
-----------------

[](#publishing-assets)

```
php artisan vendor:publish --tag=spire-mail-config      # Configuration
php artisan vendor:publish --tag=spire-mail-migrations  # Migrations
php artisan vendor:publish --tag=spire-mail-views       # Blade views
php artisan vendor:publish --tag=spire-mail-lang        # Language files
php artisan vendor:publish --tag=spire-mail-pages       # Vue pages
php artisan vendor:publish --tag=spire-mail-components  # Vue components
```

Troubleshooting
---------------

[](#troubleshooting)

### Blank page at /admin/mail

[](#blank-page-at-adminmail)

Ensure you've run the install command and built your assets:

```
php artisan spire-mail:install
npm run build
```

### 403 Forbidden

[](#403-forbidden)

Define the authorization gate or disable authorization:

```
// Option 1: Define the gate
Gate::define('manage-mail-templates', fn($user) => $user->isAdmin());

// Option 2: Disable authorization
// config/spire-mail.php
'authorization' => ['enabled' => false],
```

### Missing peer dependencies

[](#missing-peer-dependencies)

Install all required npm packages:

```
npm install vue @sabrenski/spire-ui-vue @hugeicons/core-free-icons @inertiajs/vue3
```

### Asset upload fails

[](#asset-upload-fails)

Ensure your storage is properly configured:

1. Run `php artisan storage:link`
2. Check that `storage/app/public` is writable
3. Verify `SPIRE_MAIL_DISK` is correctly configured

License
-------

[](#license)

MIT License. See [LICENSE](LICENSE) for more information.

###  Health Score

34

—

LowBetter than 75% of packages

Maintenance66

Regular maintenance activity

Popularity4

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity52

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~3 days

Total

7

Last Release

194d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/178621997?v=4)[Mohamed Sabri Ben Chaabane](/maintainers/sabristratos)[@sabristratos](https://github.com/sabristratos)

---

Top Contributors

[![sabristratos](https://avatars.githubusercontent.com/u/178621997?v=4)](https://github.com/sabristratos "sabristratos (8 commits)")

---

Tags

laravelmailemailtemplateeditorvisual editordrag-and-dropmjmlemail-builder

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/spire-mail/health.svg)

```
[![Health](https://phpackages.com/badges/spire-mail/health.svg)](https://phpackages.com/packages/spire-mail)
```

###  Alternatives

[propaganistas/laravel-disposable-email

Disposable email validator

6023.0M6](/packages/propaganistas-laravel-disposable-email)[moonshine/moonshine

Laravel administration panel

1.3k253.1k81](/packages/moonshine-moonshine)[hasinhayder/tyro-dashboard

Tyro Dashboard - Beautiful admin dashboard for managing Tyro roles, privileges, users, and settings

5443.8k](/packages/hasinhayder-tyro-dashboard)[ublabs/blade-simple-icons

A package to easily make use of Simple Icons in your Laravel Blade views.

1963.4k](/packages/ublabs-blade-simple-icons)[technikermathe/blade-lucide-icons

A package to easily make use of Lucide icons in your Laravel Blade views.

18421.4k11](/packages/technikermathe-blade-lucide-icons)[mati365/ckeditor5-livewire

CKEditor 5 integration for Laravel Livewire

447.9k](/packages/mati365-ckeditor5-livewire)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
