Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
8ee4f9462e | |||
822855d584 | |||
1a6a7e079b |
@ -31,6 +31,10 @@ steps:
|
|||||||
|
|
||||||
- name: notify
|
- name: notify
|
||||||
image: plugins/slack
|
image: plugins/slack
|
||||||
|
when:
|
||||||
|
status:
|
||||||
|
- success
|
||||||
|
- failure
|
||||||
settings:
|
settings:
|
||||||
webhook:
|
webhook:
|
||||||
from_secret: slack_webhook
|
from_secret: slack_webhook
|
||||||
|
@ -9,13 +9,17 @@ from newrelic import agent
|
|||||||
from app.db import Session
|
from app.db import Session
|
||||||
from app.email_utils import send_welcome_email
|
from app.email_utils import send_welcome_email
|
||||||
from app.utils import sanitize_email
|
from app.utils import sanitize_email
|
||||||
from app.errors import AccountAlreadyLinkedToAnotherPartnerException
|
from app.errors import (
|
||||||
|
AccountAlreadyLinkedToAnotherPartnerException,
|
||||||
|
AccountIsUsingAliasAsEmail,
|
||||||
|
)
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
from app.models import (
|
from app.models import (
|
||||||
PartnerSubscription,
|
PartnerSubscription,
|
||||||
Partner,
|
Partner,
|
||||||
PartnerUser,
|
PartnerUser,
|
||||||
User,
|
User,
|
||||||
|
Alias,
|
||||||
)
|
)
|
||||||
from app.utils import random_string
|
from app.utils import random_string
|
||||||
|
|
||||||
@ -192,11 +196,18 @@ def get_login_strategy(
|
|||||||
return ExistingUnlinkedUserStrategy(link_request, user, partner)
|
return ExistingUnlinkedUserStrategy(link_request, user, partner)
|
||||||
|
|
||||||
|
|
||||||
|
def check_alias(email: str) -> bool:
|
||||||
|
alias = Alias.get_by(email=email)
|
||||||
|
if alias is not None:
|
||||||
|
raise AccountIsUsingAliasAsEmail()
|
||||||
|
|
||||||
|
|
||||||
def process_login_case(
|
def process_login_case(
|
||||||
link_request: PartnerLinkRequest, partner: Partner
|
link_request: PartnerLinkRequest, partner: Partner
|
||||||
) -> LinkResult:
|
) -> LinkResult:
|
||||||
# Sanitize email just in case
|
# Sanitize email just in case
|
||||||
link_request.email = sanitize_email(link_request.email)
|
link_request.email = sanitize_email(link_request.email)
|
||||||
|
check_alias(link_request.email)
|
||||||
# Try to find a SimpleLogin user registered with that partner user id
|
# Try to find a SimpleLogin user registered with that partner user id
|
||||||
partner_user = PartnerUser.get_by(
|
partner_user = PartnerUser.get_by(
|
||||||
partner_id=partner.id, external_user_id=link_request.external_user_id
|
partner_id=partner.id, external_user_id=link_request.external_user_id
|
||||||
|
@ -357,6 +357,7 @@ ALERT_COMPLAINT_TRANSACTIONAL_PHASE = "alert_complaint_transactional_phase"
|
|||||||
ALERT_QUARANTINE_DMARC = "alert_quarantine_dmarc"
|
ALERT_QUARANTINE_DMARC = "alert_quarantine_dmarc"
|
||||||
|
|
||||||
ALERT_DUAL_SUBSCRIPTION_WITH_PARTNER = "alert_dual_sub_with_partner"
|
ALERT_DUAL_SUBSCRIPTION_WITH_PARTNER = "alert_dual_sub_with_partner"
|
||||||
|
ALERT_WARN_MULTIPLE_SUBSCRIPTIONS = "alert_multiple_subscription"
|
||||||
|
|
||||||
# <<<<< END ALERT EMAIL >>>>
|
# <<<<< END ALERT EMAIL >>>>
|
||||||
|
|
||||||
|
@ -215,6 +215,12 @@ def alias_transfer_receive_route():
|
|||||||
token,
|
token,
|
||||||
)
|
)
|
||||||
transfer(alias, current_user, mailboxes)
|
transfer(alias, current_user, mailboxes)
|
||||||
|
|
||||||
|
# reset transfer token
|
||||||
|
alias.transfer_token = None
|
||||||
|
alias.transfer_token_expiration = None
|
||||||
|
Session.commit()
|
||||||
|
|
||||||
flash(f"You are now owner of {alias.email}", "success")
|
flash(f"You are now owner of {alias.email}", "success")
|
||||||
return redirect(url_for("dashboard.index", highlight_alias_id=alias.id))
|
return redirect(url_for("dashboard.index", highlight_alias_id=alias.id))
|
||||||
|
|
||||||
|
@ -120,18 +120,11 @@ def custom_alias():
|
|||||||
email=full_alias
|
email=full_alias
|
||||||
)
|
)
|
||||||
custom_domain = domain_deleted_alias.domain
|
custom_domain = domain_deleted_alias.domain
|
||||||
if domain_deleted_alias.user_id == current_user.id:
|
|
||||||
flash(
|
flash(
|
||||||
f"You have deleted this alias before. You can restore it on "
|
f"You have deleted this alias before. You can restore it on "
|
||||||
f"{custom_domain.domain} 'Deleted Alias' page",
|
f"{custom_domain.domain} 'Deleted Alias' page",
|
||||||
"error",
|
"error",
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
# should never happen as user can only choose their domains
|
|
||||||
LOG.e(
|
|
||||||
"Deleted Alias %s does not belong to user %s",
|
|
||||||
domain_deleted_alias,
|
|
||||||
)
|
|
||||||
|
|
||||||
elif DeletedAlias.get_by(email=full_alias):
|
elif DeletedAlias.get_by(email=full_alias):
|
||||||
flash(general_error_msg, "error")
|
flash(general_error_msg, "error")
|
||||||
|
@ -80,8 +80,9 @@ def pricing():
|
|||||||
@dashboard_bp.route("/subscription_success")
|
@dashboard_bp.route("/subscription_success")
|
||||||
@login_required
|
@login_required
|
||||||
def subscription_success():
|
def subscription_success():
|
||||||
flash("Thanks so much for supporting SimpleLogin!", "success")
|
return render_template(
|
||||||
return redirect(url_for("dashboard.index"))
|
"dashboard/thank-you.html",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dashboard_bp.route("/coinbase_checkout")
|
@dashboard_bp.route("/coinbase_checkout")
|
||||||
|
@ -60,4 +60,5 @@ E522 = (
|
|||||||
)
|
)
|
||||||
E523 = "550 SL E523 Unknown error"
|
E523 = "550 SL E523 Unknown error"
|
||||||
E524 = "550 SL E524 Wrong use of reverse-alias"
|
E524 = "550 SL E524 Wrong use of reverse-alias"
|
||||||
|
E525 = "550 SL E525 Alias loop"
|
||||||
# endregion
|
# endregion
|
||||||
|
@ -71,7 +71,7 @@ class ErrContactErrorUpgradeNeeded(SLException):
|
|||||||
"""raised when user cannot create a contact because the plan doesn't allow it"""
|
"""raised when user cannot create a contact because the plan doesn't allow it"""
|
||||||
|
|
||||||
def error_for_user(self) -> str:
|
def error_for_user(self) -> str:
|
||||||
return f"Please upgrade to premium to create reverse-alias"
|
return "Please upgrade to premium to create reverse-alias"
|
||||||
|
|
||||||
|
|
||||||
class ErrAddressInvalid(SLException):
|
class ErrAddressInvalid(SLException):
|
||||||
@ -108,3 +108,8 @@ class AccountAlreadyLinkedToAnotherPartnerException(LinkException):
|
|||||||
class AccountAlreadyLinkedToAnotherUserException(LinkException):
|
class AccountAlreadyLinkedToAnotherUserException(LinkException):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__("This account is linked to another user")
|
super().__init__("This account is linked to another user")
|
||||||
|
|
||||||
|
|
||||||
|
class AccountIsUsingAliasAsEmail(LinkException):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("Your account has an alias as it's email address")
|
||||||
|
@ -170,6 +170,7 @@ class MailSender:
|
|||||||
LOG.e(
|
LOG.e(
|
||||||
f"Could not send message to smtp server {config.POSTFIX_SERVER}:{config.POSTFIX_PORT}"
|
f"Could not send message to smtp server {config.POSTFIX_SERVER}:{config.POSTFIX_PORT}"
|
||||||
)
|
)
|
||||||
|
if config.SAVE_UNSENT_DIR:
|
||||||
self._save_request_to_unsent_dir(send_request)
|
self._save_request_to_unsent_dir(send_request)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -44,7 +44,6 @@ from app.utils import (
|
|||||||
random_string,
|
random_string,
|
||||||
random_words,
|
random_words,
|
||||||
sanitize_email,
|
sanitize_email,
|
||||||
random_word,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
@ -519,7 +518,7 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
|
|||||||
# Keep original unsub behaviour
|
# Keep original unsub behaviour
|
||||||
unsub_behaviour = sa.Column(
|
unsub_behaviour = sa.Column(
|
||||||
IntEnumType(UnsubscribeBehaviourEnum),
|
IntEnumType(UnsubscribeBehaviourEnum),
|
||||||
default=UnsubscribeBehaviourEnum.DisableAlias,
|
default=UnsubscribeBehaviourEnum.PreserveOriginal,
|
||||||
server_default=str(UnsubscribeBehaviourEnum.DisableAlias.value),
|
server_default=str(UnsubscribeBehaviourEnum.DisableAlias.value),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
)
|
)
|
||||||
@ -1010,7 +1009,7 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
|
|||||||
"""
|
"""
|
||||||
if self.random_alias_suffix == AliasSuffixEnum.random_string.value:
|
if self.random_alias_suffix == AliasSuffixEnum.random_string.value:
|
||||||
return random_string(config.ALIAS_RANDOM_SUFFIX_LENGTH, include_digits=True)
|
return random_string(config.ALIAS_RANDOM_SUFFIX_LENGTH, include_digits=True)
|
||||||
return random_word()
|
return random_words(1, 3)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<User {self.id} {self.name} {self.email}>"
|
return f"<User {self.id} {self.name} {self.email}>"
|
||||||
@ -1269,7 +1268,7 @@ def generate_email(
|
|||||||
name = uuid.uuid4().hex if in_hex else uuid.uuid4().__str__()
|
name = uuid.uuid4().hex if in_hex else uuid.uuid4().__str__()
|
||||||
random_email = name + "@" + alias_domain
|
random_email = name + "@" + alias_domain
|
||||||
else:
|
else:
|
||||||
random_email = random_words() + "@" + alias_domain
|
random_email = random_words(2, 3) + "@" + alias_domain
|
||||||
|
|
||||||
random_email = random_email.lower().strip()
|
random_email = random_email.lower().strip()
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import random
|
||||||
import re
|
import re
|
||||||
import secrets
|
import secrets
|
||||||
import string
|
import string
|
||||||
@ -25,11 +26,16 @@ def word_exist(word):
|
|||||||
return word in _words
|
return word in _words
|
||||||
|
|
||||||
|
|
||||||
def random_words():
|
def random_words(words: int = 2, numbers: int = 0):
|
||||||
"""Generate a random words. Used to generate user-facing string, for ex email addresses"""
|
"""Generate a random words. Used to generate user-facing string, for ex email addresses"""
|
||||||
# nb_words = random.randint(2, 3)
|
# nb_words = random.randint(2, 3)
|
||||||
nb_words = 2
|
fields = [secrets.choice(_words) for i in range(words)]
|
||||||
return "_".join([secrets.choice(_words) for i in range(nb_words)])
|
|
||||||
|
if numbers > 0:
|
||||||
|
fields.append("".join([str(random.randint(0, 9)) for i in range(numbers)]))
|
||||||
|
return "".join(fields)
|
||||||
|
else:
|
||||||
|
return "_".join(fields)
|
||||||
|
|
||||||
|
|
||||||
def random_string(length=10, include_digits=False):
|
def random_string(length=10, include_digits=False):
|
||||||
|
@ -693,6 +693,36 @@ def handle_forward(envelope, msg: Message, rcpt_to: str) -> List[Tuple[bool, str
|
|||||||
LOG.d("%s unverified, do not forward", mailbox)
|
LOG.d("%s unverified, do not forward", mailbox)
|
||||||
ret.append((False, status.E517))
|
ret.append((False, status.E517))
|
||||||
else:
|
else:
|
||||||
|
# Check if the mailbox is also an alias and stop the loop
|
||||||
|
mailbox_as_alias = Alias.get_by(email=mailbox.email)
|
||||||
|
if mailbox_as_alias is not None:
|
||||||
|
LOG.info(
|
||||||
|
f"Mailbox {mailbox.id} has email {mailbox.email} that is also alias {alias.id}. Stopping loop"
|
||||||
|
)
|
||||||
|
mailbox.verified = False
|
||||||
|
Session.commit()
|
||||||
|
mailbox_url = f"{URL}/dashboard/mailbox/{mailbox.id}/"
|
||||||
|
send_email_with_rate_control(
|
||||||
|
user,
|
||||||
|
ALERT_MAILBOX_IS_ALIAS,
|
||||||
|
user.email,
|
||||||
|
f"Your mailbox {mailbox.email} is an alias",
|
||||||
|
render(
|
||||||
|
"transactional/mailbox-invalid.txt.jinja2",
|
||||||
|
mailbox=mailbox,
|
||||||
|
mailbox_url=mailbox_url,
|
||||||
|
alias=alias,
|
||||||
|
),
|
||||||
|
render(
|
||||||
|
"transactional/mailbox-invalid.html",
|
||||||
|
mailbox=mailbox,
|
||||||
|
mailbox_url=mailbox_url,
|
||||||
|
alias=alias,
|
||||||
|
),
|
||||||
|
max_nb_alert=1,
|
||||||
|
)
|
||||||
|
ret.append((False, status.E525))
|
||||||
|
continue
|
||||||
# create a copy of message for each forward
|
# create a copy of message for each forward
|
||||||
ret.append(
|
ret.append(
|
||||||
forward_email_to_mailbox(
|
forward_email_to_mailbox(
|
||||||
|
329422
app/local_data/words.txt
329422
app/local_data/words.txt
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
This is an example on how to integrate SimpleLogin
|
This is an example on how to integrate SimpleLogin
|
||||||
with Requests-OAuthlib, a popular library to work with OAuth in Python.
|
with Requests-OAuthlib, a popular library to work with OAuth in Python.
|
||||||
The step-to-step guide can be found on https://docs.simplelogin.io
|
The step-to-step guide can be found on https://simplelogin.io/docs/siwsl/app/
|
||||||
This example is based on
|
This example is based on
|
||||||
https://requests-oauthlib.readthedocs.io/en/latest/examples/real_world_example.html
|
https://requests-oauthlib.readthedocs.io/en/latest/examples/real_world_example.html
|
||||||
"""
|
"""
|
||||||
|
@ -210,5 +210,6 @@ to apply the coupon code.
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
plausible("visit pricing");
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
19
app/templates/dashboard/thank-you.html
Normal file
19
app/templates/dashboard/thank-you.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{% extends "single.html" %}
|
||||||
|
|
||||||
|
{% set active_page = "dashboard" %}
|
||||||
|
{% block title %}Thank you{% endblock %}
|
||||||
|
{% block single_content %}
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h1 class="h3">Thanks so much for supporting SimpleLogin!</h1>
|
||||||
|
<p>
|
||||||
|
SimpleLogin is 100% funded by the community.
|
||||||
|
We do not use your data, track you or show you ads.
|
||||||
|
</p>
|
||||||
|
<p>Thanks to your support, we can keep the service running and develop new features.</p>
|
||||||
|
<a class="btn btn-primary" href="/">Close</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>plausible("upgraded")</script>
|
||||||
|
{% endblock %}
|
@ -31,7 +31,7 @@
|
|||||||
<span class="icon mr-3"><i class="fe fe-alert-octagon"></i></span>Danger
|
<span class="icon mr-3"><i class="fe fe-alert-octagon"></i></span>Danger
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<a href="https://docs.simplelogin.io"
|
<a href="https://simplelogin.io/docs/siwsl/app/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="btn btn-block btn-secondary mt-4">
|
class="btn btn-block btn-secondary mt-4">
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<h4 class="alert-heading">Well done!</h4>
|
<h4 class="alert-heading">Well done!</h4>
|
||||||
<p>
|
<p>
|
||||||
Please head to our
|
Please head to our
|
||||||
<a href="https://docs.simplelogin.io"
|
<a href="https://simplelogin.io/docs/siwsl/app/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer">
|
rel="noopener noreferrer">
|
||||||
documentation <i class="fe fe-external-link"></i>
|
documentation <i class="fe fe-external-link"></i>
|
||||||
|
@ -47,7 +47,7 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="btn-group" role="group" aria-label="Basic example">
|
<div class="btn-group" role="group" aria-label="Basic example">
|
||||||
<a href="{{ url_for('developer.new_client') }}" class="btn btn-primary">New website</a>
|
<a href="{{ url_for('developer.new_client') }}" class="btn btn-primary">New website</a>
|
||||||
<a href="https://docs.simplelogin.io"
|
<a href="https://simplelogin.io/docs/siwsl/app/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="ml-2 btn btn-secondary">
|
class="ml-2 btn btn-secondary">
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% call text() %}
|
||||||
|
Hello,
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
|
{% call text() %}
|
||||||
|
Your have tried to register multiple times to {{ service }}, and this is against the terms of service of SimpleLogin. Please don't do that anymore.
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
|
{% call text() %}
|
||||||
|
If you continue registering multiple accounts to a single service we will have to disable your account.
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -0,0 +1,9 @@
|
|||||||
|
{% extends "base.txt.jinja2" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
Hello,
|
||||||
|
|
||||||
|
Your have tried to register multiple times to {{service}}, and this is against the terms of service of SimpleLogin. Please don't do that anymore.
|
||||||
|
|
||||||
|
If you continue registering multiple accounts to a single service we will have to disable your account.
|
||||||
|
{% endblock %}
|
@ -368,3 +368,19 @@ def test_send_email_from_non_canonical_matches_already_existing_user(flask_clien
|
|||||||
assert len(email_logs) == 1
|
assert len(email_logs) == 1
|
||||||
assert email_logs[0].alias_id == alias.id
|
assert email_logs[0].alias_id == alias.id
|
||||||
assert email_logs[0].mailbox_id == user.default_mailbox_id
|
assert email_logs[0].mailbox_id == user.default_mailbox_id
|
||||||
|
|
||||||
|
|
||||||
|
@mail_sender.store_emails_test_decorator
|
||||||
|
def test_break_loop_alias_as_mailbox(flask_client):
|
||||||
|
user = create_new_user()
|
||||||
|
alias = Alias.create_new_random(user)
|
||||||
|
user.default_mailbox.email = alias.email
|
||||||
|
Session.commit()
|
||||||
|
envelope = Envelope()
|
||||||
|
envelope.mail_from = random_email()
|
||||||
|
envelope.rcpt_tos = [alias.email]
|
||||||
|
msg = EmailMessage()
|
||||||
|
msg[headers.TO] = alias.email
|
||||||
|
msg[headers.SUBJECT] = random_string()
|
||||||
|
result = email_handler.handle(envelope, msg)
|
||||||
|
assert result == status.E525
|
||||||
|
@ -9,7 +9,12 @@ from app.utils import random_string, random_words, sanitize_next_url, canonicali
|
|||||||
|
|
||||||
def test_random_words():
|
def test_random_words():
|
||||||
s = random_words()
|
s = random_words()
|
||||||
assert len(s) > 0
|
assert s.find("_") > 0
|
||||||
|
assert s.count("_") == 1
|
||||||
|
assert len(s) > 3
|
||||||
|
s = random_words(2, 3)
|
||||||
|
assert s.count("_") == 0
|
||||||
|
assert s[-1] in (str(i) for i in range(10))
|
||||||
|
|
||||||
|
|
||||||
def test_random_string():
|
def test_random_string():
|
||||||
@ -66,7 +71,7 @@ def canonicalize_email_cases():
|
|||||||
yield (f"a@{domain}", f"a@{domain}")
|
yield (f"a@{domain}", f"a@{domain}")
|
||||||
yield (f"a.b@{domain}", f"ab@{domain}")
|
yield (f"a.b@{domain}", f"ab@{domain}")
|
||||||
yield (f"a.b+c@{domain}", f"ab@{domain}")
|
yield (f"a.b+c@{domain}", f"ab@{domain}")
|
||||||
yield (f"a.b+c@other.com", f"a.b+c@other.com")
|
yield ("a.b+c@other.com", "a.b+c@other.com")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("dirty,clean", canonicalize_email_cases())
|
@pytest.mark.parametrize("dirty,clean", canonicalize_email_cases())
|
||||||
|
@ -17,7 +17,7 @@ def create_new_user(email: Optional[str] = None, name: Optional[str] = None) ->
|
|||||||
if not email:
|
if not email:
|
||||||
email = f"user_{random_token(10)}@mailbox.test"
|
email = f"user_{random_token(10)}@mailbox.test"
|
||||||
if not name:
|
if not name:
|
||||||
name = f"Test User"
|
name = "Test User"
|
||||||
# new user has a different email address
|
# new user has a different email address
|
||||||
user = User.create(
|
user = User.create(
|
||||||
email=email,
|
email=email,
|
||||||
|
Reference in New Issue
Block a user