code icon Code

Archive Gmail Messages

Archive messages by removing them from inbox (not deleting)

Source Code

import fs from "fs";

const [messageIdsArg = "", outputPath = "session/archive-results.json"] =
  process.argv.slice(2);

const messageIds = messageIdsArg
  .split(",")
  .map((id) => id.trim())
  .filter(Boolean);

if (messageIds.length === 0) {
  console.error("No message IDs provided");
  console.log(JSON.stringify({ success: false, error: "no_message_ids" }));
  process.exit(1);
}

console.log(`Archiving ${messageIds.length} message(s)...`);

/**
 * Archive a batch with retry for transient failures
 */
async function archiveBatchWithRetry(batch, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const res = await fetch(
      "https://gmail.googleapis.com/gmail/v1/users/me/messages/batchModify",
      {
        method: "POST",
        headers: {
          Authorization: "Bearer PLACEHOLDER_TOKEN",
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          ids: batch,
          removeLabelIds: ["INBOX"],
        }),
      }
    );

    if (res.ok) {
      return { success: true };
    }

    // Retry on 5xx or 429 (rate limit)
    if (res.status >= 500 || res.status === 429) {
      const delay = Math.pow(2, attempt) * 1000 + Math.random() * 1000;
      console.log(
        `  Batch failed (${res.status}), retrying in ${Math.round(delay / 1000)}s...`
      );
      await new Promise((r) => setTimeout(r, delay));
      continue;
    }

    // Non-retryable error
    const errorText = await res.text();
    return { success: false, status: res.status, error: errorText };
  }

  return { success: false, status: "max_retries", error: "Exceeded retry limit" };
}

/**
 * Archive messages using Gmail batch modify API
 * Archives by removing INBOX label (messages stay in All Mail)
 */
async function archiveMessages(ids) {
  const BATCH_SIZE = 50;
  const results = { archived: [], failed: [] };
  const totalBatches = Math.ceil(ids.length / BATCH_SIZE);

  for (let i = 0; i < ids.length; i += BATCH_SIZE) {
    const batch = ids.slice(i, i + BATCH_SIZE);
    const batchNum = Math.floor(i / BATCH_SIZE) + 1;

    console.log(
      `  Archiving batch ${batchNum}/${totalBatches} (${batch.length} messages)...`
    );

    const result = await archiveBatchWithRetry(batch);

    if (result.success) {
      results.archived.push(...batch);
      console.log(`  ✓ Batch ${batchNum} complete (${results.archived.length}/${ids.length} total)`);
    } else {
      console.error(`  ✗ Batch ${batchNum} failed: ${result.status} - ${result.error}`);
      results.failed.push(...batch);
    }
  }

  return results;
}

try {
  const results = await archiveMessages(messageIds);

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

  const output = {
    success: results.failed.length === 0,
    archivedAt: new Date().toISOString(),
    archivedCount: results.archived.length,
    failedCount: results.failed.length,
    archived: results.archived,
    failed: results.failed,
  };

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

  console.log(`\n✓ Archived ${results.archived.length} message(s)`);
  if (results.failed.length > 0) {
    console.log(`✗ Failed to archive ${results.failed.length} message(s)`);
  }

  console.log(JSON.stringify(output));
} catch (error) {
  console.error("Archive failed:", error.message);
  throw error;
}