zeroclaw-labs/zeroclaw · error · anyhow::Error

Invalid iMessage target: must be a phone number (+1234567890

Error message

Invalid iMessage target: must be a phone number (+1234567890) or email (user@example.com)

What it means

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.

Source

Thrown at crates/zeroclaw-channels/src/imessage.rs:154

        ::zeroclaw_api::attribution::Role::Channel(
            ::zeroclaw_api::attribution::ChannelKind::IMessage,
        )
    }
    fn alias(&self) -> &str {
        &self.alias
    }
}

#[async_trait]
impl Channel for IMessageChannel {
    fn name(&self) -> &str {
        "imessage"
    }

    async fn send(&self, message: &SendMessage) -> anyhow::Result<()> {
        // Defense-in-depth: validate target format before any interpolation
        if !is_valid_imessage_target(&message.recipient) {
            anyhow::bail!(
                "Invalid iMessage target: must be a phone number (+1234567890) or email (user@example.com)"
            );
        }

        // SECURITY: Escape both message AND target to prevent AppleScript injection
        // See: CWE-78 (OS Command Injection)
        let escaped_msg = escape_applescript(&message.content);
        let escaped_target = escape_applescript(&message.recipient);

        let script = format!(
            r#"tell application "Messages"
    set targetService to 1st account whose service type = iMessage
    set targetBuddy to participant "{escaped_target}" of targetService
    send "{escaped_msg}" to targetBuddy
end tell"#
        );

        let output = tokio::process::Command::new("osascript")

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Normalize the recipient to E.164 with a leading '+' (e.g. '+1234567890') or a plain 'user@example.com' address before sending
  2. If the address comes from an inbound message, take the raw address field rather than a formatted/display variant
  3. Strip surrounding whitespace and any 'display name <addr>' wrapper, keeping only the addr-spec part

Example fix

// before
channel.send(&SendMessage { recipient: "1234567890".into(), content: msg, ..Default::default() }).await?;

// after
channel.send(&SendMessage { recipient: "+1234567890".into(), content: msg, ..Default::default() }).await?;
Defensive patterns

Strategy: validation

Validate before calling

// Validate/normalize before send (mirrors is_valid_imessage_target):
fn normalize_imessage_target(raw: &str) -> Option<String> {
    let t = raw.trim();
    let t = t.rsplit('<').next()?.trim_end_matches('>');
    let digits: String = t.chars().filter(|c| c.is_ascii_digit()).collect();
    if t.starts_with('+') && (7..=15).contains(&digits.len()) { return Some(t.into()); }
    let (l, d) = t.split_once('@')?;
    if !l.is_empty() && d.contains('.') { return Some(t.into()); }
    None
}
if let Some(t) = normalize_imessage_target(&msg.recipient) { /* send with t */ }

Try / catch

match channel.send(&msg).await {
    Err(e) if e.to_string().contains("Invalid iMessage target") => {
        // reject/repair the recipient upstream; do not retry unchanged
    }
    other => other?,
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/c30f0916b7da9e5e. Report an issue: GitHub.