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

**URL:** <https://meta.appinn.net/t/topic/53897>\
**Category:** 问题求助\
**Tags:** 油猴脚本, chrome, javascript\
**Created:** [2024 年3 月 15 日 08:46 UTC](https://meta.appinn.net/t/topic/53897 "2024-03-15T08:46:45Z")\
**Posts on this page:** 16\
**Page:** 1

<div class="post-metadata">

作者： ![sav3uluan](https://meta-edge.appinn.com/letter_avatar_proxy/v4/letter/s/898d66/32.png) [@sav3uluan](https://meta.appinn.net/u/sav3uluan)\
发布日期： [2024 年3 月 15 日 08:46 UTC](https://meta.appinn.net/t/topic/53897/1 "2024-03-15T08:46:45Z")

</div>

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

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

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

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

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

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

 ![1710492105](https://meta.appinn.net/uploads/default/original/3X/8/7/8778a998f80b05cf10fb0770a31fd38fd0b2b779.jpeg)

我的手搓到8楼和9楼: [油猴脚本: 在gmail中回复邮件时, 可以自定义引用文本 - #8，来自 sav3uluan](https://meta.appinn.net/t/topic/53897/8)

---

<div class="post-metadata">

作者： ![wdssmq](https://meta-edge.appinn.com/user_avatar/meta.appinn.net/wdssmq/32/9783_2.png) [@wdssmq](https://meta.appinn.net/u/wdssmq)\
发布日期： [2024 年3 月 16 日 08:56 UTC](https://meta.appinn.net/t/topic/53897/2 "2024-03-16T08:56:44Z")

</div>

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

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

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

好像不让用 innerHTML 赋值。。emmm

---

<div class="post-metadata">

作者： ![wdssmq](https://meta-edge.appinn.com/user_avatar/meta.appinn.net/wdssmq/32/9783_2.png) [@wdssmq](https://meta.appinn.net/u/wdssmq)\
发布日期： [2024 年3 月 16 日 09:49 UTC](https://meta.appinn.net/t/topic/53897/3 "2024-03-16T09:49:50Z")

</div>

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

```auto
// ==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.com/@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);
    }
  });

})();

```

---

<div class="post-metadata">

作者： ![sav3uluan](https://meta-edge.appinn.com/letter_avatar_proxy/v4/letter/s/898d66/32.png) [@sav3uluan](https://meta.appinn.net/u/sav3uluan)\
发布日期： [2024 年3 月 18 日 07:07 UTC](https://meta.appinn.net/t/topic/53897/4 "2024-03-18T07:07:58Z")

</div>

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

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

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

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

---

<div class="post-metadata">

作者： ![sav3uluan](https://meta-edge.appinn.com/letter_avatar_proxy/v4/letter/s/898d66/32.png) [@sav3uluan](https://meta.appinn.net/u/sav3uluan)\
发布日期： [2024 年3 月 18 日 07:12 UTC](https://meta.appinn.net/t/topic/53897/5 "2024-03-18T07:12:05Z")

</div>

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

供你参考.

---

<div class="post-metadata">

作者： ![wdssmq](https://meta-edge.appinn.com/user_avatar/meta.appinn.net/wdssmq/32/9783_2.png) [@wdssmq](https://meta.appinn.net/u/wdssmq)\
发布日期： [2024 年3 月 18 日 08:00 UTC](https://meta.appinn.net/t/topic/53897/6 "2024-03-18T08:00:38Z")

</div>

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

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

---

<div class="post-metadata">

作者： ![sav3uluan](https://meta-edge.appinn.com/letter_avatar_proxy/v4/letter/s/898d66/32.png) [@sav3uluan](https://meta.appinn.net/u/sav3uluan)\
发布日期： [2024 年3 月 19 日 21:05 UTC](https://meta.appinn.net/t/topic/53897/7 "2024-03-19T21:05:23Z")

</div>

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

---

<div class="post-metadata">

作者： ![sav3uluan](https://meta-edge.appinn.com/letter_avatar_proxy/v4/letter/s/898d66/32.png) [@sav3uluan](https://meta.appinn.net/u/sav3uluan)\
发布日期： [2024 年3 月 20 日 06:11 UTC](https://meta.appinn.net/t/topic/53897/8 "2024-03-20T06:11:12Z")

</div>

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

```js
    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](https://juejin.cn/post/6950470933924544519#:~:text=%E4%B8%8D%E4%BD%BF%E7%94%A8document.execCommand()API%E5%8E%BB%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E5%AF%8C%E6%96%87%E6%9C%AC)

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

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

---

<div class="post-metadata">

作者： ![sav3uluan](https://meta-edge.appinn.com/letter_avatar_proxy/v4/letter/s/898d66/32.png) [@sav3uluan](https://meta.appinn.net/u/sav3uluan)\
发布日期： [2024 年3 月 20 日 06:53 UTC](https://meta.appinn.net/t/topic/53897/9 "2024-03-20T06:53:52Z")

</div>

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

```js
// ==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);

```

---

<div class="post-metadata">

作者： ![Qingwa](https://meta-edge.appinn.com/user_avatar/meta.appinn.net/qingwa/32/58632_2.png) [@Qingwa](https://meta.appinn.net/u/Qingwa)\
发布日期： [2024 年3 月 20 日 08:29 UTC](https://meta.appinn.net/t/topic/53897/10 "2024-03-20T08:29:14Z")

</div>

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

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

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

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的问题应该可以解决。再看一下代码中其他可能导致问题的地方，如果还有问题请再向我询问。

---

<div class="post-metadata">

作者： ![sav3uluan](https://meta-edge.appinn.com/letter_avatar_proxy/v4/letter/s/898d66/32.png) [@sav3uluan](https://meta.appinn.net/u/sav3uluan)\
发布日期： [2024 年3 月 21 日 08:08 UTC](https://meta.appinn.net/t/topic/53897/11 "2024-03-21T08:08:01Z")

</div>

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

错误提示:

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

提示错误的行:

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

```

不知该怎么解决?

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

---

<div class="post-metadata">

作者： ![sav3uluan](https://meta-edge.appinn.com/letter_avatar_proxy/v4/letter/s/898d66/32.png) [@sav3uluan](https://meta.appinn.net/u/sav3uluan)\
发布日期： [2024 年3 月 21 日 13:28 UTC](https://meta.appinn.net/t/topic/53897/12 "2024-03-21T13:28:38Z")

</div>

问了gpt, 他让我将

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

```

改为

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

```

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

 ![20240321_132805](https://meta.appinn.net/uploads/default/original/3X/7/a/7a4dd3ffe5d8959023b55e28fa0b239de0f4acdf.jpeg)

好像就差一点点了, 求助. 🙂

---

<div class="post-metadata">

作者： ![sav3uluan](https://meta-edge.appinn.com/letter_avatar_proxy/v4/letter/s/898d66/32.png) [@sav3uluan](https://meta.appinn.net/u/sav3uluan)\
发布日期： [2024 年3 月 21 日 14:35 UTC](https://meta.appinn.net/t/topic/53897/13 "2024-03-21T14:35:17Z")

</div>

摸索了一下, 将

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

```

改为:

```auto
    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>');

```

就可以了.

在紧接着加入一个

```auto
editor.insertAdjacentHTML('beforeend', escaped); 

```

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

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

感谢大家的帮助.

---

<div class="post-metadata">

作者： ![dust2k](https://meta-edge.appinn.com/user_avatar/meta.appinn.net/dust2k/32/55716_2.png) [@dust2k](https://meta.appinn.net/u/dust2k)\
发布日期： [2024 年4 月 10 日 07:26 UTC](https://meta.appinn.net/t/topic/53897/14 "2024-04-10T07:26:31Z")

</div>

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

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

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

---

<div class="post-metadata">

作者： ![dust2k](https://meta-edge.appinn.com/user_avatar/meta.appinn.net/dust2k/32/55716_2.png) [@dust2k](https://meta.appinn.net/u/dust2k)\
发布日期： [2024 年4 月 11 日 03:07 UTC](https://meta.appinn.net/t/topic/53897/15 "2024-04-11T03:07:11Z")

</div>

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

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

---

<div class="post-metadata">

作者： ![sav3uluan](https://meta-edge.appinn.com/letter_avatar_proxy/v4/letter/s/898d66/32.png) [@sav3uluan](https://meta.appinn.net/u/sav3uluan)\
发布日期： [2024 年4 月 13 日 05:37 UTC](https://meta.appinn.net/t/topic/53897/16 "2024-04-13T05:37:08Z")

</div>

> [@dust2k](#):
>
> 楼主这个脚本只是自己用还是已经放到某个userscript的repo了？？

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

> [@dust2k](#):
>
> 楼主的脚本还是差很多东西，我还是自己修改我目前的脚本吧。。

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