Le jour où vos confirmations de commande arrêtent de partir, aucun voyant ne s’allume nulle part, et c’est bien ce qui rend la panne pénible à repérer avant le premier acheteur mécontent. Du vert partout dans le back-office, des commandes qui tombent comme d’habitude, et pour seule alerte quelqu’un qui râle. Je tombe souvent là-dessus. Trois coupables reviennent presque à chaque fois : un état de commande incapable de réveiller quoi que ce soit, un modèle d’e-mail introuvable dans la langue active, ou une méthode d’envoi mal paramétrée.
Tout ça vaut pour la 1.7.8, pour la 8.x et pour la 9.x (la couche d’envoi n’est plus la même en 9, j’y reviens plus bas).

Faire parler le moteur d’envoi
Pas question de bouger un réglage tant que je n’ai pas une erreur affichée sous les yeux, et le back-office vous en fournit une sous Paramètres avancés, rubrique E-mail, où le formulaire de test expédie un courrier avec ce qui est en place avant de recopier à l’écran ce que dit le serveur. Démarrez par là. Vous n’aurez pas tout non plus, puisqu’il tâte la connexion sans rejouer une seule fois le parcours complet d’un e-mail transactionnel.
Mon script ci-dessous pousse plus loin. Il commence par monter le contexte boutique, vous recrache ensuite la configuration que la classe Mail lit pour de bon, et termine en appelant la méthode d’envoi elle-même sur un gabarit du cœur. Faites-le tourner en ligne de commande sous l’utilisateur système du site, puis jetez-le derrière vous (ce genre de fichier n’a rien à faire en ligne). Si le paramètre die traîne à false, c’est exprès : j’aime mieux retrouver la cause au journal que tomber sur une page blanche qui ne m’apprend rien.
<?php
// test-mail.php : a placer a la racine de la boutique, a executer en CLI
// php test-mail.php puis a SUPPRIMER une fois le diagnostic termine.
// Le prefixe des tables est ici ps_ : adaptez-le a votre installation.
require_once __DIR__ . '/config/config.inc.php';
$destinataire = 'votre.adresse@exemple.fr';
$idLang = (int) Configuration::get('PS_LANG_DEFAULT');
// Rappel de la configuration reellement utilisee par Mail::send()
foreach (['PS_MAIL_METHOD', 'PS_MAIL_SERVER', 'PS_MAIL_SMTP_PORT', 'PS_MAIL_SMTP_ENCRYPTION', 'PS_MAIL_USER', 'PS_SHOP_EMAIL'] as $cle) {
echo str_pad($cle, 26) . ' = ' . var_export(Configuration::get($cle), true) . PHP_EOL;
}
// Le template contact existe dans le core pour toutes les langues installees.
$resultat = Mail::send(
$idLang,
'contact',
'Test envoi PrestaShop',
[
'{email}' => $destinataire,
'{message}' => 'Message de diagnostic.',
],
$destinataire,
null,
null,
null,
null,
null,
_PS_MAIL_DIR_,
false // $die : on ne veut pas couper le script, on veut lire le log
);
echo PHP_EOL . 'Mail::send() a retourne : ' . var_export($resultat, true) . PHP_EOL;
echo 'En cas de false, la cause exacte est ecrite dans la table ps_log' . PHP_EOL;
echo '(back-office : Parametres avances > Journaux).' . PHP_EOL;
Sendmail, SMTP : ce que PrestaShop fait vraiment
Le vocabulaire du back-office crée une confusion qui dure. Son option baptisée envoi par PHP ne réveille à aucun moment la fonction mail native, et je le redis parce que l’idée a vraiment la vie dure d’un article à l’autre. Depuis la 1.7, une valeur 1 dans la clé PS_MAIL_METHOD fait construire à PrestaShop un transport sendmail, autrement dit le binaire installé sur la machine. Une valeur 2 ? SMTP authentifié. Et une valeur 3 coupe tout, en faisant ressortir la méthode avant le premier essai (commencez par celle-là).
La mécanique interne diffère d’une branche à l’autre, avec SwiftMailer qui tient le poste en 1.7.8 comme en 8.x, tandis que la 9.0 a basculé sur Symfony Mailer. Cette bascule arrive en 9, nulle part avant. Pour information, je suis allé lire le code dans les trois branches, parce que beaucoup d’articles racontent que la 8 tournerait déjà sous Symfony Mailer, et c’est inexact. La requête suivante étale l’état exact de votre configuration, avec les valeurs par boutique que l’interface garde pour elle.
-- Le prefixe ps_ est celui de l'installation par defaut : adaptez-le.
-- id_shop a NULL signifie une valeur globale, sinon la valeur est propre
-- a une boutique du multiboutique.
SELECT c.name, c.id_shop, c.value, c.date_upd
FROM ps_configuration c
WHERE c.name IN (
'PS_MAIL_METHOD',
'PS_MAIL_SERVER',
'PS_MAIL_USER',
'PS_MAIL_SMTP_PORT',
'PS_MAIL_SMTP_ENCRYPTION',
'PS_MAIL_TYPE',
'PS_SHOP_EMAIL',
'PS_MAIL_DKIM_ENABLE',
'PS_LOG_EMAILS'
)
ORDER BY c.name, c.id_shop;
-- Lecture des valeurs :
-- PS_MAIL_METHOD 1 = transport sendmail du serveur
-- 2 = SMTP (constante Mail::METHOD_SMTP)
-- 3 = envoi coupe (constante Mail::METHOD_DISABLE)
-- PS_MAIL_TYPE 1 = HTML, 2 = texte, 3 = les deux (valeur par defaut)
-- PS_MAIL_SMTP_ENCRYPTION off, tls ou ssl
-- PS_MAIL_DKIM_ENABLE absente en 1.7.8, presente en 8.x et 9.x
Reprendre la main quand le back-office est muet
Il arrive que ce formulaire de test vous échappe, soit parce que la relance du mot de passe ne part plus, soit parce que la boutique tourne en maintenance. Sachez alors que vos réglages de messagerie habitent la table de configuration, et pas du tout les fichiers où Symfony range ses paramètres (oui, ça déroute). Ces fichiers-là embarquent bien des entrées mailer, laissées derrière eux par le squelette Symfony, sauf que la classe Mail ne va jamais les lire, si bien que vous aurez beau les retoucher, vos envois transactionnels n’en verront jamais la couleur.
Du coup, je bascule sur un script court appuyé sur l’API de configuration, qui m’évite les chausse-trapes du multiboutique et qui vide le cache proprement. Chez presque tous les fournisseurs, prenez un mot de passe d’application, surtout pas celui du compte. Pensez aussi à l’adresse d’expédition : elle doit appartenir à un domaine que votre SMTP accepte, sinon le relais, à l’autre bout, vous refusera le passage.
<?php
// fix-smtp.php : bascule la boutique en SMTP quand le back-office est inaccessible.
// A executer en CLI depuis la racine, puis a SUPPRIMER.
// Prefixe ps_ suppose : adaptez-le a votre installation.
require_once __DIR__ . '/config/config.inc.php';
// Mail::METHOD_SMTP vaut 2. La valeur 1 correspond au transport sendmail
// du serveur, la valeur 3 (Mail::METHOD_DISABLE) coupe tout envoi.
Configuration::updateValue('PS_MAIL_METHOD', Mail::METHOD_SMTP);
Configuration::updateValue('PS_MAIL_SERVER', 'smtp.mon-hebergeur.fr');
Configuration::updateValue('PS_MAIL_SMTP_PORT', 587);
// Valeurs acceptees par PrestaShop : off, tls, ssl.
Configuration::updateValue('PS_MAIL_SMTP_ENCRYPTION', 'tls');
Configuration::updateValue('PS_MAIL_USER', 'boutique@mon-domaine.fr');
Configuration::updateValue('PS_MAIL_PASSWD', 'mot-de-passe-application');
// L'expediteur doit appartenir au domaine autorise par le serveur SMTP.
Configuration::updateValue('PS_SHOP_EMAIL', 'boutique@mon-domaine.fr');
// Journalise les envois reussis dans la table ps_mail.
Configuration::updateValue('PS_LOG_EMAILS', 1);
echo 'Configuration de messagerie mise a jour.' . PHP_EOL;
Le modèle absent dans la langue active
Quand je relis la classe Mail, son chemin de recherche ne bouge pas : thème actif d’abord, thème parent ensuite, dossier mails du cœur pour finir. À chaque étage elle tente la langue du mail, retombe sur ce que la boutique affiche comme langue par défaut, puis termine en anglais (cet ordre-là, jamais un autre). Elle ne trouve aucun couple de fichiers ? Elle inscrit au journal applicatif sa ligne de modèle introuvable, et vous retourne false sans lever quoi que ce soit. Côté écran, silence radio.
PS_MAIL_TYPE, voilà le réglage qui vous coûtera des heures. Sa valeur d’usine expédie du HTML en même temps que du texte, donc il attend les deux extensions, un html et un txt. Prenez un dossier de langue où la migration n’a rapatrié que le html : vous tenez le symptôme au complet (je suis tombé dessus plus d’une fois). Même histoire du côté du module de contact, qui traîne ses modèles à lui. Greffer un champ de plus dans le formulaire de contact oblige à répercuter le changement jusque dans le gabarit du courrier, et sur une boutique restée en 1.6 la manipulation s’écarte assez pour avoir sa propre marche à suivre, retoucher ce même formulaire en 1.6. Mon script ci-dessous fait le tour langue par langue, du cœur aux modules en passant par le thème.
#!/bin/bash
# A lancer depuis la racine de la boutique.
# Compare les modeles presents dans la langue de reference (en) avec ceux
# de la langue a controler, pour le core et pour le theme actif.
LANGUE_REF="en"
LANGUE="fr"
THEME="classic" # nom du dossier dans themes/ : adaptez-le
for BASE in "mails" "themes/${THEME}/mails"; do
[ -d "${BASE}/${LANGUE_REF}" ] || continue
echo "### ${BASE}"
for FICHIER in "${BASE}/${LANGUE_REF}"/*.html; do
[ -e "$FICHIER" ] || continue
NOM=$(basename "$FICHIER" .html)
for EXT in html txt; do
CIBLE="${BASE}/${LANGUE}/${NOM}.${EXT}"
[ -f "$CIBLE" ] || echo "manquant : ${CIBLE}"
done
done
done
# Meme controle pour les modeles fournis par les modules.
for DOSSIER in modules/*/mails/"${LANGUE_REF}"; do
[ -d "$DOSSIER" ] || continue
MODULE=$(echo "$DOSSIER" | cut -d/ -f2)
for FICHIER in "$DOSSIER"/*.html; do
[ -e "$FICHIER" ] || continue
NOM=$(basename "$FICHIER" .html)
CIBLE="modules/${MODULE}/mails/${LANGUE}/${NOM}.html"
[ -f "$CIBLE" ] || echo "manquant : ${CIBLE}"
done
done
Régénérer les modèles depuis les sources
Derrière le html et le txt que vous voyez dorment des gabarits Twig, logés sous le dossier qui regroupe les thèmes de mails (le cœur en fournit deux). Au lancement, la commande de génération reconstruit d’abord ceux du cœur sous mails, puis ceux de chaque module chez lui, pour la locale et le thème de mails désignés par vos soins.
Deux précautions, que je rappelle toujours. Tant que l’option dédiée n’est pas passée, aucun fichier n’est écrasé (une chance quand vos modèles sont personnalisés, une déception complète quand vous attendiez du neuf). Autre point, c’est le dossier du cœur qui reçoit l’écriture et votre thème reste à l’écart, donc un thème muni de ses propres mails attendra que vous y recopiiez les fichiers tout juste produits. Sauvegardez ce dossier avant de lancer la moindre commande.
#!/bin/bash # Regeneration des modeles core et modules depuis les sources Twig. # A lancer depuis la racine, avec l'utilisateur systeme du site. # Arguments : le theme de mails (modern ou classic) puis la locale. php bin/console prestashop:mail:generate modern fr-FR # --overwrite (-o) ecrase les fichiers deja presents : a n'utiliser # qu'apres avoir sauvegarde vos modeles personnalises. # php bin/console prestashop:mail:generate modern fr-FR --overwrite # Les fichiers du core atterrissent dans mails/fr/, ceux des modules # dans modules/<module>/mails/fr/. Le theme de la boutique n'est pas # touche : recopiez-y vos personnalisations ensuite. # Purge du cache pour que PrestaShop relise l'arborescence. rm -rf var/cache/prod/* var/cache/dev/*
L’état de commande qui n’envoie rien
Deux colonnes commandent l’e-mail déclenché par un changement de statut. L’indicateur d’envoi, vous le trouvez du côté des états de commande, pendant que la table de traduction associée retient comment s’appelle le modèle, une entrée par langue. Encore faut-il qu’elles disent la même chose. Entre un import de langue, une migration et un module de paiement qui invente ses statuts à lui, vous vous retrouvez très souvent avec l’indicateur sur 1 quand le nom de modèle reste blanc, ou la situation inverse.
Conséquence, plus rien ne part, et l’écran ne vous souffle pas un mot. Passez tous les statuts en revue, pas uniquement celui qui coince, parce que ce genre de défaut arrive rarement seul. Méfiez-vous surtout de ceux qu’un module de paiement a posés (c’est là que ça casse le plus souvent) : c’est le module qui donne son nom dans cette table des états, et vos réglages faits à l’interface leur passent parfois au-dessus.
-- Etats de commande, indicateur d'envoi et modele associe, langue par langue.
SELECT os.id_order_state,
osl.id_lang,
osl.name,
os.send_email,
osl.template
FROM ps_order_state os
INNER JOIN ps_order_state_lang osl ON osl.id_order_state = os.id_order_state
WHERE os.deleted = 0
ORDER BY os.id_order_state, osl.id_lang;
-- Le cas qui casse en silence : la case d'envoi est cochee mais aucun
-- modele n'est renseigne pour cette langue.
SELECT os.id_order_state, osl.id_lang, osl.name
FROM ps_order_state os
INNER JOIN ps_order_state_lang osl ON osl.id_order_state = os.id_order_state
WHERE os.deleted = 0
AND os.send_email = 1
AND (osl.template IS NULL OR osl.template = '');
-- Les statuts poses par un module de paiement portent son nom ici.
SELECT id_order_state, module_name, send_email
FROM ps_order_state
WHERE module_name IS NOT NULL AND deleted = 0;
Lire les bons journaux
Deux tables ont le mot mail dans leur nom, et chacune vous raconte le contraire de sa voisine. Celle que remplit le journal applicatif conserve les erreurs, les modèles introuvables comme les exceptions levées par la couche d’envoi. Quand plus rien ne part, j’ouvre celle-là. L’autre ne retient que ce qui est bien parti, à condition que la journalisation des e-mails soit cochée chez vous.
Du coup, voir cette table d’envois à vide fait conclure à la panne, alors que le plus souvent personne n’avait simplement coché l’option. Personnellement, je l’active le temps du diagnostic, puis je la vide régulièrement (sur une boutique vivante elle gonfle vite et, quelques semaines plus tard, elle n’a plus rien à dire). Les deux requêtes qui suivent traitent ces deux lectures, plus le ménage. Envie que je vienne regarder chez vous ? Passez par le formulaire de contact, c’est la route la plus courte.
-- Les echecs d'envoi sont ecrits par PrestaShopLogger dans ps_log. -- severity : 1 information, 2 avertissement, 3 erreur, 4 majeur. SELECT id_log, severity, error_code, object_type, date_add, message FROM ps_log WHERE message LIKE '%mail%' OR message LIKE '%Swift%' OR message LIKE '%Mailer%' ORDER BY date_add DESC LIMIT 50; -- ps_mail ne contient que les envois qui ont abouti, et seulement si -- PS_LOG_EMAILS vaut 1. Une table vide ne prouve donc rien a elle seule. SELECT DATE(date_add) AS jour, template, COUNT(*) AS envois FROM ps_mail GROUP BY jour, template ORDER BY jour DESC LIMIT 30; -- Allegement : on ne garde que les trois derniers mois. DELETE FROM ps_mail WHERE date_add < DATE_SUB(NOW(), INTERVAL 90 DAY);
Ce que je vérifie ensuite
Flux rétabli, faites-vous une vraie commande pour de faux, depuis une adresse qui n’a rien à voir avec la boutique. Examinez le contenu du courrier autant que son arrivée : nom de l’acheteur, montant, liens qui cliquent. Un gabarit reconstruit depuis les sources abandonne au passage les personnalisations que personne n’avait reportées dans le thème. Ça se repère en une seconde.
Attaquez ensuite la délivrabilité, qui n’a rien à voir avec un envoi en panne. Un SPF qui couvre bien l’expéditeur, une signature DKIM valable, et vos confirmations arrêtent de finir en indésirable. Signer nativement, c’est possible en 8.x comme en 9.x grâce à quatre clés de configuration faites pour ça, alors que la 1.7.8 vous renvoie signer plus bas, sur la machine qui expédie. Une dernière réserve, et elle pèse lourd : tout ce que je viens d’écrire traite l’envoi, jamais l’arrivée. Un filtrage en amont chez le destinataire, ou un domaine à la réputation douteuse, et votre courrier part très bien sans jamais atterrir nulle part. Je range mes autres décorticages de ce genre sur le blog.
Questions fréquentes
Pourquoi mes e-mails de commande ne partent-ils plus du jour au lendemain ?
La plupart du temps, dans les cas que je reprends, la configuration d’envoi a bougé toute seule : on a révoqué un mot de passe d’application, l’hébergeur a refermé son port sortant, ou une migration a remis dans PS_MAIL_METHOD une valeur inadaptée (ce trio-là revient sans arrêt). Premier geste, lisez cette clé en base. À 3, l’envoi est coupé et la méthode ressort sans rien essayer. Et si cette montée de version vous a bloqué des pages au passage, réglez d’abord l’erreur 500 qui suit une mise à jour.
Deuxième cause récurrente, la langue active n’a tout simplement pas ses modèles. PrestaShop va chercher le html et le txt dans votre thème, grimpe ensuite vers le parent, atterrit chez le cœur, et à chaque étage il tente la langue du courrier, puis celle par défaut, puis l’anglais. Rien trouvé ? L’envoi rate en silence, et votre seule trace sera une ligne au journal applicatif.
Faut-il utiliser le SMTP plutôt que la méthode par défaut ?
Sur du mutualisé, oui, c’est ce que je conseille presque systématiquement. La méthode d’usine s’en remet au transport sendmail du serveur, et là, adresse d’expédition réelle comme réputation vous échappent complètement. Le SMTP authentifié vous rattache à un domaine clairement identifié, faute de quoi SPF et DKIM ne pèsent plus rien.
Le port, c’est votre fournisseur qui le dicte, pas une règle universelle. Pour le chiffrement, PrestaShop n’accepte que trois valeurs (off, tls et ssl), pas une de plus. Donnez un port orphelin, ou un serveur sans son port, et la classe Mail inscrit une erreur parlante au journal avant de s’arrêter là. Et ça, c’est déjà une piste.
Comment savoir si un état de commande déclenche bien une notification ?
Il faut que deux informations collent. Côté états de commande, vous avez l’indicateur d’envoi, et du côté de la table de traduction associée, comment ce modèle s’appelle, une entrée par langue. Indicateur sur 1 et nom de modèle vide dans la langue de l’acheteur ? Aucun mail.
Les plus exposés restent les statuts maison des modules de paiement, écrits directement en base pendant l’installation du module. Pour ma part, j’interroge les deux tables en une requête au lieu d’ouvrir les fiches une par une. Sur une boutique multilingue, ça vous fera gagner des heures.
La commande de régénération va-t-elle écraser mes modèles personnalisés ?
Pas avant que vous ne passiez l’option dédiée. De base, elle saute tout fichier déjà posé et fabrique uniquement les absents. Pour remettre d’aplomb un dossier de langue troué, c’est précisément ce que je cherche.
En revanche, l’écriture atterrit sous mails, côté cœur, ainsi que dans les dossiers propres aux modules, et votre thème n’est pas concerné du tout. Vos modèles personnalisés logés dans le thème, elle ne les lit pas et n’y touche pas, donc c’est à vous d’y recopier à la main ce qu’elle vient de produire. Dupliquez le dossier d’abord (le retour en arrière n’existe pas).
Quelle différence entre PrestaShop 8 et 9 pour l’envoi d’e-mails ?
La bibliothèque du dessous. SwiftMailer en 1.7.8 et en 8.x, Symfony Mailer en 9.0. Vos messages d’erreur au journal changent, la négociation du chiffrement aussi, et pourtant, j’ai vérifié, les clés de configuration n’ont pas bougé d’un pouce.
La signature DKIM, elle, marque une vraie rupture de fonctionnement. Signer depuis le back-office est au programme des branches 8.x et 9.x, hors de portée en 1.7.8. Toujours en 1.7.8 avec des mails qui finissent en indésirable ? Signez alors en amont, sur le relais SMTP ou sur la machine qui expédie. Tâtonner sur une production, je préfère vous l’éviter, et c’est précisément l’objet d’un accompagnement sur mesure.