code icon Code

Create Gmail Draft

Create a draft email in Gmail (does not send)

Source Code

import fs from "fs";

const [
  to = "",
  subject = "",
  body = "",
  threadId = "",
  outputPath = "session/draft-result.json",
  htmlBody = "",
  inReplyTo = "",
  cc = "",
  bcc = "",
] = process.argv.slice(2);

if (!to) {
  console.error("Recipient (to) is required");
  console.log(JSON.stringify({ success: false, error: "missing_recipient" }));
  process.exit(1);
}

if (!subject && !threadId) {
  console.error("Subject is required for new emails");
  console.log(JSON.stringify({ success: false, error: "missing_subject" }));
  process.exit(1);
}

console.log(`Creating draft to: ${to}`);
if (cc) console.log(`CC: ${cc}`);
if (bcc) console.log(`BCC: ${bcc}`);
console.log(`Subject: ${subject || "(reply)"}`);
if (htmlBody) console.log(`Format: HTML + plain text`);
if (inReplyTo) console.log(`In-Reply-To: ${inReplyTo}`);

/**
 * Build RFC 2822 formatted email message
 * Supports plain text, HTML (multipart/alternative), CC/BCC, and reply threading headers
 */
function buildRawMessage(to, subject, body, options = {}) {
  const { htmlBody, inReplyTo, cc, bcc } = options;
  const lines = [];

  // Required headers
  lines.push(`To: ${to}`);
  if (cc) lines.push(`Cc: ${cc}`);
  if (bcc) lines.push(`Bcc: ${bcc}`);
  lines.push(`Subject: ${subject}`);

  // Reply threading headers
  if (inReplyTo) {
    lines.push(`In-Reply-To: ${inReplyTo}`);
    lines.push(`References: ${inReplyTo}`);
  }

  lines.push("MIME-Version: 1.0");

  if (htmlBody) {
    // Multipart message with both plain text and HTML
    const boundary = `boundary_${Date.now()}_${Math.random().toString(36).slice(2)}`;
    lines.push(`Content-Type: multipart/alternative; boundary="${boundary}"`);
    lines.push("");

    // Plain text part
    lines.push(`--${boundary}`);
    lines.push("Content-Type: text/plain; charset=utf-8");
    lines.push("");
    lines.push(body);
    lines.push("");

    // HTML part
    lines.push(`--${boundary}`);
    lines.push("Content-Type: text/html; charset=utf-8");
    lines.push("");
    lines.push(htmlBody);
    lines.push("");

    lines.push(`--${boundary}--`);
  } else {
    // Plain text only
    lines.push("Content-Type: text/plain; charset=utf-8");
    lines.push("");
    lines.push(body);
  }

  return Buffer.from(lines.join("\r\n"))
    .toString("base64")
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/, "");
}

try {
  const raw = buildRawMessage(to, subject, body, { htmlBody, inReplyTo, cc, bcc });

  const requestBody = {
    message: { raw },
  };

  if (threadId) {
    requestBody.message.threadId = threadId;
  }

  const res = await fetch(
    "https://gmail.googleapis.com/gmail/v1/users/me/drafts",
    {
      method: "POST",
      headers: {
        Authorization: "Bearer PLACEHOLDER_TOKEN",
        "Content-Type": "application/json",
      },
      body: JSON.stringify(requestBody),
    }
  );

  if (!res.ok) {
    const errorText = await res.text();
    console.error(`Gmail API error: ${res.status}`);
    console.error(errorText);
    throw new Error(`Failed to create draft: ${res.status}`);
  }

  const draft = await res.json();

  // Write result
  const dir = outputPath.split("/").slice(0, -1).join("/");
  if (dir) fs.mkdirSync(dir, { recursive: true });

  const output = {
    success: true,
    createdAt: new Date().toISOString(),
    draftId: draft.id,
    messageId: draft.message?.id,
    threadId: draft.message?.threadId,
    to,
    subject,
    ...(cc && { cc }),
    ...(bcc && { bcc }),
    ...(htmlBody && { hasHtml: true }),
    ...(inReplyTo && { inReplyTo }),
  };

  fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));

  console.log(`\n✓ Draft created`);
  console.log(`  Draft ID: ${draft.id}`);
  console.log(`  Open Gmail to review and send.`);

  console.log(JSON.stringify(output));
} catch (error) {
  console.error("Failed to create draft:", error.message);
  throw error;
}