Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
8ee4f9462e | |||
822855d584 | |||
1a6a7e079b | |||
5210cb6515 | |||
b643f0644b |
@ -31,6 +31,10 @@ steps:
|
||||
|
||||
- name: notify
|
||||
image: plugins/slack
|
||||
when:
|
||||
status:
|
||||
- success
|
||||
- failure
|
||||
settings:
|
||||
webhook:
|
||||
from_secret: slack_webhook
|
||||
|
@ -21,3 +21,4 @@ repos:
|
||||
- id: djlint-jinja
|
||||
files: '.*\.html'
|
||||
entry: djlint --reformat
|
||||
|
||||
|
@ -9,13 +9,17 @@ from newrelic import agent
|
||||
from app.db import Session
|
||||
from app.email_utils import send_welcome_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.models import (
|
||||
PartnerSubscription,
|
||||
Partner,
|
||||
PartnerUser,
|
||||
User,
|
||||
Alias,
|
||||
)
|
||||
from app.utils import random_string
|
||||
|
||||
@ -192,11 +196,18 @@ def get_login_strategy(
|
||||
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(
|
||||
link_request: PartnerLinkRequest, partner: Partner
|
||||
) -> LinkResult:
|
||||
# Sanitize email just in case
|
||||
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
|
||||
partner_user = PartnerUser.get_by(
|
||||
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_DUAL_SUBSCRIPTION_WITH_PARTNER = "alert_dual_sub_with_partner"
|
||||
ALERT_WARN_MULTIPLE_SUBSCRIPTIONS = "alert_multiple_subscription"
|
||||
|
||||
# <<<<< END ALERT EMAIL >>>>
|
||||
|
||||
|
@ -215,6 +215,12 @@ def alias_transfer_receive_route():
|
||||
token,
|
||||
)
|
||||
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")
|
||||
return redirect(url_for("dashboard.index", highlight_alias_id=alias.id))
|
||||
|
||||
|
@ -120,18 +120,11 @@ def custom_alias():
|
||||
email=full_alias
|
||||
)
|
||||
custom_domain = domain_deleted_alias.domain
|
||||
if domain_deleted_alias.user_id == current_user.id:
|
||||
flash(
|
||||
f"You have deleted this alias before. You can restore it on "
|
||||
f"{custom_domain.domain} 'Deleted Alias' page",
|
||||
"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):
|
||||
flash(general_error_msg, "error")
|
||||
|
@ -80,8 +80,9 @@ def pricing():
|
||||
@dashboard_bp.route("/subscription_success")
|
||||
@login_required
|
||||
def subscription_success():
|
||||
flash("Thanks so much for supporting SimpleLogin!", "success")
|
||||
return redirect(url_for("dashboard.index"))
|
||||
return render_template(
|
||||
"dashboard/thank-you.html",
|
||||
)
|
||||
|
||||
|
||||
@dashboard_bp.route("/coinbase_checkout")
|
||||
|
@ -60,4 +60,5 @@ E522 = (
|
||||
)
|
||||
E523 = "550 SL E523 Unknown error"
|
||||
E524 = "550 SL E524 Wrong use of reverse-alias"
|
||||
E525 = "550 SL E525 Alias loop"
|
||||
# endregion
|
||||
|
@ -71,7 +71,7 @@ class ErrContactErrorUpgradeNeeded(SLException):
|
||||
"""raised when user cannot create a contact because the plan doesn't allow it"""
|
||||
|
||||
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):
|
||||
@ -108,3 +108,8 @@ class AccountAlreadyLinkedToAnotherPartnerException(LinkException):
|
||||
class AccountAlreadyLinkedToAnotherUserException(LinkException):
|
||||
def __init__(self):
|
||||
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")
|
||||
|
@ -17,7 +17,7 @@ from attr import dataclass
|
||||
from app import config
|
||||
from app.email import headers
|
||||
from app.log import LOG
|
||||
from app.message_utils import message_to_bytes
|
||||
from app.message_utils import message_to_bytes, message_format_base64_parts
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -170,11 +170,16 @@ class MailSender:
|
||||
LOG.e(
|
||||
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)
|
||||
return False
|
||||
|
||||
def _save_request_to_unsent_dir(self, send_request: SendRequest):
|
||||
file_name = f"DeliveryFail-{int(time.time())}-{uuid.uuid4()}.{SendRequest.SAVE_EXTENSION}"
|
||||
def _save_request_to_unsent_dir(
|
||||
self, send_request: SendRequest, prefix: str = "DeliveryFail"
|
||||
):
|
||||
file_name = (
|
||||
f"{prefix}-{int(time.time())}-{uuid.uuid4()}.{SendRequest.SAVE_EXTENSION}"
|
||||
)
|
||||
file_path = os.path.join(config.SAVE_UNSENT_DIR, file_name)
|
||||
file_contents = send_request.to_bytes()
|
||||
with open(file_path, "wb") as fd:
|
||||
@ -256,7 +261,7 @@ def sl_sendmail(
|
||||
send_request = SendRequest(
|
||||
envelope_from,
|
||||
envelope_to,
|
||||
msg,
|
||||
message_format_base64_parts(msg),
|
||||
mail_options,
|
||||
rcpt_options,
|
||||
is_forward,
|
||||
|
@ -1,21 +1,42 @@
|
||||
import re
|
||||
from email import policy
|
||||
from email.message import Message
|
||||
|
||||
from app.email import headers
|
||||
from app.log import LOG
|
||||
|
||||
# Spam assassin might flag as spam with a different line length
|
||||
BASE64_LINELENGTH = 76
|
||||
|
||||
|
||||
def message_to_bytes(msg: Message) -> bytes:
|
||||
"""replace Message.as_bytes() method by trying different policies"""
|
||||
for generator_policy in [None, policy.SMTP, policy.SMTPUTF8]:
|
||||
try:
|
||||
return msg.as_bytes(policy=generator_policy)
|
||||
except:
|
||||
except Exception:
|
||||
LOG.w("as_bytes() fails with %s policy", policy, exc_info=True)
|
||||
|
||||
msg_string = msg.as_string()
|
||||
try:
|
||||
return msg_string.encode()
|
||||
except:
|
||||
except Exception:
|
||||
LOG.w("as_string().encode() fails", exc_info=True)
|
||||
|
||||
return msg_string.encode(errors="replace")
|
||||
|
||||
|
||||
def message_format_base64_parts(msg: Message) -> Message:
|
||||
for part in msg.walk():
|
||||
if part.get(
|
||||
headers.CONTENT_TRANSFER_ENCODING
|
||||
) == "base64" and part.get_content_type() in ("text/plain", "text/html"):
|
||||
# Remove line breaks
|
||||
body = re.sub("[\r\n]", "", part.get_payload())
|
||||
# Split in 80 column lines
|
||||
chunks = [
|
||||
body[i : i + BASE64_LINELENGTH]
|
||||
for i in range(0, len(body), BASE64_LINELENGTH)
|
||||
]
|
||||
part.set_payload("\r\n".join(chunks))
|
||||
return msg
|
||||
|
@ -44,7 +44,6 @@ from app.utils import (
|
||||
random_string,
|
||||
random_words,
|
||||
sanitize_email,
|
||||
random_word,
|
||||
)
|
||||
|
||||
Base = declarative_base()
|
||||
@ -519,7 +518,7 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
|
||||
# Keep original unsub behaviour
|
||||
unsub_behaviour = sa.Column(
|
||||
IntEnumType(UnsubscribeBehaviourEnum),
|
||||
default=UnsubscribeBehaviourEnum.DisableAlias,
|
||||
default=UnsubscribeBehaviourEnum.PreserveOriginal,
|
||||
server_default=str(UnsubscribeBehaviourEnum.DisableAlias.value),
|
||||
nullable=False,
|
||||
)
|
||||
@ -1010,7 +1009,7 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
|
||||
"""
|
||||
if self.random_alias_suffix == AliasSuffixEnum.random_string.value:
|
||||
return random_string(config.ALIAS_RANDOM_SUFFIX_LENGTH, include_digits=True)
|
||||
return random_word()
|
||||
return random_words(1, 3)
|
||||
|
||||
def __repr__(self):
|
||||
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__()
|
||||
random_email = name + "@" + alias_domain
|
||||
else:
|
||||
random_email = random_words() + "@" + alias_domain
|
||||
random_email = random_words(2, 3) + "@" + alias_domain
|
||||
|
||||
random_email = random_email.lower().strip()
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import random
|
||||
import re
|
||||
import secrets
|
||||
import string
|
||||
@ -25,11 +26,16 @@ def word_exist(word):
|
||||
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"""
|
||||
# nb_words = random.randint(2, 3)
|
||||
nb_words = 2
|
||||
return "_".join([secrets.choice(_words) for i in range(nb_words)])
|
||||
fields = [secrets.choice(_words) for i in range(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):
|
||||
|
@ -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)
|
||||
ret.append((False, status.E517))
|
||||
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
|
||||
ret.append(
|
||||
forward_email_to_mailbox(
|
||||
@ -840,10 +870,12 @@ def forward_email_to_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} with "{orig_subject}" as subject""",
|
||||
f"""Forwarded by SimpleLogin to {alias.email} with <b>{orig_subject}</b> as subject""",
|
||||
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:
|
||||
|
332579
app/local_data/words.txt
332579
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
|
||||
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
|
||||
https://requests-oauthlib.readthedocs.io/en/latest/examples/real_world_example.html
|
||||
"""
|
||||
|
@ -50,7 +50,9 @@
|
||||
</p>
|
||||
<p>
|
||||
This Youtube video can also quickly walk you through the steps:
|
||||
<a href="https://www.youtube.com/watch?v=VsypF-DBaow" target="_blank">
|
||||
<a href="https://www.youtube.com/watch?v=VsypF-DBaow"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
How to send emails from an alias <i class="fe fe-external-link"></i>
|
||||
</a>
|
||||
</p>
|
||||
|
@ -23,7 +23,9 @@
|
||||
|
||||
<div class="alert alert-danger" role="alert">
|
||||
This feature is only available on Premium plan.
|
||||
<a href="{{ URL }}/dashboard/pricing" target="_blank" rel="noopener">
|
||||
<a href="{{ URL }}/dashboard/pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
Upgrade<i class="fe fe-external-link"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -78,7 +78,7 @@
|
||||
data-clipboard-text=".*suffix">.*suffix</em>
|
||||
<br />
|
||||
To test out regex, we recommend using regex tester tool like
|
||||
<a href="https://regex101.com" target="_blank">https://regex101.com↗</a>
|
||||
<a href="https://regex101.com" target="_blank" rel="noopener noreferrer">https://regex101.com↗</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
@ -158,7 +158,7 @@
|
||||
SPF
|
||||
<a href="https://en.wikipedia.org/wiki/Sender_Policy_Framework"
|
||||
target="_blank"
|
||||
rel="noopener">(Wikipedia↗)</a>
|
||||
rel="noopener noreferrer">(Wikipedia↗)</a>
|
||||
is an email
|
||||
authentication method
|
||||
designed to detect forging sender addresses during the delivery of the email.
|
||||
@ -229,7 +229,7 @@
|
||||
DKIM
|
||||
<a href="https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail"
|
||||
target="_blank"
|
||||
rel="noopener">(Wikipedia↗)</a>
|
||||
rel="noopener noreferrer">(Wikipedia↗)</a>
|
||||
is an
|
||||
email
|
||||
authentication method
|
||||
@ -335,7 +335,7 @@
|
||||
DMARC
|
||||
<a href="https://en.wikipedia.org/wiki/DMARC"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
rel="noopener noreferrer">
|
||||
(Wikipedia↗)
|
||||
</a>
|
||||
is designed to protect the domain from unauthorized use, commonly known as email spoofing.
|
||||
|
@ -72,7 +72,7 @@ PGP Encryption
|
||||
</ul>
|
||||
<div class="small-text">
|
||||
More information on our
|
||||
<a href="https://simplelogin.io/pricing" target="_blank" rel="noopener">
|
||||
<a href="https://simplelogin.io/pricing" target="_blank" rel="noopener noreferrer">
|
||||
Pricing
|
||||
Page <i class="fe fe-external-link"></i>
|
||||
</a>
|
||||
@ -120,7 +120,7 @@ Upgrade your SimpleLogin account
|
||||
<div id="normal-upgrade" class="{% if proton_upgrade %} collapse{% endif %}">
|
||||
<div class="display-6 my-3">
|
||||
🔐 Secure payments by
|
||||
<a href="https://paddle.com" target="_blank" rel="noopener">
|
||||
<a href="https://paddle.com" target="_blank" rel="noopener noreferrer">
|
||||
Paddle <i class="fe fe-external-link"></i>
|
||||
</a>
|
||||
</div>
|
||||
@ -164,13 +164,13 @@ $4/month
|
||||
<hr />
|
||||
<i class="fa fa-bitcoin"></i>
|
||||
Payment via
|
||||
<a href="https://commerce.coinbase.com/?lang=en" target="_blank">
|
||||
<a href="https://commerce.coinbase.com/?lang=en" target="_blank" rel="noopener noreferrer">
|
||||
Coinbase Commerce<i class="fe fe-external-link"></i>
|
||||
</a>
|
||||
<br />
|
||||
Currently Bitcoin, Bitcoin Cash, Dai, Ethereum, Litecoin and USD Coin are supported.
|
||||
<br />
|
||||
<a class="btn btn-outline-primary" href="{{ url_for('dashboard.coinbase_checkout_route') }}" target="_blank">
|
||||
<a class="btn btn-outline-primary" href="{{ url_for('dashboard.coinbase_checkout_route') }}" target="_blank" rel="noopener noreferrer">
|
||||
Yearly billing - Crypto
|
||||
<br />
|
||||
$30/year
|
||||
@ -210,5 +210,6 @@ to apply the coupon code.
|
||||
});
|
||||
}
|
||||
|
||||
plausible("visit pricing");
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -17,7 +17,8 @@
|
||||
<div class="alert alert-info">
|
||||
This page shows all emails that are either refused by your mailbox (bounced) or detected as spams/phishing (quarantine) via our
|
||||
<a href="https://simplelogin.io/docs/getting-started/anti-phishing/"
|
||||
target="_blank">anti-phishing program ↗</a>
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">anti-phishing program ↗</a>
|
||||
<ul class="p-4 mb-0">
|
||||
<li>
|
||||
If the email is indeed spam, this means the alias is now in the hands of a spammer,
|
||||
@ -26,7 +27,8 @@
|
||||
<li>
|
||||
If the email isn't spam and your mailbox refuses the email, we recommend to create a <b>filter</b> to avoid your mailbox provider from blocking legitimate emails. Please refer to
|
||||
<a href="https://simplelogin.io/docs/getting-started/troubleshooting/#emails-end-up-in-spam"
|
||||
target="_blank">Setting up filter for SimpleLogin emails ↗</a>
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">Setting up filter for SimpleLogin emails ↗</a>
|
||||
</li>
|
||||
<li>
|
||||
If the email is flagged as spams/phishing, this means that the sender explicitly states their emails should respect
|
||||
|
@ -73,7 +73,8 @@
|
||||
Yearly plan subscribed with cryptocurrency which expires on
|
||||
{{ coinbase_sub.end_at.format("YYYY-MM-DD") }}.
|
||||
<a href="{{ url_for('dashboard.coinbase_checkout_route') }}"
|
||||
target="_blank">
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
Extend Subscription <i class="fe fe-external-link"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -25,7 +25,7 @@
|
||||
This feature is only available on Premium plan.
|
||||
<a href="{{ url_for('dashboard.pricing') }}"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
rel="noopener noreferrer">
|
||||
Upgrade<i class="fe fe-external-link"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
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,8 +31,9 @@
|
||||
<span class="icon mr-3"><i class="fe fe-alert-octagon"></i></span>Danger
|
||||
</a>
|
||||
</div>
|
||||
<a href="https://docs.simplelogin.io"
|
||||
<a href="https://simplelogin.io/docs/siwsl/app/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-block btn-secondary mt-4">
|
||||
Documentation <i class="fe fe-external-link"></i>
|
||||
</a>
|
||||
|
@ -10,7 +10,9 @@
|
||||
<h4 class="alert-heading">Well done!</h4>
|
||||
<p>
|
||||
Please head to our
|
||||
<a href="https://docs.simplelogin.io" target="_blank" rel="noopener">
|
||||
<a href="https://simplelogin.io/docs/siwsl/app/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
documentation <i class="fe fe-external-link"></i>
|
||||
</a>
|
||||
to see how to add SIWSL into your app.
|
||||
|
@ -47,8 +47,9 @@
|
||||
<div class="col">
|
||||
<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="https://docs.simplelogin.io"
|
||||
<a href="https://simplelogin.io/docs/siwsl/app/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="ml-2 btn btn-secondary">
|
||||
Docs <i class="fe fe-external-link"></i>
|
||||
</a>
|
||||
|
@ -13,7 +13,9 @@
|
||||
|
||||
<div class="col-sm-4 col-xl-2">
|
||||
<div class="card">
|
||||
<a href="{{ client.home_url }}" target="_blank" rel="noopener">
|
||||
<a href="{{ client.home_url }}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<img class="card-img-top" src="{{ client.get_icon_url() }}">
|
||||
</a>
|
||||
<div class="card-body d-flex flex-column">
|
||||
|
@ -46,7 +46,7 @@ https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
|
||||
<a href="{{ link }}"
|
||||
class="f-fallback button"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
rel="noopener noreferrer"
|
||||
style="color: #FFF;
|
||||
border-color: #3869d4;
|
||||
border-style: solid;
|
||||
|
@ -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 %}
|
@ -145,28 +145,28 @@
|
||||
<ul class="list-group list-group-transparent list-group-white list-group-flush list-group-borderless mb-0 footer-list-group">
|
||||
<li>
|
||||
<a class="list-group-item text-white footer-item "
|
||||
rel="noopener"
|
||||
rel="noopener noreferrer"
|
||||
href="https://chrome.google.com/webstore/detail/dphilobhebphkdjbpfohgikllaljmgbn">
|
||||
Chrome Extension
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="list-group-item text-white footer-item "
|
||||
rel="noopener"
|
||||
rel="noopener noreferrer"
|
||||
href="https://addons.mozilla.org/firefox/addon/simplelogin/">
|
||||
Firefox Add-on
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="list-group-item text-white footer-item "
|
||||
rel="noopener"
|
||||
rel="noopener noreferrer"
|
||||
href="https://microsoftedge.microsoft.com/addons/detail/simpleloginreceive-sen/diacfpipniklenphgljfkmhinphjlfff">
|
||||
Edge Add-on
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="list-group-item text-white footer-item "
|
||||
rel="noopener"
|
||||
rel="noopener noreferrer"
|
||||
href="https://apps.apple.com/app/id1494051017">
|
||||
Safari
|
||||
Extension
|
||||
@ -174,7 +174,7 @@
|
||||
</li>
|
||||
<li>
|
||||
<a class="list-group-item text-white footer-item "
|
||||
rel="noopener"
|
||||
rel="noopener noreferrer"
|
||||
href="https://apps.apple.com/app/id1494359858">
|
||||
iOS
|
||||
(App Store)
|
||||
@ -182,14 +182,14 @@
|
||||
</li>
|
||||
<li>
|
||||
<a class="list-group-item text-white footer-item "
|
||||
rel="noopener"
|
||||
rel="noopener noreferrer"
|
||||
href="https://play.google.com/store/apps/details?id=io.simplelogin.android">
|
||||
Android (Play Store)
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="list-group-item text-white footer-item "
|
||||
rel="noopener"
|
||||
rel="noopener noreferrer"
|
||||
href="https://f-droid.org/en/packages/io.simplelogin.android.fdroid/">
|
||||
Android (F-Droid)
|
||||
</a>
|
||||
|
@ -75,14 +75,17 @@
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a>
|
||||
<div class="dropdown-menu dropdown-menu-left dropdown-menu-arrow">
|
||||
<div class="dropdown-item">
|
||||
<a href="https://simplelogin.io/docs/" target="_blank">
|
||||
<a href="https://simplelogin.io/docs/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
Docs
|
||||
<i class="fa fa-external-link" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="dropdown-item">
|
||||
<a href="https://github.com/simple-login/app/discussions"
|
||||
target="_blank">
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
Forum
|
||||
<i class="fa fa-external-link" aria-hidden="true"></i>
|
||||
</a>
|
||||
@ -94,7 +97,9 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="nav-item">
|
||||
<a href="https://simplelogin.io/docs/" target="_blank">
|
||||
<a href="https://simplelogin.io/docs/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
Docs
|
||||
<i class="fa fa-external-link" aria-hidden="true"></i>
|
||||
</a>
|
||||
|
@ -98,14 +98,17 @@
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a>
|
||||
<div class="dropdown-menu dropdown-menu-left dropdown-menu-arrow">
|
||||
<div class="dropdown-item">
|
||||
<a href="https://simplelogin.io/docs/" target="_blank">
|
||||
<a href="https://simplelogin.io/docs/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
Docs
|
||||
<i class="fa fa-external-link" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="dropdown-item">
|
||||
<a href="https://github.com/simple-login/app/discussions"
|
||||
target="_blank">
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
Forum
|
||||
<i class="fa fa-external-link" aria-hidden="true"></i>
|
||||
</a>
|
||||
|
39
app/tests/example_emls/bad_base64format.eml
Normal file
39
app/tests/example_emls/bad_base64format.eml
Normal file
File diff suppressed because one or more lines are too long
@ -368,3 +368,19 @@ def test_send_email_from_non_canonical_matches_already_existing_user(flask_clien
|
||||
assert len(email_logs) == 1
|
||||
assert email_logs[0].alias_id == alias.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
|
||||
|
@ -2,7 +2,8 @@ import email
|
||||
from app.email_utils import (
|
||||
copy,
|
||||
)
|
||||
from app.message_utils import message_to_bytes
|
||||
from app.message_utils import message_to_bytes, message_format_base64_parts
|
||||
from tests.utils import load_eml_file
|
||||
|
||||
|
||||
def test_copy():
|
||||
@ -33,3 +34,13 @@ def test_to_bytes():
|
||||
|
||||
msg = email.message_from_string("éèà€")
|
||||
assert message_to_bytes(msg).decode() == "\néèà€"
|
||||
|
||||
|
||||
def test_base64_line_breaks():
|
||||
msg = load_eml_file("bad_base64format.eml")
|
||||
msg = message_format_base64_parts(msg)
|
||||
for part in msg.walk():
|
||||
if part.get("content-transfer-encoding") == "base64":
|
||||
body = part.get_payload()
|
||||
for line in body.splitlines():
|
||||
assert len(line) <= 76
|
||||
|
@ -9,7 +9,12 @@ from app.utils import random_string, random_words, sanitize_next_url, canonicali
|
||||
|
||||
def test_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():
|
||||
@ -66,7 +71,7 @@ def canonicalize_email_cases():
|
||||
yield (f"a@{domain}", f"a@{domain}")
|
||||
yield (f"a.b@{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())
|
||||
|
@ -17,7 +17,7 @@ def create_new_user(email: Optional[str] = None, name: Optional[str] = None) ->
|
||||
if not email:
|
||||
email = f"user_{random_token(10)}@mailbox.test"
|
||||
if not name:
|
||||
name = f"Test User"
|
||||
name = "Test User"
|
||||
# new user has a different email address
|
||||
user = User.create(
|
||||
email=email,
|
||||
|
Reference in New Issue
Block a user