Migration mail POP et IMAP vers IMAP

Procédure complète pour migrer une boîte mail vers un serveur IMAP, y compris les cas tordus : archives .pst, dossiers locaux .ost invisibles côté serveur, Outlook qui plante, disque plein.

Migration réelle : ~15 000 mails Cible testée : cPanel / Dovecot imapsync · readpst · pypff Windows, sans droits admin
Mis à jour le 3 août 2026 14 min de lecture
À lire avant de copier quoi que ce soit

Conventions et cadre d'intervention

Toutes les valeurs propres au client sont notées en placeholders : [email protected], MDP, imap.serveur.tld. Sur un mutualisé cPanel, le nom d'hôte IMAP est celui affiché dans votre propre panneau (forme xxxx.hebergeur.tld), en 993/SSL, SMTP 465/SSL. Ne jamais laisser les identifiants d'un client dans un script partagé.

Cadre
Ces manipulations se font sur un poste dont on a la charge, avec l'accord explicite du client, et l'outil de prise en main à distance est retiré en fin d'intervention. La tâche planifiée SYSTEM décrite en phase 05 ne sert qu'à faire survivre un job long à une déconnexion : elle est supprimée dès le job terminé.
Règle
Le serveur est la source de vérité, jamais l'affichage client. Roundcube et Outlook mettent la liste des dossiers en cache et « clignotent ». On vérifie toujours l'état réel en IMAP (compteurs), pas à l'œil dans le webmail.
Par où commencer

Arbre de décision

Les phases à jouer dépendent de où vivent réellement les mails du client. On additionne les voies nécessaires.

Source IMAP ?

Les mails sont sur un serveur IMAP (Gandi, OVH…)

Migration serveur à serveur, sans toucher au poste. → Phase 01 (imapsync)

Archive POP ?

Historique dans un .pst local (compte POP)

Le vrai des mails est sur le poste, pas sur le serveur. → Phase 02 (readpst vers IMAP)

Dossiers « ce PC uniquement » ?

Dossiers marqués (uniquement cet ordinateur) ou jamais montés

Présents seulement dans le cache .ost, invisibles côté serveur. → Phase 03 (pypff vers IMAP)

Toujours

Après récupération

Phase 04 DNS · 05 Outlook · 06 dédoublonnage · 07 disque. Dans cet ordre.

00

Préparation

Quand : avant toute chose.

  • Identifiants source (ancien serveur, chaque boîte) et cible (nouveau serveur, panneau d'hébergement).
  • Accès poste : outil de prise en main à distance. Retenir que son shell tourne souvent en SYSTEM (session 0), ce qui change tout par la suite.
  • État du poste : espace disque C: (souvent le point de blocage), taille de la boîte, Outlook classique ou « nouveau ».
  • Un disque de travail avec de la place (disque externe) pour les extractions et les sauvegardes.
  • Quota cible : le défaut d'une boîte fraîchement créée est souvent 1 Go. Passer en illimité dans le panneau avant toute migration, sinon EXIT_OVERQUOTA.
Pré-vol
Vérifier l'allocation d'inodes du compte mutualisé : « disque illimité » n'est pas « inodes illimités ». Une boîte de ~15 000 mails, c'est autant de fichiers Maildir. À demander à l'hébergeur si le compte est déjà chargé.
01

Migration IMAP vers IMAP (imapsync)

Quand : la source est un serveur IMAP. Se lance en SSH sur le serveur cible.

Copie serveur à serveur, boîte par boîte. Sur un hébergement mutualisé sans droits root, imapsync s'installe en local::lib ; les modules XS qui ne compilent pas (Unicode::String, Sys::MemInfo…) se rendent optionnels en patchant la source et en posant un stub.

ssh · serveur ciblebash
# passfiles par boîte
printf '%s' 'MDP_SOURCE' > ~/pf/BOX.src
printf '%s' 'MDP_CIBLE'  > ~/pf/BOX.dst

perl -I$HOME/perl5/lib/perl5 $HOME/imapsync-2.314/imapsync \
  --host1 imap.source.tld --user1 [email protected] --passfile1 ~/pf/BOX.src --ssl1 \
  --host2 localhost        --user2 [email protected] --passfile2 ~/pf/BOX.dst --ssl2 \
  --automap --syncinternaldates --pidfile ~/pid/BOX.pid
Pièges
Un --pidfile distinct par boîte si lancements en parallèle. Ne jamais lancer deux fois la même boîte en parallèle : doublons garantis. Rattrapage de dernière minute : relancer avec --maxage 1 après la bascule DNS.
02

Archive POP .pst vers IMAP

Quand : l'historique est dans un .pst local (compte POP). Extraction sur la machine, puis téléversement IMAP.

On convertit le .pst en .eml avec readpst (natif Windows, aucun WSL), puis on téléverse en IMAP avec un script Python reprenable et dédoublonné.

Déployer readpst en natif Windows, sans droits admin

PowerShell · posteps1
# MSYS2 en dossier utilisateur (sans admin, sans redémarrage)
curl.exe -L -o F:\m\msys2.sfx.exe https://github.com/msys2/msys2-installer/releases/download/nightly-x86_64/msys2-base-x86_64-latest.sfx.exe
F:\m\msys2.sfx.exe -y -oF:\
F:\msys64\usr\bin\bash.exe -lc "pacman -Sy --noconfirm mingw-w64-ucrt-x86_64-libpst"
F:\msys64\ucrt64\bin\readpst.exe -V   # doit afficher la version

Extraire puis téléverser, en dédoublonnant

PowerShell · posteps1
F:\msys64\ucrt64\bin\readpst.exe -e -o F:\out "C:\...\archive.pst"
F:\m\py\python.exe F:\m\pst2imap.py F:\out

Le script pst2imap.py (voir les scripts) parcourt l'arborescence .eml, mappe vers INBOX.*, crée les dossiers manquants, et fait un APPEND de chaque message avec un en-tête X-Migrate-UID (sha256) plus une réconciliation côté serveur : on peut le relancer sans créer de doublon.

Ne pas
Ne jamais passer par WSL sur un poste piloté à distance : WSL exige un redémarrage et reste inaccessible depuis la session 0 / SYSTEM. L'échec est silencieux, on cherche pendant une heure. readpst natif Windows uniquement.
Encodage
readpst écrit les noms de dossiers en UTF-8, mais Windows les enregistre via la page ANSI cp1252 : on obtient du mojibake (Éléments...). Corriger dans le mapping avec s.encode('cp1252').decode('utf-8') (repli latin1). Pas chcp 65001, qui n'agit que sur la console.
03

Cache local .ost vers IMAP (pypff)

Quand : des dossiers (uniquement cet ordinateur) ou jamais synchronisés existent. Ils ne vivent que dans le .ost.

readpst plante sur les gros .ost corrompus (Out Of memory) et sur les chemins de plus de 260 caractères (Cannot open file). La parade : pypff lit le .ost directement et pousse en IMAP en mémoire, sans écrire un seul fichier sur disque (donc plus de limite de chemin), avec un try/except par message : un item corrompu est sauté, jamais de crash global.

PowerShell · posteps1
# Python embeddable : l'installeur .exe "réussit" sans rien poser sous SYSTEM
curl.exe -L -o F:\m\py.zip https://www.python.org/ftp/python/3.12.7/python-3.12.7-embed-amd64.zip
Expand-Archive -Force F:\m\py.zip F:\m\py
# activer pip, puis :
F:\m\py\python.exe -m pip install libpff-python-windows imapclient

# copie de sécurité du .ost, puis extraction directe vers IMAP
Copy-Item "C:\Users\U\AppData\Local\Microsoft\Outlook\...ost" F:\ost_actif.ost
F:\m\py\python.exe F:\m\ost2imap.py "F:\ost_actif.ost" preview   # vérifier le mapping
F:\m\py\python.exe F:\m\ost2imap.py "F:\ost_actif.ost"           # run réel
Mapping
L'arborescence .ost contient des niveaux MAPI parasites : Racine / IPM_SUBTREE / Boîte de réception / …. Le mapping doit s'ancrer sur la Boîte de réception (détectée par la sous-chaîne ception), sinon on obtient INBOX.IPM_SUBTREE.Boîte de réception.…. pypff renvoie les noms en Unicode propre (pas de mojibake), mais garde le suffixe (uniquement cet ordinateur) qu'il faut retirer. Toujours lancer preview avant le run.
04

Bascule DNS

Quand : une fois les boîtes migrées, pour rediriger le flux entrant.

MX
@ vers mail.serveur.tld, en retirant les anciens MX. Le champ « serveur de messagerie » attend le nom d'hôte : il ne va pas dans le champ Name.
SPF
v=spf1 include:spf.hebergeur.tld -all, en gardant les include légitimes déjà présents (routeur transactionnel par exemple).
DKIM
default._domainkey repris du panneau d'hébergement ; supprimer les anciens sélecteurs.
Ordre
SPF et DKIM d'abord, puis resync rapide (--maxage 2), puis changement du MX, puis rattrapages (--maxage 1 à H+1h, H+4h, J+1).
Filet
Garder les boîtes de l'ancien serveur actives quelques jours après la bascule : c'est le seul moyen de récupérer un mail arrivé pendant la propagation. Si le domaine passe par un proxy, l'enregistrement autodiscover doit rester en DNS seul, jamais proxifié, sinon le certificat casse côté Outlook.
05

Reconfiguration Outlook

Quand : après la bascule, sur chaque poste.

  • Si le shell distant tourne en SYSTEM, viser le SID de l'utilisateur connecté (Win32_ComputerSystem.UserName, puis SID, puis HKU\<SID>), jamais %APPDATA% ni HKCU : ce sont ceux de SYSTEM, et on configure un profil que personne n'ouvrira.
  • Outlook classique obligatoire : les fichiers .prf ne s'appliquent pas au « nouveau Outlook ».
  • Ajout du compte IMAP par .prf en mode OverwriteProfile=Append (ajoute sans retirer l'ancien), lancé par outlook.exe /importprf.
  • Les signatures sont de simples fichiers locaux, dans le dossier Signatures du bon SID : elles survivent à la migration sans rien faire.
Survie
Pour un job long piloté à distance (extraction, téléversement de nuit) : ne pas le lancer dans la fenêtre de la session distante, elle le tue à la déconnexion. Le détacher en tâche planifiée avec principal NT AUTHORITY\SYSTEM (LogonType ServiceAccount, ExecutionTimeLimit 0) : elle tourne en session 0 et survit. Ajouter powercfg /change standby-timeout-ac 0, et supprimer la tâche une fois le job fini.
06

Dédoublonnage et fusion

Quand : après avoir empilé plusieurs sources qui se recouvrent (.pst, .ost, copies Outlook).

Méthode en deux temps, jamais destructive d'un coup, toujours dédoublonnée par Message-ID :

  1. Fusion (COPY) : on copie dans le dossier « propre » les mails qui y manquent, identifiés par Message-ID. Non destructif, la source reste intacte.
  2. Vérification puis suppression : on ne supprime un dossier « doublon » que si l'intégralité de son contenu est retrouvée dans la cible. Script en mode aperçu par défaut, go pour exécuter.
Or
La suppression revérifie la couverture juste avant chaque dossier, pas une fois pour toutes au début. Et on ne supprime jamais « à l'œil » depuis le webmail : un dossier qui paraît vide peut simplement ne pas être rafraîchi côté client alors que le serveur, lui, est complet.

Cas typiques rencontrés : les dossiers X - Copie et X (2) (artefacts Outlook), et les doublons entre l'arbre Archive.* (issu du .pst) et les dossiers vivants (issus du .ost).

07

Disque et cache, la finalisation

Quand : tout est sur le serveur. C'est cette phase qui règle le problème de fond, le disque plein.

  1. Libérer le disque : supprimer ou déplacer le .pst local, son contenu étant désormais sur le serveur et sauvegardé. Le .pst « par défaut » ne se retire pas directement : créer un petit .pst vide, le définir par défaut, puis retirer le gros du profil, puis supprimer le fichier. Fermer Outlook si Windows le déclare « utilisé ».
  2. Limiter le cache : sur le compte IMAP, régler le curseur « Courrier à conserver hors connexion » sur 1 à 6 mois. Le récent reste local et rapide, l'ancien reste sur le serveur et se télécharge à l'ouverture. Effet de bord utile : ça reconstruit un .ost neuf et léger, ce qui règle au passage un .ost corrompu.
Réversible
Le cache « X mois » contre « tout en local » est un simple réglage, pas une re-migration. Quand le poste gagne de la place (nouveau SSD), on repasse en « télécharger tout » d'un clic. Les données du serveur ne bougent pas.
IMAP moderne
Sur Outlook récent, l'option « télécharger uniquement les en-têtes » a été remplacée par ce curseur pour les comptes IMAP. Inutile de la chercher dans les groupes d'envoi et de réception : c'est le curseur qui commande.
Ce qui fait perdre des heures

Les pièges et leurs parades

Chaque ligne est ancrable : le lien de la ligne pointe directement dessus.

SymptômeCause réelleParadeGravité
WSL « n'exécute rien » via tâche planifiée ou session distanteWSL exige un redémarrage et reste inaccessible en session 0 / SYSTEMreadpst.exe natif Windows (MSYS2). Jamais WSL à distance.bloquant
readpst Out Of memory ou Cannot open fileItem corrompu, ou chemin de plus de 260 caractèrespypff en lecture directe, téléversement IMAP en mémoire (ost2imap.py).bloquant
Noms de dossiers en Éléments...readpst écrit en UTF-8, Windows enregistre en cp1252s.encode('cp1252').decode('utf-8'). Pas chcp 65001.piège
Cibles créées en INBOX.IPM_SUBTREE.Boîte de réception.…Le .ost expose des niveaux MAPI parasitesAncrer le mapping sur la Boîte de réception (sous-chaîne ception).piège
L'installeur Python « réussit » sans rien poserExécution en contexte SYSTEM / session 0Python embeddable (zip), jamais l'installeur .exe.piège
Dossiers (uniquement cet ordinateur) absents du serveurDossiers locaux Outlook jamais synchronisés en IMAPLes extraire du .ost (phase 03).données
Le job détaché meurt à la déconnexionLe shell distant est un enfant de l'agent de prise en mainTâche planifiée avec principal SYSTEM (session 0).piège
EXIT_OVERQUOTA dès le début de l'imapsyncQuota par défaut de la boîte cible à 1 GoPasser en illimité dans le panneau avant la migration.bloquant
Impossible de supprimer le gros .pstIl est le fichier de données par défaut du profilCréer un petit .pst vide, le définir par défaut, puis retirer le gros.piège
Des dossiers « clignotent » dans le webmailCache d'affichage, ou un autre client qui synchronise en même tempsVérifier l'état réel en IMAP (compteurs), fermer Outlook, ne rien supprimer à l'œil.piège
Doublons entre l'arbre Archive et les dossiers vivantsLe .pst et le .ost contenaient les mêmes mailsFusion dédoublonnée par Message-ID, puis suppression vérifiée (phase 06).piège
Disque plein, .ost corrompu, Outlook planteCache local qui déborde un C: déjà saturéSortir l'archive du poste, supprimer le .pst local, limiter le cache (phase 07).racine
La mécanique

Les scripts

Trois outils, tous rejouables sans créer de doublon : la déduplication repose sur un en-tête injecté X-Migrate-UID (sha256 des octets d'origine) croisé avec le Message-ID, réconciliés côté serveur avant chaque APPEND. En tête de chaque script : HOST, USER, PWD et les chemins.

ost2imap.py .ost vers IMAP, robuste, le pivot

Lit le .ost avec pypff, reconstruit chaque message (en-têtes, corps, pièces jointes), fait l'APPEND en IMAP. Zéro fichier sur disque, ce qui contourne MAX_PATH, et try/except par message, ce qui contourne la corruption. Mapping ancré sur la Boîte de réception, suffixe local retiré. Mode preview en deuxième argument.

ost2imap.pypython
HOST, PORT = "imap.serveur.tld", 993
USER, PWD  = "[email protected]", "MDP"
SEP = "."
LOCAL_SUF = (" (uniquement cet ordinateur)", " (This computer only)")

def isinbox(s):
    f = "".join(c for c in unicodedata.normalize("NFKD", s or "")
                if not unicodedata.combining(c)).lower()
    return "ception" in f or "inbox" in f       # ancre sur la Boite de reception

def maptgt(path):                               # noms .ost -> INBOX.x.y
    idx = next((i for i, seg in enumerate(path) if isinbox(_strip(seg))), None)
    if idx is None: return None
    rest = [_san(_strip(s)) for s in path[idx+1:]]
    rest = [s for s in rest if s]
    if not rest: return None                    # on ne deverse pas l'inbox brut
    if rest[0].lower() == "archive": return None
    return "INBOX" + SEP + SEP.join(rest)

def _strip(s):
    for suf in LOCAL_SUF:
        if s.endswith(suf): return s[:-len(suf)]
    return s

# build_rfc822(msg) : transport_headers | plain/html (bytes) | pieces jointes
# connect() / ensure() / existing() : APPEND dedup par Message-ID + X-Migrate-UID
# walk() : recursion pypff, saute les dossiers vides, try/except par message
Résultat réel
Sur une boîte où readpst plantait : 5 829 messages traités, 3 567 remontés, 0 erreur, pièces jointes comprises. Le delta correspond aux messages déjà présents côté serveur, écartés par la déduplication.
pst2imap.py arborescence .eml (readpst) vers IMAP, reprenable

Parcourt la sortie de readpst, corrige le mojibake des noms de dossiers, mappe vers INBOX.*, fait l'APPEND avec X-Migrate-UID et réconciliation serveur. Reprend là où il s'est arrêté, sans doublon. Mode preview pour valider le mapping avant.

pst2imap.py, le cœurpython
def demojibake(s):                    # noms de dossiers readpst
    try: return s.encode("cp1252").decode("utf-8")
    except (UnicodeEncodeError, UnicodeDecodeError):
        try: return s.encode("latin1").decode("utf-8")
        except (UnicodeEncodeError, UnicodeDecodeError): return s

def dest(rel):
    p = [x for x in rel.split(os.sep) if x and x != "."]
    if p: p = p[1:]                   # retire la racine du .pst
    p = [demojibake(x).replace(".", "_") for x in p]
    return "INBOX.Archive." + ".".join(p) if p else "INBOX.Archive"

# APPEND : sha256 des octets ORIGINAUX, avant injection = idempotent au re-run
k = hashlib.sha256(raw).hexdigest()
if seen(f, k) or k in present: continue
c.append(f, b"X-Migrate-UID: " + k.encode() + eol + raw, flags=[b"\\Seen"], msg_time=dt)
fusion.py et fusion_del.py dédoublonner deux arbres, en sécurité

Deux temps. fusion copie, sans rien détruire, les mails absents de l'arbre d'origine, par Message-ID. fusion_del ne supprime un doublon que si son contenu est couvert à 100 % dans l'original, avec aperçu par défaut et go pour exécuter.

fusion_del.py, le garde-foupython
DO = len(sys.argv) > 1 and sys.argv[1] == "go"

for cop in doublons:
    orig = original_de(cop)
    manquants = mids_tree(cop) - mids_tree(orig)
    if manquants:
        print("GARDE", cop, len(manquants), "non couverts")   # on ne touche pas
        continue
    if DO:
        # bottom-up : un dossier parent ne part qu'apres ses enfants
        for f in sorted(sous_dossiers(cop), key=lambda x: -x.count(".")):
            supprimer(f)
Pack
Les versions complètes (reconstruction des pièces jointes, reprise sur incident, compteurs de contrôle) ne sont pas publiées ici : elles portent trop de réglages liés à un contexte pour être copiées à l'aveugle, et un script de migration mal calibré fait des dégâts silencieux. Si vous jouez cette procédure et que vous voulez le pack, demandez-le moi.
Vous êtes bloqué sur une de ces étapes ?

Je peux la jouer pour vous

Cette procédure est publiée telle qu'elle est appliquée en intervention réelle. Si vous préférez la déléguer, ou si votre cas sort du cadre décrit ici, écrivez-moi : je reprends la migration de bout en bout, boîtes et postes compris.

Décrire mon cas  ·  Les autres procédures

Parlons-en

Une migration ou une reprise technique à faire ?

Décrivez votre situation, je vous dis ce qui est faisable, en combien de temps et à quel prix.

Réponse sous 48h · Premier rendez-vous gratuit · [email protected]