Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions fetchmail_incoming_test/README.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association

=======================
Fetchmail Incoming Test
=======================
Expand All @@ -17,7 +13,7 @@ Fetchmail Incoming Test
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fmail-lightgray.png?logo=github
Expand All @@ -44,6 +40,11 @@ record (helpdesk ticket, lead, task, ...).
It is meant for testing mail-driven flows on environments that have no
inbound mail server, like test instances.

It also adds a **Replay Email File** button, which takes a raw message
file (usually a ``.eml`` exported from a mail client) and feeds it to
the gateway untouched. Useful to reproduce how a specific email that a
customer received was handled, byte for byte.

**Table of contents**

.. contents::
Expand All @@ -70,6 +71,15 @@ The message goes through the standard mail gateway and the created
record (e.g. a Helpdesk ticket) appears, just as if the email had been
received.

To replay a real email instead of composing one:

- Click **Replay Email File** (button on the form, or the *Action*
menu).
- Upload the message file, then click **Process**.

The file is sent to the gateway as if it was received through fetchmail.
Malformed HTML and unusual headers are preserved.

Bug Tracker
===========

Expand Down
1 change: 1 addition & 0 deletions fetchmail_incoming_test/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"data": [
"security/ir.model.access.csv",
"wizards/fetchmail_incoming_test_views.xml",
"wizards/fetchmail_incoming_test_eml_views.xml",
"views/fetchmail_server_views.xml",
],
"installable": True,
Expand Down
5 changes: 5 additions & 0 deletions fetchmail_incoming_test/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ it is then routed by alias and creates the target record

It is meant for testing mail-driven flows on environments that have no inbound mail server,
like test instances.

It also adds a **Replay Email File** button, which takes a raw message file
(usually a `.eml` exported from a mail client) and feeds it to the gateway
untouched. Useful to reproduce how a specific email that a customer received
was handled, byte for byte.
8 changes: 8 additions & 0 deletions fetchmail_incoming_test/readme/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,11 @@ To use this module, follow these steps:

The message goes through the standard mail gateway and the created record (e.g. a
Helpdesk ticket) appears, just as if the email had been received.

To replay a real email instead of composing one:

- Click **Replay Email File** (button on the form, or the *Action* menu).
- Upload the message file, then click **Process**.

The file is sent to the gateway as if it was received through fetchmail.
Malformed HTML and unusual headers are preserved.
1 change: 1 addition & 0 deletions fetchmail_incoming_test/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_fetchmail_incoming_test,fetchmail.incoming.test,model_fetchmail_incoming_test,base.group_system,1,1,1,1
access_fetchmail_incoming_test_eml,fetchmail.incoming.test.eml,model_fetchmail_incoming_test_eml,base.group_system,1,1,1,1
38 changes: 22 additions & 16 deletions fetchmail_incoming_test/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>README.rst</title>
<title>Fetchmail Incoming Test</title>
<style type="text/css">

/*
Expand Down Expand Up @@ -360,21 +360,16 @@
</style>
</head>
<body>
<div class="document">
<div class="document" id="fetchmail-incoming-test">
<h1 class="title">Fetchmail Incoming Test</h1>


<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
</a>
<div class="section" id="fetchmail-incoming-test">
<h1>Fetchmail Incoming Test</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:86970aae24f7f8dd9e8457424e8c5118ab885ce2db29dfaa499e6a7011e88d4b
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/mail/tree/18.0/fetchmail_incoming_test"><img alt="OCA/mail" src="https://img.shields.io/badge/github-OCA%2Fmail-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/mail-18-0/mail-18-0-fetchmail_incoming_test"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/mail&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/mail/tree/18.0/fetchmail_incoming_test"><img alt="OCA/mail" src="https://img.shields.io/badge/github-OCA%2Fmail-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/mail-18-0/mail-18-0-fetchmail_incoming_test"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/mail&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module lets you simulate an incoming email directly from Odoo,
without a real mail server or SMTP client.</p>
<p>It adds a <strong>Simulate Incoming Email</strong> button (and matching <em>Action</em> menu
Expand All @@ -384,6 +379,10 @@ <h1>Fetchmail Incoming Test</h1>
record (helpdesk ticket, lead, task, …).</p>
<p>It is meant for testing mail-driven flows on environments that have no
inbound mail server, like test instances.</p>
<p>It also adds a <strong>Replay Email File</strong> button, which takes a raw message
file (usually a <tt class="docutils literal">.eml</tt> exported from a mail client) and feeds it to
the gateway untouched. Useful to reproduce how a specific email that a
customer received was handled, byte for byte.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
Expand All @@ -398,7 +397,7 @@ <h1>Fetchmail Incoming Test</h1>
</ul>
</div>
<div class="section" id="usage">
<h2><a class="toc-backref" href="#toc-entry-1">Usage</a></h2>
<h1><a class="toc-backref" href="#toc-entry-1">Usage</a></h1>
<p>To use this module, follow these steps:</p>
<ul class="simple">
<li>Go to Settings &gt; Technical &gt; Email &gt; Incoming Mail Servers.</li>
Expand All @@ -416,25 +415,33 @@ <h2><a class="toc-backref" href="#toc-entry-1">Usage</a></h2>
<p>The message goes through the standard mail gateway and the created
record (e.g. a Helpdesk ticket) appears, just as if the email had been
received.</p>
<p>To replay a real email instead of composing one:</p>
<ul class="simple">
<li>Click <strong>Replay Email File</strong> (button on the form, or the <em>Action</em>
menu).</li>
<li>Upload the message file, then click <strong>Process</strong>.</li>
</ul>
<p>The file is sent to the gateway as if it was received through fetchmail.
Malformed HTML and unusual headers are preserved.</p>
</div>
<div class="section" id="bug-tracker">
<h2><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h2>
<h1><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/mail/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/mail/issues/new?body=module:%20fetchmail_incoming_test%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h2><a class="toc-backref" href="#toc-entry-3">Credits</a></h2>
<h1><a class="toc-backref" href="#toc-entry-3">Credits</a></h1>
<div class="section" id="authors">
<h3><a class="toc-backref" href="#toc-entry-4">Authors</a></h3>
<h2><a class="toc-backref" href="#toc-entry-4">Authors</a></h2>
<ul class="simple">
<li>Camptocamp</li>
</ul>
</div>
<div class="section" id="contributors">
<h3><a class="toc-backref" href="#toc-entry-5">Contributors</a></h3>
<h2><a class="toc-backref" href="#toc-entry-5">Contributors</a></h2>
<ul class="simple">
<li><a class="reference external" href="https://www.camptocamp.com">Camptocamp</a>:<ul>
<li>David Gallay &lt;<a class="reference external" href="mailto:david.gallay&#64;camptocamp.com">david.gallay&#64;camptocamp.com</a>&gt;</li>
Expand All @@ -443,7 +450,7 @@ <h3><a class="toc-backref" href="#toc-entry-5">Contributors</a></h3>
</ul>
</div>
<div class="section" id="maintainers">
<h3><a class="toc-backref" href="#toc-entry-6">Maintainers</a></h3>
<h2><a class="toc-backref" href="#toc-entry-6">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
Expand All @@ -456,6 +463,5 @@ <h3><a class="toc-backref" href="#toc-entry-6">Maintainers</a></h3>
</div>
</div>
</div>
</div>
</body>
</html>
1 change: 1 addition & 0 deletions fetchmail_incoming_test/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from . import test_fetchmail_incoming_test
from . import test_fetchmail_incoming_test_eml
44 changes: 44 additions & 0 deletions fetchmail_incoming_test/tests/test_fetchmail_incoming_test_eml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright 2026 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

import base64

from odoo.tests.common import TransactionCase


class TestFetchmailIncomingTestEml(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.domain = cls.env["mail.alias.domain"].search([], limit=1)

def _wizard(self, raw):
return self.env["fetchmail.incoming.test.eml"].create(
{
"eml_file": base64.b64encode(raw),
"eml_filename": "message.eml",
}
)

def _raw(self, email_to):
return (
b"From: Sender <sender@example.com>\r\n"
b"To: " + email_to.encode() + b"\r\n"
b"Subject: hello gateway\r\n"
b"Message-Id: <replayed@example.com>\r\n"
b"Content-Type: text/html; charset=UTF-8\r\n"
b"\r\n"
b"<p>body content</p>\r\n"
)

def test_build_raw_message_keeps_the_file_untouched(self):
raw = self._raw(f"anything@{self.domain.name}")
self.assertEqual(self._wizard(raw)._build_raw_message(), raw)

def test_process_reaches_the_gateway(self):
# No alias matches this address: the gateway must reject it. This proves
# action_process really feeds message_process instead of swallowing the
# email, without depending on any routable model.
raw = self._raw(f"no-such-alias@{self.domain.name}")
with self.assertRaisesRegex(ValueError, "No possible route found"):
self._wizard(raw).action_process()
5 changes: 5 additions & 0 deletions fetchmail_incoming_test/views/fetchmail_server_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
type="action"
string="Simulate Incoming Email"
/>
<button
name="%(action_fetchmail_incoming_test_eml)d"
type="action"
string="Replay Email File"
/>
</xpath>
</field>
</record>
Expand Down
2 changes: 2 additions & 0 deletions fetchmail_incoming_test/wizards/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from . import fetchmail_incoming_test_mixin
from . import fetchmail_incoming_test
from . import fetchmail_incoming_test_eml
22 changes: 2 additions & 20 deletions fetchmail_incoming_test/wizards/fetchmail_incoming_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
from email.message import EmailMessage
from email.utils import make_msgid

from odoo import _, fields, models
from odoo import fields, models


class FetchmailIncomingTest(models.TransientModel):
_name = "fetchmail.incoming.test"
_inherit = "fetchmail.incoming.test.mixin"
_description = "Simulate an Incoming Email"

email_from = fields.Char(
Expand Down Expand Up @@ -40,22 +41,3 @@ def _build_raw_message(self):
message["Message-Id"] = make_msgid()
message.set_content(self.body or "", subtype="html")
return message.as_bytes()

def action_process(self):
"""Feed the composed email to the mail gateway as a real inbound one."""
self.ensure_one()
thread_id = self.env["mail.thread"].message_process(
None, self._build_raw_message()
)
return {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"type": "success",
"title": _("Email processed"),
"message": _("The gateway created record #%s.", thread_id),
"sticky": False,
# Close the wizard dialog once the notification is shown.
"next": {"type": "ir.actions.act_window_close"},
},
}
27 changes: 27 additions & 0 deletions fetchmail_incoming_test/wizards/fetchmail_incoming_test_eml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright 2026 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

import base64

from odoo import fields, models


class FetchmailIncomingTestEml(models.TransientModel):
_name = "fetchmail.incoming.test.eml"
_inherit = "fetchmail.incoming.test.mixin"
_description = "Simulate an Incoming Email from a File"

eml_file = fields.Binary(
string="Email File",
required=True,
attachment=False,
help="Raw message, as exported by a mail client. Usually a .eml file.",
)
eml_filename = fields.Char()

def _build_raw_message(self):
# The file is fed to the gateway untouched: re-serializing a parsed
# message would drop what makes a real mail worth replaying, such as
# malformed HTML or unusual headers.
self.ensure_one()
return base64.b64decode(self.eml_file)
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="fetchmail_incoming_test_eml_view_form" model="ir.ui.view">
<field name="name">fetchmail.incoming.test.eml.form</field>
<field name="model">fetchmail.incoming.test.eml</field>
<field name="arch" type="xml">
<form string="Replay Email File">
<group>
<field name="eml_filename" invisible="1" />
<field name="eml_file" filename="eml_filename" />
</group>
<footer>
<button
name="action_process"
type="object"
string="Process"
class="btn-primary"
/>
<button string="Cancel" class="btn-secondary" special="cancel" />
</footer>
</form>
</field>
</record>

<record id="action_fetchmail_incoming_test_eml" model="ir.actions.act_window">
<field name="name">Replay Email File</field>
<field name="res_model">fetchmail.incoming.test.eml</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<!-- Show as an entry of the Incoming Mail Server 'Action' menu. A cog
action opens without force-saving the server record, unlike a header
button (which would fail on the required server/user fields when
those come from server_env). -->
<field name="binding_model_id" ref="mail.model_fetchmail_server" />
<field name="binding_view_types">list,form</field>
</record>
</odoo>
32 changes: 32 additions & 0 deletions fetchmail_incoming_test/wizards/fetchmail_incoming_test_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Copyright 2026 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import _, models


class FetchmailIncomingTestMixin(models.AbstractModel):
_name = "fetchmail.incoming.test.mixin"
_description = "Simulate an Incoming Email"

def _build_raw_message(self):
"""Return the incoming email as raw bytes, as a mail server would."""
raise NotImplementedError

def action_process(self):
"""Feed the email to the mail gateway as a real inbound one."""
self.ensure_one()
thread_id = self.env["mail.thread"].message_process(
None, self._build_raw_message()
)
return {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"type": "success",
"title": _("Email processed"),
"message": _("The gateway created record #%s.", thread_id),
"sticky": False,
# Close the wizard dialog once the notification is shown.
"next": {"type": "ir.actions.act_window_close"},
},
}
Loading