ProtonMail Dark Theme for Email Content

Adds dark theme to ProtonMail email reading and composition areas

As of 2024-11-05. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         ProtonMail Dark Theme for Email Content
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  Adds dark theme to ProtonMail email reading and composition areas
// @match        https://mail.proton.me/*
// @match        https://proton.me/mail/*
// @grant        GM_addStyle
// @grant        unsafeWindow
// @license      WTFPL
// ==/UserScript==

(function() {
    'use strict';

    // Main window CSS
    GM_addStyle(`
        /* Composer window and borders */
        .composer, .composer .composer-content--rich-edition {
            background-color: #1a1a1a !important;
            border-color: #333 !important;
        }

        .composer-inner {
            background-color: #1a1a1a !important;
            border-color: #333 !important;
        }

        .composer-title {
            background-color: #1a1a1a !important;
            border-color: #333 !important;
        }

        /* Message containers */
        .message-container,
        .message-content,
        .message-content-wrapper,
        .scroll-horizontal-shadow {
            background-color: #1a1a1a !important;
            border-color: #333 !important;
        }

        /* General containers and borders */
        .rounded-lg,
        .border,
        [class*="border-"] {
            border-color: #333 !important;
        }

        /* Message header and footer areas */
        .message-header-wrapper,
        .message-footer-wrapper {
            background-color: #1a1a1a !important;
        }

        /* Toolbar and button backgrounds */
        .editor-toolbar,
        .composer-toolbar {
            background-color: #262626 !important;
            border-color: #333 !important;
        }
    `);

    // CSS to inject into iframes
    const darkThemeCSS = `
        body {
            background-color: #1a1a1a !important;
            color: #e0e0e0 !important;
        }

        p, div, span, h1, h2, h3, h4, h5, h6, td, th, li {
            color: #e0e0e0 !important;
            background-color: #1a1a1a !important;
        }

        [style*="background-color"],
        [bgcolor] {
            background-color: #1a1a1a !important;
        }

        [style*="color"],
        [color] {
            color: #e0e0e0 !important;
        }

        blockquote {
            border-left-color: #404040 !important;
            background-color: #262626 !important;
            color: #e0e0e0 !important;
        }

        a {
            color: #66b3ff !important;
        }

        /* Table backgrounds */
        table, tr, td, th {
            background-color: #1a1a1a !important;
            border-color: #333 !important;
        }

        /* Force color on common elements that might have inline styles */
        [style*="color: rgb(0, 0, 0)"],
        [style*="color: black"],
        [style*="color:#000"],
        [style*="color: #000"],
        font[color] {
            color: #e0e0e0 !important;
        }

         .composer {
            background-color: #1a1a1a !important;
            border-color: #333 !important;
        }

        .composer-inner {
            background-color: #1a1a1a !important;
            border-color: #333 !important;
        }

        .composer-title {
            background-color: #1a1a1a !important;
            border-color: #333 !important;
        }

        /* Message containers */
        .message-container,
        .message-content-wrapper,
        .scroll-horizontal-shadow {
            background-color: #1a1a1a !important;
            border-color: #333 !important;
        }

        /* General containers and borders */
        .rounded-lg,
        .border,
        [class*="border-"] {
            border-color: #333 !important;
        }

        /* Message header and footer areas */
        .message-header-wrapper,
        .message-footer-wrapper {
            background-color: #1a1a1a !important;
        }

        /* Toolbar and button backgrounds */
        .editor-toolbar,
        .composer-toolbar {
            background-color: #262626 !important;
            border-color: #333 !important;
        }
    `;

    // Function to inject styles into an iframe
    function injectIframeStyles(iframe) {
        try {
            const inject = () => {
                if (!iframe.contentDocument) return;

                // Create and inject stylesheet if it doesn't exist
                if (!iframe.contentDocument.querySelector('#dark-theme-style')) {
                    const style = iframe.contentDocument.createElement('style');
                    style.id = 'dark-theme-style';
                    style.textContent = darkThemeCSS;
                    iframe.contentDocument.head.appendChild(style);
                }

                // Force color on any elements with inline styles
                const elements = iframe.contentDocument.querySelectorAll('*');
                elements.forEach(el => {
                    if (el.tagName !== 'IMG') {
                        el.style.setProperty('background-color', '#1a1a1a', 'important');
                        el.style.setProperty('color', '#e0e0e0', 'important');
                        if (el.style.backgroundImage && el.style.backgroundImage !== 'none') {
                            el.style.setProperty('background-image', 'none', 'important');
                        }
                    }
                });

                // Add mutation observer for dynamically added content within the iframe
                const observer = new MutationObserver((mutations) => {
                    mutations.forEach((mutation) => {
                        mutation.addedNodes.forEach((node) => {
                            if (node.nodeType === 1 && node.tagName !== 'IMG') {
                                node.style.setProperty('background-color', '#1a1a1a', 'important');
                                node.style.setProperty('color', '#e0e0e0', 'important');
                            }
                        });
                    });
                });

                observer.observe(iframe.contentDocument.body, {
                    childList: true,
                    subtree: true,
                    attributes: true,
                    attributeFilter: ['style', 'class']
                });
            };

            // If iframe is already loaded
            if (iframe.contentDocument && iframe.contentDocument.readyState === 'complete') {
                inject();
            }

            // Also listen for load event
            iframe.addEventListener('load', inject);

        } catch (e) {
            // Handle cross-origin errors silently
        }
    }

    // Function to watch a specific container for iframes
    function watchContainer(container) {
        if (!container) return;

        // Process any existing iframes
        container.querySelectorAll('iframe').forEach(iframe => {
            injectIframeStyles(iframe);
        });

        // Watch for new iframes
        const observer = new MutationObserver((mutations) => {
            mutations.forEach(mutation => {
                mutation.addedNodes.forEach(node => {
                    // Direct iframe
                    if (node.tagName === 'IFRAME') {
                        injectIframeStyles(node);
                    }
                    // Iframe within added node
                    if (node.querySelectorAll) {
                        node.querySelectorAll('iframe').forEach(iframe => {
                            injectIframeStyles(iframe);
                        });
                    }
                });
            });
        });

        observer.observe(container, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['src', 'srcdoc']
        });
    }

    // Function to set up all observers
    function setupObservers() {
        // Watch for new messages and composers
        const bodyObserver = new MutationObserver((mutations) => {
            mutations.forEach(mutation => {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === 1) {
                        // Watch message containers
                        if (node.classList?.contains('message-container')) {
                            watchContainer(node);
                        }
                        // Watch for reply composer
                        if (node.classList?.contains('reply-wrapper')) {
                            watchContainer(node);
                        }
                        // Watch any other potential containers
                        node.querySelectorAll('.message-container, .reply-wrapper, .composer-body-container').forEach(container => {
                            watchContainer(container);
                        });
                    }
                });
            });
        });

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

        // Process existing containers
        document.querySelectorAll('.message-container, .reply-wrapper, .composer-body-container').forEach(container => {
            watchContainer(container);
        });
    }

    // Initial setup
    setupObservers();

    // Handle route changes
    let lastUrl = location.href;
    new MutationObserver(() => {
        const url = location.href;
        if (url !== lastUrl) {
            lastUrl = url;
            setTimeout(setupObservers, 500);
        }
    }).observe(document, { subtree: true, childList: true });

    // Check periodically for new reply composers
    setInterval(() => {
        document.querySelectorAll('.reply-wrapper iframe').forEach(iframe => {
            if (!iframe.contentDocument?.querySelector('#dark-theme-style')) {
                injectIframeStyles(iframe);
            }
        });
    }, 1000);

})();