Skip to content

Instantly share code, notes, and snippets.

@jamesramsay
Last active April 8, 2024 13:05
Show Gist options
  • Save jamesramsay/9298cf3f4ac584a3dc05 to your computer and use it in GitHub Desktop.
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) {}
}
@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

@ivanski
Copy link

ivanski commented Mar 16, 2024

Thanks for all the updates, this is super useful since my old copy stopped running a while back.

I did add one tweak to the deletion logic which people may find useful:

if ( (email.getDate() < age) && (!email.isStarred()) ) {
  email.moveToTrash();
}

This allows you to prevent emails from being auto deleted by starring them when you review them.

Am also playing with having a slightly more flexible config that would allow that logic (and other criteria, such as only auto delete emails that have been read) to be configurable per-label, but I don't have that working yet:

var AUTODELETE_CONFIG =
{
  // Array of the desired labels to delete. Each entry contains a label name and criteria for autodeletion
  "labels":[
    {
      "label":"autodelete",
      "delete_starred":false, // Whether to delete starred emails
      "delete_unread":true, // Whether to delete unread emails
      "age":2 // How old should the emails be before they are deleted
    },
    {
      "label":"autodelete-when-read",
      "delete_starred":false,
      "delete_unread":false,
      "age":7
    },
    {
      "label":"promotions",
      "delete_starred":false,
      "delete_unread":false,
      "age":7
    },
    {
      "label":"notifications",
      "delete_starred":false,
      "delete_unread":false,
      "age":14
    }
  ]
};

@ivanski
Copy link

ivanski commented Mar 16, 2024

Though it might be just easier to code (if more of a hassle to config) to just let the query string itself be provided in the config, eg:

EMAILS_TO_DELETE = [
  "label:autodelete and older_than:2d and -is:starred",
  "label:autodelete-when-read and older_than:7d and -is:starred and -is:unread"
];

On the plus side, the queries can be tested in the main gmail UX (as opposed of running the risk of a bug in the query construction code doing bad things to your emails).

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