Premier article d'explication de code avec ce script efficient qui supprimera tous les doublons de tous les dossiers d'une boîte de messagerie. Le script utilise le protocole IMAP, et est compatible avec toutes les versions de Python.
Voici le script en détails (téléchargeable en bas de page) :
# Pour demander le mot de passe du compte
from getpass import getpass
# Ce qu'il faut pour gérer la connexion IMAP
from imaplib import IMAP4_SSL as imap
from socket import error, gaierror
def get_emails(srv):
""" Récupérer la liste des identifiants uniques (UID) des messages. """
emails = []
# On cherche les messages marqués comme "non supprimés"
ret, uids = srv.uid('search', None, 'UNDELETED')
if ret == 'OK':
emails = uids[0].split()
return emails
def purge(srv, folder):
""" Supprimer les doublons dans un dossier. """
print(folder)
# Il faut entourer le nom du dossier par des double-quotes pour éviter les erreurs.
# Ça protège les noms de dossier qui contiennent des espaces.
path = '"{}"'.format(folder)
# Et on se rend dans ledit dossier
ret, data = srv.select(path)
if ret != 'OK':
raise imap.error(ret)
# Récupérer la liste des courriels
uids = get_emails(srv)
total = len(uids)
if not total:
return
print('{:>6} messages'.format(total))
# Recherchons les doublons
uniq_msgs = []
duplicata = []
# La méthode imap.uid() peut traiter plusieurs messages à la fois, ce qui économise
# temps et ressources. On concatène tous les UID des messages avec une virgule.
# Le gain de temps est phénoménal.
all_uids = b','.join(uids)
# On ne récupère que le champ Message-ID de chaque message, universellement unique.
# BODY.PEEK permet de ne pas modifier l'état du message.
# Sinon, le message serait marqué comme lu.
ret, data = srv.uid('fetch', all_uids, '(BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)])')
if ret != 'OK':
raise imap.error(ret)
# data est une liste contenant UID, taille et Message-ID, entre autres.
# Pour chaque message...
for idx in range(0, len(data), 2):
# Il se peut que le message n'aie pas de Message-ID, c'est souvent le cas
# de ceux envoyés par la fonction PHP mail() ou Python smtplib.
# Du coup, on zappe. Pour y remédier, voyez en bas de page.
if not data[idx][1].strip():
continue
# On en déduit le Message-ID
msg_id = data[idx][1].split(' ')[1].replace('\r\n', '')
# Si le Message-ID a déjà été traité, alors il s'agit d'un doublon
if msg_id in uniq_msgs:
# On ajoute son UID à la liste des messages à supprimer
uid = data[idx][0].split()[2]
duplicata.append(uid)
else:
uniq_msgs.append(msg_id)
# Suppression des doublons
if duplicata:
print('{:>6} doublons'.format(len(duplicata)))
# Idem, en faisant une seule requête contenant tous les messages à supprimer,
# le gain de temps est énorme.
all_uids = ','.join(duplicata)
srv.uid('store', all_uids, '+FLAGS', '\\Deleted')
srv.close()
def main(server=None, user=None):
if not server or not user:
return 1
password = getpass()
# Connexion au serveur de messagerie
try:
srv = imap(server)
srv.login(user, password)
except (error, gaierror, imap.error) as ex:
print(ex)
return 1
# On fait le ménage dans tous les dossiers
ret, data = srv.list()
if ret != 'OK':
print(ret)
return 1
try:
# Pour chaque dossier...
for infos in data:
# infos contient plusieurs informations plus ou moins utiles.
# Cependant, certains dossiers ne sont pas sélectionnables, on zappe.
if 'Noselect' in infos:
continue
# On ne prend que ce qui nous intéresse, le nom du dossier.
folder = str(infos).split('"')[3]
purge(srv, folder)
except imap.error as ex:
print(ex)
return 1
srv.logout()
return 0
if __name__ == '__main__':
from sys import argv, exit
if len(argv) < 3:
print('python imap-delete-duplicate.py <server> <user>')
exit(1)
exit(main(argv[1], argv[2]))
Script complet : imap-delete-duplicate.py.
Et voici ce que ça donne en situation réelle :
$ python imap-delete-duplicate.py "mail.gandi.net" "test@jmsinfo.co"
Password:
Drafts
Trash
45 messages
Sent
37 messages
1 doublons
INBOX
888 messages
443 doublons
INBOX/Droit du travail
2 messages
Et avec une grosse boîte de messagerie :
$ time python imap-delete-duplicate.py "imap.gmail.com" "test@gmail.com"
Password:
Archives
10792 messages
9 doublons
INBOX
6550 messages
3 doublons
Personnel
4 messages
Re&AOc-us
[Gmail]/Billetterie
36 messages
[Gmail]/Brouillons
[Gmail]/Clef GNUPG
[Gmail]/Corbeille
37 messages
[Gmail]/Important
6153 messages
2 doublons
[Gmail]/Messages envoy&AOk-s
8684 messages
[Gmail]/Spam
1169 messages
[Gmail]/Suivis
22 messages
[Gmail]/Tous les messages
25970 messages
12 doublons
8,64s user 0,16s system 7% cpu 1:55,36 total
Note : pour ajouter le bon Message-ID aux courriels envoyés par la smtplib de Python :
from email.utils import make_msgid
msg['Message-ID'] = make_msgid()
Note : pour ajouter le bon Message-ID aux courriels envoyés par la fonction mail() de PHP :
$msg_id = sprintf("<%s-%s@%s>", uniqid(time()), md5($from.$to), $_SERVER['SERVER_NAME']);
$headers[] = "Message-ID: $msg_id";
Historique
- 2016-02-08 : optimisation et correction, certains dossiers sont inaccessibles ([Gmail] par exemple).
Source : What's the issue with Message-Id in email sent by php