Gmail Link Domain Validator (Query Safe Engine)

Warns if links in a Gmail body do not match the sender's core domain, safely handling URLs with email parameters.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

Advertisement:

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

Advertisement:

// ==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);
})();