J’avance sur mes outils de sauvegarde mais aussi sur mes explorations IA.
J’ai eu besoin d’un second programme qui va lire tous les emails d’une boite au format maildir, regarder l’année du mail, et le déplacer dans une boite maildir spécifique à cette année là.
J’aurais pu le faire en Javascript ou en Ruby mais vu ce que m’a fait l’IA en quelques minutes précédemment, je me suis dit que j’allais continuer et refaire un script Go (je n’ai jamais codé une seule ligne de Go).
Voici le résultat : github.com/edas/split-maildir-by-year.
Les 300 lignes de Go ont analysé l’intégralité de mon archive Gmail (537 000 emails quand même).
- Je ne crois pas avoir touché le code source à la main
- Le code a toujours compilé du premier coup
- Le code a toujours fait ce que je souhaitais, sans erreur
Le code est correctement structuré, des fonctions ont été créées au fur et à mesure des besoins quand le code a évolué. Quand une fonction est un peu longue, il sépare en blocs et ajoute une ligne de commentaire pour dire ce que fait le bloc, ce qui me permet de ne pas avoir à décoder un code dans un langage que je ne connais pas.
10 demandes courtes pour avoir le programme et le faire évoluer vers mes besoins, questions de relecture incluses. 17 ajouts par la suite pour traiter des cas spécifiques rencontrés.
Je n’aurais pas fait plus vite moi-même, ni en Go ni dans un langage que je connais très bien. Peut-être que ça aurait été un peu plus difficile pour un non-développeur, mais je vois mal ce que j’aurais eu à y gagner à le coder à la main.
À chaque modification j’ai le diff à valider mais aussi une bonne explication de l’IA sur ce qui a été modifié, comment et pourquoi. Ma relecture s’est souvent faite en diagonale sur la base des commentaires de code. L’IA a su répondre à mes questions quand j’ai rencontré des éléments moins évide
Plutôt que lister les étapes, je copie directement mes prompts.
I have hundreds thousands of files in the "maildir/new" directory. Each file contains a raw email with headers.
I want a program which reads all emails one by one, look for the "Date" header, return an error in the header doesn't exists or is unreadable, and otherwise move the email file in the directory "by-year/{year}/new" where {year} is the year in the Date header.
Je relis parce que ça va toucher des données réelles et que j’ai la flemme de faire des données de tests.
Je vois qu’il retourne toujours une date même quand il y a une erreur. Inhabitué de Go, j’ai peur de certaines erreurs de débutants en PHP ou en JS, où on utilise une date du jour plutôt que gérer l’erreur. Je pose ma question et je suis rassuré par sa réponse (que je trouve logique après coup vu le fonctionnement des erreurs en Go)
can the parseEmailDate return nil when it doesn't find a Date header ?
Je vois aussi un mkdir sans test d’existence préalable. Dans d’autres langages ça jette une erreur si le répertoire existe. Je pose la question et là aussi je suis rassuré par sa réponse.
what if the directory already exists line 63 ?
Je demande une adaptation, non strictement nécessaire, pour que chaque répertoire soit bien une boite maildir avec les 3 répertoires obligatoires. Ce n’est pas nécessaire à ma sauvegarde mais je préfère, au cas où ça m’évite des erreurs un jour.
Oui, j’ai parfois basculé en français. Je ne sais ni pourquoi j’ai du français ici, ni pourquoi j’ai mis de l’anglais avant. L’IA est configurée pour toujours me répondre en français. Le code est toujours commenté en anglais. Je pense que propres entrées dépendent ce sur quoi mon attention était à ce moment là (code, page web, etc.)
Si le répertoire "by-year/{year}" n'existe pas, il faut aussi créer "by-year/{year}/cur" et "by-year/{year}/tmp", même si nous ne nous en servons pas
Seconde adaptation : L’IA m’a dit plus haut qu’elle avait un code de gestion de conflit. Je vois le commentaire dans le code qui dit qu’en cas de conflit le code ajoute un suffixe avec le timestamp du moment pour éviter d’écraser un fichier existant. Normalement ça ne devrait jamais arriver mais un suffixe risquerait de casser le format de nommage des fichiers maildir donc je préfère qu’on s’arrête avec une erreur et que j’avise.
if there is a conflict, to not append a timestamp to make it unique. Return an error.
Troisième adaptation. Je traite un demi-million de fichiers. Je préfère que ça traite les fichiers au fil de l’eau plutôt qu’avoir la liste d’un demi-million de fichiers en mémoire.
Au départ c’est d’abord une question. Je ne sais pas si Go retourne un tableau ou un itérateur (oui, j’ai été flemmard jusqu’à ne même pas faire attention au typage). Je m’attendais à demander la correction dans le premier cas. Au final il modifie de lui-même le code à partir de la seconde question pour faire des itérations par lots 100 fichiers, sans que je ne le demande explicitement.
En réalité c’est du script maison, qui sera lancé juste une poignée de fois. L’optimisation est totalement inutile mais je n’ai pas encore appris à totalement lâcher prise vis-a-vis de ce que j’aurais codé moi-même.
What does return ReadDir in line 88 ?
Si le répertoire contient des millions de fichiers, est-ce que la variable files ligne 88 va tout avoir en mémoire ?
Je vais jouer avec de vraies données. Je veux voir les erreurs et m’arrêter pour corriger, pas que ça continue et que j’ai à remonter voir s’il y a eu des erreurs.
The programm should stop at the first error, not continue with the next file
Et, parce que je n’y avais pas pensé avant :
Le programme doit aussi prendre un chemin en argument. C'est là que se trouveront les différents répertoires prévus.
La première phase est faite. Je passe au test en conditions réelles, sur le demi-million d’email de mon archive. Chaque fois que j’ai une erreur, je lui indique et j’avance.
C’est là que je vois que chaque client email fait bien ce qu’il veut avec les entêtes. J’ai croisé un nombre inattendu de formats différents et d’erreurs dans les entêtes. Chaque fois le programme m’affiche l’erreur, je copie-colle la date problématique, l’IA corrige, et je relance jusqu’à l’erreur suivante.
We should also parse the format for "Mon, 21 Aug 2006 16:47:08 +0200 (CEST)"
We should also parse the date "Mon, 1 Dec 2008 10:57:10 UT"
Sur une erreur étrange, j’ouvre l’email et je me rends compte qu’il prend en compte la continuation d’une entête Received comme si c’était une date, parce qu’il ne prend pas en compte les espaces avant le mot clé Date.
TrimSpave at line 30 should only trim right space, not left space
Particularité Gmail, quand il récupère un email d’une boite tierce (via POP3 ou IMAP), il crée des entêtes à lui, saute une ligne et après pose le vrai email. Rétrospectivement je pense que j’aurais dû retirer la section ajoutée par Gmail pour retrouver un email normal. Je le ferais peut-être plus tard. Là je me suis contenté de lui faire contourner le problème.
When we find the header "X-Gmail-fetch-Info", we should ignore the blank line following if it exists
Encore des questions de dates…
We should be able to parse the Date "Tuesday 29 May 2007, 16:03"
We should also parse "Wed, 03 Mar 2010 22:36:13 +0100 CET"
We should also parse "Thu, 22 Jul 2010 23:02:50"
We should also parse "Mon, 30 Mar 2009 20:11:22 +0100"
Ce coup-ci ça ne corrige pas mon problème. Rétrospectivement j’aurais pu le comprendre parce que le message d’erreur n’était pas exactement le même, mais je le laisse trouver seul. Le mot clé DATE était en majuscules, c’était la première fois.
pourtant le script fait une erreur sur la ligne "DATE: Mon, 30 Mar 2009 20:11:22 +0100". Pourquoi ?
Le code qu’il me génère imbrique quatre fonctions de manipulation de texte sur une seule ligne. Je ne trouve pas ça lisible. Je pose la question.
que fait la ligne 49 ?
Ça semble redondant avec la ligne suivante, pré-existante. Effectivement, quand je pose la question il identifie le doublon et le supprime.
Il faut penser à relire (même si l’erreur aurait juste était du code inutile). Cursor me fait valider chaque changement sous forme de diff donc c’est assez rapide et facile à faire.
que fait la ligne 50 ?
Encore des formats de date…
Encore un format : "mon, 10 jul 2006 01:02:08 gmt"
encore un : "wed, 23 jun 2004 01:19:32 cest"
encore un "mon, 22 mar 2010 14:20:15 +0100 +0100". C'est probablement une erreur d'écriture mais il faut la prendre en compte
"wen, 16 jul 2008 22:09:05 +0200"
Les deux derniers cas sont forcément des erreurs de la part de clients emails. Pour la première erreur il choisit d’ignorer toutes les répétitions du décalage horaire.
La seconde erreur est intéressante parce que « wen » est probablement là pour « wed » (wednesday). Il identifie l’erreur et ajoute un code qui remplace toute une liste d’erreurs de frappes habituelles pour les code courts de jour de la semaine. Parfait.
"wed, 19 apr 2006 12:15 -0800"
J’ai mon premier cas d’email sans entête « Date ». Je ne sais pas si c’est autorisé ou non mais peu importe. Je lui dis de fouiller les entêtes « Received » à la place. Je sais que ces entêtes peuvent être sur plusieurs lignes.
L’IA va plus loin que moi, sait que la date est en seconde position dans ces entêtes, et regarde uniquement après le premier point virgule. Elle sais aussi comment s’appellent ses entêtes sur plusieurs lignes (lignes de continuation). Mieux que ce que j’aurais fait.
Je note que je tape vite, avec des erreurs de frappe, un guillemet en trop, etc. Peu importe, c’est destiné à l’IA. Me relire est superflu : je peux revenir en arrière si c’est mal compris.
If you don't find any Date header, try again to look if you can find a date somewhere in a "Received" header (theere may be multiple "Received" headers") or in the lines begining with a space and following a "Received" header