Last active
January 30, 2026 14:53
-
-
Save akaak/beb8c48f37e7d7cd3488cf79b898b6e4 to your computer and use it in GitHub Desktop.
Gmail cleanup script
This file contains hidden or 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
| """ | |
| Gmail cleanup helper. | |
| Searches your Gmail account for messages matching `query` and batch-moves them to | |
| Trash via the Gmail API (requires `credentials.json`; creates/uses `token.json`). | |
| Last updated Jan 30, 2026 by @akaak to be used for cleaning up the gmailinbox. | |
| """ | |
| import os.path | |
| from google.auth.transport.requests import Request | |
| from google.oauth2.credentials import Credentials | |
| from google_auth_oauthlib.flow import InstalledAppFlow | |
| from googleapiclient.discovery import build | |
| SCOPES = ['https://www.googleapis.com/auth/gmail.modify'] | |
| def get_service(): | |
| creds = None | |
| if os.path.exists('token.json'): | |
| creds = Credentials.from_authorized_user_file('token.json', SCOPES) | |
| if not creds or not creds.valid: | |
| if creds and creds.expired and creds.refresh_token: | |
| creds.refresh(Request()) | |
| else: | |
| # To obtain 'credentials.json', visit Google Cloud Console (https://console.developers.google.com), | |
| # create a project, enable the Gmail API, and create OAuth 2.0 Client ID credentials for a Desktop app. | |
| # Download the resulting file as 'credentials.json' and place it in this directory. | |
| flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES) | |
| creds = flow.run_local_server(port=0) | |
| with open('token.json', 'w') as token: | |
| token.write(creds.to_json()) | |
| return build('gmail', 'v1', credentials=creds) | |
| def main(): | |
| service = get_service() | |
| # --- STEP 1: FIND ALL MESSAGES --- | |
| # TODO: Change this query to match what you want to delete | |
| # query = "label:unread older_than:30d" | |
| # Some useful example queries for reference (uncomment and modify as needed): | |
| # Search for unread messages older than 30 days: | |
| # query = "label:unread older_than:30d" | |
| # Search for messages with attachments from a specific sender: | |
| # query = "has:attachment from:[email protected]" | |
| # Search for all messages with attachments: | |
| # query = "has:attachment" | |
| # Search for messages from a particular domain: | |
| # query = "from:@acme.com" | |
| # Search for messages from a specific sender: | |
| # query = "from:[email protected]" | |
| # Search for messages with a keyword in the subject line: | |
| # query = "subject:'Quarterly Report'" | |
| query = "label: ACME older_than:7d" | |
| all_msg_ids = [] | |
| next_page_token = None | |
| print(f"Searching for messages matching: '{query}'...") | |
| while True: | |
| results = service.users().messages().list( | |
| userId='me', | |
| q=query, | |
| pageToken=next_page_token | |
| ).execute() | |
| messages = results.get('messages', []) | |
| if messages: | |
| all_msg_ids.extend([m['id'] for m in messages]) | |
| next_page_token = results.get('nextPageToken') | |
| print(f"Collected {len(all_msg_ids)} message IDs...") | |
| if not next_page_token: | |
| break | |
| if not all_msg_ids: | |
| print("No messages found.") | |
| return | |
| # --- STEP 2: BATCH DELETE (TRASH) IN CHUNKS OF 1000 --- | |
| print(f"\nStarting deletion of {len(all_msg_ids)} messages...") | |
| # We process messages in batches of up to 1000, as recommended by Google's best practices, | |
| # to efficiently modify or trash large numbers of messages while respecting API limits. | |
| for i in range(0, len(all_msg_ids), 1000): | |
| # Slice the list to get a chunk of 1000 | |
| chunk = all_msg_ids[i : i + 1000] | |
| batch_body = { | |
| 'ids': chunk, | |
| 'addLabelIds': ['TRASH'], | |
| 'removeLabelIds': ['INBOX', 'UNREAD'] | |
| } | |
| try: | |
| # Note: The "batchModify" call below moves the messages to Trash by applying the TRASH label. | |
| # This does NOT permanently delete the messages. For complete destruction (permanent deletion), | |
| # you could use "batchDelete" instead of "batchModify". | |
| service.users().messages().batchModify(userId='me', body=batch_body).execute() | |
| print(f"Successfully trashed messages {i} through {i + len(chunk)}...") | |
| except Exception as e: | |
| print(f"Error during batch delete: {e}") | |
| print("\nCleanup complete.") | |
| if __name__ == '__main__': | |
| main() |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The
credentials.jsonis downloaded from google console and it looks like this...In google console, the app is set up as 'local' app and does NOT require any verification from Google and can be use right away on local.