Fix: Add csrf verification to directory updates (#1358)

* Fix: Add csrf verification to directory updates

* Update templates/dashboard/directory.html

* Added csrf for delete account form

* Fix tests

* Added CSRF check for settings page

* Added csrf to batch import

* Added CSRF to alias dashboard and alias transfer

* Added csrf to contact manager

* Added csrf to mailbox

* Added csrf for mailbox detail

* Added csrf to domain detail

* Lint

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
This commit is contained in:
Adrià Casajús 2022-10-27 10:04:47 +02:00 committed by GitHub
parent 2f769b38ad
commit d324e2fa79
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 235 additions and 63 deletions

View file

@ -25,7 +25,7 @@ from app.errors import (
)
from app.log import LOG
from app.models import Alias, Contact, EmailLog, User
from app.utils import sanitize_email
from app.utils import sanitize_email, CSRFValidationForm
def email_validator():
@ -258,8 +258,12 @@ def alias_contact_manager(alias_id):
return redirect(url_for("dashboard.index"))
new_contact_form = NewContactForm()
csrf_form = CSRFValidationForm()
if request.method == "POST":
if not csrf_form.validate():
flash("Invalid request", "warning")
return redirect(request.url)
if request.form.get("form-name") == "create":
if new_contact_form.validate():
contact_address = new_contact_form.email.data.strip()
@ -323,4 +327,5 @@ def alias_contact_manager(alias_id):
query=query,
nb_contact=nb_contact,
can_create_contacts=user_can_create_contacts(current_user),
csrf_form=csrf_form,
)

View file

@ -22,6 +22,7 @@ from app.models import (
ClientUser,
)
from app.models import Mailbox
from app.utils import CSRFValidationForm
def transfer(alias, new_user, new_mailboxes: [Mailbox]):
@ -105,8 +106,12 @@ def alias_transfer_send_route(alias_id):
return redirect(url_for("dashboard.index"))
alias_transfer_url = None
csrf_form = CSRFValidationForm()
if request.method == "POST":
if not csrf_form.validate():
flash("Invalid request", "warning")
return redirect(request.url)
# generate a new transfer_token
if request.form.get("form-name") == "create":
transfer_token = f"{alias.id}.{secrets.token_urlsafe(32)}"
@ -133,6 +138,7 @@ def alias_transfer_send_route(alias_id):
alias_transfer_url=alias_transfer_url,
link_active=alias.transfer_token_expiration is not None
and alias.transfer_token_expiration > arrow.utcnow(),
csrf_form=csrf_form,
)

View file

@ -8,7 +8,7 @@ from app.dashboard.base import dashboard_bp
from app.db import Session
from app.log import LOG
from app.models import File, BatchImport, Job
from app.utils import random_string
from app.utils import random_string, CSRFValidationForm
@dashboard_bp.route("/batch_import", methods=["GET", "POST"])
@ -29,14 +29,21 @@ def batch_import_route():
user_id=current_user.id, processed=False
).all()
csrf_form = CSRFValidationForm()
if request.method == "POST":
if not csrf_form.validate():
flash("Invalid request", "warning")
redirect(request.url)
if len(batch_imports) > 10:
flash(
"You have too many imports already. Wait until some get cleaned up",
"error",
)
return render_template(
"dashboard/batch_import.html", batch_imports=batch_imports
"dashboard/batch_import.html",
batch_imports=batch_imports,
csrf_form=csrf_form,
)
alias_file = request.files["alias-file"]
@ -66,4 +73,6 @@ def batch_import_route():
return redirect(url_for("dashboard.batch_import_route"))
return render_template("dashboard/batch_import.html", batch_imports=batch_imports)
return render_template(
"dashboard/batch_import.html", batch_imports=batch_imports, csrf_form=csrf_form
)

View file

@ -1,6 +1,7 @@
import arrow
from flask import flash, redirect, url_for, request, render_template
from flask_login import login_required, current_user
from flask_wtf import FlaskForm
from app.config import JOB_DELETE_ACCOUNT
from app.dashboard.base import dashboard_bp
@ -9,11 +10,21 @@ from app.log import LOG
from app.models import Subscription, Job
class DeleteDirForm(FlaskForm):
pass
@dashboard_bp.route("/delete_account", methods=["GET", "POST"])
@login_required
@sudo_required
def delete_account():
delete_form = DeleteDirForm()
if request.method == "POST" and request.form.get("form-name") == "delete-account":
if not delete_form.validate():
flash("Invalid request", "warning")
return render_template(
"dashboard/delete_account.html", delete_form=delete_form
)
sub: Subscription = current_user.get_paddle_subscription()
# user who has canceled can also re-subscribe
if sub and not sub.cancelled:
@ -36,6 +47,4 @@ def delete_account():
)
return redirect(url_for("dashboard.setting"))
return render_template(
"dashboard/delete_account.html",
)
return render_template("dashboard/delete_account.html", delete_form=delete_form)

View file

@ -1,7 +1,13 @@
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 wtforms import (
StringField,
validators,
SelectMultipleField,
BooleanField,
IntegerField,
)
from app.config import (
EMAIL_DOMAIN,
@ -21,6 +27,22 @@ class NewDirForm(FlaskForm):
)
class ToggleDirForm(FlaskForm):
directory_id = IntegerField(validators=[validators.DataRequired()])
directory_enabled = BooleanField(validators=[])
class UpdateDirForm(FlaskForm):
directory_id = IntegerField(validators=[validators.DataRequired()])
mailbox_ids = SelectMultipleField(
validators=[validators.DataRequired()], validate_choice=False, choices=[]
)
class DeleteDirForm(FlaskForm):
directory_id = IntegerField(validators=[validators.DataRequired()])
@dashboard_bp.route("/directory", methods=["GET", "POST"])
@login_required
def directory():
@ -33,54 +55,68 @@ def directory():
mailboxes = current_user.mailboxes()
new_dir_form = NewDirForm()
toggle_dir_form = ToggleDirForm()
update_dir_form = UpdateDirForm()
update_dir_form.mailbox_ids.choices = [
(str(mailbox.id), str(mailbox.id)) for mailbox in mailboxes
]
delete_dir_form = DeleteDirForm()
if request.method == "POST":
if request.form.get("form-name") == "delete":
dir_id = request.form.get("dir-id")
dir = Directory.get(dir_id)
if not delete_dir_form.validate():
flash(f"Invalid request", "warning")
return redirect(url_for("dashboard.directory"))
dir_obj = Directory.get(delete_dir_form.directory_id.data)
if not dir:
if not dir_obj:
flash("Unknown error. Refresh the page", "warning")
return redirect(url_for("dashboard.directory"))
elif dir.user_id != current_user.id:
elif dir_obj.user_id != current_user.id:
flash("You cannot delete this directory", "warning")
return redirect(url_for("dashboard.directory"))
name = dir.name
Directory.delete(dir_id)
name = dir_obj.name
Directory.delete(dir_obj.id)
Session.commit()
flash(f"Directory {name} has been deleted", "success")
return redirect(url_for("dashboard.directory"))
if request.form.get("form-name") == "toggle-directory":
dir_id = request.form.get("dir-id")
dir = Directory.get(dir_id)
if not toggle_dir_form.validate():
flash(f"Invalid request", "warning")
return redirect(url_for("dashboard.directory"))
dir_id = toggle_dir_form.directory_id.data
dir_obj = Directory.get(dir_id)
if not dir or dir.user_id != current_user.id:
if not dir_obj or dir_obj.user_id != current_user.id:
flash("Unknown error. Refresh the page", "warning")
return redirect(url_for("dashboard.directory"))
if request.form.get("dir-status") == "on":
dir.disabled = False
flash(f"On-the-fly is enabled for {dir.name}", "success")
if toggle_dir_form.directory_enabled.data:
dir_obj.disabled = False
flash(f"On-the-fly is enabled for {dir_obj.name}", "success")
else:
dir.disabled = True
flash(f"On-the-fly is disabled for {dir.name}", "warning")
dir_obj.disabled = True
flash(f"On-the-fly is disabled for {dir_obj.name}", "warning")
Session.commit()
return redirect(url_for("dashboard.directory"))
elif request.form.get("form-name") == "update":
dir_id = request.form.get("dir-id")
dir = Directory.get(dir_id)
if not update_dir_form.validate():
flash(f"Invalid request", "warning")
return redirect(url_for("dashboard.directory"))
dir_id = update_dir_form.directory_id.data
dir_obj = Directory.get(dir_id)
if not dir or dir.user_id != current_user.id:
if not dir_obj or dir_obj.user_id != current_user.id:
flash("Unknown error. Refresh the page", "warning")
return redirect(url_for("dashboard.directory"))
mailbox_ids = request.form.getlist("mailbox_ids")
mailbox_ids = update_dir_form.mailbox_ids.data
# check if mailbox is not tempered with
mailboxes = []
for mailbox_id in mailbox_ids:
@ -99,14 +135,14 @@ def directory():
return redirect(url_for("dashboard.directory"))
# first remove all existing directory-mailboxes links
DirectoryMailbox.filter_by(directory_id=dir.id).delete()
DirectoryMailbox.filter_by(directory_id=dir_obj.id).delete()
Session.flush()
for mailbox in mailboxes:
DirectoryMailbox.create(directory_id=dir.id, mailbox_id=mailbox.id)
DirectoryMailbox.create(directory_id=dir_obj.id, mailbox_id=mailbox.id)
Session.commit()
flash(f"Directory {dir.name} has been updated", "success")
flash(f"Directory {dir_obj.name} has been updated", "success")
return redirect(url_for("dashboard.directory"))
elif request.form.get("form-name") == "create":
@ -181,6 +217,9 @@ def directory():
return render_template(
"dashboard/directory.html",
dirs=dirs,
toggle_dir_form=toggle_dir_form,
update_dir_form=update_dir_form,
delete_dir_form=delete_dir_form,
new_dir_form=new_dir_form,
mailboxes=mailboxes,
EMAIL_DOMAIN=EMAIL_DOMAIN,

View file

@ -28,7 +28,7 @@ from app.models import (
Job,
)
from app.regex_utils import regex_match
from app.utils import random_string
from app.utils import random_string, CSRFValidationForm
@dashboard_bp.route("/domains/<int:custom_domain_id>/dns", methods=["GET", "POST"])
@ -47,6 +47,7 @@ def domain_detail_dns(custom_domain_id):
spf_record = f"v=spf1 include:{EMAIL_DOMAIN} ~all"
domain_validator = CustomDomainValidation(EMAIL_DOMAIN)
csrf_form = CSRFValidationForm()
dmarc_record = "v=DMARC1; p=quarantine; pct=100; adkim=s; aspf=s"
@ -54,6 +55,9 @@ def domain_detail_dns(custom_domain_id):
mx_errors = spf_errors = dkim_errors = dmarc_errors = ownership_errors = []
if request.method == "POST":
if not csrf_form.validate():
flash("Invalid request", "warning")
return redirect(request.url)
if request.form.get("form-name") == "check-ownership":
txt_records = get_txt_record(custom_domain.domain)
@ -165,6 +169,7 @@ def domain_detail_dns(custom_domain_id):
@dashboard_bp.route("/domains/<int:custom_domain_id>/info", methods=["GET", "POST"])
@login_required
def domain_detail(custom_domain_id):
csrf_form = CSRFValidationForm()
custom_domain: CustomDomain = CustomDomain.get(custom_domain_id)
mailboxes = current_user.mailboxes()
@ -173,6 +178,9 @@ def domain_detail(custom_domain_id):
return redirect(url_for("dashboard.index"))
if request.method == "POST":
if not csrf_form.validate():
flash("Invalid request", "warning")
return redirect(request.url)
if request.form.get("form-name") == "switch-catch-all":
custom_domain.catch_all = not custom_domain.catch_all
Session.commit()
@ -301,12 +309,16 @@ def domain_detail(custom_domain_id):
@dashboard_bp.route("/domains/<int:custom_domain_id>/trash", methods=["GET", "POST"])
@login_required
def domain_detail_trash(custom_domain_id):
csrf_form = CSRFValidationForm()
custom_domain = CustomDomain.get(custom_domain_id)
if not custom_domain or custom_domain.user_id != current_user.id:
flash("You cannot see this page", "warning")
return redirect(url_for("dashboard.index"))
if request.method == "POST":
if not csrf_form.validate():
flash("Invalid request", "warning")
return redirect(request.url)
if request.form.get("form-name") == "empty-all":
DomainDeletedAlias.filter_by(domain_id=custom_domain.id).delete()
Session.commit()
@ -350,6 +362,7 @@ def domain_detail_trash(custom_domain_id):
"dashboard/domain_detail/trash.html",
domain_deleted_aliases=domain_deleted_aliases,
custom_domain=custom_domain,
csrf_form=csrf_form,
)

View file

@ -17,6 +17,7 @@ from app.models import (
EmailLog,
Contact,
)
from app.utils import CSRFValidationForm
@dataclass
@ -75,8 +76,12 @@ def index():
"highlight_alias_id must be a number, received %s",
request.args.get("highlight_alias_id"),
)
csrf_form = CSRFValidationForm()
if request.method == "POST":
if not csrf_form.validate():
flash("Invalid request", "warning")
return redirect(request.url)
if request.form.get("form-name") == "create-custom-email":
if current_user.can_create_new_alias():
return redirect(url_for("dashboard.custom_alias"))
@ -204,6 +209,7 @@ def index():
sort=sort,
filter=alias_filter,
stats=stats,
csrf_form=csrf_form,
)

View file

@ -18,6 +18,7 @@ from app.email_utils import (
)
from app.log import LOG
from app.models import Mailbox, Job
from app.utils import CSRFValidationForm
class NewMailboxForm(FlaskForm):
@ -36,8 +37,12 @@ def mailbox_route():
)
new_mailbox_form = NewMailboxForm()
csrf_form = CSRFValidationForm()
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)
@ -127,6 +132,7 @@ def mailbox_route():
"dashboard/mailbox.html",
mailboxes=mailboxes,
new_mailbox_form=new_mailbox_form,
csrf_form=csrf_form,
)

View file

@ -17,7 +17,7 @@ from app.log import LOG
from app.models import Alias, AuthorizedAddress
from app.models import Mailbox
from app.pgp_utils import PGPException, load_public_key_and_check
from app.utils import sanitize_email
from app.utils import sanitize_email, CSRFValidationForm
class ChangeEmailForm(FlaskForm):
@ -35,6 +35,7 @@ def mailbox_detail_route(mailbox_id):
return redirect(url_for("dashboard.index"))
change_email_form = ChangeEmailForm()
csrf_form = CSRFValidationForm()
if mailbox.new_email:
pending_email = mailbox.new_email
@ -42,6 +43,9 @@ def mailbox_detail_route(mailbox_id):
pending_email = None
if request.method == "POST":
if not csrf_form.validate():
flash("Invalid request", "warning")
return redirect(request.url)
if (
request.form.get("form-name") == "update-email"
and change_email_form.validate_on_submit()

View file

@ -53,7 +53,7 @@ from app.models import (
UnsubscribeBehaviourEnum,
)
from app.proton.utils import get_proton_partner, perform_proton_account_unlink
from app.utils import random_string, sanitize_email
from app.utils import random_string, sanitize_email, CSRFValidationForm
class SettingForm(FlaskForm):
@ -104,6 +104,7 @@ def setting():
form = SettingForm()
promo_form = PromoCodeForm()
change_email_form = ChangeEmailForm()
csrf_form = CSRFValidationForm()
email_change = EmailChange.get_by(user_id=current_user.id)
if email_change:
@ -112,6 +113,9 @@ def setting():
pending_email = None
if request.method == "POST":
if not csrf_form.validate():
flash("Invalid request", "warning")
return redirect(url_for("dashboard.setting"))
if request.form.get("form-name") == "update-email":
if change_email_form.validate():
# whether user can proceed with the email update
@ -395,6 +399,7 @@ def setting():
return render_template(
"dashboard/setting.html",
csrf_form=csrf_form,
form=form,
PlanEnum=PlanEnum,
SenderFormatEnum=SenderFormatEnum,
@ -477,9 +482,14 @@ def cancel_email_change():
return redirect(url_for("dashboard.setting"))
@dashboard_bp.route("/unlink_proton_account", methods=["GET", "POST"])
@dashboard_bp.route("/unlink_proton_account", methods=["POST"])
@login_required
def unlink_proton_account():
csrf_form = CSRFValidationForm()
if not csrf_form.validate():
flash("Invalid request", "warning")
return redirect(url_for("dashboard.setting"))
perform_proton_account_unlink(current_user)
flash("Your Proton account has been unlinked", "success")
return redirect(url_for("dashboard.setting"))

View file

@ -6,6 +6,7 @@ import urllib.parse
from functools import wraps
from typing import List, Optional
from flask_wtf import FlaskForm
from unidecode import unidecode
from .config import WORDS_FILE_PATH, ALLOWED_REDIRECT_DOMAINS
@ -126,3 +127,7 @@ def debug_info(func):
return ret
return wrap
class CSRFValidationForm(FlaskForm):
pass

View file

@ -80,6 +80,7 @@
<div class="col-12 col-lg-6 pt-1">
<div class="float-right d-flex">
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="search">
<input type="search"
name="query"
@ -184,6 +185,7 @@
</div>
<a href="{{ url_for('dashboard.contact_detail_route', contact_id=contact.id) }}">Edit ➡</a>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="delete">
<input type="hidden" name="contact-id" value="{{ contact.id }}">
<span class="card-link btn btn-link float-right delete-forward-email text-danger">Delete</span>

View file

@ -26,6 +26,7 @@
This transfer URL is <strong>valid for 24 hours</strong>. If it hasn't been used by then it will be automatically disabled.
</p>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="remove">
<button class="btn btn-warning mt-2">Remove alias transfer URL</button>
<div class="small-text">If you don't want to share this alias anymore, you can remove the share URL.</div>
@ -34,9 +35,10 @@
{% if link_active %}
<p class="alert alert-info">
You have an active transfer link. If you don't want to share this alias any more, please delete the link.
You have an active transfer link. If you don't want to share this alias anymore, please delete the link.
</p>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="remove">
<button class="btn btn-warning mt-2">Remove alias transfer URL</button>
</form>
@ -46,6 +48,7 @@
please create the <b>Share URL</b> 👇 and send it to the other person.
</p>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="create">
<button class="btn btn-primary mt-2">Generate a new alias transfer URL</button>
</form>

View file

@ -24,6 +24,7 @@
download>Download CSV Template</a>
<hr />
<form method="post" enctype="multipart/form-data" class="mt-4">
{{ csrf_form.csrf_token }}
<input required
type="file"
name="alias-file"

View file

@ -13,6 +13,7 @@
</div>
<form method="post">
<input type="hidden" name="form-name" value="delete-account">
{{ delete_form.csrf_token }}
<span class="delete-account btn btn-outline-danger">Delete account</span>
</form>
</div>

View file

@ -66,11 +66,12 @@
<div class="d-flex">
{{ dir.name }}
<form method="post">
{{ toggle_dir_form.csrf_token }}
<input type="hidden" name="form-name" value="toggle-directory">
<input type="hidden" name="dir-id" value="{{ dir.id }}">
{{ toggle_dir_form.directory_id( type="hidden", value=dir.id) }}
<label class="custom-switch cursor" style="padding-left: 1rem" data-toggle="tooltip" {% if dir.disabled %}
title="Enable directory on-the-fly alias creation" {% else %} title="Disable directory on-the-fly alias creation" {% endif %}>
<input type="checkbox" class="custom-switch-input" name="dir-status" {{ "" if dir.disabled else "checked" }}>
{{ toggle_dir_form.directory_enabled( class="custom-switch-input", checked=(not dir.disabled) ) }}
<span class="custom-switch-indicator"></span>
</label>
</form>
@ -92,8 +93,9 @@
<br />
{% set dir_mailboxes=dir.mailboxes %}
<form method="post" class="mt-2">
{{ update_dir_form.csrf_token }}
<input type="hidden" name="form-name" value="update">
<input type="hidden" name="dir-id" value="{{ dir.id }}">
{{ update_dir_form.directory_id( type="hidden", value=dir.id) }}
<select data-width="100%"
required
class="mailbox-select"
@ -115,9 +117,9 @@
<div class="row">
<div class="col">
<form method="post">
{{ delete_dir_form.csrf_token }}
<input type="hidden" name="form-name" value="delete">
<input type="hidden" class="dir-name" value="{{ dir.name }}">
<input type="hidden" name="dir-id" value="{{ dir.id }}">
{{ delete_dir_form.directory_id( type="hidden", value=dir.id) }}
<span class="card-link btn btn-link float-right text-danger delete-dir">Delete</span>
</form>
</div>
@ -173,18 +175,20 @@
<script>
$(".delete-dir").on("click", function (e) {
let directory = $(this).parent().find(".dir-name").val();
let directory_name = $(this).parent().find("#directory_name").val();
const unsanitizedMessage = `All aliases associated with <b>${directory}</b> directory will also be deleted. ` +
const element = document.createElement('div');
element.innerText = directory_name;
const sanitized_name = element.innerHTML;
const message = `All aliases associated with <b>${sanitized_name}</b> directory will also be deleted. ` +
`As a deleted directory can't be used by someone else, deleting a directory doesn't reset your directory quota. ` +
`Your directory quota will be {{ current_user.directory_quota }} after the deletion, ` +
" please confirm.";
const element = document.createElement('div');
element.innerText = unsanitizedMessage;
const sanitizedMessage = element.innerHTML;
bootbox.confirm({
message: sanitizedMessage,
message: message,
buttons: {
confirm: {
label: 'Yes, delete it',

View file

@ -41,6 +41,7 @@
data-clipboard-text="{{ custom_domain.get_ownership_dns_txt_value() }}">{{ custom_domain.get_ownership_dns_txt_value() }}</em>
</div>
<form method="post" action="#ownership-form">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="check-ownership">
<button type="submit" class="btn btn-primary">Verify</button>
</form>
@ -107,6 +108,7 @@
</div>
{% endfor %}
<form method="post" action="#mx-form">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="check-mx">
{% if custom_domain.verified %}
@ -180,6 +182,7 @@
</em>
</div>
<form method="post" action="#spf-form">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="check-spf">
{% if custom_domain.spf_verified %}
@ -273,6 +276,7 @@
<img src="/static/images/cloudflare-proxy.png" class="w-100">
</div>
<form method="post" action="#dkim-form">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="check-dkim">
{% if custom_domain.dkim_verified %}
@ -367,6 +371,7 @@
<br />
</div>
<form method="post" action="#dmarc-form">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="check-dmarc">
{% if custom_domain.dmarc_verified %}

View file

@ -10,6 +10,7 @@
<h3 class="mb-1">Auto create/on the fly alias</h3>
<div>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="switch-catch-all">
<label class="custom-switch cursor mt-2 pl-0" data-toggle="tooltip" {% if custom_domain.catch_all %}
title="Disable catch-all" {% else %} title="Enable catch-all" {% endif %}>
@ -41,6 +42,7 @@
</div>
{% set domain_mailboxes=custom_domain.mailboxes %}
<form method="post" class="mt-2">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="update">
<input type="hidden" name="domain-id" value="{{ custom_domain.id }}">
<div class="d-flex">
@ -73,6 +75,7 @@
</div>
<div>
<form method="post" class="form-inline">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="set-name">
<div class="form-group">
<input class="form-control mr-2"
@ -94,6 +97,7 @@
<div>Add a random prefix for this domain when creating a new alias.</div>
<div>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden"
name="form-name"
value="switch-random-prefix-generation">
@ -134,6 +138,7 @@
{% endif %}
</div>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="delete">
<span class="delete-custom-domain btn btn-danger">Delete {{ custom_domain.domain }}</span>
</form>

View file

@ -26,6 +26,7 @@
{% if domain_deleted_aliases | length > 0 %}
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="empty-all">
<button class="btn btn-outline-danger">Empty Trash</button>
<div class="small-text">
@ -42,6 +43,7 @@
<b>{{ deleted_alias.email }}</b> -
deleted {{ deleted_alias.created_at | dt }}
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="remove-single">
<input type="hidden" name="deleted-alias-id" value="{{ deleted_alias.id }}">
<button class="btn btn-sm btn-outline-warning">Remove from trash</button>

View file

@ -90,6 +90,7 @@
<div>
<div class="btn-group" role="group">
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="create-custom-email">
<button data-toggle="tooltip"
title="Create a custom alias"
@ -99,6 +100,7 @@
</form>
<div class="btn-group" role="group">
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="create-random-email">
<button data-toggle="tooltip"
title="Create a totally random alias"
@ -117,6 +119,7 @@
aria-labelledby="btnGroupDrop1">
<div>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="create-random-email">
<input type="hidden"
name="generator_scheme"
@ -126,6 +129,7 @@
</div>
<div>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="create-random-email">
<input type="hidden"
name="generator_scheme"
@ -152,6 +156,7 @@
<!-- Filter Control -->
<div class="col d-flex">
<form method="get" class="form-inline">
{{ csrf_form.csrf_token }}
<select name="sort"
onchange="this.form.submit()"
class="form-control mr-3 shadow">
@ -493,6 +498,7 @@
<i class="ml-0 dropdown-icon fe fe-share-2 text-primary"></i>
</a>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="delete-alias">
<input type="hidden" name="alias-id" value="{{ alias.id }}">
<input type="hidden" name="alias" class="alias" value="{{ alias.email }}">

View file

@ -86,6 +86,7 @@
<div class="col">
<form method="post">
{{ 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 }}">
@ -97,6 +98,7 @@
{% endif %}
<div class="col">
<form method="post">
{{ csrf_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 }}">

View file

@ -87,6 +87,7 @@
{% if mailbox.pgp_finger_print %}
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="toggle-pgp">
<label class="custom-switch cursor" style="padding-left: 1rem" data-toggle="tooltip" {% if mailbox.disable_pgp %}
title="Enable PGP" {% else %} title="Disable PGP" {% endif %}>
@ -108,6 +109,7 @@
<div class="alert alert-danger" role="alert">This feature is only available in premium plan.</div>
{% endif %}
<form method="post">
{{ csrf_form.csrf_token }}
<div class="form-group">
<label class="form-label">PGP Public Key</label>
<textarea name="pgp" {% if not current_user.is_premium() %} disabled {% endif %} class="form-control" rows=10 id="pgp-public-key" placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----">{{ mailbox.pgp_public_key or "" }}</textarea>
@ -127,6 +129,7 @@
<div class="card" {% if not mailbox.pgp_enabled() %}
disabled {% endif %}>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="generic-subject">
<div class="card-body">
<div class="card-title">
@ -167,6 +170,7 @@
<div class="card" id="spf">
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="force-spf">
<div class="card-body">
<div class="card-title">
@ -210,6 +214,7 @@
<li>
{{ authorized_address.email }}
<form method="post" action="#authorized-address" style="display: inline">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="delete-authorized-address">
<input type="hidden"
name="authorized-address-id"
@ -221,6 +226,7 @@
</ul>
{% endif %}
<form method="post" action="#authorized-address" class="form-inline">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="add-authorized-address">
<input type="email" name="email" size="50" class="form-control">
<input type="submit" class="btn btn-primary" value="Add">

View file

@ -132,6 +132,7 @@
<div class="card-title">Newsletters</div>
<div class="mb-3">We will occasionally send you emails with new feature announcements.</div>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="notification-preference">
<div class="form-check">
<input type="checkbox"
@ -231,11 +232,14 @@
Your account is currently linked to the Proton account <b>{{ proton_linked_account }}</b>
<br />
</div>
<a class="btn btn-primary mt-2 proton-button"
href="{{ url_for('dashboard.unlink_proton_account') }}">
<img class="mr-2" src="/static/images/proton.svg">
Unlink account
</a>
<form method="post"
action="{{ url_for("dashboard.unlink_proton_account") }}">
{{ csrf_form.csrf_token }}
<button class="btn btn-primary mt-2 proton-button">
<img class="mr-2" src="/static/images/proton.svg">
Unlink account
</button>
</form>
{% else %}
<div class="mb-3">
You can connect your Proton and SimpleLogin accounts.
@ -262,6 +266,7 @@
<div class="card-title">Password</div>
<div class="mb-3">You will receive an email containing instructions on how to change your password.</div>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="change-password">
<button class="btn btn-outline-primary">Change password</button>
</form>
@ -278,6 +283,7 @@
Change the way random aliases are generated by default.
</div>
<form method="post" action="#random-alias" class="form-inline">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="change-alias-generator">
<select class="form-control mr-sm-2" name="alias-generator-scheme">
<option value="{{ AliasGeneratorEnum.word.value }}"
@ -299,6 +305,7 @@
Select the default domain for aliases.
</div>
<form method="post" action="#random-alias" class="form-inline">
{{ csrf_form.csrf_token }}
<input type="hidden"
name="form-name"
value="change-random-alias-default-domain">
@ -330,6 +337,7 @@
Select the default suffix generator for aliases.
</div>
<form method="post" action="#random-alias-suffix" class="form-inline">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="random-alias-suffix">
<select class="form-control mr-sm-2" name="random-alias-suffix-generator">
<option value="0"
@ -362,6 +370,7 @@
in the original form and needs to <b>transform</b> it to one of the formats below.
</div>
<form method="post" action="#sender-format">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="change-sender-format">
<select class="form-control mr-sm-2" name="sender-format">
<option value="{{ SenderFormatEnum.AT.value }}"
@ -408,6 +417,7 @@
<br />
</div>
<form method="post" action="#reverse-alias-replacement-section">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="replace-ra">
<div class="form-check">
<input type="checkbox"
@ -440,6 +450,7 @@
Please note that existing reverse-aliases won't change.
</div>
<form method="post" action="#sender-in-ra">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="sender-in-ra">
<div class="form-check">
<input type="checkbox" id="include-sender-ra" name="enable"
@ -471,6 +482,7 @@
<br />
</div>
<form method="post" action="#expand-alias-info-section">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="expand-alias-info">
<div class="form-check">
<input type="checkbox"
@ -504,6 +516,7 @@
style="max-width: 40%">
</div>
<form method="post" action="#include_website_in_one_click_alias">
{{ csrf_form.csrf_token }}
<input type="hidden"
name="form-name"
value="include_website_in_one_click_alias">
@ -535,6 +548,7 @@
{# You can disable these "loop" emails by enabling this option.#}
{# </div>#}
{# <form method="post" action="#ignore-loop-email-section">#}
{# {{ csrf_form.csrf_token }} #}
{# <input type="hidden" name="form-name" value="ignore-loop-email">#}
{# <div class="form-check">#}
{# <input type="checkbox" id="ignore-loop-email" name="enable"#}
@ -574,6 +588,7 @@
<form method="post"
action="#one-click-unsubscribe-section"
class="form-inline">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="one-click-unsubscribe">
<select class="form-control mr-sm-2" name="unsubscribe-behaviour">
<option value="{{ UnsubscribeBehaviourEnum.PreserveOriginal.name }}" {% if current_user.unsub_behaviour.value == UnsubscribeBehaviourEnum.PreserveOriginal.value %}
@ -633,6 +648,7 @@
<br />
</div>
<form method="post" action="#blocked-behaviour" class="form-inline">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="change-blocked-behaviour">
<select class="form-control mr-sm-2" name="blocked-behaviour">
<option value="{{ BlockBehaviourEnum.return_2xx.value }}"
@ -665,6 +681,7 @@
As email headers aren't encrypted, your mailbox service can know the sender address via this header.
</div>
<form method="post" action="#sender-header">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="sender-header">
<div class="form-check">
<input type="checkbox"
@ -709,6 +726,7 @@
<div class="d-flex">
<div>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="send-full-user-report">
<button class="btn btn-outline-info">
Request your data

View file

@ -2,61 +2,66 @@ from flask import url_for
from app.config import MAX_NB_DIRECTORY
from app.models import Directory
from tests.utils import login
from tests.utils import login, random_token
def test_create_directory(flask_client):
login(flask_client)
directory_name = random_token()
r = flask_client.post(
url_for("dashboard.directory"),
data={"form-name": "create", "name": "test"},
data={"form-name": "create", "name": directory_name},
follow_redirects=True,
)
assert r.status_code == 200
assert f"Directory test is created" in r.data.decode()
assert Directory.get_by(name="test") is not None
assert f"Directory {directory_name} is created" in r.data.decode()
assert Directory.get_by(name=directory_name) is not None
def test_delete_directory(flask_client):
"""cannot add domain if user personal email uses this domain"""
user = login(flask_client)
directory = Directory.create(name="test", user_id=user.id, commit=True)
directory_name = random_token()
directory = Directory.create(name=directory_name, user_id=user.id, commit=True)
r = flask_client.post(
url_for("dashboard.directory"),
data={"form-name": "delete", "dir-id": directory.id},
data={"form-name": "delete", "directory_id": directory.id},
follow_redirects=True,
)
assert r.status_code == 200
assert f"Directory test has been deleted" in r.data.decode()
assert Directory.get_by(name="test") is None
assert f"Directory {directory_name} has been deleted" in r.data.decode()
assert Directory.get_by(name=directory_name) is None
def test_create_directory_in_trash(flask_client):
user = login(flask_client)
directory_name = random_token()
directory = Directory.create(name="test", user_id=user.id, commit=True)
directory = Directory.create(name=directory_name, user_id=user.id, commit=True)
# delete the directory
r = flask_client.post(
url_for("dashboard.directory"),
data={"form-name": "delete", "dir-id": directory.id},
data={"form-name": "delete", "directory_id": directory.id},
follow_redirects=True,
)
assert Directory.get_by(name="test") is None
assert Directory.get_by(name=directory_name) is None
# try to recreate the directory
r = flask_client.post(
url_for("dashboard.directory"),
data={"form-name": "create", "name": "test"},
data={"form-name": "create", "name": directory_name},
follow_redirects=True,
)
assert r.status_code == 200
assert "test has been used before and cannot be reused" in r.data.decode()
assert (
f"{directory_name} has been used before and cannot be reused" in r.data.decode()
)
def test_create_directory_out_of_quota(flask_client):