Campaigns
On this page:
- Creating a campaign
- Testing a campaign
- Sending a campaign
- Tracking opens & clicks
- Viewing campaign statistics
- Creating custom placeholders
- Using custom mailables
- Displaying webviews
- Custom segmenting classes
- Troubleshooting
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
: theunique_open_count
divided by thesent_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
: theunique_click_count
divided by thesent_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
: theunsubscribe_count
divided by thesent_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.
PersonalizedReplacer
s 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/campaign
directory.
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 thesegment_class
field in yourmailcoach_campaigns
table is atext
field and not avarchar
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
.