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

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 the mailcoach_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, so 31 should become 3100
  • webhook_calls need the processed_at column filled in, you can set this using update 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();
Changelog
Troubleshooting