Is MailProtect: Protect your Gmail privacy for free. safe?
MailProtect sends opened Gmail message details to app.mailprotect.ai when it detects tracking pixels.
When a Gmail message is opened, MailProtect inspects the message body for zero-size tracking images. If it detects one, it sends the message subject, visible body text, sender, recipients, Gmail message and thread IDs, and timestamps to its backend.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Gmail tracker reports include opened email content
When you open a Gmail message with a zero-width/zero-height image, the extension builds a report with message text, sender, recipients, subject, Gmail message/thread IDs, timestamps, then sends it to app.mailprotect.ai/api/threats/create.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You open a Gmail message that contains a zero-size image.
The content script is registered only for mail.google.com pages.
The extension creates a report from the opened message and sends it to MailProtect.
The report includes message body text and Gmail message metadata.
- Opened message textHi Alex, the quarterly planning notes are below. (illustrative)
This can include the readable text from the Gmail message you opened.
- Sender addresssender@example.com (illustrative)
This identifies who sent the opened email.
- Recipient addressesalex@example.com, team@example.com (illustrative)
This identifies people included on the opened email.
- Email subjectQ3 planning notes (illustrative)
This can reveal the topic of the message you opened.
- Gmail message ID18f7c2a9d4b6e123 (illustrative)
This lets the report refer back to a specific Gmail message.
- Gmail thread IDthread-a:r7123456789012345678 (illustrative)
This links the report to the Gmail conversation thread.
- Read and block timestamps2026-07-13T08:35:27.978Z (illustrative)
This records when the extension processed the opened message.
- Content-Type
- application/json
- Authorization
- Bearer <redacted>
The message handler builds the report and the API client posts it
Readable equivalent of the Gmail message handler
assets/js/index.51566434.jsuseEffect(() => { async function handleOpenedGmailMessage(messageView) { const messageId = await messageView.getMessageIDAsync(); setMessageId(messageId); const bodyElement = messageView.getBodyElement(); const images = bodyElement.querySelectorAll("img"); let trackingPixelDetected = false; images.forEach((image) => { if (image.width <= 0 || image.height <= 0) { image.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; image.style.display = "none"; trackingPixelDetected = true; } }); if (trackingPixelDetected) { const categoryInput = { object: messageView.getThreadView().getSubject(), payload: messageView.getBodyElement().innerText, sender: messageView.getSender().emailAddress }; const categoryResult = detectCategory(categoryInput); const recipients = messageView.getRecipients().map((recipient) => recipient.emailAddress); const sender = messageView.getSender().emailAddress; const subject = messageView.getThreadView().getSubject(); setTrackingDetected(true); setRecipients(recipients); setSubject(subject); setSender(sender); setMessageView(messageView); let report = { recipients: messageView.getRecipients().map((recipient) => recipient.emailAddress), object: messageView.getThreadView().getSubject(), payload: messageView.getBodyElement().innerText, sender: messageView.getSender().emailAddress, blockedAt: new Date().toISOString(), readAt: new Date().toISOString(), gmailMessageId: await messageView.getMessageIDAsync(), gmailThreadId: await messageView.getThreadView().getThreadIDAsync(), category: categoryResult.category }; report = await GmailProtectAPI.getDefaultInstance().createThreat(report); setMessages((oldMessages) => { const oldMessageIndex = oldMessages.findIndex((oldMessage) => report.gmailThreadId === oldMessage.gmailThreadId); if (oldMessageIndex > -1) { oldMessages[oldMessageIndex] = report; oldMessages[oldMessageIndex].status = isOverUsageLimit(user) ? STATUS.NOT_BLOCKED : STATUS.BLOCKED; } else { report.status = isOverUsageLimit(user) ? STATUS.NOT_BLOCKED : STATUS.BLOCKED; oldMessages.push(report); } return [...oldMessages]; }); if (user) { user.monthlyBlockedThreatsCount = (user.monthlyBlockedThreatsCount || 0) + 1; setUser({ ...user }); } } else { setTrackingDetected(false); } } const unregister = inbox?.Conversations.registerMessageViewHandler(handleOpenedGmailMessage); return () => { unregister?.(); };}, [inbox]);Readable equivalent of the API post path
assets/js/Flex.82e34646.jslet domain = "https://app.mailprotect.ai";if (isDevMode && isDevMode()) { domain = "https://mailprotect.ngrok.io";}const Config = { domain };let instance = null;class GmailProtectAPI { constructor(api) { this.api = api; if (!api) { throw new Error("No axios instance provided"); } } async me() { const response = await this.api.get("/auth/initSession"); return { ...response.data?.session?.user, ip: response.data?.ip, locationData: response.data?.localisationData }; } async ip() { return (await this.api.get("/ip")).data; } async createThreat(report) { try { const response = await this.api.post("/threats/create", report); return response.data; } catch (error) { console.error({ message: "Error creating threat", description: error.message }); throw error; } } async getMessageStatus(threadId) { return (await this.api.get("/threats/status", { params: { gmailThreadId: threadId } })).data; }}GmailProtectAPI.getInstanceNotForGP = function(api) { return new GmailProtectAPI(api);};GmailProtectAPI.getDefaultInstance = function() { const token = localStorage.getItem("gmailprotect-token"); if (!token) { instance = new GmailProtectAPI(GmailProtectAPI.getAxios("")); } else if (!instance) { instance = new GmailProtectAPI(GmailProtectAPI.getAxios(token || "")); } return instance;};GmailProtectAPI.getAxios = function(token) { logInfo("Running API on " + Config.domain); const api = axios.create({ baseURL: Config.domain + "/api", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, withCredentials: true }); api.interceptors.response.use(async (response) => { if (response.status === 202) { console.log("HTTP 202 received, polling operation..."); console.log("Operation running at " + response.headers.location); let pollingResponse = await api.get(response.headers.location); console.log("Operation status is " + pollingResponse.data.status); for (let i = 0; i < 600; i++) { pollingResponse = await api.get(response.headers.location, { params: { timestamp: Date.now() } }); console.log("Operation status in loop is " + pollingResponse.data.status); if (window?.progressCallback) { window.progressCallback(pollingResponse.data); } if (pollingResponse.data.status === "completed" || pollingResponse.data.status === "failed") { break; } await new Promise((resolve) => setTimeout(resolve, 2000)); } if (pollingResponse.data.status === "completed") { return pollingResponse; } } return response; }); return api;};- app.mailprotect.ai
Receives the Gmail message report at /api/threats/create.
What it can do
Permissions this extension asks for, as declared in version 1.0.34. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.
Read and change your data on mail.google.com
*://mail.google.com/
Read and change your data on app.mailprotect.ai
*://app.mailprotect.ai/
Show you desktop notifications
notifications
Block and redirect the requests your browser makes
declarativeNetRequest
Run its own code inside the pages you visit
scripting
Where it sends data
Destinations our analysis observed The ultimate Gmail privacy guardian contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.
- app.mailprotect.ai
The ultimate Gmail privacy guardian sends data to app.mailprotect.ai. No other extension we have analysed sends data here.