Skip to content

Instantly share code, notes, and snippets.

@benbjurstrom
Last active June 8, 2025 20:38
Show Gist options
  • Save benbjurstrom/00cdfdb24e39c59c124e812d5effa39a to your computer and use it in GitHub Desktop.
Save benbjurstrom/00cdfdb24e39c59c124e812d5effa39a to your computer and use it in GitHub Desktop.
PurgeOldEmails

PurgeOldEmails

A Google Apps Script script to automatically delete unarchived mail after 7 days that hasn't been starred or marked as important

Quick Start

  1. Backup your emails using Google Takeout.
  2. Load this script into a new Google Apps Script project.
  3. Execute the setPurgeTrigger() function to set a trigger that will call the purge() function every day.

A detailed blog post with more information can be found at https://benbjurstrom.com/purge-email

Acknowledgements

Thanks to this gist by jamesramsay for getting me started in the right direction.

/*
|--------------------------------------------------------------------------
| PurgeOldEmails
|--------------------------------------------------------------------------
| https://gist.github.com/benbjurstrom/00cdfdb24e39c59c124e812d5effa39a
|
*/
// Purge messages automatically after how many days?
var DELETE_AFTER_DAYS = 7
// Maximum number of message threads to process per run.
var PAGE_SIZE = 150
/**
* Create a trigger that executes the purge function every day.
* Execute this function to install the script.
*/
function setPurgeTrigger() {
ScriptApp
.newTrigger('purge')
.timeBased()
.everyDays(1)
.create()
}
/**
* Create a trigger that executes the purgeMore function two minutes from now
*/
function setPurgeMoreTrigger(){
ScriptApp.newTrigger('purgeMore')
.timeBased()
.at(new Date((new Date()).getTime() + 1000 * 60 * 2))
.create()
}
/**
* Deletes all triggers that call the purgeMore function.
*/
function removePurgeMoreTriggers(){
var triggers = ScriptApp.getProjectTriggers()
for (var i = 0; i < triggers.length; i++) {
var trigger = triggers[i]
if(trigger.getHandlerFunction() === 'purgeMore'){
ScriptApp.deleteTrigger(trigger)
}
}
}
/**
* Deletes all of the project's triggers
* Execute this function to unintstall the script.
*/
function removeAllTriggers() {
var triggers = ScriptApp.getProjectTriggers()
for (var i = 0; i < triggers.length; i++) {
ScriptApp.deleteTrigger(triggers[i])
}
}
/**
* Wrapper for the purge function
*/
function purgeMore() {
purge()
}
/**
* Deletes any emails from the inbox that are more then 7 days old
* and not starred or marked as important.
*/
function purge() {
removePurgeMoreTriggers()
var search = 'in:inbox -in:starred -in:important older_than:' + DELETE_AFTER_DAYS + 'd'
var threads = GmailApp.search(search, 0, PAGE_SIZE)
if (threads.length === PAGE_SIZE) {
console.log('PAGE_SIZE exceeded. Setting a trigger to call the purgeMore function in 2 minutes.')
setPurgeMoreTrigger()
}
console.log('Processing ' + threads.length + ' threads...')
var cutoff = new Date()
cutoff.setDate(cutoff.getDate() - DELETE_AFTER_DAYS)
// For each thread matching our search
for (var i = 0; i < threads.length; i++) {
var thread = threads[i]
// Only delete if the newest message in the thread is older then DELETE_AFTER_DAYS
if (thread.getLastMessageDate() < cutoff) {
thread.moveToTrash();
}
}
}
@sgrimm
Copy link

sgrimm commented Jun 8, 2025

Thanks for this script! Here's a modified version (based on the changes from @jtlarson) that supports setting different retention periods for different labels. I have a filter to add a DeleteAfter3Days label to things like daily news updates from my local newspaper. I also added the ability to do a dry run, useful when testing changes to the script.

/*
|--------------------------------------------------------------------------
| PurgeOldEmails 
|--------------------------------------------------------------------------
| https://gist.github.com/benbjurstrom/00cdfdb24e39c59c124e812d5effa39a
|
| Modified to allow per-label deletion times and support dry runs.
*/

// If true, just log what would be deleted; don't actually delete
const DRY_RUN = false;

// Purge messages with these labels after this many days.
const LABELS_TO_DELETE = {
  Updates: 90,
  Social: 14,
  Promotions: 7,
  DeleteAfter3Days: 3,
};

// Maximum number of message threads to process per run. 
const PAGE_SIZE = DRY_RUN ? 10 : 150;

/**
 * Create a trigger that executes the purge function every day.
 * Execute this function to install the script.
 */
function setPurgeTrigger() {
  ScriptApp
    .newTrigger('purge')
    .timeBased()
    .everyDays(1)
    .create();
}

/**
 * Create a trigger that executes the purgeMore function two minutes from now
 */
function setPurgeMoreTrigger(){
  ScriptApp.newTrigger('purgeMore')
  .timeBased()
  .at(new Date((new Date()).getTime() + 1000 * 60 * 2))
  .create();
}

/**
 * Deletes all triggers that call the purgeMore function.
 */
function removePurgeMoreTriggers(){
  ScriptApp.getProjectTriggers().forEach((trigger) => {
    if(trigger.getHandlerFunction() === 'purgeMore'){
      ScriptApp.deleteTrigger(trigger);
    }
  });
}

/**
 * Deletes all of the project's triggers
 * Execute this function to unintstall the script.
 */
function removeAllTriggers() {
  ScriptApp.getProjectTriggers().forEach(ScriptApp.deleteTrigger);
}

/**
 * Wrapper for the purge function so it's easy to distinguish the daily purge
 * job from the one-off jobs that get scheduled when there are too many threads
 * to delete in one run.
 */
function purgeMore() {
  purge();
}

/**
 * Deletes emails from the "LABELS_TO_DELETE" variable, using the value of each
 * property as the number of days to retain.
 */
function purge() {
  removePurgeMoreTriggers();
  
  // We're going to run several searches, one for each target label, but we
  // want to limit the total number of deletions per run of the script, not
  // per label, so we need to keep a running count of the remaining deletion
  // budget.
  let threadsRemaining = PAGE_SIZE;

  Object.keys(LABELS_TO_DELETE).forEach((labelToDelete) => {
    if (threadsRemaining <= 0) {
      return;
    }

    const deleteAfterDays = LABELS_TO_DELETE[labelToDelete];

    console.log(`Scanning for threads older than ${deleteAfterDays} days with label ${labelToDelete}`);

    const search = [
      `label:${labelToDelete}`,
      `older_than:${deleteAfterDays}d`,
      'in:inbox',
      '-in:sent',
      '-is:starred',
      '-is:important',
    ].join(' ');

    const threads = GmailApp.search(search, 0, threadsRemaining)
    
    console.log(`Found ${threads.length} threads for label ${labelToDelete}`);

    threadsRemaining -= threads.length;
    if (threadsRemaining <= 0) {
      console.log('PAGE_SIZE exceeded. Setting a trigger to call the purgeMore function in 2 minutes.');

      if (!DRY_RUN) {
        setPurgeMoreTrigger();
      }
    }

    const cutoff = new Date();
    cutoff.setDate(cutoff.getDate() - deleteAfterDays);

    // For each thread matching our search
    threads
      .filter((thread) => thread.getLastMessageDate() < cutoff)
      .forEach((thread) => {
        const threadDesc = `${thread.getLastMessageDate().toISOString()} ${thread.getMessages()[0].getFrom()} - ${thread.getFirstMessageSubject()}`;
        console.log(`Deleting thread: ${threadDesc}`);

        if (!DRY_RUN) {
          thread.moveToTrash();
        }
      })
  });
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment