Dans le monde de Google Workspace, on sait que garder une trace de ses emails, c’est super important. Sécurité, conformité, archives… bref, plein de bonnes raisons. Du coup, je vais te montrer une astuce que j’ai bricolée avec Google Apps Script pour transformer tes emails en PDF et les ranger nickel dans Google Drive. C’est top pour ceux qui ont beaucoup de messages à gérer !
Pourquoi faire ça ?
Souvent, on veut garder des copies de nos emails. Mais les transformer en PDF, avec toutes les images et les pièces jointes bien rangées, c’est pas toujours simple. Alors, l’idée, c’est de :
- Sauvegarder automatiquement tous tes emails (reçus, envoyés, archivés…) sauf les spams et la corbeille.
- Garder les images dans les PDF, même celles qui sont dans le corps du message.
- Ranger les PDF dans Google Drive, dans des dossiers avec le nom des libellés Gmail.
- Que ça marche même si tu as plein d’emails et que ça ne plante pas en cours de route.
Comment ça marche, en gros ?
J’ai mis quelques astuces techniques dans le script :
- Traitement par petits bouts : Apps Script a une limite de temps. Du coup, le script travaille par petits paquets d’emails. Et si ça s’arrête, il reprend là où il s’était arrêté.
- Images intégrées : Pour que les images soient dans le PDF, le script les récupère et les met directement dans le fichier.
- Dossiers par libellés : Chaque email va dans un dossier avec le nom de son libellé Gmail. Super pratique pour s’y retrouver !
- Plus de solidité : J’ai ajouté des trucs pour que le script soit plus fiable :
- Cache pour les images : Pour ne pas télécharger 100 fois la même image.
- Noms de fichiers propres : Pour éviter les problèmes avec les caractères bizarres.
- Gestion des erreurs : Si quelque chose ne marche pas, le script continue quand même avec les autres emails.
Les avantages pour toi
- C’est automatique : Une fois configuré, tu n’as plus à t’en soucier.
- C’est bien rangé : Tes emails sont classés par libellés, facile à retrouver.
- C’est solide : Même si ça s’arrête, ça reprend sans perdre d’emails.
- C’est Google : Ça marche direct avec Gmail et Drive.
En résumé
Cette méthode pour sauvegarder tes emails en PDF, c’est vraiment pratique et efficace. Ça marche bien même si tu as beaucoup de messages, et c’est super pour garder tes données en sécurité et bien organisées. Que tu sois une entreprise ou un particulier, ça peut t’aider à gagner du temps et à être plus tranquille.
N’hésite pas à tester le script ci dessous et à le modifier pour qu’il corresponde à tes besoins. Il y a plein de ressources en ligne pour t’aider à faire ça !
// Cache global pour les images (pour éviter les fetch redondants) const imageCache = {}; /** * Fonction principale qui traite les emails en lots, sauvegarde l'état et permet de reprendre le traitement. */ function sauvegarderMailsEnPDF() { const startTime = new Date().getTime(); const maxExecutionTime = 5 * 60 * 1000; // 5 minutes maximum d'exécution pour ce lot // Paramètres de date (format "YYYY/MM/DD"). Laisser vide ("") ou null pour traiter tous les emails. const startDate = "2025/01/01"; // Exemple : "2023/01/01" const endDate = "2025/03/13"; // Exemple : "2023/03/01" // Construction de la requête pour récupérer TOUS les emails, en excluant spam et corbeille. let query = "in:anywhere -in:spam -in:trash"; if (startDate) query += ` after:${startDate}`; if (endDate) query += ` before:${endDate}`; Logger.log(`Requête Gmail: ${query}`); // Récupération ou création du dossier principal sur Drive const dossierPrincipalNom = "Gmail PDFs"; const dossierPrincipal = getOrCreateFolder(dossierPrincipalNom, DriveApp); // Recherche des threads correspondant à la requête const threads = GmailApp.search(query); Logger.log(`Nombre de threads trouvés: ${threads.length}`); // Récupération de l'index de reprise depuis les propriétés du script const scriptProperties = PropertiesService.getScriptProperties(); let resumeIndex = parseInt(scriptProperties.getProperty("lastProcessedThreadIndex") || "0", 10); Logger.log(`Reprise à partir du thread n° ${resumeIndex}`); let totalMessagesCount = 0; let processedMessagesCount = 0; let errorCount = 0; // Parcours des threads à partir de l'index de reprise for (let i = resumeIndex; i < threads.length; i++) { const thread = threads[i]; const messages = thread.getMessages(); totalMessagesCount += messages.length; // Récupération des libellés du thread const labels = thread.getLabels(); const nomsLabels = labels.map(label => label.getName()); for (const message of messages) { // Vérification de la durée d'exécution pour éviter l'expiration du script if (new Date().getTime() - startTime > maxExecutionTime - 10000) { // marge de 10 secondes Logger.log("Temps d'exécution proche de la limite. Sauvegarde de l'état et interruption."); // Sauvegarde l'index du prochain thread à traiter scriptProperties.setProperty("lastProcessedThreadIndex", String(i)); Logger.log(`Dernier thread traité: ${i - 1}. Reprenez à partir du thread n° ${i}.`); return; } try { processMessage(message, dossierPrincipal, nomsLabels); processedMessagesCount++; } catch (e) { Logger.log(`Erreur lors du traitement du message ID ${message.getId()}: ${e}`); errorCount++; } } } // Si on arrive ici, tous les threads ont été traités : on réinitialise la propriété de reprise. scriptProperties.deleteProperty("lastProcessedThreadIndex"); Logger.log(`Nombre total de messages: ${totalMessagesCount}`); Logger.log(`Nombre total de messages traités: ${processedMessagesCount}`); if (errorCount > 0) { Logger.log(`Nombre d'erreurs rencontrées: ${errorCount}`); } if (totalMessagesCount === processedMessagesCount) { Logger.log("Tous les messages ont été traités."); } else { Logger.log("Attention : Certains messages n'ont pas été traités !"); } } /** * Traite un message : extraction, conversion en PDF et sauvegarde dans Drive. * @param {GmailMessage} message Le message à traiter. * @param {Folder} dossierPrincipal Le dossier principal dans Drive. * @param {Array} nomsLabels Tableau des noms de libellés associés au thread. */ function processMessage(message, dossierPrincipal, nomsLabels) { const sujet = message.getSubject() || "Sans objet"; const expediteur = message.getFrom(); const destinataires = message.getTo(); const dateMessage = message.getDate(); let corps = message.getBody(); // Intégration des images inline avec cache corps = inlineImages(corps); // Normalisation du sujet et création d'une partie unique basée sur la date const normalizedSubject = normalizeFileName(sujet); const uniquePart = dateMessage.toISOString(); const baseFileName = `${normalizedSubject} - ${uniquePart}`; // Sauvegarde des pièces jointes et récupération de l'URL du dossier associé (si des pièces existent) let attFolderUrl = saveAttachmentsAndGetLink(message, dossierPrincipal, baseFileName); // Création du contenu HTML pour le PDF en ajoutant le lien vers les pièces jointes le cas échéant const htmlContent = getEmailPdfHtml(sujet, expediteur, destinataires, dateMessage, corps, attFolderUrl); // Conversion en PDF const pdfBlob = convertHtmlToPdf(htmlContent, baseFileName + ".pdf"); // Sauvegarde dans le(s) dossier(s) correspondant(s) aux libellés, ou dans le dossier principal if (nomsLabels.length > 0) { for (const nomSousDossier of nomsLabels) { const sousDossier = getOrCreateFolder(nomSousDossier, dossierPrincipal); sousDossier.createFile(pdfBlob.copyBlob()); } } else { dossierPrincipal.createFile(pdfBlob); } } /** * Sauvegarde les pièces jointes du message dans un sous-dossier dédié et renvoie l'URL du dossier. * Si aucune pièce jointe n'est présente, retourne null. * @param {GmailMessage} message Le message à traiter. * @param {Folder} parentFolder Le dossier dans lequel créer le sous-dossier. * @param {string} baseFileName Base utilisée pour nommer le sous-dossier. * @returns {string|null} L'URL du dossier contenant les pièces jointes ou null. */ function saveAttachmentsAndGetLink(message, parentFolder, baseFileName) { // Liste des types MIME autorisés : PDF et documents bureautiques const allowedMimeTypes = [ "application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation" ]; // Récupère toutes les pièces jointes et filtre celles qui sont dans la liste const attachments = message.getAttachments(); const filteredAttachments = attachments.filter(att => allowedMimeTypes.includes(att.getContentType())); // Si aucune pièce jointe autorisée n'est présente, retourne null if (filteredAttachments.length === 0) return null; // Crée un sous-dossier dédié aux pièces jointes const folderName = baseFileName + " - PJ"; const attFolder = getOrCreateFolder(folderName, parentFolder); // Sauvegarde chaque pièce jointe filtrée dans le dossier filteredAttachments.forEach(att => { attFolder.createFile(att.copyBlob()); }); // Configure le partage pour obtenir un lien accessible attFolder.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW); return attFolder.getUrl(); } /** * Construit le contenu HTML complet qui sera converti en PDF. * @returns {string} HTML du mail. */ function getEmailPdfHtml(sujet, expediteur, destinataires, dateMessage, corps, attFolderUrl) { let attachmentsHtml = ""; if (attFolderUrl) { attachmentsHtml = `<p><strong>Pièces jointes :</strong> <a href="${attFolderUrl}">Voir les pièces jointes</a></p>`; } return ` <html> <head> <meta charset="utf-8"> <title>${sujet}</title> </head> <body> <h1>${sujet}</h1> <p><strong>De :</strong> ${expediteur}</p> <p><strong>À :</strong> ${destinataires}</p> <p><strong>Date :</strong> ${dateMessage}</p> <hr> ${corps} ${attachmentsHtml} </body> </html> `; } /** * Convertit un contenu HTML en blob PDF et lui assigne un nom. * @param {string} htmlContent Le contenu HTML. * @param {string} fileName Le nom du PDF. * @returns {Blob} Le blob PDF. */ function convertHtmlToPdf(htmlContent, fileName) { let blob = HtmlService.createHtmlOutput(htmlContent) .getBlob() .getAs('application/pdf'); blob.setName(fileName); return blob; } /** * Retourne le dossier avec le nom donné ou le crée s'il n'existe pas. * @param {string} folderName Nom du dossier. * @param {Folder|DriveApp} parentFolderOrDriveApp Dossier parent ou DriveApp. * @returns {Folder} Le dossier trouvé ou créé. */ function getOrCreateFolder(folderName, parentFolderOrDriveApp) { let folderIterator; if (parentFolderOrDriveApp.getFoldersByName) { folderIterator = parentFolderOrDriveApp.getFoldersByName(folderName); } else { folderIterator = DriveApp.getFoldersByName(folderName); } return folderIterator.hasNext() ? folderIterator.next() : parentFolderOrDriveApp.createFolder(folderName); } /** * Parcourt le HTML et remplace chaque balise <img> dont la source n'est pas déjà en data URI * par une version encodée en base64. Utilise un cache pour limiter les fetchs redondants. * @param {string} html Le contenu HTML du mail. * @returns {string} Le HTML modifié avec les images en data URI. */ function inlineImages(html) { return html.replace(/<img\s+([^>]*?)src=(["'])(.*?)\2(.*?)>/gi, (match, beforeSrc, quote, src, afterSrc) => { // Ignore les images dont la source est de type "cid:" if (src.startsWith('cid:')) { return match; } try { if (src.startsWith('data:')) return match; if (imageCache[src]) { return match.replace(src, imageCache[src]); } const response = UrlFetchApp.fetch(src, {muteHttpExceptions: true}); if (response.getResponseCode() === 200) { const blob = response.getBlob(); const mimeType = blob.getContentType(); const base64 = Utilities.base64Encode(blob.getBytes()); const dataUri = `data:${mimeType};base64,${base64}`; imageCache[src] = dataUri; return match.replace(src, dataUri); } return match; } catch (e) { Logger.log(`Erreur lors de l'inlining de l'image ${src}: ${e}`); return match; } }); } /** * Normalise un nom de fichier en supprimant ou en remplaçant les caractères interdits. * @param {string} fileName Nom initial du fichier. * @returns {string} Nom normalisé. */ function normalizeFileName(fileName) { return fileName.replace(/[\\\/:*?"<>|]/g, '').trim(); }
Si tu as aimé cet article, partage-le avec tes collègues et suis-moi pour d’autres astuces sur Google Workspace !