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