Skip to content

Instantly share code, notes, and snippets.

@jamesramsay
Last active February 23, 2024 13:57
Star You must be signed in to star a gist
Save jamesramsay/9298cf3f4ac584a3dc05 to your computer and use it in GitHub Desktop.
Gmail: delete old emails automatically

Gmail: delete old emails automatically

Automatically deletes old emails that match the specified label.

Get started

  • Create a new Google Apps Script at https://script.google.com
  • Overwrite the placeholder with the javascript below
  • Update the following constants:
    • LABEL_TO_DELETE: the label that should be have old messages deleted
    • DELETE_AFTER_DAYS: the age of messsages after which they will be moved to trash
  • Save the script, then run:
    • Initialize
    • Install

If you ever want to remove the script, run Uninstall to remove any left over triggers.

Changelog

2017-07-31

  • Added support for multiple labels
  • Added configurable TRIGGER_NAME
  • Increased default page size
  • Decreased default delay between receipt and delete

2016-01-21

  • Removed use of deprecated Session.getTimeZone()
  • Improved efficiency for long threads by checking thread.getLastMessageDate()

Acknowledgements

H/T: Arun's post How to Auto Delete Old Emails In Any Gmail Label

// The name of the Gmail Label that is to be checked for purging?
var LABELS_TO_DELETE = [
"notifications-github",
"notifications-trello",
"notifications-hipchat"
];
var TRIGGER_NAME = "dailyDeleteGmail";
// Purge messages in the above label automatically after how many days?
var DELETE_AFTER_DAYS = "4";
var TIMEZONE = "AEST";
// Maximum number of threads to process per run
var PAGE_SIZE = 150;
// If number of threads exceeds page size, resume job after X mins (max execution time is 6 mins)
var RESUME_FREQUENCY = 10;
/*
IMPLEMENTATION
*/
function Intialize() {
return;
}
function Install() {
// First run 2 mins after install
ScriptApp.newTrigger(TRIGGER_NAME)
.timeBased()
.at(new Date((new Date()).getTime() + 1000*60*2))
.create();
// Run daily there after
ScriptApp.newTrigger(TRIGGER_NAME)
.timeBased().everyDays(1).create();
}
function Uninstall() {
var triggers = ScriptApp.getProjectTriggers();
for (var i=0; i<triggers.length; i++) {
ScriptApp.deleteTrigger(triggers[i]);
}
}
function dailyDeleteGmail() {
var age = new Date();
age.setDate(age.getDate() - DELETE_AFTER_DAYS);
var purge = Utilities.formatDate(age, TIMEZONE, "yyyy-MM-dd");
var search = "(label:" + LABELS_TO_DELETE.join(" OR label:") + ") before:" + purge;
Logger.log("PURGE: " + purge);
Logger.log("SEARCH: " + search);
try {
var threads = GmailApp.search(search, 0, PAGE_SIZE);
// Resume again in 10 minutes
if (threads.length == PAGE_SIZE) {
Logger.log("Scheduling follow up job...");
ScriptApp.newTrigger(TRIGGER_NAME)
.timeBased()
.at(new Date((new Date()).getTime() + 1000*60*RESUME_FREQUENCY))
.create();
}
// Move threads/messages which meet age criteria to trash
Logger.log("Processing " + threads.length + " threads...");
for (var i=0; i<threads.length; i++) {
var thread = threads[i];
if (thread.getLastMessageDate() < age) {
thread.moveToTrash();
} else {
var messages = GmailApp.getMessagesForThread(threads[i]);
for (var j=0; j<messages.length; j++) {
var email = messages[j];
if (email.getDate() < age) {
email.moveToTrash();
}
}
}
}
} catch (e) {}
}
@knireis
Copy link

knireis commented May 28, 2018

i have labels with many sublabels, is there an easy way to include these sublabels automatically.
Maybe with help of wildcards? Like Label/*?

@sansal
Copy link

sansal commented Jun 8, 2018

Hello,
I want to use "Automatically deletes old emails that older than 1 month."
With this command "older_than:1m" can you help, thanks.

@Raboo
Copy link

Raboo commented Oct 25, 2018

it doesn't work for me. I guess it have to do with the delete not being able to delete over million e-mails. I don't get any useful logs.
Is it possible to delete in chunks?

@reidrivenburgh
Copy link

reidrivenburgh commented Jan 7, 2019

Hi. I've been fiddling with this version and another for awhile now. I noticed that at times my triggers seem to fire and then stick around in a disabled state. I'm a novice when it comes to google scripting, but this version of your main function with the added deleteTrigger() call seems to keep things tidy for me. Feel free to use as you see fit.... Thanks.

function dailyDeleteGmail(ev) {
  var age = new Date();  
  age.setDate(age.getDate() - DELETE_AFTER_DAYS);    
  
  var purge  = Utilities.formatDate(age, TIMEZONE, "yyyy-MM-dd");
  var search = "(label:" + LABELS_TO_DELETE.join(" OR label:") + ") before:" + purge;
  console.log("PURGE: " + purge);
  console.log("SEARCH: " + search);
  
  try {
    var threads = GmailApp.search(search, 0, PAGE_SIZE);
    
    // Resume again in 10 minutes
    if (threads.length == PAGE_SIZE) {
      console.log("Scheduling follow up job...");
      ScriptApp.newTrigger(TRIGGER_NAME)
               .timeBased()
               .at(new Date((new Date()).getTime() + 1000*60*RESUME_FREQUENCY))
               .create();
    }
    
    // Move threads/messages which meet age criteria to trash
    console.log("Processing " + threads.length + " threads...");
    for (var i=0; i<threads.length; i++) {
      var thread = threads[i];
      
      if (thread.getLastMessageDate() < age) {
        thread.moveToTrash();
      } else {
        var messages = GmailApp.getMessagesForThread(threads[i]);
        for (var j=0; j<messages.length; j++) {
          var email = messages[j];       
          if (email.getDate() < age) {
            email.moveToTrash();
          }
        }
      }
    }
    
  } catch (e) {}
  
  /* Delete the trigger that caused this (if there is one), otherwise the trigger
  seems to linger in a disabled state. */
  if (ev != null) {
    if (!ScriptApp.getProjectTriggers().some(function (trigger) {
      if (trigger.getUniqueId() === ev.triggerUid) {
        ScriptApp.deleteTrigger(trigger);
        return true;
      }
      
      return false;
    })) {
      console.error("Could not find trigger with id '%s'", triggerUid);
    }
  }
}

@sergey777s
Copy link

in this script you delete messages after 4 days, but I neet to delete for example 40 oldest threads in gmail, in my account this year is 2017. How to get last thread date in script for delete only 40 very old threads?

@reidrivenburgh
Copy link

I rewrote the "delete trigger" section above, since it seemed to be deleting my nightly trigger. Here is that section:

  /* Delete the trigger that caused this (if there is one), otherwise the trigger
  seems to linger in a disabled state. Only delete if there are two, otherwise the
  nightly one seems to be getting deleted. */
  if (ev != null) {
    // Count the number of triggers.
    var triggers = ScriptApp.getProjectTriggers();
    var triggerCounter = triggers.length;  
    
    if (triggerCounter == 2) {
    if (!ScriptApp.getProjectTriggers().some(function (trigger) {
      if (trigger.getUniqueId() === ev.triggerUid) {
        console.log("Deleting " + trigger.getUniqueId());
        ScriptApp.deleteTrigger(trigger);
        return true;
      }
      
      return false;
    })) {
      console.error("Could not find trigger with id '%s'", triggerUid);
    }
    }
  }

@trusktr
Copy link

trusktr commented Jun 5, 2019

@sansal you can replace the line

  var search = "(label:" + LABELS_TO_DELETE.join(" OR label:") + ") before:" + purge;

with

  var search = "(label:" + LABELS_TO_DELETE.join(" OR label:") + ") older_than:1m"; // <--- HERE, use it

@heskyji
Copy link

heskyji commented May 2, 2020

Great stuff. Thanks mate. Is there anyway you can add an output for log.

@Zeaneth
Copy link

Zeaneth commented May 28, 2020

Thanks a ton!

@marklorschy
Copy link

Hi,
I really think the concept is terrific. Thank you. However, I am unable to make this script work. I would be grateful for some help.

The emails I would like to delete are labelled ‘Home-Security’ and is not nested underneath another label.
I adjusted the script to reflect :

  1. this label
  2. time period

The first 12 lines of script are:

// The name of the Gmail Label that is to be checked for purging?

var LABELS_TO_DELETE = [
"Home-Security"

];

var TRIGGER_NAME = "dailyDeleteGmail";

// Purge messages in the above label automatically after how many days?
var DELETE_AFTER_DAYS = "0.25";

I have also installed and initialised the script.
I attach screenshots of the reports ‘My Executions’
Slide1
Slide2

@svamja
Copy link

svamja commented Feb 26, 2021

var DELETE_AFTER_DAYS = "0.25";

@marklorschy, You cannot use fraction of days here. Only integer values!

And really thank you, @jamesramsay, for this wonderful script!

@lucasslac
Copy link

var DELETE_AFTER_DAYS = "0.25";

@marklorschy, You cannot use fraction of days here. Only integer values!

And really thank you, @jamesramsay, for this wonderful script!

I would like to auto delete some emails every 30 minutes, because I receive some sales mail hourly. What should I modify in the code?

@sudhakar6
Copy link

Thanks a lot @jamesramsay

@TheParin
Copy link

Thanking a lot for this amazing code. I am using it to get rid of hundreds of emails on a regular basis AUTOMATICALLY! This script has saved a lot of effort and increased productivity. I wrote a post about it here: https://www.linkedin.com/posts/theparin_gmail-delete-old-emails-automatically-activity-6782625622943498241-Et86

Initially, 4 months ago, I had a problem deleting emails from multiple labels when I was putting the names of multiple labels in the script and it didn't delete any of them at all. But, Just now, at this very moment, I found a 10-year old answer on https://webapps.stackexchange.com/questions/10581/filtering-based-on-multiple-labels-in-gmail , saying that we need to add '-' sign if the label names have spaces in them. And now, it is working properly as it should. just writing it here because if anyone else faces the same problem, it will help them with that tiny issue.

Thanks again, @jamesramsay !

@sympathyrs
Copy link

Thank you, helps a lot!

@kiwicodr
Copy link

This has been awesome up until about 5 months ago when gmail changed some permission stuff. I'm trying to get working again. I want to tell script to delete Inbox after 1000 days. Is there a way to do this? Adding 'Inbox' as such
// The name of the Gmail Label that is to be checked for purging?
var LABELS_TO_DELETE = [
"notifications-github",
"notifications-trello",
"notifications-hipchat",
"Inbox"
];
does not work. Any body know how to achieve this?

@flyingwolf79
Copy link

Lowercase.

inbox

That is the correct tag.

@Exniwolf
Copy link

Exniwolf commented Feb 6, 2023

Thanks a lot

@EricCholey
Copy link

Hi. I've been fiddling with this version and another for awhile now. I noticed that at times my triggers seem to fire and then stick around in a disabled state. I'm a novice when it comes to google scripting, but this version of your main function with the added deleteTrigger() call seems to keep things tidy for me. Feel free to use as you see fit.... Thanks.

function dailyDeleteGmail(ev) {
  var age = new Date();  
  age.setDate(age.getDate() - DELETE_AFTER_DAYS);    
  
  var purge  = Utilities.formatDate(age, TIMEZONE, "yyyy-MM-dd");
  var search = "(label:" + LABELS_TO_DELETE.join(" OR label:") + ") before:" + purge;
  console.log("PURGE: " + purge);
  console.log("SEARCH: " + search);
  
  try {
    var threads = GmailApp.search(search, 0, PAGE_SIZE);
    
    // Resume again in 10 minutes
    if (threads.length == PAGE_SIZE) {
      console.log("Scheduling follow up job...");
      ScriptApp.newTrigger(TRIGGER_NAME)
               .timeBased()
               .at(new Date((new Date()).getTime() + 1000*60*RESUME_FREQUENCY))
               .create();
    }
    
    // Move threads/messages which meet age criteria to trash
    console.log("Processing " + threads.length + " threads...");
    for (var i=0; i<threads.length; i++) {
      var thread = threads[i];
      
      if (thread.getLastMessageDate() < age) {
        thread.moveToTrash();
      } else {
        var messages = GmailApp.getMessagesForThread(threads[i]);
        for (var j=0; j<messages.length; j++) {
          var email = messages[j];       
          if (email.getDate() < age) {
            email.moveToTrash();
          }
        }
      }
    }
    
  } catch (e) {}
  
  /* Delete the trigger that caused this (if there is one), otherwise the trigger
  seems to linger in a disabled state. */
  if (ev != null) {
    if (!ScriptApp.getProjectTriggers().some(function (trigger) {
      if (trigger.getUniqueId() === ev.triggerUid) {
        ScriptApp.deleteTrigger(trigger);
        return true;
      }
      
      return false;
    })) {
      console.error("Could not find trigger with id '%s'", triggerUid);
    }
  }
}

What is your ev ?? (ev != null) ???

@reidrivenburgh
Copy link

So at some point awhile back I rewrote the script. Here is my latest, in case it helps. I don't remember what's different, but this version seems more robust than the previous one. I also haven't been following the discussion here....

// The name of the Gmail Label that is to be checked for purging?
var LABELS_TO_DELETE = [
  "auto_delete"
];

var TRIGGER_NAME = "dailyDeleteGmail";

// Purge messages in the above label automatically after how many days?
var DELETE_AFTER_DAYS = "30";

var TIMEZONE = "MST";

// Maximum number of threads to process per run
var PAGE_SIZE = 300;

// If number of threads exceeds page size, resume job after X mins (max execution time is 6 mins)
var RESUME_FREQUENCY = 15;

/*
IMPLEMENTATION
*/
function Intialize() {
  return;
}

/* "Install" really just runs it once. */
function Install() {

  // First run 2 mins after install
  console.log("In Install, creating trigger in 15 minutes");
  ScriptApp.newTrigger(TRIGGER_NAME)
           .timeBased()
           .at(new Date((new Date()).getTime() + 1000*60*1))
           .create();
  
  // Run daily there after
//  ScriptApp.newTrigger(TRIGGER_NAME)
//           .timeBased().everyDays(1).create();

}

function Uninstall() {
  
  var triggers = ScriptApp.getProjectTriggers();
  for (var i=0; i<triggers.length; i++) {
    ScriptApp.deleteTrigger(triggers[i]);
  }
  
}

function dailyDeleteGmail(ev) {
  var age = new Date();  
  age.setDate(age.getDate() - DELETE_AFTER_DAYS);    
  
  var purge  = Utilities.formatDate(age, TIMEZONE, "yyyy-MM-dd");
  var search = "(label:" + LABELS_TO_DELETE.join(" OR label:") + ") before:" + purge;
  console.log("PURGE: " + purge);
  console.log("SEARCH: " + search);
  
  try {
    /* Remove all triggers. We'll install a new one as appropriate later. */
    Uninstall();
    
    var threads = GmailApp.search(search, 0, PAGE_SIZE);
    
    // Resume again in x minutes
    if (threads.length === PAGE_SIZE) {
      var followUpDate = new Date((new Date()).getTime() + 1000*60*RESUME_FREQUENCY);
      var followUpdatePrint = Utilities.formatDate(followUpDate, TIMEZONE, "yyyy-MM-dd::hh:mm");
      console.log("Scheduling follow up job: " + followUpdatePrint);
//      var triggers = ScriptApp.getProjectTriggers();
//      if (triggers.length < 2) {
    /* More to do, so set a new trigger to run in a bit. */
         console.log("Creating a trigger to run in 15 minutes.");
        ScriptApp.newTrigger(TRIGGER_NAME)
                 .timeBased()
                 .at(new Date((new Date()).getTime() + 1000*60*RESUME_FREQUENCY))
                 .create();
//      }
    } else {
     // Caught up; run daily.
     console.log("Creating the daily trigger.");
      ScriptApp.newTrigger(TRIGGER_NAME)
           .timeBased().everyDays(1).create();   
    }
    
    // Move threads/messages which meet age criteria to trash
    console.log("Processing " + threads.length + " threads...");
    for (var i=0; i<threads.length; i++) {
      var thread = threads[i];
      
      if (thread.getLastMessageDate() < age) {
        thread.moveToTrash();
      } else {
        var messages = GmailApp.getMessagesForThread(threads[i]);
        for (var j=0; j<messages.length; j++) {
          var email = messages[j];       
          if (email.getDate() < age) {
            email.moveToTrash();
          }
        }
      }
    }
    
  } catch (e) { console.log(e); }

@joeyiii57
Copy link

Found this project literally by accident. Took me a couple of tries but I think I got it working. Thanks!

@pythongod
Copy link

pythongod commented Oct 24, 2023

This does not go to trash for met but will be deleted straight away. (Edit: Maybe not, just first run with a lot of mails is laggy)

@ajleach
Copy link

ajleach commented Nov 5, 2023

I added some more logging, added output if there's an error caught, and logic to clean up all triggers except the daily trigger before adding a followup trigger. I was running into an issue where there were too many triggers sitting out there, and the script was failing silently.

var LABELS_TO_DELETE = [
  // ... list of labels ...
];

// Constants for trigger names
var DAILY_TRIGGER_NAME = "dailyDeleteGmail";
var RESUME_TRIGGER_NAME = "resumeDeleteGmail";
var DELETE_AFTER_DAYS = 30;
var TIMEZONE = "America/Chicago";
var MAX_THREADS_TO_PROCESS = 100; // Adjust the number of threads to process in total
var RESUME_FREQUENCY = 5; // Minutes to wait before the next execution

function Install() {
  Logger.log('Installing triggers...');
  // Clear all existing triggers before setting up new ones
  Uninstall();

  // Set up the new daily trigger
  ScriptApp.newTrigger(DAILY_TRIGGER_NAME)
           .timeBased()
           .everyDays(1)
           .create();
  Logger.log('Installation complete.');
}

function Uninstall() {
  Logger.log('Uninstalling all triggers...');
  var triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    ScriptApp.deleteTrigger(triggers[i]);
  }
  Logger.log('All triggers have been removed.');
}

function dailyDeleteGmail() {
  Logger.log('Running daily delete for Gmail threads...');
  
  // Clear non-daily (resume) triggers
  clearNonDailyTriggers();

  processEmails();

  Logger.log('Daily Gmail deletion process complete.');
}

function resumeDeleteGmail() {
  Logger.log('Resuming deletion of Gmail threads...');

  // Clear non-daily (resume) triggers
  clearNonDailyTriggers();

  processEmails();

  Logger.log('Resumed Gmail deletion process complete.');
}

function processEmails() {
  var totalThreadsProcessed = 0;
  var labelsProcessed = {};
  var today = new Date();
  var age = new Date(today.setDate(today.getDate() - DELETE_AFTER_DAYS));
  var purge = Utilities.formatDate(age, TIMEZONE, "yyyy-MM-dd");

  LABELS_TO_DELETE.forEach(function(label) {
    var threadsToProcess = MAX_THREADS_TO_PROCESS - totalThreadsProcessed;
    if (threadsToProcess <= 0) {
      return; // Skip if the maximum number of threads to process has been reached
    }

    var search = 'label:' + label + ' before:' + purge;
    Logger.log('Searching for threads with label: ' + label);
    var threads = GmailApp.search(search, 0, threadsToProcess);
    var threadsToDelete = threads.length;

    Logger.log('Found ' + threadsToDelete + ' threads to delete for label: ' + label);
    for (var i = 0; i < threadsToDelete; i++) {
      threads[i].moveToTrash();
    }

    totalThreadsProcessed += threadsToDelete;
    labelsProcessed[label] = (labelsProcessed[label] || 0) + threadsToDelete;
  });

  // Log the deleted email count per label
  Logger.log('Deleted emails from labels:');
  for (var label in labelsProcessed) {
    Logger.log(label + ': ' + labelsProcessed[label]);
  }

  // Schedule another run if at the processing limit
  if (totalThreadsProcessed >= MAX_THREADS_TO_PROCESS) {
    Logger.log("Reached processing limit, scheduling follow-up...");
    ScriptApp.newTrigger(RESUME_TRIGGER_NAME)
             .timeBased()
             .after(RESUME_FREQUENCY * 60 * 1000)
             .create();
  } else {
    Logger.log('Did not reach processing limit, no follow-up needed.');
  }
}

function clearNonDailyTriggers() {
  var existingTriggers = ScriptApp.getProjectTriggers();
  Logger.log('Checking for non-daily triggers...');
  for (var i = 0; i < existingTriggers.length; i++) {
    var trigger = existingTriggers[i];
    if (trigger.getHandlerFunction() === RESUME_TRIGGER_NAME) {
      ScriptApp.deleteTrigger(trigger);
      Logger.log('Deleted non-daily (resume) trigger: ' + trigger.getUniqueId());
    }
  }
}

function isDailyTrigger(trigger) {
  // Determine if the trigger is the daily trigger.
  return trigger.getHandlerFunction() === DAILY_TRIGGER_NAME &&
         trigger.getEventType() === ScriptApp.EventType.CLOCK;
}

function isResumeTrigger(trigger) {
  // Determine if the trigger is a resume (10-minute interval) trigger.
  return trigger.getHandlerFunction() === RESUME_TRIGGER_NAME &&
         trigger.getEventType() === ScriptApp.EventType.CLOCK;
}

function deleteTriggers(){
  var triggers = ScriptApp.getProjectTriggers();

  triggers.forEach(function(trigger){

    try{
      ScriptApp.deleteTrigger(trigger);
    } catch(e) {
      throw e.message;
    };

    Utilities.sleep(1000);

  });

};

@sometheycallme
Copy link

sometheycallme commented Dec 24, 2023

Excellent ideas in this thread. I put together a little procedure with screenshots for simply deleting all emails in the default Gmail Promitions, Updates, Social, and Forums tabs, leaving Inbox alone. Cap at 100 emails in each basket over time and run hourly. HTH. https://github.com/Cyber-Copilot/GmailCleaner

note: if you have a lot of cleaning to do, this will need to run for a while before your tabs are kept under 100 consistently. Quotas

GmailCleaner

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