Skip to content

Commit edcaf05

Browse files
Copilotjzbahrai
andcommitted
Add documentation for notification-admin changes
Co-authored-by: jzbahrai <8869623+jzbahrai@users.noreply.github.com>
1 parent b2cb581 commit edcaf05

1 file changed

Lines changed: 305 additions & 0 deletions

File tree

ADMIN_CHANGES_REQUIRED.md

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
# notification-admin Changes Required
2+
3+
This document outlines the changes needed in the notification-admin repository to complement the suppression list removal feature implemented in notification-api.
4+
5+
## Overview
6+
7+
The notification-admin needs to provide a UI under the service settings page where users can:
8+
1. Input an email address
9+
2. Check if it's on the suppression list
10+
3. Remove it from the suppression list (if the service has sent to it before)
11+
12+
## Required Changes
13+
14+
### 1. Add Route in `app/main/views/service_settings.py`
15+
16+
Add a new route for the suppression list management page:
17+
18+
```python
19+
@main.route("/services/<uuid:service_id>/service-settings/suppression-list", methods=["GET", "POST"])
20+
@user_has_permissions("manage_service")
21+
def service_suppression_list(service_id):
22+
"""
23+
Page to manage suppression list for a service.
24+
Allows removing email addresses from the SES suppression list.
25+
"""
26+
from app.main.forms import SuppressionListRemovalForm
27+
28+
form = SuppressionListRemovalForm()
29+
30+
if form.validate_on_submit():
31+
email_address = form.email_address.data
32+
33+
try:
34+
# Call notification-api endpoint to remove from suppression list
35+
service_api_client.remove_email_from_suppression_list(
36+
service_id,
37+
email_address
38+
)
39+
40+
flash(
41+
f"Successfully removed {email_address} from the suppression list.",
42+
"default_with_tick"
43+
)
44+
return redirect(url_for(".service_suppression_list", service_id=service_id))
45+
46+
except HTTPError as e:
47+
if e.status_code == 404:
48+
flash(
49+
f"This service has not sent any emails to {email_address}. "
50+
"You can only remove email addresses that your service has sent to.",
51+
"error"
52+
)
53+
elif e.status_code == 400:
54+
flash(
55+
"Invalid email address. Please check and try again.",
56+
"error"
57+
)
58+
else:
59+
flash(
60+
"Failed to remove email from suppression list. Please try again or contact support.",
61+
"error"
62+
)
63+
64+
return render_template(
65+
"views/service-settings/suppression-list.html",
66+
form=form
67+
)
68+
```
69+
70+
### 2. Add Form in `app/main/forms.py`
71+
72+
Create a form for email address input:
73+
74+
```python
75+
class SuppressionListRemovalForm(StripWhitespaceForm):
76+
email_address = email_address(
77+
"Email address",
78+
validators=[
79+
DataRequired(message="Cannot be empty"),
80+
ValidEmail()
81+
]
82+
)
83+
```
84+
85+
### 3. Add API Client Method in `app/notify_client/service_api_client.py`
86+
87+
Add method to call the notification-api endpoint:
88+
89+
```python
90+
def remove_email_from_suppression_list(self, service_id, email_address):
91+
"""
92+
Remove an email address from the SES suppression list.
93+
94+
Args:
95+
service_id: UUID of the service
96+
email_address: Email address to remove from suppression list
97+
98+
Returns:
99+
Response from the API
100+
101+
Raises:
102+
HTTPError: If the API call fails
103+
"""
104+
data = {"email_address": email_address}
105+
return self.post(
106+
url=f"/service/{service_id}/remove-from-suppression-list",
107+
data=data
108+
)
109+
```
110+
111+
### 4. Create Template `app/templates/views/service-settings/suppression-list.html`
112+
113+
Create the HTML template:
114+
115+
```html
116+
{% extends "withnav_template.html" %}
117+
{% from "components/form.html" import form_wrapper %}
118+
{% from "components/page-header.html" import page_header %}
119+
120+
{% block service_page_title %}
121+
Remove email from suppression list
122+
{% endblock %}
123+
124+
{% block maincolumn_content %}
125+
126+
{{ page_header("Remove email from suppression list") }}
127+
128+
<div class="govuk-body">
129+
<p>
130+
If an email address is on the suppression list, Notify will not send emails to it.
131+
This can happen if:
132+
</p>
133+
<ul class="govuk-list govuk-list--bullet">
134+
<li>the email server was down when we tried to send</li>
135+
<li>the email server gave an incorrect response</li>
136+
<li>an overactive spam filter blocked the email</li>
137+
</ul>
138+
139+
<p>
140+
You can remove an email address from the suppression list if your service has
141+
previously sent to it.
142+
</p>
143+
144+
<div class="govuk-warning-text">
145+
<span class="govuk-warning-text__icon" aria-hidden="true">!</span>
146+
<strong class="govuk-warning-text__text">
147+
<span class="govuk-warning-text__assistive">Warning</span>
148+
Only remove email addresses that you know are valid. Repeatedly sending to
149+
invalid addresses can affect your service's sending reputation.
150+
</strong>
151+
</div>
152+
</div>
153+
154+
{% call form_wrapper() %}
155+
{{ form.email_address(param_extensions={"hint": {"text": "Enter the email address to remove from the suppression list"}}) }}
156+
{{ page_footer("Remove from suppression list") }}
157+
{% endcall %}
158+
159+
{% endblock %}
160+
```
161+
162+
### 5. Add Link in Service Settings Page
163+
164+
In `app/templates/views/service-settings/index.html`, add a link to the suppression list management page:
165+
166+
```html
167+
<div class="settings-table body-copy-table">
168+
<h2 class="heading-medium">Email settings</h2>
169+
<div class="govuk-grid-row bottom-gutter-3-2">
170+
<!-- Existing email settings -->
171+
</div>
172+
173+
<div class="govuk-grid-row bottom-gutter-3-2">
174+
<div class="govuk-grid-column-two-thirds">
175+
<h3 class="heading-small">Suppression list</h3>
176+
<p class="govuk-body">
177+
Remove email addresses from the suppression list if they were blocked incorrectly.
178+
</p>
179+
</div>
180+
<div class="govuk-grid-column-one-third">
181+
<a href="{{ url_for('.service_suppression_list', service_id=current_service.id) }}" class="govuk-link govuk-link--no-visited-state">
182+
Manage suppression list
183+
</a>
184+
</div>
185+
</div>
186+
</div>
187+
```
188+
189+
### 6. Add Tests
190+
191+
Create `tests/app/main/views/service_settings/test_suppression_list.py`:
192+
193+
```python
194+
import pytest
195+
from flask import url_for
196+
from unittest.mock import Mock
197+
from requests import HTTPError
198+
199+
200+
def test_service_suppression_list_page_renders(
201+
client_request,
202+
service_one,
203+
mocker
204+
):
205+
"""Test that the suppression list management page renders"""
206+
page = client_request.get(
207+
"main.service_suppression_list",
208+
service_id=service_one["id"]
209+
)
210+
211+
assert "Remove email from suppression list" in page
212+
assert "Enter the email address" in page
213+
214+
215+
def test_remove_email_from_suppression_list_success(
216+
client_request,
217+
service_one,
218+
mocker,
219+
mock_remove_email_from_suppression_list
220+
):
221+
"""Test successfully removing an email from suppression list"""
222+
client_request.post(
223+
"main.service_suppression_list",
224+
service_id=service_one["id"],
225+
_data={"email_address": "test@example.com"},
226+
_expected_redirect=url_for(
227+
"main.service_suppression_list",
228+
service_id=service_one["id"]
229+
)
230+
)
231+
232+
mock_remove_email_from_suppression_list.assert_called_once_with(
233+
service_one["id"],
234+
"test@example.com"
235+
)
236+
237+
238+
def test_remove_email_from_suppression_list_not_sent_by_service(
239+
client_request,
240+
service_one,
241+
mocker
242+
):
243+
"""Test error when service hasn't sent to the email"""
244+
mock_remove = mocker.patch(
245+
"app.service_api_client.remove_email_from_suppression_list",
246+
side_effect=HTTPError(response=Mock(status_code=404))
247+
)
248+
249+
page = client_request.post(
250+
"main.service_suppression_list",
251+
service_id=service_one["id"],
252+
_data={"email_address": "never-sent@example.com"},
253+
_expected_status=200
254+
)
255+
256+
assert "has not sent any emails" in page
257+
258+
259+
def test_remove_email_from_suppression_list_invalid_email(
260+
client_request,
261+
service_one,
262+
mocker
263+
):
264+
"""Test validation error for invalid email"""
265+
page = client_request.post(
266+
"main.service_suppression_list",
267+
service_id=service_one["id"],
268+
_data={"email_address": "not-an-email"},
269+
_expected_status=200
270+
)
271+
272+
assert "Not a valid email address" in page
273+
274+
275+
@pytest.fixture
276+
def mock_remove_email_from_suppression_list(mocker):
277+
return mocker.patch(
278+
"app.service_api_client.remove_email_from_suppression_list"
279+
)
280+
```
281+
282+
## Testing the Integration
283+
284+
1. Start both notification-api and notification-admin locally
285+
2. Log in to notification-admin
286+
3. Navigate to a service's settings page
287+
4. Click on "Manage suppression list"
288+
5. Enter an email address that the service has sent to
289+
6. Verify the success message appears
290+
7. Check that a Freshdesk ticket was created
291+
8. Verify the email is removed from the AWS SES suppression list
292+
293+
## Security Considerations
294+
295+
- The endpoint is protected by the `manage_service` permission
296+
- Users can only remove emails their service has sent to
297+
- All actions are logged and create Freshdesk tickets for audit trail
298+
- Rate limiting should be considered to prevent abuse
299+
300+
## Additional Notes
301+
302+
- The feature only works for email addresses that the service has actually sent to (verified in notification-api)
303+
- Freshdesk tickets are created automatically to track removals
304+
- The AWS SES v2 API is used for suppression list management
305+
- Users should be warned about the risks of repeatedly sending to invalid addresses

0 commit comments

Comments
 (0)