Last active
January 17, 2024 10:12
-
-
Save macabreb0b/663fbd7124ca0185b75bb0ace3a82fd5 to your computer and use it in GitHub Desktop.
email eater - google apps script (with dynamic label support)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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" | |
// runbook: | |
// - namespace labels like ee- | |
// - name labels like ee-archive-after-8d, ee-delete-after-45d | |
// - you can give a message more than one label (e.g., ee-archive-after-1d, ee-delete-after-5d) | |
// 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 AUTO_ARCHIVED_LABEL = GmailApp.createLabel("ee-auto-archived"); | |
function _getProcessBeforeDate(daysAgo) { | |
const today = new Date(); | |
const age = new Date(today.setDate(today.getDate() - daysAgo)); | |
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 _getLabelsToProcess() { | |
const allLabels = GmailApp.getUserLabels(); | |
return allLabels.filter(label => { | |
const labelName = label.getName(); | |
return labelName.startsWith('ee-') && | |
labelName !== 'ee-auto-archived'; /** TODO - find a better name for this so i don't have to filter it out */; | |
}); | |
} | |
function _processEmails() { | |
let threadsProcessedCount = 0; | |
const threadsProcessedPerLabel = {}; | |
_getLabelsToProcess().forEach(function(label) { | |
const labelName = label.getName(); | |
const [_, action, __, threshold] = labelName.split('-'); | |
const daysAgo = Number(threshold.replace('d', '')); | |
const processBeforeDate = _getProcessBeforeDate(daysAgo); | |
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 | |
} | |
let searchFilters = [ | |
'label:' + labelName, | |
'before:' + processBeforeDate, | |
]; | |
console.log('Searching for threads with label: ' + labelName); | |
const threads = GmailApp.search(searchFilters.join(' '), 0, threadBudgetLeftForThisBatch); | |
const threadsToProcessCount = threads.length; | |
console.log('Found ' + threadsToProcessCount + ' threads to process for label: ' + labelName); | |
for (let i = 0; i < threadsToProcessCount; i++) { | |
const thread = threads[i]; | |
if (action === 'delete') { | |
// 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. | |
console.log('deleting thread with subject: ' + threads[i].getFirstMessageSubject()); | |
thread.moveToTrash(); | |
} else if (action === 'archive') { | |
console.log('archiving thread with subject: ' + threads[i].getFirstMessageSubject()); | |
thread.moveToArchive() | |
.removeLabel(label) // remove current label to clean up the label query | |
.addLabel(AUTO_ARCHIVED_LABEL); // add this so it's clear this was not a user action | |
} | |
} | |
threadsProcessedCount += threadsToProcessCount; | |
threadsProcessedPerLabel[labelName] = (threadsProcessedPerLabel[labelName] || 0) + threadsToProcessCount; | |
}); | |
// Log the deleted email count per label | |
console.log('Processed emails from labels:'); | |
for (let labelName in threadsProcessedPerLabel) { | |
console.log(' ' + labelName + ': ' + threadsProcessedPerLabel[labelName]); | |
} | |
// 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('Removed 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 action process | |
console.log('START run daily batch for Gmail threads...'); | |
_processNextBatch(); | |
console.log('FINISH run daily batch.'); | |
} | |
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