Categories
Automation Google Workspace

How to Automatically Delete Old Gmail Messages with Google Apps Script

If you’re like me, you’ve been manually pruning old emails for years. I have a 7-year retention policy for my Gmail, which means every month I’d spend hours searching for emails with queries like before:2018/11/01 and deleting them by hand. This 5-10 minute monthly chore finally annoyed me enough to automate it.

Here’s how I set up a Google Apps Script that automatically deletes emails older than 7 years on the 1st of each month. This works perfectly with Google Workspace accounts.

The Solution Overview

Google Apps Script is a free automation platform built into Google Workspace. We’ll create a script that:

  1. Calculates the date from 7 years ago
  2. Searches for all emails before that date
  3. Moves them to trash (Gmail auto-deletes after 30 days)
  4. Sends you an email report with the results
  5. Runs automatically every month

Step 1: Create the Script

Go to script.google.com and create a new project. Name it something like “Email Pruner” and paste in this code:

function pruneOldEmails() {
  const RETENTION_YEARS = 7;
  const BATCH_SIZE = 100; // Process 100 threads at a time
  
  // Calculate cutoff date (7 years ago)
  const cutoffDate = new Date();
  cutoffDate.setFullYear(cutoffDate.getFullYear() - RETENTION_YEARS);
  
  const cutoffString = Utilities.formatDate(cutoffDate, Session.getScriptTimeZone(), 'yyyy/MM/dd');
  const searchQuery = `before:${cutoffString}`;
  
  let totalThreads = 0;
  let totalMessages = 0;
  let processedThreads = 0;
  const startTime = new Date();
  
  try {
    // Get all matching threads
    let threads = GmailApp.search(searchQuery, 0, BATCH_SIZE);
    
    while (threads.length > 0) {
      totalThreads += threads.length;
      
      // Count messages in these threads
      threads.forEach(thread => {
        totalMessages += thread.getMessageCount();
      });
      
      // Move to trash
      GmailApp.moveThreadsToTrash(threads);
      processedThreads += threads.length;
      
      // Get next batch
      threads = GmailApp.search(searchQuery, 0, BATCH_SIZE);
      
      // Prevent timeout - if running for more than 5 minutes, stop and notify
      if ((new Date() - startTime) > 5 * 60 * 1000) {
        sendNotification('partial', processedThreads, totalMessages, cutoffString, 'Script timeout - will continue next run');
        return;
      }
    }
    
    // Success notification
    sendNotification('success', totalThreads, totalMessages, cutoffString);
    
  } catch (error) {
    sendNotification('error', processedThreads, totalMessages, cutoffString, error.toString());
  }
}

function sendNotification(status, threads, messages, cutoffDate, errorMsg = '') {
  const subject = status === 'success' 
    ? `? Email Pruning Complete - ${threads} threads deleted`
    : status === 'partial'
    ? `? Email Pruning Partial - ${threads} threads deleted so far`
    : `? Email Pruning Error`;
  
  let body = `Email Pruning Report\n\n`;
  body += `Status: ${status.toUpperCase()}\n`;
  body += `Cutoff Date: ${cutoffDate}\n`;
  body += `Threads Moved to Trash: ${threads}\n`;
  body += `Approximate Messages: ${messages}\n`;
  body += `Timestamp: ${new Date()}\n`;
  
  if (errorMsg) {
    body += `\nError Details:\n${errorMsg}`;
  }
  
  if (status === 'success') {
    body += `\n\nAll emails older than 7 years have been moved to trash.`;
    body += `\nThey will be permanently deleted after 30 days.`;
  }
  
  GmailApp.sendEmail(Session.getActiveUser().getEmail(), subject, body);
}

// Test function - run this first to verify it works
function testPruneOldEmails() {
  const RETENTION_YEARS = 7;
  const cutoffDate = new Date();
  cutoffDate.setFullYear(cutoffDate.getFullYear() - RETENTION_YEARS);
  const cutoffString = Utilities.formatDate(cutoffDate, Session.getScriptTimeZone(), 'yyyy/MM/dd');
  const searchQuery = `before:${cutoffString}`;
  
  // Just count, don't delete
  const threads = GmailApp.search(searchQuery, 0, 500);
  let messageCount = 0;
  threads.forEach(thread => {
    messageCount += thread.getMessageCount();
  });
  
  Logger.log(`Found ${threads.length} threads (${messageCount} messages) before ${cutoffString}`);
  Logger.log(`Search query: ${searchQuery}`);
  
  // Send test email
  GmailApp.sendEmail(
    Session.getActiveUser().getEmail(), 
    'Test: Email Pruning Preview', 
    `Preview of what would be deleted:\n\nThreads: ${threads.length}\nMessages: ~${messageCount}\nCutoff: ${cutoffString}\n\nNo emails were deleted in this test.`
  );
}

Click the save icon (?) to save your script.

Step 2: Test Before You Delete Anything

Before running the actual deletion, test it first:

  1. In the function dropdown at the top (next to the Debug button), select testPruneOldEmails
  2. Click the Run button (??)
  3. First-time authorization: A popup will ask for permissions
    • Click “Review permissions”
    • Select your Google Workspace account
    • Click “Advanced” then “Go to Email Pruner (unsafe)”
    • Click “Allow”
  4. Wait 1-2 minutes for the search to complete
  5. Check your email for “Test: Email Pruning Preview”

The test email will show you how many threads and messages would be deleted without actually deleting anything. This took about 90 seconds for me to process 7 years of email history.

Step 3: Run the Actual Deletion

Once you’ve verified the counts look correct:

  1. Select pruneOldEmails from the function dropdown
  2. Click Run (??)
  3. Wait for it to complete (1-2 minutes)
  4. Check your email for the completion report

The script will move all emails older than your retention period to trash. They’ll stay in trash for 30 days before Gmail permanently deletes them, so you have time to recover anything if needed.

Step 4: Set Up Monthly Automation

Now for the magic – making this run automatically:

  1. Click the Triggers icon (?) in the left sidebar
  2. Click “+ Add Trigger” in the bottom right
  3. Configure the trigger:
    • Choose which function to run: pruneOldEmails
    • Choose which deployment should run: Head
    • Select event source: Time-driven
    • Select type of time based trigger: Month timer
    • Select day of month: 1
    • Select time of day: Pick an off-hours time (I use 2am-3am)
  4. Click Save

That’s it! The script will now run on the 1st of each month and email you a report.

How It Works

The script uses the before: search operator to find emails older than your retention period. Each month it:

  1. Calculates today’s date minus 7 years
  2. Searches for threads before that date
  3. Processes them in batches of 100 (to stay within Gmail API limits)
  4. Moves them to trash
  5. Emails you a summary

The key difference from manual pruning: it’s dynamic. Instead of calculating “7 years ago from today” by hand each month, the script does it automatically.

Customization Options

Want to adjust the script? Here are some easy modifications:

Change retention period: Modify the RETENTION_YEARS constant at the top of the functions (line 2 and 39). Want to keep 5 years instead? Change 7 to 5.

Exclude certain emails: Add to the search query on line 9. For example:

  • Skip starred emails: before:${cutoffString} -is:starred
  • Skip a specific label: before:${cutoffString} -label:important
  • Skip attachments: before:${cutoffString} -has:attachment

Change batch size: If you hit timeout issues with huge mailboxes, reduce BATCH_SIZE from 100 to 50.

Important Notes for Google Workspace Users

  • Admin policies: Check if your Workspace admin has retention policies that might conflict
  • Trash retention: Workspace trash auto-deletes after 30 days
  • Rate limits: The script handles this automatically by processing in batches
  • Execution time: Apps Script has a 6-minute timeout. For massive email volumes, the script will notify you and continue on the next run

Troubleshooting

Script hangs during test: It’s not hanging – searching 7 years of email takes time. Wait 1-2 minutes and check View ? Logs for progress.

Authorization errors: Make sure you’re authorizing with your Google Workspace account, not a personal Gmail account.

Timeout errors: If you get a timeout notification, just wait for the next monthly run. The script will pick up where it left off.

The Results

After setting this up, I’ve reclaimed a whopping 1 hour per year! The script runs quietly on the 1st of each month, I get a quick email report, and my inbox stays under the 7-year limit automatically.

Total setup time: About 10 minutes. Time saved annually: 1 hour. Worth it.

Download the Script

You can copy the full script from this post, or feel free to modify it for your needs. The code is straightforward JavaScript and well-commented if you want to customize it further.

Happy automating!