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, custom_alias,
subdomain, subdomain,
billing, billing,
alias_detail,
alias_log, alias_log,
alias_export, alias_export,
unsubscribe, 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() Session.commit()
flash(f"You are now owner of {alias.email}", "success") 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( return render_template(
"dashboard/alias_transfer_receive.html", "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 ENFORCE_SPF, MAILBOX_SECRET
from app.config import URL from app.config import URL
from app.dashboard.base import dashboard_bp from app.dashboard.base import dashboard_bp
from app.dashboard.views.enter_sudo import sudo_required
from app.db import Session from app.db import Session
from app.email_utils import email_can_be_used_as_mailbox from app.email_utils import email_can_be_used_as_mailbox
from app.email_utils import mailbox_already_used, render, send_email from app.email_utils import mailbox_already_used, render, send_email
from app.extensions import limiter
from app.log import LOG from app.log import LOG
from app.models import Alias, AuthorizedAddress from app.models import Alias, AuthorizedAddress
from app.models import Mailbox from app.models import Mailbox
@ -29,6 +31,8 @@ class ChangeEmailForm(FlaskForm):
@dashboard_bp.route("/mailbox/<int:mailbox_id>/", methods=["GET", "POST"]) @dashboard_bp.route("/mailbox/<int:mailbox_id>/", methods=["GET", "POST"])
@login_required @login_required
@sudo_required
@limiter.limit("20/minute", methods=["POST"])
def mailbox_detail_route(mailbox_id): def mailbox_detail_route(mailbox_id):
mailbox: Mailbox = Mailbox.get(mailbox_id) mailbox: Mailbox = Mailbox.get(mailbox_id)
if not mailbox or mailbox.user_id != current_user.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 /> <br />
<span> <span>
To: {{ alias.email }} 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" class="text-danger small-text"
style="text-decoration: underline"> style="text-decoration: underline">
Disable Alias Disable Alias