Skip to content

Commit 3fd2858

Browse files
committed
[FIX] fetch..folder: make manual attach work again
In the past manual attachment was limited to the model defined in the folder, by overriding the selection for the object in a fields_view_get method. This no longer works as the method is not called any more. Setting the selection to just this model through other methods also does not work, neither setting a default model on a Reference field. So basically now attaching a mail to any object is allowed.
1 parent 42042e7 commit 3fd2858

6 files changed

Lines changed: 89 additions & 102 deletions

File tree

fetchmail_attach_from_folder/models/fetchmail_server_folder.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -430,25 +430,20 @@ def _get_algorithm(self):
430430
)
431431
return None
432432

433+
@api.model
433434
def attach_mail(self, match_object, message_dict):
434435
"""Attach mail to match_object."""
435-
self.ensure_one()
436-
partner = False
437-
model_name = self.model_id.model
438-
if model_name == "res.partner":
439-
partner = match_object
440-
elif "partner_id" in self.env[model_name]._fields:
441-
partner = match_object.partner_id
442436
message_model = self.env["mail.message"]
443437
msg_values = {
444438
key: val
445439
for key, val in message_dict.items()
446440
if key in message_model._fields
447441
}
442+
partner = self._get_partner_from_object(match_object)
448443
msg_values.update(
449444
{
450445
"author_id": partner and partner.id or False,
451-
"model": model_name,
446+
"model": match_object._name,
452447
"res_id": match_object.id,
453448
"message_type": "email",
454449
}
@@ -463,10 +458,19 @@ def attach_mail(self, match_object, message_dict):
463458
message = message_model.create(msg_values)
464459
_logger.debug(
465460
"Message with id %(message_id)s created"
466-
" for %(model_name)s with id %(thread_id)s",
461+
" for %(match_object._name)s with id %(thread_id)s",
467462
{
468463
"message_id": message.id,
469-
"model_name": model_name,
464+
"match_object._name": match_object._name,
470465
"thread_id": match_object.id,
471466
},
472467
)
468+
469+
@api.model
470+
def _get_partner_from_object(self, match_object):
471+
"""Get partner from object."""
472+
if match_object._name == "res.partner":
473+
return match_object
474+
if "partner_id" in match_object._fields:
475+
return match_object.partner_id
476+
return False
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Copyright - 2015-2026 Therp BV <https://therp.nl>.
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3+
4+
5+
def get_message_body(email, subject):
6+
"""Get Message Body, as returned by fetch() from connection.
7+
8+
fetch returns a list of tuples with the message information.
9+
"""
10+
return [
11+
(
12+
"1 (RFC822 {1149}",
13+
"Return-Path: <ronald@acme.com>\r\n"
14+
"Delivered-To: demo@yourcompany.example.com\r\n"
15+
"Received: from localhost (localhost [127.0.0.1])\r\n"
16+
"\tby vanaheim.acme.com (Postfix) with ESMTP id 14A3183163\r\n"
17+
"\tfor <demo@yourcompany.example.com>;"
18+
" Wed, 23 Jul 2025 16:03:52 +0200 (CEST)\r\n"
19+
"To: Test User <nonexistingemail@yourcompany.example.com>\r\n"
20+
f"From: Reynaert de Vos <{email}>\r\n"
21+
f"Subject: {subject}\r\n"
22+
"Message-ID: <485a8041-d560-a981-5afc-d31c1f136748@acme.com>\r\n"
23+
"Date: Mon, 26 Mar 2018 16:03:51 +0200\r\n"
24+
"User-Agent: Mock Test\r\n"
25+
"MIME-Version: 1.0\r\n"
26+
"Content-Type: text/plain; charset=utf-8\r\n"
27+
"Content-Language: en-US\r\n"
28+
"Content-Transfer-Encoding: 7bit\r\n\r\n"
29+
"Hallo Wereld!\r\n",
30+
)
31+
]

fetchmail_attach_from_folder/tests/test_attach_mail_manually.py

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,21 @@
33
# pylint: disable=method-required-super
44
from unittest.mock import MagicMock, patch
55

6+
from odoo.fields import Command
67
from odoo.tests.common import TransactionCase
78

89
from odoo.addons.mail.models.fetchmail import FetchmailServer
910

11+
from .common import get_message_body
12+
13+
14+
def get_message_org(email, subject):
15+
# get_message_body returns an array of tuples, similar to what
16+
# IMAP4.fetch(...) would return. There will be a tuple
17+
# for each message. The actual messages are in the second
18+
# part of each tuple.
19+
return get_message_body(email, subject)[0][1]
20+
1021

1122
class TestAttachMailManually(TransactionCase):
1223
@classmethod
@@ -57,15 +68,8 @@ def _mock_connection(self):
5768
return mock_conn
5869

5970
def _mock_fetch_msg(self, connection, message_uid):
60-
"""Return a tuple like the real fetch_msg: (dict, bytes)"""
61-
mail_message = {
62-
"subject": "Test",
63-
"date": "2025-07-23 12:00:00",
64-
"from": "test@example.com",
65-
"body": "<p>Body</p>",
66-
}
67-
raw_message = b"Raw MIME message here"
68-
return mail_message, raw_message
71+
"""Return raw message body (bytes)."""
72+
return get_message_org("test@example.com", "Test")
6973

7074
@patch.object(FetchmailServer, "connect")
7175
def test_default_get_populates_mail_ids(self, mock_connect):
@@ -90,35 +94,24 @@ def test_attach_mails_only_with_object_id(self, mock_connect):
9094
"""Only mails with object_id should be attached."""
9195
mock_conn = self._mock_connection()
9296
mock_connect.return_value = mock_conn
97+
message_org = get_message_org("test@example.com", "With Object")
9398
with patch.object(
9499
self.folder.__class__,
95100
"fetch_msg",
96-
side_effect=lambda conn, message_uid: (
97-
{
98-
"subject": "With Object",
99-
"date": "2025-07-23",
100-
"from": "test@example.com",
101-
"body": "<p>Body</p>",
102-
},
103-
b"raw_message",
104-
),
101+
side_effect=lambda conn, message_uid: message_org,
105102
):
106103
wizard = self.Wizard.create(
107104
{
108105
"folder_id": self.folder.id,
109106
"mail_ids": [
110-
(
111-
0,
112-
0,
107+
Command.create(
113108
{
114109
"message_uid": "1",
115110
"subject": "No Object",
116111
"object_id": False,
117112
},
118113
),
119-
(
120-
0,
121-
0,
114+
Command.create(
122115
{
123116
"message_uid": "2",
124117
"subject": "With Object",
@@ -139,13 +132,13 @@ def test_prepare_mail_returns_expected_dict(self):
139132
"""Test _prepare_mail returns correct structure."""
140133
folder = self.folder
141134
message_uid = "123"
142-
mail_message = {
135+
message_dict = {
143136
"subject": "Test",
144137
"date": "2025-07-23",
145138
"from": "test@example.com",
146139
"body": "<p>Body</p>",
147140
}
148-
result = self.Wizard._prepare_mail(folder, message_uid, mail_message)
141+
result = self.Wizard._prepare_mail(folder, message_uid, message_dict)
149142
expected = {
150143
"message_uid": "123",
151144
"subject": "Test",
@@ -158,9 +151,10 @@ def test_prepare_mail_returns_expected_dict(self):
158151

159152
def test_wizard_name_is_translated(self):
160153
"""Test that default name is translated."""
154+
message_org = get_message_org("test@example.com", "With Object")
161155
with (
162156
patch.object(FetchmailServer, "connect", return_value=MagicMock()),
163-
patch.object(self.folder.__class__, "fetch_msg", return_value=({}, b"raw")),
157+
patch.object(self.folder.__class__, "fetch_msg", return_value=message_org),
164158
patch.object(
165159
self.folder.__class__, "get_message_uids", return_value=[b"1"]
166160
),

fetchmail_attach_from_folder/tests/test_match_algorithms.py

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,45 +9,13 @@
99
from odoo.tests.common import TransactionCase
1010

1111
from ..match_algorithm import email_domain
12+
from .common import get_message_body
1213

1314
TEST_EMAIL = "reynaert@dutchsagas.nl"
1415
TEST_SUBJECT = "Test subject"
1516
MAIL_MESSAGE = {"subject": TEST_SUBJECT, "to": "demo@yourcompany.example.com"}
1617

1718

18-
def get_message_body(email, subject):
19-
"""Get Message Body, as returned by fetch() from connection.
20-
21-
fetch returns a list of tuples with the message information.
22-
"""
23-
return [
24-
(
25-
"1 (RFC822 {1149}",
26-
"Return-Path: <ronald@acme.com>\r\n"
27-
"Delivered-To: demo@yourcompany.example.com\r\n"
28-
"Received: from localhost (localhost [127.0.0.1])\r\n"
29-
"\tby vanaheim.acme.com (Postfix) with ESMTP id 14A3183163\r\n"
30-
"\tfor <demo@yourcompany.example.com>;"
31-
" Mon, 26 Mar 2018 16:03:52 +0200 (CEST)\r\n"
32-
"To: Test User <nonexistingemail@yourcompany.example.com>\r\n"
33-
"From: Reynaert de Vos <%(test_email)s>\r\n"
34-
"Subject: %(test_subject)s\r\n"
35-
"Message-ID: <485a8041-d560-a981-5afc-d31c1f136748@acme.com>\r\n"
36-
"Date: Mon, 26 Mar 2018 16:03:51 +0200\r\n"
37-
"User-Agent: Mock Test\r\n"
38-
"MIME-Version: 1.0\r\n"
39-
"Content-Type: text/plain; charset=utf-8\r\n"
40-
"Content-Language: en-US\r\n"
41-
"Content-Transfer-Encoding: 7bit\r\n\r\n"
42-
"Hallo Wereld!\r\n"
43-
% {
44-
"test_email": email,
45-
"test_subject": subject,
46-
},
47-
)
48-
]
49-
50-
5119
class MockConnection:
5220
def select(self, path=None):
5321
"""Mock selecting a folder."""
Lines changed: 20 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
# Copyright 2013-2018 Therp BV <https://therp.nl>.
1+
# Copyright 2013-2026 Therp BV <https://therp.nl>.
22
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
33
import logging
44

55
from odoo import _, api, fields, models
6+
from odoo.fields import Command
67

78
_logger = logging.getLogger(__name__)
89

@@ -20,14 +21,13 @@ class AttachMailManually(models.TransientModel):
2021
)
2122

2223
@api.model
23-
def _prepare_mail(self, folder, message_uid, mail_message):
24+
def _prepare_mail(self, folder, message_uid, message_dict):
2425
return {
2526
"message_uid": message_uid,
26-
"subject": mail_message.get("subject", ""),
27-
"date": mail_message.get("date") or False,
28-
"body": mail_message.get("body", ""),
29-
"email_from": mail_message.get("from", ""),
30-
"object_id": f"{folder.model_id.model},-1",
27+
"subject": message_dict.get("subject", ""),
28+
"date": message_dict.get("date") or False,
29+
"body": message_dict.get("body", ""),
30+
"email_from": message_dict.get("from", ""),
3131
}
3232

3333
@api.model
@@ -45,9 +45,10 @@ def default_get(self, fields_list):
4545
criteria = "FLAGGED" if folder.flag_nonmatching else folder.get_criteria()
4646
message_uids = folder.get_message_uids(connection, criteria)
4747
for message_uid in message_uids[0].split():
48-
mail_message, message_org = folder.fetch_msg(connection, message_uid)
48+
message_org = folder.fetch_msg(connection, message_uid)
49+
message_dict = folder._get_message_dict(message_org)
4950
defaults["mail_ids"].append(
50-
(0, 0, self._prepare_mail(folder, message_uid, mail_message))
51+
Command.create(self._prepare_mail(folder, message_uid, message_dict))
5152
)
5253
connection.close()
5354
return defaults
@@ -62,33 +63,15 @@ def attach_mails(self):
6263
if not mail.object_id:
6364
continue
6465
message_uid = mail.message_uid
65-
mail_message, message_org = folder.fetch_msg(connection, message_uid)
66-
folder.attach_mail(mail.object_id, mail_message)
66+
message_org = folder.fetch_msg(connection, message_uid)
67+
message_dict = folder._get_message_dict(message_org)
68+
folder.attach_mail(mail.object_id, message_dict)
6769
folder.update_msg(
6870
connection, message_uid, matched=True, flagged=folder.flag_nonmatching
6971
)
7072
connection.close()
7173
return {"type": "ir.actions.act_window_close"}
7274

73-
@api.model
74-
def fields_view_get(
75-
self, view_id=None, view_type="form", toolbar=False, submenu=False
76-
):
77-
# TODO: Change or replace this...
78-
result = super(AttachMailManually, self).fields_view_get(
79-
view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu
80-
)
81-
if view_type != "form":
82-
return result
83-
folder_model = self.env["fetchmail.server.folder"]
84-
folder_id = self.env.context.get("folder_id")
85-
folder = folder_model.browse([folder_id])
86-
form = result["fields"]["mail_ids"]["views"]["form"]
87-
form["fields"]["object_id"]["selection"] = [
88-
(folder.model_id.model, folder.model_id.name)
89-
]
90-
return result
91-
9275

9376
class AttachMailManuallyMail(models.TransientModel):
9477
"""Attach single mail to selected documents."""
@@ -97,11 +80,16 @@ class AttachMailManuallyMail(models.TransientModel):
9780
_description = __doc__
9881

9982
wizard_id = fields.Many2one("fetchmail.attach.mail.manually", readonly=True)
100-
message_uid = fields.Char("Message id", readonly=True)
83+
message_uid = fields.Char("Message id")
10184
subject = fields.Char(readonly=True)
10285
date = fields.Datetime(readonly=True)
10386
email_from = fields.Char("From", readonly=True)
10487
body = fields.Html(readonly=True)
10588
object_id = fields.Reference(
106-
lambda self: [(m.model, m.name) for m in self.env["ir.model"].search([])]
89+
selection=lambda self: self._get_model_selection(),
10790
)
91+
92+
def _get_model_selection(self):
93+
"""Selection from all models in the system."""
94+
Model = self.env["ir.model"]
95+
return [(m.model, m.name) for m in Model.search([("transient", "=", False)])]

fetchmail_attach_from_folder/wizard/attach_mail_manually.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@
1010
<field name="folder_id" />
1111
<field name="mail_ids" nolabel="1" colspan="4">
1212
<tree create="0">
13+
<field name="message_uid" invisible="1" />
1314
<field name="email_from" />
1415
<field name="subject" />
1516
<field name="date" />
1617
<field name="object_id" />
1718
</tree>
1819
<form>
1920
<group>
21+
<field name="message_uid" invisible="1" />
2022
<field name="email_from" />
2123
<field name="subject" />
2224
<field name="date" />

0 commit comments

Comments
 (0)