Un script simple de "mass mailing" en PHP
Par Yves Tannier le vendredi, juin 9 2006, 18:07 - Le PHP - Lien permanent

Mise à jour !
IMPORTANT : Une nouvelle version qui corrige un bug avec Zend_mail est disponible.
Depuis le temps qu'il fallait que j'abandonne ce misérable logiciel de gestion de newsletter sous Winchose pas pratique du tout j'ai nommé groupmail dont je tairais le nom et qui expédie depuis la (petite) ligne ADSL Wanadoo...
J'ai enfin suivi le conseil : do it your self ! Après avoir regardé avec plus ou moins d'intérêt du côté des solutions de gestion de maling list sous Linux (sympa, mailman, emzl ect...) ou des scripts de gestion de newsletter en différents languages (java, php, perl...), j'ai finalement opté pour l'utilisation de PHP en ligne de commande, CLI - Command Line Interface - pour les intimes, pour réaliser mon propre script de mass mailing.
Préface : la situation
- un script d'abonnement à la newsletter "fait maison" composé de nombreux champs : nom, prénom, adresse, téléphone, mail, profession ect... Déclaré à la CNIL, ne m'envoyez pas la police

- une table Mysql pour stocker tout ça bien entendu
- un serveur dédié sous Debian avec PHP5, PEAR, Zend Framework (version subersion car quelques bug sur le Zend_Mail component actuel), Postfix et une bonne bande passante
- un message tout en html (avec quand même un texte alternatif disponible pour les fanatiques de mutt).
- et enfin, tout de même 25000 abonnés à cette liste
Introduction : comment ça marche ?
- On exécute le script en ligne de commande (dans un screen ou en tache de fond pour le laisser tourner ensuite)
- Le script (interactif) demande quel fichier html il doit utiliser pour le message. Par défaut, il propose le dernier fichier trouvé dans le répertoire $dir_html
- Les messages sont envoyés un par un par paquets de 100 (nombre de messages par paquet configurable)
- Une pause de 15 secondes est effectuée entre chaque paquets. On en profite pour réinitailiser la connection avec le serveur SMTP ( le temps de la pause est également configurable).
- A chaque envoi réussi (la validation de l'envoi est à revoir), un champ booleen "envoye" est mis à jour dans la table des abonnés. En cas de plantage, il suffit donc de relancer le script pour reprendre l'expédition.
- Avant de relancer le script pour une nouvelle lettre d'informations, une option permet de remettre le flag "envoye" à zéro.
Chapitres suite et fin 
Avant même qu'il ne soit fini, voici la première version pre-beta-alpha-0.0.1 (soyons fou et proposons des trucs en beta comme Gmail).
Prérequis et note diverses :
- le script ne fonctionne que avec PHP5
- un certain nombre de variables sont à configurer au début du fichier pour que ça fonctionne sur d'autres configurations (voir les commentaires)
- le paquet PEAR::DB est nécessaire
- les components Zend_Mail et Zend_Mime sont nécessaires
- la base de données peut-être autre chose que Mysql (voir les possibilités de Pear::DB)
Pour tester l'envoi sur une seule adresse mail :
# sendnews -e -t
Pour remettre le flag "envoye" à zéro dans la table (après un envoi réussi) :
# sendnews -r
Pour envoyer :
# sendnews -e
Pour visualiser l'aide :
# sendnews --help
Il reste pas mal de chose à faire pour que ce système d'envoi de newsletter soit quand même un peu plus complet :
- gérer les bounces, c'est à dire les retours sur les adresses non valides (c'est un "gros" boulot)
- vérifier le mode silencieux (pour lancer le programme dans un cron)
- activer les erreurs et le log qui va avec
- utiliser PDO de PHP5 à la place de PEAR::DB
- faire un paquet tout en un avec Zend_Mail et Zend_Mime.
Enfin le code !
Vous pouvez également télécharger directement le script
#!/usr/bin/php
<?php
// on supprime la limite d'execution d'un script php
set_time_limit(0);
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
/**
* Envoi d'une lettre d'information
*
* PHP version 5
*
* LICENSE: Ce programme est un logiciel libre distribue sous licence GNU/GPL
*
* @author Yves Tannier <yves_chez_grafactory.net>
* @copyright 2006 Yves Tannier
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @version 0.1.0
* @link http://www.grafactory.net
*/
/**
* librairie Pear::DB
*/
require_once 'DB.php';
/**
* librairie Zend_Mail
*/
require_once 'Zend/Mail.php';
$host = "localhost"; // nom hote de la BDD
$user = "user"; // utilisateur
$pass = "pass"; // mot de passetheatre
$bdd = "base"; // base de donnees
$type = "mysql"; // type de bdd
// expediteur
$sender = "xxx@xxx.tld";
$sender_name = "John Doe";
// adresse email du test par defaut
$defaut_send = "xxx@xxx.tld";
// sujet
$subject = "Lettre hebdomadaire";
// pour les clients qui ne lise pas le html.
$msg_text = "Si vous ne pouvez pas lire correctement ce message rendz-vous a l'adresse suivante...";
// repertoire de stockage des messages Html
$dir_html = "/home/web/newsletter/date/";
// table
$table_users = "newsletter_abonnes";
// champs de la table
$table_fields = array('id' => 'id',
'email' => 'email');
// champ flag envoi OK
$field_send = "envoye";
// instruction WHERE si besoin (peut être vid)
$where_more = " AND thea=1 ";
// serveur smtp
$smtp_server = "localhost";
// nombre d'envoi avant la pause
$per_send = 100;
// duree de la pause en seconde
$pause_time = 15;
// aide contextuelle
$help_string = "Parametres :
-s : mode silencieux (inactif)
-e : lancer l'envoi
-r : remettre le flag envoye a null
-t : envoyer un message de test
-l : voir les erreurs (inactif)
-h, --help : cette aide...
";
// recuperer les parametres passe en CLI
foreach($_SERVER['argv'] as $param) {
switch($param) {
case '-s' :
$silent = true;
break;
case '-e' :
$execute = true;
break;
case '-r' :
$renew = true;
break;
case '-t' :
$testsend = true;
break;
case '-l' :
$viewlog = true;
break;
case '-h' :
case '--help' :
echo $help_string;
exit();
case $_SERVER['SCRIPT_NAME'] :
unset ($_SERVER['argv'][0]);
break;
default :
echo "
Parametre inconnu : ".$param."
";
exit();
}
}
if (empty($_SERVER['argv'])) {
echo "
Veuillez preciser au moins un parametre
";
echo $help_string;
}
/* classe Zend_Mail modifiee pour ajouter
* la methode de remise a zero du header 'To'
*/
class MyZend_Mail extends Zend_Mail
{
public function removeAddTo()
{
unset($this->_headers[To]);
array_splice($this->_recipients,0);
}
}
// connection a la BDD
$dsn = "$type://$user:$pass@$host/$bdd";
$db = DB::connect($dsn);
$db->setFetchMode(DB_FETCHMODE_ASSOC);
// envoi
if ($execute) {
// par cours du répertoire pour le dernier fichier place
$dir = scandir($dir_html);
$files = array();
foreach ( $dir as $file ) {
if (!preg_match('/^\./', $file)) {
$files['name'][0] = $file;
$files['date'][0] = filemtime($dir_html.$file);
}
}
$defaut_file = $files['name'][0];
// on recupere le nom du fichier HTML
if(!$silent) {
fwrite(STDOUT, "Entrez le nom precis du fichier html (defaut : ".$defaut_file.") : ");
$file_name = trim(fgets(STDIN));
}
if (empty($file_name)) {
$file_name = $defaut_file;
}
// le mail en html depuis la page html
$handle = @fopen($dir_html.$file_name, "r");
if ($handle) {
while (!feof($handle)) {
$msg_html .= fgets($handle, 4096);
}
fclose($handle);
}
else {
echo "Le fichier html du message n'a pas ete trouve ou n'a pas pu etre ouvert
";
exit();
}
require_once 'Zend/Mail/Transport/Smtp.php';
$tr = new Zend_Mail_Transport_Smtp($smtp_server);
// construction du message
$mail = new MyZend_Mail();
$mail->setBodyText($msg_text);
$mail->setBodyHtml($msg_html);
$mail->setFrom($sender, $sender_name);
$mail->setSubject($subject);
// on connecte
$tr->connect();
// si ce n'est pas un test
if (!$testsend) {
// requete
$res = $db->query("SELECT ".$table_fields['id'].",".$table_fields['email']." FROM ".$table_users." WHERE 1 ".$where_more." AND ".$field_send."=0");
// incrementation
$i = 0;
// boucle
while($row = $res->fetchRow()) {
// a qui envoye ?
$mail->addTo($row[$table_fields['email']]);
// la pause (deconnection/reconnection)
if (($i % $per_send) == 0 && $i>0) {
$tr->disconnect();
if (!$silent) {
echo "==============================> Pause au niveau ".$i."
";
for($s=0;$s<$pause_time;$s++) {
echo ".";
sleep(1);
}
echo "
";
}
$tr->connect();
}
// envoi (manque le retour d'erreur)
try {
$mail->send();
$up = $db->autoExecute($table_users, array($field_send=>1), DB_AUTOQUERY_UPDATE,$table_fields['id'].'='.$row['id']);
if (!$silent) {
echo "-> Envoi en cours a ".$row[$table_fields['email']]." ".$row[$table_fields['id']]."
";
}
}
catch(Zend_Mail_Exception $e) {
echo $e->getTrace()."
";
}
// on supprime le mail dans les headers
$mail->removeAddTo($row[$table_fields['email']]);
$i++;
}
}
// sinon on test l'envoi
else {
if(!$silent) {
fwrite(STDOUT, "Entrez l'adresse de test (par defaut : ".$defaut_send.") : ");
$defaut_name = trim(fgets(STDIN));
if (!empty($defaut_name)) {
$defaut_send = $defaut_name;
}
}
// a qui envoye ?
$mail->addTo($defaut_send);
// envoi du message
$mail->send();
}
echo "Envoi termine !
";
}
// on remet le flag d'envoi a 0
if($renew) {
$renew = $db->autoExecute($table_users, array($field_send=>0), DB_AUTOQUERY_UPDATE);
if (!$silent) {
echo "-> Le flag d'envoi a bien ete reinitialise
";
}
}
?>
Commentaires
J'ai eu le même problème de newsletter que ce que tu décris, avec un mailer shareware assez onéreux en plus ! Merci beaucoup pour la mise à disposition de ce script très bien écrit.
Pour les besoins de ma newsletter (qui me semblent des besoins généréraux !), je vais devoir adapter ce script de façon à pouvoir utiliser un nombre arbitraire de colonnes dans la table des abonnés (nom, prenom, etc.) pour individualiser les messages. Il suffit juste d'une convention syntaxique dans le code HTML source, par exemple le nom de la [[colonne]] entre doubles crochets, et de faire une boucle de substitution des occurrences correspondant aux noms des colonnes.
Si tu as vocation à intégrer cette fonction dans une roadmap, je t'envoies volontiers la modif proposée. Cdlt,
[Suite du mail précédent]
aussitôt dit aussitôt fait... (et par ailleurs une petite erreur corrigée sur un tableau associatif).
A ta dispo le cas échéant par échange de mail.
a++
RC
Bonjour,

Je suis ravi de voir que ce script intéresse quand même quelqu'un
Bonne idée que de pouvoir personnaliser l'envoi. Je veux bien que tu me fasses parvenir ta modification. Je dois toujours faire évoluer le script et notamment la partie consacrée à la gestion des bounces.
Dès que j'ai fais quelques modifs, je poste une nouvelle version sur ce blog.
Mon adresse mail c'est yves chez grafactory
@++
Yves
Bonjour,
Petite question: dans cette partie du code
if (!$silent) {
echo "==============================> Pause au niveau ".$i." ";
for($s=0;$s<$pause_time;$s++) {
echo ".";
sleep(1);
}
echo " ";
}
Si on est 'silent' on ne passe pas dans le 'for sleep(1)'. Il n'y aurait donc pas de pause. Est-ce normal?
Bonjour,

le $silent, c'est pour le mode silencieux que je n'ai pas encore implémenté
Utile pour être lancé par cron par exemple.
Donc, pour répondre à ta question : ce n'est pas normal. Il doit y avoir aussi une possibilité de pause en mode silencieux. C'est juste que le echo n'a pas lieux d'être en mode silencieux. Je vais essayer de publier une nouvelle version rapidement.
++
Proposition:
if (!$silent) {
echo "==============================> Pause au niveau ".$i." ";
for($s=0;$s<$pause_time;$s++) {
echo ".";
sleep(1);
}
echo " ";
}
else
{
sleep($pause_time);
}
}
Difficile de trouver plus simple
Bonjour,
Je suis actuellemtn en train de chercher une manière d'optimiser l'envoi d'une newsletter en php, (actuellement fonction mail())) et je me suis pencher sur l'utilisation de PEAR.
J'aimerai connaître en combien de temps vous réussissez à envoyer votre newsletter à vos 25000 abonnés.
Merci d'avance....
Je n'ai pas caculé exactement mais je dirais 2/3 heures avec un réglage assez lent du débit (pause de 15 sec tout les 100, configurable dans le script). PEAR ne sert que pour la couche d'accès aux données via MDB2. L'envoi se fait avec la librarie Zend_Mail de Zend Framework qui me plaisait mieux que PEAR pour le côté mail.
Bonjour
Ce script semble être ce que je recherche. Est-ce qu'il permet l'envoi de mail par groupes (avec affichage de case à cocher) ? à bientot
Bonjour,
Merci de ces contributions.
Personnellement je me suis inspiré de cette source mais utilise
Zend_Mail() tel quel et Zend_Mail_Transport_Sendmail(), qui repose sur la fonction mail() de PHP.
Quelqu'un sait-ils quels sont les avantages / inconvénients d'utiliser l'envoi smtp ou la fonction mail de PHP ? (Limitations ? Nombre d'envois max etc ? spam ?) ?
Merci
Bonjour,
Il me semble que Zend_Mail_Transport se connecte directement au smtp sans passer par la fonction mail() ?! L'avantage d'interroger directement le smtp sans passer par la fonction mail(), c'est qu'une seule connexion smtp est utilisée pendant toutes la durée de l'exécution du script. C'est la théorie, car pour envoyer beaucoup de message, il me semble que j'avais essayé et que ça ne fonctionnait pas franchement. Il fallait "régulièrement" recréer une connection... Je dois prochainement retravailler ce script. Je vais donc regarder plus en profondeur les options... Je voulais aussi regarder du côté de Swift Mailer http://www.swiftmailer.org/... Dès que j'ai le temps
Bonjour,
J'aimerais savoir comment vous gérez les soft bounces et hard bounces avec votre scripts, le fait de forcer des envois sur une boite aux lettres inexistante risque de vous identifier comme spameur.
Je suis à la recherche d'une solution complémentaire a php pour gerez l'envoi d'email.
Je pensais a un script qui lirais un fichier csv, ou xml comprenant la liste des adresse de l'envoi, et qui retournerai un fichier de log avec les erreures ( SPAM, FULL, NO SUCH USER, TIMEOUT, etc... ), si quelqu'un pouvais me conseiller, vers ou orienter mes recherches ( languages, sites web, logiciels etc... )
Merci
Bonjour
J'ai développé une nouvelle version de mon script qui gère les hards et soft bounce. Concernant la gestion même des bounces ca fonctionne correctement. Pour faire court, j'ai ajouté un "parseur" des mails contenus dans la boite de retour (return-path) que je lance suite à chaque mailing. Par contre je n'ai pas encore fini de tester complètement le script pour le publier.
Les personnes intéressées pour tester ce nouveau script peuvent me le demander sur les commentaires.
Bonjour,
effectivement je suis intéressé par le parseur de retour en erreur. c'est assez spécial comme discipline.
Merci
J'ai quasi fini la doc...
Bonjour,
Je suis actuellement entrain de travailler sur un site emailler pour mon tfe (travail de fin d'étude). Je ne souhaite pas utiliser un script tout faire, mais en retravailler un pour bien l'intégrer dans mon site.
Je remercie le créateur du script de partager ses connaissances (je ne savait pas que il existait des fonctions zend pour la gestion d'email, mais je connaissant l'existant de pear mail...).
Je suis justement à la recherche pour la gestion des bounces, afin d'éliminer les membres qui n'aurai pas leur adresse email actif, car le site n'a plus effectué d'envois depuis plusieurs mois...
Je vous serai reconnaissant, si vous pouvez donner la source pour la gestion de ses erreurs,
Merci d'avance,
Ludovic
Je n'ai pas relut le site mais le script fonctionne : http://www.grafactory.net/sendnews/
Pour la gestion des bounces, il manque encore certaines règles mais ça marche dèjà pas mal.Je vais regarder ton code source pour l'adapter à mes besoins merci beaucoup ...
Ludovic