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