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
```
diff --git a/app/api/views/new_random_alias.py b/app/api/views/new_random_alias.py
index a2af4b6c6..3bbc69e9f 100644
--- a/app/api/views/new_random_alias.py
+++ b/app/api/views/new_random_alias.py
@@ -94,12 +94,14 @@ 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
- else:
- return jsonify(error=f"{mode} must be either word or uuid"), 400
+ try:
+ scheme = AliasGeneratorEnum[mode].value
+ except KeyError:
+ modes = ", ".join(AliasGeneratorEnum.get_names())
+ return (
+ jsonify(error=f"{mode} must be one of {modes}"),
+ 400,
+ )
alias = Alias.create_new_random(user=user, scheme=scheme, note=note)
Session.commit()
diff --git a/app/api/views/setting.py b/app/api/views/setting.py
index 58e314d60..98fdfe48d 100644
--- a/app/api/views/setting.py
+++ b/app/api/views/setting.py
@@ -17,9 +17,10 @@
def setting_to_dict(user: User):
ret = {
"notification": user.notification,
- "alias_generator": "word"
- if user.alias_generator == AliasGeneratorEnum.word.value
- else "uuid",
+ # 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)
@@ -48,7 +49,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,14 +60,11 @@ def update_setting():
if "alias_generator" in data:
alias_generator = data["alias_generator"]
- if alias_generator not in ["word", "uuid"]:
+ 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
- else:
- user.alias_generator = AliasGeneratorEnum.uuid.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 e57a8d08f..08038356a 100644
--- a/app/models.py
+++ b/app/models.py
@@ -1,9 +1,10 @@
import enum
import os
import random
+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
@@ -170,33 +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
+ try:
+ return cls(value).name
+ except ValueError:
+ return None
- return None
+ @classmethod
+ 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):
@@ -216,6 +219,7 @@ class SenderFormatEnum(EnumE):
class AliasGeneratorEnum(EnumE):
word = 1 # aliases are generated based on random words
uuid = 2 # aliases are generated based on uuid
+ random_string = 3 # aliases are generated based on a completely random string
class AliasSuffixEnum(EnumE):
@@ -1188,7 +1192,10 @@ 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.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()
diff --git a/templates/dashboard/setting.html b/templates/dashboard/setting.html
index 8e2d0ab21..1a0931986 100644
--- a/templates/dashboard/setting.html
+++ b/templates/dashboard/setting.html
@@ -266,6 +266,9 @@
+
@@ -297,7 +300,7 @@
Random word from our dictionary
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
diff --git a/tests/api/test_setting.py b/tests/api/test_setting.py
index 3f545b3d2..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):
@@ -41,6 +45,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)
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()