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()