Greasy Fork

Greasy Fork is available in English.

全站邮箱一键复制

在任意网页自动识别邮箱地址并添加一键复制按钮,支持纯文本邮箱和 mailto 链接,兼容 React/Next.js 动态渲染页面及 AI 聊天界面(如 Gumloop)。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Email Copy Button for All Sites
// @name:zh-CN   全站邮箱一键复制
// @namespace    http://tampermonkey.net/
// @version      1.7
// @description  Automatically detects email addresses on any webpage and adds a one-click copy button. Supports plain text emails and mailto links, including React/Next.js dynamically rendered pages and AI chat interfaces (e.g. Gumloop).
// @description:zh-CN 在任意网页自动识别邮箱地址并添加一键复制按钮,支持纯文本邮箱和 mailto 链接,兼容 React/Next.js 动态渲染页面及 AI 聊天界面(如 Gumloop)。
// @author       Nosy Swab
// @match        *://*/*
// @exclude      https://apps.sfc.hk/*
// @run-at       document-end
// @grant        none
// @license      MIT
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCIgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0Ij48cmVjdCB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHJ4PSIxMiIgZmlsbD0iIzI1NjNFQiIvPjxyZWN0IHg9IjEwIiB5PSIxOCIgd2lkdGg9IjM0IiBoZWlnaHQ9IjI0IiByeD0iMyIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIyLjUiLz48cG9seWxpbmUgcG9pbnRzPSIxMCwxOCAyNywzMiA0NCwxOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIyLjUiLz48cmVjdCB4PSIzNiIgeT0iMzYiIHdpZHRoPSIxNiIgaGVpZ2h0PSIxOCIgcng9IjMiIGZpbGw9IiMxRDREQjgiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMiIvPjxyZWN0IHg9IjMxIiB5PSIzMSIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE4IiByeD0iMyIgZmlsbD0iIzM3ODRGOCIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIyIi8+PC9zdmc+
// ==/UserScript==

(function () {
  'use strict';

  if (location.hostname === 'apps.sfc.hk') return;

  var EMAIL_RE = /[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/;
  var EMAIL_RE_G = /[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/g;

  // v1.7: Removed CODE and PRE from skip list to support AI chat output (e.g. Gumloop)
  var SKIP_TAGS = new Set([
    'SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT',
    'HEAD', 'IFRAME', 'BUTTON'
  ]);

  var pending = [];
  var scheduled = false;
  var MAX_WALK_PER_CALL = 2000;
  var MAX_PROCESS_PER_BATCH = 50;

  function makeBtn(email) {
    var btn = document.createElement('button');
    btn.textContent = '\uD83D\uDCCB';
    btn.title = email;
    btn.setAttribute('data-copy-btn', '1');
    btn.style.cssText =
      'display:inline-block;margin-left:5px;padding:0 5px;' +
      'background:#00695c;color:#fff;border:none;border-radius:3px;' +
      'cursor:pointer;font-size:11px;height:17px;line-height:17px;' +
      'vertical-align:middle;font-family:sans-serif';

    btn.addEventListener('click', function (e) {
      e.stopPropagation();
      e.preventDefault();

      function showSuccess() {
        btn.textContent = '\u2705';
        setTimeout(function () { btn.textContent = '\uD83D\uDCCB'; }, 2000);
      }

      if (!navigator.clipboard || !navigator.clipboard.writeText) {
        var tmp = document.createElement('textarea');
        tmp.value = email;
        tmp.style.cssText = 'position:fixed;top:0;left:-9999px;opacity:0';
        document.body.appendChild(tmp);
        tmp.select();
        try { document.execCommand('copy'); } catch (err) {}
        document.body.removeChild(tmp);
        showSuccess();
        return;
      }

      navigator.clipboard.writeText(email).then(showSuccess).catch(function () {
        var tmp = document.createElement('textarea');
        tmp.value = email;
        tmp.style.cssText = 'position:fixed;top:0;left:-9999px;opacity:0';
        document.body.appendChild(tmp);
        tmp.select();
        try { document.execCommand('copy'); } catch (err) {}
        document.body.removeChild(tmp);
        showSuccess();
      });
    });

    return btn;
  }

  function isInDocument(node) {
    try {
      return document.contains ? document.contains(node) : document.body.contains(node);
    } catch (e) {
      return false;
    }
  }

  function processMailtoLink(a) {
    if (!a || !a.parentNode) return;
    if (!isInDocument(a)) return;
    if (a.hasAttribute('data-email-tagged')) return;

    var href = a.getAttribute('href') || '';
    if (href.toLowerCase().indexOf('mailto:') !== 0) return;

    var email = href.replace(/^mailto:/i, '').split('?')[0].trim();
    if (!email) email = (a.textContent || '').trim();
    if (!email || !EMAIL_RE.test(email)) return;

    var next = a.nextSibling;
    while (next && next.nodeType === Node.TEXT_NODE && next.textContent.trim() === '') {
      next = next.nextSibling;
    }
    if (next && next.nodeType === Node.ELEMENT_NODE &&
      next.getAttribute && next.getAttribute('data-copy-btn')) return;

    a.setAttribute('data-email-tagged', '1');
    try {
      a.parentNode.insertBefore(makeBtn(email), a.nextSibling);
    } catch (e) {}
  }

  function processTextNode(node) {
    if (!node || !node.parentNode) return;
    if (!isInDocument(node)) return;

    var text = node.textContent;
    if (!text || text.indexOf('@') === -1) return;

    var parent = node.parentNode;
    if (SKIP_TAGS.has(parent.nodeName)) return;
    if (parent.nodeName === 'A') return;
    if (parent.hasAttribute &&
      (parent.hasAttribute('data-email-tagged') ||
        parent.hasAttribute('data-copy-btn'))) return;

    EMAIL_RE_G.lastIndex = 0;
    var matches = [];
    var m;
    while ((m = EMAIL_RE_G.exec(text)) !== null) {
      matches.push({ email: m[0], index: m.index, length: m[0].length });
    }
    if (!matches.length) return;

    var frag = document.createDocumentFragment();
    var last = 0;
    for (var i = 0; i < matches.length; i++) {
      var match = matches[i];
      if (match.index > last) {
        frag.appendChild(document.createTextNode(text.slice(last, match.index)));
      }
      var span = document.createElement('span');
      span.textContent = match.email;
      span.setAttribute('data-email-tagged', '1');
      frag.appendChild(span);
      frag.appendChild(makeBtn(match.email));
      last = match.index + match.length;
    }
    if (last < text.length) {
      frag.appendChild(document.createTextNode(text.slice(last)));
    }

    try {
      parent.replaceChild(frag, node);
    } catch (e) {}
  }

  function queueFromRoot(root) {
    if (!root) return;

    if (root.nodeType === Node.ELEMENT_NODE) {
      try {
        var style = window.getComputedStyle(root);
        if (style && (style.display === 'none' || style.visibility === 'hidden')) return;
      } catch (e) {
        return;
      }

      try {
        var allLinks = (root.nodeName === 'A')
          ? [root]
          : root.querySelectorAll('a[href]');
        for (var i = 0; i < allLinks.length; i++) {
          var h = allLinks[i].getAttribute('href') || '';
          if (h.toLowerCase().indexOf('mailto:') === 0) {
            pending.push({ type: 'mailto', el: allLinks[i] });
          }
        }
      } catch (e) {}
    }

    try {
      var walker = document.createTreeWalker(
        root,
        NodeFilter.SHOW_TEXT,
        {
          acceptNode: function (node) {
            var p = node.parentNode;
            if (!p) return NodeFilter.FILTER_REJECT;
            if (SKIP_TAGS.has(p.nodeName)) return NodeFilter.FILTER_REJECT;
            if (p.nodeName === 'A') return NodeFilter.FILTER_REJECT;
            if (p.hasAttribute &&
              (p.hasAttribute('data-email-tagged') ||
                p.hasAttribute('data-copy-btn'))) return NodeFilter.FILTER_REJECT;
            if (!node.textContent || node.textContent.indexOf('@') === -1)
              return NodeFilter.FILTER_REJECT;
            return NodeFilter.FILTER_ACCEPT;
          }
        }
      );

      var n;
      var limit = MAX_WALK_PER_CALL;
      while ((n = walker.nextNode()) && limit-- > 0) {
        pending.push({ type: 'text', el: n });
      }
    } catch (e) {}

    if (pending.length) schedule();
  }

  function flush(deadline) {
    scheduled = false;
    var count = 0;
    while (pending.length > 0 && count < MAX_PROCESS_PER_BATCH) {
      if (deadline && typeof deadline.timeRemaining === 'function' && deadline.timeRemaining() < 2) {
        schedule();
        return;
      }
      var item = pending.shift();
      if (item.type === 'mailto') {
        processMailtoLink(item.el);
      } else {
        processTextNode(item.el);
      }
      count++;
    }
    if (pending.length) schedule();
  }

  function schedule() {
    if (scheduled) return;
    scheduled = true;
    if ('requestIdleCallback' in window) {
      window.requestIdleCallback(flush, { timeout: 1500 });
    } else {
      setTimeout(function () { flush({}); }, 100);
    }
  }

  queueFromRoot(document.body);

  // v1.7: Extended retry delays to cover AI streaming output (up to 15s)
  [500, 1000, 2000, 3000, 5000, 8000, 12000, 15000].forEach(function (delay) {
    setTimeout(function () { queueFromRoot(document.body); }, delay);
  });

  var obs = new MutationObserver(function (mutations) {
    for (var i = 0; i < mutations.length; i++) {
      var m = mutations[i];
      // v1.7: Also handle characterData mutations for streaming text output
      if (m.type === 'characterData') {
        var tn = m.target;
        if (tn && tn.textContent && tn.textContent.indexOf('@') !== -1) {
          pending.push({ type: 'text', el: tn });
        }
      }
      for (var j = 0; j < m.addedNodes.length; j++) {
        var n = m.addedNodes[j];
        if (n.nodeType === Node.TEXT_NODE) {
          if (n.textContent && n.textContent.indexOf('@') !== -1) {
            pending.push({ type: 'text', el: n });
          }
        } else if (n.nodeType === Node.ELEMENT_NODE) {
          queueFromRoot(n);
        }
      }
    }
    if (pending.length) schedule();
  });

  try {
    // v1.7: Added characterData:true to capture streaming text mutations
    obs.observe(document.body, { childList: true, subtree: true, characterData: true });
  } catch (e) {}
})();