diff --git a/app/dashboard/__init__.py b/app/dashboard/__init__.py index ebfc38d62..3fbc534f2 100644 --- a/app/dashboard/__init__.py +++ b/app/dashboard/__init__.py @@ -5,6 +5,7 @@ custom_alias, subdomain, billing, + alias_detail, alias_log, alias_export, unsubscribe, diff --git a/app/dashboard/views/alias_detail.py b/app/dashboard/views/alias_detail.py new file mode 100644 index 000000000..88a555f09 --- /dev/null +++ b/app/dashboard/views/alias_detail.py @@ -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/", 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, + ) diff --git a/app/dashboard/views/alias_transfer.py b/app/dashboard/views/alias_transfer.py index af8fab509..7026c0a6f 100644 --- a/app/dashboard/views/alias_transfer.py +++ b/app/dashboard/views/alias_transfer.py @@ -222,7 +222,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", diff --git a/static/js/alias-detail.js b/static/js/alias-detail.js new file mode 100644 index 000000000..b89fd240a --- /dev/null +++ b/static/js/alias-detail.js @@ -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 can't be restored.`; + if (aliasDomainTrashUrl !== undefined) { + message = `Maybe you want to disable the alias instead? When it's deleted, it's moved to the domain + trash`; + } + + 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'); +} diff --git a/templates/dashboard/alias_detail.html b/templates/dashboard/alias_detail.html new file mode 100644 index 000000000..903d6d7bb --- /dev/null +++ b/templates/dashboard/alias_detail.html @@ -0,0 +1,202 @@ +{% extends "default.html" %} + +{% set active_page = "dashboard" %} +{% block title %}Alias Detail{% endblock %} +{% block default_content %} + + {% set alias = alias_info.alias %} +
+
+ +
+
+
+
+

+ {{ alias.email }} +

+ {% if alias.automatic_creation %} + +

+ Alias automatically generated because of an incoming email +

+ {% endif %} + {% if alias.hibp_breaches | length > 0 %} + +

+ Found in {{ alias.hibp_breaches | length }} data breaches. Check haveibeenpwned.com for more information + +

+ {% endif %} + {% if alias.custom_domain and not alias.custom_domain.verified %} + +

+ Cannot receive emails as the domain doesn't have MX records set up +

+ {% endif %} + +
+
+
+ {% 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 }} + + {{ email_log.created_at | dt }} + {% elif email_log.bounced %} + + {{ contact.website_email }} + + {{ email_log.created_at | dt }} + + {% elif email_log.blocked %} + {{ contact.website_email }} + + {{ email_log.created_at | dt }} + {% else %} + {{ contact.website_email }} + + {{ 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 %} +
+
+
+ +
+ + +
+ {% if mailboxes|length > 1 %} + + + + {% elif alias_info.mailbox != None and alias_info.mailbox.email != current_user.email %} +
+ Owned by {{ alias_info.mailbox.email }} mailbox +
+ {% endif %} +
+ + (automatically saved when you click outside the field or press Enter) +
+ When sending an email from this alias, the email will have '{{ alias.name or "Your Display Name" }} <{{ alias.email }}>' as sender. +
+ +
+ {% if alias.mailbox_support_pgp() %} + +
+ + +
+ {% endif %} +
+ + +
+
+ {% if alias_info.latest_email_log != None %}
Alias created {{ alias.created_at | dt }}
{% endif %} + {{ alias_info.nb_forward }} forwarded, + {{ alias_info.nb_blocked }} blocked, + {{ alias_info.nb_reply }} sent + in the last 14 days + See All  → +
+
+ +
+ + Transfer + + +
+ {{ csrf_form.csrf_token }} + + + + + Delete + + +
+
+
+
+
+
+
+{% endblock %} +{% block script %}{% endblock %} diff --git a/templates/dashboard/refused_email.html b/templates/dashboard/refused_email.html index 996e6894f..c936fcd33 100644 --- a/templates/dashboard/refused_email.html +++ b/templates/dashboard/refused_email.html @@ -68,7 +68,7 @@

Quarantine & Bounce


To: {{ alias.email }} - Disable Alias