Skip to content

Commit

Permalink
Merge pull request #50 from emailjs-com/validate-headless
Browse files Browse the repository at this point in the history
Block the headless browsers
  • Loading branch information
xr0master authored Jan 29, 2024
2 parents 98a54b1 + bbf35ec commit cb318a3
Show file tree
Hide file tree
Showing 13 changed files with 213 additions and 9 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,10 @@ Options can be declared globally using the **init** method or locally as the fou
\
The local parameter will have higher priority than the global one.

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

**Declare global settings**

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

emailjs.init({
publicKey: 'YOUR_PUBLIC_KEY',
blockHeadless: true,
});
```

Expand Down
15 changes: 15 additions & 0 deletions src/errors/headlessError/headlessError.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { it, expect } from '@jest/globals';

import { EmailJSResponseStatus } from '../../models/EmailJSResponseStatus';
import { headlessError } from './headlessError';

it('should return EmailJSResponseStatus', () => {
expect(headlessError()).toBeInstanceOf(EmailJSResponseStatus);
});

it('should return status 451', () => {
expect(headlessError()).toEqual({
status: 451,
text: 'Unavailable For Headless Browser',
});
});
5 changes: 5 additions & 0 deletions src/errors/headlessError/headlessError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { EmailJSResponseStatus } from '../../models/EmailJSResponseStatus';

export const headlessError = () => {
return new EmailJSResponseStatus(451, 'Unavailable For Headless Browser');
};
32 changes: 32 additions & 0 deletions src/it.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@ describe('send method', () => {
expect(error).toBeUndefined();
}
});

it('should call the init and the send method successfully as promise', () => {
emailjs.init({
publicKey: 'C2JWGTestKeySomething',
});

return emailjs.send('default_service', 'my_test_template').then(
(result) => {
expect(result).toEqual({ status: 200, text: 'OK' });
},
(error) => {
expect(error).toBeUndefined();
},
);
});
});

describe('send-form method', () => {
Expand All @@ -43,4 +58,21 @@ describe('send-form method', () => {
expect(error).toBeUndefined();
}
});

it('should call the init and the sendForm method successfully as promise', () => {
const form: HTMLFormElement = document.createElement('form');

emailjs.init({
publicKey: 'C2JWGTestKeySomething',
});

return emailjs.sendForm('default_service', 'my_test_template', form).then(
(result) => {
expect(result).toEqual({ status: 200, text: 'OK' });
},
(error) => {
expect(error).toBeUndefined();
},
);
});
});
1 change: 1 addition & 0 deletions src/methods/init/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const init = (
const opts = buildOptions(options);

store.publicKey = opts.publicKey;
store.blockHeadless = opts.blockHeadless;
store.limitRate = opts.limitRate || store.limitRate;
store.blockList = opts.blockList || store.blockList;
store.origin = opts.origin || origin;
Expand Down
55 changes: 55 additions & 0 deletions src/methods/send/send.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,43 @@ describe('sdk v4', () => {
).toThrow('The service ID is required');
});

it('should call the send method and fail on headless', async () => {
try {
const result = await send(
'default_service',
'my_test_template',
{},
{
publicKey: 'C2JWGTestKeySomething',
blockHeadless: true,
},
);
expect(result).toBeUndefined();
} catch (error) {
expect(error).toEqual({
status: 451,
text: 'Unavailable For Headless Browser',
});
}
});

it('should call the send method and fail on headless as promise', () => {
return send('', 'my_test_template', undefined, {
publicKey: 'C2JWGTestKeySomething',
blockHeadless: true,
}).then(
(result) => {
expect(result).toBeUndefined();
},
(error) => {
expect(error).toEqual({
status: 451,
text: 'Unavailable For Headless Browser',
});
},
);
});

it('should call the send method and fail on the template ID', () => {
expect(() =>
send('default_service', '', undefined, {
Expand All @@ -78,4 +115,22 @@ describe('sdk v4', () => {
expect(error).toBeUndefined();
}
});

it('should call the send method as promise', () => {
return send(
'default_service',
'my_test_template',
{},
{
publicKey: 'C2JWGTestKeySomething',
},
).then(
(result) => {
expect(result).toEqual({ status: 200, text: 'OK' });
},
(error) => {
expect(error).toBeUndefined();
},
);
});
});
13 changes: 10 additions & 3 deletions src/methods/send/send.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type { EmailJSResponseStatus } from '../../models/EmailJSResponseStatus';
import type { Options } from '../../types/Options';

import { store } from '../../store/store';
import { sendPost } from '../../api/sendPost';
import { buildOptions } from '../../utils/buildOptions/buildOptions';
import { validateParams } from '../../utils/validateParams/validateParams';
import { validateTemplateParams } from '../../utils/validateTemplateParams/validateTemplateParams';

import type { EmailJSResponseStatus } from '../../models/EmailJSResponseStatus';
import type { Options } from '../../types/Options';
import { isHeadless } from '../../utils/isHeadless/isHeadless';
import { headlessError } from '../../errors/headlessError/headlessError';

/**
* Send a template to the specific EmailJS service
Expand All @@ -23,6 +25,11 @@ export const send = (
): Promise<EmailJSResponseStatus> => {
const opts = buildOptions(options);
const publicKey = opts.publicKey || store.publicKey;
const blockHeadless = opts.blockHeadless || store.blockHeadless;

if (blockHeadless && isHeadless(navigator)) {
return Promise.reject(headlessError());
}

validateParams(publicKey, serviceID, templateID);
validateTemplateParams(templateParams);
Expand Down
51 changes: 51 additions & 0 deletions src/methods/sendForm/sendForm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,42 @@ describe('sdk v4', () => {
).toThrow('The template ID is required');
});

it('should call the sendForm and fail on headless', async () => {
const form: HTMLFormElement = document.createElement('form');

try {
const result = await sendForm('default_service', 'my_test_template', form, {
publicKey: 'C2JWGTestKeySomething',
blockHeadless: true,
});
expect(result).toBeUndefined();
} catch (error) {
expect(error).toEqual({
status: 451,
text: 'Unavailable For Headless Browser',
});
}
});

it('should call the sendForm and fail on headless as promise', () => {
const form: HTMLFormElement = document.createElement('form');

return sendForm('default_service', 'my_test_template', form, {
publicKey: 'C2JWGTestKeySomething',
blockHeadless: true,
}).then(
(result) => {
expect(result).toBeUndefined();
},
(error) => {
expect(error).toEqual({
status: 451,
text: 'Unavailable For Headless Browser',
});
},
);
});

it('should call the sendForm with id selector', async () => {
const form: HTMLFormElement = document.createElement('form');
form.id = 'form-id';
Expand Down Expand Up @@ -117,4 +153,19 @@ describe('sdk v4', () => {
expect(error).toBeUndefined();
}
});

it('should call the sendForm with form element as promise', () => {
const form: HTMLFormElement = document.createElement('form');

return sendForm('default_service', 'my_test_template', form, {
publicKey: 'C2JWGTestKeySomething',
}).then(
(result) => {
expect(result).toEqual({ status: 200, text: 'OK' });
},
(error) => {
expect(error).toBeUndefined();
},
);
});
});
14 changes: 11 additions & 3 deletions src/methods/sendForm/sendForm.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type { EmailJSResponseStatus } from '../../models/EmailJSResponseStatus';
import type { Options } from '../../types/Options';

import { store } from '../../store/store';
import { sendPost } from '../../api/sendPost';
import { buildOptions } from '../../utils/buildOptions/buildOptions';
import { validateForm } from '../../utils/validateForm/validateForm';
import { validateParams } from '../../utils/validateParams/validateParams';

import type { EmailJSResponseStatus } from '../../models/EmailJSResponseStatus';
import type { Options } from '../../types/Options';
import { isHeadless } from '../../utils/isHeadless/isHeadless';
import { headlessError } from '../../errors/headlessError/headlessError';

const findHTMLForm = (form: string | HTMLFormElement): HTMLFormElement | null => {
return typeof form === 'string' ? document.querySelector<HTMLFormElement>(form) : form;
Expand All @@ -27,6 +29,12 @@ export const sendForm = (
): Promise<EmailJSResponseStatus> => {
const opts = buildOptions(options);
const publicKey = opts.publicKey || store.publicKey;
const blockHeadless = opts.blockHeadless || store.blockHeadless;

if (blockHeadless && isHeadless(navigator)) {
return Promise.reject(headlessError());
}

const currentForm = findHTMLForm(form);

validateParams(publicKey, serviceID, templateID);
Expand Down
1 change: 1 addition & 0 deletions src/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Options } from '../types/Options';

export const store: Options = {
origin: 'https://api.emailjs.com',
blockHeadless: false,
limitRate: 0,
blockList: [],
};
1 change: 1 addition & 0 deletions src/types/Options.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export interface Options {
origin?: string;
publicKey?: string;
blockHeadless?: boolean;
limitRate?: number;
blockList?: string[];
}
23 changes: 23 additions & 0 deletions src/utils/isHeadless/isHeadless.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { it, expect } from '@jest/globals';
import { isHeadless } from './isHeadless';

it('should be headless browser', () => {
expect(isHeadless(navigator)).toBeTruthy();
});

it('should be headless browser without languages', () => {
expect(
isHeadless({
webdriver: false,
} as Navigator),
).toBeTruthy();
});

it('should be headfull browser', () => {
expect(
isHeadless({
webdriver: false,
languages: ['un'],
} as unknown as Navigator),
).toBeFalsy();
});
3 changes: 3 additions & 0 deletions src/utils/isHeadless/isHeadless.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const isHeadless = (navigator: Navigator) => {
return navigator.webdriver || !navigator.languages || navigator.languages.length === 0;
};

0 comments on commit cb318a3

Please sign in to comment.