refactor: improve email whitelist matching with proper email extraction

- Extract email address from sender string in `_matches_whitelist` - Add `_extract_email_from_sender` helper function to handle various sender formats - Prioritize email in angle brackets to prevent display name spoofing - Support formats: plain email, "Name <email>", and quoted display names - Add regex validation for extracted email addresses - Fallback to original sender string if no valid email found

frdel committed Mar 17, 2026 at 10:58 UTC 4b6cc7e2e09f83a314459c00723db57c440b153d
1 file changed +32 -2
plugins/_email_integration/helpers/imap_client.py
+32 -2
@@ -442,8 +442,38 @@ def _is_noreply(sender: str) -> bool:
442
443
444 def _matches_whitelist(sender: str, whitelist: list[str]) -> bool:
445 - sender_lower = sender.lower()
445 + sender_email = _extract_email_from_sender(sender.lower())
446 for pattern in whitelist:
447 - if fnmatch(sender_lower, pattern.lower()):
447 + if fnmatch(sender_email, pattern.lower()):
448 return True
449 return False
450 +
451 +
452 +def _extract_email_from_sender(sender: str) -> str:
453 + """Extract email address from sender string.
454 +
455 + Handles formats like:
456 + - "email@example.com"
457 + - "Name <email@example.com>"
458 + - "\"Display Name\" <email@example.com>"
459 +
460 + Uses content inside angle brackets as authoritative to prevent spoofing
461 + by fake emails in the display name (e.g., "John ceo@company.com <real@email.com>").
462 + """
463 + import re
464 + # Look for email inside angle brackets - this is the authoritative source
465 + match = re.search(r"<([^>]+)>", sender)
466 + if match:
467 + email = match.group(1).strip()
468 + # Validate it looks like an email
469 + if re.match(r"^[^\s<>@]+@[^\s<>@]+\.[^\s<>@]+$", email):
470 + return email
471 +
472 + # No angle brackets - extract email from the whole string
473 + # This handles plain "email@example.com" or malformed input
474 + email_match = re.search(r"[^\s<>]+@[^\s<>]+\.[^\s<>]+", sender)
475 + if email_match:
476 + return email_match.group(0)
477 +
478 + # Fallback: return the whole string (will likely fail pattern match)
479 + return sender