TeamDynamix Admins: Script for Purging Email From a TDX Google Mailbox

Environment

TeamDynamix Google Mailboxes

Issue

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.

Resolution

Use this link to check usage while logged into the mailbox: https://drive.google.com/drive/quota

Apply a new script for a label in the TDX Google mailbox to automatically purge emails.

  1. Log into the mailbox using an incognito browser.
  2. In a new tab go to https://script.google.com.
  3. Click +New Project.
  4. Click on the project name and rename to preferred name (i.e. Purge System Loops).
  5. Go to 'Editor' in the left navigation bar. Delete the provided code, rows 1-3.
  6. Copy sample script into source page. (script below.)
  7. Update LABELS_TO_DELETE to the label you want to run the script. Use the front-end label name. (i.e. System Loop)
  8. Update DELETE_AFTER_DAYS to the number of days after email creation to delete.
  9. Save Project.
  10. In the toolbar, select Install (if not already defaulted) > Click Run.  Google will ask you to grant required permissions. Click Review Permissions.
  11. A pop up window will display > Select both checkboxes.
  12. A new pop up window will display > Click on Allow.
  13. It may take a few minutes for the script to kick off. You can view the Trigger and Execution logs using the icons in the left navigation.
  14. The emails will be moved to the 'Trash' label which will automatically deleted in 30 days. You can manually empty the trash if you need to lower storage immediately.
  15. Create a new project for each label in the mailbox.

Modifying an existing script for a label in the TDX Google mailbox to automatically purge emails.

  1. Log into the mailbox using an incognito browser.
  2. In a new tab go to https://script.google.com.
  3. Click on the project to modify.
  4. Go to 'Triggers' on left navigation bar. Delete each existing trigger by clicking the ellipsis for each one on the far right. Click on Delete Trigger. Do it for all triggers listed.
  5. Go to 'Editor' in the left navigation bar. Replace the existing script with the sample script into source page. (script below.)
  6. Update LABELS_TO_DELETE to the label you want to run the script. Use the front-end label name. (i.e. System Loop)
  7. Update DELETE_AFTER_DAYS to the number of days after email creation to delete.
  8. Save Project.
  9. In the toolbar, select Install (if not already defaulted) > Click Run.
  10. It may take a few minutes for the script to kick off. You can view the Trigger and Execution logs using the icons in the left navigation.
  11. The emails will be moved to the 'Trash' label which will automatically deleted in 30 days. You can manually empty the trash if you need to lower storage immediately.

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: 

label:system-loops  before:2024/05/24

 

Script:

/**
 * 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);
    }
  });
}

Additional Information

Need additional information or assistance? Submit a request here ITS-TeamDynamix Support.

Please note that Google Apps Script is not an ITS supported service.

Print Article

Related Services / Offerings (1)

This service is a web-based platform that provides service management, asset management, and project management capabilities. TeamDynamix can support IT management and support functions and has capabilities that provide general customer support or management of other non-IT functions like facilities and human resources too.