diff --git a/tests/tools/test_send_message_tool.py b/tests/tools/test_send_message_tool.py index 3a6ad11fd..e1b6a21c6 100644 --- a/tests/tools/test_send_message_tool.py +++ b/tests/tools/test_send_message_tool.py @@ -1220,6 +1220,42 @@ class TestParseTargetRefSlack: assert _parse_target_ref("telegram", "C0B0QV5434G")[2] is False +class TestParseTargetRefEmail: + """_parse_target_ref recognizes email addresses as explicit for the email platform.""" + + def test_standard_email_is_explicit(self): + chat_id, thread_id, is_explicit = _parse_target_ref("email", "user@example.com") + assert chat_id == "user@example.com" + assert thread_id is None + assert is_explicit is True + + def test_email_with_dots_in_local_part(self): + chat_id, _, is_explicit = _parse_target_ref("email", "first.last@example.co.uk") + assert chat_id == "first.last@example.co.uk" + assert is_explicit is True + + def test_email_with_plus_tag(self): + chat_id, _, is_explicit = _parse_target_ref("email", "user+tag@gmail.com") + assert chat_id == "user+tag@gmail.com" + assert is_explicit is True + + def test_email_strips_whitespace(self): + chat_id, _, is_explicit = _parse_target_ref("email", " user@example.com ") + assert chat_id == "user@example.com" + assert is_explicit is True + + def test_invalid_email_not_explicit(self): + assert _parse_target_ref("email", "not-an-email")[2] is False + assert _parse_target_ref("email", "@example.com")[2] is False + assert _parse_target_ref("email", "user@")[2] is False + assert _parse_target_ref("email", "user@.com")[2] is False + + def test_email_not_explicit_for_other_platforms(self): + assert _parse_target_ref("telegram", "user@example.com")[2] is False + assert _parse_target_ref("discord", "user@example.com")[2] is False + assert _parse_target_ref("slack", "user@example.com")[2] is False + + class TestSendDiscordThreadId: """_send_discord uses thread_id when provided.""" diff --git a/tools/send_message_tool.py b/tools/send_message_tool.py index 9ea0b9af4..13689dc1d 100644 --- a/tools/send_message_tool.py +++ b/tools/send_message_tool.py @@ -40,6 +40,10 @@ _NUMERIC_TOPIC_RE = _TELEGRAM_TOPIC_TARGET_RE # downstream adapters (signal, etc.) expect. _PHONE_PLATFORMS = frozenset({"signal", "sms", "whatsapp"}) _E164_TARGET_RE = re.compile(r"^\s*\+(\d{7,15})\s*$") +# Email addresses — a valid email like "user@domain.com" should be treated as +# an explicit target for the email platform, not fall through to channel-name +# resolution which has no way to resolve a raw address. +_EMAIL_TARGET_RE = re.compile(r"^\s*[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\s*$") _IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".gif"} _VIDEO_EXTS = {".mp4", ".mov", ".avi", ".mkv", ".3gp"} _AUDIO_EXTS = {".ogg", ".opus", ".mp3", ".wav", ".m4a", ".flac"} @@ -383,6 +387,10 @@ def _parse_target_ref(platform_name: str, target_ref: str): if target_ref.strip().isdigit(): return f"group:{target_ref.strip()}", None, True return None, None, False + if platform_name == "email": + match = _EMAIL_TARGET_RE.fullmatch(target_ref) + if match: + return target_ref.strip(), None, True if platform_name in _PHONE_PLATFORMS: match = _E164_TARGET_RE.fullmatch(target_ref) if match: