feat: Update new project bounce thresholds for stricter email handling

This commit is contained in:
Dries Augustyns
2026-04-16 08:05:56 +02:00
parent 44c86572c2
commit 5e62f517fc
2 changed files with 22 additions and 22 deletions
+12 -12
View File
@@ -54,14 +54,14 @@ const SECURITY_THRESHOLDS = {
// === New project thresholds (projects < 30 days old) ===
// Legitimate senders ramp up gradually; spammers blast immediately
NEW_PROJECT_AGE_DAYS: 30,
NEW_PROJECT_BOUNCE_24H_CEILING_WARNING: 20,
NEW_PROJECT_BOUNCE_24H_CEILING_CRITICAL: 50,
NEW_PROJECT_BOUNCE_7DAY_CEILING_WARNING: 75,
NEW_PROJECT_BOUNCE_7DAY_CEILING_CRITICAL: 150,
NEW_PROJECT_COMPLAINT_24H_CEILING_WARNING: 5,
NEW_PROJECT_COMPLAINT_24H_CEILING_CRITICAL: 10,
NEW_PROJECT_COMPLAINT_7DAY_CEILING_WARNING: 15,
NEW_PROJECT_COMPLAINT_7DAY_CEILING_CRITICAL: 30,
NEW_PROJECT_BOUNCE_24H_CEILING_WARNING: 10,
NEW_PROJECT_BOUNCE_24H_CEILING_CRITICAL: 25,
NEW_PROJECT_BOUNCE_7DAY_CEILING_WARNING: 25,
NEW_PROJECT_BOUNCE_7DAY_CEILING_CRITICAL: 50,
NEW_PROJECT_COMPLAINT_24H_CEILING_WARNING: 3,
NEW_PROJECT_COMPLAINT_24H_CEILING_CRITICAL: 7,
NEW_PROJECT_COMPLAINT_7DAY_CEILING_WARNING: 10,
NEW_PROJECT_COMPLAINT_7DAY_CEILING_CRITICAL: 20,
} as const;
interface RateData {
@@ -179,7 +179,9 @@ export class SecurityService {
`[SECURITY] All-time stats: ${status.allTime.bounces} bounces, ${status.allTime.complaints} complaints out of ${status.allTime.total} emails`,
);
if (status.isNewProject) {
signale.info(`[SECURITY] Project is under ${SECURITY_THRESHOLDS.NEW_PROJECT_AGE_DAYS} days old — stricter ceilings apply`);
signale.info(
`[SECURITY] Project is under ${SECURITY_THRESHOLDS.NEW_PROJECT_AGE_DAYS} days old — stricter ceilings apply`,
);
}
// Send notification about critical security violations
@@ -369,9 +371,7 @@ export class SecurityService {
select: {createdAt: true},
});
const projectAgeDays = project
? (now.getTime() - project.createdAt.getTime()) / (1000 * 60 * 60 * 24)
: Infinity;
const projectAgeDays = project ? (now.getTime() - project.createdAt.getTime()) / (1000 * 60 * 60 * 24) : Infinity;
const isNewProject = projectAgeDays < SECURITY_THRESHOLDS.NEW_PROJECT_AGE_DAYS;
// Get 24-hour, 7-day and all-time rates in parallel
@@ -169,8 +169,8 @@ describe('SecurityService', () => {
describe('New project stricter thresholds', () => {
it('should apply stricter ceilings for projects under 30 days old', async () => {
// Default project is created "now", so it's a new project
// 10,000 emails, 51 bounces (above 50 new project 24h critical ceiling)
await createEmails(10000, {bouncedCount: 51});
// 10,000 emails, 26 bounces (above 25 new project 24h critical ceiling)
await createEmails(10000, {bouncedCount: 26});
const status = await SecurityService.getSecurityStatus(projectId);
expect(status.isNewProject).toBe(true);
@@ -186,26 +186,26 @@ describe('SecurityService', () => {
data: {createdAt: oldDate},
});
// 10,000 emails, 51 bounces (above 50 new project ceiling, below 100 standard ceiling)
await createEmails(10000, {bouncedCount: 51});
// 10,000 emails, 26 bounces (above 25 new project ceiling, below 50 standard warning ceiling)
await createEmails(10000, {bouncedCount: 26});
const status = await SecurityService.getSecurityStatus(projectId);
expect(status.isNewProject).toBe(false);
// 51 is above the 50-bounce 24h warning ceiling for established projects
expect(status.warnings.some(w => w.includes('24-hour bounce count'))).toBe(true);
// But below the 100-bounce 24h critical ceiling
// 26 is below the 50-bounce 24h warning ceiling for established projects
expect(status.warnings.some(w => w.includes('24-hour bounce count'))).toBe(false);
// And below the 100-bounce 24h critical ceiling
expect(status.violations.some(v => v.includes('24-hour bounce count'))).toBe(false);
});
it('should catch new project blasting emails with delayed bounces', async () => {
// Simulate the spammer scenario: new project sends 20K emails,
// only 55 bounces have come back so far (rate is tiny: 0.275%)
await createEmails(20000, {bouncedCount: 55});
// only 30 bounces have come back so far (rate is tiny: 0.15%)
await createEmails(20000, {bouncedCount: 30});
const status = await SecurityService.getSecurityStatus(projectId);
expect(status.isNewProject).toBe(true);
expect(status.shouldDisable).toBe(true);
// 55 > 50 new project 24h critical ceiling
// 30 > 25 new project 24h critical ceiling
expect(status.violations.some(v => v.includes('24-hour bounce count'))).toBe(true);
});
});