"""Search for deleted contacts in Barbara's mailbox using subprocess curl calls.""" import subprocess, json, sys, urllib.parse CLAUDE_APP = "fabb3421-8b34-484b-bc17-e46de9703418" CLAUDE_SECRET = "~QJ8Q~NyQSs4OcGqHZyPrA2CVnq9KBfKiimntbMO" TENANT_ID = "dd4a82e8-85a3-44ac-8800-07945ab4d95f" USER = "barbara@bardach.net" def curl_get(url, token, extra_headers=None): cmd = ['curl', '-s', '-H', f'Authorization: Bearer {token}'] if extra_headers: for k, v in extra_headers.items(): cmd.extend(['-H', f'{k}: {v}']) cmd.append(url) result = subprocess.run(cmd, capture_output=True, text=True) return json.loads(result.stdout) if result.stdout.strip() else {} def curl_post(url, token, body): cmd = ['curl', '-s', '-X', 'POST', '-H', f'Authorization: Bearer {token}', '-H', 'Content-Type: application/json', '-d', json.dumps(body), url] result = subprocess.run(cmd, capture_output=True, text=True) return json.loads(result.stdout) if result.stdout.strip() else {} # Get token print("[STEP 1] Getting token...") token_cmd = subprocess.run([ 'curl', '-s', '-X', 'POST', f'https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token', '-d', f'client_id={CLAUDE_APP}&client_secret={CLAUDE_SECRET}&scope=https://graph.microsoft.com/.default&grant_type=client_credentials' ], capture_output=True, text=True) token = json.loads(token_cmd.stdout)['access_token'] print("[OK] Token acquired") # Method 1: Contact folders delta - finds deleted contacts print("\n[STEP 2] Using contacts delta to enumerate all contacts including deleted...") delta_url = f"https://graph.microsoft.com/beta/users/{USER}/contacts/delta?$select=displayName,emailAddresses" all_active = [] all_removed = [] page = 0 while delta_url: page += 1 data = curl_get(delta_url, token) if 'error' in data: print(f"[ERROR] {data['error'].get('code')}: {data['error'].get('message','')[:200]}") break items = data.get('value', []) for item in items: if '@removed' in item: all_removed.append(item) else: all_active.append(item) delta_url = data.get('@odata.nextLink') delta_token_url = data.get('@odata.deltaLink') if page % 5 == 0: print(f" Page {page}: {len(all_active)} active, {len(all_removed)} removed...") if not delta_url: break print(f"\n[RESULT] Delta scan complete:") print(f" Active contacts: {len(all_active)}") print(f" Deleted contacts: {len(all_removed)}") if all_removed: print(f"\n First 20 deleted contacts:") for r in all_removed[:20]: name = r.get('displayName', '(no name)') rid = r.get('id', '?')[:30] reason = r.get('@removed', {}).get('reason', '?') print(f" {name} (reason: {reason})") # Method 2: Check the Deleted Items folder for contact-class items # Using the search API which handles IPM.Contact items print(f"\n[STEP 3] Searching Deleted Items folder via search API...") search_body = { "requests": [{ "entityTypes": ["message"], "query": {"queryString": "kind:contacts"}, "from": 0, "size": 25 }] } search_result = curl_post(f"https://graph.microsoft.com/v1.0/users/{USER}/search/query", token, search_body) if 'error' in search_result: print(f"[INFO] Search API: {search_result['error'].get('code')}: {search_result['error'].get('message','')[:200]}") elif search_result.get('value'): for resp in search_result['value']: hits = resp.get('hitsContainers', [{}]) for hc in hits: total = hc.get('total', 0) print(f" Search found {total} contact-related items") for hit in hc.get('hits', [])[:10]: resource = hit.get('resource', {}) print(f" {resource.get('subject', '?')}") else: print(f"[INFO] Search returned: {json.dumps(search_result)[:300]}")