Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(alias-used-on): Add websites where an alias is used ✨ #1708

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions app/api/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
AliasMailbox,
CustomDomain,
User,
AliasUsedOn,
)


Expand All @@ -31,6 +32,7 @@ class AliasInfo:
latest_email_log: EmailLog = None
latest_contact: Contact = None
custom_domain: Optional[CustomDomain] = None
alias_used_on: Optional[AliasUsedOn] = None

def contain_mailbox(self, mailbox_id: int) -> bool:
return mailbox_id in [m.id for m in self.mailboxes]
Expand Down Expand Up @@ -62,6 +64,7 @@ def serialize_alias_info_v2(alias_info: AliasInfo) -> dict:
"enabled": alias_info.alias.enabled,
"note": alias_info.alias.note,
"name": alias_info.alias.name,
"alias_used_on": alias_info.alias_used_on,
# activity
"nb_forward": alias_info.nb_forward,
"nb_block": alias_info.nb_blocked,
Expand Down Expand Up @@ -204,7 +207,7 @@ def get_alias_infos_with_pagination_v3(
q = list(q.limit(page_limit).offset(page_id * page_size))

ret = []
for alias, contact, email_log, nb_reply, nb_blocked, nb_forward in q:
for alias, contact, email_log, nb_reply, nb_blocked, nb_forward, hostnames in q:
ret.append(
AliasInfo(
alias=alias,
Expand All @@ -216,6 +219,7 @@ def get_alias_infos_with_pagination_v3(
latest_email_log=email_log,
latest_contact=contact,
custom_domain=alias.custom_domain,
alias_used_on=hostnames,
)
)

Expand Down Expand Up @@ -295,6 +299,12 @@ def get_alias_info_v2(alias: Alias, mailbox=None) -> AliasInfo:
alias_info.latest_contact = latest_contact
alias_info.latest_email_log = latest_email_log

q_alias_used_on = Session.query(AliasUsedOn.hostname).filter(
AliasUsedOn.alias_id == alias.id
)

alias_info.alias_used_on = list(map(lambda res: res.hostname, q_alias_used_on))

return alias_info


Expand All @@ -318,7 +328,7 @@ def get_alias_info_v3(user: User, alias_id: int) -> AliasInfo:
q = construct_alias_query(user)
q = q.filter(Alias.id == alias_id)

for alias, contact, email_log, nb_reply, nb_blocked, nb_forward in q:
for alias, contact, email_log, nb_reply, nb_blocked, nb_forward, hostnames in q:
return AliasInfo(
alias=alias,
mailbox=alias.mailbox,
Expand All @@ -329,6 +339,7 @@ def get_alias_info_v3(user: User, alias_id: int) -> AliasInfo:
latest_email_log=email_log,
latest_contact=contact,
custom_domain=alias.custom_domain,
alias_used_on=hostnames,
)


Expand Down Expand Up @@ -374,6 +385,14 @@ def construct_alias_query(user: User):
.subquery()
)

alias_used_on_subquery = (
Session.query(Alias.id, func.array_agg(AliasUsedOn.hostname).label("hostnames"))
.join(AliasUsedOn, Alias.id == AliasUsedOn.alias_id, isouter=True)
.filter(Alias.user_id == user.id)
.group_by(Alias.id)
.subquery()
)

return (
Session.query(
Alias,
Expand All @@ -382,11 +401,17 @@ def construct_alias_query(user: User):
alias_activity_subquery.c.nb_reply,
alias_activity_subquery.c.nb_blocked,
alias_activity_subquery.c.nb_forward,
alias_used_on_subquery.c.hostnames,
)
.options(joinedload(Alias.hibp_breaches))
.options(joinedload(Alias.custom_domain))
.join(Contact, Alias.id == Contact.alias_id, isouter=True)
.join(EmailLog, Contact.id == EmailLog.contact_id, isouter=True)
.join(
alias_used_on_subquery,
Alias.id == alias_used_on_subquery.c.id,
isouter=True,
)
.filter(Alias.id == alias_activity_subquery.c.id)
.filter(Alias.id == alias_contact_subquery.c.id)
.filter(
Expand Down
22 changes: 21 additions & 1 deletion app/api/views/alias.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
ErrContactAlreadyExists,
ErrAddressInvalid,
)
from app.models import Alias, Contact, Mailbox, AliasMailbox
from app.models import Alias, Contact, Mailbox, AliasMailbox, AliasUsedOn


@deprecated
Expand Down Expand Up @@ -101,6 +101,7 @@ def get_aliases_v2():
- email
- name
- reverse_alias
- alias_used_on


"""
Expand Down Expand Up @@ -311,6 +312,25 @@ def update_alias(alias_id):

changed = True

if "alias_used_on" in data:
alias_used_on = (
data.get("alias_used_on")
if type(data.get("alias_used_on")) == list
else list(data.get("alias_used_on"))
)

# <<< update alias alias_used_on >>>
# first remove all existing alias-alias_used_on links
AliasUsedOn.filter_by(alias_id=alias.id).delete()
Session.flush()

# then add all new alias_used_on
for hostname in alias_used_on:
AliasUsedOn.create(alias_id=alias.id, hostname=hostname, user_id=user.id)
# <<< END update alias alias_used_on >>>

changed = True

if "name" in data:
# to make sure alias name doesn't contain linebreak
new_name = data.get("name")
Expand Down
16 changes: 13 additions & 3 deletions app/dashboard/views/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from app.models import (
Alias,
AliasGeneratorEnum,
AliasUsedOn,
User,
EmailLog,
Contact,
Expand Down Expand Up @@ -63,9 +64,9 @@ def get_stats(user: User) -> Stats:
only_when=lambda: request.form.get("form-name") == "create-random-email",
)
def index():
query = request.args.get("query") or ""
sort = request.args.get("sort") or ""
alias_filter = request.args.get("filter") or ""
query = request.args.get("query", "")
sort = request.args.get("sort", "")
alias_filter = request.args.get("filter", "")

page = 0
if request.args.get("page"):
Expand Down Expand Up @@ -206,6 +207,14 @@ def index():
if highlight_alias_info:
alias_infos.insert(0, highlight_alias_info)

q_all_alias_used_on = (
Session.query(AliasUsedOn.hostname)
.filter(current_user.id == AliasUsedOn.user_id)
.group_by(AliasUsedOn.hostname)
)

all_alias_used_on = list(map(lambda res: res.hostname, q_all_alias_used_on))

return render_template(
"dashboard/index.html",
alias_infos=alias_infos,
Expand All @@ -220,6 +229,7 @@ def index():
filter=alias_filter,
stats=stats,
csrf_form=csrf_form,
all_alias_used_on=all_alias_used_on,
)


Expand Down
2 changes: 1 addition & 1 deletion server.py
Original file line number Diff line number Diff line change
Expand Up @@ -837,7 +837,7 @@ def send_newsletter(newsletter_id):

for user_id in user_ids:
user = User.get(user_id)
# refetch newsletter
# fetch newsletter again
newsletter = Newsletter.get(newsletter_id)

if not user:
Expand Down
61 changes: 61 additions & 0 deletions static/js/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
$('.mailbox-select').multipleSelect();
$('.alias-used-on-select').multipleSelect({
ellipsis: true,
filter: true,
onFilter: handleAliasUsedOnFilter,
formatNoMatchesFound: formatNoMatchesFound,
});

// This function doesn't seem to be documented but can be found in the code here
// https://github.com/wenzhixin/multiple-select/blob/973e585eff30e39e66f1b01ccd04e0bda4fb6862/src/MultipleSelect.js#L387
// It is the text displayed, when there is no results after filtering.
function formatNoMatchesFound () {
return 'No match found. Press Enter to add this website.';
}
function confirmDeleteAlias() {
let that = $(this);
let alias = that.data("alias-email");
Expand Down Expand Up @@ -244,6 +256,55 @@ async function handleDisplayNameChange(aliasId, aliasEmail) {

}

async function handleAliasUsedOnChange(aliasId, aliasEmail) {
const selectedOptions = document.getElementById(`alias-used-on-${aliasId}`).selectedOptions;
const hostnames = Array.from(selectedOptions).map((selectedOption) => selectedOption.value);

try {
let res = await fetch(`/api/aliases/${aliasId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
"alias_used_on": hostnames,
}),
});

if (res.ok) {
toastr.success(`Alias used on 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 handleAliasUsedOnFilter(text) {
const options_data = $(this)[0].data;
const no_option_visible = options_data.every((opt) => opt.visible === false );

// Just a workaround to get the id of the select, we set the "data-container" attribute, which is reflected in $(this), with the same value
const select_id = $(this)[0].container;

$("div.ms-parent.alias-used-on-select").off("keypress");
$("div.ms-parent.alias-used-on-select").on("keypress", function (event) {
// If press enter, add the value of the filter as an option
if (event.keyCode === 13 && no_option_visible) {
const $opt = $('<option />', {
value: text,
text: text,
});

// We need to add the options to all select here,
// otherwise only the current select will have the new option
$(`select.alias-used-on-select#${select_id}`).append($opt.prop('selected', true)).multipleSelect('refresh');
$(`select.alias-used-on-select:not(#${select_id})`).append($opt.prop('selected', false)).multipleSelect('refresh');
$(`#${select_id}`).trigger("change");
}
});
}

function handleDisplayNameFocus(aliasId) {
document.getElementById(`display-name-focus-message-${aliasId}`).classList.remove('d-none');
}
Expand Down
23 changes: 23 additions & 0 deletions templates/dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,29 @@
onblur="handleDisplayNameBlur({{ alias.id }})">
</div>
</div>
<div class="small-text">
Alias used on
</div>
<div class="d-flex">
<div class="flex-grow-1 mr-2">
<select id="alias-used-on-{{ alias.id }}"
data-width="100%"
class="alias-used-on-select"
multiple
name="alias-used-on"
onchange="handleAliasUsedOnChange('{{ alias.id }}', '{{ alias.email }}')"
placeholder="Websites eg. simplelogin.com"
data-container="alias-used-on-{{ alias.id }}">
{% for hostname in all_alias_used_on %}

<option value="{{ hostname }}"
{% if hostname in alias_info.alias_used_on %} selected{% endif %}>
{{ hostname | e }}
</option>
{% endfor %}
</select>
</div>
</div>
{% if alias.mailbox_support_pgp() %}

<div class="small-text mt-2"
Expand Down