Compare commits
9 Commits
1a529dea9e
...
d3fa437246
Author | SHA1 | Date |
---|---|---|
D-Bao | d3fa437246 | |
Adrià Casajús | 015036b499 | |
Son Nguyen Kim | d5df91aab6 | |
Adrià Casajús | 2eb5feaa8f | |
D-Bao | f2418b5972 | |
D-Bao | aa235111a3 | |
D-Bao | 72a72a93b6 | |
D-Bao | f808d42240 | |
D-Bao | 4ff9dc80ba |
|
@ -68,6 +68,12 @@ For most tests, you will need to have ``redis`` installed and started on your ma
|
|||
sh scripts/run-test.sh
|
||||
```
|
||||
|
||||
You can also run tests using a local Postgres DB to speed things up. This can be done by
|
||||
|
||||
- creating an empty test DB and running the database migration by `dropdb test && createdb test && DB_URI=postgresql://localhost:5432/test alembic upgrade head`
|
||||
|
||||
- replacing the `DB_URI` in `test.env` file by `DB_URI=postgresql://localhost:5432/test`
|
||||
|
||||
## Run the code locally
|
||||
|
||||
Install npm packages
|
||||
|
|
|
@ -308,28 +308,29 @@ def delete_alias(alias: Alias, user: User):
|
|||
Delete an alias and add it to either global or domain trash
|
||||
Should be used instead of Alias.delete, DomainDeletedAlias.create, DeletedAlias.create
|
||||
"""
|
||||
# save deleted alias to either global or domain trash
|
||||
LOG.i(f"User {user} has deleted alias {alias}")
|
||||
# save deleted alias to either global or domain tra
|
||||
if alias.custom_domain_id:
|
||||
if not DomainDeletedAlias.get_by(
|
||||
email=alias.email, domain_id=alias.custom_domain_id
|
||||
):
|
||||
LOG.d("add %s to domain %s trash", alias, alias.custom_domain_id)
|
||||
Session.add(
|
||||
DomainDeletedAlias(
|
||||
user_id=user.id,
|
||||
email=alias.email,
|
||||
domain_id=alias.custom_domain_id,
|
||||
)
|
||||
domain_deleted_alias = DomainDeletedAlias(
|
||||
user_id=user.id,
|
||||
email=alias.email,
|
||||
domain_id=alias.custom_domain_id,
|
||||
)
|
||||
Session.add(domain_deleted_alias)
|
||||
Session.commit()
|
||||
|
||||
LOG.i(
|
||||
f"Moving {alias} to domain {alias.custom_domain_id} trash {domain_deleted_alias}"
|
||||
)
|
||||
else:
|
||||
if not DeletedAlias.get_by(email=alias.email):
|
||||
LOG.d("add %s to global trash", alias)
|
||||
Session.add(DeletedAlias(email=alias.email))
|
||||
deleted_alias = DeletedAlias(email=alias.email)
|
||||
Session.add(deleted_alias)
|
||||
Session.commit()
|
||||
LOG.i(f"Moving {alias} to global trash {deleted_alias}")
|
||||
|
||||
LOG.i("delete alias %s", alias)
|
||||
Alias.filter(Alias.id == alias.id).delete()
|
||||
Session.commit()
|
||||
|
||||
|
|
|
@ -3,11 +3,13 @@ from flask_login import login_user
|
|||
|
||||
from app.auth.base import auth_bp
|
||||
from app.db import Session
|
||||
from app.extensions import limiter
|
||||
from app.log import LOG
|
||||
from app.models import EmailChange, ResetPasswordCode
|
||||
|
||||
|
||||
@auth_bp.route("/change_email", methods=["GET", "POST"])
|
||||
@limiter.limit("3/hour")
|
||||
def change_email():
|
||||
code = request.args.get("code")
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ from .views import (
|
|||
custom_alias,
|
||||
subdomain,
|
||||
billing,
|
||||
alias_detail,
|
||||
alias_log,
|
||||
alias_export,
|
||||
unsubscribe,
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
from flask import render_template, flash, redirect, url_for, request
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from app import alias_utils
|
||||
from app.api.serializer import get_alias_info_v3
|
||||
from app.dashboard.base import dashboard_bp
|
||||
from app.db import Session
|
||||
from app.log import LOG
|
||||
from app.utils import CSRFValidationForm
|
||||
|
||||
|
||||
@dashboard_bp.route("/aliases/<int:alias_id>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def aliases(alias_id):
|
||||
alias_info = get_alias_info_v3(current_user, alias_id)
|
||||
|
||||
# sanity check
|
||||
if not alias_info:
|
||||
flash("You do not have access to this page", "warning")
|
||||
return redirect(url_for("dashboard.index"))
|
||||
|
||||
alias = alias_info.alias
|
||||
|
||||
if alias.user_id != current_user.id:
|
||||
flash("You do not have access to this page", "warning")
|
||||
return redirect(url_for("dashboard.index"))
|
||||
|
||||
mailboxes = current_user.mailboxes()
|
||||
|
||||
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") in ("delete-alias", "disable-alias"):
|
||||
if request.form.get("form-name") == "delete-alias":
|
||||
LOG.d("delete alias %s", alias)
|
||||
email = alias.email
|
||||
alias_utils.delete_alias(alias, current_user)
|
||||
flash(f"Alias {email} has been deleted", "success")
|
||||
return redirect(url_for("dashboard.index"))
|
||||
|
||||
elif request.form.get("form-name") == "disable-alias":
|
||||
alias.enabled = False
|
||||
Session.commit()
|
||||
flash(f"Alias {alias.email} has been disabled", "success")
|
||||
|
||||
return redirect(url_for("dashboard.aliases", alias_id=alias.id))
|
||||
|
||||
return render_template(
|
||||
"dashboard/alias_detail.html",
|
||||
alias_info=alias_info,
|
||||
mailboxes=mailboxes,
|
||||
csrf_form=csrf_form,
|
||||
)
|
|
@ -162,7 +162,7 @@ def alias_transfer_receive_route():
|
|||
Session.commit()
|
||||
|
||||
flash(f"You are now owner of {alias.email}", "success")
|
||||
return redirect(url_for("dashboard.index", highlight_alias_id=alias.id))
|
||||
return redirect(url_for("dashboard.aliases", alias_id=alias.id))
|
||||
|
||||
return render_template(
|
||||
"dashboard/alias_transfer_receive.html",
|
||||
|
|
|
@ -14,7 +14,7 @@ from app.models import PartnerUser, SocialAuth
|
|||
from app.proton.utils import get_proton_partner
|
||||
from app.utils import sanitize_next_url
|
||||
|
||||
_SUDO_GAP = 900
|
||||
_SUDO_GAP = 120
|
||||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
|
|
|
@ -141,7 +141,7 @@ def index():
|
|||
)
|
||||
|
||||
if request.form.get("form-name") == "delete-alias":
|
||||
LOG.d("delete alias %s", alias)
|
||||
LOG.i(f"User {current_user} requested deletion of alias {alias}")
|
||||
email = alias.email
|
||||
alias_utils.delete_alias(alias, current_user)
|
||||
flash(f"Alias {email} has been deleted", "success")
|
||||
|
|
|
@ -179,8 +179,15 @@ def mailbox_detail_route(mailbox_id):
|
|||
|
||||
elif request.form.get("form-name") == "toggle-pgp":
|
||||
if request.form.get("pgp-enabled") == "on":
|
||||
mailbox.disable_pgp = False
|
||||
flash(f"PGP is enabled on {mailbox.email}", "success")
|
||||
if mailbox.is_proton():
|
||||
mailbox.disable_pgp = True
|
||||
flash(
|
||||
"Enabling PGP for a Proton Mail mailbox is redundant and does not add any security benefit",
|
||||
"info",
|
||||
)
|
||||
else:
|
||||
mailbox.disable_pgp = False
|
||||
flash(f"PGP is enabled on {mailbox.email}", "info")
|
||||
else:
|
||||
mailbox.disable_pgp = True
|
||||
flash(f"PGP is disabled on {mailbox.email}", "info")
|
||||
|
|
|
@ -227,6 +227,21 @@ def setting():
|
|||
Session.commit()
|
||||
flash("Your preference has been updated", "success")
|
||||
return redirect(url_for("dashboard.setting"))
|
||||
elif request.form.get("form-name") == "enable_data_breach_check":
|
||||
if not current_user.is_premium():
|
||||
flash("Only premium plan can enable data breach monitoring", "warning")
|
||||
return redirect(url_for("dashboard.setting"))
|
||||
choose = request.form.get("enable_data_breach_check")
|
||||
if choose == "on":
|
||||
LOG.i("User {current_user} has enabled data breach monitoring")
|
||||
current_user.enable_data_breach_check = True
|
||||
flash("Data breach monitoring is enabled", "success")
|
||||
else:
|
||||
LOG.i("User {current_user} has disabled data breach monitoring")
|
||||
current_user.enable_data_breach_check = False
|
||||
flash("Data breach monitoring is disabled", "info")
|
||||
Session.commit()
|
||||
return redirect(url_for("dashboard.setting"))
|
||||
elif request.form.get("form-name") == "sender-in-ra":
|
||||
choose = request.form.get("enable")
|
||||
if choose == "on":
|
||||
|
|
|
@ -525,6 +525,11 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
|
|||
sa.Boolean, default=True, nullable=False, server_default="1"
|
||||
)
|
||||
|
||||
# user opted in for data breach check
|
||||
enable_data_breach_check = sa.Column(
|
||||
sa.Boolean, default=False, nullable=False, server_default="0"
|
||||
)
|
||||
|
||||
# bitwise flags. Allow for future expansion
|
||||
flags = sa.Column(
|
||||
sa.BigInteger,
|
||||
|
|
|
@ -30,7 +30,9 @@ def check_bucket_limit(
|
|||
try:
|
||||
value = lock_redis.incr(bucket_lock_name, bucket_seconds)
|
||||
if value > max_hits:
|
||||
LOG.i(f"Rate limit hit for {bucket_lock_name} -> {value}/{max_hits}")
|
||||
LOG.i(
|
||||
f"Rate limit hit for {lock_name} (bucket id {bucket_id}) -> {value}/{max_hits}"
|
||||
)
|
||||
newrelic.agent.record_custom_event(
|
||||
"BucketRateLimit",
|
||||
{"lock_name": lock_name, "bucket_seconds": bucket_seconds},
|
||||
|
|
1
cron.py
1
cron.py
|
@ -1070,6 +1070,7 @@ def get_alias_to_check_hibp(
|
|||
Alias.id >= min_alias_id,
|
||||
Alias.id < max_alias_id,
|
||||
User.disabled == False, # noqa: E712
|
||||
User.enable_data_breach_check,
|
||||
or_(
|
||||
User.lifetime,
|
||||
ManualSubscription.end_at > now,
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: fa2f19bb4e5a
|
||||
Revises: 52510a633d6f
|
||||
Create Date: 2024-04-09 13:12:26.305340
|
||||
|
||||
"""
|
||||
import sqlalchemy_utils
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'fa2f19bb4e5a'
|
||||
down_revision = '52510a633d6f'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('users', sa.Column('enable_data_breach_check', sa.Boolean(), server_default='0', nullable=False))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('users', 'enable_data_breach_check')
|
||||
# ### end Alembic commands ###
|
|
@ -0,0 +1,218 @@
|
|||
$('.mailbox-select').multipleSelect();
|
||||
|
||||
function confirmDeleteAlias() {
|
||||
let that = $(this);
|
||||
let alias = that.data("alias-email");
|
||||
let aliasDomainTrashUrl = that.data("custom-domain-trash-url");
|
||||
|
||||
let message = `Maybe you want to disable the alias instead? Please note once deleted, it <b>can't</b> be restored.`;
|
||||
if (aliasDomainTrashUrl !== undefined) {
|
||||
message = `Maybe you want to disable the alias instead? When it's deleted, it's moved to the domain
|
||||
<a href="${aliasDomainTrashUrl}">trash</a>`;
|
||||
}
|
||||
|
||||
bootbox.dialog({
|
||||
title: `Delete ${alias}`,
|
||||
message: message,
|
||||
size: 'large',
|
||||
onEscape: true,
|
||||
backdrop: true,
|
||||
buttons: {
|
||||
disable: {
|
||||
label: 'Disable it',
|
||||
className: 'btn-primary',
|
||||
callback: function () {
|
||||
that.closest("form").find('input[name="form-name"]').val("disable-alias");
|
||||
that.closest("form").submit();
|
||||
}
|
||||
},
|
||||
|
||||
delete: {
|
||||
label: "Delete it, I don't need it anymore",
|
||||
className: 'btn-outline-danger',
|
||||
callback: function () {
|
||||
that.closest("form").submit();
|
||||
}
|
||||
},
|
||||
|
||||
cancel: {
|
||||
label: 'Cancel',
|
||||
className: 'btn-outline-primary'
|
||||
},
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(".enable-disable-alias").change(async function () {
|
||||
let aliasId = $(this).data("alias");
|
||||
let alias = $(this).data("alias-email");
|
||||
|
||||
await disableAlias(aliasId, alias);
|
||||
});
|
||||
|
||||
async function disableAlias(aliasId, alias) {
|
||||
let oldValue;
|
||||
try {
|
||||
let res = await fetch(`/api/aliases/${aliasId}/toggle`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
let json = await res.json();
|
||||
|
||||
if (json.enabled) {
|
||||
toastr.success(`${alias} is enabled`);
|
||||
$(`#send-email-${aliasId}`).removeClass("disabled");
|
||||
} else {
|
||||
toastr.success(`${alias} is disabled`);
|
||||
$(`#send-email-${aliasId}`).addClass("disabled");
|
||||
}
|
||||
} else {
|
||||
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Unknown Error");
|
||||
// reset to the original value
|
||||
oldValue = !$(this).prop("checked");
|
||||
$(this).prop("checked", oldValue);
|
||||
}
|
||||
} catch (e) {
|
||||
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Unknown Error");
|
||||
// reset to the original value
|
||||
oldValue = !$(this).prop("checked");
|
||||
$(this).prop("checked", oldValue);
|
||||
}
|
||||
}
|
||||
|
||||
$(".enable-disable-pgp").change(async function (e) {
|
||||
let aliasId = $(this).data("alias");
|
||||
let alias = $(this).data("alias-email");
|
||||
const oldValue = !$(this).prop("checked");
|
||||
let newValue = !oldValue;
|
||||
|
||||
try {
|
||||
let res = await fetch(`/api/aliases/${aliasId}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
disable_pgp: oldValue,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
if (newValue) {
|
||||
toastr.success(`PGP is enabled for ${alias}`);
|
||||
} else {
|
||||
toastr.info(`PGP is disabled for ${alias}`);
|
||||
}
|
||||
} else {
|
||||
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Unknown Error");
|
||||
// reset to the original value
|
||||
$(this).prop("checked", oldValue);
|
||||
}
|
||||
} catch (err) {
|
||||
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Unknown Error");
|
||||
// reset to the original value
|
||||
$(this).prop("checked", oldValue);
|
||||
}
|
||||
});
|
||||
|
||||
async function handleNoteChange(aliasId, aliasEmail) {
|
||||
const note = document.getElementById(`note-${aliasId}`).value;
|
||||
|
||||
try {
|
||||
let res = await fetch(`/api/aliases/${aliasId}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
note: note,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
toastr.success(`Description saved for ${aliasEmail}`);
|
||||
} else {
|
||||
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Unknown Error");
|
||||
}
|
||||
} catch (e) {
|
||||
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Unknown Error");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function handleNoteFocus(aliasId) {
|
||||
document.getElementById(`note-focus-message-${aliasId}`).classList.remove('d-none');
|
||||
}
|
||||
|
||||
function handleNoteBlur(aliasId) {
|
||||
document.getElementById(`note-focus-message-${aliasId}`).classList.add('d-none');
|
||||
}
|
||||
|
||||
async function handleMailboxChange(aliasId, aliasEmail) {
|
||||
const selectedOptions = document.getElementById(`mailbox-${aliasId}`).selectedOptions;
|
||||
const mailbox_ids = Array.from(selectedOptions).map((selectedOption) => selectedOption.value);
|
||||
|
||||
if (mailbox_ids.length === 0) {
|
||||
toastr.error("You must select at least a mailbox", "Error");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let res = await fetch(`/api/aliases/${aliasId}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
mailbox_ids: mailbox_ids,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
toastr.success(`Mailbox updated for ${aliasEmail}`);
|
||||
} else {
|
||||
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Unknown Error");
|
||||
}
|
||||
} catch (e) {
|
||||
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Unknown Error");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function handleDisplayNameChange(aliasId, aliasEmail) {
|
||||
const name = document.getElementById(`alias-name-${aliasId}`).value;
|
||||
|
||||
try {
|
||||
let res = await fetch(`/api/aliases/${aliasId}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
toastr.success(`Display name saved for ${aliasEmail}`);
|
||||
} else {
|
||||
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Unknown Error");
|
||||
}
|
||||
} catch (e) {
|
||||
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Unknown Error");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function handleDisplayNameFocus(aliasId) {
|
||||
document.getElementById(`display-name-focus-message-${aliasId}`).classList.remove('d-none');
|
||||
}
|
||||
|
||||
function handleDisplayNameBlur(aliasId) {
|
||||
document.getElementById(`display-name-focus-message-${aliasId}`).classList.add('d-none');
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
{% extends "default.html" %}
|
||||
|
||||
{% set active_page = "dashboard" %}
|
||||
{% block title %}Alias Detail{% endblock %}
|
||||
{% block default_content %}
|
||||
|
||||
{% set alias = alias_info.alias %}
|
||||
<div class="row">
|
||||
<div class="col-lg-3 order-lg-1 mb-4">
|
||||
<!-- side panel here for alias log and contact manager in the future -->
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h3 class="clipboard cursor d-inline-block text-break"
|
||||
data-toggle="tooltip"
|
||||
title="Click to copy"
|
||||
data-clipboard-text="{{ alias.email }}">
|
||||
{{ alias.email }}
|
||||
</h3>
|
||||
{% if alias.automatic_creation %}
|
||||
|
||||
<p class="mb-0">
|
||||
<span class="fa fa-inbox" data-toggle="tooltip" title=""></span> Alias automatically generated because of an incoming email
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if alias.hibp_breaches | length > 0 %}
|
||||
|
||||
<p class="mb-0">
|
||||
<span class="fa fa-warning text-danger" data-toggle="tooltip" title=""></span> Found in {{ alias.hibp_breaches | length }} data breaches. <a href="https://haveibeenpwned.com/account/{{ alias.email }}">Check haveibeenpwned.com for more information <span class="fa fa-external-link"></span>
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if alias.custom_domain and not alias.custom_domain.verified %}
|
||||
|
||||
<p class="">
|
||||
<span class="fa fa-warning text-warning" data-toggle="tooltip" title=""></span> Cannot receive emails as the domain doesn't have MX records set up
|
||||
</p>
|
||||
{% endif %}
|
||||
<!-- Email Activity -->
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<div class="small-text">
|
||||
{% if alias_info.latest_email_log != None %}
|
||||
|
||||
{% set email_log = alias_info.latest_email_log %}
|
||||
{% set contact = alias_info.latest_contact %}
|
||||
{% if email_log.is_reply %}
|
||||
|
||||
{{ contact.website_email }}
|
||||
<i class="fa fa-reply mr-2"
|
||||
data-toggle="tooltip"
|
||||
title="Email reply/sent from alias"></i>
|
||||
{{ email_log.created_at | dt }}
|
||||
{% elif email_log.bounced %}
|
||||
<span class="text-danger">
|
||||
{{ contact.website_email }}
|
||||
<i class="fa fa-warning mr-2"
|
||||
data-toggle="tooltip"
|
||||
title="Email bounced and cannot be forwarded to your mailbox"></i>
|
||||
{{ email_log.created_at | dt }}
|
||||
</span>
|
||||
{% elif email_log.blocked %}
|
||||
{{ contact.website_email }}
|
||||
<i class="fa fa-ban mr-2 text-danger"
|
||||
data-toggle="tooltip"
|
||||
title="Email blocked"></i>
|
||||
{{ email_log.created_at | dt }}
|
||||
{% else %}
|
||||
{{ contact.website_email }}
|
||||
<i class="fa fa-paper-plane mr-2"
|
||||
data-toggle="tooltip"
|
||||
title="Email sent to alias"></i>
|
||||
{{ email_log.created_at | dt }}
|
||||
{% include 'partials/toggle_contact.html' %}
|
||||
|
||||
{% endif %}
|
||||
{% else %}
|
||||
No emails received/sent in the last 14 days. Created {{ alias.created_at | dt }}.
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END Email Activity -->
|
||||
<div class="mb-4">
|
||||
<label class="mb-0" for="note-{{ alias.id }}">
|
||||
Alias description <span id="note-focus-message-{{ alias.id }}"
|
||||
class="d-none small-text font-italic">(automatically saved when you click outside the field)</span>
|
||||
</label>
|
||||
<textarea id="note-{{ alias.id }}" name="note" class="form-control" rows="2" placeholder="e.g. where the alias is used or why is it created" onchange="handleNoteChange({{ alias.id }}, '{{ alias.email }}')" onfocus="handleNoteFocus({{ alias.id }})" onblur="handleNoteBlur({{ alias.id }})">{{ alias.note or "" }}</textarea>
|
||||
</div>
|
||||
{% if mailboxes|length > 1 %}
|
||||
|
||||
<label class="mb-0" for="mailbox-{{ alias.id }}">Current mailbox</label>
|
||||
<select required
|
||||
id="mailbox-{{ alias.id }}"
|
||||
data-width="100%"
|
||||
class="mailbox-select mb-4"
|
||||
multiple
|
||||
name="mailbox"
|
||||
onchange="handleMailboxChange({{ alias.id }}, '{{ alias.email }}')">
|
||||
{% for mailbox in mailboxes %}
|
||||
|
||||
<option value="{{ mailbox.id }}" {% if alias_info.contain_mailbox(mailbox.id) %}
|
||||
selected {% endif %}>
|
||||
{{ mailbox.email }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% elif alias_info.mailbox != None and alias_info.mailbox.email != current_user.email %}
|
||||
<div class="small-text mb-4">
|
||||
Owned by <b>{{ alias_info.mailbox.email }}</b> mailbox
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mb-4">
|
||||
<label class="mb-0" for="alias-name-{{ alias.id }}">Display name</label>
|
||||
<span id="display-name-focus-message-{{ alias.id }}"
|
||||
class="d-none small-text font-italic">(automatically saved when you click outside the field or press Enter)</span>
|
||||
<div class="small-text">
|
||||
When sending an email from this alias, the email will have '{{ alias.name or "Your Display Name" }} <{{ alias.email }}>' as sender.
|
||||
</div>
|
||||
<input id="alias-name-{{ alias.id }}"
|
||||
value="{{ alias.name or '' }}"
|
||||
class="form-control"
|
||||
placeholder="{{ alias.custom_domain.name or "Alias name" }}"
|
||||
onchange="handleDisplayNameChange({{ alias.id }}, '{{ alias.email }}')"
|
||||
onfocus="handleDisplayNameFocus({{ alias.id }})"
|
||||
onblur="handleDisplayNameBlur({{ alias.id }})">
|
||||
</div>
|
||||
{% if alias.mailbox_support_pgp() %}
|
||||
|
||||
<div class="d-flex align-items-start mb-4">
|
||||
<label class="custom-switch cursor pl-0 mt-1">
|
||||
<input id="enable-disable-pgp-{{ alias.id }}" type="checkbox" class="enable-disable-pgp custom-switch-input" data-alias="{{ alias.id }}" data-alias-email="{{ alias.email }}" {{ "checked" if alias.pgp_enabled() else "" }}>
|
||||
<span class="custom-switch-indicator"></span>
|
||||
</label>
|
||||
<label for="enable-disable-pgp-{{ alias.id }}" class="ml-2">
|
||||
PGP
|
||||
<br>
|
||||
<span class="small-text">It can be useful to disable PGP if the sender already encrypts the emails</span>
|
||||
</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="d-flex align-items-start mb-4">
|
||||
<label class="custom-switch cursor pl-0 mt-1">
|
||||
<input type="checkbox" id="enable-disable-alias-{{ alias.id }}" class="enable-disable-alias custom-switch-input" data-alias="{{ alias.id }}" data-alias-email="{{ alias.email }}" {{ "checked" if alias.enabled else "" }}>
|
||||
<span class="custom-switch-indicator"></span>
|
||||
</label>
|
||||
<label for="enable-disable-alias-{{ alias.id }}" class="ml-2">
|
||||
Alias enabled
|
||||
<br>
|
||||
<span class="small-text">{{ "To stop receiving emails sent to this alias, you can disable the alias." if alias.enabled else "Enable the alias to start receiving emails sent to this alias." }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
{% if alias_info.latest_email_log != None %}<div>Alias created {{ alias.created_at | dt }}</div>{% endif %}
|
||||
<span class="alias-activity">{{ alias_info.nb_forward }}</span> forwarded,
|
||||
<span class="alias-activity">{{ alias_info.nb_blocked }}</span> blocked,
|
||||
<span class="alias-activity">{{ alias_info.nb_reply }}</span> sent
|
||||
in the last 14 days
|
||||
<a href="{{ url_for('dashboard.alias_log', alias_id=alias.id) }}"
|
||||
class="btn btn-sm btn-link">See All →</a>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<div class="mr-auto">
|
||||
<a href="{{ url_for('dashboard.alias_contact_manager', alias_id=alias.id) }}"
|
||||
id="send-email-{{ alias.id }}"
|
||||
class="btn btn-sm btn-outline-primary {% if not alias.enabled %}disabled{% endif %} "
|
||||
data-toggle="tooltip"
|
||||
title="Add new contact, manage your existing contacts">
|
||||
Contacts <i class="fe fe-send"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="btn-group ml-auto" role="group" aria-label="Basic example">
|
||||
<a href="{{ url_for('dashboard.alias_transfer_send_route', alias_id=alias.id) }}"
|
||||
class="btn btn-sm btn-link">
|
||||
Transfer
|
||||
<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 }}">
|
||||
<span class="btn btn-link btn-sm float-right text-danger"
|
||||
onclick="confirmDeleteAlias.call(this)"
|
||||
{% if alias.custom_domain %} data-custom-domain-trash-url="{{ alias.custom_domain.get_trash_url() }}"{% endif %}
|
||||
data-alias="{{ alias.id }}"
|
||||
data-alias-email="{{ alias.email }}">
|
||||
Delete
|
||||
<i class="ml-0 dropdown-icon fe fe-trash-2 text-danger"></i>
|
||||
</span>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block script %}<script src="/static/js/alias-detail.js"></script>{% endblock %}
|
|
@ -68,7 +68,7 @@
|
|||
<br />
|
||||
<span>
|
||||
To: {{ alias.email }}
|
||||
<a href='{{ url_for("dashboard.index", highlight_alias_id=alias.id) }}'
|
||||
<a href='{{ url_for("dashboard.aliases", alias_id=alias.id) }}'
|
||||
class="text-danger small-text"
|
||||
style="text-decoration: underline">
|
||||
Disable Alias
|
||||
|
|
|
@ -249,6 +249,42 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- END Random alias -->
|
||||
<!-- Data breach check -->
|
||||
<div class="card" id="data-breach">
|
||||
<div class="card-body">
|
||||
<div class="card-title">Data breach monitoring</div>
|
||||
<div class="mt-1 mb-3">
|
||||
{% if not current_user.is_premium() %}
|
||||
|
||||
<div class="alert alert-info" role="alert">
|
||||
This feature is only available on Premium plan.
|
||||
<a href="{{ url_for('dashboard.pricing') }}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
Upgrade<i class="fe fe-external-link"></i>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
If enabled, we will inform you via email if one of your aliases appears in a data breach.
|
||||
<br>
|
||||
SimpleLogin uses <a href="https://haveibeenpwned.com/">HaveIBeenPwned</a> API for checking for data breaches.
|
||||
</div>
|
||||
<form method="post" action="#data-breach">
|
||||
{{ csrf_form.csrf_token }}
|
||||
<input type="hidden" name="form-name" value="enable_data_breach_check">
|
||||
<div class="form-check">
|
||||
<input type="checkbox"
|
||||
id="enable_data_breach_check"
|
||||
name="enable_data_breach_check"
|
||||
{% if current_user.enable_data_breach_check %} checked{% endif %}
|
||||
class="form-check-input">
|
||||
<label for="enable_data_breach_check">Enable data breach monitoring</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-outline-primary">Update</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END Data breach check -->
|
||||
<!-- Sender Format -->
|
||||
<div class="card" id="sender-format">
|
||||
<div class="card-body">
|
||||
|
@ -285,7 +321,9 @@
|
|||
No Name (i.e. only reverse-alias)
|
||||
</option>
|
||||
</select>
|
||||
<button class="btn btn-outline-primary mt-3">Update</button>
|
||||
<button class="btn btn-outline-primary mt-3">
|
||||
Update
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -295,7 +333,9 @@
|
|||
<div class="card-body">
|
||||
<div class="card-title">
|
||||
Reverse Alias Replacement
|
||||
<div class="badge badge-warning">Experimental</div>
|
||||
<div class="badge badge-warning">
|
||||
Experimental
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
When replying to a forwarded email, the <b>reverse-alias</b> can be automatically included
|
||||
|
@ -312,9 +352,13 @@
|
|||
name="replace-ra"
|
||||
{% if current_user.replace_reverse_alias %} checked{% endif %}
|
||||
class="form-check-input">
|
||||
<label for="replace-ra">Enable replacing reverse alias</label>
|
||||
<label for="replace-ra">
|
||||
Enable replacing reverse alias
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-outline-primary">Update</button>
|
||||
<button type="submit" class="btn btn-outline-primary">
|
||||
Update
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -31,6 +31,7 @@ def test_get_alias_for_free_user_has_no_alias():
|
|||
def test_get_alias_for_lifetime_with_null_hibp_date():
|
||||
user = create_new_user()
|
||||
user.lifetime = True
|
||||
user.enable_data_breach_check = True
|
||||
alias_id = Alias.create_new_random(user).id
|
||||
Session.commit()
|
||||
aliases = list(
|
||||
|
@ -42,6 +43,7 @@ def test_get_alias_for_lifetime_with_null_hibp_date():
|
|||
def test_get_alias_for_lifetime_with_old_hibp_date():
|
||||
user = create_new_user()
|
||||
user.lifetime = True
|
||||
user.enable_data_breach_check = True
|
||||
alias = Alias.create_new_random(user)
|
||||
alias.hibp_last_check = arrow.now().shift(days=-1)
|
||||
alias_id = alias.id
|
||||
|
@ -97,6 +99,7 @@ sub_generator_list = [
|
|||
@pytest.mark.parametrize("sub_generator", sub_generator_list)
|
||||
def test_get_alias_for_sub(sub_generator):
|
||||
user = create_new_user()
|
||||
user.enable_data_breach_check = True
|
||||
sub_generator(user)
|
||||
alias_id = Alias.create_new_random(user).id
|
||||
Session.commit()
|
||||
|
@ -140,3 +143,26 @@ def test_already_checked_is_not_checked():
|
|||
cron.get_alias_to_check_hibp(arrow.now(), [user.id], alias_id, alias_id + 1)
|
||||
)
|
||||
assert len(aliases) == 0
|
||||
|
||||
|
||||
def test_outed_in_user_is_checked():
|
||||
user = create_new_user()
|
||||
user.lifetime = True
|
||||
user.enable_data_breach_check = True
|
||||
alias_id = Alias.create_new_random(user).id
|
||||
Session.commit()
|
||||
aliases = list(
|
||||
cron.get_alias_to_check_hibp(arrow.now(), [], alias_id, alias_id + 1)
|
||||
)
|
||||
assert len(aliases) == 1
|
||||
|
||||
|
||||
def test_outed_out_user_is_not_checked():
|
||||
user = create_new_user()
|
||||
user.lifetime = True
|
||||
alias_id = Alias.create_new_random(user).id
|
||||
Session.commit()
|
||||
aliases = list(
|
||||
cron.get_alias_to_check_hibp(arrow.now(), [], alias_id, alias_id + 1)
|
||||
)
|
||||
assert len(aliases) == 0
|
||||
|
|
Loading…
Reference in New Issue