Purpose of this commit is to call toggle_active or archive / unarchive methods instead of manually writing on active field. Indeed this allows to trigger business code related to archive / unarchive which is normally located in toggle_archive (called by action_archive and action_unarchive). Task ID 2170708 Community PR odoo/odoo#46563
228 lines
9.8 KiB
Python
228 lines
9.8 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from odoo import api, fields, models, tools, _
|
|
from odoo.exceptions import AccessError, UserError
|
|
|
|
|
|
class MailBlackList(models.Model):
|
|
""" Model of blacklisted email addresses to stop sending emails."""
|
|
_name = 'mail.blacklist'
|
|
_inherit = ['mail.thread']
|
|
_description = 'Mail Blacklist'
|
|
_rec_name = 'email'
|
|
|
|
email = fields.Char(string='Email Address', required=True, index=True, help='This field is case insensitive.',
|
|
tracking=True)
|
|
active = fields.Boolean(default=True, tracking=True)
|
|
|
|
_sql_constraints = [
|
|
('unique_email', 'unique (email)', 'Email address already exists!')
|
|
]
|
|
|
|
@api.model_create_multi
|
|
def create(self, values):
|
|
# First of all, extract values to ensure emails are really unique (and don't modify values in place)
|
|
new_values = []
|
|
all_emails = []
|
|
for value in values:
|
|
email = tools.email_normalize(value.get('email'))
|
|
if not email:
|
|
raise UserError(_('Invalid email address %r') % value['email'])
|
|
if email in all_emails:
|
|
continue
|
|
all_emails.append(email)
|
|
new_value = dict(value, email=email)
|
|
new_values.append(new_value)
|
|
|
|
""" To avoid crash during import due to unique email, return the existing records if any """
|
|
sql = '''SELECT email, id FROM mail_blacklist WHERE email = ANY(%s)'''
|
|
emails = [v['email'] for v in new_values]
|
|
self._cr.execute(sql, (emails,))
|
|
bl_entries = dict(self._cr.fetchall())
|
|
to_create = [v for v in new_values if v['email'] not in bl_entries]
|
|
|
|
# TODO DBE Fixme : reorder ids according to incoming ids.
|
|
results = super(MailBlackList, self).create(to_create)
|
|
return self.env['mail.blacklist'].browse(bl_entries.values()) | results
|
|
|
|
def write(self, values):
|
|
if 'email' in values:
|
|
values['email'] = tools.email_normalize(values['email'])
|
|
return super(MailBlackList, self).write(values)
|
|
|
|
def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
|
|
""" Override _search in order to grep search on email field and make it
|
|
lower-case and sanitized """
|
|
if args:
|
|
new_args = []
|
|
for arg in args:
|
|
if isinstance(arg, (list, tuple)) and arg[0] == 'email' and isinstance(arg[2], str):
|
|
normalized = tools.email_normalize(arg[2])
|
|
if normalized:
|
|
new_args.append([arg[0], arg[1], normalized])
|
|
else:
|
|
new_args.append(arg)
|
|
else:
|
|
new_args.append(arg)
|
|
else:
|
|
new_args = args
|
|
return super(MailBlackList, self)._search(new_args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid)
|
|
|
|
def _add(self, email):
|
|
normalized = tools.email_normalize(email)
|
|
record = self.env["mail.blacklist"].with_context(active_test=False).search([('email', '=', normalized)])
|
|
if len(record) > 0:
|
|
record.action_unarchive()
|
|
else:
|
|
record = self.create({'email': email})
|
|
return record
|
|
|
|
def action_remove_with_reason(self, email, reason=None):
|
|
record = self._remove(email)
|
|
if reason:
|
|
record.message_post(body=_("Unblacklisting Reason: %s" % (reason)))
|
|
|
|
return record
|
|
|
|
def _remove(self, email):
|
|
normalized = tools.email_normalize(email)
|
|
record = self.env["mail.blacklist"].with_context(active_test=False).search([('email', '=', normalized)])
|
|
if len(record) > 0:
|
|
record.action_archive()
|
|
else:
|
|
record = record.create({'email': email, 'active': False})
|
|
return record
|
|
|
|
def mail_action_blacklist_remove(self):
|
|
return {
|
|
'name': 'Are you sure you want to unblacklist this Email Address?',
|
|
'type': 'ir.actions.act_window',
|
|
'view_mode': 'form',
|
|
'res_model': 'mail.blacklist.remove',
|
|
'target': 'new',
|
|
}
|
|
|
|
def action_add(self):
|
|
self._add(self.email)
|
|
|
|
class MailBlackListMixin(models.AbstractModel):
|
|
""" Mixin that is inherited by all model with opt out. This mixin stores a normalized
|
|
email based on 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).
|
|
|
|
Using this email_normalized field, blacklist status is computed.
|
|
|
|
Mail Thread capabilities are required for this mixin. """
|
|
|
|
_name = 'mail.thread.blacklist'
|
|
_inherit = ['mail.thread']
|
|
_description = 'Mail Blacklist mixin'
|
|
_primary_email = 'email'
|
|
|
|
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.")
|
|
# 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",
|
|
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.depends(lambda self: [self._primary_email])
|
|
def _compute_email_normalized(self):
|
|
self._assert_primary_email()
|
|
for record in self:
|
|
record.email_normalized = tools.email_normalize(record[self._primary_email])
|
|
|
|
@api.model
|
|
def _search_is_blacklisted(self, operator, value):
|
|
# Assumes operator is '=' or '!=' and value is True or False
|
|
self.flush(['email_normalized'])
|
|
self.env['mail.blacklist'].flush(['email', 'active'])
|
|
self._assert_primary_email()
|
|
if operator != '=':
|
|
if operator == '!=' and isinstance(value, bool):
|
|
value = not value
|
|
else:
|
|
raise NotImplementedError()
|
|
|
|
if value:
|
|
query = """
|
|
SELECT m.id
|
|
FROM mail_blacklist bl
|
|
JOIN %s m
|
|
ON m.email_normalized = bl.email AND bl.active
|
|
"""
|
|
else:
|
|
query = """
|
|
SELECT m.id
|
|
FROM %s m
|
|
LEFT JOIN mail_blacklist bl
|
|
ON m.email_normalized = bl.email AND bl.active
|
|
WHERE bl.id IS NULL
|
|
"""
|
|
self._cr.execute(query % self._table)
|
|
res = self._cr.fetchall()
|
|
if not res:
|
|
return [(0, '=', 1)]
|
|
return [('id', 'in', [r[0] for r in res])]
|
|
|
|
@api.depends('email_normalized')
|
|
def _compute_is_blacklisted(self):
|
|
# TODO : Should remove the sudo as compute_sudo defined on methods.
|
|
# But if user doesn't have access to mail.blacklist, doen't work without sudo().
|
|
blacklist = set(self.env['mail.blacklist'].sudo().search([
|
|
('email', 'in', self.mapped('email_normalized'))]).mapped('email'))
|
|
for record in self:
|
|
record.is_blacklisted = record.email_normalized in blacklist
|
|
|
|
def _assert_primary_email(self):
|
|
if not hasattr(self, "_primary_email") or not isinstance(self._primary_email, str):
|
|
raise UserError(_('Invalid primary email field on model %s') % self._name)
|
|
if self._primary_email not in self._fields or self._fields[self._primary_email].type != 'char':
|
|
raise UserError(_('Invalid primary email field on model %s') % self._name)
|
|
|
|
def _message_receive_bounce(self, email, partner):
|
|
""" Override of mail.thread generic method. Purpose is to increment the
|
|
bounce counter of the record. """
|
|
super(MailBlackListMixin, self)._message_receive_bounce(email, partner)
|
|
for record in self:
|
|
record.message_bounce = record.message_bounce + 1
|
|
|
|
def _message_reset_bounce(self, email):
|
|
""" Override of mail.thread generic method. Purpose is to reset the
|
|
bounce counter of the record. """
|
|
super(MailBlackListMixin, self)._message_reset_bounce(email)
|
|
self.write({'message_bounce': 0})
|
|
|
|
def mail_action_blacklist_remove(self):
|
|
# wizard access rights currently not working as expected and allows users without access to
|
|
# open this wizard, therefore we check to make sure they have access before the wizard opens.
|
|
can_access = self.env['mail.blacklist'].check_access_rights('write', raise_exception=False)
|
|
if can_access:
|
|
return {
|
|
'name': 'Are you sure you want to unblacklist this Email Address?',
|
|
'type': 'ir.actions.act_window',
|
|
'view_mode': 'form',
|
|
'res_model': 'mail.blacklist.remove',
|
|
'target': 'new',
|
|
}
|
|
else:
|
|
raise AccessError("You do not have the access right to unblacklist emails. Please contact your administrator.")
|