From 4ff9dc80ba13ac33299e283d8351f5803282165d Mon Sep 17 00:00:00 2001 From: D-Bao <49440133+D-Bao@users.noreply.github.com> Date: Fri, 14 Apr 2023 12:32:25 +0200 Subject: [PATCH 1/5] create 'alias detail' page --- app/dashboard/__init__.py | 1 + app/dashboard/views/alias_detail.py | 60 +++++++ static/js/alias-detail.js | 218 ++++++++++++++++++++++++++ templates/dashboard/alias_detail.html | 198 +++++++++++++++++++++++ 4 files changed, 477 insertions(+) create mode 100644 app/dashboard/views/alias_detail.py create mode 100644 static/js/alias-detail.js create mode 100644 templates/dashboard/alias_detail.html 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..f4c065602 --- /dev/null +++ b/app/dashboard/views/alias_detail.py @@ -0,0 +1,60 @@ +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 not alias or alias.user_id != current_user.id: + flash("Unknown error, sorry for the inconvenience", "error") + return redirect(url_for("dashboard.index")) + + 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/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..5139c1ee3 --- /dev/null +++ b/templates/dashboard/alias_detail.html @@ -0,0 +1,198 @@ +{% extends "default.html" %} + +{% set active_page = "dashboard" %} +{% block title %}Alias Activity{% endblock %} +{% block default_content %} + + {% set alias = alias_info.alias %} +
+ + Back to Aliases + +
+

+ {{ 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 %} From f808d422405c832279abd9c60050e8baed448bb6 Mon Sep 17 00:00:00 2001 From: D-Bao <49440133+D-Bao@users.noreply.github.com> Date: Fri, 14 Apr 2023 15:03:29 +0200 Subject: [PATCH 2/5] modify 2 redirections to alias detail page --- app/dashboard/views/alias_transfer.py | 2 +- templates/dashboard/refused_email.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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 From 72a72a93b6e61a44f4d86887066ee366f3dd7764 Mon Sep 17 00:00:00 2001 From: D-Bao <49440133+D-Bao@users.noreply.github.com> Date: Fri, 14 Apr 2023 15:08:40 +0200 Subject: [PATCH 3/5] rename alias detail page title --- templates/dashboard/alias_detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/dashboard/alias_detail.html b/templates/dashboard/alias_detail.html index 5139c1ee3..ae904f45c 100644 --- a/templates/dashboard/alias_detail.html +++ b/templates/dashboard/alias_detail.html @@ -1,7 +1,7 @@ {% extends "default.html" %} {% set active_page = "dashboard" %} -{% block title %}Alias Activity{% endblock %} +{% block title %}Alias Detail{% endblock %} {% block default_content %} {% set alias = alias_info.alias %} From aa235111a3cfe8f893c5057c53d1840a77c8256d Mon Sep 17 00:00:00 2001 From: D-Bao <49440133+D-Bao@users.noreply.github.com> Date: Mon, 17 Apr 2023 12:16:19 +0200 Subject: [PATCH 4/5] reduce width for more space for future side panel --- templates/dashboard/alias_detail.html | 354 +++++++++++++------------- 1 file changed, 179 insertions(+), 175 deletions(-) diff --git a/templates/dashboard/alias_detail.html b/templates/dashboard/alias_detail.html index ae904f45c..903d6d7bb 100644 --- a/templates/dashboard/alias_detail.html +++ b/templates/dashboard/alias_detail.html @@ -5,192 +5,196 @@ {% block default_content %} {% set alias = alias_info.alias %} -
- - Back to Aliases - -
-

- {{ alias.email }} -

+
+
+
- {% if alias.automatic_creation %} +
+
+
+

+ {{ alias.email }} +

+ {% if alias.automatic_creation %} -

- Alias automatically generated because of an incoming email -

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

+ 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 %} +

+ 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 %} +

+ 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 %} + {% 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' %} + {{ 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 %} + + +
+ {% if mailboxes|length > 1 %} - - + {% for mailbox in mailboxes %} - - {% endfor %} - - {% 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() %} + + {% endfor %} + + {% 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 - - -
+
+ + +
+ {% 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 + + +
+
+
+
From f2418b597270ae8cf4f11d497ba8a6fed08d3812 Mon Sep 17 00:00:00 2001 From: D-Bao <49440133+D-Bao@users.noreply.github.com> Date: Mon, 17 Apr 2023 12:16:44 +0200 Subject: [PATCH 5/5] remove redundant condition --- app/dashboard/views/alias_detail.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/dashboard/views/alias_detail.py b/app/dashboard/views/alias_detail.py index f4c065602..88a555f09 100644 --- a/app/dashboard/views/alias_detail.py +++ b/app/dashboard/views/alias_detail.py @@ -34,10 +34,6 @@ def aliases(alias_id): flash("Invalid request", "warning") return redirect(request.url) if request.form.get("form-name") in ("delete-alias", "disable-alias"): - if not alias or alias.user_id != current_user.id: - flash("Unknown error, sorry for the inconvenience", "error") - return redirect(url_for("dashboard.index")) - if request.form.get("form-name") == "delete-alias": LOG.d("delete alias %s", alias) email = alias.email