Skip to content

Instantly share code, notes, and snippets.

@macabreb0b
Last active January 17, 2024 10:12
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/663fbd7124ca0185b75bb0ace3a82fd5 to your computer and use it in GitHub Desktop.
Save macabreb0b/663fbd7124ca0185b75bb0ace3a82fd5 to your computer and use it in GitHub Desktop.
email eater - google apps script (with dynamic label support)
// 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