Les articles précédents de la série ont suivi l'email sortant : du message_post jusqu'au relais SMTP, en passant par le from_filter et les adresses techniques de mail.alias.domain. Reste l'autre moitié du cycle — celle qui ramène les réponses des clients dans le bon fil de discussion et fait revenir les rebonds sur la bonne adresse. C'est le rôle de la passerelle entrante : le modèle fetchmail.server, qui relève les boîtes IMAP ou POP3 et confie chaque message au moteur de routage d'Odoo.
Cet article décrit fetchmail.server en Odoo 19 : ses types de serveurs, leur comportement — non destructif ou destructif —, le cron qui orchestre la relève, le routage des messages vers les enregistrements via les alias, et l'authentification OAuth entrante. Il prolonge directement l'article sur mail.alias.domain (les adresses catchall et bounce y sont définies) et complète le panorama ouvert par l'article HUB sur les 4 couches mail.
mail.alias.domain est recommandée : le routage entrant s'appuie sur les alias qui y sont définis.
1. Pourquoi une passerelle entrante
Un ERP qui n'envoie que des emails reste sourd à la moitié de la conversation. Quand un client répond à un devis, quand un fournisseur confirme une commande, ou quand un serveur distant retourne un rapport de non-distribution, ce message doit réintégrer Odoo et se rattacher au bon enregistrement — la commande, le ticket, le fil de discussion d'origine.
Odoo ne « reçoit » pas les emails au sens d'un serveur de messagerie : il relève périodiquement une boîte aux lettres existante (IMAP ou POP3), ou reçoit les messages poussés par un agent de transport local (MTA). Chaque message relevé est passé au moteur message_process, qui décide à quel enregistrement le rattacher en s'appuyant sur les adresses définies dans mail.alias.domain et sur les mail.alias métier. fetchmail.server est la brique qui réalise cette relève.
Le rapprochement avec les articles précédents est direct : chaque email sortant porte un Reply-To sur le catchall et un Return-Path sur le bounce. La passerelle entrante est ce qui donne un sens à ces adresses — sans elle, les réponses adressées au catchall et les rebonds adressés au bounce arrivent dans une boîte que personne ne relève. Configurer l'envoi sans configurer la réception revient à écrire une adresse de retour sur une enveloppe que l'on ne relèvera jamais.
2. Anatomie de fetchmail.server
Le modèle se configure dans Settings → Technical → Email → Incoming Mail Servers. Ses champs couvrent la connexion, le traitement et le diagnostic.
| Champ | Rôle |
|---|---|
server_type | imap, pop ou local (+ outlook/gmail via modules OAuth) |
server / port | Hôte et port ; le port se règle automatiquement selon le type et is_ssl |
is_ssl | Connexion chiffrée sur port dédié (IMAPS 993, POP3S 995) |
user / password | Identifiants de la boîte relevée |
object_id | « Create a New Record » : type de document créé pour une nouvelle conversation |
priority | Ordre de traitement (valeur basse = priorité haute), défaut 5 |
attach | « Keep Attachments » (défaut activé) : conserver ou retirer les pièces jointes |
original | « Keep Original » : joindre une copie brute complète de chaque email |
state | draft ou done (confirmé après test de connexion) |
date / error_date / error_message | Dernière relève réussie / dernière erreur et son message |
Les ports n'ont pas à être saisis à la main : un onchange les positionne en fonction du type et du chiffrement — 993 pour un IMAP en SSL, 143 sinon ; 995 pour un POP3 en SSL, 110 sinon.
fetchmail.server et le comportement distinct de ses trois types — la différence IMAP / POP3 est déterminante car POP3 efface les messages relevés.
3. IMAP, POP3 ou local : un choix qui n'est pas neutre
Le server_type n'est pas un simple détail de protocole : il change ce qu'il advient des messages sur le serveur d'origine.
| Type | Mécanisme | Effet sur le serveur |
|---|---|---|
| IMAP | Lit les messages non lus, les marque \Seen | Non destructif — les emails restent sur le serveur |
| POP3 | Récupère le message puis appelle dele() | Destructif — les emails sont supprimés après relève |
| local | Reçoit un pipe d'un MTA (postfix) via odoo-mailgate.py | Aucune relève réseau ; le MTA pousse les messages |
dele()). Si une seule instance Odoo relève une boîte dédiée, c'est acceptable ; mais brancher POP3 sur une boîte partagée — lue aussi par un humain ou une autre application — fait disparaître les messages pour les autres lecteurs. En cas de doute, préférer IMAP, qui se contente de marquer les messages comme lus.
Le type local répond à un besoin différent : une réception poussée plutôt que relevée. Le MTA de l'hôte (postfix, par exemple) redirige les emails d'un alias vers le script odoo-mailgate.py, qui injecte le message dans Odoo. Aucun cron n'intervient — c'est le serveur de mail système qui déclenche le traitement à l'arrivée.
4. Le cron de relève et son cycle
Pour les serveurs IMAP et POP3, la relève est orchestrée par un cron unique, mail.ir_cron_mail_gateway_action. La méthode _fetch_mails qu'il appelle est réservée à un usage cron — elle vérifie l'identité du cron appelant et refuse toute exécution manuelle directe. Elle ne traite que les serveurs en état done dont le type n'est pas local.
Le traitement est conçu pour être robuste sur de gros volumes : la relève procède par lots (batch_limit = 50 par défaut) et valide la progression après chaque message, chaque email étant traité dans sa propre transaction (un curseur dédié, distinct de celui qui suit l'avancement). Si un message provoque une erreur, il est compté comme échoué et annulé individuellement — sans interrompre le traitement des suivants.
# Cœur de _fetch_mail() (fetchmail.server, Odoo 19) — extrait simplifié (commit/progress omis)
thread_process_message = functools.partial(
MailThread.message_process,
model=server.object_id.model, # type de doc cible (object_id)
save_original=server.original, # garder la copie brute ?
strip_attachments=(not server.attach), # retirer les pièces jointes ?
)
for message_num, message in server_connection.retrieve_unread_messages():
try:
thread_process_message(message=message) # routage vers l'enregistrement
except Exception:
MailThread.env.cr.rollback() # échec isolé, on continue
server_connection.handled_message(message_num) # \Seen (IMAP) ou dele() (POP3)
Le commit après chaque message n'est pas un détail : il garantit qu'une coupure en cours de relève — redémarrage, perte réseau — ne fasse pas reperdre les messages déjà traités. Chaque serveur est par ailleurs verrouillé pendant sa relève, ce qui évite que deux exécutions concurrentes du cron interrogent la même boîte en parallèle et traitent deux fois le même email.
Deux automatismes encadrent ce cron. À chaque création, modification ou suppression d'un fetchmail.server, la méthode _update_cron active ou désactive le cron selon qu'il reste ou non des serveurs IMAP/POP confirmés. Et lorsqu'un serveur accumule des erreurs de connexion pendant plus de cinq jours, Odoo le repasse en draft et notifie l'administrateur — un garde-fou contre les relèves qui échouent en boucle.
Pour les tests et les diagnostics, un déclenchement manuel reste disponible : le bouton Fetch Now de la fiche serveur (visible une fois le serveur confirmé) force une relève immédiate sans attendre le passage du cron — utile pour valider une configuration fraîchement posée.
5. Le routage des messages entrants
Une fois un message relevé, tout se joue dans message_process. Cette méthode analyse les en-têtes (To, Cc, References, In-Reply-To) et cherche à quel enregistrement rattacher le message. La résolution s'appuie sur les adresses définies dans l'article précédent : le catchall et le bounce de mail.alias.domain, ainsi que les mail.alias métier.
Deux cas se présentent. Si le message répond à une notification existante (en-tête References ou In-Reply-To pointant un identifiant de message déjà connu d'Odoo, ou destinataire correspondant au catchall d'un enregistrement), il est rattaché en suivi au fil de discussion d'origine. Sinon, et si le serveur définit un object_id, Odoo crée un nouvel enregistrement du type indiqué — un nouveau ticket de support, un nouveau lead — et y attache le message comme premier élément de la conversation.
L'ordre de priorité de cette résolution explique pourquoi le Message-Id compte autant : c'est lui qu'Odoo encode dans ses notifications sortantes et qu'il recherche dans les References des réponses. Un client de messagerie qui casse cette chaîne d'en-têtes — en réécrivant le sujet sans conserver les References, par exemple — force la résolution à retomber sur la correspondance d'adresse, voire sur la création d'un nouvel enregistrement, d'où des « doublons » occasionnels dans les fils. Le rôle des alias et du catchall dans cette résolution est détaillé dans l'article mail.alias.domain.
message_process, qui le rattache à un fil existant ou crée un nouveau document selon object_id.
6. L'authentification OAuth entrante (Outlook, Gmail)
Le constat posé pour l'envoi vaut pour la réception : sur Microsoft 365, l'authentification basique a cédé la place à OAuth. Le module d'intégration Outlook ajoute au fetchmail.server un server_type dédié, outlook, qui bascule la connexion IMAP sur le protocole XOAUTH2 — exactement comme le serveur sortant décrit dans l'article sur les relais SMTP.
En sélectionnant ce type, Odoo verrouille la configuration : serveur imap.outlook.com, port 993, SSL obligatoire (une contrainte lève une erreur si le SSL est désactivé), et un scope OAuth dédié à la lecture IMAP. L'authentification ne passe plus par un mot de passe mais par un jeton obtenu via le portail Azure :
# Module microsoft_outlook : OAuth entrant sur fetchmail.server (Odoo 19)
_OUTLOOK_SCOPE = 'https://outlook.office.com/IMAP.AccessAsUser.All'
# onchange_server_type pour server_type == 'outlook' :
# server = 'imap.outlook.com'
# is_ssl = True # SSL imposé (sinon UserError)
# port = 993
# _imap_login__ : connection.authenticate('XOAUTH2', ...) puis select('INBOX')
Le même mécanisme existe pour Gmail via son module d'intégration. Dans les deux cas, la logique est cohérente avec l'évolution du secteur : la réception de mails par mot de passe sur les grands fournisseurs disparaît au profit des jetons OAuth.
7. Pièces jointes, copie originale et diagnostic
Deux options pèsent sur le volume de la base. Le champ attach (« Keep Attachments », activé par défaut) décide si les pièces jointes des emails entrants sont conservées ; le désactiver les retire avant traitement (strip_attachments). Le champ original (« Keep Original ») va plus loin : il joint à chaque message une copie brute complète de l'email d'origine — précieux pour le débogage, mais qui double typiquement la taille de la base de messages. À réserver aux phases de mise au point.
Pour le diagnostic, deux leviers. Le bouton Test & Confirm de la fiche serveur ouvre une connexion réelle et remonte une erreur explicite en cas de problème (nom de serveur invalide, absence de réponse, exception SSL, identifiants refusés). Et en fonctionnement, les champs error_date et error_message conservent la trace de la dernière défaillance — le premier point à consulter quand les réponses clients cessent d'arriver.
8. Configuration pratique et pièges
La création se fait par l'interface (Incoming Mail Servers → New) ou par script pour un déploiement reproductible :
# À exécuter dans odoo-bin shell (mode debug), ou depuis une migration de module.
server = env['fetchmail.server'].create({
'name': 'Réception support — example.com',
'server_type': 'imap', # IMAP : non destructif (recommandé)
'server': 'imap.example.com',
'is_ssl': True, # → port 993 positionné automatiquement
'user': 'support@example.com',
'password': '',
'object_id': env.ref('helpdesk.model_helpdesk_ticket').id, # doc créé si nouvelle conv.
'priority': 5,
'attach': True,
})
server.button_confirm_login() # teste la connexion → state = 'done'
env.cr.commit()
Les pièges récurrents en production :
- POP3 sur une boîte partagée. Le piège n°1 : POP3 supprime les messages relevés. Sur une boîte lue par ailleurs, les emails disparaissent pour les autres. Préférer IMAP en cas de doute.
- Serveur resté en
draft. Le cron ne relève que les serveurs confirmés (done). Un serveur jamais validé par Test & Confirm n'est jamais interrogé. object_idabsent. Sans type de document cible, les messages d'une conversation nouvelle n'ont nulle part où aller. Définir l'object_id(ticket, lead…) selon l'usage de la boîte.- Auto-désactivation silencieuse. Après cinq jours d'erreurs, le serveur repasse en
draftautomatiquement. Une boîte qui « ne reçoit plus » peut simplement avoir été désactivée — vérifiererror_messageet l'état. - SSL et ports. Laisser l'onchange régler les ports (993/995/143/110) plutôt que de les forcer ; pour l'OAuth Outlook, le SSL est imposé.
À retenir : fetchmail.server ferme la boucle du cycle email d'Odoo. Bien réglé — IMAP de préférence, serveur confirmé, object_id défini et alias cohérents avec mail.alias.domain —, il fait revenir chaque réponse dans son fil et chaque rebond sur la bonne adresse. Mal réglé, il est la cause silencieuse des conversations qui se perdent et des clients qui « n'ont jamais eu de réponse ».
L'article suivant de la série quittera l'infrastructure de transport pour le cœur applicatif : le mixin mail.thread et le système des abonnés (followers), qui décident qui reçoit quoi sur chaque enregistrement.
Voir aussi dans ce parcours Infrastructure
mail.alias.domain — bounce, catchall, default_from
Les adresses sur lesquelles s'appuie le routage entrant pour rattacher les réponses.
Lire l'article →Relais SMTP alternatifs — Mailjet, SES, Microsoft 365
Le pendant sortant, et le même OAuth XOAUTH2 que la réception Outlook.
Lire l'article →Série Tech-Email — Article 6/14 — Parcours Infrastructure emailing Odoo 19.
Articles complémentaires
Architecture mail Odoo — les 4 couches
HUB de la série : transport / message / template / mass et flux complet.
Lire l'article →Actions serveur, cron et automations
ir.cron et base.automation — le moteur qui déclenche la relève fetchmail.
Source officielle : Odoo 19 — Email communication.