0.Chrome扩展开发(Gmail附件管理助手)系列之〇——概述
1.Chrome扩展开发之一——Chrome扩展的文件结构
2.Chrome扩展开发之二——Chrome扩展中脚本的运行机制和通信方式
3.Chrome扩展开发之三——Chrome扩展中的数据本地存储和下载
4.Chrome扩展开发之四——核心功能的实现思路
5.Chrome扩展开发之五——采用指数退避算法实现ajax请求的重发,全部完成时触发回调函数
如果你对GmailAssist感兴趣,可以在chrome商店中搜索“Gmail助手”,或点击这里直接访问商店来安装试用;
如果你对GmailAssist的源码感兴趣,可以在我的GitHub上查看它的源码。
原计划专门用一篇博文简单介绍一下 gmail api 的,但考虑了一下觉得官方的文档已经很清楚了,也没什么不好理解的地方,我就不再专门写一篇文章来说它了。介绍 GmailAssist 的功能实现的过程中,如果有关于 gmail api 的需要再说明的地方,我会补充。
用户点击地址栏图标后,调用前一篇提到的用于chrome扩展进行 OAuth2 授权的库中的函数 authorize 来完成授权(详情可以查看其源码,很直观)。并向当前页面的 content script 发消息,使其通过修改页面的DOM元素,来显示 GmailAssist 的 UI。
需要在后台脚本中监听点击图标的事件,因而代码如下:
}); }); }); })
这个很有说头。
先看几个相关的 gmail API:
首先几乎 gmail API 中所有的方法都需要提供授权信息,即通过OAuth2获得的token。几乎所有的方法也都要求一个参数指定所要操作的邮箱(userId),而这个参数可以通过赋值”me”来表明当前用户完成授权的邮箱。gmail API 的调用,都可以通过 HTTP 请求来完成。
用户点击“获取附件列表”按钮,就调用 gmail api 中的 Users.messages: list 方法来获取符合条件的(该方法可以带参数,获取特定的邮件集合)全部邮件。但该方法返回的结果中每个 message 默认只有 messageId 和 threadId 两个字段(后来了解到,应该可以通过指定参数,使其返回更多的字段),如这样:
}, {
"id": "152a348995acc143"
, "threadId": "152a348995acc143"
}, ... {
"id": "14956ab84abc30c8"
, "threadId": "14956ab84abc30c8"
} ],
"nextPageToken": "16041633375627414246"
, "resultSizeEstimate": 103
}
在GmailAssist 中,我们要的只是带有附件的邮件,因而可以在list方法的参数中指定 q=has:attachment 来获取所有含附件的邮件信息。
这个参数是咋回事呢?可以在请求的URL中指定q字段。例如:
能不能指定别的搜索条件?让q等于别的内容?可以。这个参数的值是支持gmail内置的搜索过滤条件语法的,详情可以参考这里。
从上面的 HTTP 请求结果示例中不难看出,返回信息中有个字段:nextPageToken。顾名思义,这是获取list结果的下一页的令牌。list方法返回的结果在比较多时会分页,每页默认上限100项邮件信息,因而需要返回一页后用这个token去拿下一页。最后一页没有这个字段,因此可以通过
if (list.nextPageToken) { fetchNextList(list.nextPageToken); }
来递归地获取到完整的邮件列表。
通过 list 方法拿到了带附件的所有邮件的 id,接下来为了拿到里面的附件,就得用 get 方法,挨个获取具体的邮件,并从中提取出附件的信息。get 方法的具体信息,可以看 google 的文档,不多提了。
其中,附件的下载,没有直接的 api 可用,也不需要。根据自己多次手动在邮箱里操作的试验,找到了附件下载地址的规律,在程序里把它拼出来,在用户要下载相应附件时,把下载地址从content script 发到后台即可。对具体的下载地址长什么样子感兴趣的话,可以看源码。
(补充一点:如果你熟悉 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():
//上面这句是先完成把'-'和'_'替换成'+'和'/'(即把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,附件和邮件正文分开。
很关键!
点链接进去看官方文档,具体细节都说得很清楚。我这里大致说几点:
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来提高程序的性能。顾名思义,即把多个请求合并为一个请求。但这种方式下,这个合成的请求里的每个单独的请求所消耗的配额,仍然是单独计算的。