Warns if links in a Gmail body do not match the sender's core domain, safely handling URLs with email parameters.
// ==UserScript==
// @name Gmail Link Domain Validator (Query Safe Engine)
// @namespace http://tampermonkey.net/
// @version 1.5
// @description Warns if links in a Gmail body do not match the sender's core domain, safely handling URLs with email parameters.
// @author KevinP
// @match https://mail.google.com/mail/*
// @grant none
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Extracts the primary domain block (e.g., "google.com" from "sub.mail.google.com")
function getRootDomain(urlOrEmail) {
try {
let clean = urlOrEmail.trim().toLowerCase();
// If it's an email address, just look at what's after the '@'
if (clean.includes('@') && !clean.startsWith('http://') && !clean.startsWith('https://')) {
clean = clean.split('@')[1];
}
// Strip out query parameters and paths entirely before parsing to prevent query injection bugs
if (clean.includes('?')) clean = clean.split('?')[0];
if (clean.includes('/')) {
// Keep the protocol if present, otherwise split by slash
if (clean.startsWith('http://') || clean.startsWith('https://')) {
const urlParts = clean.split('/');
clean = urlParts[2]; // Index 2 is the hostname (e.g., accounts.google.com)
} else {
clean = clean.split('/')[0];
}
}
// Ensure it has a protocol string for the native URL parser to read it as a hostname
if (!clean.startsWith('http://') && !clean.startsWith('https://')) {
clean = 'https://' + clean;
}
const parsed = new URL(clean);
const hostname = parsed.hostname;
const parts = hostname.split('.');
if (parts.length <= 2) return hostname;
const secondToLast = parts[parts.length - 2];
const commonCcTlds = ['co', 'com', 'org', 'net', 'gov', 'edu', 'ac'];
if (commonCcTlds.includes(secondToLast) && parts.length > 2) {
return parts.slice(-3).join('.');
}
return parts.slice(-2).join('.');
} catch (e) {
return null;
}
}
function verifyGmailLinks() {
// 1. Target Gmail's primary content view area
const activeThread = document.querySelector('div[role="main"]');
if (!activeThread) return;
// 2. Select any element that looks like an email body card
const emailBodies = activeThread.querySelectorAll('.a3s, [dir="ltr"]');
emailBodies.forEach(body => {
if (body.innerHTML.length < 50 || body.getAttribute('data-domain-checked') === 'true') return;
// 3. Ascend up the tree to isolate this specific email card block
let currentParent = body.parentElement;
let senderElement = null;
let loops = 0;
while (currentParent && !senderElement && loops < 10) {
senderElement = currentParent.querySelector('span[email]');
if (!senderElement) {
currentParent = currentParent.parentElement;
loops++;
}
}
if (!senderElement) return;
const senderEmail = senderElement.getAttribute('email');
const senderRoot = getRootDomain(senderEmail);
if (!senderRoot) return;
// Mark this body block processed
body.setAttribute('data-domain-checked', 'true');
// 4. Find all hyperlinks inside this message body
const links = body.querySelectorAll('a[href]');
links.forEach(link => {
const linkHref = link.getAttribute('href');
const linkRoot = getRootDomain(linkHref);
// Ignore anchors, local layout actions, and email links
if (!linkRoot || linkHref.startsWith('mailto:') || linkHref.startsWith('#')) {
return;
}
// 5. Check if domains match or satisfy special exception rules
let isMatch = (linkRoot === senderRoot);
// Exception: Allow google.com senders to safely link to c.gle targets
if (senderRoot === 'google.com' && linkRoot === 'c.gle') {
isMatch = true;
}
// 6. Highlight mismatched targets
if (!isMatch) {
link.style.backgroundColor = '#ffebee';
link.style.border = '2px dashed #d32f2f';
link.style.padding = '2px 4px';
link.style.borderRadius = '3px';
link.title = `⚠️ WARNING: Link targets ${linkRoot}, but sender is ${senderRoot}`;
if (!link.querySelector('.domain-warn-icon')) {
const icon = document.createElement('span');
icon.className = 'domain-warn-icon';
icon.innerText = '⚠️ ';
link.insertBefore(icon, link.firstChild);
}
}
});
});
}
// Check the interface every 1 second
setInterval(verifyGmailLinks, 1000);
})();