from __future__ import annotations import json from dataclasses import asdict, dataclass from typing import Optional import itsdangerous from app import config from app.log import LOG from app.models import User, AliasOptions, SLDomain signer = itsdangerous.TimestampSigner(config.CUSTOM_ALIAS_SECRET) @dataclass class AliasSuffix: # whether this is a custom domain is_custom: bool # Suffix suffix: str # Suffix signature signed_suffix: str # whether this is a premium SL domain. Not apply to custom domain is_premium: bool # can be either Custom or SL domain domain: str # if custom domain, whether the custom domain has MX verified, i.e. can receive emails mx_verified: bool = True def serialize(self): return json.dumps(asdict(self)) @classmethod def deserialize(cls, data: str) -> AliasSuffix: return AliasSuffix(**json.loads(data)) def check_suffix_signature(signed_suffix: str) -> Optional[str]: # hypothesis: user will click on the button in the 600 secs try: return signer.unsign(signed_suffix, max_age=600).decode() except itsdangerous.BadSignature: return None def verify_prefix_suffix( user: User, alias_prefix, alias_suffix, alias_options: Optional[AliasOptions] = None ) -> bool: """verify if user could create an alias with the given prefix and suffix""" if not alias_prefix or not alias_suffix: # should be caught on frontend return False user_custom_domains = [cd.domain for cd in user.verified_custom_domains()] # make sure alias_suffix is either .random_word@simplelogin.co or @my-domain.com alias_suffix = alias_suffix.strip() # alias_domain_prefix is either a .random_word or "" alias_domain_prefix, alias_domain = alias_suffix.split("@", 1) # alias_domain must be either one of user custom domains or built-in domains if alias_domain not in user.available_alias_domains(alias_options=alias_options): LOG.e("wrong alias suffix %s, user %s", alias_suffix, user) return False # SimpleLogin domain case: # 1) alias_suffix must start with "." and # 2) alias_domain_prefix must come from the word list if ( alias_domain in user.available_sl_domains(alias_options=alias_options) and alias_domain not in user_custom_domains # when DISABLE_ALIAS_SUFFIX is true, alias_domain_prefix is empty and not config.DISABLE_ALIAS_SUFFIX ): if not alias_domain_prefix.startswith("."): LOG.e("User %s submits a wrong alias suffix %s", user, alias_suffix) return False else: if alias_domain not in user_custom_domains: if not config.DISABLE_ALIAS_SUFFIX: LOG.e("wrong alias suffix %s, user %s", alias_suffix, user) return False if alias_domain not in user.available_sl_domains( alias_options=alias_options ): LOG.e("wrong alias suffix %s, user %s", alias_suffix, user) return False return True def get_alias_suffixes( user: User, alias_options: Optional[AliasOptions] = None ) -> [AliasSuffix]: """ Similar to as get_available_suffixes() but also return custom domain that doesn't have MX set up. """ user_custom_domains = user.verified_custom_domains() alias_suffixes: [AliasSuffix] = [] # put custom domain first # for each user domain, generate both the domain and a random suffix version for custom_domain in user_custom_domains: if custom_domain.random_prefix_generation: suffix = ( f".{user.get_random_alias_suffix(custom_domain)}@{custom_domain.domain}" ) alias_suffix = AliasSuffix( is_custom=True, suffix=suffix, signed_suffix=signer.sign(suffix).decode(), is_premium=False, domain=custom_domain.domain, mx_verified=custom_domain.verified, ) if user.default_alias_custom_domain_id == custom_domain.id: alias_suffixes.insert(0, alias_suffix) else: alias_suffixes.append(alias_suffix) suffix = f"@{custom_domain.domain}" alias_suffix = AliasSuffix( is_custom=True, suffix=suffix, signed_suffix=signer.sign(suffix).decode(), is_premium=False, domain=custom_domain.domain, mx_verified=custom_domain.verified, ) # put the default domain to top # only if random_prefix_generation isn't enabled if ( user.default_alias_custom_domain_id == custom_domain.id and not custom_domain.random_prefix_generation ): alias_suffixes.insert(0, alias_suffix) else: alias_suffixes.append(alias_suffix) # then SimpleLogin domain sl_domains = user.get_sl_domains(alias_options=alias_options) default_domain_found = False for sl_domain in sl_domains: prefix = ( "" if config.DISABLE_ALIAS_SUFFIX else f".{user.get_random_alias_suffix()}" ) suffix = f"{prefix}@{sl_domain.domain}" alias_suffix = AliasSuffix( is_custom=False, suffix=suffix, signed_suffix=signer.sign(suffix).decode(), is_premium=sl_domain.premium_only, domain=sl_domain.domain, mx_verified=True, ) # No default or this is not the default if ( user.default_alias_public_domain_id is None or user.default_alias_public_domain_id != sl_domain.id ): alias_suffixes.append(alias_suffix) # If no default domain mark it as found default_domain_found = user.default_alias_public_domain_id is None else: default_domain_found = True alias_suffixes.insert(0, alias_suffix) if not default_domain_found: domain_conditions = {"id": user.default_alias_public_domain_id, "hidden": False} if not user.is_premium(): domain_conditions["premium_only"] = False sl_domain = SLDomain.get_by(**domain_conditions) if sl_domain: prefix = ( "" if config.DISABLE_ALIAS_SUFFIX else f".{user.get_random_alias_suffix()}" ) suffix = f"{prefix}@{sl_domain.domain}" alias_suffix = AliasSuffix( is_custom=False, suffix=suffix, signed_suffix=signer.sign(suffix).decode(), is_premium=sl_domain.premium_only, domain=sl_domain.domain, mx_verified=True, ) alias_suffixes.insert(0, alias_suffix) return alias_suffixes