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) {}
}
@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