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.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);
})();