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
- 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
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
- 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
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
- iMessage send failed: {stderr}
- Messages database not found at {}. Ensure Messages.app is se
- Gmail OAuth token is not configured for sending
- send failed {context}: status={status}, body={body}
- send failed after token refresh: status={retry_status}, body
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/c30f0916b7da9e5e.
Report an issue: GitHub.