diff --git a/README.md b/README.md index 7525cac3..b6ea063a 100644 --- a/README.md +++ b/README.md @@ -1262,5 +1262,7 @@ 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

+ Sibren Vasse
Sibren Vasse

diff --git a/app/api/base.py b/app/api/base.py index 36d9bd50..f7ef67c0 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,19 @@ 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 + + +@api_bp.app_errorhandler(405) +def wrong_method(e): + return jsonify(error="Method not allowed"), 405 diff --git a/app/api/views/apple.py b/app/api/views/apple.py index ac3b3810..2b0979ce 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, ) @@ -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, @@ -490,6 +489,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) diff --git a/app/api/views/auth.py b/app/api/views/auth.py index e85dd017..bbf150ab 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,9 @@ 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 +238,9 @@ 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 +292,9 @@ 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/templates/auth/fido.html b/app/auth/templates/auth/fido.html index 836dbc0d..34d67078 100644 --- a/app/auth/templates/auth/fido.html +++ b/app/auth/templates/auth/fido.html @@ -11,56 +11,62 @@ {% 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); + + {% if auto_activate %} + + {% endif %} + +
{% 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/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/app/auth/views/register.py b/app/auth/views/register.py index e4caa022..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 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 +40,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/templates/dashboard/alias_log.html b/app/dashboard/templates/dashboard/alias_log.html index 40a3b6bb..b26f75f5 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/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 e29bbc88..f30a7a26 100644 --- a/app/dashboard/templates/dashboard/custom_alias.html +++ b/app/dashboard/templates/dashboard/custom_alias.html @@ -7,86 +7,87 @@ {% 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(es) that owns this alias. +
+
+ +
+ The mailbox(es) that owns this alias. +
-
-
-
- +
+
+ +
-
- -
-
- Create +
+
+ Create +
-
- + +
{% 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/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: -

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/index.html b/app/dashboard/templates/dashboard/index.html index 66fcf271..7e286c52 100644 --- a/app/dashboard/templates/dashboard/index.html +++ b/app/dashboard/templates/dashboard/index.html @@ -400,13 +400,15 @@
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/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", ) diff --git a/app/dashboard/views/index.py b/app/dashboard/views/index.py index fc314c50..2d9f1df6 100644 --- a/app/dashboard/views/index.py +++ b/app/dashboard/views/index.py @@ -1,11 +1,11 @@ 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 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 +140,22 @@ 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, 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..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 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 +73,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 582d1aa1..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 @@ -503,7 +499,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 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 %} 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: diff --git a/email_handler.py b/email_handler.py index bf8cfcc0..128d8179 100644 --- a/email_handler.py +++ b/email_handler.py @@ -114,13 +114,30 @@ 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 """ + # 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 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: if contact.name != contact_name: @@ -327,7 +344,7 @@ def handle_forward( LOG.d("alias %s cannot be created on-the-fly, return 550", address) return [(False, "550 SL E3")] - 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: @@ -363,25 +380,20 @@ def forward_email_to_mailbox( ) -> (bool, str): LOG.d("Forward %s -> %s -> %s", contact, alias, mailbox) 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") @@ -616,9 +628,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 diff --git a/server.py b/server.py index 1508a42e..13651699 100644 --- a/server.py +++ b/server.py @@ -91,13 +91,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) @@ -140,6 +140,7 @@ def fake_data(): activated=True, is_admin=True, otp_secret="base32secret3232", + can_use_fido=True, ) db.session.commit() user.trial_end = None diff --git a/static/assets/css/darkmode.css b/static/assets/css/darkmode.css index b96687f0..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"] { @@ -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); 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/js/theme.js b/static/js/theme.js new file mode 100644 index 00000000..721b2556 --- /dev/null +++ b/static/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/static/style.css b/static/style.css index 0e89d84f..08dadf6d 100644 --- a/static/style.css +++ b/static/style.css @@ -72,14 +72,18 @@ em { } /*Left border for alert zone*/ -.alert-primary{ +.alert-primary { border-left: 5px #467fcf solid; } -.alert-danger{ +.alert-danger { border-left: 5px #6b1110 solid; } .alert-danger::before { - content: "⚠️"; -} \ No newline at end of file + content: "⚠️"; +} + +.dns-record { + border: 1px dotted #E3156A; +} diff --git a/templates/base.html b/templates/base.html index 70adda75..18dbb0f9 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,7 +1,7 @@ {% from "_formhelpers.html" import render_field, render_field_errors %} - + + + @@ -175,7 +177,8 @@ - + + {% block script %} 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 @@ 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 ---------------- diff --git a/templates/emails/com/newsletter/mobile-darkmode.html b/templates/emails/com/newsletter/mobile-darkmode.html index b4db6efd..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,16 +86,19 @@ {% 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 %} {% 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() %} 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.
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():