Greasy Fork is available in English.

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