En 2020, j’ai choisi de ne plus gérer mes e-mails via Gmail et j’ai redirigé mon ancienne adresse vers une nouvelle (avec les filtres de Gmail, pour éviter de rediriger tout le spam).
Entre temps, j’ai aussi atteint la limite fatidique des 15 Go au delà de laquelle il faut souscrire un abonnement pour une extension d’espace ou faire le ménage parmi les tous les emails accumulés (ce qui me semble être à la limite de l’impossible quand on tient à sa santé mentale, même si ces 15 Go incluent les autres services de Google).
Après avoir procrastiné sur le sujet quelques années, j’ai fini par trouver des outils à la fois simples et fiables: dovecot pour le serveur IMAP sur K3s, Thunderbird comme client IMAP sous Windows, et imapsync pour la synchronisation de gmail vers mon serveur IMAP local.
A noter que dovecot propose également un outil pour la migration en IMAP mais je ne m’y suis pas intéressé car j’ai trouvé leur documentation sur le sujet moins accessible que celle d’imapsync qui m’a immédiatement plu.
On pourra apprécier le style « old school » du site https://imapsync.lamiral.info/ qui aurait été digne de figurer dans le « Small web » de Kagi s’il s’agissait d’un blog. Si cela fait peur au premier abord, l’information fournie est remarquablement accessible et « droit au but ».
Dovecot et imapsync sont déployés sur mon cluster local K3s: le premier est le serveur IMAP, le second est un programme à lancer sous la forme d’un CronJob. Avec Thunderbird, je peux ensuite consulter, rechercher, et éventuellement trier ces emails historiques avec pour seule limite mon espace de stockage local. Comme il s’agit surtout du passé, je n’ai pas besoin d’avoir accès à ces emails depuis l’extérieur, un accès local est largement suffisant. Un backup des données reste à prévoir en cas de défaillance de mon serveur. Pour ça, j’utilise Borg, mais je n’en parlerai pas dans cet article.
Spécificités de Gmail
Gmail se donne beaucoup de libertés sur son implémentation IMAP. Notamment, tout est fait pour vous encourager à ne jamais réellement supprimer vos emails. Par défaut, supprimer un email revient à lui retirer tous ses labels, et le message reste stocké dans le dossier spécial « All Mail ».
Cela est réglable dans « Settings », « Forwarding and POP/IMAP », « IMAP access ». Ayant découvert cela après coup, voici les paramètres que j’ai défini après la migration, mais qu’il me semble opportun de définir avant: « Auto-Expunge off », « Immediately delete the message forever When a message is marked as deleted and expunged from the last visible IMAP folder ».
Déployer Dovecot
Les manifestes pour les secrets, le volume persistant, le service, la configuration et le déploiement sont tous combinés ci-dessous. Il faudra évidemment penser à adapter le manifeste des secrets, au moins pour modifier les mots de passe.
Il est également possible de gérer les secrets de façon plus sécurisée selon le mode de déploiement du manifeste, par exemple avec Argo CD et Argo CD Vault Plugin (voir cet autre article de mon blog sur le sujet).
apiVersion: v1
kind: Secret
metadata:
name: dovecot-secrets
stringData:
USER_PASSWORD: "secret"
DOVEADM_PASSWORD: "supersecret"
DOVEADM_API_KEY: "supersecret"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: dovecot-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 15Gi
---
apiVersion: v1
kind: ConfigMap
metadata:
name: dovecot-config
namespace: default
labels:
app.kubernetes.io/name: dovecot
data:
conf.d: |
# https://doc.dovecot.org/main/core/config/service.html#service_vsz_limit
service_vsz_limit = unlimited
doveadm_api_key = $ENV:DOVEADM_API_KEY
doveadm_password = $ENV:DOVEADM_PASSWORD
---
apiVersion: v1
kind: Service
metadata:
name: dovecot-service
labels:
app.kubernetes.io/name: dovecot
spec:
selector:
app.kubernetes.io/name: dovecot
type: ClusterIP
ports:
- name: imap
port: 143
protocol: TCP
targetPort: 31143
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: dovecot-deployment
labels:
app.kubernetes.io/name: dovecot
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: dovecot
template:
metadata:
labels:
app.kubernetes.io/name: dovecot
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 65534
volumes:
- name: data-volume
persistentVolumeClaim:
claimName: dovecot-data
- name: dovecot-config
configMap:
name: dovecot-config
containers:
- name: dovecot
image: dovecot/dovecot
ports:
- containerPort: 31143
- containerPort: 31993
- containerPort: 8080
- containerPort: 9090
volumeMounts:
- mountPath: /srv/vmail
name: data-volume
- mountPath: /etc/dovecot/conf.d/dovecot-custom.conf
name: dovecot-config
subPath: conf.d
env:
- name: USER_PASSWORD
valueFrom:
secretKeyRef:
name: dovecot-secrets
key: USER_PASSWORD
- name: DOVEADM_PASSWORD
valueFrom:
secretKeyRef:
name: dovecot-secrets
key: DOVEADM_PASSWORD
- name: DOVEADM_API_KEY
valueFrom:
secretKeyRef:
name: dovecot-secrets
key: DOVEADM_API_KEY
Les variables « DOVEADM_PASSWORD » et « DOVEADM_API_KEY » contiennent un mot de passe « admin » de dovecot. Cela peut être utile pour effectuer des opérations de maintenance du serveur IMAP, comme supprimer un compte. Nous n’avons pas besoin de l’utiliser dans cet article.
Avec le service nommé « dovecot-service » décrit ci-dessus, le service IMAP sera exposé sur le port 143 uniquement au sein du cluster Kubernetes. Il faudra donc une redirection de port si on veut y accéder depuis un client tel que Thunderbird depuis un poste Windows. Si on souhaite exposer le port du service IMAP directement à l’extérieur du cluster sans avoir explicitement à faire une redirection de port, on pourra remplacer le service par celui-ci:
apiVersion: v1
kind: Service
metadata:
name: dovecot-service
labels:
app.kubernetes.io/name: dovecot
spec:
selector:
app.kubernetes.io/name: dovecot
type: NodePort
ports:
- name: imap
port: 31143
protocol: TCP
targetPort: 31143
nodePort: 31143
Dans le cas d’un service de type NodePort, le port IMAP exposé sera le 31143 (correspond à la plage prévue par défaut sur K3s, à adapter selon votre cas).
Une fois ces manifestes déployés, notre serveur IMAP devrait être rapidement opérationnel. Toute connexion depuis un client IMAP, avec n’importe quel nom d’utilisateur et le mot de passe défini dans la variable d’environnement « DOVEADM_PASSWORD » sera accepté. Le nom d’utilisateur sera utilisé comme nom de boîte mail, et cette boîte (qui ressemble plus à un dossier principal du point de vue du serveur IMAP) sera automatiquement créée vide si elle n’existe pas. Il est donc facile de créer quelques dossiers inutiles par inadvertance, notamment avec certains clients IMAP tel que Thunderbird qui essaiera de vous assister un peu trop pour la définition des paramètres de connexion.
Lancer imapsync
Première copie
Dans un premier temps, on ne lancera pas un CronJob, mais un simple Job imapsync adapté à un compte gmail comme source, et un compte standard non gmail comme destination (en l’occurence notre serveur IMAP local dovecot).
Le manifeste « imapsync-secrets » doit être adapté avec le mot de passe d’accès à Gmail via IMAP. Ce mot de passe n’est pas votre mot de passe principal de Google, mais un « App password ».
La valeur des variables « USER1 » et « USER2 » devrait également être adaptée. « USER1 » est votre adresse gmail complète (utilisé comme login) et « USER2 » est le nom de la boîte que vous décidez de créer (si elle n’existe pas déjà) dans votre serveur IMAP local, par exemple « my.gmail » (sans la seconde partie décrivant l’adresse du serveur).
apiVersion: v1
kind: Secret
metadata:
name: imapsync-secrets
stringData:
PASSWORD1: "gmail password"
---
apiVersion: batch/v1
kind: Job
metadata:
name: imapsync
namespace: default
spec:
template:
metadata:
labels:
app: imapsync
spec:
containers:
- name: imapsync
image: gilleslamiral/imapsync:2.306
# See https://github.com/imapsync/imapsync/issues/437
command: [
"imapsync",
"--host1", "imap.gmail.com",
"--user1", "$(USER1)",
"--password1", "$(PASSWORD1)",
"--gmail1",
"--host2", "dovecot-service",
"--user2", "$(USER2)",
"--password2", "$(PASSWORD2)",
"--noresyncflags",
"--nossl2"
]
env:
- name: USER1
value: "your gmail address"
- name: PASSWORD1
valueFrom:
secretKeyRef:
name: imapsync-secrets
key: PASSWORD1
- name: USER2
value: "my.gmail"
- name: PASSWORD2
valueFrom:
secretKeyRef:
name: dovecot-secrets
key: USER_PASSWORD
restartPolicy: Never
backoffLimit: 1
Cette commande va tenter de copier tous les messages de gmail vers le serveur IMAP local, sans supprimer aucun message à la source après copie. Si la connexion se passe bien, attendez-vous à ce que l’opération puisse durer plusieurs jours.
Pour lancer le job:
kubectl apply -f <filepath>
A la fin du traitement, la sortie console d’imapsync affiche un résumé très détaillé similaire à celui-ci:
Dans mon cas, on peut voir que 151 723 messages différents ont été copiés pour un espace de stockage de 8 Go environ. La façon dont Gmail gère les labels en IMAP fait que les messages sont dupliqués dans chaque label, imapsync a géré cette déduplication en prenant compte seulement du premier label pour classer le message dans son dossier correspondant.
En bas de la capture d’écran, on peut voir qu’il y a eu 33 « messages » qui n’ont pas pu être copiés, il s’agit de vieilles archives de Google Talk, le service de conversation intégré à Gmail qui a successivement été remplacé par Google Hangout, puis Google Chat.
Vérifier le résultat avec le client Thunderbird
Assurez-vous d’avoir exposé le service IMAP dovecot à l’extérieur du cluster Kubernetes, soit avec une redirection de port, soit avec un service de type NodePort (exemple donné plus haut). Une alternative serait de déployer un serveur webmail dans Kubernetes; dans mon cas ça ne m’a pas paru utile car j’ai rarement besoin d’accéder à mon backup de gmail, et l’UX de Thunderbird est bien meilleure qu’un webmail.
Dans mon cas, le port IMAP est 143 sur localhost car j’utilise une redirection de port et non un service de type NodePort. Avec un service de type NodePort, il faudrait utiliser le port local adéquat et le nom de serveur de Kubernetes.
Thunderbird requiert également des paramètres pour le serveur SMTP, mais je crois bien que l’on peut y mettre n’importe quoi (je ne prévois pas d’envoyer des emails avec).
Une fois connecté au serveur IMAP, Thunderbird mettra lui-même un certain temps à se synchroniser la première fois. Une fois sa magie opérée, on y retrouve tous nos emails en provenance de Gmail (dans mon cas, pour les messages les plus récents et visibles dans la capture ci-dessous, il s’agit essentiellement de spam et autres publicités non sollicitées car ce compte est abandonné depuis plusieurs années):
Pour savoir tout ce qu’il est possible de faire via la recherche d’emails dans Thunderbird, voir cette documentation: Thunderbird Global Search.
Relancer imapsync en mode suppression
Une fois satisfait, on peut relancer un nouveau Job imapsync avec le flag additionnel --delete1
pour qu’il supprime les messages copiés (et uniquement ceux-là). On modifie donc le manifeste du job « imapsync » avec ces paramètres de commande:
command: [
"imapsync",
"--host1", "imap.gmail.com",
"--user1", "$(USER1)",
"--password1", "$(PASSWORD1)",
"--gmail1",
"--delete1",
"--host2", "dovecot-service",
"--user2", "$(USER2)",
"--password2", "$(PASSWORD2)",
"--noresyncflags",
"--nossl2"
]
On supprime l’ancien job terminé:
kubectl delete job imapsync
Et on reapplique le manifeste du job pour le relancer:
kubectl apply -f <filepath>
Cela prendra longtemps, mais moins longtemps que la copie initiale car il n’y a pas besoin de transférer le contenu de tous les messages.
Vérifier le résultat dans Gmail
Si comme moi, vous n’observez aucun gain de place sur Gmail après la suppression des messages par imapsync, c’est probablement à cause de vos paramètres sur Gmail (décrit en début d’article).
Pour supprimer manuellement tous les messages depuis l’interface web de Gmail, c’est possible sans être très intuitif:
Effectuer une recherche telle que « before:2025-06-24 » (avec la date d’exécution d’imapsync), puis pour tout cocher il faut d’abord cocher la case générale en haut à gauche, puis cliquer sur le lien « Select all conversations that match this search », et enfin cliquer sur le bouton « Delete » et confirmer quand la boîte de dialogue « Confirm bulk action » s’affiche.
Il faut ensuite, après un peu de temps, vider le dossier « Trash » pour que la suppression soit complète. Cette fois, lorsqu’il y a énormément de messages à supprimer, la progression s’affiche dans une boîte de dialogue. Si vous avez été trop pressé comme moi pour le faire, il faudra le faire plusieurs fois car le déplacement des messages vers le dossier « Trash » est lui-même long (en arrière plan, sans progression affichée dans l’interface).
A la fin, vous devriez voir l’espace libéré. Dans mon cas, je suis passé de 98% à 51% parce que j’ai encore des fichiers sur Google Drive.
Pour avoir une vision d’ensemble par service, c’est ici: https://one.google.com/storage. Notez que pour un backup de Google Photos ou Google Drive, le service Google Takeout est très facile à utiliser.
Synchronisation périodique
Comme je souhaite laisser vivre mon compte Gmail, je peux lancer le même job imapsync (avec le flag --delete1
) de façon périodique. Ainsi, les messages sont copiés vers le serveur IMAP local avant d’être supprimés de Gmail. On peut voir le comportement d’imapsync dans le résumé d’un tel job:
Cette fois, si Gmail est correctement paramétré, il devrait être entièrement vidé sans qu’on ait besoin de supprimer manuellement les messages synchronisés :
Notez la petite phrase Calimero quand on parvient à vider sa boîte Gmail: « You don’t have any mail! Our servers are feeling unloved ».
On peut rendre ce job périodique en le transformant en CronJob. L’exemple suivant va lancer le job deux fois par jour à 8h et à 18h. Si on souhaite effectuer la synchronisation toutes les minutes, on peut utiliser schedule: "* * * * *"
.
apiVersion: batch/v1
kind: CronJob
metadata:
name: imapsync
namespace: default
spec:
schedule: "00 08,18 * * *"
jobTemplate:
spec:
template:
metadata:
labels:
app: imapsync
spec:
containers:
- name: imapsync
image: gilleslamiral/imapsync:2.306
# See https://github.com/imapsync/imapsync/issues/437
command: [
"imapsync",
"--host1", "imap.gmail.com",
"--user1", "$(USER1)",
"--password1", "$(PASSWORD1)",
"--gmail1",
"--delete1",
"--host2", "dovecot-service",
"--user2", "$(USER2)",
"--password2", "$(PASSWORD2)",
"--noresyncflags",
"--nossl2"
]
env:
- name: USER1
value: "your gmail address"
- name: PASSWORD1
valueFrom:
secretKeyRef:
name: imapsync-secrets
key: PASSWORD1
- name: USER2
value: "my.gmail"
- name: PASSWORD2
valueFrom:
secretKeyRef:
name: dovecot-secrets
key: USER_PASSWORD
restartPolicy: Never
backoffLimit: 1
kubectl delete job imapsync
kubectl apply -f <filepath>
On pourra surveiller le statut du CronJob avec la commande suivante:
kubectl get cronjob imapsync
On peut également connaître la liste des Jobs créés par ce CronJob avec la commande:
kubectl get jobs
NAME STATUS COMPLETIONS DURATION AGE
imapsync-29203440 Complete 1/1 25s 34h
imapsync-29204160 Complete 1/1 33s 22h
imapsync-29204880 Complete 1/1 26s 10h
Dans l’exemple ci-dessus, on voit que le CronJob s’éxécute bien deux fois par jour, avec une durée d’environ 30 secondes. Pour obtenir les logs d’exécution d’un job particulier:
kubectl logs job.batch/imapsync-29204160
[...]
++++ Statistics
Transfer started on : Friday 11 July 2025-07-11 16:00:00 +0000 UTC
Transfer ended on : Friday 11 July 2025-07-11 16:00:31 +0000 UTC
Transfer time : 30.2 sec
Folders synced : 9/9 synced
Folders deleted on host2 : 0
Messages transferred : 1
Messages skipped : 0
Messages found duplicate on host1 : 0
Messages found duplicate on host2 : 0
Messages found crossduplicate on host2 : 0
Messages void (noheader) on host1 : 0
Messages void (noheader) on host2 : 0
Messages found in host1 not in host2 : 0 messages
Messages found in host2 not in host1 : 92596 messages
Messages deleted on host1 : 1
Messages deleted on host2 : 0
Total bytes transferred : 117803 (115.042 KiB)
Total bytes skipped : 0 (0.000 KiB)
Message rate : 0.0 messages/s
Average bandwidth rate : 3.8 KiB/s
Reconnections to host1 : 0
Reconnections to host2 : 0
Memory consumption at the end : 439.9 MiB (*time 3.7 MiB*h) (started with 161.3 MiB)
Load end is : 0.39 0.29 0.35 2/2779 on 4 cores
CPU time and %cpu : 14.93 sec 49.5 %cpu 12.4 %allcpus
Biggest message transferred : 117803 bytes (115.042 KiB)
Memory/biggest message ratio : 3915.4
Start difference host2 - host1 : 92596 messages, 6536304079 bytes (6.087 GiB)
Final difference host2 - host1 : 92598 messages, 6536539685 bytes (6.088 GiB)
The sync looks good, all 1 identified messages in host1 are on host2.
There is no unidentified message on host1.
The sync is not strict, there are 92596 among 92597 identified messages in host2 that are not on host1. Use --delete2 and sync again to delete them and have a strict sync.
Detected 0 errors
Dans le log ci-dessus, on voit que ce job imapsync a eu un seul email à copier et supprimer de Gmail.
Enfin si on souhaite un jour désactiver ce job périodique, on pourra le supprimer avec cette commande:
kubectl delete cronjob imapsync
Trier ses e-mails dans Thunderbird
La prise en main de Thunderbird est très rapide, et la vitesse d’exécution des recherches même dans une boîte de volume important est impressionnante. La suppression de milliers de messages ou de dossiers de ce volume peut mettre un certain temps à se synchroniser sur le serveur IMAP (local). Le statut s’affiche dans la barre en bas de la fenêtre, et on peut obtenir le détail en double cliquant dessus (une fenêtre « Activity Manager » devrait s’afficher).
Je ne vais pas faire un tutoriel sur Thunderbird, mais dans mon cas j’ai trouvé particulièrement utile de mettre en place quelques réglages:
- Dans la première barre qui affiche l’arborescence des dossiers, j’ai activé l’affichage du nombre total de messages par dossier et leur taille. D’un coup d’oeil, je vois les dossiers les plus importants à trier. Par exemple, les newsletters que je reçois de Wired, et qui étaient automatiquement archivées dans un dossier spécifique, occupent presque 300 Mo.
- Dans la deuxième barre qui affiche les messages d’un dossier, j’ai activé la colonne qui affiche la taille de chaque message, et j’ai trié le contenu du dossier sur cette colonne pour voir les plus volumineux en premier.
- Pour voir combien de messages un expéditeur m’a envoyé, j’ai effectué une recherche sur son adresse email. Cela permet de tous les supprimer d’un seul coup. Un graphique du volume d’emails par période de temps est même affiché en bonus.
- J’ai installé l’add-on ThirdStats pour avoir davantage de statistiques, notamment par expéditeur, sans avoir à faire une recherche sur chacun d’entre eux.
Je ne prévois pas de recevoir beaucoup d’e-mails sur ce compte mais si c’était le cas, le prochain sujet d’intérêt serait les filtres de Thunderbird, pour l’exécution automatique de règles à la réception de nouveaux messages.
Cela conclut cet article: j’ai une boîte gmail quasiment tout le temps vide, sans avoir besoin de la clôturer définitivement, et sans avoir perdu mon historique de messages. Comme cette technique est applicable à n’importe quel compte IMAP, je pourrai éventuellement la réutiliser sur mon compte email actuel (j’utilise Fastmail, payant, qui permet l’accès IMAP), notamment pour réduire son historique si je devais approcher sa limite (ou juste pour ne pas laisser un historique de plusieurs années inutilement sur leurs serveurs).