Skip to content

Commit 4e9d42e

Browse files
authored
Merge pull request #1970 from maxmind/greg/email-verification-inputs
STF-2006: Add email verification inputs
2 parents 5c99130 + 890cbe3 commit 4e9d42e

5 files changed

Lines changed: 128 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ CHANGELOG
1414
by providing `phoneVerificationMethod`, `phoneWasVerificationSuccessful`,
1515
and `phoneVerificationTime` to `Billing` or `Shipping`. The
1616
`PhoneVerificationMethod` enum lists the valid methods.
17+
* Added the inputs `/email/verification_time` and
18+
`/email/was_verification_successful`. They describe the most recent
19+
verification of the email address. You may provide them by providing
20+
`verificationTime` and `wasVerificationSuccessful` to `Email`.
1721
* `Event` now throws an `ArgumentError` if `time` is not a valid `Date`.
1822
Before, the client sent an invalid `Date` as `null` and did not report an
1923
error.

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ try {
206206
email: new minFraud.Email({
207207
address: 'foo@bar.com',
208208
domain: 'bar.com',
209+
verificationTime: new Date('2026-10-01T14:25:00Z'),
210+
wasVerificationSuccessful: true,
209211
}),
210212
billing: new minFraud.Billing({
211213
address: '123 Robot Ave.',

‎src/request/email.spec.ts‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,63 @@ import crypto from 'node:crypto';
44
import Email from './email.js';
55

66
describe('Email()', () => {
7+
it('stores the verification properties', () => {
8+
const time = new Date('2026-10-01T14:25:00Z');
9+
const email = new Email({
10+
domain: 'example.com',
11+
verificationTime: time,
12+
wasVerificationSuccessful: false,
13+
});
14+
15+
expect(email.domain).toBe('example.com');
16+
expect(email.verificationTime).toEqual(time);
17+
expect(email.wasVerificationSuccessful).toBe(false);
18+
});
19+
20+
it('copies `verificationTime`', () => {
21+
const time = new Date('2026-10-01T14:25:00Z');
22+
const email = new Email({ verificationTime: time });
23+
24+
time.setTime(0);
25+
26+
expect(email.verificationTime).toEqual(new Date('2026-10-01T14:25:00Z'));
27+
});
28+
29+
it('accepts null verification properties', () => {
30+
expect(
31+
() =>
32+
new Email({
33+
verificationTime: null,
34+
wasVerificationSuccessful: null,
35+
} as unknown as ConstructorParameters<typeof Email>[0])
36+
).not.toThrow();
37+
});
38+
39+
it.each([
40+
['an invalid Date', new Date('not a date')],
41+
['a string', '2026-10-01T14:25:00Z'],
42+
['a number', 1790000000000],
43+
])('throws an ArgumentError if verificationTime is %s', (_, value) => {
44+
const test = () =>
45+
new Email({
46+
verificationTime: value as unknown as Date,
47+
});
48+
expect(test).toThrow(ArgumentError);
49+
expect(test).toThrow('verificationTime');
50+
});
51+
52+
it.each(['true', 1, 0])(
53+
'throws an ArgumentError for wasVerificationSuccessful %j',
54+
(value) => {
55+
const test = () =>
56+
new Email({
57+
wasVerificationSuccessful: value as unknown as boolean,
58+
});
59+
expect(test).toThrow(ArgumentError);
60+
expect(test).toThrow('wasVerificationSuccessful');
61+
}
62+
);
63+
764
it('throws an error if email.address is not valid', () => {
865
const email = () =>
966
new Email({

‎src/request/email.ts‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import crypto from 'node:crypto';
22
import { domainToASCII } from 'node:url';
33
import { ArgumentError } from '../errors.js';
4+
import { assertValidDate } from '../utils.js';
45

56
// A fully-qualified domain name: two or more dot-separated labels of letters
67
// (including non-ASCII), combining marks, digits, or hyphens, with no label
@@ -51,6 +52,19 @@ interface EmailProps {
5152
* true, the address will instead be sent as an MD5 hash.
5253
*/
5354
hashAddress?: boolean;
55+
/**
56+
* The date and time of the most recent attempt to verify the email address,
57+
* if known. The time must not be before 1970-01-01T00:00:00Z or more than two
58+
* days in the future. The web service ignores a time outside this range and
59+
* returns a warning.
60+
*/
61+
verificationTime?: Date;
62+
/**
63+
* Whether the most recent verification that the end user controls the email
64+
* address succeeded, for example by confirming a link sent to the address.
65+
* Omit this if no verification was attempted.
66+
*/
67+
wasVerificationSuccessful?: boolean;
5468
}
5569

5670
/**
@@ -64,6 +78,19 @@ export default class Email implements EmailProps {
6478
public address?: string;
6579
/** @inheritDoc EmailProps.domain */
6680
public domain?: string;
81+
/**
82+
* The date and time of the most recent attempt to verify the email address,
83+
* if known. The time must not be before 1970-01-01T00:00:00Z or more than two
84+
* days in the future. The web service ignores a time outside this range and
85+
* returns a warning.
86+
*/
87+
public verificationTime?: Date;
88+
/**
89+
* Whether the most recent verification that the end user controls the email
90+
* address succeeded, for example by confirming a link sent to the address.
91+
* Omit this if no verification was attempted.
92+
*/
93+
public wasVerificationSuccessful?: boolean;
6794

6895
private static readonly typoDomains: { [key: string]: string } = {
6996
// gmail.com
@@ -327,6 +354,19 @@ export default class Email implements EmailProps {
327354
throw new ArgumentError('`email.domain` is an invalid domain');
328355
}
329356

357+
if (email.verificationTime != null) {
358+
assertValidDate(email.verificationTime, 'email.verificationTime');
359+
}
360+
361+
if (
362+
email.wasVerificationSuccessful != null &&
363+
typeof email.wasVerificationSuccessful !== 'boolean'
364+
) {
365+
throw new ArgumentError(
366+
'`email.wasVerificationSuccessful` must be a boolean'
367+
);
368+
}
369+
330370
if (email.address) {
331371
if (email.hashAddress) {
332372
this.address = crypto.hash(
@@ -344,6 +384,14 @@ export default class Email implements EmailProps {
344384
if (email.domain == null && email.address != null) {
345385
this.domain = email.address.substring(email.address.indexOf('@') + 1);
346386
}
387+
388+
// Copy the Date so that a later change to the caller's object does not
389+
// change the request.
390+
if (email.verificationTime != null) {
391+
this.verificationTime = new Date(email.verificationTime.getTime());
392+
}
393+
394+
this.wasVerificationSuccessful = email.wasVerificationSuccessful;
347395
}
348396

349397
private cleanEmailAddress(address: string) {

‎src/request/transaction.spec.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,23 @@ describe('Transaction()', () => {
250250
expect(test.toString()).toContain('"email":{"domain":"foo.com"}');
251251
});
252252

253+
it('it handles email verification fields', () => {
254+
const test = new Transaction({
255+
device: new Device({
256+
ipAddress: '1.1.1.1',
257+
}),
258+
email: new Email({
259+
verificationTime: new Date('2026-10-01T14:25:00Z'),
260+
wasVerificationSuccessful: false,
261+
}),
262+
});
263+
264+
expect(JSON.parse(test.toString()).email).toEqual({
265+
verification_time: '2026-10-01T14:25:00.000Z',
266+
was_verification_successful: false,
267+
});
268+
});
269+
253270
it('it handles optional billing field', () => {
254271
const test = new Transaction({
255272
billing: new Billing({

0 commit comments

Comments
 (0)