
本文详解如何在浏览器端通过 Gmail API 发送带文件附件的邮件,重点解决因 FileReader 异步读取导致的 files[i] is undefined 错误,并提供可直接运行的 async/await 改写方案与完整 MIME 构建逻辑。
本文详解如何在浏览器端通过 gmail api 发送带文件附件的邮件,重点解决因 filereader 异步读取导致的 `files[i] is undefined` 错误,并提供可直接运行的 `async/await` 改写方案与完整 mime 构建逻辑。
在 Web 前端调用 Gmail API 发送带附件的邮件时,一个常见且隐蔽的陷阱是:FileReader 的异步特性与循环变量 i 的闭包作用域冲突。原始代码中,reader.onload 回调函数在所有 FileReader 实例启动后才陆续执行,此时 for 循环早已结束,i 的值已等于 files.length,因此 files[i] 必然为 undefined——这正是报错的根本原因,而非文件本身为空。
要彻底解决该问题,必须确保每个文件的读取操作顺序完成、上下文隔离。推荐采用 async/await + Promise 封装 FileReader 的方式,使异步流程线性可控。以下是经过验证的生产级实现:
将一个或多个 UI 原型图、线框图、产品截图或设计稿转换为标准化、可直接交付的 PRD 风格文档,供产品、设计、前端和测试共同使用。适用于 Codex 需要分析 PC 网页、桌面端、移动端、响应式页面、独立弹窗、表单、表格、看板、数据大屏或多页面流程截图,还原页面结构与交互,强制套用固定文档模板,抽取跨页面共享规则,或把一组原型沉淀为单页规范文档加整体业务流程文档的场景。不适用于纯视觉审美点评、代码实现、接口设计、埋点方案、竞品分析或无 UI 输入的业务需求创作。
async function sendEmailWithAttachments(to, subject, body, files) {
const boundary = '----GmailWebClientBoundary_' + Date.now();
let multipart = '';
// Step 1: 同步读取所有附件(按序等待每个 FileReader 完成)
for (let i = 0; i < files.length; i++) {
const file = files[i];
console.log(`正在处理附件: ${file.name} (${file.type}, ${file.size} bytes)`);
const reader = new FileReader();
await new Promise((resolve) => {
reader.onload = () => resolve();
reader.onerror = () => {
console.error(`读取附件失败: ${file.name}`, reader.error);
resolve(); // 防止阻塞后续流程,可根据需求 throw
};
reader.readAsDataURL(file);
});
if (!reader.result) {
console.warn(`跳过无效附件: ${file.name}`);
continue;
}
const base64Data = reader.result.split(',')[1]; // 去除 data URL 前缀
multipart += `--${boundary}\r\n` +
`Content-Type: ${file.type || 'application/octet-stream'}\r\n` +
`MIME-Version: 1.0\r\n` +
`Content-Transfer-Encoding: base64\r\n` +
`Content-Disposition: attachment; filename="${encodeURIComponent(file.name)}"\r\n\r\n` +
`${base64Data}\r\n`;
}
// Step 2: 构建完整 RFC 2822 格式的原始邮件(含纯文本正文 + 附件)
const emailContent =
`Content-Type: multipart/mixed; boundary="${boundary}"\r\n` +
`MIME-Version: 1.0\r\n` +
`To: ${to}\r\n` +
`Subject: ${subject}\r\n\r\n` +
`--${boundary}\r\n` +
`Content-Type: text/plain; charset="UTF-8"\r\n` +
`MIME-Version: 1.0\r\n` +
`Content-Transfer-Encoding: 7bit\r\n\r\n` +
`${body}\r\n\r\n` +
`${multipart}` +
`--${boundary}--`;
// Step 3: Base64 编码并发送(注意:window.btoa 不支持 Unicode,需确保 content 为 ASCII)
// ⚠️ 关键:先 UTF-8 编码再 btoa(否则中文会乱码)
const utf8Bytes = new TextEncoder().encode(emailContent);
const rawBase64 = btoa(String.fromCharCode(...utf8Bytes));
try {
const request = gapi.client.gmail.users.messages.send({
userId: 'me',
resource: { raw: rawBase64 }
});
const response = await request.execute();
console.log('✅ 邮件发送成功:', response.id);
return response;
} catch (error) {
console.error('❌ 邮件发送失败:', error);
throw error;
}
}✅ 关键注意事项:
-
编码安全:
window.btoa()仅接受 Latin-1 字符串。若邮件内容含中文、Emoji 等 Unicode 字符,必须先用TextEncoder转为 UTF-8 字节数组,再转为字符串后编码(如上所示),否则将触发InvalidCharacterError。 -
附件类型识别:使用
file.type获取 MIME 类型,但部分浏览器可能返回空字符串,建议 fallback 到application/octet-stream或根据扩展名映射(如.pdf→application/pdf)。 -
大小限制:Gmail API 单封邮件总大小上限为 35 MB(含附件),超限将返回
400 Bad Request;前端应提前校验files.reduce((sum, f) => sum + f.size, 0)。 -
授权范围:确保 OAuth 2.0 token 已授权
https://www.googleapis.com/auth/gmail.send范围,仅gmail.readonly或gmail.modify无法发送。 -
调试技巧:将
emailContent字符串打印到控制台并粘贴至 RFC 2822 校验工具 可快速验证格式合法性。
通过以上结构化实现,你不仅能规避经典的闭包陷阱,还能构建出符合 Gmail API 规范、健壮且可维护的邮件发送逻辑。


















