油猴脚本: 在gmail中回复邮件时, 可以自定义引用文本

古早的gmail有个lab功能,
可以在邮件正文中选中一段文字,
然后按下回复的快捷键r,
这段文字就会自动加入到回复文本框的最上面, 作为引用文字.
然后下面可以输入自己的回复.

我一直觉得这样的引用非常有针对性.
比嵌套性的引用所有以前邮件的内容要清晰明了的多.
但不出意料的是,
这样的功能被google取消了.

我尝试用油猴脚本实现类似的功能,
但是自己水平有限,
手搓到下面这些后,
我就进行不下去了.

请问有没有熟悉js的朋友可以帮我实现这个功能?
万分感谢.

我期望的回复样式是这样的:

  • 引用内容的时候同时会附加原邮件的时间, 发信人和邮箱地址
  • 下面使用引用格式, 后面自动附加我高亮的文字.
  • 可以在一个主题邮件的多个邮件中重复: 高亮 > r 的动作. 它们会自动的叠起来. (比如下图中就有两个叠加在了一起.) 这样便于我处理同个主题下多封邮件的回复.

我的手搓到8楼和9楼: 油猴脚本: 在gmail中回复邮件时, 可以自定义引用文本 - #8,来自 sav3uluan

回复框好像可以用 ‘div[role=“textbox”]’ 选择器获取,目测只是个可以编辑的 div,也没有数据绑定。。。

默认仍然会将原始内容追加到后边发送,有一个省略号按钮,点击后才显示完整内容 - -

补充: TypeError: Failed to set the ‘innerHTML’ property on ‘Element’: This document requires ‘TrustedHTML’ assignment.

好像不让用 innerHTML 赋值。。emmm

还有一个方案是写入剪切板自己粘贴覆盖一次。。

// ==UserScript==
// @name         Gmail Reply Enhancer
// @namespace    https://www.wdssmq.com/
// @version      1.0.0
// @author       沉冰浮水
// @description  Enhance Gmail reply functionality
// @license      MIT
// @null         ----------------------------
// @contributionURL    https://github.com/wdssmq#%E4%BA%8C%E7%BB%B4%E7%A0%81
// @contributionAmount 5.93
// @null         ----------------------------
// @link         https://github.com/wdssmq/userscript
// @link         https://afdian.net/@wdssmq
// @link         https://greasyfork.org/zh-CN/users/6865-wdssmq
// @null         ----------------------------
// @noframes
// @run-at       document-end
// @match        https://mail.google.com/*
// @grant        none
// ==/UserScript==

/* eslint-disable */
/* jshint esversion: 6 */

(function () {
  'use strict';

  const _sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

  // -------------------------------------

  // const $ = window.$ || unsafeWindow.$;
  function $n(e) {
    return document.querySelector(e);
  }
  function $na(e) {
    return document.querySelectorAll(e);
  }

  function genReplayCon() {
    // 获取选中的文字
    const selectedText = window.getSelection()?.toString()?.trim() || "";
    console.log('Selected text:', selectedText);

    // 获取发件人姓名
    let senderName = '';
    const senderNameElement = $n('span[email]');
    if (senderNameElement) {
      senderName = senderNameElement.getAttribute('name') || '';
    }
    console.log('Sender name:', senderName);

    // 获取发件人地址
    let senderAddress = '';
    const senderAddressElement = $n('span[email]');
    if (senderAddressElement) {
      senderAddress = senderAddressElement.getAttribute('email') || '';
    }
    console.log('Sender address:', senderAddress);

    // 获取邮件接收时间,这里选择器还是不对
    const receivedTime = '';
    const receivedTimeElement = $n('div[aria-expanded="false"] > span.g3');
    if (receivedTimeElement) {
      receivedTime = receivedTimeElement.getAttribute('title') || '';
    }
    console.log('Received time:', receivedTime);

    // 构建回复内容
    let replyContent = [
      senderName ? `${senderName} ` : '',
      senderAddress ? `<${senderAddress}> ` : '',
      receivedTime ? `${receivedTime}<br>` : '',
      selectedText ? `<br>${selectedText}<br><br>` : '<br><br>'
    ].join('');
    console.log('Reply content:', replyContent);
    return replyContent;
  }

  // 创建新回复
  async function createNewReply(con) {
    // 获取回复按钮
    const $$el = $na('[role="link"]');
    // 遍历找到内容为回复的按钮
    let replyButton = null;
    for (let i = 0; i < $$el.length; i++) {
      if ($$el[i].textContent.trim() === '回复') {
        replyButton = $$el[i];
        break;
      }
    }
    console.log(replyButton);
    if (replyButton) {
      replyButton.click(); // 触发点击事件
      // 这里有很高几率实际并没有触发点击,不知道为什么
      console.log('reply button toggled');
    }

    // 等待回复框加载完成
    await _sleep(2000);

    // 获取回复框
    const replyBox = $n('div[role="textbox"]');
    console.log(replyBox);
    if (!replyBox) {
      return;
    }

    // 显示隐藏内容
    const btnMore = $n("[aria-label='显示删减的内容']");
    console.log(btnMore);
    if (btnMore) {
      btnMore.click();
    }

    await _sleep(1000);

    // 写入回复内容
    // replyBox.innerHTML = con; // ← 好像不让用这个 -_-!
    replyBox.textContent = con;

  }

  document.addEventListener('keydown', async function(event) {
    // 检查是否按下 'r' 键(键码为 82)
    if (event.key === 'r' && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) {
      // 阻止默认事件,避免 Gmail 的默认行为
      event.preventDefault();

      alert('按下了 r 键');

      const replyContent = genReplayCon();
      await createNewReply(replyContent);
    }
  });

})();

感谢您的回复, 但是似乎有两个问题:

关键的一个问题是: 回复框并未打开. :frowning: 我的js水平只能照猫画虎, 尝试的改前改后, 也没有办法让回复框打开.

另外, Received time:没有获取到数据, 在log里面是空的.
还有 replyBox和btnMore, 在log中都是null

不知道有什么办法? 非常感谢你拨冗为我写这个小脚本.

我尝试将128行的 event.preventDefault(); 这句 注释后,
回复框是能打开, 等一秒内容也加上了, 但是 1) 加入的是个html码, 2) 而且gmail自动引用的部分还是保留的.

供你参考.

我注释里写了,有很高的几率触发不了回复按钮, 代码执行了,但是没产生预期效果,导致后边代码也无从执行。。

一般都是主要逻辑跑通后再考虑细节,问题是主要逻辑跑不通 (╯﹏╰)

无论如何感谢你的帮助. 我也再摸索摸索.

我尝试拼凑了一些代码, 但在下面这句出现了问题:

    document.execCommand('insertHTML', false, '<div class="gmail_quote">' +
      `${date} ${name == email ? '' : name + ' '}&lt;<a href="${email}" target="_blank">${email}</a>&gt;<br>` +
      `<blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex">${selText}</blockquote>` +
      '</div><br><br><br>');

错误的提示是:

Failed to execute 'execCommand' on 'Document': This document requires 'TrustedHTML' assignment.

我查了一下, 似乎是新的chrome加紧了安全, 不允许使用 execCommand 更改内容? via

我水品有限, 不知道如果使用别的方法, 该如何实现这句代码的功能呢?

希望大家给我个提点. 感谢.

以下是完整的代码, 出问题的在80行.

// ==UserScript==
// @name         gmail 'r' key quotes selection - 80 issue
// @description  Pressing 'r' will quote selection (only when selection is within an email)
// @version      0.1
// @author       anyone
// @namespace    anywhere
// @match        https://mail.google.com/*
// @license      MIT License
// ==/UserScript==

// 监听键盘按下事件
window.addEventListener('keydown', e => {
  console.log('Key pressed:', e.key); // 添加调试语句,输出按下的键
  // 如果按下的键同时按下了 Ctrl、Alt、Meta 键,或者按下的键不是 'r',则直接返回
  if (e.ctrlKey || e.altKey || e.metaKey || e.key != 'r')
    return;

  // 获取当前选中的文本
  const sel = getSelection();
  const selText = sel.toString().trim().replace(/[\r\n]/g, '<br>');
  // 如果没有选中文本,则直接返回
  if (!selText)
    return;

  // 获取包含当前选中文本的邮件项
  const item = sel.anchorNode.parentElement.closest('[role="listitem"]');
  // 如果找不到邮件项,则直接返回
  if (!item)
    return;

  // 获取发件人邮箱和姓名以及邮件发送日期
  const emailNode = item.querySelector('[email]');
  const email = emailNode && emailNode.getAttribute('email');
  const name = emailNode && emailNode.getAttribute('name');
  const date = (item.querySelector('span[title]') || {}).title || '';

  // 阻止默认行为、冒泡和立即停止事件传播
  e.preventDefault();
  e.stopPropagation();
  e.stopImmediatePropagation();

  // 输出调试信息:选中文本、发件人邮箱、发件人姓名和邮件发送日期
  console.log('Selected text:', selText); // 添加调试语句,输出选中的文本
  console.log('Email:', email); // 添加调试语句,输出邮箱地址
  console.log('Name:', name); // 添加调试语句,输出发件人姓名
  console.log('Date:', date); // 添加调试语句,输出邮件发送日期

  // 获取邮件编辑器,如果没有找到,则创建一个新的
  const getEditor = () => item.querySelector('.editable') || item.parentElement.querySelector('.editable');
  Promise.resolve(getEditor() || new Promise(resolve => {
    // 获取回复按钮
    const replyMenu = item.parentElement.querySelector('[role="button"] + [aria-haspopup="true"]');
    // 如果找不到回复按钮,则返回
    if (!replyMenu)
      return;
    // 模拟点击回复按钮
    replyMenu.previousElementSibling.dispatchEvent(new MouseEvent('click'));
    const t0 = performance.now();
    // 每100毫秒检查一次是否找到编辑器或超时1秒
    const interval = setInterval(() => {
      const editor = getEditor();
      if (editor || performance.now() - t0 > 1000) {
        // 如果找到编辑器,则模拟点击发送按钮,并清空编辑器内容
        if (editor) {
          editor.closest('table').parentNode.closest('table').querySelector('[role="button"]').click();
          editor.textContent = '';
        }
        clearInterval(interval);
        resolve(editor);
      }
    }, 100);
  })).then(editor => {
    // 如果没有编辑器,则返回
    if (!editor)
      return;
    // 让编辑器获得焦点
    editor.focus();
    // 在编辑器中插入引用邮件内容和选中文本
    // 下面的语句出现了 Failed to execute 'execCommand' on 'Document': This document requires 'TrustedHTML' assignment.
    document.execCommand('insertHTML', false, '<div class="gmail_quote">' +
      `${date} ${name == email ? '' : name + ' '}&lt;<a href="${email}" target="_blank">${email}</a>&gt;<br>` +
      `<blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex">${selText}</blockquote>` +
      '</div><br><br><br>');


    let isLastEmpty = true, lastNode = getSelection().focusNode.parentElement;
    // 循环检查是否为最后一个空节点
    while ((lastNode = lastNode.nextElementSibling) && isLastEmpty)
      isLastEmpty = !lastNode.textContent.trim();
    // 向后调整选区位置,以便在引用邮件后面继续输入文本
    getSelection().modify('move', 'backward', 'character');
    if (!isLastEmpty)
      getSelection().modify('move', 'backward', 'character');
  });
}, true);

根据错误,问题出在’document.execCommand’的调用上。在Chrome 68+版本,现有的执行插入操作将被禁止由非信任源(TrustedHTML)插入,此举是为了防止跨站脚本注入(XSS)攻击。

你可以尝试把document.execCommand(‘insertHTML’, false, ‘HTML内容’) 这段代码替换掉,改用创建元素和文本节点的方式来插入HTML。这样也可以追踪元素的创建,可以尽量避免XSS攻击。

// 在编辑器中插入引用邮件内容和选中文本

const blockQuoteEmail = document.createElement('div');

blockQuoteEmail.classList.add('gmail_quote');

const emailAnchor = document.createElement('a');

emailAnchor.href = email;

emailAnchor.target = '_blank';

emailAnchor.innerText = email;

blockQuoteEmail.innerHTML = `${date} ${name == email ? '' : name + ' '}&lt;`;

blockQuoteEmail.appendChild(emailAnchor);

blockQuoteEmail.innerHTML += '&gt;<br>';

const blockquote = document.createElement('blockquote');

blockquote.classList.add('gmail_quote');

blockquote.style = "margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex";

blockquote.innerHTML = selText;

blockQuoteEmail.appendChild(blockquote);

editor.appendChild(blockQuoteEmail);

editor.innerHTML += '<br><br><br>';

这样,插入HTML的问题应该可以解决。再看一下代码中其他可能导致问题的地方,如果还有问题请再向我询问。

感谢你的回复. 我尝试将你给我的这段代码替换了之前的 document.execCommand, 在gmail中运行出现错误, 反馈如下:

错误提示:

Uncaught (in promise) TypeError: Failed to set the 'innerHTML' property on 'Element': This document requires 'TrustedHTML' assignment.

提示错误的行:

blockQuoteEmail.innerHTML = `${date} ${name == email ? '' : name + ' '}&lt;`;

不知该怎么解决?

我的猜想: 是不是所有的.innerhtml的属性都有安全问题?
否则, 只需要建立好第一个blockQuoteEmail元素后, 直接在其上用.innerHTML属性加入html代码即可? 不知道我理解的对不对?

问了gpt, 他让我将

blockQuoteEmail.innerHTML = `${date} ${name == email ? '' : name + ' '}&lt;`;

改为

blockQuoteEmail.textContent = `${date} ${name == email ? '' : name + ' '}&lt;`;

我理解可能是将innerHTML, 变为textContent.
@Qingwa 的代码中相应的属性全部修改后, 确实可以输出了. 但是完全没有格式:

好像就差一点点了, 求助. :slight_smile:

摸索了一下, 将

blockQuoteEmail.innerHTML = `${date} ${name == email ? '' : name + ' '}&lt;`;

改为:

    const escapeHTMLPolicy = trustedTypes.createPolicy("myEscapePolicy", {
      createHTML: (string) => string,
    });

    const escaped = escapeHTMLPolicy.createHTML('<div class="gmail_quote">' +
        `${date} ${name == email ? '' : name + ' '}&lt;<a href="${email}" target="_blank">${email}</a>&gt;<br>` +
        `<blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex">${selText}</blockquote>` +
        '</div><br><br><br>');

就可以了.

在紧接着加入一个

editor.insertAdjacentHTML('beforeend', escaped); 

让多次引用可以堆叠起来. 而不是替换.

这样就基本满足我的需要啦.

感谢大家的帮助.

楼主这个脚本只是自己用还是已经放到某个userscript的repo了??

我两三年前也是自己参照别人的脚本写了一个,放到我自己的站点上:
https://bbs.ausmis.com/userscripts/QuoteSelectedTextInGmail.user.js

两周前不工作了,我也一直没来得及折腾,今天早上有点时间看了一下,搜了一下,找到你这个贴子和另一个:
https://greasyfork.org/en/discussions/development/220765-this-document-requires-trustedhtml-assignment
现在还没调通,如果楼主的这个已经放出来,我就不折腾了。。

算了,楼主的脚本还是差很多东西,我还是自己修改我目前的脚本吧。。

现在console里的错误提示提示的问题其实是我引用了Jquery的问题,已经去掉了JQuery的引用。

抱歉, 回复晚了. 我完全的小白,
只会自用, 不知道如何搞repo. 哈哈

你要有了repo, 可否分享下. 感谢.