diff --git a/.gitignore b/.gitignore index de5ebfe1..e7cea4dd 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ venv/ .coverage htmlcov adhoc +.env.* \ No newline at end of file diff --git a/app/api/views/mailbox.py b/app/api/views/mailbox.py index bb1e94e4..e99fd82c 100644 --- a/app/api/views/mailbox.py +++ b/app/api/views/mailbox.py @@ -78,12 +78,15 @@ def delete_mailbox(mailbox_id): Delete mailbox Input: mailbox_id: in url + (optional) transfer_aliases_to: in body. Id of the new mailbox for the aliases. + If omitted or the value is set to -1, + the aliases of the mailbox will be deleted too. Output: 200 if deleted successfully """ user = g.user - mailbox = Mailbox.get(mailbox_id) + mailbox = Mailbox.get(id=mailbox_id) if not mailbox or mailbox.user_id != user.id: return jsonify(error="Forbidden"), 403 @@ -91,11 +94,36 @@ def delete_mailbox(mailbox_id): if mailbox.id == user.default_mailbox_id: return jsonify(error="You cannot delete the default mailbox"), 400 + data = request.get_json() or {} + transfer_mailbox_id = data.get("transfer_aliases_to") + if transfer_mailbox_id and int(transfer_mailbox_id) >= 0: + transfer_mailbox = Mailbox.get(transfer_mailbox_id) + + if not transfer_mailbox or transfer_mailbox.user_id != user.id: + return ( + jsonify(error="You must transfer the aliases to a mailbox you own."), + 403, + ) + + if transfer_mailbox_id == mailbox_id: + return ( + jsonify( + error="You can not transfer the aliases to the mailbox you want to delete." + ), + 400, + ) + + if not transfer_mailbox.verified: + return jsonify(error="Your new mailbox is not verified"), 400 + # Schedule delete account job LOG.w("schedule delete mailbox job for %s", mailbox) Job.create( name=JOB_DELETE_MAILBOX, - payload={"mailbox_id": mailbox.id}, + payload={ + "mailbox_id": mailbox.id, + "transfer_mailbox_id": transfer_mailbox_id, + }, run_at=arrow.now(), commit=True, ) diff --git a/app/dashboard/views/mailbox.py b/app/dashboard/views/mailbox.py index 2bbba0d8..e6f41edc 100644 --- a/app/dashboard/views/mailbox.py +++ b/app/dashboard/views/mailbox.py @@ -3,7 +3,7 @@ from flask import render_template, request, redirect, url_for, flash from flask_login import login_required, current_user from flask_wtf import FlaskForm from itsdangerous import TimestampSigner -from wtforms import validators +from wtforms import validators, IntegerField from wtforms.fields.html5 import EmailField from app import parallel_limiter @@ -28,6 +28,13 @@ class NewMailboxForm(FlaskForm): ) +class DeleteMailboxForm(FlaskForm): + mailbox_id = IntegerField( + validators=[validators.DataRequired()], + ) + transfer_mailbox_id = IntegerField() + + @dashboard_bp.route("/mailbox", methods=["GET", "POST"]) @login_required @parallel_limiter.lock(only_when=lambda: request.method == "POST") @@ -40,28 +47,53 @@ def mailbox_route(): new_mailbox_form = NewMailboxForm() csrf_form = CSRFValidationForm() + delete_mailbox_form = DeleteMailboxForm() if request.method == "POST": - if not csrf_form.validate(): - flash("Invalid request", "warning") - return redirect(request.url) if request.form.get("form-name") == "delete": - mailbox_id = request.form.get("mailbox-id") - mailbox = Mailbox.get(mailbox_id) + if not delete_mailbox_form.validate(): + flash("Invalid request", "warning") + return redirect(request.url) + mailbox = Mailbox.get(delete_mailbox_form.mailbox_id.data) if not mailbox or mailbox.user_id != current_user.id: - flash("Unknown error. Refresh the page", "warning") + flash("Invalid mailbox. Refresh the page", "warning") return redirect(url_for("dashboard.mailbox_route")) if mailbox.id == current_user.default_mailbox_id: flash("You cannot delete default mailbox", "error") return redirect(url_for("dashboard.mailbox_route")) + transfer_mailbox_id = delete_mailbox_form.transfer_mailbox_id.data + if transfer_mailbox_id and transfer_mailbox_id > 0: + transfer_mailbox = Mailbox.get(transfer_mailbox_id) + + if not transfer_mailbox or transfer_mailbox.user_id != current_user.id: + flash("You must transfer the aliases to a mailbox you own.") + return redirect(url_for("dashboard.mailbox_route")) + + if transfer_mailbox.id == mailbox.id: + flash( + "You can not transfer the aliases to the mailbox you want to delete." + ) + return redirect(url_for("dashboard.mailbox_route")) + + if not transfer_mailbox.verified: + flash("Your new mailbox is not verified") + return redirect(url_for("dashboard.mailbox_route")) + # Schedule delete account job - LOG.w("schedule delete mailbox job for %s", mailbox) + LOG.w( + f"schedule delete mailbox job for {mailbox.id} with transfer to mailbox {transfer_mailbox_id}" + ) Job.create( name=JOB_DELETE_MAILBOX, - payload={"mailbox_id": mailbox.id}, + payload={ + "mailbox_id": mailbox.id, + "transfer_mailbox_id": transfer_mailbox_id + if transfer_mailbox_id > 0 + else None, + }, run_at=arrow.now(), commit=True, ) @@ -74,7 +106,10 @@ def mailbox_route(): return redirect(url_for("dashboard.mailbox_route")) if request.form.get("form-name") == "set-default": - mailbox_id = request.form.get("mailbox-id") + if not csrf_form.validate(): + flash("Invalid request", "warning") + return redirect(request.url) + mailbox_id = request.form.get("mailbox_id") mailbox = Mailbox.get(mailbox_id) if not mailbox or mailbox.user_id != current_user.id: @@ -112,12 +147,12 @@ def mailbox_route(): elif not email_can_be_used_as_mailbox(mailbox_email): flash(f"You cannot use {mailbox_email}.", "error") else: - new_mailbox = Mailbox.create( + transfer_mailbox = Mailbox.create( email=mailbox_email, user_id=current_user.id ) Session.commit() - send_verification_email(current_user, new_mailbox) + send_verification_email(current_user, transfer_mailbox) flash( f"You are going to receive an email to confirm {mailbox_email}.", @@ -126,7 +161,8 @@ def mailbox_route(): return redirect( url_for( - "dashboard.mailbox_detail_route", mailbox_id=new_mailbox.id + "dashboard.mailbox_detail_route", + mailbox_id=transfer_mailbox.id, ) ) @@ -134,36 +170,11 @@ def mailbox_route(): "dashboard/mailbox.html", mailboxes=mailboxes, new_mailbox_form=new_mailbox_form, + delete_mailbox_form=delete_mailbox_form, csrf_form=csrf_form, ) -def delete_mailbox(mailbox_id: int): - from server import create_light_app - - with create_light_app().app_context(): - mailbox = Mailbox.get(mailbox_id) - if not mailbox: - return - - mailbox_email = mailbox.email - user = mailbox.user - - Mailbox.delete(mailbox_id) - Session.commit() - LOG.d("Mailbox %s %s deleted", mailbox_id, mailbox_email) - - send_email( - user.email, - f"Your mailbox {mailbox_email} has been deleted", - f"""Mailbox {mailbox_email} along with its aliases are deleted successfully. - -Regards, -SimpleLogin team. - """, - ) - - def send_verification_email(user, mailbox): s = TimestampSigner(MAILBOX_SECRET) mailbox_id_signed = s.sign(str(mailbox.id)).decode() diff --git a/docs/api.md b/docs/api.md index cb451f6b..9f9011f8 100644 --- a/docs/api.md +++ b/docs/api.md @@ -764,6 +764,7 @@ Input: - `Authentication` header that contains the api key - `mailbox_id`: in url +- (optional) `transfer_aliases_to`: in body as json. id of the new mailbox for the aliases. If omitted or set to -1, the aliases will be delete with the mailbox. Output: diff --git a/job_runner.py b/job_runner.py index d4081060..0a710c37 100644 --- a/job_runner.py +++ b/job_runner.py @@ -124,6 +124,58 @@ def welcome_proton(user): ) +def delete_mailbox_job(job: Job): + mailbox_id = job.payload.get("mailbox_id") + mailbox = Mailbox.get(mailbox_id) + if not mailbox: + return + + transfer_mailbox_id = job.payload.get("transfer_mailbox_id") + alias_transferred_to = None + if transfer_mailbox_id: + transfer_mailbox = Mailbox.get(transfer_mailbox_id) + if transfer_mailbox: + alias_transferred_to = transfer_mailbox.email + + for alias in mailbox.aliases: + if alias.mailbox_id == mailbox.id: + alias.mailbox_id = transfer_mailbox.id + if transfer_mailbox in alias._mailboxes: + alias._mailboxes.remove(transfer_mailbox) + else: + alias._mailboxes.remove(mailbox) + if transfer_mailbox not in alias._mailboxes: + alias._mailboxes.append(transfer_mailbox) + Session.commit() + + mailbox_email = mailbox.email + user = mailbox.user + Mailbox.delete(mailbox_id) + Session.commit() + LOG.d("Mailbox %s %s deleted", mailbox_id, mailbox_email) + + if alias_transferred_to: + send_email( + user.email, + f"Your mailbox {mailbox_email} has been deleted", + f"""Mailbox {mailbox_email} and its alias have been transferred to {alias_transferred_to}. + Regards, + SimpleLogin team. + """, + retries=3, + ) + else: + send_email( + user.email, + f"Your mailbox {mailbox_email} has been deleted", + f"""Mailbox {mailbox_email} along with its aliases have been deleted successfully. + Regards, + SimpleLogin team. + """, + retries=3, + ) + + def process_job(job: Job): if job.name == config.JOB_ONBOARDING_1: user_id = job.payload.get("user_id") @@ -178,27 +230,7 @@ def process_job(job: Job): retries=3, ) elif job.name == config.JOB_DELETE_MAILBOX: - mailbox_id = job.payload.get("mailbox_id") - mailbox = Mailbox.get(mailbox_id) - if not mailbox: - return - - mailbox_email = mailbox.email - user = mailbox.user - - Mailbox.delete(mailbox_id) - Session.commit() - LOG.d("Mailbox %s %s deleted", mailbox_id, mailbox_email) - - send_email( - user.email, - f"Your mailbox {mailbox_email} has been deleted", - f"""Mailbox {mailbox_email} along with its aliases are deleted successfully. -Regards, -SimpleLogin team. -""", - retries=3, - ) + delete_mailbox_job(job) elif job.name == config.JOB_DELETE_DOMAIN: custom_domain_id = job.payload.get("custom_domain_id") diff --git a/templates/dashboard/mailbox.html b/templates/dashboard/mailbox.html index 587208ea..d0dcba61 100644 --- a/templates/dashboard/mailbox.html +++ b/templates/dashboard/mailbox.html @@ -25,17 +25,18 @@ @@ -74,11 +75,12 @@
Created {{ mailbox.created_at | dt }} -
+
{{ mailbox.nb_alias() }} aliases. -
+
- Edit ➡ + Edit + ➡