Compare commits

..

1 Commits

Author SHA1 Message Date
f025458998 4.22.1 2023-02-10 12:00:04 +00:00
54 changed files with 326774 additions and 11848 deletions

View File

@ -31,15 +31,9 @@ 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,7 +1,5 @@
# 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,4 +21,3 @@ repos:
- id: djlint-jinja - id: djlint-jinja
files: '.*\.html' files: '.*\.html'
entry: djlint --reformat entry: djlint --reformat

View File

@ -9,17 +9,13 @@ 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 ( from app.errors import AccountAlreadyLinkedToAnotherPartnerException
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
@ -196,18 +192,11 @@ 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,8 +620,3 @@ 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

@ -6,7 +6,8 @@ from typing import Optional
import itsdangerous import itsdangerous
from app import config from app import config
from app.log import LOG from app.log import LOG
from app.models import User, AliasOptions from app.models import User
signer = itsdangerous.TimestampSigner(config.CUSTOM_ALIAS_SECRET) signer = itsdangerous.TimestampSigner(config.CUSTOM_ALIAS_SECRET)
@ -42,9 +43,7 @@ def check_suffix_signature(signed_suffix: str) -> Optional[str]:
return None return None
def verify_prefix_suffix( def verify_prefix_suffix(user: User, alias_prefix, alias_suffix) -> bool:
user: User, alias_prefix, alias_suffix, alias_options: Optional[AliasOptions] = None
) -> bool:
"""verify if user could create an alias with the given prefix and suffix""" """verify if user could create an alias with the given prefix and suffix"""
if not alias_prefix or not alias_suffix: # should be caught on frontend if not alias_prefix or not alias_suffix: # should be caught on frontend
return False return False
@ -57,7 +56,7 @@ def verify_prefix_suffix(
alias_domain_prefix, alias_domain = alias_suffix.split("@", 1) alias_domain_prefix, alias_domain = alias_suffix.split("@", 1)
# alias_domain must be either one of user custom domains or built-in domains # alias_domain must be either one of user custom domains or built-in domains
if alias_domain not in user.available_alias_domains(alias_options=alias_options): if alias_domain not in user.available_alias_domains():
LOG.e("wrong alias suffix %s, user %s", alias_suffix, user) LOG.e("wrong alias suffix %s, user %s", alias_suffix, user)
return False return False
@ -65,7 +64,7 @@ def verify_prefix_suffix(
# 1) alias_suffix must start with "." and # 1) alias_suffix must start with "." and
# 2) alias_domain_prefix must come from the word list # 2) alias_domain_prefix must come from the word list
if ( if (
alias_domain in user.available_sl_domains(alias_options=alias_options) alias_domain in user.available_sl_domains()
and alias_domain not in user_custom_domains and alias_domain not in user_custom_domains
# when DISABLE_ALIAS_SUFFIX is true, alias_domain_prefix is empty # when DISABLE_ALIAS_SUFFIX is true, alias_domain_prefix is empty
and not config.DISABLE_ALIAS_SUFFIX and not config.DISABLE_ALIAS_SUFFIX
@ -81,18 +80,14 @@ def verify_prefix_suffix(
LOG.e("wrong alias suffix %s, user %s", alias_suffix, user) LOG.e("wrong alias suffix %s, user %s", alias_suffix, user)
return False return False
if alias_domain not in user.available_sl_domains( if alias_domain not in user.available_sl_domains():
alias_options=alias_options
):
LOG.e("wrong alias suffix %s, user %s", alias_suffix, user) LOG.e("wrong alias suffix %s, user %s", alias_suffix, user)
return False return False
return True return True
def get_alias_suffixes( def get_alias_suffixes(user: User) -> [AliasSuffix]:
user: User, alias_options: Optional[AliasOptions] = None
) -> [AliasSuffix]:
""" """
Similar to as get_available_suffixes() but also return custom domain that doesn't have MX set up. Similar to as get_available_suffixes() but also return custom domain that doesn't have MX set up.
""" """
@ -104,12 +99,7 @@ def get_alias_suffixes(
# for each user domain, generate both the domain and a random suffix version # for each user domain, generate both the domain and a random suffix version
for custom_domain in user_custom_domains: for custom_domain in user_custom_domains:
if custom_domain.random_prefix_generation: if custom_domain.random_prefix_generation:
suffix = ( suffix = "." + user.get_random_alias_suffix() + "@" + custom_domain.domain
"."
+ user.get_random_alias_suffix(custom_domain)
+ "@"
+ custom_domain.domain
)
alias_suffix = AliasSuffix( alias_suffix = AliasSuffix(
is_custom=True, is_custom=True,
suffix=suffix, suffix=suffix,
@ -144,7 +134,7 @@ def get_alias_suffixes(
alias_suffixes.append(alias_suffix) alias_suffixes.append(alias_suffix)
# then SimpleLogin domain # then SimpleLogin domain
for sl_domain in user.get_sl_domains(alias_options=alias_options): for sl_domain in user.get_sl_domains():
suffix = ( suffix = (
( (
"" ""

View File

@ -357,7 +357,7 @@ def auth_payload(user, device) -> dict:
@api_bp.route("/auth/forgot_password", methods=["POST"]) @api_bp.route("/auth/forgot_password", methods=["POST"])
@limiter.limit("2/minute") @limiter.limit("10/minute")
def forgot_password(): def forgot_password():
""" """
User forgot password User forgot password

View File

@ -357,7 +357,6 @@ 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 >>>>

View File

@ -215,12 +215,6 @@ 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))

View File

@ -120,11 +120,18 @@ 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")

View File

@ -80,9 +80,8 @@ def pricing():
@dashboard_bp.route("/subscription_success") @dashboard_bp.route("/subscription_success")
@login_required @login_required
def subscription_success(): def subscription_success():
return render_template( flash("Thanks so much for supporting SimpleLogin!", "success")
"dashboard/thank-you.html", return redirect(url_for("dashboard.index"))
)
@dashboard_bp.route("/coinbase_checkout") @dashboard_bp.route("/coinbase_checkout")

View File

@ -60,5 +60,4 @@ 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 "Please upgrade to premium to create reverse-alias" return f"Please upgrade to premium to create reverse-alias"
class ErrAddressInvalid(SLException): class ErrAddressInvalid(SLException):
@ -108,8 +108,3 @@ 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, message_format_base64_parts from app.message_utils import message_to_bytes
@dataclass @dataclass
@ -170,16 +170,11 @@ 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
def _save_request_to_unsent_dir( def _save_request_to_unsent_dir(self, send_request: SendRequest):
self, send_request: SendRequest, prefix: str = "DeliveryFail" file_name = f"DeliveryFail-{int(time.time())}-{uuid.uuid4()}.{SendRequest.SAVE_EXTENSION}"
):
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:
@ -261,7 +256,7 @@ def sl_sendmail(
send_request = SendRequest( send_request = SendRequest(
envelope_from, envelope_from,
envelope_to, envelope_to,
message_format_base64_parts(msg), msg,
mail_options, mail_options,
rcpt_options, rcpt_options,
is_forward, is_forward,

View File

@ -1,42 +1,21 @@
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 Exception: except:
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 Exception: except:
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

@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
import base64 import base64
import dataclasses
import enum import enum
import hashlib import hashlib
import hmac import hmac
@ -19,7 +18,7 @@ from flanker.addresslib import address
from flask import url_for from flask import url_for
from flask_login import UserMixin from flask_login import UserMixin
from jinja2 import FileSystemLoader, Environment from jinja2 import FileSystemLoader, Environment
from sqlalchemy import orm, or_ from sqlalchemy import orm
from sqlalchemy import text, desc, CheckConstraint, Index, Column from sqlalchemy import text, desc, CheckConstraint, Index, Column
from sqlalchemy.dialects.postgresql import TSVECTOR from sqlalchemy.dialects.postgresql import TSVECTOR
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
@ -45,6 +44,7 @@ from app.utils import (
random_string, random_string,
random_words, random_words,
sanitize_email, sanitize_email,
random_word,
) )
Base = declarative_base() Base = declarative_base()
@ -274,12 +274,6 @@ class IntEnumType(sa.types.TypeDecorator):
return self._enum_type(enum_value) return self._enum_type(enum_value)
@dataclasses.dataclass
class AliasOptions:
show_sl_domains: bool = True
show_partner_domains: Optional[Partner] = None
class Hibp(Base, ModelMixin): class Hibp(Base, ModelMixin):
__tablename__ = "hibp" __tablename__ = "hibp"
name = sa.Column(sa.String(), nullable=False, unique=True, index=True) name = sa.Column(sa.String(), nullable=False, unique=True, index=True)
@ -525,7 +519,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.PreserveOriginal, default=UnsubscribeBehaviourEnum.DisableAlias,
server_default=str(UnsubscribeBehaviourEnum.DisableAlias.value), server_default=str(UnsubscribeBehaviourEnum.DisableAlias.value),
nullable=False, nullable=False,
) )
@ -874,16 +868,14 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
def custom_domains(self): def custom_domains(self):
return CustomDomain.filter_by(user_id=self.id, verified=True).all() return CustomDomain.filter_by(user_id=self.id, verified=True).all()
def available_domains_for_random_alias( def available_domains_for_random_alias(self) -> List[Tuple[bool, str]]:
self, alias_options: Optional[AliasOptions] = None
) -> List[Tuple[bool, str]]:
"""Return available domains for user to create random aliases """Return available domains for user to create random aliases
Each result record contains: Each result record contains:
- whether the domain belongs to SimpleLogin - whether the domain belongs to SimpleLogin
- the domain - the domain
""" """
res = [] res = []
for domain in self.available_sl_domains(alias_options=alias_options): for domain in self.available_sl_domains():
res.append((True, domain)) res.append((True, domain))
for custom_domain in self.verified_custom_domains(): for custom_domain in self.verified_custom_domains():
@ -968,55 +960,30 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
return None, "", False return None, "", False
def available_sl_domains( def available_sl_domains(self) -> [str]:
self, alias_options: Optional[AliasOptions] = None
) -> [str]:
""" """
Return all SimpleLogin domains that user can use when creating a new alias, including: Return all SimpleLogin domains that user can use when creating a new alias, including:
- SimpleLogin public domains, available for all users (ALIAS_DOMAIN) - SimpleLogin public domains, available for all users (ALIAS_DOMAIN)
- SimpleLogin premium domains, only available for Premium accounts (PREMIUM_ALIAS_DOMAIN) - SimpleLogin premium domains, only available for Premium accounts (PREMIUM_ALIAS_DOMAIN)
""" """
return [ return [sl_domain.domain for sl_domain in self.get_sl_domains()]
sl_domain.domain
for sl_domain in self.get_sl_domains(alias_options=alias_options)
]
def get_sl_domains( def get_sl_domains(self) -> List["SLDomain"]:
self, alias_options: Optional[AliasOptions] = None query = SLDomain.filter_by(hidden=False).order_by(SLDomain.order)
) -> list["SLDomain"]:
if alias_options is None: if self.is_premium():
alias_options = AliasOptions()
conditions = [SLDomain.hidden == False] # noqa: E712
if not self.is_premium():
conditions.append(SLDomain.premium_only == False) # noqa: E712
partner_domain_cond = [] # noqa:E711
if alias_options.show_partner_domains is not None:
partner_user = PartnerUser.filter_by(
user_id=self.id, partner_id=alias_options.show_partner_domains.id
).first()
if partner_user is not None:
partner_domain_cond.append(
SLDomain.partner_id == partner_user.partner_id
)
if alias_options.show_sl_domains:
partner_domain_cond.append(SLDomain.partner_id == None) # noqa:E711
if len(partner_domain_cond) == 1:
conditions.append(partner_domain_cond[0])
else:
conditions.append(or_(*partner_domain_cond))
query = Session.query(SLDomain).filter(*conditions).order_by(SLDomain.order)
return query.all() return query.all()
else:
return query.filter_by(premium_only=False).all()
def available_alias_domains( def available_alias_domains(self) -> [str]:
self, alias_options: Optional[AliasOptions] = None
) -> [str]:
"""return all domains that user can use when creating a new alias, including: """return all domains that user can use when creating a new alias, including:
- SimpleLogin public domains, available for all users (ALIAS_DOMAIN) - SimpleLogin public domains, available for all users (ALIAS_DOMAIN)
- SimpleLogin premium domains, only available for Premium accounts (PREMIUM_ALIAS_DOMAIN) - SimpleLogin premium domains, only available for Premium accounts (PREMIUM_ALIAS_DOMAIN)
- Verified custom domains - Verified custom domains
""" """
domains = self.available_sl_domains(alias_options=alias_options) domains = self.available_sl_domains()
for custom_domain in self.verified_custom_domains(): for custom_domain in self.verified_custom_domains():
domains.append(custom_domain.domain) domains.append(custom_domain.domain)
@ -1034,21 +1001,16 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
> 0 > 0
) )
def get_random_alias_suffix(self, custom_domain: Optional["CustomDomain"] = None): def get_random_alias_suffix(self):
"""Get random suffix for an alias based on user's preference. """Get random suffix for an alias based on user's preference.
Use a shorter suffix in case of custom domain
Returns: Returns:
str: the random suffix generated str: the random suffix generated
""" """
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()
if custom_domain is None:
return random_words(1, 3)
return random_words(1)
def __repr__(self): def __repr__(self):
return f"<User {self.id} {self.name} {self.email}>" return f"<User {self.id} {self.name} {self.email}>"
@ -1307,7 +1269,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(2, 3) + "@" + alias_domain random_email = random_words() + "@" + alias_domain
random_email = random_email.lower().strip() random_email = random_email.lower().strip()
@ -2802,31 +2764,6 @@ class Notification(Base, ModelMixin):
) )
class Partner(Base, ModelMixin):
__tablename__ = "partner"
name = sa.Column(sa.String(128), unique=True, nullable=False)
contact_email = sa.Column(sa.String(128), unique=True, nullable=False)
@staticmethod
def find_by_token(token: str) -> Optional[Partner]:
hmaced = PartnerApiToken.hmac_token(token)
res = (
Session.query(Partner, PartnerApiToken)
.filter(
and_(
PartnerApiToken.token == hmaced,
Partner.id == PartnerApiToken.partner_id,
)
)
.first()
)
if res:
partner, partner_api_token = res
return partner
return None
class SLDomain(Base, ModelMixin): class SLDomain(Base, ModelMixin):
"""SimpleLogin domains""" """SimpleLogin domains"""
@ -2844,13 +2781,6 @@ class SLDomain(Base, ModelMixin):
sa.Boolean, nullable=False, default=False, server_default="0" sa.Boolean, nullable=False, default=False, server_default="0"
) )
partner_id = sa.Column(
sa.ForeignKey(Partner.id, ondelete="cascade"),
nullable=True,
default=None,
sever_default="NULL",
)
# if enabled, do not show this domain when user creates a custom alias # if enabled, do not show this domain when user creates a custom alias
hidden = sa.Column(sa.Boolean, nullable=False, default=False, server_default="0") hidden = sa.Column(sa.Boolean, nullable=False, default=False, server_default="0")
@ -3297,6 +3227,31 @@ class ProviderComplaint(Base, ModelMixin):
refused_email = orm.relationship(RefusedEmail, foreign_keys=[refused_email_id]) refused_email = orm.relationship(RefusedEmail, foreign_keys=[refused_email_id])
class Partner(Base, ModelMixin):
__tablename__ = "partner"
name = sa.Column(sa.String(128), unique=True, nullable=False)
contact_email = sa.Column(sa.String(128), unique=True, nullable=False)
@staticmethod
def find_by_token(token: str) -> Optional[Partner]:
hmaced = PartnerApiToken.hmac_token(token)
res = (
Session.query(Partner, PartnerApiToken)
.filter(
and_(
PartnerApiToken.token == hmaced,
Partner.id == PartnerApiToken.partner_id,
)
)
.first()
)
if res:
partner, partner_api_token = res
return partner
return None
class PartnerApiToken(Base, ModelMixin): class PartnerApiToken(Base, ModelMixin):
__tablename__ = "partner_api_token" __tablename__ = "partner_api_token"

View File

@ -1,4 +1,3 @@
import random
import re import re
import secrets import secrets
import string import string
@ -26,16 +25,11 @@ def word_exist(word):
return word in _words return word in _words
def random_words(words: int = 2, numbers: int = 0): def random_words():
"""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)
fields = [secrets.choice(_words) for i in range(words)] nb_words = 2
return "_".join([secrets.choice(_words) for i in range(nb_words)])
if numbers > 0:
digits = "".join([str(random.randint(0, 9)) for i in range(numbers)])
return "_".join(fields) + digits
else:
return "_".join(fields)
def random_string(length=10, include_digits=False): def random_string(length=10, include_digits=False):

View File

@ -693,36 +693,6 @@ 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(
@ -870,12 +840,10 @@ 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} from "{sender}" with "{orig_subject}" as subject""", f"""Forwarded by SimpleLogin to {alias.email} with "{orig_subject}" as subject""",
f"""Forwarded by SimpleLogin to {alias.email} from "{sender}" with <b>{orig_subject}</b> as subject""", f"""Forwarded by SimpleLogin to {alias.email} with <b>{orig_subject}</b> as subject""",
) )
try: try:

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +0,0 @@
"""empty message
Revision ID: 5f4a5625da66
Revises: 2c2093c82bc0
Create Date: 2023-04-03 18:30:46.488231
"""
import sqlalchemy_utils
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '5f4a5625da66'
down_revision = '2c2093c82bc0'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('public_domain', sa.Column('partner_id', sa.Integer(), nullable=True, sever_default='NULL'))
op.create_foreign_key(None, 'public_domain', 'partner', ['partner_id'], ['id'], ondelete='cascade')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'public_domain', type_='foreignkey')
op.drop_column('public_domain', 'partner_id')
# ### end Alembic commands ###

View File

@ -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://simplelogin.io/docs/siwsl/app/ The step-to-step guide can be found on https://docs.simplelogin.io
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
""" """

4400
app/poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -110,7 +110,7 @@ twilio = "^7.3.2"
Deprecated = "^1.2.13" Deprecated = "^1.2.13"
cryptography = "37.0.1" cryptography = "37.0.1"
SQLAlchemy = "1.3.24" SQLAlchemy = "1.3.24"
redis = "^4.5.3" redis = "^4.3.4"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pytest = "^7.0.0" pytest = "^7.0.0"

View File

@ -44,7 +44,6 @@ 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
@ -106,7 +105,6 @@ 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
@ -766,7 +764,6 @@ 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

@ -8,8 +8,7 @@ function enableDragDropForPGPKeys(inputID) {
let files = event.dataTransfer.files; let files = event.dataTransfer.files;
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
let file = files[i]; let file = files[i];
const isValidPgpFile = file.type === 'text/plain' || file.name.endsWith('.asc') || file.name.endsWith('.pub') || file.name.endsWith('.pgp') || file.name.endsWith('.key'); if(file.type !== 'text/plain'){
if (!isValidPgpFile) {
toastr.warning(`File ${file.name} is not a public key file`); toastr.warning(`File ${file.name} is not a public key file`);
continue; continue;
} }
@ -17,7 +16,6 @@ function enableDragDropForPGPKeys(inputID) {
reader.onloadend = onFileLoaded; reader.onloadend = onFileLoaded;
reader.readAsBinaryString(file); reader.readAsBinaryString(file);
} }
dropArea.classList.remove("dashed-outline");
} }
function onFileLoaded(event) { function onFileLoaded(event) {
@ -26,20 +24,5 @@ function enableDragDropForPGPKeys(inputID) {
} }
const dropArea = $(inputID).get(0); const dropArea = $(inputID).get(0);
dropArea.addEventListener("dragenter", (event) => {
event.stopPropagation();
event.preventDefault();
dropArea.classList.add("dashed-outline");
});
dropArea.addEventListener("dragover", (event) => {
event.stopPropagation();
event.preventDefault();
dropArea.classList.add("dashed-outline");
});
dropArea.addEventListener("dragleave", (event) => {
event.stopPropagation();
event.preventDefault();
dropArea.classList.remove("dashed-outline");
});
dropArea.addEventListener("drop", drop, false); dropArea.addEventListener("drop", drop, false);
} }

View File

@ -218,8 +218,3 @@ textarea.parsley-error {
.italic { .italic {
font-style: italic; font-style: italic;
} }
/* dashed outline to indicate droppable area */
.dashed-outline {
outline: 4px dashed gray;
}

View File

@ -50,9 +50,7 @@
</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" <a href="https://www.youtube.com/watch?v=VsypF-DBaow" target="_blank">
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

@ -43,7 +43,7 @@
{% endif %} {% endif %}
<div class="form-group"> <div class="form-group">
<label class="form-label">PGP Public Key</label> <label class="form-label">PGP Public Key</label>
<textarea name="pgp" {% if not current_user.is_premium() %} disabled {% endif %} class="form-control" rows=10 id="pgp-public-key" placeholder="(Drag and drop or paste your pgp public key here)&#10;-----BEGIN PGP PUBLIC KEY BLOCK-----">{{ contact.pgp_public_key or "" }}</textarea> <textarea name="pgp" {% if not current_user.is_premium() %} disabled {% endif %} class="form-control" rows=10 id="pgp-public-key" placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----">{{ contact.pgp_public_key or "" }}</textarea>
</div> </div>
<button class="btn btn-primary" name="action" {% if not current_user.is_premium() %} <button class="btn btn-primary" name="action" {% if not current_user.is_premium() %}
disabled {% endif %} value="save"> disabled {% endif %} value="save">

View File

@ -23,9 +23,7 @@
<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" <a href="{{ URL }}/dashboard/pricing" target="_blank" rel="noopener">
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" rel="noopener noreferrer">https://regex101.com↗</a> <a href="https://regex101.com" target="_blank">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 noreferrer">(Wikipedia↗)</a> rel="noopener">(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 noreferrer">(Wikipedia↗)</a> rel="noopener">(Wikipedia↗)</a>
is an is an
email email
authentication method authentication method
@ -266,9 +266,7 @@
<i>dkim._domainkey.{{ custom_domain.domain }}</i> as domain value instead. <i>dkim._domainkey.{{ custom_domain.domain }}</i> as domain value instead.
<br /> <br />
If you are using a subdomain, e.g. <i>subdomain.domain.com</i>, If you are using a subdomain, e.g. <i>subdomain.domain.com</i>,
you need to use <i>dkim._domainkey.subdomain</i> as the domain instead. you need to use <i>dkim._domainkey.subdomain</i> as domain value instead.
<br />
That means, if your domain is <i>mail.domain.com</i> you should enter <i>dkim._domainkey.mail.domain.com</i> as the Domain.
<br /> <br />
</div> </div>
<div class="alert alert-info"> <div class="alert alert-info">
@ -337,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 noreferrer"> rel="noopener">
(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

@ -112,7 +112,7 @@
{{ csrf_form.csrf_token }} {{ csrf_form.csrf_token }}
<div class="form-group"> <div class="form-group">
<label class="form-label">PGP Public Key</label> <label class="form-label">PGP Public Key</label>
<textarea name="pgp" {% if not current_user.is_premium() %} disabled {% endif %} class="form-control" rows=10 id="pgp-public-key" placeholder="(Drag and drop or paste your pgp public key here)&#10;-----BEGIN PGP PUBLIC KEY BLOCK-----">{{ mailbox.pgp_public_key or "" }}</textarea> <textarea name="pgp" {% if not current_user.is_premium() %} disabled {% endif %} class="form-control" rows=10 id="pgp-public-key" placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----">{{ mailbox.pgp_public_key or "" }}</textarea>
</div> </div>
<input type="hidden" name="form-name" value="pgp"> <input type="hidden" name="form-name" value="pgp">
<button class="btn btn-primary" name="action" {% if not current_user.is_premium() %} <button class="btn btn-primary" name="action" {% if not current_user.is_premium() %}

View File

@ -8,8 +8,7 @@
<script> <script>
if (window.Paddle === undefined) { if (window.Paddle === undefined) {
console.log("cannot load Paddle from CDN"); console.log("cannot load Paddle from CDN");
// split string to avoid djlint incorrectly formatting the file document.write('<script src="/static/vendor/paddle.js"><\/script>')
document.write('<' + 'script src="/static/vendor/paddle.js"><\/script' + '>');
} }
</script> </script>
<style type="text/css"> <style type="text/css">
@ -26,23 +25,6 @@
[data-toggle="collapse"]:not(.collapsed) .if-collapsed { [data-toggle="collapse"]:not(.collapsed) .if-collapsed {
display: none; display: none;
} }
.btn-no-pointer {
pointer-events: none !important;
}
.tab-yearly__badge {
top: -8px !important;
left: 52px !important;
}
.border-2 {
border-width: 2px !important;
}
.text-start {
text-align: start !important;
}
</style> </style>
{% endblock %} {% endblock %}
{% block announcement %} {% block announcement %}
@ -57,13 +39,51 @@
{% endblock %} {% endblock %}
{% block default_content %} {% block default_content %}
<div class="pb-8"> <div class="row">
<div class="text-center mx-md-auto mb-8 mt-6"> <div class="col-sm-6 col-lg-6">
<h1>Upgrade to unlock premium features</h1> <div class="card">
<div class="card-body text-center">
<div class="h3">Premium</div>
<ul class="list-unstyled leading-loose mb-3">
<li>
<i class="fe fe-check text-success mr-2" aria-hidden="true"></i>
Unlimited aliases
</li>
<li>
<i class="fe fe-check text-success mr-2" aria-hidden="true"></i>
Unlimited custom domains
</li>
<li>
<i class="fe fe-check text-success mr-2" aria-hidden="true"></i>
Catch-all (or wildcard) aliases
</li>
<li>
<i class="fe fe-check text-success mr-2" aria-hidden="true"></i>
Up to 50 directories (or usernames)
</li>
<li>
<i class="fe fe-check text-success mr-2" aria-hidden="true"></i>
Unlimited mailboxes
</li>
<li>
<i class="fe fe-check text-success mr-2" aria-hidden="true"></i>
PGP Encryption
</li>
</ul>
<div class="small-text">
More information on our
<a href="https://simplelogin.io/pricing" target="_blank" rel="noopener">
Pricing
Page <i class="fe fe-external-link"></i>
</a>
</div> </div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-6">
{% if manual_sub %} {% if manual_sub %}
<div class="alert alert-info mt-0 mb-6"> <div class="alert alert-info">
You currently have a subscription until <b>{{ manual_sub.end_at.format("YYYY-MM-DD") }}</b> You currently have a subscription until <b>{{ manual_sub.end_at.format("YYYY-MM-DD") }}</b>
({{ (manual_sub.end_at - now).days }} days left). ({{ (manual_sub.end_at - now).days }} days left).
<br /> <br />
@ -71,10 +91,43 @@
</div> </div>
<hr /> <hr />
{% endif %} {% endif %}
{% if proton_upgrade %}
<div id="proton-upgrade">
<h4>Proton Unlimited, Business and Visionary plans include SimpleLogin premium and more!</h4>
<a class="btn btn-primary" role="button" href="https://account.proton.me/u/0/mail/upgrade">
<b>Upgrade your Proton account</b>
</a>
<p class="mt-2 small">
Starts at $9.99/month (billed yearly), starting with 500GB of storage, VPN, encrypted
calendar & file storage and more.
</p>
<div class="middle-line my-5 h4">OR</div>
<div id="normal-upgrade-button">
<a class="btn btn-secondary collapsed" data-toggle="collapse" href="#normal-upgrade" role="button">
Upgrade your SimpleLogin account
<span class="if-collapsed">
<i class="fe fe-chevron-down"></i>
</span>
<span class="if-not-collapsed">
<i class="fe fe-chevron-up"></i>
</span>
</a>
<p class="mt-2 small">Starts at $2.5/month (billed yearly)</p>
</div>
</div>
{% endif %}
<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">
Paddle <i class="fe fe-external-link"></i>
</a>
</div>
{% set sub = current_user.get_paddle_subscription() %} {% set sub = current_user.get_paddle_subscription() %}
{% if sub and sub.cancelled %} {% if sub and sub.cancelled %}
<div class="alert alert-primary mt-0 mb-6" role="alert"> <div class="alert alert-primary" role="alert">
You have an active subscription until {{ sub.next_bill_date.strftime("%Y-%m-%d") }}. You have an active subscription until {{ sub.next_bill_date.strftime("%Y-%m-%d") }}.
<br /> <br />
Please note that if you re-subscribe now, this will be a completely Please note that if you re-subscribe now, this will be a completely
@ -84,679 +137,78 @@
{% endif %} {% endif %}
{% if coinbase_sub %} {% if coinbase_sub %}
<div class="alert alert-info mt-0 mb-6"> <div class="alert alert-info">
You currently have a Coinbase subscription until <b>{{ coinbase_sub.end_at.format("YYYY-MM-DD") }}</b> You currently have a Coinbase subscription until <b>{{ coinbase_sub.end_at.format("YYYY-MM-DD") }}</b>
({{ (coinbase_sub.end_at - now).days }} days left). ({{ (coinbase_sub.end_at - now).days }} days left).
<br /> <br />
Please note that the time left will <b>not</b> be taken into account in a new Paddle subscription. Please note that the time left will <b>not</b> be taken into account in a new Paddle subscription.
</div> </div>
{% endif %} {% endif %}
<div class="nav btn-group mb-4 justify-content-center position-relative flex-nowrap d-flex" <div class="mb-3">
id="pills-tab" Paddle supports bank cards
role="tablist"> (Mastercard, Visa, American Express, etc) and PayPal.
<a class="btn btn-outline-primary flex-grow-0 px-8 py-2"
id="monthly-plan-tab"
data-toggle="tab"
href="#monthly-plan"
role="tab"
aria-controls="monthly-plan"
aria-selected="false">Monthly</a>
<a class="btn btn-outline-primary flex-grow-0 px-8 py-2 position-relative active"
id="yearly-plan-tab"
data-toggle="tab"
href="#yearly-plan"
role="tab"
aria-controls="yearly-plan"
aria-selected="true">Yearly<span class="badge badge-success position-absolute tab-yearly__badge"
style="font-size: 12px">Save $18</span></a>
</div> </div>
<div class="tab-content mb-8"> <button class="btn btn-primary" onclick="upgrade({{ PADDLE_YEARLY_PRODUCT_ID }})">
<!-- monthly tab content --> Yearly billing
<div class="tab-pane" <span class="badge badge-success">Save $18</span>
id="monthly-plan" <br />
role="tabpanel" <span style="font-size: 18px">$30/year</span>
aria-labelledby="monthly-plan-tab">
<div class="row row-cards">
<!-- monthly free plan -->
<div class="{{ 'col-md-6 col-lg-4' if proton_upgrade else 'col-md-6' }}">
<div class="card card-md flex-grow-1">
<div class="card-body">
<div class="text-center">
<div class="h3">Free</div>
<div class="h3 my-3">$0</div>
<div class="text-center mt-4 mb-6">
{% set sub = current_user.get_paddle_subscription() %}
<button class="{{ 'invisible' if sub or manual_sub or coinbase_sub }} btn btn-lg btn-outline-secondary w-100 btn-no-pointer"
aria-disabled="true"
disabled>
Current plan
</button> </button>
</div> <button class="btn btn-secondary" onclick="upgrade({{ PADDLE_MONTHLY_PRODUCT_ID }})">
</div> Monthly billing
<ul class="list-unstyled"> <br />
<li class="d-flex"> <b>
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i> $4/month
10 aliases </b>
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
1 mailbox
</li>
</ul>
</div>
</div>
</div>
<!-- END monthly free plan -->
<!-- monthly premium plan -->
<div class="{{ 'col-md-6 col-lg-4' if proton_upgrade else 'col-md-6' }}">
<div class="card card-md flex-grow-1 border-primary border-2">
<div class="card-body">
<div class="text-center">
<div class="h3">SimpleLogin Premium</div>
<div class="h3 my-3">$4 / month</div>
<div class="text-center mt-4 mb-6">
<button class="btn btn-primary btn-lg w-100"
onclick="upgradePaddle({{ PADDLE_MONTHLY_PRODUCT_ID }})">
Upgrade to Premium
</button> </button>
</div>
</div>
<ul class="list-unstyled">
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
Unlimited aliases
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
Unlimited mailboxes
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
Custom domains: bring your own domain to create aliases like contact@your-domain.com
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
Catch-all (or wildcard) domain
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
Initiate a new email from your alias
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
5 subdomains
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
50 directories
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
PGP Encryption
</li>
</ul>
</div>
</div>
</div>
<!-- END monthly premium plan -->
<!-- monthly Proton plan -->
{% if proton_upgrade %}
<div class="col-md-6 col-lg-4">
<div class="card card-md flex-grow-1">
<div class="card-body">
<div class="text-center">
<div class="h3">Proton plan</div>
<div class="h3 my-3">Starts at $11.99 / month</div>
<div class="text-center mt-4 mb-6">
<a class="btn btn-lg btn-outline-primary w-100"
role="button"
href="https://account.proton.me/u/0/mail/upgrade"
target="_blank">Upgrade your Proton account</a>
</div>
</div>
<p>Proton Unlimited / Business plans include:</p>
<ul class="list-unstyled">
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
SimpleLogin Premium
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
500 GB storage
</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">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
Unlimited folders, labels, and filters
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
Unlimited messages per day
</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">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
20 Calendars
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
10 high-speed VPN connections
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
3 custom email domains
</li>
</ul>
</div>
</div>
</div>
{% endif %}
<!-- END monthly Proton plan -->
</div>
</div>
<!-- END monthly tab content -->
<!-- yearly tab content -->
<div class="tab-pane show active"
id="yearly-plan"
role="tabpanel"
aria-labelledby="yearly-plan-tab">
<div class="row row-cards">
<!-- yearly free plan (identical to monthly) -->
<div class="{{ 'col-md-6 col-lg-4' if proton_upgrade else 'col-md-6' }}">
<div class="card card-md flex-grow-1">
<div class="card-body">
<div class="text-center">
<div class="h3">Free</div>
<div class="h3 my-3">$0</div>
<div class="text-center mt-4 mb-6">
{% set sub = current_user.get_paddle_subscription() %}
<button class="{{ 'invisible' if sub or manual_sub or coinbase_sub }} btn btn-lg btn-outline-secondary w-100 btn-no-pointer"
aria-disabled="true"
disabled>
Current plan
</button>
</div>
</div>
<ul class="list-unstyled">
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
10 aliases
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
1 mailbox
</li>
</ul>
</div>
</div>
</div>
<!-- END yearly free plan -->
<!-- yearly premium plan -->
<div class="{{ 'col-md-6 col-lg-4' if proton_upgrade else 'col-md-6' }}">
<div class="card card-md flex-grow-1 border-primary border-2">
<div class="card-body">
<div class="text-center">
<div class="h3">SimpleLogin Premium</div>
<div class="h3 my-3">$30 / year</div>
<div class="text-center mt-4 mb-6">
<button class="btn btn-primary btn-lg w-100"
onclick="upgradePaddle({{ PADDLE_YEARLY_PRODUCT_ID }})">
Upgrade to Premium
</button>
</div>
</div>
<ul class="list-unstyled">
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
Unlimited aliases
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
Unlimited mailboxes
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
Custom domains: bring your own domain to create aliases like contact@your-domain.com
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
Catch-all (or wildcard) domain
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
Initiate a new email from your alias
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
5 subdomains
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
50 directories
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
PGP Encryption
</li>
</ul>
</div>
</div>
</div>
<!-- END yearly premium plan -->
<!-- yearly Proton plan -->
{% if proton_upgrade %}
<div class="col-md-6 col-lg-4">
<div class="card card-md flex-grow-1">
<div class="card-body">
<div class="text-center">
<div class="h3">Proton plan</div>
<div class="h3 my-3">Starts at $119.88 / year</div>
<div class="text-center mt-4 mb-6">
<a class="btn btn-lg btn-outline-primary w-100"
role="button"
href="https://account.proton.me/u/0/mail/upgrade"
target="_blank">Upgrade your Proton account</a>
</div>
</div>
<p>Proton Unlimited / Business plans include:</p>
<ul class="list-unstyled">
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
SimpleLogin Premium
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
500 GB storage
</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">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
Unlimited folders, labels, and filters
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
Unlimited messages per day
</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">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
20 Calendars
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
10 high-speed VPN connections
</li>
<li class="d-flex">
<i class="fe fe-check text-success mr-2 mt-1" aria-hidden="true"></i>
3 custom email domains
</li>
</ul>
</div>
</div>
</div>
{% endif %}
<!-- END yearly Proton plan -->
</div>
</div>
<!-- END yearly tab content -->
</div>
<hr /> <hr />
<!-- FAQ section --> <i class="fa fa-bitcoin"></i>
<div> Payment via
<h3 class="text-center mb-5 mt-7">Frequently asked questions</h3> <a href="https://commerce.coinbase.com/?lang=en" target="_blank">
<div id="pricing-faq">
<div class="card mb-3">
<div class="card-header card-collapse p-0"
id="pricing-faq-question-payment-methods">
<h5 class="mb-0 w-100">
<button class="btn btn-link btn-block d-flex justify-content-between card-btn p-4 collapsed text-decoration-none"
data-toggle="collapse"
data-target="#pricing-faq-answer-payment-methods"
aria-controls="pricing-faq-answer-payment-methods"
aria-expanded="false">
<span class="text-start">Which payment methods (credit cards, PayPal, cryptocurrencies...) do you support?</span>
<span class="if-collapsed">
<i class="fe fe-chevron-down"></i>
</span>
<span class="if-not-collapsed">
<i class="fe fe-chevron-up"></i>
</span>
</button>
</h5>
</div>
<div id="pricing-faq-answer-payment-methods"
class="collapse"
aria-labelledby="pricing-faq-question-payment-methods"
data-parent="#pricing-faq">
<div class="card-body">
<p>
We use <a href="https://paddle.com" target="_blank" rel="noopener noreferrer">Paddle <i class="fe fe-external-link"></i></a> by default for handling payments via credit cards and PayPal. Paddle currently supports the following payment methods:
</p>
<ul>
<li>
Cards (including Mastercard, Visa, Maestro, American Express, Discover, Diners Club, JCB, UnionPay, and Mada)
</li>
<li>
PayPal
</li>
<li>
Apple Pay
</li>
<li>
Wire Transfers (ACH/SEPA/BACS)
</li>
</ul>
<p>
More information can be found on
<a href="https://paddle.com/support/which-payment-methods-do-you-support/"
target="_blank"
rel="noopener noreferrer">
Paddle supported payment methods <i class="fe fe-external-link"></i>
</a>.
</p>
<hr />
<p>
Furthermore we also support cryptocurrencies for the yearly plan via
<a href="https://commerce.coinbase.com"
target="_blank"
rel="noopener noreferrer">
Coinbase Commerce<i class="fe fe-external-link"></i> Coinbase Commerce<i class="fe fe-external-link"></i>
</a>, which currently supports Bitcoin, Bitcoin Cash, DAI, ApeCoin, Dogecoin, Ethereum, Litecoin, SHIBA INU, Tether and USD Coin. </a>
</p> <br />
<p> Currently Bitcoin, Bitcoin Cash, Dai, Ethereum, Litecoin and USD Coin are supported.
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. <br />
</p> <a class="btn btn-outline-primary" href="{{ url_for('dashboard.coinbase_checkout_route') }}" target="_blank">
<div class="d-flex justify-content-center"> Yearly billing - Crypto
<a class="btn btn-outline-primary text-center"
href="{{ url_for('dashboard.coinbase_checkout_route') }}"
target="_blank"
rel="noopener noreferrer">
Upgrade to Premium - cryptocurrency
<br /> <br />
$30/year $30/year
<i class="fe fe-external-link"></i> <i class="fe fe-external-link"></i>
</a> </a>
<hr />
If you have bought a coupon, please go to the
<a href="{{ url_for('dashboard.coupon_route') }}">coupon page</a>
to apply the coupon code.
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="card mb-3">
<div class="card-header card-collapse p-0"
id="pricing-faq-question-coupon">
<h5 class="mb-0 w-100">
<button class="btn btn-link btn-block d-flex justify-content-between card-btn p-4 collapsed text-decoration-none"
data-toggle="collapse"
data-target="#pricing-faq-answer-coupon"
aria-controls="pricing-faq-answer-coupon"
aria-expanded="false">
<span class="text-start">Where can I redeem / buy a coupon?</span>
<span class="if-collapsed">
<i class="fe fe-chevron-down"></i>
</span>
<span class="if-not-collapsed">
<i class="fe fe-chevron-up"></i>
</span>
</button>
</h5>
</div>
<div id="pricing-faq-answer-coupon"
class="collapse"
aria-labelledby="pricing-faq-question-coupon"
data-parent="#pricing-faq">
<div class="card-body">
<p>
To redeem or buy a coupon, please go to the
<a href="{{ url_for('dashboard.coupon_route') }}">coupon page</a>. The coupon code can be used by you or given to someone as a gift.
</p>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header card-collapse p-0"
id="pricing-faq-question-aliases-sub-stopped">
<h5 class="mb-0 w-100">
<button class="btn btn-link btn-block d-flex justify-content-between card-btn p-4 collapsed text-decoration-none"
data-toggle="collapse"
data-target="#pricing-faq-answer-aliases-sub-stopped"
aria-controls="pricing-faq-answer-aliases-sub-stopped"
aria-expanded="false">
<span class="text-start">What happens to my aliases when I stop the subscription?</span>
<span class="if-collapsed">
<i class="fe fe-chevron-down"></i>
</span>
<span class="if-not-collapsed">
<i class="fe fe-chevron-up"></i>
</span>
</button>
</h5>
</div>
<div id="pricing-faq-answer-aliases-sub-stopped"
class="collapse"
aria-labelledby="pricing-faq-question-aliases-sub-stopped"
data-parent="#pricing-faq">
<div class="card-body">
<p>
When your subscription ends, all aliases you created continue working normally, both on receiving and
sending emails. Concretely:
</p>
<ul>
<li>
All aliases/domains/directories/mailboxes you have created are kept and continue working normally.
</li>
<li>
You cannot create new aliases if you exceed the free plan limit, i.e. have more than 10 aliases.
</li>
<li>
As features like catch-all or directory allow you to create aliases on-the-fly, those aliases cannot be automatically created if you have more than 10 aliases.
</li>
<li>
You cannot add new domain, directory or mailbox.
</li>
</ul>
<p>
For example, if you have 100 aliases by the time your subscription ends, these 100 aliases will continue receiving and sending emails normally. You cannot however create new aliases.
</p>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header card-collapse p-0"
id="pricing-faq-question-aliases-max">
<h5 class="mb-0 w-100">
<button class="btn btn-link btn-block d-flex justify-content-between card-btn p-4 collapsed text-decoration-none"
data-toggle="collapse"
data-target="#pricing-faq-answer-aliases-max"
aria-controls="pricing-faq-answer-aliases-max"
aria-expanded="false">
<span class="text-start">What happens when I reach the maximum number of alias in free plan?</span>
<span class="if-collapsed">
<i class="fe fe-chevron-down"></i>
</span>
<span class="if-not-collapsed">
<i class="fe fe-chevron-up"></i>
</span>
</button>
</h5>
</div>
<div id="pricing-faq-answer-aliases-max"
class="collapse"
aria-labelledby="pricing-faq-question-aliases-max"
data-parent="#pricing-faq">
<div class="card-body">
<p>
If you are in the free plan, you cannot create new aliases when you reach the maximum number of aliases
(i.e. 10 aliases).
<br>
Aliases that would otherwise be created automatically via the catch-all domain or directory feature also cannot be created.
</p>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header card-collapse p-0"
id="pricing-faq-question-discounts">
<h5 class="mb-0 w-100">
<button class="btn btn-link btn-block d-flex justify-content-between card-btn p-4 collapsed text-decoration-none"
data-toggle="collapse"
data-target="#pricing-faq-answer-discounts"
aria-controls="pricing-faq-answer-discounts"
aria-expanded="false">
<span class="text-start">Do you offer discounts?</span>
<span class="if-collapsed">
<i class="fe fe-chevron-down"></i>
</span>
<span class="if-not-collapsed">
<i class="fe fe-chevron-up"></i>
</span>
</button>
</h5>
</div>
<div id="pricing-faq-answer-discounts"
class="collapse"
aria-labelledby="pricing-faq-question-discounts"
data-parent="#pricing-faq">
<div class="card-body">
<p>
We offer important discounts or free premium for:
</p>
<ul>
<li>
students, professors or technical staffs working at an educational institute
</li>
<li>
activists, dissidents or journalists
</li>
<li>
charity organizations
</li>
</ul>
<p>
Please send us an email at <a href="mailto:support@simplelogin.zendesk.com">support@simplelogin.zendesk.com</a> for more info.
</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.
</p>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header card-collapse p-0"
id="pricing-faq-question-refund">
<h5 class="mb-0 w-100">
<button class="btn btn-link btn-block d-flex justify-content-between card-btn p-4 collapsed text-decoration-none"
data-toggle="collapse"
data-target="#pricing-faq-answer-refund"
aria-controls="pricing-faq-answer-refund"
aria-expanded="false">
<span class="text-start">Do you have a refund policy?</span>
<span class="if-collapsed">
<i class="fe fe-chevron-down"></i>
</span>
<span class="if-not-collapsed">
<i class="fe fe-chevron-up"></i>
</span>
</button>
</h5>
</div>
<div id="pricing-faq-answer-refund"
class="collapse"
aria-labelledby="pricing-faq-question-refund"
data-parent="#pricing-faq">
<div class="card-body">
<p>
No we don't have a refund policy because SimpleLogin has a trial period where you can try all premium features.
</p>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header card-collapse p-0"
id="pricing-faq-question-family">
<h5 class="mb-0 w-100">
<button class="btn btn-link btn-block d-flex justify-content-between card-btn p-4 collapsed text-decoration-none"
data-toggle="collapse"
data-target="#pricing-faq-answer-family"
aria-controls="pricing-faq-answer-family"
aria-expanded="false">
<span class="text-start">Do you have a family plan?</span>
<span class="if-collapsed">
<i class="fe fe-chevron-down"></i>
</span>
<span class="if-not-collapsed">
<i class="fe fe-chevron-up"></i>
</span>
</button>
</h5>
</div>
<div id="pricing-faq-answer-family"
class="collapse"
aria-labelledby="pricing-faq-question-family"
data-parent="#pricing-faq">
<div class="card-body">
<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.
</p>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header card-collapse p-0"
id="pricing-faq-question-other-ways">
<h5 class="mb-0 w-100">
<button class="btn btn-link btn-block d-flex justify-content-between card-btn p-4 collapsed text-decoration-none"
data-toggle="collapse"
data-target="#pricing-faq-answer-other-ways"
aria-controls="pricing-faq-answer-other-ways"
aria-expanded="false">
<span class="text-start">Are there other ways to buy SimpleLogin subscriptions?</span>
<span class="if-collapsed">
<i class="fe fe-chevron-down"></i>
</span>
<span class="if-not-collapsed">
<i class="fe fe-chevron-up"></i>
</span>
</button>
</h5>
</div>
<div id="pricing-faq-answer-other-ways"
class="collapse"
aria-labelledby="pricing-faq-question-other-ways"
data-parent="#pricing-faq">
<div class="card-body">
<p>
Yes you can also buy SimpleLogin subscription coupon via <a href="https://proxysto.re/en/index.html" target="_blank">ProxyStore <i class="fe fe-external-link"></i></a>, our official reseller.
</p>
</div>
</div>
</div>
</div>
</div>
<!-- END FAQ section -->
</div>
<script type="text/javascript"> <script type="text/javascript">
Paddle.Setup({vendor: {{ PADDLE_VENDOR_ID }}}); Paddle.Setup({vendor: {{ PADDLE_VENDOR_ID }}});
function upgradePaddle(productId) { function upgrade(productId) {
bootbox.dialog({
title: `Payment with credit card or PayPal via Paddle`,
message: `Paddle will ask for an email address for sending out the invoices, please feel free to use an alias. <br />
You don't have to use your SimpleLogin account email address`,
size: 'large',
onEscape: true,
backdrop: true,
buttons: {
got_it: {
label: 'Got it!',
className: 'btn-outline-primary',
callback: function () {
Paddle.Checkout.open({ Paddle.Checkout.open({
product: productId, product: productId,
success: "{{ success_url }}", success: "{{ success_url }}",
passthrough: "{\"user_id\": {{current_user.id}} }" passthrough: "{\"user_id\": {{current_user.id}} }"
}); });
} }
},
}
});
}
</script> </script>
{% endblock %} {% endblock %}

View File

@ -17,8 +17,7 @@
<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" target="_blank">anti-phishing program ↗</a>
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,
@ -27,8 +26,7 @@
<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" target="_blank">Setting up filter for SimpleLogin emails ↗</a>
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,8 +73,7 @@
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 noreferrer"> rel="noopener">
Upgrade<i class="fe fe-external-link"></i> Upgrade<i class="fe fe-external-link"></i>
</a> </a>
</div> </div>

View File

@ -1,18 +0,0 @@
{% 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>
{% endblock %}

View File

@ -31,9 +31,8 @@
<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://simplelogin.io/docs/siwsl/app/" <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,9 +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://simplelogin.io/docs/siwsl/app/" <a href="https://docs.simplelogin.io" target="_blank" rel="noopener">
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

@ -47,9 +47,8 @@
<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://simplelogin.io/docs/siwsl/app/" <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,9 +13,7 @@
<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 }}" <a href="{{ client.home_url }}" target="_blank" rel="noopener">
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 noreferrer" rel="noopener"
style="color: #FFF; style="color: #FFF;
border-color: #3869d4; border-color: #3869d4;
border-style: solid; border-style: solid;

View File

@ -1,17 +0,0 @@
{% 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 %}

View File

@ -1,9 +0,0 @@
{% 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 %}

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 noreferrer" rel="noopener"
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 noreferrer" rel="noopener"
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 noreferrer" rel="noopener"
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 noreferrer" rel="noopener"
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 noreferrer" rel="noopener"
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 noreferrer" rel="noopener"
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 noreferrer" rel="noopener"
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,17 +75,14 @@
<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/" <a href="https://simplelogin.io/docs/" target="_blank">
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>
@ -97,9 +94,7 @@
</div> </div>
{% else %} {% else %}
<div class="nav-item"> <div class="nav-item">
<a href="https://simplelogin.io/docs/" <a href="https://simplelogin.io/docs/" target="_blank">
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,17 +98,14 @@
<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/" <a href="https://simplelogin.io/docs/" target="_blank">
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

@ -1,7 +1,13 @@
from app import config from app import config
from app.db import Session from app.db import Session
from app.models import User, Job from app.models import User, Job
from tests.utils import random_email from tests.utils import create_new_user, random_email
def test_available_sl_domains(flask_client):
user = create_new_user()
assert set(user.available_sl_domains()) == {"d1.test", "d2.test", "sl.local"}
def test_create_from_partner(flask_client): def test_create_from_partner(flask_client):

View File

@ -1,130 +0,0 @@
from app.db import Session
from app.models import SLDomain, PartnerUser, AliasOptions
from app.proton.utils import get_proton_partner
from init_app import add_sl_domains
from tests.utils import create_new_user, random_token
def setup_module():
Session.query(SLDomain).delete()
SLDomain.create(
domain="hidden", premium_only=False, flush=True, order=5, hidden=True
)
SLDomain.create(domain="free_non_partner", premium_only=False, flush=True, order=4)
SLDomain.create(
domain="premium_non_partner", premium_only=True, flush=True, order=3
)
SLDomain.create(
domain="free_partner",
premium_only=False,
flush=True,
partner_id=get_proton_partner().id,
order=2,
)
SLDomain.create(
domain="premium_partner",
premium_only=True,
flush=True,
partner_id=get_proton_partner().id,
order=1,
)
Session.commit()
def teardown_module():
Session.query(SLDomain).delete()
add_sl_domains()
def test_get_non_partner_domains():
user = create_new_user()
domains = user.get_sl_domains()
# Premium
assert len(domains) == 2
assert domains[0].domain == "premium_non_partner"
assert domains[1].domain == "free_non_partner"
assert [d.domain for d in domains] == user.available_sl_domains()
# Free
user.trial_end = None
Session.flush()
domains = user.get_sl_domains()
assert len(domains) == 1
assert domains[0].domain == "free_non_partner"
assert [d.domain for d in domains] == user.available_sl_domains()
def test_get_free_with_partner_domains():
user = create_new_user()
user.trial_end = None
PartnerUser.create(
partner_id=get_proton_partner().id,
user_id=user.id,
external_user_id=random_token(10),
flush=True,
)
domains = user.get_sl_domains()
# Default
assert len(domains) == 1
assert domains[0].domain == "free_non_partner"
assert [d.domain for d in domains] == user.available_sl_domains()
# Show partner domains
options = AliasOptions(
show_sl_domains=True, show_partner_domains=get_proton_partner()
)
domains = user.get_sl_domains(alias_options=options)
assert len(domains) == 2
assert domains[0].domain == "free_partner"
assert domains[1].domain == "free_non_partner"
assert [d.domain for d in domains] == user.available_sl_domains(
alias_options=options
)
# Only partner domains
options = AliasOptions(
show_sl_domains=False, show_partner_domains=get_proton_partner()
)
domains = user.get_sl_domains(alias_options=options)
assert len(domains) == 1
assert domains[0].domain == "free_partner"
assert [d.domain for d in domains] == user.available_sl_domains(
alias_options=options
)
def test_get_premium_with_partner_domains():
user = create_new_user()
PartnerUser.create(
partner_id=get_proton_partner().id,
user_id=user.id,
external_user_id=random_token(10),
flush=True,
)
domains = user.get_sl_domains()
# Default
assert len(domains) == 2
assert domains[0].domain == "premium_non_partner"
assert domains[1].domain == "free_non_partner"
assert [d.domain for d in domains] == user.available_sl_domains()
# Show partner domains
options = AliasOptions(
show_sl_domains=True, show_partner_domains=get_proton_partner()
)
domains = user.get_sl_domains(alias_options=options)
assert len(domains) == 4
assert domains[0].domain == "premium_partner"
assert domains[1].domain == "free_partner"
assert domains[2].domain == "premium_non_partner"
assert domains[3].domain == "free_non_partner"
assert [d.domain for d in domains] == user.available_sl_domains(
alias_options=options
)
# Only partner domains
options = AliasOptions(
show_sl_domains=False, show_partner_domains=get_proton_partner()
)
domains = user.get_sl_domains(alias_options=options)
assert len(domains) == 2
assert domains[0].domain == "premium_partner"
assert domains[1].domain == "free_partner"
assert [d.domain for d in domains] == user.available_sl_domains(
alias_options=options
)

View File

@ -368,19 +368,3 @@ 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,8 +2,7 @@ import email
from app.email_utils import ( from app.email_utils import (
copy, copy,
) )
from app.message_utils import message_to_bytes, message_format_base64_parts from app.message_utils import message_to_bytes
from tests.utils import load_eml_file
def test_copy(): def test_copy():
@ -34,13 +33,3 @@ 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

@ -9,12 +9,7 @@ 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 s.find("_") > 0 assert len(s) > 0
assert s.count("_") == 1
assert len(s) > 3
s = random_words(2, 3)
assert s.count("_") == 1
assert s[-1] in (str(i) for i in range(10))
def test_random_string(): def test_random_string():
@ -71,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 ("a.b+c@other.com", "a.b+c@other.com") yield (f"a.b+c@other.com", f"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 = "Test User" name = f"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,