Skip to content

Commit cb318a3

Browse files
authored
Merge pull request #50 from emailjs-com/validate-headless
Block the headless browsers
2 parents 98a54b1 + bbf35ec commit cb318a3

File tree

13 files changed

+213
-9
lines changed

13 files changed

+213
-9
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,10 @@ Options can be declared globally using the **init** method or locally as the fou
115115
\
116116
The local parameter will have higher priority than the global one.
117117

118-
| Name | Type | Description |
119-
| --------- | ------ | ------------------------------------------------- |
120-
| publicKey | String | The public key is required to invoke the command. |
118+
| Name | Type | Default | Description |
119+
| ------------- | ------- | ------- | -------------------------------------------------------- |
120+
| publicKey | String | | The public key is required to invoke the method. |
121+
| blockHeadless | Boolean | False | Method will return error 451 if the browser is headless. |
121122

122123
**Declare global settings**
123124

@@ -126,6 +127,7 @@ import emailjs from '@emailjs/browser';
126127

127128
emailjs.init({
128129
publicKey: 'YOUR_PUBLIC_KEY',
130+
blockHeadless: true,
129131
});
130132
```
131133

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { it, expect } from '@jest/globals';
2+
3+
import { EmailJSResponseStatus } from '../../models/EmailJSResponseStatus';
4+
import { headlessError } from './headlessError';
5+
6+
it('should return EmailJSResponseStatus', () => {
7+
expect(headlessError()).toBeInstanceOf(EmailJSResponseStatus);
8+
});
9+
10+
it('should return status 451', () => {
11+
expect(headlessError()).toEqual({
12+
status: 451,
13+
text: 'Unavailable For Headless Browser',
14+
});
15+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { EmailJSResponseStatus } from '../../models/EmailJSResponseStatus';
2+
3+
export const headlessError = () => {
4+
return new EmailJSResponseStatus(451, 'Unavailable For Headless Browser');
5+
};

src/it.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,21 @@ describe('send method', () => {
2626
expect(error).toBeUndefined();
2727
}
2828
});
29+
30+
it('should call the init and the send method successfully as promise', () => {
31+
emailjs.init({
32+
publicKey: 'C2JWGTestKeySomething',
33+
});
34+
35+
return emailjs.send('default_service', 'my_test_template').then(
36+
(result) => {
37+
expect(result).toEqual({ status: 200, text: 'OK' });
38+
},
39+
(error) => {
40+
expect(error).toBeUndefined();
41+
},
42+
);
43+
});
2944
});
3045

3146
describe('send-form method', () => {
@@ -43,4 +58,21 @@ describe('send-form method', () => {
4358
expect(error).toBeUndefined();
4459
}
4560
});
61+
62+
it('should call the init and the sendForm method successfully as promise', () => {
63+
const form: HTMLFormElement = document.createElement('form');
64+
65+
emailjs.init({
66+
publicKey: 'C2JWGTestKeySomething',
67+
});
68+
69+
return emailjs.sendForm('default_service', 'my_test_template', form).then(
70+
(result) => {
71+
expect(result).toEqual({ status: 200, text: 'OK' });
72+
},
73+
(error) => {
74+
expect(error).toBeUndefined();
75+
},
76+
);
77+
});
4678
});

src/methods/init/init.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const init = (
1717
const opts = buildOptions(options);
1818

1919
store.publicKey = opts.publicKey;
20+
store.blockHeadless = opts.blockHeadless;
2021
store.limitRate = opts.limitRate || store.limitRate;
2122
store.blockList = opts.blockList || store.blockList;
2223
store.origin = opts.origin || origin;

src/methods/send/send.spec.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,43 @@ describe('sdk v4', () => {
5555
).toThrow('The service ID is required');
5656
});
5757

58+
it('should call the send method and fail on headless', async () => {
59+
try {
60+
const result = await send(
61+
'default_service',
62+
'my_test_template',
63+
{},
64+
{
65+
publicKey: 'C2JWGTestKeySomething',
66+
blockHeadless: true,
67+
},
68+
);
69+
expect(result).toBeUndefined();
70+
} catch (error) {
71+
expect(error).toEqual({
72+
status: 451,
73+
text: 'Unavailable For Headless Browser',
74+
});
75+
}
76+
});
77+
78+
it('should call the send method and fail on headless as promise', () => {
79+
return send('', 'my_test_template', undefined, {
80+
publicKey: 'C2JWGTestKeySomething',
81+
blockHeadless: true,
82+
}).then(
83+
(result) => {
84+
expect(result).toBeUndefined();
85+
},
86+
(error) => {
87+
expect(error).toEqual({
88+
status: 451,
89+
text: 'Unavailable For Headless Browser',
90+
});
91+
},
92+
);
93+
});
94+
5895
it('should call the send method and fail on the template ID', () => {
5996
expect(() =>
6097
send('default_service', '', undefined, {
@@ -78,4 +115,22 @@ describe('sdk v4', () => {
78115
expect(error).toBeUndefined();
79116
}
80117
});
118+
119+
it('should call the send method as promise', () => {
120+
return send(
121+
'default_service',
122+
'my_test_template',
123+
{},
124+
{
125+
publicKey: 'C2JWGTestKeySomething',
126+
},
127+
).then(
128+
(result) => {
129+
expect(result).toEqual({ status: 200, text: 'OK' });
130+
},
131+
(error) => {
132+
expect(error).toBeUndefined();
133+
},
134+
);
135+
});
81136
});

src/methods/send/send.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
import type { EmailJSResponseStatus } from '../../models/EmailJSResponseStatus';
2+
import type { Options } from '../../types/Options';
3+
14
import { store } from '../../store/store';
25
import { sendPost } from '../../api/sendPost';
36
import { buildOptions } from '../../utils/buildOptions/buildOptions';
47
import { validateParams } from '../../utils/validateParams/validateParams';
58
import { validateTemplateParams } from '../../utils/validateTemplateParams/validateTemplateParams';
6-
7-
import type { EmailJSResponseStatus } from '../../models/EmailJSResponseStatus';
8-
import type { Options } from '../../types/Options';
9+
import { isHeadless } from '../../utils/isHeadless/isHeadless';
10+
import { headlessError } from '../../errors/headlessError/headlessError';
911

1012
/**
1113
* Send a template to the specific EmailJS service
@@ -23,6 +25,11 @@ export const send = (
2325
): Promise<EmailJSResponseStatus> => {
2426
const opts = buildOptions(options);
2527
const publicKey = opts.publicKey || store.publicKey;
28+
const blockHeadless = opts.blockHeadless || store.blockHeadless;
29+
30+
if (blockHeadless && isHeadless(navigator)) {
31+
return Promise.reject(headlessError());
32+
}
2633

2734
validateParams(publicKey, serviceID, templateID);
2835
validateTemplateParams(templateParams);

src/methods/sendForm/sendForm.spec.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,42 @@ describe('sdk v4', () => {
9090
).toThrow('The template ID is required');
9191
});
9292

93+
it('should call the sendForm and fail on headless', async () => {
94+
const form: HTMLFormElement = document.createElement('form');
95+
96+
try {
97+
const result = await sendForm('default_service', 'my_test_template', form, {
98+
publicKey: 'C2JWGTestKeySomething',
99+
blockHeadless: true,
100+
});
101+
expect(result).toBeUndefined();
102+
} catch (error) {
103+
expect(error).toEqual({
104+
status: 451,
105+
text: 'Unavailable For Headless Browser',
106+
});
107+
}
108+
});
109+
110+
it('should call the sendForm and fail on headless as promise', () => {
111+
const form: HTMLFormElement = document.createElement('form');
112+
113+
return sendForm('default_service', 'my_test_template', form, {
114+
publicKey: 'C2JWGTestKeySomething',
115+
blockHeadless: true,
116+
}).then(
117+
(result) => {
118+
expect(result).toBeUndefined();
119+
},
120+
(error) => {
121+
expect(error).toEqual({
122+
status: 451,
123+
text: 'Unavailable For Headless Browser',
124+
});
125+
},
126+
);
127+
});
128+
93129
it('should call the sendForm with id selector', async () => {
94130
const form: HTMLFormElement = document.createElement('form');
95131
form.id = 'form-id';
@@ -117,4 +153,19 @@ describe('sdk v4', () => {
117153
expect(error).toBeUndefined();
118154
}
119155
});
156+
157+
it('should call the sendForm with form element as promise', () => {
158+
const form: HTMLFormElement = document.createElement('form');
159+
160+
return sendForm('default_service', 'my_test_template', form, {
161+
publicKey: 'C2JWGTestKeySomething',
162+
}).then(
163+
(result) => {
164+
expect(result).toEqual({ status: 200, text: 'OK' });
165+
},
166+
(error) => {
167+
expect(error).toBeUndefined();
168+
},
169+
);
170+
});
120171
});

src/methods/sendForm/sendForm.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
import type { EmailJSResponseStatus } from '../../models/EmailJSResponseStatus';
2+
import type { Options } from '../../types/Options';
3+
14
import { store } from '../../store/store';
25
import { sendPost } from '../../api/sendPost';
36
import { buildOptions } from '../../utils/buildOptions/buildOptions';
47
import { validateForm } from '../../utils/validateForm/validateForm';
58
import { validateParams } from '../../utils/validateParams/validateParams';
6-
7-
import type { EmailJSResponseStatus } from '../../models/EmailJSResponseStatus';
8-
import type { Options } from '../../types/Options';
9+
import { isHeadless } from '../../utils/isHeadless/isHeadless';
10+
import { headlessError } from '../../errors/headlessError/headlessError';
911

1012
const findHTMLForm = (form: string | HTMLFormElement): HTMLFormElement | null => {
1113
return typeof form === 'string' ? document.querySelector<HTMLFormElement>(form) : form;
@@ -27,6 +29,12 @@ export const sendForm = (
2729
): Promise<EmailJSResponseStatus> => {
2830
const opts = buildOptions(options);
2931
const publicKey = opts.publicKey || store.publicKey;
32+
const blockHeadless = opts.blockHeadless || store.blockHeadless;
33+
34+
if (blockHeadless && isHeadless(navigator)) {
35+
return Promise.reject(headlessError());
36+
}
37+
3038
const currentForm = findHTMLForm(form);
3139

3240
validateParams(publicKey, serviceID, templateID);

src/store/store.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Options } from '../types/Options';
22

33
export const store: Options = {
44
origin: 'https://api.emailjs.com',
5+
blockHeadless: false,
56
limitRate: 0,
67
blockList: [],
78
};

0 commit comments

Comments
 (0)