From 155e81d5e7be2d67b284bf92cee76d34984e9ce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 9 Mar 2017 13:59:57 +0100 Subject: [PATCH 1/5] [IMP] mail: ease members management on channels Using a m2m to partners does not allow a real management of members because it allows to create partners, and require access rights on partner model. Instead it now uses the o2m to channel members allowing creation, update and removal of members without having to work on partner records. --- addons/mail/models/mail_channel.py | 1 + addons/mail/views/mail_channel_views.xml | 9 ++++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index 63bd82a5f87..ed1b7b771a7 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -20,6 +20,7 @@ class ChannelPartner(models.Model): _rec_name = 'partner_id' partner_id = fields.Many2one('res.partner', string='Recipient', ondelete='cascade') + partner_email = fields.Char('Email', related='partner_id.email') channel_id = fields.Many2one('mail.channel', string='Channel', ondelete='cascade') seen_message_id = fields.Many2one('mail.message', string='Last Seen') fold_state = fields.Selection([('open', 'Open'), ('folded', 'Folded'), ('closed', 'Closed')], string='Conversation Fold State', default='open') diff --git a/addons/mail/views/mail_channel_views.xml b/addons/mail/views/mail_channel_views.xml index e78ed6928ba..0c1ed4a8ce9 100644 --- a/addons/mail/views/mail_channel_views.xml +++ b/addons/mail/views/mail_channel_views.xml @@ -120,11 +120,10 @@ - - - - - + + + + From 634821c68868678127f4aff9a85b1aa525cc1d1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 10 Mar 2017 10:53:49 +0100 Subject: [PATCH 2/5] [IMP] mail: correctly use alias mixin on channel model Currently channel model manually defines alias and inherits instead of using the alias.mixin. This commit proposes to use alias feature through the mixin to have a standard behavior. --- addons/mail/models/mail_channel.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index ed1b7b771a7..69031688c94 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -35,8 +35,7 @@ class Channel(models.Model): _name = 'mail.channel' _mail_flat_thread = False _mail_post_access = 'read' - _inherit = ['mail.thread'] - _inherits = {'mail.alias': 'alias_id'} + _inherit = ['mail.thread', 'mail.alias.mixin'] MAX_BOUNCE_LIMIT = 10 @@ -82,9 +81,6 @@ class Channel(models.Model): help="Small-sized photo of the group. It is automatically " "resized as a 64x64px image, with aspect ratio preserved. " "Use this field anywhere a small image is required.") - alias_id = fields.Many2one( - 'mail.alias', 'Alias', ondelete="restrict", required=True, - help="The email address associated with this group. New emails received will automatically create new topics.") is_subscribed = fields.Boolean( 'Is Subscribed', compute='_compute_is_subscribed') @@ -145,6 +141,9 @@ class Channel(models.Model): self._subscribe_users() return result + def get_alias_model_name(self, vals): + return vals.get('alias_model', 'mail.channel') + def _subscribe_users(self): for mail_channel in self: mail_channel.write({'channel_partner_ids': [(4, pid) for pid in mail_channel.mapped('group_ids').mapped('users').mapped('partner_id').ids]}) From 78ac6de52d690d278fd0cde5476bbeaec86cb124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 20 Mar 2017 16:11:47 +0100 Subject: [PATCH 3/5] [IMP] mail: factorize code that handles alias security in mail gateway Currently check of alias security is directly embedded in mail.thread routing method. In this commit we move this check in the mail.alias.mixin class itself. Doing this allows models to extend or improve the behavior depending on how inheriting models use aliases. --- addons/hr/models/__init__.py | 1 - addons/hr/models/mail_alias.py | 21 +++++++++++- addons/hr/models/mail_thread.py | 56 ++++++++++--------------------- addons/mail/models/mail_alias.py | 18 ++++++++++ addons/mail/models/mail_thread.py | 26 ++++++-------- 5 files changed, 67 insertions(+), 55 deletions(-) diff --git a/addons/hr/models/__init__.py b/addons/hr/models/__init__.py index 33a14fc6317..e396b616d67 100644 --- a/addons/hr/models/__init__.py +++ b/addons/hr/models/__init__.py @@ -4,6 +4,5 @@ import hr import hr_config_settings import mail_alias -import mail_thread import res_partner import res_users diff --git a/addons/hr/models/mail_alias.py b/addons/hr/models/mail_alias.py index 710b7efaff7..a88a6a4530f 100644 --- a/addons/hr/models/mail_alias.py +++ b/addons/hr/models/mail_alias.py @@ -1,10 +1,29 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import fields, models +from odoo import fields, models, tools class Alias(models.Model): _inherit = 'mail.alias' alias_contact = fields.Selection(selection_add=[('employees', 'Authenticated Employees')]) + + +class MailAlias(models.AbstractModel): + _inherit = 'mail.alias.mixin' + + def _alias_check_contact(self, message, message_dict, alias): + if alias.alias_contact == 'employees' and self.ids: + email_from = tools.decode_message_header(message, 'From') + email_address = tools.email_split(email_from)[0] + employee = self.env['hr.employee'].search([('work_email', 'ilike', email_address)], limit=1) + if not employee: + employee = self.env['hr.employee'].search([('user_id.email', 'ilike', email_address)], limit=1) + if not employee: + return { + 'error_message': 'restricted to employees', + 'error_template': self.env.ref('hr.mail_template_data_unknown_employee_email_address').body_html, + } + return True + return super(MailAlias, self)._alias_check_contact(message, message_dict, alias) diff --git a/addons/hr/models/mail_thread.py b/addons/hr/models/mail_thread.py index 06eec55240d..25ee48bcd6d 100644 --- a/addons/hr/models/mail_thread.py +++ b/addons/hr/models/mail_thread.py @@ -1,44 +1,24 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import logging - -from odoo import api, models, _ -from odoo.tools import decode_message_header, email_split - -_logger = logging.getLogger(__name__) +from odoo import models, tools +from odoo.tools import email_split -class MailThread(models.AbstractModel): - _inherit = "mail.thread" +class MailAlias(models.AbstractModel): + _inherit = 'mail.alias.mixin' - @api.model - def message_route_verify(self, message, message_dict, route, - update_author=True, assert_model=True, - create_fallback=True, allow_private=False, - drop_alias=False): - res = super(MailThread, self).message_route_verify( - message, message_dict, route, - update_author=update_author, - assert_model=assert_model, - create_fallback=create_fallback, - allow_private=allow_private, - drop_alias=drop_alias) - - if res: - alias = route[4] - email_from = decode_message_header(message, 'From') - message_id = message.get('Message-Id') - - # Alias: check alias_contact settings for employees - if alias and alias.alias_contact == 'employees': - email_address = email_split(email_from)[0] - employee = self.env['hr.employee'].search([('work_email', 'ilike', email_address)], limit=1) - if not employee: - employee = self.env['hr.employee'].search([('user_id.email', 'ilike', email_address)], limit=1) - if not employee: - mail_template = self.env.ref('hr.mail_template_data_unknown_employee_email_address') - self._routing_warn(_('alias %s does not accept unknown employees') % alias.alias_name, _('skipping'), message_id, route, False) - self._routing_create_bounce_email(email_from, mail_template.body_html, message) - return False - return res + def _alias_check_contact(self, message, message_dict, alias): + if alias.alias_contact == 'employees' and self.ids: + email_from = tools.decode_message_header(message, 'From') + email_address = email_split(email_from)[0] + employee = self.env['hr.employee'].search([('work_email', 'ilike', email_address)], limit=1) + if not employee: + employee = self.env['hr.employee'].search([('user_id.email', 'ilike', email_address)], limit=1) + if not employee: + return { + 'error_message': 'restricted to employees', + 'error_template': self.env.ref('hr.mail_template_data_unknown_employee_email_address').body_html, + } + return True + return super(MailAlias, self)._alias_check_contact(message, message_dict, alias) diff --git a/addons/mail/models/mail_alias.py b/addons/mail/models/mail_alias.py index 67233efcd77..df0d84062e7 100644 --- a/addons/mail/models/mail_alias.py +++ b/addons/mail/models/mail_alias.py @@ -262,3 +262,21 @@ class AliasMixin(models.AbstractModel): record.with_context({'mail_notrack': True}).alias_id = alias _logger.info('Mail alias created for %s %s (id %s)', record._name, record.display_name, record.id) + + def _alias_check_contact(self, message, message_dict, alias): + author = self.env['res.partner'].browse(message_dict.get('author_id', False)) + if alias.alias_contact == 'followers' and self.ids: + if not hasattr(self, "message_partner_ids") or not hasattr(self, "message_channel_ids"): + return { + 'error_mesage': _('incorrectly configured alias'), + } + accepted_partner_ids = self.message_partner_ids | self.message_channel_ids.mapped('channel_partner_ids') + if not author or author not in accepted_partner_ids: + return { + 'error_mesage': _('restricted to followers'), + } + elif alias.alias_contact == 'partners' and not author: + return { + 'error_message': _('restricted to known authors') + } + return True diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 4e8a1ada142..baffc4fc174 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -944,27 +944,23 @@ class MailThread(models.AbstractModel): if not author_id and update_author: author_ids = self.env['mail.thread']._find_partner_from_emails([email_from], res_model=model, res_id=thread_id) if author_ids: - author_id = author_ids[0] - message_dict['author_id'] = author_id + message_dict['author_id'] = author_ids[0] # Alias: check alias_contact settings - if alias and alias.alias_contact == 'followers' and (thread_id or alias.alias_parent_thread_id): + if alias: if thread_id: obj = record_set[0] - else: + elif alias.alias_parent_thread_id: obj = self.env[alias.alias_parent_model_id.model].browse(alias.alias_parent_thread_id) - accepted_partner_ids = list( - set(partner.id for partner in obj.message_partner_ids) | - set(partner.id for channel in obj.message_channel_ids for partner in channel.channel_partner_ids) - ) - if not author_id or author_id not in accepted_partner_ids: - self._routing_warn(_('alias %s restricted to internal followers') % alias.alias_name, _('skipping'), message_id, route, False) - self._routing_create_bounce_email(email_from, _generic_bounce_body_html, message) + elif model and hasattr(record_set, '_alias_check_contact'): + obj = self.env[model] + else: + obj = self.env['mail.alias.mixin'] + check_result = obj._alias_check_contact(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'))), _('skipping'), message_id, route, False) + self._routing_create_bounce_email(email_from, check_result.get('error_template', _generic_bounce_body_html), message) return False - elif alias and alias.alias_contact == 'partners' and not author_id: - self._routing_warn(_('alias %s does not accept unknown author') % alias.alias_name, _('skipping'), message_id, route, False) - self._routing_create_bounce_email(email_from, _generic_bounce_body_html, message) - return False if not model and not thread_id and not alias and not allow_private: return False From e5e549a4cf12e62fc72c846dc32828182fae37e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 20 Mar 2017 16:12:18 +0100 Subject: [PATCH 4/5] [IMP] mail: make channels check their members instead of followers for alias This commit makes channel use 'followers' based alias security use members instead. Indeed channels do not use followers anymore. Instead channels have members, and alias security of followers should use members instead. --- addons/mail/models/mail_channel.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index 69031688c94..29f0e927978 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -232,6 +232,16 @@ class Channel(models.Model): message = super(Channel, self.with_context(mail_create_nosubscribe=True)).message_post(body=body, subject=subject, message_type=message_type, subtype=subtype, parent_id=parent_id, attachments=attachments, content_subtype=content_subtype, **kwargs) return message + def _alias_check_contact(self, message, message_dict, alias): + 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_mesage': _('restricted to channel members'), + } + return True + return super(Channel, self)._alias_check_contact(message, message_dict, alias) + @api.model_cr def init(self): self._cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('mail_channel_partner_seen_message_id_idx',)) From 66bd7707f5ba679ff3b878adafd72475250965a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 10 Mar 2017 11:54:37 +0100 Subject: [PATCH 5/5] [IMP] mail: protect by default alias of private or group based channels Otherwise by default their alias are public. This makes no sense for private groups to have by default public aliases. Field is set as invisible on the view because it makes sense to configure only the channel and not details of its alias. Anyway it is always possible to tweak the alias configuration under Technical menu. --- addons/mail/models/mail_channel.py | 14 ++++++++++++++ addons/mail/views/mail_channel_views.xml | 3 +-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index 29f0e927978..6012dc9c22b 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -43,6 +43,13 @@ class Channel(models.Model): image_path = modules.get_module_resource('mail', 'static/src/img', 'groupdefault.png') return tools.image_resize_image_big(open(image_path, 'rb').read().encode('base64')) + @api.model + def default_get(self, fields): + res = super(Channel, self).default_get(fields) + if not res.get('alias_contact') and (not fields or 'alias_contact' in fields): + res['alias_contact'] = 'everyone' if res.get('public', 'private') == 'public' else 'followers' + return res + name = fields.Char('Name', required=True, translate=True) channel_type = fields.Selection([ ('chat', 'Chat Discussion'), @@ -99,6 +106,13 @@ class Channel(models.Model): for record in self: record.is_member = record in membership_ids + @api.onchange('public') + def _onchange_public(self): + if self.public == 'public': + self.alias_contact = 'everyone' + else: + self.alias_contact = 'followers' + @api.model def create(self, vals): tools.image_resize_images(vals) diff --git a/addons/mail/views/mail_channel_views.xml b/addons/mail/views/mail_channel_views.xml index 0c1ed4a8ce9..58c6d724a4f 100644 --- a/addons/mail/views/mail_channel_views.xml +++ b/addons/mail/views/mail_channel_views.xml @@ -106,8 +106,7 @@ @ -