From d3724c0be68858e9a2816526c5b5e7f5f9f12ebc Mon Sep 17 00:00:00 2001 From: liuhao1024 Date: Sat, 30 May 2026 01:24:30 +0800 Subject: [PATCH] fix(tools): recognize email addresses as explicit targets in send_message When using send_message with the email platform, valid email addresses like user@example.com were not recognized as explicit targets by _parse_target_ref(). This caused the function to return (None, None, False), forcing the system into channel-name resolution which has no way to resolve a raw email address, resulting in 'No home channel set for email' errors. Add _EMAIL_TARGET_RE pattern and email platform handler in _parse_target_ref() so email addresses are treated as explicit targets and routed directly without requiring a home target configuration. --- tests/tools/test_send_message_tool.py | 36 +++++++++++++++++++++++++++ tools/send_message_tool.py | 8 ++++++ 2 files changed, 44 insertions(+) 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: