diff --git a/app/api/views/alias.py b/app/api/views/alias.py index 14d65ae4..e737f1c9 100644 --- a/app/api/views/alias.py +++ b/app/api/views/alias.py @@ -410,7 +410,7 @@ def create_contact_route(alias_id): alias_id=alias.id, website_email=contact_email, name=contact_name, - reply_email=generate_reply_email(contact_email), + reply_email=generate_reply_email(contact_email, user), ) LOG.d("create reverse-alias for %s %s", contact_addr, alias) diff --git a/app/dashboard/templates/dashboard/alias_contact_manager.html b/app/dashboard/templates/dashboard/alias_contact_manager.html index aa056202..6c7af559 100644 --- a/app/dashboard/templates/dashboard/alias_contact_manager.html +++ b/app/dashboard/templates/dashboard/alias_contact_manager.html @@ -43,7 +43,12 @@ {% endif %}

This video can also quickly walk you through the steps:

- + diff --git a/app/dashboard/templates/dashboard/setting.html b/app/dashboard/templates/dashboard/setting.html index 891c8b9a..d81cef27 100644 --- a/app/dashboard/templates/dashboard/setting.html +++ b/app/dashboard/templates/dashboard/setting.html @@ -305,6 +305,31 @@ {% endif %} + +
+
+
Include sender address in reverse-alias
+
+ By default, the reverse-alias is randomly generated and doesn't contain any information about + the sender.
+ + You can however enable this option to include the sender address in the reverse-alias.
+ + This can be useful when setting up an email filter and makes the reverse-alias more readable. +
+
+ +
+ + +
+ +
+
+
+ +
Quarantine
diff --git a/app/dashboard/views/alias_contact_manager.py b/app/dashboard/views/alias_contact_manager.py index fad783d1..31e3750b 100644 --- a/app/dashboard/views/alias_contact_manager.py +++ b/app/dashboard/views/alias_contact_manager.py @@ -203,7 +203,7 @@ def alias_contact_manager(alias_id): alias_id=alias.id, website_email=contact_email, name=contact_name, - reply_email=generate_reply_email(contact_email), + reply_email=generate_reply_email(contact_email, current_user), ) LOG.d("create reverse-alias for %s", contact_addr) diff --git a/app/dashboard/views/setting.py b/app/dashboard/views/setting.py index ff0762c1..4e51aeaa 100644 --- a/app/dashboard/views/setting.py +++ b/app/dashboard/views/setting.py @@ -255,6 +255,16 @@ def setting(): flash("Your preference has been updated", "success") return redirect(url_for("dashboard.setting")) + elif request.form.get("form-name") == "sender-in-ra": + choose = request.form.get("enable") + if choose == "on": + current_user.include_sender_in_reverse_alias = True + else: + current_user.include_sender_in_reverse_alias = False + db.session.commit() + flash("Your preference has been updated", "success") + return redirect(url_for("dashboard.setting")) + elif request.form.get("form-name") == "export-data": data = { "email": current_user.email, diff --git a/app/email_utils.py b/app/email_utils.py index 654e16fd..bacc9ce2 100644 --- a/app/email_utils.py +++ b/app/email_utils.py @@ -802,7 +802,7 @@ def replace(msg: Message, old, new) -> Message: return msg -def generate_reply_email(contact_email: str) -> str: +def generate_reply_email(contact_email: str, user: User) -> str: """ generate a reply_email (aka reverse-alias), make sure it isn't used by any contact """ @@ -811,7 +811,14 @@ def generate_reply_email(contact_email: str) -> str: # "The maximum total length of a user name or other local-part is 64 # octets." - if contact_email: + # todo: turns this to False after Dec 20 2020 + include_sender_in_reverse_alias = True + + # user has chosen an option explicitly + if user.include_sender_in_reverse_alias is not None: + include_sender_in_reverse_alias = user.include_sender_in_reverse_alias + + if include_sender_in_reverse_alias and contact_email: # control char: 4 chars (ra+, +) # random suffix: max 10 chars # maximum: 64 @@ -825,13 +832,13 @@ def generate_reply_email(contact_email: str) -> str: # not use while to avoid infinite loop for _ in range(1000): - if contact_email: + if include_sender_in_reverse_alias and contact_email: random_length = random.randint(5, 10) reply_email = ( f"ra+{contact_email}+{random_string(random_length)}@{EMAIL_DOMAIN}" ) else: - random_length = random.randint(10, 50) + random_length = random.randint(20, 50) reply_email = f"ra+{random_string(random_length)}@{EMAIL_DOMAIN}" if not Contact.get_by(reply_email=reply_email): diff --git a/app/models.py b/app/models.py index 5a9c723d..c8315f97 100644 --- a/app/models.py +++ b/app/models.py @@ -272,6 +272,11 @@ class User(db.Model, ModelMixin, UserMixin): db.ForeignKey("alias.id", ondelete="SET NULL"), nullable=True, default=None ) + # whether to include the sender address in reverse-alias + include_sender_in_reverse_alias = db.Column( + db.Boolean, default=False, nullable=True + ) + @classmethod def create(cls, email, name, password=None, **kwargs): user: User = super(User, cls).create(email=email, name=name, **kwargs) diff --git a/email_handler.py b/email_handler.py index ec47bc5e..2134a6e9 100644 --- a/email_handler.py +++ b/email_handler.py @@ -229,7 +229,7 @@ def get_or_create_contact(from_header: str, mail_from: str, alias: Alias) -> Con name=contact_name, mail_from=mail_from, from_header=from_header, - reply_email=generate_reply_email(contact_email) + reply_email=generate_reply_email(contact_email, alias.user) if is_valid_email(contact_email) else NOREPLY, ) @@ -292,7 +292,7 @@ def replace_header_when_forward(msg: Message, alias: Alias, header: str): alias_id=alias.id, website_email=contact_email, name=contact_name, - reply_email=generate_reply_email(contact_email), + reply_email=generate_reply_email(contact_email, alias.user), is_cc=header.lower() == "cc", from_header=addr, ) diff --git a/migrations/versions/2020_120619_c0d91ff18f77_.py b/migrations/versions/2020_120619_c0d91ff18f77_.py new file mode 100644 index 00000000..abfbd5f3 --- /dev/null +++ b/migrations/versions/2020_120619_c0d91ff18f77_.py @@ -0,0 +1,29 @@ +"""empty message + +Revision ID: c0d91ff18f77 +Revises: 56c790ec8ab4 +Create Date: 2020-12-06 19:28:11.733022 + +""" +import sqlalchemy_utils +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'c0d91ff18f77' +down_revision = '56c790ec8ab4' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('users', sa.Column('include_sender_in_reverse_alias', sa.Boolean(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('users', 'include_sender_in_reverse_alias') + # ### end Alembic commands ### diff --git a/tests/test_email_utils.py b/tests/test_email_utils.py index 5f46bffc..1a0646f5 100644 --- a/tests/test_email_utils.py +++ b/tests/test_email_utils.py @@ -25,6 +25,7 @@ from app.email_utils import ( ) from app.extensions import db from app.models import User, CustomDomain +from tests.utils import login def test_get_email_domain_part(): @@ -464,23 +465,50 @@ def test_to_bytes(): def test_generate_reply_email(flask_client): - reply_email = generate_reply_email("test@example.org") + user = User.create( + email="a@b.c", + password="password", + name="Test User", + activated=True, + ) + reply_email = generate_reply_email("test@example.org", user) # return something like - # ra+test.at.example.org+gjbnnddll@sl.local - assert reply_email.startswith("ra+test.at.example.org+") + # ra+@sl.local assert reply_email.endswith(EMAIL_DOMAIN) - reply_email = generate_reply_email("") + reply_email = generate_reply_email("", user) # return something like # ra+qdrcxzppngmvtajklnhqvvuyyzgkyityrzjwikk@sl.local assert reply_email.startswith("ra+") assert reply_email.endswith(EMAIL_DOMAIN) - reply_email = generate_reply_email("👌汉字@example.org") + +def test_generate_reply_email_include_sender_in_reverse_alias(flask_client): + # user enables include_sender_in_reverse_alias + user = User.create( + email="a@b.c", + password="password", + name="Test User", + activated=True, + include_sender_in_reverse_alias=True, + ) + reply_email = generate_reply_email("test@example.org", user) + # return something like + # ra+test.at.example.org+gjbnnddll@sl.local + assert reply_email.startswith("ra+test.at.example.org+") + assert reply_email.endswith(EMAIL_DOMAIN) + + reply_email = generate_reply_email("", user) + # return something like + # ra+qdrcxzppngmvtajklnhqvvuyyzgkyityrzjwikk@sl.local + assert reply_email.startswith("ra+") + assert reply_email.endswith(EMAIL_DOMAIN) + + reply_email = generate_reply_email("👌汉字@example.org", user) assert reply_email.startswith("ra+yizi.at.example.org+") # make sure reply_email only contain lowercase - reply_email = generate_reply_email("TEST@example.org") + reply_email = generate_reply_email("TEST@example.org", user) assert reply_email.startswith("ra+test.at.example.org")