Skip to content

Instantly share code, notes, and snippets.

@zoidy
Created March 2, 2025 18:52
Show Gist options
  • Save zoidy/aa10dba3ad0f7d596380b13be6974a34 to your computer and use it in GitHub Desktop.
Save zoidy/aa10dba3ad0f7d596380b13be6974a34 to your computer and use it in GitHub Desktop.
/*
Deletes attachments from selected emails in Gmail without deleting the email.
v1.0
https://gist.github.com/zoidy
1. Use any method to find the messages E.g., using GMail's search function:
has:attachment larger:2M older_than:2y
2. Add a label called "DelAttch" to those threads that have messages with attachments that you want deleted.
3. Create a new Apps Script in your Google Drive and paste this code, then click the Save button
4. Under Services on the left hand side of the Apps Script editor, add Gmail
5. Select the go() function from the Apps Script toolbar and press the Run button.
Note:
All messages within the threads that have the label "DelAttch" will have their attachments deleted. To only delete the attachments of certain messages within the thread, add a star to those messages and set ONLY_STARRED to true.
6. The messages that were successfully processed will appear under the label "DelAttchDone". The original message will also appear there unless DELETE_ORIGINAL = true.
*/
// label for threads that should have their attachments removed.
MSG_LABEL = "DelAttch";
MSG_LABEL_PROCESSED = MSG_LABEL + "Done";
// Only starred messages with the label MSG_LABEL will be processed.
// If false, ALL messages with attachments in threads labeled with MSG_LABEL will be processed.
ONLY_STARRED = false;
// Trashes the original message with attachments after processing.
DELETE_ORIGINAL = true;
/*************************************************************/
// Maximum number of email threads to process per run.
PAGE_SIZE = 175;
function go() {
Logger.log('Starting deleteAttachments for all messages with label ' + MSG_LABEL);
var proccessedLabel = GmailApp.getUserLabelByName(MSG_LABEL_PROCESSED);
if(!proccessedLabel) {
Logger.log('Label ' + MSG_LABEL_PROCESSED + ' not found. Creating');
proccessedLabel = GmailApp.createLabel(MSG_LABEL_PROCESSED);
}
var label = GmailApp.getUserLabelByName(MSG_LABEL);
var totalthreads = (label ? label.getThreads().length : 0);
var processthreads = GmailApp.search('label:' + MSG_LABEL.replace(' ', '-'), 0, PAGE_SIZE);
if(processthreads.length > 0){
Logger.log('Found ' + (totalthreads == 500 ? 'at least ' + totalthreads : totalthreads) +
' threads tagged with ' + MSG_LABEL + '. Scanning ' + Math.min(processthreads.length, PAGE_SIZE));
var msgs = GmailApp.getMessagesForThreads(processthreads);
var counter = 0;
var bytes_saved = 0;
try {
for (var i = 0 ; i < msgs.length; i++) {
for (var j = 0; j < msgs[i].length; j++) {
var attachments = msgs[i][j].getAttachments();
if(attachments.length > 0 && (ONLY_STARRED ? msgs[i][j].isStarred() : true)) {
Logger.log('Message "%s" contains these attachments:', msgs[i][j].getSubject());
for (let k = 0; k < attachments.length; k++)
Logger.log('"%s" (%s bytes)', attachments[k].getName(), attachments[k].getSize());
bytes_saved += removeAttachments(msgs[i][j]);
counter++;
msgs[i][j].getThread().addLabel(proccessedLabel);
msgs[i][j].getThread().removeLabel(label);
if(DELETE_ORIGINAL)
GmailApp.moveMessageToTrash(msgs[i][j]);
//uncomment for debugging but it slows things down
Logger.log('processed message: "' + msgs[i][j].getSubject() + '" to ' + msgs[i][j].getTo());
}
}
}
}
catch(e){
Logger.log('Error processing: ' + e);
}
Logger.log('Processed ' + counter + ' messages in ' + processthreads.length + ' threads');
Logger.log('Approximate total size of attachments removed: ' + bytes_saved / 1024 / 1024 + 'MB');
}
else
Logger.log('Found ' + processthreads.length + ' emails marked for processing. Exiting.');
}
var BOUNDARY_MARKER = 'boundary=';
//var BOUNDARY_REGEX = new RegExp(BOUNDARY_MARKER + '\"(.+)\"');
var BOUNDARY_REGEX = new RegExp(BOUNDARY_MARKER + '(.+)');
function filterEmail(email, level = -1){
level++;
var boundaryresult = BOUNDARY_REGEX.exec(email);
//if we found a boundary definition, check the section recursively for more boundaries until none are found.
//if no boundary definition was found, just return the text.
if(boundaryresult){
//split the section into the part before the boundary definition and the part after
var boundary_id = boundaryresult[1];
//var boundarydefinitionsplit = email.split(BOUNDARY_MARKER + '"' + boundary_id + '"');
var boundarydefinitionsplit = email.split(BOUNDARY_MARKER + boundary_id);
//now split the part after using the boundary calculated by the definition (the boundary line should not include any quotes in the boundary id)
var boundaryLine = '--' + boundary_id.replaceAll('"','');
var sections = boundarydefinitionsplit[1].split(boundaryLine);
//filter the sections to exclude ones with attachments
var filteredSections = sections.filter(function(value, index, arr) {
//Does "filename=" appear within the first 12 lines of the section?
//If yes, the section is likely an attachment. Checking only the first 12
//lines minimizes the likelyhood of accidentally removing a non attachment section if it
//happens to contain the text "filename=" somewhere.
//return value.indexOf('filename=') == -1;
var lines = value.split('\n', 12);
for(i=0; i<lines.length; i++){
if(lines[i].indexOf('filename=') >= 0 || lines[i].indexOf('filename*=') >= 0)
return false;
}
return true;
});
//check the filtered sections for more boundaries
for(i=0; i<filteredSections.length; i++)
filteredSections[i] = filterEmail(filteredSections[i], level);
//return boundarydefinitionsplit[0] + BOUNDARY_MARKER + '"' + boundary_id + '"' + filteredSections.join(boundaryLine);
return boundarydefinitionsplit[0] + BOUNDARY_MARKER + boundary_id + filteredSections.join(boundaryLine);
}else
return email;
}
function removeAttachments(emailmsg) {
/* Sources: https://stackoverflow.com/a/46591868 https://stackoverflow.com/a/58840269 */
var email = emailmsg.getRawContent();
var resultBody = filterEmail(email);
//Logger.log(resultBody);
var encodedResultBody = Utilities.base64EncodeWebSafe(resultBody);
var resource = {'raw': encodedResultBody, 'threadId': emailmsg.getThread().getId()};
resource['labelIds'] = [];
/* OPTIONAL */
//LABEL YOUR NEWLY CREATED MAIL AS 'INBOX' & 'UNREAD'
// resource['labelIds'] = ['INBOX', 'UNREAD'];
//add the processed label to the new message.
var labelId = Gmail.Users.Labels.list('me').labels.filter(function(e){return e.name == MSG_LABEL_PROCESSED})[0].id;
resource['labelIds'].push(labelId);
// Re-insert the email with the original date/time, into the original thread
var response = Gmail.Users.Messages.insert(resource, 'me', null, {'internalDateSource': 'dateHeader'});
return email.length - resultBody.length;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment