Skip to content

Instantly share code, notes, and snippets.

@austinsonger
Last active December 13, 2024 19:55
Show Gist options
  • Save austinsonger/1c80e08204769f26fcfe7f51a099012e to your computer and use it in GitHub Desktop.
Save austinsonger/1c80e08204769f26fcfe7f51a099012e to your computer and use it in GitHub Desktop.
Google Apps Script code automate a user’s offboarding process within a company. It retains the user’s emails by creating a Google Group and assigning the offboarded user’s primary email address to the group. The group is then automatically assigned to the offboarded employee's manager. Requires: Google Admin SDK and Gmail API.

This Google App Script code is designed to automate the offboarding process of a user from a company's email system using Google Admin SDK and Gmail API.


The key tasks include:

  • Group Mailbox Setup: Establishing a mailbox with the address [email protected].
  • Email Migration: Transferring the employee's email messages into the Group Mailbox, which is the most time-intensive step.
  • Manager Access: Providing the manager with access to the Group Mailbox for legitimate business purposes, such as retrieving work-related emails, ongoing projects, or client communications.
  • Temporary Alias Creation: Adding a temporary alias ([email protected]) to the user’s primary email.
  • Primary Email Update: Changing the user’s primary email to the temporary alias and removing the old primary email from the user account.
  • Alias Assignment: Adding the old email address as an alias to the Group Mailbox. (5 minutes or Less)

Purpose

Manage the offboarding process by:

  • Creating a group email for the offboarded user to store emails.
  • Forwarding existing emails to the group in manageable chunks.
  • Automating alias switching for the user's email account.
  • Handling errors and sending notifications about the process's success or failure.

Key Components

Constants

Configuration for group emails, temporary emails, and thresholds for validation:

  • OFFBOARDED_GROUP_EMAIL: Email group for the offboarded user.
  • TEMP_EMAIL: Temporary alias email.
  • MANAGER_EMAIL: Manager's email for notifications.
  • SLEEP_TIME: Time delay to avoid rate limits during API calls.

Validation

  • CONFIG_VALIDATION: Contains rules like email format and rate-limiting thresholds.

Main Function

Input Validation

  • Ensures all email addresses have valid formats.
  • Checks if the SLEEP_TIME meets the minimum required to avoid rate-limiting.

Core Offboarding Steps

  1. Create a Group:
    • Adds the manager as a member.
  2. Forward Emails:
    • Transfers user emails to the group in manageable chunks.
  3. Switch Aliases:
    • Automates alias switching (primary and temporary).

Error Handling

  • Catches and logs critical errors.
  • Sends failure notifications.

Key Functions

createGroupAndAddMember

  • Creates a new Google Group for storing offboarded emails.
  • Adds the manager to the group as a member.

forwardEmailsInChunks

  • Fetches user emails in chunks using pagination and forwards them to the offboarded group.
  • Implements exponential backoff for error retries.
  • Handles execution time limits with resumable triggers.

batchForwardEmails

  • Sends a batch of emails to the group.

automateAliasSwitching

  • Automates switching a user's primary email address to a temporary alias and re-adding the old email as an alias.

sendCompletionEmail

  • Sends an email notification to the manager about the offboarding process status.

getOAuthService & getAdminService

  • Sets up OAuth2 services for authentication with Gmail API and Admin SDK.

validateGroupExists

  • Checks if the group email already exists.

retryApiCall

  • Implements retry logic with exponential backoff for API calls.

setResumeTrigger

  • Creates a trigger to resume the script if execution time exceeds Google Apps Script limits.

Error Handling and Logging

  • Logs each step's success or failure using Logger.
  • Uses retryApiCall for handling transient API failures.

API Requirements

Enabled APIs

  • Google Admin SDK
  • Gmail API

Scopes

  • Access to Gmail messages and Admin Directory services.
{
"timeZone": "America/Chicago",
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"oauthScopes": [
"https://www.googleapis.com/auth/admin.directory.group",
"https://www.googleapis.com/auth/admin.directory.group.member",
"https://www.googleapis.com/auth/admin.directory.user",
"https://www.googleapis.com/auth/gmail.modify",
"https://www.googleapis.com/auth/script.external_request",
"https://www.googleapis.com/auth/script.scriptapp"
],
"dependencies": {
"enabledAdvancedServices": [
{
"userSymbol": "AdminDirectory",
"serviceId": "admin",
"version": "directory_v1"
},
{
"userSymbol": "Gmail",
"serviceId": "gmail",
"version": "v1"
}
]
}
}
/**
* Script to manage offboarding and setup email configurations, including user suspension.
* Requires Google Admin SDK and Gmail API to be enabled.
*/
// Constants
const OFFBOARDED_GROUP_EMAIL = "[email protected]"; // Group email address
const TEMP_EMAIL = "[email protected]";
const MANAGER_EMAIL = "[email protected]"; // Manager's email for notifications
const USER_EMAIL = "[email protected]"; // Regular user email address to offboard
const MEMBER_EMAIL = null; // Set to null if MEMBER_EMAIL and MANAGER_EMAIL are the same, or provide a distinct value
const GROUP_NAME = "Employee Offboarded Group";
const GROUP_DESCRIPTION = "This mailbox stores emails of the offboarded user";
const SLEEP_TIME = 2000; // Delay to avoid rate limits (2 seconds)
const CONFIG_VALIDATION = {
EMAIL_REGEX: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
MIN_SLEEP_TIME: 1000,
MIN_QUOTA_THRESHOLD: 500
};
function main() {
try {
// Determine the email to use as MEMBER_EMAIL
const resolvedMemberEmail = MEMBER_EMAIL || MANAGER_EMAIL;
// Validate inputs
const emails = [OFFBOARDED_GROUP_EMAIL, TEMP_EMAIL, MANAGER_EMAIL, USER_EMAIL, resolvedMemberEmail];
if (!emails.every(email => CONFIG_VALIDATION.EMAIL_REGEX.test(email))) {
throw new Error('Invalid email format detected');
}
if (SLEEP_TIME < CONFIG_VALIDATION.MIN_SLEEP_TIME) {
throw new Error('Sleep time too low - risk of rate limiting');
}
// Execute main steps with error handling
createGroupAndAddMember(OFFBOARDED_GROUP_EMAIL, GROUP_NAME, GROUP_DESCRIPTION, resolvedMemberEmail);
forwardEmailsInChunks(USER_EMAIL, OFFBOARDED_GROUP_EMAIL);
automateAliasSwitching(USER_EMAIL, TEMP_EMAIL, OFFBOARDED_GROUP_EMAIL);
suspendUser(USER_EMAIL);
sendCompletionEmail(MANAGER_EMAIL, 'Offboarding Complete',
`Successfully offboarded and suspended ${USER_EMAIL}`);
} catch (error) {
Logger.log(`Critical error in offboarding process: ${error.message}`);
sendCompletionEmail(MANAGER_EMAIL, 'Offboarding Failed',
`Offboarding process for ${USER_EMAIL} failed: ${error.message}`);
throw error;
}
}
/**
* Suspends a user account after offboarding is complete.
*
* @param {string} userEmail - The email of the user to be suspended.
*/
function suspendUser(userEmail) {
const service = getAdminService();
if (!service.hasAccess()) {
Logger.log(`Authorization required. Visit: ${service.getAuthorizationUrl()}`);
return;
}
try {
const url = `https://admin.googleapis.com/admin/directory/v1/users/${userEmail}`;
UrlFetchApp.fetch(url, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${service.getAccessToken()}`,
'Content-Type': 'application/json',
},
payload: JSON.stringify({
suspended: true
}),
});
Logger.log(`User ${userEmail} has been successfully suspended.`);
} catch (error) {
Logger.log(`Failed to suspend user ${userEmail}: ${error.message}`);
throw new Error(`User suspension failed: ${error.message}`);
}
}
function createGroupAndAddMember(groupEmail, groupName, description, memberEmail) {
const service = getAdminService();
if (!service.hasAccess()) {
Logger.log(`Authorization required. Visit: ${service.getAuthorizationUrl()}`);
return;
}
// Create the Group
const groupPayload = {
email: groupEmail,
name: groupName,
description: description,
};
const createGroupUrl = 'https://admin.googleapis.com/admin/directory/v1/groups';
const createGroupResponse = UrlFetchApp.fetch(createGroupUrl, {
method: 'POST',
contentType: 'application/json',
headers: {
Authorization: `Bearer ${service.getAccessToken()}`,
},
payload: JSON.stringify(groupPayload),
});
const createdGroup = JSON.parse(createGroupResponse.getContentText());
Logger.log(`Group created successfully: ${createdGroup.email}`);
// Add a Member to the Group
const memberPayload = {
email: memberEmail,
role: 'MEMBER',
};
const addMemberUrl = `https://admin.googleapis.com/admin/directory/v1/groups/${groupEmail}/members`;
const addMemberResponse = UrlFetchApp.fetch(addMemberUrl, {
method: 'POST',
contentType: 'application/json',
headers: {
Authorization: `Bearer ${service.getAccessToken()}`,
},
payload: JSON.stringify(memberPayload),
});
const addedMember = JSON.parse(addMemberResponse.getContentText());
Logger.log(`Member added successfully: ${addedMember.email}`);
}
function forwardEmailsInChunks(userEmail, groupEmail) {
const service = getOAuthService(userEmail);
const url = `https://gmail.googleapis.com/gmail/v1/users/${userEmail}/messages`;
let nextPageToken = PropertiesService.getScriptProperties().getProperty("NEXT_PAGE_TOKEN");
let processedCount = parseInt(PropertiesService.getScriptProperties().getProperty("PROCESSED_COUNT") || "0");
do {
const paginatedUrl = nextPageToken ? `${url}?pageToken=${nextPageToken}` : url;
const response = UrlFetchApp.fetch(paginatedUrl, {
headers: {
Authorization: `Bearer ${service.getAccessToken()}`,
},
});
const result = JSON.parse(response.getContentText());
const messages = result.messages || [];
if (messages.length > 0) {
const messageIds = messages.map((msg) => msg.id);
batchForwardEmails(userEmail, groupEmail, messageIds);
processedCount += messageIds.length;
Logger.log(`Processed ${processedCount} emails so far.`);
}
nextPageToken = result.nextPageToken;
PropertiesService.getScriptProperties().setProperty("NEXT_PAGE_TOKEN", nextPageToken);
PropertiesService.getScriptProperties().setProperty("PROCESSED_COUNT", processedCount);
if (nextPageToken) Utilities.sleep(SLEEP_TIME);
} while (nextPageToken && ScriptApp.getRemainingDailyQuota() > CONFIG_VALIDATION.MIN_QUOTA_THRESHOLD);
if (nextPageToken) {
Logger.log("Execution time exceeded. Resuming...");
setResumeTrigger();
} else {
Logger.log("Email forwarding complete.");
PropertiesService.getScriptProperties().deleteAllProperties();
}
}
function batchForwardEmails(userEmail, groupEmail, messageIds) {
const service = getOAuthService(userEmail);
const batchSendUrl = `https://gmail.googleapis.com/gmail/v1/users/${groupEmail}/messages/send`;
messageIds.forEach((messageId) => {
try {
const messageUrl = `https://gmail.googleapis.com/gmail/v1/users/${userEmail}/messages/${messageId}`;
const messageResponse = UrlFetchApp.fetch(messageUrl, {
headers: {
Authorization: `Bearer ${service.getAccessToken()}`,
},
});
const rawEmail = JSON.parse(messageResponse.getContentText()).raw;
// Forward the email
UrlFetchApp.fetch(batchSendUrl, {
method: "POST",
contentType: "application/json",
headers: {
Authorization: `Bearer ${service.getAccessToken()}`,
},
payload: JSON.stringify({
raw: rawEmail
}),
});
} catch (error) {
Logger.log(`Error forwarding email ID ${messageId}: ${error.message}`);
}
});
Logger.log(`Batch forwarded ${messageIds.length} messages.`);
}
/**
* Automates the process of switching email aliases for a user.
*
* This function performs the following steps:
* 1. Adds a temporary alias to the current primary email.
* 2. Waits for 2 minutes to ensure the alias is added.
* 3. Changes the primary email to the temporary alias.
* 4. Waits for 2 minutes to ensure the primary email is switched.
* 5. Adds the old email as an alias to the new primary account.
*
* @param {string} oldEmail - The current primary email address of the user.
* @param {string} tempAlias - The temporary alias to be added to the current primary email.
* @param {string} newPrimary - The new primary email address to which the old email will be added as an alias.
*
* @throws Will log an error message if any step in the alias switching process fails.
*/
function automateAliasSwitching(oldEmail, tempAlias, newPrimary) {
const service = getAdminService();
if (!service.hasAccess()) {
Logger.log(`Authorization required. Visit: ${service.getAuthorizationUrl()}`);
return;
}
try {
// Step 1: Add a temporary alias to the current primary email
Logger.log(`Adding alias ${tempAlias} to ${oldEmail}`);
UrlFetchApp.fetch(`https://admin.googleapis.com/admin/directory/v1/users/${oldEmail}/aliases`, {
method: 'POST',
headers: {
Authorization: `Bearer ${service.getAccessToken()}`,
'Content-Type': 'application/json',
},
payload: JSON.stringify({
alias: tempAlias
}),
});
// Wait 2 minutes
Utilities.sleep(120000);
// Step 2: Change the primary email to the temporary alias
Logger.log(`Switching primary email from ${oldEmail} to ${tempAlias}`);
UrlFetchApp.fetch(`https://admin.googleapis.com/admin/directory/v1/users/${oldEmail}`, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${service.getAccessToken()}`,
'Content-Type': 'application/json',
},
payload: JSON.stringify({
primaryEmail: tempAlias
}),
});
// Wait 2 minutes
Utilities.sleep(120000);
// Step 3: Add the old email as an alias to the new primary account
Logger.log(`Adding alias ${oldEmail} to ${newPrimary}`);
UrlFetchApp.fetch(`https://admin.googleapis.com/admin/directory/v1/users/${newPrimary}/aliases`, {
method: 'POST',
headers: {
Authorization: `Bearer ${service.getAccessToken()}`,
'Content-Type': 'application/json',
},
payload: JSON.stringify({
alias: oldEmail
}),
});
Logger.log('Alias switching process completed successfully!');
} catch (error) {
Logger.log(`Error during alias switching: ${error.message}`);
throw new Error(`Failed to switch aliases: ${error.message}`);
}
}
function setResumeTrigger() {
ScriptApp.newTrigger("forwardEmailsInChunks")
.timeBased()
.after(1000) // Execute after 1 second
.create();
}
function getOAuthService(userEmail) {
return OAuth2.createService(`Gmail-${userEmail}`)
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
.setClientId('YOUR_CLIENT_ID')
.setClientSecret('YOUR_CLIENT_SECRET')
.setCallbackFunction('authCallback')
.setPropertyStore(PropertiesService.getScriptProperties())
.setScope('https://www.googleapis.com/auth/gmail.modify');
}
function getAdminService() {
const key = PropertiesService.getScriptProperties().getProperty('SERVICE_ACCOUNT_KEY');
if (!key) {
throw new Error('SERVICE_ACCOUNT_KEY is missing in Script Properties.');
}
const serviceAccountKey = JSON.parse(Utilities.newBlob(Utilities.base64Decode(key)).getDataAsString());
return OAuth2.createService('AdminSDK')
.setTokenUrl('https://oauth2.googleapis.com/token')
.setPrivateKey(serviceAccountKey.private_key)
.setClientId(serviceAccountKey.client_id)
.setIssuer(serviceAccountKey.client_email)
.setScope([
'https://www.googleapis.com/auth/admin.directory.group',
'https://www.googleapis.com/auth/admin.directory.group.member',
'https://www.googleapis.com/auth/admin.directory.user'
].join(' '))
.setPropertyStore(PropertiesService.getScriptProperties());
}
function sendCompletionEmail(recipient, subject, message) {
GmailApp.sendEmail(recipient, subject, message);
}
function validateGroupExists(groupEmail) {
try {
const service = getAdminService();
const url = `https://admin.googleapis.com/admin/directory/v1/groups/${groupEmail}`;
const response = UrlFetchApp.fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${service.getAccessToken()}`,
},
});
Logger.log(`Group ${groupEmail} exists.`);
return true;
} catch (error) {
Logger.log(`Group ${groupEmail} does not exist: ${error.message}`);
return false;
}
}
function retryApiCall(apiFunction, retries = 3, delay = 5000) {
for (let attempt = 0; attempt < retries; attempt++) {
try {
return apiFunction();
} catch (error) {
if (attempt === retries - 1) throw error;
Utilities.sleep(delay * Math.pow(2, attempt)); // Exponential backoff
Logger.log(`Retry attempt ${attempt + 1} after error: ${error.message}`);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment