Compare commits
13 commits
remove-bad
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f81f8ca032 | ||
![]() |
31896ff262 | ||
![]() |
45575261dc | ||
![]() |
627ad302d2 | ||
![]() |
08862a35c3 | ||
![]() |
75dd3cf925 | ||
![]() |
a097e33abe | ||
![]() |
e5cc8b9628 | ||
![]() |
d149686296 | ||
![]() |
babf4b058a | ||
![]() |
eb8f8caeb8 | ||
![]() |
70fc9c383a | ||
![]() |
b68f074783 |
61 changed files with 398 additions and 170 deletions
|
@ -7,18 +7,19 @@ repos:
|
|||
hooks:
|
||||
- id: check-yaml
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 3.9.2
|
||||
hooks:
|
||||
- id: flake8
|
||||
- repo: https://github.com/Riverside-Healthcare/djLint
|
||||
rev: v1.3.0
|
||||
hooks:
|
||||
- id: djlint-jinja
|
||||
files: '.*\.html'
|
||||
entry: djlint --reformat
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.1.5
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
args: [ --fix ]
|
||||
# Run the formatter.
|
||||
- id: ruff-format
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ COPY poetry.lock pyproject.toml ./
|
|||
# Install and setup poetry
|
||||
RUN pip install -U pip \
|
||||
&& apt-get update \
|
||||
&& apt install -y curl netcat-traditional gcc python3-dev gnupg git libre2-dev \
|
||||
&& apt install -y curl netcat-traditional gcc python3-dev gnupg git libre2-dev cmake ninja-build\
|
||||
&& curl -sSL https://install.python-poetry.org | python3 - \
|
||||
# Remove curl and netcat from the image
|
||||
&& apt-get purge -y curl netcat-traditional \
|
||||
|
@ -31,7 +31,7 @@ RUN pip install -U pip \
|
|||
&& poetry config virtualenvs.create false \
|
||||
&& poetry install --no-interaction --no-ansi --no-root \
|
||||
# Clear apt cache \
|
||||
&& apt-get purge -y libre2-dev \
|
||||
&& apt-get purge -y libre2-dev cmake ninja-build\
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
|
|
@ -5,10 +5,11 @@ from typing import Optional
|
|||
|
||||
from arrow import Arrow
|
||||
from newrelic import agent
|
||||
from sqlalchemy import or_
|
||||
|
||||
from app.db import Session
|
||||
from app.email_utils import send_welcome_email
|
||||
from app.utils import sanitize_email
|
||||
from app.utils import sanitize_email, canonicalize_email
|
||||
from app.errors import (
|
||||
AccountAlreadyLinkedToAnotherPartnerException,
|
||||
AccountIsUsingAliasAsEmail,
|
||||
|
@ -131,8 +132,9 @@ class ClientMergeStrategy(ABC):
|
|||
class NewUserStrategy(ClientMergeStrategy):
|
||||
def process(self) -> LinkResult:
|
||||
# Will create a new SL User with a random password
|
||||
canonical_email = canonicalize_email(self.link_request.email)
|
||||
new_user = User.create(
|
||||
email=self.link_request.email,
|
||||
email=canonical_email,
|
||||
name=self.link_request.name,
|
||||
password=random_string(20),
|
||||
activated=True,
|
||||
|
@ -166,7 +168,6 @@ class NewUserStrategy(ClientMergeStrategy):
|
|||
|
||||
class ExistingUnlinkedUserStrategy(ClientMergeStrategy):
|
||||
def process(self) -> LinkResult:
|
||||
|
||||
partner_user = ensure_partner_user_exists_for_user(
|
||||
self.link_request, self.user, self.partner
|
||||
)
|
||||
|
@ -213,11 +214,21 @@ def process_login_case(
|
|||
partner_id=partner.id, external_user_id=link_request.external_user_id
|
||||
)
|
||||
if partner_user is None:
|
||||
canonical_email = canonicalize_email(link_request.email)
|
||||
# We didn't find any SimpleLogin user registered with that partner user id
|
||||
# Make sure they aren't using an alias as their link email
|
||||
check_alias(link_request.email)
|
||||
check_alias(canonical_email)
|
||||
# Try to find it using the partner's e-mail address
|
||||
user = User.get_by(email=link_request.email)
|
||||
users = User.filter(
|
||||
or_(User.email == link_request.email, User.email == canonical_email)
|
||||
).all()
|
||||
if len(users) > 1:
|
||||
user = [user for user in users if user.email == canonical_email][0]
|
||||
elif len(users) == 1:
|
||||
user = users[0]
|
||||
else:
|
||||
user = None
|
||||
return get_login_strategy(link_request, user, partner).process()
|
||||
else:
|
||||
# We found the SL user registered with that partner user id
|
||||
|
|
|
@ -611,6 +611,26 @@ class NewsletterAdmin(SLModelView):
|
|||
else:
|
||||
flash(error_msg, "error")
|
||||
|
||||
@action(
|
||||
"clone_newsletter",
|
||||
"Clone this newsletter",
|
||||
)
|
||||
def clone_newsletter(self, newsletter_ids):
|
||||
if len(newsletter_ids) != 1:
|
||||
flash("you can only select 1 newsletter", "error")
|
||||
return
|
||||
|
||||
newsletter_id = newsletter_ids[0]
|
||||
newsletter: Newsletter = Newsletter.get(newsletter_id)
|
||||
new_newsletter = Newsletter.create(
|
||||
subject=newsletter.subject,
|
||||
html=newsletter.html,
|
||||
plain_text=newsletter.plain_text,
|
||||
commit=True,
|
||||
)
|
||||
|
||||
flash(f"Newsletter {new_newsletter.subject} has been cloned", "success")
|
||||
|
||||
|
||||
class NewsletterUserAdmin(SLModelView):
|
||||
column_searchable_list = ["id"]
|
||||
|
|
|
@ -70,7 +70,6 @@ def verify_prefix_suffix(
|
|||
# when DISABLE_ALIAS_SUFFIX is true, alias_domain_prefix is empty
|
||||
and not config.DISABLE_ALIAS_SUFFIX
|
||||
):
|
||||
|
||||
if not alias_domain_prefix.startswith("."):
|
||||
LOG.e("User %s submits a wrong alias suffix %s", user, alias_suffix)
|
||||
return False
|
||||
|
|
|
@ -16,3 +16,22 @@ from .views import (
|
|||
sudo,
|
||||
user,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"alias_options",
|
||||
"new_custom_alias",
|
||||
"custom_domain",
|
||||
"new_random_alias",
|
||||
"user_info",
|
||||
"auth",
|
||||
"auth_mfa",
|
||||
"alias",
|
||||
"apple",
|
||||
"mailbox",
|
||||
"notification",
|
||||
"setting",
|
||||
"export",
|
||||
"phone",
|
||||
"sudo",
|
||||
"user",
|
||||
]
|
||||
|
|
|
@ -24,6 +24,7 @@ from app.errors import (
|
|||
ErrContactAlreadyExists,
|
||||
ErrAddressInvalid,
|
||||
)
|
||||
from app.extensions import limiter
|
||||
from app.models import Alias, Contact, Mailbox, AliasMailbox
|
||||
|
||||
|
||||
|
@ -71,6 +72,9 @@ def get_aliases():
|
|||
|
||||
|
||||
@api_bp.route("/v2/aliases", methods=["GET", "POST"])
|
||||
@limiter.limit(
|
||||
"5/minute",
|
||||
)
|
||||
@require_api_auth
|
||||
def get_aliases_v2():
|
||||
"""
|
||||
|
|
|
@ -45,7 +45,7 @@ def create_mailbox():
|
|||
mailbox_email = sanitize_email(request.get_json().get("email"))
|
||||
|
||||
if not user.is_premium():
|
||||
return jsonify(error=f"Only premium plan can add additional mailbox"), 400
|
||||
return jsonify(error="Only premium plan can add additional mailbox"), 400
|
||||
|
||||
if not is_valid_email(mailbox_email):
|
||||
return jsonify(error=f"{mailbox_email} invalid"), 400
|
||||
|
|
|
@ -150,7 +150,7 @@ def new_custom_alias_v3():
|
|||
if not data:
|
||||
return jsonify(error="request body cannot be empty"), 400
|
||||
|
||||
if type(data) is not dict:
|
||||
if not isinstance(data, dict):
|
||||
return jsonify(error="request body does not follow the required format"), 400
|
||||
|
||||
alias_prefix = data.get("alias_prefix", "").strip().lower().replace(" ", "")
|
||||
|
@ -168,7 +168,7 @@ def new_custom_alias_v3():
|
|||
return jsonify(error="alias prefix invalid format or too long"), 400
|
||||
|
||||
# check if mailbox is not tempered with
|
||||
if type(mailbox_ids) is not list:
|
||||
if not isinstance(mailbox_ids, list):
|
||||
return jsonify(error="mailbox_ids must be an array of id"), 400
|
||||
mailboxes = []
|
||||
for mailbox_id in mailbox_ids:
|
||||
|
|
|
@ -17,3 +17,23 @@ from .views import (
|
|||
recovery,
|
||||
api_to_cookie,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"login",
|
||||
"logout",
|
||||
"register",
|
||||
"activate",
|
||||
"resend_activation",
|
||||
"reset_password",
|
||||
"forgot_password",
|
||||
"github",
|
||||
"google",
|
||||
"facebook",
|
||||
"proton",
|
||||
"change_email",
|
||||
"mfa",
|
||||
"fido",
|
||||
"social",
|
||||
"recovery",
|
||||
"api_to_cookie",
|
||||
]
|
||||
|
|
|
@ -62,7 +62,7 @@ def fido():
|
|||
browser = MfaBrowser.get_by(token=request.cookies.get("mfa"))
|
||||
if browser and not browser.is_expired() and browser.user_id == user.id:
|
||||
login_user(user)
|
||||
flash(f"Welcome back!", "success")
|
||||
flash("Welcome back!", "success")
|
||||
# Redirect user to correct page
|
||||
return redirect(next_url or url_for("dashboard.index"))
|
||||
else:
|
||||
|
@ -110,7 +110,7 @@ def fido():
|
|||
|
||||
session["sudo_time"] = int(time())
|
||||
login_user(user)
|
||||
flash(f"Welcome back!", "success")
|
||||
flash("Welcome back!", "success")
|
||||
|
||||
# Redirect user to correct page
|
||||
response = make_response(redirect(next_url or url_for("dashboard.index")))
|
||||
|
|
|
@ -55,7 +55,7 @@ def mfa():
|
|||
browser = MfaBrowser.get_by(token=request.cookies.get("mfa"))
|
||||
if browser and not browser.is_expired() and browser.user_id == user.id:
|
||||
login_user(user)
|
||||
flash(f"Welcome back!", "success")
|
||||
flash("Welcome back!", "success")
|
||||
# Redirect user to correct page
|
||||
return redirect(next_url or url_for("dashboard.index"))
|
||||
else:
|
||||
|
@ -73,7 +73,7 @@ def mfa():
|
|||
Session.commit()
|
||||
|
||||
login_user(user)
|
||||
flash(f"Welcome back!", "success")
|
||||
flash("Welcome back!", "success")
|
||||
|
||||
# Redirect user to correct page
|
||||
response = make_response(redirect(next_url or url_for("dashboard.index")))
|
||||
|
|
|
@ -53,7 +53,7 @@ def recovery_route():
|
|||
del session[MFA_USER_ID]
|
||||
|
||||
login_user(user)
|
||||
flash(f"Welcome back!", "success")
|
||||
flash("Welcome back!", "success")
|
||||
|
||||
recovery_code.used = True
|
||||
recovery_code.used_at = arrow.now()
|
||||
|
|
|
@ -94,9 +94,7 @@ def register():
|
|||
try:
|
||||
send_activation_email(user, next_url)
|
||||
RegisterEvent(RegisterEvent.ActionType.success).send()
|
||||
DailyMetric.get_or_create_today_metric().nb_new_web_non_proton_user += (
|
||||
1
|
||||
)
|
||||
DailyMetric.get_or_create_today_metric().nb_new_web_non_proton_user += 1
|
||||
Session.commit()
|
||||
except Exception:
|
||||
flash("Invalid email, are you sure the email is correct?", "error")
|
||||
|
|
|
@ -33,3 +33,39 @@ from .views import (
|
|||
notification,
|
||||
support,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"index",
|
||||
"pricing",
|
||||
"setting",
|
||||
"custom_alias",
|
||||
"subdomain",
|
||||
"billing",
|
||||
"alias_log",
|
||||
"alias_export",
|
||||
"unsubscribe",
|
||||
"api_key",
|
||||
"custom_domain",
|
||||
"alias_contact_manager",
|
||||
"enter_sudo",
|
||||
"mfa_setup",
|
||||
"mfa_cancel",
|
||||
"fido_setup",
|
||||
"coupon",
|
||||
"fido_manage",
|
||||
"domain_detail",
|
||||
"lifetime_licence",
|
||||
"directory",
|
||||
"mailbox",
|
||||
"mailbox_detail",
|
||||
"refused_email",
|
||||
"referral",
|
||||
"contact_detail",
|
||||
"setup_done",
|
||||
"batch_import",
|
||||
"alias_transfer",
|
||||
"app",
|
||||
"delete_account",
|
||||
"notification",
|
||||
"support",
|
||||
]
|
||||
|
|
|
@ -87,6 +87,6 @@ def get_alias_log(alias: Alias, page_id=0) -> [AliasLog]:
|
|||
contact=contact,
|
||||
)
|
||||
logs.append(al)
|
||||
logs = sorted(logs, key=lambda l: l.when, reverse=True)
|
||||
logs = sorted(logs, key=lambda log: log.when, reverse=True)
|
||||
|
||||
return logs
|
||||
|
|
|
@ -1,14 +1,9 @@
|
|||
from app.db import Session
|
||||
|
||||
"""
|
||||
List of apps that user has used via the "Sign in with SimpleLogin"
|
||||
"""
|
||||
|
||||
from flask import render_template, request, flash, redirect
|
||||
from flask_login import login_required, current_user
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from app.dashboard.base import dashboard_bp
|
||||
from app.db import Session
|
||||
from app.models import (
|
||||
ClientUser,
|
||||
)
|
||||
|
@ -17,6 +12,10 @@ from app.models import (
|
|||
@dashboard_bp.route("/app", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def app_route():
|
||||
"""
|
||||
List of apps that user has used via the "Sign in with SimpleLogin"
|
||||
"""
|
||||
|
||||
client_users = (
|
||||
ClientUser.filter_by(user_id=current_user.id)
|
||||
.options(joinedload(ClientUser.client))
|
||||
|
|
|
@ -100,7 +100,7 @@ def coupon_route():
|
|||
commit=True,
|
||||
)
|
||||
flash(
|
||||
f"Your account has been upgraded to Premium, thanks for your support!",
|
||||
"Your account has been upgraded to Premium, thanks for your support!",
|
||||
"success",
|
||||
)
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@ def directory():
|
|||
if request.method == "POST":
|
||||
if request.form.get("form-name") == "delete":
|
||||
if not delete_dir_form.validate():
|
||||
flash(f"Invalid request", "warning")
|
||||
flash("Invalid request", "warning")
|
||||
return redirect(url_for("dashboard.directory"))
|
||||
dir_obj = Directory.get(delete_dir_form.directory_id.data)
|
||||
|
||||
|
@ -87,7 +87,7 @@ def directory():
|
|||
|
||||
if request.form.get("form-name") == "toggle-directory":
|
||||
if not toggle_dir_form.validate():
|
||||
flash(f"Invalid request", "warning")
|
||||
flash("Invalid request", "warning")
|
||||
return redirect(url_for("dashboard.directory"))
|
||||
dir_id = toggle_dir_form.directory_id.data
|
||||
dir_obj = Directory.get(dir_id)
|
||||
|
@ -109,7 +109,7 @@ def directory():
|
|||
|
||||
elif request.form.get("form-name") == "update":
|
||||
if not update_dir_form.validate():
|
||||
flash(f"Invalid request", "warning")
|
||||
flash("Invalid request", "warning")
|
||||
return redirect(url_for("dashboard.directory"))
|
||||
dir_id = update_dir_form.directory_id.data
|
||||
dir_obj = Directory.get(dir_id)
|
||||
|
|
|
@ -57,6 +57,10 @@ def get_stats(user: User) -> Stats:
|
|||
methods=["POST"],
|
||||
exempt_when=lambda: request.form.get("form-name") != "create-random-email",
|
||||
)
|
||||
@limiter.limit(
|
||||
"5/minute",
|
||||
methods=["GET"],
|
||||
)
|
||||
@login_required
|
||||
@parallel_limiter.lock(
|
||||
name="alias_creation",
|
||||
|
|
|
@ -128,7 +128,6 @@ def setting():
|
|||
new_email_valid = True
|
||||
new_email = canonicalize_email(change_email_form.email.data)
|
||||
if new_email != current_user.email and not pending_email:
|
||||
|
||||
# check if this email is not already used
|
||||
if personal_email_already_used(new_email) or Alias.get_by(
|
||||
email=new_email
|
||||
|
|
|
@ -75,12 +75,11 @@ def block_contact(contact_id):
|
|||
@dashboard_bp.route("/unsubscribe/encoded/<encoded_request>", methods=["GET"])
|
||||
@login_required
|
||||
def encoded_unsubscribe(encoded_request: str):
|
||||
|
||||
unsub_data = UnsubscribeHandler().handle_unsubscribe_from_request(
|
||||
current_user, encoded_request
|
||||
)
|
||||
if not unsub_data:
|
||||
flash(f"Invalid unsubscribe request", "error")
|
||||
flash("Invalid unsubscribe request", "error")
|
||||
return redirect(url_for("dashboard.index"))
|
||||
if unsub_data.action == UnsubscribeAction.DisableAlias:
|
||||
alias = Alias.get(unsub_data.data)
|
||||
|
@ -97,14 +96,14 @@ def encoded_unsubscribe(encoded_request: str):
|
|||
)
|
||||
)
|
||||
if unsub_data.action == UnsubscribeAction.UnsubscribeNewsletter:
|
||||
flash(f"You've unsubscribed from the newsletter", "success")
|
||||
flash("You've unsubscribed from the newsletter", "success")
|
||||
return redirect(
|
||||
url_for(
|
||||
"dashboard.index",
|
||||
)
|
||||
)
|
||||
if unsub_data.action == UnsubscribeAction.OriginalUnsubscribeMailto:
|
||||
flash(f"The original unsubscribe request has been forwarded", "success")
|
||||
flash("The original unsubscribe request has been forwarded", "success")
|
||||
return redirect(
|
||||
url_for(
|
||||
"dashboard.index",
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
from .views import index, new_client, client_detail
|
||||
|
||||
__all__ = ["index", "new_client", "client_detail"]
|
||||
|
|
|
@ -87,7 +87,7 @@ def client_detail(client_id):
|
|||
)
|
||||
|
||||
flash(
|
||||
f"Thanks for submitting, we are informed and will come back to you asap!",
|
||||
"Thanks for submitting, we are informed and will come back to you asap!",
|
||||
"success",
|
||||
)
|
||||
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
from .views import index
|
||||
|
||||
__all__ = ["index"]
|
||||
|
|
|
@ -93,7 +93,7 @@ def send_welcome_email(user):
|
|||
|
||||
send_email(
|
||||
comm_email,
|
||||
f"Welcome to SimpleLogin",
|
||||
"Welcome to SimpleLogin",
|
||||
render("com/welcome.txt", user=user, alias=alias),
|
||||
render("com/welcome.html", user=user, alias=alias),
|
||||
unsubscribe_link,
|
||||
|
@ -104,7 +104,7 @@ def send_welcome_email(user):
|
|||
def send_trial_end_soon_email(user):
|
||||
send_email(
|
||||
user.email,
|
||||
f"Your trial will end soon",
|
||||
"Your trial will end soon",
|
||||
render("transactional/trial-end.txt.jinja2", user=user),
|
||||
render("transactional/trial-end.html", user=user),
|
||||
ignore_smtp_error=True,
|
||||
|
@ -114,7 +114,7 @@ def send_trial_end_soon_email(user):
|
|||
def send_activation_email(email, activation_link):
|
||||
send_email(
|
||||
email,
|
||||
f"Just one more step to join SimpleLogin",
|
||||
"Just one more step to join SimpleLogin",
|
||||
render(
|
||||
"transactional/activation.txt",
|
||||
activation_link=activation_link,
|
||||
|
@ -768,7 +768,7 @@ def get_header_unicode(header: Union[str, Header]) -> str:
|
|||
ret = ""
|
||||
for to_decoded_str, charset in decode_header(header):
|
||||
if charset is None:
|
||||
if type(to_decoded_str) is bytes:
|
||||
if isinstance(to_decoded_str, bytes):
|
||||
decoded_str = to_decoded_str.decode()
|
||||
else:
|
||||
decoded_str = to_decoded_str
|
||||
|
@ -805,13 +805,13 @@ def to_bytes(msg: Message):
|
|||
for generator_policy in [None, policy.SMTP, policy.SMTPUTF8]:
|
||||
try:
|
||||
return msg.as_bytes(policy=generator_policy)
|
||||
except:
|
||||
except Exception:
|
||||
LOG.w("as_bytes() fails with %s policy", policy, exc_info=True)
|
||||
|
||||
msg_string = msg.as_string()
|
||||
try:
|
||||
return msg_string.encode()
|
||||
except:
|
||||
except Exception:
|
||||
LOG.w("as_string().encode() fails", exc_info=True)
|
||||
|
||||
return msg_string.encode(errors="replace")
|
||||
|
@ -906,7 +906,7 @@ def add_header(msg: Message, text_header, html_header=None) -> Message:
|
|||
if content_type == "text/plain":
|
||||
encoding = get_encoding(msg)
|
||||
payload = msg.get_payload()
|
||||
if type(payload) is str:
|
||||
if isinstance(payload, str):
|
||||
clone_msg = copy(msg)
|
||||
new_payload = f"""{text_header}
|
||||
------------------------------
|
||||
|
@ -916,7 +916,7 @@ def add_header(msg: Message, text_header, html_header=None) -> Message:
|
|||
elif content_type == "text/html":
|
||||
encoding = get_encoding(msg)
|
||||
payload = msg.get_payload()
|
||||
if type(payload) is str:
|
||||
if isinstance(payload, str):
|
||||
new_payload = f"""<table width="100%" style="width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0; margin: 0; padding: 0;">
|
||||
<tr>
|
||||
|
@ -972,7 +972,7 @@ def add_header(msg: Message, text_header, html_header=None) -> Message:
|
|||
|
||||
|
||||
def replace(msg: Union[Message, str], old, new) -> Union[Message, str]:
|
||||
if type(msg) is str:
|
||||
if isinstance(msg, str):
|
||||
msg = msg.replace(old, new)
|
||||
return msg
|
||||
|
||||
|
@ -995,7 +995,7 @@ def replace(msg: Union[Message, str], old, new) -> Union[Message, str]:
|
|||
if content_type in ("text/plain", "text/html"):
|
||||
encoding = get_encoding(msg)
|
||||
payload = msg.get_payload()
|
||||
if type(payload) is str:
|
||||
if isinstance(payload, str):
|
||||
if encoding == EmailEncoding.QUOTED:
|
||||
LOG.d("handle quoted-printable replace %s -> %s", old, new)
|
||||
# first decode the payload
|
||||
|
|
|
@ -34,10 +34,10 @@ def apply_dmarc_policy_for_forward_phase(
|
|||
|
||||
from_header = get_header_unicode(msg[headers.FROM])
|
||||
|
||||
warning_plain_text = f"""This email failed anti-phishing checks when it was received by SimpleLogin, be careful with its content.
|
||||
warning_plain_text = """This email failed anti-phishing checks when it was received by SimpleLogin, be careful with its content.
|
||||
More info on https://simplelogin.io/docs/getting-started/anti-phishing/
|
||||
"""
|
||||
warning_html = f"""
|
||||
warning_html = """
|
||||
<p style="color:red">
|
||||
This email failed anti-phishing checks when it was received by SimpleLogin, be careful with its content.
|
||||
More info on <a href="https://simplelogin.io/docs/getting-started/anti-phishing/">anti-phishing measure</a>
|
||||
|
|
|
@ -221,7 +221,7 @@ def handle_complaint(message: Message, origin: ProviderComplaintOrigin) -> bool:
|
|||
return True
|
||||
|
||||
if is_deleted_alias(msg_info.sender_address):
|
||||
LOG.i(f"Complaint is for deleted alias. Do nothing")
|
||||
LOG.i("Complaint is for deleted alias. Do nothing")
|
||||
return True
|
||||
|
||||
contact = Contact.get_by(reply_email=msg_info.sender_address)
|
||||
|
@ -231,7 +231,7 @@ def handle_complaint(message: Message, origin: ProviderComplaintOrigin) -> bool:
|
|||
alias = find_alias_with_address(msg_info.rcpt_address)
|
||||
|
||||
if is_deleted_alias(msg_info.rcpt_address):
|
||||
LOG.i(f"Complaint is for deleted alias. Do nothing")
|
||||
LOG.i("Complaint is for deleted alias. Do nothing")
|
||||
return True
|
||||
|
||||
if not alias:
|
||||
|
|
|
@ -54,9 +54,8 @@ class UnsubscribeEncoder:
|
|||
def encode_subject(
|
||||
cls, action: UnsubscribeAction, data: Union[int, UnsubscribeOriginalData]
|
||||
) -> str:
|
||||
if (
|
||||
action != UnsubscribeAction.OriginalUnsubscribeMailto
|
||||
and type(data) is not int
|
||||
if action != UnsubscribeAction.OriginalUnsubscribeMailto and not isinstance(
|
||||
data, int
|
||||
):
|
||||
raise ValueError(f"Data has to be an int for an action of type {action}")
|
||||
if action == UnsubscribeAction.OriginalUnsubscribeMailto:
|
||||
|
|
|
@ -30,7 +30,7 @@ def handle_batch_import(batch_import: BatchImport):
|
|||
|
||||
LOG.d("Download file %s from %s", batch_import.file, file_url)
|
||||
r = requests.get(file_url)
|
||||
lines = [line.decode() for line in r.iter_lines()]
|
||||
lines = [line.decode("utf-8") for line in r.iter_lines()]
|
||||
|
||||
import_from_csv(batch_import, user, lines)
|
||||
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
from .integrations import set_enable_proton_cookie
|
||||
from .exit_sudo import exit_sudo_mode
|
||||
|
||||
__all__ = ["set_enable_proton_cookie", "exit_sudo_mode"]
|
||||
|
|
|
@ -39,7 +39,6 @@ from app.models import (
|
|||
|
||||
|
||||
class ExportUserDataJob:
|
||||
|
||||
REMOVE_FIELDS = {
|
||||
"User": ("otp_secret", "password"),
|
||||
"Alias": ("ts_vector", "transfer_token", "hibp_last_check"),
|
||||
|
|
|
@ -22,7 +22,6 @@ from app.message_utils import message_to_bytes, message_format_base64_parts
|
|||
|
||||
@dataclass
|
||||
class SendRequest:
|
||||
|
||||
SAVE_EXTENSION = "sendrequest"
|
||||
|
||||
envelope_from: str
|
||||
|
|
|
@ -3184,7 +3184,7 @@ class MessageIDMatching(Base, ModelMixin):
|
|||
|
||||
# to track what email_log that has created this matching
|
||||
email_log_id = sa.Column(
|
||||
sa.ForeignKey("email_log.id", ondelete="cascade"), nullable=True
|
||||
sa.ForeignKey("email_log.id", ondelete="cascade"), nullable=True, index=True
|
||||
)
|
||||
|
||||
email_log = orm.relationship("EmailLog")
|
||||
|
@ -3517,7 +3517,7 @@ class PartnerSubscription(Base, ModelMixin):
|
|||
|
||||
class Newsletter(Base, ModelMixin):
|
||||
__tablename__ = "newsletter"
|
||||
subject = sa.Column(sa.String(), nullable=False, unique=True, index=True)
|
||||
subject = sa.Column(sa.String(), nullable=False, index=True)
|
||||
|
||||
html = sa.Column(sa.Text)
|
||||
plain_text = sa.Column(sa.Text)
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
from . import views
|
||||
|
||||
__all__ = ["views"]
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
from .views import authorize, token, user_info
|
||||
|
||||
__all__ = ["authorize", "token", "user_info"]
|
||||
|
|
|
@ -64,7 +64,7 @@ def _split_arg(arg_input: Union[str, list]) -> Set[str]:
|
|||
- the response_type/scope passed as a list ?scope=scope_1&scope=scope_2
|
||||
"""
|
||||
res = set()
|
||||
if type(arg_input) is str:
|
||||
if isinstance(arg_input, str):
|
||||
if " " in arg_input:
|
||||
for x in arg_input.split(" "):
|
||||
if x:
|
||||
|
|
|
@ -5,3 +5,11 @@ from .views import (
|
|||
account_activated,
|
||||
extension_redirect,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"index",
|
||||
"final",
|
||||
"setup_done",
|
||||
"account_activated",
|
||||
"extension_redirect",
|
||||
]
|
||||
|
|
|
@ -39,7 +39,6 @@ class _InnerLock:
|
|||
lock_redis.storage.delete(lock_name)
|
||||
|
||||
def __call__(self, f: Callable[..., Any]):
|
||||
|
||||
if self.lock_suffix is None:
|
||||
lock_suffix = f.__name__
|
||||
else:
|
||||
|
|
|
@ -5,3 +5,11 @@ from .views import (
|
|||
provider1_callback,
|
||||
provider2_callback,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"index",
|
||||
"phone_reservation",
|
||||
"twilio_callback",
|
||||
"provider1_callback",
|
||||
"provider2_callback",
|
||||
]
|
||||
|
|
|
@ -6,7 +6,6 @@ from app.session import RedisSessionStore
|
|||
|
||||
|
||||
def initialize_redis_services(app: flask.Flask, redis_url: str):
|
||||
|
||||
if redis_url.startswith("redis://") or redis_url.startswith("rediss://"):
|
||||
storage = limits.storage.RedisStorage(redis_url)
|
||||
app.session_interface = RedisSessionStore(storage.storage, storage.storage, app)
|
||||
|
|
|
@ -75,7 +75,7 @@ class RedisSessionStore(SessionInterface):
|
|||
try:
|
||||
data = pickle.loads(val)
|
||||
return ServerSession(data, session_id=session_id)
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
return ServerSession(session_id=str(uuid.uuid4()))
|
||||
|
||||
|
|
16
cron.py
16
cron.py
|
@ -105,7 +105,7 @@ def delete_logs():
|
|||
rows_to_delete = EmailLog.filter(EmailLog.created_at < cutoff_time).count()
|
||||
expected_queries = int(rows_to_delete / batch_size)
|
||||
sql = text(
|
||||
f"DELETE FROM email_log WHERE id IN (SELECT id FROM email_log WHERE created_at < :cutoff_time order by created_at limit :batch_size)"
|
||||
"DELETE FROM email_log WHERE id IN (SELECT id FROM email_log WHERE created_at < :cutoff_time order by created_at limit :batch_size)"
|
||||
)
|
||||
str_cutoff_time = cutoff_time.isoformat()
|
||||
while total_deleted < rows_to_delete:
|
||||
|
@ -161,7 +161,7 @@ def notify_premium_end():
|
|||
|
||||
send_email(
|
||||
user.email,
|
||||
f"Your subscription will end soon",
|
||||
"Your subscription will end soon",
|
||||
render(
|
||||
"transactional/subscription-end.txt",
|
||||
user=user,
|
||||
|
@ -218,7 +218,7 @@ def notify_manual_sub_end():
|
|||
LOG.d("Remind user %s that their manual sub is ending soon", user)
|
||||
send_email(
|
||||
user.email,
|
||||
f"Your subscription will end soon",
|
||||
"Your subscription will end soon",
|
||||
render(
|
||||
"transactional/manual-subscription-end.txt",
|
||||
user=user,
|
||||
|
@ -590,21 +590,21 @@ nb_total_bounced_last_24h: {stats_today.nb_total_bounced_last_24h} - {increase_p
|
|||
"""
|
||||
|
||||
monitoring_report += "\n====================================\n"
|
||||
monitoring_report += f"""
|
||||
monitoring_report += """
|
||||
# Account bounce report:
|
||||
"""
|
||||
|
||||
for email, bounces in bounce_report():
|
||||
monitoring_report += f"{email}: {bounces}\n"
|
||||
|
||||
monitoring_report += f"""\n
|
||||
monitoring_report += """\n
|
||||
# Alias creation report:
|
||||
"""
|
||||
|
||||
for email, nb_alias, date in alias_creation_report():
|
||||
monitoring_report += f"{email}, {date}: {nb_alias}\n"
|
||||
|
||||
monitoring_report += f"""\n
|
||||
monitoring_report += """\n
|
||||
# Full bounce detail report:
|
||||
"""
|
||||
monitoring_report += all_bounce_report()
|
||||
|
@ -1099,14 +1099,14 @@ def notify_hibp():
|
|||
)
|
||||
|
||||
LOG.d(
|
||||
f"Send new breaches found email to %s for %s breaches aliases",
|
||||
"Send new breaches found email to %s for %s breaches aliases",
|
||||
user,
|
||||
len(breached_aliases),
|
||||
)
|
||||
|
||||
send_email(
|
||||
user.email,
|
||||
f"You were in a data breach",
|
||||
"You were in a data breach",
|
||||
render(
|
||||
"transactional/hibp-new-breaches.txt.jinja2",
|
||||
user=user,
|
||||
|
|
|
@ -235,7 +235,6 @@ def get_or_create_contact(from_header: str, mail_from: str, alias: Alias) -> Con
|
|||
contact.mail_from = mail_from
|
||||
Session.commit()
|
||||
else:
|
||||
|
||||
try:
|
||||
contact = Contact.create(
|
||||
user_id=alias.user_id,
|
||||
|
@ -1197,7 +1196,7 @@ def handle_reply(envelope, msg: Message, rcpt_to: str) -> (bool, str):
|
|||
)
|
||||
|
||||
# replace reverse alias by real address for all contacts
|
||||
for (reply_email, website_email) in contact_query.values(
|
||||
for reply_email, website_email in contact_query.values(
|
||||
Contact.reply_email, Contact.website_email
|
||||
):
|
||||
msg = replace(msg, reply_email, website_email)
|
||||
|
@ -1952,7 +1951,7 @@ def handle_bounce(envelope, email_log: EmailLog, msg: Message) -> str:
|
|||
for is_delivered, smtp_status in handle_forward(envelope, msg, alias.email):
|
||||
res.append((is_delivered, smtp_status))
|
||||
|
||||
for (is_success, smtp_status) in res:
|
||||
for is_success, smtp_status in res:
|
||||
# Consider all deliveries successful if 1 delivery is successful
|
||||
if is_success:
|
||||
return smtp_status
|
||||
|
@ -2272,7 +2271,7 @@ def handle(envelope: Envelope, msg: Message) -> str:
|
|||
if nb_success > 0 and nb_non_success > 0:
|
||||
LOG.e(f"some deliveries fail and some success, {mail_from}, {rcpt_tos}, {res}")
|
||||
|
||||
for (is_success, smtp_status) in res:
|
||||
for is_success, smtp_status in res:
|
||||
# Consider all deliveries successful if 1 delivery is successful
|
||||
if is_success:
|
||||
return smtp_status
|
||||
|
|
|
@ -89,7 +89,6 @@ aghast
|
|||
agile
|
||||
agility
|
||||
aging
|
||||
agnostic
|
||||
agonize
|
||||
agonizing
|
||||
agony
|
||||
|
@ -375,8 +374,6 @@ augmented
|
|||
august
|
||||
authentic
|
||||
author
|
||||
autism
|
||||
autistic
|
||||
autograph
|
||||
automaker
|
||||
automated
|
||||
|
@ -446,7 +443,6 @@ backyard
|
|||
bacon
|
||||
bacteria
|
||||
bacterium
|
||||
badass
|
||||
badge
|
||||
badland
|
||||
badly
|
||||
|
@ -1106,7 +1102,6 @@ clinic
|
|||
clinking
|
||||
clip
|
||||
clique
|
||||
cloak
|
||||
clobber
|
||||
clock
|
||||
clone
|
||||
|
@ -1776,7 +1771,6 @@ diagnosis
|
|||
diagram
|
||||
dial
|
||||
diameter
|
||||
diaper
|
||||
diaphragm
|
||||
diary
|
||||
dice
|
||||
|
@ -2032,9 +2026,6 @@ duffel
|
|||
dugout
|
||||
duh
|
||||
duke
|
||||
duller
|
||||
dullness
|
||||
duly
|
||||
dumping
|
||||
dumpling
|
||||
dumpster
|
||||
|
@ -2527,8 +2518,6 @@ feisty
|
|||
feline
|
||||
felt-tip
|
||||
feminine
|
||||
feminism
|
||||
feminist
|
||||
feminize
|
||||
femur
|
||||
fence
|
||||
|
@ -2667,7 +2656,6 @@ fondness
|
|||
fondue
|
||||
font
|
||||
food
|
||||
fool
|
||||
footage
|
||||
football
|
||||
footbath
|
||||
|
@ -2777,7 +2765,6 @@ gag
|
|||
gainfully
|
||||
gaining
|
||||
gains
|
||||
gala
|
||||
gallantly
|
||||
galleria
|
||||
gallery
|
||||
|
@ -3164,8 +3151,6 @@ hardware
|
|||
hardwired
|
||||
hardwood
|
||||
hardy
|
||||
harmful
|
||||
harmless
|
||||
harmonica
|
||||
harmonics
|
||||
harmonize
|
||||
|
@ -3340,7 +3325,6 @@ identical
|
|||
identify
|
||||
identity
|
||||
ideology
|
||||
idiocy
|
||||
idiom
|
||||
idly
|
||||
igloo
|
||||
|
@ -3357,7 +3341,6 @@ imaging
|
|||
imbecile
|
||||
imitate
|
||||
imitation
|
||||
immature
|
||||
immerse
|
||||
immersion
|
||||
imminent
|
||||
|
@ -3387,14 +3370,10 @@ implode
|
|||
implosion
|
||||
implosive
|
||||
imply
|
||||
impolite
|
||||
important
|
||||
importer
|
||||
impose
|
||||
imposing
|
||||
impotence
|
||||
impotency
|
||||
impotent
|
||||
impound
|
||||
imprecise
|
||||
imprint
|
||||
|
@ -3424,8 +3403,6 @@ irritable
|
|||
irritably
|
||||
irritant
|
||||
irritate
|
||||
islamic
|
||||
islamist
|
||||
isolated
|
||||
isolating
|
||||
isolation
|
||||
|
@ -3524,7 +3501,6 @@ june
|
|||
junior
|
||||
juniper
|
||||
junkie
|
||||
junkman
|
||||
junkyard
|
||||
jurist
|
||||
juror
|
||||
|
@ -3570,9 +3546,6 @@ king
|
|||
kinship
|
||||
kinsman
|
||||
kinswoman
|
||||
kissable
|
||||
kisser
|
||||
kissing
|
||||
kitchen
|
||||
kite
|
||||
kitten
|
||||
|
@ -3649,7 +3622,6 @@ laundry
|
|||
laurel
|
||||
lavender
|
||||
lavish
|
||||
laxative
|
||||
lazily
|
||||
laziness
|
||||
lazy
|
||||
|
@ -3690,7 +3662,6 @@ liable
|
|||
liberty
|
||||
librarian
|
||||
library
|
||||
licking
|
||||
licorice
|
||||
lid
|
||||
life
|
||||
|
@ -3741,8 +3712,6 @@ livestock
|
|||
lividly
|
||||
living
|
||||
lizard
|
||||
lubricant
|
||||
lubricate
|
||||
lucid
|
||||
luckily
|
||||
luckiness
|
||||
|
@ -3878,7 +3847,6 @@ marshland
|
|||
marshy
|
||||
marsupial
|
||||
marvelous
|
||||
marxism
|
||||
mascot
|
||||
masculine
|
||||
mashed
|
||||
|
@ -3914,8 +3882,6 @@ maximum
|
|||
maybe
|
||||
mayday
|
||||
mayflower
|
||||
moaner
|
||||
moaning
|
||||
mobile
|
||||
mobility
|
||||
mobilize
|
||||
|
@ -4124,7 +4090,6 @@ nemeses
|
|||
nemesis
|
||||
neon
|
||||
nephew
|
||||
nerd
|
||||
nervous
|
||||
nervy
|
||||
nest
|
||||
|
@ -4139,7 +4104,6 @@ never
|
|||
next
|
||||
nibble
|
||||
nickname
|
||||
nicotine
|
||||
niece
|
||||
nifty
|
||||
nimble
|
||||
|
@ -4167,14 +4131,10 @@ nuptials
|
|||
nursery
|
||||
nursing
|
||||
nurture
|
||||
nutcase
|
||||
nutlike
|
||||
nutmeg
|
||||
nutrient
|
||||
nutshell
|
||||
nuttiness
|
||||
nutty
|
||||
nuzzle
|
||||
nylon
|
||||
oaf
|
||||
oak
|
||||
|
@ -4205,7 +4165,6 @@ obstinate
|
|||
obstruct
|
||||
obtain
|
||||
obtrusive
|
||||
obtuse
|
||||
obvious
|
||||
occultist
|
||||
occupancy
|
||||
|
@ -4446,7 +4405,6 @@ palpitate
|
|||
paltry
|
||||
pampered
|
||||
pamperer
|
||||
pampers
|
||||
pamphlet
|
||||
panama
|
||||
pancake
|
||||
|
@ -4651,7 +4609,6 @@ plated
|
|||
platform
|
||||
plating
|
||||
platinum
|
||||
platonic
|
||||
platter
|
||||
platypus
|
||||
plausible
|
||||
|
@ -4777,8 +4734,6 @@ prancing
|
|||
pranker
|
||||
prankish
|
||||
prankster
|
||||
prayer
|
||||
praying
|
||||
preacher
|
||||
preaching
|
||||
preachy
|
||||
|
@ -4796,8 +4751,6 @@ prefix
|
|||
preflight
|
||||
preformed
|
||||
pregame
|
||||
pregnancy
|
||||
pregnant
|
||||
preheated
|
||||
prelaunch
|
||||
prelaw
|
||||
|
@ -4937,7 +4890,6 @@ prudishly
|
|||
prune
|
||||
pruning
|
||||
pry
|
||||
psychic
|
||||
public
|
||||
publisher
|
||||
pucker
|
||||
|
@ -4957,8 +4909,7 @@ punctual
|
|||
punctuate
|
||||
punctured
|
||||
pungent
|
||||
punisher
|
||||
punk
|
||||
punishe
|
||||
pupil
|
||||
puppet
|
||||
puppy
|
||||
|
@ -5040,7 +4991,6 @@ quote
|
|||
rabid
|
||||
race
|
||||
racing
|
||||
racism
|
||||
rack
|
||||
racoon
|
||||
radar
|
||||
|
@ -5155,7 +5105,6 @@ recount
|
|||
recoup
|
||||
recovery
|
||||
recreate
|
||||
rectal
|
||||
rectangle
|
||||
rectified
|
||||
rectify
|
||||
|
@ -5622,7 +5571,6 @@ sarcastic
|
|||
sardine
|
||||
sash
|
||||
sasquatch
|
||||
sassy
|
||||
satchel
|
||||
satiable
|
||||
satin
|
||||
|
@ -5651,7 +5599,6 @@ scaling
|
|||
scallion
|
||||
scallop
|
||||
scalping
|
||||
scam
|
||||
scandal
|
||||
scanner
|
||||
scanning
|
||||
|
@ -5928,8 +5875,6 @@ silent
|
|||
silica
|
||||
silicon
|
||||
silk
|
||||
silliness
|
||||
silly
|
||||
silo
|
||||
silt
|
||||
silver
|
||||
|
@ -6196,7 +6141,6 @@ splinter
|
|||
splotchy
|
||||
splurge
|
||||
spoilage
|
||||
spoiled
|
||||
spoiler
|
||||
spoiling
|
||||
spoils
|
||||
|
@ -7078,7 +7022,6 @@ undocked
|
|||
undoing
|
||||
undone
|
||||
undrafted
|
||||
undress
|
||||
undrilled
|
||||
undusted
|
||||
undying
|
||||
|
|
|
@ -158677,16 +158677,6 @@ isis
|
|||
isize
|
||||
isl
|
||||
islay
|
||||
islam
|
||||
islamic
|
||||
islamism
|
||||
islamist
|
||||
islamistic
|
||||
islamite
|
||||
islamitic
|
||||
islamitish
|
||||
islamization
|
||||
islamize
|
||||
island
|
||||
islanded
|
||||
islander
|
||||
|
|
39
migrations/versions/2023_100510_46ecb648a47e_.py
Normal file
39
migrations/versions/2023_100510_46ecb648a47e_.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: 46ecb648a47e
|
||||
Revises: ec7fdde8da9f
|
||||
Create Date: 2023-10-05 10:43:35.668902
|
||||
|
||||
"""
|
||||
import sqlalchemy_utils
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "46ecb648a47e"
|
||||
down_revision = "ec7fdde8da9f"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.get_context().autocommit_block():
|
||||
op.create_index(
|
||||
op.f("ix_message_id_matching_email_log_id"),
|
||||
"message_id_matching",
|
||||
["email_log_id"],
|
||||
unique=False,
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.get_context().autocommit_block():
|
||||
op.drop_index(
|
||||
op.f("ix_message_id_matching_email_log_id"),
|
||||
table_name="message_id_matching",
|
||||
)
|
||||
# ### end Alembic commands ###
|
31
migrations/versions/2023_110714_4bc54632d9aa_.py
Normal file
31
migrations/versions/2023_110714_4bc54632d9aa_.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: 4bc54632d9aa
|
||||
Revises: 46ecb648a47e
|
||||
Create Date: 2023-11-07 14:02:17.610226
|
||||
|
||||
"""
|
||||
import sqlalchemy_utils
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4bc54632d9aa'
|
||||
down_revision = '46ecb648a47e'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index('ix_newsletter_subject', table_name='newsletter')
|
||||
op.create_index(op.f('ix_newsletter_subject'), 'newsletter', ['subject'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_newsletter_subject'), table_name='newsletter')
|
||||
op.create_index('ix_newsletter_subject', 'newsletter', ['subject'], unique=True)
|
||||
# ### end Alembic commands ###
|
30
poetry.lock
generated
30
poetry.lock
generated
|
@ -1,4 +1,4 @@
|
|||
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
|
@ -2831,6 +2831,32 @@ files = [
|
|||
docs = ["ryd"]
|
||||
jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.1.5"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.1.5-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:32d47fc69261c21a4c48916f16ca272bf2f273eb635d91c65d5cd548bf1f3d96"},
|
||||
{file = "ruff-0.1.5-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:171276c1df6c07fa0597fb946139ced1c2978f4f0b8254f201281729981f3c17"},
|
||||
{file = "ruff-0.1.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17ef33cd0bb7316ca65649fc748acc1406dfa4da96a3d0cde6d52f2e866c7b39"},
|
||||
{file = "ruff-0.1.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b2c205827b3f8c13b4a432e9585750b93fd907986fe1aec62b2a02cf4401eee6"},
|
||||
{file = "ruff-0.1.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb408e3a2ad8f6881d0f2e7ad70cddb3ed9f200eb3517a91a245bbe27101d379"},
|
||||
{file = "ruff-0.1.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f20dc5e5905ddb407060ca27267c7174f532375c08076d1a953cf7bb016f5a24"},
|
||||
{file = "ruff-0.1.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aafb9d2b671ed934998e881e2c0f5845a4295e84e719359c71c39a5363cccc91"},
|
||||
{file = "ruff-0.1.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a4894dddb476597a0ba4473d72a23151b8b3b0b5f958f2cf4d3f1c572cdb7af7"},
|
||||
{file = "ruff-0.1.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a00a7ec893f665ed60008c70fe9eeb58d210e6b4d83ec6654a9904871f982a2a"},
|
||||
{file = "ruff-0.1.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a8c11206b47f283cbda399a654fd0178d7a389e631f19f51da15cbe631480c5b"},
|
||||
{file = "ruff-0.1.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fa29e67b3284b9a79b1a85ee66e293a94ac6b7bb068b307a8a373c3d343aa8ec"},
|
||||
{file = "ruff-0.1.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9b97fd6da44d6cceb188147b68db69a5741fbc736465b5cea3928fdac0bc1aeb"},
|
||||
{file = "ruff-0.1.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:721f4b9d3b4161df8dc9f09aa8562e39d14e55a4dbaa451a8e55bdc9590e20f4"},
|
||||
{file = "ruff-0.1.5-py3-none-win32.whl", hash = "sha256:f80c73bba6bc69e4fdc73b3991db0b546ce641bdcd5b07210b8ad6f64c79f1ab"},
|
||||
{file = "ruff-0.1.5-py3-none-win_amd64.whl", hash = "sha256:c21fe20ee7d76206d290a76271c1af7a5096bc4c73ab9383ed2ad35f852a0087"},
|
||||
{file = "ruff-0.1.5-py3-none-win_arm64.whl", hash = "sha256:82bfcb9927e88c1ed50f49ac6c9728dab3ea451212693fe40d08d314663e412f"},
|
||||
{file = "ruff-0.1.5.tar.gz", hash = "sha256:5cbec0ef2ae1748fb194f420fb03fb2c25c3258c86129af7172ff8f198f125ab"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "s3transfer"
|
||||
version = "0.3.3"
|
||||
|
@ -3674,4 +3700,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "8bf71c74c8f4d1afe6b1ab0912702cdb47086474168bed8a9230c398abf349dd"
|
||||
content-hash = "01afc410d21eeac0a0ac7e8ef6eeb0a991cf4bc091c3351049263462e205ff63"
|
||||
|
|
|
@ -18,6 +18,9 @@ exclude = '''
|
|||
)
|
||||
'''
|
||||
|
||||
[tool.ruff]
|
||||
ignore-init-module-imports = true
|
||||
|
||||
[tool.djlint]
|
||||
indent = 2
|
||||
profile = "jinja"
|
||||
|
@ -121,6 +124,9 @@ black = "^22.1.0"
|
|||
djlint = "^1.3.0"
|
||||
pylint = "^2.14.4"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "^0.1.5"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry>=0.12"]
|
||||
build-backend = "poetry.masonry.api"
|
||||
|
|
|
@ -641,7 +641,7 @@ def setup_paddle_callback(app: Flask):
|
|||
|
||||
@app.route("/paddle_coupon", methods=["GET", "POST"])
|
||||
def paddle_coupon():
|
||||
LOG.d(f"paddle coupon callback %s", request.form)
|
||||
LOG.d("paddle coupon callback %s", request.form)
|
||||
|
||||
if not paddle_utils.verify_incoming_request(dict(request.form)):
|
||||
LOG.e("request not coming from paddle. Request data:%s", dict(request.form))
|
||||
|
|
7
shell.py
7
shell.py
|
@ -1,13 +1,12 @@
|
|||
from time import sleep
|
||||
|
||||
import flask_migrate
|
||||
from IPython import embed
|
||||
from sqlalchemy_utils import create_database, database_exists, drop_database
|
||||
|
||||
from app import models
|
||||
from app.config import DB_URI
|
||||
from app.models import *
|
||||
|
||||
from app.db import Session
|
||||
from app.log import LOG
|
||||
from app.models import User, RecoveryCode
|
||||
|
||||
if False:
|
||||
# noinspection PyUnreachableCode
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 38 KiB |
|
@ -86,6 +86,12 @@
|
|||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
{% if current_user.is_authenticated and current_user.should_show_upgrade_button() %}
|
||||
|
||||
<div class="alert alert-success text-center mb-0" role="alert">
|
||||
Black Friday: $20 for the first year instead of $30. Available until December 1st.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% block announcement %}{% endblock %}
|
||||
<div class="container">
|
||||
<!-- For flash messages -->
|
||||
|
|
|
@ -57,6 +57,19 @@
|
|||
{% endblock %}
|
||||
{% block default_content %}
|
||||
|
||||
<div class="alert alert-info">
|
||||
Black Friday Deal: 33% off on the yearly plan for the <b>first</b> year ($20 instead of $30).
|
||||
<br>
|
||||
Please use this coupon code
|
||||
<em data-toggle="tooltip"
|
||||
title="Click to copy"
|
||||
class="clipboard"
|
||||
data-clipboard-text="BF2023">BF2023</em> during the checkout.
|
||||
<br>
|
||||
<img src="/static/images/coupon.png" class="m-2" style="max-width: 300px">
|
||||
<br>
|
||||
Available until December 1, 2023.
|
||||
</div>
|
||||
<div class="pb-8">
|
||||
<div class="text-center mx-md-auto mb-8 mt-6">
|
||||
<h1>Upgrade to unlock premium features</h1>
|
||||
|
|
|
@ -58,7 +58,7 @@ def test_different_scenarios_v4_2(flask_client):
|
|||
assert r.json["suffixes"]
|
||||
assert r.json["prefix_suggestion"] == "" # no hostname => no suggestion
|
||||
|
||||
for (suffix, signed_suffix) in r.json["suffixes"]:
|
||||
for suffix, signed_suffix in r.json["suffixes"]:
|
||||
assert signed_suffix.startswith(suffix)
|
||||
|
||||
# <<< with hostname >>>
|
||||
|
|
|
@ -56,13 +56,15 @@ def test_get_jobs_to_run(flask_client):
|
|||
run_at=now.shift(hours=3),
|
||||
)
|
||||
# Job out of attempts
|
||||
Job.create(
|
||||
name="",
|
||||
payload="",
|
||||
state=JobState.taken.value,
|
||||
taken_at=now.shift(minutes=-(config.JOB_TAKEN_RETRY_WAIT_MINS + 10)),
|
||||
attempts=config.JOB_MAX_ATTEMPTS + 1,
|
||||
),
|
||||
(
|
||||
Job.create(
|
||||
name="",
|
||||
payload="",
|
||||
state=JobState.taken.value,
|
||||
taken_at=now.shift(minutes=-(config.JOB_TAKEN_RETRY_WAIT_MINS + 10)),
|
||||
attempts=config.JOB_MAX_ATTEMPTS + 1,
|
||||
),
|
||||
)
|
||||
Session.commit()
|
||||
jobs = get_jobs_to_run()
|
||||
assert len(jobs) == len(expected_jobs_to_run)
|
||||
|
|
|
@ -18,7 +18,7 @@ from app.db import Session
|
|||
from app.errors import AccountAlreadyLinkedToAnotherPartnerException
|
||||
from app.models import Partner, PartnerUser, User
|
||||
from app.proton.utils import get_proton_partner
|
||||
from app.utils import random_string
|
||||
from app.utils import random_string, canonicalize_email
|
||||
from tests.utils import random_email
|
||||
|
||||
|
||||
|
@ -377,3 +377,48 @@ def test_link_account_with_uppercase(flask_client):
|
|||
)
|
||||
assert partner_user.partner_id == get_proton_partner().id
|
||||
assert partner_user.external_user_id == partner_user_id
|
||||
|
||||
|
||||
def test_login_to_account_with_canonical_email(flask_client):
|
||||
email = "a.{rand}@gmail.com".format(rand=random_string(10))
|
||||
canonical_email = canonicalize_email(email)
|
||||
assert email != canonical_email
|
||||
partner_user_id = random_string()
|
||||
link_request = random_link_request(
|
||||
external_user_id=partner_user_id, email=email.upper()
|
||||
)
|
||||
user = create_user(canonical_email)
|
||||
assert user.email == canonical_email
|
||||
res = process_login_case(link_request, get_proton_partner())
|
||||
assert res.user.id == user.id
|
||||
|
||||
|
||||
def test_login_to_account_with_canonical_email_if_there_is_also_non_canonical(
|
||||
flask_client,
|
||||
):
|
||||
email = "a.{rand}@gmail.com".format(rand=random_string(10))
|
||||
canonical_email = canonicalize_email(email)
|
||||
assert email != canonical_email
|
||||
partner_user_id = random_string()
|
||||
link_request = random_link_request(
|
||||
external_user_id=partner_user_id, email=email.upper()
|
||||
)
|
||||
user = create_user(canonical_email)
|
||||
create_user(email)
|
||||
assert user.email == canonical_email
|
||||
res = process_login_case(link_request, get_proton_partner())
|
||||
assert res.user.id == user.id
|
||||
|
||||
|
||||
def test_login_creates_account_with_canonical_email(
|
||||
flask_client,
|
||||
):
|
||||
email = "a.{rand}@gmail.com".format(rand=random_string(10))
|
||||
canonical_email = canonicalize_email(email)
|
||||
assert email != canonical_email
|
||||
partner_user_id = random_string()
|
||||
link_request = random_link_request(
|
||||
external_user_id=partner_user_id, email=email.upper()
|
||||
)
|
||||
res = process_login_case(link_request, get_proton_partner())
|
||||
assert res.user.email == canonical_email
|
||||
|
|
|
@ -7,7 +7,7 @@ import arrow
|
|||
import pytest
|
||||
|
||||
from app import config
|
||||
from app.config import MAX_ALERT_24H, EMAIL_DOMAIN, ROOT_DIR
|
||||
from app.config import MAX_ALERT_24H, ROOT_DIR
|
||||
from app.db import Session
|
||||
from app.email_utils import (
|
||||
get_email_domain_part,
|
||||
|
@ -16,7 +16,6 @@ from app.email_utils import (
|
|||
delete_header,
|
||||
add_or_replace_header,
|
||||
send_email_with_rate_control,
|
||||
copy,
|
||||
get_spam_from_header,
|
||||
get_header_from_bounce,
|
||||
add_header,
|
||||
|
|
|
@ -17,7 +17,7 @@ def test_encode_decode(flask_client):
|
|||
|
||||
jwt_token = make_id_token(client_user)
|
||||
|
||||
assert type(jwt_token) is str
|
||||
assert isinstance(jwt_token, str)
|
||||
assert verify_id_token(jwt_token)
|
||||
|
||||
|
||||
|
|
|
@ -49,9 +49,9 @@ def encrypt_decrypt_text(text: str):
|
|||
priv = pgpy.PGPKey()
|
||||
priv.parse(private_key)
|
||||
decrypted = priv.decrypt(encrypted).message
|
||||
if type(decrypted) == str:
|
||||
if isinstance(decrypted, str):
|
||||
assert decrypted == text
|
||||
elif type(decrypted) == bytearray:
|
||||
elif isinstance(decrypted, bytearray):
|
||||
assert decrypted.decode() == text
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue