目录:

如果你对GmailAssist感兴趣,可以在chrome商店中搜索“Gmail助手”,或点击这里直接访问商店来安装试用;
如果你对GmailAssist的源码感兴趣,可以在我的GitHub上查看它的源码。


原计划专门用一篇博文简单介绍一下 gmail api 的,但考虑了一下觉得官方的文档已经很清楚了,也没什么不好理解的地方,我就不再专门写一篇文章来说它了。介绍 GmailAssist 的功能实现的过程中,如果有关于 gmail api 的需要再说明的地方,我会补充。


一、授权及界面显示

用户点击地址栏图标后,调用前一篇提到的用于chrome扩展进行 OAuth2 授权的库中的函数 authorize 来完成授权(详情可以查看其源码,很直观)。并向当前页面的 content script 发消息,使其通过修改页面的DOM元素,来显示 GmailAssist 的 UI。

需要在后台脚本中监听点击图标的事件,因而代码如下:

chrome.pageAction.onClicked.addListener(function () {
google.authorize(function () {
chrome.tabs.query({active: true, currentWindow: true}, function (tabs) {
chrome.tabs.sendMessage(tabs[0].id, {token: google.getAccessToken()}, function (response) {
//可以写点处理response的逻辑。另外,上面google.getAccessToken() 是库中的函数。

            });
});
});
})

二、获取附件列表

这个很有说头。

先看几个相关的 gmail API:

首先几乎 gmail API 中所有的方法都需要提供授权信息,即通过OAuth2获得的token。几乎所有的方法也都要求一个参数指定所要操作的邮箱(userId),而这个参数可以通过赋值”me”来表明当前用户完成授权的邮箱。gmail API 的调用,都可以通过 HTTP 请求来完成。

1. Users.messages: list

用户点击“获取附件列表”按钮,就调用 gmail api 中的 Users.messages: list 方法来获取符合条件的(该方法可以带参数,获取特定的邮件集合)全部邮件。但该方法返回的结果中每个 message 默认只有 messageId 和 threadId 两个字段(后来了解到,应该可以通过指定参数,使其返回更多的字段),如这样:

{ "messages": [ { "id": "152a445862514939", "threadId": "152a445862514939"

  },
{

"id": "152a348995acc143", "threadId": "152a348995acc143"

  },
...
{

"id": "14956ab84abc30c8", "threadId": "14956ab84abc30c8"

  }
],

"nextPageToken": "16041633375627414246", "resultSizeEstimate": 103

}

在GmailAssist 中,我们要的只是带有附件的邮件,因而可以在list方法的参数中指定 q=has:attachment 来获取所有含附件的邮件信息。

这个参数是咋回事呢?可以在请求的URL中指定q字段。例如:

GET https://www.googleapis.com/gmail/v1/users/me/messages?q=has:attachment

能不能指定别的搜索条件?让q等于别的内容?可以。这个参数的值是支持gmail内置的搜索过滤条件语法的,详情可以参考这里

从上面的 HTTP 请求结果示例中不难看出,返回信息中有个字段:nextPageToken。顾名思义,这是获取list结果的下一页的令牌。list方法返回的结果在比较多时会分页,每页默认上限100项邮件信息,因而需要返回一页后用这个token去拿下一页。最后一页没有这个字段,因此可以通过

if (list.nextPageToken) {
fetchNextList(list.nextPageToken);
}

来递归地获取到完整的邮件列表。

2. Users.messages: get

通过 list 方法拿到了带附件的所有邮件的 id,接下来为了拿到里面的附件,就得用 get 方法,挨个获取具体的邮件,并从中提取出附件的信息。get 方法的具体信息,可以看 google 的文档,不多提了。

其中,附件的下载,没有直接的 api 可用,也不需要。根据自己多次手动在邮箱里操作的试验,找到了附件下载地址的规律,在程序里把它拼出来,在用户要下载相应附件时,把下载地址从content script 发到后台即可。对具体的下载地址长什么样子感兴趣的话,可以看源码

3. Users.drafts: listUsers.drafts: getUsers.drafts: update

(补充一点:如果你熟悉 gmail,或者已经玩了玩它的这些 API 的话,不难发现,gmail 中除草稿之外的信件们都是 message 类型的,而草稿是draft类型包装了一个message。)

要完成把选中的附件插到最新草稿中,没有直接的API可用,那就只能让程序麻烦一点了。我现在采用的思路如下:

首先获取最新草稿,即先调用drafts. list 方法返回草稿列表,由于结果是按照时间顺序排列的,所以我们再拿第一个草稿的 id 来调用 get 方法,获得这封草稿的全文。

把要插入的附件所在的邮件的全文用 messages.get 获取到,从中截取出选中的附件,将这些(如果有多个的话)附件插入一个队列中,然后再依次出队,拼到之前拿到的原草稿末尾。(这中间需要一点操作,主要是为了保证格式正确。细节可以参考RFC822协议,或者自己get几个 RAW 格式的message,按base64urlsafe格式来转码看看就清楚格式了)

拼好之后,用 drafts.update 方法直接把整个拼好的新草稿按base64urlsafe格式转码的结果,作为RAW字段传回服务器即可完成草稿的更新,也即完成了附件的插入。

这三个方法具体操作跟上面的 messages.list 和 messages.get 是类似的。需要注意的是,根据上述思路,通过 get 来获取草稿时,需要在参数中指明返回 RAW 类型的草稿内容。获得的RAW字段是base64编码后的,在本地要操作需要先解码,操作完之后再编码,再发回服务器。这个编码解码可以用js的函数atob()、btoa():

decodedMsg = atob(raw.replace(/-/g, '+').replace(/_/g, '/')); //raw是指服务器返回的raw格式的message全文(是一个字符串)

//上面这句是先完成把'-'和'_'替换成'+'和'/'(即把base64urlsafe变成base64),再进行base64解码
//

之所以有这样的替换,是因为'+'和'/'出现在URI中是有含义的

encodedMsg 

= btoa(newdraft).replace(/\//g, '_').replace(/\+/g, '-'); //newdraft是一个字符串,即待编码的内容

//上面这句和第一句差不多,先base64编码,然后完成替换,变成base64urlsafe

当然,这种思路虽然可以实现想要的功能,但并不好。我认为在这种思路的基础上还可以有一种优化:

在获取附件时,不通过 messages.get 方法获取邮件全文,而是获取每个附件的AttachmentId字段,用相应的字段结合 messageId 一起,通过 messages.attachments.get 方法获取附件内容。这样获取到的是MIME的一些注释和base64编码的附件内容。

进一步还可以考虑在本地 cache 一波,就是把用过的附件内容(包括相应MIME 字段)用 chrome.storage 接口来保存在本地。这里需要注意,如果这样,就需要在manifest中声明一个unlimited storage权限,否则会受到5MB的存储上限限制。

或者再换一种思路,直接上传附件。但这种思路要求先把附件文件下载,然后用 drafts.update 方法来完成附件上传。

总之几种思路都是一个核心想法:把附件先下载下来,然后再上传到草稿中,只不过咱的程序是把这套工作自动完成从而解放了用户。为啥非得这样整呢?下载再上传,多麻烦啊?没辙。因为邮件本身的格式就这么限制着了,附件都是作为MIME part 写在邮件文中的,而并不是像我最初想象的那样——在邮件中留一个指向附件的URI,附件和邮件正文分开。

三、gmail API 的使用限制

很关键!

点链接进去看官方文档,具体细节都说得很清楚。我这里大致说几点:

1. 限制分两种:总数限制速率限制

前者是针对 你的整个程序 而言的,即不同的gmail用户共享着你的程序拥有的配额。你的程序要使用gmail API是需要在google注册的(这个过程没啥复杂的,照着官方文档一点点弄就行),然后你的程序就相当于有一个自己的账号,这个所谓的“总数限制”就是针对你这个程序的“账号”而言的。具体是:免费用户(就是你了,开发者)一天可以发 10亿个单位 的请求。注意,是你的程序每天有这么多配额。具体到GmailAssist而言吧,不同的gmail用户使用它的时候,是都在消耗我的开发者账号的这个 10亿单位/天 的配额总数的。每天下来你的配额都会刷新。那么怎么查看自己的程序这一天消耗了多少配额呢?

进入Google Developer Console,选择你要查看的项目,点下一步。然后就可以在右边看到配额啊,用量啊之类的信息了。

后者是针对 具体的gmail用户 而言的,免费项目支持的最大请求速率限制为 250单位/秒/用户。但这个限制并不是绝对的,google允许短暂的burst,即你的程序可以暂时突破这一限制,但要是你持续突破它,你就会收到 HTTP 429 或者 403 或者 502 等错误,表明服务器受不了了,你太猛了。(这时候咋办?采用指数退避算法尝试重发请求。具体的我会在下一篇博文中说。)

2. 描述限制或描述使用情况的单位是啥?

我在上面都说的是多少多少单位。即官方文档中所谓的 quota unit,它是衡量和描述你的API使用量的最小单位,每次调用API中的不同方法,会消耗不同数量的 quota unit,从5到100不等,具体可以参考官方文档

3. 除 gmail API 本身之外的限制

上面说的都是gmail API本身的限制,但用户在用你的程序时,还受到 gmail 本身使用的速率限制等,不过你的程序开发中不需要考虑这一点,你也几乎没法针对这个限制做什么。

4. Batch request 不是一个突破限制的trick

gmail API 鼓励开发者使用batch request来提高程序的性能。顾名思义,即把多个请求合并为一个请求。但这种方式下,这个合成的请求里的每个单独的请求所消耗的配额,仍然是单独计算的。

Chrome扩展开发之四——核心功能的实现思路的更多相关文章

  1. Chrome扩展开发之二——Chrome扩展中脚本的运行机制和通信方式

    目录: 0.Chrome扩展开发(Gmail附件管理助手)系列之〇——概述 1.Chrome扩展开发之一——Chrome扩展的文件结构 2.Chrome扩展开发之二——Chrome扩展中脚本的运行机制 ...

  2. Chrome扩展开发(Gmail附件管理助手)系列之〇——概述

    目录: 0.Chrome扩展开发(Gmail附件管理助手)系列之〇——概述 1.Chrome扩展开发之一——Chrome扩展的文件结构 2.Chrome扩展开发之二——Chrome扩展中脚本的运行机制 ...

  3. Chrome扩展开发之一——Chrome扩展的文件结构

    目录: 0.Chrome扩展开发(Gmail附件管理助手)系列之〇——概述 1.Chrome扩展开发之一——Chrome扩展的文件结构 2.Chrome扩展开发之二——Chrome扩展中脚本的运行机制 ...

  4. Chrome扩展开发之三——Chrome扩展中的数据本地存储和下载

    目录: 0.Chrome扩展开发(Gmail附件管理助手)系列之〇——概述 1.Chrome扩展开发之一——Chrome扩展的文件结构 2.Chrome扩展开发之二——Chrome扩展中脚本的运行机制 ...

  5. 【转发】NPAPI学习(Firefox和Chrome扩展开发 )

    NPAPI学习(Firefox和Chrome扩展开发 ) 2011-11-08 14:41:02 by [6yang], 1172 visits, 收藏 | 返回 Firefox和Chrome扩展开发 ...

  6. Chrome扩展开发基础教程(附HelloWorld)

    1 概述 Chrome扩展开发的基础教程,代码基于原生JS+H5,教程内容基于谷歌扩展开发官方文档. 2 环境 Chrome 88.0.4324.96 Chromium 87.0.4280.141 B ...

  7. 基于 webpack 的 chrome 扩展开发探索

    起 最近利用闲暇时间在进行一款 chrome 扩展 V2EX-HELPER 的开发(如果巧遇 V 友欢迎试用),今天把它彻底改成了用 webpack 打包依赖的模式,不由得感概 webpack 的强大 ...

  8. 手把手教你Chrome扩展开发:本地存储篇

    手把手教你开发chrome扩展一:开发Chrome Extenstion其实很简单 手把手教你开发Chrome扩展二:为html添加行为 手把手教你开发Chrome扩展三:关于本地存储数据 HTML5 ...

  9. 【java框架】MyBatis-Plus(1)--MyBatis-Plus快速上手开发及核心功能体验

    1.MyBatis-Plus入门开发及配置 1.1.MyBatis-Plus简介 MyBatis-Plus(简称 MP)是一个 MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变, ...

随机推荐

  1. MySQL 中的 FOUND_ROWS() 与 ROW_COUNT() 函数

    移植sql server 的存储过程到mysql中,遇到了sql server中的: IF @@ROWCOUNT < 1 对应到mysql中可以使用 FOUND_ROWS() 函数来替换. 1. ...

  2. mysql插入数据与删除重复记录的几个例子(收藏)

    mysql插入数据与删除重复记录的几个例子 12-26shell脚本实现mysql数据的批量插入 12-26mysql循环语句插入数据的例子 12-26mysql批量插入数据(insert into ...

  3. 数据结构--AC自动机--hdu 2896

    病毒侵袭 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Submi ...

  4. MongoDB配置多个ConfigDB的问题(笔记)

    由于在部署集群之前没有做好的规划,在集群中只有一个configserver和一个mongos.网上都推荐多个configserver,本人在使用的过程中发现在启动mongos进程时,congfigdb ...

  5. docker containerd 中的create 容器操作

    containerd的create container的API如下所示: type CreateContainerRequest struct { Id string BundlePath strin ...

  6. Golang 实现简单的滚动读取文本更新

    这个小程序要实现的效果,简单地说,就是将目标文件的内容读取输出到终端,并且目标文件并不是静态的,而是随时会添加新的内容.我们的目标就是一旦目标文件添加了新的内容,就把它读取出来并且显示到终端上. 实现 ...

  7. 《TCP/IP详解 卷一》读书笔记-----TCP连接建立

    1.在每个TCP报文段中,头部的flag字段里的SYN,FIN,RST,PSH可以多个有效,并没有限定为必须只有一个 2.TCP连接建立过程: 1)客户端发送一个SYN报文段,其中包含了客户端要传送的 ...

  8. At least one object must implement IComparable

    中文:必须至少有一个对象实现 IComparable. 序列排序时报这个错误 lstReports.OrderBy(r => new { r.DepartmentName, r.ReportNo ...

  9. bzoj-3170 3170: [Tjoi 2013]松鼠聚会(计算几何)

    题目链接: 3170: [Tjoi 2013]松鼠聚会 Time Limit: 10 Sec  Memory Limit: 128 MB Description 有N个小松鼠,它们的家用一个点x,y表 ...

  10. poj2387 Til the Cows Come Home 最短路径dijkstra算法

    Description Bessie is out in the field and wants to get back to the barn to get as much sleep as pos ...