"""Search for deleted contacts using Graph search and extended properties.""" import subprocess, json, sys CLAUDE_APP = "fabb3421-8b34-484b-bc17-e46de9703418" CLAUDE_SECRET = "~QJ8Q~NyQSs4OcGqHZyPrA2CVnq9KBfKiimntbMO" TENANT_ID = "dd4a82e8-85a3-44ac-8800-07945ab4d95f" USER = "barbara@bardach.net" def curl(method, url, token, body=None): cmd = ['curl', '-s', '-X', method, '-H', f'Authorization: Bearer {token}', '-H', 'Content-Type: application/json'] if body: cmd.extend(['-d', json.dumps(body)]) cmd.append(url) result = subprocess.run(cmd, capture_output=True, text=True) try: return json.loads(result.stdout) if result.stdout.strip() else {} except json.JSONDecodeError: return {'_raw': result.stdout[:500]} # Get 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: Use the search API (POST to /search/query at root level) print("\n[METHOD 1] Graph Search API for contacts...") search_body = { "requests": [{ "entityTypes": ["contact"], "query": {"queryString": "*"}, "from": 0, "size": 25 }] } result = curl('POST', 'https://graph.microsoft.com/v1.0/search/query', token, search_body) if 'error' in result: print(f" {result['error'].get('code')}: {result['error'].get('message','')[:300]}") elif result.get('value'): for resp in result['value']: for hc in resp.get('hitsContainers', []): total = hc.get('total', 0) more = hc.get('moreResultsAvailable', False) print(f" Found {total} contacts (more available: {more})") else: print(f" Response: {json.dumps(result)[:300]}") # Method 2: Enumerate Deleted Items looking for items with specific properties # The Graph messages endpoint in deleted items will include contact items in some cases # Let's get the deleted items folder children (subfolders) that might be contacts print("\n[METHOD 2] Deleted Items sub-folders...") result2 = curl('GET', f'https://graph.microsoft.com/v1.0/users/{USER}/mailFolders/deleteditems/childFolders?$top=50&$select=displayName,totalItemCount,childFolderCount', token) if result2.get('value'): for f in result2['value']: print(f" {f.get('displayName','?')}: {f.get('totalItemCount','?')} items") elif 'error' in result2: print(f" {result2['error'].get('code')}: {result2['error'].get('message','')[:200]}") # Method 3: Use beta to get contacts with explicit parentFolderId # Actually, let's just scan all items in deleted items to count contacts vs mail print("\n[METHOD 3] Scanning Deleted Items for item types...") # Get count of items in deleted items folder_info = curl('GET', f'https://graph.microsoft.com/v1.0/users/{USER}/mailFolders/deleteditems?$select=totalItemCount,childFolderCount', token) total = folder_info.get('totalItemCount', 0) print(f" Total items in Deleted Items: {total}") print(f" Child folders: {folder_info.get('childFolderCount', 0)}") # Method 4: Use the beta endpoint to get items with extended properties # This lets us see the PR_MESSAGE_CLASS to identify contacts print("\n[METHOD 4] Sampling Deleted Items with message class property...") sample_url = ( f"https://graph.microsoft.com/beta/users/{USER}/mailFolders/deleteditems/messages" f"?$top=100&$select=subject,lastModifiedDateTime" f"&$expand=singleValueExtendedProperties($filter=id%20eq%20'String%200x001A')" f"&$orderby=lastModifiedDateTime%20desc" ) result4 = curl('GET', sample_url, token) if result4.get('value'): items = result4['value'] contact_count = 0 mail_count = 0 other_count = 0 contact_names = [] for item in items: props = item.get('singleValueExtendedProperties', []) msg_class = None for p in props: if p.get('id') == 'String 0x001A': msg_class = p.get('value', '') break if msg_class and 'IPM.Contact' in msg_class: contact_count += 1 contact_names.append(item.get('subject', '(no name)')) elif msg_class and 'IPM.Note' in msg_class: mail_count += 1 else: other_count += 1 print(f" In sample of {len(items)} most recent deleted items:") print(f" Contacts (IPM.Contact): {contact_count}") print(f" Mail (IPM.Note): {mail_count}") print(f" Other: {other_count}") if contact_names: print(f"\n Deleted contact names found:") for name in contact_names[:20]: print(f" - {name}") # If we found contacts, let's page through ALL deleted items to count total contacts if contact_count > 0: print(f"\n[STEP 5] Full scan of Deleted Items for contacts...") all_contacts = [] all_contact_names = [] scan_url = ( f"https://graph.microsoft.com/beta/users/{USER}/mailFolders/deleteditems/messages" f"?$top=200&$select=subject,lastModifiedDateTime" f"&$expand=singleValueExtendedProperties($filter=id%20eq%20'String%200x001A')" ) page = 0 total_scanned = 0 while scan_url and page < 200: # Safety limit page += 1 data = curl('GET', scan_url, token) if 'error' in data: print(f" Error on page {page}: {data['error'].get('message','')[:200]}") break items = data.get('value', []) if not items: break total_scanned += len(items) for item in items: props = item.get('singleValueExtendedProperties', []) for p in props: if p.get('id') == 'String 0x001A' and 'IPM.Contact' in p.get('value', ''): all_contacts.append(item) all_contact_names.append(item.get('subject', '(no name)')) break scan_url = data.get('@odata.nextLink') if page % 10 == 0: print(f" Page {page}: scanned {total_scanned}, found {len(all_contacts)} contacts...") print(f"\n[FINAL RESULT]") print(f" Total items scanned: {total_scanned}") print(f" Deleted contacts found: {len(all_contacts)}") if all_contact_names: # Save full list with open('D:/ClaudeTools/temp/bardach_deleted_contacts.json', 'w') as f: json.dump(all_contacts, f, indent=2) print(f" Full list saved to D:/ClaudeTools/temp/bardach_deleted_contacts.json") print(f"\n Sample of deleted contact names:") for name in all_contact_names[:30]: print(f" - {name}") if len(all_contact_names) > 30: print(f" ... and {len(all_contact_names) - 30} more") elif 'error' in result4: print(f" {result4['error'].get('code')}: {result4['error'].get('message','')[:300]}") else: print(f" Empty response")