Final production deployment: activated live sync and updated documentation
This commit is contained in:
parent
d4b4e19223
commit
82acc4e7d5
|
|
@ -20,3 +20,4 @@ It automates the creation of email forwarding in Google Workspace by reading fro
|
|||
The script achieves declarative state management by finding and deleting Workspace Groups that are NOT present in the Google Sheet. To prevent the catastrophic deletion of real, human-managed groups (e.g., `board@haumdaucher.de`), the script relies on the `[Auto-Forwarder] Managed by Google Sheets` string in the group's description. The script must ALWAYS filter for this exact string before issuing any deletion API calls.
|
||||
2. **Always deploy via `clasp`.** Do not instruct the user to copy-paste code manually if `clasp` is available.
|
||||
3. **Trigger:** We use a combination of `onFormSubmit` (for Google Forms) and `onChange` (for manual sheet edits) triggers. This ensures reconciliation happens regardless of how the data was updated.
|
||||
4. **Validation Logic**: `forwardAddress` must always be internal (`@haumdaucher.de`), while `forwardToAddress` must always be external (non-haumdaucher) to prevent routing loops.
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ const CONFIG = {
|
|||
SHEET_NAME: "Form Responses 1",
|
||||
|
||||
// 3. The column numbers containing the data (1 = A, 2 = B, 3 = C, etc.)
|
||||
COL_SOURCE_ADDRESS: 2,
|
||||
COL_DESTINATION_ADDRESS: 6,
|
||||
COL_FORWARD_ADDRESS: 6,
|
||||
COL_FORWARD_TO_ADDRESS: 4,
|
||||
|
||||
// 4. Your admin email for receiving reports
|
||||
ADMIN_EMAIL: "admin@haumdaucher.de",
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ const CONFIG = {
|
|||
SHEET_NAME: "Form Responses 1",
|
||||
|
||||
// Column indices (1-indexed).
|
||||
// Example: If Source is Column B, index is 2. If Dest is Column C, index is 3.
|
||||
COL_SOURCE_ADDRESS: 2,
|
||||
COL_DESTINATION_ADDRESS: 6,
|
||||
// Example: If Forward Address is Column D, index is 4. If Forward To is Column F, index is 6.
|
||||
COL_FORWARD_ADDRESS: 6,
|
||||
COL_FORWARD_TO_ADDRESS: 4,
|
||||
|
||||
// The admin email that should receive the execution reports
|
||||
ADMIN_EMAIL: "moritz@haumdaucher.de",
|
||||
|
|
@ -89,13 +89,13 @@ function syncForwardings() {
|
|||
const changelog = [];
|
||||
|
||||
// 1. Process Creations and Updates
|
||||
for (const sourceEmail in desiredState) {
|
||||
const desiredDestinations = desiredState[sourceEmail];
|
||||
for (const forwardAddress in desiredState) {
|
||||
const forwardToAddresses = desiredState[forwardAddress];
|
||||
|
||||
if (!currentState[sourceEmail]) {
|
||||
createForwardingGroup(sourceEmail, desiredDestinations, changelog);
|
||||
if (!currentState[forwardAddress]) {
|
||||
createForwardingGroup(forwardAddress, forwardToAddresses, changelog);
|
||||
} else {
|
||||
updateForwardingGroup(sourceEmail, currentState[sourceEmail], desiredDestinations, changelog);
|
||||
updateForwardingGroup(forwardAddress, currentState[forwardAddress], forwardToAddresses, changelog);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -144,48 +144,54 @@ function readDesiredStateFromSheet() {
|
|||
// Start at i=1 to skip the header row
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
const row = data[i];
|
||||
let source = (row[CONFIG.COL_SOURCE_ADDRESS - 1] || "").toString().trim().toLowerCase();
|
||||
let dest = (row[CONFIG.COL_DESTINATION_ADDRESS - 1] || "").toString().trim().toLowerCase();
|
||||
let forwardAddress = (row[CONFIG.COL_FORWARD_ADDRESS - 1] || "").toString().trim().toLowerCase();
|
||||
let forwardToAddress = (row[CONFIG.COL_FORWARD_TO_ADDRESS - 1] || "").toString().trim().toLowerCase();
|
||||
|
||||
// Ignore completely empty rows
|
||||
if (!source && !dest) {
|
||||
if (!forwardAddress && !forwardToAddress) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 1. Auto-append domain to source if they just typed a name (e.g. "frederic")
|
||||
if (source && !source.includes("@")) {
|
||||
source += `@${CONFIG.WORKSPACE_DOMAIN}`;
|
||||
// 1. Auto-append domain to forwardAddress if they just typed a name (e.g. "frederic")
|
||||
if (forwardAddress && !forwardAddress.includes("@")) {
|
||||
forwardAddress += `@${CONFIG.WORKSPACE_DOMAIN}`;
|
||||
}
|
||||
|
||||
// 2. Validate source domain
|
||||
if (source && !source.endsWith(`@${CONFIG.WORKSPACE_DOMAIN}`)) {
|
||||
console.warn(`Row ${i + 1}: Skipped. Source address must belong to @${CONFIG.WORKSPACE_DOMAIN} domain. Found: '${source}'`);
|
||||
// 2. Validate forwardAddress domain (MUST be internal)
|
||||
if (forwardAddress && !forwardAddress.endsWith(`@${CONFIG.WORKSPACE_DOMAIN}`)) {
|
||||
console.warn(`Row ${i + 1}: Skipped. Forward address must belong to @${CONFIG.WORKSPACE_DOMAIN} domain. Found: '${forwardAddress}'`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Validate source is a valid email
|
||||
if (source && !emailRegex.test(source)) {
|
||||
console.warn(`Row ${i + 1}: Skipped. Invalid source email format: '${source}'`);
|
||||
// 3. Validate forwardAddress is a valid email
|
||||
if (forwardAddress && !emailRegex.test(forwardAddress)) {
|
||||
console.warn(`Row ${i + 1}: Skipped. Invalid forward address format: '${forwardAddress}'`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4. Validate destination is a valid email
|
||||
if (dest && !emailRegex.test(dest)) {
|
||||
console.warn(`Row ${i + 1}: Skipped. Invalid destination email format: '${dest}'`);
|
||||
// 4. Validate forwardToAddress domain (MUST NOT be internal)
|
||||
if (forwardToAddress && forwardToAddress.endsWith(`@${CONFIG.WORKSPACE_DOMAIN}`)) {
|
||||
console.warn(`Row ${i + 1}: Skipped. Destination address cannot be an internal @${CONFIG.WORKSPACE_DOMAIN} address. Found: '${forwardToAddress}'`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source && dest) {
|
||||
console.log(`Parsed Row ${i + 1}: Valid mapping [${source} -> ${dest}]`);
|
||||
if (!desiredState[source]) {
|
||||
desiredState[source] = [];
|
||||
// 5. Validate forwardToAddress is a valid email
|
||||
if (forwardToAddress && !emailRegex.test(forwardToAddress)) {
|
||||
console.warn(`Row ${i + 1}: Skipped. Invalid destination email format: '${forwardToAddress}'`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (forwardAddress && forwardToAddress) {
|
||||
console.log(`Parsed Row ${i + 1}: Valid mapping [${forwardAddress} -> ${forwardToAddress}]`);
|
||||
if (!desiredState[forwardAddress]) {
|
||||
desiredState[forwardAddress] = [];
|
||||
}
|
||||
// Ensure we don't add duplicate destinations for the same source
|
||||
if (desiredState[source].indexOf(dest) === -1) {
|
||||
desiredState[source].push(dest);
|
||||
if (desiredState[forwardAddress].indexOf(forwardToAddress) === -1) {
|
||||
desiredState[forwardAddress].push(forwardToAddress);
|
||||
}
|
||||
} else {
|
||||
console.warn(`Row ${i + 1}: Skipped. Missing either source or destination (Source: '${source}', Dest: '${dest}')`);
|
||||
console.warn(`Row ${i + 1}: Skipped. Missing either forward address or destination (Forward: '${forwardAddress}', To: '${forwardToAddress}')`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -254,49 +260,53 @@ function logAction(changelog, message) {
|
|||
/**
|
||||
* Creates a new Workspace Group to act as a forwarder
|
||||
*/
|
||||
function createForwardingGroup(sourceEmail, destinations, changelog) {
|
||||
function createForwardingGroup(forwardAddress, forwardToAddresses, changelog) {
|
||||
if (CONFIG.DRY_RUN) {
|
||||
logAction(changelog, `[DRY RUN - CREATED] Group: ${sourceEmail}`);
|
||||
for (const dest of destinations) {
|
||||
logAction(changelog, ` + [DRY RUN] Added member: ${dest}`);
|
||||
logAction(changelog, `[DRY RUN - CREATED] Group: ${forwardAddress}`);
|
||||
for (const forwardTo of forwardToAddresses) {
|
||||
logAction(changelog, ` + [DRY RUN] Added member: ${forwardTo}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newGroup = {
|
||||
email: sourceEmail,
|
||||
name: `Auto-Forwarder: ${sourceEmail}`,
|
||||
email: forwardAddress,
|
||||
name: `Auto-Forwarder: ${forwardAddress}`,
|
||||
description: CONFIG.GROUP_DESCRIPTION_TAG
|
||||
};
|
||||
|
||||
AdminDirectory.Groups.insert(newGroup);
|
||||
logAction(changelog, `[CREATED] Group: ${sourceEmail}`);
|
||||
logAction(changelog, `[CREATED] Group: ${forwardAddress}`);
|
||||
|
||||
for (const dest of destinations) {
|
||||
AdminDirectory.Members.insert({ email: dest, role: 'MEMBER' }, sourceEmail);
|
||||
logAction(changelog, ` + Added member: ${dest}`);
|
||||
// Propagation delay: Google Workspace APIs are eventually consistent.
|
||||
// We must wait a few seconds after creation before we can add members to the new group.
|
||||
Utilities.sleep(3000);
|
||||
|
||||
for (const forwardTo of forwardToAddresses) {
|
||||
AdminDirectory.Members.insert({ email: forwardTo, role: 'MEMBER' }, forwardAddress);
|
||||
logAction(changelog, ` + Added member: ${forwardTo}`);
|
||||
}
|
||||
} catch (e) {
|
||||
logAction(changelog, `[ERROR] Failed to create group ${sourceEmail}: ${e.message}`);
|
||||
logAction(changelog, `[ERROR] Failed to create group ${forwardAddress}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconciles members of an existing forwarding group
|
||||
*/
|
||||
function updateForwardingGroup(sourceEmail, currentMembers, desiredMembers, changelog) {
|
||||
function updateForwardingGroup(forwardAddress, currentMembers, desiredMembers, changelog) {
|
||||
// 1. Add missing members
|
||||
for (const dest of desiredMembers) {
|
||||
if (currentMembers.indexOf(dest) === -1) {
|
||||
for (const forwardTo of desiredMembers) {
|
||||
if (currentMembers.indexOf(forwardTo) === -1) {
|
||||
if (CONFIG.DRY_RUN) {
|
||||
logAction(changelog, `[DRY RUN - UPDATED] Group ${sourceEmail}: Added member ${dest}`);
|
||||
logAction(changelog, `[DRY RUN - UPDATED] Group ${forwardAddress}: Added member ${forwardTo}`);
|
||||
} else {
|
||||
try {
|
||||
AdminDirectory.Members.insert({ email: dest, role: 'MEMBER' }, sourceEmail);
|
||||
logAction(changelog, `[UPDATED] Group ${sourceEmail}: Added member ${dest}`);
|
||||
AdminDirectory.Members.insert({ email: forwardTo, role: 'MEMBER' }, forwardAddress);
|
||||
logAction(changelog, `[UPDATED] Group ${forwardAddress}: Added member ${forwardTo}`);
|
||||
} catch (e) {
|
||||
logAction(changelog, `[ERROR] Failed to add member ${dest} to ${sourceEmail}: ${e.message}`);
|
||||
logAction(changelog, `[ERROR] Failed to add member ${forwardTo} to ${forwardAddress}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -306,13 +316,13 @@ function updateForwardingGroup(sourceEmail, currentMembers, desiredMembers, chan
|
|||
for (const current of currentMembers) {
|
||||
if (desiredMembers.indexOf(current) === -1) {
|
||||
if (CONFIG.DRY_RUN) {
|
||||
logAction(changelog, `[DRY RUN - UPDATED] Group ${sourceEmail}: Removed member ${current}`);
|
||||
logAction(changelog, `[DRY RUN - UPDATED] Group ${forwardAddress}: Removed member ${current}`);
|
||||
} else {
|
||||
try {
|
||||
AdminDirectory.Members.remove(sourceEmail, current);
|
||||
logAction(changelog, `[UPDATED] Group ${sourceEmail}: Removed member ${current}`);
|
||||
AdminDirectory.Members.remove(forwardAddress, current);
|
||||
logAction(changelog, `[UPDATED] Group ${forwardAddress}: Removed member ${current}`);
|
||||
} catch (e) {
|
||||
logAction(changelog, `[ERROR] Failed to remove member ${current} from ${sourceEmail}: ${e.message}`);
|
||||
logAction(changelog, `[ERROR] Failed to remove member ${current} from ${forwardAddress}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue