Synced files: - Quote wizard frontend (all components, hooks, types, config) - API updates (config, models, routers, schemas, services) - Client work (bg-builders, gurushow) - Scripts (BGB Lesley termination, CIPP, Datto, migration) - Temp files (Bardach contacts, VWP investigation, misc) - Credentials and session logs - Email service, PHP API, session logs Machine: ACG-M-L5090 Timestamp: 2026-03-10 19:11:00 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
174 lines
7.1 KiB
Python
174 lines
7.1 KiB
Python
"""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")
|