from email import errors as email_errors from email import headerregistry as email_reg from email import utils as email_utils from django.core import exceptions, validators class CourrielError(Exception): """ Something went wrong and you'll have to take a guess """ class InvalidAddressError(exceptions.ValidationError, CourrielError): """ Failed to parse an address definition """ class InvalidRecipientsError(CourrielError): """ Can't be send to recipients """ class InvalidSenderError(CourrielError): """ Missing or invalid sender """ class InvalidGabarit(CourrielError): """ Something is wrong with the template """ class InvalidFrontMatter(InvalidGabarit): """ Failed to parse template front matter """ class MarkdownEmailError(InvalidGabarit): """ Error in markdown email or template loading """ class UnresolvedVariableError(CourrielError): """ A template variable could not be resolved """ class ValidAddress(email_reg.Address): """ extends email.headerregistry.Address so it can be used where django expect an email address. Except it should never be invalid """ def __new__(cls, display_name='', username='', domain='', addr_spec=None): """ Builds a ValidAddress from either an Address or a string, or raise InvalidAddressError """ try: spec = addr_spec or f"{username}@{domain}" cls._validate_spec(spec) instance = super().__new__(cls) return instance except email_errors.InvalidHeaderDefect as e: raise InvalidAddressError(f"{spec}") from e @classmethod def _validate_spec(cls, spec): try: validators.validate_email(spec) except exceptions.ValidationError as e: raise InvalidAddressError(f"{spec}") from e @classmethod def factory(cls, input) -> 'ValidAddress': """ ensure Address instance from str, None or Address """ if isinstance(input, ValidAddress): return input if isinstance(input, email_reg.Address): # converting to subclass return ValidAddress(display_name=input.display_name, username=input.username, domain=input.domain) if input is None: return ValidAddress(display_name='') if isinstance(input, str): try: name, spec = email_utils.parseaddr(input) return cls(name, addr_spec=spec) except email_errors.InvalidHeaderDefect: # parsing failed and may have split username, let's keep full input as display name return cls(display_name=input) raise TypeError(f"Cannot create ValidAddress from {type(input).__name__}") @property def email(self): """ shorthand for addr_spec""" return self.addr_spec class ValidAddressList(list): """ only accepts valid addresses """ def _validate(self, value): """ converts to ValidAddress, may raise InvalidAddress """ return ValidAddress.factory(value) def __init__(self, iterable=()): validated = [self._validate(i) for i in iterable] super().__init__(validated) def append(self, item): super().append(self._validate(item)) def insert(self, index, item): super().insert(index, self._validate(item)) def extend(self, items): for item in items: super().append(self._validate(item)) def __setitem__(self, index, item): super().__setitem__(index, self._validate(item))