Skip to content
Permalink
Browse files

[MERGE] mail, various: privatize mail.thread methods and improve blac…

…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 c07dc6e022ac38d54356899e438038119c7dfae1
@@ -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
@@ -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):
@@ -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)
@@ -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:
@@ -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 '')
@@ -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:
@@ -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:
@@ -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'))
@@ -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()
@@ -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.
@@ -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)
@@ -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
@@ -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
@@ -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):
@@ -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):
@@ -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
@@ -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):
@@ -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):
@@ -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)

0 comments on commit c07dc6e

Please sign in to comment.
You can’t perform that action at this time.