diff --git a/README.md b/README.md index 8089d5d..94d53f1 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,11 @@ SendEmailResponse response = email // Metadata and tags .metadata(metadata) - .tag("welcome", "onboarding") + .tag("legacy-tag") + .tags( + new MessageTag("campaign", "welcome"), + new MessageTag("customer", "new") + ) // Idempotency .idempotencyKey("unique-request-key") @@ -109,6 +113,9 @@ SendEmailResponse response = email .send(); ``` +`tag()` remains available for the legacy single tag. The previous map form of +`tags()` also remains available. + Existing constructor-based sending usage still works: ```java diff --git a/src/main/java/co/lettermint/endpoints/EmailEndpoint.java b/src/main/java/co/lettermint/endpoints/EmailEndpoint.java index 4ddf102..6d40b3d 100644 --- a/src/main/java/co/lettermint/endpoints/EmailEndpoint.java +++ b/src/main/java/co/lettermint/endpoints/EmailEndpoint.java @@ -3,6 +3,7 @@ import co.lettermint.client.LettermintClient; import co.lettermint.models.Attachment; import co.lettermint.models.SendEmailResponse; +import co.lettermint.models.MessageTag; import co.lettermint.models.api.SendMailRequest; import co.lettermint.models.api.SendMailResponse; import com.fasterxml.jackson.core.type.TypeReference; @@ -28,7 +29,7 @@ public class EmailEndpoint extends Endpoint { private String route; private Map metadata; private String tag; - private List> tags; + private List tags; private Map settings; private String idempotencyKey; @@ -211,6 +212,9 @@ public EmailEndpoint metadata(String key, Object value) { * Set the email tag. */ public EmailEndpoint tag(String tag) { + if (tag != null && tags.size() >= 20) { + throw new IllegalArgumentException("A legacy tag and no more than 19 message tags are permitted"); + } this.tag = tag; return this; } @@ -220,6 +224,27 @@ public EmailEndpoint tag(String tag) { */ @SafeVarargs public final EmailEndpoint tags(Map... tags) { + MessageTag[] normalized = new MessageTag[tags.length]; + for (int index = 0; index < tags.length; index++) { + normalized[index] = new MessageTag(tags[index].get("name"), tags[index].get("value")); + } + return tags(normalized); + } + + /** + * Set typed reusable name-value tags for the email. + */ + public final EmailEndpoint tags(MessageTag... tags) { + int maximum = tag == null ? 20 : 19; + if (tags.length > maximum) { + throw new IllegalArgumentException("No more than " + maximum + " message tags are permitted"); + } + Set names = new HashSet<>(); + for (MessageTag messageTag : tags) { + if (!names.add(messageTag.getName())) { + throw new IllegalArgumentException("Message tag names must be unique and case-sensitive"); + } + } this.tags = new ArrayList<>(Arrays.asList(tags)); return this; } diff --git a/src/main/java/co/lettermint/models/MessageTag.java b/src/main/java/co/lettermint/models/MessageTag.java new file mode 100644 index 0000000..264c314 --- /dev/null +++ b/src/main/java/co/lettermint/models/MessageTag.java @@ -0,0 +1,35 @@ +package co.lettermint.models; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Locale; +import java.util.regex.Pattern; + +/** A reusable exact-match message tag. */ +public final class MessageTag { + private static final Pattern NAME = Pattern.compile("^[A-Za-z0-9_-]{1,32}$"); + private static final Pattern VALUE = Pattern.compile("^[A-Za-z0-9_-]{1,64}$"); + + @JsonProperty("name") + private final String name; + + @JsonProperty("value") + private final String value; + + public MessageTag(String name, String value) { + if (name == null || !NAME.matcher(name).matches()) { + throw new IllegalArgumentException("Message tag names must match ^[A-Za-z0-9_-]{1,32}$"); + } + if (name.toLowerCase(Locale.ROOT).startsWith("__lettermint")) { + throw new IllegalArgumentException("Message tag names must not start with __lettermint"); + } + if (value == null || !VALUE.matcher(value).matches()) { + throw new IllegalArgumentException("Message tag values must match ^[A-Za-z0-9_-]{1,64}$"); + } + this.name = name; + this.value = value; + } + + public String getName() { return name; } + public String getValue() { return value; } +} diff --git a/src/test/java/co/lettermint/EmailEndpointTest.java b/src/test/java/co/lettermint/EmailEndpointTest.java index 5cc526c..961f9ab 100644 --- a/src/test/java/co/lettermint/EmailEndpointTest.java +++ b/src/test/java/co/lettermint/EmailEndpointTest.java @@ -1,6 +1,7 @@ package co.lettermint; import co.lettermint.models.SendEmailResponse; +import co.lettermint.models.MessageTag; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; @@ -263,4 +264,36 @@ void testSingleMetadataMethod() throws Exception { assertTrue(body.contains("\"key1\":\"value1\"")); assertTrue(body.contains("\"key2\":123")); } + + @Test + void testTypedMessageTags() throws Exception { + mockWebServer.enqueue(new MockResponse() + .setBody("{\"message_id\": \"msg_123\", \"status\": \"queued\"}") + .setHeader("Content-Type", "application/json")); + + lettermint.email() + .from("sender@example.com") + .to("recipient@example.com") + .subject("Test") + .text("Test") + .tags(new MessageTag("campaign", "welcome"), new MessageTag("customer", "new")) + .send(); + + String body = mockWebServer.takeRequest().getBody().readUtf8(); + assertTrue(body.contains("\"tags\":[{\"name\":\"campaign\",\"value\":\"welcome\"}")); + } + + @Test + void testMessageTagValidation() { + assertThrows(IllegalArgumentException.class, () -> new MessageTag("invalid name", "value")); + assertThrows(IllegalArgumentException.class, () -> new MessageTag("__LETTERMINT_internal", "value")); + assertThrows(IllegalArgumentException.class, () -> lettermint.email().tags( + new MessageTag("same", "one"), new MessageTag("same", "two"))); + + MessageTag[] tags = new MessageTag[20]; + for (int index = 0; index < tags.length; index++) { + tags[index] = new MessageTag("tag_" + index, "value"); + } + assertThrows(IllegalArgumentException.class, () -> lettermint.email().tag("legacy").tags(tags)); + } }