Gmail Shield

Gmail Sender Shield with GitHub Gist cloud sync

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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