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.
Temps de lecture : 15 minutes
Prérequis : Symfony 8, PHP 8.4, PostgreSQL, Postfix
Table des matières
- Architecture générale
- Gestion des contacts et import CSV
- Segmentation avec filtres dynamiques
- Campagnes d'emailing et templates
- Envoi asynchrone avec Symfony Messenger
- Tracking des ouvertures et clics
- Configuration DNS pour la délivrabilité
- 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.
É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.
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 :
- Prévisualiser le rendu exact de l'email
- Envoyer un email de test à sa propre adresse
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=606. 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.comLe 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 :
| Protocole | Configuration | Rôle |
|---|---|---|
| SPF | v=spf1 ip4:51.222.28.99 mx a -all | Qui peut envoyer des emails |
| DKIM | mail._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.
8. Mise en production et supervision
Déploiement automatisé
Un hook Git déclenche le déploiement à chaque nouveau tag :
- Extraction des fichiers dans un dossier temporaire
- Copie sélective vers le dossier de production
- Exécution d'un script post-déploiement
- 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.