Traiter les emails entrants (Exim + SpamAssassin + PHP)

Il y a quelque temps, j’ai passé en revue plusieurs services servant à envoyer et recevoir des emails dans un mode SaaS. Je reste persuadé que, la plupart du temps, ces services permettent de gagner du temps. On crée un compte, on écrit un bout de code pour s’y connecter, et c’est prêt.

Malgré tout, il y a des cas où ça coince fonctionnellement. Et quand on a besoin de souplesse, il n’y a rien de mieux que de le faire soi-même.
En plus, Adrien semblait dire en commentaire que c’est quelque chose d’assez facile à faire, alors j’ai voulu expérimenter la chose.

Pour l’environnement technique, je suis parti d’un serveur sous Linux, en utilisant la distribution Ubuntu version 11.04. J’ai utilisé le serveur mail Exim, car c’est celui sur lequel j’ai le plus d’expérience ; il y a quelques années, c’était le seul MTA qui proposait une intégration poussée avec le filtre anti-spam SpamAssassin (du moins dans les paquets fournis par Debian/Ubuntu).
Enfin, j’ai fait en sorte que les emails reçus soient traités par un script écrit en PHP (je ne vais pas revenir là-dessus).

Je vais vous expliquer comment j’ai procédé. Pour l’exemple, on va dire que le serveur n’est utilisé que pour le seul domaine toto.com, et que tous les messages reçu sont envoyés au même script PHP, quelle que soit l’adresse email du destinataire (c’est donc un « catch-all » sur le domaine toto.com).

Installation

Pour commencer, j’ai installé PHP en mode ligne de commande :

$ sudo apt-get install php5-cli

Puis Exim et SpamAssassin :

$ sudo apt-get install exim4-daemon-heavy sa-exim spamassassin

Nous allons maintenant voir la configuration d’Exim et de SpamAssassin. Je ne vais pas parler de la configuration de PHP (ce sera peut-être pour un autre article).

Configuration de SpamAssassin

Éditer le fichier /etc/default/spamassassin :

ENABLED=1
CRON=1

Cela lui indique de s’activer, et de mettre ses filtres à jour en utilisant la crontab.

Démarrer de démon spamd :

$ sudo /etc/init.d/spamassassin start

Mise-à-jour des règles de spam :

$ sudo spamassassin -D --lint
$ sudo sa-update
$ sudo sa-compile
$ sudo sa-update -D channel,dns
$ sudo spamassassin -D --lint

Éditer le fichier /etc/spamassassin/local.cf :

report_safe 1
use_bayes 1
bayes_auto_learn 1
bayes_ignore_header X-Bogosity
bayes_ignore_header X-Spam-Flag
bayes_ignore_header X-Spam-Status

Pour activer la détection basique de virus intégrée à SpamAssassin, il faut éditer le fichier /etc/spamassassin/v310.pre et décommenter la ligne suivante :

loadplugin Mail::SpamAssassin::Plugin::AntiVirus

Enfin, redémarrer SpamAssassin :

$ sudo /etc/init.d/spamassassin restart

Configuration d’Exim

Pour que Exim écarte automatiquement les spams, il faut lui dire d’utiliser SpamAssassin. Éditer le fichier /etc/exim4/sa-exim.conf et commenter la ligne suivante :

SAEximRunCond: 0

Éditer le fichier /etc/exim4/update-exim4.conf.conf pour indiquer le nom de domaine local :

dc_other_hostnames='toto.com'

Comme un bourrin, je lui dis d’écouter sur toutes les interfaces réseau, sur le port 25. Il faut éditer le fichier /etc/exim4/conf.d/main/01_exim4-config_listmacrodefs :

local_interfaces = 0.0.0.0.25

Un petit détail qui a son importance, il faut dire à Exim de ne pas essayer de chercher l’identité de l’expéditeur. C’est un truc inutile, qui risque de générer un timeout qui ralenti le traitement de tous les messages reçu. Il faut donc éditer le fichier /etc/exim4/conf.d/main/02_exim4-config_options :

rfc1413_query_timeout = 0s

On va maintenant créer une route qui prendra en compte les messages reçus. On va créer le fichier /etc/exim4/conf.d/router/999_exim4-config_toto :

toto:
  debug_print = "R: toto for $local_part@$domain"
  driver = accept
  domains = +local_domains
  transport = toto_pipe

Il faut ensuite créer une méthode de transport, qui va exécuter la commande spécifiée, ouvrir un « pipe » vers ce programme et y envoyer le contenu du message reçu. On va créer le fichier /etc/exim4/conf.d/transport/999_exim4-config_toto_pipe :

toto_pipe:
  debug_print = "T: toto_pipe for $local_part@$domain"
  driver = pipe
  path = "/bin:/usr/bin:/usr/local/bin"
  command = "/chemin/vers/mon/script.php"
  return_path_add
  delivery_date_add
  envelope_to_add

J’ai mis en gras la partie qui nous intéresse le plus, celle qui détermine le programme qui sera exécuté, et à qui Exim transmettra le contenu de chaque message reçu.

On n’a plus qu’à redémarrer Exim :

$ sudo /etc/init.d/exim4 restart

Programme PHP

Le programme qui va traiter les messages doit simplement lire sur son entrée standard pour récupérer le contenu brut du message.

Voici une première version simpliste qui se contente d’enregistrer le contenu du message dans un fichier :

#!/usr/bin/php
<?php

$email = file_get_contents('php://stdin');
$filename = tempnam('/tmp', 'mail-');
file_put_contents($filename, $email);

?>

J’ai essayé d’envoyer des emails, et j’ai bien récupéré les fichiers correspondant. J’ai même pu vérifier que le filtre anti-spam fonctionnait : J’ai envoyé des emails en mettant comme adresse d’expéditeur un nom de domaine dont le champ SPF indiquait une adresse IP différente de celle que j’utilisais pour envoyer le message (vous avez suivi ? sinon dites-le moi en commentaire, j’expliquerai plus longuement), et le message a directement été refusé par Exim, sans même le transmettre au script PHP.

Pour aller plus loin, il faudrait décoder les parties MIME du message, pour en extraire les différentes informations. Il existe suffisamment de tutoriaux sur ce sujet pour pouvoir s’arrêter là.

5 commentaires pour “Traiter les emails entrants (Exim + SpamAssassin + PHP)

  1. pour le script php, je préfère une solution a base de fopen, fread, fwrite.
    Parce qu’avec file_get_contents, le script php va facilement monter en mémoire dès que les emails grossissent.

    en esperant qu’exim n’ouvre pas 10 pipes pour 10 emails de 100Mo en même temps.

  2. @Fneufneu : On est d’accord. Mon exemple de code cherchait à être le plus concis possible, pas le plus optimal 😉

    A priori, Exim instanciera autant de sous-processus et de pipes qu’il reçoit d’emails. Alors oui, il va falloir faire attention à la consommation mémoire…

  3. Bonjour,

    Tout d’abord très bon tuto, c’est très clair, mais petite question :

    Cette configuration fonctionne en catchall, mais si on veut limiter uniquement à un domaine ou même à une adresse, est-ce possible ?

    Merci

  4. Bah faut croire que @pascall n’a pas trouvé en 2 ans… dommage pour moi : je vais devoir chercher ^^

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Notifiez-moi des commentaires à venir via email. Vous pouvez aussi vous abonner sans commenter.