Revert "Add code verification for creating mailboxes (#1725)" (#1727)

This reverts commit a5e7da10dd.

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
This commit is contained in:
Adrià Casajús 2023-05-09 18:04:04 +02:00 committed by GitHub
parent a5e7da10dd
commit 6dfb6bb3e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 221 additions and 457 deletions

View File

@ -7,14 +7,15 @@ 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
@ -45,14 +46,29 @@ def create_mailbox():
if not user.is_premium():
return jsonify(error=f"Only premium plan can add additional mailbox"), 400
try:
mailbox = create_mailbox_and_send_verification(user, mailbox_email)
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):
return (
jsonify(mailbox_to_dict(mailbox)),
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)),
201,
)
except MailboxError as e:
return jsonify(error=str(e)), 400
@api_bp.route("/mailboxes/<int:mailbox_id>", methods=["DELETE"])

View File

@ -1,3 +1,4 @@
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
@ -6,17 +7,18 @@ from wtforms import validators, IntegerField
from wtforms.fields.html5 import EmailField
from app import parallel_limiter
from app.config import MAILBOX_SECRET
from app.config import MAILBOX_SECRET, URL, JOB_DELETE_MAILBOX
from app.dashboard.base import dashboard_bp
from app.db import Session
from app.log import LOG
from app.mailbox_utils import (
create_mailbox_and_send_verification,
set_mailbox_verified,
MailboxError,
delete_mailbox,
from app.email_utils import (
email_can_be_used_as_mailbox,
mailbox_already_used,
render,
send_email,
is_valid_email,
)
from app.models import Mailbox
from app.log import LOG
from app.models import Mailbox, Job
from app.utils import CSRFValidationForm
@ -52,16 +54,53 @@ def mailbox_route():
if not delete_mailbox_form.validate():
flash("Invalid request", "warning")
return redirect(request.url)
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")
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")
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",
@ -98,23 +137,37 @@ 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"))
mailbox_email = new_mailbox_form.email.data.lower().strip().replace(" ", "")
try:
new_mailbox = create_mailbox_and_send_verification(
current_user, mailbox_email
if new_mailbox_form.validate():
mailbox_email = (
new_mailbox_form.email.data.lower().strip().replace(" ", "")
)
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,
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,
)
)
)
except MailboxError as e:
flash(str(e), "error")
return render_template(
"dashboard/mailbox.html",
@ -125,6 +178,30 @@ 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)
@ -141,7 +218,8 @@ def mailbox_verify():
flash("Invalid link", "error")
return redirect(url_for("dashboard.mailbox_route"))
set_mailbox_verified(mailbox)
mailbox.verified = True
Session.commit()
LOG.d("Mailbox %s is verified", mailbox)

View File

@ -1,161 +0,0 @@
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

View File

@ -2517,11 +2517,6 @@ 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"),)

View File

@ -1,33 +0,0 @@
"""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
View File

@ -1,123 +1,169 @@
{
"name": "simplelogin",
"version": "1.0.0",
"lockfileVersion": 1,
"lockfileVersion": 3,
"requires": true,
"dependencies": {
"@sentry/browser": {
"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": {
"version": "5.30.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-5.30.0.tgz",
"integrity": "sha512-rOb58ZNVJWh1VuMuBG1mL9r54nZqKeaIlwSlvzJfc89vyfd7n6tQ1UXMN383QBz/MS5H5z44Hy5eE+7pCrYAfw==",
"requires": {
"dependencies": {
"@sentry/core": "5.30.0",
"@sentry/types": "5.30.0",
"@sentry/utils": "5.30.0",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=6"
}
},
"@sentry/core": {
"node_modules/@sentry/core": {
"version": "5.30.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.30.0.tgz",
"integrity": "sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg==",
"requires": {
"dependencies": {
"@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"
}
},
"@sentry/hub": {
"node_modules/@sentry/hub": {
"version": "5.30.0",
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.30.0.tgz",
"integrity": "sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ==",
"requires": {
"dependencies": {
"@sentry/types": "5.30.0",
"@sentry/utils": "5.30.0",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=6"
}
},
"@sentry/minimal": {
"node_modules/@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==",
"requires": {
"dependencies": {
"@sentry/hub": "5.30.0",
"@sentry/types": "5.30.0",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=6"
}
},
"@sentry/types": {
"node_modules/@sentry/types": {
"version": "5.30.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz",
"integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw=="
"integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw==",
"engines": {
"node": ">=6"
}
},
"@sentry/utils": {
"node_modules/@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==",
"requires": {
"dependencies": {
"@sentry/types": "5.30.0",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=6"
}
},
"bootbox": {
"node_modules/bootbox": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/bootbox/-/bootbox-5.5.3.tgz",
"integrity": "sha512-B4mnm1DYgNHzoNtD7I0L/fixqvya4EEQy5bFF/yNmGI2Eq3WwVVwdfWf3hoF8KS+EaV4f0uIMqtxB1EAZwZPhQ=="
"integrity": "sha512-B4mnm1DYgNHzoNtD7I0L/fixqvya4EEQy5bFF/yNmGI2Eq3WwVVwdfWf3hoF8KS+EaV4f0uIMqtxB1EAZwZPhQ==",
"peerDependencies": {
"bootstrap": "^3.1.0 || ^4.4.0",
"jquery": "^3.5.1",
"popper.js": "^1.16.0"
}
},
"font-awesome": {
"node_modules/font-awesome": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz",
"integrity": "sha512-U6kGnykA/6bFmg1M/oT9EkFeIYv7JlX3bozwQJWiiLz6L0w3F5vBVPxHlwyX/vtNq1ckcpRKOB9f2Qal/VtFpg=="
"integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM=",
"engines": {
"node": ">=0.10.3"
}
},
"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/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=="
},
"intro.js": {
"node_modules/intro.js": {
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/intro.js/-/intro.js-2.9.3.tgz",
"integrity": "sha512-hC+EXWnEuJeA3CveGMat3XHePd2iaXNFJIVfvJh2E9IzBMGLTlhWvPIVHAgKlOpO4lNayCxEqzr4N02VmHFr9Q=="
},
"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/jquery": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg=="
},
"multiple-select": {
"node_modules/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=="
"integrity": "sha512-sTNNRrjnTtB1b1+HTKcjQ/mjWY7Gvigo9F3C/3oTQCTFEpYzwaRYFPRAOu2SogfA1hEfyJTXjyS1VAbanJMsmA==",
"peerDependencies": {
"jquery": "1.9.1 - 3"
}
},
"parsleyjs": {
"node_modules/parsleyjs": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/parsleyjs/-/parsleyjs-2.9.2.tgz",
"integrity": "sha512-DKS2XXTjEUZ1BJWUzgXAr+550kFBZrom2WYweubqdV7WzdNC1hjOajZDfeBPoAZMkXumJPlB3v37IKatbiW8zQ==",
"requires": {
"dependencies": {
"jquery": ">=1.8.0"
}
},
"qrious": {
"node_modules/qrious": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/qrious/-/qrious-4.0.2.tgz",
"integrity": "sha512-xWPJIrK1zu5Ypn898fBp8RHkT/9ibquV2Kv24S/JY9VYEhMBMKur1gHVsOiNUh7PHP9uCgejjpZUHUIXXKoU/g=="
},
"toastr": {
"node_modules/toastr": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/toastr/-/toastr-2.1.4.tgz",
"integrity": "sha512-LIy77F5n+sz4tefMmFOntcJ6HL0Fv3k1TDnNmFZ0bU/GcvIIfy6eG2v7zQmMiYgaalAiUv75ttFrPn5s0gyqlA==",
"requires": {
"integrity": "sha1-i0O+ZPudDEFIcURvLbjoyk6V8YE=",
"dependencies": {
"jquery": ">=1.12.0"
}
},
"tslib": {
"node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"vue": {
"node_modules/vue": {
"version": "2.6.14",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz",
"integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ=="

View File

@ -3,15 +3,9 @@
{% block content %}
{{ render_text("Hi") }}
{{ 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("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 />

View File

@ -3,17 +3,11 @@
{% 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 email will only be valid during the next 15 minutes.
This link will only be valid during the next 15 minutes.
{% endblock %}

View File

@ -28,7 +28,7 @@ def test_create_mailbox(flask_client):
)
assert r.status_code == 400
assert r.json == {"error": "Invalid address gmail.com"}
assert r.json == {"error": "gmail.com invalid"}
def test_create_mailbox_fail_for_free_user(flask_client):

View File

@ -1,165 +0,0 @@
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)