mirror of
https://github.com/simple-login/app.git
synced 2026-06-06 19:37:35 +00:00
Merge branch 'master' into multiple-mailboxes
# Conflicts: # app/dashboard/templates/dashboard/custom_alias.html # email_handler.py # templates/emails/com/newsletter/mobile-darkmode.html
This commit is contained in:
@@ -1262,5 +1262,7 @@ Thanks go to these wonderful people:
|
||||
<td align="center"><a href="https://github.com/NinhDinh"><img src="https://avatars2.githubusercontent.com/u/1419742?s=460&v=4" width="100px;" alt="Ninh Dinh"/><br /><sub><b>Ninh Dinh</b></sub></a><br /></td>
|
||||
<td align="center"><a href="https://github.com/ntung"><img src="https://avatars1.githubusercontent.com/u/663341?s=460&v=4" width="100px;" alt="Tung Nguyen V. N."/><br /><sub><b>Tung Nguyen V. N.</b></sub></a><br /></td>
|
||||
<td align="center"><a href="https://www.linkedin.com/in/nguyenkims/"><img src="https://simplelogin.io/about/me.jpeg" width="100px;" alt="Son Nguyen Kim"/><br /><sub><b>Son Nguyen Kim</b></sub></a><br /></td>
|
||||
<td align="center"><a href="https://github.com/developStorm"><img src="https://avatars1.githubusercontent.com/u/59678453?s=460&u=3813d29a125b3edeb44019234672b704f7b9b76a&v=4" width="100px;" alt="Raymond Nook"/><br /><sub><b>Raymond Nook</b></sub></a><br /></td>
|
||||
<td align="center"><a href="https://github.com/SibrenVasse"><img src="https://avatars1.githubusercontent.com/u/5833571?s=460&u=78aea62ffc215885a0319437fc629a7596ddea31&v=4" width="100px;" alt="Sibren Vasse"/><br /><sub><b>Sibren Vasse</b></sub></a><br /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
+11
-5
@@ -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)
|
||||
|
||||
@@ -11,56 +11,62 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block single_content %}
|
||||
<div class="bg-white p-6" style="margin: auto">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
|
||||
<div class="mb-2">
|
||||
Your account is protected with your security key (WebAuthn). <br><br>
|
||||
Follow your browser's steps to continue the sign-in process.
|
||||
</div>
|
||||
|
||||
<form id="formRegisterKey" method="post">
|
||||
{{ fido_token_form.csrf_token }}
|
||||
{{ fido_token_form.sk_assertion(class="form-control", placeholder="") }}
|
||||
</form>
|
||||
<div class="text-center">
|
||||
<button id="btnVerifyKey" class="btn btn-success mt-2">Use your security key</button>
|
||||
</div>
|
||||
|
||||
{% if enable_otp %}
|
||||
<div class="text-center text-muted mb-6" style="margin-top: 1em;">
|
||||
Don't have your key with you? <br> <a href="{{ url_for('auth.mfa') }}">Verify by One-Time Password</a>
|
||||
<div class="mb-2">
|
||||
Your account is protected with your security key (WebAuthn). <br><br>
|
||||
Follow your browser's steps to continue the sign-in process.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
async function verifyKey () {
|
||||
$("#btnVerifyKey").prop('disabled', true);
|
||||
$("#btnVerifyKey").text('Waiting for Security Key...');
|
||||
|
||||
const credentialRequestOptions = transformCredentialRequestOptions(
|
||||
JSON.parse('{{webauthn_assertion_options|tojson|safe}}')
|
||||
)
|
||||
<form id="formRegisterKey" method="post">
|
||||
{{ fido_token_form.csrf_token }}
|
||||
{{ fido_token_form.sk_assertion(class="form-control", placeholder="") }}
|
||||
</form>
|
||||
<div class="text-center">
|
||||
<button id="btnVerifyKey" class="btn btn-success mt-2">Use your security key</button>
|
||||
</div>
|
||||
|
||||
let assertion;
|
||||
try {
|
||||
assertion = await navigator.credentials.get({
|
||||
publicKey: credentialRequestOptions
|
||||
});
|
||||
} catch (err) {
|
||||
toastr.error("An error occurred when we trying to verify your key.");
|
||||
$("#btnVerifyKey").prop('disabled', false);
|
||||
$("#btnVerifyKey").text('Use your security key');
|
||||
return console.error("Error when trying to get credential:", err);
|
||||
{% if enable_otp %}
|
||||
<div class="text-center text-muted mb-6" style="margin-top: 1em;">
|
||||
Don't have your key with you? <br> <a href="{{ url_for('auth.mfa') }}">Verify by One-Time Password</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
async function verifyKey() {
|
||||
$("#btnVerifyKey").prop('disabled', true);
|
||||
$("#btnVerifyKey").text('Waiting for Security Key...');
|
||||
|
||||
const credentialRequestOptions = transformCredentialRequestOptions(
|
||||
JSON.parse('{{webauthn_assertion_options|tojson|safe}}')
|
||||
)
|
||||
|
||||
let assertion;
|
||||
try {
|
||||
assertion = await navigator.credentials.get({
|
||||
publicKey: credentialRequestOptions
|
||||
});
|
||||
} catch (err) {
|
||||
toastr.error("An error occurred when we trying to verify your key.");
|
||||
$("#btnVerifyKey").prop('disabled', false);
|
||||
$("#btnVerifyKey").text('Use your security key');
|
||||
return console.error("Error when trying to get credential:", err);
|
||||
}
|
||||
|
||||
const skAssertion = transformAssertionForServer(assertion);
|
||||
$('#sk_assertion').val(JSON.stringify(skAssertion));
|
||||
$('#formRegisterKey').submit();
|
||||
}
|
||||
|
||||
const skAssertion = transformAssertionForServer(assertion);
|
||||
$('#sk_assertion').val(JSON.stringify(skAssertion));
|
||||
$('#formRegisterKey').submit();
|
||||
}
|
||||
|
||||
$("#btnVerifyKey").click(verifyKey);
|
||||
</script>
|
||||
$("#btnVerifyKey").click(verifyKey);
|
||||
</script>
|
||||
|
||||
{% if auto_activate %}
|
||||
<script>$('document').ready(verifyKey());</script>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -7,33 +7,35 @@
|
||||
|
||||
|
||||
{% block single_content %}
|
||||
<div class="bg-white p-6" style="margin: auto">
|
||||
<div class="card">
|
||||
<div class="card-body p-6">
|
||||
|
||||
<div class="mb-2">
|
||||
Your account is protected with multi-factor authentication (MFA). <br><br>
|
||||
To continue with the sign-in you need to provide the access code from your authenticator.
|
||||
<div class="mb-2">
|
||||
Your account is protected with multi-factor authentication (MFA). <br><br>
|
||||
To continue with the sign-in you need to provide the access code from your authenticator.
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
{{ otp_token_form.csrf_token }}
|
||||
<input type="hidden" name="form-name" value="create">
|
||||
|
||||
<div class="font-weight-bold mt-5">Token</div>
|
||||
<div class="small-text">Please enter the 6-digit number displayed in your MFA application
|
||||
(Google Authenticator, Authy, MyDigiPassword, etc) here
|
||||
</div>
|
||||
|
||||
{{ otp_token_form.token(class="form-control", autofocus="true") }}
|
||||
{{ render_field_errors(otp_token_form.token) }}
|
||||
<button class="btn btn-success mt-2">Validate</button>
|
||||
</form>
|
||||
|
||||
{% if enable_fido %}
|
||||
<div class="text-center text-muted mb-6" style="margin-top: 1em;">
|
||||
Having trouble with your authenticator? <br> <a href="{{ url_for('auth.fido') }}">Verify by your security
|
||||
key</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
{{ otp_token_form.csrf_token }}
|
||||
<input type="hidden" name="form-name" value="create">
|
||||
|
||||
<div class="font-weight-bold mt-5">Token</div>
|
||||
<div class="small-text">Please enter the 6-digit number displayed in your MFA application
|
||||
(Google Authenticator, Authy, MyDigiPassword, etc) here
|
||||
</div>
|
||||
|
||||
{{ otp_token_form.token(class="form-control", autofocus="true") }}
|
||||
{{ render_field_errors(otp_token_form.token) }}
|
||||
<button class="btn btn-success mt-2">Validate</button>
|
||||
</form>
|
||||
|
||||
{% if enable_fido %}
|
||||
<div class="text-center text-muted mb-6" style="margin-top: 1em;">
|
||||
Having trouble with your authenticator? <br> <a href="{{ url_for('auth.fido') }}">Verify by your security key</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -145,12 +145,13 @@
|
||||
|
||||
<nav aria-label="Alias log navigation">
|
||||
<ul class="pagination">
|
||||
<li class="page-item {% if page_id == 0 %}disabled{% endif %}">
|
||||
<a class="page-link"
|
||||
<li class="page-item">
|
||||
<a class="btn btn-outline-secondary {% if page_id == 0 %}disabled{% endif %}"
|
||||
href="{{ url_for('dashboard.alias_log', alias_id=alias_id, page_id=page_id-1) }}">Previous</a>
|
||||
</li>
|
||||
<li class="page-item {% if last_page %}disabled{% endif %}">
|
||||
<a class="page-link" href="{{ url_for('dashboard.alias_log', alias_id=alias_id, page_id=page_id+1) }}">Next</a>
|
||||
<li class="page-item">
|
||||
<a class="btn btn-outline-secondary {% if last_page %}disabled{% endif %}"
|
||||
href="{{ url_for('dashboard.alias_log', alias_id=alias_id, page_id=page_id+1) }}">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@@ -8,77 +8,79 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block default_content %}
|
||||
<div class="bg-white p-6" style="max-width: 60em; margin: auto">
|
||||
<h1 class="h3 mb-5"> Billing </h1>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h1 class="h3 mb-5"> Billing </h1>
|
||||
|
||||
{% if sub.cancelled %}
|
||||
<p>
|
||||
You are on the <b>{{ sub.plan_name() }}</b> plan. <br>
|
||||
You have canceled your subscription and it will end on {{ sub.next_bill_date.strftime("%Y-%m-%d") }}
|
||||
</p>
|
||||
{% if sub.cancelled %}
|
||||
<p>
|
||||
You are on the <b>{{ sub.plan_name() }}</b> plan. <br>
|
||||
You have canceled your subscription and it will end on {{ sub.next_bill_date.strftime("%Y-%m-%d") }}
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
<p>
|
||||
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 <b>immediately</b>.
|
||||
<br>
|
||||
<hr>
|
||||
<p>
|
||||
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 <b>immediately</b>.
|
||||
<br>
|
||||
|
||||
We are going to send you an email by the end of the subscription so maybe you can upgrade at that time.
|
||||
<br>
|
||||
<a href="{{ url_for('dashboard.pricing') }}" class="btn btn-primary mt-2">Re-subscribe</a>
|
||||
</p>
|
||||
We are going to send you an email by the end of the subscription so maybe you can upgrade at that time.
|
||||
<br>
|
||||
<a href="{{ url_for('dashboard.pricing') }}" class="btn btn-primary mt-2">Re-subscribe</a>
|
||||
</p>
|
||||
|
||||
{% else %}
|
||||
<p>
|
||||
You are on the <b>{{ sub.plan_name() }}</b> plan. Thank you very much for supporting
|
||||
SimpleLogin. 🙌 <br>
|
||||
The next billing cycle starts at {{ sub.next_bill_date.strftime("%Y-%m-%d") }}.
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
You are on the <b>{{ sub.plan_name() }}</b> plan. Thank you very much for supporting
|
||||
SimpleLogin. 🙌 <br>
|
||||
The next billing cycle starts at {{ sub.next_bill_date.strftime("%Y-%m-%d") }}.
|
||||
</p>
|
||||
|
||||
<div class="mt-3">
|
||||
Click here to update billing information on Paddle, our payment partner: <br>
|
||||
<a class="btn btn-outline-success mt-2" href="{{ sub.update_url }}"> Update billing information </a>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="mt-6">
|
||||
<h4>Change Plan</h4>
|
||||
You can change the plan at any moment. <br>
|
||||
Please note that the new billing cycle starts instantly
|
||||
i.e. you will be charged <b>immediately</b> the annual fee when switching from monthly plan or vice-versa
|
||||
<b>without pro rata computation </b>. <br>
|
||||
<div class="mt-3">
|
||||
Click here to update billing information on Paddle, our payment partner: <br>
|
||||
<a class="btn btn-outline-success mt-2" href="{{ sub.update_url }}"> Update billing information </a>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="mt-6">
|
||||
<h4>Change Plan</h4>
|
||||
You can change the plan at any moment. <br>
|
||||
Please note that the new billing cycle starts instantly
|
||||
i.e. you will be charged <b>immediately</b> the annual fee when switching from monthly plan or vice-versa
|
||||
<b>without pro rata computation </b>. <br>
|
||||
|
||||
To change the plan you can also cancel the current one and subscribe a new one <b>by the end</b> of this plan.
|
||||
To change the plan you can also cancel the current one and subscribe a new one <b>by the end</b> of this plan.
|
||||
|
||||
{% if sub.plan == PlanEnum.yearly %}
|
||||
<form method="post">
|
||||
<input type="hidden" name="form-name" value="change-monthly">
|
||||
<button class="btn btn-outline-primary mt-2">Change to Monthly Plan</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form method="post">
|
||||
<input type="hidden" name="form-name" value="change-yearly">
|
||||
<button class="btn btn-outline-primary mt-2">Change to Yearly Plan</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div>
|
||||
<h4>Cancel subscription</h4>
|
||||
Don't want to protect your inbox anymore? <br>
|
||||
|
||||
{% if sub.plan == PlanEnum.yearly %}
|
||||
<form method="post">
|
||||
<input type="hidden" name="form-name" value="change-monthly">
|
||||
<button class="btn btn-outline-primary mt-2">Change to Monthly Plan</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form method="post">
|
||||
<input type="hidden" name="form-name" value="change-yearly">
|
||||
<button class="btn btn-outline-primary mt-2">Change to Yearly Plan</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
<input type="hidden" name="form-name" value="cancel">
|
||||
|
||||
<hr>
|
||||
|
||||
<div>
|
||||
<h4>Cancel subscription</h4>
|
||||
Don't want to protect your inbox anymore? <br>
|
||||
|
||||
<form method="post">
|
||||
<input type="hidden" name="form-name" value="cancel">
|
||||
|
||||
<span class="cancel btn btn-outline-danger mt-2">
|
||||
<span class="cancel btn btn-outline-danger mt-2">
|
||||
Cancel subscription <i class="fe fe-alert-triangle text-danger"></i>
|
||||
</span>
|
||||
</form>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -7,86 +7,87 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block default_content %}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h1 class="h3">New Email Alias</h1>
|
||||
|
||||
<div class="bg-white p-6 mt-5" style="max-width: 55em; margin: auto">
|
||||
<h1 class="h3 mb-5">New Email Alias</h1>
|
||||
|
||||
{% if user_custom_domains|length == 0 and not DISABLE_ALIAS_SUFFIX %}
|
||||
<div class="row">
|
||||
<div class="col p-1">
|
||||
<div class="alert alert-primary" role="alert">
|
||||
You might notice a random word after the dot(<em>.</em>) in the alias.
|
||||
This part is to avoid a person taking all the "nice" aliases like <b>hello@{{ FIRST_ALIAS_DOMAIN }}</b>,
|
||||
<b>me@{{ FIRST_ALIAS_DOMAIN }}</b>, etc. <br>
|
||||
If you add your own domain, this restriction is removed and you can fully customize the alias. <br>
|
||||
{% if user_custom_domains|length == 0 and not DISABLE_ALIAS_SUFFIX %}
|
||||
<div class="row">
|
||||
<div class="col p-1">
|
||||
<div class="alert alert-primary" role="alert">
|
||||
You might notice a random word after the dot(<em>.</em>) in the alias.
|
||||
This part is to avoid a person taking all the "nice" aliases like
|
||||
<b>hello@{{ FIRST_ALIAS_DOMAIN }}</b>,
|
||||
<b>me@{{ FIRST_ALIAS_DOMAIN }}</b>, etc. <br>
|
||||
If you add your own domain, this restriction is removed and you can fully customize the alias. <br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<form method="post">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6 mb-1 p-1" style="min-width: 4em">
|
||||
<input name="prefix" class="form-control"
|
||||
id="prefix"
|
||||
type="text"
|
||||
pattern="[0-9a-z-_]{1,}"
|
||||
title="Only lowercase letter, number, dash (-), underscore (_) can be used in alias prefix."
|
||||
placeholder="email alias, for example newsletter-123_xyz"
|
||||
autofocus required>
|
||||
<div class="small-text">
|
||||
Only lowercase letter, number, dash (-), underscore (_) can be used.
|
||||
<form method="post">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6 mb-1 p-1" style="min-width: 4em">
|
||||
<input name="prefix" class="form-control"
|
||||
id="prefix"
|
||||
type="text"
|
||||
pattern="[0-9a-z-_]{1,}"
|
||||
title="Only lowercase letter, number, dash (-), underscore (_) can be used in alias prefix."
|
||||
placeholder="email alias, for example newsletter-123_xyz"
|
||||
autofocus required>
|
||||
<div class="small-text">
|
||||
Only lowercase letter, number, dash (-), underscore (_) can be used.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-sm-6 p-1">
|
||||
<select class="form-control" name="suffix">
|
||||
{% for suffix in suffixes %}
|
||||
<option value="{{ suffix[2] }}">
|
||||
{% if suffix[0] %}
|
||||
{{ suffix[1] }} (your domain)
|
||||
{% else %}
|
||||
{{ suffix[1] }}
|
||||
{% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-sm-6 p-1">
|
||||
<select class="form-control" name="suffix">
|
||||
{% for suffix in suffixes %}
|
||||
<option value="{{ suffix[2] }}">
|
||||
{% if suffix[0] %}
|
||||
{{ suffix[1] }} (your domain)
|
||||
{% else %}
|
||||
{{ suffix[1] }}
|
||||
{% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col p-1">
|
||||
<select class="form-control custom-select selectpicker" id="mailboxes" multiple name="mailboxes">
|
||||
{% for mailbox in mailboxes %}
|
||||
<option value="{{ mailbox.id }}" {% if mailbox.id == current_user.default_mailbox_id %}
|
||||
selected {% endif %}>
|
||||
{{ mailbox.email }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="small-text">
|
||||
The mailbox(es) that owns this alias.
|
||||
<div class="row mb-2">
|
||||
<div class="col p-1">
|
||||
<select class="form-control custom-select selectpicker" id="mailboxes" multiple name="mailboxes">
|
||||
{% for mailbox in mailboxes %}
|
||||
<option value="{{ mailbox.id }}" {% if mailbox.id == current_user.default_mailbox_id %}
|
||||
selected {% endif %}>
|
||||
{{ mailbox.email }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="small-text">
|
||||
The mailbox(es) that owns this alias.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col p-1">
|
||||
<textarea name="note"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
placeholder="Note, can be anything to help you remember WHY you create this alias. This field is optional."></textarea>
|
||||
<div class="row mb-2">
|
||||
<div class="col p-1">
|
||||
<textarea name="note"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
placeholder="Note, can be anything to help you remember WHY you create this alias. This field is optional."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col p-1">
|
||||
<span id="submit" class="btn btn-primary mt-1">Create</span>
|
||||
<div class="row">
|
||||
<div class="col p-1">
|
||||
<span id="submit" class="btn btn-primary mt-1">Create</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
2️⃣ Quickly use one of the following formats to create an alias on-the-fly <b>without creating this alias
|
||||
beforehand</b>
|
||||
</div>
|
||||
<div class="pl-3 py-2 bg-white">
|
||||
<div class="pl-3 py-2 bg-secondary">
|
||||
<em>my_dir/<b>anything</b>@{{ FIRST_ALIAS_DOMAIN }}</em> or <br>
|
||||
<em>my_dir+<b>anything</b>@{{ FIRST_ALIAS_DOMAIN }}</em> or <br>
|
||||
<em>my_dir#<b>anything</b>@{{ FIRST_ALIAS_DOMAIN }}</em> <br>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
</div>
|
||||
|
||||
{% for priority, email_server in EMAIL_SERVERS_WITH_PRIORITY %}
|
||||
<div class="mb-3 p-3 bg-secondary">
|
||||
<div class="mb-3 p-3 dns-record">
|
||||
Record: MX <br>
|
||||
Domain: {{ custom_domain.domain }} or @ <br>
|
||||
Priority: {{ priority }} <br>
|
||||
@@ -62,7 +62,7 @@
|
||||
{% if not mx_ok %}
|
||||
<div class="text-danger mt-4">
|
||||
Your DNS is not correctly set. The MX record we obtain is:
|
||||
<div class="mb-3 p-3 bg-secondary">
|
||||
<div class="mb-3 p-3 dns-record">
|
||||
{% if not mx_errors %}
|
||||
(Empty)
|
||||
{% endif %}
|
||||
@@ -97,7 +97,7 @@
|
||||
|
||||
<div class="mb-2">Add the following TXT DNS record to your domain.</div>
|
||||
|
||||
<div class="mb-2 p-3 bg-secondary">
|
||||
<div class="mb-2 p-3 dns-record">
|
||||
Record: TXT <br>
|
||||
Domain: {{ custom_domain.domain }} or @ <br>
|
||||
Value:
|
||||
@@ -125,7 +125,7 @@
|
||||
{% if not spf_ok %}
|
||||
<div class="text-danger mt-4">
|
||||
Your DNS is not correctly set. The TXT record we obtain is:
|
||||
<div class="mb-3 p-3 bg-secondary">
|
||||
<div class="mb-3 p-3 dns-record">
|
||||
{% if not spf_errors %}
|
||||
(Empty)
|
||||
{% endif %}
|
||||
@@ -162,7 +162,7 @@
|
||||
|
||||
<div class="mb-2">Add the following CNAME DNS record to your domain.</div>
|
||||
|
||||
<div class="mb-2 p-3 bg-secondary">
|
||||
<div class="mb-2 p-3 dns-record">
|
||||
Record: CNAME <br>
|
||||
Domain: <em data-toggle="tooltip"
|
||||
title="Click to copy"
|
||||
@@ -197,7 +197,7 @@
|
||||
The CNAME record we obtain for
|
||||
<em>dkim._domainkey.{{ custom_domain.domain }}</em> is:
|
||||
|
||||
<div class="mb-3 p-3 bg-secondary">
|
||||
<div class="mb-3 p-3 dns-record">
|
||||
{% for r in dkim_errors %}
|
||||
{{ r }} <br>
|
||||
{% endfor %}
|
||||
@@ -231,7 +231,7 @@
|
||||
|
||||
<div class="mb-2">Add the following TXT DNS record to your domain.</div>
|
||||
|
||||
<div class="mb-2 p-3 bg-secondary">
|
||||
<div class="mb-2 p-3 dns-record">
|
||||
Record: TXT <br>
|
||||
Domain: <em data-toggle="tooltip"
|
||||
title="Click to copy"
|
||||
|
||||
@@ -6,22 +6,23 @@
|
||||
|
||||
|
||||
{% block default_content %}
|
||||
<div class="bg-white p-6" style="max-width: 60em; margin: auto">
|
||||
<h1 class="h2">Unlink Your Security Key</h1>
|
||||
<p>
|
||||
Please enter the password of your account so that we can ensure it's you.
|
||||
</p>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h1 class="h2">Unlink Your Security Key</h1>
|
||||
<p>
|
||||
Please enter the password of your account so that we can ensure it's you.
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
{{ password_check_form.csrf_token }}
|
||||
<form method="post">
|
||||
{{ password_check_form.csrf_token }}
|
||||
|
||||
<div class="font-weight-bold mt-5">Password</div>
|
||||
|
||||
{{ password_check_form.password(class="form-control", autofocus="true") }}
|
||||
{{ render_field_errors(password_check_form.password) }}
|
||||
<button class="btn btn-lg btn-danger mt-2">Unlink Key</button>
|
||||
</form>
|
||||
<div class="font-weight-bold mt-5">Password</div>
|
||||
|
||||
{{ password_check_form.password(class="form-control", autofocus="true") }}
|
||||
{{ render_field_errors(password_check_form.password) }}
|
||||
<button class="btn btn-lg btn-danger mt-2">Unlink Key</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -11,48 +11,50 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block default_content %}
|
||||
<div class="bg-white p-6" style="max-width: 60em; margin: auto">
|
||||
<h1 class="h2 text-center">Register Your Security Key</h1>
|
||||
<p class="text-center">Follow your browser's steps to register your security key with SimpleLogin</p>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h1 class="h2 text-center">Register Your Security Key</h1>
|
||||
<p class="text-center">Follow your browser's steps to register your security key with SimpleLogin</p>
|
||||
|
||||
<form id="formRegisterKey" method="post">
|
||||
{{ fido_token_form.csrf_token }}
|
||||
{{ fido_token_form.sk_assertion(class="form-control", placeholder="") }}
|
||||
</form>
|
||||
<div class="text-center">
|
||||
<button id="btnRegisterKey" class="btn btn-lg btn-primary mt-2">Register Key</button>
|
||||
</div>
|
||||
<form id="formRegisterKey" method="post">
|
||||
{{ fido_token_form.csrf_token }}
|
||||
{{ fido_token_form.sk_assertion(class="form-control", placeholder="") }}
|
||||
</form>
|
||||
<div class="text-center">
|
||||
<button id="btnRegisterKey" class="btn btn-lg btn-primary mt-2">Register Key</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function registerKey () {
|
||||
$("#btnRegisterKey").prop('disabled', true);
|
||||
$("#btnRegisterKey").text('Waiting for Security Key...');
|
||||
<script>
|
||||
async function registerKey() {
|
||||
$("#btnRegisterKey").prop('disabled', true);
|
||||
$("#btnRegisterKey").text('Waiting for Security Key...');
|
||||
|
||||
const pkCredentialCreateOptions = transformCredentialCreateOptions(
|
||||
JSON.parse('{{credential_create_options|tojson|safe}}')
|
||||
)
|
||||
const pkCredentialCreateOptions = transformCredentialCreateOptions(
|
||||
JSON.parse('{{credential_create_options|tojson|safe}}')
|
||||
)
|
||||
|
||||
let credential
|
||||
try {
|
||||
credential = await navigator.credentials.create({
|
||||
publicKey: pkCredentialCreateOptions
|
||||
});
|
||||
} catch (err) {
|
||||
toastr.error("An error occurred when we trying to register your key.");
|
||||
$("#btnRegisterKey").prop('disabled', false);
|
||||
$("#btnRegisterKey").text('Register Key');
|
||||
return console.error("Error when trying to create credential:", err);
|
||||
let credential
|
||||
try {
|
||||
credential = await navigator.credentials.create({
|
||||
publicKey: pkCredentialCreateOptions
|
||||
});
|
||||
} catch (err) {
|
||||
toastr.error("An error occurred when we trying to register your key.");
|
||||
$("#btnRegisterKey").prop('disabled', false);
|
||||
$("#btnRegisterKey").text('Register Key');
|
||||
return console.error("Error when trying to create credential:", err);
|
||||
}
|
||||
|
||||
const skAssertion = transformNewAssertionForServer(credential);
|
||||
|
||||
$('#sk_assertion').val(JSON.stringify(skAssertion));
|
||||
$('#formRegisterKey').submit();
|
||||
}
|
||||
|
||||
const skAssertion = transformNewAssertionForServer(credential);
|
||||
|
||||
$('#sk_assertion').val(JSON.stringify(skAssertion));
|
||||
$('#formRegisterKey').submit();
|
||||
}
|
||||
|
||||
$("#btnRegisterKey").click(registerKey);
|
||||
$('document').ready(registerKey());
|
||||
</script>
|
||||
$("#btnRegisterKey").click(registerKey);
|
||||
$('document').ready(registerKey());
|
||||
</script>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -400,13 +400,15 @@
|
||||
<div class="col">
|
||||
<nav aria-label="Alias navigation">
|
||||
<ul class="pagination">
|
||||
<li class="page-item {% if page == 0 %}disabled{% endif %}">
|
||||
<a class="page-link"
|
||||
href="{{ url_for('dashboard.index', page=page-1, query=query, sort=sort, filter=filter) }}">Previous</a>
|
||||
<li class="page-item">
|
||||
<a class="btn btn-outline-secondary {% if page == 0 %}disabled{% endif %}"
|
||||
href="{{ url_for('dashboard.index', page=page-1, query=query, sort=sort, filter=filter) }}">
|
||||
Previous</a>
|
||||
</li>
|
||||
<li class="page-item {% if last_page %}disabled{% endif %}">
|
||||
<a class="page-link"
|
||||
href="{{ url_for('dashboard.index', page=page+1, query=query, sort=sort, filter=filter) }}">Next</a>
|
||||
<li class="page-item">
|
||||
<a class="btn btn-outline-secondary {% if last_page %}disabled{% endif %}"
|
||||
href="{{ url_for('dashboard.index', page=page+1, query=query, sort=sort, filter=filter) }}">
|
||||
Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@@ -7,20 +7,22 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block default_content %}
|
||||
<div class="bg-white p-6" style="max-width: 60em; margin: auto">
|
||||
<h1 class="h2">Lifetime Licence</h1>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1 class="h3">Lifetime Licence</h1>
|
||||
|
||||
<div class="mb-4">
|
||||
If you have a lifetime licence, please paste it here. <br>
|
||||
<div class="mb-4">
|
||||
If you have a lifetime licence, please paste it here. <br>
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
{{ coupon_form.csrf_token }}
|
||||
|
||||
{{ coupon_form.code(class="form-control", placeholder="Licence Code") }}
|
||||
{{ render_field_errors(coupon_form.code) }}
|
||||
<button class="btn btn-success mt-2">Apply</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
{{ coupon_form.csrf_token }}
|
||||
|
||||
{{ coupon_form.code(class="form-control", placeholder="Licence Code") }}
|
||||
{{ render_field_errors(coupon_form.code) }}
|
||||
<button class="btn btn-success mt-2">Apply</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -6,24 +6,25 @@
|
||||
|
||||
|
||||
{% block default_content %}
|
||||
<div class="bg-white p-6" style="max-width: 60em; margin: auto">
|
||||
<h1 class="h2">Multi Factor Authentication</h1>
|
||||
<p>
|
||||
To cancel MFA, please enter the 6-digit number in your TOTP application
|
||||
(Google Authenticator, Authy, MyDigiPassword, etc) here.
|
||||
</p>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h1 class="h2">Multi Factor Authentication</h1>
|
||||
<p>
|
||||
To cancel MFA, please enter the 6-digit number in your TOTP application
|
||||
(Google Authenticator, Authy, MyDigiPassword, etc) here.
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
{{ otp_token_form.csrf_token }}
|
||||
<form method="post">
|
||||
{{ otp_token_form.csrf_token }}
|
||||
|
||||
<div class="font-weight-bold mt-5">Token</div>
|
||||
<div class="small-text">The 6-digit number displayed on your phone.</div>
|
||||
|
||||
{{ otp_token_form.token(class="form-control", autofocus="true") }}
|
||||
{{ render_field_errors(otp_token_form.token) }}
|
||||
<button class="btn btn-lg btn-danger mt-2">Cancel MFA</button>
|
||||
</form>
|
||||
<div class="font-weight-bold mt-5">Token</div>
|
||||
<div class="small-text">The 6-digit number displayed on your phone.</div>
|
||||
|
||||
{{ otp_token_form.token(class="form-control", autofocus="true") }}
|
||||
{{ render_field_errors(otp_token_form.token) }}
|
||||
<button class="btn btn-lg btn-danger mt-2">Cancel MFA</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -9,43 +9,42 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block default_content %}
|
||||
<div class="bg-white p-6" style="max-width: 60em; margin: auto">
|
||||
<h1 class="h2">Multi Factor Authentication</h1>
|
||||
<p>Please open a TOTP application (Google Authenticator, Authy, MyDigiPassword, etc)
|
||||
on your smartphone and scan the following QR Code:
|
||||
</p>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h1 class="h3">Multi Factor Authentication</h1>
|
||||
<p>Please open a TOTP application (Google Authenticator, Authy, MyDigiPassword, etc)
|
||||
on your smartphone and scan the following QR Code:
|
||||
</p>
|
||||
|
||||
<canvas id="qr"></canvas>
|
||||
<canvas id="qr"></canvas>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var qr = new QRious({
|
||||
element: document.getElementById('qr'),
|
||||
value: '{{otp_uri}}'
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
(function () {
|
||||
var qr = new QRious({
|
||||
element: document.getElementById('qr'),
|
||||
value: '{{otp_uri}}'
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<div class="mt-3 mb-2">
|
||||
Or you can use the manual entry with the following key:
|
||||
</div>
|
||||
|
||||
<input class="form-control" disabled value="{{ current_user.otp_secret }}">
|
||||
|
||||
|
||||
<form method="post">
|
||||
{{ otp_token_form.csrf_token }}
|
||||
|
||||
<div class="font-weight-bold mt-5">Token</div>
|
||||
<div class="small-text">Please enter the 6-digit number displayed on your phone.</div>
|
||||
|
||||
{{ otp_token_form.token(class="form-control", placeholder="") }}
|
||||
{{ render_field_errors(otp_token_form.token) }}
|
||||
<button class="btn btn-lg btn-success mt-2">Validate</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-3 mb-2">
|
||||
Or you can use the manual entry with the following key:
|
||||
</div>
|
||||
|
||||
<div class="mb-3 p-3" style="background-color: #eee">
|
||||
{{ current_user.otp_secret }}
|
||||
</div>
|
||||
|
||||
|
||||
<form method="post">
|
||||
{{ otp_token_form.csrf_token }}
|
||||
|
||||
<div class="font-weight-bold mt-5">Token</div>
|
||||
<div class="small-text">Please enter the 6-digit number displayed on your phone.</div>
|
||||
|
||||
{{ otp_token_form.token(class="form-control", placeholder="") }}
|
||||
{{ render_field_errors(otp_token_form.token) }}
|
||||
<button class="btn btn-lg btn-success mt-2">Validate</button>
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -8,20 +8,22 @@
|
||||
|
||||
{% block default_content %}
|
||||
|
||||
<div class="col-md-6 offset-md-3 text-center bg-white p-3 mt-5">
|
||||
<h1 class="h3">
|
||||
Block alias
|
||||
</h1>
|
||||
<p>
|
||||
You are about to block the alias <a href="mailto:{{alias}}">{{alias}}</a>
|
||||
</p>
|
||||
<p>
|
||||
After this, you will stop receiving all emails sent to this alias, please confirm.
|
||||
</p>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h1 class="h3">
|
||||
Block alias
|
||||
</h1>
|
||||
<p>
|
||||
You are about to block the alias <a href="mailto:{{ alias }}">{{ alias }}</a>
|
||||
</p>
|
||||
<p>
|
||||
After this, you will stop receiving all emails sent to this alias, please confirm.
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
<button class="btn btn-warning">Confirm</button>
|
||||
</form>
|
||||
<form method="post">
|
||||
<button class="btn btn-warning">Confirm</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
+10
-10
@@ -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
|
||||
|
||||
|
||||
@@ -1,61 +1,64 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bg-white p-6" style="margin: auto; max-width: 600px">
|
||||
<div class="text-center mb-6">
|
||||
<a href="https://simplelogin.io">
|
||||
<img src="/static/logo.svg" style="background-color: transparent; height: 24px">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<b>{{ client.name }}</b> would like to have access to your following data:
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ul class="mt-3">
|
||||
{% for scope in client.get_scopes() %}
|
||||
<li>
|
||||
{% if scope == Scope.AVATAR_URL %}
|
||||
avatar
|
||||
{% else %}
|
||||
{{ scope.value }}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
In order to accept the request, you need to sign in.
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="btn-group w-100">
|
||||
|
||||
<a href="{{ url_for('auth.login', next=next) }}" class="btn btn-success">
|
||||
Login
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('auth.register', next=next) }}" class="btn btn-info">
|
||||
Sign Up
|
||||
<div class="card mx-auto" style="max-width: 600px">
|
||||
<div class="card-body">
|
||||
<div class="text-center mb-6">
|
||||
<a href="https://simplelogin.io">
|
||||
<img src="/static/logo.svg" style="background-color: transparent; height: 24px">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<b>{{ client.name }}</b> would like to have access to your following data:
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ul class="mt-3">
|
||||
{% for scope in client.get_scopes() %}
|
||||
<li>
|
||||
{% if scope == Scope.AVATAR_URL %}
|
||||
avatar
|
||||
{% else %}
|
||||
{{ scope.value }}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
In order to accept the request, you need to sign in.
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="btn-group w-100">
|
||||
|
||||
<a href="{{ url_for('auth.login', next=next) }}" class="btn btn-success">
|
||||
Login
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('auth.register', next=next) }}" class="btn btn-info">
|
||||
Sign Up
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="">
|
||||
<p class="text-center col">Cancel and go back to <b>{{ client.name }}</b></p>
|
||||
<a class="btn btn-block btn-secondary back-or-close">
|
||||
<i class="fe fe-arrow-left mr-2"></i>Cancel
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="small-text mt-4">
|
||||
<a href="https://simplelogin.io">SimpleLogin</a> is an open source social login provider that protects your
|
||||
privacy.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="">
|
||||
<p class="text-center col">Cancel and go back to <b>{{ client.name }}</b></p>
|
||||
<a class="btn btn-block btn-secondary back-or-close">
|
||||
<i class="fe fe-arrow-left mr-2"></i>Cancel
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="small-text mt-4">
|
||||
<a href="https://simplelogin.io">SimpleLogin</a> is an open source social login provider that protects your privacy.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -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:
|
||||
|
||||
+27
-18
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
});
|
||||
Vendored
+8
-4
@@ -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: "⚠️";
|
||||
}
|
||||
content: "⚠️";
|
||||
}
|
||||
|
||||
.dns-record {
|
||||
border: 1px dotted #E3156A;
|
||||
}
|
||||
|
||||
+5
-2
@@ -1,7 +1,7 @@
|
||||
{% from "_formhelpers.html" import render_field, render_field_errors %}
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en" dir="ltr">
|
||||
<html lang="en" dir="ltr" data-theme="{% if request.cookies.get('dark-mode') == 'true' %}dark{% endif %}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
@@ -69,6 +69,8 @@
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css?v={{ VERSION }}">
|
||||
|
||||
<script src="{{ url_for('static', filename='js/theme.js') }}"></script>
|
||||
|
||||
<script>
|
||||
toastr.options.closeButton = true;
|
||||
</script>
|
||||
@@ -175,7 +177,8 @@
|
||||
|
||||
</script>
|
||||
|
||||
<script src="/static/local-storage-polyfill.js"></script>
|
||||
<script src="{{ url_for('static', filename='local-storage-polyfill.js') }}"></script>
|
||||
|
||||
|
||||
<!-- For additional script -->
|
||||
{% block script %}
|
||||
|
||||
@@ -449,7 +449,7 @@
|
||||
<tr>
|
||||
<td class="email-masthead" style="word-break: break-word; font-family: Helvetica, Arial, sans-serif; font-size: 16px; text-align: center; padding: 25px 0;" align="center">
|
||||
<a href="https://simplelogin.io" class="f-fallback email-masthead_name" style="color: #A8AAAF; font-size: 16px; font-weight: bold; text-decoration: none; text-shadow: 0 1px 0 white;">
|
||||
SimpleLogin
|
||||
<img src="https://simplelogin.io/logo.png" style="width: 150px; margin: auto">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -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
|
||||
<a href="https://app.simplelogin.io/dashboard/setting#notification">Settings</a>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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
|
||||
----------------
|
||||
|
||||
|
||||
@@ -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 :). <br>
|
||||
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 :). <br>
|
||||
Please find below some of our latest news. <br>
|
||||
|
||||
{% 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.<br>
|
||||
|
||||
We have also created a new <a href="https://simplelogin.io/security/">security page</a> that goes into the technical details of SimpleLogin.
|
||||
Our <a href="https://simplelogin.io/privacy/">privacy page</a> is also rewritten from scratch: nothing changes about your data protection
|
||||
We have also created a new <a href="https://simplelogin.io/security/">security page</a> that goes into the technical
|
||||
details of SimpleLogin.
|
||||
Our <a href="https://simplelogin.io/privacy/">privacy page</a> 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.
|
||||
<hr style="margin: 10px;">
|
||||
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 <a href="https://twitter.com/simple_login">Twitter</a>,
|
||||
<a href="https://www.reddit.com/r/Simplelogin/">Reddit</a>, where you can also follow all our latest updates.
|
||||
<br>
|
||||
That's all for today. If you want to follow all our latest features, you can follow our
|
||||
<a href="https://twitter.com/simple_login">Twitter</a> or join our
|
||||
<a href="https://www.reddit.com/r/Simplelogin/">Reddit</a>
|
||||
or subscribe to our <a href="https://feed43.com/simplelogin.xml">RSS feed</a>. <br>
|
||||
Now back to coding :).
|
||||
{% endcall %}
|
||||
|
||||
{% call text() %}
|
||||
|
||||
@@ -33,8 +33,7 @@
|
||||
</div>
|
||||
<div class="col-12 col-lg-auto mt-3 mt-lg-0 text-center">
|
||||
Copyright © {{ YEAR }}
|
||||
<a href="https://simplelogin.io" target="_blank">SimpleLogin
|
||||
</a>.
|
||||
<a href="https://simplelogin.io" target="_blank">SimpleLogin</a>.
|
||||
All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+11
-11
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user