You are currently reading the documentation for v6, while the latest version is v8.

Upgrading

On this page:

Upgrading to v6

This version adds numerous new features and a completely new look to Mailcoach.

Some notable features:

  • Completely new design with improved UX
  • Add & manage multiple mailers with different providers with automatic setup
  • Show a website archive of your email list’s campaigns
  • A full-featured template system
  • A new & improved Markdown editor
  • Send outgoing webhooks
  • Improved list insights & charts
  • A new “Manage preferences” screen where subscribers can manage the (public) tags attached to them
  • A command palette
  • Automations can be configured to run more than once for a subscriber
  • The ability to override, replace and extend every page of Mailcoach

Updating your composer.json

  • If your composer.json contains spatie/mailcoach-ui, you can remove this as it’s now included within the core laravel-mailcoach package.
  • Update the requirement of spatie/laravel-mailcoach to ^6.0
  • Mailcoach now requires PHP 8.1, so make sure to update that requirement as well
"require": {
-     "php": "^8.0",
+     "php": "^8.1",
    "fruitcake/laravel-cors": "^2.0.5",
    "guzzlehttp/guzzle": "^7.2",
    "laravel/framework": "^9.0",
    "laravel/horizon": "^5.9",
    "laravel/tinker": "^2.7",
-     "spatie/laravel-mailcoach": "^5.0",
+     "spatie/laravel-mailcoach": "^6.0",
-     "spatie/mailcoach-ui": "^5.0"
},

Composer scripts

  • The composer hook command has been renamed, make sure to edit this in your scripts section if it is present, usually only in a standalone installation:
  • The necessary migrations will now be published by the publish command
"scripts": {
    "post-autoload-dump": [
        "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
        "@php artisan package:discover --ansi"
    ],
    "post-root-package-install": [
        "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
    ],
    "post-create-project-cmd": [
        "@php artisan key:generate --ansi",
-         "@php artisan vendor:publish --tag mailcoach-migrations",
-         "@php artisan vendor:publish --tag mailcoach-ui-migrations",
        "@php artisan mailcoach:prepare-git-ignore",
-         "@php artisan mailcoach:execute-composer-hook"
+         "@php artisan mailcoach:publish"
    ],
    "post-update-cmd": [
-         "@php artisan mailcoach:execute-composer-hook",
+         "@php artisan mailcoach:publish",
        "@php artisan vendor:publish --tag=laravel-assets --ansi --force"
    ]
}

Remove the mailcoach-ui routes

Mailcoach UI required separate routes to be registered, in the standalone version these are registered inside your RouteServiceProvider, you can remove these and replace them by the default Mailcoach route macro:

public function boot()
{
    ...

-     Route::mailcoachUi('/');
+     Route::mailcoach('/');

    ...
}

After this, you can run a composer update

Update your Schedule

There have been a few changes to the scheduled commands, make sure your Mailcoach commands in your app/Console/Kernel.php file look like this:

protected function schedule(Schedule $schedule)
{
    $schedule->command('mailcoach:send-automation-mails')->everyMinute();
    $schedule->command('mailcoach:send-scheduled-campaigns')->everyMinute();
    $schedule->command('mailcoach:send-campaign-mails')->everyMinute();

    $schedule->command('mailcoach:run-automation-triggers')->everyMinute();
    $schedule->command('mailcoach:run-automation-actions')->everyMinute();

    $schedule->command('mailcoach:calculate-statistics')->everyMinute();
    $schedule->command('mailcoach:calculate-automation-mail-statistics')->everyMinute();
    $schedule->command('mailcoach:send-campaign-summary-mail')->hourly();
    $schedule->command('mailcoach:cleanup-processed-feedback')->hourly();
    $schedule->command('mailcoach:send-email-list-summary-mail')->mondays()->at('9:00');
    $schedule->command('mailcoach:delete-old-unconfirmed-subscribers')->daily();
}

Methods like runInBackground() and withoutOverlapping() are no longer necessary as we dispatch unique jobs inside the command instead.

Update the login route

Mailcoach has now prefixed its authentication routes, if you don’t have any login route of your own, add the following to your app/Exceptions/Handler.php:

protected function unauthenticated($request, AuthenticationException $exception)
{
    return $this->shouldReturnJson($request, $exception)
        ? response()->json(['message' => $exception->getMessage()], 401)
        : redirect()->guest($exception->redirectTo() ?? route('mailcoach.login'));
}

Configuration updates

  • Delete the config/mailcoach-ui.php file if present in your installation, be sure to port over any changes to the config/mailcoach.php config file.

A full diff of the config file can be found here

We recommend running php artisan vendor:publish --tag=mailcoach-config --force which will override your old configuration file with the new one. Using a git diff you can then re-apply any changes you previously made to your configuration file.

Notable changes to the config file are documented below:

Throttling

Throttling config is no longer set inside the Mailcoach configuration file, these settings can be controlled for each mailer

Welcome mails

Any configuration relating to welcome mails have been removed, you can now create a welcome automation instead.

Livewire

All views now use Livewire components which you can override in the config to add your own functionality if necessary.

Horizon

A schedule queue was added to Mailcoach, make sure your horizon config contains it:

'mailcoach-general' => [
    'connection' => 'mailcoach-redis',
-     'queue' => ['general', 'mailcoach', 'mailcoach-feedback', 'send-mail', 'send-automation-mail'],
+     'queue' => ['general', 'mailcoach-schedule', 'mailcoach', 'mailcoach-feedback', 'send-mail', 'send-automation-mail'],
    'balance' => 'auto',
    'processes' => 10,
    'tries' => 2,
    'timeout' => 60 * 60,
],

If you’re using simple balancing, make sure the schedule queue is defined early as that determines priority.

User model

If you use your own User model, make sure to replace the one in config/mailcoach.php with your own:

'models' => [
    ...
-     'user' => \Spatie\Mailcoach\Domain\Settings\Models\User::class,
+     'user' => \App\Models\User::class,
    ...
]

The standalone starter project defined the Auth user model as \Spatie\MailcoachUi\Models\User, replace this with the new User model, or your own model in config/auth.php.

'users' => [
    'driver' => 'eloquent',
-     'model' => Spatie\MailcoachUi\Models\User::class,
+     'model' => \Spatie\Mailcoach\Domain\Settings\Models\User::class,
],

Views

Delete the resources/views/vendor/mailcoach folder if you did not have any changes made to the views.

Otherwise, we recommend deleting the folder anyway and re-publishing to port over any of your changes.

Run php artisan view:clear to delete any compiled views.

Migration changes

There have been a few additions and changes to the Mailcoach tables, you can use the migration below to update your database:

Some notable changes:

  • All models now have a UUID
  • Custom confirmation mails now use transactional mails instead of raw html

Make sure you have doctrine/dbal installed in your app for rename migrations to work.

composer require doctrine/dbal

Below you can find the full upgrade to v6 migration, depending on how much data you have this could take a long time. It might be a good idea to split this up into separate migrations.

<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;

return new class extends Migration
{
    public function up()
    {
        Schema::table('mailcoach_email_lists', function (Blueprint $table) {
            $table->unique('uuid');

            $table->foreignId('confirmation_mail_id')->after('requires_confirmation')->nullable();

            $table->after('allowed_form_extra_attributes', function (Blueprint $table) {
                $table->string('honeypot_field')->nullable();
                $table->boolean('has_website')->default(false);
                $table->boolean('show_subscription_form_on_website')->default(true);
                $table->string('website_slug')->nullable();
                $table->string('website_title')->nullable();
                $table->text('website_intro')->nullable();
                $table->string('website_primary_color')->nullable();
                $table->string('website_theme')->default('default');
                $table->text('website_subscription_description')->nullable();
            });

            $table->dropColumn([
                'confirmation_mail_subject',
                'confirmation_mail_content',
                'send_welcome_mail',
                'welcome_mail_subject',
                'welcome_mail_content',
                'welcome_mailable_class',
                'welcome_mail_delay_in_minutes',
            ]);
        });

        Schema::table('mailcoach_subscribers', function (Blueprint $table) {
            $table->unique('uuid');
        });

        Schema::table('mailcoach_segments', function (Blueprint $table) {
            $table->uuid('uuid')->after('id');
        });
        DB::table('mailcoach_segments')->update([
            'uuid' => DB::raw('uuid()'),
        ]);
        Schema::table('mailcoach_segments', function (Blueprint $table) {
            $table->unique('uuid');
        });

        Schema::table('mailcoach_campaigns', function (Blueprint $table) {
            $table->unique('uuid');
            $table->index('sent_at');
            $table->index(['scheduled_at', 'status']);

            $table->boolean('show_publicly')->after('subject')->default(true);
            $table->unsignedBigInteger('template_id')->after('email_list_id')->nullable();

            $table->after('segment_description', function (Blueprint $table) {
                $table->boolean('add_subscriber_tags')->default(false);
                $table->boolean('add_subscriber_link_tags')->default(false);
            });

            $table->dropColumn([
                'track_opens',
                'track_clicks',
            ]);
        });

        Schema::table('mailcoach_campaign_links', function (Blueprint $table) {
            $table->uuid('uuid')->after('id');
        });
        DB::table('mailcoach_campaign_links')->update([
            'uuid' => DB::raw('uuid()'),
        ]);
        Schema::table('mailcoach_campaign_links', function (Blueprint $table) {
            $table->unique('uuid');
        });

        Schema::rename('mailcoach_transactional_mails', 'mailcoach_transactional_mail_log_items');
        Schema::table('mailcoach_transactional_mail_log_items', function (Blueprint $table) {
            $table->uuid('uuid')->after('id');
            $table->json('attachments')->after('bcc')->nullable();

            $table->dropColumn([
                'track_opens',
                'track_clicks',
            ]);
        });
        DB::table('mailcoach_transactional_mail_log_items')->update([
            'uuid' => DB::raw('uuid()'),
        ]);
        Schema::table('mailcoach_transactional_mail_log_items', function (Blueprint $table) {
            $table->unique('uuid');
        });

        Schema::table('mailcoach_automation_mails', function (Blueprint $table) {
            $table->unique('uuid');
            $table->unsignedBigInteger('template_id')->after('subject')->nullable();

            $table->after('utm_tags', function (Blueprint $table) {
                $table->boolean('add_subscriber_tags')->default(false);
                $table->boolean('add_subscriber_link_tags')->default(false);
            });

            $table->dropColumn([
                'track_opens',
                'track_clicks',
            ]);
        });

        Schema::table('mailcoach_sends', function (Blueprint $table) {
            $table->renameColumn('transactional_mail_id', 'transactional_mail_log_item_id');
            $table->timestamp('invalidated_at')->after('sent_at')->nullable();

            $table->index('transport_message_id');
            $table->index(['sending_job_dispatched_at', 'sent_at'], 'sent_index');
        });

        Schema::table('mailcoach_campaign_clicks', function (Blueprint $table) {
            $table->uuid('uuid')->after('id');
        });
        DB::table('mailcoach_campaign_clicks')->update([
            'uuid' => DB::raw('uuid()'),
        ]);
        Schema::table('mailcoach_campaign_clicks', function (Blueprint $table) {
            $table->unique('uuid');
        });

        Schema::table('mailcoach_campaign_opens', function (Blueprint $table) {
            $table->uuid('uuid')->after('id');
        });
        DB::table('mailcoach_campaign_opens')->update([
            'uuid' => DB::raw('uuid()'),
        ]);
        Schema::table('mailcoach_campaign_opens', function (Blueprint $table) {
            $table->unique('uuid');
        });

        Schema::table('mailcoach_campaign_unsubscribes', function (Blueprint $table) {
            $table->uuid('uuid')->after('id');
        });
        DB::table('mailcoach_campaign_unsubscribes')->update([
            'uuid' => DB::raw('uuid()'),
        ]);
        Schema::table('mailcoach_campaign_unsubscribes', function (Blueprint $table) {
            $table->unique('uuid');
        });

        Schema::table('mailcoach_send_feedback_items', function (Blueprint $table) {
            $table->uuid('uuid')->after('id');
        });
        DB::table('mailcoach_send_feedback_items')->update([
            'uuid' => DB::raw('uuid()'),
        ]);
        Schema::table('mailcoach_send_feedback_items', function (Blueprint $table) {
            $table->unique('uuid');
        });

        Schema::table('mailcoach_templates', function (Blueprint $table) {
            $table->uuid('uuid')->after('id');
            $table->boolean('contains_placeholders')->after('name')->default(false);
        });
        DB::table('mailcoach_templates')->update([
            'uuid' => DB::raw('uuid()'),
        ]);
        Schema::table('mailcoach_templates', function (Blueprint $table) {
            $table->unique('uuid');
        });

        Schema::table('mailcoach_subscriber_imports', function (Blueprint $table) {
            $table->unique('uuid');
            $table->text('errors')->after('imported_subscribers_count')->nullable();

            $table->dropColumn('error_count');
        });

        Schema::table('mailcoach_tags', function (Blueprint $table) {
            $table->uuid('uuid')->after('id');
            $table->boolean('visible_in_preferences')->after('type')->default(false);
        });
        DB::table('mailcoach_tags')->update([
            'uuid' => DB::raw('uuid()'),
        ]);
        Schema::table('mailcoach_tags', function (Blueprint $table) {
            $table->unique('uuid');
        });

        Schema::table('mailcoach_email_list_subscriber_tags', function (Blueprint $table) {
            $table->index(['subscriber_id', 'tag_id'], 'subscriber_id_tag_id_index');
        });

        Schema::table('mailcoach_automations', function (Blueprint $table) {
            $table->unique('uuid');

            $table->after('interval', function (Blueprint $table) {
                $table->boolean('repeat_enabled')->default(false);
                $table->boolean('repeat_only_after_halt')->default(true);
            });
        });

        Schema::table('mailcoach_automation_actions', function (Blueprint $table) {
            $table->unique('uuid');
        });

        Schema::table('mailcoach_automation_triggers', function (Blueprint $table) {
            $table->unique('uuid');
        });

        Schema::table('mailcoach_automation_action_subscriber', function (Blueprint $table) {
            $table->uuid('uuid')->after('id');

            $table->index('action_id');
            $table->index('subscriber_id');
        });
        DB::table('mailcoach_automation_action_subscriber')->update([
            'uuid' => DB::raw('uuid()'),
        ]);
        Schema::table('mailcoach_automation_action_subscriber', function (Blueprint $table) {
            $table->unique('uuid');
        });

        Schema::table('mailcoach_automation_mail_opens', function (Blueprint $table) {
            $table->uuid('uuid');
        });
        DB::table('mailcoach_automation_mail_opens')->update([
            'uuid' => DB::raw('uuid()'),
        ]);
        Schema::table('mailcoach_automation_mail_opens', function (Blueprint $table) {
            $table->unique('uuid');
        });

        Schema::table('mailcoach_automation_mail_links', function (Blueprint $table) {
            $table->uuid('uuid');
        });
        DB::table('mailcoach_automation_mail_links')->update([
            'uuid' => DB::raw('uuid()'),
        ]);
        Schema::table('mailcoach_automation_mail_links', function (Blueprint $table) {
            $table->unique('uuid');
        });

        Schema::table('mailcoach_automation_mail_clicks', function (Blueprint $table) {
            $table->uuid('uuid');
        });
        DB::table('mailcoach_automation_mail_clicks')->update([
            'uuid' => DB::raw('uuid()'),
        ]);
        Schema::table('mailcoach_automation_mail_clicks', function (Blueprint $table) {
            $table->unique('uuid');
        });

        Schema::table('mailcoach_automation_mail_unsubscribes', function (Blueprint $table) {
            $table->uuid('uuid');
        });
        DB::table('mailcoach_automation_mail_unsubscribes')->update([
            'uuid' => DB::raw('uuid()'),
        ]);
        Schema::table('mailcoach_automation_mail_unsubscribes', function (Blueprint $table) {
            $table->unique('uuid');
        });

        Schema::table('mailcoach_transactional_mail_opens', function (Blueprint $table) {
            $table->uuid('uuid')->index();
        });
        DB::table('mailcoach_transactional_mail_opens')->update([
            'uuid' => DB::raw('uuid()'),
        ]);
        Schema::table('mailcoach_transactional_mail_opens', function (Blueprint $table) {
            $table->unique('uuid');
        });

        Schema::table('mailcoach_transactional_mail_clicks', function (Blueprint $table) {
            $table->uuid('uuid');
        });
        DB::table('mailcoach_transactional_mail_clicks')->update([
            'uuid' => DB::raw('uuid()'),
        ]);
        Schema::table('mailcoach_transactional_mail_clicks', function (Blueprint $table) {
            $table->unique('uuid');
        });

        Schema::rename('mailcoach_transactional_mail_templates', 'mailcoach_transactional_mails');
        Schema::table('mailcoach_transactional_mails', function (Blueprint $table) {
            $table->uuid('uuid')->after('id');
            $table->unsignedBigInteger('template_id')->after('bcc')->nullable();

            $table->dropColumn([
                'track_opens',
                'track_clicks',
            ]);
        });
        DB::table('mailcoach_transactional_mails')->update([
            'uuid' => DB::raw('uuid()'),
        ]);
        Schema::table('mailcoach_transactional_mails', function (Blueprint $table) {
            $table->unique('uuid');        
        });

        if (! Schema::hasColumn('users', 'welcome_valid_until')) {
            Schema::table('users', function (Blueprint $table) {
                $table->timestamp('welcome_valid_until')->nullable();
            });
        }

        if (! Schema::hasTable('mailcoach_uploads')) {
            Schema::create('mailcoach_uploads', function (Blueprint $table) {
                $table->bigIncrements('id');
                $table->uuid('uuid')->unique();
                $table->timestamps();
            });
        } else {
            Schema::table('mailcoach_uploads', function (Blueprint $table) {
                $table->uuid('uuid')->after('id');
            });
            DB::table('mailcoach_uploads')->update([
                'uuid' => DB::raw('uuid()'),
            ]);
            Schema::table('mailcoach_uploads', function (Blueprint $table) {
                $table->unique('uuid');
            });
        }

        if (! Schema::hasTable('mailcoach_settings')) {
            Schema::create('mailcoach_settings', function (Blueprint $table) {
                $table->string('key')->index();
                $table->longText('value')->nullable();
            });
        }

        Schema::create('mailcoach_mailers', function (Blueprint $table) {
            $table->id();
            $table->uuid()->unique();
            $table->string('name');
            $table->string('config_key_name')->index();
            $table->string('transport');
            $table->longText('configuration')->nullable();
            $table->boolean('default')->default(false);
            $table->boolean('ready_for_use')->default(false);
            $table->timestamps();
        });

        Schema::create('mailcoach_webhook_configurations', function (Blueprint $table) {
            $table->id();
            $table->uuid()->unique();
            $table->string('name');
            $table->text('url');
            $table->string('secret');
            $table->boolean('use_for_all_lists')->default(true);
            $table->timestamps();
        });

        Schema::create('mailcoach_webhook_configuration_email_lists', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('webhook_configuration_id');
            $table->unsignedBigInteger('email_list_id');
            $table->timestamps();

            $table
                ->foreign('webhook_configuration_id', 'wc_idx')
                ->references('id')->on('mailcoach_webhook_configurations')
                ->cascadeOnDelete();

            $table
                ->foreign('email_list_id', 'mel_idx')
                ->references('id')->on('mailcoach_email_lists')
                ->cascadeOnDelete();
        });
        
        if (! Schema::hasColumn('personal_access_tokens', 'expires_at')) {
            Schema::table('personal_access_tokens', function (Blueprint $table) {
                $table->timestamp('expires_at')->after('last_used_at')->nullable();
            });
        }
    }
};

If you’re already using Laravel MediaLibrary make sure that the media table in your project has the same fields as the media migration included with Mailcoach.

Open & Click tracking

The Campaign, AutomationMail and TransactionalMail no longer have the ability to enable/disable open & click tracking.

This is because this is a setting at the provider level that Mailcoach has no direct control over. You could disable tracking in Mailcoach but still have tracking links & pixels present because the settings was enabled at your provider.

When setting up mailers through the new mailer UI, Mailcoach will make the necessary API calls to enable/disable tracking for that provider.

Transactional models renamed

The Spatie\Mailcoach\Domain\TransactionalMail\Models\TransactionalMail model has been renamed to Spatie\Mailcoach\Domain\TransactionalMail\Models\TransactionalMailLogItem

The Spatie\Mailcoach\Domain\TransactionalMail\Models\TransactionalMailTemplate model has been renamed to Spatie\Mailcoach\Domain\TransactionalMail\Models\TransactionalMail

Make sure to rename these in the correct order if you reference these inside your application.

Translations

We’ve reworked how Mailcoach translations work, if you had published them make sure to republish the translations.

If you referenced Mailcoach translations somewhere in your app, make sure to replace it by the new helper, the mailcoach - prefix is no longer necessary:

- {{ __('mailcoach - Some translation string') }}
+ {{ __mc('Some translation string') }}

- {{ trans_choice('mailcoach - One|More', 2) }}
+ {{ __mc_choice('One|More', 2) }}

API

All API resources & endpoints now use uuids instead of ids for references to models. Update your integrations with the API if necessary.

Unlayer users

This version of Mailcoach uses an newer version of Unlayer. Your old templates might not be compatible. To make Unlayer templates compatible, you might try the code mentioned in this issue. Also take look at this issue

If you still reference Route::mailcoachUnlayer() somewhere in project, you should remove it.

Changelog
Troubleshooting