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",