feat: Update new project bounce thresholds for stricter email handling
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user