Apps Script 11 AI Email Draft Assistant for Gmail

https://github.com/lsvekis/Apps-Script-Code-Snippets

🚀 Apps Script + Gemini Mastery — Issue #11

AI Email Draft Assistant for Gmail

Generate reply drafts from a Gmail thread using Apps Script + Gemini (tone + action items + subject options).


⭐ What You Will Build

You’ll build an AI assistant that:

📥 Pulls the most recent email thread from Gmail (or a message by ID)
🧠 Summarizes the thread + extracts key asks
✍️ Drafts a reply in a chosen tone (friendly / professional / firm)
✅ Outputs a clean reply + action items + optional subject lines
🧩 Runs from a Google Sheet sidebar (so it’s easy to use + repeat)

This is not auto-sending email. It generates a draft you can copy/paste (safe by design).


🧠 Learning Objectives

Readers will learn how to:

✔ Search Gmail using GmailApp.search()
✔ Extract message bodies safely (and limit size)
✔ Prompt Gemini to write emails with constraints
✔ Return structured output (reply + bullets + subject lines)
✔ Build a sidebar UI for selecting tone + query


✅ Build Steps (Full Lesson)

1️⃣ Add Menu + Sidebar

Code.gs

function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu("AI Tools")
    .addItem("Gmail Draft Assistant", "showGmailAssistant")
    .addToUi();
}

function showGmailAssistant() {
  SpreadsheetApp.getUi().showSidebar(
    HtmlService.createHtmlOutputFromFile("Sidebar")
      .setTitle("Gmail Draft Assistant")
  );
}

2️⃣ Sidebar UI

Sidebar.html

<div style="font-family:Arial;padding:14px;">
  <h2>Gmail Draft Assistant</h2>

  <label><b>Gmail Search Query</b></label>
  <input id="query" style="width:100%;margin-bottom:8px;"
         placeholder='e.g., from:client subject:"invoice" newer_than:7d' />

  <label><b>Tone</b></label>
  <select id="tone" style="width:100%;margin-bottom:8px;">
    <option>Professional</option>
    <option>Friendly</option>
    <option>Firm</option>
    <option>Concise</option>
  </select>

  <label><b>Extra context (optional)</b></label>
  <textarea id="context" style="width:100%;height:70px;"></textarea>

  <button onclick="run()">Generate Draft</button>

  <pre id="out" style="white-space:pre-wrap;margin-top:12px;max-height:280px;overflow:auto;"></pre>

  <script>
    function run() {
      document.getElementById("out").textContent = "Loading email + drafting reply...";
      google.script.run
        .withSuccessHandler(msg => document.getElementById("out").textContent = msg)
        .draftReply(
          document.getElementById("query").value,
          document.getElementById("tone").value,
          document.getElementById("context").value
        );
    }
  </script>
</div>

3️⃣ Pull the Latest Matching Thread

GmailReader.gs

function getLatestThreadText_(query) {
  const threads = GmailApp.search(query || "in:inbox", 0, 1);
  if (!threads.length) throw new Error("No threads matched your query.");

  const messages = threads[0].getMessages();
  const last = messages[messages.length - 1];

  const from = last.getFrom();
  const subject = last.getSubject();
  const date = last.getDate();

  // IMPORTANT: keep it small for Gemini.
  const body = stripQuotedText_(last.getPlainBody()).slice(0, 8000);

  return { from, subject, date: String(date), body };
}

function stripQuotedText_(text) {
  // Simple heuristic: cut off at common reply separators
  const separators = [
    "\nOn ", "\nFrom:", "\nSent:", "\n-----Original Message-----"
  ];

  let cut = text.length;
  separators.forEach(sep => {
    const idx = text.indexOf(sep);
    if (idx !== -1 && idx < cut) cut = idx;
  });

  return text.substring(0, cut).trim();
}

4️⃣ Gemini Prompt → Structured Draft Output

DraftAssistant.gs

function draftReply(query, tone, extraContext) {
  let email;
  try {
    email = getLatestThreadText_(query);
  } catch (e) {
    return "Gmail error: " + e;
  }

  const prompt = `
You are an expert email assistant.

Write a reply draft based on the message below.

Constraints:
- Tone: ${tone || "Professional"}
- Keep it clear and realistic.
- Do NOT invent details.
- If key info is missing, ask 1-3 precise questions.
- Return JSON ONLY in this format:

{
  "reply": "full email reply text",
  "actionItems": ["item 1", "item 2"],
  "subjectOptions": ["subject 1", "subject 2"]
}

Extra context (if any):
${extraContext || ""}

Email:
From: ${email.from}
Subject: ${email.subject}
Date: ${email.date}

Body:
${email.body}
`;

  let out;
  try {
    out = callGemini(prompt, "");
  } catch (e) {
    return "Gemini error: " + e;
  }

  out = out.trim().replace(/```json/i, "").replace(/```/g, "").trim();

  let json;
  try {
    json = JSON.parse(out);
  } catch (e) {
    Logger.log("Bad JSON:\n" + out);
    return "Could not parse Gemini JSON. Check logs for the raw response.";
  }

  return formatDraftOutput_(json, email);
}

function formatDraftOutput_(json, email) {
  const lines = [];
  lines.push("✅ Draft created for:");
  lines.push("Subject: " + email.subject);
  lines.push("");

  if (json.subjectOptions && json.subjectOptions.length) {
    lines.push("Subject options:");
    json.subjectOptions.forEach(s => lines.push("- " + s));
    lines.push("");
  }

  lines.push("Reply draft:");
  lines.push(json.reply || "(no reply returned)");
  lines.push("");

  if (json.actionItems && json.actionItems.length) {
    lines.push("Action items:");
    json.actionItems.forEach(a => lines.push("- " + a));
  }

  return lines.join("\n");
}

5️⃣ Gemini Helper (Reuse from Prior Issues)

GeminiHelpers.gs

const GEMINI_API_KEY = "YOUR_API_KEY_HERE";
const GEMINI_MODEL   = "gemini-2.5-flash";

function callGemini(prompt, text) {
  if (!GEMINI_API_KEY || GEMINI_API_KEY === "YOUR_API_KEY_HERE") {
    throw new Error("Set your GEMINI_API_KEY in GeminiHelpers.gs");
  }

  const url = `https://generativelanguage.googleapis.com/v1/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`;

  const payload = {
    contents: [{ parts: [{ text: prompt + (text ? "\\n\\n---\\n\\n" + text : "") }] }]
  };

  const res = UrlFetchApp.fetch(url, {
    method: "post",
    contentType: "application/json",
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  });

  const json = JSON.parse(res.getContentText());
  if (json.error) throw new Error(json.error.message);

  return json.candidates[0].content.parts[0].text;
}

function testGeminiConnection() {
  Logger.log(callGemini("Say hello in one sentence.", ""));
}

🔐 Required Permissions

The first time you run it, Apps Script will request access to:

  • Gmail (read thread content)
  • Sheets UI (sidebar)
  • External requests (Gemini API)

If you see a permissions error, confirm appsscript.json includes:

  • https://www.googleapis.com/auth/gmail.readonly
  • https://www.googleapis.com/auth/script.external_request

🧪 Testing Setup

Use this Gmail query to start:

  • in:inbox newer_than:7d

Then try:

  • from:someone@domain.com newer_than:30d
  • subject:"invoice" newer_than:90d

🔜 Coming Next (Issue #12)

AI Meeting Notes → Action Plan for Google Docs
Paste meeting notes, get decisions + action items + follow-up email draft.