Skip to content

Instantly share code, notes, and snippets.

@adamberenzweig
Last active April 28, 2022 13:21
Show Gist options
  • Save adamberenzweig/0248b77ef480be5944c941d6676295e2 to your computer and use it in GitHub Desktop.
Save adamberenzweig/0248b77ef480be5944c941d6676295e2 to your computer and use it in GitHub Desktop.
Gmail Bulk Delete and Unsubscribe Script
/*
Gmail bulk delete
Based on:
https://gist.github.com/benbjurstrom/00cdfdb24e39c59c124e812d5effa39a
https://github.com/labnol/unsubscribe-gmail
1. Export gmail Promotions folder via Google takeout: https://takeout.google.com/
2. grep '^From:' Category\ Promotions-001.mbox | cut -d'<' -f2 | tr -d '>' | sort | uniq -c | sort -rn > senders.txt
3. Create a Google Sheets spreadsheet and import senders.txt.
4. Attach this script to the spreadsheet via Extensions > Apps Script.
** Scroll through it and delete any rows that you don't want to delete! Or any that look badly formatted.
5. Set rangeSpec, see below.
6. Run launchProcess to install the daily trigger, or run deleteFromSenders for a one-off.
*/
// *** Set rangeSpec to a string specifying the range, e.g. "B91:D295" ***
//
// - The first column contains the email address of the sender to delete
// - The range should have three columns. The script writes to the other two as follows:
// - the second column will be marked with 'x' for completed rows, and '*' for in-progress.
// - the third column will be marked with 'x' for senders that were also successfully unsubscribed from.
//
// Ideally this would be passed as an arg to deleteFromSenders and run from the spreadsheet, but then we can't
// write back to arbitrary cells.
// https://support.google.com/docs/thread/112098787/error-you-do-not-have-permission-to-call-setvalue?hl=en
var rangeSpec = "B690:D875";
var SHOULD_ATTEMPT_UNSUBSCRIBE = true;
// Maximum number of message threads to process per run.
var PAGE_SIZE = 300
const scriptStartTime = Date.now();
const MAX_EXECUTION_TIME = 1000 * 60 * 4; // 6 minutes, but give a 2 minute buffer.
const isTimeLeft = () => {
return MAX_EXECUTION_TIME > Date.now() - scriptStartTime;
};
function deleteFromSenders() {
var mySS = SpreadsheetApp.getActiveSpreadsheet();
var range = mySS.getRange(rangeSpec);
removeTimeoutTriggers();
for (i=0; i < range.getNumRows(); i+=1) {
var doneCell = range.getCell(i+1, 2);
if (doneCell.getValue() != "x") {
var sender = range.getCell(i+1, 1).getValue();
console.log("Doing " + sender);
doneCell.setValue("*"); // Mark in progress
if (!isTimeLeft()) {
setTimeoutTrigger();
return;
}
const [finishedThisSender, unsubscribedFromSender] = deleteThreadsFrom(sender);
if (unsubscribedFromSender) {
var unsubscribeStatusCell = range.getCell(i+1, 3);
unsubscribeStatusCell.setValue("x");
}
if (finishedThisSender) {
doneCell.setValue("x");
} else {
setTimeoutTrigger();
return;
}
}
}
console.log("All done!");
}
// Target of daily trigger.
// To keep separate from removeTimeoutTriggers().
function launchProcess() {
deleteFromSenders();
}
/**
* Create a trigger that executes the purge function every day.
* Execute this function to install the script.
*/
function setDailyTrigger() {
ScriptApp
.newTrigger('launchProcess')
.timeBased()
.everyDays(1)
.create()
}
/**
* Deletes all of the project's triggers
* Execute this function to unintstall the script.
*/
function removeAllTriggers() {
var triggers = ScriptApp.getProjectTriggers()
for (var i = 0; i < triggers.length; i++) {
ScriptApp.deleteTrigger(triggers[i])
}
}
function setTimeoutTrigger(){
var secondsFromNow = 60
console.log('Setting a trigger to run again in ' + secondsFromNow + " seconds")
ScriptApp.newTrigger('deleteFromSenders')
.timeBased()
.at(new Date((new Date()).getTime() + 1000 * secondsFromNow))
.create()
}
function removeTimeoutTriggers(){
var triggers = ScriptApp.getProjectTriggers()
for (var i = 0; i < triggers.length; i++) {
var trigger = triggers[i]
if(trigger.getHandlerFunction() === 'deleteFromSenders'){
ScriptApp.deleteTrigger(trigger)
}
}
}
function deleteThreadsFrom(senderString) {
if (senderString === "") {
console.log("Empty sender, ignoring");
return;
}
var search = 'from:' + senderString;
console.log('Deleting with query: ' + search);
var threads = GmailApp.search(search, 0, PAGE_SIZE)
var doneWithSender = (threads.length < PAGE_SIZE);
var unsubscribedFromSender = false;
if (SHOULD_ATTEMPT_UNSUBSCRIBE && threads.length > 0) {
unsubscribedFromSender = unsubscribeFromSender(senderString, threads[0]);
}
console.log('Deleting ' + threads.length + ' threads...')
// Delete in batches
// https://gist.github.com/gene1wood/0f455239490e5342fa49
BATCH_SIZE = 100
for (j = 0; j < threads.length; j+=BATCH_SIZE) {
GmailApp.moveThreadsToTrash(threads.slice(j, j+BATCH_SIZE));
}
return [doneWithSender, unsubscribedFromSender];
}
function unsubscribeFromSender(sender, thread) {
var unsubscribed = false;
var message = thread.getMessages()[0];
var value = message.getRawContent()
.match(/^List-Unsubscribe: ((.|\r\n\s)+)\r\n/m);
if (!value) { return unsubscribed; }
value = value[1];
if (value) {
console.log("value: " + value); // FIM
var url = value.match(/<(https?:\/\/[^>]+)>/);
if (url) {
url = url[1];
if (url) {
try {
var status = UrlFetchApp.fetch(url).getResponseCode();
console.log("Unsubscribe from " + sender + ": " + status + " " + url);
unsubscribed = true;
} catch (error) {
console.log("Failed to unsubscribe: " + error);
}
}
} else {
// TODO(madadam): Handle mailto:(...)?subject=... etc.
var mailto = value.match(/<mailto:([^>]+)>/);
if (mailto) {
mailto = mailto[1];
try {
var draft = GmailApp.createDraft(mailto, "unsubscribe", "This message was automatically generated by Gmail.");
draft.send();
console.log("Unsubscribe via mail to from " + sender);
unsubscribed = true;
} catch (error) {
console.log("Failed to send unsubscribe email: " + error);
}
}
}
}
return unsubscribed;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment