From 50a105f156fd05b6ebeebf4d1ee9a9d7c39df8ca Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Mon, 11 May 2020 14:44:57 +0200 Subject: [PATCH 01/33] fix mailbox newsletter --- templates/emails/com/newsletter/mailbox.html | 2 +- templates/emails/com/newsletter/mailbox.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/emails/com/newsletter/mailbox.html b/templates/emails/com/newsletter/mailbox.html index a1e5a12e..3f494768 100644 --- a/templates/emails/com/newsletter/mailbox.html +++ b/templates/emails/com/newsletter/mailbox.html @@ -30,6 +30,6 @@ maybe for different uses: a Gmail account for social networks & forums, a Pronto {% endblock %} {% block footer %} - This email is sent to {{ user.email }} and is part of our onboarding series. Unsubscribe on + This email is sent to {{ user.email }}. Unsubscribe on Settings {% endblock %} diff --git a/templates/emails/com/newsletter/mailbox.txt b/templates/emails/com/newsletter/mailbox.txt index b9871308..b8c86e8f 100644 --- a/templates/emails/com/newsletter/mailbox.txt +++ b/templates/emails/com/newsletter/mailbox.txt @@ -1,4 +1,4 @@ -This email is sent to {{ user.email }} and is part of our onboarding series. +This email is sent to {{ user.email }}. Unsubscribe from our emails on https://app.simplelogin.io/dashboard/setting#notification ---------------- From 54ce1dc9644be92517ef25d512e0ead835f4810a Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Mon, 11 May 2020 14:46:18 +0200 Subject: [PATCH 02/33] remove unnecessary max_nb_email limit in spf --- email_handler.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/email_handler.py b/email_handler.py index b7fad61f..35532d33 100644 --- a/email_handler.py +++ b/email_handler.py @@ -606,9 +606,6 @@ def spf_pass( subject=msg["Subject"], time=arrow.now(), ), - # as the returned error status is 4**, - # the sender will try to resend the email. Send the error message only once - max_alert_24h=1, ) return False From 70e842789e1c17737c8f501891a32ef329138133 Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Mon, 11 May 2020 23:22:06 +0200 Subject: [PATCH 03/33] make pages compatible with dark-theme --- app/auth/templates/auth/fido.html | 88 +++++++------ app/auth/templates/auth/mfa.html | 52 ++++---- .../templates/dashboard/billing.html | 120 ++++++++--------- .../templates/dashboard/custom_alias.html | 122 +++++++++--------- .../templates/dashboard/directory.html | 2 +- .../templates/dashboard/fido_cancel.html | 27 ++-- .../templates/dashboard/fido_setup.html | 74 +++++------ .../templates/dashboard/lifetime_licence.html | 26 ++-- .../templates/dashboard/mfa_cancel.html | 31 ++--- .../templates/dashboard/mfa_setup.html | 67 +++++----- .../templates/dashboard/unsubscribe.html | 28 ++-- .../oauth/authorize_nonlogin_user.html | 107 +++++++-------- 12 files changed, 381 insertions(+), 363 deletions(-) diff --git a/app/auth/templates/auth/fido.html b/app/auth/templates/auth/fido.html index 836dbc0d..0d8c0305 100644 --- a/app/auth/templates/auth/fido.html +++ b/app/auth/templates/auth/fido.html @@ -11,56 +11,58 @@ {% endblock %} {% block single_content %} -
+
+
-
- Your account is protected with your security key (WebAuthn).

- Follow your browser's steps to continue the sign-in process. -
- -
- {{ fido_token_form.csrf_token }} - {{ fido_token_form.sk_assertion(class="form-control", placeholder="") }} -
-
- -
- - {% if enable_otp %} -
- Don't have your key with you?
Verify by One-Time Password +
+ Your account is protected with your security key (WebAuthn).

+ Follow your browser's steps to continue the sign-in process.
- {% endif %} - - + $("#btnVerifyKey").click(verifyKey); + +
{% endblock %} \ No newline at end of file diff --git a/app/auth/templates/auth/mfa.html b/app/auth/templates/auth/mfa.html index a347745a..7746a0fe 100644 --- a/app/auth/templates/auth/mfa.html +++ b/app/auth/templates/auth/mfa.html @@ -7,33 +7,35 @@ {% block single_content %} -
+
+
-
- Your account is protected with multi-factor authentication (MFA).

- To continue with the sign-in you need to provide the access code from your authenticator. +
+ Your account is protected with multi-factor authentication (MFA).

+ To continue with the sign-in you need to provide the access code from your authenticator. +
+ +
+ {{ otp_token_form.csrf_token }} + + +
Token
+
Please enter the 6-digit number displayed in your MFA application + (Google Authenticator, Authy, MyDigiPassword, etc) here +
+ + {{ otp_token_form.token(class="form-control", autofocus="true") }} + {{ render_field_errors(otp_token_form.token) }} + +
+ + {% if enable_fido %} +
+ Having trouble with your authenticator?
Verify by your security + key +
+ {% endif %}
- -
- {{ otp_token_form.csrf_token }} - - -
Token
-
Please enter the 6-digit number displayed in your MFA application - (Google Authenticator, Authy, MyDigiPassword, etc) here -
- - {{ otp_token_form.token(class="form-control", autofocus="true") }} - {{ render_field_errors(otp_token_form.token) }} - -
- - {% if enable_fido %} -
- Having trouble with your authenticator?
Verify by your security key -
- {% endif %} -
{% endblock %} \ No newline at end of file diff --git a/app/dashboard/templates/dashboard/billing.html b/app/dashboard/templates/dashboard/billing.html index 522f7d11..5be4913a 100644 --- a/app/dashboard/templates/dashboard/billing.html +++ b/app/dashboard/templates/dashboard/billing.html @@ -8,77 +8,79 @@ {% endblock %} {% block default_content %} -
-

Billing

+
+
+

Billing

- {% if sub.cancelled %} -

- You are on the {{ sub.plan_name() }} plan.
- You have canceled your subscription and it will end on {{ sub.next_bill_date.strftime("%Y-%m-%d") }} -

+ {% if sub.cancelled %} +

+ You are on the {{ sub.plan_name() }} plan.
+ You have canceled your subscription and it will end on {{ sub.next_bill_date.strftime("%Y-%m-%d") }} +

-
-

- If you change your mind you can subscribe again to SimpleLogin but please note that this will be a completely - new subscription and - your payment method will be charged immediately. -
+


+

+ If you change your mind you can subscribe again to SimpleLogin but please note that this will be a completely + new subscription and + your payment method will be charged immediately. +
- We are going to send you an email by the end of the subscription so maybe you can upgrade at that time. -
- Re-subscribe -

+ We are going to send you an email by the end of the subscription so maybe you can upgrade at that time. +
+ Re-subscribe +

- {% else %} -

- You are on the {{ sub.plan_name() }} plan. Thank you very much for supporting - SimpleLogin. 🙌
- The next billing cycle starts at {{ sub.next_bill_date.strftime("%Y-%m-%d") }}. -

+ {% else %} +

+ You are on the {{ sub.plan_name() }} plan. Thank you very much for supporting + SimpleLogin. 🙌
+ The next billing cycle starts at {{ sub.next_bill_date.strftime("%Y-%m-%d") }}. +

-
- Click here to update billing information on Paddle, our payment partner:
- Update billing information -
-
-
-

Change Plan

- You can change the plan at any moment.
- Please note that the new billing cycle starts instantly - i.e. you will be charged immediately the annual fee when switching from monthly plan or vice-versa - without pro rata computation .
+
+ Click here to update billing information on Paddle, our payment partner:
+ Update billing information +
+
+
+

Change Plan

+ You can change the plan at any moment.
+ Please note that the new billing cycle starts instantly + i.e. you will be charged immediately the annual fee when switching from monthly plan or vice-versa + without pro rata computation .
- To change the plan you can also cancel the current one and subscribe a new one by the end of this plan. + To change the plan you can also cancel the current one and subscribe a new one by the end of this plan. + + {% if sub.plan == PlanEnum.yearly %} +
+ + +
+ {% else %} +
+ + +
+ {% endif %} +
+ +
+ +
+

Cancel subscription

+ Don't want to protect your inbox anymore?
- {% if sub.plan == PlanEnum.yearly %}
- - -
- {% else %} -
- - -
- {% endif %} -
+ -
- -
-

Cancel subscription

- Don't want to protect your inbox anymore?
- -
- - - + Cancel subscription - + -
- {% endif %} +
+ {% endif %} +
{% endblock %} diff --git a/app/dashboard/templates/dashboard/custom_alias.html b/app/dashboard/templates/dashboard/custom_alias.html index edb7d9af..3a5f8db1 100644 --- a/app/dashboard/templates/dashboard/custom_alias.html +++ b/app/dashboard/templates/dashboard/custom_alias.html @@ -7,84 +7,86 @@ {% endblock %} {% block default_content %} +
+
+

New Email Alias

-
-

New Email Alias

- - {% if user_custom_domains|length == 0 and not DISABLE_ALIAS_SUFFIX %} -
-
- - {% endif %} + {% endif %} -
-
-
- -
- Only lowercase letter, number, dash (-), underscore (_) can be used. + +
+
+ +
+ Only lowercase letter, number, dash (-), underscore (_) can be used. +
+
+ + +
+
- -
- -
-
- -
-
- -
- The mailbox that owns this alias. +
+
+ +
+ The mailbox that owns this alias. +
-
-
-
+
+
+
-
-
-
- +
+
+ +
-
- + +
{% endblock %} diff --git a/app/dashboard/templates/dashboard/directory.html b/app/dashboard/templates/dashboard/directory.html index f5d353d4..ca0102f7 100644 --- a/app/dashboard/templates/dashboard/directory.html +++ b/app/dashboard/templates/dashboard/directory.html @@ -32,7 +32,7 @@ 2️⃣ Quickly use one of the following formats to create an alias on-the-fly without creating this alias beforehand
-
+
my_dir/anything@{{ FIRST_ALIAS_DOMAIN }} or
my_dir+anything@{{ FIRST_ALIAS_DOMAIN }} or
my_dir#anything@{{ FIRST_ALIAS_DOMAIN }}
diff --git a/app/dashboard/templates/dashboard/fido_cancel.html b/app/dashboard/templates/dashboard/fido_cancel.html index f9696bb5..0df4c53d 100644 --- a/app/dashboard/templates/dashboard/fido_cancel.html +++ b/app/dashboard/templates/dashboard/fido_cancel.html @@ -6,22 +6,23 @@ {% block default_content %} -
-

Unlink Your Security Key

-

- Please enter the password of your account so that we can ensure it's you. -

+
+
+

Unlink Your Security Key

+

+ Please enter the password of your account so that we can ensure it's you. +

-
- {{ password_check_form.csrf_token }} + + {{ password_check_form.csrf_token }} -
Password
- - {{ password_check_form.password(class="form-control", autofocus="true") }} - {{ render_field_errors(password_check_form.password) }} - -
+
Password
+ {{ password_check_form.password(class="form-control", autofocus="true") }} + {{ render_field_errors(password_check_form.password) }} + + +
{% endblock %} \ No newline at end of file diff --git a/app/dashboard/templates/dashboard/fido_setup.html b/app/dashboard/templates/dashboard/fido_setup.html index 69c069f2..f82b8895 100644 --- a/app/dashboard/templates/dashboard/fido_setup.html +++ b/app/dashboard/templates/dashboard/fido_setup.html @@ -11,48 +11,50 @@ {% endblock %} {% block default_content %} -
-

Register Your Security Key

-

Follow your browser's steps to register your security key with SimpleLogin

+
+
+

Register Your Security Key

+

Follow your browser's steps to register your security key with SimpleLogin

-
- {{ fido_token_form.csrf_token }} - {{ fido_token_form.sk_assertion(class="form-control", placeholder="") }} -
-
- -
+
+ {{ fido_token_form.csrf_token }} + {{ fido_token_form.sk_assertion(class="form-control", placeholder="") }} +
+
+ +
- + $("#btnRegisterKey").click(registerKey); + $('document').ready(registerKey()); + +
{% endblock %} diff --git a/app/dashboard/templates/dashboard/lifetime_licence.html b/app/dashboard/templates/dashboard/lifetime_licence.html index 99c107d9..0f9bb232 100644 --- a/app/dashboard/templates/dashboard/lifetime_licence.html +++ b/app/dashboard/templates/dashboard/lifetime_licence.html @@ -7,20 +7,22 @@ {% endblock %} {% block default_content %} -
-

Lifetime Licence

+
+
+

Lifetime Licence

-
- If you have a lifetime licence, please paste it here.
+
+ If you have a lifetime licence, please paste it here.
+
+ +
+ {{ coupon_form.csrf_token }} + + {{ coupon_form.code(class="form-control", placeholder="Licence Code") }} + {{ render_field_errors(coupon_form.code) }} + +
- -
- {{ coupon_form.csrf_token }} - - {{ coupon_form.code(class="form-control", placeholder="Licence Code") }} - {{ render_field_errors(coupon_form.code) }} - -
{% endblock %} \ No newline at end of file diff --git a/app/dashboard/templates/dashboard/mfa_cancel.html b/app/dashboard/templates/dashboard/mfa_cancel.html index d6bfcaaa..4d3183d1 100644 --- a/app/dashboard/templates/dashboard/mfa_cancel.html +++ b/app/dashboard/templates/dashboard/mfa_cancel.html @@ -6,24 +6,25 @@ {% block default_content %} -
-

Multi Factor Authentication

-

- To cancel MFA, please enter the 6-digit number in your TOTP application - (Google Authenticator, Authy, MyDigiPassword, etc) here. -

+
+
+

Multi Factor Authentication

+

+ To cancel MFA, please enter the 6-digit number in your TOTP application + (Google Authenticator, Authy, MyDigiPassword, etc) here. +

-
- {{ otp_token_form.csrf_token }} + + {{ otp_token_form.csrf_token }} -
Token
-
The 6-digit number displayed on your phone.
- - {{ otp_token_form.token(class="form-control", autofocus="true") }} - {{ render_field_errors(otp_token_form.token) }} - -
+
Token
+
The 6-digit number displayed on your phone.
+ {{ otp_token_form.token(class="form-control", autofocus="true") }} + {{ render_field_errors(otp_token_form.token) }} + + +
{% endblock %} diff --git a/app/dashboard/templates/dashboard/mfa_setup.html b/app/dashboard/templates/dashboard/mfa_setup.html index 44712184..b0995e8e 100644 --- a/app/dashboard/templates/dashboard/mfa_setup.html +++ b/app/dashboard/templates/dashboard/mfa_setup.html @@ -9,43 +9,42 @@ {% endblock %} {% block default_content %} -
-

Multi Factor Authentication

-

Please open a TOTP application (Google Authenticator, Authy, MyDigiPassword, etc) - on your smartphone and scan the following QR Code: -

+
+
+

Multi Factor Authentication

+

Please open a TOTP application (Google Authenticator, Authy, MyDigiPassword, etc) + on your smartphone and scan the following QR Code: +

- + - + + +
+ Or you can use the manual entry with the following key: +
+ + + + +
+ {{ otp_token_form.csrf_token }} + +
Token
+
Please enter the 6-digit number displayed on your phone.
+ + {{ otp_token_form.token(class="form-control", placeholder="") }} + {{ render_field_errors(otp_token_form.token) }} + +
-
- Or you can use the manual entry with the following key:
- -
- {{ current_user.otp_secret }} -
- - -
- {{ otp_token_form.csrf_token }} - -
Token
-
Please enter the 6-digit number displayed on your phone.
- - {{ otp_token_form.token(class="form-control", placeholder="") }} - {{ render_field_errors(otp_token_form.token) }} - -
- -
{% endblock %} diff --git a/app/dashboard/templates/dashboard/unsubscribe.html b/app/dashboard/templates/dashboard/unsubscribe.html index ecd8b262..63c09ad2 100644 --- a/app/dashboard/templates/dashboard/unsubscribe.html +++ b/app/dashboard/templates/dashboard/unsubscribe.html @@ -8,20 +8,22 @@ {% block default_content %} -
-

- Block alias -

-

- You are about to block the alias {{alias}} -

-

- After this, you will stop receiving all emails sent to this alias, please confirm. -

+
+
+

+ Block alias +

+

+ You are about to block the alias {{ alias }} +

+

+ After this, you will stop receiving all emails sent to this alias, please confirm. +

-
- -
+
+ +
+
{% endblock %} diff --git a/app/oauth/templates/oauth/authorize_nonlogin_user.html b/app/oauth/templates/oauth/authorize_nonlogin_user.html index 602a259b..9cb3df75 100644 --- a/app/oauth/templates/oauth/authorize_nonlogin_user.html +++ b/app/oauth/templates/oauth/authorize_nonlogin_user.html @@ -1,61 +1,64 @@ {% extends "base.html" %} {% block content %} -
-
- - - -
- -
- {{ client.name }} would like to have access to your following data: -
- -
-
    - {% for scope in client.get_scopes() %} -
  • - {% if scope == Scope.AVATAR_URL %} - avatar - {% else %} - {{ scope.value }} - {% endif %} -
  • - {% endfor %} -
-
- -
- In order to accept the request, you need to sign in. -
- -
-
- - - Login - - - - Sign Up +
+
+ + +
+ {{ client.name }} would like to have access to your following data: +
+ +
+
    + {% for scope in client.get_scopes() %} +
  • + {% if scope == Scope.AVATAR_URL %} + avatar + {% else %} + {{ scope.value }} + {% endif %} +
  • + {% endfor %} +
+
+ +
+ In order to accept the request, you need to sign in. +
+ + + +
+ +
+

Cancel and go back to {{ client.name }}

+ + Cancel + +
+ +
+ SimpleLogin is an open source social login provider that protects your + privacy. +
+
- -
- -
-

Cancel and go back to {{ client.name }}

- - Cancel - -
- -
- SimpleLogin is an open source social login provider that protects your privacy. -
-
{% endblock %} From 591fee301ef3a15168bbb98a3022867b82ca1e9d Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Mon, 11 May 2020 23:23:08 +0200 Subject: [PATCH 04/33] prettify dns page --- .../templates/dashboard/domain_detail/dns.html | 14 +++++++------- static/style.css | 12 ++++++++---- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/app/dashboard/templates/dashboard/domain_detail/dns.html b/app/dashboard/templates/dashboard/domain_detail/dns.html index c5f1baec..eec9aa0d 100644 --- a/app/dashboard/templates/dashboard/domain_detail/dns.html +++ b/app/dashboard/templates/dashboard/domain_detail/dns.html @@ -35,7 +35,7 @@
{% for priority, email_server in EMAIL_SERVERS_WITH_PRIORITY %} -
+
Record: MX
Domain: {{ custom_domain.domain }} or @
Priority: {{ priority }}
@@ -62,7 +62,7 @@ {% if not mx_ok %}
Your DNS is not correctly set. The MX record we obtain is: -
+
{% if not mx_errors %} (Empty) {% endif %} @@ -97,7 +97,7 @@
Add the following TXT DNS record to your domain.
-
+
Record: TXT
Domain: {{ custom_domain.domain }} or @
Value: @@ -125,7 +125,7 @@ {% if not spf_ok %}
Your DNS is not correctly set. The TXT record we obtain is: -
+
{% if not spf_errors %} (Empty) {% endif %} @@ -162,7 +162,7 @@
Add the following CNAME DNS record to your domain.
-
+
Record: CNAME
Domain: dkim._domainkey.{{ custom_domain.domain }} is: -
+
{% for r in dkim_errors %} {{ r }}
{% endfor %} @@ -231,7 +231,7 @@
Add the following TXT DNS record to your domain.
-
+
Record: TXT
Domain: Date: Mon, 11 May 2020 23:23:19 +0200 Subject: [PATCH 05/33] use logo in email base template --- templates/emails/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/emails/base.html b/templates/emails/base.html index 09767357..49105e3e 100644 --- a/templates/emails/base.html +++ b/templates/emails/base.html @@ -449,7 +449,7 @@ From 5ce2cca63fbb903e83a85fed41b982b9f07fade1 Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Mon, 11 May 2020 23:26:37 +0200 Subject: [PATCH 06/33] Add Raymond as contributor --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1a362923..9d862c77 100644 --- a/README.md +++ b/README.md @@ -1273,5 +1273,6 @@ Thanks go to these wonderful people: Ninh Dinh
Ninh Dinh

Tung Nguyen V. N.
Tung Nguyen V. N.

Son Nguyen Kim
Son Nguyen Kim

+ Raymond Nook
Raymond Nook

From a9967c9a4dc21f67e882ff9b02bb5e9bfa8ebd63 Mon Sep 17 00:00:00 2001 From: devStorm <59678453+developStorm@users.noreply.github.com> Date: Mon, 11 May 2020 19:17:51 -0700 Subject: [PATCH 07/33] Auto activate WebAuthn authentication --- app/auth/templates/auth/fido.html | 4 ++++ app/auth/views/fido.py | 3 +++ server.py | 1 + 3 files changed, 8 insertions(+) diff --git a/app/auth/templates/auth/fido.html b/app/auth/templates/auth/fido.html index 0d8c0305..34d67078 100644 --- a/app/auth/templates/auth/fido.html +++ b/app/auth/templates/auth/fido.html @@ -62,6 +62,10 @@ $("#btnVerifyKey").click(verifyKey); + {% if auto_activate %} + + {% endif %} +
diff --git a/app/auth/views/fido.py b/app/auth/views/fido.py index d145f916..ae1ba200 100644 --- a/app/auth/views/fido.py +++ b/app/auth/views/fido.py @@ -35,6 +35,7 @@ def fido(): flash("Only user with security key linked should go to this page", "warning") return redirect(url_for("auth.login")) + auto_activate = True fido_token_form = FidoTokenForm() next_url = request.args.get("next") @@ -69,6 +70,7 @@ def fido(): except Exception as e: LOG.error(f"An error occurred in WebAuthn verification process: {e}") flash("Key verification failed.", "warning") + auto_activate = False else: user.fido_sign_count = new_sign_count db.session.commit() @@ -101,4 +103,5 @@ def fido(): fido_token_form=fido_token_form, webauthn_assertion_options=webauthn_assertion_options, enable_otp=user.enable_otp, + auto_activate=auto_activate, ) diff --git a/server.py b/server.py index f20799fc..70b5e0c3 100644 --- a/server.py +++ b/server.py @@ -136,6 +136,7 @@ def fake_data(): activated=True, is_admin=True, otp_secret="base32secret3232", + can_use_fido=True, ) db.session.commit() user.trial_end = None From 2b71fee71265f3334a0fc9b95b597656e3219d47 Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Wed, 13 May 2020 21:38:25 +0200 Subject: [PATCH 08/33] use warning log level for "No existing AppleSub" error --- app/api/views/apple.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/views/apple.py b/app/api/views/apple.py index ac3b3810..ac325e94 100644 --- a/app/api/views/apple.py +++ b/app/api/views/apple.py @@ -283,7 +283,7 @@ def apple_update_notification(): db.session.commit() return jsonify(ok=True), 200 else: - LOG.error( + LOG.warning( "No existing AppleSub for original_transaction_id %s", original_transaction_id, ) From e6c37cad0ba81ae22948e14106533c3ae202cf1c Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Wed, 13 May 2020 21:41:34 +0200 Subject: [PATCH 09/33] Handle case where data["receipt"]["in_app"] is empty --- app/api/views/apple.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/api/views/apple.py b/app/api/views/apple.py index ac325e94..afa54ce9 100644 --- a/app/api/views/apple.py +++ b/app/api/views/apple.py @@ -490,6 +490,10 @@ def verify_receipt(receipt_data, user, password) -> Optional[AppleSubscription]: # "is_in_intro_offer_period": "false", # } transactions = data["receipt"]["in_app"] + if not transactions: + LOG.warning("Empty transactions in data %s", data) + return None + latest_transaction = max(transactions, key=lambda t: int(t["expires_date_ms"])) original_transaction_id = latest_transaction["original_transaction_id"] expires_date = arrow.get(int(latest_transaction["expires_date_ms"]) / 1000) From 405c5f8a69fb2ead48ef6809f5d8e91a8a6a6d12 Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Wed, 13 May 2020 21:52:07 +0200 Subject: [PATCH 10/33] Add Sibren to contributor list --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9d862c77..6348a75c 100644 --- a/README.md +++ b/README.md @@ -1274,5 +1274,6 @@ Thanks go to these wonderful people: Tung Nguyen V. N.
Tung Nguyen V. N.

Son Nguyen Kim
Son Nguyen Kim

Raymond Nook
Raymond Nook

+ Sibren Vasse
Sibren Vasse

From 5c8c741a6a5ee703e1a726dbe4f0a17cddd7c4a2 Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Wed, 13 May 2020 22:02:38 +0200 Subject: [PATCH 11/33] API Error handling for 404 and 500 --- app/api/base.py | 12 ++++++++++++ server.py | 5 +++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/api/base.py b/app/api/base.py index 36d9bd50..1d5bdb96 100644 --- a/app/api/base.py +++ b/app/api/base.py @@ -5,6 +5,7 @@ from flask import Blueprint, request, jsonify, g from flask_login import current_user from app.extensions import db +from app.log import LOG from app.models import ApiKey api_bp = Blueprint(name="api", import_name=__name__, url_prefix="/api") @@ -32,3 +33,14 @@ def require_api_auth(f): return f(*args, **kwargs) return decorated + + +@api_bp.app_errorhandler(404) +def not_found(e): + return jsonify(error="No such endpoint"), 404 + + +@api_bp.app_errorhandler(Exception) +def internal_error(e): + LOG.exception(e) + return jsonify(error="Internal error"), 500 diff --git a/server.py b/server.py index f20799fc..780671a4 100644 --- a/server.py +++ b/server.py @@ -87,13 +87,13 @@ def create_app() -> Flask: app.config["SESSION_COOKIE_SECURE"] = True app.config["SESSION_COOKIE_SAMESITE"] = "Lax" + setup_error_page(app) + init_extensions(app) register_blueprints(app) set_index_page(app) jinja2_filter(app) - setup_error_page(app) - setup_favicon_route(app) setup_openid_metadata(app) @@ -136,6 +136,7 @@ def fake_data(): activated=True, is_admin=True, otp_secret="base32secret3232", + can_use_fido=True, ) db.session.commit() user.trial_end = None From c43fa65cd4686c201d1047140278678e1b811cbc Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Wed, 13 May 2020 22:35:27 +0200 Subject: [PATCH 12/33] If From header is empty, try creating contact with envelope sender --- email_handler.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/email_handler.py b/email_handler.py index 35532d33..19bc9c7b 100644 --- a/email_handler.py +++ b/email_handler.py @@ -117,13 +117,20 @@ def new_app(): return app -def get_or_create_contact(contact_from_header: str, alias: Alias) -> Contact: +def get_or_create_contact( + contact_from_header: str, mail_from: str, alias: Alias +) -> Contact: """ contact_from_header is the RFC 2047 format FROM header """ # force convert header to string, sometimes contact_from_header is Header object contact_from_header = str(contact_from_header) contact_name, contact_email = parseaddr_unicode(contact_from_header) + if not contact_email: + # From header is empty, try with mail_from + LOG.warning("From header is empty, parse mail_from %s %s", mail_from, alias) + contact_name, contact_email = parseaddr_unicode(mail_from) + contact = Contact.get_by(alias_id=alias.id, website_email=contact_email) if contact: if contact.name != contact_name: @@ -339,7 +346,7 @@ def handle_forward(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> (bool, s LOG.d("Forward from %s to %s, nothing to do", envelope.mail_from, mailbox_email) return False, _SELF_FORWARDING_STATUS - contact = get_or_create_contact(msg["From"], alias) + contact = get_or_create_contact(msg["From"], envelope.mail_from, alias) email_log = EmailLog.create(contact_id=contact.id, user_id=contact.user_id) if not alias.enabled: From ec2d912bb8de1d11ed8e1e8b75c9fa3674995613 Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Wed, 13 May 2020 22:08:14 +0200 Subject: [PATCH 13/33] mobile-darkmode newsletter --- shell.py | 22 ++++ .../com/newsletter/mobile-darkmode.html | 108 ++++++++++++++++++ .../emails/com/newsletter/mobile-darkmode.txt | 71 ++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 templates/emails/com/newsletter/mobile-darkmode.html create mode 100644 templates/emails/com/newsletter/mobile-darkmode.txt diff --git a/shell.py b/shell.py index 695d5a35..ff61e143 100644 --- a/shell.py +++ b/shell.py @@ -73,6 +73,28 @@ def send_pgp_newsletter(): LOG.warning("Cannot send to user %s", user) +def send_mobile_newsletter(): + count = 0 + for user in User.query.order_by(User.id).all(): + if user.notification and user.activated: + count += 1 + try: + LOG.d("#%s: send to %s", count, user) + send_email( + user.email, + "Mobile and Dark Mode", + render("com/newsletter/mobile-darkmode.txt", user=user), + render("com/newsletter/mobile-darkmode.html", user=user), + ) + except Exception: + LOG.warning("Cannot send to user %s", user) + + if count % 5 == 0: + # sleep every 5 sends to avoid hitting email limits + LOG.d("Sleep 1s") + sleep(1) + + app = create_app() with app.app_context(): diff --git a/templates/emails/com/newsletter/mobile-darkmode.html b/templates/emails/com/newsletter/mobile-darkmode.html new file mode 100644 index 00000000..b4db6efd --- /dev/null +++ b/templates/emails/com/newsletter/mobile-darkmode.html @@ -0,0 +1,108 @@ +{% extends "base.html" %} + +{% block content %} + {{ render_text("Hi " + user.name) }} + + {% call text() %} + Son from SimpleLogin here. I hope you are doing well and are staying at home in this difficult time. By the way I'm writing this newsletter from my couch with my cats proofreading the text :).
+ Please find below some of our latest news.
+ + {% endcall %} + + {% call text() %} + 1) Mobile apps

+ +

+ + Now you can quickly create aliases on-the-go with SimpleLogin Android and iOS app, + thanks to our mobile guy Thanh-Nhon!
+ + Download the Android app on + Play Store and the iOS app on + App Store.
+ + With the release of the mobile apps, SimpleLogin now covers most major platforms:
+ + - Desktop with SimpleLogin web app or Chrome, Firefox and Safari extension
+ - Mobile with Android and iOS app
+ + The code is of course open-source and available on our Github + + {% endcall %} + + {% call text() %} + 2) Dark mode

+ +

+ + You have asked for it and now the dark mode is finally available, kudos to Dung - our full-stack guy.
+ You can finally enjoy using SimpleLogin in the dark. + {% endcall %} + + {% call text() %} + 3) Alias name, new UI, security page, new policy privacy

+ +

+ + You might have noticed that the web UI is now more compact: the web app has undergone a remake + to make it more responsive for usual actions like enabling/disabling an alias, updating alias note, etc.
+ + You can set a name for your alias too: this name is used when you send emails or reply from your alias.
+ + We have also created a new security page that goes into the technical details of SimpleLogin. + Our privacy page is also rewritten from scratch: nothing changes about your data protection + but the page is more clear and detailed now. + + {% endcall %} + + {% call text() %} + 4) Facebook, Google, Github login deprecation
+ We have decided to deprecate those social login options because of several reasons:
+ + - Privacy: every time you sign in using one of these methods, the respective company knows and + we have no information on what they do with this data.
+ - Not fully open-standard compatible: these platforms enjoy their monopolies and + don't play well with open standards like OAuth2/OpenID: in fact, implementations on mobile of these social login + require their SDK that we refuse to add because of privacy concern.
+ - Uniform experiences for all users: to have these social login in our iOS app, we need to support "Sign in with + Apple" that isn't broadly available for Android users. + Again, another big tech enjoying its monopoly.
+ + If you happen to use one of these social login options, please create a password for your account on the + Setting page
+ + You can still sign in using these social login until 2020-05-31. After this date, they will be removed. + {% endcall %} + + {% call text() %} + 5) WebAuthn (Beta)
+ + Thanks to Raymond, a user of SimpleLogin, the WebAuthn is now available in Beta. + Please reply to this email if you want to try this out. + + {% endcall %} + + {% call text() %} + We want to say thank you to all users who have helped to improve SimpleLogin code and even + contribute important features. + That means a lot to us as SimpleLogin is after all an open-source project. + + {% endcall %} + + {% call text() %} + We always welcome your feedback. Get in touch on our Twitter, + Reddit, where you can also follow all our latest updates. +
+ {% endcall %} + + {% call text() %} + Best,
+ Son. + {% endcall %} + +{% endblock %} + +{% block footer %} + This email is sent to {{ user.email }}. Unsubscribe on + Settings +{% endblock %} diff --git a/templates/emails/com/newsletter/mobile-darkmode.txt b/templates/emails/com/newsletter/mobile-darkmode.txt new file mode 100644 index 00000000..694e1545 --- /dev/null +++ b/templates/emails/com/newsletter/mobile-darkmode.txt @@ -0,0 +1,71 @@ +This email is sent to {{ user.email }}. +Unsubscribe from our emails on https://app.simplelogin.io/dashboard/setting#notification +---------------- + +Hi {{user.name}} + +Son from SimpleLogin here. I hope you are doing well and are staying at home in this difficult time. +By the way I'm writing this newsletter from my couch with my cats proofreading the text :). + +Here are some of our latest news: + +1) Mobile apps + +Now you can quickly create aliases on-the-go with SimpleLogin Android and iOS app, thanks to our mobile guy Thanh-Nhon! +Download: +- the Android app on Play Store https://play.google.com/store/apps/details?id=io.simplelogin.android +- the iOS app on App Store https://apps.apple.com/us/app/simplelogin-anti-spam/id1494359858 + +With the release of the mobile apps, SimpleLogin now covers most major platforms: + +- Desktop with SimpleLogin web app or Chrome, Firefox and Safari extension +- Mobile with Android and iOS app + +The apps code is of course open-source and available on our Github http://github.com/simple-login/ + +2) Dark mode + +No worries, we are not going to the dark side :). +You have asked for it and now the dark mode is finally available, thanks to Dung - our full-stack guy. +You can finally enjoy using SimpleLogin in the dark. + +3) Alias name, new UI, security page, new policy privacy + +You might have noticed that the web UI is now more compact: the web app has undergone a remake +to make it more responsive for usual actions like enabling/disabling an alias, updating alias note, etc. + +You can set a name for your alias: this name is used when you send emails or reply from your alias. + +We have also created a new security page that goes into the technical details of SimpleLogin. +Our privacy page is also rewritten from scratch: nothing changes about your data protection +but the page is now much more clear and detailed now. + +4) Facebook, Google, Github login deprecation + +We have decided to deprecate those social login options because of several reasons: + +- Privacy: every time you sign in using one of these methods, the respective company knows and + we have no information on what they do with this data. +- Not fully open-standard compatible: these platforms enjoy their monopolies and + don't play well with open standards like OAuth2/OpenID: in fact, implementations on mobile of these social login + require their SDK that we refuse to add because of privacy concern. +- Uniform experiences for all users: to have these social login in our iOS app, we need to support "Sign in with Apple" + that isn't broadly available for Android users. Again, another big tech enjoying its monopoly. + +If you happen to use one of these social login options, please create a password for your account on the Setting page +https://app.simplelogin.io/dashboard/setting + +You can still sign in using these social login until 2020-05-31. After this date, they will be removed. + +5) WebAuthn (Beta) + +Thanks to one of SimpleLogin users, the WebAuthn is now available in Beta. +Please reply to this email if you want to try this out. + +We want to say thank you to all users who have helped to improve SimpleLogin code and even contribute important features. +That means a lot to us as SimpleLogin is after all an open-source project. + +We always welcome your feedback. Get in touch on social media, where you can also follow all our latest updates. + +Best regards, +Son. \ No newline at end of file From d802615faa68ab8a67cc13b8b46665faff2d7c5d Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Wed, 13 May 2020 22:46:57 +0200 Subject: [PATCH 14/33] improve newsletter wording --- templates/emails/com/newsletter/mobile-darkmode.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/emails/com/newsletter/mobile-darkmode.html b/templates/emails/com/newsletter/mobile-darkmode.html index b4db6efd..001d2847 100644 --- a/templates/emails/com/newsletter/mobile-darkmode.html +++ b/templates/emails/com/newsletter/mobile-darkmode.html @@ -83,8 +83,8 @@ {% endcall %} {% call text() %} - We want to say thank you to all users who have helped to improve SimpleLogin code and even - contribute important features. + On behalf of the team, I want to say thank you to all users who have helped to improve SimpleLogin code + and even contribute important features. That means a lot to us as SimpleLogin is after all an open-source project. {% endcall %} From 092d934feb72b966f3a328370781bc298c4b23f9 Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Wed, 13 May 2020 22:55:15 +0200 Subject: [PATCH 15/33] improve wording --- .../emails/com/newsletter/mobile-darkmode.html | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/templates/emails/com/newsletter/mobile-darkmode.html b/templates/emails/com/newsletter/mobile-darkmode.html index 001d2847..c970293d 100644 --- a/templates/emails/com/newsletter/mobile-darkmode.html +++ b/templates/emails/com/newsletter/mobile-darkmode.html @@ -4,7 +4,8 @@ {{ render_text("Hi " + user.name) }} {% call text() %} - Son from SimpleLogin here. I hope you are doing well and are staying at home in this difficult time. By the way I'm writing this newsletter from my couch with my cats proofreading the text :).
+ Son from SimpleLogin here. I hope you are doing well and are staying at home in this difficult time. By the way I'm + writing this newsletter from my couch with my cats proofreading the text :).
Please find below some of our latest news.
{% endcall %} @@ -49,8 +50,10 @@ You can set a name for your alias too: this name is used when you send emails or reply from your alias.
- We have also created a new security page that goes into the technical details of SimpleLogin. - Our privacy page is also rewritten from scratch: nothing changes about your data protection + We have also created a new security page that goes into the technical + details of SimpleLogin. + Our privacy page is also rewritten from scratch: nothing changes about + your data protection but the page is more clear and detailed now. {% endcall %} @@ -83,6 +86,7 @@ {% endcall %} {% call text() %} +
On behalf of the team, I want to say thank you to all users who have helped to improve SimpleLogin code and even contribute important features. That means a lot to us as SimpleLogin is after all an open-source project. @@ -90,9 +94,11 @@ {% endcall %} {% call text() %} - We always welcome your feedback. Get in touch on our Twitter, - Reddit, where you can also follow all our latest updates. -
+ That's all for today. If you want to follow all our latest features, you can follow our + Twitter or join our + Reddit + or subscribe to our RSS feed.
+ Now back to coding :). {% endcall %} {% call text() %} From 85130e175bf01f35ba1eb00fb02373db9c85f717 Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Wed, 13 May 2020 23:02:29 +0200 Subject: [PATCH 16/33] fix dark-mode for modal --- static/assets/css/darkmode.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/assets/css/darkmode.css b/static/assets/css/darkmode.css index b96687f0..eefa1600 100644 --- a/static/assets/css/darkmode.css +++ b/static/assets/css/darkmode.css @@ -46,7 +46,7 @@ hr { background-color: var(--input-bg-color); } -.form-control:focus, .dataTables_wrapper .dataTables_length select:focus, .dataTables_wrapper .dataTables_filter input:focus { +.form-control:focus, .dataTables_wrapper .dataTables_length select:focus, .dataTables_wrapper .dataTables_filter input:focus, .modal-content { border-color: #1991eb; outline: 0; box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25); From ee19957d5d7285838bfd70b8e6be156fa9288598 Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Wed, 13 May 2020 23:28:00 +0200 Subject: [PATCH 17/33] Add 405 error --- app/api/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/api/base.py b/app/api/base.py index 1d5bdb96..f7ef67c0 100644 --- a/app/api/base.py +++ b/app/api/base.py @@ -44,3 +44,8 @@ def not_found(e): def internal_error(e): LOG.exception(e) return jsonify(error="Internal error"), 500 + + +@api_bp.app_errorhandler(405) +def wrong_method(e): + return jsonify(error="Method not allowed"), 405 From 3a1af9f42425ff1dc0e5fe148d736e1523fad76f Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Thu, 14 May 2020 13:27:04 +0200 Subject: [PATCH 18/33] fall back for UnicodeDecodeError --- app/email_utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/email_utils.py b/app/email_utils.py index 582d1aa1..7dc60a03 100644 --- a/app/email_utils.py +++ b/app/email_utils.py @@ -503,7 +503,11 @@ def parseaddr_unicode(addr) -> (str, str): name = name.strip() decoded_string, charset = decode_header(name)[0] if charset is not None: - name = decoded_string.decode(charset) + try: + name = decoded_string.decode(charset) + except UnicodeDecodeError: + LOG.warning("Cannot decode addr name %s", name) + name = "" else: name = decoded_string From c7530947d3e5b99f02e4670860940279cef0ba46 Mon Sep 17 00:00:00 2001 From: Sibren Vasse Date: Thu, 14 May 2020 15:05:04 +0200 Subject: [PATCH 19/33] On domain check fail, update database --- app/dashboard/views/domain_detail.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/dashboard/views/domain_detail.py b/app/dashboard/views/domain_detail.py index 5f1fae21..497b3471 100644 --- a/app/dashboard/views/domain_detail.py +++ b/app/dashboard/views/domain_detail.py @@ -37,6 +37,8 @@ def domain_detail_dns(custom_domain_id): if sorted(mx_domains) != sorted(EMAIL_SERVERS_WITH_PRIORITY): flash("The MX record is not correctly set", "warning") + custom_domain.verified = False + db.session.commit() mx_ok = False # build mx_errors to show to user mx_errors = [ @@ -66,6 +68,8 @@ def domain_detail_dns(custom_domain_id): ) ) else: + custom_domain.spf_verified = False + db.session.commit() flash( f"SPF: {EMAIL_DOMAIN} is not included in your SPF record.", "warning", @@ -86,6 +90,8 @@ def domain_detail_dns(custom_domain_id): ) ) else: + custom_domain.dkim_verified = False + db.session.commit() flash("DKIM: the CNAME record is not correctly set", "warning") dkim_ok = False dkim_errors = [dkim_record or "[Empty]"] @@ -102,6 +108,8 @@ def domain_detail_dns(custom_domain_id): ) ) else: + custom_domain.dmarc_verified = False + db.session.commit() flash( f"DMARC: The TXT record is not correctly set", "warning", ) From c6e291f7e875f2023b554209d9a34cb9cd476a10 Mon Sep 17 00:00:00 2001 From: Sibren Vasse Date: Thu, 14 May 2020 13:16:51 +0200 Subject: [PATCH 20/33] Dark theme: prevent white flash on page load --- static/assets/js/core.js | 15 +-------------- static/assets/js/theme.js | 40 +++++++++++++++++++++++++++++++++++++++ templates/base.html | 4 +++- 3 files changed, 44 insertions(+), 15 deletions(-) create mode 100644 static/assets/js/theme.js diff --git a/static/assets/js/core.js b/static/assets/js/core.js index 7b02b27d..25de1c94 100755 --- a/static/assets/js/core.js +++ b/static/assets/js/core.js @@ -104,17 +104,4 @@ $(document).ready(function() { }); }); } - - /** Dark mode controller */ - if (store.get('dark-mode') === true) { - document.documentElement.setAttribute('data-theme', 'dark') - } - $('[data-toggle="dark-mode"]').on('click', function () { - if (store.get('dark-mode') === true) { - store.set('dark-mode', false); - return document.documentElement.setAttribute('data-theme', 'light') - } - store.set('dark-mode', true) - document.documentElement.setAttribute('data-theme', 'dark') - }) -}); \ No newline at end of file +}); diff --git a/static/assets/js/theme.js b/static/assets/js/theme.js new file mode 100644 index 00000000..721b2556 --- /dev/null +++ b/static/assets/js/theme.js @@ -0,0 +1,40 @@ +let setCookie = function(name, value, days) { + if (!name || !value) return false; + let expires = ''; + let secure = ''; + if (location.protocol === 'https:') secure = 'Secure; '; + + if (days) { + let date = new Date(); + date.setTime(date.getTime() + (days * 24*60*60*1000)); + expires = 'Expires=' + date.toUTCString() + '; '; + } + + document.cookie = name + '=' + value + '; ' + + expires + + secure + + 'sameSite=Lax; ' + + 'domain=' + window.location.hostname + '; ' + + 'path=/'; + return true; + } + +let getCookie = function(name) { + let match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); + if (match) return match[2]; +} + +$(document).ready(function() { + /** Dark mode controller */ + if (getCookie('dark-mode') === "true") { + document.documentElement.setAttribute('data-theme', 'dark'); + } + $('[data-toggle="dark-mode"]').on('click', function () { + if (getCookie('dark-mode') === "true") { + setCookie('dark-mode', 'false', 30); + return document.documentElement.setAttribute('data-theme', 'light') + } + setCookie('dark-mode', 'true', 30); + document.documentElement.setAttribute('data-theme', 'dark') + }) +}); diff --git a/templates/base.html b/templates/base.html index c7539b7b..0f1827c8 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,7 +1,7 @@ {% from "_formhelpers.html" import render_field, render_field_errors %} - + + + From bdc3102420115bd7616c5e9d90462a2148119447 Mon Sep 17 00:00:00 2001 From: Sibren Vasse Date: Thu, 14 May 2020 13:18:41 +0200 Subject: [PATCH 21/33] Fix space error footer --- templates/footer.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/templates/footer.html b/templates/footer.html index 19645413..407bc7eb 100644 --- a/templates/footer.html +++ b/templates/footer.html @@ -33,8 +33,7 @@
Copyright © {{ YEAR }} - SimpleLogin - . + SimpleLogin. All rights reserved.
From cb269a1bbe861d2dfa89c7e22bd6565c0e350811 Mon Sep 17 00:00:00 2001 From: Sibren Vasse Date: Thu, 14 May 2020 13:49:53 +0200 Subject: [PATCH 22/33] Change pagination style --- app/dashboard/templates/dashboard/alias_log.html | 4 ++-- app/dashboard/templates/dashboard/index.html | 14 ++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/dashboard/templates/dashboard/alias_log.html b/app/dashboard/templates/dashboard/alias_log.html index 66cc21d3..fea2bbae 100644 --- a/app/dashboard/templates/dashboard/alias_log.html +++ b/app/dashboard/templates/dashboard/alias_log.html @@ -146,11 +146,11 @@ diff --git a/app/dashboard/templates/dashboard/index.html b/app/dashboard/templates/dashboard/index.html index 71471fc1..6985b6f7 100644 --- a/app/dashboard/templates/dashboard/index.html +++ b/app/dashboard/templates/dashboard/index.html @@ -400,13 +400,15 @@
From e44d92705c1b64d4b0ede5f141758e312909d6b6 Mon Sep 17 00:00:00 2001 From: Sibren Vasse Date: Thu, 14 May 2020 13:50:17 +0200 Subject: [PATCH 23/33] Add missing last_page check (index) Move disabled class to correct element (alias_log) --- app/dashboard/templates/dashboard/alias_log.html | 9 +++++---- app/dashboard/views/index.py | 13 ++++++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app/dashboard/templates/dashboard/alias_log.html b/app/dashboard/templates/dashboard/alias_log.html index fea2bbae..aca5a498 100644 --- a/app/dashboard/templates/dashboard/alias_log.html +++ b/app/dashboard/templates/dashboard/alias_log.html @@ -145,12 +145,13 @@ diff --git a/app/dashboard/views/index.py b/app/dashboard/views/index.py index fc314c50..d8cff532 100644 --- a/app/dashboard/views/index.py +++ b/app/dashboard/views/index.py @@ -6,6 +6,7 @@ from sqlalchemy.orm import joinedload from app import alias_utils from app.api.serializer import get_alias_infos_with_pagination_v2 +from app.config import PAGE_LIMIT from app.dashboard.base import dashboard_bp from app.extensions import db from app.log import LOG @@ -140,18 +141,24 @@ def index(): stats = get_stats(current_user) + alias_infos = get_alias_infos_with_pagination_v2( + current_user, page, query, sort, alias_filter + ) + last_page = ( + len(alias_infos) < PAGE_LIMIT + ) + return render_template( "dashboard/index.html", client_users=client_users, - alias_infos=get_alias_infos_with_pagination_v2( - current_user, page, query, sort, alias_filter - ), + alias_infos=alias_infos, highlight_alias_id=highlight_alias_id, query=query, AliasGeneratorEnum=AliasGeneratorEnum, mailboxes=mailboxes, show_intro=show_intro, page=page, + last_page=last_page, sort=sort, filter=alias_filter, stats=stats, From c0041d55bc8202487473ed0a24368cb309e44cca Mon Sep 17 00:00:00 2001 From: Sibren Vasse Date: Thu, 14 May 2020 14:23:29 +0200 Subject: [PATCH 24/33] Set input background to white for light theme --- static/assets/css/darkmode.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/assets/css/darkmode.css b/static/assets/css/darkmode.css index eefa1600..9a7b5b4f 100644 --- a/static/assets/css/darkmode.css +++ b/static/assets/css/darkmode.css @@ -9,7 +9,7 @@ --heading-color: #818cab; --heading-background: #FFF; --border: 1px solid rgba(0, 40, 100, 0.12); - --input-bg-color: var(--light); + --input-bg-color: var(--white); } [data-theme="dark"] { From 012bc52694d2a6088142a64164f3cf9175bdf647 Mon Sep 17 00:00:00 2001 From: Sibren Vasse Date: Thu, 14 May 2020 14:29:36 +0200 Subject: [PATCH 25/33] Fix formatting --- app/dashboard/views/index.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/dashboard/views/index.py b/app/dashboard/views/index.py index d8cff532..2d9f1df6 100644 --- a/app/dashboard/views/index.py +++ b/app/dashboard/views/index.py @@ -1,5 +1,4 @@ from dataclasses import dataclass - from flask import render_template, request, redirect, url_for, flash from flask_login import login_required, current_user from sqlalchemy.orm import joinedload @@ -144,9 +143,7 @@ def index(): alias_infos = get_alias_infos_with_pagination_v2( current_user, page, query, sort, alias_filter ) - last_page = ( - len(alias_infos) < PAGE_LIMIT - ) + last_page = len(alias_infos) < PAGE_LIMIT return render_template( "dashboard/index.html", From 355b4dc2cf1522114c85d7ee0365d3ad57fab480 Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Fri, 15 May 2020 15:30:19 +0200 Subject: [PATCH 26/33] remove too verbose log --- app/api/views/apple.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/api/views/apple.py b/app/api/views/apple.py index afa54ce9..2b0979ce 100644 --- a/app/api/views/apple.py +++ b/app/api/views/apple.py @@ -313,7 +313,6 @@ def verify_receipt(receipt_data, user, password) -> Optional[AppleSubscription]: ) data = r.json() - LOG.d("response from Apple %s", data) # data has the following format # { # "status": 0, From c8f1244d819af89db6b109929af6efc09d72da21 Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Fri, 15 May 2020 15:31:58 +0200 Subject: [PATCH 27/33] optimize cron job --- cron.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/cron.py b/cron.py index 407bfb4e..e825b6cf 100644 --- a/cron.py +++ b/cron.py @@ -163,17 +163,16 @@ def stats_before(moment: Arrow) -> Stats: LOG.d("total number alias %s", nb_alias) # email log stats - q = db.session.query(EmailLog, Contact, Alias, User).filter( - EmailLog.contact_id == Contact.id, - Contact.alias_id == Alias.id, - Alias.user_id == User.id, - EmailLog.created_at < moment, + q = ( + db.session.query(EmailLog) + .join(User, EmailLog.user_id == User.id) + .filter(EmailLog.created_at < moment,) ) for ie in IGNORED_EMAILS: q = q.filter(~User.email.contains(ie)) nb_spam = nb_bounced = nb_forward = nb_block = nb_reply = 0 - for email_log, _, _, _ in q: + for email_log in q: if email_log.bounced: nb_bounced += 1 elif email_log.is_spam: From 3d4b44dd151a0e8a118117ca41d285ddcbdc8af4 Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Fri, 15 May 2020 15:46:37 +0200 Subject: [PATCH 28/33] handle the case contact_from_header can be None --- email_handler.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/email_handler.py b/email_handler.py index 19bc9c7b..b67f654e 100644 --- a/email_handler.py +++ b/email_handler.py @@ -123,13 +123,23 @@ def get_or_create_contact( """ contact_from_header is the RFC 2047 format FROM header """ + # contact_from_header can be None, use mail_from in this case instead + contact_from_header = contact_from_header or mail_from + # force convert header to string, sometimes contact_from_header is Header object contact_from_header = str(contact_from_header) + contact_name, contact_email = parseaddr_unicode(contact_from_header) if not contact_email: - # From header is empty, try with mail_from + # From header is wrongly formatted, try with mail_from LOG.warning("From header is empty, parse mail_from %s %s", mail_from, alias) contact_name, contact_email = parseaddr_unicode(mail_from) + if not contact_email: + LOG.error( + "Cannot parse contact from from_header:%s, mail_from:%s", + contact_from_header, + mail_from, + ) contact = Contact.get_by(alias_id=alias.id, website_email=contact_email) if contact: From 8769383724f35c58547539527d27a2c62d61e06e Mon Sep 17 00:00:00 2001 From: Sibren Vasse Date: Fri, 15 May 2020 16:34:07 +0200 Subject: [PATCH 29/33] Also enable spam check when pgp is enabled --- email_handler.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/email_handler.py b/email_handler.py index 19bc9c7b..afed1d4f 100644 --- a/email_handler.py +++ b/email_handler.py @@ -356,26 +356,20 @@ def handle_forward(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> (bool, s db.session.commit() return True, "250 Message accepted for delivery" - spam_check = True + is_spam, spam_status = get_spam_info(msg) + if is_spam: + LOG.warning("Email detected as spam. Alias: %s, from: %s", alias, contact) + email_log.is_spam = True + email_log.spam_status = spam_status + + handle_spam(contact, alias, msg, user, mailbox_email, email_log) + return False, "550 SL E1" # create PGP email if needed if mailbox.pgp_finger_print and user.is_premium(): LOG.d("Encrypt message using mailbox %s", mailbox) msg = prepare_pgp_message(msg, mailbox.pgp_finger_print) - # no need to spam check for encrypted message - spam_check = False - - if spam_check: - is_spam, spam_status = get_spam_info(msg) - if is_spam: - LOG.warning("Email detected as spam. Alias: %s, from: %s", alias, contact) - email_log.is_spam = True - email_log.spam_status = spam_status - - handle_spam(contact, alias, msg, user, mailbox_email, email_log) - return False, "550 SL E1" - # add custom header add_or_replace_header(msg, "X-SimpleLogin-Type", "Forward") From 2978bfb281450bfcbb8bb21a0d2f75c43b6e5823 Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Fri, 15 May 2020 23:18:30 +0200 Subject: [PATCH 30/33] Fix user cannot change personal email back and better naming. Happens when user - changes their personal email - wants to change back: they can't as this email is already used as mailbox --- app/api/views/auth.py | 10 +++++----- app/auth/views/register.py | 6 +++--- app/dashboard/views/mailbox.py | 4 ++-- app/dashboard/views/mailbox_detail.py | 4 ++-- app/dashboard/views/setting.py | 6 +++--- app/email_utils.py | 14 +++++--------- tests/test_email_utils.py | 22 +++++++++++----------- 7 files changed, 31 insertions(+), 35 deletions(-) diff --git a/app/api/views/auth.py b/app/api/views/auth.py index e85dd017..75aa54f5 100644 --- a/app/api/views/auth.py +++ b/app/api/views/auth.py @@ -12,8 +12,8 @@ from app.api.base import api_bp from app.config import FLASK_SECRET, DISABLE_REGISTRATION from app.dashboard.views.setting import send_reset_password_email from app.email_utils import ( - can_be_used_as_personal_email, - email_already_used, + email_domain_can_be_used_as_mailbox, + personal_email_already_used, send_email, render, ) @@ -84,7 +84,7 @@ def auth_register(): if DISABLE_REGISTRATION: return jsonify(error="registration is closed"), 400 - if not can_be_used_as_personal_email(email) or email_already_used(email): + if not email_domain_can_be_used_as_mailbox(email) or personal_email_already_used(email): return jsonify(error=f"cannot use {email} as personal inbox"), 400 if not password or len(password) < 8: @@ -236,7 +236,7 @@ def auth_facebook(): if not user: if DISABLE_REGISTRATION: return jsonify(error="registration is closed"), 400 - if not can_be_used_as_personal_email(email) or email_already_used(email): + if not email_domain_can_be_used_as_mailbox(email) or personal_email_already_used(email): return jsonify(error=f"cannot use {email} as personal inbox"), 400 LOG.d("create facebook user with %s", user_info) @@ -288,7 +288,7 @@ def auth_google(): if not user: if DISABLE_REGISTRATION: return jsonify(error="registration is closed"), 400 - if not can_be_used_as_personal_email(email) or email_already_used(email): + if not email_domain_can_be_used_as_mailbox(email) or personal_email_already_used(email): return jsonify(error=f"cannot use {email} as personal inbox"), 400 LOG.d("create Google user with %s", user_info) diff --git a/app/auth/views/register.py b/app/auth/views/register.py index e4caa022..ee72ef96 100644 --- a/app/auth/views/register.py +++ b/app/auth/views/register.py @@ -7,7 +7,7 @@ from app import email_utils, config from app.auth.base import auth_bp from app.auth.views.login_utils import get_referral from app.config import URL -from app.email_utils import can_be_used_as_personal_email, email_already_used +from app.email_utils import email_domain_can_be_used_as_mailbox, personal_email_already_used from app.extensions import db from app.log import LOG from app.models import User, ActivationCode @@ -37,10 +37,10 @@ def register(): if form.validate_on_submit(): email = form.email.data.strip().lower() - if not can_be_used_as_personal_email(email): + if not email_domain_can_be_used_as_mailbox(email): flash("You cannot use this email address as your personal inbox.", "error") else: - if email_already_used(email): + if personal_email_already_used(email): flash(f"Email {email} already used", "error") else: LOG.debug("create user %s", form.email.data) diff --git a/app/dashboard/views/mailbox.py b/app/dashboard/views/mailbox.py index 1174d5fe..9e02e0b1 100644 --- a/app/dashboard/views/mailbox.py +++ b/app/dashboard/views/mailbox.py @@ -8,7 +8,7 @@ from wtforms.fields.html5 import EmailField from app.config import EMAIL_DOMAIN, ALIAS_DOMAINS, MAILBOX_SECRET, URL from app.dashboard.base import dashboard_bp from app.email_utils import ( - can_be_used_as_personal_email, + email_domain_can_be_used_as_mailbox, mailbox_already_used, render, send_email, @@ -86,7 +86,7 @@ def mailbox_route(): if mailbox_already_used(mailbox_email, current_user): flash(f"{mailbox_email} already used", "error") - elif not can_be_used_as_personal_email(mailbox_email): + elif not email_domain_can_be_used_as_mailbox(mailbox_email): flash(f"You cannot use {mailbox_email}.", "error") else: new_mailbox = Mailbox.create( diff --git a/app/dashboard/views/mailbox_detail.py b/app/dashboard/views/mailbox_detail.py index b28057f2..01bb8aa8 100644 --- a/app/dashboard/views/mailbox_detail.py +++ b/app/dashboard/views/mailbox_detail.py @@ -10,7 +10,7 @@ from wtforms.fields.html5 import EmailField from app.config import ENFORCE_SPF, MAILBOX_SECRET from app.config import URL from app.dashboard.base import dashboard_bp -from app.email_utils import can_be_used_as_personal_email +from app.email_utils import email_domain_can_be_used_as_mailbox from app.email_utils import mailbox_already_used, render, send_email from app.extensions import db from app.log import LOG @@ -54,7 +54,7 @@ def mailbox_detail_route(mailbox_id): or DeletedAlias.get_by(email=new_email) ): flash(f"Email {new_email} already used", "error") - elif not can_be_used_as_personal_email(new_email): + elif not email_domain_can_be_used_as_mailbox(new_email): flash("You cannot use this email address as your mailbox", "error") else: mailbox.new_email = new_email diff --git a/app/dashboard/views/setting.py b/app/dashboard/views/setting.py index aa647112..9879cf15 100644 --- a/app/dashboard/views/setting.py +++ b/app/dashboard/views/setting.py @@ -12,7 +12,7 @@ from wtforms.fields.html5 import EmailField from app import s3, email_utils from app.config import URL from app.dashboard.base import dashboard_bp -from app.email_utils import can_be_used_as_personal_email, email_already_used +from app.email_utils import email_domain_can_be_used_as_mailbox, personal_email_already_used from app.extensions import db from app.log import LOG from app.models import ( @@ -70,12 +70,12 @@ def setting(): # check if this email is not already used if ( - email_already_used(new_email) + personal_email_already_used(new_email) or Alias.get_by(email=new_email) or DeletedAlias.get_by(email=new_email) ): flash(f"Email {new_email} already used", "error") - elif not can_be_used_as_personal_email(new_email): + elif not email_domain_can_be_used_as_mailbox(new_email): flash( "You cannot use this email address as your personal inbox.", "error", diff --git a/app/email_utils.py b/app/email_utils.py index 7dc60a03..4ddf865c 100644 --- a/app/email_utils.py +++ b/app/email_utils.py @@ -346,10 +346,11 @@ def email_belongs_to_alias_domains(address: str) -> bool: return False -def can_be_used_as_personal_email(email: str) -> bool: - """return True if an email can be used as a personal email. Currently the only condition is email domain is not +def email_domain_can_be_used_as_mailbox(email: str) -> bool: + """return True if an email can be used as a personal email. An email domain can be used if it is not - one of ALIAS_DOMAINS - one of custom domains + - disposable domain """ domain = get_email_domain_part(email) if not domain: @@ -402,17 +403,12 @@ def get_mx_domain_list(domain) -> [str]: return [d[:-1] for _, d in priority_domains] -def email_already_used(email: str) -> bool: - """test if an email can be used when: - - user signs up - - add a new mailbox +def personal_email_already_used(email: str) -> bool: + """test if an email can be used as user email """ if User.get_by(email=email): return True - if Mailbox.get_by(email=email): - return True - return False diff --git a/tests/test_email_utils.py b/tests/test_email_utils.py index c043d58f..ef2cc522 100644 --- a/tests/test_email_utils.py +++ b/tests/test_email_utils.py @@ -4,7 +4,7 @@ from app.config import MAX_ALERT_24H from app.email_utils import ( get_email_domain_part, email_belongs_to_alias_domains, - can_be_used_as_personal_email, + email_domain_can_be_used_as_mailbox, delete_header, add_or_replace_header, parseaddr_unicode, @@ -29,10 +29,10 @@ def test_email_belongs_to_alias_domains(): def test_can_be_used_as_personal_email(flask_client): # default alias domain - assert not can_be_used_as_personal_email("ab@sl.local") - assert not can_be_used_as_personal_email("hey@d1.test") + assert not email_domain_can_be_used_as_mailbox("ab@sl.local") + assert not email_domain_can_be_used_as_mailbox("hey@d1.test") - assert can_be_used_as_personal_email("hey@ab.cd") + assert email_domain_can_be_used_as_mailbox("hey@ab.cd") # custom domain user = User.create( email="a@b.c", password="password", name="Test User", activated=True @@ -40,17 +40,17 @@ def test_can_be_used_as_personal_email(flask_client): db.session.commit() CustomDomain.create(user_id=user.id, domain="ab.cd", verified=True) db.session.commit() - assert not can_be_used_as_personal_email("hey@ab.cd") + assert not email_domain_can_be_used_as_mailbox("hey@ab.cd") # disposable domain - assert not can_be_used_as_personal_email("abcd@10minutesmail.fr") - assert not can_be_used_as_personal_email("abcd@temp-mail.com") + assert not email_domain_can_be_used_as_mailbox("abcd@10minutesmail.fr") + assert not email_domain_can_be_used_as_mailbox("abcd@temp-mail.com") # subdomain will not work - assert not can_be_used_as_personal_email("abcd@sub.temp-mail.com") + assert not email_domain_can_be_used_as_mailbox("abcd@sub.temp-mail.com") # valid domains should not be affected - assert can_be_used_as_personal_email("abcd@protonmail.com") - assert can_be_used_as_personal_email("abcd@gmail.com") - assert can_be_used_as_personal_email("abcd@example.com") + assert email_domain_can_be_used_as_mailbox("abcd@protonmail.com") + assert email_domain_can_be_used_as_mailbox("abcd@gmail.com") + assert email_domain_can_be_used_as_mailbox("abcd@example.com") def test_delete_header(): From 7ed77a66b21ab8a86155a451a92f5c9005035928 Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Fri, 15 May 2020 23:18:42 +0200 Subject: [PATCH 31/33] format --- app/api/views/auth.py | 12 +++++++++--- app/auth/views/register.py | 5 ++++- app/dashboard/views/setting.py | 5 ++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/api/views/auth.py b/app/api/views/auth.py index 75aa54f5..bbf150ab 100644 --- a/app/api/views/auth.py +++ b/app/api/views/auth.py @@ -84,7 +84,9 @@ def auth_register(): if DISABLE_REGISTRATION: return jsonify(error="registration is closed"), 400 - if not email_domain_can_be_used_as_mailbox(email) or personal_email_already_used(email): + if not email_domain_can_be_used_as_mailbox(email) or personal_email_already_used( + email + ): return jsonify(error=f"cannot use {email} as personal inbox"), 400 if not password or len(password) < 8: @@ -236,7 +238,9 @@ def auth_facebook(): if not user: if DISABLE_REGISTRATION: return jsonify(error="registration is closed"), 400 - if not email_domain_can_be_used_as_mailbox(email) or personal_email_already_used(email): + if not email_domain_can_be_used_as_mailbox( + email + ) or personal_email_already_used(email): return jsonify(error=f"cannot use {email} as personal inbox"), 400 LOG.d("create facebook user with %s", user_info) @@ -288,7 +292,9 @@ def auth_google(): if not user: if DISABLE_REGISTRATION: return jsonify(error="registration is closed"), 400 - if not email_domain_can_be_used_as_mailbox(email) or personal_email_already_used(email): + if not email_domain_can_be_used_as_mailbox( + email + ) or personal_email_already_used(email): return jsonify(error=f"cannot use {email} as personal inbox"), 400 LOG.d("create Google user with %s", user_info) diff --git a/app/auth/views/register.py b/app/auth/views/register.py index ee72ef96..a616771c 100644 --- a/app/auth/views/register.py +++ b/app/auth/views/register.py @@ -7,7 +7,10 @@ from app import email_utils, config from app.auth.base import auth_bp from app.auth.views.login_utils import get_referral from app.config import URL -from app.email_utils import email_domain_can_be_used_as_mailbox, personal_email_already_used +from app.email_utils import ( + email_domain_can_be_used_as_mailbox, + personal_email_already_used, +) from app.extensions import db from app.log import LOG from app.models import User, ActivationCode diff --git a/app/dashboard/views/setting.py b/app/dashboard/views/setting.py index 9879cf15..0a935667 100644 --- a/app/dashboard/views/setting.py +++ b/app/dashboard/views/setting.py @@ -12,7 +12,10 @@ from wtforms.fields.html5 import EmailField from app import s3, email_utils from app.config import URL from app.dashboard.base import dashboard_bp -from app.email_utils import email_domain_can_be_used_as_mailbox, personal_email_already_used +from app.email_utils import ( + email_domain_can_be_used_as_mailbox, + personal_email_already_used, +) from app.extensions import db from app.log import LOG from app.models import ( From c9b75c338e00a5082d65a03e9a5b39561ab9366b Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Fri, 15 May 2020 23:40:30 +0200 Subject: [PATCH 32/33] move theme.js to static/ and include it in base.html --- static/{assets => }/js/theme.js | 0 templates/base.html | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) rename static/{assets => }/js/theme.js (100%) diff --git a/static/assets/js/theme.js b/static/js/theme.js similarity index 100% rename from static/assets/js/theme.js rename to static/js/theme.js diff --git a/templates/base.html b/templates/base.html index 0f1827c8..c5e21753 100644 --- a/templates/base.html +++ b/templates/base.html @@ -170,7 +170,8 @@ - + + {% block script %} From a7a29ab8c9f8ee142cb7d2e4f71230b2e79943ba Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Fri, 15 May 2020 23:53:17 +0200 Subject: [PATCH 33/33] fix file name --- templates/base.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/base.html b/templates/base.html index c5e21753..975c183b 100644 --- a/templates/base.html +++ b/templates/base.html @@ -40,8 +40,6 @@ - - @@ -64,6 +62,8 @@ + + @@ -170,8 +170,8 @@ - - + + {% block script %}