Skip to content

Commit 67a89f9

Browse files
authored
Merge pull request #52 from emailjs-com/limit-rate
Add the limit rate settings
2 parents 274f6ba + 41989f5 commit 67a89f9

File tree

17 files changed

+363
-10
lines changed

17 files changed

+363
-10
lines changed

README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,18 +120,30 @@ The local parameter will have higher priority than the global one.
120120
| publicKey | String | | The public key is required to invoke the method. |
121121
| blockHeadless | Boolean | False | Method will return error 451 if the browser is headless. |
122122
| blockList | BlockList | | Block list settings. |
123+
| limitRate | LimitRate | | Limit rate configuration. |
123124

124125
**BlockList**
125126

126127
Allows to ignore a method call if the watched variable contains a value from the block list.
127128
\
128-
Method will return error 403 if request is blocked.
129+
The method will return the error 403 if the request is blocked.
129130

130131
| Name | Type | Description |
131132
| ------------- | -------- | -------------------------------------------------- |
132133
| list | String[] | The array of strings contains values for blocking. |
133134
| watchVariable | String | A name of the variable to be watched. |
134135

136+
**LimitRate**
137+
138+
Allows to set the limit rate for calling a method.
139+
\
140+
If the request hits the limit rate, the method will return the error 429.
141+
142+
| Name | Type | Default | Description |
143+
| -------- | ------ | --------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
144+
| id | String | page path | The limit rate is per page by default. To override the behavior, set the ID. It can be a custom ID for each page, group, or application. |
145+
| throttle | Number | | _(ms)_ After how many milliseconds a next request is allowed. |
146+
135147
**Declare global settings**
136148

137149
```js
@@ -143,6 +155,9 @@ emailjs.init({
143155
blockList: {
144156
list: ['foo@emailjs.com', 'bar@emailjs.com'],
145157
},
158+
limitRate: {
159+
throttle: 10000, // 10s
160+
},
146161
});
147162
```
148163

@@ -162,6 +177,9 @@ emailjs
162177
blockList: {
163178
watchVariable: 'userEmail',
164179
},
180+
limitRate: {
181+
throttle: 0, // turn off the limit rate for these requests
182+
},
165183
})
166184
.then(
167185
(response) => {
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 { limitRateError } from './limitRateError';
5+
6+
it('should return EmailJSResponseStatus', () => {
7+
expect(limitRateError()).toBeInstanceOf(EmailJSResponseStatus);
8+
});
9+
10+
it('should return status 451', () => {
11+
expect(limitRateError()).toEqual({
12+
status: 429,
13+
text: 'Too Many Requests',
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 limitRateError = () => {
4+
return new EmailJSResponseStatus(429, 'Too Many Requests');
5+
};

src/methods/init/init.spec.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { store } from '../../store/store';
55

66
beforeEach(() => {
77
store.origin = 'https://api.emailjs.com';
8-
store.limitRate = 0;
98
store.publicKey = undefined;
109
});
1110

@@ -16,7 +15,6 @@ describe('sdk v3', () => {
1615
expect(store).toEqual({
1716
origin: 'https://api.emailjs.com',
1817
publicKey: 'C2JWGTestKeySomething',
19-
limitRate: 0,
2018
});
2119
});
2220
});
@@ -27,7 +25,6 @@ describe('sdk v4', () => {
2725

2826
expect(store).toEqual({
2927
origin: 'https://api.emailjs.com',
30-
limitRate: 0,
3128
});
3229
});
3330

@@ -37,7 +34,9 @@ describe('sdk v4', () => {
3734
blockList: {
3835
list: ['block@email.com'],
3936
},
40-
limitRate: 10000,
37+
limitRate: {
38+
throttle: 10000,
39+
},
4140
});
4241

4342
expect(store).toEqual({
@@ -46,7 +45,9 @@ describe('sdk v4', () => {
4645
blockList: {
4746
list: ['block@email.com'],
4847
},
49-
limitRate: 10000,
48+
limitRate: {
49+
throttle: 10000,
50+
},
5051
});
5152
});
5253
});

src/methods/init/init.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ export const init = (
1919
store.publicKey = opts.publicKey;
2020
store.blockHeadless = opts.blockHeadless;
2121
store.blockList = opts.blockList;
22-
store.limitRate = opts.limitRate || store.limitRate;
22+
store.limitRate = opts.limitRate;
2323
store.origin = opts.origin || origin;
2424
};

src/methods/send/send.spec.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,66 @@ describe('sdk v4', () => {
152152
);
153153
});
154154

155+
it('should call the send method and fail on limit rate', async () => {
156+
const sendEmail = () =>
157+
send('default_service', 'my_test_template', undefined, {
158+
publicKey: 'C2JWGTestKeySomething',
159+
limitRate: {
160+
id: 'async-send',
161+
throttle: 100,
162+
},
163+
});
164+
165+
try {
166+
const result = await sendEmail();
167+
expect(result).toEqual({ status: 200, text: 'OK' });
168+
} catch (error) {
169+
expect(error).toBeUndefined();
170+
}
171+
172+
try {
173+
const result = await sendEmail();
174+
expect(result).toBeUndefined();
175+
} catch (error) {
176+
expect(error).toEqual({
177+
status: 429,
178+
text: 'Too Many Requests',
179+
});
180+
}
181+
});
182+
183+
it('should call the send method and fail on limit rate as promise', () => {
184+
const sendEmail = () =>
185+
send('default_service', 'my_test_template', undefined, {
186+
publicKey: 'C2JWGTestKeySomething',
187+
limitRate: {
188+
id: 'promise-send',
189+
throttle: 100,
190+
},
191+
});
192+
193+
return sendEmail().then(
194+
(result) => {
195+
expect(result).toEqual({ status: 200, text: 'OK' });
196+
197+
return sendEmail().then(
198+
(result) => {
199+
expect(result).toBeUndefined();
200+
},
201+
(error) => {
202+
expect(error).toEqual({
203+
status: 429,
204+
text: 'Too Many Requests',
205+
});
206+
},
207+
);
208+
},
209+
(error) => {
210+
expect(error).toBeUndefined();
211+
},
212+
);
213+
});
214+
155215
it('should call the send method successfully with 4 params', async () => {
156216
try {
157217
const result = await send(

src/methods/send/send.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { isHeadless } from '../../utils/isHeadless/isHeadless';
1010
import { headlessError } from '../../errors/headlessError/headlessError';
1111
import { isBlockedValueInParams } from '../../utils/isBlockedValue/isBlockedValue';
1212
import { blockedEmailError } from '../../errors/blockedEmailError/blockedEmailError';
13+
import { isLimitRateHit } from '../../utils/isLimitRateHit/isLimitRateHit';
14+
import { limitRateError } from '../../errors/limitRateError/limitRateError';
1315

1416
/**
1517
* Send a template to the specific EmailJS service
@@ -29,6 +31,7 @@ export const send = (
2931
const publicKey = opts.publicKey || store.publicKey;
3032
const blockHeadless = opts.blockHeadless || store.blockHeadless;
3133
const blockList = { ...store.blockList, ...opts.blockList };
34+
const limitRate = { ...store.limitRate, ...opts.limitRate };
3235

3336
if (blockHeadless && isHeadless(navigator)) {
3437
return Promise.reject(headlessError());
@@ -41,6 +44,10 @@ export const send = (
4144
return Promise.reject(blockedEmailError());
4245
}
4346

47+
if (isLimitRateHit(localStorage, location.pathname, limitRate)) {
48+
return Promise.reject(limitRateError());
49+
}
50+
4451
const params = {
4552
lib_version: process.env.npm_package_version,
4653
user_id: publicKey,

src/methods/sendForm/sendForm.spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,70 @@ describe('sdk v4', () => {
183183
);
184184
});
185185

186+
it('should call the sendForm method and fail on limit rate', async () => {
187+
const form: HTMLFormElement = document.createElement('form');
188+
189+
const sendEmail = () =>
190+
sendForm('default_service', 'my_test_template', form, {
191+
publicKey: 'C2JWGTestKeySomething',
192+
limitRate: {
193+
id: 'async-form',
194+
throttle: 100,
195+
},
196+
});
197+
198+
try {
199+
const result = await sendEmail();
200+
expect(result).toEqual({ status: 200, text: 'OK' });
201+
} catch (error) {
202+
expect(error).toBeUndefined();
203+
}
204+
205+
try {
206+
const result = await sendEmail();
207+
expect(result).toBeUndefined();
208+
} catch (error) {
209+
expect(error).toEqual({
210+
status: 429,
211+
text: 'Too Many Requests',
212+
});
213+
}
214+
});
215+
216+
it('should call the sendForm method and fail on limit rate as promise', () => {
217+
const form: HTMLFormElement = document.createElement('form');
218+
219+
const sendEmail = () =>
220+
sendForm('default_service', 'my_test_template', form, {
221+
publicKey: 'C2JWGTestKeySomething',
222+
limitRate: {
223+
id: 'promise-form',
224+
throttle: 100,
225+
},
226+
});
227+
228+
return sendEmail().then(
229+
(result) => {
230+
expect(result).toEqual({ status: 200, text: 'OK' });
231+
232+
return sendEmail().then(
233+
(result) => {
234+
expect(result).toBeUndefined();
235+
},
236+
(error) => {
237+
expect(error).toEqual({
238+
status: 429,
239+
text: 'Too Many Requests',
240+
});
241+
},
242+
);
243+
},
244+
(error) => {
245+
expect(error).toBeUndefined();
246+
},
247+
);
248+
});
249+
186250
it('should call the sendForm with id selector', async () => {
187251
const form: HTMLFormElement = document.createElement('form');
188252
form.id = 'form-id';

src/methods/sendForm/sendForm.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { isHeadless } from '../../utils/isHeadless/isHeadless';
1010
import { headlessError } from '../../errors/headlessError/headlessError';
1111
import { isBlockedValueInParams } from '../../utils/isBlockedValue/isBlockedValue';
1212
import { blockedEmailError } from '../../errors/blockedEmailError/blockedEmailError';
13+
import { isLimitRateHit } from '../../utils/isLimitRateHit/isLimitRateHit';
14+
import { limitRateError } from '../../errors/limitRateError/limitRateError';
1315

1416
const findHTMLForm = (form: string | HTMLFormElement): HTMLFormElement | null => {
1517
return typeof form === 'string' ? document.querySelector<HTMLFormElement>(form) : form;
@@ -33,6 +35,7 @@ export const sendForm = (
3335
const publicKey = opts.publicKey || store.publicKey;
3436
const blockHeadless = opts.blockHeadless || store.blockHeadless;
3537
const blockList = { ...store.blockList, ...opts.blockList };
38+
const limitRate = { ...store.limitRate, ...opts.limitRate };
3639

3740
if (blockHeadless && isHeadless(navigator)) {
3841
return Promise.reject(headlessError());
@@ -49,6 +52,10 @@ export const sendForm = (
4952
return Promise.reject(blockedEmailError());
5053
}
5154

55+
if (isLimitRateHit(localStorage, location.pathname, limitRate)) {
56+
return Promise.reject(limitRateError());
57+
}
58+
5259
formData.append('lib_version', process.env.npm_package_version!);
5360
formData.append('service_id', serviceID);
5461
formData.append('template_id', templateID);

src/store/store.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,4 @@ import type { Options } from '../types/Options';
33
export const store: Options = {
44
origin: 'https://api.emailjs.com',
55
blockHeadless: false,
6-
limitRate: 0,
76
};

0 commit comments

Comments
 (0)