Skip to content

Instantly share code, notes, and snippets.

@macabreb0b
Last active January 17, 2024 00:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save macabreb0b/dfa622f8aa03aebc67a3dd5f89da709c to your computer and use it in GitHub Desktop.
Save macabreb0b/dfa622f8aa03aebc67a3dd5f89da709c to your computer and use it in GitHub Desktop.
email eater - google apps script
// script forked from https://gist.github.com/jamesramsay/9298cf3f4ac584a3dc05
// to start timed execution: select "Install" from the menu above and hit "run"
// to stop timed execution: select "Uninstall" from the menu above and hit "run"
// to initialize a batch run ad-hoc and immediately: select "_processNextBatch" from the menu above and hit "run"
const LABELS_TO_DELETE = [
// ... list of labels ...
// 'test-delete-after-label',
'spammy-alerts',
// 'delete-after-30d',
// TODO - add support for different time bombs
// 'delete-after-7d',
// 'delete-after-1d',
// TODO - add support for archive
// 'archive-after-30d',
// 'archive-after-7d',
];
// Constants for trigger names
const DAILY_TRIGGER_NAME = "dailyProcessTrigger";
const DELAYED_BATCH_TRIGGER_NAME = "delayedBatchTrigger";
// timezone probably doesn't really matter if we're only working in days.
const TIMEZONE = "America/Los Angeles";
// Batch size and timing configuration
const MAX_THREADS_TO_PROCESS_PER_BATCH = 500; // max argument size is 500
const MINUTES_TO_WAIT_BETWEEN_BATCHES = 5; // TODO - why?
const DELETE_AFTER_DAYS = 30;
function _getPurgeBeforeDate() {
const today = new Date();
// TODO - change DELETE_AFTER_DAYS based on the label
const age = new Date(today.setDate(today.getDate() - DELETE_AFTER_DAYS));
return Utilities.formatDate(age, TIMEZONE, "yyyy-MM-dd");
}
function _processNextBatch() {
console.log('START process next batch');
// Clear all batch triggers
_clearAllDelayedBatchTriggers();
_processEmails();
console.log('FINISH process batch');
}
function _processEmails() {
let threadsProcessedCount = 0;
const threadsProcessedPerLabel = {};
const purgeBeforeDate = _getPurgeBeforeDate()
LABELS_TO_DELETE.forEach(function(label) {
const threadBudgetLeftForThisBatch = MAX_THREADS_TO_PROCESS_PER_BATCH - threadsProcessedCount;
if (threadBudgetLeftForThisBatch <= 0) {
return; // Skip this label if the maximum number of threads to process has been reached
}
const search = 'label:' + label + ' before:' + purgeBeforeDate;
console.log('Searching for threads with label: ' + label);
const threads = GmailApp.search(search, 0, threadBudgetLeftForThisBatch);
const threadsToDeleteCount = threads.length;
console.log('Found ' + threadsToDeleteCount + ' threads to delete for label: ' + label);
for (let i = 0; i < threadsToDeleteCount; i++) {
// NB: this moves the thread to the trash. so thread will not get *permanently* deleted
// until 30 days *after* it gets moved to the trash.
threads[i].moveToTrash();
}
threadsProcessedCount += threadsToDeleteCount;
threadsProcessedPerLabel[label] = (threadsProcessedPerLabel[label] || 0) + threadsToDeleteCount;
});
// Log the deleted email count per label
console.log('Deleted emails from labels:');
for (let label in threadsProcessedPerLabel) {
console.log(' ' + label + ': ' + threadsProcessedPerLabel[label]);
}
// Schedule another run if we hit the processing limit.
// NB: Since we're only loading up to MAX_THREADS_TO_PROCESS_PER_BATCH at a time, we don't
// know if there are more threads to process in the next execution. So, if we reach the limit,
// there are *probably* more threads to process. It's possible there's no more left but it's not
// a big deal to just run it again.
if (threadsProcessedCount >= MAX_THREADS_TO_PROCESS_PER_BATCH) {
// TODO - when would threadsProcessedCount be above MAX_THREADS_TO_PROCESS?
console.log("Reached processing limit, scheduling next batch...");
ScriptApp.newTrigger(DELAYED_BATCH_TRIGGER_NAME)
.timeBased()
.after(MINUTES_TO_WAIT_BETWEEN_BATCHES * 60 * 1000)
.create();
} else {
console.log('Did not reach batch limit, no follow-up needed.');
}
}
function _clearAllDelayedBatchTriggers() {
// cancel all delayed jobs
const existingTriggers = ScriptApp.getProjectTriggers();
console.log('Checking for triggers other than the daily trigger...');
let foundDelayedBatchTrigger = false;
for (let i = 0; i < existingTriggers.length; i++) {
const trigger = existingTriggers[i];
if (trigger.getHandlerFunction() === DELAYED_BATCH_TRIGGER_NAME) {
ScriptApp.deleteTrigger(trigger);
console.log('Deleted delayed batch trigger: ' + trigger.getUniqueId());
foundDelayedBatchTrigger = true;
}
}
if (!foundDelayedBatchTrigger) {
console.log('No delayed batch triggers found.')
}
}
function dailyProcessTrigger() {
// run this daily to initiate the auto delete process
console.log('START run daily deletion process for Gmail threads...');
_processNextBatch();
console.log('FINISH run daily deletion process.');
}
function delayedBatchTrigger() {
// run this to start the process again if we didn't finish the first time
console.log('START process delayed batch');
_processNextBatch();
console.log('FINISH process delayed batch');
}
function Install() {
console.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();
console.log('Installation complete.');
}
function Uninstall() {
console.log('Uninstalling all triggers...');
const triggers = ScriptApp.getProjectTriggers();
for (let i = 0; i < triggers.length; i++) {
ScriptApp.deleteTrigger(triggers[i]);
}
console.log('All triggers have been removed.');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment