diff --git a/addons/hr_recruitment/models/hr_recruitment.py b/addons/hr_recruitment/models/hr_recruitment.py
index 0a033fa8517..b08d43051be 100644
--- a/addons/hr_recruitment/models/hr_recruitment.py
+++ b/addons/hr_recruitment/models/hr_recruitment.py
@@ -445,12 +445,12 @@ class Applicant(models.Model):
view_id = self.env.ref('hr_recruitment.hr_applicant_view_form_interviewer').id
return super().get_view(view_id, view_type, **options)
- def _notify_get_recipients(self, message, msg_vals):
+ def _notify_get_recipients(self, message, msg_vals, **kwargs):
"""
Do not notify members of the Recruitment Interviewer group, as this
might leak some data they shouldn't have access to.
"""
- recipients = super()._notify_get_recipients(message, msg_vals)
+ recipients = super()._notify_get_recipients(message, msg_vals, **kwargs)
interviewer_group = self.env.ref('hr_recruitment.group_hr_recruitment_interviewer').id
return [recipient for recipient in recipients if interviewer_group not in recipient['groups']]
diff --git a/addons/im_livechat/models/mail_channel.py b/addons/im_livechat/models/mail_channel.py
index 26d3e4c70e5..79bb445f835 100644
--- a/addons/im_livechat/models/mail_channel.py
+++ b/addons/im_livechat/models/mail_channel.py
@@ -148,7 +148,7 @@ class MailChannel(models.Model):
'id': message.id,
'body': message.body,
})
- super()._message_update_content_after_hook(message=message)
+ return super()._message_update_content_after_hook(message=message)
def _get_visitor_leave_message(self, operator=False, cancel=False):
return _('Visitor has left the conversation.')
diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py
index 002c3707485..9f496b8abcc 100644
--- a/addons/mail/__manifest__.py
+++ b/addons/mail/__manifest__.py
@@ -73,6 +73,7 @@ For more specific needs, you may also assign custom-defined actions
'views/mail_tracking_views.xml',
'views/mail_notification_views.xml',
'views/mail_message_views.xml',
+ 'views/mail_message_schedule_views.xml',
'views/mail_mail_views.xml',
'views/mail_followers_views.xml',
'views/mail_ice_server_views.xml',
diff --git a/addons/mail/controllers/discuss.py b/addons/mail/controllers/discuss.py
index 7a748fea508..ebeacc8a6a0 100644
--- a/addons/mail/controllers/discuss.py
+++ b/addons/mail/controllers/discuss.py
@@ -241,7 +241,13 @@ class DiscussController(http.Controller):
message_sudo = guest.env['mail.message'].browse(message_id).sudo().exists()
if not message_sudo.is_current_user_or_guest_author and not guest.env.user._is_admin():
raise NotFound()
- message_sudo._update_content(body=body, attachment_ids=attachment_ids)
+ if not message_sudo.model or not message_sudo.res_id:
+ raise NotFound()
+ request.env[message_sudo.model].browse([message_sudo.res_id])._message_update_content(
+ message_sudo,
+ body,
+ attachment_ids=attachment_ids
+ )
return {
'id': message_sudo.id,
'body': message_sudo.body,
diff --git a/addons/mail/data/ir_cron_data.xml b/addons/mail/data/ir_cron_data.xml
index 8362160d762..5bc687622f1 100644
--- a/addons/mail/data/ir_cron_data.xml
+++ b/addons/mail/data/ir_cron_data.xml
@@ -54,5 +54,16 @@
+
+
+ Notification: Send scheduled message notifications
+ 1
+ hours
+ -1
+
+
+ model._send_notifications_cron()
+ code
+
diff --git a/addons/mail/models/__init__.py b/addons/mail/models/__init__.py
index f1a94f443e4..30f6753923b 100644
--- a/addons/mail/models/__init__.py
+++ b/addons/mail/models/__init__.py
@@ -24,6 +24,7 @@ from . import mail_blacklist
from . import mail_followers
from . import mail_gateway_allowed
from . import mail_message_reaction
+from . import mail_message_schedule
from . import mail_message_subtype
from . import mail_message
from . import mail_mail
diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py
index c8a4891a3df..5b49132a8a2 100644
--- a/addons/mail/models/mail_channel.py
+++ b/addons/mail/models/mail_channel.py
@@ -498,13 +498,14 @@ class Channel(models.Model):
# MAILING
# ------------------------------------------------------------
- def _notify_get_recipients(self, message, msg_vals):
+ def _notify_get_recipients(self, message, msg_vals, **kwargs):
""" Override recipients computation as channel is not a standard
mail.thread document. Indeed there are no followers on a channel.
Instead of followers it has members that should be notified.
:param message: see ``MailThread._notify_get_recipients()``;
:param msg_vals: see ``MailThread._notify_get_recipients()``;
+ :param kwargs: see ``MailThread._notify_get_recipients()``;
:return recipients: structured data holding recipients data. See
``MailThread._notify_thread()`` for more details about its content
diff --git a/addons/mail/models/mail_mail.py b/addons/mail/models/mail_mail.py
index afef9897340..02f8edbb129 100644
--- a/addons/mail/models/mail_mail.py
+++ b/addons/mail/models/mail_mail.py
@@ -85,7 +85,7 @@ class MailMail(models.Model):
'Auto Delete',
help="This option permanently removes any track of email after it's been sent, including from the Technical menu in the Settings, in order to preserve storage space of your Odoo database.")
to_delete = fields.Boolean('To Delete', help='If set, the mail will be deleted during the next Email Queue CRON run.')
- scheduled_date = fields.Char('Scheduled Send Date',
+ scheduled_date = fields.Datetime('Scheduled Send Date',
help="If set, the queue manager will send the email after the date. If not set, the email will be send as soon as possible. Unless a timezone is specified, it is considered as being in UTC timezone.")
fetchmail_server_id = fields.Many2one('fetchmail.server', "Inbound Mail Server", readonly=True)
@@ -132,10 +132,9 @@ class MailMail(models.Model):
values['is_notification'] = True
if values.get('scheduled_date'):
parsed_datetime = self._parse_scheduled_datetime(values['scheduled_date'])
- if parsed_datetime:
- values['scheduled_date'] = parsed_datetime.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
- else:
- values['scheduled_date'] = False
+ values['scheduled_date'] = parsed_datetime.replace(tzinfo=None) if parsed_datetime else False
+ else:
+ values['scheduled_date'] = False # void string crashes
new_mails = super(MailMail, self).create(values_list)
new_mails_w_attach = self
@@ -150,10 +149,7 @@ class MailMail(models.Model):
def write(self, vals):
if vals.get('scheduled_date'):
parsed_datetime = self._parse_scheduled_datetime(vals['scheduled_date'])
- if parsed_datetime:
- vals['scheduled_date'] = parsed_datetime.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
- else:
- vals['scheduled_date'] = False
+ vals['scheduled_date'] = parsed_datetime.replace(tzinfo=None) if parsed_datetime else False
res = super(MailMail, self).write(vals)
if vals.get('attachment_ids'):
for mail in self:
diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py
index 5f86648fcf7..a8bb8e81e6c 100644
--- a/addons/mail/models/mail_message.py
+++ b/addons/mail/models/mail_message.py
@@ -808,25 +808,6 @@ class Message(models.Model):
reaction.unlink()
self.env[self.model].browse(self.res_id)._message_remove_reaction_after_hook(message=self, content=content)
- def _update_content(self, body, attachment_ids):
- self.ensure_one()
- thread = self.env[self.model].browse(self.res_id)
- thread._check_can_update_message_content(self)
- self.body = body
- if not attachment_ids:
- self.attachment_ids._delete_and_notify()
- else:
- message_values = {
- 'model': self.model,
- 'body': body,
- 'res_id': self.res_id,
- }
- attachement_values = thread._message_post_process_attachments([], attachment_ids, message_values)
- self.update(attachement_values)
- # Cleanup related message data if the message is empty
- self.sudo()._filter_empty()._cleanup_side_records()
- thread._message_update_content_after_hook(self)
-
# ------------------------------------------------------
# MESSAGE READ / FETCH / FAILURE API
# ------------------------------------------------------
diff --git a/addons/mail/models/mail_message_schedule.py b/addons/mail/models/mail_message_schedule.py
new file mode 100644
index 00000000000..e8fa207404b
--- /dev/null
+++ b/addons/mail/models/mail_message_schedule.py
@@ -0,0 +1,135 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import json
+import logging
+
+from datetime import datetime
+
+from odoo import api, fields, models
+
+_logger = logging.getLogger(__name__)
+
+
+class MailMessageSchedule(models.Model):
+ """ Mail message notification schedule queue.
+
+ This model is used to store the mail messages scheduled. So we can
+ delay the sending of the notifications. A scheduled date field already
+ exists on the but it does not allow us to delay the sending
+ of the notifications.
+ """
+ _name = 'mail.message.schedule'
+ _description = 'Scheduled Messages'
+ _order = 'scheduled_datetime DESC, id DESC'
+ _rec_name = 'mail_message_id'
+
+ mail_message_id = fields.Many2one(
+ 'mail.message', string='Message',
+ ondelete='cascade', required=True)
+ notification_parameters = fields.Text('Notification Parameter')
+ scheduled_datetime = fields.Datetime(
+ 'Scheduled Send Date', required=True,
+ help='Datetime at which notification should be sent.')
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ schedules = super().create(vals_list)
+ if schedules:
+ self.env.ref('mail.ir_cron_send_scheduled_message')._trigger_list(
+ set(schedules.mapped('scheduled_datetime'))
+ )
+ return schedules
+
+ @api.model
+ def _send_notifications_cron(self):
+ messages_scheduled = self.env['mail.message.schedule'].search(
+ [('scheduled_datetime', '<=', datetime.utcnow())]
+ )
+ if messages_scheduled:
+ _logger.info('Send %s scheduled messages', len(messages_scheduled))
+ messages_scheduled._send_notifications()
+
+ def force_send(self):
+ """ Launch notification process independently from the expected date. """
+ return self._send_notifications()
+
+ def _send_notifications(self, default_notify_kwargs=None):
+ """ Send notification for scheduled messages.
+
+ :param dict default_notify_kwargs: optional parameters to propagate to
+ ``notify_thread``. Those are default values overridden by content of
+ ``notification_parameters`` field.
+ """
+ for model, schedules in self._group_by_model().items():
+ if model:
+ records = self.env[model].browse(schedules.mapped('mail_message_id.res_id'))
+ else:
+ records = [self.env['mail.thread']] * len(schedules)
+
+ for record, schedule in zip(records, schedules):
+ notify_kwargs = dict(default_notify_kwargs or {}, skip_existing=True)
+ try:
+ schedule_notify_kwargs = json.loads(schedule.notification_parameters)
+ except Exception:
+ pass
+ else:
+ schedule_notify_kwargs.pop('scheduled_date', None)
+ notify_kwargs.update(schedule_notify_kwargs)
+
+ record._notify_thread(schedule.mail_message_id, msg_vals=False, **notify_kwargs)
+
+ self.unlink()
+ return True
+
+ @api.model
+ def _send_message_notifications(self, messages, default_notify_kwargs=None):
+ """ Send scheduled notification for given messages.
+
+ :param messages: scheduled sending related to those messages
+ will be sent now;
+ :param dict default_notify_kwargs: optional parameters to propagate to
+ ``notify_thread``. Those are default values overridden by content of
+ ``notification_parameters`` field.
+
+ :return bool: False if no schedule has been found, True otherwise
+ """
+ messages_scheduled = self.search(
+ [('mail_message_id', 'in', messages.ids)]
+ )
+ if not messages_scheduled:
+ return False
+
+ messages_scheduled._send_notifications(default_notify_kwargs=default_notify_kwargs)
+ return True
+
+ @api.model
+ def _update_message_scheduled_datetime(self, messages, new_datetime):
+ """ Update scheduled datetime for scheduled sending related to messages.
+
+ :param messages: scheduled sending related to those messages
+ will be updated. Missing one are skipped;
+ :param datetime new_datetime: new datetime for sending. New triggers
+ are created based on it;
+
+ :return bool: False if no schedule has been found, True otherwise
+ """
+ messages_scheduled = self.search(
+ [('mail_message_id', 'in', messages.ids)]
+ )
+ if not messages_scheduled:
+ return False
+
+ messages_scheduled.scheduled_datetime = new_datetime
+ self.env.ref('mail.ir_cron_send_scheduled_message')._trigger(new_datetime)
+ return True
+
+ def _group_by_model(self):
+ grouped = {}
+ for schedule in self:
+ model = schedule.mail_message_id.model if schedule.mail_message_id.model and schedule.mail_message_id.res_id else False
+ if model not in grouped:
+ grouped[model] = schedule
+ else:
+ grouped[model] += schedule
+ return grouped
diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py
index 0040412c4e2..e647dfcf122 100644
--- a/addons/mail/models/mail_template.py
+++ b/addons/mail/models/mail_template.py
@@ -4,7 +4,6 @@
import base64
import logging
-
from odoo import _, api, fields, models, tools, Command
from odoo.exceptions import UserError
from odoo.tools import is_html_empty
@@ -205,6 +204,12 @@ class MailTemplate(models.Model):
values = results[res_id]
if values.get('body_html'):
values['body'] = tools.html_sanitize(values['body_html'])
+ # if asked in fields to return, parse generated date into tz agnostic UTC as expected by ORM
+ scheduled_date = values.pop('scheduled_date', None)
+ if 'scheduled_date' in fields and scheduled_date:
+ parsed_datetime = self.env['mail.mail']._parse_scheduled_datetime(scheduled_date)
+ values['scheduled_date'] = parsed_datetime.replace(tzinfo=None) if parsed_datetime else False
+
# technical settings
values.update(
mail_server_id=template.mail_server_id.id or False,
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py
index 661742115a2..7df8b71d74e 100644
--- a/addons/mail/models/mail_thread.py
+++ b/addons/mail/models/mail_thread.py
@@ -9,6 +9,7 @@ import email
import email.policy
import hashlib
import hmac
+import json
import lxml
import logging
import pytz
@@ -18,7 +19,7 @@ import threading
from collections import namedtuple
from email.message import EmailMessage
-from email import message_from_string, policy
+from email import message_from_string
from lxml import etree
from werkzeug import urls
from xmlrpc import client as xmlrpclib
@@ -433,14 +434,20 @@ class MailThread(models.AbstractModel):
return self.with_context(lang=self.env.user.lang)
return self
- def _check_can_update_message_content(self, message):
- """" Checks that the current user can update the content of the message. """
+ def _check_can_update_message_content(self, messages):
+ """" Checks that the current user can update the content of the message.
+ Current heuristic is
+
+ * limited to note;
+ * if no tracking;
+ * only for user generated content;
+ """
note_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note')
- if not message.subtype_id.id == note_id:
+ if any(message.subtype_id.id != note_id for message in messages):
raise exceptions.UserError(_("Only logged notes can have their content updated on model '%s'", self._name))
- if message.tracking_value_ids:
+ if messages.tracking_value_ids:
raise exceptions.UserError(_("Messages with tracking values cannot be modified"))
- if not message.message_type == 'comment':
+ if any(message.message_type != 'comment' for message in messages):
raise exceptions.UserError(_("Only messages type comment can have their content updated"))
# ------------------------------------------------------
@@ -1417,7 +1424,7 @@ class MailThread(models.AbstractModel):
if email_part:
if email_part.get_content_type() == 'text/rfc822-headers':
# Convert the message body into a message itself
- email_payload = message_from_string(email_part.get_payload(), policy=policy.SMTP)
+ email_payload = message_from_string(email_part.get_payload(), policy=email.policy.SMTP)
else:
email_payload = email_part.get_payload()[0]
bounced_msg_id = tools.mail_header_msgid_re.findall(tools.decode_message_header(email_payload, 'Message-Id'))
@@ -2298,7 +2305,11 @@ class MailThread(models.AbstractModel):
access message content in db;
Kwargs allow to pass various parameters that are given to sub notification
- methods. See those methods for more details about the additional parameters.
+ methods. See those methods for more details about supported parameters.
+ Specific kwargs used in this method:
+
+ * ``scheduled_date``: delay notification sending if set in the future.
+ This is done using the ``mail.message.schedule`` intermediate model;
:return: recipients data (see ``MailThread._notify_get_recipients()``)
"""
@@ -2306,12 +2317,27 @@ class MailThread(models.AbstractModel):
self = self._fallback_lang()
msg_vals = msg_vals if msg_vals else {}
- recipients_data = self._notify_get_recipients(message, msg_vals)
+ recipients_data = self._notify_get_recipients(message, msg_vals, **kwargs)
if not recipients_data:
return recipients_data
- self._notify_thread_by_inbox(message, recipients_data, msg_vals=msg_vals, **kwargs)
- self._notify_thread_by_email(message, recipients_data, msg_vals=msg_vals, **kwargs)
+ # if scheduled for later: add in queue instead of generating notifications
+ scheduled_date = kwargs.pop('scheduled_date', None)
+ if scheduled_date:
+ parsed_datetime = self.env['mail.mail']._parse_scheduled_datetime(scheduled_date)
+ scheduled_date = parsed_datetime.replace(tzinfo=None) if parsed_datetime else False
+ if scheduled_date and scheduled_date > datetime.datetime.utcnow():
+ # send the message notifications at the scheduled date
+ self.env['mail.message.schedule'].sudo().create({
+ 'scheduled_datetime': scheduled_date,
+ 'mail_message_id': message.id,
+ 'notification_parameters': json.dumps(kwargs),
+ })
+ else:
+ # generate immediately the
+ # and send the and the notifications
+ self._notify_thread_by_inbox(message, recipients_data, msg_vals=msg_vals, **kwargs)
+ self._notify_thread_by_email(message, recipients_data, msg_vals=msg_vals, **kwargs)
return recipients_data
@@ -2355,9 +2381,9 @@ class MailThread(models.AbstractModel):
self.env['bus.bus'].sudo()._sendmany(bus_notifications)
def _notify_thread_by_email(self, message, recipients_data, msg_vals=False,
- mail_auto_delete=True, # mail.mail
+ mail_auto_delete=True, # mail.mail
model_description=False, force_email_company=False, force_email_lang=False, # rendering
- check_existing=False, force_send=True, send_after_commit=True, # email send
+ resend_existing=False, force_send=True, send_after_commit=True, # email send
**kwargs):
""" Method to send email linked to notified messages.
@@ -2384,7 +2410,7 @@ class MailThread(models.AbstractModel):
:param force_email_company: see ``_notify_by_email_prepare_rendering_context``;
:param force_email_lang: see ``_notify_by_email_prepare_rendering_context``;
- :param check_existing: check for existing notifications to update based on
+ :param resend_existing: check for existing notifications to update based on
mailed recipient, otherwise create new notifications;
:param force_send: send emails directly instead of using queue;
:param send_after_commit: if force_send, tells whether to send emails after
@@ -2449,7 +2475,7 @@ class MailThread(models.AbstractModel):
if new_email and recipients_ids_chunk:
tocreate_recipient_ids = list(recipients_ids_chunk)
- if check_existing:
+ if resend_existing:
existing_notifications = self.env['mail.notification'].sudo().search([
('mail_message_id', '=', message.id),
('notification_type', '=', 'email'),
@@ -2654,13 +2680,21 @@ class MailThread(models.AbstractModel):
final_mail_values.update(additional_values)
return final_mail_values
- def _notify_get_recipients(self, message, msg_vals):
+ def _notify_get_recipients(self, message, msg_vals, **kwargs):
""" Compute recipients to notify based on subtype and followers. This
method returns data structured as expected for ``_notify_recipients``.
TDE/XDO TODO: flag rdata directly, with for example r['notif'] = 'ocn_client' and r['needaction']=False
and correctly override _notify_get_recipients
+ Kwargs allow to pass various parameters that are used by sub notification
+ methods. See those methods for more details about supported parameters.
+ Specific kwargs used in this method:
+
+ * ``skip_existing``: check existing notifications and skip them in order
+ to avoid having several notifications / partner as it would make
+ constraints crash. This is disabled by default to optimize speed;
+
:return list recipients_data: this is a list of recipients information (see
``MailFollowers._get_recipient_data()`` for more details) formatted like
[{'active': partner.active;
@@ -2690,6 +2724,20 @@ class MailThread(models.AbstractModel):
if pdata['active'] is False:
continue
recipients_data.append(pdata)
+
+ # avoid double notification (on demand due to additional queries)
+ if kwargs.pop('skip_existing', False):
+ pids = [r['id'] for r in recipients_data]
+ if pids:
+ existing_notifications = self.env['mail.notification'].sudo().search([
+ ('res_partner_id', 'in', pids),
+ ('mail_message_id', 'in', message.ids)
+ ])
+ recipients_data = [
+ r for r in recipients_data
+ if r['id'] not in existing_notifications.res_partner_id.ids
+ ]
+
return recipients_data
def _notify_get_recipients_groups(self, msg_vals=None):
@@ -3122,8 +3170,66 @@ class MailThread(models.AbstractModel):
msg_not_comment.write(msg_vals)
return True
+ def _message_update_content(self, message, body, attachment_ids=None,
+ strict=True, **kwargs):
+ """ Update message content. Currently does not support attachments
+ specific code (see ``_message_post_process_attachments``), to be added
+ when necessary.
+
+ Private method to use for tooling, do not expose to interface as editing
+ messages should be avoided at all costs (think of: notifications already
+ sent, ...).
+
+ :param message: message to update, should be linked to self through
+ model and res_id;
+ :param str body: new body (None to skip its update);
+ :param list attachment_ids: list of new attachments IDs, replacing old one (None
+ to skip its update);
+ :param bool strict: whether to check for allowance before updating
+ content. This should be skipped only when really necessary as it
+ creates issues with already-sent notifications, lack of content
+ tracking, ...
+
+ Kwargs are supported, notably to match mail.message fields to update.
+ See content of this method for more details about supported keys.
+ """
+ self.ensure_one()
+ if strict:
+ self._check_can_update_message_content(message.sudo())
+
+ msg_values = {'body': body} if body is not None else {}
+ if attachment_ids:
+ msg_values.update(
+ self._message_post_process_attachments([], attachment_ids, {
+ 'body': body,
+ 'model': self._name,
+ 'res_id': self.id,
+ })
+ )
+ elif attachment_ids is not None: # None means "no update"
+ message.attachment_ids._delete_and_notify()
+ if msg_values:
+ message.write(msg_values)
+
+ if 'scheduled_date' in kwargs:
+ # update scheduled datetime
+ if kwargs['scheduled_date']:
+ self.env['mail.message.schedule'].sudo()._update_message_scheduled_datetime(
+ message,
+ kwargs['scheduled_date']
+ )
+ # (re)send notifications
+ else:
+ self.env['mail.message.schedule'].sudo()._send_message_notifications(message)
+
+ # cleanup related message data if the message is empty
+ message.sudo()._filter_empty()._cleanup_side_records()
+
+ return self._message_update_content_after_hook(message)
+
def _message_update_content_after_hook(self, message):
""" Hook to add custom behavior after having updated the message content. """
+ return True
# ------------------------------------------------------
# CONTROLLERS
diff --git a/addons/mail/security/ir.model.access.csv b/addons/mail/security/ir.model.access.csv
index 8b53ea2c560..5ee0c537886 100644
--- a/addons/mail/security/ir.model.access.csv
+++ b/addons/mail/security/ir.model.access.csv
@@ -3,6 +3,8 @@ access_fetchmail_server,fetchmail.server,model_fetchmail_server,base.group_syste
access_mail_message_all,mail.message.all,model_mail_message,,1,0,0,0
access_mail_message_portal,mail.message.portal,model_mail_message,base.group_portal,1,1,1,1
access_mail_message_user,mail.message.user,model_mail_message,base.group_user,1,1,1,1
+access_mail_message_scheduled_all,mail.message.scheduled.all,model_mail_message_schedule,,0,0,0,0
+access_mail_message_scheduled_system,mail.message.scheduled.system,model_mail_message_schedule,base.group_system,1,1,1,1
access_mail_mail_all,mail.mail.all,model_mail_mail,,0,0,0,0
access_mail_mail_portal,mail.mail.portal,model_mail_mail,base.group_portal,0,0,0,0
access_mail_mail_user,mail.mail.user,model_mail_mail,base.group_user,0,0,0,0
diff --git a/addons/mail/tests/common.py b/addons/mail/tests/common.py
index 66dd23d8a80..004b2a959ae 100644
--- a/addons/mail/tests/common.py
+++ b/addons/mail/tests/common.py
@@ -721,6 +721,17 @@ class MailCase(MockEmail):
found_bus_notifs = self.assertBusNotifications(channels, message_items=message_items)
self.assertEqual(self._new_bus_notifs, found_bus_notifs)
+ @contextmanager
+ def assertMsgWithoutNotifications(self, mail_unlink_sent=False):
+ try:
+ with self.mock_mail_gateway(mail_unlink_sent=mail_unlink_sent), self.mock_bus(), self.mock_mail_app():
+ yield
+ finally:
+ self.assertTrue(self._new_msgs)
+ self.assertFalse(bool(self._new_notifs))
+ self.assertFalse(bool(self._new_mails))
+ self.assertFalse(bool(self._mails))
+
@contextmanager
def assertNoNotifications(self):
try:
diff --git a/addons/mail/views/mail_menus.xml b/addons/mail/views/mail_menus.xml
index 9671148395c..4ea13471017 100644
--- a/addons/mail/views/mail_menus.xml
+++ b/addons/mail/views/mail_menus.xml
@@ -62,6 +62,11 @@
parent="mail.mail_menu_technical"
action="action_view_mail_message"
sequence="1"/>
+