Upgrading
On this page:
From v4 to v5
This version adds full support for Laravel 9. Under the hood a lot of performance improvements have been made, which decrease load and increase reliability when sending large campaigns.
Updating the database
Some changes were made to the database, use the migration below to update your database to the latest schema:
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; return new class extends Migration { public function up() { Schema::table('mailcoach_campaigns', function (Blueprint $table) { $table->dropColumn(['send_batch_id', 'all_jobs_added_to_batch_at']); $table->timestamp('all_sends_created_at')->nullable(); $table->timestamp('all_sends_dispatched_at')->nullable(); }); Schema::table('mailcoach_email_lists', function(Blueprint $table) { $table->string('automation_mailer')->after('campaign_mailer')->nullable(); }); Schema::table('mailcoach_sends', function (Blueprint $table) { $table->timestamp('sending_job_dispatched_at')->nullable(); }); Schema::table('mailcoach_automation_action_subscriber', function (Blueprint $table) { $table->timestamp('job_dispatched_at')->nullable(); }); // uncomment this if your `webhook_calls` table doesn't have a `url` or `headers` column /* Schema::table('webhook_calls', function (Blueprint $table) { $table->string('url')->nullable(); $table->json('headers')->nullable(); }); */ } };
The job_batches
table is no longer necessary, if you’re not using it in your own project, feel free to remove this table from your database.
You should set the automation_mailer
value to the name of the mailer that will send automation mails. Usually it’s safe to copy the value from the campaign_mailer
column.
For any campaigns / automations that have already been sent, you can optionally fill the all_sends_created_at
, all_sends_dispatched_at
, sending_job_dispatched_at
, job_dispatched_at
with the current date.
Updating the config file
The config file was changed. Change the throttling
key to the following structure in both the campaigns
and automations
key, and add send_campaign_maximum_job_runtime_in_seconds
and send_automation_mails_maximum_job_runtime_in_seconds
keys respectively
Or you could publish the config file again using php artisan vendor:publish --tag=mailcoach-config --force
and review the changes yourself.
'campaigns' => [ ... /* * By default only 10 mails per second will be sent to avoid overwhelming your * e-mail sending service. */ 'throttling' => [ 'allowed_number_of_jobs_in_timespan' => 10, 'timespan_in_seconds' => 1, /* * Throttling relies on the cache. Here you can specify the store to be used. * * When passing `null`, we'll use the default store. */ 'cache_store' => null, ], /* * The job that will send a campaign could take a long time when your list contains a lot of subscribers. * Here you can define the maximum run time of the job. If the job hasn't fully sent your campaign, it * will redispatch itself. */ 'send_campaign_maximum_job_runtime_in_seconds' => 60 * 10, ... ], 'automation' => [ ... /* * By default only 10 mails per second will be sent to avoid overwhelming your * e-mail sending service. */ 'throttling' => [ 'allowed_number_of_jobs_in_timespan' => 10, 'timespan_in_seconds' => 1, /* * Throttling relies on the cache. Here you can specify the store to be used. * * When passing `null`, we'll use the default store. */ 'cache_store' => null, ], /* * The job that will send a campaign could take a long time when your list contains a lot of subscribers. * Here you can define the maximum run time of the job. If the job hasn't fully sent your campaign, it * will redispatch itself. */ 'send_automation_mails_maximum_job_runtime_in_seconds' => 60 * 10, ... ]
Throttling is managed differently, and Redis isn’t required for large lists anymore. It does rely on cache, and you can specify any store you’d like in the cache_store
key of the mailcoach
config file.
Two new commands have been added and we’ve made some changes to the suggested way of running the commands, the new scheduled commands should look like this:
$schedule->command('mailcoach:send-automation-mails')->everyMinute()->withoutOverlapping()->runInBackground(); $schedule->command('mailcoach:send-scheduled-campaigns')->everyMinute()->withoutOverlapping()->runInBackground(); $schedule->command('mailcoach:run-automation-triggers')->everyMinute()->runInBackground(); $schedule->command('mailcoach:run-automation-actions')->everyMinute()->runInBackground(); $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();
Updating the mail provider packages
The mail provider packages have been updated. Make sure to update the one you are using to this minimum version
-
spatie/laravel-mailcoach-mailgun-feedback
: ^4.0 -
spatie/laravel-mailcoach-ses-feedback
: ^4.0 -
spatie/laravel-mailcoach-sendgrid-feedback
: ^4.0 -
spatie/laravel-mailcoach-postmark-feedback
: ^4.0
Translations
All translations have been prefixed with mailcoach -
to not interfere with translations you could have in your own application. If you have added translations for Mailcoach in your project, make sure to prefix those keys.
From v3 to v4
Update satis
From now on, Mailcoach can be installed using the Satis installation on the spatie.be
domain. Using Satis on mailcoach.app
is not possible anymore.
If you are still using satis.mailcoach.app
in your composer.json
file, replace it with satis.spatie.be
.
Update your dependencies
These are all the new package versions, make sure any you’re using are up to date in your composer.json
{ "spatie/laravel-mailcoach": "^4.0", "spatie/laravel-mailcoach-mailgun-feedback": "^3.0", "spatie/laravel-mailcoach-monaco": "^2.0", "spatie/laravel-mailcoach-postmark-feedback": "^3.0", "spatie/laravel-mailcoach-sendgrid-feedback": "^3.0", "spatie/laravel-mailcoach-ses-feedback": "^3.0", "spatie/laravel-mailcoach-unlayer": "^2.0", "spatie/laravel-welcome-notification": "^2.0", }
Upgrading the database schema
There’s been a lot of changes to the database, use the migration below to update your database to the latest schema:
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; class UpgradeMailcoachV3Tov5 extends Migration { public function up() { Schema::table('mailcoach_campaigns', function (Blueprint $table) { $table->boolean('utm_tags')->default(false)->after('track_clicks'); }); Schema::table('mailcoach_subscribers', function (Blueprint $table) { $table->index(['email_list_id', 'created_at'], 'email_list_id_created_at'); // This index might already exist, then you don't need to add it. $table->index([ 'email_list_id', 'subscribed_at', 'unsubscribed_at' ], 'email_list_subscribed_index'); }); Schema::create('mailcoach_transactional_mails', function (Blueprint $table) { $table->id(); $table->text('subject'); $table->json('from'); $table->json('to'); $table->json('cc')->nullable(); $table->json('bcc')->nullable(); $table->longText('body')->nullable(); $table->longText('structured_html')->nullable(); $table->boolean('track_opens')->default(false); $table->boolean('track_clicks')->default(false); $table->string('mailable_class'); $table->timestamps(); }); Schema::create('mailcoach_automation_mails', function (Blueprint $table) { $table->id(); $table->string('name')->nullable(); $table->uuid('uuid'); $table->string('from_email')->nullable(); $table->string('from_name')->nullable(); $table->string('reply_to_email')->nullable(); $table->string('reply_to_name')->nullable(); $table->string('subject')->nullable(); $table->longText('html')->nullable(); $table->longText('structured_html')->nullable(); $table->longText('email_html')->nullable(); $table->longText('webview_html')->nullable(); $table->string('mailable_class')->nullable(); $table->json('mailable_arguments')->nullable(); $table->boolean('track_opens')->default(false); $table->boolean('track_clicks')->default(false); $table->boolean('utm_tags')->default(false); $table->integer('sent_to_number_of_subscribers')->default(0); $table->integer('open_count')->default(0); $table->integer('unique_open_count')->default(0); $table->integer('open_rate')->default(0); $table->integer('click_count')->default(0); $table->integer('unique_click_count')->default(0); $table->integer('click_rate')->default(0); $table->integer('unsubscribe_count')->default(0); $table->integer('unsubscribe_rate')->default(0); $table->integer('bounce_count')->default(0); $table->integer('bounce_rate')->default(0); $table->timestamp('statistics_calculated_at')->nullable(); $table->timestamp('last_modified_at')->nullable(); $table->timestamps(); }); Schema::table('mailcoach_sends', function (Blueprint $table) { $table ->foreignId('campaign_id') ->nullable() ->change(); $table ->foreignId('subscriber_id') ->nullable() ->change(); $table ->foreignId('automation_mail_id') ->nullable() ->constrained('mailcoach_automation_mails') ->cascadeOnDelete() ->after('campaign_id'); $table ->foreignId('transactional_mail_id') ->nullable() ->constrained('mailcoach_transactional_mails') ->cascadeOnDelete() ->after('automation_mail_id'); }); Schema::table('mailcoach_subscriber_imports', function (Blueprint $table) { $table->boolean('replace_tags')->default(false)->after('unsubscribe_others'); }); Schema::table('mailcoach_tags', function (Blueprint $table) { $table->string('type')->default('default')->after('name'); }); Schema::create('mailcoach_automations', function (Blueprint $table) { $table->id(); $table ->foreignId('email_list_id') ->nullable() ->constrained('mailcoach_email_lists') ->cascadeOnDelete(); $table->uuid('uuid'); $table->string('name')->nullable(); $table->string('interval')->nullable(); $table->string('status'); $table->text('segment_class')->nullable(); $table ->foreignId('segment_id') ->nullable() ->constrained('mailcoach_segments') ->nullOnDelete(); $table->string('segment_description')->default(0); $table->timestamp('run_at')->nullable(); $table->timestamp('last_ran_at')->nullable(); $table->timestamps(); }); Schema::create('mailcoach_automation_actions', function (Blueprint $table) { $table->id(); $table ->foreignId('automation_id') ->nullable() ->constrained('mailcoach_automations') ->cascadeOnDelete(); $table ->foreignId('parent_id') ->nullable() ->constrained('mailcoach_automation_actions') ->cascadeOnDelete(); $table->uuid('uuid'); $table->string('key')->nullable(); $table->text('action')->nullable(); $table->integer('order'); $table->timestamps(); }); Schema::create('mailcoach_automation_triggers', function (Blueprint $table) { $table->id(); $table ->foreignId('automation_id') ->nullable() ->constrained('mailcoach_automations') ->cascadeOnDelete(); $table->uuid('uuid'); $table->text('trigger')->nullable(); $table->timestamps(); }); Schema::create('mailcoach_automation_action_subscriber', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('action_id'); $table->unsignedBigInteger('subscriber_id'); $table->timestamp('run_at')->nullable(); $table->timestamp('completed_at')->nullable(); $table->timestamp('halted_at')->nullable(); $table->timestamps(); $table ->foreign('action_id') ->references('id')->on('mailcoach_automation_actions') ->onDelete('cascade'); $table ->foreign('subscriber_id') ->references('id')->on('mailcoach_subscribers') ->onDelete('cascade'); }); Schema::create('mailcoach_automation_mail_opens', function (Blueprint $table) { $table->id(); $table->foreignId('send_id')->constrained('mailcoach_sends'); $table ->foreignId('subscriber_id') ->nullable() ->constrained('mailcoach_subscribers') ->cascadeOnDelete(); $table ->foreignId('automation_mail_id') ->nullable() ->constrained('mailcoach_automation_mails') ->cascadeOnDelete(); $table->timestamps(); }); Schema::create('mailcoach_automation_mail_links', function (Blueprint $table) { $table->id(); $table ->foreignId('automation_mail_id') ->constrained('mailcoach_automation_mails') ->cascadeOnDelete(); $table->string('url', 2048); $table->integer('click_count')->default(0); $table->integer('unique_click_count')->default(0); $table->nullableTimestamps(); }); Schema::create('mailcoach_automation_mail_clicks', function (Blueprint $table) { $table->id(); $table->foreignId('send_id')->constrained('mailcoach_sends'); $table->foreignId('automation_mail_link_id')->constrained('mailcoach_automation_mail_links'); $table ->foreignId('subscriber_id') ->nullable() ->constrained('mailcoach_subscribers') ->cascadeOnDelete(); $table->timestamps(); }); Schema::create('mailcoach_automation_mail_unsubscribes', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('automation_mail_id'); $table ->foreign('automation_mail_id', 'auto_unsub_automation_mail_id') ->references('id')->on('mailcoach_automation_mails') ->cascadeOnDelete(); $table ->foreignId('subscriber_id') ->constrained('mailcoach_subscribers') ->cascadeOnDelete(); $table->timestamps(); }); Schema::create('mailcoach_transactional_mail_opens', function (Blueprint $table) { $table->id(); $table->foreignId('send_id')->constrained('mailcoach_sends'); $table->timestamps(); }); Schema::create('mailcoach_transactional_mail_clicks', function (Blueprint $table) { $table->id(); $table->foreignId('send_id')->constrained('mailcoach_sends'); $table->longText('url'); $table->timestamps(); }); Schema::create('mailcoach_transactional_mail_templates', function (Blueprint $table) { $table->id(); $table->json('cc')->nullable(); $table->string('label')->nullable(); $table->string('name'); $table->string('subject')->nullable(); $table->text('from')->nullable(); $table->json('to')->nullable(); $table->json('bcc')->nullable(); $table->longText('structured_html')->nullable(); $table->longText('body')->nullable(); $table->string('type'); // html, blade, markdown $table->json('replacers')->nullable(); $table->boolean('store_mail')->default(false); $table->boolean('track_opens')->default(false); $table->boolean('track_clicks')->default(false); $table->text('test_using_mailable')->nullable(); $table->timestamps(); }); } }
You’ll notice that the migration contains a few change()
calls. In order to run the migration you’ll need to install the doctrine/dbal
package, like instructed in the Laravel docs.
Config file changes
The mailcoach.php
config file has changed significantly. We recommend renaming the mailcoach.php
config file, so you can still reference it.
Publish the new config file using
php artisan vendor:publish --tag=mailcoach-config
Make sure to bring over any customizations you did to the old config file. After you’re done, you can delete the old, renamed config file.
Sanctum auth
Make sure the api middleware config contains auth:sanctum
as seen here.
Horizon config
This is the new recommended horizon config, the only real change is the addition of send-automation-mail
:
'mailcoach-general' => [ 'connection' => 'mailcoach-redis', 'queue' => ['mailcoach', 'mailcoach-feedback', 'send-mail', 'send-automation-mail'], 'balance' => 'auto', 'processes' => 10, 'tries' => 2, 'timeout' => 60 * 60, ], 'mailcoach-heavy' => [ 'connection' => 'mailcoach-redis', 'queue' => ['send-campaign'], 'balance' => 'auto', 'processes' => 3, 'tries' => 1, 'timeout' => 60 * 60, ],
View changes
If you had customized views, you’ll need to reapply your own customizations to the new views.
Publish the new views using
php artisan vendor:publish --tag=mailcoach-views
Namespace changes
Most namespaces have been changed to a new Domain based structure separated into Audience
, Campaign
, Automation
, TransactionalMail
and Shared
.
If you’re using any of the Mailcoach classes in your own project, make sure to validate the namespace imports. Below are some of the most impactful old namespaces and their resulting namespace:
Audience
-
\Spatie\Mailcoach\Models\Subscriber
has been moved to\Spatie\Mailcoach\Domain\Audience\Models\Subscriber
-
\Spatie\Mailcoach\Models\EmailList
has been moved to\Spatie\Mailcoach\Domain\Audience\Models\EmailList
-
\Spatie\Mailcoach\Models\Tag
has been moved to\Spatie\Mailcoach\Domain\Audience\Models\Tag
-
\Spatie\Mailcoach\Models\TagSegment
has been moved to\Spatie\Mailcoach\Domain\Audience\Models\TagSegment
-
All Subscriber actions were moved from
\Spatie\Mailcoach\Actions\Subscribers
to\Spatie\Mailcoach\Domain\Audience\Actions\Subscribers
-
All EmailList actions were moved from
\Spatie\Mailcoach\Actions\EmailLists
to\Spatie\Mailcoach\Domain\Audience\Actions\EmailLists
Campaigns
-
\Spatie\Mailcoach\Models\Campaign
has been moved to\Spatie\Mailcoach\Domain\Campaign\Models\Campaign
-
\Spatie\Mailcoach\Models\CampaignClick
has been moved to\Spatie\Mailcoach\Domain\Campaign\Models\CampaignClick
-
\Spatie\Mailcoach\Models\CampaignLink
has been moved to\Spatie\Mailcoach\Domain\Campaign\Models\CampaignLink
-
\Spatie\Mailcoach\Models\CampaignOpen
has been moved to\Spatie\Mailcoach\Domain\Campaign\Models\CampaignOpen
-
\Spatie\Mailcoach\Models\CampaignUnsubscribe
has been moved to\Spatie\Mailcoach\Domain\Campaign\Models\CampaignUnsubscribe
-
\Spatie\Mailcoach\Models\Template
has been moved to\Spatie\Mailcoach\Domain\Campaign\Models\Template
-
\Spatie\Mailcoach\Enums\CampaignStatus
has been moved to\Spatie\Mailcoach\Domain\Campaign\Enums\CampaignStatus
-
All Campaign actions were moved from
\Spatie\Mailcoach\Actions\Campaigns
to\Spatie\Mailcoach\Domain\Campaign\Actions
Segments
If you have campaigns with existing segmentation, you can use the following Artisan command in your routes/console.php
file to migrate those namespaces automatically:
use Spatie\Mailcoach\Domain\Campaign\Models\Campaign; Artisan::command('migrate-mailcoach', function () { Campaign::each(function (Campaign $campaign) { if ($campaign->segment_class === 'Spatie\Mailcoach\Support\Segments\SubscribersWithTagsSegment') { $campaign->update([ 'segment_class' => 'Spatie\Mailcoach\Domain\Audience\Support\Segments\SubscribersWithTagsSegment', ]); } if ($campaign->segment_class === 'Spatie\Mailcoach\Support\Segments\EverySubscriberSegment') { $campaign->update([ 'segment_class' => 'Spatie\Mailcoach\Domain\Audience\Support\Segments\EverySubscriberSegment', ]); } }); });
You can then run php artisan migrate-mailcoach
to run the command.
Scheduled jobs
Add these new scheduled jobs to your application’s schedule:
$schedule->command('mailcoach:run-automation-triggers')->everyMinute()->runInBackground(); $schedule->command('mailcoach:run-automation-actions')->everyMinute()->runInBackground(); $schedule->command('mailcoach:calculate-automation-mail-statistics')->everyMinute();
Automation mail queue
Make sure to add the send-automation-mail
, queue to the mailcoach-general
key in your horizon.php
config file.
'mailcoach-general' => [ 'connection' => 'mailcoach-redis', 'queue' => ['mailcoach', 'mailcoach-feedback', 'send-mail', 'send-automation-mail'], 'balance' => 'auto', 'processes' => 10, 'tries' => 2, 'timeout' => 60 * 60, ],
From v2 to v3
Laravel 8
Mailcoach v3 requires Laravel 8, make sure to upgrade your project first.
Mailcoach uses job batching under the hood. Make sure you add the required database table, as mentioned in the Laravel docs on Job batching.
Upgrading the database schema
In your database you should add a few columns. You can add them manually like described below, or use the migration mentioned in this comment on GitHub.
mailcoach_campaigns
-
all_jobs_added_to_batch_at
: timestamp, nullable -
send_batch_id
: string, nullable -
reply_to_email
: string, nullable -
reply_to_name
: string, nullable
mailcoach_subscribers
-
imported_via_import_uuid
: uuid, nullable
mailcoach_subscriber_imports
-
subscribe_unsubscribed
: boolean, default: false -
unsubscribe_others
: boolean, default false,
mailcoach_email_lists
-
default_reply_to_email
: string, nullable -
default_reply_to_name
: string, nullable -
allowed_form_extra_attributes
: text, nullable
mailcoach_sends
- add an index on
uuid
webhook_calls
-
processed_at
: timestamp, nullable. -
external_id
: string, nullable. Make sure to add an index for performance.
Upgrading database content
-
open_rate
,click_rate
,bounce_rate
,unsubscribe_rate
of themailcoach_campaigs
table: v3 of mailcoach now assumes that the two last numbers are the digits. For campaigns that were sent using v2 you should add two zeroes, so31
should become3100
-
webhook_calls
need theprocessed_at
column filled in, you can set this usingupdate webhook_calls set processed_at = NOW() where processed_at is null;
Updating the config file
The middleware
option now contains an array with web
and api
. This is the new default.
If you don’t have a middleware
key in your config file, you don’t need to do anything as the default will be used. If you do have a middleware
key, update it accordingly.
/* * These middleware will be assigned to every Mailcoach routes, giving you the chance * to add your own middleware to this stack or override any of the existing middleware. */ 'middleware' => [ 'web' => [ 'web', Spatie\Mailcoach\Http\App\Middleware\Authenticate::class, Spatie\Mailcoach\Http\App\Middleware\Authorize::class, Spatie\Mailcoach\Http\App\Middleware\SetMailcoachDefaults::class, ], 'api' => [ 'api', 'auth:api', ], ],
Horizon configuration
We now suggest a new horizon configuration for balancing the queue that Mailcoach uses, make sure mailcoach-general
and mailcoach-heavy
are present in your production and local Horizon environments:
// config/horizon.php 'environments' => [ 'production' => [ 'supervisor-1' => [ 'connection' => 'redis', 'queue' => ['default'], 'balance' => 'simple', 'processes' => 10, 'tries' => 2, 'timeout' => 60 * 60, ], 'mailcoach-general' => [ 'connection' => 'mailcoach-redis', 'queue' => ['mailcoach', 'mailcoach-feedback', 'send-mail'], 'balance' => 'auto', 'processes' => 10, 'tries' => 2, 'timeout' => 60 * 60, ], 'mailcoach-heavy' => [ 'connection' => 'mailcoach-redis', 'queue' => ['send-campaign'], 'balance' => 'auto', 'processes' => 3, 'tries' => 1, 'timeout' => 60 * 60, ], ], 'local' => [ 'supervisor-1' => [ 'connection' => 'redis', 'queue' => ['default'], 'balance' => 'simple', 'processes' => 10, 'tries' => 2, 'timeout' => 60 * 60, ], 'mailcoach-general' => [ 'connection' => 'mailcoach-redis', 'queue' => ['mailcoach', 'mailcoach-feedback', 'send-mail'], 'balance' => 'auto', 'processes' => 10, 'tries' => 2, 'timeout' => 60 * 60, ], 'mailcoach-heavy' => [ 'connection' => 'mailcoach-redis', 'queue' => ['send-campaign'], 'balance' => 'auto', 'processes' => 3, 'tries' => 1, 'timeout' => 60 * 60, ], ], ],
New command for cleanup
We’ve added a new command for cleanup of processed feedback in the webhook_calls
table, make sure to add this to your \App\Console\Kernel
schedule.
Be aware that email providers such as SES are a ‘deliver at least once’ service. Duplicate feedback delivery could be seen weeks after the event. Mailcoach prevents duplicates from SES by checking for old matching feedback. As such, cleaning up historical feedback webhooks could lead to duplicate feedbacks items being processed multiple times. The end result is inflated open and click metrics.
$schedule->command('mailcoach:cleanup-processed-feedback')->hourly();