如何不使用Node.js通过Gmail API获取邮件列表并解决同站Cookie报错
问题描述
开发无Node.js依赖的纯前端项目,目标是获取*收件箱(Inbox)或全部邮件(All Mails)*列表,检索公开资料未找到可直接复用的实现,自行调试代码后功能未达预期,初始实现Demo代码如下:
<!doctype html> <html> <head> <title>Gmail API demo</title> <meta charset="UTF-8"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap-theme.min.css"> </head> <body> <div class="container"> <h1>Gmail API demo</h1> <button id="authorize-button" class="btn btn-primary">Authorize</button> <table class="table table-striped table-inbox"> <thead> <tr> <th>From</th> <th>Subject</th> <th>Date/Time</th> </tr> </thead> <tbody></tbody> </table> </div> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script> <script type="text/javascript"> var clientId = 'XXXXXXXXXXXX.apps.googleusercontent.com'; var apiKey = 'XXXXXXXXXXXXXXX'; function handleClientLoad() { gapi.client.setApiKey(apiKey); window.setTimeout(checkAuth, 1); } function checkAuth() { gapi.auth.authorize({ client_id: clientId, scope: scopes, immediate: true }, handleAuthResult); } function handleAuthClick() { gapi.auth.authorize({ client_id: clientId, scope: scopes, immediate: false }, handleAuthResult); return false; } function handleAuthResult(authResult) { if(authResult && !authResult.error) { loadGmailApi(); $('#authorize-button').remove(); $('.table-inbox').removeClass("hidden"); } else { $('#authorize-button').removeClass("hidden"); $('#authorize-button').on('click', function(){ handleAuthClick(); }); } } function loadGmailApi() { gapi.client.load('gmail', 'v1', displayInbox); } function displayInbox() { var request = gapi.client.gmail.users.messages.list({ 'userId': 'me', 'labelIds': 'INBOX', 'maxResults': 10 }); request.execute(function(response) { $.each(response.messages, function() { var messageRequest = gapi.client.gmail.users.messages.get({ 'userId': 'me', 'id': this.id }); messageRequest.execute(appendMessageRow); }); }); } function appendMessageRow(message) { $('.table-inbox tbody').append( '<tr>\ <td>'+getHeader(message.payload.headers, 'From')+'</td>\ <td>\ <a href="#message-modal-' + message.id + '" data-toggle="modal" id="message-link-' + message.id+'">' + getHeader(message.payload.headers, 'Subject') + '</a>\ </td>\ <td>'+getHeader(message.payload.headers, 'Date')+'</td>\ </tr>' ); $('body').append( '<div class="modal fade" id="message-modal-' + message.id + '" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">\ <div class="modal-dialog modal-lg">\ <div class="modal-content">\ <div class="modal-header">\ <button type="button"\ class="close"\ data-dismiss="modal"\ aria-label="Close">\ <span aria-hidden="true">×</span></button>\ <h4 class="modal-title" id="myModalLabel">' + getHeader(message.payload.headers, 'Subject') + '</h4>\ </div>\ <div class="modal-body">\ <iframe id="message-iframe-'+message.id+'" srcdoc="<p>Loading...</p>">\ </iframe>\ </div>\ </div>\ </div>\ </div>' ); $('#message-link-'+message.id).on('click', function(){ var ifrm = $('#message-iframe-'+message.id)[0].contentWindow.document; $('body', ifrm).html(getBody(message.payload)); }); } function getHeader(headers, index) { var header = ''; $.each(headers, function(){ if(this.name === index){ header = this.value; } }); return header; } function getBody(message) { var encodedBody = ''; if(typeof message.parts === 'undefined') { encodedBody = message.body.data; } else { encodedBody = getHTMLPart(message.parts); } encodedBody = encodedBody.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, ''); return decodeURIComponent(escape(window.atob(encodedBody))); } function getHTMLPart(arr) { for(var x = 0; x <= arr.length; x++) { if(typeof arr[x].parts === 'undefined') { if(arr[x].mimeType === 'text/html') { return arr[x].body.data; } } else { return getHTMLPart(arr[x].parts); } } return ''; } </script> <script src="https://apis.google.com/js/client.js?onload=handleClientLoad"></script> </body> </html>
核心故障:iframe渲染邮件内容时触发Same Site Cookie及相关Cookie安全策略限制,Gmail相关请求被拦截,报错参考:
修复方案
- 从根源规避iframe跨域上下文问题
现代浏览器的SameSite Cookie策略是强制安全规则,不存在长期可用的绕过方式,直接放弃iframe渲染逻辑,改用普通div容器承载解析后的邮件内容,完全避开跨域上下文的Cookie校验,需要做样式隔离时可配合影子DOM实现,隔离效果比iframe更稳定。
对应代码修改:- 模态框结构里移除iframe,替换为普通内容容器
// 替换原模态框拼接逻辑 $('body').append( '<div class="modal fade" id="message-modal-' + message.id + '" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">\ <div class="modal-dialog modal-lg">\ <div class="modal-content">\ <div class="modal-header">\ <button type="button" class="close" data-dismiss="modal" aria-label="Close">\ <span aria-hidden="true">×</span></button>\ <h4 class="modal-title" id="myModalLabel">' + getHeader(message.payload.headers, 'Subject') + '</h4>\ </div>\ <div class="modal-body" id="message-content-'+message.id+'">\ <p>Loading...</p>\ </div>\ </div>\ </div>\ </div>' ); // 替换原iframe点击赋值逻辑 $('#message-link-'+message.id).on('click', function(){ $('#message-content-'+message.id).html(getBody(message.payload)); }); - 修复原有代码的显性bug
- 缺失授权范围声明,脚本开头补充:
var scopes = 'https://www.googleapis.com/auth/gmail.readonly'; - 旧版
gapi.auth.authorize授权接口已被Google废弃,本身存在跨域Cookie兼容问题,替换为官方推荐的新版GIS授权库即可,无需调整后端配置。 - 邮件正文解析函数存在边界错误和逻辑漏洞:循环条件
x <= arr.length会读取到数组undefined项触发报错;遇到嵌套multipart格式邮件时直接返回第一个子part,会漏掉html正文。修复后代码:
function getHTMLPart(arr) { for(var x = 0; x < arr.length; x++) { if(typeof arr[x].parts === 'undefined') { if(arr[x].mimeType === 'text/html') { return arr[x].body.data; } } else { let nestedRes = getHTMLPart(arr[x].parts); if(nestedRes) return nestedRes; } } // 无html内容时回退渲染纯文本 for(var x = 0; x < arr.length; x++) { if(arr[x].mimeType === 'text/plain') return arr[x].body.data.replace(/\n/g, '<br>'); } return ''; } - 缺失授权范围声明,脚本开头补充:
- 特殊场景必须使用iframe时的兼容处理
如果需要用iframe做严格的内容权限隔离,给iframe添加sandbox属性限制权限,禁止其携带第三方Cookie发起请求:
不要添加<iframe sandbox="allow-same-origin allow-popups" id="message-iframe-xxx"></iframe>allow-scripts等高风险权限,同时对解码后的邮件HTML做预处理,给所有外链资源添加referrerpolicy="no-referrer"属性,避免触发第三方Cookie校验。
内容的提问来源于stack exchange,提问作者Extraterrestrial
相关产品推荐
相关产品推荐

