Compare commits
8 Commits
d3fa437246
...
9f92fa5509
Author | SHA1 | Date |
---|---|---|
D-Bao | 9f92fa5509 | |
Son Nguyen Kim | 037bc9da36 | |
Son Nguyen Kim | ee0be3688f | |
D-Bao | f2418b5972 | |
D-Bao | aa235111a3 | |
D-Bao | 72a72a93b6 | |
D-Bao | f808d42240 | |
D-Bao | 4ff9dc80ba |
|
@ -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",
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue