fix: Enhance email bounce notification with latest bounce details

This commit is contained in:
Dries Augustyns
2026-02-18 10:20:58 +01:00
parent 43ea660d52
commit e639c72e8b
+29 -41
View File
@@ -229,7 +229,7 @@ export class NtfyService {
/** /**
* Notify about email bounce - LOW priority (high volume) * Notify about email bounce - LOW priority (high volume)
* Rate-limited to only send notification every 20 bounces with per-project breakdown * Rate-limited to only send notification every 20 bounces with latest 20 bounce details
*/ */
public static async notifyEmailBounce( public static async notifyEmailBounce(
projectName: string, projectName: string,
@@ -240,63 +240,51 @@ export class NtfyService {
// Import redis at runtime to avoid circular dependencies // Import redis at runtime to avoid circular dependencies
const {redis} = await import('../database/redis.js'); const {redis} = await import('../database/redis.js');
// Use Redis counters to track bounce count globally and per-project // Use Redis counters to track bounce count globally
const globalCountKey = `ntfy:bounce:count`; const globalCountKey = `ntfy:bounce:count`;
const projectCountKey = `ntfy:bounce:count:${projectId}`; const bounceListKey = `ntfy:bounce:latest`;
const projectListKey = `ntfy:bounce:projects`;
// Increment global counter // Increment global counter
const globalCount = await redis.incr(globalCountKey); const globalCount = await redis.incr(globalCountKey);
// Increment project-specific counter // Store this bounce event in a list (keep latest 20)
await redis.incr(projectCountKey); const bounceEvent = JSON.stringify({
projectName,
// Add project to the set of projects with bounces (for tracking) projectId,
await redis.sadd(projectListKey, projectId); recipientEmail,
bounceType: bounceType || 'Unknown',
timestamp: new Date().toISOString(),
});
await redis.lpush(bounceListKey, bounceEvent);
await redis.ltrim(bounceListKey, 0, 19); // Keep only latest 20
// Set expiry on first increment (24 hour rolling window) // Set expiry on first increment (24 hour rolling window)
if (globalCount === 1) { if (globalCount === 1) {
await redis.expire(globalCountKey, 86400); await redis.expire(globalCountKey, 86400);
await redis.expire(projectListKey, 86400); await redis.expire(bounceListKey, 86400);
} }
// Always refresh project counter expiry to match global window
await redis.expire(projectCountKey, 86400);
// Only send notification every 20 bounces // Only send notification every 20 bounces
if (globalCount % 20 === 0) { if (globalCount % 20 === 0) {
// Get all projects with bounces // Get the latest 20 bounces
const projectIds = await redis.smembers(projectListKey); const latestBounces = await redis.lrange(bounceListKey, 0, 19);
// Get bounce counts for each project // Parse and format the bounce events
const projectCounts: Array<{projectId: string; count: number; name: string}> = []; const bounceDetails = latestBounces
for (const pid of projectIds) { .map(bounce => {
const count = await redis.get(`ntfy:bounce:count:${pid}`); try {
if (count) { const parsed = JSON.parse(bounce);
// Fetch project name from database return `${parsed.recipientEmail} (${parsed.bounceType}) - ${parsed.projectName}`;
const {prisma} = await import('../database/prisma.js'); } catch {
const project = await prisma.project.findUnique({ return null;
where: {id: pid}, }
select: {name: true}, })
}); .filter(Boolean)
.join('\n');
projectCounts.push({
projectId: pid,
count: parseInt(count, 10),
name: project?.name || 'Unknown',
});
}
}
// Sort by count descending
projectCounts.sort((a, b) => b.count - a.count);
// Build breakdown message
const breakdown = projectCounts.map(p => `${p.name} (${p.projectId}): ${p.count}`).join('\n');
const bounceInfo = bounceType ? ` (${bounceType})` : '';
await this.send({ await this.send({
title: 'Email Bounces', title: 'Email Bounces',
message: `20 email bounces detected (total: ${globalCount})\n\nBreakdown by project:\n${breakdown}\n\nLatest: ${recipientEmail}${bounceInfo} in "${projectName}"`, message: `20 email bounces detected (total: ${globalCount})\n\nLatest 20 bounces:\n${bounceDetails}`,
priority: NtfyPriority.LOW, priority: NtfyPriority.LOW,
tags: [NtfyTag.WARNING], tags: [NtfyTag.WARNING],
}); });