Skip to content
Draft
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
8 changes: 8 additions & 0 deletions app/main/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -1169,6 +1169,14 @@ class ServiceReplyToEmailForm(StripWhitespaceForm):
is_default = BooleanField(_l("Make this email address the default"))


class RemoveEmailFromSuppressionListForm(StripWhitespaceForm):
email_address = email_address(label=_l("Email address"), gov_user=False)
request_details = TextAreaField(
_l("Request details"),
validators=[Length(max=500, message=_l("Must be 500 characters or fewer"))],
)


class ServiceSmsSenderForm(StripWhitespaceForm):
sms_sender = StringField(
_l("Text message sender"),
Expand Down
33 changes: 33 additions & 0 deletions app/main/views/service_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
LinkOrganisationsForm,
MessageLimit,
PreviewBranding,
RemoveEmailFromSuppressionListForm,
RenameServiceForm,
SearchByNameForm,
SelectLogoForm,
Expand Down Expand Up @@ -608,6 +609,38 @@ def service_email_reply_to(service_id):
return render_template("views/service-settings/email_reply_to.html")


@main.route(
"/services/<service_id>/service-settings/email-suppression/removal",
methods=["GET", "POST"],
)
@user_has_permissions("manage_service")
def service_remove_email_from_suppression_list(service_id):
form = RemoveEmailFromSuppressionListForm()

if form.validate_on_submit():
try:
service_api_client.remove_email_from_suppression_list(
service_id=service_id,
email_address=form.email_address.data,
user_id=current_user.id,
request_details=form.request_details.data,
)
except HTTPError as e:
if e.status_code == 400 and isinstance(e.message, dict):
form.email_address.errors += e.message.get("email_address", [])
form.request_details.errors += e.message.get("request_details", [])
else:
raise e
else:
flash(_("Email address removed from suppression list"), "default_with_tick")
return redirect(url_for(".service_remove_email_from_suppression_list", service_id=service_id))

return render_template(
"views/service-settings/remove-from-email-suppression-list.html",
form=form,
)


@main.route(
"/services/<service_id>/service-settings/email-reply-to/add",
methods=["GET", "POST"],
Expand Down
9 changes: 9 additions & 0 deletions app/notify_client/service_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,15 @@ def verify_reply_to_email_address(self, service_id, email_address):
data={"email": email_address},
)

def remove_email_from_suppression_list(self, service_id, email_address, user_id, request_details=""):
data = {
"email_address": email_address,
"updated_by_id": user_id,
}
if request_details:
data["request_details"] = request_details
return self.post("/service/{}/email-suppression/removal".format(service_id), data=data)

@cache.delete("service-{service_id}")
def add_reply_to_email_address(self, service_id, email_address, is_default=False):
return self.post(
Expand Down
14 changes: 14 additions & 0 deletions app/templates/views/service-settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,20 @@ <h2 class="heading-small p-0 m-0">{{ _('Your service is in trial mode') }}</h2>

{% endcall %}

{% if 'email' in current_service.permissions and current_user.has_permissions('manage_service') %}
<div class="mt-12 border border-gray-300 p-gutterHalf md:p-gutter">
<h2 class="heading-medium mt-0">{{ _('Email suppression list') }}</h2>
<p>
{{ _('Remove an email address from the GC Notify suppression list if it was incorrectly blocked and your service has previously sent to it.') }}
</p>
<p class="mb-0">
<a href="{{ url_for('.service_remove_email_from_suppression_list', service_id=current_service.id) }}">
{{ _('Remove an email address from the suppression list') }}
</a>
</p>
</div>
{% endif %}

<p class="mt-12">
{{ _("Problems, questions, or comments? <a href='{}'>Contact us</a>.").format(url_for('.contact')) }}
</p>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{% extends "admin_template.html" %}
{% from "components/textbox.html" import textbox %}
{% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}

{% block service_page_title %}
{{ _('Remove an email address from the suppression list') }}
{% endblock %}

{% block maincolumn_content %}

{{ page_header(
_('Remove an email address from the suppression list'),
back_link=url_for('main.service_settings', service_id=current_service.id)
) }}

<p>
{{ _('Use this if an address was blocked by mistake. You can only remove an address if your service has previously sent to it.') }}
</p>

{% call form_wrapper() %}
{{ textbox(
form.email_address,
width='w-full md:w-2/3',
hint=_('Enter the email address exactly as it was used when sending messages.'),
safe_error_message=True
) }}

{{ textbox(
form.request_details,
width='w-full md:w-2/3',
rows=5,
required=false,
hint=_('Add context for the audit trail (for example, confirmation from the recipient).'),
safe_error_message=True
) }}

{{ page_footer(_('Remove email address')) }}
{% endcall %}

{% endblock %}
10 changes: 10 additions & 0 deletions app/translations/csv/fr.csv
Original file line number Diff line number Diff line change
Expand Up @@ -1953,6 +1953,16 @@
"Policy","Politique"
"Check that your email address is formatted correctly.","Veuillez vérifier que votre adresse courriel est correctement formatée."
"GC Notify service level agreement","Accord sur les niveaux de service de Notification GC"
"Email suppression list","Liste de suppression des courriels"
"Remove an email address from the GC Notify suppression list if it was incorrectly blocked and your service has previously sent to it.","Retirez une adresse courriel de la liste de suppression de Notification GC si elle a été bloquée par erreur et que votre service lui a déjà envoyé des messages."
"Remove an email address from the suppression list","Retirer une adresse courriel de la liste de suppression"
"Use this if an address was blocked by mistake. You can only remove an address if your service has previously sent to it.","Utilisez cette option si une adresse a été bloquée par erreur. Vous ne pouvez retirer une adresse que si votre service lui a déjà envoyé des messages."
"Enter the email address exactly as it was used when sending messages.","Entrez l'adresse courriel exactement telle qu'elle a été utilisée lors de l'envoi des messages."
"Add context for the audit trail (for example, confirmation from the recipient).","Ajoutez du contexte pour la piste d'audit (par exemple, une confirmation du destinataire)."
"Remove email address","Retirer l'adresse courriel"
"Request details","Détails de la demande"
"Must be 500 characters or fewer","Doit contenir 500 caractères ou moins"
"Email address removed from suppression list","Adresse courriel retirée de la liste de suppression"
"Service level objectives: What to expect from GC Notify","Objectifs de niveau de service&nbsp;: qu’attendre de Notification GC"
"Integrate the API","Intégrer l’API"
"Sitemap","Plan de site"
Expand Down
110 changes: 110 additions & 0 deletions tests/app/main/views/test_service_settings_suppression_list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from unittest.mock import Mock

from flask import url_for
from notifications_python_client.errors import HTTPError

from tests import validate_route_permission
from tests.conftest import normalize_spaces


def test_service_settings_shows_suppression_list_link(
client_request,
service_one,
no_reply_to_email_addresses,
no_letter_contact_blocks,
single_sms_sender,
mock_get_service_organisation,
mock_get_all_letter_branding,
mock_get_inbound_number_for_service,
mock_get_free_sms_fragment_limit,
mock_get_service_data_retention,
):
page = client_request.get("main.service_settings", service_id=service_one["id"])

expected_link = url_for("main.service_remove_email_from_suppression_list", service_id=service_one["id"])
link = page.select_one(f"a[href='{expected_link}']")

assert link is not None
assert normalize_spaces(link.text) == "Remove an email address from the suppression list"


def test_service_remove_email_from_suppression_list_page(
client_request,
service_one,
):
page = client_request.get("main.service_remove_email_from_suppression_list", service_id=service_one["id"])

assert normalize_spaces(page.find("h1").text) == "Remove an email address from the suppression list"


def test_service_remove_email_from_suppression_list_success(
client_request,
mocker,
service_one,
active_user_with_permissions,
):
mock_remove = mocker.patch("app.service_api_client.remove_email_from_suppression_list")
client_request.login(active_user_with_permissions, service_one)

page = client_request.post(
"main.service_remove_email_from_suppression_list",
service_id=service_one["id"],
_data={
"email_address": "person@example.com",
"request_details": "Recipient confirmed mailbox is active",
},
_follow_redirects=True,
)

mock_remove.assert_called_once_with(
service_id=service_one["id"],
email_address="person@example.com",
user_id=active_user_with_permissions["id"],
request_details="Recipient confirmed mailbox is active",
)
assert "Email address removed from suppression list" in normalize_spaces(page.text)


def test_service_remove_email_from_suppression_list_shows_api_validation_error(
client_request,
mocker,
service_one,
):
mocker.patch(
"app.service_api_client.remove_email_from_suppression_list",
side_effect=HTTPError(
response=Mock(status_code=400),
message={"email_address": ["You can only remove email addresses your service has previously emailed."]},
),
)

page = client_request.post(
"main.service_remove_email_from_suppression_list",
service_id=service_one["id"],
_data={
"email_address": "unknown@example.com",
"request_details": "",
},
_expected_status=200,
)

assert "You can only remove email addresses your service has previously emailed." in normalize_spaces(page.text)


def test_service_remove_email_from_suppression_list_route_permissions(
mocker,
app_,
client,
api_user_active,
service_one,
):
validate_route_permission(
mocker,
app_,
"GET",
200,
url_for("main.service_remove_email_from_suppression_list", service_id=service_one["id"]),
["manage_service"],
api_user_active,
service_one,
)
21 changes: 21 additions & 0 deletions tests/app/notify_client/test_service_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,27 @@ def test_client_gets_service(mocker):
mock_get.assert_called_once_with("/service/foo")


def test_client_removes_email_from_suppression_list(mocker):
client = ServiceAPIClient()
mock_post = mocker.patch.object(client, "post", return_value={"data": {"email_address": "person@example.com"}})

client.remove_email_from_suppression_list(
SERVICE_ONE_ID,
"person@example.com",
"user-id",
request_details="Mailbox issue resolved",
)

mock_post.assert_called_once_with(
"/service/{}/email-suppression/removal".format(SERVICE_ONE_ID),
data={
"email_address": "person@example.com",
"updated_by_id": "user-id",
"request_details": "Mailbox issue resolved",
},
)


@pytest.mark.parametrize(
"today_only, limit_days",
[
Expand Down
Loading