From 199b317f76e16b8614cb18c0e3121a29ea489391 Mon Sep 17 00:00:00 2001
From: Ricardo van der Heijden
<20791917+ricardovdheijden@users.noreply.github.com>
Date: Wed, 20 May 2026 17:25:25 +0200
Subject: [PATCH 1/7] #612 Adds run configs for IntelliJ
---
.gitignore | 1 -
.run/BE - InviteServerApplication.run.xml | 15 +++++++++++++++
.run/BE - MockProvisioningApplication.run.xml | 9 +++++++++
.run/FE - Client.run.xml | 12 ++++++++++++
4 files changed, 36 insertions(+), 1 deletion(-)
create mode 100644 .run/BE - InviteServerApplication.run.xml
create mode 100644 .run/BE - MockProvisioningApplication.run.xml
create mode 100644 .run/FE - Client.run.xml
diff --git a/.gitignore b/.gitignore
index 6b029826..01ee2385 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,7 +45,6 @@ private_key_pkcs8.pem
JSON.md
spieldata
teams-api-calls.md
-.run/
client/locale_parser.js
PlayGroundTest.java
spieldata
diff --git a/.run/BE - InviteServerApplication.run.xml b/.run/BE - InviteServerApplication.run.xml
new file mode 100644
index 00000000..91cbd183
--- /dev/null
+++ b/.run/BE - InviteServerApplication.run.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/BE - MockProvisioningApplication.run.xml b/.run/BE - MockProvisioningApplication.run.xml
new file mode 100644
index 00000000..6b166172
--- /dev/null
+++ b/.run/BE - MockProvisioningApplication.run.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/FE - Client.run.xml b/.run/FE - Client.run.xml
new file mode 100644
index 00000000..c80aff7c
--- /dev/null
+++ b/.run/FE - Client.run.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
From a186f7e44fd8c094287cf772ed15d5f315d7518c Mon Sep 17 00:00:00 2001
From: Ricardo van der Heijden
<20791917+ricardovdheijden@users.noreply.github.com>
Date: Wed, 20 May 2026 17:26:30 +0200
Subject: [PATCH 2/7] #612 resolves image and converts to base64 and preparing
it to be used in an img-tag in email
---
server/src/main/java/invite/mail/MailBox.java | 28 ++++++++++++++++++-
1 file changed, 27 insertions(+), 1 deletion(-)
diff --git a/server/src/main/java/invite/mail/MailBox.java b/server/src/main/java/invite/mail/MailBox.java
index b1a4772e..6e37ff8f 100644
--- a/server/src/main/java/invite/mail/MailBox.java
+++ b/server/src/main/java/invite/mail/MailBox.java
@@ -19,6 +19,10 @@
import java.io.IOException;
import java.io.StringWriter;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
import java.util.*;
import java.util.stream.Collectors;
@@ -73,7 +77,29 @@ public void sendInviteMail(Provisionable provisionable, Invitation invitation,
.map(idp -> idp.getName())
.orElse(user.getSchacHomeOrganization()));
variables.put("institutionLogoUrl", identityProvider
- .map(idp -> idp.getLogoUrl())
+ .map(idp -> {
+ String imageUrl = idp.getLogoUrl();
+ String htmlImgSrc;
+
+ try {
+ HttpClient client = HttpClient.newHttpClient();
+ HttpRequest request = HttpRequest.newBuilder().uri(URI.create(imageUrl)).build();
+ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
+
+ String contentType = response.headers()
+ .firstValue("Content-Type")
+ .orElse("image/png");
+
+ String base64Data = Base64.getEncoder().encodeToString(response.body());
+
+ htmlImgSrc = "data:" + contentType + ";base64," + base64Data;
+ } catch (Exception e) {
+ System.err.println("Error fetching image: " + e.getMessage());
+ htmlImgSrc = "";
+ }
+
+ return htmlImgSrc;
+ })
.orElse(null));
} else {
variables.put("institutionName", "SURF");
From 2f47dc5c3a933b2dfabbf6613cb666d58058d0e2 Mon Sep 17 00:00:00 2001
From: Ricardo van der Heijden
<20791917+ricardovdheijden@users.noreply.github.com>
Date: Wed, 20 May 2026 17:44:04 +0200
Subject: [PATCH 3/7] #612 Extracts reusable fetchAsDataUrl util function
---
.../main/java/invite/mail/ImageEmbedder.java | 44 +++++++++++++++++++
server/src/main/java/invite/mail/MailBox.java | 42 ++++++------------
2 files changed, 57 insertions(+), 29 deletions(-)
create mode 100644 server/src/main/java/invite/mail/ImageEmbedder.java
diff --git a/server/src/main/java/invite/mail/ImageEmbedder.java b/server/src/main/java/invite/mail/ImageEmbedder.java
new file mode 100644
index 00000000..f9a96dbe
--- /dev/null
+++ b/server/src/main/java/invite/mail/ImageEmbedder.java
@@ -0,0 +1,44 @@
+package invite.mail;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.Base64;
+import java.util.Optional;
+
+public class ImageEmbedder {
+
+ private static final Log LOG = LogFactory.getLog(ImageEmbedder.class);
+ private static final String DEFAULT_CONTENT_TYPE = "image/png";
+ private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
+
+ private ImageEmbedder() {
+ }
+
+ /**
+ * Fetches a remote image and returns it as a data: URL for use in an HTML.
+ *
+ * @param imageUrl the absolute URL of the image to fetch
+ * @return the data: URL, or empty if the image cannot be fetched
+ */
+ public static Optional fetchAsDataUrl(String imageUrl) {
+ try {
+ HttpRequest request = HttpRequest.newBuilder().uri(URI.create(imageUrl)).build();
+ HttpResponse response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofByteArray());
+
+ String contentType = response.headers()
+ .firstValue("Content-Type")
+ .orElse(DEFAULT_CONTENT_TYPE);
+ String base64Data = Base64.getEncoder().encodeToString(response.body());
+
+ return Optional.of("data:" + contentType + ";base64," + base64Data);
+ } catch (Exception e) {
+ LOG.warn(String.format("Error fetching image from %s: %s", imageUrl, e.getMessage()));
+ return Optional.empty();
+ }
+ }
+}
diff --git a/server/src/main/java/invite/mail/MailBox.java b/server/src/main/java/invite/mail/MailBox.java
index 6e37ff8f..c35a894e 100644
--- a/server/src/main/java/invite/mail/MailBox.java
+++ b/server/src/main/java/invite/mail/MailBox.java
@@ -2,11 +2,17 @@
import invite.cron.IdPMetaDataResolver;
import invite.cron.IdentityProvider;
-import invite.model.*;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.MustacheFactory;
+import invite.model.Authority;
+import invite.model.GroupedProviders;
+import invite.model.Invitation;
+import invite.model.Language;
+import invite.model.Provisionable;
+import invite.model.User;
+import invite.model.UserRole;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.SneakyThrows;
@@ -19,11 +25,11 @@
import java.io.IOException;
import java.io.StringWriter;
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.util.*;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
import java.util.stream.Collectors;
@SuppressWarnings("unchecked")
@@ -77,29 +83,7 @@ public void sendInviteMail(Provisionable provisionable, Invitation invitation,
.map(idp -> idp.getName())
.orElse(user.getSchacHomeOrganization()));
variables.put("institutionLogoUrl", identityProvider
- .map(idp -> {
- String imageUrl = idp.getLogoUrl();
- String htmlImgSrc;
-
- try {
- HttpClient client = HttpClient.newHttpClient();
- HttpRequest request = HttpRequest.newBuilder().uri(URI.create(imageUrl)).build();
- HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
-
- String contentType = response.headers()
- .firstValue("Content-Type")
- .orElse("image/png");
-
- String base64Data = Base64.getEncoder().encodeToString(response.body());
-
- htmlImgSrc = "data:" + contentType + ";base64," + base64Data;
- } catch (Exception e) {
- System.err.println("Error fetching image: " + e.getMessage());
- htmlImgSrc = "";
- }
-
- return htmlImgSrc;
- })
+ .flatMap(idp -> ImageEmbedder.fetchAsDataUrl(idp.getLogoUrl()))
.orElse(null));
} else {
variables.put("institutionName", "SURF");
From cda106e827e96e9ae7b9e56f6b3b43b6a3d956c6 Mon Sep 17 00:00:00 2001
From: Ricardo van der Heijden
<20791917+ricardovdheijden@users.noreply.github.com>
Date: Wed, 20 May 2026 18:26:46 +0200
Subject: [PATCH 4/7] #612 Improves readability to introduce a toDataUrl
function. Adds unit tests
---
.../main/java/invite/mail/ImageEmbedder.java | 7 +++--
.../java/invite/mail/ImageEmbedderTest.java | 28 +++++++++++++++++++
2 files changed, 33 insertions(+), 2 deletions(-)
create mode 100644 server/src/test/java/invite/mail/ImageEmbedderTest.java
diff --git a/server/src/main/java/invite/mail/ImageEmbedder.java b/server/src/main/java/invite/mail/ImageEmbedder.java
index f9a96dbe..203b640c 100644
--- a/server/src/main/java/invite/mail/ImageEmbedder.java
+++ b/server/src/main/java/invite/mail/ImageEmbedder.java
@@ -33,12 +33,15 @@ public static Optional fetchAsDataUrl(String imageUrl) {
String contentType = response.headers()
.firstValue("Content-Type")
.orElse(DEFAULT_CONTENT_TYPE);
- String base64Data = Base64.getEncoder().encodeToString(response.body());
- return Optional.of("data:" + contentType + ";base64," + base64Data);
+ return Optional.of(toDataUrl(contentType, response.body()));
} catch (Exception e) {
LOG.warn(String.format("Error fetching image from %s: %s", imageUrl, e.getMessage()));
return Optional.empty();
}
}
+
+ static String toDataUrl(String contentType, byte[] body) {
+ return "data:" + contentType + ";base64," + Base64.getEncoder().encodeToString(body);
+ }
}
diff --git a/server/src/test/java/invite/mail/ImageEmbedderTest.java b/server/src/test/java/invite/mail/ImageEmbedderTest.java
new file mode 100644
index 00000000..a4de6177
--- /dev/null
+++ b/server/src/test/java/invite/mail/ImageEmbedderTest.java
@@ -0,0 +1,28 @@
+package invite.mail;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ImageEmbedderTest {
+
+ @Test
+ void toDataUrl() {
+ // base64 encoding is "iVBORw0KGgo=".
+ byte[] pngBytes = {(byte) 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A};
+
+ String dataUrl = ImageEmbedder.toDataUrl("image/png", pngBytes);
+
+ assertEquals("data:image/png;base64,iVBORw0KGgo=", dataUrl);
+ }
+
+ @Test
+ void fetchAsDataUrlReturnsEmptyOnFailure() {
+ Optional dataUrl = ImageEmbedder.fetchAsDataUrl("not a url");
+
+ assertTrue(dataUrl.isEmpty());
+ }
+}
From 1a29c9585f9a1bce182c248fe8925292e18c9a4c Mon Sep 17 00:00:00 2001
From: Ricardo van der Heijden
<20791917+ricardovdheijden@users.noreply.github.com>
Date: Thu, 21 May 2026 11:11:59 +0200
Subject: [PATCH 5/7] #612 Adds unit tests
---
.../main/java/invite/mail/ImageEmbedder.java | 4 +-
.../java/invite/mail/ImageEmbedderTest.java | 49 +++++++++++++++++--
2 files changed, 48 insertions(+), 5 deletions(-)
diff --git a/server/src/main/java/invite/mail/ImageEmbedder.java b/server/src/main/java/invite/mail/ImageEmbedder.java
index 203b640c..38c4d71f 100644
--- a/server/src/main/java/invite/mail/ImageEmbedder.java
+++ b/server/src/main/java/invite/mail/ImageEmbedder.java
@@ -14,7 +14,7 @@ public class ImageEmbedder {
private static final Log LOG = LogFactory.getLog(ImageEmbedder.class);
private static final String DEFAULT_CONTENT_TYPE = "image/png";
- private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
+ static HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
private ImageEmbedder() {
}
@@ -41,7 +41,7 @@ public static Optional fetchAsDataUrl(String imageUrl) {
}
}
- static String toDataUrl(String contentType, byte[] body) {
+ private static String toDataUrl(String contentType, byte[] body) {
return "data:" + contentType + ";base64," + Base64.getEncoder().encodeToString(body);
}
}
diff --git a/server/src/test/java/invite/mail/ImageEmbedderTest.java b/server/src/test/java/invite/mail/ImageEmbedderTest.java
index a4de6177..075ec62a 100644
--- a/server/src/test/java/invite/mail/ImageEmbedderTest.java
+++ b/server/src/test/java/invite/mail/ImageEmbedderTest.java
@@ -1,22 +1,65 @@
package invite.mail;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpHeaders;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.List;
+import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
class ImageEmbedderTest {
+ @AfterEach
+ void resetClient() {
+ ImageEmbedder.HTTP_CLIENT = HttpClient.newHttpClient();
+ }
+
@Test
- void toDataUrl() {
+ @SuppressWarnings("unchecked")
+ void fetchAsDataUrl() throws Exception {
+ // Given
+
// base64 encoding is "iVBORw0KGgo=".
byte[] pngBytes = {(byte) 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A};
- String dataUrl = ImageEmbedder.toDataUrl("image/png", pngBytes);
+ HttpHeaders headers = HttpHeaders.of(
+ Map.of("Content-Type", List.of("image/png")),
+ (name, value) -> true);
+ HttpResponse response = mock(HttpResponse.class);
+ when(response.headers()).thenReturn(headers);
+ when(response.body()).thenReturn(pngBytes);
+
+ HttpClient mockClient = mock(HttpClient.class);
+ doReturn(response).when(mockClient).send(any(HttpRequest.class), any());
+ ImageEmbedder.HTTP_CLIENT = mockClient;
+
+ // When
+ Optional dataUrl = ImageEmbedder.fetchAsDataUrl("http://example.com/logo.png");
+
+ // Then
+ assertTrue(dataUrl.isPresent());
+ assertEquals("data:image/png;base64,iVBORw0KGgo=", dataUrl.get());
+
+ ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
+ verify(mockClient).send(requestCaptor.capture(), any());
- assertEquals("data:image/png;base64,iVBORw0KGgo=", dataUrl);
+ HttpRequest sentRequest = requestCaptor.getValue();
+ assertEquals(URI.create("http://example.com/logo.png"), sentRequest.uri());
+ assertEquals("GET", sentRequest.method());
}
@Test
From 62bfa7f1ee8aec25c1b9bbedc3f18df1666a092e Mon Sep 17 00:00:00 2001
From: Ricardo van der Heijden
<20791917+ricardovdheijden@users.noreply.github.com>
Date: Thu, 21 May 2026 11:21:42 +0200
Subject: [PATCH 6/7] #612 Using wiremock and makes httpclient private static
---
.../main/java/invite/mail/ImageEmbedder.java | 2 +-
.../java/invite/mail/ImageEmbedderTest.java | 63 ++++++-------------
2 files changed, 20 insertions(+), 45 deletions(-)
diff --git a/server/src/main/java/invite/mail/ImageEmbedder.java b/server/src/main/java/invite/mail/ImageEmbedder.java
index 38c4d71f..2a7a2377 100644
--- a/server/src/main/java/invite/mail/ImageEmbedder.java
+++ b/server/src/main/java/invite/mail/ImageEmbedder.java
@@ -14,7 +14,7 @@ public class ImageEmbedder {
private static final Log LOG = LogFactory.getLog(ImageEmbedder.class);
private static final String DEFAULT_CONTENT_TYPE = "image/png";
- static HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
+ private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
private ImageEmbedder() {
}
diff --git a/server/src/test/java/invite/mail/ImageEmbedderTest.java b/server/src/test/java/invite/mail/ImageEmbedderTest.java
index 075ec62a..fa5196ab 100644
--- a/server/src/test/java/invite/mail/ImageEmbedderTest.java
+++ b/server/src/test/java/invite/mail/ImageEmbedderTest.java
@@ -1,69 +1,44 @@
package invite.mail;
-import org.junit.jupiter.api.AfterEach;
+import invite.WireMockExtension;
import org.junit.jupiter.api.Test;
-import org.mockito.ArgumentCaptor;
+import org.junit.jupiter.api.extension.RegisterExtension;
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpHeaders;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.util.List;
-import java.util.Map;
import java.util.Optional;
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.verify;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
class ImageEmbedderTest {
- @AfterEach
- void resetClient() {
- ImageEmbedder.HTTP_CLIENT = HttpClient.newHttpClient();
- }
+ @RegisterExtension
+ WireMockExtension mockServer = new WireMockExtension(8093);
@Test
- @SuppressWarnings("unchecked")
- void fetchAsDataUrl() throws Exception {
- // Given
-
- // base64 encoding is "iVBORw0KGgo=".
+ void fetchAsDataUrl() {
+ // base64 encoding: "iVBORw0KGgo="
byte[] pngBytes = {(byte) 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A};
+ stubFor(get(urlPathEqualTo("/logo.png")).willReturn(aResponse()
+ .withHeader("Content-Type", "image/png")
+ .withBody(pngBytes)));
- HttpHeaders headers = HttpHeaders.of(
- Map.of("Content-Type", List.of("image/png")),
- (name, value) -> true);
- HttpResponse response = mock(HttpResponse.class);
- when(response.headers()).thenReturn(headers);
- when(response.body()).thenReturn(pngBytes);
-
- HttpClient mockClient = mock(HttpClient.class);
- doReturn(response).when(mockClient).send(any(HttpRequest.class), any());
- ImageEmbedder.HTTP_CLIENT = mockClient;
+ Optional dataUrl = ImageEmbedder.fetchAsDataUrl("http://localhost:8093/logo.png");
- // When
- Optional dataUrl = ImageEmbedder.fetchAsDataUrl("http://example.com/logo.png");
-
- // Then
assertTrue(dataUrl.isPresent());
assertEquals("data:image/png;base64,iVBORw0KGgo=", dataUrl.get());
-
- ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
- verify(mockClient).send(requestCaptor.capture(), any());
-
- HttpRequest sentRequest = requestCaptor.getValue();
- assertEquals(URI.create("http://example.com/logo.png"), sentRequest.uri());
- assertEquals("GET", sentRequest.method());
+ verify(getRequestedFor(urlPathEqualTo("/logo.png")));
}
@Test
void fetchAsDataUrlReturnsEmptyOnFailure() {
+ // URI.create rejects strings with whitespace, so the util's try/catch
+ // produces Optional.empty() without making any HTTP call.
Optional dataUrl = ImageEmbedder.fetchAsDataUrl("not a url");
assertTrue(dataUrl.isEmpty());
From 685d00e69ebd3e76703895c2bc6abad911cb4066 Mon Sep 17 00:00:00 2001
From: Ricardo van der Heijden
<20791917+ricardovdheijden@users.noreply.github.com>
Date: Fri, 22 May 2026 10:49:51 +0200
Subject: [PATCH 7/7] #612 Implements a limit on the logo size. Currently set
to 1MB. Adds unit tests
---
.../main/java/invite/mail/ImageEmbedder.java | 26 +++++++++++++++--
.../java/invite/mail/ImageEmbedderTest.java | 28 +++++++++++++++++--
2 files changed, 50 insertions(+), 4 deletions(-)
diff --git a/server/src/main/java/invite/mail/ImageEmbedder.java b/server/src/main/java/invite/mail/ImageEmbedder.java
index 2a7a2377..195a3391 100644
--- a/server/src/main/java/invite/mail/ImageEmbedder.java
+++ b/server/src/main/java/invite/mail/ImageEmbedder.java
@@ -3,6 +3,7 @@
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
+import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
@@ -15,6 +16,7 @@ public class ImageEmbedder {
private static final Log LOG = LogFactory.getLog(ImageEmbedder.class);
private static final String DEFAULT_CONTENT_TYPE = "image/png";
private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
+ private static final int MAX_IMAGE_BYTES = 1 * 1024 * 1024; // 1 MegaByte
private ImageEmbedder() {
}
@@ -28,19 +30,39 @@ private ImageEmbedder() {
public static Optional fetchAsDataUrl(String imageUrl) {
try {
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(imageUrl)).build();
- HttpResponse response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofByteArray());
+ HttpResponse response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofInputStream());
+
+ Optional body = readBounded(response.body(), imageUrl);
+ if (body.isEmpty()) {
+ return Optional.empty();
+ }
String contentType = response.headers()
.firstValue("Content-Type")
.orElse(DEFAULT_CONTENT_TYPE);
- return Optional.of(toDataUrl(contentType, response.body()));
+ return Optional.of(toDataUrl(contentType, body.get()));
} catch (Exception e) {
LOG.warn(String.format("Error fetching image from %s: %s", imageUrl, e.getMessage()));
return Optional.empty();
}
}
+ private static Optional readBounded(InputStream source, String imageUrl) {
+ try (InputStream in = source) {
+ byte[] bytes = in.readNBytes(MAX_IMAGE_BYTES + 1);
+ if (bytes.length > MAX_IMAGE_BYTES) {
+ LOG.warn(String.format("Image at %s exceeds maximum size of %d bytes; aborting download",
+ imageUrl, MAX_IMAGE_BYTES));
+ return Optional.empty();
+ }
+ return Optional.of(bytes);
+ } catch (Exception e) {
+ LOG.warn(String.format("Error reading image from %s: %s", imageUrl, e.getMessage()));
+ return Optional.empty();
+ }
+ }
+
private static String toDataUrl(String contentType, byte[] body) {
return "data:" + contentType + ";base64," + Base64.getEncoder().encodeToString(body);
}
diff --git a/server/src/test/java/invite/mail/ImageEmbedderTest.java b/server/src/test/java/invite/mail/ImageEmbedderTest.java
index fa5196ab..8c532ec1 100644
--- a/server/src/test/java/invite/mail/ImageEmbedderTest.java
+++ b/server/src/test/java/invite/mail/ImageEmbedderTest.java
@@ -1,5 +1,6 @@
package invite.mail;
+import com.github.tomakehurst.wiremock.http.Fault;
import invite.WireMockExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
@@ -35,10 +36,33 @@ void fetchAsDataUrl() {
verify(getRequestedFor(urlPathEqualTo("/logo.png")));
}
+ @Test
+ void fetchAsDataUrlReturnsEmptyWhenImageExceedsMaxSize() {
+ // One byte over the 1 MB MAX_IMAGE_BYTES cap
+ byte[] oversizedBody = new byte[1024 * 1024 + 1];
+ stubFor(get(urlPathEqualTo("/huge.png")).willReturn(aResponse()
+ .withHeader("Content-Type", "image/png")
+ .withBody(oversizedBody)));
+
+ Optional dataUrl = ImageEmbedder.fetchAsDataUrl("http://localhost:8093/huge.png");
+
+ assertTrue(dataUrl.isEmpty());
+ verify(getRequestedFor(urlPathEqualTo("/huge.png")));
+ }
+
+ @Test
+ void fetchAsDataUrlReturnsEmptyOnBodyReadFailure() {
+ stubFor(get(urlPathEqualTo("/broken.png")).willReturn(aResponse()
+ .withFault(Fault.MALFORMED_RESPONSE_CHUNK)));
+
+ Optional dataUrl = ImageEmbedder.fetchAsDataUrl("http://localhost:8093/broken.png");
+
+ assertTrue(dataUrl.isEmpty());
+ verify(getRequestedFor(urlPathEqualTo("/broken.png")));
+ }
+
@Test
void fetchAsDataUrlReturnsEmptyOnFailure() {
- // URI.create rejects strings with whitespace, so the util's try/catch
- // produces Optional.empty() without making any HTTP call.
Optional dataUrl = ImageEmbedder.fetchAsDataUrl("not a url");
assertTrue(dataUrl.isEmpty());