Compare commits

..

8 Commits

Author SHA1 Message Date
822855d584 4.22.5 2023-03-14 12:00:06 +00:00
1a6a7e079b Update '.drone.yml' 2023-03-08 18:32:53 +00:00
5210cb6515 4.22.4 2023-03-08 12:00:06 +00:00
b643f0644b 4.22.3 2023-03-01 12:00:06 +00:00
5d093db4f6 4.22.2 2023-02-16 12:00:05 +00:00
0b16fcac67 Update 'README.md' 2023-02-10 13:00:46 +00:00
a0d294da53 Update 'README.md' 2023-01-27 16:29:12 +00:00
c3f755aede Update '.drone.yml' 2023-01-27 16:26:22 +00:00
33 changed files with 226 additions and 3205 deletions

View File

@ -31,9 +31,15 @@ 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
icon_url:
from_secret: slack_avatar
trigger: trigger:
event: event:

View File

@ -1,5 +1,7 @@
# Simple Login # Simple Login
[![Build Status](https://drone.mrmeeb.stream/api/badges/MrMeeb/simple-login/status.svg?ref=refs/heads/main)](https://drone.mrmeeb.stream/MrMeeb/simple-login)
This repo exists to automatically capture any releases of the SaaS edition of SimpleLogin. It checks once a day, and builds the latest one automatically if it is newer than the currentlty built version. This repo exists to automatically capture any releases of the SaaS edition of SimpleLogin. It checks once a day, and builds the latest one automatically if it is newer than the currentlty built version.
This exists to simplify deployment of SimpleLogin in a self-hosted capacity, while also allowing the use of the latest version; SimpleLogin do not provide an up-to-date version for this use. This exists to simplify deployment of SimpleLogin in a self-hosted capacity, while also allowing the use of the latest version; SimpleLogin do not provide an up-to-date version for this use.

View File

@ -21,3 +21,4 @@ repos:
- id: djlint-jinja - id: djlint-jinja
files: '.*\.html' files: '.*\.html'
entry: djlint --reformat entry: djlint --reformat

View File

@ -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

View File

@ -620,3 +620,8 @@ class MetricAdmin(SLModelView):
column_exclude_list = ["created_at", "updated_at", "id"] column_exclude_list = ["created_at", "updated_at", "id"]
can_export = True can_export = True
class InvalidMailboxDomainAdmin(SLModelView):
can_create = True
can_delete = True

View File

@ -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

View File

@ -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")

View File

@ -17,7 +17,7 @@ from attr import dataclass
from app import config from app import config
from app.email import headers from app.email import headers
from app.log import LOG 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 @dataclass
@ -173,8 +173,12 @@ class MailSender:
self._save_request_to_unsent_dir(send_request) self._save_request_to_unsent_dir(send_request)
return False return False
def _save_request_to_unsent_dir(self, send_request: SendRequest): def _save_request_to_unsent_dir(
file_name = f"DeliveryFail-{int(time.time())}-{uuid.uuid4()}.{SendRequest.SAVE_EXTENSION}" 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_path = os.path.join(config.SAVE_UNSENT_DIR, file_name)
file_contents = send_request.to_bytes() file_contents = send_request.to_bytes()
with open(file_path, "wb") as fd: with open(file_path, "wb") as fd:
@ -256,7 +260,7 @@ def sl_sendmail(
send_request = SendRequest( send_request = SendRequest(
envelope_from, envelope_from,
envelope_to, envelope_to,
msg, message_format_base64_parts(msg),
mail_options, mail_options,
rcpt_options, rcpt_options,
is_forward, is_forward,

View File

@ -1,21 +1,42 @@
import re
from email import policy from email import policy
from email.message import Message from email.message import Message
from app.email import headers
from app.log import LOG 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: def message_to_bytes(msg: Message) -> bytes:
"""replace Message.as_bytes() method by trying different policies""" """replace Message.as_bytes() method by trying different policies"""
for generator_policy in [None, policy.SMTP, policy.SMTPUTF8]: for generator_policy in [None, policy.SMTP, policy.SMTPUTF8]:
try: try:
return msg.as_bytes(policy=generator_policy) return msg.as_bytes(policy=generator_policy)
except: except Exception:
LOG.w("as_bytes() fails with %s policy", policy, exc_info=True) LOG.w("as_bytes() fails with %s policy", policy, exc_info=True)
msg_string = msg.as_string() msg_string = msg.as_string()
try: try:
return msg_string.encode() return msg_string.encode()
except: except Exception:
LOG.w("as_string().encode() fails", exc_info=True) LOG.w("as_string().encode() fails", exc_info=True)
return msg_string.encode(errors="replace") 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

View File

@ -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(
@ -840,10 +870,12 @@ def forward_email_to_mailbox(
orig_subject = msg[headers.SUBJECT] orig_subject = msg[headers.SUBJECT]
orig_subject = get_header_unicode(orig_subject) orig_subject = get_header_unicode(orig_subject)
add_or_replace_header(msg, "Subject", mailbox.generic_subject) add_or_replace_header(msg, "Subject", mailbox.generic_subject)
sender = msg[headers.FROM]
sender = get_header_unicode(sender)
msg = add_header( msg = add_header(
msg, msg,
f"""Forwarded by SimpleLogin to {alias.email} with "{orig_subject}" as subject""", f"""Forwarded by SimpleLogin to {alias.email} from "{sender}" 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 <b>{orig_subject}</b> as subject""",
) )
try: try:

File diff suppressed because it is too large Load Diff

View File

@ -44,6 +44,7 @@ from app.admin_model import (
NewsletterUserAdmin, NewsletterUserAdmin,
DailyMetricAdmin, DailyMetricAdmin,
MetricAdmin, MetricAdmin,
InvalidMailboxDomainAdmin,
) )
from app.api.base import api_bp from app.api.base import api_bp
from app.auth.base import auth_bp from app.auth.base import auth_bp
@ -105,6 +106,7 @@ from app.models import (
NewsletterUser, NewsletterUser,
DailyMetric, DailyMetric,
Metric2, Metric2,
InvalidMailboxDomain,
) )
from app.monitor.base import monitor_bp from app.monitor.base import monitor_bp
from app.newsletter_utils import send_newsletter_to_user from app.newsletter_utils import send_newsletter_to_user
@ -764,6 +766,7 @@ def init_admin(app):
admin.add_view(NewsletterUserAdmin(NewsletterUser, Session)) admin.add_view(NewsletterUserAdmin(NewsletterUser, Session))
admin.add_view(DailyMetricAdmin(DailyMetric, Session)) admin.add_view(DailyMetricAdmin(DailyMetric, Session))
admin.add_view(MetricAdmin(Metric2, Session)) admin.add_view(MetricAdmin(Metric2, Session))
admin.add_view(InvalidMailboxDomainAdmin(InvalidMailboxDomain, Session))
def register_custom_commands(app): def register_custom_commands(app):

View File

@ -50,7 +50,9 @@
</p> </p>
<p> <p>
This Youtube video can also quickly walk you through the steps: 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> How to send emails from an alias <i class="fe fe-external-link"></i>
</a> </a>
</p> </p>

View File

@ -23,7 +23,9 @@
<div class="alert alert-danger" role="alert"> <div class="alert alert-danger" role="alert">
This feature is only available on Premium plan. 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> Upgrade<i class="fe fe-external-link"></i>
</a> </a>
</div> </div>

View File

@ -78,7 +78,7 @@
data-clipboard-text=".*suffix">.*suffix</em> data-clipboard-text=".*suffix">.*suffix</em>
<br /> <br />
To test out regex, we recommend using regex tester tool like 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> </div>
<div class="form-group"> <div class="form-group">

View File

@ -158,7 +158,7 @@
SPF SPF
<a href="https://en.wikipedia.org/wiki/Sender_Policy_Framework" <a href="https://en.wikipedia.org/wiki/Sender_Policy_Framework"
target="_blank" target="_blank"
rel="noopener">(Wikipedia↗)</a> rel="noopener noreferrer">(Wikipedia↗)</a>
is an email is an email
authentication method authentication method
designed to detect forging sender addresses during the delivery of the email. designed to detect forging sender addresses during the delivery of the email.
@ -229,7 +229,7 @@
DKIM DKIM
<a href="https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail" <a href="https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail"
target="_blank" target="_blank"
rel="noopener">(Wikipedia↗)</a> rel="noopener noreferrer">(Wikipedia↗)</a>
is an is an
email email
authentication method authentication method
@ -335,7 +335,7 @@
DMARC DMARC
<a href="https://en.wikipedia.org/wiki/DMARC" <a href="https://en.wikipedia.org/wiki/DMARC"
target="_blank" target="_blank"
rel="noopener"> rel="noopener noreferrer">
(Wikipedia↗) (Wikipedia↗)
</a> </a>
is designed to protect the domain from unauthorized use, commonly known as email spoofing. is designed to protect the domain from unauthorized use, commonly known as email spoofing.

View File

@ -72,7 +72,7 @@ PGP Encryption
</ul> </ul>
<div class="small-text"> <div class="small-text">
More information on our 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 Pricing
Page <i class="fe fe-external-link"></i> Page <i class="fe fe-external-link"></i>
</a> </a>
@ -120,7 +120,7 @@ Upgrade your SimpleLogin account
<div id="normal-upgrade" class="{% if proton_upgrade %} collapse{% endif %}"> <div id="normal-upgrade" class="{% if proton_upgrade %} collapse{% endif %}">
<div class="display-6 my-3"> <div class="display-6 my-3">
🔐 Secure payments by 🔐 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> Paddle <i class="fe fe-external-link"></i>
</a> </a>
</div> </div>
@ -164,13 +164,13 @@ $4/month
<hr /> <hr />
<i class="fa fa-bitcoin"></i> <i class="fa fa-bitcoin"></i>
Payment via 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> Coinbase Commerce<i class="fe fe-external-link"></i>
</a> </a>
<br /> <br />
Currently Bitcoin, Bitcoin Cash, Dai, Ethereum, Litecoin and USD Coin are supported. Currently Bitcoin, Bitcoin Cash, Dai, Ethereum, Litecoin and USD Coin are supported.
<br /> <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 Yearly billing - Crypto
<br /> <br />
$30/year $30/year

View File

@ -17,7 +17,8 @@
<div class="alert alert-info"> <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 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/" <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"> <ul class="p-4 mb-0">
<li> <li>
If the email is indeed spam, this means the alias is now in the hands of a spammer, If the email is indeed spam, this means the alias is now in the hands of a spammer,
@ -26,7 +27,8 @@
<li> <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 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" <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>
<li> <li>
If the email is flagged as spams/phishing, this means that the sender explicitly states their emails should respect If the email is flagged as spams/phishing, this means that the sender explicitly states their emails should respect

View File

@ -73,7 +73,8 @@
Yearly plan subscribed with cryptocurrency which expires on Yearly plan subscribed with cryptocurrency which expires on
{{ coinbase_sub.end_at.format("YYYY-MM-DD") }}. {{ coinbase_sub.end_at.format("YYYY-MM-DD") }}.
<a href="{{ url_for('dashboard.coinbase_checkout_route') }}" <a href="{{ url_for('dashboard.coinbase_checkout_route') }}"
target="_blank"> target="_blank"
rel="noopener noreferrer">
Extend Subscription <i class="fe fe-external-link"></i> Extend Subscription <i class="fe fe-external-link"></i>
</a> </a>
</div> </div>

View File

@ -25,7 +25,7 @@
This feature is only available on Premium plan. This feature is only available on Premium plan.
<a href="{{ url_for('dashboard.pricing') }}" <a href="{{ url_for('dashboard.pricing') }}"
target="_blank" target="_blank"
rel="noopener"> rel="noopener noreferrer">
Upgrade<i class="fe fe-external-link"></i> Upgrade<i class="fe fe-external-link"></i>
</a> </a>
</div> </div>

View File

@ -33,6 +33,7 @@
</div> </div>
<a href="https://docs.simplelogin.io" <a href="https://docs.simplelogin.io"
target="_blank" target="_blank"
rel="noopener noreferrer"
class="btn btn-block btn-secondary mt-4"> class="btn btn-block btn-secondary mt-4">
Documentation <i class="fe fe-external-link"></i> Documentation <i class="fe fe-external-link"></i>
</a> </a>

View File

@ -10,7 +10,9 @@
<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" target="_blank" rel="noopener"> <a href="https://docs.simplelogin.io"
target="_blank"
rel="noopener noreferrer">
documentation <i class="fe fe-external-link"></i> documentation <i class="fe fe-external-link"></i>
</a> </a>
to see how to add SIWSL into your app. to see how to add SIWSL into your app.

View File

@ -49,6 +49,7 @@
<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://docs.simplelogin.io"
target="_blank" target="_blank"
rel="noopener noreferrer"
class="ml-2 btn btn-secondary"> class="ml-2 btn btn-secondary">
Docs <i class="fe fe-external-link"></i> Docs <i class="fe fe-external-link"></i>
</a> </a>

View File

@ -13,7 +13,9 @@
<div class="col-sm-4 col-xl-2"> <div class="col-sm-4 col-xl-2">
<div class="card"> <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() }}"> <img class="card-img-top" src="{{ client.get_icon_url() }}">
</a> </a>
<div class="card-body d-flex flex-column"> <div class="card-body d-flex flex-column">

View File

@ -46,7 +46,7 @@ https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
<a href="{{ link }}" <a href="{{ link }}"
class="f-fallback button" class="f-fallback button"
target="_blank" target="_blank"
rel="noopener" rel="noopener noreferrer"
style="color: #FFF; style="color: #FFF;
border-color: #3869d4; border-color: #3869d4;
border-style: solid; border-style: solid;

View File

@ -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"> <ul class="list-group list-group-transparent list-group-white list-group-flush list-group-borderless mb-0 footer-list-group">
<li> <li>
<a class="list-group-item text-white footer-item " <a class="list-group-item text-white footer-item "
rel="noopener" rel="noopener noreferrer"
href="https://chrome.google.com/webstore/detail/dphilobhebphkdjbpfohgikllaljmgbn"> href="https://chrome.google.com/webstore/detail/dphilobhebphkdjbpfohgikllaljmgbn">
Chrome Extension Chrome Extension
</a> </a>
</li> </li>
<li> <li>
<a class="list-group-item text-white footer-item " <a class="list-group-item text-white footer-item "
rel="noopener" rel="noopener noreferrer"
href="https://addons.mozilla.org/firefox/addon/simplelogin/"> href="https://addons.mozilla.org/firefox/addon/simplelogin/">
Firefox Add-on Firefox Add-on
</a> </a>
</li> </li>
<li> <li>
<a class="list-group-item text-white footer-item " <a class="list-group-item text-white footer-item "
rel="noopener" rel="noopener noreferrer"
href="https://microsoftedge.microsoft.com/addons/detail/simpleloginreceive-sen/diacfpipniklenphgljfkmhinphjlfff"> href="https://microsoftedge.microsoft.com/addons/detail/simpleloginreceive-sen/diacfpipniklenphgljfkmhinphjlfff">
Edge Add-on Edge Add-on
</a> </a>
</li> </li>
<li> <li>
<a class="list-group-item text-white footer-item " <a class="list-group-item text-white footer-item "
rel="noopener" rel="noopener noreferrer"
href="https://apps.apple.com/app/id1494051017"> href="https://apps.apple.com/app/id1494051017">
Safari Safari
Extension Extension
@ -174,7 +174,7 @@
</li> </li>
<li> <li>
<a class="list-group-item text-white footer-item " <a class="list-group-item text-white footer-item "
rel="noopener" rel="noopener noreferrer"
href="https://apps.apple.com/app/id1494359858"> href="https://apps.apple.com/app/id1494359858">
iOS iOS
(App Store) (App Store)
@ -182,14 +182,14 @@
</li> </li>
<li> <li>
<a class="list-group-item text-white footer-item " <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"> href="https://play.google.com/store/apps/details?id=io.simplelogin.android">
Android (Play Store) Android (Play Store)
</a> </a>
</li> </li>
<li> <li>
<a class="list-group-item text-white footer-item " <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/"> href="https://f-droid.org/en/packages/io.simplelogin.android.fdroid/">
Android (F-Droid) Android (F-Droid)
</a> </a>

View File

@ -75,14 +75,17 @@
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a> <a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a>
<div class="dropdown-menu dropdown-menu-left dropdown-menu-arrow"> <div class="dropdown-menu dropdown-menu-left dropdown-menu-arrow">
<div class="dropdown-item"> <div class="dropdown-item">
<a href="https://simplelogin.io/docs/" target="_blank"> <a href="https://simplelogin.io/docs/"
target="_blank"
rel="noopener noreferrer">
Docs Docs
<i class="fa fa-external-link" aria-hidden="true"></i> <i class="fa fa-external-link" aria-hidden="true"></i>
</a> </a>
</div> </div>
<div class="dropdown-item"> <div class="dropdown-item">
<a href="https://github.com/simple-login/app/discussions" <a href="https://github.com/simple-login/app/discussions"
target="_blank"> target="_blank"
rel="noopener noreferrer">
Forum Forum
<i class="fa fa-external-link" aria-hidden="true"></i> <i class="fa fa-external-link" aria-hidden="true"></i>
</a> </a>
@ -94,7 +97,9 @@
</div> </div>
{% else %} {% else %}
<div class="nav-item"> <div class="nav-item">
<a href="https://simplelogin.io/docs/" target="_blank"> <a href="https://simplelogin.io/docs/"
target="_blank"
rel="noopener noreferrer">
Docs Docs
<i class="fa fa-external-link" aria-hidden="true"></i> <i class="fa fa-external-link" aria-hidden="true"></i>
</a> </a>

View File

@ -98,14 +98,17 @@
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a> <a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a>
<div class="dropdown-menu dropdown-menu-left dropdown-menu-arrow"> <div class="dropdown-menu dropdown-menu-left dropdown-menu-arrow">
<div class="dropdown-item"> <div class="dropdown-item">
<a href="https://simplelogin.io/docs/" target="_blank"> <a href="https://simplelogin.io/docs/"
target="_blank"
rel="noopener noreferrer">
Docs Docs
<i class="fa fa-external-link" aria-hidden="true"></i> <i class="fa fa-external-link" aria-hidden="true"></i>
</a> </a>
</div> </div>
<div class="dropdown-item"> <div class="dropdown-item">
<a href="https://github.com/simple-login/app/discussions" <a href="https://github.com/simple-login/app/discussions"
target="_blank"> target="_blank"
rel="noopener noreferrer">
Forum Forum
<i class="fa fa-external-link" aria-hidden="true"></i> <i class="fa fa-external-link" aria-hidden="true"></i>
</a> </a>

File diff suppressed because one or more lines are too long

View File

@ -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

View File

@ -2,7 +2,8 @@ import email
from app.email_utils import ( from app.email_utils import (
copy, 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(): def test_copy():
@ -33,3 +34,13 @@ def test_to_bytes():
msg = email.message_from_string("éèà€") msg = email.message_from_string("éèà€")
assert message_to_bytes(msg).decode() == "\néèà€" 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

View File

@ -66,7 +66,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())

View File

@ -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,