Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,17 @@ public function headers()
}
```

## Skip Tracking for Specific Links

If you want to prevent tracking on specific links within an email while still tracking other links, you can add the `data-dont-track` attribute to individual anchor tags. This is useful when you have some links that you want to track and others that you don't.

```html
<a href="https://example.com/track-this">This link will be tracked</a>
<a href="https://example.com/dont-track-this" data-dont-track>This link will not be tracked</a>
```

The `data-dont-track` attribute will be removed from the final HTML before the email is sent, so it won't be visible to recipients.

## Skipping Open/Click Tracking for Anti-virus/Spam Filters

Some mail servers might scan emails before they deliver which can trigger the tracking pixel, or even clicked links. You can add an event listener to the ValidActionEvent to handle this.
Expand Down
10 changes: 9 additions & 1 deletion src/MailTracker.php
Original file line number Diff line number Diff line change
Expand Up @@ -198,12 +198,20 @@ protected function injectLinkTracker($html, $hash)
{
$this->hash = $hash;

// Find all links without data-dont-track attribute and replace them
$html = preg_replace_callback(
"/(<a[^>]*href=[\"])([^\"]*)/",
"/(<a(?![^>]*data-dont-track)[^>]*href=[\"])([^\"]*)/",
[$this, 'inject_link_callback'],
$html
);

// Remove the data-dont-track attributes so the final html is clean
$html = preg_replace(
'/\s*data-dont-track(?=\s|>)/',
'',
$html
);

return $html;
}

Expand Down
167 changes: 167 additions & 0 deletions tests/MailTrackerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1261,6 +1261,173 @@ public function sent_email_url_clicked_model_can_be_changed()

MailTracker::useSentEmailUrlClickedModel(SentEmailUrlClicked::class);
}

/**
* @test
*/
public function it_does_not_track_links_with_data_dont_track_attribute()
{
Event::fake(LinkClickedEvent::class);
Config::set('mail-tracker.track-links', true);
Config::set('mail-tracker.inject-pixel', true);
Config::set('mail.driver', 'array');
(new MailServiceProvider(app()))->register();

$faker = Factory::create();
$email = $faker->email;
$subject = $faker->sentence;
$name = $faker->firstName . ' ' . $faker->lastName;
View::addLocation(__DIR__);

Mail::send('email.testDontTrack', [], function ($message) use ($email, $subject, $name) {
$message->from('[email protected]', 'From Name');
$message->sender('[email protected]', 'Sender Name');
$message->to($email, $name);
$message->subject($subject);
});

$driver = app('mailer')->getSymfonyTransport();
$this->assertEquals(1, count($driver->messages()));

$mes = $driver->messages()[0];
$body = $mes->getOriginalMessage()->getBody()->getBody();

// Extract tracked-link.com URL
preg_match('/<a[^>]*href=[\'"]([^\'"]*tracked-link\.com[^\'"]*)[\'"]/', $body, $trackedMatch);
$this->assertNotEmpty($trackedMatch, 'Should find tracked-link.com link');
$this->assertStringContainsString('/n?h=', $trackedMatch[1], 'Tracked link should contain tracking route');

// Extract google.com URL
preg_match('/<a[^>]*href=[\'"]([^\'"]*google\.com[^\'"]*)[\'"]/', $body, $googleMatch);
$this->assertNotEmpty($googleMatch, 'Should find google.com link');
$this->assertStringContainsString('/n?h=', $googleMatch[1], 'Google link should be tracked');

// Check that untracked links (with data-dont-track) remain unchanged and are NOT tracked
preg_match('/<a[^>]*href=[\'"]([^\'"]*untracked-link\.com[^\'"]*)[\'"]/', $body, $untrackedMatch);
$this->assertNotEmpty($untrackedMatch, 'Should find untracked-link.com link');
$this->assertEquals(
'http://www.untracked-link.com',
$untrackedMatch[1],
'Untracked link should be original URL'
);
$this->assertStringNotContainsString(
'/n?h=',
$untrackedMatch[1],
'Untracked link should NOT contain tracking route'
);

// Check privacy-link.com
preg_match('/<a[^>]*href=[\'"]([^\'"]*privacy-link\.com[^\'"]*)[\'"]/', $body, $privacyMatch);
$this->assertNotEmpty($privacyMatch, 'Should find privacy-link.com link');
$this->assertEquals('http://www.privacy-link.com', $privacyMatch[1], 'Privacy link should be original URL');
$this->assertStringNotContainsString(
'/n?h=',
$privacyMatch[1],
'Privacy link should NOT contain tracking route'
);

// Verify data-dont-track attribute has been removed
$this->assertStringNotContainsString(
'data-dont-track',
$body,
'data-dont-track attribute should be removed from final HTML'
);
}

/**
* @test
*/
public function it_removes_data_dont_track_attribute_from_final_html()
{
Event::fake(LinkClickedEvent::class);
Config::set('mail-tracker.track-links', true);
Config::set('mail-tracker.inject-pixel', true);
Config::set('mail.driver', 'array');
(new MailServiceProvider(app()))->register();

$faker = Factory::create();
$email = $faker->email;
$subject = $faker->sentence;
$name = $faker->firstName . ' ' . $faker->lastName;
View::addLocation(__DIR__);

Mail::send('email.testDontTrack', [], function ($message) use ($email, $subject, $name) {
$message->from('[email protected]', 'From Name');
$message->to($email, $name);
$message->subject($subject);
});

$driver = app('mailer')->getSymfonyTransport();
$mes = $driver->messages()[0];
$body = $mes->getOriginalMessage()->getBody()->getBody();

// Verify that data-dont-track attribute has been removed from final HTML
$this->assertStringNotContainsString(
'data-dont-track',
$body,
'data-dont-track attribute should be removed from final HTML'
);
}

/**
* @test
*/
public function it_handles_mixed_tracked_and_untracked_links()
{
Event::fake(LinkClickedEvent::class);
Config::set('mail-tracker.track-links', true);
Config::set('mail-tracker.inject-pixel', true);
Config::set('mail.driver', 'array');
(new MailServiceProvider(app()))->register();

$faker = Factory::create();
$email = $faker->email;
$subject = $faker->sentence;
$name = $faker->firstName . ' ' . $faker->lastName;
View::addLocation(__DIR__);

Mail::send('email.testDontTrack', [], function ($message) use ($email, $subject, $name) {
$message->from('[email protected]', 'From Name');
$message->to($email, $name);
$message->subject($subject);
});

$driver = app('mailer')->getSymfonyTransport();
$mes = $driver->messages()[0];
$body = $mes->getOriginalMessage()->getBody()->getBody();
$hash = $mes->getOriginalMessage()->getHeaders()->get('X-Mailer-Hash')->getValue();

// Count total links in the original template (4 links total: 2 tracked, 2 untracked)
preg_match_all('/<a[^>]*href=[\'"]([^\'"]*)/', $body, $allLinks);

// Verify we have all 4 links
$this->assertGreaterThanOrEqual(4, count($allLinks[1]), 'Should have at least 4 links');

// Count how many links contain the tracking route
$trackedCount = 0;
$untrackedCount = 0;

foreach ($allLinks[0] as $link) {
if (strpos($link, '/n?h=') !== false) {
$trackedCount++;
} else {
// Check if it's one of the original untracked URLs
if (strpos($link, 'untracked-link.com') !== false || strpos($link, 'privacy-link.com') !== false) {
$untrackedCount++;
}
}
}

// We should have exactly 2 tracked links and 2 untracked links
$this->assertEquals(2, $trackedCount, 'Should have exactly 2 tracked links');
$this->assertEquals(2, $untrackedCount, 'Should have exactly 2 untracked links');

// Verify specific URLs
$this->assertStringContainsString('tracked-link.com', $body);
$this->assertStringContainsString('untracked-link.com', $body);
$this->assertStringContainsString('google.com', $body);
$this->assertStringContainsString('privacy-link.com', $body);
}
}

class SentEmailStub extends Model
Expand Down
28 changes: 28 additions & 0 deletions tests/email/testDontTrack.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<head>
This is the head
</head>
<body>
<h1>
This is a test for dont-track attribute!
</h1>
<p>
This link should be tracked:
<a href="http://www.tracked-link.com">Click Here to be Tracked</a>
</p>
<p>
This link should NOT be tracked:
<a data-dont-track href="http://www.untracked-link.com">Click Here Without Tracking</a>
</p>
<p>
Another tracked link:
<a class="test" href="http://www.google.com?q=foo&amp;x=bar">
Click here for Google
</a>
</p>
<p>
Another untracked link with attributes:
<a class="test" data-dont-track href="http://www.privacy-link.com">
Privacy Link
</a>
</p>
</body>