feat(watcher): add agent Gmail CLI with audit and defaults

Provide list/read/summarize plus reason-gated archive, label, draft, and forward-as-draft, with SQLite audit logging and default-mailbox resolution.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-22 10:09:25 +07:00
co-authored by Cursor
parent 770ed8121a
commit f270f531ea
13 changed files with 1643 additions and 145 deletions
+102
View File
@@ -0,0 +1,102 @@
/**
* Build RFC822 raw message bodies for drafts (create only — never send).
*/
function encodeSubject(subject) {
// ASCII-safe; UTF-8 subjects use encoded-word if non-ascii
if (!/[^\x20-\x7E]/.test(subject || "")) return subject || "";
const b64 = Buffer.from(subject, "utf8").toString("base64");
return `=?UTF-8?B?${b64}?=`;
}
function encodeBase64Url(raw) {
return Buffer.from(raw, "utf8")
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
/**
* @param {{
* to: string,
* subject: string,
* body: string,
* cc?: string,
* inReplyTo?: string,
* references?: string,
* }} opts
*/
export function buildDraftRaw({
to,
subject,
body,
cc,
inReplyTo,
references,
}) {
const lines = [];
lines.push(`To: ${to}`);
if (cc) lines.push(`Cc: ${cc}`);
lines.push(`Subject: ${encodeSubject(subject)}`);
if (inReplyTo) lines.push(`In-Reply-To: ${inReplyTo}`);
if (references) lines.push(`References: ${references}`);
lines.push("MIME-Version: 1.0");
lines.push('Content-Type: text/plain; charset="UTF-8"');
lines.push("Content-Transfer-Encoding: 8bit");
lines.push("");
lines.push(body || "");
return lines.join("\r\n");
}
/**
* @param {{
* to: string,
* original: { from?: string, to?: string, subject?: string, date?: string, body?: string },
* note?: string,
* cc?: string,
* }} opts
*/
export function buildForwardDraftRaw({ to, original, note, cc }) {
const origSubject = original.subject || "(no subject)";
const subject = origSubject.toLowerCase().startsWith("fwd:")
? origSubject
: `Fwd: ${origSubject}`;
const parts = [];
if (note) {
parts.push(note.trim());
parts.push("");
}
parts.push("---------- Forwarded message ---------");
if (original.from) parts.push(`From: ${original.from}`);
if (original.date) parts.push(`Date: ${original.date}`);
parts.push(`Subject: ${origSubject}`);
if (original.to) parts.push(`To: ${original.to}`);
parts.push("");
parts.push(original.body || "");
return buildDraftRaw({
to,
subject,
body: parts.join("\n"),
cc,
});
}
export function rawToGmailMessage(raw) {
return { raw: encodeBase64Url(raw) };
}
/** Request body for archive: remove INBOX label. */
export function archiveModifyBody() {
return { removeLabelIds: ["INBOX"] };
}
/** Request body for label add/remove. */
export function labelModifyBody({ add = [], remove = [] } = {}) {
const body = {};
if (add.length) body.addLabelIds = add;
if (remove.length) body.removeLabelIds = remove;
return body;
}