You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何不使用Node.js通过Gmail API获取邮件列表并解决同站Cookie报错

纯前端获取Gmail邮件列表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">&times;</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相关请求被拦截,报错参考:
Issues

修复方案

  • 从根源规避iframe跨域上下文问题
    现代浏览器的SameSite Cookie策略是强制安全规则,不存在长期可用的绕过方式,直接放弃iframe渲染逻辑,改用普通div容器承载解析后的邮件内容,完全避开跨域上下文的Cookie校验,需要做样式隔离时可配合影子DOM实现,隔离效果比iframe更稳定。
    对应代码修改:
    1. 模态框结构里移除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">&times;</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
    1. 缺失授权范围声明,脚本开头补充:var scopes = 'https://www.googleapis.com/auth/gmail.readonly';
    2. 旧版gapi.auth.authorize授权接口已被Google废弃,本身存在跨域Cookie兼容问题,替换为官方推荐的新版GIS授权库即可,无需调整后端配置。
    3. 邮件正文解析函数存在边界错误和逻辑漏洞:循环条件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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 07:33:24