Gmail Shield

Gmail Sender Shield with GitHub Gist cloud sync

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name           Gmail Shield
// @name:es        Gmail Escudo
// @namespace      http://tampermonkey.net/
// @version        3.0
// @description    Gmail Sender Shield with GitHub Gist cloud sync
// @description:es Gmail Escudo de Correos con sincronización en la nube
// @author         IgnaV
// @match          https://mail.google.com/*
// @icon           https://ssl.gstatic.com/ui/v1/icons/mail/rfr/gmail.ico
// @license        MIT
// @grant          GM_setValue
// @grant          GM_getValue
// @grant          GM_xmlhttpRequest
// ==/UserScript==

(function() {
    'use strict';

    if (window.self !== window.top) return;

    class GistSync {
        constructor() {
            this.token = GM_getValue('gms_token') ?? GM_setValue('gms_token', '') ?? '';
            this.gistId = GM_getValue('gms_gistId') ?? GM_setValue('gms_gistId', '') ?? '';
        }

        isConfigured() {
            return !!this.token && !!this.gistId;
        }

        async fetchGist() {
            if (!this.isConfigured()) return null;

            return new Promise((resolve) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: `https://api.github.com/gists/${this.gistId}`,
                    headers: {
                        'Authorization': `token ${this.token}`,
                        'Accept': 'application/vnd.github.v3+json'
                    },
                    onload: (response) => {
                        try {
                            if (response.status === 200) {
                                const gist = JSON.parse(response.responseText);
                                const filename = Object.keys(gist.files)[0];
                                resolve(JSON.parse(gist.files[filename].content));
                            } else {
                                resolve(null);
                            }
                        } catch (e) {
                            resolve(null);
                        }
                    },
                    onerror: () => resolve(null)
                });
            });
        }

        async updateGist(data) {
            if (!this.isConfigured()) return;

            GM_xmlhttpRequest({
                method: 'PATCH',
                url: `https://api.github.com/gists/${this.gistId}`,
                headers: {
                    'Authorization': `token ${this.token}`,
                    'Accept': 'application/vnd.github.v3+json',
                    'Content-Type': 'application/json'
                },
                data: JSON.stringify({
                    files: {
                        'gmail_shield_data.json': {
                            content: JSON.stringify(data, null, 2)
                        }
                    }
                }),
                onerror: () => {}
            });
        }
    }

    function obtenerCorreo() {
        return new Promise((resolve) => {
            const buscarMeta = () => {
                const meta = document.querySelector('meta[name="og-profile-acct"]');
                if (meta?.content?.includes('@')) {
                    resolve(meta.content);
                    return true;
                }
                return false;
            };

            if (buscarMeta()) return;

            const observer = new MutationObserver(() => {
                if (buscarMeta()) {
                    observer.disconnect();
                }
            });

            observer.observe(document.head, { childList: true, subtree: true });
        });
    }

    obtenerCorreo().then(async (email) => {
        const userId = email || window.location.href.match(/\/u\/(\d+)\//)[1];
        const gistSync = new GistSync();

        const localDomains = GM_getValue('allowedDomains', []);
        const localUserDomains = GM_getValue('allowedUserDomains', {});

        const addIcon = GM_getValue('addIcon', true);
        const addDomain = GM_getValue('addDomain', true);
        const processedElements = new Set();
        const channel = new BroadcastChannel('mi-canal');

        channel.onmessage = () => {
            updateAllDomainStates();
        };

        const hasDomains = (localDomains.length > 0) || (Object.keys(localUserDomains).length > 0);

        if (!addIcon && !addDomain && !hasDomains) return;

        gistSync.fetchGist().then((gistData) => {
            if (gistData) {
                const safeGistData = {
                    addIcon: gistData.addIcon !== undefined ? gistData.addIcon : true,
                    addDomain: gistData.addDomain !== undefined ? gistData.addDomain : true,
                    allowedDomains: gistData.allowedDomains || [],
                    allowedUserDomains: gistData.allowedUserDomains || {}
                };

                const mergedDomains = [...new Set([
                    ...localDomains,
                    ...safeGistData.allowedDomains
                ])];

                const mergedUserDomains = { ...safeGistData.allowedUserDomains };
                for (const id in localUserDomains) {
                    if (!mergedUserDomains[id]) mergedUserDomains[id] = [];
                    mergedUserDomains[id] = [...new Set([
                        ...mergedUserDomains[id],
                        ...localUserDomains[id]
                    ])];
                }

                GM_setValue('allowedDomains', mergedDomains);
                GM_setValue('allowedUserDomains', mergedUserDomains);
                updateAllDomainStates();
            }
        }).catch(() => {});

        setInterval(async () => {
            const freshGist = await gistSync.fetchGist();
            if (freshGist) {
                GM_setValue('allowedDomains', freshGist.allowedDomains || []);
                GM_setValue('allowedUserDomains', freshGist.allowedUserDomains || {});
                updateAllDomainStates();
            }
        }, 5 * 60 * 1000);

        function getCommonDomains() {
            return GM_getValue('allowedDomains', []);
        }

        function getUserDomains() {
            const allUserDomains = GM_getValue('allowedUserDomains', {});
            return allUserDomains[userId] || [];
        }

        function getAllowedDomains() {
            return getCommonDomains().concat(getUserDomains());
        }

        function sortDomainList(list) {
            return list.sort((a, b) => {
                const getDomain = (str) => str.includes('@') ? str.split('@')[1] : str;
                const aDomain = getDomain(a);
                const bDomain = getDomain(b);

                const getSLD = (domain) => {
                    const parts = domain.split('.');
                    return parts[parts.length - 2];
                };

                const aSLD = getSLD(aDomain);
                const bSLD = getSLD(bDomain);

                if (aSLD !== bSLD) return aSLD.localeCompare(bSLD);
                if (aDomain !== bDomain) return aDomain.localeCompare(bDomain);
                return a.localeCompare(b);
            });
        }

        function saveAndSync() {
            const dataToSync = {
                addIcon,
                addDomain,
                allowedDomains: getCommonDomains(),
                allowedUserDomains: GM_getValue('allowedUserDomains', {})
            };

            gistSync.updateGist(dataToSync);
        }

        function extractDomain(email) {
            const domainParts = email.split('@')[1].split('.');
            if (domainParts[domainParts.length - 2] === 'com') {
                return domainParts.slice(-3).join('.');
            }
            return domainParts.slice(-2).join('.');
        }

        function updateDomainState(container, email) {
            const domain = extractDomain(email);
            const allowedDomains = getAllowedDomains();
            container.classList.remove('not-allowed-domain', 'allowed-domain');
            if (allowedDomains.includes(email) || allowedDomains.includes(domain)) {
                container.classList.add('allowed-domain');
            } else {
                container.classList.add('not-allowed-domain');
            }
        }

        function addIconToContainer(domainContainer, domain) {
            const icon = document.createElement('img');
            icon.src = `https://www.google.com/s2/favicons?domain=${domain}`;
            icon.className = 'domain-icon';
            domainContainer.appendChild(icon);
        }

        function addDomainToContainer(domainContainer, domain) {
            const domainSpan = document.createElement('span');
            domainSpan.className = 'domain-text';
            domainSpan.textContent = domain;
            domainContainer.appendChild(domainSpan);
        }

        function addDomainContainer(element, email) {
            const domain = extractDomain(email);
            const domainContainer = document.createElement('div');
            domainContainer.className = 'domain-container';
            domainContainer.title = email;
            domainContainer.onclick = () => domainContainerEvent(domainContainer, email);
            updateDomainState(domainContainer, email);

            addIconToContainer(domainContainer, domain);
            addDomainToContainer(domainContainer, domain);
            element.appendChild(domainContainer);
            return domainContainer;
        }

        function showTooltip(element, message) {
            const tooltip = document.createElement('span');
            tooltip.className = 'custom-tooltip';
            tooltip.textContent = message;

            element.appendChild(tooltip);
            setTimeout(() => {
                if (element.contains(tooltip)) {
                    element.removeChild(tooltip);
                }
            }, 3000);
        }

        function domainContainerEvent(domainContainer, email) {
            event.preventDefault();
            event.stopPropagation();

            const domain = extractDomain(email);
            const commonDomains = getCommonDomains();
            const userDomainsLocal = getUserDomains();
            let message;

            if (userDomainsLocal.includes(domain)) {
                const updated = userDomainsLocal.filter(d => d !== domain);
                const newCommon = sortDomainList([...commonDomains, domain]);

                GM_setValue('allowedDomains', newCommon);
                const allUserDomains = GM_getValue('allowedUserDomains', {});
                allUserDomains[userId] = updated;
                GM_setValue('allowedUserDomains', allUserDomains);
                message = `+ Empresa (Todas las cuentas)`;
            } else if (commonDomains.includes(domain)) {
                const updated = commonDomains.filter(d => d !== domain);
                const newUserDomains = sortDomainList([...userDomainsLocal, email]);

                GM_setValue('allowedDomains', updated);
                const allUserDomains = GM_getValue('allowedUserDomains', {});
                allUserDomains[userId] = newUserDomains;
                GM_setValue('allowedUserDomains', allUserDomains);
                message = `+ Correo (Esta cuenta)`;
            } else if (userDomainsLocal.includes(email)) {
                const updated = userDomainsLocal.filter(d => d !== email);
                const newCommon = sortDomainList([...commonDomains, email]);

                GM_setValue('allowedDomains', newCommon);
                const allUserDomains = GM_getValue('allowedUserDomains', {});
                allUserDomains[userId] = updated;
                GM_setValue('allowedUserDomains', allUserDomains);
                message = `+ Correo (Todas las cuentas)`;
            } else if (commonDomains.includes(email)) {
                const updated = commonDomains.filter(d => d !== email);
                GM_setValue('allowedDomains', updated);
                message = `Eliminado`;
            } else {
                const newUserDomains = sortDomainList([...userDomainsLocal, domain]);
                const allUserDomains = GM_getValue('allowedUserDomains', {});
                allUserDomains[userId] = newUserDomains;
                GM_setValue('allowedUserDomains', allUserDomains);
                message = `+ Empresa (Esta cuenta)`;
            }

            updateAllDomainStates();
            showTooltip(domainContainer, message);
            saveAndSync();

            setTimeout(() => channel.postMessage(message), 200);
        }

        function updateAllDomainStates() {
            const nameElements = document.querySelectorAll('.bA4, .bAK, .bAp');
            nameElements.forEach((element) => {
                const emailElement = element.querySelector('[email]');
                if (!emailElement) return;

                const email = emailElement.getAttribute('email');
                const domainContainer = element.querySelector('.domain-container');

                if (domainContainer) {
                    updateDomainState(domainContainer, email);
                } else {
                    addDomainContainer(element, email);
                }
            });
        }

        function addDomainBelowName() {
            const nameElements = document.querySelectorAll('.bA4, .bAK, .bAp');

            nameElements.forEach((element) => {
                if (processedElements.has(element)) return;

                const emailElement = element.querySelector('[email]');
                if (!emailElement) return;

                const email = emailElement.getAttribute('email');
                addDomainContainer(element, email);
                processedElements.add(element);
            });
        }

        function addStyles() {
            const style = document.createElement('style');
            style.type = 'text/css';
            const css = `
                .bA4, .bAK, .bAp {
                    padding-top: 9px;
                }
                .domain-container {
                    display: flex;
                    align-items: center;
                    margin-top: -4px;
                    font-size: 10px;
                    color: #888;
                    width: fit-content;
                    height: 11px;
                    padding: 1px 2px;
                }
                .domain-container:hover {
                    background-color: #b1b1b1;
                }
                .domain-container.not-allowed-domain:hover {
                    background-color: #e5afaf;
                }
                .domain-icon {
                    width: 10px;
                    height: 10px;
                    margin-right: 3px;
                }
                .domain-text {
                    white-space: nowrap;
                    overflow: hidden;
                    text-overflow: ellipsis;
                    font-size: 10px;
                    color: inherit;
                }
                .not-allowed-domain {
                    background-color: #f8d7da;
                    color: #721c24;
                }
                .allowed-domain {
                    background-color: transparent;
                    color: inherit;
                }
                .custom-tooltip {
                    position: absolute;
                    background-color: #000;
                    color: #fff;
                    padding: 4px;
                    border-radius: 4px;
                    font-size: 12px;
                    white-space: nowrap;
                    z-index: 1000;
                    top: 40px;
                    opacity: 0;
                    transition: opacity 0.3s ease-in-out;
                }
                .custom-tooltip:has(+ .custom-tooltip) {
                    display: none;
                }
                .domain-container:hover .custom-tooltip {
                    opacity: 1;
                }
            `;
            style.appendChild(document.createTextNode(css));
            document.head.appendChild(style);
        }

        addStyles();
        addDomainBelowName();

        const observer = new MutationObserver(() => {
            addDomainBelowName();
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        window.addEventListener('load', () => {
            addDomainBelowName();
        });
    });
})();