"""Send message tool using AppleScript."""
import subprocess
from typing import Optional
from ..exceptions import (
AppleScriptError,
MessagesNotRunningError,
AutomationDeniedError,
)
def _escape_applescript_string(text: str) -> str:
"""Escape a string for safe use in AppleScript.
Handles quotes, backslashes, and other special characters.
Args:
text: The string to escape
Returns:
Escaped string safe for AppleScript
"""
# Escape backslashes first, then quotes
text = text.replace("\\", "\\\\")
text = text.replace('"', '\\"')
# Handle newlines
text = text.replace("\n", "\\n")
text = text.replace("\r", "\\r")
return text
def _run_applescript(script: str, timeout: float = 30.0) -> tuple[bool, str]:
"""Execute an AppleScript and return the result.
Args:
script: The AppleScript code to execute
timeout: Timeout in seconds
Returns:
Tuple of (success, output_or_error)
"""
try:
result = subprocess.run(
["/usr/bin/osascript", "-e", script],
capture_output=True,
text=True,
timeout=timeout,
)
if result.returncode == 0:
return True, result.stdout.strip()
else:
return False, result.stderr.strip()
except subprocess.TimeoutExpired:
return False, "AppleScript timed out"
except Exception as e:
return False, str(e)
def _detect_applescript_error(error_msg: str) -> AppleScriptError:
"""Detect the type of AppleScript error and return appropriate exception.
Args:
error_msg: The error message from AppleScript
Returns:
Appropriate exception type
"""
error_lower = error_msg.lower()
if "not running" in error_lower or "-600" in error_msg:
return MessagesNotRunningError(
"Messages.app is not running. Please open Messages first."
)
if (
"not allowed" in error_lower
or "permission" in error_lower
or "-1743" in error_msg
):
return AutomationDeniedError(
"Automation permission denied. Please grant permission in "
"System Settings > Privacy & Security > Automation."
)
if "can't get buddy" in error_lower or "-1728" in error_msg:
return AppleScriptError(
"Contact not found. You can only send messages to contacts "
"with existing conversations in Messages.",
is_retryable=False,
)
return AppleScriptError(f"AppleScript error: {error_msg}", is_retryable=False)
async def send_message(
recipient: str,
message: str,
service: str = "iMessage",
) -> dict:
"""Send a message to an existing conversation via AppleScript.
IMPORTANT LIMITATIONS:
- Can only send to contacts with EXISTING conversations in Messages
- No delivery confirmation - the message may silently fail to send
- Messages.app must be running
- Requires Automation permission for Messages.app
Args:
recipient: Phone number or email address of the recipient
message: The message text to send
service: Service type - "iMessage" or "SMS" (default: iMessage)
Returns:
Dictionary with success status and any warnings/errors.
"""
if not recipient:
return {
"success": False,
"error": "Recipient is required",
}
if not message:
return {
"success": False,
"error": "Message text is required",
}
if service not in ("iMessage", "SMS"):
return {
"success": False,
"error": f"Invalid service: {service}. Must be 'iMessage' or 'SMS'",
}
# Escape the message for AppleScript
escaped_message = _escape_applescript_string(message)
escaped_recipient = _escape_applescript_string(recipient)
# Build the AppleScript
script = f'''
tell application "Messages"
set targetService to 1st service whose service type = {service}
set targetBuddy to buddy "{escaped_recipient}" of targetService
send "{escaped_message}" to targetBuddy
end tell
'''
success, output = _run_applescript(script)
if success:
return {
"success": True,
"recipient": recipient,
"service": service,
"message_length": len(message),
"warning": (
"Message was sent to Messages.app, but delivery is not confirmed. "
"The message may fail to send if the recipient is not a valid buddy."
),
}
else:
error = _detect_applescript_error(output)
return {
"success": False,
"error": str(error),
"error_type": type(error).__name__,
"is_retryable": error.is_retryable,
}