Compare commits

...

8 Commits

Author SHA1 Message Date
D-Bao 9f92fa5509
Merge f2418b5972 into 037bc9da36 2024-04-28 15:28:16 +05:30
Son Nguyen Kim 037bc9da36
mailbox page requires sudo (#2094)
Co-authored-by: Son NK <son@simplelogin.io>
2024-04-23 22:25:37 +02:00
Son Nguyen Kim ee0be3688f
Data breach (#2093)
* add User.enable_data_breach_check column

* user can turn on/off the data breach check

* only run data breach check for user who enables it

* add tips to run tests using a local DB (without docker)

* refactor True check

* trim trailing space

* fix test

* Apply suggestions from code review

Co-authored-by: Adrià Casajús <acasajus@users.noreply.github.com>

* format

---------

Co-authored-by: Son NK <son@simplelogin.io>
Co-authored-by: Adrià Casajús <acasajus@users.noreply.github.com>
2024-04-23 22:16:36 +02:00
D-Bao f2418b5972 remove redundant condition 2023-04-17 12:16:44 +02:00
D-Bao aa235111a3 reduce width for more space for future side panel 2023-04-17 12:16:19 +02:00
D-Bao 72a72a93b6 rename alias detail page title 2023-04-14 15:08:40 +02:00
D-Bao f808d42240 modify 2 redirections to alias detail page 2023-04-14 15:03:29 +02:00
D-Bao 4ff9dc80ba create 'alias detail' page 2023-04-14 12:32:25 +02:00
7 changed files with 483 additions and 2 deletions

View File

@ -5,6 +5,7 @@ from .views import (
custom_alias,
subdomain,
billing,
alias_detail,
alias_log,
alias_export,
unsubscribe,

View File

@ -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,
)

View File

@ -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",

View File

@ -11,9 +11,11 @@ from wtforms.fields.html5 import EmailField
from app.config import ENFORCE_SPF, MAILBOX_SECRET
from app.config import URL
from app.dashboard.base import dashboard_bp
from app.dashboard.views.enter_sudo import sudo_required
from app.db import Session
from app.email_utils import email_can_be_used_as_mailbox
from app.email_utils import mailbox_already_used, render, send_email
from app.extensions import limiter
from app.log import LOG
from app.models import Alias, AuthorizedAddress
from app.models import Mailbox
@ -29,6 +31,8 @@ class ChangeEmailForm(FlaskForm):
@dashboard_bp.route("/mailbox/<int:mailbox_id>/", methods=["GET", "POST"])
@login_required
@sudo_required
@limiter.limit("20/minute", methods=["POST"])
def mailbox_detail_route(mailbox_id):
mailbox: Mailbox = Mailbox.get(mailbox_id)
if not mailbox or mailbox.user_id != current_user.id:

218
static/js/alias-detail.js Normal file
View File

@ -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');
}

View File

@ -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" }} &lt;{{ alias.email }}&gt;' 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 &nbsp;</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&nbsp; &nbsp;<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 %}

View File

@ -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