{"record":{"id":"c30f0916b7da9e5e","repo":"zeroclaw-labs/zeroclaw","slug":"invalid-imessage-target-must-be-a-phone-number","errorCode":null,"errorMessage":"Invalid iMessage target: must be a phone number (+1234567890) or email (user@example.com)","messagePattern":"Invalid iMessage target: must be a phone number \\(\\+1234567890\\) or email \\(user@example\\.com\\)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/imessage.rs","lineNumber":154,"sourceCode":"        ::zeroclaw_api::attribution::Role::Channel(\n            ::zeroclaw_api::attribution::ChannelKind::IMessage,\n        )\n    }\n    fn alias(&self) -> &str {\n        &self.alias\n    }\n}\n\n#[async_trait]\nimpl Channel for IMessageChannel {\n    fn name(&self) -> &str {\n        \"imessage\"\n    }\n\n    async fn send(&self, message: &SendMessage) -> anyhow::Result<()> {\n        // Defense-in-depth: validate target format before any interpolation\n        if !is_valid_imessage_target(&message.recipient) {\n            anyhow::bail!(\n                \"Invalid iMessage target: must be a phone number (+1234567890) or email (user@example.com)\"\n            );\n        }\n\n        // SECURITY: Escape both message AND target to prevent AppleScript injection\n        // See: CWE-78 (OS Command Injection)\n        let escaped_msg = escape_applescript(&message.content);\n        let escaped_target = escape_applescript(&message.recipient);\n\n        let script = format!(\n            r#\"tell application \"Messages\"\n    set targetService to 1st account whose service type = iMessage\n    set targetBuddy to participant \"{escaped_target}\" of targetService\n    send \"{escaped_msg}\" to targetBuddy\nend tell\"#\n        );\n\n        let output = tokio::process::Command::new(\"osascript\")","sourceCodeStart":136,"sourceCodeEnd":172,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/imessage.rs#L136-L172","documentation":"IMessageChannel::send validates message.recipient with is_valid_imessage_target before doing anything else, as defense-in-depth before the target is interpolated into an AppleScript. A valid target is a phone number starting with '+' whose digit count (including country code) is 7-15, or an email with a non-empty local part (alphanumeric plus . _ + -) and a dotted domain of alphanumerics, dots and hyphens. Anything else — bare 10-digit numbers, display names, 'user@localhost' — bails with this message.","triggerScenarios":"Calling send() (or letting the listen loop reply) with a recipient like '1234567890' (missing +), 'phone: +1 234 567 8900 extra', 'John Doe <j@x.com>', or 'user@localhost' (domain has no dot). The check runs before escape_applescript and the osascript invocation, so no AppleScript side effects occur.","commonSituations":"Upstream system stores recipients in national format without '+'; the agent extracts a display name instead of the raw address; an email-style identifier on an internal host without a dotted domain.","solutions":["Normalize the recipient to E.164 with a leading '+' (e.g. '+1234567890') or a plain 'user@example.com' address before sending","If the address comes from an inbound message, take the raw address field rather than a formatted/display variant","Strip surrounding whitespace and any 'display name <addr>' wrapper, keeping only the addr-spec part"],"exampleFix":"// before\nchannel.send(&SendMessage { recipient: \"1234567890\".into(), content: msg, ..Default::default() }).await?;\n\n// after\nchannel.send(&SendMessage { recipient: \"+1234567890\".into(), content: msg, ..Default::default() }).await?;","handlingStrategy":"validation","validationCode":"// Validate/normalize before send (mirrors is_valid_imessage_target):\nfn normalize_imessage_target(raw: &str) -> Option<String> {\n    let t = raw.trim();\n    let t = t.rsplit('<').next()?.trim_end_matches('>');\n    let digits: String = t.chars().filter(|c| c.is_ascii_digit()).collect();\n    if t.starts_with('+') && (7..=15).contains(&digits.len()) { return Some(t.into()); }\n    let (l, d) = t.split_once('@')?;\n    if !l.is_empty() && d.contains('.') { return Some(t.into()); }\n    None\n}\nif let Some(t) = normalize_imessage_target(&msg.recipient) { /* send with t */ }","typeGuard":null,"tryCatchPattern":"match channel.send(&msg).await {\n    Err(e) if e.to_string().contains(\"Invalid iMessage target\") => {\n        // reject/repair the recipient upstream; do not retry unchanged\n    }\n    other => other?,\n}","preventionTips":["Store recipients in E.164 with a leading '+' at the boundary where they enter your system","Strip display-name wrappers and keep only the addr-spec / raw phone before building SendMessage"],"tags":["imessage","recipient-validation","macos","send"],"backgroundTag":"invalid-recipient-format","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}