diff --git a/addons/mail/models/mail_alias.py b/addons/mail/models/mail_alias.py
index 839123896c8..43c2350fdb3 100644
--- a/addons/mail/models/mail_alias.py
+++ b/addons/mail/models/mail_alias.py
@@ -7,7 +7,7 @@ import re
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
-from odoo.tools import remove_accents
+from odoo.tools import remove_accents, is_html_empty
_logger = logging.getLogger(__name__)
@@ -68,6 +68,9 @@ class Alias(models.Model):
"- everyone: everyone can post\n"
"- partners: only authenticated partners\n"
"- followers: only followers of the related document or members of following channels\n")
+ alias_bounced_content = fields.Html(
+ "Custom Bounced Message", translate=True,
+ help="If set, this content will automatically be sent out to unauthorized users instead of the default message.")
_sql_constraints = [
('alias_unique', 'UNIQUE(alias_name)', 'Unfortunately this email alias is already used, please choose a unique one')
@@ -171,6 +174,36 @@ class Alias(models.Model):
'type': 'ir.actions.act_window',
}
+ def _get_alias_bounced_body_fallback(self, message_dict):
+ return _("""Hi,
+The following email sent to %s cannot be accepted because this is a private email address.
+Only allowed people can contact us at this address.""" % message_dict.get('to'))
+
+ def _get_alias_bounced_body(self, message_dict):
+ """Get the body of the email return in case of bounced email.
+
+ :param message_dict: dictionary of mail values
+ """
+ lang_author = False
+ if message_dict.get('author_id'):
+ try:
+ lang_author = self.env['res.partner'].browse(message_dict['author_id']).lang
+ except:
+ pass
+
+ if lang_author:
+ self = self.with_context(lang=lang_author)
+
+ if not is_html_empty(self.alias_bounced_content):
+ body = self.alias_bounced_content
+ else:
+ body = self._get_alias_bounced_body_fallback(message_dict)
+ template = self.env.ref('mail.mail_bounce_alias_security', raise_if_not_found=True)
+ return template.render({
+ 'body': body,
+ 'message': message_dict
+ }, engine='ir.qweb', minimal_qcontext=True)
+
class AliasMixin(models.AbstractModel):
""" A mixin for models that inherits mail.alias. This mixin initializes the
@@ -255,20 +288,12 @@ class AliasMixin(models.AbstractModel):
author = self.env['res.partner'].browse(message_dict.get('author_id', False))
if alias.alias_contact == 'followers':
if not record.ids:
- return {
- 'error_message': _('incorrectly configured alias (unknown reference record)'),
- }
+ return _('incorrectly configured alias (unknown reference record)')
if not hasattr(record, "message_partner_ids") or not hasattr(record, "message_channel_ids"):
- return {
- 'error_message': _('incorrectly configured alias'),
- }
+ return _('incorrectly configured alias')
accepted_partner_ids = record.message_partner_ids | record.message_channel_ids.mapped('channel_partner_ids')
if not author or author not in accepted_partner_ids:
- return {
- 'error_message': _('restricted to followers'),
- }
+ return _('restricted to followers')
elif alias.alias_contact == 'partners' and not author:
- return {
- 'error_message': _('restricted to known authors')
- }
+ return _('restricted to known authors')
return True
diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py
index 72779279b68..04642b70126 100644
--- a/addons/mail/models/mail_channel.py
+++ b/addons/mail/models/mail_channel.py
@@ -383,9 +383,7 @@ class Channel(models.Model):
if alias.alias_contact == 'followers' and self.ids:
author = self.env['res.partner'].browse(message_dict.get('author_id', False))
if not author or author not in self.channel_partner_ids:
- return {
- 'error_message': _('restricted to channel members'),
- }
+ return _('restricted to channel members')
return True
return super(Channel, self)._alias_check_contact(message, message_dict, alias)
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py
index 748cf08ec71..90aebbff672 100644
--- a/addons/mail/models/mail_thread.py
+++ b/addons/mail/models/mail_thread.py
@@ -30,7 +30,7 @@ from odoo import _, api, exceptions, fields, models, tools, registry, SUPERUSER_
from odoo.exceptions import MissingError
from odoo.osv import expression
-from odoo.tools import pycompat, ustr
+from odoo.tools import ustr
from odoo.tools.misc import clean_context, split_every
_logger = logging.getLogger(__name__)
@@ -820,7 +820,6 @@ class MailThread(models.AbstractModel):
fallback on a message_new by resetting thread_id
2 - check that message_update exists if thread_id is set; or at least
that message_new exist
- [ - find author_id if udpate_author is set]
3 - if there is an alias, check alias_contact:
'followers' and thread_id:
check on target document that the author is in the followers
@@ -848,12 +847,6 @@ class MailThread(models.AbstractModel):
model, thread_id, alias = route[0], route[1], route[4]
record_set = None
- _generic_bounce_body_html = """
-
Hello,
-
The following email sent to %s cannot be accepted because this is a private email address.
- Only allowed people can contact us at this address.
-
%s
""" % (message_dict.get('to'), message_dict.get('body'))
-
# Wrong model
if not model:
self._routing_warn(_('target model unspecified'), message_id, route, raise_exception)
@@ -877,7 +870,7 @@ class MailThread(models.AbstractModel):
self._routing_warn(_('model %s does not accept document creation') % model, message_id, route, raise_exception)
return ()
- # Update message author if asked. We do it now because we need it for aliases (contact settings)
+ # Update message author. We do it now because we need it for aliases (contact settings)
if not author_id:
if record_set:
authors = self._mail_find_partner_from_emails([email_from], records=record_set)
@@ -898,12 +891,13 @@ class MailThread(models.AbstractModel):
else:
obj = self.env[model]
if hasattr(obj, '_alias_check_contact'):
- check_result = obj._alias_check_contact(message, message_dict, alias)
+ error_code = obj._alias_check_contact(message, message_dict, alias)
else:
- check_result = self.env['mail.alias.mixin']._alias_check_contact_on_record(obj, message, message_dict, alias)
- if check_result is not True:
- self._routing_warn(_('alias %s: %s') % (alias.alias_name, check_result.get('error_message', _('unknown error'))), message_id, route, False)
- self._routing_create_bounce_email(email_from, check_result.get('error_template', _generic_bounce_body_html), message)
+ error_code = self.env['mail.alias.mixin']._alias_check_contact_on_record(obj, message, message_dict, alias)
+ if error_code is not True:
+ self._routing_warn(_('alias %s: %s') % (alias.alias_name, error_code or _('unknown error')), message_id, route, False)
+ body = alias._get_alias_bounced_body(message_dict)
+ self._routing_create_bounce_email(email_from, body, message, references=message_id)
return False
return (model, thread_id, route[2], route[3], route[4])
@@ -1055,7 +1049,7 @@ class MailThread(models.AbstractModel):
body = self.env.ref('mail.mail_bounce_catchall').render({
'message': message,
}, engine='ir.qweb')
- self._routing_create_bounce_email(email_from, body, message, reply_to=self.env.company.email)
+ self._routing_create_bounce_email(email_from, body, message, references=message_id, reply_to=self.env.company.email)
return []
dest_aliases = self.env['mail.alias'].search([('alias_name', 'in', rcpt_tos_valid_localparts)])
diff --git a/addons/mail/views/mail_alias_views.xml b/addons/mail/views/mail_alias_views.xml
index 5bfe53265b4..3584bac53e7 100644
--- a/addons/mail/views/mail_alias_views.xml
+++ b/addons/mail/views/mail_alias_views.xml
@@ -26,6 +26,7 @@
+
diff --git a/addons/test_mail/tests/test_mail_gateway.py b/addons/test_mail/tests/test_mail_gateway.py
index 698ba38264a..5a63940756b 100644
--- a/addons/test_mail/tests/test_mail_gateway.py
+++ b/addons/test_mail/tests/test_mail_gateway.py
@@ -235,6 +235,32 @@ class TestMailgateway(TestMailCommon):
# Alias configuration
# --------------------------------------------------
+ @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
+ def test_message_process_alias_config_bounced_content(self):
+ """ Custom bounced message for the alias => Received this custom message """
+ self.alias.write({
+ 'alias_contact': 'partners',
+ 'alias_bounced_content': '
What Is Dead May Never Die
'
+ })
+
+ # Test: custom bounced content
+ with self.mock_mail_gateway():
+ record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'groups@test.com', subject='Should Bounce')
+ self.assertFalse(record, 'message_process: should have bounced')
+ self.assertSentEmail('"MAILER-DAEMON"
', ['whatever-2a840@postmaster.twitter.com'], body_content='What Is Dead May Never Die
')
+
+ self.alias.write({
+ 'alias_contact': 'partners',
+ 'alias_bounced_content': ''
+ })
+
+ # Test: with "empty" bounced content (simulate view, putting always '' in html field)
+ with self.mock_mail_gateway():
+ record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'groups@test.com', subject='Should Bounce')
+ self.assertFalse(record, 'message_process: should have bounced')
+ # Check if default (hardcoded) value is in the mail content
+ self.assertSentEmail('"MAILER-DAEMON" ', ['whatever-2a840@postmaster.twitter.com'], body_content='The following email sent to')
+
@mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
def test_message_process_alias_user_id(self):
""" Test alias ownership """
diff --git a/odoo/addons/base/tests/test_mail.py b/odoo/addons/base/tests/test_mail.py
index e1b80c52c28..c2e9899fca1 100644
--- a/odoo/addons/base/tests/test_mail.py
+++ b/odoo/addons/base/tests/test_mail.py
@@ -9,7 +9,8 @@ import threading
from odoo.tests.common import BaseCase, SavepointCase, TransactionCase
from odoo.tools import (
- html_sanitize, append_content_to_html, plaintext2html, email_split,
+ is_html_empty, html_sanitize, append_content_to_html, plaintext2html,
+ email_split,
misc, formataddr,
)
@@ -103,10 +104,10 @@ class TestSanitizer(BaseCase):
not_emails = [
'cat
',
'
']
- for email in not_emails:
- sanitized = html_sanitize(email)
- left_part = email.split('>')[0] # take only left part, as the sanitizer could add data information on node
- self.assertNotIn(misc.html_escape(email), sanitized, 'html_sanitize stripped emails of original html')
+ for not_email in not_emails:
+ sanitized = html_sanitize(not_email)
+ left_part = not_email.split('>')[0] # take only left part, as the sanitizer could add data information on node
+ self.assertNotIn(misc.html_escape(not_email), sanitized, 'html_sanitize stripped emails of original html')
self.assertIn(left_part, sanitized)
def test_style_parsing(self):
@@ -318,6 +319,19 @@ class TestHtmlTools(BaseCase):
for html, content, plaintext_flag, preserve_flag, container_tag, expected in test_samples:
self.assertEqual(append_content_to_html(html, content, plaintext_flag, preserve_flag, container_tag), expected, 'append_content_to_html is broken')
+ def test_is_html_empty(self):
+ void_strings_samples = ['', False, ' ']
+ for content in void_strings_samples:
+ self.assertTrue(is_html_empty(content))
+
+ void_html_samples = ['
', '
']
+ for content in void_html_samples:
+ self.assertTrue(is_html_empty(content), 'Failed with %s' % content)
+
+ valid_html_samples = ['
1
', '1
']
+ for content in valid_html_samples:
+ self.assertFalse(is_html_empty(content))
+
class TestEmailTools(BaseCase):
""" Test some of our generic utility functions for emails """
diff --git a/odoo/tools/mail.py b/odoo/tools/mail.py
index 50467df606e..21756fe0640 100644
--- a/odoo/tools/mail.py
+++ b/odoo/tools/mail.py
@@ -254,6 +254,21 @@ def html_sanitize(src, silent=True, sanitize_tags=True, sanitize_attributes=Fals
# HTML/Text management
#----------------------------------------------------------
+
+def is_html_empty(html_content):
+ """Check if a html content is empty. If there are only formatting tags or
+ a void content return True. Famous use case if a '
' added by
+ some web editor.
+
+ :param str html_content: html content, coming from example from an HTML field
+ :returns: bool, True if no content found or if containing only void formatting tags
+ """
+ if not html_content:
+ return True
+ tag_re = re.compile(r'\<\s*\/?(?:p|div|span|br|b|i)\s*\>')
+ return not bool(re.sub(tag_re, '', html_content).strip())
+
+
def html_keep_url(text):
""" Transform the url into clickable link with tag """
idx = 0