From 757f153042f14973e96e87064c85208f994c94a2 Mon Sep 17 00:00:00 2001 From: MrMeeb Date: Fri, 23 Feb 2024 12:00:07 +0000 Subject: [PATCH] 4.39.2 --- app/app/account_linking.py | 4 ++++ app/app/api/base.py | 3 +++ app/app/models.py | 8 ++++++++ app/app/oauth/views/authorize.py | 2 +- app/cron.py | 13 ++++++++++--- app/crontab.yml | 2 +- app/email_handler.py | 13 +++++++++++++ app/local_data/words.txt | 2 -- app/server.py | 2 ++ app/tests/test_cron.py | 18 ++++++++++-------- 10 files changed, 52 insertions(+), 15 deletions(-) diff --git a/app/app/account_linking.py b/app/app/account_linking.py index 967b6c6..1e5f297 100644 --- a/app/app/account_linking.py +++ b/app/app/account_linking.py @@ -168,6 +168,8 @@ class NewUserStrategy(ClientMergeStrategy): class ExistingUnlinkedUserStrategy(ClientMergeStrategy): def process(self) -> LinkResult: + # IF it was scheduled to be deleted. Unschedule it. + self.user.delete_on = None partner_user = ensure_partner_user_exists_for_user( self.link_request, self.user, self.partner ) @@ -246,6 +248,8 @@ def link_user( ) -> LinkResult: # Sanitize email just in case link_request.email = sanitize_email(link_request.email) + # If it was scheduled to be deleted. Unschedule it. + current_user.delete_on = None partner_user = ensure_partner_user_exists_for_user( link_request, current_user, partner ) diff --git a/app/app/api/base.py b/app/app/api/base.py index 7bf177c..a792e8d 100644 --- a/app/app/api/base.py +++ b/app/app/api/base.py @@ -33,6 +33,9 @@ def authorize_request() -> Optional[Tuple[str, int]]: if g.user.disabled: return jsonify(error="Disabled account"), 403 + if not g.user.is_active(): + return jsonify(error="Account does not exist"), 401 + g.api_key = api_key return None diff --git a/app/app/models.py b/app/app/models.py index 6164060..213f409 100644 --- a/app/app/models.py +++ b/app/app/models.py @@ -727,6 +727,11 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle): return True + def is_active(self) -> bool: + if self.delete_on is None: + return True + return self.delete_on < arrow.now() + def in_trial(self): """return True if user does not have lifetime licence or an active subscription AND is in trial period""" if self.lifetime_or_active_subscription(): @@ -828,6 +833,9 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle): Whether user can create a new alias. User can't create a new alias if - has more than 15 aliases in the free plan, *even in the free trial* """ + if not self.is_active(): + return False + if self.disabled: return False diff --git a/app/app/oauth/views/authorize.py b/app/app/oauth/views/authorize.py index 47afc1a..29b6aa9 100644 --- a/app/app/oauth/views/authorize.py +++ b/app/app/oauth/views/authorize.py @@ -140,7 +140,7 @@ def authorize(): Scope=Scope, ) else: # POST - user allows or denies - if not current_user.is_authenticated or not current_user.is_active: + if not current_user.is_authenticated or not current_user.is_active(): LOG.i( "Attempt to validate a OAUth allow request by an unauthenticated user" ) diff --git a/app/cron.py b/app/cron.py index 61fee4c..3626d69 100644 --- a/app/cron.py +++ b/app/cron.py @@ -62,6 +62,8 @@ from app.proton.utils import get_proton_partner from app.utils import sanitize_email from server import create_light_app +DELETE_GRACE_DAYS = 30 + def notify_trial_end(): for user in User.filter( @@ -1126,14 +1128,19 @@ def notify_hibp(): Session.commit() -def clear_users_scheduled_to_be_deleted(): +def clear_users_scheduled_to_be_deleted(dry_run=False): users = User.filter( - and_(User.delete_on.isnot(None), User.delete_on < arrow.now()) + and_( + User.delete_on.isnot(None), + User.delete_on <= arrow.now().shift(days=-DELETE_GRACE_DAYS), + ) ).all() for user in users: LOG.i( f"Scheduled deletion of user {user} with scheduled delete on {user.delete_on}" ) + if dry_run: + continue User.delete(user.id) Session.commit() @@ -1206,4 +1213,4 @@ if __name__ == "__main__": load_unsent_mails_from_fs_and_resend() elif args.job == "delete_scheduled_users": LOG.d("Deleting users scheduled to be deleted") - clear_users_scheduled_to_be_deleted() + clear_users_scheduled_to_be_deleted(dry_run=True) diff --git a/app/crontab.yml b/app/crontab.yml index 86877a5..75a5d20 100644 --- a/app/crontab.yml +++ b/app/crontab.yml @@ -62,7 +62,7 @@ jobs: captureStderr: true - name: SimpleLogin delete users scheduled to be deleted - command: echo disabled_user_deletion #python /code/cron.py -j delete_scheduled_users + command: python /code/cron.py -j delete_scheduled_users shell: /bin/bash schedule: "15 11 * * *" captureStderr: true diff --git a/app/email_handler.py b/app/email_handler.py index 46e9c75..0ac8e18 100644 --- a/app/email_handler.py +++ b/app/email_handler.py @@ -636,6 +636,10 @@ def handle_forward(envelope, msg: Message, rcpt_to: str) -> List[Tuple[bool, str user = alias.user + if not user.is_active(): + LOG.w(f"User {user} has been soft deleted") + return False, status.E502 + if not user.can_send_or_receive(): LOG.i(f"User {user} cannot receive emails") if should_ignore_bounce(envelope.mail_from): @@ -1055,6 +1059,9 @@ def handle_reply(envelope, msg: Message, rcpt_to: str) -> (bool, str): if not contact: LOG.w(f"No contact with {reply_email} as reverse alias") return False, status.E502 + if not contact.user.is_active(): + LOG.w(f"User {contact.user} has been soft deleted") + return False, status.E502 alias = contact.alias alias_address: str = contact.alias.email @@ -1921,6 +1928,9 @@ def handle_bounce(envelope, email_log: EmailLog, msg: Message) -> str: contact, alias, ) + if not email_log.user.is_active(): + LOG.d(f"User {email_log.user} is not active") + return status.E510 if email_log.is_reply: content_type = msg.get_content_type().lower() @@ -1982,6 +1992,9 @@ def send_no_reply_response(mail_from: str, msg: Message): if not mailbox: LOG.d("Unknown sender. Skipping reply from {}".format(NOREPLY)) return + if not mailbox.user.is_active(): + LOG.d(f"User {mailbox.user} is soft-deleted. Skipping sending reply response") + return send_email_at_most_times( mailbox.user, ALERT_TO_NOREPLY, diff --git a/app/local_data/words.txt b/app/local_data/words.txt index f3c10dc..a3f5fdb 100644 --- a/app/local_data/words.txt +++ b/app/local_data/words.txt @@ -7460,9 +7460,7 @@ villain vindicate vineyard vintage -violate violation -violator violet violin viper diff --git a/app/server.py b/app/server.py index f0f57a3..57e9cf1 100644 --- a/app/server.py +++ b/app/server.py @@ -228,6 +228,8 @@ def load_user(alternative_id): sentry_sdk.set_user({"email": user.email, "id": user.id}) if user.disabled: return None + if not user.is_active(): + return None return user diff --git a/app/tests/test_cron.py b/app/tests/test_cron.py index 834200f..6bdbf05 100644 --- a/app/tests/test_cron.py +++ b/app/tests/test_cron.py @@ -39,15 +39,17 @@ def test_cleanup_tokens(flask_client): def test_cleanup_users(): u_delete_none_id = create_new_user().id - u_delete_after = create_new_user() - u_delete_after_id = u_delete_after.id - u_delete_before = create_new_user() - u_delete_before_id = u_delete_before.id + u_delete_grace_has_expired = create_new_user() + u_delete_grace_has_expired_id = u_delete_grace_has_expired.id + u_delete_grace_has_not_expired = create_new_user() + u_delete_grace_has_not_expired_id = u_delete_grace_has_not_expired.id now = arrow.now() - u_delete_after.delete_on = now.shift(minutes=1) - u_delete_before.delete_on = now.shift(minutes=-1) + u_delete_grace_has_expired.delete_on = now.shift(days=-(cron.DELETE_GRACE_DAYS + 1)) + u_delete_grace_has_not_expired.delete_on = now.shift( + days=-(cron.DELETE_GRACE_DAYS - 1) + ) Session.flush() cron.clear_users_scheduled_to_be_deleted() assert User.get(u_delete_none_id) is not None - assert User.get(u_delete_after_id) is not None - assert User.get(u_delete_before_id) is None + assert User.get(u_delete_grace_has_not_expired_id) is not None + assert User.get(u_delete_grace_has_expired_id) is None