Gmail Email Scheduler

An automated Google Apps Script that monitors important emails and intelligently creates calendar events using Google's Gemini AI to extract dates and details.

How It Works

Trigger: User marks an email as "Important."

Process: System scans inbox hourly, aggregates thread history, and queries the Gemini API.

Action: Updates Google Calendar (Create/Edit/Cancel) and provides a direct link to the email source.

// ==========================================
// CONFIGURATION
// ==========================================
// ๐Ÿ”ด IMPORTANT: Replace this with your NEW key. Do not publish real keys!
var GEMINI_API_KEY = "PASTE_YOUR_KEY_HERE"; // <--- DOUBLE CHECK THIS!
var CALENDAR_ID = "primary"; 

// ==========================================
// MAIN FUNCTION (Run this Hourly)
// ==========================================
function processImportantEmails() {
  // 1. Garbage Collection (Self-Healing DB)
  cleanupOldMemory();

  var threads = GmailApp.search('is:important', 0, 5);
  if (threads.length === 0) {
    Logger.log("No important emails found. Standing by.");
    return;
  }

  Logger.log("Found " + threads.length + " important emails...");

  for (var i = 0; i < threads.length; i++) {
    var thread = threads[i];
    var messages = thread.getMessages(); // Get FULL history
    var subject = messages[0].getSubject();

    // 2. Stitch Full Conversation (Solving the "Agreement Wall")
    var fullConversation = "";
    for (var m = 0; m < messages.length; m++) {
      var msg = messages[m];
      // Simple cleanup to remove excess whitespace
      fullConversation += `\n--- Message ${m+1} (From: ${msg.getFrom()}) ---\n`;
      fullConversation += msg.getPlainBody().replace(/\s+/g, " ").trim() + "\n";
    }

    // 3. AI Analysis
    var aiResult = callGeminiAPI(subject, fullConversation);

    if (aiResult && aiResult.isValid) { // <--- FIXED: Matches prompt JSON key
      
      // SCENARIO: CANCELLATION
      if (aiResult.action === "cancel") {
        handleCancellation(thread.getId());
        Logger.log("โŒ Event Cancelled: " + subject);
      } 
      
      // SCENARIO: CREATE or UPDATE
      else {
        // If updating, we first delete the old event (Update = Delete + Create)
        handleCancellation(thread.getId()); 
        createCalendarEvent(aiResult, thread.getPermalink(), thread.getId());
        Logger.log("โœ… Scheduled/Updated: " + aiResult.title);
      }
      
      // Cleanup: Remove Important marker
      thread.markUnimportant();
      
    } else {
      // SCENARIO: AMBIGUOUS / FAILURE
      thread.markUnimportant();
      
      // Star the LATEST message so you see it at the top of your inbox
      messages[messages.length - 1].star(); 
      Logger.log("โš ๏ธ Ambiguous. Starred for review: " + subject);
    }
  }
}

// ==========================================
// DATABASE & CALENDAR LOGIC
// ==========================================

function createCalendarEvent(data, emailLink, threadId) {
  var calendar = CalendarApp.getCalendarById(CALENDAR_ID);
  var event = calendar.createEvent(data.title, new Date(data.startTime), new Date(data.endTime), {
    location: data.location,
    description: data.description + "\n\n๐Ÿ”— Source Email: " + emailLink
  });

  // SAVE TO MEMORY (Persistence)
  var props = PropertiesService.getScriptProperties();
  var payload = JSON.stringify({
    eventId: event.getId(),
    eventDate: data.startTime
  });
  props.setProperty(threadId, payload);
}

function handleCancellation(threadId) {
  var props = PropertiesService.getScriptProperties();
  var record = props.getProperty(threadId);

  if (record) {
    try {
      var data = JSON.parse(record);
      var calendar = CalendarApp.getCalendarById(CALENDAR_ID);
      var event = calendar.getEventById(data.eventId);
      if (event) {
        event.deleteEvent();
      }
      props.deleteProperty(threadId); // Remove from DB
    } catch (e) {
      Logger.log("Error cancelling event: " + e);
    }
  }
}

function cleanupOldMemory() {
  var props = PropertiesService.getScriptProperties();
  var data = props.getProperties();
  var now = new Date();
  var keysToDelete = [];

  for (var key in data) {
    try {
      var record = JSON.parse(data[key]);
      var eventDate = new Date(record.eventDate);
      var diffDays = (now - eventDate) / (1000 * 60 * 60 * 24);
      if (diffDays > 7) keysToDelete.push(key); // Delete if older than 7 days
    } catch (e) {
      keysToDelete.push(key);
    }
  }

  if (keysToDelete.length > 0) {
    for (var k = 0; k < keysToDelete.length; k++) {
      props.deleteProperty(keysToDelete[k]);
    }
    Logger.log("๐Ÿงน Cleaned " + keysToDelete.length + " old records.");
  }
}

// ==========================================
// GEMINI API HELPER (Version 4.2: Timezone Fix)
// ==========================================
function callGeminiAPI(subject, conversation) {
  var url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-latest:generateContent?key=" + GEMINI_API_KEY;
  var cleanText = conversation.substring(0, 30000); 
  
  // 1. Get User's Timezone dynamically (e.g., "America/Los_Angeles")
  var userTimeZone = Session.getScriptTimeZone();
  
  var prompt = `
    You are a scheduling assistant. Analyze this email thread history.
    
    Subject: ${subject}
    Conversation History: ${cleanText}
    
    CONTEXT:
    - User's Timezone: ${userTimeZone}
    - Today's Date: ${new Date().toString()}
    
    CRITICAL INSTRUCTIONS:
    1. TIMESTAMPS: Return all 'startTime' and 'endTime' in ISO 8601 format WITH THE CORRECT OFFSET for the User's Timezone (e.g., "2026-01-25T16:00:00-08:00"). Do NOT use UTC (Z) unless the user specifically asks for UTC.
    2. LATEST DATA: The conversation is chronological. The LAST message is the source of truth.
    3. IGNORE QUOTES: If the last message contains quoted history (e.g. "On Jan 24..."), IGNORE the history. Only read the new text typed by the sender.
    
    Determine the FINAL agreed-upon intent.
    Return RAW JSON:
    - isValid: boolean (true if a meeting is confirmed OR cancelled)
    - action: string ("create" or "cancel")
    - title: string (event title)
    - startTime: string (ISO 8601 with offset)
    - endTime: string (ISO 8601 with offset)
    - location: string (or "TBD")
    - description: string (summary)
  `;

  var payload = { "contents": [{ "parts": [{"text": prompt}] }] };
  var options = {
    "method": "post",
    "contentType": "application/json",
    "payload": JSON.stringify(payload),
    "muteHttpExceptions": true
  };

  try {
    var response = UrlFetchApp.fetch(url, options);
    var json = JSON.parse(response.getContentText());
    if (json.error || !json.candidates) return null;

var rawText = json.candidates[0].content.parts[0].text;
    //Logger.log("๐Ÿค– RAW AI RESPONSE: " + rawText); // <--- Add this line!
    rawText = rawText.replace(/```json/g, "").replace(/```/g, "").trim();
    return JSON.parse(rawText);
  } catch (e) {
    //Logger.log("API Error: " + e.toString());
    return null;
  }
}

Key Features

  • Context-Aware Analysis: Aggregates full email threads (not just the last message) to resolve complex references and "agreement walls."
  • CRUD Lifecycle Management: Distinct logic for Creating, Reading, Updating, and Deleting events based on changing email context.
  • Self-Healing Architecture: Includes a garbage collector that automatically purges database records older than 7 days to ensure long-term performance.
  • State Persistence: Leverages PropertiesService to map Gmail threads to Calendar IDs, preventing duplicate entries during rescheduling.
  • Smart Fallback: Automatically "Stars" ambiguous emails for human review rather than guessing or failing silently.
  • Source of Truth Linking: Embeds a "View Email" link directly into every calendar event for instant context retrieval.

Technologies: Google Apps Script, Gmail API, Google Calendar API, Gemini AI, PropertiesService

Status: Deployed for Personal Use