-
Notifications
You must be signed in to change notification settings - Fork 118
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add script for email stress test
- Loading branch information
1 parent
63048b2
commit 8f945ad
Showing
6 changed files
with
307 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# @scripts | ||
|
||
## Usage | ||
|
||
To send emails using the script, run the following command: | ||
|
||
```sh | ||
pnpm stress:email email=some@**gmail**.com amount=100 [interval=60] [mode=individual|reply-chain] | ||
``` | ||
|
||
- `email`: The recipient's email address. | ||
- `amount`: Number of emails to send. | ||
- `interval` (optional): Time in seconds between emails. Default is 60. | ||
- `mode` (optional): `individual` or `reply-chain`. Default is `individual`. | ||
|
||
**Note:** This script assumes a `.env.test.local` file exists with the following values as specified in `env.ts`: | ||
|
||
``` | ||
MAILBRIDGE_TRANSACTIONAL_CREDENTIALS={"apiUrl":"https://example.com","apiKey":"your-api-key","sendAsName":"Sender Name","sendAsEmail":"[email protected]"} | ||
``` | ||
|
||
--- | ||
|
||
## Example | ||
|
||
```sh | ||
pnpm stress:email [email protected] amount=10 interval=30 mode=reply-chain | ||
``` | ||
|
||
This will send 10 emails to `[email protected]` with a 30-second interval in reply-chain mode. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
import crypto from 'crypto'; | ||
import { env } from './env'; | ||
|
||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); | ||
|
||
type EmailData = { | ||
to: string[]; | ||
cc: string[]; | ||
from: string; | ||
sender: string; | ||
subject: string; | ||
plain_body: string; | ||
html_body: string; | ||
attachments: unknown[]; | ||
headers: Record<string, string>; | ||
}; | ||
|
||
type PostalResponse = | ||
| { | ||
status: 'success'; | ||
time: number; | ||
flags: unknown; | ||
data: { | ||
message_id: string; | ||
messages: Record< | ||
string, | ||
{ | ||
id: number; | ||
token: string; | ||
} | ||
>; | ||
}; | ||
} | ||
| { | ||
status: 'parameter-error'; | ||
time: number; | ||
flags: unknown; | ||
data: { | ||
message: string; | ||
}; | ||
}; | ||
|
||
type EmailMode = 'individual' | 'reply-chain'; | ||
|
||
async function sendEmail(emailData: EmailData): Promise<PostalResponse> { | ||
const config = env.MAILBRIDGE_TRANSACTIONAL_CREDENTIALS; | ||
const sendMailPostalResponse = (await fetch( | ||
`${config.apiUrl}/api/v1/send/message`, | ||
{ | ||
method: 'POST', | ||
headers: { | ||
'X-Server-API-Key': `${config.apiKey}`, | ||
'Content-Type': 'application/json' | ||
}, | ||
body: JSON.stringify(emailData) | ||
} | ||
) | ||
.then((res) => res.json()) | ||
.catch((e) => { | ||
console.error('🚨 error sending email', e); | ||
return { | ||
status: 'parameter-error', | ||
time: Date.now(), | ||
flags: {}, | ||
data: { message_id: 'console', messages: {} } | ||
}; | ||
})) as PostalResponse; | ||
|
||
return sendMailPostalResponse; | ||
} | ||
|
||
const sendIndividualEmails = async ( | ||
email: string, | ||
amount: number, | ||
interval: number, | ||
testIdentifier: string | ||
) => { | ||
for (let i = 0; i < amount; i++) { | ||
const uniqueId = Date.now() + i; | ||
const subject = `Test Email ${i + 1} (ID: ${uniqueId}, testId: ${testIdentifier})`; | ||
const content = `This is a test email (${i + 1} of ${amount}).`; | ||
|
||
const emailData = { | ||
to: [email], | ||
cc: [], | ||
from: `${env.MAILBRIDGE_TRANSACTIONAL_CREDENTIALS.sendAsName} <${env.MAILBRIDGE_TRANSACTIONAL_CREDENTIALS.sendAsEmail}>`, | ||
sender: env.MAILBRIDGE_TRANSACTIONAL_CREDENTIALS.sendAsEmail, | ||
subject, | ||
plain_body: content, | ||
html_body: `<p>${content}</p>`, | ||
attachments: [], | ||
headers: {} | ||
}; | ||
|
||
try { | ||
const response = await sendEmail(emailData); | ||
|
||
if (response.status === 'success') { | ||
console.info(`Email ${i + 1} sent successfully:`, response.data); | ||
} else { | ||
console.error(`Error sending email ${i + 1}:`, response.data); | ||
} | ||
} catch (error) { | ||
console.error(`Unexpected error sending email ${i + 1}:`, error); | ||
} | ||
|
||
if (i < amount - 1) { | ||
console.info(`Waiting ${interval} seconds before sending next email...`); | ||
await sleep(interval * 1000); | ||
} | ||
} | ||
}; | ||
|
||
const sendReplyChain = async ( | ||
email: string, | ||
amount: number, | ||
interval: number, | ||
testIdentifier: string | ||
) => { | ||
let previousMessageId: string | undefined; | ||
|
||
for (let i = 0; i < amount; i++) { | ||
const subject = | ||
i === 0 | ||
? `Initial Email (Hash: ${testIdentifier})` | ||
: `Re: Initial Email (Hash: ${testIdentifier})`; | ||
const content = `This is ${i === 0 ? 'the initial email' : `a reply (${i} of ${amount - 1})`}.`; | ||
|
||
const emailData: EmailData = { | ||
to: [email], | ||
cc: [], | ||
from: `${env.MAILBRIDGE_TRANSACTIONAL_CREDENTIALS.sendAsName} <${env.MAILBRIDGE_TRANSACTIONAL_CREDENTIALS.sendAsEmail}>`, | ||
sender: env.MAILBRIDGE_TRANSACTIONAL_CREDENTIALS.sendAsEmail, | ||
subject, | ||
plain_body: content, | ||
html_body: `<p>${content}</p>`, | ||
attachments: [], | ||
headers: previousMessageId ? { 'In-Reply-To': previousMessageId } : {} | ||
}; | ||
|
||
try { | ||
const response = await sendEmail(emailData); | ||
|
||
if (response.status === 'success') { | ||
console.info(`Email ${i + 1} sent successfully:`, response.data); | ||
previousMessageId = response.data.message_id; | ||
} else { | ||
console.error(`Error sending email ${i + 1}:`, response.data); | ||
} | ||
} catch (error) { | ||
console.error(`Unexpected error sending email ${i + 1}:`, error); | ||
} | ||
|
||
if (i < amount - 1) { | ||
console.info(`Waiting ${interval} seconds before sending next email...`); | ||
await sleep(interval * 1000); | ||
} | ||
} | ||
}; | ||
|
||
const main = () => { | ||
const args = process.argv.slice(2); | ||
const emailArg = args.find((arg) => arg.startsWith('email=')); | ||
const amountArg = args.find((arg) => arg.startsWith('amount=')); | ||
const intervalArg = args.find((arg) => arg.startsWith('interval=')); | ||
const modeArg = args.find((arg) => arg.startsWith('mode=')); | ||
|
||
if (!emailArg || !amountArg) { | ||
console.error( | ||
'Usage: pnpm stress:email [email protected] amount=100 [interval=60] [mode=individual|reply-chain]' | ||
); | ||
process.exit(1); | ||
} | ||
|
||
const email = emailArg.split('=')[1]; | ||
const amount = parseInt(amountArg.split('=')[1], 10); | ||
const interval = intervalArg ? parseInt(intervalArg.split('=')[1], 10) : 60; | ||
const mode = (modeArg?.split('=')[1] as EmailMode) || 'individual'; | ||
|
||
if (isNaN(amount) || amount <= 0) { | ||
console.error('Amount must be a positive number'); | ||
process.exit(1); | ||
} | ||
|
||
if (isNaN(interval) || interval < 0) { | ||
console.error('Interval must be a non-negative number'); | ||
process.exit(1); | ||
} | ||
|
||
if (mode !== 'individual' && mode !== 'reply-chain') { | ||
console.error('Mode must be either "individual" or "reply-chain"'); | ||
process.exit(1); | ||
} | ||
|
||
const testIdentifier = crypto.randomBytes(4).toString('hex'); | ||
console.info(`Starting email script with test id: ${testIdentifier}`); | ||
|
||
const sendFunction = | ||
mode === 'individual' ? sendIndividualEmails : sendReplyChain; | ||
|
||
sendFunction(email, amount, interval, testIdentifier) | ||
.then(() => { | ||
console.info(`Finished sending emails. test id: ${testIdentifier}`); | ||
}) | ||
.catch((error) => { | ||
console.error('An error occurred:', error); | ||
console.info( | ||
`Script terminated. Unique test identifier: ${testIdentifier}` | ||
); | ||
}); | ||
}; | ||
|
||
main(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { createEnv } from '@t3-oss/env-core'; | ||
import { z } from 'zod'; | ||
|
||
const stringToJSON = z.string().transform((str, ctx) => { | ||
try { | ||
return JSON.parse(str) as unknown; | ||
} catch (e) { | ||
ctx.addIssue({ code: 'custom', message: 'Invalid JSON' }); | ||
return z.NEVER; | ||
} | ||
}); | ||
|
||
export const env = createEnv({ | ||
server: { | ||
MAILBRIDGE_TRANSACTIONAL_CREDENTIALS: stringToJSON.pipe( | ||
z.object({ | ||
apiUrl: z.string().url(), | ||
apiKey: z.string().min(1), | ||
sendAsName: z.string().min(1), | ||
sendAsEmail: z.string().email() | ||
}) | ||
) | ||
}, | ||
runtimeEnv: process.env, | ||
emptyStringAsUndefined: true | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
{ | ||
"name": "@u22n/scripts", | ||
"private": true, | ||
"type": "module", | ||
"version": "0.1.0", | ||
"scripts": { | ||
"email": "tsx email.ts" | ||
}, | ||
"dependencies": { | ||
"@t3-oss/env-core": "^0.11.0", | ||
"@u22n/platform": "workspace:^", | ||
"zod": "^3.23.8" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^20.14.10", | ||
"typescript": "5.5.3" | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.