From 535a42658325b37b6ac32c4f0b18e3360efd5bdd Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 9 Oct 2026 20:59:36 +0000 Subject: [PATCH 1/2] Add email verification inputs Add verificationTime and wasVerificationSuccessful to the Email request object. They send /email/verification_time and /email/was_verification_successful. These describe the most recent verification of the email address. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 ++ README.md | 2 + .../com/maxmind/minfraud/request/Email.java | 48 +++++++++++++++++++ .../maxmind/minfraud/request/EmailTest.java | 46 ++++++++++++++++++ .../minfraud/request/RequestTestHelper.java | 4 ++ .../test-data/full-request-email-md5.json | 4 +- .../resources/test-data/full-request.json | 4 +- 7 files changed, 110 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a202025..934f13fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ CHANGELOG `DELIVERED_CODE`, `NETWORK`, and `OTHER`. Do not set `phoneVerificationMethod` or `phoneWasVerificationSuccessful` if no verification was attempted. +* Added `verificationTime` and `wasVerificationSuccessful` to the `Email` + request object. These describe the most recent verification of the email + address. Do not set `wasVerificationSuccessful` if no verification was + attempted. * Fixed enum serialization, enum deserialization, and email address hashing in locales that change case conversion, such as Turkish (`tr`) and Azerbaijani (`az`). In those locales, some enum values serialized with a diff --git a/README.md b/README.md index caffc206..d48ed6f4 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,8 @@ Transaction request = new Transaction.Builder( new Email.Builder() .address("fraud@ster.com") .domain("ster.com") + .verificationTime(ZonedDateTime.parse("2026-10-01T14:30:00Z")) + .wasVerificationSuccessful(true) .build() ).event( new Event.Builder() diff --git a/src/main/java/com/maxmind/minfraud/request/Email.java b/src/main/java/com/maxmind/minfraud/request/Email.java index 5840821a..47f37721 100644 --- a/src/main/java/com/maxmind/minfraud/request/Email.java +++ b/src/main/java/com/maxmind/minfraud/request/Email.java @@ -8,6 +8,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.Normalizer; +import java.time.ZonedDateTime; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -22,6 +23,8 @@ public final class Email extends AbstractModel { private final String address; private final boolean hashAddress; private final String domain; + private final ZonedDateTime verificationTime; + private final Boolean wasVerificationSuccessful; private static final Map typoDomains; private static final Map typoTlds; private static final Map equivalentDomains; @@ -299,6 +302,8 @@ private Email(Email.Builder builder) { address = builder.address; hashAddress = builder.hashAddress; domain = builder.domain; + verificationTime = builder.verificationTime; + wasVerificationSuccessful = builder.wasVerificationSuccessful; } /** @@ -309,6 +314,8 @@ public static final class Builder { private String address; private boolean hashAddress; private String domain; + private ZonedDateTime verificationTime; + private Boolean wasVerificationSuccessful; /** * The constructor for the builder. @@ -384,6 +391,30 @@ public Email.Builder domain(String domain) { return this; } + /** + * @param time The date and time of the most recent attempt to verify the email address, + * if known. The time must not be before 1970-01-01T00:00:00Z or more than + * two days in the future. The web service ignores a time outside this range + * and returns a warning. + * @return The builder object. + */ + public Email.Builder verificationTime(ZonedDateTime time) { + verificationTime = time; + return this; + } + + /** + * @param wasSuccessful Whether the most recent verification that the end user controls + * the email address succeeded, for example by confirming a link + * sent to the address. Do not set this if no verification was + * attempted. + * @return The builder object. + */ + public Email.Builder wasVerificationSuccessful(Boolean wasSuccessful) { + wasVerificationSuccessful = wasSuccessful; + return this; + } + /** * @return An instance of {@code Email} created from the fields set on this builder. */ @@ -572,4 +603,21 @@ private static boolean isValidDomainLabel(String label) { public String domain() { return domain; } + + /** + * @return The date and time of the most recent attempt to verify the email address. + */ + @JsonProperty("verification_time") + public ZonedDateTime verificationTime() { + return verificationTime; + } + + /** + * @return Whether the most recent verification that the end user controls the email + * address succeeded. + */ + @JsonProperty("was_verification_successful") + public Boolean wasVerificationSuccessful() { + return wasVerificationSuccessful; + } } diff --git a/src/test/java/com/maxmind/minfraud/request/EmailTest.java b/src/test/java/com/maxmind/minfraud/request/EmailTest.java index 820ae1b0..b9d4f98e 100644 --- a/src/test/java/com/maxmind/minfraud/request/EmailTest.java +++ b/src/test/java/com/maxmind/minfraud/request/EmailTest.java @@ -1,6 +1,7 @@ package com.maxmind.minfraud.request; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -10,11 +11,14 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.skyscreamer.jsonassert.JSONAssert; public class EmailTest { @@ -318,4 +322,46 @@ void testInvalidDomains(String invalidDomain) { () -> new Builder().domain(invalidDomain).build() ); } + + @Test + public void testVerificationTime() { + var time = ZonedDateTime.now(); + var email = new Builder().verificationTime(time).build(); + assertEquals(time, email.verificationTime()); + } + + @Test + public void testWasVerificationSuccessful() { + var email = new Builder().wasVerificationSuccessful(false).build(); + assertFalse(email.wasVerificationSuccessful()); + } + + @Test + public void testVerificationSerialization() throws Exception { + var email = new Builder() + .verificationTime(ZonedDateTime.parse("2026-10-01T14:30:00Z")) + .wasVerificationSuccessful(false) + .build(); + + var expectedJSON = "{verification_time:'2026-10-01T14:30:00Z'," + + "was_verification_successful:false}"; + JSONAssert.assertEquals(expectedJSON, email.toJson(), true); + } + + @Test + public void testVerificationTimeSerializationWithRegionZone() throws Exception { + var email = new Builder() + .verificationTime( + ZonedDateTime.of(2026, 10, 1, 10, 30, 0, 0, ZoneId.of("America/New_York"))) + .build(); + + JSONAssert.assertEquals( + "{verification_time:'2026-10-01T10:30:00-04:00'}", email.toJson(), true); + } + + @Test + public void testVerificationOmittedWhenUnset() throws Exception { + var email = new Builder().build(); + JSONAssert.assertEquals("{}", email.toJson(), true); + } } diff --git a/src/test/java/com/maxmind/minfraud/request/RequestTestHelper.java b/src/test/java/com/maxmind/minfraud/request/RequestTestHelper.java index 1c20de68..304a0efa 100644 --- a/src/test/java/com/maxmind/minfraud/request/RequestTestHelper.java +++ b/src/test/java/com/maxmind/minfraud/request/RequestTestHelper.java @@ -34,6 +34,8 @@ public static Transaction fullTransaction() throws Exception { return makeTransaction(new Email.Builder() .address("test@maxmind.com") .domain("maxmind.com") + .verificationTime(ZonedDateTime.parse("2026-10-01T14:30:00Z")) + .wasVerificationSuccessful(true) .build()); } @@ -42,6 +44,8 @@ public static Transaction fullTransactionEmailMd5() throws Exception { .address("test@maxmind.com") .hashAddress() .domain("maxmind.com") + .verificationTime(ZonedDateTime.parse("2026-10-01T14:30:00Z")) + .wasVerificationSuccessful(true) .build()); } diff --git a/src/test/resources/test-data/full-request-email-md5.json b/src/test/resources/test-data/full-request-email-md5.json index 361ad816..e77ff164 100644 --- a/src/test/resources/test-data/full-request-email-md5.json +++ b/src/test/resources/test-data/full-request-email-md5.json @@ -12,7 +12,9 @@ }, "email": { "address": "977577b140bfb7c516e4746204fbdb01", - "domain": "maxmind.com" + "domain": "maxmind.com", + "verification_time": "2026-10-01T14:30:00Z", + "was_verification_successful": true }, "billing": { "first_name": "First", diff --git a/src/test/resources/test-data/full-request.json b/src/test/resources/test-data/full-request.json index 17fb0b9e..531a5de5 100644 --- a/src/test/resources/test-data/full-request.json +++ b/src/test/resources/test-data/full-request.json @@ -12,7 +12,9 @@ }, "email": { "address": "test@maxmind.com", - "domain": "maxmind.com" + "domain": "maxmind.com", + "verification_time": "2026-10-01T14:30:00Z", + "was_verification_successful": true }, "billing": { "first_name": "First", From 7b97801c514f685fe6a757a86b65cc835ed21be6 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Mon, 26 Oct 2026 18:48:25 +0000 Subject: [PATCH 2/2] Use a recent event time in test data The test event time was years before the test verification times, so the most recent verification came after the event. The web service also ignores an event time older than one year. Co-Authored-By: Claude Opus 5.5 --- .../java/com/maxmind/minfraud/request/RequestTestHelper.java | 2 +- src/test/resources/test-data/full-request-email-md5.json | 2 +- src/test/resources/test-data/full-request.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/maxmind/minfraud/request/RequestTestHelper.java b/src/test/java/com/maxmind/minfraud/request/RequestTestHelper.java index 304a0efa..b601a2d1 100644 --- a/src/test/java/com/maxmind/minfraud/request/RequestTestHelper.java +++ b/src/test/java/com/maxmind/minfraud/request/RequestTestHelper.java @@ -66,7 +66,7 @@ private static Transaction makeTransaction(Email e) throws Exception { .party(Event.Party.CUSTOMER) .transactionId("txn3134133") .shopId("s2123") - .time(ZonedDateTime.parse("2012-04-12T23:20:50.52Z")) + .time(ZonedDateTime.parse("2026-10-05T23:20:50.52Z")) .type(Event.Type.PURCHASE) .build() ) diff --git a/src/test/resources/test-data/full-request-email-md5.json b/src/test/resources/test-data/full-request-email-md5.json index e77ff164..53091331 100644 --- a/src/test/resources/test-data/full-request-email-md5.json +++ b/src/test/resources/test-data/full-request-email-md5.json @@ -3,7 +3,7 @@ "party": "customer", "transaction_id": "txn3134133", "shop_id": "s2123", - "time": "2012-04-12T23:20:50.52Z", + "time": "2026-10-05T23:20:50.52Z", "type": "purchase" }, "account": { diff --git a/src/test/resources/test-data/full-request.json b/src/test/resources/test-data/full-request.json index 531a5de5..323e2ba2 100644 --- a/src/test/resources/test-data/full-request.json +++ b/src/test/resources/test-data/full-request.json @@ -3,7 +3,7 @@ "party": "customer", "transaction_id": "txn3134133", "shop_id": "s2123", - "time": "2012-04-12T23:20:50.52Z", + "time": "2026-10-05T23:20:50.52Z", "type": "purchase" }, "account": {