Skip to content

Instantly share code, notes, and snippets.

@Linerre
Created March 19, 2020 01:44
Show Gist options
  • Save Linerre/0a5ea08086b957fcd0ec7c9914b40bee to your computer and use it in GitHub Desktop.
Save Linerre/0a5ea08086b957fcd0ec7c9914b40bee to your computer and use it in GitHub Desktop.
Some Google Apps scripts to keep Gmail snappy
// Labels required: Review/Discard, Review/Keep, Kept
// Gmail's importance markers should be turned on
// Star a message to signal it's important
// (NOTE: messages will be automatically unstarred after treatment, e.g., after being auto-removed from Review/Discard)
/* tidyInbox.gs (run every 15 minutes) */
function tidyInbox() {
// Apply label "Kept" to starred threads with label "Review/Keep"
label('Kept').addToThreads(find('is:starred label:Review/Keep'));
// Clear unwanted labels from starred threads
const REMOVE_IF_STARRED = ["Review/Discard", "Review/Keep"];
for (var l=0; l<REMOVE_IF_STARRED.length; l++) {
label(REMOVE_IF_STARRED[l]).removeFromThreads(find('is:starred label:' + REMOVE_IF_STARRED[l]));
}
// If a message has two labels, allow one to take priority and remove the other
const OVERRIDING = ["Kept"]
const OVERRIDDEN = ["Review/Keep"];
for (var l=0; l<OVERRIDDEN.length; l++) {
for (var m=0; m<OVERRIDING.length; m++) {
label(OVERRIDDEN[l]).removeFromThreads(find('label:' + OVERRIDDEN[l] + ' label:' + OVERRIDING[m]));
}
}
// Move any archived, trashed, or spammed threads with a star
// back to the inbox, make sure they're all marked important, and remove stars.
GmailApp.moveThreadsToInbox(find('is:starred (in:trash OR in:spam OR -label:inbox)'));
GmailApp.markThreadsImportant(find('is:starred -label:important'));
var starred = find('is:starred');
for (var t=0; t<starred.length; t++) {
GmailApp.unstarMessages(starred[t].getMessages());
}
// Archiving, trashing, or spam-marking should take precedence over marking important,
// so remove "important" markers from these threads.
GmailApp.markThreadsUnimportant(find('label:important (in:trash OR in:spam OR -label:inbox'));
// Archive all unimportant threads that have been read.
GmailApp.moveThreadsToArchive(find('label:read -label:important label:inbox'));
// Mark all archived threads as read.
GmailApp.markThreadsRead(find('label:unread -label:inbox'));
};
/* markLargeThreads.gs (run every 24 hours) */
function markLargeThreads() {
// GmailApp doesn't have a method for finding X threads with the greatest
// file size, so this is a binary search algorithm to do that.
function getXLargest(X, otherFilters) {
var minSize = 1;
var maxSize = 25000000; // Gmail's max attachment size is 25MB, but multiple
// large attachments may be present in one thread, so we raise the ceiling:
while (find(otherFilters + ' size:' + maxSize.toString()).length > 0) { maxSize *= 2; }
var midSize = Math.floor((minSize + maxSize)/2);
var threads = [];
while (threads.length != X && (maxSize>midSize && midSize>minSize)) {
// shouldLimit (below) is set to false: GmailApp's data limit of 100 threads
// doesn't apply to searches, only to changes, like adding a label or star
threads = find(otherFilters + ' size:' + midSize.toString(), shouldLimit=false);
if (threads.length > X) {
minSize = midSize;
midSize = Math.floor((minSize + maxSize)/2);
} else if (threads.length < X) {
maxSize = midSize;
midSize = Math.floor((minSize + maxSize)/2);
}
}
return threads;
}
// Get a bunch of large, old, unimportant emails and put them in one place.
// The batch will have twice as many threads as the number of new, UNimportant
// threads in the last 24 hours, max 100 (recommended to run once daily)
const REVIEW_DISCARD_LENGTH = find('newer_than:1d -label:inbox', batchLength=50).length * 2;
label("Review/Discard").addToThreads(getXLargest(REVIEW_DISCARD_LENGTH, '-label:inbox -in:trash'));
// Get a bunch of large, old, but marked-important emails and put them in one place.
// This batch will have twice as many threads as the number of new, important
// threads in the last 24 hours, max 100 (run daily)
const REVIEW_KEEP_LENGTH = find('newer_than:1d label:inbox', batchLength=50).length * 2;
label("Review/Keep").addToThreads(getXLargest(REVIEW_KEEP_LENGTH, 'label:inbox -label:Kept'));
}
/* findFrequentSenders.gs (run every 24 hours) */
function findFrequentSenders() {
function isTimeUp(start) {
var now = new Date();
return now.getTime() - start.getTime() > 300000; // 5 minutes
}
var filters = '-in:inbox';
var froms = Object.create(null);
var batch = [];
var BATCH_SIZE = 500;
var index = 0;
var threadsFromThisPerson = [];
var scannedThreads = Object.create(null);
var draftBody = "";
var start = new Date();
timedBlock:
do {
batch = GmailApp.search(filters, index * BATCH_SIZE, BATCH_SIZE);
for (var t=0; t<batch.length; t++) {
if (isTimeUp(start)) { break timedBlock; }
var thread;
while (t<batch.length && scannedThreads[batch[t].getId()]) {
Logger.log("Skipping this thread! End of batch? " + !batch[t] + ". t=" + t);
t += 1;
}
if (t>=batch.length) {
Logger.log("Out of batch. Fetching new batch...index=" + index);
Logger.log("Object.keys(scannedThreads).length = " + Object.keys(scannedThreads).length);
break;
}
thread = batch[t];
var messages = thread.getMessages();
for (var m=0; m<messages.length; m++) {
var message = messages[m];
var from = message.getFrom();
if (!(from in froms)) {
threadsFromThisPerson = GmailApp.search(filters + " from:" + from);
froms[from] = Object.keys(threadsFromThisPerson).length;
for (var u=0; u<threadsFromThisPerson.length; u++) {
scannedThreads[threadsFromThisPerson[u].getId()] = true;
}
}
}
}
index += 1;
Logger.log("Reached end of do-while loop.");
} while (batch.length == BATCH_SIZE);
Logger.log("Exited do-while loop.");
if (isTimeUp(start)) {
draftBody += "Aborted to avoid execution time limit. " + (Object.keys(scannedThreads).length) + " threads were examined with a batch size of " + BATCH_SIZE + ".\n\n";
}
var keys = Object.keys(froms);
var heap = new BinaryHeap(function ([key, value]) { return -value; });
var newElement = [];
for (var i=0; i<keys.length; i++) {
newElement = [keys[i], froms[keys[i]]];
heap.push(newElement);
}
var currentElement = [];
for (var k = 0; k<100; k++) {
currentElement = heap.pop();
if (currentElement) {
draftBody += "#" + (k+1) + ": " + currentElement[0] + ": " + currentElement[1] + "\n";
}
}
GmailApp.createDraft("[email protected]", "Your 100 most frequent senders from filter '" + filters + "'", draftBody).send();
}
/* find.gs */
function find(searchString, shouldLimit, batchLength) {
// The Gmail Service won't make changes to more than 100 threads
// at a time, so batchLength defaults to 100.
shouldLimit = (typeof shouldLimit !== 'undefined') ? shouldLimit : true;
batchLength = (typeof batchLength !== 'undefined') ? batchLength : 100;
if (shouldLimit) {
return GmailApp.search(searchString, 0, batchLength);
} else {
return GmailApp.search(searchString);
}
}
/* label.gs */
function label(name) {
// This only works for user-defined labels,
// not system labels like "Spam."
return GmailApp.getUserLabelByName(name);
}
/* BinaryHeap.gs (source: http://eloquentjavascript.net/1st_edition/appendix2.html) */
function BinaryHeap(scoreFunction){
this.content = [];
this.scoreFunction = scoreFunction;
}
BinaryHeap.prototype = {
push: function(element) {
// Add the new element to the end of the array.
this.content.push(element);
// Allow it to bubble up.
this.bubbleUp(this.content.length - 1);
},
pop: function() {
// Store the first element so we can return it later.
var result = this.content[0];
// Get the element at the end of the array.
var end = this.content.pop();
// If there are any elements left, put the end element at the
// start, and let it sink down.
if (this.content.length > 0) {
this.content[0] = end;
this.sinkDown(0);
}
return result;
},
remove: function(node) {
var length = this.content.length;
// To remove a value, we must search through the array to find
// it.
for (var i = 0; i < length; i++) {
if (this.content[i] != node) continue;
// When it is found, the process seen in 'pop' is repeated
// to fill up the hole.
var end = this.content.pop();
// If the element we popped was the one we needed to remove,
// we're done.
if (i == length - 1) break;
// Otherwise, we replace the removed element with the popped
// one, and allow it to float up or sink down as appropriate.
this.content[i] = end;
this.bubbleUp(i);
this.sinkDown(i);
break;
}
},
size: function() {
return this.content.length;
},
bubbleUp: function(n) {
// Fetch the element that has to be moved.
var element = this.content[n], score = this.scoreFunction(element);
// When at 0, an element can not go up any further.
while (n > 0) {
// Compute the parent element's index, and fetch it.
var parentN = Math.floor((n + 1) / 2) - 1,
parent = this.content[parentN];
// If the parent has a lesser score, things are in order and we
// are done.
if (score >= this.scoreFunction(parent))
break;
// Otherwise, swap the parent with the current element and
// continue.
this.content[parentN] = element;
this.content[n] = parent;
n = parentN;
}
},
sinkDown: function(n) {
// Look up the target element and its score.
var length = this.content.length,
element = this.content[n],
elemScore = this.scoreFunction(element);
while(true) {
// Compute the indices of the child elements.
var child2N = (n + 1) * 2, child1N = child2N - 1;
// This is used to store the new position of the element,
// if any.
var swap = null;
// If the first child exists (is inside the array)...
if (child1N < length) {
// Look it up and compute its score.
var child1 = this.content[child1N],
child1Score = this.scoreFunction(child1);
// If the score is less than our element's, we need to swap.
if (child1Score < elemScore)
swap = child1N;
}
// Do the same checks for the other child.
if (child2N < length) {
var child2 = this.content[child2N],
child2Score = this.scoreFunction(child2);
if (child2Score < (swap == null ? elemScore : child1Score))
swap = child2N;
}
// No need to swap further, we are done.
if (swap == null) break;
// Otherwise, swap and continue.
this.content[n] = this.content[swap];
this.content[swap] = element;
n = swap;
}
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment