From 60ab45ceb2fd993dc13ac508c0d1ac711009e2a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Za=20F=C5=ABru=20=5B=E3=82=B6=E3=83=95=E3=83=BC=E3=83=AB?= =?UTF-8?q?=5D?= <40970016+zafuru@users.noreply.github.com> Date: Tue, 17 May 2022 00:21:26 -0400 Subject: [PATCH 01/15] Added TOTP value to AliasGeneratorEnum --- app/models.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/models.py b/app/models.py index e57a8d08f..1c995d3cd 100644 --- a/app/models.py +++ b/app/models.py @@ -1,6 +1,7 @@ import enum import os import random +import string import uuid from email.utils import formataddr from typing import List, Tuple, Optional @@ -216,6 +217,7 @@ class SenderFormatEnum(EnumE): class AliasGeneratorEnum(EnumE): word = 1 # aliases are generated based on random words uuid = 2 # aliases are generated based on uuid + totp = 3 # aliases are generated based on random characters class AliasSuffixEnum(EnumE): @@ -1188,8 +1190,11 @@ def generate_email( if scheme == AliasGeneratorEnum.uuid.value: name = uuid.uuid4().hex if in_hex else uuid.uuid4().__str__() random_email = name + "@" + alias_domain - else: + elif scheme == AliasGeneratorEnum.word.value: random_email = random_words() + "@" + alias_domain + else: + name = "".join(random.choices(string.digits + string.ascii_lowercase, k=6)) + random_email = name + "@" + alias_domain random_email = random_email.lower().strip() From 76bb7a3279efc78727e62f6215818a7f121d0574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Za=20F=C5=ABru=20=5B=E3=82=B6=E3=83=95=E3=83=BC=E3=83=AB?= =?UTF-8?q?=5D?= <40970016+zafuru@users.noreply.github.com> Date: Tue, 17 May 2022 00:22:05 -0400 Subject: [PATCH 02/15] Implemented TOTP alias generation --- app/api/views/new_random_alias.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/api/views/new_random_alias.py b/app/api/views/new_random_alias.py index a2af4b6c6..632805c93 100644 --- a/app/api/views/new_random_alias.py +++ b/app/api/views/new_random_alias.py @@ -98,8 +98,10 @@ def new_random_alias(): scheme = AliasGeneratorEnum.word.value elif mode == "uuid": scheme = AliasGeneratorEnum.uuid.value + elif mode == "totp": + scheme = AliasGeneratorEnum.totp.value else: - return jsonify(error=f"{mode} must be either word or uuid"), 400 + return jsonify(error=f"{mode} must be either word, uuid, or totp"), 400 alias = Alias.create_new_random(user=user, scheme=scheme, note=note) Session.commit() From dfe31fd8d0c8e256343b1bcb6c532fdd98ad0f4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Za=20F=C5=ABru=20=5B=E3=82=B6=E3=83=95=E3=83=BC=E3=83=AB?= =?UTF-8?q?=5D?= <40970016+zafuru@users.noreply.github.com> Date: Tue, 17 May 2022 00:22:30 -0400 Subject: [PATCH 03/15] Added TOTP alias selection to settings template --- templates/dashboard/setting.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/templates/dashboard/setting.html b/templates/dashboard/setting.html index 8e2d0ab21..d700b419b 100644 --- a/templates/dashboard/setting.html +++ b/templates/dashboard/setting.html @@ -266,6 +266,9 @@ + From 66f9c2932c83429f5356226d5c7204327ad16ad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Za=20F=C5=ABru=20=5B=E3=82=B6=E3=83=95=E3=83=BC=E3=83=AB?= =?UTF-8?q?=5D?= <40970016+zafuru@users.noreply.github.com> Date: Tue, 17 May 2022 00:26:51 -0400 Subject: [PATCH 04/15] Added a tiny extra detail to CONTRIBUTING.md --- CONTRIBUTING.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ebd12459e..7814e7dc3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,9 +64,10 @@ Install npm packages cd static && npm install ``` -To run the code locally, please create a local setting file based on `example.env`: +To run the code locally, go back to the root folder, and please create a local setting file based on `example.env`: ``` +cd .. cp example.env .env ``` From 49e8f68e5b197dd0e5013e1809cec30bef111b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Za=20F=C5=ABru=20=5B=E3=82=B6=E3=83=95=E3=83=BC=E3=83=AB?= =?UTF-8?q?=5D?= <40970016+zafuru@users.noreply.github.com> Date: Tue, 17 May 2022 11:43:29 -0400 Subject: [PATCH 05/15] Renamed new mode to "random_string" and increased keyspace to 9 --- app/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models.py b/app/models.py index 1c995d3cd..a66b6bf43 100644 --- a/app/models.py +++ b/app/models.py @@ -217,7 +217,7 @@ class SenderFormatEnum(EnumE): class AliasGeneratorEnum(EnumE): word = 1 # aliases are generated based on random words uuid = 2 # aliases are generated based on uuid - totp = 3 # aliases are generated based on random characters + random_string = 3 # aliases are generated based on a completely random string class AliasSuffixEnum(EnumE): @@ -1193,7 +1193,7 @@ def generate_email( elif scheme == AliasGeneratorEnum.word.value: random_email = random_words() + "@" + alias_domain else: - name = "".join(random.choices(string.digits + string.ascii_lowercase, k=6)) + name = "".join(random.choices(string.digits + string.ascii_lowercase, k=9)) random_email = name + "@" + alias_domain random_email = random_email.lower().strip() From 97e85a528bc2b66806d0506ef838e8d334b31dbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Za=20F=C5=ABru=20=5B=E3=82=B6=E3=83=95=E3=83=BC=E3=83=AB?= =?UTF-8?q?=5D?= <40970016+zafuru@users.noreply.github.com> Date: Tue, 17 May 2022 11:44:05 -0400 Subject: [PATCH 06/15] Renamed new mode to "random_string" --- app/api/views/new_random_alias.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/api/views/new_random_alias.py b/app/api/views/new_random_alias.py index 632805c93..6b064bba4 100644 --- a/app/api/views/new_random_alias.py +++ b/app/api/views/new_random_alias.py @@ -98,10 +98,15 @@ def new_random_alias(): scheme = AliasGeneratorEnum.word.value elif mode == "uuid": scheme = AliasGeneratorEnum.uuid.value - elif mode == "totp": - scheme = AliasGeneratorEnum.totp.value + elif mode == "random_string": + scheme = AliasGeneratorEnum.random_string.value else: - return jsonify(error=f"{mode} must be either word, uuid, or totp"), 400 + return ( + jsonify( + error=f"{mode} must be either word, uuid, or random_string" + ), + 400, + ) alias = Alias.create_new_random(user=user, scheme=scheme, note=note) Session.commit() From d583cae7aff4e997e7bfbe3456b0f988db68ff85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Za=20F=C5=ABru=20=5B=E3=82=B6=E3=83=95=E3=83=BC=E3=83=AB?= =?UTF-8?q?=5D?= <40970016+zafuru@users.noreply.github.com> Date: Tue, 17 May 2022 12:15:01 -0400 Subject: [PATCH 07/15] Modified test case for alias generator selection --- tests/api/test_setting.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/api/test_setting.py b/tests/api/test_setting.py index 3f545b3d2..29cab8c20 100644 --- a/tests/api/test_setting.py +++ b/tests/api/test_setting.py @@ -41,6 +41,10 @@ def test_update_settings_alias_generator(flask_client): assert r.status_code == 200 assert user.alias_generator == AliasGeneratorEnum.uuid.value + r = flask_client.patch("/api/setting", json={"alias_generator": "random_string"}) + assert r.status_code == 200 + assert user.alias_generator == AliasGeneratorEnum.random_string.value + def test_update_settings_random_alias_default_domain(flask_client): user = login(flask_client) From cb5e1b3e1d9481d5158bfbafc924b94762506e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Za=20F=C5=ABru=20=5B=E3=82=B6=E3=83=95=E3=83=BC=E3=83=AB?= =?UTF-8?q?=5D?= <40970016+zafuru@users.noreply.github.com> Date: Tue, 17 May 2022 12:16:17 -0400 Subject: [PATCH 08/15] Added a test case for random_string mode --- tests/api/test_new_random_alias.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/api/test_new_random_alias.py b/tests/api/test_new_random_alias.py index b8dbbdd7a..4a3ff6ba5 100644 --- a/tests/api/test_new_random_alias.py +++ b/tests/api/test_new_random_alias.py @@ -101,6 +101,32 @@ def test_custom_mode(flask_client): assert ge.note == "test note" +def test_random_string_mode(flask_client): + login(flask_client) + + # without note + r = flask_client.post( + url_for("api.new_random_alias", mode="random_string"), + ) + + assert r.status_code == 201 + # extract the uuid part + alias = r.json["alias"] + random_string_part: str = alias[: len(alias) - len(EMAIL_DOMAIN) - 1] + assert random_string_part.isalnum() + + # with note + r = flask_client.post( + url_for("api.new_random_alias", mode="random_string"), + json={"note": "test note"}, + ) + + assert r.status_code == 201 + alias = r.json["alias"] + ge = Alias.get_by(email=alias) + assert ge.note == "test note" + + def test_out_of_quota(flask_client): user = login(flask_client) user.trial_end = None From 09afba32ada25395c3d8269922a463c3f72169f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Za=20F=C5=ABru=20=5B=E3=82=B6=E3=83=95=E3=83=BC=E3=83=AB?= =?UTF-8?q?=5D?= <40970016+zafuru@users.noreply.github.com> Date: Tue, 17 May 2022 12:19:41 -0400 Subject: [PATCH 09/15] Added a test case for random_string mode --- tests/test_models.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_models.py b/tests/test_models.py index cc3c6b9fd..63057870f 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -8,6 +8,7 @@ from app.db import Session from app.email_utils import parse_full_address from app.models import ( + AliasGeneratorEnum, generate_email, Alias, Contact, @@ -32,6 +33,14 @@ def test_generate_email(flask_client): assert UUID(email_uuid.split("@")[0], version=4) +def test_generate_email_with_random_string(flask_client): + email = generate_email(scheme=AliasGeneratorEnum.random_string.value) + assert email.endswith("@" + EMAIL_DOMAIN) + + email_random_string = generate_email(scheme=3) + assert (email_random_string.split("@")[0]).isalnum() + + def test_profile_picture_url(flask_client): user = create_new_user() From 31f50db7c3a071a46392596471b0af13c29533dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Za=20F=C5=ABru=20=5B=E3=82=B6=E3=83=95=E3=83=BC=E3=83=AB?= =?UTF-8?q?=5D?= <40970016+zafuru@users.noreply.github.com> Date: Tue, 17 May 2022 12:55:10 -0400 Subject: [PATCH 10/15] Included new mode into selection --- app/api/views/setting.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/app/api/views/setting.py b/app/api/views/setting.py index 58e314d60..afdc8e770 100644 --- a/app/api/views/setting.py +++ b/app/api/views/setting.py @@ -15,11 +15,14 @@ def setting_to_dict(user: User): + alias_generator = { + "word": AliasGeneratorEnum.word.value, + "uuid": AliasGeneratorEnum.uuid.value, + "random_string": AliasGeneratorEnum.random_string.value, + } ret = { "notification": user.notification, - "alias_generator": "word" - if user.alias_generator == AliasGeneratorEnum.word.value - else "uuid", + "alias_generator": alias_generator.get(user.alias_generator, "word"), "random_alias_default_domain": user.default_random_alias_domain(), # return the default sender format (AT) in case user uses a non-supported sender format "sender_format": SenderFormatEnum.get_name(user.sender_format) @@ -48,7 +51,7 @@ def update_setting(): Update user setting Input: - notification: bool - - alias_generator: word|uuid + - alias_generator: word|uuid|random_string - random_alias_default_domain: str """ user = g.user @@ -59,13 +62,15 @@ def update_setting(): if "alias_generator" in data: alias_generator = data["alias_generator"] - if alias_generator not in ["word", "uuid"]: + if alias_generator not in ["word", "uuid", "random_string"]: return jsonify(error="Invalid alias_generator"), 400 if alias_generator == "word": user.alias_generator = AliasGeneratorEnum.word.value - else: + elif alias_generator == "uuid": user.alias_generator = AliasGeneratorEnum.uuid.value + else: + user.alias_generator = AliasGeneratorEnum.random_string.value if "sender_format" in data: sender_format = data["sender_format"] From 4ceec234d8e0ed1ae85151e57b792d2eaca19b84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Za=20F=C5=ABru=20=5B=E3=82=B6=E3=83=95=E3=83=BC=E3=83=AB?= =?UTF-8?q?=5D?= <40970016+zafuru@users.noreply.github.com> Date: Tue, 17 May 2022 12:55:36 -0400 Subject: [PATCH 11/15] Included new mode into selection and fixed a typo --- templates/dashboard/setting.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/dashboard/setting.html b/templates/dashboard/setting.html index d700b419b..1a0931986 100644 --- a/templates/dashboard/setting.html +++ b/templates/dashboard/setting.html @@ -266,9 +266,9 @@ - + @@ -300,7 +300,7 @@ Random word from our dictionary From e951a6e764b1543d647b9054b7d9ece3512dcd1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Za=20F=C5=ABru=20=28=E3=82=B6=E3=83=95=E3=83=BC=E3=83=AB?= =?UTF-8?q?=29?= <40970016+zafuru@users.noreply.github.com> Date: Thu, 26 May 2022 12:12:39 -0400 Subject: [PATCH 12/15] Update app/api/views/setting.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adrià Casajús --- app/api/views/setting.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/api/views/setting.py b/app/api/views/setting.py index afdc8e770..4f0886eee 100644 --- a/app/api/views/setting.py +++ b/app/api/views/setting.py @@ -15,11 +15,10 @@ def setting_to_dict(user: User): - alias_generator = { - "word": AliasGeneratorEnum.word.value, - "uuid": AliasGeneratorEnum.uuid.value, - "random_string": AliasGeneratorEnum.random_string.value, - } + try: + alias_generator = AliasGeneratorEnum[user.alias_generator].value + except KeyError: + alias_generator = AliasGeneratorEnum.word.value ret = { "notification": user.notification, "alias_generator": alias_generator.get(user.alias_generator, "word"), From e8c4f7a94ef6b143a179576ec3defa1d1bfecff4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Za=20F=C5=ABru=20=7C=20=E3=82=B6=E3=83=95=E3=83=BC?= =?UTF-8?q?=E3=83=AB?= <40970016+zafuru@users.noreply.github.com> Date: Thu, 26 May 2022 13:26:24 -0400 Subject: [PATCH 13/15] Integrated first suggestion --- app/api/views/setting.py | 9 ++++----- tests/api/test_setting.py | 6 +++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/api/views/setting.py b/app/api/views/setting.py index 4f0886eee..122725316 100644 --- a/app/api/views/setting.py +++ b/app/api/views/setting.py @@ -15,13 +15,12 @@ def setting_to_dict(user: User): - try: - alias_generator = AliasGeneratorEnum[user.alias_generator].value - except KeyError: - alias_generator = AliasGeneratorEnum.word.value ret = { "notification": user.notification, - "alias_generator": alias_generator.get(user.alias_generator, "word"), + # return the default alias generator in case user uses a non-supported alias generator + "alias_generator": user.alias_generator + if AliasGeneratorEnum.has_name(user.alias_generator) + else AliasGeneratorEnum.get_name(AliasGeneratorEnum.word.value), "random_alias_default_domain": user.default_random_alias_domain(), # return the default sender format (AT) in case user uses a non-supported sender format "sender_format": SenderFormatEnum.get_name(user.sender_format) diff --git a/tests/api/test_setting.py b/tests/api/test_setting.py index 29cab8c20..b651224c9 100644 --- a/tests/api/test_setting.py +++ b/tests/api/test_setting.py @@ -12,13 +12,17 @@ def test_get_setting(flask_client): r = flask_client.get("/api/setting") assert r.status_code == 200 - assert r.json == { + e_json = { "alias_generator": "word", "notification": True, "random_alias_default_domain": "sl.local", "sender_format": "AT", "random_alias_suffix": "random_string", } + for key in e_json: + # Assert on a per-key basis for better error reporting + assert r.json[key] == e_json[key] + assert len(r.json) == len(e_json) def test_update_settings_notification(flask_client): From a50a080cd372badc2363e77c13f4eb148e00e821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Za=20F=C5=ABru=20=7C=20=E3=82=B6=E3=83=95=E3=83=BC?= =?UTF-8?q?=E3=83=AB?= <40970016+zafuru@users.noreply.github.com> Date: Thu, 26 May 2022 13:46:08 -0400 Subject: [PATCH 14/15] Applied suggestion in other places --- app/api/views/new_random_alias.py | 15 +++++---------- app/api/views/setting.py | 11 +++-------- app/models.py | 12 ++++++++---- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/app/api/views/new_random_alias.py b/app/api/views/new_random_alias.py index 6b064bba4..3bbc69e9f 100644 --- a/app/api/views/new_random_alias.py +++ b/app/api/views/new_random_alias.py @@ -94,17 +94,12 @@ def new_random_alias(): scheme = user.alias_generator mode = request.args.get("mode") if mode: - if mode == "word": - scheme = AliasGeneratorEnum.word.value - elif mode == "uuid": - scheme = AliasGeneratorEnum.uuid.value - elif mode == "random_string": - scheme = AliasGeneratorEnum.random_string.value - else: + try: + scheme = AliasGeneratorEnum[mode].value + except KeyError: + modes = ", ".join(AliasGeneratorEnum.get_names()) return ( - jsonify( - error=f"{mode} must be either word, uuid, or random_string" - ), + jsonify(error=f"{mode} must be one of {modes}"), 400, ) diff --git a/app/api/views/setting.py b/app/api/views/setting.py index 122725316..98fdfe48d 100644 --- a/app/api/views/setting.py +++ b/app/api/views/setting.py @@ -60,16 +60,11 @@ def update_setting(): if "alias_generator" in data: alias_generator = data["alias_generator"] - if alias_generator not in ["word", "uuid", "random_string"]: + try: + user.alias_generator = AliasGeneratorEnum[alias_generator].value + except KeyError: return jsonify(error="Invalid alias_generator"), 400 - if alias_generator == "word": - user.alias_generator = AliasGeneratorEnum.word.value - elif alias_generator == "uuid": - user.alias_generator = AliasGeneratorEnum.uuid.value - else: - user.alias_generator = AliasGeneratorEnum.random_string.value - if "sender_format" in data: sender_format = data["sender_format"] if not SenderFormatEnum.has_name(sender_format): diff --git a/app/models.py b/app/models.py index a66b6bf43..ad1367d04 100644 --- a/app/models.py +++ b/app/models.py @@ -4,7 +4,7 @@ import string import uuid from email.utils import formataddr -from typing import List, Tuple, Optional +from typing import List, Set, Tuple, Optional import arrow import sqlalchemy as sa @@ -183,6 +183,10 @@ def get_name(cls, value: int) -> Optional[str]: return None + @classmethod + def get_names(cls) -> Set[str]: + return set(item.name for item in cls) + @classmethod def has_name(cls, name: str) -> bool: for item in cls: @@ -1190,11 +1194,11 @@ def generate_email( if scheme == AliasGeneratorEnum.uuid.value: name = uuid.uuid4().hex if in_hex else uuid.uuid4().__str__() random_email = name + "@" + alias_domain - elif scheme == AliasGeneratorEnum.word.value: - random_email = random_words() + "@" + alias_domain - else: + elif scheme == AliasGeneratorEnum.random_string.value: name = "".join(random.choices(string.digits + string.ascii_lowercase, k=9)) random_email = name + "@" + alias_domain + else: # use word.value as the default just like the original code + random_email = random_words() + "@" + alias_domain random_email = random_email.lower().strip() From 99c27add3a050f2407cdcd57552cf76aa3f849d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Za=20F=C5=ABru=20=7C=20=E3=82=B6=E3=83=95=E3=83=BC?= =?UTF-8?q?=E3=83=AB?= <40970016+zafuru@users.noreply.github.com> Date: Thu, 26 May 2022 14:11:30 -0400 Subject: [PATCH 15/15] Modified EnumE class so that it uses constant-time lookups instead of linear searches --- app/models.py | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/app/models.py b/app/models.py index ad1367d04..08038356a 100644 --- a/app/models.py +++ b/app/models.py @@ -171,37 +171,35 @@ def __repr__(self): class EnumE(enum.Enum): - @classmethod - def has_value(cls, value: int) -> bool: - return value in set(item.value for item in cls) - @classmethod def get_name(cls, value: int) -> Optional[str]: - for item in cls: - if item.value == value: - return item.name - - return None + try: + return cls(value).name + except ValueError: + return None @classmethod - def get_names(cls) -> Set[str]: - return set(item.name for item in cls) + def get_names(cls) -> List[str]: + return [item.name for item in cls] @classmethod def has_name(cls, name: str) -> bool: - for item in cls: - if item.name == name: - return True - - return False + return not cls.get_value(name) is None @classmethod def get_value(cls, name: str) -> Optional[int]: - for item in cls: - if item.name == name: - return item.value + try: + return cls[name].value + except KeyError: + return None - return None + @classmethod + def get_values(cls) -> List[int]: + return [item.value for item in cls] + + @classmethod + def has_value(cls, value: int) -> bool: + return not cls.get_name(value) is None class PlanEnum(EnumE):