Browse Source

Add normalization and optional uniqueness to partner_email_check

- partner_email_check now uses email-validator instead of
  validate_email
- Email addresses are normalized
- There is a setting to enforce uniqueness of partner email addresses
- There is a setting to check whether emails are
  deliverable (i.e. whether the domain resolves)
12.0
Jan Verbeek 5 years ago
committed by Abraham Anes
parent
commit
b5e9f08ddc
No known key found for this signature in database GPG Key ID: 1865EBA8F25B5188
  1. 19
      partner_email_check/README.rst
  2. 7
      partner_email_check/__manifest__.py
  3. 1
      partner_email_check/models/__init__.py
  4. 42
      partner_email_check/models/res_config_settings.py
  5. 85
      partner_email_check/models/res_partner.py
  6. 126
      partner_email_check/tests/test_partner_email_check.py
  7. 39
      partner_email_check/views/base_config_view.xml

19
partner_email_check/README.rst

@ -6,12 +6,27 @@
Partner Email Check Partner Email Check
=================== ===================
This module validate the field ``email`` in the module ``res.partner``.
This module validates and normalizes the field ``email`` in the model
``res.partner``.
As part of the normalization, email addresses are converted to lowercase.
Optionally, multiple partners can not be allowed to have the same address.
This will not work with multiple comma-separated email addresses in the field,
although validation and normalization are still supported in such cases.
Configuration Configuration
============= =============
Install python package validate_email: ``sudo pip install validate_email``.
Install python package email-validator: ``sudo pip install email-validator``.
To not allow multiple partners to have the same email address, use the
"Filter duplicate email addresses"/``partner_email_check_filter_duplicates``
setting.
To validate that email addresses are deliverable (that the hostname exists),
use the "Check deliverability of email addresses"/``partner_email_check_check_deliverability``
setting.
Usage Usage
===== =====

7
partner_email_check/__manifest__.py

@ -8,11 +8,14 @@
'author': "Komit, Odoo Community Association (OCA)", 'author': "Komit, Odoo Community Association (OCA)",
'website': 'https://github.com/OCA/partner-contact', 'website': 'https://github.com/OCA/partner-contact',
'category': 'Tools', 'category': 'Tools',
'depends': ['base'],
'depends': ['base_setup'],
'installable': True, 'installable': True,
'application': False, 'application': False,
'license': 'AGPL-3', 'license': 'AGPL-3',
'external_dependencies': { 'external_dependencies': {
'python': ['validate_email']
'python': ['email_validator']
}, },
'data': [
'views/base_config_view.xml',
]
} }

1
partner_email_check/models/__init__.py

@ -1,3 +1,4 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import res_config_settings
from . import res_partner from . import res_partner

42
partner_email_check/models/res_config_settings.py

@ -0,0 +1,42 @@
from odoo import api, fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
partner_email_check_filter_duplicates = fields.Boolean(
string="Filter duplicate partner email addresses",
help="Don't allow multiple partners to have the same email address.",
)
partner_email_check_check_deliverability = fields.Boolean(
string="Check deliverability of email addresses",
help="Don't allow email addresses with providers that don't exist",
)
@api.model
def get_values(self):
res = super(ResConfigSettings, self).get_values()
conf = self.env['ir.config_parameter'].sudo()
res.update(
partner_email_check_filter_duplicates=conf.get_param(
'partner_email_check_filter_duplicates', 'False'
) == 'True',
partner_email_check_check_deliverability=conf.get_param(
'partner_email_check_check_deliverability', 'False'
) == 'True',
)
return res
@api.multi
def set_values(self):
super(ResConfigSettings, self).set_values()
conf = self.env['ir.config_parameter'].sudo()
conf.set_param(
'partner_email_check_filter_duplicates',
self.partner_email_check_filter_duplicates
)
conf.set_param(
'partner_email_check_check_deliverability',
self.partner_email_check_check_deliverability
)

85
partner_email_check/models/res_partner.py

@ -3,32 +3,89 @@
import logging import logging
from odoo import api, models, _ from odoo import api, models, _
from odoo.exceptions import UserError
from odoo.exceptions import UserError, ValidationError
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
try: try:
from validate_email import validate_email
from email_validator import (
validate_email,
EmailSyntaxError,
EmailUndeliverableError,
)
except ImportError: except ImportError:
_logger.debug('Cannot import "validate_email".')
_logger.debug('Cannot import "email_validator".')
def validate_email(email):
_logger.warning(
'Can not validate email, '
'python dependency required "validate_email"')
return True
validate_email = None
class ResPartner(models.Model): class ResPartner(models.Model):
_inherit = 'res.partner' _inherit = 'res.partner'
@api.model
def email_check(self, emails):
return ','.join(self._normalize_email(email.strip())
for email in emails.split(','))
@api.constrains('email') @api.constrains('email')
def constrains_email(self):
def _check_email_unique(self):
if self._should_filter_duplicates():
for rec in self.filtered("email"): for rec in self.filtered("email"):
self.email_check(rec.email)
if ',' in rec.email:
raise UserError(
_("Field contains multiple email addresses. This is "
"not supported when duplicate email addresses are "
"not allowed.")
)
if self.search_count(
[('email', '=', rec.email), ('id', '!=', rec.id)]
):
raise UserError(
_("Email '%s' is already in use.") % rec.email.strip()
)
def _normalize_email(self, email):
if validate_email is None:
_logger.warning(
'Can not validate email, '
'python dependency required "email_validator"')
return email
try:
result = validate_email(
email,
check_deliverability=self._should_check_deliverability(),
)
except EmailSyntaxError:
raise ValidationError(
_("%s is an invalid email") % email.strip()
)
except EmailUndeliverableError:
raise ValidationError(
_("Cannot deliver to email address %s") % email.strip()
)
return result['local'].lower() + '@' + result['domain_i18n']
def _should_filter_duplicates(self):
conf = self.env['ir.config_parameter'].get_param(
'partner_email_check_filter_duplicates', 'False'
)
return conf == 'True'
def _should_check_deliverability(self):
conf = self.env['ir.config_parameter'].get_param(
'partner_email_check_check_deliverability', 'False'
)
return conf == 'True'
@api.model @api.model
def email_check(self, email):
if validate_email(email):
return True
raise UserError(_('Invalid e-mail!'))
def create(self, vals):
if vals.get('email'):
vals['email'] = self.email_check(vals['email'])
return super(ResPartner, self).create(vals)
@api.multi
def write(self, vals):
if vals.get('email'):
vals['email'] = self.email_check(vals['email'])
return super(ResPartner, self).write(vals)

126
partner_email_check/tests/test_partner_email_check.py

@ -1,8 +1,11 @@
# Copyright 2019 Komit <https://komit-consulting.com> # Copyright 2019 Komit <https://komit-consulting.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from unittest.mock import patch
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
from odoo.tests.common import TransactionCase from odoo.tests.common import TransactionCase
from odoo.tools.misc import mute_logger
class TestPartnerEmailCheck(TransactionCase): class TestPartnerEmailCheck(TransactionCase):
@ -11,6 +14,10 @@ class TestPartnerEmailCheck(TransactionCase):
self.test_partner = self.env['res.partner'].create({ self.test_partner = self.env['res.partner'].create({
'name': 'test', 'name': 'test',
}) })
self.wizard = self.env['res.config.settings'].create({})
self.wizard.partner_email_check_filter_duplicates = False
self.wizard.partner_email_check_check_deliverability = False
self.wizard.set_values()
def test_bad_email(self): def test_bad_email(self):
"""Test rejection of bad emails.""" """Test rejection of bad emails."""
@ -21,3 +28,122 @@ class TestPartnerEmailCheck(TransactionCase):
"""Test acceptance of good""" """Test acceptance of good"""
self.test_partner.email = 'goodemail@domain.com' self.test_partner.email = 'goodemail@domain.com'
self.assertTrue(self.test_partner.email) self.assertTrue(self.test_partner.email)
def test_bad_emails(self):
"""Test rejection of bad emails."""
with self.assertRaises(ValidationError):
self.test_partner.email = 'good@domain.com,bad@email@domain..com'
def test_good_emails(self):
"""Test acceptance of good"""
self.test_partner.email = 'goodemail@domain.com,goodemail2@domain.com'
self.assertTrue(self.test_partner.email)
def test_email_domain_normalization(self):
"""Test normalization of email domain names, including punycode."""
self.test_partner.write({'email': 'goodemail@xn--xamPle-9ua.com'})
self.assertEqual(self.test_partner.email, u'goodemail@éxample.com')
def test_multi_email_domain_normalization(self):
"""Test normalization of email domain names of multiple addresses."""
self.test_partner.write({
'email': 'goodemail@doMAIN.com,othergood@xn--xample-9ua.com'
})
self.assertEqual(
self.test_partner.email,
u'goodemail@domain.com,othergood@éxample.com'
)
def test_email_local_normalization(self):
"""Test normalization of the local part of email addresses."""
self.test_partner.write({'email': 'Me@mail.org'})
# .lower() is locale-dependent, so don't hardcode the result
self.assertEqual(self.test_partner.email, 'Me'.lower() + '@mail.org')
def test_multi_email_local_normalization(self):
"""Test normalization of the local part of multiple addresses."""
self.test_partner.write({'email': 'You@mAiL.net,mE@mail.com'})
self.assertEqual(
self.test_partner.email,
'You'.lower() + '@mail.net,' + 'mE'.lower() + '@mail.com'
)
def disallow_duplicates(self):
self.wizard.partner_email_check_filter_duplicates = True
self.wizard.set_values()
def test_duplicate_addresses_disallowed(self):
self.disallow_duplicates()
self.test_partner.write({'email': 'email@domain.tld'})
with self.assertRaises(ValidationError):
self.env['res.partner'].create({
'name': 'alsotest',
'email': 'email@domain.tld'
})
def test_duplicate_after_normalization_addresses_disallowed(self):
self.disallow_duplicates()
self.env['res.partner'].create({
'name': 'alsotest',
'email': 'email@doMAIN.tld'
})
with self.assertRaises(ValidationError):
self.test_partner.email = 'email@domain.tld'
def test_multiple_addresses_disallowed_when_duplicates_filtered(self):
self.disallow_duplicates()
with self.assertRaises(ValidationError):
self.test_partner.email = 'foo@bar.org,email@domain.tld'
def test_duplicate_addresses_allowed_by_default(self):
self.env['res.partner'].create({
'name': 'alsotest',
'email': 'email@domain.tld',
})
self.test_partner.email = 'email@domain.tld'
def check_deliverability(self):
self.wizard.partner_email_check_check_deliverability = True
self.wizard.set_values()
def test_deliverable_addresses_allowed(self):
self.check_deliverability()
# We only need a resolving domain, not a real user
self.test_partner.email = 'gooddomain-icraglusrk@gmail.com'
self.assertTrue(self.test_partner.email)
def test_nondeliverable_addresses_not_allowed(self):
self.check_deliverability()
with self.assertRaises(ValidationError):
# This domain may resolve by mistake on certain network setups
# At least until a new version of email-validator is released
# See https://github.com/JoshData/python-email-validator/pull/30
self.test_partner.email = 'cezrik@acoa.nrdkt'
def test_config_getters(self):
other_wizard = self.env['res.config.settings'].create({})
self.assertFalse(other_wizard.partner_email_check_check_deliverability)
self.assertFalse(other_wizard.partner_email_check_filter_duplicates)
self.disallow_duplicates()
self.check_deliverability()
other_wizard = self.env['res.config.settings'].create({})
self.assertTrue(other_wizard.partner_email_check_check_deliverability)
self.assertTrue(other_wizard.partner_email_check_filter_duplicates)
@mute_logger('odoo.addons.partner_email_check.models.res_partner')
def test_lacking_dependency_does_not_halt_execution(self):
with patch('odoo.addons.partner_email_check.models.res_partner.'
'validate_email', None):
self.test_partner.email = 'notatallvalid@@domain'
@mute_logger('odoo.addons.partner_email_check.models.res_partner')
def test_lacking_dependency_keeps_uniqueness_constraint_working(self):
self.disallow_duplicates()
with patch('odoo.addons.partner_email_check.models.res_partner.'
'validate_email', None):
self.env['res.partner'].create({
'name': 'alsotest',
'email': 'email@domain.tld'
})
with self.assertRaises(ValidationError):
self.test_partner.email = 'email@domain.tld'

39
partner_email_check/views/base_config_view.xml

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_general_configuration" model="ir.ui.view">
<field name="name">partner_email_check</field>
<field name="model">res.config.settings</field>
<field name="inherit_id"
ref="base_setup.res_config_settings_view_form" />
<field name="arch" type="xml">
<xpath expr="//div[@name='multi_company']" position='after'>
<h2>Email validation</h2>
<div class="row mt16 o_settings_container"
name="partner_email_check">
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="partner_email_check_filter_duplicates" />
</div>
<div class="o_setting_right_pane">
<label for="partner_email_check_filter_duplicates" />
<div class="text-muted">
Require partner email addresses to be unique
</div>
</div>
</div>
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="partner_email_check_check_deliverability" />
</div>
<div class="o_setting_right_pane">
<label for="partner_email_check_check_deliverability" />
<div class="text-muted">
Ensure that partner email addresses can be delivered to
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>
Loading…
Cancel
Save