Si vous avez déjà reçu un email WordPress “mot de passe réinitialisé” au rendu cassé, ou pire, un message qui arrive en texte brut alors que vous aviez soigneusement préparé un template HTML, vous avez déjà touché le vrai problème : WordPress envoie des emails depuis des endroits très différents (core, plugins, formulaires), avec des en-têtes variables, et sans système de templates HTML unifié.

Le problème / Le besoin

Vous voulez que les emails sortants de votre site aient une identité cohérente : logo, couleurs, typographie, pied de page légal, et idéalement une version texte alternative. Vous voulez aussi éviter les “emails qui n’arrivent pas” à cause d’en-têtes mal formés ou d’un HTML trop lourd.

Ce besoin concerne autant les sites simples (emails de commentaires, de compte utilisateur) que les sites plus avancés (WooCommerce, formulaires, membership). Dans mon expérience, le point de friction arrive quand on mélange plusieurs sources d’emails : un thème qui force le HTML, un plugin SMTP qui réécrit les headers, et un builder qui injecte du CSS non compatible email.

À la fin, vous saurez :

  • forcer un envoi HTML propre via wp_mail() sans casser les plugins,
  • centraliser un template HTML réutilisable,
  • gérer des variables (titre, contenu, bouton, footer) de façon sûre,
  • déboguer les headers et le rendu,
  • prévoir des variantes (emails admin, emails utilisateurs, emails “transactionnels”).

Résumé rapide

  • On crée un mini-plugin qui encapsule wp_mail() et applique un template HTML systématique.
  • On utilise des filtres WordPress fiables : wp_mail_content_type, wp_mail_from, wp_mail_from_name, et wp_mail (pour inspecter/ajuster les arguments).
  • On génère le HTML avec une fonction de rendu qui accepte des variables, et on échappe correctement (XSS dans les emails, ça arrive).
  • On ajoute une version texte (fallback) et des en-têtes cohérents.
  • On documente une stratégie de test (MailHog / Mailpit, logs, inspection des headers) et une méthode de dépannage.

Quand utiliser cette solution

  • Vous voulez une charte email unifiée pour les emails envoyés via wp_mail() (core + vos plugins, dans la mesure où ils passent par l’API WordPress).
  • Vous avez besoin d’un wrapper “maison” pour vos emails personnalisés (inscription, relance, notification interne).
  • Vous devez corriger des emails qui partent en texte brut alors que vous avez un HTML prévu.
  • Vous voulez ajouter un pied de page légal, un lien de désinscription (si pertinent), ou des informations de contact.
  • Vous avez des exigences de délivrabilité et vous voulez maîtriser le From: et le Reply-To:.

Quand ne PAS utiliser cette solution

  • Vous envoyez des campagnes marketing (newsletter) : utilisez un ESP (Brevo, Mailchimp, etc.). WordPress n’est pas fait pour le volume, et vous allez vous battre avec la délivrabilité.
  • Vous avez WooCommerce et vous voulez uniquement personnaliser ses emails : WooCommerce a son propre système de templates. Vous pouvez le faire, mais ce tutoriel vise le pipeline wp_mail() générique.
  • Vous avez besoin d’un éditeur visuel d’emails pour des non-tech : un plugin spécialisé (avec templates) sera plus adapté.
  • Vous devez gérer du multilingue complexe par utilisateur : vous pouvez le faire ici, mais une couche i18n plus structurée (ou une solution dédiée) sera plus confortable.

Prérequis / avant de commencer

Contexte : avril 2026, WordPress 6.9.4, PHP 8.1+. Le code ci-dessous cible ces versions et évite les patterns d’anciens tutoriels (notamment les hacks globaux non réversibles sur wp_mail_content_type).

  • Une sauvegarde (fichiers + base) avant toute modification sur un site en production.
  • Un environnement de staging ou local.
  • Accès à wp-content/plugins pour créer un mini-plugin (recommandé) ou, à défaut, un plugin de snippets fiable.
  • Un outil de test email local :
    • Mailpit ou MailHog (capture SMTP), ou
    • un plugin SMTP qui permet de journaliser les emails (en restant prudent avec les données personnelles).

Sources de référence utiles :

L’approche naïve (et pourquoi l’éviter)

Le code que je vois le plus souvent en audit ressemble à ça : on force le HTML globalement, on colle du HTML inline dans le message, et on oublie de revenir au content-type précédent. Résultat : un plugin tiers envoie un email en HTML alors qu’il attendait du texte, ou inversement.

<?php
// ❌ Exemple naïf : à ne pas copier tel quel.
add_filter('wp_mail_content_type', function () {
    return 'text/html';
});

function mon_email_naif($to) {
    $subject = 'Bienvenue';
    $message = '<h1>Bienvenue</h1><p>Merci !</p>'; // HTML brut
    wp_mail($to, $subject, $message);
}

Pourquoi c’est un anti-pattern :

  • Effet de bord global : tous les emails du site passent en HTML, y compris ceux de plugins qui ne l’ont pas prévu.
  • Headers incohérents : certains plugins SMTP ajoutent leurs propres headers. Un mauvais mélange peut casser la délivrabilité.
  • Maintenabilité nulle : vous allez dupliquer le HTML dans dix endroits.
  • Sécurité : si le contenu HTML contient des données utilisateur non échappées, vous pouvez injecter du HTML dans des emails (phishing interne, tracking non voulu, etc.).

La bonne approche — tutoriel pas à pas

Étape 1 — Créer un mini-plugin (plutôt qu’un snippet dans le thème)

Le thème (même enfant) n’est pas un bon endroit pour la logique d’emails : vous risquez de tout casser lors d’un changement de thème. Créez un plugin : wp-content/plugins/bpcab-email-kit/bpcab-email-kit.php.

Étape 2 — Définir une “couche email” : wrapper + template

L’idée : vous appelez bpcab_mail() dans votre code. Cette fonction :

  • prépare les headers,
  • génère un HTML propre via un template,
  • force le content-type uniquement pendant l’envoi,
  • retourne le résultat de wp_mail().

Étape 3 — Générer un HTML compatible email (et réaliste)

Les clients email n’aiment pas :

  • le CSS externe,
  • les flexbox/grids,
  • les <style> trop ambitieux,
  • les images sans dimensions,
  • les liens sans URL absolue.

On reste donc sur une table principale, avec du style inline simple. C’est “old school”, mais c’est ce qui passe partout.

Étape 4 — Sécuriser les variables (échappement + whitelist)

Vous n’avez pas besoin d’autoriser tout HTML dans le corps. Dans la plupart des cas, une whitelist via wp_kses() suffit. Pour les URLs, utilisez esc_url(). Pour le texte, esc_html().

Étape 5 — Ajouter une version texte (option mais utile)

Beaucoup de solutions “HTML only” oublient les lecteurs texte, et certains systèmes anti-spam apprécient un fallback. Ici, on génère aussi une version texte simple (et on peut l’envoyer séparément si vous le souhaitez, ou l’utiliser pour debug).

Étape 6 — Prévoir la personnalisation via filtres

Vous allez vouloir adapter :

  • logo / nom du site,
  • couleur du bouton,
  • footer,
  • adresse “From”.

On expose des filtres dédiés dans notre plugin, plutôt que de demander aux gens de modifier le fichier.

Code complet

Copiez-collez le fichier ci-dessous dans wp-content/plugins/bpcab-email-kit/bpcab-email-kit.php, activez-le, puis testez avec une action simple (voir section “Vérifications”).

<?php
/**
 * Plugin Name: BPCAB Email Kit (HTML templates)
 * Description: Centralise la personnalisation des emails WordPress (HTML + filtres) via wp_mail().
 * Version: 1.0.0
 * Requires at least: 6.9
 * Requires PHP: 8.1
 * Author: BPCAB
 * License: GPLv2 or later
 */

if (!defined('ABSPATH')) {
    exit;
}

/**
 * Retourne l'adresse email "From" par défaut.
 * Vous pouvez la surcharger via le filtre bpcab_email_from.
 */
function bpcab_email_default_from(): string
{
    $admin_email = get_option('admin_email');
    $domain = wp_parse_url(home_url(), PHP_URL_HOST);

    // Si l'admin_email est invalide, on tente un fallback.
    if (!is_email($admin_email) && is_string($domain) && $domain !== '') {
        $admin_email = 'no-reply@' . $domain;
    }

    /**
     * Filtre interne : permet de définir l'adresse From.
     * Retour attendu: string (email).
     */
    $from = apply_filters('bpcab_email_from', $admin_email);

    $from = sanitize_email($from);
    if (!is_email($from)) {
        // Dernier filet de sécurité : évite d'envoyer un header From cassé.
        $from = 'no-reply@' . (is_string($domain) && $domain !== '' ? $domain : 'example.invalid');
    }

    return $from;
}

/**
 * Retourne le nom "From" par défaut.
 * Surcharge via bpcab_email_from_name.
 */
function bpcab_email_default_from_name(): string
{
    $name = wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES);

    /**
     * Filtre interne : permet de définir le nom From.
     * Retour attendu: string.
     */
    $name = apply_filters('bpcab_email_from_name', $name);

    // Évite les retours à la ligne dans les headers.
    $name = preg_replace("/[rn]+/", ' ', (string) $name);
    return trim($name);
}

/**
 * Construit un template HTML compatible email.
 *
 * @param array $args {
 *   @type string $title        Titre de l'email (affiché).
 *   @type string $preheader    Texte court (préheader).
 *   @type string $content_html Contenu HTML (whitelisté).
 *   @type string $button_text  Texte bouton (optionnel).
 *   @type string $button_url   URL bouton (optionnel).
 *   @type string $footer_html  Footer HTML (optionnel).
 * }
 * @return string HTML complet.
 */
function bpcab_email_render_html(array $args): string
{
    $defaults = [
        'title'        => '',
        'preheader'    => '',
        'content_html' => '',
        'button_text'  => '',
        'button_url'   => '',
        'footer_html'  => '',
    ];
    $args = array_merge($defaults, $args);

    $site_name = wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES);
    $site_url  = home_url('/');

    // Couleurs simples, modifiables via filtres.
    $brand_color = (string) apply_filters('bpcab_email_brand_color', '#1e40af'); // bleu
    $bg_color    = (string) apply_filters('bpcab_email_bg_color', '#f3f4f6');    // gris clair
    $card_color  = (string) apply_filters('bpcab_email_card_color', '#ffffff');

    // Préheader : souvent affiché dans les clients email.
    $preheader = trim((string) $args['preheader']);
    $preheader_esc = esc_html($preheader);

    // Whitelist HTML autorisé dans le contenu.
    $allowed = [
        'a'      => ['href' => true, 'target' => true, 'rel' => true],
        'br'     => [],
        'p'      => [],
        'strong' => [],
        'em'     => [],
        'ul'     => [],
        'ol'     => [],
        'li'     => [],
        'span'   => [],
    ];

    $title = esc_html((string) $args['title']);
    $content_html = wp_kses((string) $args['content_html'], $allowed);

    $button_text = trim((string) $args['button_text']);
    $button_url  = trim((string) $args['button_url']);

    $has_button = ($button_text !== '' && $button_url !== '');
    $button_url_esc = $has_button ? esc_url($button_url) : '';
    $button_text_esc = $has_button ? esc_html($button_text) : '';

    $default_footer = sprintf(
        '<p style="margin:0;font-size:12px;line-height:18px;color:#6b7280;">Email envoyé par <a href="%s" target="_blank" rel="noopener" style="color:#6b7280;text-decoration:underline;">%s</a>.</p>',
        esc_url($site_url),
        esc_html($site_name)
    );

    $footer_html = (string) $args['footer_html'];
    $footer_html = $footer_html !== '' ? wp_kses($footer_html, $allowed) : $default_footer;

    // Petit “hack” classique : préheader caché (sans CSS avancé).
    $preheader_block = $preheader !== ''
        ? '<div style="display:none;max-height:0;overflow:hidden;color:transparent;opacity:0;height:0;width:0;">' . $preheader_esc . '</div>'
        : '';

    $html = '
<!doctype html>
<html lang="fr">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="x-apple-disable-message-reformatting">
  <title>' . $title . '</title>
</head>
<body style="margin:0;padding:0;background:' . esc_attr($bg_color) . ';">
  ' . $preheader_block . '
  <table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="background:' . esc_attr($bg_color) . ';">
    <tr>
      <td align="center" style="padding:24px 12px;">

        <table role="presentation" cellpadding="0" cellspacing="0" border="0" width="600" style="width:600px;max-width:600px;">
          <tr>
            <td style="padding:0 0 12px 0;font-family:Arial,Helvetica,sans-serif;">
              <div style="font-size:14px;line-height:20px;color:#111827;">
                <a href="' . esc_url($site_url) . '" target="_blank" rel="noopener" style="color:#111827;text-decoration:none;"><strong>' . esc_html($site_name) . '</strong></a>
              </div>
            </td>
          </tr>

          <tr>
            <td style="background:' . esc_attr($card_color) . ';border-radius:12px;padding:22px;font-family:Arial,Helvetica,sans-serif;">
              <h2 style="margin:0 0 12px 0;font-size:20px;line-height:28px;color:#111827;">' . $title . '</h2>

              <div style="font-size:14px;line-height:22px;color:#111827;">
                ' . $content_html . '
              </div>
';

    if ($has_button) {
        $html .= '
              <div style="padding:18px 0 0 0;">
                <a href="' . $button_url_esc . '" target="_blank" rel="noopener"
                   style="display:inline-block;background:' . esc_attr($brand_color) . ';color:#ffffff;text-decoration:none;padding:10px 14px;border-radius:10px;font-size:14px;line-height:20px;">
                  ' . $button_text_esc . '
                </a>
              </div>
';
    }

    $html .= '
              <hr style="border:none;border-top:1px solid #e5e7eb;margin:20px 0;">
              <div style="font-size:12px;line-height:18px;color:#6b7280;">
                ' . $footer_html . '
              </div>
            </td>
          </tr>

          <tr>
            <td style="padding:12px 0 0 0;font-family:Arial,Helvetica,sans-serif;">
              <div style="font-size:11px;line-height:16px;color:#9ca3af;">
                <span>Si vous ne reconnaissez pas cet email, vous pouvez l’ignorer.</span>
              </div>
            </td>
          </tr>

        </table>
      </td>
    </tr>
  </table>
</body>
</html>
';

    /**
     * Filtre interne : permet d'altérer le HTML final (ex: tracking, footer légal).
     * Attention aux impacts délivrabilité.
     */
    return (string) apply_filters('bpcab_email_html', $html, $args);
}

/**
 * Génère une version texte simple à partir de paramètres proches.
 * Utile pour fallback, logs, ou envoi alternatif.
 */
function bpcab_email_render_text(array $args): string
{
    $title = (string) ($args['title'] ?? '');
    $content_html = (string) ($args['content_html'] ?? '');

    // Convertit grossièrement : dans les emails, on préfère simple et lisible.
    $content_text = wp_strip_all_tags($content_html, true);

    $lines = [];
    if ($title !== '') {
        $lines[] = $title;
        $lines[] = str_repeat('=', min(60, max(10, strlen($title))));
    }
    if (trim($content_text) !== '') {
        $lines[] = trim($content_text);
    }

    return trim(implode("nn", $lines));
}

/**
 * Envoie un email HTML via wp_mail(), en limitant au maximum les effets de bord.
 *
 * @param string|array $to
 * @param string $subject
 * @param array $template_args Voir bpcab_email_render_html()
 * @param array $mail_args {
 *   @type array  $headers Headers supplémentaires (optionnel).
 *   @type array  $attachments Pièces jointes (optionnel).
 *   @type string $reply_to Email Reply-To (optionnel).
 * }
 * @return bool True si wp_mail() a accepté l'envoi.
 */
function bpcab_mail($to, string $subject, array $template_args, array $mail_args = []): bool
{
    $subject = wp_specialchars_decode($subject, ENT_QUOTES);
    $subject = preg_replace("/[rn]+/", ' ', $subject); // protège les headers

    $html = bpcab_email_render_html($template_args);

    $headers = [];

    // From (cohérent, filtrable).
    $from_email = bpcab_email_default_from();
    $from_name  = bpcab_email_default_from_name();
    $headers[]  = 'From: ' . $from_name . ' <' . $from_email . '>';

    // Reply-To optionnel.
    if (!empty($mail_args['reply_to'])) {
        $reply_to = sanitize_email((string) $mail_args['reply_to']);
        if (is_email($reply_to)) {
            $headers[] = 'Reply-To: ' . $reply_to;
        }
    }

    // Headers additionnels.
    if (!empty($mail_args['headers']) && is_array($mail_args['headers'])) {
        foreach ($mail_args['headers'] as $h) {
            // On évite d'injecter n'importe quoi dans les headers.
            $h = (string) $h;
            $h = preg_replace("/[rn]+/", '', $h);
            if ($h !== '') {
                $headers[] = $h;
            }
        }
    }

    $attachments = [];
    if (!empty($mail_args['attachments']) && is_array($mail_args['attachments'])) {
        $attachments = $mail_args['attachments'];
    }

    /**
     * Filtre interne : permet d'altérer les arguments finaux.
     * Exemple: ajouter un header List-Unsubscribe.
     */
    $payload = apply_filters('bpcab_mail_payload', [
        'to'          => $to,
        'subject'     => $subject,
        'message'     => $html,
        'headers'     => $headers,
        'attachments' => $attachments,
        'template'    => $template_args,
    ]);

    // Forcer le content-type uniquement pendant cet envoi.
    $content_type_callback = static function () {
        return 'text/html; charset=UTF-8';
    };

    add_filter('wp_mail_content_type', $content_type_callback, 1000);

    // Optionnel : on peut aussi filtrer From/FromName globalement,
    // mais ici on les met explicitement dans les headers pour éviter la “guerre” des filtres.

    try {
        $sent = wp_mail(
            $payload['to'] ?? $to,
            $payload['subject'] ?? $subject,
            $payload['message'] ?? $html,
            $payload['headers'] ?? $headers,
            $payload['attachments'] ?? $attachments
        );
    } finally {
        remove_filter('wp_mail_content_type', $content_type_callback, 1000);
    }

    return (bool) $sent;
}

/**
 * Exemple de point d'entrée pour tester rapidement (admin uniquement).
 * URL: /wp-admin/?bpcab_test_mail=1
 */
add_action('admin_init', function () {
    if (!current_user_can('manage_options')) {
        return;
    }
    if (!isset($_GET['bpcab_test_mail'])) {
        return;
    }

    check_admin_referer('bpcab_test_mail');

    $to = get_option('admin_email');

    $ok = bpcab_mail(
        $to,
        'Test email HTML (BPCAB Email Kit)',
        [
            'title'        => 'Votre template HTML fonctionne',
            'preheader'    => 'Préheader: vérifiez le rendu dans votre boîte de réception.',
            'content_html' => '<p>Si vous voyez cet email avec un bouton et un footer, le pipeline HTML est OK.</p><p><strong>Astuce</strong> : testez sur Gmail + Outlook, pas uniquement votre webmail.</p>',
            'button_text'  => 'Ouvrir le site',
            'button_url'   => home_url('/'),
        ],
        [
            'reply_to' => get_option('admin_email'),
        ]
    );

    $redirect = add_query_arg([
        'bpcab_test_mail_sent' => $ok ? '1' : '0',
    ], admin_url());

    wp_safe_redirect($redirect);
    exit;
});

/**
 * Affiche une notice après test.
 */
add_action('admin_notices', function () {
    if (!current_user_can('manage_options')) {
        return;
    }
    if (!isset($_GET['bpcab_test_mail_sent'])) {
        return;
    }

    $ok = $_GET['bpcab_test_mail_sent'] === '1';
    $msg = $ok
        ? 'Email de test déclenché. Vérifiez la réception (et le dossier spam).'
        : 'Échec wp_mail(). Vérifiez SMTP/serveur, logs PHP et configuration.';

    echo '<div class="notice ' . ($ok ? 'notice-success' : 'notice-error') . '"><p>' . esc_html($msg) . '</p></div>';
});

Pour déclencher le test proprement (avec nonce) :

<?php
// Collez ce lien dans votre navigateur (en étant connecté admin).
// Remplacez NONCE par la valeur générée par wp_nonce_url().

Le plus simple : ajoutez temporairement un lien dans une page d’admin via un snippet, ou générez-le dans une console WP-CLI. En pratique, je fais souvent :

wp eval "echo wp_nonce_url(admin_url('/?bpcab_test_mail=1'), 'bpcab_test_mail') . PHP_EOL;"

Explication du code

Pourquoi un wrapper plutôt que “tout filtrer”

WordPress expose des filtres globaux (wp_mail_from, wp_mail_from_name, wp_mail_content_type). Le piège, c’est qu’ils s’appliquent à tous les emails, y compris ceux d’un plugin critique (sécurité, facturation) qui attend un format particulier.

Ici, on choisit une approche hybride :

  • on utilise wp_mail() (donc compatible avec la plupart des plugins SMTP),
  • on force le content-type uniquement pendant l’appel (ajout + suppression du filtre),
  • on met le From: dans les headers de cet email, pour limiter les conflits de priorités.

Le rendu HTML : wp_kses() plutôt que “HTML libre”

Le corps de l’email contient souvent des fragments venant d’options, de champs, ou d’inputs. Même si vous pensez “c’est interne”, j’ai déjà vu des champs profil utilisateur injectés dans des emails admin.

On applique une whitelist minimaliste :

  • liens <a> (avec href),
  • mise en forme <strong>, <em>,
  • listes, paragraphes.

Si vous avez besoin de tableaux ou d’images, ajoutez-les explicitement dans la whitelist (section “Variantes”).

Pourquoi le content-type est ajouté à priorité 1000

Sur des sites chargés en plugins, vous avez souvent plusieurs filtres sur wp_mail_content_type. Mettre une priorité élevée augmente vos chances de gagner… sans bloquer le reste, puisque vous retirez le filtre juste après. J’ai souvent croisé des sites où un plugin “force text/plain” à priorité 10, ce qui rend les templates HTML “aléatoires”.

Headers : prévention de l’injection

Les en-têtes email sont sensibles aux retours chariot. Une donnée non nettoyée peut créer un header supplémentaire. On supprime donc r et n dans le sujet et les headers additionnels.

Pourquoi pas PHPMailer directement

WordPress utilise PHPMailer en interne, mais passer par wp_mail() reste la voie la plus compatible (plugins SMTP, logs, hooks). Toucher PHPMailer directement via phpmailer_init peut être utile, mais c’est une autre couche de complexité, et vous pouvez casser des réglages SMTP.

Références officielles utiles :

Variantes et cas d’usage

Variante 1 — Personnaliser “From” globalement via filtres core

Si vous préférez centraliser au niveau WordPress (plutôt que dans les headers de bpcab_mail()), vous pouvez activer ces filtres. Attention : ça impacte tous les emails.

<?php
add_filter('wp_mail_from', function ($from) {
    $custom = bpcab_email_default_from();
    return is_email($custom) ? $custom : $from;
}, 20);

add_filter('wp_mail_from_name', function ($name) {
    return bpcab_email_default_from_name();
}, 20);

Variante 2 — Autoriser des images et des tableaux dans le contenu

Cas fréquent : emails “commande” ou “récap” avec un petit tableau. Ajoutez explicitement img et table à la whitelist. Ne faites pas “autoriser tout”.

<?php
// Exemple : à intégrer dans bpcab_email_render_html(), dans $allowed.
$allowed['img'] = [
    'src' => true,
    'alt' => true,
    'width' => true,
    'height' => true,
    'style' => true,
];
$allowed['table'] = ['role' => true, 'cellpadding' => true, 'cellspacing' => true, 'border' => true, 'width' => true, 'style' => true];
$allowed['tbody'] = [];
$allowed['thead'] = [];
$allowed['tr'] = [];
$allowed['td'] = ['style' => true, 'colspan' => true, 'rowspan' => true];
$allowed['th'] = ['style' => true, 'colspan' => true, 'rowspan' => true];

Variante 3 — Ajouter un header List-Unsubscribe (délivrabilité)

Pour certains emails récurrents (notification, digest), ajouter List-Unsubscribe peut aider. Ne le mettez pas sur des emails purement transactionnels sans logique de désinscription.

<?php
add_filter('bpcab_mail_payload', function (array $payload) {
    $unsubscribe_url = add_query_arg([
        'my_unsub' => 1,
        'email'    => rawurlencode(is_array($payload['to']) ? (string) $payload['to'][0] : (string) $payload['to']),
    ], home_url('/'));

    $payload['headers'][] = 'List-Unsubscribe: <' . esc_url($unsubscribe_url) . '>';
    return $payload;
});

Compatibilité Divi 5 / Elementor / Avada

Les page builders interviennent rarement directement dans wp_mail(), mais ils influencent le contenu que vous pourriez injecter dans un email (shortcodes, CSS, blocs). Trois cas reviennent souvent.

Divi 5

  • Évitez d’injecter des shortcodes Divi dans les emails : le rendu dépend du front, des assets, et d’un contexte de page.
  • Si vous devez réutiliser un texte édité via Divi, stockez une version “email-safe” (texte + liens) séparée, ou passez par une conversion très contrôlée (ex: extraire uniquement les paragraphes).

Elementor

  • Ne tentez pas de rendre un template Elementor dans un email. Les emails ne chargent pas vos CSS/JS front.
  • Si vous récupérez du contenu depuis un widget Elementor (stocké en post meta), considérez-le comme non fiable et passez-le dans wp_kses() avec une whitelist stricte.

Avada / Fusion Builder

  • Même logique : les shortcodes Fusion ne sont pas pensés pour l’email.
  • Si vous récupérez du contenu “builder”, transformez-le en contenu email minimal (p, ul, a), sinon vous aurez des emails vides ou illisibles.

Règle pratique : un email HTML doit être autonome. Pas de dépendance à un CSS de thème, pas de JS, pas de webfonts “obligatoires”.

Vérifications après mise en place

  1. Testez l’envoi avec l’URL nonce (section code) et vérifiez que wp_mail() retourne true.
  2. Vérifiez les headers via votre outil SMTP (Mailpit/MailHog) : Content-Type: text/html; charset=UTF-8, From: correct.
  3. Ouvrez l’email sur au moins :
    • Gmail web,
    • Outlook (desktop ou web),
    • un client mobile.
  4. Contrôlez les liens : URLs absolues, pas de liens relatifs.
  5. Contrôlez le spam : envoyez-vous l’email sur une boîte externe. Si ça part en spam, revoyez From/DKIM/DMARC (souvent hors WordPress).

Tableau de diagnostic (rapide)

Symptôme Cause probable Vérification Solution
Email reçu en texte brut Content-Type non appliqué ou écrasé Inspecter headers dans Mailpit/MailHog Vérifier que le filtre wp_mail_content_type est bien ajouté/retiré, augmenter la priorité, vérifier conflits plugins
Logo/liens cassés URLs relatives dans le HTML Voir le code source de l’email Utiliser home_url() + esc_url(), éviter /wp-content/... sans domaine
wp_mail() retourne false SMTP/serveur refuse, config PHP mail désactivée Logs serveur, plugin SMTP, test en local Configurer SMTP, vérifier DNS, logs, restrictions hébergeur
Rendu “moche” sur Outlook CSS non supporté Test Outlook Réduire CSS, privilégier tables + inline styles simples
Erreur 500 après ajout du code Syntaxe PHP (point-virgule, parenthèse) Activer WP_DEBUG, consulter logs Corriger la syntaxe, déployer via staging, utiliser un éditeur avec lint

Si ça ne marche pas

1) Confirmez que votre code est chargé

  • Le plugin est-il activé ?
  • Le fichier est-il au bon endroit (wp-content/plugins/bpcab-email-kit/bpcab-email-kit.php) ?
  • Pas d’erreur PHP au chargement (regardez les logs) ?

2) Activez le debug proprement

Sur un staging, activez :

<?php
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);

3) Vérifiez les conflits de filtres

J’ai souvent vu :

  • un plugin “Email customizer” qui force text/plain,
  • un plugin SMTP qui réécrit les headers,
  • un snippet ancien qui laisse wp_mail_content_type en HTML en permanence.

Désactivez temporairement ces éléments et retestez.

4) Vérifiez PHP et l’hébergement

  • PHP 8.1+ (sinon, erreurs de type ou compatibilité).
  • Fonction mail désactivée ? (fréquent sur certains hébergeurs).
  • Limites sortantes SMTP ?

5) Videz les caches

  • Cache plugin (page cache / object cache) si vous testez via une UI qui dépend d’options.
  • Cache navigateur si vous avez ajouté un bouton admin.

Pièges et erreurs courantes

Erreur Cause Solution
Code collé dans functions.php du thème parent Mise à jour du thème = perte du code Mettre dans un mini-plugin ou un thème enfant
Oubli d’un ; ou d’une accolade Copier-coller partiel Activer WP_DEBUG_LOG, vérifier la ligne dans les logs, utiliser un IDE
Confusion action/filtre Utiliser add_action au lieu de add_filter Vérifier la doc des hooks (wp_mail_content_type est un filtre)
Hook inadapté / priorité trop basse Un plugin écrase votre content-type après vous Monter la priorité (ex: 1000) et retirer le filtre après l’envoi
Template HTML “vide” en réception HTML cassé (tags non fermés) ou contenu filtré trop strictement Valider le HTML, élargir la whitelist wp_kses() de façon contrôlée
Images non affichées URL relative, blocage images côté client URL absolue, ajouter alt, dimensions, accepter que certains clients bloquent par défaut
Email en spam From non aligné avec SPF/DKIM/DMARC Aligner domaine d’envoi, config DNS, éviter gmail.com en From
Test sur production sans sauvegarde Changement de headers = impact global Staging d’abord, puis déploiement progressif
Snippet cassé par plugin de snippets Ordre de chargement / erreur fatale Mini-plugin versionné, tests automatisés si possible
Tutoriel ancien qui recommande PHPMailer direct Incompatibilités et conflits SMTP Préférer wp_mail() + filtres, n’utiliser phpmailer_init que si nécessaire

Conseils sécurité, performance et maintenance

Sécurité

  • Ne mettez jamais du HTML utilisateur brut dans un email : passez par wp_kses().
  • Nettoyez systématiquement les headers (suppression des retours ligne) pour éviter l’injection.
  • Évitez d’exposer une URL de test sans nonce (ici on utilise check_admin_referer()).

Performance

  • Le rendu HTML est léger : pas de requêtes SQL additionnelles en dehors de get_option/get_bloginfo.
  • Si vous envoyez beaucoup d’emails, déplacez l’envoi vers une queue (Action Scheduler, cron) plutôt que de bloquer une requête front.

Maintenance

  • Gardez le template stable et versionné. Les “petits changements” de HTML email peuvent casser Outlook.
  • Documentez vos filtres internes (bpcab_email_brand_color, bpcab_email_html, etc.).
  • Après mise à jour WordPress/SMTP plugin, retestez au moins l’email de reset password et votre email transactionnel principal.

Ressources

FAQ

Pourquoi mes emails WordPress n’arrivent pas, même avec un template HTML correct ?

Le HTML n’est généralement pas la cause principale. Le plus fréquent : absence/mauvaise config SMTP, domaine non aligné SPF/DKIM/DMARC, ou hébergeur qui bloque mail(). Commencez par vérifier l’acheminement (logs SMTP), puis le contenu.

Est-ce que ce plugin va personnaliser les emails de tous mes plugins ?

Uniquement ceux qui passent par wp_mail(). La plupart le font, mais certains systèmes (ou services externes) envoient autrement. Pour WooCommerce, il y a en plus sa couche de templates.

Pourquoi ne pas laisser wp_mail_content_type en HTML en permanence ?

Parce que vous allez casser des emails texte attendus par certains plugins (ou des intégrations). Le pattern “ajouter le filtre puis le retirer” limite les effets de bord.

Comment ajouter un logo en haut de l’email ?

Ajoutez une balise <img> dans le header du template, avec URL absolue, width/height, et autorisez img dans la whitelist wp_kses(). Gardez en tête que certains clients bloquent les images par défaut.

Puis-je utiliser le contenu d’une page (Gutenberg) comme corps d’email ?

Techniquement oui, mais évitez de rendre des blocs complexes. Récupérez le contenu, passez-le dans une whitelist stricte, et vérifiez le rendu. Les emails ne chargeront pas votre CSS de thème.

Comment tester sans envoyer de vrais emails ?

Utilisez Mailpit/MailHog en local, ou un SMTP de test. En staging, vous pouvez aussi loguer les payloads (attention aux données personnelles).

Je vois des caractères bizarres (accents) dans l’email

Vérifiez le charset=UTF-8 dans le content-type. Ici on renvoie text/html; charset=UTF-8. Vérifiez aussi que votre sujet n’a pas de caractères mal encodés (on utilise wp_specialchars_decode).

Mon bouton ne ressemble pas à un bouton dans Outlook

Outlook est notoirement capricieux. Restez sur des styles inline simples. Si vous avez besoin d’un rendu “bulletproof”, passez à une structure plus spécifique (bouton en table). Gardez le template minimal.

Est-ce compatible PHP 8.1+ et WordPress 6.9.4 ?

Oui. Le code évite les APIs obsolètes et reste dans le cadre wp_mail() + fonctions d’échappement/sanitization standard.

Comment brancher ça sur mes formulaires (Contact Form 7, WPForms, etc.) ?

Deux approches : soit vous laissez le plugin formulaire envoyer via wp_mail() et vous personnalisez globalement (avec prudence), soit vous interceptez ses hooks spécifiques et vous appelez bpcab_mail() pour vos emails personnalisés. La seconde est souvent plus stable.