Campaigns

On this page:

In this section you’ll learn how to programmatically work with campaigns.

Creating a campaign

To send an email to all subscribers of your list, you must create a campaign.

A campaign can be created like this:

$campaign = Campaign::create()
    ->from('sender@example.com')
    ->to($emailList);

$campaign->name = 'My newsletter #1';
$campaign->save();

$campaign->contentItem->subject = 'Newsletter #1';
$campaign->contentItem->html = $html;
$campaign->contentItem->save();

Alternatively, you could manually set the attributes on a Campaign model.

Campaign::create([
   'name' => 'My newsletter #1',
   'email_list_id' => $emailList->id,
]);

Setting the content and using placeholders

You can set the content of a campaign by setting it’s HTML attribute.

$campaign->contentItem->html = $yourHtml;
$campaign->contentItem->save();

In that HTML you can use these placeholders which will be replaced when sending out the campaign:

  • {{ unsubscribeUrl }}: this string will be replaced with the URL that, when hit, will immediately unsubscribe the person that clicked it
  • {{ unsubscribeTag['your tag'] }}: this string will be replaced with the URL that, when hit, will remove the “your tag” tag from the subscriber that clicked it
  • {{ webviewUrl }}: this string will be replaced with the URL that will display the content of your campaign.

If there is no way for a subscriber to unsubscribe, it will result in a lot of frustration on the part of the subscriber. We always recommend using {{ unsubscribeUrl }} in the HTML of each campaign you send.

Setting a from name

To set a from name, just pass the name as a second parameter to from

Campaign::create()->from('sender@example.com', 'Sender name')

Setting a reply to

Optionally, you can set a reply to email and name like this

Campaign::create()->replyTo('john@example.com', 'John Doe')

Testing a campaign

Before sending a campaign to an entire list, you can send a test to a given email address.

// to a single email address
$campaign->sendTestMail('john@example.com');

// to multiple email addresses at once
$campaign->sendTestMail(['john@example.com', 'paul@example.com'])

In the sent mail the placeholders won’t be replaced. Rest assured that when you send the mail to your entire list, they will be replaced.

Sending a campaign

Before sending a campaign, ensure that the subject, HTML and email_list_id attributes are set.

A campaign can be sent with the send method.

$campaign->send();

Alternatively you can set the email list and send the campaign in one go:

$campaign->sendTo($emailList);

If you don’t want to send email to your entire list, but only to a subset of subscribers, you can use a segment.

What happens when a campaign is being sent

When you send a campaign, a job called SendCampaign job will be dispatched. This job will create a MailSend model for each of the subscribers of the list you’re sending the campaign to. A MailSend represents a mail that should be sent to one subscriber.

For each created SendMail model, a SendMailJob will be started. That job will send that actual mail. After the job has sent the mail, it will mark the SendMail as sent, by filling sent_at with the current timestamp.

You can customize on which queue the SendCampaignJob and SendMailJob jobs are dispatched in the perform_on_queue in the email-campaigns config file. We recommend the SendMailJob having its own queue because it could contain many pending jobs if you are sending a campaign to a large list of subscribers.

The SendMailJob is throttled by default so that it doesn’t overwhelm your email service with a large number of calls. Only 5 of those jobs will be handled per second. To learn more about this, read the docs on throttling sends.

Tracking opens & clicks

The package can track when and how many times a subscriber opens or clicks a campaign.

Enabling tracking

To use this feature, you must configure your email provider to track opens and/or clicks.

How it works under the hood

Open tracking

When you send a campaign, the email service provider will add a web beacon to it.. A web beacon is an extra img tag in the HTML of your mail. Its src attribute points to an unique endpoint on the domain of the email service provider.

Each time an email client tries to display the web beacon it will send a get request to email server. This way the email service provider will know the mail has been opened.

Via a webhook, the email service provider will let Mailcoach know that the mail has been opened.

Click tracking

When you send a campaign that has click tracking enabled, the email service provider you use will replace each link in your mail by a unique link on its own domain. When somebody clicks that link, the email service provider will get a request, and it will know that the link was clicked. Next the request will be redirected to the original link.

Via a webhook, the email service provider will let Mailcoach know that a link has been clicked.

Viewing campaign statistics

After a campaign is sent, some statistics will be made available.

Available statistics

On a campaign

The scheduled ‘email-campaigns:calculate-statistics’ will fill these attributes on the Campaign model:

  • sent_to_number_of_subscribers
  • open_count: this is the total number of times your campaign was opened. Multiple opens by a single subscriber will be counted.
  • unique_open_count: the number of subscribers that opened your campaign.
  • open_rate: the unique_open_count divided by the sent_to_number_of_subscribers. The result is multiplied by 100. The maximum value for this attribute is 100, and the minimum is 0.
  • click_count: the total number of times the links in your campaign were clicked. Multiple clicks on the same link by a subscriber will be counted.
  • unique_click_count: the number of subscribers who clicked any of the links in your campaign.
  • click_rate: the unique_click_count divided by the sent_to_number_of_subscribers. The result is multiplied by 100. The maximum value for this attribute is 100, the minimum is 0.
  • unsubscribe_count: the number of people that unsubscribed from the email list using the unsubscribe link from this campaign
  • unsubscribe_rate: the unsubscribe_count divided by the sent_to_number_of_subscribers. The result is multiplied by 100. The maximum value for this attribute is 100, the minimum is 0.

You can also get the opens and clicks stats for a campaign. Here’s an example using the opens relation to retrieve who first opened the mail.

$open = $campaign->opens->first();
$email = $open->subscriber->email;

On a campaign link

If you enabled click tracking, a CampaignLink will have been created for each link in your campaign.

It contains these two attributes that hold statistical data:

  • click_count: the total number of times this link was clicked. Multiple clicks on the same link by a subscriber will each be counted.
  • unique_click_count: the number of subscribers who clicked this link.

To know who clicked which link, you can use the relations on CampaignLink model. Here’s an example where we get the email of the subscriber who first clicked the first link of a campaign.

$campaignLink = $campaign->links->first();
$campaignClick = $campaignLink->links->first();
$email = $campaingClick->subscriber->email;

When are statistics calculated

The statistics are calculated by the scheduled email-campaigns:calculate-statistics. This job will recalculate statistics:

  • each minute for campaigns that were sent between 0 and 5 minutes ago
  • every 10 minutes for campaigns that were send between 5 minutes and two hours ago
  • every hour for campaigns that were sent between two hours and a day
  • every four hours for campaigns that were sent between a day and two weeks ago

After two weeks, no further statistics are calculated.

Manually recalculate statistics

To manually trigger a recalculation, execute the command using the campaign id as a parameter.

php artisan mailcoach:calculate-statistics <campaign-id>

Creating custom placeholders

By default, Mailcoach offers a couple of placeholders you can use in the subject or content of your campaign, such as webviewUrl and unsubscribeUrl.

Creating a replacer

Custom placeholders can be created. Do this you must create a class and let it implement Spatie\Mailcoach\Support\Replacers\Replacer interface.

This interface contains two methods. In replace you must do the actual replacement. In helpText you must return the helptext that will be visible on the campaign content screen.

Here is the code of the WebviewReplacer that ships with Mailcoach.

namespace Spatie\Mailcoach\Support\Replacers;

use Spatie\Mailcoach\Domain\Campaign\Models\Campaign;
use Spatie\Mailcoach\Domain\Campaign\Support\Replacers\CampaignReplacer;

class WebviewReplacer implements CampaignReplacer
{
    public function helpText(): array
    {
        return [
            'webviewUrl' =>  'This url will display the html of the campaign',
        ];
    }

    public function replace(string $html, Campaign $campaign): string
    {
        $webviewUrl = $campaign->webviewUrl();

        return str_ireplace('::webviewUrl::', $webviewUrl, $html);
    }
}

After creating a replacer you must register it in the campaigns.replacers config key of the mailcoach config file.

Creating a personalized replacer

A regular replace will do it’s job when the campaign mail is being prepared. This will only happen once when sending a campaign. There’s also a second kind of replacer: Spatie\Mailcoach\Support\Replacers\Replacer\PersonalizedReplacer. These replacer will get executed for each mail that is being sent out in a campaign.
PersonalizedReplacers have access to subscriber they are sent to via the Send object given in the replace method.

Here is the code of the UnsubscribeUrlReplacer that ships with Mailcoach.

namespace Spatie\Mailcoach\Support\Replacers;

use Spatie\Mailcoach\Domain\Shared\Models\Send;
use Spatie\Mailcoach\Domain\Campaign\Support\Replacers\PersonalizedReplacer;

class UnsubscribeUrlReplacer implements PersonalizedReplacer
{
    public function helpText(): array
    {
        return [
            'unsubscribeUrl' => 'The url where users can unsubscribe',
        ];
    }

    public function replace(string $html, Send $pendingSend): string
    {
        $unsubscribeUrl = $pendingSend->subscriber->unsubscribeUrl($pendingSend);

        return str_ireplace('::unsubscribeUrl::', $unsubscribeUrl, $html);
    }
}

Using custom mailables

You can use your own mailable to be used when sending a campaign. Any mailable that extends Spatie\Mailcoach\Mails\CampaignMail is valid.

Here’s an example mailable;

use Spatie\Mailcoach\Mails\CampaignMail;

class MyCustomMailable extends CampaignMail
{
    public function build()
    {
        return $this->view('emails.your-custom-view');
    }
}

You can use it in a campaign like this:

Campaign::create()
    ->useMailable(MyCustomMailable::class)
    ->sendTo($emailList);

Displaying webviews

Whenever you send a campaign, a webview is also created. A webview is a hard to guess URL that people who didn’t subscribe can visit to read the content of your campaign.

You can get to the URL of the webview of a campaign:

$campaign->webViewUrl();

Customizing the webview

You can customize the webview. To do this, you must publish all views:

php artisan vendor:publish --provider="Spatie\Mailcoach\MailcoachServiceProvider" --tag="mailcoach-views"

After that, you can customize the webview.blade.php view in the resources/views/vendor/mailcoach/campaigndirectory.

Hiding elements from the generated webview

You can hide elements from the generated webview by wrapping them in <!-- webview:hide --> ... <!-- /webview:hide --> tags.

For example:

<html>
	<body>
		<!-- webview:hide -->
		<span>Is this email not displaying correctly? <a href="{{ webviewUrl }}">View online version</a></span>
		<!-- /webview:hide -->

		<div>
			... The rest of your email content
		</div>
		
		<!-- webview:hide -->
		<a href="{{ unsubscribeUrl }}">Unsubscribe</a>
		<!-- /webview:hide -->
	</body>
</html>

This will hide the unsubscribe and webview links from the generated webview HTML, making your email only display its essentials.

Custom segmenting classes

If you wish to send a campaign to only a part of an email list you can use a segment when sending your campaign. A segment is a class that is responsible for selecting subscribers on an email list. It should always extend Spatie\Mailcoach\Support\Segments\Segment

A first example

Here’s a silly segment that will only select subscriber whose email address begin with an ‘a’

class OnlyEmailAddressesStartingWithA extends Segment
{
    public function shouldSend(Subscriber $subscriber): bool
    {
        return Str::startsWith($subscriber->email, 'a');
    }
}

When sending a campaign this is how the segment can be used:

Campaign::create()
   ->content($yourHtml)
   ->segment(OnlyEmailAddressesStartingWithA::class)
   ->sendTo($emailList);

Using an instantiated Segment object

Since 1.7.1
make sure the segment_class field in your mailcoach_campaigns table is a text field and not a varchar

Here’s the same segment that will only select subscriber whose email address begin with a configurable character ‘b’

class OnlyEmailAddressesStartingWith extends Segment
{
    public string $character;

    public function __construct(string $character) {
        $this->character = $character;
    }

    public function shouldSend(Subscriber $subscriber): bool
    {
        return Str::startsWith($subscriber->email, $this->character);
    }
}

When sending a campaign this is how the segment can be used:

Campaign::create()
   ->content($yourHtml)
   ->segment(new OnlyEmailAddressesStartingWith('b'))
   ->sendTo($emailList);

The object will be serialized when saved to the campaign, and unserialized when used for segmenting.

Using a query

If you have a very large list, it might be better to use a query to select the subscribers of your segment. This can be done with the subscribersQuery method on a segment.

Here’s an example:

class OnlyEmailAddressesStartingWithA extends Segment
{
    public function subscribersQuery(Builder $subscribersQuery): void
    {
        $subscribersQuery->where('email','like', 'a%');
    }
}

No matter what you do in subscribersQuery, the package will never mail people that haven’t subscribed to the email list you’re sending the campaign to.

Segment description

Spatie\Mailcoach\Support\Segments\Segment allows us to give our custom segment a unique name. This is required by the interface and can be done very simple:

    public function description(): string
    {
        return 'My cool segment';
    }

Accessing the Campaign model

If you need to get any campaign details somewhere in your segment logic, you can use $this->segmentable to access the model object of the campaign that is being sent.

Troubleshooting

When sending a campaign, the package will create Send models for each mail to be sent. A Send has a property sent_at that stores the date time of when an email was actually sent. If that attribute is null the email has not yet been sent.

If you experience problems while sending, and the state of your queues is been lost, you should dispatch a SendMailJob for each Send that has sent_at set to null.

use Spatie\Mailcoach\Domain\Shared\Models\Send;
use Spatie\Mailcoach\Domain\Campaign\Jobs\SendCampaignMailJob;

Send::whereNull('sent_at')->each(function(Send $send) {
   dispatch(new SendCampaignMailJob($send));
});

You can run the above code by executing this command:

php artisan mailcoach:retry-pending-sends

Should, for any reason, two jobs for the same Send be scheduled, it is highly likely that only one mail will be sent. After a SendMailJob has sent an email it will update sent_at with the current timestamp. The job will not send a mail for a SendMail whose sent_at is not set to null.

Automations
Scaling Mailcoach