Skip to content

Commit

Permalink
[MERGE] mail, various: privatize mail.thread methods and improve blac…
Browse files Browse the repository at this point in the history
…klist mixin

Purpose: make some methods from mail.thread mixin private and update their
naming if necessary. Some methods prepare data for business flows and deal
with internal information. Public methods should either use of give some
access to those methods. Computation itself should be keps internal.

Another purpose is to prepare blacklist and bounce management improvements
by improving blacklist mixin. It is now reflected on models and build on
top of mail.thread. It is renamed to mail.thread.blacklist to be coherent
with mail.thread and mail.thread.cc mixins.

Related to task ID 1911679

closes #29483

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
  • Loading branch information
robodoo committed May 13, 2019
2 parents e740be2 + dbd4aab commit c07dc6e
Show file tree
Hide file tree
Showing 28 changed files with 295 additions and 242 deletions.
10 changes: 6 additions & 4 deletions addons/account/models/account_invoice.py
Expand Up @@ -754,24 +754,26 @@ def message_new(self, msg_dict, custom_values=None):
"""
# Split `From` and `CC` email address from received email to look for related partners to subscribe on the invoice
subscribed_emails = email_split((msg_dict.get('from') or '') + ',' + (msg_dict.get('cc') or ''))
seen_partner_ids = [pid for pid in self._find_partner_from_emails(subscribed_emails) if pid]
seen_partner_ids = [partner.id for partner in self.env['mail.thread']._mail_find_partner_from_emails(subscribed_emails, records=self) if partner]

# Detection of the partner_id of the invoice:
# 1) check if the email_from correspond to a supplier
email_from = msg_dict.get('from') or ''
email_from = email_escape_char(email_split(email_from)[0])
partner_id = self._search_on_partner(email_from, extra_domain=[('supplier', '=', True)])
partners = self.env['mail.thread'].sudo()._mail_search_on_partner(email_from, extra_domain=[('supplier', '=', True)]).id
partner_id = partners.ids[0] if partners else False

# 2) otherwise, if the email sender is from odoo internal users then it is likely that the vendor sent the bill
# by mail to the internal user who, inturn, forwarded that email to the alias to automatically generate the bill
# on behalf of the vendor.
if not partner_id:
user_partner_id = self._search_on_user(email_from)
user_partners = self.env['mail.thread'].sudo()._mail_search_on_user(email_from).id
user_partner_id = user_partners.ids[0] if user_partners else False
if user_partner_id and user_partner_id in self.env.ref('base.group_user').users.mapped('partner_id').ids:
# In this case, we will look for the vendor's email address in email's body and assume if will come first
email_addresses = email_re.findall(msg_dict.get('body'))
if email_addresses:
partner_ids = [pid for pid in self._find_partner_from_emails([email_addresses[0]], force_create=False) if pid]
partner_ids = [partner.id for partner in self.env['mail.thread']._mail_find_partner_from_emails([email_addresses[0]], records=self, force_create=False) if partner]
partner_id = partner_ids and partner_ids[0]
# otherwise, there's no fallback on the partner_id found for the regular author of the mail.message as we want
# the partner_id to stay empty
Expand Down
26 changes: 11 additions & 15 deletions addons/crm/models/crm_lead.py
Expand Up @@ -50,7 +50,7 @@ class Lead(models.Model):
_name = "crm.lead"
_description = "Lead/Opportunity"
_order = "priority desc,activity_date_deadline,id desc"
_inherit = ['mail.thread.cc', 'mail.activity.mixin', 'utm.mixin', 'format.address.mixin', 'mail.blacklist.mixin']
_inherit = ['mail.thread.cc', 'mail.thread.blacklist', 'mail.activity.mixin', 'utm.mixin', 'format.address.mixin']
_primary_email = 'email_from'

def _default_probability(self):
Expand Down Expand Up @@ -96,9 +96,6 @@ def _default_stage_id(self):
date_last_stage_update = fields.Datetime(string='Last Stage Update', index=True, default=fields.Datetime.now)
date_conversion = fields.Datetime('Conversion Date', readonly=True)

# Messaging and marketing
message_bounce = fields.Integer('Bounce', help="Counter of the number of bounced emails for this contact", default=0)

# Only used for type opportunity
probability = fields.Float('Probability', group_operator="avg", default=lambda self: self._default_probability())
planned_revenue = fields.Monetary('Expected Revenue', currency_field='company_currency', tracking=True)
Expand Down Expand Up @@ -1163,17 +1160,16 @@ def get_formview_id(self, access_uid=None):
return view_id

@api.multi
def message_get_default_recipients(self):
return {
r.id : {'partner_ids': [],
'email_to': r.email_normalized,
'email_cc': False}
for r in self.sudo()
}
def _message_get_default_recipients(self):
return {r.id: {
'partner_ids': [],
'email_to': r.email_normalized,
'email_cc': False}
for r in self}

@api.multi
def message_get_suggested_recipients(self):
recipients = super(Lead, self).message_get_suggested_recipients()
def _message_get_suggested_recipients(self):
recipients = super(Lead, self)._message_get_suggested_recipients()
try:
for lead in self:
if lead.partner_id:
Expand Down Expand Up @@ -1229,8 +1225,8 @@ def _message_post_after_hook(self, message, *args, **kwargs):
return super(Lead, self)._message_post_after_hook(message, *args, **kwargs)

@api.multi
def message_partner_info_from_emails(self, emails, link_mail=False):
result = super(Lead, self).message_partner_info_from_emails(emails, link_mail=link_mail)
def _message_partner_info_from_emails(self, emails, link_mail=False):
result = super(Lead, self)._message_partner_info_from_emails(emails, link_mail=link_mail)
for partner_info in result:
if not partner_info.get('partner_id') and (self.partner_name or self.contact_name):
emails = email_re.findall(partner_info['full_name'] or '')
Expand Down
17 changes: 8 additions & 9 deletions addons/event/models/event.py
Expand Up @@ -474,8 +474,8 @@ def _onchange_partner(self):
self.phone = contact.phone or self.phone

@api.multi
def message_get_suggested_recipients(self):
recipients = super(EventRegistration, self).message_get_suggested_recipients()
def _message_get_suggested_recipients(self):
recipients = super(EventRegistration, self)._message_get_suggested_recipients()
public_users = self.env['res.users'].sudo()
public_groups = self.env.ref("base.group_public", raise_if_not_found=False)
if public_groups:
Expand All @@ -492,15 +492,14 @@ def message_get_suggested_recipients(self):
return recipients

@api.multi
def message_get_default_recipients(self):
def _message_get_default_recipients(self):
# Prioritize registration email over partner_id, which may be shared when a single
# partner booked multiple seats
return {
r.id: {'partner_ids': [],
'email_to': r.email,
'email_cc': False}
for r in self
}
return {r.id: {
'partner_ids': [],
'email_to': r.email,
'email_cc': False}
for r in self}

def _message_post_after_hook(self, message, *args, **kwargs):
if self.email and not self.partner_id:
Expand Down
4 changes: 2 additions & 2 deletions addons/hr_recruitment/models/hr_recruitment.py
Expand Up @@ -381,8 +381,8 @@ def _notify_get_reply_to(self, default=None, records=None, company=None, doc_nam
return res

@api.multi
def message_get_suggested_recipients(self):
recipients = super(Applicant, self).message_get_suggested_recipients()
def _message_get_suggested_recipients(self):
recipients = super(Applicant, self)._message_get_suggested_recipients()
for applicant in self:
if applicant.partner_id:
applicant._message_add_suggested_recipient(recipients, partner=applicant.partner_id, reason=_('Contact'))
Expand Down
20 changes: 20 additions & 0 deletions addons/mail/controllers/main.py
Expand Up @@ -279,3 +279,23 @@ def mail_init_messaging(self):
'moderation_channel_ids': request.env.user.moderation_channel_ids.ids,
}
return values

@http.route('/mail/get_partner_info', type='json', auth='user')
def message_partner_info_from_emails(self, model, res_ids, emails, link_mail=False):
records = request.env[model].browse(res_ids)
try:
records.check_access_rule('read')
records.check_access_rights('read')
except:
return []
return records._message_partner_info_from_emails(emails, link_mail=link_mail)

@http.route('/mail/get_suggested_recipients', type='json', auth='user')
def message_get_suggested_recipients(self, model, res_ids):
records = request.env[model].browse(res_ids)
try:
records.check_access_rule('read')
records.check_access_rights('read')
except:
return {}
return records._message_get_suggested_recipients()
13 changes: 12 additions & 1 deletion addons/mail/models/ir_model.py
Expand Up @@ -17,6 +17,10 @@ class IrModel(models.Model):
string="Mail Activity", default=False,
help="Whether this model supports activities.",
)
is_mail_blacklist = fields.Boolean(
string="Mail Blacklist", default=False,
help="Whether this model supports blacklist.",
)

def unlink(self):
# Delete followers, messages and attachments for models that will be unlinked.
Expand Down Expand Up @@ -51,13 +55,15 @@ def unlink(self):

@api.multi
def write(self, vals):
if self and ('is_mail_thread' in vals or 'is_mail_activity' in vals):
if self and ('is_mail_thread' in vals or 'is_mail_activity' in vals or 'is_mail_blacklist' in vals):
if not all(rec.state == 'manual' for rec in self):
raise UserError(_('Only custom models can be modified.'))
if 'is_mail_thread' in vals and not all(rec.is_mail_thread <= vals['is_mail_thread'] for rec in self):
raise UserError(_('Field "Mail Thread" cannot be changed to "False".'))
if 'is_mail_activity' in vals and not all(rec.is_mail_activity <= vals['is_mail_activity'] for rec in self):
raise UserError(_('Field "Mail Activity" cannot be changed to "False".'))
if 'is_mail_blacklist' in vals and not all(rec.is_mail_blacklist <= vals['is_mail_blacklist'] for rec in self):
raise UserError(_('Field "Mail Blacklist" cannot be changed to "False".'))
res = super(IrModel, self).write(vals)
# setup models; this reloads custom models in registry
self.pool.setup_models(self._cr)
Expand All @@ -72,6 +78,7 @@ def _reflect_model_params(self, model):
vals = super(IrModel, self)._reflect_model_params(model)
vals['is_mail_thread'] = issubclass(type(model), self.pool['mail.thread'])
vals['is_mail_activity'] = issubclass(type(model), self.pool['mail.activity.mixin'])
vals['is_mail_blacklist'] = issubclass(type(model), self.pool['mail.thread.blacklist'])
return vals

@api.model
Expand All @@ -85,4 +92,8 @@ def _instanciate(self, model_data):
parents = model_class._inherit or []
parents = [parents] if isinstance(parents, str) else parents
model_class._inherit = parents + ['mail.activity.mixin']
if model_data.get('is_mail_blacklist') and model_class._name != 'mail.thread.blacklist':
parents = model_class._inherit or []
parents = [parents] if isinstance(parents, str) else parents
model_class._inherit = parents + ['mail.thread.blacklist']
return model_class
32 changes: 16 additions & 16 deletions addons/mail/models/mail_address_mixin.py
Expand Up @@ -6,25 +6,25 @@


class MailAddressMixin(models.AbstractModel):
""" Purpose of this mixing is to store a normalized email based on the primary email field.
A normalized email is considered as :
- having a left part + @ + a right part (the domain can be without '.something')
- being lower case
- having no name before the address. Typically, having no 'Name <>'
Ex:
- Formatted Email : 'Name <NaMe@DoMaIn.CoM>'
- Normalized Email : 'name@domain.com'
The primary email field can be specified on the parent model, if it differs from the default one ('email')
The email_normalized field can than be used on that model to search quickly on emails (by simple comparison
and not using time consuming regex anymore).
"""
""" Purpose of this mixin is to store a normalized email based on the primary email field.
A normalized email is considered as :
- having a left part + @ + a right part (the domain can be without '.something')
- being lower case
- having no name before the address. Typically, having no 'Name <>'
Ex:
- Formatted Email : 'Name <NaMe@DoMaIn.CoM>'
- Normalized Email : 'name@domain.com'
The primary email field can be specified on the parent model, if it differs from the default one ('email')
The email_normalized field can than be used on that model to search quickly on emails (by simple comparison
and not using time consuming regex anymore). """
_name = 'mail.address.mixin'
_description = 'Email address mixin'
_description = 'Email Address Mixin'
_primary_email = 'email'

email_normalized = fields.Char(string='Normalized email address', compute="_compute_email_normalized", invisible=True,
compute_sudo=True, store=True, help="""This field is used to search on email address,
as the primary email field can contain more than strictly an email address.""")
email_normalized = fields.Char(
string='Normalized Email', compute="_compute_email_normalized", compute_sudo=True,
store=True, invisible=True,
help="This field is used to search on email address as the primary email field can contain more than strictly an email address.")

@api.depends(lambda self: [self._primary_email])
def _compute_email_normalized(self):
Expand Down
27 changes: 19 additions & 8 deletions addons/mail/models/mail_blacklist.py
Expand Up @@ -94,19 +94,22 @@ def _remove(self, email):


class MailBlackListMixin(models.AbstractModel):
""" Mixin that is inherited by all model with opt out.
This mixin is inheriting of another mixin, mail.address.mixin which defines the _primary_email variable
and the email_normalized field that are mandatory to use the blacklist mixin.
"""
_name = 'mail.blacklist.mixin'
""" Mixin that is inherited by all model with opt out. This mixin inherits from
mail.address.mixin which defines the _primary_email variable and the email_normalized
field that are mandatory to use the blacklist mixin. Mail Thread capabilities
are required for this mixin. """
_name = 'mail.thread.blacklist'
_description = 'Mail Blacklist mixin'
_inherit = ['mail.address.mixin']
_inherit = ['mail.thread', 'mail.address.mixin']

# Note : is_blacklisted sould only be used for display. As the compute is not depending on the blacklist,
# once read, it won't be re-computed again if the blacklist is modified in the same request.
is_blacklisted = fields.Boolean(string='Blacklist', compute="_compute_is_blacklisted", compute_sudo=True,
store=False, search="_search_is_blacklisted", groups="base.group_user",
is_blacklisted = fields.Boolean(
string='Blacklist', compute="_compute_is_blacklisted", compute_sudo=True, store=False,
search="_search_is_blacklisted", groups="base.group_user",
help="If the email address is on the blacklist, the contact won't receive mass mailing anymore, from any list")
# messaging
message_bounce = fields.Integer('Bounce', help="Counter of the number of bounced emails for this contact", default=0)

@api.model
def _search_is_blacklisted(self, operator, value):
Expand Down Expand Up @@ -147,3 +150,11 @@ def _compute_is_blacklisted(self):
('email', 'in', self.mapped('email_normalized'))]).mapped('email'))
for record in self:
record.is_blacklisted = record.email_normalized in blacklist

@api.multi
def _message_receive_bounce(self, email, partner, mail_id=None):
""" Override of mail.thread generic method. Purpose is to increment the
bounce counter of the record. """
super(MailBlackListMixin, self)._message_receive_bounce(email, partner, mail_id=mail_id)
for record in self:
record.message_bounce = record.message_bounce + 1
4 changes: 2 additions & 2 deletions addons/mail/models/mail_cc_mixin.py
Expand Up @@ -44,8 +44,8 @@ def message_update(self, msg_dict, update_vals=None):
return super(MailCCMixin, self).message_update(msg_dict, cc_values)

@api.multi
def message_get_suggested_recipients(self):
recipients = super(MailCCMixin, self).message_get_suggested_recipients()
def _message_get_suggested_recipients(self):
recipients = super(MailCCMixin, self)._message_get_suggested_recipients()
for record in self:
if record.email_cc:
for email in tools.email_split_and_format(record.email_cc):
Expand Down
4 changes: 2 additions & 2 deletions addons/mail/models/mail_channel.py
Expand Up @@ -338,12 +338,12 @@ def _notify_specific_email_values(self, message):
return res

@api.multi
def message_receive_bounce(self, email, partner, mail_id=None):
def _message_receive_bounce(self, email, partner, mail_id=None):
""" Override bounce management to unsubscribe bouncing addresses """
for p in partner:
if p.message_bounce >= self.MAX_BOUNCE_LIMIT:
self._action_unfollow(p)
return super(Channel, self).message_receive_bounce(email, partner, mail_id=mail_id)
return super(Channel, self)._message_receive_bounce(email, partner, mail_id=mail_id)

@api.multi
def _notify_email_recipients(self, message, recipient_ids):
Expand Down
3 changes: 2 additions & 1 deletion addons/mail/models/mail_template.py
Expand Up @@ -384,7 +384,8 @@ def generate_recipients(self, results, res_ids):
self.ensure_one()

if self.use_default_to or self._context.get('tpl_force_default_to'):
default_recipients = self.env['mail.thread'].message_get_default_recipients(res_model=self.model, res_ids=res_ids)
records = self.env[self.model].browse(res_ids).sudo()
default_recipients = self.env['mail.thread']._message_get_default_recipients_on_records(records)
for res_id, recipients in default_recipients.items():
results[res_id].pop('partner_to', None)
results[res_id].update(recipients)
Expand Down

0 comments on commit c07dc6e

Please sign in to comment.