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:
Son NK
2020-05-16 11:28:25 +02:00
41 changed files with 611 additions and 490 deletions
+2
View File
@@ -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>
+17
View File
@@ -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
+5 -2
View File
@@ -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
View File
@@ -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)
+49 -43
View File
@@ -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 %}
+27 -25
View File
@@ -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 %}
+3
View File
@@ -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,
)
+6 -3
View File
@@ -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>
+61 -59
View File
@@ -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 %}
+8 -6
View File
@@ -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 %}
+8
View File
@@ -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",
)
+8 -4
View File
@@ -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,
+2 -2
View File
@@ -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(
+2 -2
View File
@@ -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
+6 -3
View File
@@ -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
View File
@@ -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 %}
+5 -6
View File
@@ -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
View File
@@ -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
+3 -2
View File
@@ -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
+2 -2
View File
@@ -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);
+1 -14
View File
@@ -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')
})
});
});
+40
View File
@@ -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')
})
});
+8 -4
View File
@@ -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
View File
@@ -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 %}
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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 -1
View File
@@ -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() %}
+1 -2
View File
@@ -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
View File
@@ -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():