En tant qu’administrateur Google Workspace, la gestion des comptes utilisateurs peut rapidement devenir complexe, surtout dans les organisations de grande taille. Les doublons de comptes sont un problème courant qui peut entraîner des coûts inutiles, des problèmes de sécurité et une confusion opérationnelle. C’est pourquoi j’ai développé un script Google Apps Script pour identifier et notifier automatiquement les comptes Google Workspace potentiellement en double.
Pourquoi un tel script est-il nécessaire ?
Les doublons de comptes peuvent survenir pour diverses raisons : erreurs de saisie, réintégration d’employés, fusions d’entreprises, ou simplement un manque de processus rigoureux lors de la création des comptes. Ces doublons ont des conséquences directes :
- Coûts accrus : Chaque compte actif consomme une licence. Des doublons signifient des licences payées inutilement.
- Sécurité compromise : Des comptes orphelins ou mal gérés peuvent devenir des points d’entrée pour des accès non autorisés.
- Complexité de gestion : Il est plus difficile de maintenir une vue d’ensemble claire de votre annuaire d’utilisateurs.
- Problèmes de conformité : La non-maîtrise de l’inventaire des comptes peut poser des problèmes lors des audits.
Mon script vise à résoudre ces problèmes en offrant une solution proactive et automatisée.
Comment fonctionne le script ?
Le cœur de ce script réside dans sa capacité à identifier les doublons potentiels en se basant sur des critères intelligents et normalisés :
- Récupération exhaustive des utilisateurs : Le script interroge l’API Admin Directory pour récupérer tous les comptes utilisateurs actifs de votre domaine Google Workspace.
- Filtrage intelligent : Il ne considère que les comptes actifs, non archivés, et disposant des informations essentielles (prénom, nom, email primaire).
- Normalisation des données : C’est la clé de la détection précise. Le script normalise le prénom et le nom de famille (en ignorant les accents et la casse) ainsi que le préfixe de l’adresse e-mail primaire (en ignorant certains suffixes courants comme _int, _stg, _externe). Cette normalisation permet de détecter des doublons qui ne seraient pas évidents au premier coup d’œil (par exemple, « Jean Dupont » et « jéan dupont » avec des emails « jean.dupont » et « jean.dupont_int »).
- Enrichissement des données : Pour chaque compte, le script récupère des informations précieuses comme la fonction, le service et le centre de coûts, ce qui est crucial pour l’analyse et la prise de décision.
- Identification des groupes de doublons : Une fois les données normalisées, le script regroupe les comptes partageant les mêmes critères normalisés de prénom, nom et préfixe d’email.
- Notification automatisée : Si des doublons potentiels sont détectés, un email détaillé est envoyé à l’adresse de support technique configurée. Cet email, au format HTML soigné, présente un tableau récapitulatif des groupes de doublons, incluant les informations pertinentes de chaque compte (email, ID, unité organisationnelle, fonction, service, centre de coûts, date de création, dernier login).
- Rapports réguliers : Un déclencheur hebdomadaire est mis en place pour exécuter automatiquement le script, assurant une surveillance continue sans intervention manuelle. Si aucun doublon n’est trouvé, un email de confirmation « RAS » est envoyé pour valider la bonne exécution du script.
Les avantages pour votre organisation
- Gain de temps : Automatisation d’une tâche de surveillance fastidieuse.
- Réduction des coûts : Identification rapide des licences inutiles.
- Amélioration de la sécurité : Meilleure maîtrise de l’inventaire des comptes.
- Précision : Détection de doublons subtils grâce à la normalisation avancée.
- Visibilité : Des rapports clairs et détaillés pour faciliter l’analyse et la prise de décision.
- Proactivité : Une surveillance continue pour anticiper les problèmes.
Ce script est un outil puissant pour tout administrateur Google Workspace soucieux d’optimiser la gestion de son annuaire d’utilisateurs. Il représente une étape importante vers une administration plus efficace et plus sécurisée de votre environnement Google Workspace.
/**
* @OnlyCurrentDoc
* Developpeur: Fabrice Faucheux
* Description: Ce script identifie les comptes Google Workspace potentiellement en double.
* Un doublon est défini comme deux ou plusieurs comptes utilisateurs distincts dans la console d'administration
* partageant le même prénom (insensible aux accents et à la casse), nom de famille (insensible aux accents et à la casse),
* et le même identifiant d'email avant le symbole "@" (normalisé pour ignorer certains suffixes et insensible à la casse).
* Il récupère également la fonction, le service et le centre de coûts de chaque compte pour aider à l'analyse.
* Il ne considère pas les alias d'un même compte comme des doublons entre eux, mais compare les attributs des comptes
* utilisateurs distincts tels que récupérés depuis l'API Admin Directory.
* Une notification par email est envoyée avec la liste des doublons potentiels détectés.
* Date de derniere modification: 4 juin 2025
*/
// Configuration globale du script
const CONFIGURATION = {
EMAIL_SOUTIEN_TECHNIQUE: "support@@domaine.com",
NOM_EXPEDITEUR_EMAIL: "Google Workspace Alerts",
SUJET_EMAIL_ALERTE_DOUBLONS: "Alerte : Détection de comptes Google Workspace potentiellement en double",
SUJET_EMAIL_ERREUR_SCRIPT: "Erreur Script : Détection Doublons Workspace",
SUJET_EMAIL_AUCUN_DOUBLON: "RAS : Vérification des doublons de comptes Workspace",
ID_CLIENT_GOOGLE: 'xxxxxx',
PROJECTION_UTILISATEUR: 'full',
MAX_RESULTATS_PAR_PAGE: 500,
HEURE_DECLENCHEUR: 8,
SUFFIXES_PREFIXE_EMAIL_A_IGNORER: ["_int", "_stg", "_externe"] // Liste des suffixes à ignorer dans le préfixe email pour la comparaison
};
const ANNEE_ACTUELLE = new Date().getFullYear();
/**
* @function detecterEtNotifierDoublonsComptes
* @summary Fonction principale qui récupère les utilisateurs, identifie les doublons potentiels
* selon les critères normalisés (prénom, nom, préfixe de l'email primaire),
* récupère les informations organisationnelles et envoie un email de notification.
* @description La détection se base sur des comptes utilisateurs distincts.
*/
function detecterEtNotifierDoublonsComptes() {
try {
let jetonPageSuivante;
let tousLesUtilisateursBruts = [];
Logger.log("Début de la récupération des utilisateurs...");
do {
const reponsePage = AdminDirectory.Users.list({
customer: CONFIGURATION.ID_CLIENT_GOOGLE,
maxResults: CONFIGURATION.MAX_RESULTATS_PAR_PAGE,
pageToken: jetonPageSuivante,
projection: CONFIGURATION.PROJECTION_UTILISATEUR,
orderBy: 'email'
});
if (reponsePage.users && reponsePage.users.length > 0) {
tousLesUtilisateursBruts = tousLesUtilisateursBruts.concat(reponsePage.users);
}
jetonPageSuivante = reponsePage.nextPageToken;
} while (jetonPageSuivante);
Logger.log(`Nombre total d'utilisateurs bruts récupérés: ${tousLesUtilisateursBruts.length}`);
if (tousLesUtilisateursBruts.length === 0) {
Logger.log('Aucun utilisateur trouvé dans le domaine.');
return;
}
const utilisateursFiltres = tousLesUtilisateursBruts.filter(utilisateur => {
const estValide = !utilisateur.suspended &&
!utilisateur.archived &&
utilisateur.name &&
utilisateur.name.givenName &&
utilisateur.name.familyName &&
utilisateur.primaryEmail;
return estValide;
});
Logger.log(`Nombre d'utilisateurs après filtrage (actifs, infos complètes): ${utilisateursFiltres.length}`);
if (utilisateursFiltres.length === 0) {
Logger.log('Aucun utilisateur pertinent trouvé après filtrage.');
return;
}
const carteDoublonsPotentiels = new Map();
utilisateursFiltres.forEach(utilisateur => {
const prenomNormalisePourCle = (utilisateur.name.givenName || '')
.toLowerCase()
.trim()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, "");
const nomFamilleNormalisePourCle = (utilisateur.name.familyName || '')
.toLowerCase()
.trim()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, "");
const emailPrimaireUtilisateur = utilisateur.primaryEmail;
const indexArobase = emailPrimaireUtilisateur.indexOf('@');
if (indexArobase === -1) {
Logger.log(`Format d'email primaire invalide pour l'utilisateur ${emailPrimaireUtilisateur} (ID: ${utilisateur.id}), ignoré.`);
return;
}
let prefixeEmailPourCle = emailPrimaireUtilisateur.substring(0, indexArobase).toLowerCase().trim();
// Normalisation du préfixe email en ignorant les suffixes définis
for (const suffixe of CONFIGURATION.SUFFIXES_PREFIXE_EMAIL_A_IGNORER) {
if (prefixeEmailPourCle.endsWith(suffixe)) {
prefixeEmailPourCle = prefixeEmailPourCle.substring(0, prefixeEmailPourCle.length - suffixe.length);
// Optionnel: nettoyer les caractères comme '.' ou '_' qui pourraient rester à la fin après suppression
// prefixeEmailPourCle = prefixeEmailPourCle.replace(/[._]$/, "");
break; // On suppose qu'un seul suffixe pertinent de la liste sera à la fin
}
}
const cleDoublon = `${prenomNormalisePourCle}#${nomFamilleNormalisePourCle}#${prefixeEmailPourCle}`;
let fonction = 'N/A';
let service = 'N/A';
let centreDeCouts = 'N/A';
if (utilisateur.organizations && utilisateur.organizations.length > 0) {
let organisationPrincipale = utilisateur.organizations.find(org => org.primary === true);
if (!organisationPrincipale) {
organisationPrincipale = utilisateur.organizations[0];
}
if (organisationPrincipale) {
fonction = organisationPrincipale.title || 'N/A';
service = organisationPrincipale.department || 'N/A';
centreDeCouts = organisationPrincipale.costCenter || 'N/A';
}
}
if (!carteDoublonsPotentiels.has(cleDoublon)) {
carteDoublonsPotentiels.set(cleDoublon, {
prenomOriginal: utilisateur.name.givenName,
nomFamilleOriginal: utilisateur.name.familyName,
prefixeEmailCommunAffiche: prefixeEmailPourCle, // Utiliser le préfixe normalisé commun pour l'affichage du groupe
comptes: []
});
}
carteDoublonsPotentiels.get(cleDoublon).comptes.push({
email: emailPrimaireUtilisateur, // Email original complet du compte
idUtilisateur: utilisateur.id,
dateCreation: utilisateur.creationTime ? new Date(utilisateur.creationTime).toLocaleDateString('fr-FR') : 'N/A',
dernierLogin: utilisateur.lastLoginTime ? new Date(utilisateur.lastLoginTime).toLocaleDateString('fr-FR') : 'N/A',
orgUnitPath: utilisateur.orgUnitPath || 'N/A',
fonction: fonction,
service: service,
centreDeCouts: centreDeCouts
});
});
const doublonsReels = [];
carteDoublonsPotentiels.forEach(groupe => {
if (groupe.comptes.length > 1) {
doublonsReels.push(groupe);
}
});
if (doublonsReels.length === 0) {
Logger.log('Aucun compte potentiellement en double trouvé après analyse.');
const corpsEmailRas = `<p>Bonjour,</p><p>La vérification quotidienne des comptes Google Workspace n'a détecté aucun nouveau doublon potentiel aujourd'hui, basé sur les critères établis (même prénom/nom (normalisés sans accents), et préfixe de l'email primaire (normalisé sans certains suffixes)).</p><p>Ce message confirme que le script s'est exécuté correctement.</p><p>Cordialement,<br>${CONFIGURATION.NOM_EXPEDITEUR_EMAIL}</p>`;
MailApp.sendEmail({
to: CONFIGURATION.EMAIL_SOUTIEN_TECHNIQUE,
subject: CONFIGURATION.SUJET_EMAIL_AUCUN_DOUBLON,
name: CONFIGURATION.NOM_EXPEDITEUR_EMAIL,
htmlBody: corpsEmailRas
});
return;
}
Logger.log(`Nombre de groupes de doublons potentiels détectés: ${doublonsReels.length}`);
const lignesHtmlDoublons = doublonsReels.map(groupe => {
const detailsComptes = groupe.comptes.map(compte =>
`<b>Email:</b> ${compte.email}<br>
ID: ${compte.idUtilisateur}<br>
OU: ${compte.orgUnitPath}<br>
<b>Fonction:</b> ${compte.fonction}<br>
<b>Service:</b> ${compte.service}<br>
<b>Branche:</b> ${compte.centreDeCouts}<br>
Créé le: ${compte.dateCreation}<br>
Dernier login: ${compte.dernierLogin}`
).join('<hr style="border-top: 1px dashed #ccc; margin: 5px 0;">');
return `
<tr>
<td>${groupe.prenomOriginal}</td>
<td>${groupe.nomFamilleOriginal}</td>
<td>${groupe.prefixeEmailCommunAffiche}</td>
<td>${detailsComptes}</td>
</tr>
`;
}).join('');
const corpsHtml = `
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Notification Comptes Google Workspace</title>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Roboto', Arial, sans-serif; margin: 0; padding: 0; background-color: #f8f9fa; color: #202124; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
.email-wrapper { padding: 20px; background-color: #f8f9fa; }
.container { background-color: #ffffff; border: 1px solid #dadce0; padding: 32px; border-radius: 8px; max-width: 850px; margin: 0 auto; box-shadow: 0 1px 2px 0 rgba(60,64,67,0.3), 0 1px 3px 1px rgba(60,64,67,0.15); }
.header { padding-bottom: 16px; margin-bottom: 24px; border-bottom: 1px solid #e0e0e0; text-align: left; }
.logo-container img { display: block; height: 24px; width: auto; margin-bottom: 16px;}
.header h1 { font-size: 22px; font-weight: 500; color: #202124; margin: 0; line-height: 1.3; }
.content { margin-top: 0; line-height: 1.6; font-size: 14px; color: #3c4043; }
.content p { margin: 0 0 16px 0; }
.content table { width: 100%; border-collapse: collapse; margin-top: 16px; margin-bottom: 16px; font-size: 13px; }
.content th, .content td { text-align: left; padding: 10px 12px; border: 1px solid #e0e0e0; vertical-align: top; word-break: break-word; }
.content th { font-weight: 500; color: #202124; background-color: #f8f9fa; }
.footer { margin-top: 32px; padding-top: 16px; border-top: 1px solid #e0e0e0; font-size: 12px; color: #5f6368; text-align: center; line-height: 1.5; }
</style>
</head>
<body>
<div class="email-wrapper">
<div class="container">
<div class="header">
<div class="logo-container">
<img src="https://workspace.google.com/static/img/google-workspace-logo.png" alt="Google Workspace Logo" />
</div>
<h1>${CONFIGURATION.SUJET_EMAIL_ALERTE_DOUBLONS}</h1>
</div>
<div class="content">
<p>Bonjour,</p>
<p>Le système a détecté des comptes utilisateurs Google Workspace qui semblent être des doublons potentiels. Ces comptes (listés comme des entrées distinctes dans la console d'administration) partagent le même prénom (comparaison insensible aux accents et à la casse), nom de famille (comparaison insensible aux accents et à la casse), et la même partie de leur adresse e-mail primaire avant le symbole "@" (après normalisation pour ignorer certains suffixes comme "_int", "_stg", etc.). </p>
<p>Veuillez examiner attentivement la liste ci-dessous :</p>
<table>
<thead>
<tr>
<th>Prénom</th>
<th>Nom</th>
<th>Identifiant email commun</th>
<th>Détails des comptes</th>
</tr>
</thead>
<tbody>
${lignesHtmlDoublons}
</tbody>
</table>
<p>Il est recommandé de vérifier ces comptes pour déterminer s'il s'agit de doublons réels nécessitant une action. Chaque groupe de détails ci-dessus correspond à des comptes utilisateurs distincts existant dans la console d'administration Google Workspace.</p>
</div>
<div class="footer">
<p>Cet e-mail a été envoyé automatiquement par le système de surveillance de ${CONFIGURATION.NOM_EXPEDITEUR_EMAIL}.</p>
<p>© ${ANNEE_ACTUELLE}.</p>
</div>
</div>
</div>
</body>
</html>
`;
MailApp.sendEmail({
to: CONFIGURATION.EMAIL_SOUTIEN_TECHNIQUE,
subject: CONFIGURATION.SUJET_EMAIL_ALERTE_DOUBLONS,
name: CONFIGURATION.NOM_EXPEDITEUR_EMAIL,
htmlBody: corpsHtml
});
Logger.log(`Email de notification de doublons envoyé à ${CONFIGURATION.EMAIL_SOUTIEN_TECHNIQUE} avec ${doublonsReels.length} groupe(s) de doublons potentiels.`);
} catch (erreur) {
const messageErreur = `Erreur critique lors de la détection des doublons ou de l'envoi de l'email: ${erreur.toString()}\nFichier: ${erreur.fileName || 'N/A'}, Ligne: ${erreur.lineNumber || 'N/A'}\nStack: ${erreur.stack}`;
Logger.log(messageErreur);
try {
MailApp.sendEmail(
CONFIGURATION.EMAIL_SOUTIEN_TECHNIQUE,
CONFIGURATION.SUJET_EMAIL_ERREUR_SCRIPT,
`Une erreur est survenue lors de l'exécution du script de détection des doublons de comptes Google Workspace.\n\n` +
messageErreur +
`\n\nVeuillez vérifier les logs du script Google Apps Script pour plus de détails.`
);
} catch (erreurEmailNotification) {
Logger.log(`Impossible d'envoyer l'email de notification d'erreur: ${erreurEmailNotification.toString()}`);
}
}
}
/**
* @function creerDeclencheurHebdomadaire
* @summary Crée un déclencheur temporel (trigger) pour exécuter la fonction detecterEtNotifierDoublonsComptes() chaque semaine.
* @description Vérifie d'abord si un déclencheur pour la même fonction n'existe pas déjà afin d'éviter les doublons de déclencheurs.
* Developpeur: Fabrice Faucheux
*/
function creerDeclencheur() {
const nomFonctionADeclencher = "detecterEtNotifierDoublonsComptes";
try {
const declencheursExistants = ScriptApp.getProjectTriggers();
const declencheurDejaPresent = declencheursExistants.some(
declencheur => declencheur.getHandlerFunction() === nomFonctionADeclencher
);
if (declencheurDejaPresent) {
Logger.log(`Un déclencheur pour la fonction '${nomFonctionADeclencher}' existe déjà.`);
return;
}
ScriptApp.newTrigger(nomFonctionADeclencher)
.timeBased()
.onWeekDay(ScriptApp.WeekDay.WEDNESDAY)
.atHour(CONFIGURATION.HEURE_DECLENCHEUR)
.inTimezone(Session.getScriptTimeZone())
.create();
Logger.log(`Déclencheur hebdomadaire créé avec succès pour '${nomFonctionADeclencher}' tous les mercredi à ${CONFIGURATION.HEURE_DECLENCHEUR}h (timezone: ${Session.getScriptTimeZone()}).`);
} catch (erreur) {
const messageErreur = `Erreur lors de la création du déclencheur hebdomadaire pour '${nomFonctionADeclencher}': ${erreur.toString()}`;
Logger.log(messageErreur);
MailApp.sendEmail(
CONFIGURATION.EMAIL_SOUTIEN_TECHNIQUE,
"Erreur création déclencheur script doublons",
`${messageErreur}\n\nVeuillez vérifier les permissions et la configuration du script.`
);
}
}
N’hésitez pas à me contacter si vous souhaitez en savoir plus sur ce développement ou si vous avez des questions sur son implémentation.