diff --git a/plugin/apple_mail_mcp/tools/search.py b/plugin/apple_mail_mcp/tools/search.py index bebf0cd..2593bcf 100644 --- a/plugin/apple_mail_mcp/tools/search.py +++ b/plugin/apple_mail_mcp/tools/search.py @@ -79,7 +79,11 @@ def _parse_search_records(output: str) -> List[Dict[str, Any]]: "received_date": parts[7].strip(), } if internet_message_id: - record["mail_link"] = "message:" + quote(internet_message_id, safe="") + # Apple Mail requires: message:// scheme, angle brackets (percent-encoded), + # and raw @ in the Message-ID. Normalize ID in case angle brackets are + # present or missing (AppleScript returns both forms). + msg_id = internet_message_id.strip("<>") + record["mail_link"] = f"message://%3C{quote(msg_id, safe='@')}%3E" if len(parts) > 8 and parts[8].strip(): record["content_preview"] = parts[8].strip() records.append(record) diff --git a/tests/test_mail_search_tools.py b/tests/test_mail_search_tools.py index 3be5adf..9024865 100644 --- a/tests/test_mail_search_tools.py +++ b/tests/test_mail_search_tools.py @@ -77,7 +77,7 @@ def fake_run(script, timeout=120): self.assertEqual(response["next_offset"], 3) self.assertEqual( response["items"][0]["mail_link"], - "message:%3Cabc%40example.com%3E", + "message://%3Cabc@example.com%3E", ) self.assertIn("set offsetRemaining to 1", captured["script"]) self.assertIn("set collectLimit to 3", captured["script"]) @@ -188,7 +188,38 @@ def fake_run(script, timeout=120): ) self.assertEqual( response["items"][0]["mail_link"], - "message:%3CQwcH6OP9REaEX0pi8aR6-g%40geopod-ismtpd-60%3E", + "message://%3CQwcH6OP9REaEX0pi8aR6-g@geopod-ismtpd-60%3E", + ) + + def test_search_emails_mail_link_normalizes_missing_angle_brackets(self): + """AppleScript sometimes returns the Message-ID without angle brackets; + the mail_link should still include them (percent-encoded).""" + + def fake_run(script, timeout=120): + return _record_line( + 402, + "Unbracketed Ticket", + internet_message_id="abc@example.com", + ) + + with patch("apple_mail_mcp.tools.search.run_applescript", side_effect=fake_run): + response = json.loads( + search_tools.search_emails( + account="Work", + subject_keyword="Unbracketed", + output_format="json", + limit=1, + max_results=None, + ) + ) + + self.assertEqual( + response["items"][0]["internet_message_id"], + "abc@example.com", + ) + self.assertEqual( + response["items"][0]["mail_link"], + "message://%3Cabc@example.com%3E", ) def test_search_emails_account_none_iterates_all_accounts(self):