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:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user