4.22.0
This commit is contained in:
parent
92f4ad2237
commit
0aea62c222
1
app/.gitignore
vendored
1
app/.gitignore
vendored
@ -15,3 +15,4 @@ venv/
|
||||
.coverage
|
||||
htmlcov
|
||||
adhoc
|
||||
.env.*
|
@ -2,7 +2,7 @@
|
||||
FROM node:10.17.0-alpine AS npm
|
||||
WORKDIR /code
|
||||
COPY ./static/package*.json /code/static/
|
||||
RUN cd /code/static && npm install
|
||||
RUN cd /code/static && npm ci
|
||||
|
||||
# Main image
|
||||
FROM python:3.10
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -7,6 +7,7 @@ from app.dashboard.base import dashboard_bp
|
||||
from app.dashboard.views.enter_sudo import sudo_required
|
||||
from app.db import Session
|
||||
from app.models import ApiKey
|
||||
from app.utils import CSRFValidationForm
|
||||
|
||||
|
||||
class NewApiKeyForm(FlaskForm):
|
||||
@ -23,9 +24,13 @@ def api_key():
|
||||
.all()
|
||||
)
|
||||
|
||||
csrf_form = CSRFValidationForm()
|
||||
new_api_key_form = NewApiKeyForm()
|
||||
|
||||
if request.method == "POST":
|
||||
if not csrf_form.validate():
|
||||
flash("Invalid request", "warning")
|
||||
return redirect(request.url)
|
||||
if request.form.get("form-name") == "delete":
|
||||
api_key_id = request.form.get("api-key-id")
|
||||
|
||||
@ -62,5 +67,8 @@ def api_key():
|
||||
return redirect(url_for("dashboard.api_key"))
|
||||
|
||||
return render_template(
|
||||
"dashboard/api_key.html", api_keys=api_keys, new_api_key_form=new_api_key_form
|
||||
"dashboard/api_key.html",
|
||||
api_keys=api_keys,
|
||||
new_api_key_form=new_api_key_form,
|
||||
csrf_form=csrf_form,
|
||||
)
|
||||
|
@ -34,7 +34,7 @@ def batch_import_route():
|
||||
if request.method == "POST":
|
||||
if not csrf_form.validate():
|
||||
flash("Invalid request", "warning")
|
||||
redirect(request.url)
|
||||
return redirect(request.url)
|
||||
if len(batch_imports) > 10:
|
||||
flash(
|
||||
"You have too many imports already. Wait until some get cleaned up",
|
||||
|
@ -3,6 +3,7 @@ from flask_login import login_required, current_user
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, validators
|
||||
|
||||
from app import parallel_limiter
|
||||
from app.config import EMAIL_SERVERS_WITH_PRIORITY
|
||||
from app.dashboard.base import dashboard_bp
|
||||
from app.db import Session
|
||||
@ -19,6 +20,7 @@ class NewCustomDomainForm(FlaskForm):
|
||||
|
||||
@dashboard_bp.route("/custom_domain", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@parallel_limiter.lock(only_when=lambda: request.method == "POST")
|
||||
def custom_domain():
|
||||
custom_domains = CustomDomain.filter_by(
|
||||
user_id=current_user.id, is_sl_subdomain=False
|
||||
|
@ -9,6 +9,7 @@ from wtforms import (
|
||||
IntegerField,
|
||||
)
|
||||
|
||||
from app import parallel_limiter
|
||||
from app.config import (
|
||||
EMAIL_DOMAIN,
|
||||
ALIAS_DOMAINS,
|
||||
@ -45,6 +46,7 @@ class DeleteDirForm(FlaskForm):
|
||||
|
||||
@dashboard_bp.route("/directory", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@parallel_limiter.lock(only_when=lambda: request.method == "POST")
|
||||
def directory():
|
||||
dirs = (
|
||||
Directory.filter_by(user_id=current_user.id)
|
||||
|
@ -2,10 +2,11 @@ import arrow
|
||||
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 Signer
|
||||
from wtforms import validators
|
||||
from itsdangerous import TimestampSigner
|
||||
from wtforms import validators, IntegerField
|
||||
from wtforms.fields.html5 import EmailField
|
||||
|
||||
from app import parallel_limiter
|
||||
from app.config import MAILBOX_SECRET, URL, JOB_DELETE_MAILBOX
|
||||
from app.dashboard.base import dashboard_bp
|
||||
from app.db import Session
|
||||
@ -27,8 +28,16 @@ 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")
|
||||
def mailbox_route():
|
||||
mailboxes = (
|
||||
Mailbox.filter_by(user_id=current_user.id)
|
||||
@ -38,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,
|
||||
)
|
||||
@ -72,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:
|
||||
@ -110,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}.",
|
||||
@ -124,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,
|
||||
)
|
||||
)
|
||||
|
||||
@ -132,38 +170,13 @@ 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 = Signer(MAILBOX_SECRET)
|
||||
s = TimestampSigner(MAILBOX_SECRET)
|
||||
mailbox_id_signed = s.sign(str(mailbox.id)).decode()
|
||||
verification_url = (
|
||||
URL + "/dashboard/mailbox_verify" + f"?mailbox_id={mailbox_id_signed}"
|
||||
@ -188,11 +201,11 @@ def send_verification_email(user, mailbox):
|
||||
|
||||
@dashboard_bp.route("/mailbox_verify")
|
||||
def mailbox_verify():
|
||||
s = Signer(MAILBOX_SECRET)
|
||||
s = TimestampSigner(MAILBOX_SECRET)
|
||||
mailbox_id = request.args.get("mailbox_id")
|
||||
|
||||
try:
|
||||
r_id = int(s.unsign(mailbox_id))
|
||||
r_id = int(s.unsign(mailbox_id, max_age=900))
|
||||
except Exception:
|
||||
flash("Invalid link. Please delete and re-add your mailbox", "error")
|
||||
return redirect(url_for("dashboard.mailbox_route"))
|
||||
|
@ -4,7 +4,7 @@ from email_validator import validate_email, EmailNotValidError
|
||||
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 Signer
|
||||
from itsdangerous import TimestampSigner
|
||||
from wtforms import validators
|
||||
from wtforms.fields.html5 import EmailField
|
||||
|
||||
@ -210,7 +210,7 @@ def mailbox_detail_route(mailbox_id):
|
||||
|
||||
|
||||
def verify_mailbox_change(user, mailbox, new_email):
|
||||
s = Signer(MAILBOX_SECRET)
|
||||
s = TimestampSigner(MAILBOX_SECRET)
|
||||
mailbox_id_signed = s.sign(str(mailbox.id)).decode()
|
||||
verification_url = (
|
||||
f"{URL}/dashboard/mailbox/confirm_change?mailbox_id={mailbox_id_signed}"
|
||||
@ -262,11 +262,11 @@ def cancel_mailbox_change_route(mailbox_id):
|
||||
|
||||
@dashboard_bp.route("/mailbox/confirm_change")
|
||||
def mailbox_confirm_change_route():
|
||||
s = Signer(MAILBOX_SECRET)
|
||||
s = TimestampSigner(MAILBOX_SECRET)
|
||||
signed_mailbox_id = request.args.get("mailbox_id")
|
||||
|
||||
try:
|
||||
mailbox_id = int(s.unsign(signed_mailbox_id))
|
||||
mailbox_id = int(s.unsign(signed_mailbox_id, max_age=900))
|
||||
except Exception:
|
||||
flash("Invalid link", "error")
|
||||
return redirect(url_for("dashboard.index"))
|
||||
|
@ -5,6 +5,7 @@ from app.dashboard.base import dashboard_bp
|
||||
from app.dashboard.views.enter_sudo import sudo_required
|
||||
from app.db import Session
|
||||
from app.models import RecoveryCode
|
||||
from app.utils import CSRFValidationForm
|
||||
|
||||
|
||||
@dashboard_bp.route("/mfa_cancel", methods=["GET", "POST"])
|
||||
@ -15,8 +16,13 @@ def mfa_cancel():
|
||||
flash("you don't have MFA enabled", "warning")
|
||||
return redirect(url_for("dashboard.index"))
|
||||
|
||||
csrf_form = CSRFValidationForm()
|
||||
|
||||
# user cancels TOTP
|
||||
if request.method == "POST":
|
||||
if not csrf_form.validate():
|
||||
flash("Invalid request", "warning")
|
||||
return redirect(request.url)
|
||||
current_user.enable_otp = False
|
||||
current_user.otp_secret = None
|
||||
Session.commit()
|
||||
@ -28,4 +34,4 @@ def mfa_cancel():
|
||||
flash("TOTP is now disabled", "warning")
|
||||
return redirect(url_for("dashboard.index"))
|
||||
|
||||
return render_template("dashboard/mfa_cancel.html")
|
||||
return render_template("dashboard/mfa_cancel.html", csrf_form=csrf_form)
|
||||
|
@ -2,7 +2,10 @@ import re
|
||||
|
||||
from flask import render_template, request, redirect, url_for, flash
|
||||
from flask_login import login_required, current_user
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, validators
|
||||
|
||||
from app import parallel_limiter
|
||||
from app.config import MAX_NB_SUBDOMAIN
|
||||
from app.dashboard.base import dashboard_bp
|
||||
from app.errors import SubdomainInTrashError
|
||||
@ -13,8 +16,18 @@ from app.models import CustomDomain, Mailbox, SLDomain
|
||||
_SUBDOMAIN_PATTERN = r"[0-9a-z-]{1,}"
|
||||
|
||||
|
||||
class NewSubdomainForm(FlaskForm):
|
||||
domain = StringField(
|
||||
"domain", validators=[validators.DataRequired(), validators.Length(max=64)]
|
||||
)
|
||||
subdomain = StringField(
|
||||
"subdomain", validators=[validators.DataRequired(), validators.Length(max=64)]
|
||||
)
|
||||
|
||||
|
||||
@dashboard_bp.route("/subdomain", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@parallel_limiter.lock(only_when=lambda: request.method == "POST")
|
||||
def subdomain_route():
|
||||
if not current_user.subdomain_is_available():
|
||||
flash("Unknown error, redirect to the home page", "error")
|
||||
@ -26,9 +39,13 @@ def subdomain_route():
|
||||
).all()
|
||||
|
||||
errors = {}
|
||||
new_subdomain_form = NewSubdomainForm()
|
||||
|
||||
if request.method == "POST":
|
||||
if request.form.get("form-name") == "create":
|
||||
if not new_subdomain_form.validate():
|
||||
flash("Invalid new subdomain", "warning")
|
||||
return redirect(url_for("dashboard.subdomain_route"))
|
||||
if not current_user.is_premium():
|
||||
flash("Only premium plan can add subdomain", "warning")
|
||||
return redirect(request.url)
|
||||
@ -39,8 +56,8 @@ def subdomain_route():
|
||||
)
|
||||
return redirect(request.url)
|
||||
|
||||
subdomain = request.form.get("subdomain").lower().strip()
|
||||
domain = request.form.get("domain").lower().strip()
|
||||
subdomain = new_subdomain_form.subdomain.data.lower().strip()
|
||||
domain = new_subdomain_form.domain.data.lower().strip()
|
||||
|
||||
if len(subdomain) < 3:
|
||||
flash("Subdomain must have at least 3 characters", "error")
|
||||
@ -108,4 +125,5 @@ def subdomain_route():
|
||||
sl_domains=sl_domains,
|
||||
errors=errors,
|
||||
subdomains=subdomains,
|
||||
new_subdomain_form=new_subdomain_form,
|
||||
)
|
||||
|
@ -42,9 +42,11 @@ class UnsubscribeLink:
|
||||
class UnsubscribeEncoder:
|
||||
@staticmethod
|
||||
def encode(
|
||||
action: UnsubscribeAction, data: Union[int, UnsubscribeOriginalData]
|
||||
action: UnsubscribeAction,
|
||||
data: Union[int, UnsubscribeOriginalData],
|
||||
force_web: bool = False,
|
||||
) -> UnsubscribeLink:
|
||||
if config.UNSUBSCRIBER:
|
||||
if config.UNSUBSCRIBER and not force_web:
|
||||
return UnsubscribeLink(UnsubscribeEncoder.encode_mailto(action, data), True)
|
||||
return UnsubscribeLink(UnsubscribeEncoder.encode_url(action, data), False)
|
||||
|
||||
|
@ -49,7 +49,7 @@ class UnsubscribeHandler:
|
||||
return status.E507
|
||||
mailbox = Mailbox.get_by(email=envelope.mail_from)
|
||||
if not mailbox:
|
||||
LOG.w("Unknown mailbox %s", msg[headers.SUBJECT])
|
||||
LOG.w("Unknown mailbox %s", envelope.mail_from)
|
||||
return status.E507
|
||||
|
||||
if unsub_data.action == UnsubscribeAction.DisableAlias:
|
||||
|
@ -27,13 +27,15 @@ def send_newsletter_to_user(newsletter, user) -> (bool, str):
|
||||
comm_alias_id = comm_alias.id
|
||||
|
||||
unsubscribe_oneclick = unsubscribe_link
|
||||
if via_email:
|
||||
if via_email and comm_alias_id > -1:
|
||||
unsubscribe_oneclick = UnsubscribeEncoder.encode(
|
||||
UnsubscribeAction.DisableAlias, comm_alias_id
|
||||
)
|
||||
UnsubscribeAction.DisableAlias,
|
||||
comm_alias_id,
|
||||
force_web=True,
|
||||
).link
|
||||
|
||||
send_email(
|
||||
comm_alias.email,
|
||||
comm_email,
|
||||
newsletter.subject,
|
||||
text_template.render(
|
||||
user=user,
|
||||
|
@ -7,7 +7,7 @@ from app.session import RedisSessionStore
|
||||
|
||||
def initialize_redis_services(app: flask.Flask, redis_url: str):
|
||||
|
||||
if redis_url.startswith("redis://"):
|
||||
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)
|
||||
set_redis_concurrent_lock(storage)
|
||||
|
@ -387,7 +387,7 @@ Input:
|
||||
|
||||
- `Authentication` header that contains the api key
|
||||
- (Optional but recommended) `hostname` passed in query string
|
||||
- (Optional) mode: either `uuid` or `word`. By default, use the user setting when creating new random alias.
|
||||
- (Optional) mode: either `uuid` or `word` passed in query string. By default, use the user setting when creating new random alias.
|
||||
- Request Message Body in json (`Content-Type` is `application/json`)
|
||||
- (Optional) note: alias note
|
||||
|
||||
@ -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:
|
||||
|
||||
|
@ -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")
|
||||
|
@ -3552,7 +3552,6 @@ impute
|
||||
inaner
|
||||
inborn
|
||||
inbred
|
||||
incest
|
||||
inched
|
||||
inches
|
||||
incing
|
||||
|
@ -1494,7 +1494,6 @@ youth
|
||||
pressure
|
||||
submitted
|
||||
boston
|
||||
incest
|
||||
debt
|
||||
keywords
|
||||
medium
|
||||
@ -45883,7 +45882,6 @@ yue
|
||||
piu
|
||||
oligo
|
||||
chairpersons
|
||||
incesto
|
||||
spca
|
||||
zapper
|
||||
materialized
|
||||
@ -69506,7 +69504,6 @@ shaadi
|
||||
lovehoney
|
||||
austrians
|
||||
annemarie
|
||||
incesttaboo
|
||||
fml
|
||||
craves
|
||||
teleportation
|
||||
@ -77183,7 +77180,6 @@ schwul
|
||||
objectivist
|
||||
aftershocks
|
||||
ornette
|
||||
incestuous
|
||||
antic
|
||||
worland
|
||||
abed
|
||||
@ -78300,7 +78296,6 @@ acheive
|
||||
pochette
|
||||
mutcd
|
||||
kirschner
|
||||
incestquest
|
||||
tras
|
||||
babor
|
||||
shirin
|
||||
@ -82745,7 +82740,6 @@ snagging
|
||||
viviun
|
||||
iniquities
|
||||
oav
|
||||
inceststories
|
||||
incinerated
|
||||
ornstein
|
||||
matc
|
||||
@ -84958,7 +84952,6 @@ repartee
|
||||
pharmacie
|
||||
skus
|
||||
lyttelton
|
||||
inceste
|
||||
barska
|
||||
comida
|
||||
ciated
|
||||
@ -93685,7 +93678,6 @@ amundson
|
||||
tinta
|
||||
withholds
|
||||
wfn
|
||||
incestcartoons
|
||||
westpoint
|
||||
cancelable
|
||||
houseplant
|
||||
@ -118454,7 +118446,6 @@ jtr
|
||||
zeilenga
|
||||
arenaria
|
||||
pazza
|
||||
incests
|
||||
upmann
|
||||
jezabel
|
||||
dowlnoad
|
||||
@ -119737,7 +119728,6 @@ therrien
|
||||
spysweeper
|
||||
psrc
|
||||
polgar
|
||||
incestgrrl
|
||||
dunnville
|
||||
speeders
|
||||
redraws
|
||||
@ -176740,7 +176730,6 @@ cebas
|
||||
tenebril
|
||||
rcsdiff
|
||||
leclercq
|
||||
incestual
|
||||
gouse
|
||||
anga
|
||||
peats
|
||||
@ -279833,7 +279822,6 @@ freshies
|
||||
ceravolo
|
||||
caespitosa
|
||||
streeet
|
||||
incestincest
|
||||
huwag
|
||||
disordering
|
||||
burdur
|
||||
@ -296614,7 +296602,6 @@ outwits
|
||||
oblog
|
||||
mulqueen
|
||||
menck
|
||||
incestlinks
|
||||
imputable
|
||||
guandong
|
||||
gorgan
|
||||
@ -316826,7 +316813,6 @@ wartung
|
||||
portinatx
|
||||
orfeon
|
||||
observar
|
||||
incesticide
|
||||
herro
|
||||
didt
|
||||
comosus
|
||||
@ -322556,7 +322542,6 @@ manorhaven
|
||||
lounsbery
|
||||
linuxtracker
|
||||
liberales
|
||||
incestos
|
||||
haramayn
|
||||
greyer
|
||||
goflo
|
||||
|
@ -149803,11 +149803,6 @@ incessant
|
||||
incessantly
|
||||
incessantness
|
||||
incession
|
||||
incest
|
||||
incests
|
||||
incestuous
|
||||
incestuously
|
||||
incestuousness
|
||||
incgrporate
|
||||
inch
|
||||
inchain
|
||||
@ -204633,9 +204628,6 @@ nonincandescent
|
||||
nonincandescently
|
||||
nonincarnate
|
||||
nonincarnated
|
||||
nonincestuous
|
||||
nonincestuously
|
||||
nonincestuousness
|
||||
nonincident
|
||||
nonincidental
|
||||
nonincidentally
|
||||
@ -344408,8 +344400,6 @@ unincarnated
|
||||
unincensed
|
||||
uninceptive
|
||||
uninceptively
|
||||
unincestuous
|
||||
unincestuously
|
||||
uninchoative
|
||||
unincidental
|
||||
unincidentally
|
||||
@ -370100,4 +370090,4 @@ zwinglianism
|
||||
zwinglianist
|
||||
zwitter
|
||||
zwitterion
|
||||
zwitterionic
|
||||
zwitterionic
|
||||
|
@ -43,6 +43,7 @@
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<form method="post">
|
||||
{{ csrf_form.csrf_token }}
|
||||
<input type="hidden" name="form-name" value="delete">
|
||||
<input type="hidden" name="api-key-id" value="{{ api_key.id }}">
|
||||
<span class="card-link btn btn-link float-right text-danger delete-api-key">Delete</span>
|
||||
@ -57,6 +58,7 @@
|
||||
{% if api_keys|length > 0 %}
|
||||
|
||||
<form method="post">
|
||||
{{ csrf_form.csrf_token }}
|
||||
<input type="hidden" name="form-name" value="delete-all">
|
||||
<span class="delete btn btn-outline-danger delete-all-api-keys float-right">
|
||||
Delete All <i class="fe fe-trash"></i>
|
||||
@ -66,7 +68,7 @@
|
||||
{% endif %}
|
||||
<hr />
|
||||
<form method="post">
|
||||
{{ new_api_key_form.csrf_token }}
|
||||
{{ csrf_form.csrf_token }}
|
||||
<input type="hidden" name="form-name" value="create">
|
||||
<h2 class="h4">New API Key</h2>
|
||||
{{ new_api_key_form.name(class="form-control", placeholder="Chrome") }}
|
||||
|
@ -25,17 +25,18 @@
|
||||
<div class="alert alert-primary collapse {% if mailboxes|length == 1 %} show{% endif %}"
|
||||
id="howtouse"
|
||||
role="alert">
|
||||
A <em>mailbox</em> is just another personal email address. When creating a new alias, you could choose the
|
||||
A <em>mailbox</em> is just another personal email address. When creating a new alias, you could choose
|
||||
the
|
||||
mailbox that <em>owns</em> this alias, i.e:
|
||||
<br />
|
||||
<br/>
|
||||
- all emails sent to this alias will be forwarded to this mailbox
|
||||
<br />
|
||||
<br/>
|
||||
- from this mailbox, you can reply/send emails from the alias.
|
||||
<br />
|
||||
<br />
|
||||
<br/>
|
||||
<br/>
|
||||
When you signed up, a mailbox is automatically created with your email <b>{{ current_user.email }}</b>
|
||||
<br />
|
||||
<br />
|
||||
<br/>
|
||||
<br/>
|
||||
The mailbox doesn't have to be your email: it can be your friend's email
|
||||
if you want to create aliases for your buddy.
|
||||
</div>
|
||||
@ -74,11 +75,12 @@
|
||||
</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">
|
||||
Created {{ mailbox.created_at | dt }}
|
||||
<br />
|
||||
<br/>
|
||||
<span class="font-weight-bold">{{ mailbox.nb_alias() }}</span> aliases.
|
||||
<br />
|
||||
<br/>
|
||||
</h6>
|
||||
<a href="{{ url_for('dashboard.mailbox_detail_route', mailbox_id=mailbox.id) }}">Edit ➡</a>
|
||||
<a href="{{ url_for('dashboard.mailbox_detail_route', mailbox_id=mailbox.id) }}">Edit
|
||||
➡</a>
|
||||
</div>
|
||||
<div class="card-footer p-0">
|
||||
<div class="row">
|
||||
@ -89,7 +91,7 @@
|
||||
{{ csrf_form.csrf_token }}
|
||||
<input type="hidden" name="form-name" value="set-default">
|
||||
<input type="hidden" class="mailbox" value="{{ mailbox.email }}">
|
||||
<input type="hidden" name="mailbox-id" value="{{ mailbox.id }}">
|
||||
<input type="hidden" name="mailbox_id" value="{{ mailbox.id }}">
|
||||
<button class="card-link btn btn-link {% if mailbox.id == current_user.default_mailbox_id %} disabled{% endif %}">
|
||||
Set As Default Mailbox
|
||||
</button>
|
||||
@ -98,10 +100,24 @@
|
||||
{% endif %}
|
||||
<div class="col">
|
||||
<form method="post">
|
||||
{{ csrf_form.csrf_token }}
|
||||
{{ delete_mailbox_form.csrf_token }}
|
||||
<input type="hidden" name="form-name" value="delete">
|
||||
<input type="hidden" class="mailbox" value="{{ mailbox.email }}">
|
||||
<input type="hidden" name="mailbox-id" value="{{ mailbox.id }}">
|
||||
<input type="hidden" name="mailbox_id" value="{{ mailbox.id }}">
|
||||
<select hidden name="transfer_mailbox_id" value="">
|
||||
<option value="-1">
|
||||
Delete my aliases
|
||||
</option>
|
||||
{% for mailbox_opt in mailboxes %}
|
||||
|
||||
{% if mailbox_opt.verified and mailbox_opt.id != mailbox.id %}
|
||||
|
||||
<option value="{{ mailbox_opt.id }}">
|
||||
{{ mailbox_opt.email }}
|
||||
</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<span class="card-link btn btn-link text-danger float-right delete-mailbox {% if mailbox.id == current_user.default_mailbox_id %} disabled{% endif %}">
|
||||
Delete
|
||||
</span>
|
||||
@ -128,31 +144,39 @@
|
||||
{% block script %}
|
||||
|
||||
<script>
|
||||
$(".delete-mailbox").on("click", function (e) {
|
||||
let mailbox = $(this).parent().find(".mailbox").val();
|
||||
$(".delete-mailbox").on("click", function (e) {
|
||||
let mailbox = $(this).parent().find(".mailbox").val();
|
||||
|
||||
let that = $(this);
|
||||
let message = `All aliases owned by this mailbox <b>${mailbox}</b> will be also deleted, ` +
|
||||
" please confirm.";
|
||||
let new_mailboxes = $(this).parent().find("select[name='transfer_mailbox_id']").find("option")
|
||||
let inputOptions = new_mailboxes.map((index, option) => { return {["value"]: option.value, ["text"]: option.text}}).toArray()
|
||||
|
||||
bootbox.confirm({
|
||||
message: message,
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Yes, delete it',
|
||||
className: 'btn-danger'
|
||||
},
|
||||
cancel: {
|
||||
label: 'Cancel',
|
||||
className: 'btn-outline-primary'
|
||||
}
|
||||
},
|
||||
callback: function (result) {
|
||||
if (result) {
|
||||
that.closest("form").submit();
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
let that = $(this);
|
||||
let message = `All aliases owned by the mailbox <b>${mailbox}</b> will be also deleted.<br>` +
|
||||
"You can choose to transfer them to a different mailbox:<br><br>";
|
||||
|
||||
bootbox.prompt({
|
||||
title: '<b>Delete Mailbox</b>',
|
||||
message: message,
|
||||
value: ["-1"],
|
||||
inputType: 'select',
|
||||
inputOptions: inputOptions,
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Yes, delete it',
|
||||
className: 'btn-danger'
|
||||
},
|
||||
cancel: {
|
||||
label: 'Cancel',
|
||||
className: 'btn-outline-primary mr-auto'
|
||||
}
|
||||
},
|
||||
callback: function (result) {
|
||||
if (result) {
|
||||
that.closest("form").find("select[name='transfer_mailbox_id']").val(result)
|
||||
that.closest("form").submit();
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -12,6 +12,7 @@
|
||||
or use WebAuthn (FIDO).
|
||||
</div>
|
||||
<form method="post">
|
||||
{{ csrf_form.csrf_token }}
|
||||
<button class="btn btn-danger mt-2">Disable TOTP</button>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -177,10 +177,6 @@ $30/year
|
||||
<i class="fe fe-external-link"></i>
|
||||
</a>
|
||||
<hr />
|
||||
For other payment options, please send us an email at
|
||||
<a href="mailto:hi@simplelogin.io">hi@simplelogin.io</a>
|
||||
.
|
||||
<br />
|
||||
If you have bought a coupon, please go to the
|
||||
<a href="{{ url_for('dashboard.coupon_route') }}">coupon page</a>
|
||||
to apply the coupon code.
|
||||
|
@ -38,7 +38,7 @@
|
||||
Handy when you need to quickly give out an email address, for example on a phone call, in a meeting or just
|
||||
anywhere you want.
|
||||
<br />
|
||||
After choosing a subdomain, simply use <b>anything@my-subdomain.simplelogin.co</b>
|
||||
After choosing a subdomain, simply use <b>anything@my-subdomain.simplelogin.com</b>
|
||||
next time you need an alias:
|
||||
it'll be <b>automatically created</b> the first time it receives an email.
|
||||
<br />
|
||||
@ -72,6 +72,7 @@
|
||||
<div class="card-body">
|
||||
<h2 class="h4 mb-1">New Subdomain</h2>
|
||||
<form method="post" class="mt-2" data-parsley-validate>
|
||||
{{ new_subdomain_form.csrf_token }}
|
||||
<input type="hidden" name="form-name" value="create">
|
||||
<div class="form-group">
|
||||
<label>Subdomain</label>
|
||||
|
@ -9,8 +9,7 @@
|
||||
{% endcall %}
|
||||
|
||||
{% call text() %}
|
||||
Please contact us at
|
||||
<a href="mailto:hi@simplelogin.io">hi@simplelogin.io</a>
|
||||
Please <a href="https://app.simplelogin.io/dashboard/support">contact us</a>
|
||||
to renew your subscription.
|
||||
{% endcall %}
|
||||
|
||||
|
@ -2,4 +2,6 @@
|
||||
|
||||
{% block content %}
|
||||
Your subscription will end on {{ manual_sub.end_at.format("YYYY-MM-DD") }}
|
||||
|
||||
Please contact us on https://app.simplelogin.io/dashboard/support to renew your subscription.
|
||||
{% endblock %}
|
||||
|
@ -6,6 +6,7 @@
|
||||
{{ render_text("You recently requested to change mailbox <b>"+ mailbox_email +"</b> to <b>" + mailbox_new_email + "</b>.") }}
|
||||
{{ render_text("To confirm, please click on the button below.") }}
|
||||
{{ render_button("Confirm mailbox change", link) }}
|
||||
{{ render_text("This email will only be valid for the next 15 minutes.") }}
|
||||
{{ render_text('Thanks,
|
||||
<br />
|
||||
SimpleLogin Team.') }}
|
||||
|
@ -8,4 +8,6 @@ You recently requested to change mailbox {{mailbox_email}} to {{mailbox_new_emai
|
||||
To confirm, please click on this link:
|
||||
|
||||
{{link}}
|
||||
|
||||
This link will only be valid during the next 15 minutes.
|
||||
{% endblock %}
|
||||
|
@ -6,6 +6,7 @@
|
||||
{{ render_text("You have added <b>"+ mailbox_email +"</b> as an additional mailbox.") }}
|
||||
{{ render_text("To confirm, please click on the button below.") }}
|
||||
{{ render_button("Confirm mailbox", link) }}
|
||||
{{ render_text("This email will only be valid for the next 15 minutes.") }}
|
||||
{{ render_text('Thanks,
|
||||
<br />
|
||||
SimpleLogin Team.') }}
|
||||
|
@ -8,4 +8,6 @@ You have added {{mailbox_email}} as an additional mailbox.
|
||||
To confirm, please click on this link:
|
||||
|
||||
{{link}}
|
||||
|
||||
This link will only be valid during the next 15 minutes.
|
||||
{% endblock %}
|
||||
|
90
app/tests/jobs/test_delete_mailbox_job.py
Normal file
90
app/tests/jobs/test_delete_mailbox_job.py
Normal file
@ -0,0 +1,90 @@
|
||||
from sqlalchemy_utils.types.arrow import arrow
|
||||
|
||||
from app.config import JOB_DELETE_MAILBOX
|
||||
from app.db import Session
|
||||
from app.mail_sender import mail_sender
|
||||
from app.models import Alias, Mailbox, Job, AliasMailbox
|
||||
from job_runner import delete_mailbox_job
|
||||
from tests.utils import create_new_user, random_email
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_delete_mailbox_transfer_mailbox_primary(flask_client):
|
||||
user = create_new_user()
|
||||
m1 = Mailbox.create(
|
||||
user_id=user.id, email=random_email(), verified=True, flush=True
|
||||
)
|
||||
m2 = Mailbox.create(
|
||||
user_id=user.id, email=random_email(), verified=True, flush=True
|
||||
)
|
||||
|
||||
alias_id = Alias.create_new(user, "prefix", mailbox_id=m1.id).id
|
||||
AliasMailbox.create(alias_id=alias_id, mailbox_id=m2.id)
|
||||
job = Job.create(
|
||||
name=JOB_DELETE_MAILBOX,
|
||||
payload={"mailbox_id": m1.id, "transfer_mailbox_id": m2.id},
|
||||
run_at=arrow.now(),
|
||||
commit=True,
|
||||
)
|
||||
Session.commit()
|
||||
delete_mailbox_job(job)
|
||||
alias = Alias.get(alias_id)
|
||||
assert alias.mailbox_id == m2.id
|
||||
assert len(alias._mailboxes) == 0
|
||||
mails_sent = mail_sender.get_stored_emails()
|
||||
assert len(mails_sent) == 1
|
||||
assert str(mails_sent[0].msg).find("alias have been transferred") > -1
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_delete_mailbox_transfer_mailbox_in_list(flask_client):
|
||||
user = create_new_user()
|
||||
m1 = Mailbox.create(
|
||||
user_id=user.id, email=random_email(), verified=True, flush=True
|
||||
)
|
||||
m2 = Mailbox.create(
|
||||
user_id=user.id, email=random_email(), verified=True, flush=True
|
||||
)
|
||||
m3 = Mailbox.create(
|
||||
user_id=user.id, email=random_email(), verified=True, flush=True
|
||||
)
|
||||
|
||||
alias_id = Alias.create_new(user, "prefix", mailbox_id=m1.id).id
|
||||
AliasMailbox.create(alias_id=alias_id, mailbox_id=m2.id)
|
||||
job = Job.create(
|
||||
name=JOB_DELETE_MAILBOX,
|
||||
payload={"mailbox_id": m2.id, "transfer_mailbox_id": m3.id},
|
||||
run_at=arrow.now(),
|
||||
commit=True,
|
||||
)
|
||||
Session.commit()
|
||||
delete_mailbox_job(job)
|
||||
alias = Alias.get(alias_id)
|
||||
assert alias.mailbox_id == m1.id
|
||||
assert len(alias._mailboxes) == 1
|
||||
assert alias._mailboxes[0].id == m3.id
|
||||
mails_sent = mail_sender.get_stored_emails()
|
||||
assert len(mails_sent) == 1
|
||||
assert str(mails_sent[0].msg).find("alias have been transferred") > -1
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_delete_mailbox_no_transfer(flask_client):
|
||||
user = create_new_user()
|
||||
m1 = Mailbox.create(
|
||||
user_id=user.id, email=random_email(), verified=True, flush=True
|
||||
)
|
||||
|
||||
alias_id = Alias.create_new(user, "prefix", mailbox_id=m1.id).id
|
||||
job = Job.create(
|
||||
name=JOB_DELETE_MAILBOX,
|
||||
payload={"mailbox_id": m1.id},
|
||||
run_at=arrow.now(),
|
||||
commit=True,
|
||||
)
|
||||
Session.commit()
|
||||
delete_mailbox_job(job)
|
||||
assert Alias.get(alias_id) is None
|
||||
mails_sent = mail_sender.get_stored_emails()
|
||||
assert len(mails_sent) == 1
|
||||
assert str(mails_sent[0].msg).find("along with its aliases have been deleted") > -1
|
Loading…
x
Reference in New Issue
Block a user