This commit is contained in:
MrMeeb 2023-08-25 12:00:05 +01:00
parent 88f270c6a1
commit 6d24db50bd
12 changed files with 175 additions and 160 deletions

View File

@ -191,25 +191,16 @@ def mailbox_detail_route(mailbox_id):
) )
elif request.form.get("form-name") == "generic-subject": elif request.form.get("form-name") == "generic-subject":
if request.form.get("action") == "save": if request.form.get("action") == "save":
if not mailbox.pgp_enabled():
flash(
"Generic subject can only be used on PGP-enabled mailbox",
"error",
)
return redirect(
url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id)
)
mailbox.generic_subject = request.form.get("generic-subject") mailbox.generic_subject = request.form.get("generic-subject")
Session.commit() Session.commit()
flash("Generic subject for PGP-encrypted email is enabled", "success") flash("Generic subject is enabled", "success")
return redirect( return redirect(
url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id) url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id)
) )
elif request.form.get("action") == "remove": elif request.form.get("action") == "remove":
mailbox.generic_subject = None mailbox.generic_subject = None
Session.commit() Session.commit()
flash("Generic subject for PGP-encrypted email is disabled", "success") flash("Generic subject is disabled", "success")
return redirect( return redirect(
url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id) url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id)
) )

View File

@ -121,3 +121,10 @@ class AccountAlreadyLinkedToAnotherUserException(LinkException):
class AccountIsUsingAliasAsEmail(LinkException): class AccountIsUsingAliasAsEmail(LinkException):
def __init__(self): def __init__(self):
super().__init__("Your account has an alias as it's email address") super().__init__("Your account has an alias as it's email address")
class ProtonAccountNotVerified(LinkException):
def __init__(self):
super().__init__(
"The Proton account you are trying to use has not been verified"
)

View File

@ -7,11 +7,12 @@ from typing import Optional
from app.account_linking import SLPlan, SLPlanType from app.account_linking import SLPlan, SLPlanType
from app.config import PROTON_EXTRA_HEADER_NAME, PROTON_EXTRA_HEADER_VALUE from app.config import PROTON_EXTRA_HEADER_NAME, PROTON_EXTRA_HEADER_VALUE
from app.errors import ProtonAccountNotVerified
from app.log import LOG from app.log import LOG
_APP_VERSION = "OauthClient_1.0.0" _APP_VERSION = "OauthClient_1.0.0"
PROTON_ERROR_CODE_NOT_EXISTS = 2501 PROTON_ERROR_CODE_HV_NEEDED = 9001
PLAN_FREE = 1 PLAN_FREE = 1
PLAN_PREMIUM = 2 PLAN_PREMIUM = 2
@ -57,6 +58,15 @@ def convert_access_token(access_token_response: str) -> AccessCredentials:
) )
def handle_response_not_ok(status: int, body: dict, text: str) -> Exception:
if status == HTTPStatus.UNPROCESSABLE_ENTITY:
res_code = body.get("Code")
if res_code == PROTON_ERROR_CODE_HV_NEEDED:
return ProtonAccountNotVerified()
return Exception(f"Unexpected status code. Wanted 200 and got {status}: " + text)
class ProtonClient(ABC): class ProtonClient(ABC):
@abstractmethod @abstractmethod
def get_user(self) -> Optional[UserInformation]: def get_user(self) -> Optional[UserInformation]:
@ -124,11 +134,11 @@ class HttpProtonClient(ProtonClient):
@staticmethod @staticmethod
def __validate_response(res: Response) -> dict: def __validate_response(res: Response) -> dict:
status = res.status_code status = res.status_code
if status != HTTPStatus.OK:
raise Exception(
f"Unexpected status code. Wanted 200 and got {status}: " + res.text
)
as_json = res.json() as_json = res.json()
if status != HTTPStatus.OK:
raise HttpProtonClient.__handle_response_not_ok(
status=status, body=as_json, text=res.text
)
res_code = as_json.get("Code") res_code = as_json.get("Code")
if not res_code or res_code != 1000: if not res_code or res_code != 1000:
raise Exception( raise Exception(

View File

@ -878,21 +878,22 @@ def forward_email_to_mailbox(
headers_to_keep.append(headers.AUTHENTICATION_RESULTS) headers_to_keep.append(headers.AUTHENTICATION_RESULTS)
delete_all_headers_except(msg, headers_to_keep) delete_all_headers_except(msg, headers_to_keep)
if mailbox.generic_subject:
LOG.d("Use a generic subject for %s", mailbox)
orig_subject = msg[headers.SUBJECT]
orig_subject = get_header_unicode(orig_subject)
add_or_replace_header(msg, "Subject", mailbox.generic_subject)
sender = msg[headers.FROM]
sender = get_header_unicode(sender)
msg = add_header(
msg,
f"""Forwarded by SimpleLogin to {alias.email} from "{sender}" with "{orig_subject}" as subject""",
f"""Forwarded by SimpleLogin to {alias.email} from "{sender}" with <b>{orig_subject}</b> as subject""",
)
# create PGP email if needed # create PGP email if needed
if mailbox.pgp_enabled() and user.is_premium() and not alias.disable_pgp: if mailbox.pgp_enabled() and user.is_premium() and not alias.disable_pgp:
LOG.d("Encrypt message using mailbox %s", mailbox) LOG.d("Encrypt message using mailbox %s", mailbox)
if mailbox.generic_subject:
LOG.d("Use a generic subject for %s", mailbox)
orig_subject = msg[headers.SUBJECT]
orig_subject = get_header_unicode(orig_subject)
add_or_replace_header(msg, "Subject", mailbox.generic_subject)
sender = msg[headers.FROM]
sender = get_header_unicode(sender)
msg = add_header(
msg,
f"""Forwarded by SimpleLogin to {alias.email} from "{sender}" with "{orig_subject}" as subject""",
f"""Forwarded by SimpleLogin to {alias.email} from "{sender}" with <b>{orig_subject}</b> as subject""",
)
try: try:
msg = prepare_pgp_message( msg = prepare_pgp_message(

View File

@ -133,6 +133,7 @@
<div> <div>
<span> <span>
<a href="{{ 'mailto:' + contact.website_send_to() }}" <a href="{{ 'mailto:' + contact.website_send_to() }}"
target="_blank"
data-toggle="tooltip" data-toggle="tooltip"
title="You can click on this to open your email client. Or use the copy button 👉" title="You can click on this to open your email client. Or use the copy button 👉"
class="font-weight-bold"> class="font-weight-bold">

View File

@ -48,7 +48,7 @@
{% if scope == "email" %} {% if scope == "email" %}
Email: Email:
<a href="mailto:{{ val }}">{{ val }}</a> <a href="mailto:{{ val }}" target="_blank">{{ val }}</a>
{% elif scope == "name" %} {% elif scope == "name" %}
Name: {{ val }} Name: {{ val }}
{% endif %} {% endif %}

View File

@ -137,123 +137,115 @@
</form> </form>
</div> </div>
</div> </div>
<div class="card" {% if not mailbox.pgp_enabled() %} </div>
disabled {% endif %}> <div class="card" id="generic-subject">
<form method="post"> <form method="post" action="#generic-subject">
{{ csrf_form.csrf_token }} {{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="generic-subject"> <input type="hidden" name="form-name" value="generic-subject">
<div class="card-body">
<div class="card-title">
Hide email subject when PGP is enabled
<div class="small-text mt-1">
When PGP is enabled, you can choose to use a <b>generic</b> subject for the forwarded emails.
The original subject is then added into the email body.
<br />
As PGP does not encrypt the email subject and the email subject might contain sensitive information,
this option will allow a further protection of your email content.
</div>
</div>
<div class="alert alert-info">
As the email is encrypted, a subject like "Email for you"
will probably be rejected by your mailbox since it sounds like a spam.
<br />
Something like "Encrypted Email" would work much better :).
</div>
<div class="form-group">
<label class="form-label">Generic Subject</label>
<input name="generic-subject" {% if not mailbox.pgp_enabled() %}
disabled {% endif %} class="form-control" maxlength="78" placeholder="Generic Subject" value="{{ mailbox.generic_subject or "" }}">
</div>
<button class="btn btn-primary" name="action" {% if not mailbox.pgp_enabled() %}
disabled {% endif %} value="save">
Save
</button>
{% if mailbox.generic_subject %}
<button class="btn btn-danger float-right" name="action" value="remove">Remove</button>
{% endif %}
</div>
</form>
</div>
</div>
<hr />
<h2 class="h4">Advanced Options</h2>
{% if spf_available %}
<div class="card" id="spf">
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="force-spf">
<div class="card-body">
<div class="card-title">
Enforce SPF
<div class="small-text">
To avoid email-spoofing, SimpleLogin blocks email that
<em data-toggle="tooltip"
title="Email that has your mailbox as envelope-sender address">seems</em> to come from your
mailbox
but sent from <em data-toggle="tooltip"
title="IP Address that is not known by your mailbox email service">unknown</em>
IP address.
<br />
Only turn off this option if you know what you're doing :).
</div>
</div>
<label class="custom-switch cursor mt-2 pl-0" data-toggle="tooltip" {% if mailbox.force_spf %}
title="Disable SPF enforcement" {% else %} title="Enable SPF enforcement" {% endif %}>
<input type="checkbox" name="spf-status" class="custom-switch-input" {{ "checked" if mailbox.force_spf else "" }}>
<span class="custom-switch-indicator"></span>
</label>
</div>
</form>
</div>
{% endif %}
<div class="card" id="authorized-address">
<div class="card-body"> <div class="card-body">
<div class="card-title"> <div class="card-title">
Authorized addresses Hide email subject
<div class="small-text"> <div class="small-text mt-1">
Emails sent from these addresses to a <b>reverse-alias</b> are considered as being sent The original subject will be added to the email body and all forwarded emails will have the generic subject.
from {{ mailbox.email }} <br />
This option is often used when PGP is enabled.
As PGP does not encrypt the email subject, it allows a further protection of your email content.
</div> </div>
</div> </div>
{% if mailbox.authorized_addresses | length == 0 %} <div class="form-group">
<label class="form-label">Generic Subject</label>
<input name="generic-subject"
class="form-control"
maxlength="78"
placeholder="Generic Subject"
value="{{ mailbox.generic_subject or "" }}">
</div>
<button class="btn btn-primary" name="action" value="save">Save</button>
{% if mailbox.generic_subject %}
{% else %} <button class="btn btn-danger float-right" name="action" value="remove">Remove</button>
<ul>
{% for authorized_address in mailbox.authorized_addresses %}
<li>
{{ authorized_address.email }}
<form method="post" action="#authorized-address" style="display: inline">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="delete-authorized-address">
<input type="hidden"
name="authorized-address-id"
value="{{ authorized_address.id }}">
<input type="submit" class="btn btn-sm btn-outline-warning" value="Delete">
</form>
</li>
{% endfor %}
</ul>
{% endif %} {% endif %}
<form method="post" action="#authorized-address" class="form-inline">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="add-authorized-address">
<input type="email" name="email" size="50" class="form-control" required>
<input type="submit" class="btn btn-primary" value="Add">
</form>
</div> </div>
</form>
</div>
<hr />
<h2 class="h4">Advanced Options</h2>
{% if spf_available %}
<div class="card" id="spf">
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="force-spf">
<div class="card-body">
<div class="card-title">
Enforce SPF
<div class="small-text">
To avoid email-spoofing, SimpleLogin blocks email that
<em data-toggle="tooltip"
title="Email that has your mailbox as envelope-sender address">seems</em> to come from your
mailbox
but sent from <em data-toggle="tooltip"
title="IP Address that is not known by your mailbox email service">unknown</em>
IP address.
<br />
Only turn off this option if you know what you're doing :).
</div>
</div>
<label class="custom-switch cursor mt-2 pl-0" data-toggle="tooltip" {% if mailbox.force_spf %}
title="Disable SPF enforcement" {% else %} title="Enable SPF enforcement" {% endif %}>
<input type="checkbox" name="spf-status" class="custom-switch-input" {{ "checked" if mailbox.force_spf else "" }}>
<span class="custom-switch-indicator"></span>
</label>
</div>
</form>
</div>
{% endif %}
<div class="card" id="authorized-address">
<div class="card-body">
<div class="card-title">
Authorized addresses
<div class="small-text">
Emails sent from these addresses to a <b>reverse-alias</b> are considered as being sent
from {{ mailbox.email }}
</div>
</div>
{% if mailbox.authorized_addresses | length == 0 %}
{% else %}
<ul>
{% for authorized_address in mailbox.authorized_addresses %}
<li>
{{ authorized_address.email }}
<form method="post" action="#authorized-address" style="display: inline">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="delete-authorized-address">
<input type="hidden"
name="authorized-address-id"
value="{{ authorized_address.id }}">
<input type="submit" class="btn btn-sm btn-outline-warning" value="Delete">
</form>
</li>
{% endfor %}
</ul>
{% endif %}
<form method="post" action="#authorized-address" class="form-inline">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="add-authorized-address">
<input type="email" name="email" size="50" class="form-control" required>
<input type="submit" class="btn btn-primary" value="Add">
</form>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} </div>
{% block script %} {% endblock %}
<script src="/static/js/utils/drag-drop-into-text.js"></script> {% block script %}
<script> <script src="/static/js/utils/drag-drop-into-text.js"></script>
<script>
$(".custom-switch-input").change(function (e) { $(".custom-switch-input").change(function (e) {
$(this).closest("form").submit(); $(this).closest("form").submit();
}); });
enableDragDropForPGPKeys('#pgp-public-key'); enableDragDropForPGPKeys('#pgp-public-key');
</script> </script>
{% endblock %} {% endblock %}

View File

@ -207,7 +207,7 @@
<div class="card-body"> <div class="card-body">
<div class="text-center"> <div class="text-center">
<div class="h3">Proton plan</div> <div class="h3">Proton plan</div>
<div class="h3 my-3">Starts at $11.99 / month</div> <div class="h3 my-3">Starts at $12.99 / month</div>
<div class="text-center mt-4 mb-6"> <div class="text-center mt-4 mb-6">
<a class="btn btn-lg btn-outline-primary w-100" <a class="btn btn-lg btn-outline-primary w-100"
role="button" role="button"
@ -225,10 +225,6 @@
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i> <i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
500 GB storage 500 GB storage
</li> </li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
15 email addresses
</li>
<li class="d-flex"> <li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i> <i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
Unlimited folders, labels, and filters Unlimited folders, labels, and filters
@ -239,11 +235,7 @@
</li> </li>
<li class="d-flex"> <li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i> <i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
15 email addresses 25 calendars
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
20 Calendars
</li> </li>
<li class="d-flex"> <li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i> <i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
@ -376,10 +368,6 @@
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i> <i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
500 GB storage 500 GB storage
</li> </li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
15 email addresses/aliases
</li>
<li class="d-flex"> <li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i> <i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
Unlimited folders, labels, and filters Unlimited folders, labels, and filters
@ -390,11 +378,7 @@
</li> </li>
<li class="d-flex"> <li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i> <i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
15 email addresses/aliases 25 calendars
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
20 Calendars
</li> </li>
<li class="d-flex"> <li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i> <i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
@ -478,7 +462,7 @@
</a>, which currently supports Bitcoin, Bitcoin Cash, DAI, ApeCoin, Dogecoin, Ethereum, Litecoin, SHIBA INU, Tether and USD Coin. </a>, which currently supports Bitcoin, Bitcoin Cash, DAI, ApeCoin, Dogecoin, Ethereum, Litecoin, SHIBA INU, Tether and USD Coin.
</p> </p>
<p> <p>
In the future, we are going to support Monero as well. In the meantime, please send us an email at <a href="mailto:support@simplelogin.zendesk.com">support@simplelogin.zendesk.com</a> if you want to use this cryptocurrency. In the future, we are going to support Monero as well. In the meantime, please send us an email at <a href="mailto:support@simplelogin.zendesk.com" target="_blank">support@simplelogin.zendesk.com</a> if you want to use this cryptocurrency.
</p> </p>
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<a class="btn btn-outline-primary text-center" <a class="btn btn-outline-primary text-center"
@ -645,7 +629,7 @@
</li> </li>
</ul> </ul>
<p> <p>
Please send us an email at <a href="mailto:support@simplelogin.zendesk.com">support@simplelogin.zendesk.com</a> for more info. Please send us an email at <a href="mailto:support@simplelogin.zendesk.com" target="_blank">support@simplelogin.zendesk.com</a> for more info.
</p> </p>
<p> <p>
We used to offer free premium accounts for students but this program ended at June 17 2021. Please note this doesn't affect existing accounts who have already benefited from the program or requests sent before this date. We used to offer free premium accounts for students but this program ended at June 17 2021. Please note this doesn't affect existing accounts who have already benefited from the program or requests sent before this date.
@ -708,7 +692,7 @@
data-parent="#pricing-faq"> data-parent="#pricing-faq">
<div class="card-body"> <div class="card-body">
<p> <p>
No we don't have a family plan but offer 30% reduction for additional subscriptions. Please contact us at <a href="mailto:support@simplelogin.zendesk.com">support@simplelogin.zendesk.com</a> for more information. No we don't have a family plan but offer 30% reduction for additional subscriptions. Please contact us at <a href="mailto:support@simplelogin.zendesk.com" target="_blank">support@simplelogin.zendesk.com</a> for more information.
</p> </p>
</div> </div>
</div> </div>

View File

@ -22,7 +22,7 @@
For every user who <b>upgrades</b> and stays with us at least 3 months, you'll get $5 :). For every user who <b>upgrades</b> and stays with us at least 3 months, you'll get $5 :).
<br /> <br />
The payout can be initiated any time, just send us an email at The payout can be initiated any time, just send us an email at
<a href="mailto:hi@simplelogin.io">hi@simplelogin.io</a> <a href="mailto:hi@simplelogin.io" target="_blank">hi@simplelogin.io</a>
when you want to receive the payout. when you want to receive the payout.
</div> </div>
{% if referrals|length == 0 %} {% if referrals|length == 0 %}

View File

@ -9,7 +9,7 @@
<h1 class="h3">Block alias</h1> <h1 class="h3">Block alias</h1>
<p> <p>
You are about to block the alias You are about to block the alias
<a href="mailto:{{ alias }}">{{ alias }}</a> <a href="mailto:{{ alias }}" target="_blank">{{ alias }}</a>
</p> </p>
<p>After this, you will stop receiving all emails sent to this alias, please confirm.</p> <p>After this, you will stop receiving all emails sent to this alias, please confirm.</p>
<form method="post"> <form method="post">

View File

@ -61,7 +61,7 @@
<img src="{{ user_info[scope.value] }}" class="avatar"> <img src="{{ user_info[scope.value] }}" class="avatar">
{% elif scope == Scope.EMAIL %} {% elif scope == Scope.EMAIL %}
{{ scope.value }}: {{ scope.value }}:
<a href="mailto:{{ user_info[scope.value] }}">{{ user_info[scope.value] }}</a> <a href="mailto:{{ user_info[scope.value] }}" target="_blank">{{ user_info[scope.value] }}</a>
{% elif scope == Scope.NAME %} {% elif scope == Scope.NAME %}
{{ scope.value }}: <b>{{ user_info[scope.value] }}</b> {{ scope.value }}: <b>{{ user_info[scope.value] }}</b>
{% endif %} {% endif %}

View File

@ -1,5 +1,7 @@
import pytest import pytest
from http import HTTPStatus
from app.errors import ProtonAccountNotVerified
from app.proton import proton_client from app.proton import proton_client
@ -19,3 +21,30 @@ def test_convert_access_token_not_containing_invalid_length():
for case in cases: for case in cases:
with pytest.raises(Exception): with pytest.raises(Exception):
proton_client.convert_access_token(case) proton_client.convert_access_token(case)
def test_handle_response_not_ok_account_not_verified():
res = proton_client.handle_response_not_ok(
status=HTTPStatus.UNPROCESSABLE_ENTITY,
body={"Code": proton_client.PROTON_ERROR_CODE_HV_NEEDED},
text="",
)
assert isinstance(res, ProtonAccountNotVerified)
def test_handle_response_unprocessable_entity_not_account_not_verified():
error_text = "some error text"
res = proton_client.handle_response_not_ok(
status=HTTPStatus.UNPROCESSABLE_ENTITY, body={"Code": 4567}, text=error_text
)
assert error_text in res.args[0]
def test_handle_response_not_ok_unknown_error():
error_text = "some error text"
res = proton_client.handle_response_not_ok(
status=123,
body={"Code": proton_client.PROTON_ERROR_CODE_HV_NEEDED},
text=error_text,
)
assert error_text in res.args[0]