[FIX] mail: restrict access to mail.mail model
From now on mail.mail is considered as a technical model. Indeed people should not really manually craft mails by hand. Instead various functional flows should either send mails, either craft mails based on some user input. We therefore make mail restricted to admin users. Flows creating mail.mail are updated to use sudo, and ensure it was done in a context that makes sense to delegate this power to the user. Task ID 1853147 PR #32243
This commit is contained in:
@@ -213,7 +213,7 @@ class Attendee(models.Model):
|
||||
mail_ids.append(invitation_template.send_mail(attendee.id, email_values=email_values, notif_layout='mail.mail_notification_light'))
|
||||
|
||||
if force_send and mail_ids:
|
||||
res = self.env['mail.mail'].browse(mail_ids).send()
|
||||
res = self.env['mail.mail'].sudo().browse(mail_ids).send()
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@@ -398,7 +398,7 @@ class TestCalendar(SavepointCaseWithUserDemo):
|
||||
def _test_one_mail_per_attendee(self, m, partners):
|
||||
# check that every attendee receive a (single) mail for the event
|
||||
for partner in partners:
|
||||
mail = self.env['mail.mail'].search([
|
||||
mail = self.env['mail.mail'].sudo().search([
|
||||
('recipient_ids', 'in', partner.id),
|
||||
('subject', 'like', m.name),
|
||||
])
|
||||
|
||||
@@ -56,7 +56,7 @@ class TestMailSchedule(TestEventCommon):
|
||||
# verify that subscription scheduler was auto-executed after each registration
|
||||
self.assertEqual(len(schedulers[0].mail_registration_ids), 2, 'event: incorrect number of mail scheduled date')
|
||||
|
||||
mails = self.env['mail.mail'].search([('subject', 'ilike', 'registration'), ('date', '>=', now)], order='date DESC', limit=3)
|
||||
mails = self.env['mail.mail'].sudo().search([('subject', 'ilike', 'registration'), ('date', '>=', now)], order='date DESC', limit=3)
|
||||
self.assertEqual(len(mails), 2, 'event: wrong number of registration mail sent')
|
||||
|
||||
for registration in schedulers[0].mail_registration_ids:
|
||||
@@ -73,5 +73,5 @@ class TestMailSchedule(TestEventCommon):
|
||||
self.assertTrue(schedulers[0].mail_sent, 'event: reminder scheduler should have sent an email')
|
||||
self.assertTrue(schedulers[0].done, 'event: reminder scheduler should be done')
|
||||
|
||||
mails = self.env['mail.mail'].search([('subject', 'ilike', 'TestEventMail'), ('date', '>=', now)], order='date DESC', limit=3)
|
||||
mails = self.env['mail.mail'].sudo().search([('subject', 'ilike', 'TestEventMail'), ('date', '>=', now)], order='date DESC', limit=3)
|
||||
self.assertEqual(len(mails), 3, 'event: wrong number of reminders in outgoing mail queue')
|
||||
|
||||
@@ -607,6 +607,7 @@ class HrExpense(models.Model):
|
||||
expense_template = self.env.ref(mail_template_id)
|
||||
rendered_body = expense_template.render({'expense': expense}, engine='ir.qweb')
|
||||
body = self.env['mail.thread']._replace_local_links(rendered_body)
|
||||
# TDE TODO: seems louche, check to use notify
|
||||
if expense.employee_id.user_id.partner_id:
|
||||
expense.message_post(
|
||||
partner_ids=expense.employee_id.user_id.partner_id.ids,
|
||||
@@ -616,7 +617,7 @@ class HrExpense(models.Model):
|
||||
email_layout_xmlid='mail.mail_notification_light',
|
||||
)
|
||||
else:
|
||||
self.env['mail.mail'].create({
|
||||
self.env['mail.mail'].sudo().create({
|
||||
'email_from': self.env.user.email_formatted,
|
||||
'author_id': self.env.user.partner_id.id,
|
||||
'body_html': body,
|
||||
|
||||
@@ -152,6 +152,8 @@ class LivechatController(http.Controller):
|
||||
|
||||
@http.route('/im_livechat/email_livechat_transcript', type='json', auth='public', cors="*")
|
||||
def email_livechat_transcript(self, uuid, email):
|
||||
channel = request.env['mail.channel'].sudo().search([('uuid', '=', uuid)], limit=1)
|
||||
channel = request.env['mail.channel'].sudo().search([
|
||||
('channel_type', '=', 'livechat'),
|
||||
('uuid', '=', uuid)], limit=1)
|
||||
if channel:
|
||||
channel._email_livechat_transcript(email)
|
||||
|
||||
@@ -171,7 +171,7 @@ class MailChannel(models.Model):
|
||||
template = self.env.ref('im_livechat.livechat_email_template')
|
||||
mail_body = template.render(render_context, engine='ir.qweb', minimal_qcontext=True)
|
||||
mail_body = self.env['mail.thread']._replace_local_links(mail_body)
|
||||
mail = self.env['mail.mail'].create({
|
||||
mail = self.env['mail.mail'].sudo().create({
|
||||
'subject': _('Conversation with %s') % self.livechat_operator_id.name,
|
||||
'email_from': company.catchall_formatted or company.email_formatted,
|
||||
'author_id': self.env.user.partner_id.id,
|
||||
|
||||
@@ -278,7 +278,7 @@ class AccountMove(models.Model):
|
||||
'attachment_ids': [(6, 0, self.l10n_it_einvoice_id.ids)],
|
||||
})
|
||||
|
||||
mail_fattura = self.env['mail.mail'].with_context(wo_return_path=True).create({
|
||||
mail_fattura = self.env['mail.mail'].sudo().with_context(wo_return_path=True).create({
|
||||
'mail_message_id': message.id,
|
||||
'email_to': self.env.company.l10n_it_address_recipient_fatturapa,
|
||||
})
|
||||
|
||||
@@ -367,7 +367,7 @@ class Channel(models.Model):
|
||||
# Notifies the message author when his message is pending moderation if required on channel.
|
||||
# The fields "email_from" and "reply_to" are filled in automatically by method create in model mail.message.
|
||||
if self.moderation_notify and self.moderation_notify_msg and message_type == 'email' and moderation_status == 'pending_moderation':
|
||||
self.env['mail.mail'].create({
|
||||
self.env['mail.mail'].sudo().create({
|
||||
'author_id': self.env.user.partner_id.id,
|
||||
'email_from': self.env.user.company_id.catchall_formatted or self.env.user.company_id.email_formatted,
|
||||
'body_html': self.moderation_notify_msg,
|
||||
@@ -426,7 +426,7 @@ class Channel(models.Model):
|
||||
'subject': _("Guidelines of channel %s") % self.name,
|
||||
'recipient_ids': [(4, partner.id)]
|
||||
}
|
||||
mail = self.env['mail.mail'].create(create_values)
|
||||
mail = self.env['mail.mail'].sudo().create(create_values)
|
||||
return True
|
||||
|
||||
def _update_moderation_email(self, emails, status):
|
||||
|
||||
@@ -134,7 +134,7 @@ class Message(models.Model):
|
||||
# By setting up the inverse one2many, we avoid to have to do a search to find the mails linked to the `mail.message`
|
||||
# as the cache value for this inverse one2many is up-to-date.
|
||||
# Besides for new messages, and messages never sending emails, there was no mail, and it was searching for nothing.
|
||||
mail_ids = fields.One2many('mail.mail', 'mail_message_id', string='Mails')
|
||||
mail_ids = fields.One2many('mail.mail', 'mail_message_id', string='Mails', groups="base.group_system")
|
||||
canned_response_ids = fields.One2many('mail.shortcode', 'message_ids', string="Canned Responses", store=False)
|
||||
|
||||
def _get_needaction(self):
|
||||
|
||||
@@ -405,6 +405,11 @@ class MailTemplate(models.Model):
|
||||
# EMAIL
|
||||
# ----------------------------------------
|
||||
|
||||
def _send_check_access(self, res_ids):
|
||||
records = self.env[self.model].browse(res_ids)
|
||||
records.check_access_rights('read')
|
||||
records.check_access_rule('read')
|
||||
|
||||
def send_mail(self, res_id, force_send=False, raise_exception=False, email_values=None, notif_layout=False):
|
||||
""" Generates a new mail.mail. Template is rendered on record given by
|
||||
res_id and model coming from template.
|
||||
@@ -417,8 +422,11 @@ class MailTemplate(models.Model):
|
||||
:param str notif_layout: optional notification layout to encapsulate the
|
||||
generated email;
|
||||
:returns: id of the mail.mail that was created """
|
||||
|
||||
# Grant access to send_mail only if access to related document
|
||||
self.ensure_one()
|
||||
Mail = self.env['mail.mail']
|
||||
self._send_check_access([res_id])
|
||||
|
||||
Attachment = self.env['ir.attachment'] # TDE FIXME: should remove default_type from context
|
||||
|
||||
# create a mail_mail based on values, without attachments
|
||||
@@ -447,7 +455,7 @@ class MailTemplate(models.Model):
|
||||
}
|
||||
body = template.render(template_ctx, engine='ir.qweb', minimal_qcontext=True)
|
||||
values['body_html'] = self.env['mail.thread']._replace_local_links(body)
|
||||
mail = Mail.create(values)
|
||||
mail = self.env['mail.mail'].sudo().create(values)
|
||||
|
||||
# manage attachments
|
||||
for attachment in attachments:
|
||||
|
||||
@@ -697,7 +697,7 @@ class MailThread(models.AbstractModel):
|
||||
else:
|
||||
bounce_mail_values['email_from'] = tools.decode_message_header(message, 'To')
|
||||
bounce_mail_values.update(mail_values)
|
||||
self.env['mail.mail'].create(bounce_mail_values).send()
|
||||
self.env['mail.mail'].sudo().create(bounce_mail_values).send()
|
||||
|
||||
@api.model
|
||||
def _routing_handle_bounce(self, email_message, message_dict):
|
||||
|
||||
@@ -4,7 +4,7 @@ access_mail_message_portal,mail.message.portal,model_mail_message,base.group_por
|
||||
access_mail_message_user,mail.message.user,model_mail_message,base.group_user,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,1,1,1,0
|
||||
access_mail_mail_user,mail.mail.user,model_mail_mail,base.group_user,0,0,0,0
|
||||
access_mail_mail_system,mail.mail.system,model_mail_mail,base.group_system,1,1,1,1
|
||||
access_mail_followers_all,mail.followers.all,model_mail_followers,,1,0,0,0
|
||||
access_mail_followers_portal,mail.followers.portal,model_mail_followers,base.group_portal,1,1,1,0
|
||||
@@ -28,7 +28,6 @@ access_mail_tracking_value_all,mail.tracking.value.all,model_mail_tracking_value
|
||||
access_mail_tracking_value_portal,mail.tracking.value.portal,model_mail_tracking_value,base.group_portal,0,0,0,0
|
||||
access_mail_tracking_value_user,mail.tracking.value.user,model_mail_tracking_value,base.group_user,0,0,0,0
|
||||
access_mail_tracking_value_system,mail.tracking.value.system,model_mail_tracking_value,base.group_system,1,1,1,1
|
||||
access_mail_thread_all,mail.thread.all,model_mail_thread,,1,1,1,1
|
||||
access_publisher_warranty_contract_all,publisher.warranty.contract.all,model_publisher_warranty_contract,,1,1,1,1
|
||||
access_mail_template,mail.template,model_mail_template,base.group_user,1,1,1,0
|
||||
access_mail_template_system,mail.template_system,model_mail_template,base.group_system,1,1,1,1
|
||||
|
||||
|
@@ -208,7 +208,6 @@ class MailComposer(models.TransientModel):
|
||||
# Mass Mailing
|
||||
mass_mode = wizard.composition_mode in ('mass_mail', 'mass_post')
|
||||
|
||||
Mail = self.env['mail.mail']
|
||||
ActiveModel = self.env[wizard.model] if wizard.model and hasattr(self.env[wizard.model], 'message_post') else self.env['mail.thread']
|
||||
if wizard.composition_mode == 'mass_post':
|
||||
# do not send emails directly but use the queue instead
|
||||
@@ -233,11 +232,15 @@ class MailComposer(models.TransientModel):
|
||||
subtype_id = self.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment')
|
||||
|
||||
for res_ids in sliced_res_ids:
|
||||
batch_mails = Mail
|
||||
# mass mail mode: mail are sudo-ed, as when going through get_mail_values
|
||||
# standard access rights on related records will be checked when browsing them
|
||||
# to compute mail values. If people have access to the records they have rights
|
||||
# to create lots of emails in sudo as it is consdiered as a technical model.
|
||||
batch_mails_sudo = self.env['mail.mail'].sudo()
|
||||
all_mail_values = wizard.get_mail_values(res_ids)
|
||||
for res_id, mail_values in all_mail_values.items():
|
||||
if wizard.composition_mode == 'mass_mail':
|
||||
batch_mails |= Mail.create(mail_values)
|
||||
batch_mails_sudo |= self.env['mail.mail'].sudo().create(mail_values)
|
||||
else:
|
||||
post_params = dict(
|
||||
message_type=wizard.message_type,
|
||||
@@ -258,7 +261,7 @@ class MailComposer(models.TransientModel):
|
||||
ActiveModel.browse(res_id).message_post(**post_params)
|
||||
|
||||
if wizard.composition_mode == 'mass_mail':
|
||||
batch_mails.send(auto_commit=auto_commit)
|
||||
batch_mails_sudo.send(auto_commit=auto_commit)
|
||||
|
||||
def get_mail_values(self, res_ids):
|
||||
"""Generate the values that will be used by send_mail to create mail_messages
|
||||
|
||||
@@ -23,8 +23,7 @@ class MailMail(models.Model):
|
||||
mails = super(MailMail, self).create(values_list)
|
||||
for mail, values in zip(mails, values_list):
|
||||
if values.get('mailing_trace_ids'):
|
||||
mail_sudo = mail.sudo()
|
||||
mail_sudo.mailing_trace_ids.write({'message_id': mail_sudo.message_id, 'state': 'outgoing'})
|
||||
mail.mailing_trace_ids.write({'message_id': mail.message_id, 'state': 'outgoing'})
|
||||
return mails
|
||||
|
||||
def _get_tracking_url(self):
|
||||
|
||||
@@ -68,7 +68,7 @@ Email: <a id="url4" href="mailto:test@odoo.com">test@odoo.com</h1>
|
||||
mass_mailing.action_put_in_queue()
|
||||
mass_mailing._process_mass_mailing_queue()
|
||||
|
||||
sent_mails = self.env['mail.mail'].search([('mailing_id', '=', mass_mailing.id)])
|
||||
sent_mails = self.env['mail.mail'].sudo().search([('mailing_id', '=', mass_mailing.id)])
|
||||
sent_messages = sent_mails.mapped('mail_message_id')
|
||||
|
||||
self.assertEqual(len(mailing_list_A.contact_ids), len(sent_messages),
|
||||
|
||||
@@ -14,7 +14,7 @@ class TestMassMailing(models.TransientModel):
|
||||
|
||||
def send_mail_test(self):
|
||||
self.ensure_one()
|
||||
mails = self.env['mail.mail']
|
||||
mails_sudo = self.env['mail.mail'].sudo()
|
||||
mailing = self.mass_mailing_id
|
||||
test_emails = tools.email_split(self.email_to)
|
||||
mass_mail_layout = self.env.ref('mass_mailing.mass_mailing_mail_layout')
|
||||
@@ -33,7 +33,7 @@ class TestMassMailing(models.TransientModel):
|
||||
'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids],
|
||||
'auto_delete': True,
|
||||
}
|
||||
mail = self.env['mail.mail'].create(mail_values)
|
||||
mails |= mail
|
||||
mails.send()
|
||||
mail = self.env['mail.mail'].sudo().create(mail_values)
|
||||
mails_sudo |= mail
|
||||
mails_sudo.send()
|
||||
return True
|
||||
|
||||
@@ -133,7 +133,7 @@ class TestCertificationFlow(common.TestSurveyCommon, HttpCase):
|
||||
self.assertNotIn("I think they're great!", user_inputs.mapped('user_input_line_ids.value_free_text'))
|
||||
self.assertIn("Just kidding, I don't like it...", user_inputs.mapped('user_input_line_ids.value_free_text'))
|
||||
|
||||
certification_email = self.env['mail.mail'].search([], limit=1, order="create_date desc")
|
||||
certification_email = self.env['mail.mail'].sudo().search([], limit=1, order="create_date desc")
|
||||
# Check certification email correctly sent and contains document
|
||||
self.assertIn("User Certification for SO lines", certification_email.subject)
|
||||
self.assertIn("employee@example.com", certification_email.email_to)
|
||||
|
||||
@@ -12,7 +12,7 @@ class TestServerActionsEmail(TestMailCommon, TestServerActionsBase):
|
||||
self.action.write({'state': 'email', 'template_id': email_template.id})
|
||||
self.action.with_context(self.context).run()
|
||||
# check an email is waiting for sending
|
||||
mail = self.env['mail.mail'].search([('subject', '=', 'About TestingPartner')])
|
||||
mail = self.env['mail.mail'].sudo().search([('subject', '=', 'About TestingPartner')])
|
||||
self.assertEqual(len(mail), 1)
|
||||
# check email content
|
||||
self.assertEqual(mail.body, '<p>Hello TestingPartner</p>')
|
||||
|
||||
@@ -15,8 +15,7 @@ class TestMailMail(TestMailCommon):
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mail_message_notify_from_mail_mail(self):
|
||||
# Due ot post-commit hooks, store send emails in every step
|
||||
# self.email_to_list = []
|
||||
mail = self.env['mail.mail'].create({
|
||||
mail = self.env['mail.mail'].sudo().create({
|
||||
'body_html': '<p>Test</p>',
|
||||
'email_to': 'test@example.com',
|
||||
'partner_ids': [(4, self.user_employee.partner_id.id)]
|
||||
@@ -28,7 +27,7 @@ class TestMailMail(TestMailCommon):
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mail_message_values_unicode(self):
|
||||
mail = self.env['mail.mail'].create({
|
||||
mail = self.env['mail.mail'].sudo().create({
|
||||
'body_html': '<p>Test</p>',
|
||||
'email_to': 'test.😊@example.com',
|
||||
'partner_ids': [(4, self.user_employee.partner_id.id)]
|
||||
@@ -45,7 +44,7 @@ class TestMailMailRace(common.TransactionCase):
|
||||
'name': 'Ernest Partner',
|
||||
})
|
||||
# we need to simulate a mail sent by the cron task, first create mail, message and notification by hand
|
||||
mail = self.env['mail.mail'].create({
|
||||
mail = self.env['mail.mail'].sudo().create({
|
||||
'body_html': '<p>Test</p>',
|
||||
'notification': True,
|
||||
'state': 'outgoing',
|
||||
|
||||
@@ -48,7 +48,7 @@ class TestMailTemplate(TestMailCommon, TestRecipients):
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_template_send_email(self):
|
||||
mail_id = self.email_template.send_mail(self.test_record.id)
|
||||
mail = self.env['mail.mail'].browse(mail_id)
|
||||
mail = self.env['mail.mail'].sudo().browse(mail_id)
|
||||
self.assertEqual(mail.subject, 'About %s' % self.test_record.name)
|
||||
self.assertEqual(mail.email_to, self.email_template.email_to)
|
||||
self.assertEqual(mail.email_cc, self.email_template.email_cc)
|
||||
|
||||
@@ -72,7 +72,7 @@ class TestComposer(TestMailCommon, TestRecipients):
|
||||
}).send_mail()
|
||||
|
||||
# check mail_mail
|
||||
mails = self.env['mail.mail'].search([('subject', 'ilike', 'Testing')])
|
||||
mails = self.env['mail.mail'].sudo().search([('subject', 'ilike', 'Testing')])
|
||||
for mail in mails:
|
||||
self.assertEqual(mail.recipient_ids, self.partner_1 | self.partner_2,
|
||||
'compose wizard: mail_mail mass mailing: mail.mail in mass mail incorrect recipients')
|
||||
|
||||
@@ -86,7 +86,7 @@ class TestMessagePost(TestMailCommon, TestRecipients):
|
||||
self.assertEqual(msg.channel_ids, self.env['mail.channel'])
|
||||
|
||||
# notifications emails should have been deleted
|
||||
self.assertFalse(self.env['mail.mail'].search([('mail_message_id', '=', msg.id)]),
|
||||
self.assertFalse(self.env['mail.mail'].sudo().search([('mail_message_id', '=', msg.id)]),
|
||||
'message_post: mail.mail notifications should have been auto-deleted')
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
@@ -101,7 +101,7 @@ class TestMessagePost(TestMailCommon, TestRecipients):
|
||||
)
|
||||
|
||||
# notifications emails should not have been deleted: one for customers, one for user
|
||||
self.assertEqual(len(self.env['mail.mail'].search([('mail_message_id', '=', msg.id)])), 2)
|
||||
self.assertEqual(len(self.env['mail.mail'].sudo().search([('mail_message_id', '=', msg.id)])), 2)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_post_notifications_emails_tweak(self):
|
||||
|
||||
@@ -459,8 +459,8 @@ class TestMailComplexPerformance(BaseMailPerformance):
|
||||
'recipient_ids': [(4, pid) for pid in self.partners.ids],
|
||||
})
|
||||
mail_ids = mail.ids
|
||||
with self.assertQueryCount(__system__=8, emp=9):
|
||||
self.env['mail.mail'].browse(mail_ids).send()
|
||||
with self.assertQueryCount(__system__=8, emp=8):
|
||||
self.env['mail.mail'].sudo().browse(mail_ids).send()
|
||||
|
||||
self.assertEqual(mail.body_html, '<p>Test</p>')
|
||||
self.assertEqual(mail.reply_to, formataddr(('%s %s' % (self.env.company.name, self.umbrella.name), 'test-alias@example.com')))
|
||||
|
||||
Reference in New Issue
Block a user