diff --git a/app/config.py b/app/config.py index 2abd8e22..a071b73c 100644 --- a/app/config.py +++ b/app/config.py @@ -385,6 +385,8 @@ ALIAS_LIMIT = os.environ.get("ALIAS_LIMIT") or "100/day;50/hour;5/minute" ENABLE_SPAM_ASSASSIN = "ENABLE_SPAM_ASSASSIN" in os.environ +ALIAS_RANDOM_SUFFIX_LENGTH = int(os.environ.get("ALIAS_RAND_SUFFIX_LENGTH", 5)) + try: HIBP_SCAN_INTERVAL_DAYS = int(os.environ.get("HIBP_SCAN_INTERVAL_DAYS")) except Exception: diff --git a/app/dashboard/templates/dashboard/setting.html b/app/dashboard/templates/dashboard/setting.html index 16322884..410bf7ef 100644 --- a/app/dashboard/templates/dashboard/setting.html +++ b/app/dashboard/templates/dashboard/setting.html @@ -247,6 +247,22 @@ + +
Select the default suffix generator for aliases.
+
+ + + +
+ diff --git a/app/dashboard/views/custom_alias.py b/app/dashboard/views/custom_alias.py index b17ac447..e6da8f4b 100644 --- a/app/dashboard/views/custom_alias.py +++ b/app/dashboard/views/custom_alias.py @@ -23,7 +23,7 @@ from app.models import ( AliasMailbox, DomainDeletedAlias, ) -from app.utils import random_word, word_exist +from app.utils import get_suffix signer = TimestampSigner(CUSTOM_ALIAS_SECRET) @@ -54,7 +54,7 @@ def get_available_suffixes(user: User) -> [SuffixInfo]: # 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 = "." + random_word() + "@" + custom_domain.domain + suffix = "." + get_suffix(user) + "@" + custom_domain.domain suffix_info = SuffixInfo(True, suffix, signer.sign(suffix).decode(), False) if user.default_alias_custom_domain_id == custom_domain.id: suffixes.insert(0, suffix_info) @@ -77,7 +77,7 @@ def get_available_suffixes(user: User) -> [SuffixInfo]: # then SimpleLogin domain for sl_domain in user.get_sl_domains(): suffix = ( - ("" if DISABLE_ALIAS_SUFFIX else "." + random_word()) + ("" if DISABLE_ALIAS_SUFFIX else "." + get_suffix(user)) + "@" + sl_domain.domain ) @@ -281,14 +281,6 @@ def verify_prefix_suffix(user: User, alias_prefix, alias_suffix) -> bool: LOG.exception("User %s submits a wrong alias suffix %s", user, alias_suffix) return False - random_word_part = alias_domain_prefix[1:] - if not word_exist(random_word_part): - LOG.exception( - "alias suffix %s needs to start with a random word, user %s", - alias_suffix, - user, - ) - return False else: if alias_domain not in user_custom_domains: if not DISABLE_ALIAS_SUFFIX: diff --git a/app/dashboard/views/setting.py b/app/dashboard/views/setting.py index b4b837d8..c8db8202 100644 --- a/app/dashboard/views/setting.py +++ b/app/dashboard/views/setting.py @@ -15,7 +15,12 @@ from wtforms import StringField, validators from wtforms.fields.html5 import EmailField from app import s3, email_utils -from app.config import URL, FIRST_ALIAS_DOMAIN, JOB_DELETE_ACCOUNT +from app.config import ( + URL, + FIRST_ALIAS_DOMAIN, + JOB_DELETE_ACCOUNT, + ALIAS_RANDOM_SUFFIX_LENGTH, +) from app.dashboard.base import dashboard_bp from app.email_utils import ( email_can_be_used_as_mailbox, @@ -32,6 +37,7 @@ from app.models import ( Alias, CustomDomain, AliasGeneratorEnum, + AliasSuffixEnum, ManualSubscription, SenderFormatEnum, SLDomain, @@ -240,6 +246,14 @@ def setting(): flash("Your preference has been updated", "success") return redirect(url_for("dashboard.setting")) + elif request.form.get("form-name") == "random-alias-suffix": + scheme = int(request.form.get("random-alias-suffix-generator")) + if AliasSuffixEnum.has_value(scheme): + current_user.random_alias_suffix = scheme + db.session.commit() + flash("Your preference has been updated", "success") + return redirect(url_for("dashboard.setting")) + elif request.form.get("form-name") == "change-sender-format": sender_format = int(request.form.get("sender-format")) if SenderFormatEnum.has_value(sender_format): @@ -292,6 +306,7 @@ def setting(): apple_sub=apple_sub, coinbase_sub=coinbase_sub, FIRST_ALIAS_DOMAIN=FIRST_ALIAS_DOMAIN, + ALIAS_RAND_SUFFIX_LENGTH=ALIAS_RANDOM_SUFFIX_LENGTH, ) diff --git a/app/models.py b/app/models.py index 2c34cd4f..3ca044cf 100644 --- a/app/models.py +++ b/app/models.py @@ -34,8 +34,8 @@ from app.utils import ( convert_to_id, random_string, random_words, - random_word, sanitize_email, + get_suffix, ) @@ -162,6 +162,11 @@ class AliasGeneratorEnum(EnumE): uuid = 2 # aliases are generated based on uuid +class AliasSuffixEnum(EnumE): + word = 0 # Random word from dictionary file + random_string = 1 # Completely random string + + class Hibp(db.Model, ModelMixin): __tablename__ = "hibp" name = db.Column(db.String(), nullable=False, unique=True, index=True) @@ -293,6 +298,16 @@ class User(db.Model, ModelMixin, UserMixin, PasswordOracle): db.Boolean, default=False, nullable=False, server_default="0" ) + # whether to use random string or random word as suffix + # Random word from dictionary file -> 0 + # Completely random string -> 1 + random_alias_suffix = db.Column( + db.Integer, + nullable=False, + default=AliasSuffixEnum.random_string.value, + server_default=str(AliasSuffixEnum.random_string.value), + ) + @classmethod def create(cls, email, name="", password=None, **kwargs): user: User = super(User, cls).create(email=email, name=name, **kwargs) @@ -1132,7 +1147,7 @@ class Alias(db.Model, ModelMixin): # find the right suffix - avoid infinite loop by running this at max 1000 times for i in range(1000): - suffix = random_word() + suffix = get_suffix(user) email = f"{prefix}.{suffix}@{FIRST_ALIAS_DOMAIN}" if not cls.get_by(email=email) and not DeletedAlias.get_by(email=email): diff --git a/app/utils.py b/app/utils.py index 8b716fc5..743c0dea 100644 --- a/app/utils.py +++ b/app/utils.py @@ -4,8 +4,9 @@ import urllib.parse from unidecode import unidecode -from .config import WORDS_FILE_PATH +from .config import WORDS_FILE_PATH, ALIAS_RANDOM_SUFFIX_LENGTH from .log import LOG +from .models import User, AliasSuffixEnum with open(WORDS_FILE_PATH) as f: LOG.d("load words file: %s", WORDS_FILE_PATH) @@ -16,6 +17,20 @@ def random_word(): return random.choice(_words) +def get_suffix(user: User) -> str: + """Get random suffix for an alias based on user's preference. + + Args: + user (User): the user who is trying to create an alias + + Returns: + str: the random suffix generated + """ + if user.random_alias_suffix == AliasSuffixEnum.random_string.value: + return random_string(ALIAS_RANDOM_SUFFIX_LENGTH, include_digits=True) + return random_word() + + def word_exist(word): return word in _words @@ -27,9 +42,12 @@ def random_words(): return "_".join([random.choice(_words) for i in range(nb_words)]) -def random_string(length=10): +def random_string(length=10, include_digits=False): """Generate a random string of fixed length """ letters = string.ascii_lowercase + if include_digits: + letters += string.digits + return "".join(random.choice(letters) for _ in range(length))