Skip to content

Instantly share code, notes, and snippets.

@gene1wood
Last active March 10, 2024 17:53
Star You must be signed in to star a gist
Save gene1wood/0f455239490e5342fa49 to your computer and use it in GitHub Desktop.
A Google Apps Script script to bulk delete large amounts of email in Gmail while avoiding the error #793 which Gmail encounters normally
/*
This script, when used with Google Apps Scripts, will delete 400 emails and
can be triggered to run every few minutes without user interaction enabling you
to bulk delete email in Gmail without getting the #793 error from Gmail.
Google returns a maximum of 500 email threads in a single API call.
This script fetches 400 threads in case 500 threads is causing timeouts
Configure the search query in the code below to match the type of emails
you want to delete
See - https://developers.google.com/apps-script/reference/gmail/gmail-app#search(String)
and https://support.google.com/mail/answer/7190
Browse to https://script.google.com/
Start a script and paste in the code below.
After you paste it in, save it.
Now you need to set the authorized scopes for the script ( https://developers.google.com/apps-script/concepts/scopes )
On the left side of the screen, hover over the gear symbol and then click on Project Settings.
Enable 'Show "appsscript.json" manifest file in editor'
On the left side of the screen, click on the < > to return to the code editor
You should see a new file appear, appscript.json. Click on it.
Edit the file to add an oauthScopes entry for mail.google.com. It should look something like this.
{
"oauthScopes": [
"https://mail.google.com/",
"https://www.googleapis.com/auth/gmail.readonly",
"https://www.googleapis.com/auth/gmail.modify"
],
"timeZone": "America/New_York",
"dependencies": {
"enabledAdvancedServices": [
{
"userSymbol": "Gmail",
"version": "v1",
"serviceId": "gmail"
}
]
},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8"
}
In the drop down at the top select the function you want
to run. For example, you could run the batchDeleteEmail function.
This gist contains a few different functions to give examples of how to do other actions
besides deleting emails. For example if you wanted to mark all mail with the "work" label as read
you could run the markReadLabelWork function
Next click the little clock looking button.
This is for your triggers. You can set up how frequently you want the script
to run (I did mine for every minute but others are seeing execution take longer than
a minute in which case you may want to run every 5 or 15 minutes).
This writeup from @timur-tabi goes into more detail : https://docs.google.com/document/d/1PLfAnNus-B87gHS1pkbmzFTkWckAPNcqmvO7hFo_gBc/edit
Source : # https://productforums.google.com/d/msg/gmail/YeQVDuPIQzA/kpZPDDj8TXkJ
This gist includes additions by @kulemantu found in their fork : https://gist.github.com/kulemantu/84682cfebe72eb925cfe/revisions
*/
function batchDeleteEmail() {
processEmail('label:inbox from:user@example.com', 'moveThreadsToTrash');
}
function markReadLabelWork() {
processEmail('label:work', 'markThreadsRead');
}
function markReadFromInfoExample() {
processEmail('from:user@example.com', 'markThreadsRead');
}
function processEmail(search, batchAction) {
var batchSize = 100; // Process up to 100 threads at once
var searchSize = 400; // Limit search result to a max of 400 threads. Use this if you encounter the "Exceeded maximum execution time" error
var threads = GmailApp.search(search, 0, searchSize);
for (j = 0; j < threads.length; j += batchSize) {
GmailApp[batchAction](threads.slice(j, j + batchSize));
}
}
@avramovic
Copy link

@fancywriter Disable your popup blocker and allow the script to access your email, it worked for me and works flawlessly, except I had to put it to run every 5 minutes cause GMail was timing out when set to run every minute. Each time it's executed it runs for about 2-3 minutes and deletes up to 500 threads, so it should be done by the morning (in my case)

@timur-tabi
Copy link

timur-tabi commented Jan 4, 2021

When I try to implement this script, I was prompted to log into my Gmail account. At that point, I got this pop-up message:

This app is blocked

This app tried to access sensitive info in your Google Account. To keep your account safe, Google blocked this access.

How do I get around this?

Update: I wrote up instructions to get around this issue: https://docs.google.com/document/d/1PLfAnNus-B87gHS1pkbmzFTkWckAPNcqmvO7hFo_gBc/

@sharan779
Copy link

This is great. thanks much!

@DestinyChan281
Copy link

hello, im trying to figure this out
i wanna delete emails from my social inbox but im having trouble figuring out how to set up this code

@gene1wood
Copy link
Author

@DestinyChan281 Follow the instructions in the comments of the code above and share what step you're getting stuck at and what problem you're encountering.

@DestinyChan281
Copy link

DestinyChan281 commented Feb 24, 2021

nevermin, I think I figured it out
though i did get this error message alot
"Exception: Service invoked too many times for one day: gmail.
batchDeleteEmail @ Code.gs:19"

@gene1wood
Copy link
Author

Service invoked too many times for one day

Sounds like Google is ratelimiting calls to the moveThreadsToTrash endpoint (or the search endpoint)

@DestinyChan281
Copy link

oh okay.
I guess I’ll just keep coming back to the script each day to reset the rate limit

@newboydj169
Copy link

really a nice feature, thank you so much.

@Kilarisowjanya
Copy link

I want to delete mails from Social Label and done the steps mentioned in the comments exactly but It's not deleting my mails but the code is running successfully. I can see the triggers are happening at every 1 minute interval.
Kindly help me if anyone have any idea on it.
Thank you

@acechase
Copy link

acechase commented Aug 2, 2021

This is a great script! Thank you so much!!

Playing around a bit, I'm seeing timeouts occurring pretty regularly by the third or fourth batch. I'm also seeing each batch take a full minute to run so I pulled back the trigger interval to be once every 5 minutes rather than every 1 minute. I imagine it could cause odd behavior to have the same script running in parallel multiple times.

For anyone who's just cut & pasting and wants to see logging info, here's my full script with debug output:

function batchDeleteEmail() {
  var batchSize = 100 // Process up to 100 threads at once
  var threads = GmailApp.search('category:promotions older_than:3m');
  console.log('batch size is: ' + threads.length);
  for (j = 0; j < threads.length; j+=batchSize) {
    console.log('removing batch: ' + j);
    GmailApp.moveThreadsToTrash(threads.slice(j, j+batchSize));
  }
}

You can see the before after on how long each script run was taking when they were running in parallel:
Screen Shot 2021-08-02 at 9 08 35 AM

Since it's now finishing much more quickly, rather than increasingly the trigger interval back to 1m, the ideal scenario would be to increase the query size coming back from GmailApp.search. Unfortunately, Google caps that at 500 results per query.

@acechase
Copy link

acechase commented Aug 5, 2021

Screen Shot 2021-08-05 at 8 51 47 AM

LOL. Come on, Google! I've deleted about 70k of 160k promotion emails so far. So I'm on track to clear out that folder over the course of ~7 days.

@asifiqbal
Copy link

asifiqbal commented Aug 8, 2021 via email

@timur-tabi
Copy link

timur-tabi commented Aug 8, 2021

Try this version. It limits to 400 threads:

function batchDeleteEmail() 
{
  // moveThreadsToTrash() only allows up to 100 threads at once
  var batchSize = 100

  // returns no more than 500 results, but that takes longer than
  // six minutes, so return less.  We don't want timeouts.
  var threads = GmailApp.search('label:inbox from:user@example.com', 0, 400);
  
  for (j = 0; j < threads.length; j += batchSize) {
    GmailApp.moveThreadsToTrash(threads.slice(j, j + batchSize));
  }
}

I suggest running this script not more often than once per hour.

@asifiqbal
Copy link

Try this version. It limits to 400 threads:

function batchDeleteEmail() 
{
  // moveThreadsToTrash() only allows up to 100 threads at once
  var batchSize = 100

  // returns no more than 500 results, but that takes longer than
  // six minutes, so return less.  We don't want timeouts.
  var threads = GmailApp.search('label:inbox from:user@example.com', 0, 400);
  
  for (j = 0; j < threads.length; j += batchSize) {
    GmailApp.moveThreadsToTrash(threads.slice(j, j + batchSize));
  }
}

I suggest running this script not more often than once per hour.

That worked perfect. Now I just setup a trigger to run hourly. Does it start at the beginning of the hour? There is nothing under Last run yet
Screen Shot 2021-08-08 at 6 09 49 PM

@asifiqbal
Copy link

Try this version. It limits to 400 threads:

function batchDeleteEmail() 
{
  // moveThreadsToTrash() only allows up to 100 threads at once
  var batchSize = 100

  // returns no more than 500 results, but that takes longer than
  // six minutes, so return less.  We don't want timeouts.
  var threads = GmailApp.search('label:inbox from:user@example.com', 0, 400);
  
  for (j = 0; j < threads.length; j += batchSize) {
    GmailApp.moveThreadsToTrash(threads.slice(j, j + batchSize));
  }
}

I suggest running this script not more often than once per hour.

That worked perfect. Now I just setup a trigger to run hourly. Does it start at the beginning of the hour? There is nothing under Last run yet
Screen Shot 2021-08-08 at 6 09 49 PM

I changed it to run every 15 mins and I do see it running. I will change it back to run it every hour. Thanks!

@gene1wood
Copy link
Author

gene1wood commented Aug 8, 2021

@timur-tabi Thank you, I've added your suggestion to the gist above and linked to your Google Doc.

@asifiqbal
Copy link

asifiqbal commented Aug 19, 2021

So the script has been working fine for most times (trigger is setup for one per hr) with searchSize = 400. However I still get the Exceeded maximum execution time once in a while and sometimes says Exception: Gmail operation not allowed. Out of 24 a day (one/hr) I get 3 failed with gmail operation not allowed and 3 timedout with execeeded max execution time. 75% success rate (yikes). Google GMAIL api is pretty unstable seems like.

@ginoliang1130
Copy link

Thank you. This script is very helpful to me. I release a new version to everyone. By the way, I added Mandarin in the comments. Hope it helps others.

function batchDeleteEmail() {
  var batchSize = 100 // Process up to 100 threads at once
  // 如果需要時間可以加上 [older_than:00m、00d、00h],[category:] 可換成 [label:]
  // If you need to add time conditions, you can use d(day) and h(hours) instead.
  // [category:] can be replace [label:].
  var threads = GmailApp.search('category:forums');
  console.log('batch size is: ' + threads.length);
  for (j = 0; j < threads.length; j+=batchSize) {
    console.log('removing batch: ' + j);
    GmailApp.moveThreadsToTrash(threads.slice(j, j+batchSize));
  }
}

@aalemayhu
Copy link

aalemayhu commented Nov 6, 2021

Thank you so much! For anyone else wanting to delete patches, this is my

function batchDeleteEmail() {
  var batchSize = 100 // Process up to 100 threads at once
  var searchSize = 400 // Limit search result to a max of 400 threads. Use this if you encounter the "Exceeded maximum execution time" error
  
  var threads = GmailApp.search('subject: PATCH', 0, searchSize);
  for (j = 0; j < threads.length; j+=batchSize) {
    GmailApp.moveThreadsToTrash(threads.slice(j, j+batchSize));
  }
}

@birajblg1987
Copy link

birajblg1987 commented Apr 24, 2022

How to add multiple email addresses to the list?

@Hectorhammett
Copy link

I have an issue and I can't debug it. Is it me or the docs are a little bit stingy? Anyways, I cannot run the script as it says
Exception: Gmail Operation not allowed
On the line using GmailApp.search. Oddly enough, I left it running for something like an hour and it was working but then it stopped working. It sounds like a limit hit? but I am unsure as I am getting a Gmail operation not allowed and not something like limit error or something else. Any possible ideas?

Thank you!

@gene1wood
Copy link
Author

How to add multiple email addresses to the list?

@birajblg1987 You can customize the GMail search terms on line 27 in any way you wish, including adding additional email addresses.

@gene1wood
Copy link
Author

@Hectorhammett Indeed, @asifiqbal also mentions above encountering that limit.

@RenegadeMaster
Copy link

This script changed my life

@TheCosmonaut
Copy link

My script appears to be running but I don't see the overall count going down. Is there any code I can add which would allow me to log whether the threads were successfully removed?

@ethaniel
Copy link

ethaniel commented Nov 17, 2022

Thank you for an amazing script. However, in my case, I have around 200k messages that I need to delete. Here is how I altered the code to keep it running continuously:

function processEmail(search, batchAction) {
  var batchSize = 100; // Process up to 100 threads at once
  var searchSize = 500; // Limit search result to a max of 500 threads. (google doesn't allow more)
  do {
    var threads = GmailApp.search(search, 0, searchSize);
    console.log('processing ' + threads.length + ' threads');
    for (j = 0; j < threads.length; j += batchSize) {
      GmailApp[batchAction](threads.slice(j, j + batchSize));
    }
  } while (threads.length>0) // keep script running until there are no threads left
}

@Jose-Bustamante
Copy link

For the people like me that have thousands of emails to delete and don't have a fancy business account, if you use @ethaniel script you will get Exceeded maximum execution time after 6 minutes, a way to get over it is setting a trigger 🕐 to execute every 5 minutes calling the function 😄 Hope that helps!

@ethaniel
Copy link

For the people like me that have thousands of emails to delete and don't have a fancy business account, if you use @ethaniel script you will get Exceeded maximum execution time after 6 minutes, a way to get over it is setting a trigger 🕐 to execute every 5 minutes calling the function 😄 Hope that helps!

And don’t forget to “deploy” the script, so the trigger would actually work!

@jmjeanjm
Copy link

hi! i'm getting this message when i run the program. i may not be getting the proper authorization scope. please advise. thanks

Error
Exception: The script does not have permission to perform that action. Required permissions: (https://www.googleapis.com/auth/gmail.metadata || https://www.googleapis.com/auth/gmail.readonly || https://www.googleapis.com/auth/gmail.modify || https://mail.google.com/)
batchDeleteEmail @ BatchDeleteEmails.gs:3

@timur-tabi
Copy link

hi! i'm getting this message when i run the program. i may not be getting the proper authorization scope. please advise. thanks

Error Exception: The script does not have permission to perform that action. Required permissions: (https://www.googleapis.com/auth/gmail.metadata || https://www.googleapis.com/auth/gmail.readonly || https://www.googleapis.com/auth/gmail.modify || https://mail.google.com/) batchDeleteEmail @ BatchDeleteEmails.gs:3

https://gist.github.com/gene1wood/0f455239490e5342fa49?permalink_comment_id=3581939#gistcomment-3581939

@jmjeanjm
Copy link

it looks like i'm missing the authorization to log onto my gmail account. which i think should be built into the code.
no one has experienced that error message?
Error Exception: The script does not have permission to perform that action. Required permissions: (https://www.googleapis.com/auth/gmail.metadata || https://www.googleapis.com/auth/gmail.readonly || https://www.googleapis.com/auth/gmail.modify || https://mail.google.com/) batchDeleteEmail @ BatchDeleteEmails.gs:3

@LucipherVenus
Copy link

@jmjeanjm @timur-tabi have you guys solved this problem?

@gene1wood
Copy link
Author

@jmjeanjm @LucipherVenus I've updated the comment section of the code to include the information @timur-tabi linked above

Try that and let me know if it resolves the issue for you.

@celestialeye
Copy link

I was getting the following error:

Exception: Access denied: : Metadata scope does not support 'q' parameter.
    at processEmail(Code:70:26)
    at batchDeleteEmail(Code:55:3)

But was able to fix after removing the following from oauthScopes:

"https://www.googleapis.com/auth/gmail.metadata",

@gene1wood
Copy link
Author

@celestialeye Thanks, I'll update the instructions to remove that scope.

@ajk99
Copy link

ajk99 commented Mar 29, 2023

How nice it is to see the emails from that massive folder slowly but surely disappearing. Thanks for sharing.

@ankhaa-g
Copy link

Thank you! Deleted 1.6M emails in one night.

@MadonnaMat
Copy link

This is fantastic! Thank you!

@mdklapwijk
Copy link

mdklapwijk commented Oct 4, 2023

@Hectorhammett, @asifiqbal, what I noticed is that this worked/works perfectly:

function batchDeleteEmail() {
  processEmail('in:inbox after:2018/01/01 before:2022/09/01', 'moveThreadsToTrash');
}

function processEmail(search, batchAction) {
  var batchSize = 100; // Process up to 100 threads at once
  var searchSize = 500; // Limit search result to a max of 400 threads. Use this if you encounter the "Exceeded maximum execution time" error
  
  console.log('starting with search size: ' + searchSize);
  var threads = GmailApp.search(search, 0, searchSize);
  for (j = 0; j < threads.length; j += batchSize) {
    console.log('removing batch: ' + j + '-' + Number(j + batchSize));
    GmailApp[batchAction](threads.slice(j, j + batchSize));
  }
  console.log('done with search size: ' + threads.length);
}
9:03:13 AM	Notice	Execution started
9:03:15 AM	Info	starting with search size: 500
9:03:15 AM	Info	done with search size: 0
9:03:14 AM	Notice	Execution completed

But this fails:

function batchDeleteEmail() {
  processEmail('in:inbox after:2022/01/01 before:2022/12/01', 'moveThreadsToTrash');
}

function processEmail(search, batchAction) {
  var batchSize = 100; // Process up to 100 threads at once
  var searchSize = 500; // Limit search result to a max of 400 threads. Use this if you encounter the "Exceeded maximum execution time" error
  
  console.log('starting with search size: ' + searchSize);
  var threads = GmailApp.search(search, 0, searchSize);
  for (j = 0; j < threads.length; j += batchSize) {
    console.log('removing batch: ' + j + '-' + Number(j + batchSize));
    GmailApp[batchAction](threads.slice(j, j + batchSize));
  }
  console.log('done with search size: ' + threads.length);
}
9:06:53 AM	Notice	Execution started
9:06:54 AM	Info	starting with search size: 500
9:07:15 AM	Error	
Exception: Gmail operation not allowed. 
processEmail	@ Code.gs:10
batchDeleteEmail	@ Code.gs:2

I ran it with 1 year at a time and it removed vast amounts of emails, but at 2022/2023 it borked, by reducing to before:2022-06-01 it worked again up until before:2022-10-01 which fails again.

Update some more results:
before:2022/09/15

9:20:25 AM	Notice	Execution started
9:20:27 AM	Info	starting with search size: 500
9:20:33 AM	Info	removing batch: 0-100
9:21:48 AM	Info	removing batch: 100-200
9:23:02 AM	Info	removing batch: 200-300
9:24:18 AM	Info	removing batch: 300-400
9:25:33 AM	Info	removing batch: 400-500
9:26:51 AM	Info	done with search size: 500
9:26:50 AM	Notice	Execution completed

before:2022/09/25

9:27:50 AM	Notice	Execution started
9:27:52 AM	Info	starting with search size: 500
9:27:54 AM	Info	removing batch: 0-100
9:29:10 AM	Info	removing batch: 100-200
9:30:25 AM	Info	removing batch: 200-300
9:31:20 AM	Info	removing batch: 300-400
9:31:47 AM	Info	removing batch: 400-500
9:32:12 AM	Info	done with search size: 500
9:32:11 AM	Notice	Execution completed

before:2022/09/28

9:35:23 AM	Notice	Execution started
9:35:25 AM	Info	starting with search size: 500
9:35:27 AM	Info	removing batch: 0-100
9:36:02 AM	Info	removing batch: 100-200
9:36:34 AM	Info	removing batch: 200-300
9:37:07 AM	Info	removing batch: 300-400
9:37:38 AM	Info	removing batch: 400-500
9:38:09 AM	Info	done with search size: 500
9:38:08 AM	Notice	Execution completed

before:2022/09/29:

9:38:57 AM	Notice	Execution started
9:38:58 AM	Info	starting with search size: 500
9:39:51 AM	Error	
Exception: Gmail operation not allowed. 
processEmail	@ Code.gs:10
batchDeleteEmail	@ Code.gs:2

Confirmed failure on before:2022/09/29 3 times more and then ran before:2022/09/28 again which was succesfull again.

Head	batchDeleteEmail	Editor	Oct 4, 2023, 9:41:44 AM	285.671 s	Completed
Head	batchDeleteEmail	Editor	Oct 4, 2023, 9:40:36 AM	23.158 s	Failed
Head	batchDeleteEmail	Editor	Oct 4, 2023, 9:40:04 AM	22.636 s	Failed
Head	batchDeleteEmail	Editor	Oct 4, 2023, 9:38:58 AM	54.405 s	Failed
Head	batchDeleteEmail	Editor	Oct 4, 2023, 9:35:24 AM	164.667 s	Completed
Head	batchDeleteEmail	Editor	Oct 4, 2023, 9:27:51 AM	261.174 s	Completed
Head	batchDeleteEmail	Editor	Oct 4, 2023, 9:20:26 AM	384.693 s	Completed

@bnh1
Copy link

bnh1 commented Dec 31, 2023

Thanks for this code!

I added a try/catch to keep running despite the very frequent "Gmail operation not allowed" exceptions, and some random delays to try to reduce those exceptions. This gets my triggered runs to execute for the full 1800 seconds allowed, and a new one is kicked off every half an hour. I have managed to move some 8,000,000 messages to the trash in a few days.

-ben

//source: https://gist.github.com/gene1wood/0f455239490e5342fa49

var email_search = 'label:<mylabel> from:an@email.address' 
function batchDeleteEmail() {
  try {
    processEmail(email_search, 'moveThreadsToTrash');
  }
  catch(error) {
    Logger.log(error)
    Logger.log('Re-trying...')
    batchDeleteEmail();
  }
}

function processEmail(search, batchAction) {
  var removed = 0;
  var batchSize = 100; // Process up to 100 threads at once
  var searchSize = 500; // Maximum search result size is 500

  var threads = GmailApp.search(search, 0, searchSize);
  var rando = 0;
  Logger.log('Found '+threads.length+' threads matching search: "'+search+'".')
  while (threads) {
    for (j = 0; j < threads.length; j += batchSize) {
      rando = Math.floor(Math.random() * 3000);
      Logger.log('Removing threads '+j+' to '+(j + batchSize)+'...');
      GmailApp[batchAction](threads.slice(j, j + batchSize));
      Logger.log('Sleeping for '+rando+' milliseconds.');
      Utilities.sleep(rando);
      removed += batchSize;
    }

    if ( ( removed % 2000 ) == 0 ) {
      rando = Math.floor(Math.random() * 40000);
      while ( rando < 20000 ) {
        rando = Math.floor(Math.random() * 40000);
      }
      Logger.log('Long sleeping for '+rando+' milliseconds.');
      Utilities.sleep(rando); // long sleep to try to keep api calls working...
    }

    threads = GmailApp.search(search, 0, searchSize);
    Logger.log('Total removed so far: '+removed+'.');
    Logger.log('Found '+threads.length+' more threads, removing...');
    
  }
}

@Sabapathy80
Copy link

Sabapathy80 commented Mar 6, 2024

I want to create a script to delete emails from a specific bank with a transaction amount of less than some number, once a day or a couple of times per week. Any suggestions will help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment