Skip to content

Commit 878f9fd

Browse files
committed
fix(notify): dedup welcome emails — max 1 per email per 24h
Resend monthly limit hit by duplicate welcome emails. Added recent_welcomes HashMap tracking last welcome time per email. Skips if same email welcomed within 24 hours. Co-Authored-By: claude-flow <ruv@ruv.net>
1 parent 5f543a1 commit 878f9fd

1 file changed

Lines changed: 17 additions & 1 deletion

File tree

crates/mcp-brain-server/src/notify.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ pub struct ResendNotifier {
104104
from_email: String,
105105
from_name: String,
106106
recipient: String,
107+
/// Dedup: track recently welcomed emails (max 1 per 24h)
108+
recent_welcomes: std::sync::Arc<parking_lot::Mutex<std::collections::HashMap<String, std::time::Instant>>>,
107109
/// Per-category rate limiter: category -> (last_sent, cooldown)
108110
rate_limits: std::sync::Arc<Mutex<HashMap<String, (Instant, Duration)>>>,
109111
/// Open tracking
@@ -187,6 +189,7 @@ impl ResendNotifier {
187189
recipient,
188190
rate_limits: std::sync::Arc::new(Mutex::new(HashMap::new())),
189191
tracker: OpenTracker::new(),
192+
recent_welcomes: std::sync::Arc::new(parking_lot::Mutex::new(std::collections::HashMap::new())),
190193
})
191194
}
192195

@@ -334,8 +337,21 @@ impl ResendNotifier {
334337

335338
// ── Pre-built email templates ────────────────────────────────────
336339

337-
/// Welcome email for new users connecting to the brain
340+
/// Welcome email for new users connecting to the brain.
341+
/// Rate-limited: max 1 welcome per email per 24 hours.
338342
pub async fn send_welcome(&self, user_email: &str, user_name: Option<&str>) -> Result<String, String> {
343+
// Dedup: check if we already welcomed this email recently
344+
{
345+
let mut recent = self.recent_welcomes.lock();
346+
let now = std::time::Instant::now();
347+
// Clean entries older than 24h
348+
recent.retain(|_, t| now.duration_since(*t) < std::time::Duration::from_secs(86400));
349+
if recent.contains_key(user_email) {
350+
tracing::info!("Welcome dedup: skipping {} (already welcomed recently)", user_email);
351+
return Ok("dedup-skipped".to_string());
352+
}
353+
recent.insert(user_email.to_string(), now);
354+
}
339355
let name = user_name.unwrap_or("Explorer");
340356
let html = format!(
341357
r#"<div style="{container}">

0 commit comments

Comments
 (0)