Compare commits

...

3 Commits

Author SHA1 Message Date
01dba12ed0 4.59.3
All checks were successful
Build-Release-Image / Build-Image (linux/arm64) (push) Successful in 3m43s
Build-Release-Image / Build-Image (linux/amd64) (push) Successful in 3m57s
Build-Release-Image / Merge-Images (push) Successful in 53s
Build-Release-Image / Create-Release (push) Successful in 8s
Build-Release-Image / Notify (push) Successful in 3s
2024-11-16 12:00:07 +00:00
c872d43c3d 4.59.2
All checks were successful
Build-Release-Image / Build-Image (linux/arm64) (push) Successful in 4m7s
Build-Release-Image / Build-Image (linux/amd64) (push) Successful in 4m46s
Build-Release-Image / Merge-Images (push) Successful in 14s
Build-Release-Image / Create-Release (push) Successful in 9s
Build-Release-Image / Notify (push) Successful in 5s
2024-11-14 12:00:07 +00:00
3e6867bc17 4.58
All checks were successful
Build-Release-Image / Build-Image (linux/amd64) (push) Successful in 3m7s
Build-Release-Image / Build-Image (linux/arm64) (push) Successful in 3m49s
Build-Release-Image / Merge-Images (push) Successful in 15s
Build-Release-Image / Create-Release (push) Successful in 8s
Build-Release-Image / Notify (push) Successful in 3s
2024-11-07 12:00:06 +00:00
21 changed files with 628 additions and 291 deletions

View File

@ -84,7 +84,7 @@ For email gurus, we have chosen 1024 key length instead of 2048 for DNS simplici
### DNS ### DNS
Please note that DNS changes could take up to 24 hours to propagate. In practice, it's a lot faster though (~1 minute or so in our test). In DNS setup, we usually use domain with a trailing dot (`.`) at the end to to force using absolute domain. Please note that DNS changes could take up to 24 hours to propagate. In practice, it's a lot faster though (~1 minute or so in our test). In DNS setup, we usually use domain with a trailing dot (`.`) at the end to force using absolute domain.
#### MX record #### MX record

View File

@ -6,6 +6,7 @@ from typing import Optional
import arrow import arrow
from arrow import Arrow from arrow import Arrow
from newrelic import agent from newrelic import agent
from psycopg2.errors import UniqueViolation
from sqlalchemy import or_ from sqlalchemy import or_
from app.db import Session from app.db import Session
@ -160,15 +161,55 @@ class ClientMergeStrategy(ABC):
class NewUserStrategy(ClientMergeStrategy): class NewUserStrategy(ClientMergeStrategy):
def process(self) -> LinkResult: def process(self) -> LinkResult:
# Will create a new SL User with a random password
canonical_email = canonicalize_email(self.link_request.email) canonical_email = canonicalize_email(self.link_request.email)
new_user = User.create( try:
email=canonical_email, # Will create a new SL User with a random password
name=self.link_request.name, new_user = User.create(
password=random_string(20), email=canonical_email,
activated=True, name=self.link_request.name,
from_partner=self.link_request.from_partner, password=random_string(20),
activated=True,
from_partner=self.link_request.from_partner,
)
self.create_partner_user(new_user)
Session.commit()
if not new_user.created_by_partner:
send_welcome_email(new_user)
agent.record_custom_event(
"PartnerUserCreation", {"partner": self.partner.name}
)
return LinkResult(
user=new_user,
strategy=self.__class__.__name__,
)
except UniqueViolation:
return self.create_missing_link(canonical_email)
def create_missing_link(self, canonical_email: str):
# If there's a unique key violation due to race conditions try to create only the partner if needed
partner_user = PartnerUser.get_by(
external_user_id=self.link_request.external_user_id,
partner_id=self.partner.id,
) )
if partner_user is None:
# Get the user by canonical email and if not by normal email
user = User.get_by(email=canonical_email) or User.get_by(
email=self.link_request.email
)
if not user:
raise RuntimeError(
"Tried to create only partner on UniqueViolation but cannot find the user"
)
partner_user = self.create_partner_user(user)
Session.commit()
return LinkResult(
user=partner_user.user, strategy=ExistingUnlinkedUserStrategy.__name__
)
def create_partner_user(self, new_user: User):
partner_user = create_partner_user( partner_user = create_partner_user(
user=new_user, user=new_user,
partner_id=self.partner.id, partner_id=self.partner.id,
@ -182,17 +223,7 @@ class NewUserStrategy(ClientMergeStrategy):
partner_user, partner_user,
self.link_request.plan, self.link_request.plan,
) )
Session.commit() return partner_user
if not new_user.created_by_partner:
send_welcome_email(new_user)
agent.record_custom_event("PartnerUserCreation", {"partner": self.partner.name})
return LinkResult(
user=new_user,
strategy=self.__class__.__name__,
)
class ExistingUnlinkedUserStrategy(ClientMergeStrategy): class ExistingUnlinkedUserStrategy(ClientMergeStrategy):

View File

@ -58,7 +58,7 @@ def verify_prefix_suffix(
# 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(alias_options=alias_options):
LOG.e("wrong alias suffix %s, user %s", alias_suffix, user) LOG.i("wrong alias suffix %s, user %s", alias_suffix, user)
return False return False
# SimpleLogin domain case: # SimpleLogin domain case:
@ -75,17 +75,17 @@ def verify_prefix_suffix(
and not config.DISABLE_ALIAS_SUFFIX and not config.DISABLE_ALIAS_SUFFIX
): ):
if not alias_domain_prefix.startswith("."): if not alias_domain_prefix.startswith("."):
LOG.e("User %s submits a wrong alias suffix %s", user, alias_suffix) LOG.i("User %s submits a wrong alias suffix %s", user, alias_suffix)
return False return False
else: else:
if alias_domain not in user_custom_domains: if alias_domain not in user_custom_domains:
if not config.DISABLE_ALIAS_SUFFIX: if not config.DISABLE_ALIAS_SUFFIX:
LOG.e("wrong alias suffix %s, user %s", alias_suffix, user) LOG.i("wrong alias suffix %s, user %s", alias_suffix, user)
return False return False
if alias_domain not in available_sl_domains: if alias_domain not in available_sl_domains:
LOG.e("wrong alias suffix %s, user %s", alias_suffix, user) LOG.i("wrong alias suffix %s, user %s", alias_suffix, user)
return False return False
return True return True

View File

@ -153,7 +153,8 @@ def new_custom_alias_v3():
if not isinstance(data, dict): if not isinstance(data, dict):
return jsonify(error="request body does not follow the required format"), 400 return jsonify(error="request body does not follow the required format"), 400
alias_prefix = data.get("alias_prefix", "").strip().lower().replace(" ", "") alias_prefix_data = data.get("alias_prefix", "") or ""
alias_prefix = alias_prefix_data.strip().lower().replace(" ", "")
signed_suffix = data.get("signed_suffix", "") or "" signed_suffix = data.get("signed_suffix", "") or ""
signed_suffix = signed_suffix.strip() signed_suffix = signed_suffix.strip()

View File

@ -227,7 +227,10 @@ def alias_contact_manager(alias_id):
page = 0 page = 0
if request.args.get("page"): if request.args.get("page"):
page = int(request.args.get("page")) try:
page = int(request.args.get("page"))
except ValueError:
pass
query = request.args.get("query") or "" query = request.args.get("query") or ""

View File

@ -71,7 +71,10 @@ def index():
page = 0 page = 0
if request.args.get("page"): if request.args.get("page"):
page = int(request.args.get("page")) try:
page = int(request.args.get("page"))
except ValueError:
pass
highlight_alias_id = None highlight_alias_id = None
if request.args.get("highlight_alias_id"): if request.args.get("highlight_alias_id"):

View File

@ -43,7 +43,10 @@ def notification_route(notification_id):
def notifications_route(): def notifications_route():
page = 0 page = 0
if request.args.get("page"): if request.args.get("page"):
page = int(request.args.get("page")) try:
page = int(request.args.get("page"))
except ValueError:
pass
notifications = ( notifications = (
Notification.filter_by(user_id=current_user.id) Notification.filter_by(user_id=current_user.id)

View File

@ -158,6 +158,8 @@ class File(Base, ModelMixin):
path = sa.Column(sa.String(128), unique=True, nullable=False) path = sa.Column(sa.String(128), unique=True, nullable=False)
user_id = sa.Column(sa.ForeignKey("users.id", ondelete="cascade"), nullable=True) user_id = sa.Column(sa.ForeignKey("users.id", ondelete="cascade"), nullable=True)
__table_args__ = (sa.Index("ix_file_user_id", "user_id"),)
def get_url(self, expires_in=3600): def get_url(self, expires_in=3600):
return s3.get_url(self.path, expires_in) return s3.get_url(self.path, expires_in)
@ -319,6 +321,8 @@ class HibpNotifiedAlias(Base, ModelMixin):
notified_at = sa.Column(ArrowType, default=arrow.utcnow, nullable=False) notified_at = sa.Column(ArrowType, default=arrow.utcnow, nullable=False)
__table_args__ = (sa.Index("ix_hibp_notified_alias_user_id", "user_id"),)
class Fido(Base, ModelMixin): class Fido(Base, ModelMixin):
__tablename__ = "fido" __tablename__ = "fido"
@ -333,6 +337,8 @@ class Fido(Base, ModelMixin):
name = sa.Column(sa.String(128), nullable=False, unique=False) name = sa.Column(sa.String(128), nullable=False, unique=False)
user_id = sa.Column(sa.ForeignKey("users.id", ondelete="cascade"), nullable=True) user_id = sa.Column(sa.ForeignKey("users.id", ondelete="cascade"), nullable=True)
__table_args__ = (sa.Index("ix_fido_user_id", "user_id"),)
class User(Base, ModelMixin, UserMixin, PasswordOracle): class User(Base, ModelMixin, UserMixin, PasswordOracle):
__tablename__ = "users" __tablename__ = "users"
@ -565,6 +571,11 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
"ix_users_activated_trial_end_lifetime", activated, trial_end, lifetime "ix_users_activated_trial_end_lifetime", activated, trial_end, lifetime
), ),
sa.Index("ix_users_delete_on", delete_on), sa.Index("ix_users_delete_on", delete_on),
sa.Index("ix_users_default_mailbox_id", default_mailbox_id),
sa.Index(
"ix_users_default_alias_custom_domain_id", default_alias_custom_domain_id
),
sa.Index("ix_users_profile_picture_id", profile_picture_id),
) )
@property @property
@ -1221,6 +1232,8 @@ class ActivationCode(Base, ModelMixin):
expired = sa.Column(ArrowType, nullable=False, default=_expiration_1h) expired = sa.Column(ArrowType, nullable=False, default=_expiration_1h)
__table_args__ = (sa.Index("ix_activation_code_user_id", "user_id"),)
def is_expired(self): def is_expired(self):
return self.expired < arrow.now() return self.expired < arrow.now()
@ -1237,6 +1250,8 @@ class ResetPasswordCode(Base, ModelMixin):
expired = sa.Column(ArrowType, nullable=False, default=_expiration_1h) expired = sa.Column(ArrowType, nullable=False, default=_expiration_1h)
__table_args__ = (sa.Index("ix_reset_password_code_user_id", "user_id"),)
def is_expired(self): def is_expired(self):
return self.expired < arrow.now() return self.expired < arrow.now()
@ -1279,6 +1294,8 @@ class MfaBrowser(Base, ModelMixin):
user = orm.relationship(User) user = orm.relationship(User)
__table_args__ = (sa.Index("ix_mfa_browser_user_id", "user_id"),)
@classmethod @classmethod
def create_new(cls, user, token_length=64) -> "MfaBrowser": def create_new(cls, user, token_length=64) -> "MfaBrowser":
found = False found = False
@ -1337,6 +1354,12 @@ class Client(Base, ModelMixin):
user = orm.relationship(User) user = orm.relationship(User)
referral = orm.relationship("Referral") referral = orm.relationship("Referral")
__table_args__ = (
sa.Index("ix_client_user_id", "user_id"),
sa.Index("ix_client_icon_id", "icon_id"),
sa.Index("ix_client_referral_id", "referral_id"),
)
def nb_user(self): def nb_user(self):
return ClientUser.filter_by(client_id=self.id).count() return ClientUser.filter_by(client_id=self.id).count()
@ -1385,6 +1408,8 @@ class RedirectUri(Base, ModelMixin):
client = orm.relationship(Client, backref="redirect_uris") client = orm.relationship(Client, backref="redirect_uris")
__table_args__ = (sa.Index("ix_redirect_uri_client_id", "client_id"),)
class AuthorizationCode(Base, ModelMixin): class AuthorizationCode(Base, ModelMixin):
__tablename__ = "authorization_code" __tablename__ = "authorization_code"
@ -1406,6 +1431,11 @@ class AuthorizationCode(Base, ModelMixin):
expired = sa.Column(ArrowType, nullable=False, default=_expiration_5m) expired = sa.Column(ArrowType, nullable=False, default=_expiration_5m)
__table_args__ = (
sa.Index("ix_authorization_code_client_id", "client_id"),
sa.Index("ix_authorization_code_user_id", "user_id"),
)
def is_expired(self): def is_expired(self):
return self.expired < arrow.now() return self.expired < arrow.now()
@ -1428,6 +1458,11 @@ class OauthToken(Base, ModelMixin):
expired = sa.Column(ArrowType, nullable=False, default=_expiration_1h) expired = sa.Column(ArrowType, nullable=False, default=_expiration_1h)
__table_args__ = (
sa.Index("ix_oauth_token_user_id", "user_id"),
sa.Index("ix_oauth_token_client_id", "client_id"),
)
def is_expired(self): def is_expired(self):
return self.expired < arrow.now() return self.expired < arrow.now()
@ -1581,6 +1616,7 @@ class Alias(Base, ModelMixin):
postgresql_ops={"note": "gin_trgm_ops"}, postgresql_ops={"note": "gin_trgm_ops"},
postgresql_using="gin", postgresql_using="gin",
), ),
Index("ix_alias_original_owner_id", "original_owner_id"),
) )
user = orm.relationship(User, foreign_keys=[user_id]) user = orm.relationship(User, foreign_keys=[user_id])
@ -1666,6 +1702,11 @@ class Alias(Base, ModelMixin):
custom_domain = Alias.get_custom_domain(email) custom_domain = Alias.get_custom_domain(email)
if custom_domain: if custom_domain:
new_alias.custom_domain_id = custom_domain.id new_alias.custom_domain_id = custom_domain.id
else:
custom_domain = CustomDomain.get(kw["custom_domain_id"])
# If it comes from a custom domain created from partner. Mark it as created from partner
if custom_domain is not None and custom_domain.partner_id is not None:
new_alias.flags = (new_alias.flags or 0) | Alias.FLAG_PARTNER_CREATED
Session.add(new_alias) Session.add(new_alias)
DailyMetric.get_or_create_today_metric().nb_alias += 1 DailyMetric.get_or_create_today_metric().nb_alias += 1
@ -2069,7 +2110,12 @@ class Contact(Base, ModelMixin):
class EmailLog(Base, ModelMixin): class EmailLog(Base, ModelMixin):
__tablename__ = "email_log" __tablename__ = "email_log"
__table_args__ = (Index("ix_email_log_created_at", "created_at"),) __table_args__ = (
Index("ix_email_log_created_at", "created_at"),
Index("ix_email_log_mailbox_id", "mailbox_id"),
Index("ix_email_log_bounced_mailbox_id", "bounced_mailbox_id"),
Index("ix_email_log_refused_email_id", "refused_email_id"),
)
user_id = sa.Column( user_id = sa.Column(
sa.ForeignKey(User.id, ondelete="cascade"), nullable=False, index=True sa.ForeignKey(User.id, ondelete="cascade"), nullable=False, index=True
@ -2345,6 +2391,7 @@ class AliasUsedOn(Base, ModelMixin):
__table_args__ = ( __table_args__ = (
sa.UniqueConstraint("alias_id", "hostname", name="uq_alias_used"), sa.UniqueConstraint("alias_id", "hostname", name="uq_alias_used"),
sa.Index("ix_alias_used_on_user_id", "user_id"),
) )
alias_id = sa.Column( alias_id = sa.Column(
@ -2371,6 +2418,11 @@ class ApiKey(Base, ModelMixin):
user = orm.relationship(User) user = orm.relationship(User)
__table_args__ = (
sa.Index("ix_api_key_code", "code"),
sa.Index("ix_api_key_user_id", "user_id"),
)
@classmethod @classmethod
def create(cls, user_id, name=None, **kwargs): def create(cls, user_id, name=None, **kwargs):
code = random_string(60) code = random_string(60)
@ -2529,6 +2581,7 @@ class AutoCreateRule(Base, ModelMixin):
sa.UniqueConstraint( sa.UniqueConstraint(
"custom_domain_id", "order", name="uq_auto_create_rule_order" "custom_domain_id", "order", name="uq_auto_create_rule_order"
), ),
sa.Index("ix_auto_create_rule_custom_domain_id", "custom_domain_id"),
) )
custom_domain_id = sa.Column( custom_domain_id = sa.Column(
@ -2572,6 +2625,7 @@ class DomainDeletedAlias(Base, ModelMixin):
__table_args__ = ( __table_args__ = (
sa.UniqueConstraint("domain_id", "email", name="uq_domain_trash"), sa.UniqueConstraint("domain_id", "email", name="uq_domain_trash"),
sa.Index("ix_domain_deleted_alias_user_id", "user_id"),
) )
email = sa.Column(sa.String(256), nullable=False) email = sa.Column(sa.String(256), nullable=False)
@ -2632,6 +2686,8 @@ class Coupon(Base, ModelMixin):
# a coupon can have an expiration # a coupon can have an expiration
expires_date = sa.Column(ArrowType, nullable=True) expires_date = sa.Column(ArrowType, nullable=True)
__table_args__ = (sa.Index("ix_coupon_used_by_user_id", "used_by_user_id"),)
class Directory(Base, ModelMixin): class Directory(Base, ModelMixin):
__tablename__ = "directory" __tablename__ = "directory"
@ -2646,6 +2702,8 @@ class Directory(Base, ModelMixin):
"Mailbox", secondary="directory_mailbox", lazy="joined" "Mailbox", secondary="directory_mailbox", lazy="joined"
) )
__table_args__ = (sa.Index("ix_directory_user_id", "user_id"),)
@property @property
def mailboxes(self): def mailboxes(self):
if self._mailboxes: if self._mailboxes:
@ -2747,7 +2805,10 @@ class Mailbox(Base, ModelMixin):
generic_subject = sa.Column(sa.String(78), nullable=True) generic_subject = sa.Column(sa.String(78), nullable=True)
__table_args__ = (sa.UniqueConstraint("user_id", "email", name="uq_mailbox_user"),) __table_args__ = (
sa.UniqueConstraint("user_id", "email", name="uq_mailbox_user"),
sa.Index("ix_mailbox_pgp_finger_print", "pgp_finger_print"),
)
user = orm.relationship(User, foreign_keys=[user_id]) user = orm.relationship(User, foreign_keys=[user_id])
@ -2884,6 +2945,8 @@ class RefusedEmail(Base, ModelMixin):
# toggle this when email content (stored at full_report_path & path are deleted) # toggle this when email content (stored at full_report_path & path are deleted)
deleted = sa.Column(sa.Boolean, nullable=False, default=False, server_default="0") deleted = sa.Column(sa.Boolean, nullable=False, default=False, server_default="0")
__table_args__ = (sa.Index("ix_refused_email_user_id", "user_id"),)
def get_url(self, expires_in=3600): def get_url(self, expires_in=3600):
if self.path: if self.path:
return s3.get_url(self.path, expires_in) return s3.get_url(self.path, expires_in)
@ -2906,6 +2969,8 @@ class Referral(Base, ModelMixin):
user = orm.relationship(User, foreign_keys=[user_id], backref="referrals") user = orm.relationship(User, foreign_keys=[user_id], backref="referrals")
__table_args__ = (sa.Index("ix_referral_user_id", "user_id"),)
@property @property
def nb_user(self) -> int: def nb_user(self) -> int:
return User.filter_by(referral_id=self.id, activated=True).count() return User.filter_by(referral_id=self.id, activated=True).count()
@ -2945,6 +3010,8 @@ class SentAlert(Base, ModelMixin):
to_email = sa.Column(sa.String(256), nullable=False) to_email = sa.Column(sa.String(256), nullable=False)
alert_type = sa.Column(sa.String(256), nullable=False) alert_type = sa.Column(sa.String(256), nullable=False)
__table_args__ = (sa.Index("ix_sent_alert_user_id", "user_id"),)
class AliasMailbox(Base, ModelMixin): class AliasMailbox(Base, ModelMixin):
__tablename__ = "alias_mailbox" __tablename__ = "alias_mailbox"
@ -3190,6 +3257,11 @@ class BatchImport(Base, ModelMixin):
file = orm.relationship(File) file = orm.relationship(File)
user = orm.relationship(User) user = orm.relationship(User)
__table_args__ = (
sa.Index("ix_batch_import_file_id", "file_id"),
sa.Index("ix_batch_import_user_id", "user_id"),
)
def nb_alias(self): def nb_alias(self):
return Alias.filter_by(batch_import_id=self.id).count() return Alias.filter_by(batch_import_id=self.id).count()
@ -3210,6 +3282,7 @@ class AuthorizedAddress(Base, ModelMixin):
__table_args__ = ( __table_args__ = (
sa.UniqueConstraint("mailbox_id", "email", name="uq_authorize_address"), sa.UniqueConstraint("mailbox_id", "email", name="uq_authorize_address"),
sa.Index("ix_authorized_address_user_id", "user_id"),
) )
mailbox = orm.relationship(Mailbox, backref="authorized_addresses") mailbox = orm.relationship(Mailbox, backref="authorized_addresses")
@ -3351,6 +3424,8 @@ class Payout(Base, ModelMixin):
user = orm.relationship(User) user = orm.relationship(User)
__table_args__ = (sa.Index("ix_payout_user_id", "user_id"),)
class IgnoredEmail(Base, ModelMixin): class IgnoredEmail(Base, ModelMixin):
"""If an email has mail_from and rcpt_to present in this table, discard it by returning 250 status.""" """If an email has mail_from and rcpt_to present in this table, discard it by returning 250 status."""
@ -3452,6 +3527,8 @@ class PhoneReservation(Base, ModelMixin):
start = sa.Column(ArrowType, nullable=False) start = sa.Column(ArrowType, nullable=False)
end = sa.Column(ArrowType, nullable=False) end = sa.Column(ArrowType, nullable=False)
__table_args__ = (sa.Index("ix_phone_reservation_user_id", "user_id"),)
class PhoneMessage(Base, ModelMixin): class PhoneMessage(Base, ModelMixin):
__tablename__ = "phone_message" __tablename__ = "phone_message"
@ -3626,6 +3703,11 @@ class ProviderComplaint(Base, ModelMixin):
user = orm.relationship(User, foreign_keys=[user_id]) user = orm.relationship(User, foreign_keys=[user_id])
refused_email = orm.relationship(RefusedEmail, foreign_keys=[refused_email_id]) refused_email = orm.relationship(RefusedEmail, foreign_keys=[refused_email_id])
__table_args__ = (
sa.Index("ix_provider_complaint_user_id", "user_id"),
sa.Index("ix_provider_complaint_refused_email_id", "refused_email_id"),
)
class PartnerApiToken(Base, ModelMixin): class PartnerApiToken(Base, ModelMixin):
__tablename__ = "partner_api_token" __tablename__ = "partner_api_token"
@ -3749,6 +3831,8 @@ class NewsletterUser(Base, ModelMixin):
user = orm.relationship(User) user = orm.relationship(User)
newsletter = orm.relationship(Newsletter) newsletter = orm.relationship(Newsletter)
__table_args__ = (sa.Index("ix_newsletter_user_user_id", "user_id"),)
class ApiToCookieToken(Base, ModelMixin): class ApiToCookieToken(Base, ModelMixin):
__tablename__ = "api_cookie_token" __tablename__ = "api_cookie_token"
@ -3759,6 +3843,11 @@ class ApiToCookieToken(Base, ModelMixin):
user = orm.relationship(User) user = orm.relationship(User)
api_key = orm.relationship(ApiKey) api_key = orm.relationship(ApiKey)
__table_args__ = (
sa.Index("ix_api_to_cookie_token_api_key_id", "api_key_id"),
sa.Index("ix_api_to_cookie_token_user_id", "user_id"),
)
@classmethod @classmethod
def create(cls, **kwargs): def create(cls, **kwargs):
code = secrets.token_urlsafe(32) code = secrets.token_urlsafe(32)

View File

@ -7000,7 +7000,6 @@ unfunded
unglazed unglazed
ungloved ungloved
unglue unglue
ungodly
ungraded ungraded
ungreased ungreased
unguarded unguarded

View File

@ -0,0 +1,30 @@
"""add missing indices on user and mailbox
Revision ID: 842ac670096e
Revises: bc9aa210efa3
Create Date: 2024-11-13 15:55:28.798506
"""
import sqlalchemy_utils
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '842ac670096e'
down_revision = 'bc9aa210efa3'
branch_labels = None
depends_on = None
def upgrade():
with op.get_context().autocommit_block():
op.create_index('ix_mailbox_pgp_finger_print', 'mailbox', ['pgp_finger_print'], unique=False)
op.create_index('ix_users_default_mailbox_id', 'users', ['default_mailbox_id'], unique=False)
# ### end Alembic commands ###
def downgrade():
with op.get_context().autocommit_block():
op.drop_index('ix_users_default_mailbox_id', table_name='users')
op.drop_index('ix_mailbox_pgp_finger_print', table_name='mailbox')

View File

@ -0,0 +1,29 @@
"""add missing indices on email log
Revision ID: 12274da2299f
Revises: 842ac670096e
Create Date: 2024-11-14 10:27:20.371191
"""
import sqlalchemy_utils
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '12274da2299f'
down_revision = '842ac670096e'
branch_labels = None
depends_on = None
def upgrade():
with op.get_context().autocommit_block():
op.create_index('ix_email_log_bounced_mailbox_id', 'email_log', ['bounced_mailbox_id'], unique=False)
op.create_index('ix_email_log_mailbox_id', 'email_log', ['mailbox_id'], unique=False)
def downgrade():
with op.get_context().autocommit_block():
op.drop_index('ix_email_log_mailbox_id', table_name='email_log')
op.drop_index('ix_email_log_bounced_mailbox_id', table_name='email_log')

View File

@ -0,0 +1,102 @@
"""add missing indices for fk constraints
Revision ID: 0f3ee15b0014
Revises: 12274da2299f
Create Date: 2024-11-15 12:29:10.739938
"""
import sqlalchemy_utils
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '0f3ee15b0014'
down_revision = '12274da2299f'
branch_labels = None
depends_on = None
def upgrade():
with op.get_context().autocommit_block():
op.create_index('ix_activation_code_user_id', 'activation_code', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_alias_original_owner_id', 'alias', ['original_owner_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_alias_used_on_user_id', 'alias_used_on', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_api_to_cookie_token_api_key_id', 'api_cookie_token', ['api_key_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_api_to_cookie_token_user_id', 'api_cookie_token', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_api_key_code', 'api_key', ['code'], unique=False, postgresql_concurrently=True)
op.create_index('ix_api_key_user_id', 'api_key', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_authorization_code_client_id', 'authorization_code', ['client_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_authorization_code_user_id', 'authorization_code', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_authorized_address_user_id', 'authorized_address', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_auto_create_rule_custom_domain_id', 'auto_create_rule', ['custom_domain_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_batch_import_file_id', 'batch_import', ['file_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_batch_import_user_id', 'batch_import', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_client_icon_id', 'client', ['icon_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_client_referral_id', 'client', ['referral_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_client_user_id', 'client', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_coupon_used_by_user_id', 'coupon', ['used_by_user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_directory_user_id', 'directory', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_domain_deleted_alias_user_id', 'domain_deleted_alias', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_email_log_refused_email_id', 'email_log', ['refused_email_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_fido_user_id', 'fido', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_file_user_id', 'file', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_hibp_notified_alias_user_id', 'hibp_notified_alias', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_mfa_browser_user_id', 'mfa_browser', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_newsletter_user_user_id', 'newsletter_user', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_oauth_token_client_id', 'oauth_token', ['client_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_oauth_token_user_id', 'oauth_token', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_payout_user_id', 'payout', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_phone_reservation_user_id', 'phone_reservation', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_provider_complaint_refused_email_id', 'provider_complaint', ['refused_email_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_provider_complaint_user_id', 'provider_complaint', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_redirect_uri_client_id', 'redirect_uri', ['client_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_referral_user_id', 'referral', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_refused_email_user_id', 'refused_email', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_reset_password_code_user_id', 'reset_password_code', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_sent_alert_user_id', 'sent_alert', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_users_default_alias_custom_domain_id', 'users', ['default_alias_custom_domain_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_users_profile_picture_id', 'users', ['profile_picture_id'], unique=False, postgresql_concurrently=True)
def downgrade():
with op.get_context().autocommit_block():
op.drop_index('ix_users_profile_picture_id', table_name='users')
op.drop_index('ix_users_default_alias_custom_domain_id', table_name='users')
op.drop_index('ix_sent_alert_user_id', table_name='sent_alert')
op.drop_index('ix_reset_password_code_user_id', table_name='reset_password_code')
op.drop_index('ix_refused_email_user_id', table_name='refused_email')
op.drop_index('ix_referral_user_id', table_name='referral')
op.drop_index('ix_redirect_uri_client_id', table_name='redirect_uri')
op.drop_index('ix_provider_complaint_user_id', table_name='provider_complaint')
op.drop_index('ix_provider_complaint_refused_email_id', table_name='provider_complaint')
op.drop_index('ix_phone_reservation_user_id', table_name='phone_reservation')
op.drop_index('ix_payout_user_id', table_name='payout')
op.drop_index('ix_oauth_token_user_id', table_name='oauth_token')
op.drop_index('ix_oauth_token_client_id', table_name='oauth_token')
op.drop_index('ix_newsletter_user_user_id', table_name='newsletter_user')
op.drop_index('ix_mfa_browser_user_id', table_name='mfa_browser')
op.drop_index('ix_hibp_notified_alias_user_id', table_name='hibp_notified_alias')
op.drop_index('ix_file_user_id', table_name='file')
op.drop_index('ix_fido_user_id', table_name='fido')
op.drop_index('ix_email_log_refused_email_id', table_name='email_log')
op.drop_index('ix_domain_deleted_alias_user_id', table_name='domain_deleted_alias')
op.drop_index('ix_directory_user_id', table_name='directory')
op.drop_index('ix_coupon_used_by_user_id', table_name='coupon')
op.drop_index('ix_client_user_id', table_name='client')
op.drop_index('ix_client_referral_id', table_name='client')
op.drop_index('ix_client_icon_id', table_name='client')
op.drop_index('ix_batch_import_user_id', table_name='batch_import')
op.drop_index('ix_batch_import_file_id', table_name='batch_import')
op.drop_index('ix_auto_create_rule_custom_domain_id', table_name='auto_create_rule')
op.drop_index('ix_authorized_address_user_id', table_name='authorized_address')
op.drop_index('ix_authorization_code_user_id', table_name='authorization_code')
op.drop_index('ix_authorization_code_client_id', table_name='authorization_code')
op.drop_index('ix_api_key_user_id', table_name='api_key')
op.drop_index('ix_api_key_code', table_name='api_key')
op.drop_index('ix_api_to_cookie_token_user_id', table_name='api_cookie_token')
op.drop_index('ix_api_to_cookie_token_api_key_id', table_name='api_cookie_token')
op.drop_index('ix_alias_used_on_user_id', table_name='alias_used_on')
op.drop_index('ix_alias_original_owner_id', table_name='alias')
op.drop_index('ix_activation_code_user_id', table_name='activation_code')

View File

@ -1,286 +1,295 @@
{% extends 'admin/master.html' %} {% extends 'admin/master.html' %}
{% macro show_user(user) -%} {% macro show_user(user) -%}
<h4>User {{ user.email }} with ID {{ user.id }}.</h4> <h4>User {{ user.email }} with ID {{ user.id }}.</h4>
{% set pu = helper.partner_user(user) %} {% set pu = helper.partner_user(user) %}
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th scope="col">User ID</th> <th scope="col">User ID</th>
<th scope="col">Email</th> <th scope="col">Email</th>
<th scope="col">Verified</th> <th scope="col">Verified</th>
<th scope="col">Status</th> <th scope="col">Status</th>
<th scope="col">Paid</th> <th scope="col">Paid</th>
<th scope="col">Premium</th> <th scope="col">Premium</th>
<th>Subscription</th> <th>Subscription</th>
<th>Created At</th> <th>Created At</th>
<th>Updated At</th> <th>Updated At</th>
<th>Connected with Proton account</th> <th>Connected with Proton account</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>{{ user.id }}</td> <td>{{ user.id }}</td>
<td><a href="?email={{ user.email }}">{{ user.email }}</a></td> <td>
{% if user.activated %} <a href="?email={{ user.email }}">{{ user.email }}</a>
<td class="text-success">Activated</td> </td>
{% else %} {% if user.activated %}
<td class="text-warning">Pending</td>
{% endif %}
{% if user.disabled %}
<td class="text-danger">Disabled</td>
{% else %}
<td class="text-success">Enabled</td>
{% endif %}
<td>{{ "yes" if user.is_paid() else "No" }}</td>
<td>{{ "yes" if user.is_premium() else "No" }}</td>
<td>{{ user.get_active_subscription() }}</td>
<td>{{ user.created_at }}</td>
<td>{{ user.updated_at }}</td>
{% if pu %}
<td><a href="?email={{ pu.partner_email }}">{{ pu.partner_email }}</a></td> <td class="text-success">Activated</td>
{% else %} {% else %}
<td>No</td> <td class="text-warning">Pending</td>
{% endif %} {% endif %}
</tr> {% if user.disabled %}
</tbody>
</table> <td class="text-danger">Disabled</td>
{% else %}
<td class="text-success">Enabled</td>
{% endif %}
<td>{{ "yes" if user.is_paid() else "No" }}</td>
<td>{{ "yes" if user.is_premium() else "No" }}</td>
<td>{{ user.get_active_subscription() }}</td>
<td>{{ user.created_at }}</td>
<td>{{ user.updated_at }}</td>
{% if pu %}
<td>
<a href="?email={{ pu.partner_email }}">{{ pu.partner_email }}</a>
</td>
{% else %}
<td>No</td>
{% endif %}
</tr>
</tbody>
</table>
{%- endmacro %} {%- endmacro %}
{% macro list_mailboxes(message, mbox_count, mboxes) %} {% macro list_mailboxes(message, mbox_count, mboxes) %}
<h4> <h4>
{{ mbox_count }} {{ message }}. {{ mbox_count }} {{ message }}.
{% if mbox_count>10 %}Showing only the last 10.{% endif %} {% if mbox_count>10 %}Showing only the last 10.{% endif %}
</h4> </h4>
<table class="table"> <table class="table">
<thead> <thead>
<tr>
<th>Mailbox ID</th>
<th>Email</th>
<th>Verified</th>
<th>Created At</th>
</tr>
</thead>
<tbody>
{% for mailbox in mboxes %}
<tr> <tr>
<th>Mailbox ID</th> <td>{{ mailbox.id }}</td>
<th>Email</th> <td>
<th>Verified</th> <a href="?email={{ mailbox.email }}">{{ mailbox.email }}</a>
<th>Created At</th> </td>
<td>{{ "Yes" if mailbox.verified else "No" }}</td>
<td>{{ mailbox.created_at }}</td>
</tr> </tr>
</thead> {% endfor %}
<tbody> </tbody>
{% for mailbox in mboxes %} </table>
<tr>
<td>{{ mailbox.id }}</td>
<td><a href="?email={{ mailbox.email }}">{{ mailbox.email }}</a></td>
<td>{{ "Yes" if mailbox.verified else "No" }}</td>
<td>
{{ mailbox.created_at }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endmacro %} {% endmacro %}
{% macro list_alias(alias_count, aliases) %} {% macro list_alias(alias_count, aliases) %}
<h4> <h4>
{{ alias_count }} Aliases found. {{ alias_count }} Aliases found.
{% if alias_count>10 %}Showing only the last 10.{% endif %} {% if alias_count>10 %}Showing only the last 10.{% endif %}
</h4> </h4>
<table class="table"> <table class="table">
<thead> <thead>
<tr>
<th>Alias ID</th>
<th>Email</th>
<th>Enabled</th>
<th>Created At</th>
</tr>
</thead>
<tbody>
{% for alias in aliases %}
<tr> <tr>
<th> <td>{{ alias.id }}</td>
Alias ID <td>
</th> <a href="?email={{ alias.email }}">{{ alias.email }}</a>
<th> </td>
Email <td>{{ "Yes" if alias.enabled else "No" }}</td>
</th> <td>{{ alias.created_at }}</td>
<th>
Enabled
</th>
<th>
Created At
</th>
</tr> </tr>
</thead> {% endfor %}
<tbody> </tbody>
{% for alias in aliases %} </table>
<tr>
<td>{{ alias.id }}</td>
<td><a href="?email={{ alias.email }}">{{ alias.email }}</a></td>
<td>{{ "Yes" if alias.enabled else "No" }}</td>
<td>{{ alias.created_at }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endmacro %} {% endmacro %}
{% macro show_deleted_alias(deleted_alias) -%} {% macro show_deleted_alias(deleted_alias) -%}
<h4>Deleted Alias {{ deleted_alias.email }} with ID {{ deleted_alias.id }}.</h4> <h4>Deleted Alias {{ deleted_alias.email }} with ID {{ deleted_alias.id }}.</h4>
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th scope="col">Deleted Alias ID</th> <th scope="col">Deleted Alias ID</th>
<th scope="col">Email</th> <th scope="col">Email</th>
<th scope="col">Deleted At</th> <th scope="col">Deleted At</th>
<th scope="col">Reason</th> <th scope="col">Reason</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>{{ deleted_alias.id }}</td> <td>{{ deleted_alias.id }}</td>
<td>{{ deleted_alias.email }}</td> <td>{{ deleted_alias.email }}</td>
<td>{{ deleted_alias.created_at }}</td> <td>{{ deleted_alias.created_at }}</td>
<td>{{ deleted_alias.reason }}</td> <td>{{ deleted_alias.reason }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
{%- endmacro %} {%- endmacro %}
{% macro show_domain_deleted_alias(dom_deleted_alias) -%} {% macro show_domain_deleted_alias(dom_deleted_alias) -%}
<h4> <h4>
Domain Deleted Alias {{ dom_deleted_alias.email }} with ID {{ dom_deleted_alias.id }} for Domain Deleted Alias {{ dom_deleted_alias.email }} with ID {{ dom_deleted_alias.id }} for
domain {{ dom_deleted_alias.domain.domain }} domain {{ dom_deleted_alias.domain.domain }}
</h4> </h4>
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th scope="col">Deleted Alias ID</th> <th scope="col">Deleted Alias ID</th>
<th scope="col">Email</th> <th scope="col">Email</th>
<th scope="col">Domain</th> <th scope="col">Domain</th>
<th scope="col">Domain ID</th> <th scope="col">Domain ID</th>
<th scope="col">Domain owner user ID</th> <th scope="col">Domain owner user ID</th>
<th scope="col">Domain owner user email</th> <th scope="col">Domain owner user email</th>
<th scope="col">Deleted At</th> <th scope="col">Deleted At</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>{{ dom_deleted_alias.id }}</td> <td>{{ dom_deleted_alias.id }}</td>
<td>{{ dom_deleted_alias.email }}</td> <td>{{ dom_deleted_alias.email }}</td>
<td>{{ dom_deleted_alias.domain.domain }}</td> <td>{{ dom_deleted_alias.domain.domain }}</td>
<td>{{ dom_deleted_alias.domain.id }}</td> <td>{{ dom_deleted_alias.domain.id }}</td>
<td>{{ dom_deleted_alias.domain.user_id }}</td> <td>{{ dom_deleted_alias.domain.user_id }}</td>
<td>{{ dom_deleted_alias.created_at }}</td> <td>{{ dom_deleted_alias.created_at }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
{{ show_user(data.domain_deleted_alias.domain.user) }} {{ show_user(data.domain_deleted_alias.domain.user) }}
{%- endmacro %} {%- endmacro %}
{% macro list_alias_audit_log(alias_audit_log) %} {% macro list_alias_audit_log(alias_audit_log) %}
<h4>Alias Audit Log</h4> <h4>Alias Audit Log</h4>
<table class="table"> <table class="table">
<thead> <thead>
<tr>
<th>User ID</th>
<th>Alias ID</th>
<th>Alias Email</th>
<th>Action</th>
<th>Message</th>
<th>Time</th>
</tr>
</thead>
<tbody>
{% for entry in alias_audit_log %}
<tr> <tr>
<th>User ID</th> <td>{{ entry.user_id }}</td>
<th>Alias ID</th> <td>{{ entry.alias_id }}</td>
<th>Alias Email</th> <td>
<th>Action</th> <a href="?email={{ entry.alias_email }}">{{ entry.alias_email }}</a>
<th>Message</th> </td>
<th>Time</th> <td>{{ entry.action }}</td>
<td>{{ entry.message }}</td>
<td>{{ entry.created_at }}</td>
</tr> </tr>
</thead> {% endfor %}
<tbody> </tbody>
{% for entry in alias_audit_log %} </table>
<tr>
<td>{{ entry.user_id }}</td>
<td>{{ entry.alias_id }}</td>
<td><a href="?email={{ entry.alias_email }}">{{ entry.alias_email }}</a></td>
<td>{{ entry.action }}</td>
<td>{{ entry.message }}</td>
<td>{{ entry.created_at }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endmacro %} {% endmacro %}
{% macro list_user_audit_log(user_audit_log) %} {% macro list_user_audit_log(user_audit_log) %}
<h4>User Audit Log</h4> <h4>User Audit Log</h4>
<table class="table"> <table class="table">
<thead> <thead>
<tr>
<th>User email</th>
<th>Action</th>
<th>Message</th>
<th>Time</th>
</tr>
</thead>
<tbody>
{% for entry in user_audit_log %}
<tr> <tr>
<th>User email</th> <td>
<th>Action</th> <a href="?email={{ entry.user_email }}">{{ entry.user_email }}</a>
<th>Message</th> </td>
<th>Time</th> <td>{{ entry.action }}</td>
<td>{{ entry.message }}</td>
<td>{{ entry.created_at }}</td>
</tr> </tr>
</thead> {% endfor %}
<tbody> </tbody>
{% for entry in user_audit_log %} </table>
<tr>
<td><a href="?email={{ entry.user_email }}">{{ entry.user_email }}</a></td>
<td>{{ entry.action }}</td>
<td>{{ entry.message }}</td>
<td>{{ entry.created_at }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endmacro %} {% endmacro %}
{% block body %} {% block body %}
<div class="border border-dark border-2 mt-1 mb-2 p-3">
<form method="get">
<div class="form-group">
<label for="email">Email to search:</label>
<input type="text"
class="form-control"
name="email"
value="{{ email or '' }}" />
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
{% if data.no_match and email %}
<div class="border border-dark border-2 mt-1 mb-2 p-3 alert alert-warning"
role="alert">No user, alias or mailbox found for {{ email }}</div>
{% endif %}
{% if data.alias %}
<div class="border border-dark border-2 mt-1 mb-2 p-3"> <div class="border border-dark border-2 mt-1 mb-2 p-3">
<form method="get"> <h3 class="mb-3">Found Alias {{ data.alias.email }}</h3>
<div class="form-group"> {{ list_alias(1,[data.alias]) }}
<label for="email">Email to search:</label> {{ list_alias_audit_log(data.alias_audit_log) }}
<input type="text" {{ list_mailboxes("Mailboxes for alias", helper.alias_mailbox_count(data.alias) , helper.alias_mailboxes(data.alias)) }}
class="form-control" {{ show_user(data.alias.user) }}
name="email"
value="{{ email or '' }}"/>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div> </div>
{% if data.no_match and email %} {% endif %}
<div class="border border-dark border-2 mt-1 mb-2 p-3 alert alert-warning" {% if data.user %}
role="alert">No user, alias or mailbox found for {{ email }}</div>
{% endif %}
{% if data.alias %} <div class="border border-dark border-2 mt-1 mb-2 p-3">
<div class="border border-dark border-2 mt-1 mb-2 p-3"> <h3 class="mb-3">Found User {{ data.user.email }}</h3>
<h3 class="mb-3">Found Alias {{ data.alias.email }}</h3> {{ show_user(data.user) }}
{{ list_alias(1,[data.alias]) }} {{ list_mailboxes("Mailboxes for user", helper.mailbox_count(data.user) , helper.mailbox_list(data.user) ) }}
{{ list_alias_audit_log(data.alias_audit_log) }} {{ list_alias(helper.alias_count(data.user) ,helper.alias_list(data.user)) }}
{{ list_mailboxes("Mailboxes for alias", helper.alias_mailbox_count(data.alias), helper.alias_mailboxes(data.alias)) }} </div>
{{ show_user(data.alias.user) }} {% endif %}
</div> {% if data.user_audit_log %}
{% endif %}
{% if data.user %} <div class="border border-dark border-2 mt-1 mb-2 p-3">
<div class="border border-dark border-2 mt-1 mb-2 p-3"> <h3 class="mb-3">Audit log entries for user {{ data.query }}</h3>
<h3 class="mb-3">Found User {{ data.user.email }}</h3> {{ list_user_audit_log(data.user_audit_log) }}
{{ show_user(data.user) }} </div>
{{ list_mailboxes("Mailboxes for user", helper.mailbox_count(data.user) , helper.mailbox_list(data.user) ) }} {% endif %}
{{ list_alias(helper.alias_count(data.user) ,helper.alias_list(data.user)) }} {% if data.mailbox_count > 10 %}
</div>
{% endif %}
{% if data.user_audit_log %}
<div class="border border-dark border-2 mt-1 mb-2 p-3">
<h3 class="mb-3">Audit log entries for user {{ data.query }}</h3>
{{ list_user_audit_log(data.user_audit_log) }}
</div>
{% endif %}
{% if data.mailbox_count > 10 %}
<h3>Found more than 10 mailboxes for {{ email }}. Showing the last 10</h3>
{% elif data.mailbox_count > 0 %}
<h3>Found {{ data.mailbox_count }} mailbox(es) for {{ email }}</h3>
{% endif %}
{% for mailbox in data.mailbox %}
<div class="border border-dark mt-1 mb-2 p-3"> <h3>Found more than 10 mailboxes for {{ email }}. Showing the last 10</h3>
<h3 class="mb-3">Found Mailbox {{ mailbox.email }}</h3> {% elif data.mailbox_count > 0 %}
{{ list_mailboxes("Mailbox found", 1, [mailbox]) }} <h3>Found {{ data.mailbox_count }} mailbox(es) for {{ email }}</h3>
{{ show_user(mailbox.user) }} {% endif %}
</div> {% for mailbox in data.mailbox %}
{% endfor %}
{% if data.deleted_alias %}
<div class="border border-dark mt-1 mb-2 p-3"> <div class="border border-dark mt-1 mb-2 p-3">
<h3 class="mb-3">Found DeletedAlias {{ data.deleted_alias.email }}</h3> <h3 class="mb-3">Found Mailbox {{ mailbox.email }}</h3>
{{ show_deleted_alias(data.deleted_alias) }} {{ list_mailboxes("Mailbox found", 1, [mailbox]) }}
{{ list_alias_audit_log(data.deleted_alias_audit_log) }} {{ show_user(mailbox.user) }}
</div> </div>
{% endif %} {% endfor %}
{% if data.domain_deleted_alias %} {% if data.deleted_alias %}
<div class="border border-dark mt-1 mb-2 p-3"> <div class="border border-dark mt-1 mb-2 p-3">
<h3 class="mb-3">Found DomainDeletedAlias {{ data.domain_deleted_alias.email }}</h3> <h3 class="mb-3">Found DeletedAlias {{ data.deleted_alias.email }}</h3>
{{ show_domain_deleted_alias(data.domain_deleted_alias) }} {{ show_deleted_alias(data.deleted_alias) }}
{{ list_alias_audit_log(data.domain_deleted_alias_audit_log) }} {{ list_alias_audit_log(data.deleted_alias_audit_log) }}
</div> </div>
{% endif %} {% endif %}
{% if data.domain_deleted_alias %}
<div class="border border-dark mt-1 mb-2 p-3">
<h3 class="mb-3">Found DomainDeletedAlias {{ data.domain_deleted_alias.email }}</h3>
{{ show_domain_deleted_alias(data.domain_deleted_alias) }}
{{ list_alias_audit_log(data.domain_deleted_alias_audit_log) }}
</div>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -43,7 +43,7 @@
You can change the plan at any moment. You can change the plan at any moment.
<br /> <br />
Please note that the new billing cycle starts instantly Please note that the new billing cycle starts instantly
i.e. you will be charged <b>immediately</b> the annual fee ($30) when switching from monthly plan or vice-versa i.e. you will be charged <b>immediately</b> the annual fee ($36) when switching from monthly plan or vice-versa
<b>without pro rata computation </b>. <b>without pro rata computation </b>.
<br /> <br />
To change the plan you can also cancel the current one and subscribe a new one <b>by the end</b> of this plan. To change the plan you can also cancel the current one and subscribe a new one <b>by the end</b> of this plan.

View File

@ -94,4 +94,3 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -91,7 +91,6 @@
<br /> <br />
Some domain registrars (Namecheap, CloudFlare, etc) might also use <em>@</em> for the root domain. Some domain registrars (Namecheap, CloudFlare, etc) might also use <em>@</em> for the root domain.
</div> </div>
{% for record in expected_mx_records %} {% for record in expected_mx_records %}
<div class="mb-3 p-3 dns-record"> <div class="mb-3 p-3 dns-record">
@ -108,7 +107,6 @@
data-clipboard-text="{{ record.domain }}">{{ record.domain }}</em> data-clipboard-text="{{ record.domain }}">{{ record.domain }}</em>
</div> </div>
{% endfor %} {% endfor %}
<form method="post" action="#mx-form"> <form method="post" action="#mx-form">
{{ csrf_form.csrf_token }} {{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="check-mx"> <input type="hidden" name="form-name" value="check-mx">

View File

@ -22,7 +22,8 @@
<p>Alternatively you can use your Proton credentials to ensure it's you.</p> <p>Alternatively you can use your Proton credentials to ensure it's you.</p>
</div> </div>
<a class="btn btn-primary btn-block mt-2 proton-button" <a class="btn btn-primary btn-block mt-2 proton-button"
href="{{ url_for('auth.proton_login', next=next) }}" style="max-width: 400px"> href="{{ url_for('auth.proton_login', next=next) }}"
style="max-width: 400px">
<img class="mr-2" src="/static/images/proton.svg" /> <img class="mr-2" src="/static/images/proton.svg" />
Authenticate with Proton Authenticate with Proton
</a> </a>
@ -38,4 +39,4 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -11,7 +11,7 @@
<div> <div>
<a class="buy-with-crypto" <a class="buy-with-crypto"
data-custom="{{ current_user.id }}" data-custom="{{ current_user.id }}"
href="{{ coinbase_url }}">Extend for 1 year - $30</a> href="{{ coinbase_url }}">Extend for 1 year - $36</a>
<script src="https://commerce.coinbase.com/v1/checkout.js?version=201807"></script> <script src="https://commerce.coinbase.com/v1/checkout.js?version=201807"></script>
</div> </div>
<div class="mt-2"> <div class="mt-2">

View File

@ -77,6 +77,11 @@
<div class="text-center mx-md-auto mb-8 mt-6"> <div class="text-center mx-md-auto mb-8 mt-6">
<h1>Upgrade to unlock premium features</h1> <h1>Upgrade to unlock premium features</h1>
</div> </div>
<div class="alert alert-info">
<span class="badge badge-success">new</span> SimpleLogin Premium now includes Proton Pass premium features.
<a href="https://simplelogin.io/blog/sl-premium-including-pass-plus/"
target="_blank">Learn more ↗</a>
</div>
{% if manual_sub %} {% if manual_sub %}
<div class="alert alert-info mt-0 mb-6"> <div class="alert alert-info mt-0 mb-6">
@ -306,7 +311,7 @@
<div class="card-body"> <div class="card-body">
<div class="text-center"> <div class="text-center">
<div class="h3">SimpleLogin Premium</div> <div class="h3">SimpleLogin Premium</div>
<div class="h3 my-3">$30 / year</div> <div class="h3 my-3">$36 / year</div>
<div class="text-center mt-4 mb-6"> <div class="text-center mt-4 mb-6">
<button class="btn btn-primary btn-lg w-100" <button class="btn btn-primary btn-lg w-100"
onclick="upgradePaddle({{ PADDLE_YEARLY_PRODUCT_ID }})">Upgrade to Premium</button> onclick="upgradePaddle({{ PADDLE_YEARLY_PRODUCT_ID }})">Upgrade to Premium</button>
@ -471,7 +476,7 @@
rel="noopener noreferrer"> rel="noopener noreferrer">
Upgrade to Premium - cryptocurrency Upgrade to Premium - cryptocurrency
<br /> <br />
$30 / year $36 / year
<i class="fe fe-external-link"></i> <i class="fe fe-external-link"></i>
</a> </a>
</div> </div>

View File

@ -1,5 +1,5 @@
from app.db import Session from app.db import Session
from app.models import Alias, Mailbox, AliasMailbox, User from app.models import Alias, Mailbox, AliasMailbox, User, CustomDomain
from tests.utils import create_new_user, random_email from tests.utils import create_new_user, random_email
@ -29,3 +29,23 @@ def test_alias_create_from_partner_flags_also_the_user():
flush=True, flush=True,
) )
assert alias.user.flags & User.FLAG_CREATED_ALIAS_FROM_PARTNER > 0 assert alias.user.flags & User.FLAG_CREATED_ALIAS_FROM_PARTNER > 0
def test_alias_create_from_partner_domain_flags_the_alias():
user = create_new_user()
domain = CustomDomain.create(
domain=random_email(),
verified=True,
user_id=user.id,
partner_id=1,
)
Session.flush()
email = random_email()
alias = Alias.create(
user_id=user.id,
email=email,
mailbox_id=user.default_mailbox_id,
custom_domain_id=domain.id,
flush=True,
)
assert alias.flags & Alias.FLAG_PARTNER_CREATED > 0

View File

@ -144,6 +144,21 @@ def test_login_case_from_web():
assert audit_logs[0].action == UserAuditLogAction.LinkAccount.value assert audit_logs[0].action == UserAuditLogAction.LinkAccount.value
def test_new_user_strategy_create_missing_link():
email = random_email()
user = User.create(email, commit=True)
nus = NewUserStrategy(
link_request=random_link_request(
email=user.email, external_user_id=random_string(), from_partner=False
),
user=None,
partner=get_proton_partner(),
)
result = nus.create_missing_link(user.email)
assert result.user.id == user.id
assert result.strategy == ExistingUnlinkedUserStrategy.__name__
def test_get_strategy_existing_sl_user(): def test_get_strategy_existing_sl_user():
email = random_email() email = random_email()
user = User.create(email, commit=True) user = User.create(email, commit=True)