diff --git a/modules/marketing_email/exceptions.py b/modules/marketing_email/exceptions.py
index 4296c57e58..0355d79c06 100644
--- a/modules/marketing_email/exceptions.py
+++ b/modules/marketing_email/exceptions.py
@@ -6,3 +6,7 @@ from trytond.model.exceptions import ValidationError
class TemplateError(ValidationError):
pass
+
+
+class EMailValidationError(ValidationError):
+ pass
diff --git a/modules/marketing_email/marketing.py b/modules/marketing_email/marketing.py
index d1cb4a8bdf..a47e869fc5 100644
--- a/modules/marketing_email/marketing.py
+++ b/modules/marketing_email/marketing.py
@@ -31,12 +31,13 @@ from trytond.pyson import Eval
from trytond.report import Report, get_email
from trytond.sendmail import SMTPDataManager, sendmail_transactional
from trytond.tools import grouped_slice, reduce_ids
-from trytond.tools.email_ import convert_ascii_email, set_from_header
+from trytond.tools.email_ import (
+ EmailNotValidError, convert_ascii_email, set_from_header, validate_email)
from trytond.transaction import Transaction, inactive_records
from trytond.url import http_host
from trytond.wizard import Button, StateTransition, StateView, Wizard
-from .exceptions import TemplateError
+from .exceptions import EMailValidationError, TemplateError
if not config.get(
'html', 'plugins-marketing.email.message-content'):
@@ -145,6 +146,26 @@ class Email(DeactivableMixin, ModelSQL, ModelView):
records = sum(args[0:None:2], [])
cls._format_email(records)
+ @classmethod
+ def validate_fields(cls, records, fields_names):
+ super().validate_fields(records, fields_names)
+ cls.check_valid_email(records, fields_names)
+
+ @classmethod
+ def check_valid_email(cls, records, fields_names=None):
+ if fields_names and 'email' not in fields_names:
+ return
+ for record in records:
+ if record.email:
+ try:
+ validate_email(record.email)
+ except EmailNotValidError as e:
+ raise EMailValidationError(gettext(
+ 'marketing_email.msg_email_invalid',
+ record=record.rec_name,
+ email=record.email),
+ str(e)) from e
+
def get_email_subscribe(self, report_name='marketing.email.subscribe'):
pool = Pool()
ActionReport = pool.get('ir.action.report')
diff --git a/modules/marketing_email/message.xml b/modules/marketing_email/message.xml
index 5c4336b70c..a631826e0f 100644
--- a/modules/marketing_email/message.xml
+++ b/modules/marketing_email/message.xml
@@ -10,5 +10,8 @@ this repository contains the full copyright notices and license terms. -->
Invalid content in message "%(message)s" with exception "%(exception)s".
+
+ The email address "%(email)s" for "%(record)s" is not valid.
+
diff --git a/modules/party/contact_mechanism.py b/modules/party/contact_mechanism.py
index 64da90d810..8f43f43109 100644
--- a/modules/party/contact_mechanism.py
+++ b/modules/party/contact_mechanism.py
@@ -14,9 +14,10 @@ from trytond.model import (
fields, sequence_ordered)
from trytond.model.exceptions import AccessError
from trytond.pyson import Eval
+from trytond.tools.email_ import EmailNotValidError, validate_email
from trytond.transaction import Transaction
-from .exceptions import InvalidPhoneNumber
+from .exceptions import InvalidEMail, InvalidPhoneNumber
_TYPES = [
('phone', 'Phone'),
@@ -306,6 +307,7 @@ class ContactMechanism(
def validate_fields(cls, mechanisms, field_names):
super().validate_fields(mechanisms, field_names)
cls.check_valid_phonenumber(mechanisms, field_names)
+ cls.check_valid_email(mechanisms, field_names)
@classmethod
def check_valid_phonenumber(cls, mechanisms, field_names=None):
@@ -321,6 +323,21 @@ class ContactMechanism(
gettext('party.msg_invalid_phone_number',
phone=mechanism.value, party=mechanism.party.rec_name))
+ @classmethod
+ def check_valid_email(cls, mechanisms, field_names=None):
+ if field_names and not (field_names & {'type', 'value'}):
+ return
+ for mechanism in mechanisms:
+ if mechanism.type == 'email' and mechanism.value:
+ try:
+ validate_email(mechanism.value)
+ except EmailNotValidError as e:
+ raise InvalidEMail(gettext(
+ 'party.msg_email_invalid',
+ email=mechanism.value,
+ party=mechanism.party.rec_name),
+ str(e)) from e
+
@classmethod
def usages(cls, _fields=None):
"Returns the selection list of usage"
diff --git a/modules/party/exceptions.py b/modules/party/exceptions.py
index c760365e45..83df598460 100644
--- a/modules/party/exceptions.py
+++ b/modules/party/exceptions.py
@@ -12,6 +12,10 @@ class InvalidPhoneNumber(ValidationError):
pass
+class InvalidEMail(ValidationError):
+ pass
+
+
class VIESUnavailable(UserError):
pass
diff --git a/modules/party/message.xml b/modules/party/message.xml
index 4340effe2d..1642423fc0 100644
--- a/modules/party/message.xml
+++ b/modules/party/message.xml
@@ -15,6 +15,9 @@ this repository contains the full copyright notices and license terms. -->
The phone number "%(phone)s" for party "%(party)s" is not valid.
+
+ The email address "%(email)s" for party "%(party)s" is not valid.
+
The %(type)s "%(code)s" for party "%(party)s" is not valid.
diff --git a/modules/sale_gift_card/exceptions.py b/modules/sale_gift_card/exceptions.py
index 7f094879d9..a4596a969f 100644
--- a/modules/sale_gift_card/exceptions.py
+++ b/modules/sale_gift_card/exceptions.py
@@ -2,7 +2,7 @@
# this repository contains the full copyright notices and license terms.
from trytond.model.exceptions import (
- RequiredValidationError, SizeValidationError)
+ RequiredValidationError, SizeValidationError, ValidationError)
from trytond.modules.product.exceptions import TemplateValidationError
@@ -13,3 +13,7 @@ class GiftCardValidationError(TemplateValidationError):
class MoveGiftCardValidationError(
RequiredValidationError, SizeValidationError):
pass
+
+
+class GiftCardLineValidationError(ValidationError):
+ pass
diff --git a/modules/sale_gift_card/message.xml b/modules/sale_gift_card/message.xml
index 022ecb0d4f..c78ec492a1 100644
--- a/modules/sale_gift_card/message.xml
+++ b/modules/sale_gift_card/message.xml
@@ -12,5 +12,8 @@ this repository contains the full copyright notices and license terms. -->
To do the move "%(move)s", you must use exactly %(quantity)d gift card(s).
+
+ The email address "%(email)s" for "%(card)s" is not valid.
+
diff --git a/modules/sale_gift_card/sale.py b/modules/sale_gift_card/sale.py
index 951fd256e0..1e4913a7e9 100644
--- a/modules/sale_gift_card/sale.py
+++ b/modules/sale_gift_card/sale.py
@@ -7,6 +7,7 @@ from sql.functions import CharLength
from sql.operators import Equal
from trytond.config import config
+from trytond.i18n import gettext
from trytond.model import Exclude, ModelSQL, ModelView, Workflow, fields
from trytond.modules.company.model import CompanyValueMixin
from trytond.modules.currency.fields import Monetary
@@ -14,10 +15,13 @@ from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id
from trytond.report import Report, get_email
from trytond.sendmail import sendmail_transactional
-from trytond.tools.email_ import set_from_header
+from trytond.tools.email_ import (
+ EmailNotValidError, set_from_header, validate_email)
from trytond.transaction import Transaction
from trytond.wizard import Button, StateTransition, StateView
+from .exceptions import GiftCardLineValidationError
+
class Configuration(metaclass=PoolMeta):
__name__ = 'sale.configuration'
@@ -460,6 +464,26 @@ class _LineMixin:
cards.append(card)
return cards
+ @classmethod
+ def validate_fields(cls, cards, fields_names):
+ super().validate_fields(cards, fields_names)
+ cls.check_valid_email(cards, fields_names)
+
+ @classmethod
+ def check_valid_email(cls, cards, fields_names=None):
+ if fields_names and 'email' not in fields_names:
+ return
+ for card in cards:
+ if card.gift_card_email:
+ try:
+ validate_email(card.gift_card_email)
+ except EmailNotValidError as e:
+ raise GiftCardLineValidationError(gettext(
+ 'sale_gift_card.msg_gift_card_email_invalid',
+ card=card.rec_name,
+ email=card.gift_card_email),
+ str(e)) from e
+
class Line(_LineMixin, metaclass=PoolMeta):
__name__ = 'sale.line'
diff --git a/modules/web_user/exceptions.py b/modules/web_user/exceptions.py
new file mode 100644
index 0000000000..9a4e133182
--- /dev/null
+++ b/modules/web_user/exceptions.py
@@ -0,0 +1,8 @@
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+
+from trytond.model.exceptions import ValidationError
+
+
+class UserValidationError(ValidationError):
+ pass
diff --git a/modules/web_user/message.xml b/modules/web_user/message.xml
index 809c5f6574..9a1a098ee2 100644
--- a/modules/web_user/message.xml
+++ b/modules/web_user/message.xml
@@ -9,5 +9,8 @@ this repository contains the full copyright notices and license terms. -->
Web user session key must be unique.
+
+ The email address "%(email)s" for "%(user)s" is not valid.
+
diff --git a/modules/web_user/user.py b/modules/web_user/user.py
index 549f96a4af..6e7f899051 100644
--- a/modules/web_user/user.py
+++ b/modules/web_user/user.py
@@ -26,6 +26,7 @@ from sql.operators import Equal
from trytond.config import config
from trytond.exceptions import RateLimitException
+from trytond.i18n import gettext
from trytond.model import (
DeactivableMixin, Exclude, Index, ModelSQL, ModelView, Unique,
avatar_mixin, fields)
@@ -34,9 +35,12 @@ from trytond.pyson import Eval
from trytond.report import Report, get_email
from trytond.res.user import CRYPT_CONTEXT, LoginAttempt
from trytond.sendmail import sendmail_transactional
-from trytond.tools.email_ import set_from_header
+from trytond.tools.email_ import (
+ EmailNotValidError, set_from_header, validate_email)
from trytond.transaction import Transaction
+from .exceptions import UserValidationError
+
logger = logging.getLogger(__name__)
@@ -168,6 +172,26 @@ class User(avatar_mixin(100), DeactivableMixin, ModelSQL, ModelView):
users = sum(args[0:None:2], [])
cls._format_email(users)
+ @classmethod
+ def validate_fields(cls, users, fields_names):
+ super().validate_fields(users, fields_names)
+ cls.check_valid_email(users, fields_names)
+
+ @classmethod
+ def check_valid_email(cls, users, fields_names=None):
+ if fields_names and 'email' not in fields_names:
+ return
+ for user in users:
+ if user.email:
+ try:
+ validate_email(user.email)
+ except EmailNotValidError as e:
+ raise UserValidationError(gettext(
+ 'web_user.msg_email_invalid',
+ user=user.rec_name,
+ email=user.email),
+ str(e)) from e
+
@classmethod
def authenticate(cls, email, password):
pool = Pool()
diff --git a/trytond/trytond/res/exceptions.py b/trytond/trytond/res/exceptions.py
index ca145f3be1..c8344ce98a 100644
--- a/trytond/trytond/res/exceptions.py
+++ b/trytond/trytond/res/exceptions.py
@@ -1,9 +1,10 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
-from .user import DeleteError, PasswordError
+from .user import DeleteError, PasswordError, UserValidationError
__all__ = [
PasswordError,
DeleteError,
+ UserValidationError,
]
diff --git a/trytond/trytond/res/message.xml b/trytond/trytond/res/message.xml
index aa1ac449a5..ab619f7820 100644
--- a/trytond/trytond/res/message.xml
+++ b/trytond/trytond/res/message.xml
@@ -18,6 +18,9 @@ this repository contains the full copyright notices and license terms. -->
The password cannot be the same as user's email address.
+
+ The email address "%(email)s" for "%(user)s" is not valid.
+
For logging purposes users cannot be deleted, instead they should be deactivated.
diff --git a/trytond/trytond/res/user.py b/trytond/trytond/res/user.py
index 0ffcb5a7ca..be59e02cdd 100644
--- a/trytond/trytond/res/user.py
+++ b/trytond/trytond/res/user.py
@@ -47,13 +47,15 @@ from trytond.i18n import gettext
from trytond.model import (
DeactivableMixin, Index, ModelSQL, ModelView, Unique, Workflow,
avatar_mixin, fields)
+from trytond.model.exceptions import ValidationError
from trytond.pool import Pool
from trytond.pyson import Bool, Eval, PYSONEncoder
from trytond.report import Report, get_email
from trytond.rpc import RPC
from trytond.sendmail import sendmail_transactional
from trytond.tools import grouped_slice
-from trytond.tools.email_ import set_from_header
+from trytond.tools.email_ import (
+ EmailNotValidError, set_from_header, validate_email)
from trytond.transaction import Transaction
from trytond.url import host, http_host
from trytond.wizard import Button, StateTransition, StateView, Wizard
@@ -106,6 +108,10 @@ class DeleteError(UserError):
pass
+class UserValidationError(ValidationError):
+ pass
+
+
class User(avatar_mixin(100, 'login'), DeactivableMixin, ModelSQL, ModelView):
"User"
__name__ = "res.user"
@@ -454,6 +460,26 @@ class User(avatar_mixin(100, 'login'), DeactivableMixin, ModelSQL, ModelView):
new_users.append(new_user)
return new_users
+ @classmethod
+ def validate_fields(cls, users, fields_names):
+ super().validate_fields(users, fields_names)
+ cls.check_valid_email(users, fields_names)
+
+ @classmethod
+ def check_valid_email(cls, users, fields_names=None):
+ if fields_names and 'email' not in fields_names:
+ return
+ for user in users:
+ if user.email:
+ try:
+ validate_email(user.email)
+ except EmailNotValidError as e:
+ raise UserValidationError(gettext(
+ 'res.msg_email_invalid',
+ user=user.rec_name,
+ email=user.email),
+ str(e)) from e
+
@classmethod
def _get_preferences(cls, user, context_only=False):
pool = Pool()