Proton Mail Signature Remover

Automatically removes email signature for free users of Proton Mail

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

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

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

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

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

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

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name Proton Mail Signature Remover
// @description Automatically removes email signature for free users of Proton Mail
// @version 2.0.10
// @author guihkx
// @match https://mail.protonmail.com/*
// @match https://mail.proton.me/*
// @run-at document-idle
// @license MIT; https://opensource.org/licenses/MIT
// @namespace https://github.com/guihkx
// @icon https://mail.proton.me/assets/favicon.ico
// @homepageURL https://github.com/guihkx/user-scripts
// @supportURL https://github.com/guihkx/user-scripts/issues
// @noframes
// ==/UserScript==

/**
 * Changelog:
 *
 * v2.0.10 (2025-01-03):
 * - Replace the "onload()" event from the rich-text composer iframe by periodic setInterval() checks.
 *
 * v2.0.9 (2024-09-20):
 * - Fix compatibility with Proton Mail v5.0.47.8.
 *
 * v2.0.8 (2024-01-11):
 * - Avoid deleting the custom user signature.
 *
 * v2.0.7 (2024-01-09):
 * - Remove debug statement.
 *
 * v2.0.6 (2024-01-09):
 * - Update selector for the HTML composer.
 * - Use 'instanceof' where applicable.
 *
 * v2.0.5 (2022-10-27):
 * - Fix script icon.
 *
 * v2.0.4 (2022-10-27):
 * - Fix compatibility with Proton Mail v5.0.10.
 * - Prevent script from running twice due to nested frames.
 *
 * v2.0.3 (2022-05-25):
 * - Fixed signature in text-mode editor, again (why do they keep changing it?)
 * - Added support for their new domain (mail.proton.me)
 * - Updated @icon metadata
 * - Replaced 'ProtonMail' by 'Proton Mail'
 *
 * v2.0.2 (2022-04-19):
 * - Updated a bunch of class names, so now the script should work again.
 * - Fixed signature detection in the text-only editor.
 *
 * v2.0.1 (2022-02-08):
 * - Fixed signature not being removed in the HTML composer ("rich text editor").
 * - Added logging for when a signature is found and removed.
 *
 * v2.0.0 (2021-10-03):
 * - Huge code rewrite
 * - Remove support for old.protonmail.com (Sorry!)
 * - Now removes the signature from both HTML and text-based emails (previously it could only do HTML-based ones)
 * - Updated script icon
 *
 * v1.0.0 (2020-04-28):
 * - First release
 */

;(() => {
  const log = console.log.bind(console, '[Proton Mail Signature Remover]')

  log('Waiting for the composer container...')

  const id = setInterval(() => {
    const composerContainer = document.querySelector('div.app-root > div:not([class]):last-child')

    if (composerContainer === null) {
      return
    }
    clearInterval(id)

    log('Ready.')

    const composerContainerMonitor = new MutationObserver(observeComposerContainer)
    composerContainerMonitor.observe(composerContainer, {
      childList: true
    })
  }, 500)

  function observeComposerContainer (mutations) {
    let composerFound = false
    let composerNode
    for (const mutation of mutations) {
      for (const addedNode of mutation.addedNodes) {
        if (!(addedNode instanceof HTMLElement)) {
          continue
        }
        if (!addedNode.classList.contains('composer')) {
          log('Unexpected non-composer node:', addedNode)
          continue
        }
        composerFound = true
        composerNode = addedNode
        break
      }
      if (composerFound) {
        break
      }
    }
    if (!composerFound) {
      return
    }
    // Loop until the email composer is fully rendered...
    const id = setInterval(() => {
      // Determine if this is a plain-text email composer.
      const textareaEmailBody = composerNode.querySelector('textarea[data-testid="editor-textarea"]')

      if (textareaEmailBody !== null) {
        clearInterval(id)
        // The is a pkain-text email composer.
        log('A plain-text email composer has been rendered.')
        // Although at this point the text-only email composer has been rendered,
        // the textarea that contains the email body will still be empty.
        // As I couldn't figure out a proper way to determine when the textarea has been filled,
        // this ugly, hacky approach keeps monitoring the textarea until its value is not empty anymore.
        removeTextSignature(textareaEmailBody)
        return
      }
      // Determine if this is rich-text email composer.
      const composerFrame = composerNode.querySelector('iframe[data-testid="rooster-iframe"]')

      if (composerFrame !== null) {
        // The is a rich-text email composer.
        clearInterval(id)
        const id2 = setInterval(() => {
          const roosterEditor = composerFrame.contentDocument.getElementById('rooster-editor')

          if (!roosterEditor) {
            log('Rich-text email composer not ready yet...')
            return
          }
          clearInterval(id2)
          log('A rich-text email composer has been rendered.')
          const signatureNode = roosterEditor.querySelector('div.protonmail_signature_block')

          if (signatureNode) {
            // Signature node has been already rendered, remove it directly.
            removeHTMLSignature(signatureNode)
          } else {
            // Signature node hasn't been rendered yet, so monitor it.
            setupHTMLComposerObserver(roosterEditor)
          }
        // roosterEditor is not ready, try again in 50ms...
        }, 50)
      }
      // The composer frame is not ready, try again in 50ms...
    }, 50)
  }

  function removeTextSignature (textareaEmailBody) {
    const textSignature = /\n\n^Sent with Proton Mail secure email\.$/m

    // Monitors the textarea containing the text-based email body.
    // Runs at every 100ms and stops once the textarea's value is not empty.
    const id = setInterval(() => {
      if (textareaEmailBody.value === '') {
        return
      }
      if (!textSignature.test(textareaEmailBody.value)) {
        clearInterval(id)
        return
      }
      clearInterval(id)

      textareaEmailBody.value = textareaEmailBody.value.replace(textSignature, '')
      log('Signature successfully removed from text-only email.')

      // Move the text cursor back to the beginning of the textarea.
      textareaEmailBody.setSelectionRange(0, 0)
    }, 100)
  }

  function setupHTMLComposerObserver (roosterEditor) {
    const htmlComposerMonitor = new MutationObserver(detectHTMLSignatureNode)
    htmlComposerMonitor.observe(roosterEditor, {
      childList: true
    })
  }

  function detectHTMLSignatureNode (mutations, observer) {
    for (const mutation of mutations) {
      for (const addedNode of mutation.addedNodes) {
        if (!(addedNode instanceof addedNode.ownerDocument.defaultView.HTMLElement)) {
          continue
        }
        if (!addedNode.classList.contains('protonmail_signature_block')) {
          continue
        }
        // Found the signature node, stop observing and remove it.
        observer.disconnect()
        removeHTMLSignature(addedNode)
        return
      }
    }
  }

  function isBlankLine (node) {
    if (!node) {
      return false
    }
    if (!(node instanceof node.ownerDocument.defaultView.HTMLDivElement)) {
      return false
    }
    if (!node.firstElementChild) {
      return false
    }
    if (!(node.firstElementChild instanceof node.firstElementChild.ownerDocument.defaultView.HTMLBRElement)) {
      return false
    }
    return true
  }

  function removeHTMLSignature (signatureNode) {
    // Email composer structure with custom user signature:
    //
    // <div id="rooster-editor">
    //   <div><br></div>
    //   <div><br></div>
    //   <div class="protonmail_signature_block">
    //     <div class="protonmail_signature_block-user">
    //       <div>Thanks,</div>
    //       <div>Guilherme<br></div>
    //     </div>
    //     <div><br></div>
    //     <div class="protonmail_signature_block-proton">
    //       Sent with <a href="https://proton.me/">Proton Mail</a> secure email.
    //     </div>
    //    </div>
    //  </div>

    // Email composer structure without custom user signature:
    //
    // <div id="rooster-editor">
    //   <div><br></div>
    //   <div><br></div>
    //   <div class="protonmail_signature_block">
    //     <div class="protonmail_signature_block-user protonmail_signature_block-empty"></div>
    //     <div class="protonmail_signature_block-proton">Sent with <a href="https://proton.me/">Proton Mail</a> secure email.</div>
    //   </div>
    // </div>
    const protonSignature = signatureNode.querySelector('.protonmail_signature_block-proton')

    if (!protonSignature) {
      log('BUG: Unable to find Proton\'s HTML signature to remove.')
      return
    }
    // Checks if there is a custom user signature.
    const userSignature = signatureNode.querySelector('.protonmail_signature_block-user:not(.protonmail_signature_block-empty)')

    if (!userSignature) {
      // Since there is no custom user signature, we can remove two blank lines added above the Proton signature.
      for (let i = 0; i < 2; i++) {
        if (isBlankLine(signatureNode.previousElementSibling)) {
          signatureNode.previousElementSibling.remove()
        }
      }
    }

    // Remove a blank line added below the Proton signature.
    if (isBlankLine(signatureNode.nextElementSibling)) {
      signatureNode.nextElementSibling.remove()
    }

    // Remove a blank line below the custom user signature.
    if (userSignature && isBlankLine(userSignature.nextElementSibling)) {
      userSignature.nextElementSibling.remove()
    }

    // Finally, remove the Proton signature node itself.
    protonSignature.remove()
    log('Signature successfully removed from HTML email.')
  }
})()