diff --git a/CHANGELOG.md b/CHANGELOG.md index 012a8c69..4fe7287c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 6.1.0-RELEASE +* Add a `sanitiseContentFor` parameter to `sendEmail`, a list of placeholders, the content of which you want Notify to sanitise. + ## 6.0.0-RELEASE * Remove Java 8, 9, and 10 from supported versions. This library only supports Java 11+ from now on. diff --git a/pom.xml b/pom.xml index 668f888a..88c1c28a 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ uk.gov.service.notify notifications-java-client - 6.0.1-RELEASE + 6.1.0-RELEASE jar GOV.UK Notify Java client diff --git a/src/main/java/uk/gov/service/notify/NotificationClient.java b/src/main/java/uk/gov/service/notify/NotificationClient.java index ba15ea8d..fc3ec298 100644 --- a/src/main/java/uk/gov/service/notify/NotificationClient.java +++ b/src/main/java/uk/gov/service/notify/NotificationClient.java @@ -22,6 +22,7 @@ import java.security.NoSuchAlgorithmException; import java.util.Map; import java.util.Objects; +import java.util.List; import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; @@ -138,7 +139,7 @@ public SendEmailResponse sendEmail(String templateId, String emailAddress, Map personalisation, String reference) throws NotificationClientException { - return sendEmail(templateId, emailAddress, personalisation, reference, "", null); + return sendEmail(templateId, emailAddress, personalisation, reference, "", null, null); } @Override @@ -147,7 +148,7 @@ public SendEmailResponse sendEmail(String templateId, Map personalisation, String reference, String emailReplyToId) throws NotificationClientException { - return sendEmail(templateId, emailAddress, personalisation, reference, emailReplyToId, null); + return sendEmail(templateId, emailAddress, personalisation, reference, emailReplyToId, null, null); } @Override @@ -157,6 +158,18 @@ public SendEmailResponse sendEmail(String templateId, String reference, String emailReplyToId, URI oneClickUnsubscribeURL) throws NotificationClientException { + return sendEmail(templateId, emailAddress, personalisation, reference, emailReplyToId, oneClickUnsubscribeURL, null); + } + + @Override + public SendEmailResponse sendEmail(String templateId, + String emailAddress, + Map personalisation, + String reference, + String emailReplyToId, + URI oneClickUnsubscribeURL, + List sanitiseContentFor) throws NotificationClientException { + JSONObject body = createBodyForPostRequest(templateId, null, @@ -176,6 +189,11 @@ public SendEmailResponse sendEmail(String templateId, body.put("one_click_unsubscribe_url", oneClickUnsubscribeURL); } + if(sanitiseContentFor != null) + { + body.put("sanitise_content_for", sanitiseContentFor); + } + HttpURLConnection conn = createConnectionAndSetHeaders(baseUrl + "/v2/notifications/email", "POST"); String response = performPostRequest(conn, body, HttpsURLConnection.HTTP_CREATED); return new SendEmailResponse(response); diff --git a/src/main/java/uk/gov/service/notify/NotificationClientApi.java b/src/main/java/uk/gov/service/notify/NotificationClientApi.java index fea5e4ca..9424ba19 100644 --- a/src/main/java/uk/gov/service/notify/NotificationClientApi.java +++ b/src/main/java/uk/gov/service/notify/NotificationClientApi.java @@ -4,6 +4,7 @@ import java.io.File; import java.io.InputStream; import java.net.URI; +import java.util.List; import java.util.Map; public interface NotificationClientApi { @@ -61,6 +62,27 @@ public interface NotificationClientApi { */ SendEmailResponse sendEmail(String templateId, String emailAddress, Map personalisation, String reference, String emailReplyToId, URI oneClickUnsubscribeURL) throws NotificationClientException; + /** + * The sendEmail method will create an HTTPS POST request. A JWT token will be created and added as an Authorization header to the request. + * + * @param templateId The template id is visible on the template page in the application. + * @param emailAddress The email address + * @param personalisation Map representing the placeholders for the template if any. For example, key=name value=Bob + * Can be an empty map or null when the template does not require placeholders. + * @param reference A reference specified by the service for the notification. Get all notifications can be filtered by this reference. + * This reference can be unique or used used to refer to a batch of notifications. + * Can be an empty string or null, when you do not require a reference for the notifications. + * @param emailReplyToId An optional identifier for a reply to email address for the notification, rather than use the service default. + * Service emailReplyToIds can be accessed via the service settings / manage email reply to addresses page. + * Omit this argument to use the default service email reply to address. + * @param oneClickUnsubscribeURL A link so users can unsubscribe, see https://www.notifications.service.gov.uk/using-notify/unsubscribe-links + * @param sanitiseContentFor A list of placeholders, the content of which should be sanitised + * by Notify. Notify will remove links and escape Markdown characters for the selected content. + * @return SendEmailResponse + * @throws NotificationClientException see https://docs.notifications.service.gov.uk/java.html#send-an-email-error-codes + */ + SendEmailResponse sendEmail(String templateId, String emailAddress, Map personalisation, String reference, String emailReplyToId, URI oneClickUnsubscribeURL, List sanitiseContentFor) throws NotificationClientException; + /** * The sendSms method will create an HTTPS POST request. A JWT token will be created and added as an Authorization header to the request. * diff --git a/src/main/java/uk/gov/service/notify/SendEmailResponse.java b/src/main/java/uk/gov/service/notify/SendEmailResponse.java index 8132e8fd..609dc2a6 100644 --- a/src/main/java/uk/gov/service/notify/SendEmailResponse.java +++ b/src/main/java/uk/gov/service/notify/SendEmailResponse.java @@ -16,6 +16,7 @@ public class SendEmailResponse { private final String subject; private final String fromEmail; private final URI oneClickUnsubscribeURL; + private final JSONObject sanitisedContent; public SendEmailResponse(String response) { JSONObject data = new JSONObject(response); @@ -30,6 +31,7 @@ public SendEmailResponse(String response) { templateVersion = template.getInt("version"); templateUri = template.getString("uri"); oneClickUnsubscribeURL = data.isNull("one_click_unsubscribe_url") ? null : URI.create(data.getString("one_click_unsubscribe_url")); + sanitisedContent = data.isNull("sanitised_content") ? null : data.getJSONObject("sanitised_content"); } public UUID getNotificationId() { @@ -68,6 +70,10 @@ public Optional getOneClickUnsubscribeURL() { return Optional.ofNullable(oneClickUnsubscribeURL); } + public Optional getSanitisedContent() { + return Optional.ofNullable(sanitisedContent); + } + @Override public String toString() { return "SendEmailResponse{" + @@ -80,6 +86,7 @@ public String toString() { ", subject='" + subject + '\'' + ", fromEmail='" + fromEmail + '\'' + ", oneClickUnsubscribeURL=" + oneClickUnsubscribeURL + + ", sanitisedContent=" + sanitisedContent + '}'; } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ae24944f..4202c92f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,4 +6,4 @@ # - PATCH version when you make backwards-compatible bug fixes. # # -- http://semver.org/ -project.version=6.0.1-RELEASE +project.version=6.1.0-RELEASE diff --git a/src/test/java/uk/gov/service/notify/NotificationClientTest.java b/src/test/java/uk/gov/service/notify/NotificationClientTest.java index 5cc4848c..9d4306d8 100644 --- a/src/test/java/uk/gov/service/notify/NotificationClientTest.java +++ b/src/test/java/uk/gov/service/notify/NotificationClientTest.java @@ -45,6 +45,7 @@ import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; import java.time.temporal.ChronoUnit; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -392,6 +393,44 @@ public void testSendEmailWithUnsubscribeURL() throws NotificationClientException assertEquals(oneClickUnsubscribeURL, requestReceivedByNotifyApi.getOneClickUnsubscribeURL()); } + @Test + public void testSendEmailWithsanitiseContentFor() throws NotificationClientException, IOException { + NotifyEmailResponse expected = objectMapper.readValue(this.getClass().getClassLoader().getResourceAsStream("v2_notifications_email_sanitise_personalisation_response.json"), NotifyEmailResponse.class); + wireMockRule.stubFor(post("/v2/notifications/email") + .willReturn(created() + .withResponseBody(new Body(objectMapper.writeValueAsString(expected))))); + NotificationClient client = new NotificationClient(COMBINED_API_KEY, BASE_URL); + // setting up this map can be replaced with Map.of() in later Java versions + Map personalisation = new HashMap<>(); + personalisation.put("name", "Anne Example, now [click this evil link](https://evil.link)"); + URI oneClickUnsubscribeURL = URI.create("http://localhost/unsubscribe"); + String[] placeholdersToSanitise = {"name"}; + List sanitiseContentFor = Arrays.asList(placeholdersToSanitise); + + SendEmailResponse actual = client.sendEmail("aTemplateId", "anEmailAddress", personalisation, "aReference", "an emailReplyToId", oneClickUnsubscribeURL, sanitiseContentFor); + + assertEquals(expected.getNotificationId(), actual.getNotificationId()); + assertEquals(expected.getReference(), actual.getReference().get()); + assertEquals(expected.getContent().getBody(), actual.getBody()); + assertEquals(expected.getContent().getSubject(), actual.getSubject()); + assertEquals(expected.getContent().getFromEmail(), actual.getFromEmail().get()); + // No notification uri in SendEmailResponse? +// assertEquals(expected.getUri(), actual.getUri()); + assertEquals(expected.getTemplate().getId(), actual.getTemplateId()); + assertEquals(expected.getTemplate().getVersion(), actual.getTemplateVersion()); + assertEquals(expected.getTemplate().getUri().toString(), actual.getTemplateUri()); + assertEquals(objectMapper.writeValueAsString(expected.getSanitisedContent()), actual.getSanitisedContent().orElseGet(JSONObject::new).toString()); + + LoggedRequest request = validateRequest(); + NotifyEmailRequest requestReceivedByNotifyApi = objectMapper.readValue(request.getBodyAsString(), NotifyEmailRequest.class); + assertEquals("anEmailAddress", requestReceivedByNotifyApi.getEmailAddress()); + assertEquals("aTemplateId", requestReceivedByNotifyApi.getTemplateId()); + assertEquals(personalisation, requestReceivedByNotifyApi.getPersonalisation()); + assertEquals("aReference", requestReceivedByNotifyApi.getReference()); + assertEquals("an emailReplyToId", requestReceivedByNotifyApi.getEmailReplyToId()); + assertEquals(sanitiseContentFor, requestReceivedByNotifyApi.getSanitiseContentFor()); + } + @Test public void testSendSmsHandlesErrors() { wireMockRule.stubFor(post("/v2/notifications/sms") diff --git a/src/test/java/uk/gov/service/notify/domain/NotifyEmailRequest.java b/src/test/java/uk/gov/service/notify/domain/NotifyEmailRequest.java index 0ef28280..68cfc30d 100644 --- a/src/test/java/uk/gov/service/notify/domain/NotifyEmailRequest.java +++ b/src/test/java/uk/gov/service/notify/domain/NotifyEmailRequest.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.net.URI; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -14,19 +15,24 @@ public class NotifyEmailRequest { private final String reference; private final String emailReplyToId; private final URI oneClickUnsubscribeURL; + private final List sanitiseContentFor; - public NotifyEmailRequest(@JsonProperty("email_address") String emailAddress, - @JsonProperty("template_id") String templateId, - @JsonProperty("personalisation") Map personalisation, - @JsonProperty("reference") String reference, - @JsonProperty("email_reply_to_id") String emailReplyToId, - @JsonProperty("one_click_unsubscribe_url") URI oneClickUnsubscribeURL) { + public NotifyEmailRequest( + @JsonProperty("email_address") String emailAddress, + @JsonProperty("template_id") String templateId, + @JsonProperty("personalisation") Map personalisation, + @JsonProperty("reference") String reference, + @JsonProperty("email_reply_to_id") String emailReplyToId, + @JsonProperty("one_click_unsubscribe_url") URI oneClickUnsubscribeURL, + @JsonProperty("sanitise_content_for") List sanitiseContentFor + ) { this.emailAddress = emailAddress; this.templateId = templateId; this.personalisation = personalisation; this.reference = reference; this.emailReplyToId = emailReplyToId; this.oneClickUnsubscribeURL = oneClickUnsubscribeURL; + this.sanitiseContentFor = sanitiseContentFor; } @JsonProperty("email_address") @@ -59,17 +65,24 @@ public URI getOneClickUnsubscribeURL() { return oneClickUnsubscribeURL; } + @JsonProperty("sanitise_content_for") + public List getSanitiseContentFor() { + return sanitiseContentFor; + } + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; NotifyEmailRequest request = (NotifyEmailRequest) o; - return Objects.equals(emailAddress, request.emailAddress) && Objects.equals(templateId, request.templateId) && Objects.equals(personalisation, request.personalisation) && Objects.equals(reference, request.reference) && Objects.equals(emailReplyToId, request.emailReplyToId) && Objects.equals(oneClickUnsubscribeURL, request.oneClickUnsubscribeURL); + return Objects.equals(emailAddress, request.emailAddress) && Objects.equals(templateId, request.templateId) && Objects.equals(personalisation, request.personalisation) && Objects.equals(reference, request.reference) && Objects.equals(emailReplyToId, request.emailReplyToId) && Objects.equals(oneClickUnsubscribeURL, request.oneClickUnsubscribeURL) && Objects.equals(sanitiseContentFor, request.sanitiseContentFor); } @Override public int hashCode() { - return Objects.hash(emailAddress, templateId, personalisation, reference, emailReplyToId, oneClickUnsubscribeURL); + return Objects.hash( + emailAddress, templateId, personalisation, reference, emailReplyToId, oneClickUnsubscribeURL, sanitiseContentFor + ); } @Override @@ -81,6 +94,7 @@ public String toString() { ", reference='" + reference + '\'' + ", emailReplyToId='" + emailReplyToId + '\'' + ", oneClickUnsubscribeURL=" + oneClickUnsubscribeURL + + ", sanitiseContentFor=" + sanitiseContentFor + '}'; } } diff --git a/src/test/java/uk/gov/service/notify/domain/NotifyEmailResponse.java b/src/test/java/uk/gov/service/notify/domain/NotifyEmailResponse.java index 261392eb..2e10cc9f 100644 --- a/src/test/java/uk/gov/service/notify/domain/NotifyEmailResponse.java +++ b/src/test/java/uk/gov/service/notify/domain/NotifyEmailResponse.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.net.URI; +import java.util.Map; import java.util.Objects; import java.util.UUID; @@ -116,19 +117,24 @@ public String toString() { private final URI uri; private final Template template; private final URI oneClickUnsubscribeURL; - - public NotifyEmailResponse(@JsonProperty("id") UUID notificationId, - @JsonProperty("reference") String reference, - @JsonProperty("content") Content content, - @JsonProperty("uri") URI uri, - @JsonProperty("template") Template template, - @JsonProperty("one_click_unsubscribe_url") URI oneClickUnsubscribeURL) { + private final Map> sanitisedContent; + + public NotifyEmailResponse( + @JsonProperty("id") UUID notificationId, + @JsonProperty("reference") String reference, + @JsonProperty("content") Content content, + @JsonProperty("uri") URI uri, + @JsonProperty("template") Template template, + @JsonProperty("one_click_unsubscribe_url") URI oneClickUnsubscribeURL, + @JsonProperty("sanitised_content") Map> sanitisedContent + ) { this.notificationId = notificationId; this.reference = reference; this.content = content; this.uri = uri; this.template = template; this.oneClickUnsubscribeURL = oneClickUnsubscribeURL; + this.sanitisedContent = sanitisedContent; } @JsonProperty("id") @@ -161,17 +167,22 @@ public URI getOneClickUnsubscribeURL() { return oneClickUnsubscribeURL; } + @JsonProperty("sanitised_content") + public Map> getSanitisedContent() { + return sanitisedContent; + } + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; NotifyEmailResponse that = (NotifyEmailResponse) o; - return Objects.equals(notificationId, that.notificationId) && Objects.equals(reference, that.reference) && Objects.equals(content, that.content) && Objects.equals(uri, that.uri) && Objects.equals(template, that.template) && Objects.equals(oneClickUnsubscribeURL, that.oneClickUnsubscribeURL); + return Objects.equals(notificationId, that.notificationId) && Objects.equals(reference, that.reference) && Objects.equals(content, that.content) && Objects.equals(uri, that.uri) && Objects.equals(template, that.template) && Objects.equals(oneClickUnsubscribeURL, that.oneClickUnsubscribeURL) && Objects.equals(sanitisedContent, that.sanitisedContent); } @Override public int hashCode() { - return Objects.hash(notificationId, reference, content, uri, template, oneClickUnsubscribeURL); + return Objects.hash(notificationId, reference, content, uri, template, oneClickUnsubscribeURL, sanitisedContent); } @Override @@ -183,6 +194,7 @@ public String toString() { ", uri=" + uri + ", template=" + template + ", oneClickUnsubscribeURL=" + oneClickUnsubscribeURL + + ", sanitisedContent=" + sanitisedContent + '}'; } } diff --git a/src/test/resources/v2_notifications_email_sanitise_personalisation_response.json b/src/test/resources/v2_notifications_email_sanitise_personalisation_response.json new file mode 100644 index 00000000..48cae2fe --- /dev/null +++ b/src/test/resources/v2_notifications_email_sanitise_personalisation_response.json @@ -0,0 +1,21 @@ +{ + "id": "7dd0b55b-fca1-496f-b52a-787692045f71", + "reference": "a reference", + "content": { + "body": "a content`body", + "subject": "a subject", + "from_email": "a from email" + }, + "uri": null, + "template": { + "id": "cc378915-2e6c-4e04-86a7-5f71949582b7", + "version": -1, + "uri": "http://localhost" + }, + "sanitised_content": { + "name": { + "unsanitised": "Anne Example, now [click this evil link](https://evil.link)", + "sanitised": "Anne Example, now \\[click this evil link\\]\\(\\)" + } + } +}