Apple Mail Privacy Protection (MPP), rolled out in iOS 15, proxy-loads tracking pixels through Apple CDN servers the instant an email arrives — before the recipient ever opens it. This inflates measured open rates by 30–60% for audiences with significant iOS or Apple Mail usage, silently corrupting the engagement signals your send-time optimization, list segmentation, and campaign ROI calculations depend on. Campaign decisions made on these numbers — suppressing disengaged contacts, picking a winning A/B variant by open rate, or attributing revenue to high-open-rate sequences — all degrade in quality. The data-integrity taxon is directly implicated: the metric exists but measures something fundamentally different from human engagement.
Critical because MPP distortion silently corrupts every downstream metric that uses open rate as an input — A/B test results, segmentation criteria, engagement scoring — producing confident but wrong decisions at scale.
Filter MPP bot opens at ingest time by checking the AppleMailDoPre user-agent and persisting a machine_open boolean on every event row. Add the flag in your pixel or webhook handler:
const isMppBot = (userAgent: string | null): boolean =>
userAgent?.includes('AppleMailDoPre') ?? false
async function recordOpen(
contactId: string,
campaignId: string,
userAgent: string | null
) {
await db.emailEvents.insert({
type: 'open',
contact_id: contactId,
campaign_id: campaignId,
machine_open: isMppBot(userAgent),
user_agent: userAgent,
occurred_at: new Date()
})
}
Then query open rates using WHERE machine_open = false. Expose both the filtered and raw rate in your reporting UI so the distinction is auditable.
ID: campaign-analytics-attribution.tracking-implementation.mpp-aware-open-rates
Severity: critical
What to look for: Examine how open events are processed and how open rates are computed. Look for any filtering logic that identifies Apple MPP bot prefetches. Apple MPP proxy-loads tracking pixels from IP ranges owned by Apple CDN — signals to filter include: user-agent strings containing "AppleMailDoPre", multiple opens from the same contact within seconds of send, or opens from known Apple CDN IP blocks. Check whether open rate denominators include contacts where opens may be bot-generated. Look at how the codebase distinguishes "machine open" from "human open."
Pass criteria: Open rate calculation explicitly accounts for MPP by either (a) filtering known MPP bot user-agent patterns before counting opens, (b) using a "machine-open-aware" open rate that excludes suspected bot prefetches, or (c) displaying click-to-open rate alongside raw open rate with a note that raw open rates include MPP inflation. Enumerate all open-event processing paths and count how many include MPP filtering — report the ratio even on pass.
Fail criteria: Open tracking events are counted without any MPP filtering. Raw open counts are used directly to compute open rates with no mention of MPP distortion. A comment or TODO mentioning MPP without implemented filtering logic does not count as pass.
Skip (N/A) when: The project does not implement email open tracking of any kind.
Cross-reference: The Deliverability Engineering Audit covers bounce-rate inflation from bot activity, which compounds with MPP distortion on open metrics.
Detail on fail: Describe what was found. Example: "Open events stored and counted without any MPP filtering — open rates will be inflated by 30-60% for audiences with high iOS/Apple Mail usage" or "No user-agent or IP-based bot detection found in open event processing"
Remediation: Filter MPP bot opens before computing rates. At minimum, check for the canonical MPP signal in the user-agent:
// In your open event webhook handler or pixel endpoint
const isMppBot = (userAgent: string | null): boolean => {
if (!userAgent) return false
// Apple MPP prefetch user-agent signature
return userAgent.includes('AppleMailDoPre')
}
// When recording opens
async function recordOpen(contactId: string, campaignId: string, userAgent: string | null) {
const machineOpen = isMppBot(userAgent)
await db.emailEvents.insert({
type: 'open',
contact_id: contactId,
campaign_id: campaignId,
machine_open: machineOpen,
user_agent: userAgent,
occurred_at: new Date()
})
}
// When computing open rates
async function getOpenRate(campaignId: string, excludeMachine = true) {
const where = excludeMachine
? { campaign_id: campaignId, type: 'open', machine_open: false }
: { campaign_id: campaignId, type: 'open' }
const opens = await db.emailEvents.count({ where })
const sent = await db.sends.count({ where: { campaign_id: campaignId } })
return sent > 0 ? opens / sent : 0
}
For a deeper analysis of engagement scoring that builds on accurate open rates, the Campaign Orchestration & Sequencing Audit covers engagement scoring in detail.