A macOS sample app for testing MEContentBlocker (MailKit) with AdGuard-style filter rules.
It verifies that:
MEContentBlockerloads and is applied by Mail.appif-domain/unless-domainbehaviour in the Mail rendering contextcss-display-noneapplicability in email rendering- The rule conversion pipeline stays within the 300,000 combined budget (2 extensions × 150,000)
Reference implementation: ameshkov/safari-blocker
- macOS 12.0 (Monterey) or later
- Xcode 15+
- xcodegen (
brew install xcodegen)
This repo is template-ready. Replace placeholders:
neshumov→ your reverse-domain prefix (example:com.acme)YOUR_TEAM_ID→ your Apple Developer Team ID (example:ABCDE12345)
| File | Setting |
|---|---|
Shared/AppGroupConstants.swift |
AppGroup.id, BundleIDs.* |
project.yml |
PRODUCT_BUNDLE_IDENTIFIER (both targets), APP_GROUP_ID |
MailTrackerBlockerApp/MailTrackerBlockerApp.entitlements |
App Group string |
MailExtension/MailExtension.entitlements |
App Group string |
MailExtension2/MailExtension2.entitlements |
App Group string |
After replacement, values should look like this:
- App bundle id:
com.acme.mail-tracker-blocker - Extension bundle id:
com.acme.mail-tracker-blocker.MailExtension - Extension 2 bundle id:
com.acme.mail-tracker-blocker.MailExtension2 - App Group:
group.com.acme.mail-tracker-blocker
In project.yml set DEVELOPMENT_TEAM: YOUR_TEAM_ID for all targets, or set Team in Xcode Signing & Capabilities.
In the Apple Developer portal, create an App Group with the same ID you set above, and add it to both App IDs.
make generate
# or
make open # generates and opens in XcodeIf you changed placeholders after generating once, run make generate again.
- Build and run
MailTrackerBlockerApp. - Click Load filter.txt to load
filters/filter.txt(or any AdGuard-format filter). - Configure pipeline options:
- Strip $domain= — removes
domain=modifiers before conversion (testsif-domainhandling). - Include css-display-none rules — passes cosmetic
##selectorrules through the pipeline.
- Strip $domain= — removes
- Click Run Pipeline. The app converts the rules and saves the JSON to the shared App Group container.
- Open Mail.app → Settings → Extensions → enable both:
- Mail Tracker Blocker Extension
- Mail Tracker Blocker Extension 2
- Restart Mail.app to pick up the new rules.
Add tracking-pixel domains to filters/filter.txt, run the pipeline, open a test email containing those URLs in Mail.app, and verify the requests are blocked (use Proxyman or Charles).
Run with a $domain= rule first without Strip $domain=, then with it. Compare whether the rule fires in each case to determine how Mail.app assigns the document domain.
Enable Include css-display-none rules, add a ##img[width="1"][height="1"] rule, and check in Accessibility Inspector whether the element is hidden after opening the email.
Load a large filter list. The pipeline stats panel shows total finalJSONEntryCount / 300,000 and split counts per extension (/ 150,000 each).
Edit filter.txt, click Run Pipeline again without restarting the app, and confirm Mail.app uses the updated rules.
mail-tracker-blocker/
├── MailTrackerBlockerApp/ Container app (SwiftUI)
├── MailExtension/ MEContentBlocker extension
├── MailExtension2/ Second MEContentBlocker extension
├── Shared/ Code shared between both targets
├── filters/filter.txt Sample test rules
├── project.yml XcodeGen spec
└── Makefile
| Library | Source | Purpose |
|---|---|---|
| SafariConverterLib | https://github.com/AdguardTeam/SafariConverterLib | Converts AdGuard filter rules to WebKit content blocker JSON |
Added automatically via Swift Package Manager when you open the generated project.