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.readonlyhttps://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:30dsubject:"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.