[FW][MERGE] mail: provide fixes for gateway, composer and partner merge

Merge fixes coming notably from community reporting.

Containing
    [FIX] mail: handle wrong Final-Recipient header in incoming emails
    [FIX] mail: handle parsing of void body in incoming emails
    [FIX] mail: fix ACLs issue with mail composer in new mode
    [FW][IMP] test_mail: test models with type do not mess with attachmen…
    [FIX] mail: prevent useless write when pinning a channel
    [IMP] base: merge activities of merged partners

See sub commits for more details

Task-2641572

closes odoo/odoo#77005

Forward-port-of: odoo/odoo#76506
Forward-port-of: odoo/odoo#76159
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Odoo's Mergebot
2021-09-22 21:25:57 +02:00
committed by GitHub
13 changed files with 322 additions and 9 deletions
+2
View File
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_automation
from . import test_mail_composer
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import tagged, HttpCase
@tagged('-at_install', 'post_install', 'mail_composer')
class TestMailFullComposer(HttpCase):
def test_full_composer_tour(self):
self.env['mail.template'].create({
'name': 'Test template', # name hardcoded for test
'partner_to': '${object.id}',
'lang': '${object.lang}',
'auto_delete': True,
'model_id': self.ref('base.model_res_partner'),
})
test_user = self.env['res.users'].create({
'email': 'testuser@testuser.com',
'groups_id': [
(6, 0, [self.ref('base.group_user'), self.ref('base.group_partner_manager')]),
],
'name': 'Test User',
'login': 'testuser',
'password': 'testuser',
})
automated_action = self.env['base.automation'].create({
'name': 'Test',
'active': True,
'trigger': 'on_change',
'on_change_field_ids': (4, self.ref('mail.field_mail_compose_message__template_id'),),
'state': 'code',
'model_id': self.env.ref('mail.model_mail_compose_message').id,
})
self.start_tour("/web#id=%d&model=res.partner" % test_user.partner_id, 'mail/static/tests/tours/mail_full_composer_test_tour.js', login='testuser')
automated_action.unlink()
+5 -3
View File
@@ -1199,8 +1199,8 @@ class MailThread(models.AbstractModel):
Indeed those aspects should be covered by the html_sanitize method
located in tools. """
body, attachments = payload_dict['body'], payload_dict['attachments']
if not body:
return payload_dict
if not body.strip():
return {'body': body, 'attachments': attachments}
try:
root = lxml.html.fromstring(body)
except ValueError:
@@ -1321,7 +1321,9 @@ class MailThread(models.AbstractModel):
if dsn_part and len(dsn_part.get_payload()) > 1:
dsn = dsn_part.get_payload()[1]
final_recipient_data = tools.decode_message_header(dsn, 'Final-Recipient')
bounced_email = tools.email_normalize(final_recipient_data.split(';', 1)[1].strip())
# old servers may hold void or invalid Final-Recipient header
if final_recipient_data and ";" in final_recipient_data:
bounced_email = tools.email_normalize(final_recipient_data.split(';', 1)[1].strip())
if bounced_email:
bounced_partner = self.env['res.partner'].sudo().search([('email_normalized', '=', bounced_email)])
+10 -4
View File
@@ -109,20 +109,26 @@ class MockEmail(common.BaseCase):
def format(self, template, to='groups@example.com, other@gmail.com', subject='Frogs',
email_from='Sylvie Lelitre <test.sylvie.lelitre@agrolait.com>', return_path='', cc='',
extra='', msg_id='<1198923581.41972151344608186760.JavaMail@agrolait.com>'):
extra='', msg_id='<1198923581.41972151344608186760.JavaMail@agrolait.com>',
**kwargs):
return template.format(
subject=subject, to=to, cc=cc,
email_from=email_from, return_path=return_path,
extra=extra, msg_id=msg_id)
extra=extra, msg_id=msg_id,
**kwargs)
def format_and_process(self, template, email_from, to, subject='Frogs', cc='',
return_path='', extra='', msg_id=False,
model=None, target_model='mail.test.gateway', target_field='name'):
model=None, target_model='mail.test.gateway', target_field='name',
**kwargs):
self.assertFalse(self.env[target_model].search([(target_field, '=', subject)]))
if not msg_id:
msg_id = "<%.7f-test@iron.sky>" % (time.time())
mail = self.format(template, to=to, subject=subject, cc=cc, return_path=return_path, extra=extra, email_from=email_from, msg_id=msg_id)
mail = self.format(template, to=to, subject=subject, cc=cc,
return_path=return_path, extra=extra,
email_from=email_from, msg_id=msg_id,
**kwargs)
self.env['mail.thread'].message_process(model, mail)
return self.env[target_model].search([(target_field, '=', subject)])
+1 -1
View File
@@ -5,7 +5,7 @@ from odoo.tests.common import tagged, HttpCase
from odoo import Command
@tagged('-at_install', 'post_install')
@tagged('-at_install', 'post_install', 'mail_composer')
class TestMailFullComposer(HttpCase):
def test_full_composer_tour(self):
+46
View File
@@ -2,6 +2,8 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.mail.tests.common import MailCommon
from odoo.tests.common import Form, users
# samples use effective TLDs from the Mozilla public suffix
# list at http://publicsuffix.org
@@ -123,3 +125,47 @@ class TestPartner(MailCommon):
new_msg = new_user.message_ids[0]
self.assertIn('Portal Access Granted', new_msg.body)
self.assertEqual(new_msg.subtype_id, subtype_note)
@users('admin')
def test_res_partner_merge_wizards(self):
Partner = self.env['res.partner']
p1 = Partner.create({'name': 'Customer1', 'email': 'test1@test.example.com'})
p1_msg_ids_init = p1.message_ids
p2 = Partner.create({'name': 'Customer2', 'email': 'test2@test.example.com'})
p2_msg_ids_init = p2.message_ids
p3 = Partner.create({'name': 'Other (dup email)', 'email': 'test1@test.example.com'})
# add some mail related documents
p1.message_subscribe(partner_ids=p3.ids)
p1_act1 = p1.activity_schedule(act_type_xmlid='mail.mail_activity_data_todo')
p1_msg1 = p1.message_post(
body='<p>Log on P1</p>',
subtype_id=self.env.ref('mail.mt_comment').id
)
self.assertEqual(p1.activity_ids, p1_act1)
self.assertEqual(p1.message_follower_ids.partner_id, self.partner_admin + p3)
self.assertEqual(p1.message_ids, p1_msg_ids_init + p1_msg1)
self.assertEqual(p2.activity_ids, self.env['mail.activity'])
self.assertEqual(p2.message_follower_ids.partner_id, self.partner_admin)
self.assertEqual(p2.message_ids, p2_msg_ids_init)
MergeForm = Form(self.env['base.partner.merge.automatic.wizard'].with_context(
active_model='res.partner',
active_ids=(p1 + p2).ids
))
self.assertEqual(MergeForm.partner_ids[:], p1 + p2)
self.assertEqual(MergeForm.dst_partner_id, p2)
merge_form = MergeForm.save()
merge_form.action_merge()
# check destination and removal
self.assertFalse(p1.exists())
self.assertTrue(p2.exists())
# check mail documents have been moved
self.assertEqual(p2.activity_ids, p1_act1)
# TDE note: currently not working as soon as there is a single partner duplicated -> should be improved
# self.assertEqual(p2.message_follower_ids.partner_id, self.partner_admin + p3)
all_msg = p2_msg_ids_init + p1_msg_ids_init + p1_msg1
self.assertEqual(len(p2.message_ids), len(all_msg) + 1, 'Should have original messages + a log')
self.assertTrue(all(msg in p2.message_ids for msg in all_msg))
@@ -75,6 +75,10 @@ class MailComposer(models.TransientModel):
if result.get('composition_mode') == 'comment' and (set(fields) & set(['model', 'res_id', 'partner_ids', 'record_name', 'subject'])):
result.update(self.get_record_data(result))
# when being in new mode, create_uid is not granted -> ACLs issue may arise
if 'create_uid' in fields and 'create_uid' not in result:
result['create_uid'] = self.env.uid
filtered_result = dict((fname, result[fname]) for fname in result if fname in fields)
return filtered_result
+130
View File
@@ -43,6 +43,50 @@ Content-Transfer-Encoding: quoted-printable
------=_Part_4200734_24778174.1344608186754--
"""
MAIL_TEMPLATE_EXTRA_HTML = """Return-Path: <whatever-2a840@postmaster.twitter.com>
To: {to}
cc: {cc}
Received: by mail1.openerp.com (Postfix, from userid 10002)
id 5DF9ABFB2A; Fri, 10 Aug 2012 16:16:39 +0200 (CEST)
From: {email_from}
Subject: {subject}
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="----=_Part_4200734_24778174.1344608186754"
Date: Fri, 10 Aug 2012 14:16:26 +0000
Message-ID: {msg_id}
{extra}
------=_Part_4200734_24778174.1344608186754
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
Please call me as soon as possible this afternoon!
--
Sylvie
------=_Part_4200734_24778174.1344608186754
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>=20
<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8" />
</head>=20
<body style=3D"margin: 0; padding: 0; background: #ffffff;-webkit-text-size-adjust: 100%;">=20
<p>Please call me as soon as possible this afternoon!</p>
{extra_html}
<p>--<br/>
Sylvie
<p>
</body>
</html>
------=_Part_4200734_24778174.1344608186754--
"""
MAIL_TEMPLATE_PLAINTEXT = """Return-Path: <whatever-2a840@postmaster.twitter.com>
To: {to}
Received: by mail1.openerp.com (Postfix, from userid 10002)
@@ -883,3 +927,89 @@ OyI+T2RvbzwvYT4uCjwvcD4KPC9kaXY+CiAgICAgICAg
--92726A5F09.1555335666/mail2.test.ironsky--
"""
MAIL_NO_BODY = '''\
Return-Path: <{email_from}>
Delivered-To: catchall@xxxx.xxxx
Received: from in66.mail.ovh.net (unknown [10.101.4.66])
by vr38.mail.ovh.net (Postfix) with ESMTP id 4GLCGr70Kyz1myr75
for <catchall@xxxx.xxxx>; Thu, 8 Jul 2021 10:30:12 +0000 (UTC)
X-Comment: SPF check N/A for local connections - client-ip=213.186.33.59; helo=mail663.ha.ovh.net; envelope-from={email_from}; receiver=catchall@xxxx.xxxx
Authentication-Results: in66.mail.ovh.net; dkim=none; dkim-atps=neutral
Delivered-To: xxxx.xxxx-{email_to}
X-ME-Helo: opme11oxm23aub.bagnolet.francetelecom.fr
X-ME-Auth: ZnJlZGVyaWMuYmxhY2hvbjA3QG9yYW5nZS5mcg==
X-ME-Date: Thu, 08 Jul 2021 12:30:11 +0200
X-ME-IP: 86.221.151.111
Date: Thu, 8 Jul 2021 12:30:11 +0200 (CEST)
From: =?UTF-8?Q?Fr=C3=A9d=C3=A9ric_BLACHON?= <{email_from}>
Reply-To:
=?UTF-8?Q?Fr=C3=A9d=C3=A9ric_BLACHON?= <{email_from}>
To: {email_to}
Message-ID: <1024471522.82574.1625740211606.JavaMail.open-xchange@opme11oxm23aub.bagnolet.francetelecom.fr>
Subject: transport autorisation 19T
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="----=_Part_82573_178179506.1625740211587"
------=_Part_82573_178179506.1625740211587
MIME-Version: 1.0
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: 7bit
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"><head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head><body style="font-family: arial,helvetica,sans-serif; font-size: 13pt"></body></html>
'''
MAIL_NO_FINAL_RECIPIENT = """\
Return-Path: <bounce-md_9656353.6125275c.v1-f28f7746389e45f0bfbf9faefe9e0dc8@mandrillapp.com>
Delivered-To: catchall@xxxx.xxxx
Received: from in58.mail.ovh.net (unknown [10.101.4.58])
by vr46.mail.ovh.net (Postfix) with ESMTP id 4GvFsq2QLYz1t0N7r
for <catchall@xxxx.xxxx>; Tue, 24 Aug 2021 17:07:43 +0000 (UTC)
Received-SPF: Softfail (mailfrom) identity=mailfrom; client-ip=46.105.72.169; helo=40.mo36.mail-out.ovh.net; envelope-from=bounce-md_9656353.6125275c.v1-f28f7746389e45f0bfbf9faefe9e0dc8@mandrillapp.com; receiver=catchall@xxxx.xxxx
Authentication-Results: in58.mail.ovh.net;
dkim=pass (1024-bit key; unprotected) header.d=mandrillapp.com header.i=bounces-noreply@mandrillapp.com header.b="TDzUcdJs";
dkim=pass (1024-bit key) header.d=mandrillapp.com header.i=@mandrillapp.com header.b="MyjddTY5";
dkim-atps=neutral
Delivered-To: xxxx.xxxx-{email_to}
Authentication-Results: in62.mail.ovh.net;
dkim=pass (1024-bit key; unprotected) header.d=mandrillapp.com header.i=bounces-noreply@mandrillapp.com header.b="TDzUcdJs";
dkim=pass (1024-bit key) header.d=mandrillapp.com header.i=@mandrillapp.com header.b="MyjddTY5";
dkim-atps=neutral
From: MAILER-DAEMON <bounces-noreply@mandrillapp.com>
Subject: Undelivered Mail Returned to Sender
To: {email_to}
X-Report-Abuse: Please forward a copy of this message, including all headers, to abuse@mandrill.com
X-Report-Abuse: You can also report abuse here: http://mandrillapp.com/contact/abuse?id=9656353.f28f7746389e45f0bfbf9faefe9e0dc8
X-Mandrill-User: md_9656353
Feedback-ID: 9656353:9656353.20210824:md
Message-Id: <9656353.20210824170740.6125275cf21879.17950539@mail9.us4.mandrillapp.com>
Date: Tue, 24 Aug 2021 17:07:40 +0000
MIME-Version: 1.0
Content-Type: multipart/report; boundary="_av-UfLe6y6qxNo54-urtAxbJQ"
--_av-UfLe6y6qxNo54-urtAxbJQ
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 7bit
--- The following addresses had delivery problems ---
<{email_from}> (5.7.1 <{email_from}>: Recipient address rejected: Access denied)
--_av-UfLe6y6qxNo54-urtAxbJQ
Content-Type: message/delivery-status
Content-Transfer-Encoding: 7bit
Original-Recipient: <{email_from}>
Action: failed
Diagnostic-Code: smtp; 554 5.7.1 <{email_from}>: Recipient address rejected: Access denied
Remote-MTA: 10.245.192.40
--_av-UfLe6y6qxNo54-urtAxbJQ--"""
+2 -1
View File
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_mail_models
from . import test_mail_corner_case_models
from . import test_mail_models
from . import test_mail_thread_models
@@ -32,6 +32,28 @@ class MailPerformanceTracking(models.Model):
field_2 = fields.Char(tracking=True)
class MailTestFieldType(models.Model):
""" Test default values, notably type, messing through models during gateway
processing (i.e. lead.type versus attachment.type). """
_description = 'Test Field Type'
_name = 'mail.test.field.type'
_inherit = ['mail.thread']
name = fields.Char()
email_from = fields.Char()
datetime = fields.Datetime(default=fields.Datetime.now)
customer_id = fields.Many2one('res.partner', 'Customer')
type = fields.Selection([('first', 'First'), ('second', 'Second')])
user_id = fields.Many2one('res.users', 'Responsible', tracking=True)
@api.model_create_multi
def create(self, vals_list):
# Emulate an addon that alters the creation context, such as `crm`
if not self._context.get('default_type'):
self = self.with_context(default_type='first')
return super(MailTestFieldType, self).create(vals_list)
class MailTestLang(models.Model):
""" A simple chatter model with lang-based capabilities, allowing to
test translations. """
@@ -9,6 +9,8 @@ access_mail_test_track_portal,mail.test.track.portal,model_mail_test_track,base.
access_mail_test_track_user,mail.test.track.user.employee,model_mail_test_track,base.group_user,1,1,1,1
access_mail_test_activity_portal,mail.test.activity.portal,model_mail_test_activity,base.group_portal,1,0,0,0
access_mail_test_activity_user,mail.test.activity.user,model_mail_test_activity,base.group_user,1,1,1,1
access_mail_test_field_type_portal,mail.test.field.type.portal,model_mail_test_field_type,base.group_portal,0,0,0,0
access_mail_test_field_type_user,mail.test.field.type.user,model_mail_test_field_type,base.group_user,1,1,1,1
access_mail_test_ticket_portal,mail.test.ticket.portal,model_mail_test_ticket,base.group_portal,0,0,0,0
access_mail_test_ticket_user,mail.test.ticket.user,model_mail_test_ticket,base.group_user,1,1,1,1
access_mail_test_composer_mixin_all,mail.test.composer.mixin.all,model_mail_test_composer_mixin,,0,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
9 access_mail_test_track_user mail.test.track.user.employee model_mail_test_track base.group_user 1 1 1 1
10 access_mail_test_activity_portal mail.test.activity.portal model_mail_test_activity base.group_portal 1 0 0 0
11 access_mail_test_activity_user mail.test.activity.user model_mail_test_activity base.group_user 1 1 1 1
12 access_mail_test_field_type_portal mail.test.field.type.portal model_mail_test_field_type base.group_portal 0 0 0 0
13 access_mail_test_field_type_user mail.test.field.type.user model_mail_test_field_type base.group_user 1 1 1 1
14 access_mail_test_ticket_portal mail.test.ticket.portal model_mail_test_ticket base.group_portal 0 0 0 0
15 access_mail_test_ticket_user mail.test.ticket.user model_mail_test_ticket base.group_user 1 1 1 1
16 access_mail_test_composer_mixin_all mail.test.composer.mixin.all model_mail_test_composer_mixin 0 0 0 0
@@ -55,6 +55,15 @@ class TestEmailParsing(TestMailCommon):
res = self.env['mail.thread'].message_parse(self.from_string(test_mail_data.MAIL_MULTIPART_WEIRD_FILENAME))
self.assertEqual(res['attachments'][0][0], '62_@;,][)=.(ÇÀÉ.txt')
def test_message_parse_bugs(self):
""" Various corner cases or message parsing """
# message without Final-Recipient
self.env['mail.thread'].message_parse(self.from_string(test_mail_data.MAIL_NO_FINAL_RECIPIENT))
# message with empty body (including only void characters)
res = self.env['mail.thread'].message_parse(self.from_string(test_mail_data.MAIL_NO_BODY))
self.assertEqual(res['body'], '\n \n', 'Gateway should not crash with void content')
def test_message_parse_eml(self):
# Test that the parsing of mail with embedded emails as eml(msg) which generates empty attachments, can be processed.
mail = self.format(test_mail_data.MAIL_EML_ATTACHMENT, email_from='"Sylvie Lelitre" <test.sylvie.lelitre@agrolait.com>', to='generic@test.com')
@@ -1131,6 +1140,55 @@ class TestMailgateway(TestMailCommon):
self.assertFalse(incoming_msg.parent_id)
self.assertTrue(incoming_msg.res_id == res_test.id)
# --------------------------------------------------
# Gateway / Record synchronization
# --------------------------------------------------
@mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
def test_gateway_values_base64_image(self):
"""New record with mail that contains base64 inline image."""
target_model = "mail.test.field.type"
alias = self.env["mail.alias"].create({
"alias_name": "base64-lover",
"alias_model_id": self.env["ir.model"]._get(target_model).id,
"alias_defaults": "{}",
"alias_contact": "everyone",
})
record = self.format_and_process(
test_mail_data.MAIL_TEMPLATE_EXTRA_HTML, self.email_from,
'%s@%s' % (alias.alias_name, self.alias_catchall),
subject='base64 image to alias',
target_model=target_model,
extra_html='<img src="data:image/png;base64,iV/+OkI=">',
)
self.assertEqual(record.type, "first")
self.assertEqual(len(record.message_ids[0].attachment_ids), 1)
self.assertEqual(record.message_ids[0].attachment_ids[0].name, "image0")
self.assertEqual(record.message_ids[0].attachment_ids[0].type, "binary")
@mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
def test_gateway_values_base64_image_walias(self):
"""New record with mail that contains base64 inline image + default values
coming from alias."""
target_model = "mail.test.field.type"
alias = self.env["mail.alias"].create({
"alias_name": "base64-lover",
"alias_model_id": self.env["ir.model"]._get(target_model).id,
"alias_defaults": "{'type': 'second'}",
"alias_contact": "everyone",
})
record = self.format_and_process(
test_mail_data.MAIL_TEMPLATE_EXTRA_HTML, self.email_from,
'%s@%s' % (alias.alias_name, self.alias_catchall),
subject='base64 image to alias',
target_model=target_model,
extra_html='<img src="data:image/png;base64,iV/+OkI=">',
)
self.assertEqual(record.type, "second")
self.assertEqual(len(record.message_ids[0].attachment_ids), 1)
self.assertEqual(record.message_ids[0].attachment_ids[0].name, "image0")
self.assertEqual(record.message_ids[0].attachment_ids[0].type, "binary")
# --------------------------------------------------
# Thread formation: mail gateway corner cases
# --------------------------------------------------
@@ -204,6 +204,7 @@ class MergePartnerAutomatic(models.TransientModel):
update_records('calendar', src=partner, field_model='model_id.model')
update_records('ir.attachment', src=partner, field_model='res_model')
update_records('mail.followers', src=partner, field_model='res_model')
update_records('mail.activity', src=partner, field_model='res_model')
update_records('mail.message', src=partner)
update_records('ir.model.data', src=partner)