From ab641b765629dfc2057988d06ed28cf47b9fed7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Valu=C5=A1ek?= Date: Thu, 16 Oct 2025 00:06:04 +0200 Subject: [PATCH] feat: add data-dont-track attribute support --- README.md | 11 ++ src/MailTracker.php | 10 +- tests/MailTrackerTest.php | 167 ++++++++++++++++++++++++++++ tests/email/testDontTrack.blade.php | 28 +++++ 4 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 tests/email/testDontTrack.blade.php diff --git a/README.md b/README.md index 6054211..300ea2e 100644 --- a/README.md +++ b/README.md @@ -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 +This link will be tracked +This link will not be tracked +``` + +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. diff --git a/src/MailTracker.php b/src/MailTracker.php index 6eb4f10..049f4d2 100644 --- a/src/MailTracker.php +++ b/src/MailTracker.php @@ -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( - "/(]*href=[\"])([^\"]*)/", + "/(]*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; } diff --git a/tests/MailTrackerTest.php b/tests/MailTrackerTest.php index 0712b18..3215d7f 100644 --- a/tests/MailTrackerTest.php +++ b/tests/MailTrackerTest.php @@ -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('from@johndoe.com', 'From Name'); + $message->sender('sender@johndoe.com', '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('/]*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('/]*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('/]*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('/]*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('from@johndoe.com', '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('from@johndoe.com', '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('/]*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 diff --git a/tests/email/testDontTrack.blade.php b/tests/email/testDontTrack.blade.php new file mode 100644 index 0000000..0f266cd --- /dev/null +++ b/tests/email/testDontTrack.blade.php @@ -0,0 +1,28 @@ + + This is the head + + +

+ This is a test for dont-track attribute! +

+

+ This link should be tracked: + Click Here to be Tracked +

+

+ This link should NOT be tracked: + Click Here Without Tracking +

+

+ Another tracked link: + + Click here for Google + +

+

+ Another untracked link with attributes: + + Privacy Link + +

+