The new storage usage limit for shared accounts is set to 15 GB. Emails will need to be purged to the trash to avoid exceeding the storage limit.
The script should automatically run once an email date reaches the number of days entered into the script. It may take time for the script to run through all the emails for a label, especially if there are a large number of them. Check the number of emails of each label and ensure the number decreases.
Search the emails filtered on a specific date to ensure they have been deleted. Here’s an example filter:
/**
* Standalone Apps Script: delete Gmail messages/threads in specific label(s)
* older than N days. Runs periodically and (if needed) schedules one-off “resume” runs.
*
* Key behaviors:
* - Permanent scheduled trigger + one-off resume triggers for catch-up
* - Resume triggers self-delete (so they don't accumulate)
* - Lock prevents overlap
* - Best-effort operations: one bad/restricted thread/message will NOT fail the whole run
*/
const LABELS_TO_DELETE = ["Processed"];
const DELETE_AFTER_DAYS = 90;
const TIMEZONE = "America/New_York";
const PAGE_SIZE = 150;
const RESUME_FREQUENCY_MIN = 30;
const MAIN_FREQUENCY_HOURS = 5;
const DAILY_HANDLER = "dailyDeleteGmail";
const RESUME_HANDLER = "resumeDeleteGmail";
const MAX_RUNTIME_MS = 8 * 60 * 1000;
/** Run once to (re)create triggers. */
function Install() {
ScriptApp.getProjectTriggers().forEach(function (t) {
var fn = t.getHandlerFunction();
if (fn === DAILY_HANDLER || fn === RESUME_HANDLER) {
ScriptApp.deleteTrigger(t);
}
});
// One-time kickoff (~2 minutes from now)
ScriptApp.newTrigger(RESUME_HANDLER)
.timeBased()
.at(new Date(Date.now() + 2 * 60 * 1000))
.create();
// Main schedule
ScriptApp.newTrigger(DAILY_HANDLER)
.timeBased()
.everyHours(MAIN_FREQUENCY_HOURS)
.create();
console.log(
"Installed kickoff + main trigger: " +
RESUME_HANDLER +
" (once) and " +
DAILY_HANDLER +
" every " +
MAIN_FREQUENCY_HOURS +
" hour(s)"
);
}
/** Persistent breadcrumbs (survive even if execution logs don’t show). */
function recordRunMarker_(where, phase) {
PropertiesService.getScriptProperties().setProperty(
"LAST_RUN_MARKER",
JSON.stringify({ where: where, phase: phase, time: new Date().toISOString() })
);
}
function recordError_(where, err) {
PropertiesService.getScriptProperties().setProperty(
"LAST_ERROR",
JSON.stringify({
where: where,
message: err && err.message ? err.message : String(err),
stack: err && err.stack ? err.stack : "",
time: new Date().toISOString()
})
);
}
/** Optional: run manually to inspect last marker/error. */
function DebugStatus() {
var props = PropertiesService.getScriptProperties();
Logger.log("LAST_RUN_MARKER: " + props.getProperty("LAST_RUN_MARKER"));
Logger.log("LAST_ERROR: " + props.getProperty("LAST_ERROR"));
}
/** Best-effort Gmail helpers (prevents one bad item from failing the whole run). */
function safeMoveThreadToTrash_(thread) {
try {
thread.moveToTrash();
return true;
} catch (err) {
console.error(
"Could not trash thread. id=" +
thread.getId() +
" err=" +
(err && err.message ? err.message : err)
);
return false;
}
}
function safeMoveMessageToTrash_(msg) {
try {
msg.moveToTrash();
return true;
} catch (err) {
console.error(
"Could not trash message. id=" +
msg.getId() +
" err=" +
(err && err.message ? err.message : err)
);
return false;
}
}
/** NEW: wrap getMessagesForThread because it can throw "Gmail operation not allowed". */
function safeGetMessagesForThread_(thread) {
try {
return GmailApp.getMessagesForThread(thread);
} catch (err) {
console.error(
"Could not read messages for thread. id=" +
thread.getId() +
" err=" +
(err && err.message ? err.message : err)
);
return null; // caller will skip this thread
}
}
/** NEW: wrap getLastMessageDate because metadata calls can also throw on restricted threads. */
function safeGetLastMessageDate_(thread) {
try {
return thread.getLastMessageDate();
} catch (err) {
console.error(
"Could not read last message date for thread. id=" +
thread.getId() +
" err=" +
(err && err.message ? err.message : err)
);
return null; // caller will skip this thread
}
}
/** Main scheduled entrypoint. */
function dailyDeleteGmail(e) {
recordRunMarker_("dailyDeleteGmail", "start");
try {
runPurge_();
recordRunMarker_("dailyDeleteGmail", "end");
} catch (err) {
recordError_("dailyDeleteGmail", err);
console.error("dailyDeleteGmail failed: " + (err && err.message ? err.message : err));
throw err;
}
}
/** One-off resume entrypoint. */
function resumeDeleteGmail(e) {
recordRunMarker_("resumeDeleteGmail", "start");
try {
// Delete the trigger that fired FIRST
deleteFiringTrigger_(e);
runPurge_();
recordRunMarker_("resumeDeleteGmail", "end");
} catch (err) {
recordError_("resumeDeleteGmail", err);
console.error("resumeDeleteGmail failed: " + (err && err.message ? err.message : err));
throw err;
}
}
function runPurge_() {
var lock = LockService.getScriptLock();
if (!lock.tryLock(30000)) {
console.warn("Another run is in progress; exiting.");
return;
}
try {
var start = Date.now();
var age = new Date();
age.setDate(age.getDate() - DELETE_AFTER_DAYS);
var purgeDate = Utilities.formatDate(age, TIMEZONE, "yyyy-MM-dd");
var labelQuery = LABELS_TO_DELETE
.map(function (l) {
return 'label:"' + String(l).replace(/"/g, '\\"') + '"';
})
.join(" OR ");
var search = "(" + labelQuery + ") before:" + purgeDate;
console.log("PURGE DATE: " + purgeDate);
console.log("SEARCH: " + search);
var threads = GmailApp.search(search, 0, PAGE_SIZE);
console.log("Fetched " + threads.length + " threads (page size " + PAGE_SIZE + ").");
var processed = 0;
for (var i = 0; i < threads.length; i++) {
if (Date.now() - start > MAX_RUNTIME_MS) {
console.warn("Approaching runtime limit; stopping early at thread index " + i);
scheduleResumeIfNeeded_();
break;
}
var thread = threads[i];
// Use safe last-date getter so restricted threads don’t crash the run
var lastDate = safeGetLastMessageDate_(thread);
if (!lastDate) {
processed++;
continue;
}
// If whole thread is older, attempt to trash whole thread (best effort)
if (lastDate < age) {
safeMoveThreadToTrash_(thread);
processed++;
continue;
}
// Otherwise, trash only messages older than cutoff (best effort per message)
var messages = safeGetMessagesForThread_(thread);
if (!messages) {
processed++;
continue;
}
for (var j = 0; j < messages.length; j++) {
if (Date.now() - start > MAX_RUNTIME_MS) {
console.warn("Approaching runtime limit; stopping early mid-thread.");
scheduleResumeIfNeeded_();
return;
}
if (messages[j].getDate() < age) {
safeMoveMessageToTrash_(messages[j]);
}
}
processed++;
}
// If we pulled a full page, there may be more work
if (threads.length === PAGE_SIZE) {
scheduleResumeIfNeeded_();
}
console.log("Processed threads this run: " + processed);
} finally {
lock.releaseLock();
}
}
function scheduleResumeIfNeeded_() {
var alreadyPending = ScriptApp.getProjectTriggers().some(function (t) {
return t.getHandlerFunction() === RESUME_HANDLER;
});
if (alreadyPending) {
console.log("Resume trigger already pending; not scheduling another.");
return;
}
console.log("Scheduling resume run in " + RESUME_FREQUENCY_MIN + " minutes.");
ScriptApp.newTrigger(RESUME_HANDLER)
.timeBased()
.at(new Date(Date.now() + RESUME_FREQUENCY_MIN * 60 * 1000))
.create();
}
function deleteFiringTrigger_(e) {
if (!e || !e.triggerUid) return;
ScriptApp.getProjectTriggers().forEach(function (t) {
if (typeof t.getUniqueId === "function" && t.getUniqueId() === e.triggerUid) {
ScriptApp.deleteTrigger(t);
console.log("Deleted firing resume trigger: " + e.triggerUid);
}
});
}
Please note that Google Apps Script is not an ITS supported service.