Skip to content

Commit

Permalink
feat: add script for email stress test
Browse files Browse the repository at this point in the history
  • Loading branch information
benjaminshafii authored and BlankParticle committed Aug 30, 2024
1 parent 63048b2 commit 8f945ad
Show file tree
Hide file tree
Showing 6 changed files with 307 additions and 1 deletion.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"db:drop": "dotenv -e .env.local -- turbo run db:drop",
"start:all:r": "infisical run --env=remote -- turbo run start",
"dev:r": "infisical run --env=remote -- turbo run dev",
"dev:spaces": "infisical run --env=spacesremote -- turbo run dev",
"stress:email": "dotenv -e .env.test.local -- pnpm --filter=@u22n/scripts run email",
"db:push:r": "infisical run --env=remote -- pnpm --dir packages/database db:push",
"db:studio:r": "infisical run --env=remote -- turbo run db:studio",
"db:generate:r": "infisical run --env=remote -- turbo run db:generate",
Expand Down
30 changes: 30 additions & 0 deletions packages/scripts/README.md
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.
213 changes: 213 additions & 0 deletions packages/scripts/email.ts
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();
26 changes: 26 additions & 0 deletions packages/scripts/env.ts
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
});
18 changes: 18 additions & 0 deletions packages/scripts/package.json
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"
}
}
19 changes: 19 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 8f945ad

Please sign in to comment.