"""Apple Contacts tools.""" from helpers import run_applescript, safe_applescript_string def search_contacts(query: str) -> list[dict]: """Search contacts by name, email, or phone.""" safe_query = safe_applescript_string(query) script = f''' tell application "Contacts" set output to "" set results to (every person whose name contains "{safe_query}") repeat with p in results set pName to name of p set pId to id of p set pEmail to "" try set pEmail to value of first email of p end try set pPhone to "" try set pPhone to value of first phone of p end try set pOrg to "" try set pOrg to organization of p end try set output to output & pName & "|||" & pEmail & "|||" & pPhone & "|||" & pOrg & "|||" & pId & linefeed end repeat -- Also search by email set emailResults to (every person whose value of emails contains "{safe_query}") repeat with p in emailResults set pName to name of p set pId to id of p if output does not contain pId then set pEmail to "" try set pEmail to value of first email of p end try set pPhone to "" try set pPhone to value of first phone of p end try set pOrg to "" try set pOrg to organization of p end try set output to output & pName & "|||" & pEmail & "|||" & pPhone & "|||" & pOrg & "|||" & pId & linefeed end if end repeat return output end tell''' raw = run_applescript(script, timeout=30) results = [] seen_ids = set() for line in raw.strip().split("\n"): if not line.strip(): continue parts = line.split("|||") if len(parts) < 5: continue cid = parts[4].strip() if cid in seen_ids: continue seen_ids.add(cid) results.append({ "name": parts[0].strip(), "email": parts[1].strip() or None, "phone": parts[2].strip() or None, "organization": parts[3].strip() or None, "id": cid, }) return results def get_contact(name: str) -> dict: """Get detailed info for a contact by name.""" safe_name = safe_applescript_string(name) script = f''' tell application "Contacts" set results to (every person whose name is "{safe_name}") if (count of results) = 0 then set results to (every person whose name contains "{safe_name}") end if if (count of results) = 0 then return "NOT_FOUND" end if set p to item 1 of results set pName to name of p set pFirst to first name of p set pLast to last name of p set pId to id of p set pOrg to "" try set pOrg to organization of p end try set pTitle to "" try set pTitle to job title of p end try set pNote to "" try set pNote to note of p end try -- Emails set emailList to "" repeat with e in emails of p set emailList to emailList & (label of e) & ":" & (value of e) & "," end repeat -- Phones set phoneList to "" repeat with ph in phones of p set phoneList to phoneList & (label of ph) & ":" & (value of ph) & "," end repeat -- Addresses set addrList to "" repeat with a in addresses of p set addrList to addrList & (label of a) & ":" & (formatted address of a) & ";;;" end repeat return pName & "|||" & pFirst & "|||" & pLast & "|||" & pOrg & "|||" & pTitle & "|||" & pNote & "|||" & emailList & "|||" & phoneList & "|||" & addrList & "|||" & pId end tell''' raw = run_applescript(script, timeout=30) if raw == "NOT_FOUND": raise RuntimeError(f"Contact '{name}' not found") parts = raw.split("|||") if len(parts) < 10: raise RuntimeError("Failed to parse contact details") emails = [] for e in parts[6].strip().rstrip(",").split(","): if ":" in e: label, value = e.split(":", 1) emails.append({"label": label.strip(), "value": value.strip()}) phones = [] for ph in parts[7].strip().rstrip(",").split(","): if ":" in ph: label, value = ph.split(":", 1) phones.append({"label": label.strip(), "value": value.strip()}) addresses = [] for a in parts[8].strip().split(";;;"): if ":" in a: label, value = a.split(":", 1) addresses.append({"label": label.strip(), "address": value.strip()}) return { "name": parts[0].strip(), "first_name": parts[1].strip() or None, "last_name": parts[2].strip() or None, "organization": parts[3].strip() or None, "job_title": parts[4].strip() or None, "note": parts[5].strip() or None, "emails": emails, "phones": phones, "addresses": addresses, "id": parts[9].strip(), } def list_groups() -> list[dict]: """List all contact groups.""" script = ''' tell application "Contacts" set output to "" repeat with g in groups set gName to name of g set gId to id of g set gCount to count of people of g set output to output & gName & "|||" & gId & "|||" & (gCount as string) & linefeed end repeat return output end tell''' raw = run_applescript(script) results = [] for line in raw.strip().split("\n"): if not line.strip(): continue parts = line.split("|||") if len(parts) >= 3: results.append({ "name": parts[0].strip(), "id": parts[1].strip(), "member_count": int(parts[2].strip()) if parts[2].strip().isdigit() else 0, }) return results def create_contact(first_name: str, last_name: str | None = None, email: str | None = None, phone: str | None = None, organization: str | None = None, job_title: str | None = None, note: str | None = None) -> dict: """Create a new contact.""" safe_first = safe_applescript_string(first_name) props = [f'first name:"{safe_first}"'] if last_name: props.append(f'last name:"{safe_applescript_string(last_name)}"') if organization: props.append(f'organization:"{safe_applescript_string(organization)}"') if job_title: props.append(f'job title:"{safe_applescript_string(job_title)}"') if note: props.append(f'note:"{safe_applescript_string(note)}"') props_str = ", ".join(props) email_line = "" if email: email_line = f'make new email at end of emails of newPerson with properties {{label:"work", value:"{safe_applescript_string(email)}"}}' phone_line = "" if phone: phone_line = f'make new phone at end of phones of newPerson with properties {{label:"mobile", value:"{safe_applescript_string(phone)}"}}' script = f''' tell application "Contacts" set newPerson to make new person with properties {{{props_str}}} {email_line} {phone_line} save return id of newPerson end tell''' contact_id = run_applescript(script) return { "id": contact_id.strip(), "first_name": first_name, "last_name": last_name, "created": True, } def update_contact(name: str, email: str | None = None, phone: str | None = None, organization: str | None = None, job_title: str | None = None, note: str | None = None) -> dict: """Update an existing contact by name.""" safe_name = safe_applescript_string(name) updates = [] if organization is not None: updates.append(f'set organization of p to "{safe_applescript_string(organization)}"') if job_title is not None: updates.append(f'set job title of p to "{safe_applescript_string(job_title)}"') if note is not None: updates.append(f'set note of p to "{safe_applescript_string(note)}"') if email: updates.append(f'make new email at end of emails of p with properties {{label:"work", value:"{safe_applescript_string(email)}"}}') if phone: updates.append(f'make new phone at end of phones of p with properties {{label:"mobile", value:"{safe_applescript_string(phone)}"}}') if not updates: raise RuntimeError("No updates specified") updates_str = "\n ".join(updates) script = f''' tell application "Contacts" set results to (every person whose name contains "{safe_name}") if (count of results) = 0 then return "NOT_FOUND" end if set p to item 1 of results {updates_str} save return "updated" end tell''' result = run_applescript(script) if result == "NOT_FOUND": raise RuntimeError(f"Contact '{name}' not found") return {"name": name, "updated": True}