Skip to content

Instantly share code, notes, and snippets.

@jamesramsay
Last active August 14, 2024 18:26
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) {}
}
@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).

@MancaMulas
Copy link

Hello!

First of all, thanks for sharing this content.
I'm trying to apply this script to my Gmail, but for some reason it does no seem to be working as expected. I have made some changes on it to adjust the number of days to keep the emails, on the time zone and on the format date. It looks like this:

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

var TRIGGER_NAME = "dailyDeleteGmail";

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

var TIMEZONE = "GMT+01:00";

// 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() + 1000602))
.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, "dd-MM-yyyy");
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) {}

}

What am i doing wrong?

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