Add code verification for creating mailboxes (#1725)
* Add code verification for creating mailboxes * Added validation checks * Use exceptions * Added delete to the mailbox utils * Fix test * Update package.lock * Fix delete error --------- Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
This commit is contained in:
parent
5ddbca05b2
commit
a5e7da10dd
|
@ -7,15 +7,14 @@ from flask import request
|
|||
|
||||
from app.api.base import api_bp, require_api_auth
|
||||
from app.config import JOB_DELETE_MAILBOX
|
||||
from app.dashboard.views.mailbox import send_verification_email
|
||||
from app.dashboard.views.mailbox_detail import verify_mailbox_change
|
||||
from app.db import Session
|
||||
from app.email_utils import (
|
||||
mailbox_already_used,
|
||||
email_can_be_used_as_mailbox,
|
||||
is_valid_email,
|
||||
)
|
||||
from app.log import LOG
|
||||
from app.mailbox_utils import create_mailbox_and_send_verification, MailboxError
|
||||
from app.models import Mailbox, Job
|
||||
from app.utils import sanitize_email
|
||||
|
||||
|
@ -46,29 +45,14 @@ def create_mailbox():
|
|||
|
||||
if not user.is_premium():
|
||||
return jsonify(error=f"Only premium plan can add additional mailbox"), 400
|
||||
|
||||
if not is_valid_email(mailbox_email):
|
||||
return jsonify(error=f"{mailbox_email} invalid"), 400
|
||||
elif mailbox_already_used(mailbox_email, user):
|
||||
return jsonify(error=f"{mailbox_email} already used"), 400
|
||||
elif not email_can_be_used_as_mailbox(mailbox_email):
|
||||
try:
|
||||
mailbox = create_mailbox_and_send_verification(user, mailbox_email)
|
||||
return (
|
||||
jsonify(
|
||||
error=f"{mailbox_email} cannot be used. Please note a mailbox cannot "
|
||||
f"be a disposable email address"
|
||||
),
|
||||
400,
|
||||
)
|
||||
else:
|
||||
new_mailbox = Mailbox.create(email=mailbox_email, user_id=user.id)
|
||||
Session.commit()
|
||||
|
||||
send_verification_email(user, new_mailbox)
|
||||
|
||||
return (
|
||||
jsonify(mailbox_to_dict(new_mailbox)),
|
||||
jsonify(mailbox_to_dict(mailbox)),
|
||||
201,
|
||||
)
|
||||
except MailboxError as e:
|
||||
return jsonify(error=str(e)), 400
|
||||
|
||||
|
||||
@api_bp.route("/mailboxes/<int:mailbox_id>", methods=["DELETE"])
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
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
|
||||
|
@ -7,18 +6,17 @@ 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.config import MAILBOX_SECRET
|
||||
from app.dashboard.base import dashboard_bp
|
||||
from app.db import Session
|
||||
from app.email_utils import (
|
||||
email_can_be_used_as_mailbox,
|
||||
mailbox_already_used,
|
||||
render,
|
||||
send_email,
|
||||
is_valid_email,
|
||||
)
|
||||
from app.log import LOG
|
||||
from app.models import Mailbox, Job
|
||||
from app.mailbox_utils import (
|
||||
create_mailbox_and_send_verification,
|
||||
set_mailbox_verified,
|
||||
MailboxError,
|
||||
delete_mailbox,
|
||||
)
|
||||
from app.models import Mailbox
|
||||
from app.utils import CSRFValidationForm
|
||||
|
||||
|
||||
|
@ -54,53 +52,16 @@ def mailbox_route():
|
|||
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("Invalid mailbox. Refresh the page", "warning")
|
||||
try:
|
||||
mailbox = delete_mailbox(
|
||||
current_user,
|
||||
delete_mailbox_form.mailbox_id.data,
|
||||
delete_mailbox_form.transfer_mailbox_id.data,
|
||||
)
|
||||
except MailboxError as e:
|
||||
flash(str(e), "error")
|
||||
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.", "error"
|
||||
)
|
||||
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.",
|
||||
"error",
|
||||
)
|
||||
return redirect(url_for("dashboard.mailbox_route"))
|
||||
|
||||
if not transfer_mailbox.verified:
|
||||
flash("Your new mailbox is not verified", "error")
|
||||
return redirect(url_for("dashboard.mailbox_route"))
|
||||
|
||||
# Schedule delete account job
|
||||
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,
|
||||
"transfer_mailbox_id": transfer_mailbox_id
|
||||
if transfer_mailbox_id > 0
|
||||
else None,
|
||||
},
|
||||
run_at=arrow.now(),
|
||||
commit=True,
|
||||
)
|
||||
|
||||
flash(
|
||||
f"Mailbox {mailbox.email} scheduled for deletion."
|
||||
f"You will receive a confirmation email when the deletion is finished",
|
||||
|
@ -137,37 +98,23 @@ def mailbox_route():
|
|||
if not current_user.is_premium():
|
||||
flash("Only premium plan can add additional mailbox", "warning")
|
||||
return redirect(url_for("dashboard.mailbox_route"))
|
||||
|
||||
if new_mailbox_form.validate():
|
||||
mailbox_email = (
|
||||
new_mailbox_form.email.data.lower().strip().replace(" ", "")
|
||||
mailbox_email = new_mailbox_form.email.data.lower().strip().replace(" ", "")
|
||||
try:
|
||||
new_mailbox = create_mailbox_and_send_verification(
|
||||
current_user, mailbox_email
|
||||
)
|
||||
|
||||
if not is_valid_email(mailbox_email):
|
||||
flash(f"{mailbox_email} invalid", "error")
|
||||
elif mailbox_already_used(mailbox_email, current_user):
|
||||
flash(f"{mailbox_email} already used", "error")
|
||||
elif not email_can_be_used_as_mailbox(mailbox_email):
|
||||
flash(f"You cannot use {mailbox_email}.", "error")
|
||||
else:
|
||||
new_mailbox = Mailbox.create(
|
||||
email=mailbox_email, user_id=current_user.id
|
||||
)
|
||||
Session.commit()
|
||||
|
||||
send_verification_email(current_user, new_mailbox)
|
||||
|
||||
flash(
|
||||
f"You are going to receive an email to confirm {mailbox_email}.",
|
||||
"success",
|
||||
)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"dashboard.mailbox_detail_route",
|
||||
mailbox_id=new_mailbox.id,
|
||||
)
|
||||
flash(
|
||||
f"You are going to receive an email to confirm {mailbox_email}.",
|
||||
"success",
|
||||
)
|
||||
return redirect(
|
||||
url_for(
|
||||
"dashboard.mailbox_detail_route",
|
||||
mailbox_id=new_mailbox.id,
|
||||
)
|
||||
)
|
||||
except MailboxError as e:
|
||||
flash(str(e), "error")
|
||||
|
||||
return render_template(
|
||||
"dashboard/mailbox.html",
|
||||
|
@ -178,30 +125,6 @@ def mailbox_route():
|
|||
)
|
||||
|
||||
|
||||
def send_verification_email(user, mailbox):
|
||||
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}"
|
||||
)
|
||||
send_email(
|
||||
mailbox.email,
|
||||
f"Please confirm your mailbox {mailbox.email}",
|
||||
render(
|
||||
"transactional/verify-mailbox.txt.jinja2",
|
||||
user=user,
|
||||
link=verification_url,
|
||||
mailbox_email=mailbox.email,
|
||||
),
|
||||
render(
|
||||
"transactional/verify-mailbox.html",
|
||||
user=user,
|
||||
link=verification_url,
|
||||
mailbox_email=mailbox.email,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dashboard_bp.route("/mailbox_verify")
|
||||
def mailbox_verify():
|
||||
s = TimestampSigner(MAILBOX_SECRET)
|
||||
|
@ -218,8 +141,7 @@ def mailbox_verify():
|
|||
flash("Invalid link", "error")
|
||||
return redirect(url_for("dashboard.mailbox_route"))
|
||||
|
||||
mailbox.verified = True
|
||||
Session.commit()
|
||||
set_mailbox_verified(mailbox)
|
||||
|
||||
LOG.d("Mailbox %s is verified", mailbox)
|
||||
|
||||
|
|
161
app/mailbox_utils.py
Normal file
161
app/mailbox_utils.py
Normal file
|
@ -0,0 +1,161 @@
|
|||
import secrets
|
||||
from typing import Optional
|
||||
|
||||
import arrow
|
||||
from itsdangerous import TimestampSigner
|
||||
|
||||
from app import config
|
||||
from app.config import JOB_DELETE_MAILBOX
|
||||
from app.db import Session
|
||||
from app.email_utils import (
|
||||
is_valid_email,
|
||||
mailbox_already_used,
|
||||
email_can_be_used_as_mailbox,
|
||||
send_email,
|
||||
render,
|
||||
)
|
||||
from app.log import LOG
|
||||
from app.models import User, Mailbox, Job
|
||||
|
||||
MAX_MAILBOX_VERIFICATION_TRIES = 3
|
||||
|
||||
|
||||
class MailboxError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _attach_new_validation_code_for_mailbox(mailbox: Mailbox) -> Mailbox:
|
||||
mailbox.verification_code = secrets.randbelow(10**6)
|
||||
mailbox.verification_expiration = arrow.utcnow().shift(minutes=15)
|
||||
Session.commit()
|
||||
return mailbox
|
||||
|
||||
|
||||
def _send_verification_email(user, mailbox):
|
||||
s = TimestampSigner(config.MAILBOX_SECRET)
|
||||
mailbox_id_signed = s.sign(str(mailbox.id)).decode()
|
||||
verification_url = (
|
||||
config.URL + "/dashboard/mailbox_verify" + f"?mailbox_id={mailbox_id_signed}"
|
||||
)
|
||||
send_email(
|
||||
mailbox.email,
|
||||
f"Please confirm your mailbox {mailbox.email}",
|
||||
render(
|
||||
"transactional/verify-mailbox.txt.jinja2",
|
||||
user=user,
|
||||
link=verification_url,
|
||||
mailbox=mailbox,
|
||||
),
|
||||
render(
|
||||
"transactional/verify-mailbox.html",
|
||||
user=user,
|
||||
link=verification_url,
|
||||
mailbox=mailbox,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def create_mailbox_and_send_verification(
|
||||
user: User, email: str, use_code_validation: bool = False
|
||||
) -> Mailbox:
|
||||
mailbox_email = email.lower().strip().replace(" ", "")
|
||||
|
||||
if not is_valid_email(mailbox_email):
|
||||
raise MailboxError(f"Invalid address {mailbox_email}")
|
||||
elif mailbox_already_used(mailbox_email, user):
|
||||
raise MailboxError(f"Mailbox {mailbox_email} already exists")
|
||||
elif not email_can_be_used_as_mailbox(mailbox_email):
|
||||
raise MailboxError(f"Invalid address {mailbox_email}")
|
||||
|
||||
new_mailbox = Mailbox.create(email=mailbox_email, user_id=user.id)
|
||||
if use_code_validation:
|
||||
new_mailbox.verification_tries = 0
|
||||
new_mailbox = _attach_new_validation_code_for_mailbox(new_mailbox)
|
||||
Session.commit()
|
||||
_send_verification_email(user, new_mailbox)
|
||||
|
||||
return new_mailbox
|
||||
|
||||
|
||||
def send_new_verification_to_mailbox(user: User, mailbox: Mailbox):
|
||||
if mailbox.verified:
|
||||
return
|
||||
if mailbox.verification_tries > MAX_MAILBOX_VERIFICATION_TRIES:
|
||||
mailbox.delete()
|
||||
Session.commit()
|
||||
return
|
||||
mailbox = _attach_new_validation_code_for_mailbox(mailbox)
|
||||
Session.commit()
|
||||
_send_verification_email(user, mailbox)
|
||||
|
||||
|
||||
def set_mailbox_verified(mailbox: Mailbox) -> Mailbox:
|
||||
mailbox.verified = True
|
||||
mailbox.verification_code = None
|
||||
mailbox.verification_expiration = None
|
||||
mailbox.verification_tries = 0
|
||||
Session.commit()
|
||||
return mailbox
|
||||
|
||||
|
||||
def verify_mailbox_with_code(user: User, mailbox_id: int, code: str) -> Mailbox:
|
||||
mailbox = Mailbox.get_by(id=mailbox_id)
|
||||
if mailbox is None:
|
||||
raise MailboxError("Invalid mailbox")
|
||||
if mailbox.user_id != user.id:
|
||||
raise MailboxError("Invalid mailbox")
|
||||
if mailbox.verified:
|
||||
return mailbox
|
||||
if mailbox.verification_expiration < arrow.utcnow():
|
||||
mailbox = _attach_new_validation_code_for_mailbox(mailbox)
|
||||
_send_verification_email(user, mailbox)
|
||||
raise MailboxError("Code has expired. A new one has been sent")
|
||||
if mailbox.verification_code != code:
|
||||
mailbox.verification_tries += 1
|
||||
if mailbox.verification_tries >= MAX_MAILBOX_VERIFICATION_TRIES:
|
||||
mailbox.delete()
|
||||
Session.commit()
|
||||
raise MailboxError("Too many tries")
|
||||
raise MailboxError("Invalid code")
|
||||
|
||||
return set_mailbox_verified(mailbox)
|
||||
|
||||
|
||||
def delete_mailbox(
|
||||
user: User, mailbox_id: int, transfer_mailbox_id: Optional[int] = None
|
||||
) -> Mailbox:
|
||||
mailbox = Mailbox.get(mailbox_id)
|
||||
if mailbox is None:
|
||||
raise MailboxError("Invalid mailbox")
|
||||
if mailbox.user_id != user.id:
|
||||
raise MailboxError("Invalid mailbox")
|
||||
if mailbox.id == user.default_mailbox_id:
|
||||
raise MailboxError("You cannot delete your default mailbox")
|
||||
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 != user.id:
|
||||
raise MailboxError("You must transfer the aliases to a mailbox you own")
|
||||
|
||||
if transfer_mailbox.id == mailbox.id:
|
||||
raise MailboxError(
|
||||
"You can not transfer the aliases to the mailbox you want to delete"
|
||||
)
|
||||
|
||||
if not transfer_mailbox.verified:
|
||||
raise MailboxError("Your new mailbox is not verified")
|
||||
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,
|
||||
"transfer_mailbox_id": transfer_mailbox_id
|
||||
if transfer_mailbox_id is not None
|
||||
else None,
|
||||
},
|
||||
run_at=arrow.now(),
|
||||
commit=True,
|
||||
)
|
||||
return mailbox
|
|
@ -2517,6 +2517,11 @@ class Mailbox(Base, ModelMixin):
|
|||
disabled = sa.Column(sa.Boolean, default=False, nullable=False, server_default="0")
|
||||
|
||||
generic_subject = sa.Column(sa.String(78), nullable=True)
|
||||
verification_code = sa.Column(sa.String(128), nullable=True)
|
||||
verification_expiration = sa.Column(ArrowType, nullable=True)
|
||||
verification_tries = sa.Column(
|
||||
sa.Integer, nullable=False, default=0, server_default="0"
|
||||
)
|
||||
|
||||
__table_args__ = (sa.UniqueConstraint("user_id", "email", name="uq_mailbox_user"),)
|
||||
|
||||
|
|
33
migrations/versions/2023_050911_f5f2e08fbd2e_.py
Normal file
33
migrations/versions/2023_050911_f5f2e08fbd2e_.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: f5f2e08fbd2e
|
||||
Revises: 2634b41f54db
|
||||
Create Date: 2023-05-09 11:52:41.100335
|
||||
|
||||
"""
|
||||
import sqlalchemy_utils
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'f5f2e08fbd2e'
|
||||
down_revision = '2634b41f54db'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('mailbox', sa.Column('verification_code', sa.String(length=128), nullable=True))
|
||||
op.add_column('mailbox', sa.Column('verification_expiration', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True))
|
||||
op.add_column('mailbox', sa.Column('verification_tries', sa.Integer(), server_default='0', nullable=False))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('mailbox', 'verification_tries')
|
||||
op.drop_column('mailbox', 'verification_expiration')
|
||||
op.drop_column('mailbox', 'verification_code')
|
||||
# ### end Alembic commands ###
|
120
static/package-lock.json
generated
vendored
120
static/package-lock.json
generated
vendored
|
@ -1,169 +1,123 @@
|
|||
{
|
||||
"name": "simplelogin",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "simplelogin",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/browser": "^5.30.0",
|
||||
"bootbox": "^5.5.3",
|
||||
"font-awesome": "^4.7.0",
|
||||
"htmx.org": "^1.6.1",
|
||||
"intro.js": "^2.9.3",
|
||||
"multiple-select": "^1.5.2",
|
||||
"parsleyjs": "^2.9.2",
|
||||
"qrious": "^4.0.2",
|
||||
"toastr": "^2.1.4",
|
||||
"vue": "^2.6.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/browser": {
|
||||
"dependencies": {
|
||||
"@sentry/browser": {
|
||||
"version": "5.30.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-5.30.0.tgz",
|
||||
"integrity": "sha512-rOb58ZNVJWh1VuMuBG1mL9r54nZqKeaIlwSlvzJfc89vyfd7n6tQ1UXMN383QBz/MS5H5z44Hy5eE+7pCrYAfw==",
|
||||
"dependencies": {
|
||||
"requires": {
|
||||
"@sentry/core": "5.30.0",
|
||||
"@sentry/types": "5.30.0",
|
||||
"@sentry/utils": "5.30.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/core": {
|
||||
"@sentry/core": {
|
||||
"version": "5.30.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.30.0.tgz",
|
||||
"integrity": "sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg==",
|
||||
"dependencies": {
|
||||
"requires": {
|
||||
"@sentry/hub": "5.30.0",
|
||||
"@sentry/minimal": "5.30.0",
|
||||
"@sentry/types": "5.30.0",
|
||||
"@sentry/utils": "5.30.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/hub": {
|
||||
"@sentry/hub": {
|
||||
"version": "5.30.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.30.0.tgz",
|
||||
"integrity": "sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ==",
|
||||
"dependencies": {
|
||||
"requires": {
|
||||
"@sentry/types": "5.30.0",
|
||||
"@sentry/utils": "5.30.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/minimal": {
|
||||
"@sentry/minimal": {
|
||||
"version": "5.30.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.30.0.tgz",
|
||||
"integrity": "sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw==",
|
||||
"dependencies": {
|
||||
"requires": {
|
||||
"@sentry/hub": "5.30.0",
|
||||
"@sentry/types": "5.30.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/types": {
|
||||
"@sentry/types": {
|
||||
"version": "5.30.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz",
|
||||
"integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
"integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw=="
|
||||
},
|
||||
"node_modules/@sentry/utils": {
|
||||
"@sentry/utils": {
|
||||
"version": "5.30.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.30.0.tgz",
|
||||
"integrity": "sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==",
|
||||
"dependencies": {
|
||||
"requires": {
|
||||
"@sentry/types": "5.30.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/bootbox": {
|
||||
"bootbox": {
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/bootbox/-/bootbox-5.5.3.tgz",
|
||||
"integrity": "sha512-B4mnm1DYgNHzoNtD7I0L/fixqvya4EEQy5bFF/yNmGI2Eq3WwVVwdfWf3hoF8KS+EaV4f0uIMqtxB1EAZwZPhQ==",
|
||||
"peerDependencies": {
|
||||
"bootstrap": "^3.1.0 || ^4.4.0",
|
||||
"jquery": "^3.5.1",
|
||||
"popper.js": "^1.16.0"
|
||||
}
|
||||
"integrity": "sha512-B4mnm1DYgNHzoNtD7I0L/fixqvya4EEQy5bFF/yNmGI2Eq3WwVVwdfWf3hoF8KS+EaV4f0uIMqtxB1EAZwZPhQ=="
|
||||
},
|
||||
"node_modules/font-awesome": {
|
||||
"font-awesome": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz",
|
||||
"integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM=",
|
||||
"engines": {
|
||||
"node": ">=0.10.3"
|
||||
}
|
||||
"integrity": "sha512-U6kGnykA/6bFmg1M/oT9EkFeIYv7JlX3bozwQJWiiLz6L0w3F5vBVPxHlwyX/vtNq1ckcpRKOB9f2Qal/VtFpg=="
|
||||
},
|
||||
"node_modules/htmx.org": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.6.1.tgz",
|
||||
"integrity": "sha512-i+1k5ee2eFWaZbomjckyrDjUpa3FMDZWufatUSBmmsjXVksn89nsXvr1KLGIdAajiz+ZSL7TE4U/QaZVd2U2sA=="
|
||||
"htmx.org": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.7.0.tgz",
|
||||
"integrity": "sha512-wIQ3yNq7yiLTm+6BhV7Z8qKKTzEQv9xN/I4QsN5FvdGi69SNWTsSMlhH69HPa1rpZ8zSq1A/e7gTbTySxliP8g=="
|
||||
},
|
||||
"node_modules/intro.js": {
|
||||
"intro.js": {
|
||||
"version": "2.9.3",
|
||||
"resolved": "https://registry.npmjs.org/intro.js/-/intro.js-2.9.3.tgz",
|
||||
"integrity": "sha512-hC+EXWnEuJeA3CveGMat3XHePd2iaXNFJIVfvJh2E9IzBMGLTlhWvPIVHAgKlOpO4lNayCxEqzr4N02VmHFr9Q=="
|
||||
},
|
||||
"node_modules/jquery": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
|
||||
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg=="
|
||||
"jquery": {
|
||||
"version": "3.6.4",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.4.tgz",
|
||||
"integrity": "sha512-v28EW9DWDFpzcD9O5iyJXg3R3+q+mET5JhnjJzQUZMHOv67bpSIHq81GEYpPNZHG+XXHsfSme3nxp/hndKEcsQ=="
|
||||
},
|
||||
"node_modules/multiple-select": {
|
||||
"multiple-select": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/multiple-select/-/multiple-select-1.5.2.tgz",
|
||||
"integrity": "sha512-sTNNRrjnTtB1b1+HTKcjQ/mjWY7Gvigo9F3C/3oTQCTFEpYzwaRYFPRAOu2SogfA1hEfyJTXjyS1VAbanJMsmA==",
|
||||
"peerDependencies": {
|
||||
"jquery": "1.9.1 - 3"
|
||||
}
|
||||
"integrity": "sha512-sTNNRrjnTtB1b1+HTKcjQ/mjWY7Gvigo9F3C/3oTQCTFEpYzwaRYFPRAOu2SogfA1hEfyJTXjyS1VAbanJMsmA=="
|
||||
},
|
||||
"node_modules/parsleyjs": {
|
||||
"parsleyjs": {
|
||||
"version": "2.9.2",
|
||||
"resolved": "https://registry.npmjs.org/parsleyjs/-/parsleyjs-2.9.2.tgz",
|
||||
"integrity": "sha512-DKS2XXTjEUZ1BJWUzgXAr+550kFBZrom2WYweubqdV7WzdNC1hjOajZDfeBPoAZMkXumJPlB3v37IKatbiW8zQ==",
|
||||
"dependencies": {
|
||||
"requires": {
|
||||
"jquery": ">=1.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qrious": {
|
||||
"qrious": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/qrious/-/qrious-4.0.2.tgz",
|
||||
"integrity": "sha512-xWPJIrK1zu5Ypn898fBp8RHkT/9ibquV2Kv24S/JY9VYEhMBMKur1gHVsOiNUh7PHP9uCgejjpZUHUIXXKoU/g=="
|
||||
},
|
||||
"node_modules/toastr": {
|
||||
"toastr": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/toastr/-/toastr-2.1.4.tgz",
|
||||
"integrity": "sha1-i0O+ZPudDEFIcURvLbjoyk6V8YE=",
|
||||
"dependencies": {
|
||||
"integrity": "sha512-LIy77F5n+sz4tefMmFOntcJ6HL0Fv3k1TDnNmFZ0bU/GcvIIfy6eG2v7zQmMiYgaalAiUv75ttFrPn5s0gyqlA==",
|
||||
"requires": {
|
||||
"jquery": ">=1.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"vue": {
|
||||
"version": "2.6.14",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz",
|
||||
"integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ=="
|
||||
|
|
|
@ -3,9 +3,15 @@
|
|||
{% block content %}
|
||||
|
||||
{{ render_text("Hi") }}
|
||||
{{ 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("You have added <b>"+ mailbox.email +"</b> as an additional mailbox.") }}
|
||||
{% if mailbox.verification_code is not none -%}
|
||||
|
||||
{{ render_text("To confirm, please write this code to verify") }}
|
||||
{{ render_text("<strong>{}</strong>".format(mailbox.verification_code.zfill(6))) }}
|
||||
{% else -%}
|
||||
{{ render_text("To confirm, please click on the button below.") }}
|
||||
{{ render_button("Confirm mailbox", link) }}
|
||||
{% endif %}
|
||||
{{ render_text("This email will only be valid for the next 15 minutes.") }}
|
||||
{{ render_text('Thanks,
|
||||
<br />
|
||||
|
|
|
@ -3,11 +3,17 @@
|
|||
{% block content %}
|
||||
Hi
|
||||
|
||||
You have added {{mailbox_email}} as an additional mailbox.
|
||||
You have added {{mailbox.email}} as an additional mailbox.
|
||||
|
||||
{% if mailbox.verification_code is not none -%}
|
||||
To confirm, please write this code to verify:
|
||||
|
||||
{{ mailbox.verification_code.zfill(6) }}
|
||||
{% else -%}
|
||||
To confirm, please click on this link:
|
||||
|
||||
{{link}}
|
||||
{% endif %}
|
||||
|
||||
This link will only be valid during the next 15 minutes.
|
||||
This email will only be valid during the next 15 minutes.
|
||||
{% endblock %}
|
||||
|
|
|
@ -28,7 +28,7 @@ def test_create_mailbox(flask_client):
|
|||
)
|
||||
|
||||
assert r.status_code == 400
|
||||
assert r.json == {"error": "gmail.com invalid"}
|
||||
assert r.json == {"error": "Invalid address gmail.com"}
|
||||
|
||||
|
||||
def test_create_mailbox_fail_for_free_user(flask_client):
|
||||
|
|
165
tests/test_mailbox_utils.py
Normal file
165
tests/test_mailbox_utils.py
Normal file
|
@ -0,0 +1,165 @@
|
|||
import arrow
|
||||
import pytest
|
||||
from sqlalchemy import desc
|
||||
|
||||
from app import config
|
||||
from app.config import JOB_DELETE_MAILBOX
|
||||
from app.db import Session
|
||||
from app.mail_sender import mail_sender
|
||||
from app.mailbox_utils import (
|
||||
create_mailbox_and_send_verification,
|
||||
verify_mailbox_with_code,
|
||||
MailboxError,
|
||||
delete_mailbox,
|
||||
)
|
||||
from app.models import Mailbox, Job
|
||||
from tests.utils import create_new_user, random_email
|
||||
|
||||
test_user = None
|
||||
|
||||
|
||||
def setup_module():
|
||||
global test_user
|
||||
config.SKIP_MX_LOOKUP_ON_CHECK = True
|
||||
test_user = create_new_user()
|
||||
|
||||
|
||||
def teardown_module():
|
||||
config.SKIP_MX_LOOKUP_ON_CHECK = False
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_mailbox_creation_sends_link_verification():
|
||||
mailbox = create_mailbox_and_send_verification(test_user, random_email())
|
||||
assert mailbox is not None
|
||||
assert mailbox.verification_code is None
|
||||
assert mailbox.verification_expiration is None
|
||||
assert mailbox.verification_tries == 0
|
||||
mails = mail_sender.get_stored_emails()
|
||||
assert len(mails) == 1
|
||||
assert "link" in str(mails[0].msg)
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_mailbox_creation_sends_code_verification():
|
||||
mailbox = create_mailbox_and_send_verification(test_user, random_email(), True)
|
||||
assert mailbox is not None
|
||||
assert mailbox.verification_code is not None
|
||||
assert mailbox.verification_expiration is not None
|
||||
assert mailbox.verification_tries == 0
|
||||
mails = mail_sender.get_stored_emails()
|
||||
assert len(mails) == 1
|
||||
assert mailbox.verification_code in str(mails[0].msg)
|
||||
|
||||
|
||||
def test_verification_with_code():
|
||||
mailbox = create_mailbox_and_send_verification(test_user, random_email(), True)
|
||||
mailbox_id = mailbox.id
|
||||
verified_mbox = verify_mailbox_with_code(
|
||||
test_user, mailbox_id, mailbox.verification_code
|
||||
)
|
||||
assert verified_mbox.id == mailbox_id
|
||||
assert verified_mbox.verified
|
||||
assert verified_mbox.verification_code is None
|
||||
assert verified_mbox.verification_expiration is None
|
||||
assert verified_mbox.verification_tries == 0
|
||||
|
||||
|
||||
def test_fail_verification_with_code():
|
||||
mailbox = create_mailbox_and_send_verification(test_user, random_email(), True)
|
||||
mailbox_id = mailbox.id
|
||||
with pytest.raises(MailboxError):
|
||||
verify_mailbox_with_code(test_user, mailbox_id, "INVALID")
|
||||
mbox = Mailbox.get_by(id=mailbox_id)
|
||||
assert mbox.id == mailbox_id
|
||||
assert not mbox.verified
|
||||
assert mbox.verification_code is not None
|
||||
assert mbox.verification_expiration is not None
|
||||
assert mbox.verification_tries == 1
|
||||
|
||||
|
||||
def test_fail_verification_with_invalid_mbox_id():
|
||||
with pytest.raises(MailboxError):
|
||||
verify_mailbox_with_code(test_user, 99999999, "INVALID")
|
||||
|
||||
|
||||
def test_verification_with_verified_mbox_is_ok():
|
||||
mailbox = Mailbox.create(email=random_email(), user_id=test_user.id, verified=True)
|
||||
Session.commit()
|
||||
verified_mbox = verify_mailbox_with_code(test_user, mailbox.id, "INVALID")
|
||||
assert verified_mbox.id == mailbox.id
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_validate_expired_sends_a_new_email():
|
||||
mailbox = Mailbox.create(
|
||||
email=random_email(),
|
||||
user_id=test_user.id,
|
||||
verification_expiration=arrow.utcnow().shift(days=-1),
|
||||
)
|
||||
Session.commit()
|
||||
with pytest.raises(MailboxError):
|
||||
verify_mailbox_with_code(test_user, mailbox.id, mailbox.verification_code)
|
||||
sent_emails = mail_sender.get_stored_emails()
|
||||
assert len(sent_emails) == 1
|
||||
assert mailbox.verification_code in str(sent_emails[0].msg)
|
||||
|
||||
|
||||
def test_delete_mailbox_without_transfer():
|
||||
mailbox = Mailbox.create(email=random_email(), user_id=test_user.id, verified=True)
|
||||
Session.commit()
|
||||
delete_mailbox(test_user, mailbox.id)
|
||||
job = (
|
||||
Session.query(Job)
|
||||
.filter_by(name=JOB_DELETE_MAILBOX)
|
||||
.order_by(desc(Job.id))
|
||||
.first()
|
||||
)
|
||||
assert job.payload["mailbox_id"] == mailbox.id
|
||||
assert job.payload["transfer_mailbox_id"] is None
|
||||
|
||||
|
||||
def test_delete_mailbox_with_transfer():
|
||||
mailbox = Mailbox.create(email=random_email(), user_id=test_user.id, verified=True)
|
||||
transfer_mailbox = Mailbox.create(
|
||||
email=random_email(), user_id=test_user.id, verified=True
|
||||
)
|
||||
Session.commit()
|
||||
delete_mailbox(test_user, mailbox.id, transfer_mailbox.id)
|
||||
job = (
|
||||
Session.query(Job)
|
||||
.filter_by(name=JOB_DELETE_MAILBOX)
|
||||
.order_by(desc(Job.id))
|
||||
.first()
|
||||
)
|
||||
assert job.payload["mailbox_id"] == mailbox.id
|
||||
assert job.payload["transfer_mailbox_id"] == transfer_mailbox.id
|
||||
|
||||
|
||||
def test_cannot_delete_primary_mailbox():
|
||||
with pytest.raises(MailboxError):
|
||||
delete_mailbox(test_user, test_user.default_mailbox_id)
|
||||
|
||||
|
||||
def test_cannot_delete_another_users_mailbox():
|
||||
other_user = create_new_user()
|
||||
mailbox = Mailbox.create(email=random_email(), user_id=test_user.id, verified=True)
|
||||
with pytest.raises(MailboxError):
|
||||
delete_mailbox(other_user, mailbox.id)
|
||||
|
||||
|
||||
def test_cannot_delete_transfer_being_the_same():
|
||||
mailbox = Mailbox.create(email=random_email(), user_id=test_user.id, verified=True)
|
||||
with pytest.raises(MailboxError):
|
||||
delete_mailbox(test_user, mailbox.id, mailbox.id)
|
||||
|
||||
|
||||
def test_cannot_transfer_to_another_user_mailbox():
|
||||
other_user = create_new_user()
|
||||
mailbox = Mailbox.create(email=random_email(), user_id=test_user.id, verified=True)
|
||||
transfer_mailbox = Mailbox.create(
|
||||
email=random_email(), user_id=other_user.id, verified=True
|
||||
)
|
||||
Session.commit()
|
||||
with pytest.raises(MailboxError):
|
||||
delete_mailbox(test_user, mailbox.id, transfer_mailbox.id)
|
Loading…
Reference in a new issue