Se rendre au contenu

Architecture mail Odoo — les 4 couches transport, message, template, mass

Série Tech-Email · Article 1/14 · HUB du parcours Infrastructure
6 juin 2026 par
Architecture mail Odoo — les 4 couches transport, message, template, mass
B.Mustapha
| Aucun commentaire pour l'instant

La couche mail d'Odoo 19 ressemble souvent à une boîte noire vue de l'extérieur : un bouton « Envoyer » dans le chatter, et l'email arrive (ou pas). En réalité, ce qui se passe entre record.message_post() et la boîte de réception du destinataire passe par quatre couches d'abstraction distinctes, chacune portée par des modèles précis du framework. Comprendre ces quatre couches, c'est savoir où chercher quand une queue de messages se bloque, où surcharger pour customiser un comportement, et comment relier les concepts évoqués dans les treize articles suivants de cette série.

Cet article HUB cartographie les quatre couches mail d'Odoo 19 : transport, message, template et mass. Pour chacune, on identifie les modèles clés, les fichiers source à connaître, et les usages typiques. Le diagramme central donne en un coup d'œil le flux complet d'un message_post jusqu'au SMTP.

Prérequis lecteur. Avoir installé Odoo 19, lu un manifest, ouvert un modèle Python (models.Model, _inherit, _name). Aucune connaissance SMTP/DNS requise — ces points sont couverts en profondeur par les articles dédiés #04 DKIM/SPF/DMARC et les futurs articles SMTP/Fetchmail de la série.

1. Pourquoi l'email est central dans Odoo

Bien plus qu'un simple module de notification, l'email dans Odoo est une infrastructure transverse consommée par presque toutes les applications métier. Le chatter d'une commande de vente, l'envoi automatique d'une facture PDF, la newsletter de la base marketing, le ticket helpdesk créé par email entrant : tous ces flux mobilisent les mêmes briques techniques.

Cinq usages principaux structurent l'usage de la couche mail :

  • Chatter et discussion sur un record — modèle mail.thread hérité par tout document qui doit accepter messages, followers et activités.
  • Notifications utilisateur dans Discuss et par email — modèles mail.notification et mail.message.
  • Automation transactionnelle — un template mail.template déclenché par une action serveur, un cron ou un changement d'état métier.
  • Marketing de masse — campagnes mailing.mailing avec tracking d'ouverture, clics et rebonds.
  • Gateway entrant — réception de messages via fetchmail.server, routage sur un record par mail.alias.

Ces cinq usages s'appuient sur la même fondation à quatre couches. C'est cette fondation que cet article met à plat.

2. Le diagramme des 4 couches

3. Couche transport — ir.mail_server et fetchmail.server

La couche transport est l'interface entre Odoo et les serveurs SMTP / IMAP du monde extérieur. Elle se décompose en deux modèles principaux : ir.mail_server (sortant) et fetchmail.server (entrant), complétés par mail.alias et mail.alias.domain pour le routage des messages reçus.

Sortant — ir.mail_server. Défini dans odoo/addons/base/models/ir_mail_server.py et étendu par le module mail, ce modèle stocke la configuration SMTP (host, port, TLS, credentials) et expose la méthode send_email(). La sélection du serveur pour un message donné passe par _find_mail_server(), qui croise l'en-tête From avec le champ from_filter de chaque serveur configuré. Ce mécanisme est détaillé dans l'article dédié à DKIM/SPF/DMARC car il conditionne l'alignement d'authentification.

Depuis la v17, le module mail ajoute trois champs au modèle de base : owner_user_id (serveur personnel d'un utilisateur), owner_limit_time et owner_limit_count (throttling, défaut 30 emails/minute, 10 pour Outlook). Ces serveurs personnels coexistent avec les serveurs globaux et sont sélectionnés en priorité quand l'auteur du message correspond.

⚠️ Changement v17. Les serveurs SMTP personnels n'existaient pas en v16 et antérieures. Un module qui exposait sa propre gestion par-utilisateur doit être migré vers owner_user_id et le throttling intégré.

Entrant — fetchmail.server. Le modèle fetchmail.server (odoo/addons/mail/models/fetchmail.py) interroge périodiquement un compte IMAP ou POP3 et injecte chaque message reçu dans la chaîne de routage mail.thread.message_route(). La sécurité de désactivation automatique au bout de MAIL_SERVER_DEACTIVATE_TIME jours d'erreurs successives (5 jours par défaut) évite les boucles d'erreur sur un compte cassé.

Alias et domaine. Le modèle mail.alias mappe une adresse email à un modèle Odoo cible : support@ crée un helpdesk.ticket, commercial@ crée un crm.lead. Depuis la v17, le préfixe de domaine n'est plus stocké dans un paramètre système (mail.catchall.domain déprécié) mais dans le modèle mail.alias.domain, scopé par société : chaque res.company peut avoir son propre domaine d'envoi et de réception.

⚠️ Changement v17 → v19. Les ICP mail.catchall.domain, mail.bounce.alias et mail.default.from ont été remplacés par le modèle mail.alias.domain. Une migration automatique via _migrate_icp_to_domain() est jouée à l'installation, mais un module legacy qui lit encore l'ancien ICP renverra une chaîne vide en v19.

4. Couche message — mail.message, mail.thread, mail.followers

Au-dessus du transport, la couche message porte l'identité éditoriale d'un email : qui parle, à qui, dans quel contexte métier. C'est cette couche qui rend possible le chatter universel d'Odoo : chaque sale.order, chaque project.task, chaque hr.employee peut accueillir une discussion structurée.

mail.message. Le message immuable, créé une fois et plus jamais modifié. Il stocke le contenu HTML, l'auteur (author_id), les destinataires (partner_ids), et un lien polymorphe vers le record source (model + res_id). Tous les emails envoyés et reçus passent par ce modèle, qui agit comme journal d'audit.

mail.thread. AbstractModel défini dans l'article dédié aux mail.template et au chatter. Tout modèle qui hérite de mail.thread obtient automatiquement la possibilité de recevoir des messages (message_post()), des followers, et un chatter dans la vue formulaire. Options de configuration via les attributs de classe : _mail_flat_thread (vue plate ou imbriquée), _mail_post_access (qui peut écrire dans le chatter).

mail.followers. Modèle de jonction qui lie un partner à un record avec un ensemble de subtypes (mail.message.subtype). Un follower n'est pas notifié de tout : il choisit ses subtypes (création, changement d'étape, notes internes). C'est ce filtrage qui empêche un commercial de recevoir 200 emails par jour sur les commandes auxquelles il est abonné.

mail.notification (modèle qui matérialise la livraison d'un message à un destinataire donné — un message envoyé à N destinataires génère N mail.notification). Une notification par couple (message, destinataire). Statuts possibles : ready, sent, bounce, exception, canceled. C'est sur ce modèle qu'on inspecte un échec d'envoi : un message envoyé à 50 destinataires génère 50 notifications, chacune avec son propre statut. La table mail.notification est aussi le point d'observation principal pour le monitoring de la délivrabilité.

5. Couche template — mail.template et mail.render.mixin

La couche template découple le rendu du contenu de son envoi. Sans elle, chaque module aurait son propre code de génération d'email — recette idéale pour la duplication et les régressions. Avec elle, on déclare un template une fois, et n'importe quel record du modèle ciblé peut le rendre.

mail.template. Stocke un sujet, un corps HTML, des destinataires par défaut, des pièces jointes dynamiques. Le corps est interprété en QWeb inline : la syntaxe {{ object.name }} et {% for line in object.order_line %} est convertie automatiquement à la volée par la méthode convert_inline_template_to_qweb(). Depuis la v17, Jinja2 a été retiré complètement — un template legacy en Jinja sera converti au premier rendu.

⚠️ Changement v17 → v19. Le moteur de templating Jinja2 a été supprimé. Tout module v16 (ou antérieur) qui dépendait de filtres Jinja personnalisés (safe, format_date via Jinja, etc.) doit migrer vers les helpers QWeb équivalents fournis par mail.render.mixin.

mail.render.mixin. Le moteur de rendu lui-même. Mixin partagée par mail.template et mailing.mailing, elle expose _render_field() et un drapeau de classe critique : _unrestricted_rendering. Quand ce drapeau est à False (défaut), seuls les membres du groupe Template Editor peuvent éditer un template, car le QWeb arbitraire permet d'exécuter du Python via t-esc et représente une surface d'attaque côté serveur. Le passer à True sur un modèle custom doit être un acte conscient, jamais un raccourci.

Pour des cas pratiques de templates déclenchés sur create et write, l'article dédié aux email templates et mail.thread couvre les patterns recommandés et les pièges classiques. L'article spécifique à la mixin mail.render.mixin arrive plus tard dans cette série.

6. Couche mass — mailing.mailing et mailing.trace

La couche mass est au-dessus des trois autres, et non à côté. Une campagne mailing.mailing est cliente du moteur de templating (pour rendre son corps), cliente de la couche message (chaque envoi génère un mail.message), et cliente de la couche transport (un ir.mail_server est sélectionné comme pour n'importe quel email).

mailing.mailing. Hérite de quatre mixins : mail.thread (audit), mail.activity.mixin (planification), mail.render.mixin (rendu), utm.source.mixin (intégration des paramètres UTM — utm_source, utm_medium, utm_campaign — automatiquement injectés dans les liens trackés). Cette accumulation reflète la complexité réelle d'une campagne : il faut un journal, des activités liées, un rendu paramétrable, et un suivi de source pour l'attribution marketing.

mailing.trace (modèle dédié au tracking — une ligne par destinataire et par campagne, indépendante de la queue d'envoi). Table de statistiques par envoi, détachée de mail.mail. Cette séparation est volontaire : mail.mail est purgée régulièrement pour ne pas saturer la base, alors que mailing.trace conserve durablement les statuts outgoing, process, pending, sent, opened, replied, error, bounce, cancel. Sans cette table, impossible de calculer un taux d'ouverture sur six mois.

Les modules mass_mailing_* (sms, sale, crm, event, etc.) ajoutent des canaux et des intégrations métier, mais le squelette reste celui décrit ci-dessus. Pour le détail de l'implémentation et des patterns recommandés, les articles spécifiques aux campagnes et à mailing.trace arrivent en deuxième moitié de cette série.

7. Le flux complet : du message_post au SMTP

Une fois les quatre couches identifiées, le flux d'un email transactionnel devient lisible. Voici la chaîne d'appels typique quand un utilisateur écrit un message dans le chatter d'une commande de vente :

# Flux de référence — Odoo 19.0, code distillé depuis
# odoo/addons/mail/models/mail_thread.py (commit 7196ee3, juin 2025)
# et odoo/addons/base/models/ir_mail_server.py
# Pseudo-code documenté : aucun __init__ à exécuter, c'est la séquence
# d'appels réelle observée en lançant un test mail.thread sur 19.0.

# 1. L'utilisateur clique "Envoyer un message" → wizard ouvre mail.compose.message
# 2. Validation → appel de message_post() sur la sale.order cible

order.message_post(
    body=composer.body,
    partner_ids=composer.partner_ids.ids,
    subtype_xmlid='mail.mt_comment',
    message_type='comment',
)

# 3. Création d'un mail.message lié au record
#    → champ model='sale.order', res_id=order.id, author_id=user.partner_id.id

# 4. Résolution mail.followers : tous les partners suivant l'ordre + subtype matchant
#    → ajout aux destinataires

# 5. Création d'une mail.notification par destinataire
#    → état initial : ready, type : email (ou inbox si destinataire interne)

# 6. Enqueue dans mail.mail (queue d'envoi)
#    → état initial : outgoing, recipient_ids = partners externes

# 7. Cron mail.mail.queue.process déclenche _send() (toutes les minutes)
#    → pour chaque mail.mail outgoing :
#      a. Si attaché à un mail.template, rendu via mail.render.mixin
#      b. Sélection de l'ir.mail_server via _find_mail_server() + from_filter
#      c. Appel SMTP via send_email() → connexion, AUTH, DATA, .

# 8. Selon le résultat :
#    - succès → mail.mail.state = sent, mail.notification.notification_status = sent
#    - rebond → state = exception, notification_status = bounce, failure_type renseigné

# 9. Si la sale.order est cliente d'une mailing.mailing :
#    → en parallèle, mailing.trace est mise à jour (opened/replied via tracking pixel/link)

Cette chaîne en 9 étapes traverse les 4 couches : étape 2-5 sur la couche message, étape 7a sur la couche template, étape 7b-c sur la couche transport, étape 9 sur la couche mass. Quand un email ne part pas, identifier la couche du blocage est la première question à poser : mail.mail reste-t-il en outgoing (cron à vérifier), arrive-t-il en exception (transport à inspecter), ou n'est-il jamais créé (couche message — followers, subtypes mal configurés) ?

8. Pièges classiques et récap

Quatre erreurs récurrentes méritent une attention particulière sur la couche mail Odoo 19 :

  • Queue mail.mail bloquée en outgoing. Symptôme : les emails ne partent pas, aucun log d'erreur. Cause typique : le cron Mail: Email Queue Manager est désactivé (vérifier ir.cron dans les paramètres techniques) ou le worker ne tourne plus. Diagnostic via l'article sur les crons et automations serveur.
  • from_filter mal réglé. Symptôme : les emails sont envoyés mais l'auteur visible est réécrit en "X via Notifications <notifications@odoo.com>". Cause : le filtre du serveur SMTP ne match pas l'expéditeur souhaité, Odoo bascule sur un serveur de notifications par défaut. Conséquence directe sur DKIM, traitée en détail dans l'article DKIM/SPF/DMARC.
  • Fetchmail désactivé après 5 jours d'erreurs. Symptôme : les emails entrants ne sont plus routés. Cause : MAIL_SERVER_DEACTIVATE_TIME = 5 dans fetchmail.py — Odoo désactive automatiquement un serveur en erreur continue. Solution : corriger le compte source, réactiver manuellement le serveur dans le backend.
  • Confusion mail.template vs email.template. Le second n'existe pas en v19 (c'était un alias legacy de la v10). Toute référence à email.template dans un module v17+ doit être migrée vers mail.template.

À retenir des quatre couches :

  • Transport = canal physique (SMTP, IMAP, alias, domaine).
  • Message = contenu et audit (qui parle, à qui, dans quel record).
  • Template = rendu (séparation forme/envoi, QWeb inline).
  • Mass = surcouche de campagne (statistiques, tracking, UTM) — pas un canal séparé.

Cette cartographie est la grille de lecture des treize articles suivants de la Série Tech-Email, qui creusent chaque couche en profondeur : SMTP transactionnel, fetchmail, alias, mail.template avancé, render mixin, notifications Discuss, mass mailing, mailing.trace, debug de queue, et industrialisation par cron et tests automatisés.

Voir aussi dans ce parcours Infrastructure

DKIM, SPF, DMARC pour Odoo

Authentifier ses emails sortants — alignement from_filter et configuration DNS Brevo.

Lire l'article →
Email templates et mail.thread en Odoo 19

Envoi automatique depuis create et write — patterns chatter et templates.

Lire l'article →
← Premier article de la série Suivant — Configurer Brevo SMTP →

Série Tech-Email — Article 1/14 — Hub du parcours Infrastructure emailing Odoo 19.

Articles complémentaires

Actions serveur, cron et automations

ir.cron, ir.actions.server, base.automation — le moteur qui déclenche l'envoi des emails templatés.

Lire l'article →
Controllers HTTP et API REST

Recevoir un webhook d'événement email (bounce, open, click) depuis un relais SMTP transactionnel.

Lire l'article →

Documentation Odoo 19 sur la couche mail : odoo.com/documentation/19.0 — Email communication .

Se connecter pour laisser un commentaire.
DKIM, SPF, DMARC pour Odoo — authentifier ses emails sortants
Série Tech-Email · Article 4/14 · Parcours Infrastructure