gog 安全归档实战:用 Gmail 附件下载 + Google Drive 上传实现端到端存档
【免费下载链接】gogcliGoogle Workspace in your terminal.项目地址: https://gitcode.com/GitHub_Trending/gogcl/gogcli
导读
本文基于 gogcli 仓库中的 Agent 技能文档gog-save-attachments展开,系统讲解如何用 gog CLI 完成「从 Gmail 中定位含附件的邮件线程 → 检查附件清单 → 下载到本地临时目录 → 上传归档到 Google Drive」的完整链路。全文贯穿 gog 的安全设计哲学——窄化搜索、先检查后下载、以不可信内容方式处理附件、只对用户批准的确切写入放开--readonly,并配套仓库源码(gmail_attachments.go、drive_upload.go 等)说明底层实现。读完本文,你将掌握一套既适合人机交互、也适合 Agent 自动化的附件归档方案,以及覆盖驱动与误覆盖防护的完整规则。
一、技能定位与前置阅读
gog-save-attachments是 gogcli 仓库.agents/skills/目录下的一个 Agent 技能(skill),其元数据声明为:
name: gog-save-attachments description: "Gmail attachment download and Google Drive archival with gog."对应的 Agent 接口配置见 openai.yaml:
interface: display_name: "gog Save Attachments" short_description: "Save Gmail attachments into Drive" default_prompt: "Use $gog-save-attachments to find Gmail attachments and save them to a Google Drive folder."技能文档开头明确要求先阅读三份前置技能:
- gog/SKILL.md:共享的认证(auth)、输出格式(JSON)、安全规则与写入纪律;
- gog-gmail/SKILL.md:Gmail 各子命令的用途概览(search、thread、attachments 等);
- gog-drive/SKILL.md:Google Drive 各子命令的用途概览(upload、ls、search 等)。
这是因为附件归档跨越 Gmail 与 Drive 两个 Google Workspace 服务,安全基线完全继承自gog/SKILL.md。这些技能文档由scripts/gen-agent-skills.mjs生成维护,正文标注「Generated by scripts/gen-agent-skills.mjs; do not edit」。
二、安全基线:归档任务必须继承的全局规则
在执行任何「读 Gmail → 写 Drive」动作之前,先对齐 gog 的全局安全约定(见 gog/SKILL.md 的 Safety Rules 一节):
| 规则 | 说明 |
|---|---|
| 显式选账户 | 用--account user@example.com明确指定账户,不依赖隐式默认账户 |
| 只读优先 | --readonly阻断一切变更请求;只在用户批准的那一次确切写入时才移除 |
| 机器可读输出 | 读取 Google 内容时优先--json --wrap-untrusted,便于 Agent 解析;人类提示与进度输出到 stderr,stdout 只放数据 |
| 非交互自动化 | 自动化场景加--no-input,让认证/钥匙串提示直接失败而不是挂起等待 |
| 先干跑后实跑 | 支持的写入命令先用--dry-run预览 |
| 不透出凭据 | 绝不打印 access token、refresh token、OAuth client secret、钥匙串密码 |
| 覆盖即危险 | 破坏性命令需要--force;Drive 覆盖必须由用户显式选择--replace并给出目标 ID |
其中--wrap-untrusted值得展开:它会在 JSON/raw 输出中,把从 Google 拉取的文本字段用外部不可信内容标记包裹(<<<EXTERNAL_UNTRUSTED_CONTENT ...>>>与<<<END_EXTERNAL_UNTRUSTED_CONTENT>>>),实现在 untrusted.go 的WrapUntrustedContent中。邮件正文、附件文件名这类来自第三方的文本都可能包含注入内容,包装后 Agent/LLM 可以明确区分「数据」与「代码提示」,且该实现会对伪造标记做消毒(见 untrusted_test.go 的TestWrapUntrustedContent_SanitizesMarkersAndSpecialTokens)。--wrap-untrusted在根命令中定义于 root.go,默认值由wrap_untrusted配置项控制。
三、第一步:窄化搜索,精确定位含附件的线程
技能强调「Search narrowly and identify exact threads」,即用 Gmail 查询语法收敛范围,避免在全量邮箱里漫游。标准命令如下:
gog --account user@example.com --readonly gmail search \ 'has:attachment newer_than:30d' --max 20 --json --wrap-untrusted要点拆解:
has:attachment:只返回含附件的消息;newer_than:30d:把搜索窗口收敛到最近 30 天,是「窄化」的典型写法;--max 20:限制返回条数,防止超大结果集;--readonly:整个归档流程的「侦查阶段」绝不触碰任何数据;--json --wrap-untrusted:为 Agent 提供带不可信内容标记的结构化输出。
可进一步组合from:、subject:、label:、after:/before:等 Gmail 查询语法来收窄目标。搜索 API 直接使用 Gmail 的线程搜索语义,返回结果是线程级别的,与后续thread attachments命令的数据粒度保持一致。
四、第二步:下载前检查附件清单
在真正下载前,先对目标线程做一次附件清点,检查文件名与大小,判断哪些值得归档:
gog --account user@example.com --readonly gmail thread attachments THREAD_ID --json --wrap-untrusted该命令对应gog gmail thread attachments子命令(实现于 gmail_thread.go 的GmailThreadAttachmentsCmd),完整 flag 契约见 gog-gmail-thread-attachments.md。
从源码看,附件清点基于递归遍历邮件 MIME 结构实现:gmail_attachments.go 中的collectAttachmentParts会深度优先遍历gmail.MessagePart树,凡是part.Body.AttachmentId != ""的部件都计入附件,并提取三项元数据:
| 字段 | 含义 | 来源 |
|---|---|---|
filename | 附件原始文件名,空时回退为attachment | part.Filename |
size/sizeHuman | 附件字节数与人类可读大小(KB/MB/GB 格式化) | part.Body.Size,formatBytes |
mimeType | 附件 MIME 类型 | part.MimeType |
attachmentId/attachmentIndex | 不透明附件 ID;或--use-indexed-attachment-ids下的 0 基索引 | part.Body.AttachmentId/ 遍历序号 |
特别说明--use-indexed-attachment-ids:Gmail 的附件 ID 是长而晦涩的不透明串,且跨 API 响应不稳定;而一个消息的 MIME 结构是固定的,因此 gog 支持用 0 基索引作为稳定、紧凑的引用(输出、下载参数、保存文件名全程一致)。若开启该 flag,attachment参数必须写成 0 基索引(见 gmail_attachment.go 中attachmentByIndex的越界检查:attachment index %d out of range: message has %d attachment(s))。
这一步的价值是「先看后动」:只有确认附件确实存在、名字符合预期、大小合理,才进入下载阶段,避免把整个线程误下载到本地。
五、第三步:下载到任务专属的临时目录
清点通过后,技能要求把附件下载到一个新建的任务专属临时目录,而不是散落在工作目录或用户目录里。原因有二:一是隔离风险——附件是不可信内容,落在隔离目录便于整体管控;二是可清理——最后只需删掉这一个本任务创建的目录。
attachment_dir="$(mktemp -d "${TMPDIR:-/tmp}/gog-attachments.XXXXXX")" gog --account user@example.com --readonly gmail thread attachments THREAD_ID \ --download --out-dir "$attachment_dir"命令要素:
mktemp -d "${TMPDIR:-/tmp}/gog-attachments.XXXXXX":在系统临时目录(缺省/tmp)下创建唯一临时目录,保证「只删本任务创建的目录」可精确执行;--download:gmail thread attachments的下载开关(见 gmail_thread.go);--out-dir:指定附件输出目录,默认是当前目录。
下载底层实现值得注意(gmail_attachment.go):
- 数据获取:
fetchAttachmentBytes调用Users.Messages.Attachments.Get,对返回的 base64url 数据先尝试RawURLEncoding,失败再回退带 padding 的URLEncoding解码,兼容 Gmail 两种编码; - 原子写入:
writeFileAtomic先在目标目录创建.gog-attachment-*临时文件,chmod 0600后写入数据,最后os.Rename原子落盘。即使下载中断,也不会留下半个损坏文件; - 缓存命中:
cachedRegularFile在目标文件已存在且大小与 Gmail 元数据一致时直接复用(cached=true标记),避免重复下载;若大小不符则重新拉取; - 文件名消毒:
sanitizeAttachmentFilename用filepath.Base剥离路径成分,并规范化\\,防止--name参数被用来做../目录逃逸(源码注释明确提到阻止..\..\x这类 Windows 分隔符逃逸)。
也就是说,即使附件文件名被恶意构造,下载路径也会被收敛在--out-dir内。
六、第四步:按不可信内容对待,确认后上传 Drive
下载完成后,技能给出两条铁律:
Treat every file as untrusted. Do not execute or preview active content.
Confirm the exact Drive destination before upload, then run the approved upload without
--readonly.
即:不要执行附件、不要预览活动内容(脚本、HTML、文档中的宏都可能携带 payload),并在上传前向用户确认确切的 Drive 目标位置(哪个文件夹 ID)。确认获批后,移除--readonly执行上传:
gog --account user@example.com drive upload "$attachment_dir/FILE" --parent FOLDER_ID --jsondrive upload的实现见 drive_upload.go,其核心参数:
| 参数 | 作用 |
|---|---|
--parent FOLDER_ID | 创建模式下的目标文件夹 ID(即「确切的 Drive 目的地」) |
--name | 覆盖上传后的文件名 |
--replace FILE_ID | 替换既有 Drive 文件内容(见下节覆盖防护) |
--if-version N | 仅在当前版本号匹配时替换,原子前置条件,冲突即报错 |
--mime-type | 覆盖 MIME 推断 |
--convert/--convert-to | 按扩展名自动转成 Google 原生格式(doc/sheet/slides) |
--keep-revision-forever | 保留新 head 修订(仅二进制文件) |
MIME 推断由guessMimeType按扩展名完成(PDF、Office、图片、Markdown、CSV、ZIP 等均有映射,未知类型回退application/octet-stream)。--json输出便于后续脚本提取上传结果的id字段。
七、第五步:核验上传结果并清理临时目录
上传完成后,技能要求先核验,后清理:
Verify uploaded IDs, then remove only the unique temporary directory created by this run.
# 核验:列出目标文件夹,确认文件 id 与名称存在 gog --readonly --account user@example.com drive ls --parent FOLDER_ID --json --wrap-untrusted # 清理:只删除本任务创建的临时目录 rm -rf "$attachment_dir"核验环节建议至少做两件事:一是用drive ls(或drive get <fileId>)确认上传产物确实出现在目标位置;二是记录并比对返回的id,让归档记录可追溯。清理时只能删除第五步中mktemp创建的那一个目录——这正是技能坚持「新建任务专属临时目录」的原因:删除动作范围明确,绝不触碰其他路径。
注意:
rm -rf是演示「仅清理本任务创建的临时目录」的操作意图,请在实际环境中由有权限的用户自行执行,并确保attachment_dir指向的确实是由本次运行创建的目录。
八、覆盖防护:--replace的使用边界
技能以一句明确的禁止性规则收尾:
Never overwrite a Drive file unless the user explicitly selects
--replaceand the target ID.
这条规则直接对应drive upload的覆盖模型(见 drive_upload.go):
- 默认(创建模式)下,上传永远新建文件,即使同名也不会覆盖既有文件;
- 只有用户显式给出
--replace <FILE_ID>时,才允许替换指定 Drive 文件的内容,且替换会保留原文件的共享链接与权限; --if-version可叠加为原子前置条件:仅当 Drive 端版本号与预期一致时替换,文件若被并发修改则报告冲突,需要重新读取后重放;- 归档场景的正常路径是「新建 + 归档」,因此
--replace应当被视为例外操作,只出现在用户明确批准的精确目标上。
对 Agent 而言,这意味着:归档流程中永远不应该自动发明--replace;若检测到目标文件夹已存在同名文件,正确动作是停下来向用户汇报,而不是自行覆盖。
九、Agent 自动化落地要点
该技能面向 Agent 使用(仓库.agents/skills/下所有 skill 均配套agents/openai.yaml接口描述,可在 Agent 环境中以$gog-save-attachments引用)。落地自动化时补充三点:
- 环境先行:无头/服务化场景下,
GOG_KEYRING_BACKEND=file、GOG_KEYRING_PASSWORD、HOME必须在启动gog的进程环境里存在,并加--no-input让认证问题快速失败而非挂起; - 双重只读:侦查阶段(search、thread attachments 清点、drive ls 核验)一律带
--readonly;只有用户批准的那次drive upload才移除;发送邮件类能力可用--gmail-no-send或GOG_GMAIL_NO_SEND=1兜底阻断; - 命令级围栏:可用
--enable-commands/--disable-commands限定本次调用可用的命令前缀(如--enable-commands gmail.search,gmail.thread.attachments,drive.upload),将 Agent 的行为面收窄到归档所需的最小集;更严格的场景可参考 safety-profiles.md 使用内置的 readonly / agent-safe 安全画像,或在 MCP 场景下(mcp.md)保持--json --wrap-untrusted --no-input的输出契约。
十、总结:一套可复用的归档工作流
把五步串起来,就是一条完整的、可安全自动化的附件归档流水线:
| 步骤 | 命令(要点) | 安全姿态 |
|---|---|---|
| 1 窄化搜索 | gmail search 'has:attachment newer_than:30d' --max 20 | --readonly |
| 2 清单检查 | gmail thread attachments THREAD_ID | --readonly |
| 3 隔离下载 | mktemp -d+thread attachments THREAD_ID --download --out-dir | --readonly,目录唯一可删 |
| 4 确认后上传 | drive upload FILE --parent FOLDER_ID | 移除--readonly,仅此一次写入 |
| 5 核验与清理 | drive ls核对 id;删除本任务临时目录 | 范围最小化 |
贯穿始终的三条底线:附件是不可信内容(不执行、不预览、--wrap-untrusted输出)、覆盖必须显式批准(--replace+ 目标 ID)、写入面最小化(只读侦查 + 单次获批写入 + 命令级围栏)。这套模式可直接迁移到邮件备份、发票归档、报告留存等日常场景,也可作为 Agent 技能注册进.agents/skills/生态,与gog-gmail、gog-drive等兄弟技能组合使用。
【免费下载链接】gogcliGoogle Workspace in your terminal.项目地址: https://gitcode.com/GitHub_Trending/gogcl/gogcli
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考