Aller au contenu principal

HBK Mailer : Construire sa propre plateforme d'emailing avec Symfony 8

Introduction

En 2026, envoyer des emails depuis son application web est devenu un besoin incontournable. Newsletters, campagnes marketing, notifications transactionnelles... les usages sont nombreux. Pourtant, beaucoup de développeurs se tournent vers des solutions tierces comme Brevo, Mailchimp ou SendGrid, souvent par méconnaissance de ce qu'il est possible de faire avec Symfony et un serveur bien configuré.

Cet article vous montre comment j'ai construit HBK Mailer, une plateforme d'emailing complète, de A à Z. Import de contacts, segmentation avancée, campagnes personnalisées, tracking des ouvertures et clics, le tout sur une infrastructure maîtrisée avec une délivrabilité optimale.

📋 En bref
Temps de lecture : 15 minutes
Prérequis : Symfony 8, PHP 8.4, PostgreSQL, Postfix

Table des matières

  1. Architecture générale
  2. Gestion des contacts et import CSV
  3. Segmentation avec filtres dynamiques
  4. Campagnes d'emailing et templates
  5. Envoi asynchrone avec Symfony Messenger
  6. Tracking des ouvertures et clics
  7. Configuration DNS pour la délivrabilité
  8. Mise en production et supervision

1. Architecture générale

L'application repose sur une architecture classique Symfony 8, avec une file d'attente pour les traitements asynchrones.


┌──────────────────────────────────────────────────────────┐
│                    HBK Mailer                             │
│                                                          │
│  ┌─────────────┐    ┌──────────────┐    ┌─────────────┐  │
│  │  Contacts   │    │  Campagnes   │    │  Tracking   │  │
│  └─────────────┘    └──────────────┘    └─────────────┘  │
│         │                  │                  │          │
│         └──────────────────┴──────────────────┘          │
│                            │                              │
│                     Base de données (PostgreSQL)          │
│                            │                              │
│              ┌─────────────┴─────────────┐               │
│              │                           │               │
│         SMTP (envoi)              Postfix (réception)     │
│              │                           │               │
│              ▼                           ▼               │
│          Internet ←────────────────── Internet           │
└──────────────────────────────────────────────────────────┘

La particularité : tout est asynchrone. L'import de contacts comme l'envoi de campagnes passent par Symfony Messenger. L'utilisateur ne voit jamais une page blanche.

2. Gestion des contacts et import CSV

L'entité Contact est le cœur du système. Elle stocke :

  • Les informations de base : email, prénom, nom
  • Des données de segmentation : pays, source, tags, score d'engagement
  • Des métadonnées RGPD : consentement, date de vérification
  • Des données de tracking : dernière ouverture, dernier clic

Import CSV avec mapping dynamique

L'import se fait en deux étapes :

Étape 1 : Téléversement et mapping. L'utilisateur uploade un fichier CSV. L'application lit la première ligne (les en-têtes) et propose un formulaire de mapping. L'utilisateur associe chaque colonne du CSV à un champ de l'entité Contact.

[Capture d'écran du mapping CSV à insérer ici]

Étape 2 : Traitement asynchrone. Une fois le mapping validé, l'import est placé dans une file d'attente. Un worker le traite par lots de 100 lignes, avec une pause de quelques secondes entre chaque lot pour ne pas saturer le serveur.

// Message : un lot de 100 contacts à importer
final class ImportContactsMessage
{
    public function __construct(
        private readonly int $importId,
        private readonly int $offset = 0,
        private readonly int $batchSize = 100,
    ) {}
}

Les erreurs sont loguées ligne par ligne : email invalide, doublon, violation de contrainte. L'administrateur peut consulter les logs pour comprendre ce qui a échoué.

3. Segmentation avec filtres dynamiques

C'est la fonctionnalité qui distingue HBK Mailer d'un simple script d'envoi. Les filtres permettent de cibler précisément les destinataires d'une campagne.

Un DTO pour représenter un filtre

class FilterCriterion
{
    public ?string $field;    // Le champ Contact à filtrer
    public ?string $operator; // L'opérateur (eq, neq, gt, gte, lt, lte, like, in)
    public ?string $value;    // La valeur recherchée
}

Les opérateurs disponibles incluent eq, neq, gt, gte, lt, lte, like, in, null, notnull.

[Capture d'écran des filtres de campagne à insérer ici]

4. Campagnes d'emailing et templates

Une campagne se compose de :

  • Un nom et un sujet
  • Un contenu HTML (via un éditeur WYSIWYG)
  • Des filtres de ciblage (optionnels)
  • Un statut : brouillon, en cours, envoyé, échoué, annulé

Templates email avec Tailwind CSS

Les emails sont conçus avec Tailwind CSS, compilé spécifiquement pour les emails. La configuration désactive les fonctionnalités non supportées par les clients mail (animations, grilles CSS, flexbox).

/* mailer.css — uniquement les classes utilisées dans les templates email */
@reference "tailwindcss";

.email-header {
    @apply bg-slate-900 px-8 py-6 text-center;
}

.email-button {
    @apply inline-block bg-blue-600 text-white font-semibold 
           text-sm px-6 py-3 rounded-lg no-underline;
}

Prévisualisation et test

Avant de lancer une campagne, l'utilisateur peut :

  1. Prévisualiser le rendu exact de l'email
  2. Envoyer un email de test à sa propre adresse
[Capture d'écran de la prévisualisation à insérer ici]

5. Envoi asynchrone avec Symfony Messenger

L'envoi en masse ne se fait jamais dans la requête HTTP. Tout passe par Messenger.

Le Dispatcher crée un message et le place dans la file d'attente :

$bus->dispatch(new SendCampaignMessage($campaign->getId(), 10));

Le Handler exécute l'envoi par lots :

#[AsMessageHandler]
final class SendCampaignHandler
{
    public function __invoke(SendCampaignMessage $message): void
    {
        $hasMore = $this->senderService->sendBatch(
            $message->getCampaignId(),
            $message->getBatchSize(),
        );

        // Relancer un lot avec 2 secondes de délai
        if ($hasMore) {
            $this->bus->dispatch(
                new SendCampaignMessage($message->getCampaignId(), 10),
                [new DelayStamp(2000)]
            );
        }
    }
}

Le Worker

En production, un worker systemd écoute la file d'attente. Il travaille par cycles de 10 minutes, avec 1 minute de pause :

[Service]
ExecStart=/usr/bin/php8.4 bin/console messenger:consume async --time-limit=600 --memory-limit=128M
Restart=always
RestartSec=60

6. Tracking des ouvertures et clics

Pixel d'ouverture

Un pixel invisible de 1x1 pixel est inséré dans chaque email :

<img src="https://habeuk.com/track/open/42/abc123" width="1" height="1" />

Quand le destinataire ouvre l'email, le contrôleur enregistre l'ouverture et retourne un pixel transparent.

Clics trackés

https://habeuk.com/track/click/42?url=https://example.com

Le contrôleur enregistre le clic, puis redirige vers l'URL d'origine.

Score d'engagement

Score = (taux d'ouverture × 0,4) + (taux de clic × 0,6)

7. Configuration DNS pour la délivrabilité

Une bonne délivrabilité repose sur trois piliers :

ProtocoleConfigurationRôle
SPFv=spf1 ip4:51.222.28.99 mx a -allQui peut envoyer des emails
DKIMmail._domainkey.habeuk.com TXT "v=DKIM1..."Signature cryptographique
DMARC_dmarc.habeuk.com TXT "v=DMARC1; p=reject..."Que faire en cas d'échec

Avec cette configuration, j'obtiens un score de 10/10 sur Mail-Tester. Les emails arrivent dans la boîte de réception, pas dans les spams.

[Capture d'écran du test Mail-Tester à insérer ici]

8. Mise en production et supervision

Déploiement automatisé

Un hook Git déclenche le déploiement à chaque nouveau tag :

  1. Extraction des fichiers dans un dossier temporaire
  2. Copie sélective vers le dossier de production
  3. Exécution d'un script post-déploiement
  4. Nettoyage du dossier temporaire

Worker supervisé par systemd

$ sudo systemctl status app-mailer-symfony-messenger
● app-mailer-symfony-messenger.service - Symfony Messenger Worker
     Active: active (running)
     Memory: 86.8M (limit: 256.0M)

Conclusion

Construire sa propre plateforme d'emailing est un investissement rentable. HBK Mailer m'a permis de :

  • Réduire les coûts en supprimant les abonnements aux services tiers
  • Maîtriser la délivrabilité avec une configuration DNS optimale
  • Automatiser les envois grâce à Messenger et systemd
  • Rester conforme RGPD avec un désabonnement en un clic

Le code est disponible sur demande. N'hésitez pas à me contacter pour toute question.


Article rédigé le 3 juin 2026. Symfony 8, PHP 8.4, PostgreSQL 16.

Profile picture for user admin Stephane K

Écrit le

Il y'a 8 heures
Modifié
Il y'a 8 heures
Loading ...
Need personalized advice?
Envoyer