""" Operation 2: Move unique contacts from Temp to Main Contacts folder (278 contacts) Tries move endpoint first, falls back to copy+delete. """ import json import subprocess import time import urllib.parse TENANT_ID = "dd4a82e8-85a3-44ac-8800-07945ab4d95f" CLIENT_ID = "fabb3421-8b34-484b-bc17-e46de9703418" CLIENT_SECRET = "~QJ8Q~NyQSs4OcGqHZyPrA2CVnq9KBfKiimntbMO" SCOPE = "https://graph.microsoft.com/.default" USER = "barbara@bardach.net" DATA_FILE = "D:/ClaudeTools/temp/bardach_temp_vs_main.json" OUTPUT_FILE = "D:/ClaudeTools/temp/bardach_op2_move_unique.json" # Contact fields to copy in fallback mode CONTACT_FIELDS = [ "givenName", "surname", "displayName", "middleName", "nickName", "title", "jobTitle", "companyName", "department", "officeLocation", "businessHomePage", "personalNotes", "generation", "imAddresses", "emailAddresses", "homePhones", "mobilePhone", "businessPhones", "homeAddress", "businessAddress", "otherAddress", "birthday", "yomiGivenName", "yomiSurname", "yomiCompanyName", "fileAs", "initials", "manager", "assistantName", "profession", "spouseName", "children" ] def get_token(): url = f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token" data = ( f"client_id={CLIENT_ID}" f"&scope={urllib.parse.quote(SCOPE)}" f"&client_secret={urllib.parse.quote(CLIENT_SECRET)}" f"&grant_type=client_credentials" ) result = subprocess.run( ["curl", "-s", "-X", "POST", url, "-H", "Content-Type: application/x-www-form-urlencoded", "-d", data], capture_output=True, text=True ) resp = json.loads(result.stdout) if "access_token" not in resp: print(f"[ERROR] Token acquisition failed: {resp}") raise Exception("Failed to get token") print("[OK] Token acquired") return resp["access_token"] def get_contacts_folder_id(token): """Get the default Contacts folder ID.""" url = f"https://graph.microsoft.com/v1.0/users/{USER}/contactFolders" result = subprocess.run( ["curl", "-s", "-X", "GET", url, "-H", f"Authorization: Bearer {token}"], capture_output=True, text=True ) resp = json.loads(result.stdout) for folder in resp.get("value", []): if folder.get("displayName") == "Contacts": return folder["id"] return None def try_move(token, contact_id, dest_id): """Try the /move endpoint.""" url = f"https://graph.microsoft.com/v1.0/users/{USER}/contacts/{contact_id}/move" body = json.dumps({"destinationId": dest_id}) result = subprocess.run( ["curl", "-s", "-w", "\n%{http_code}", "-X", "POST", url, "-H", f"Authorization: Bearer {token}", "-H", "Content-Type: application/json", "-d", body], capture_output=True, text=True ) lines = result.stdout.rsplit("\n", 1) status = lines[-1].strip() if len(lines) > 1 else "000" body_text = lines[0] if len(lines) > 1 else result.stdout return status, body_text def get_contact(token, contact_id): """Read full contact data.""" url = f"https://graph.microsoft.com/v1.0/users/{USER}/contacts/{contact_id}" result = subprocess.run( ["curl", "-s", "-X", "GET", url, "-H", f"Authorization: Bearer {token}"], capture_output=True, text=True ) return json.loads(result.stdout) def create_contact(token, contact_data): """Create a contact in the default Contacts folder.""" url = f"https://graph.microsoft.com/v1.0/users/{USER}/contacts" # Build clean payload with only writable fields payload = {} for field in CONTACT_FIELDS: val = contact_data.get(field) if val is not None and val != "" and val != [] and val != {}: payload[field] = val body = json.dumps(payload) result = subprocess.run( ["curl", "-s", "-w", "\n%{http_code}", "-X", "POST", url, "-H", f"Authorization: Bearer {token}", "-H", "Content-Type: application/json", "-d", body], capture_output=True, text=True ) lines = result.stdout.rsplit("\n", 1) status = lines[-1].strip() if len(lines) > 1 else "000" body_text = lines[0] if len(lines) > 1 else result.stdout return status, body_text def delete_contact(token, contact_id): url = f"https://graph.microsoft.com/v1.0/users/{USER}/contacts/{contact_id}" result = subprocess.run( ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "-X", "DELETE", url, "-H", f"Authorization: Bearer {token}"], capture_output=True, text=True ) return result.stdout.strip() def main(): print("=" * 60) print("OPERATION 2: Move unique contacts to Main Contacts (278)") print("=" * 60) with open(DATA_FILE, "r") as f: data = json.load(f) unique = data["unique_to_temp"] total = len(unique) print(f"[INFO] Loaded {total} unique contacts to move") token = get_token() # Get the contacts folder ID for move endpoint folder_id = get_contacts_folder_id(token) print(f"[INFO] Main Contacts folder ID: {folder_id}") # Test move endpoint with first contact move_works = False if total > 0: test_id = unique[0]["temp_id"] # Try with "contacts" string first status, body = try_move(token, test_id, "contacts") if status in ("200", "201"): move_works = True print("[OK] Move endpoint works with 'contacts' destination") elif folder_id: # Try with actual folder ID status, body = try_move(token, test_id, folder_id) if status in ("200", "201"): move_works = True print("[OK] Move endpoint works with folder ID") else: print(f"[WARNING] Move endpoint returned {status}, falling back to copy+delete") print(f" Response: {body[:200]}") else: print(f"[WARNING] Move returned {status} and no folder ID found, using copy+delete") use_move = move_works dest_id = "contacts" if not folder_id else folder_id # If the first contact was already moved successfully via test, track it start_index = 1 if move_works else 0 successes = [] failures = [] method_used = "move" if use_move else "copy+delete" print(f"[INFO] Using method: {method_used}") if move_works: # First one already moved successes.append({ "temp_id": unique[0]["temp_id"], "displayName": unique[0].get("displayName", ""), "method": "move" }) start_time = time.time() for i in range(start_index, total): contact = unique[i] # Re-acquire token every 250 operations if i > 0 and i % 250 == 0: print(f"[INFO] Re-acquiring token at operation {i}...") token = get_token() temp_id = contact["temp_id"] display = contact.get("displayName", "") if use_move: status, body = try_move(token, temp_id, dest_id) if status in ("200", "201"): successes.append({"temp_id": temp_id, "displayName": display, "method": "move"}) else: failures.append({"temp_id": temp_id, "displayName": display, "status": status, "error": body[:200]}) else: # Fallback: copy + delete try: cdata = get_contact(token, temp_id) if "error" in cdata: failures.append({"temp_id": temp_id, "displayName": display, "status": "read_fail", "error": str(cdata["error"])[:200]}) continue cstatus, cbody = create_contact(token, cdata) if cstatus in ("200", "201"): # Delete original dstatus = delete_contact(token, temp_id) if dstatus in ("204", "200"): successes.append({"temp_id": temp_id, "displayName": display, "method": "copy+delete"}) else: successes.append({"temp_id": temp_id, "displayName": display, "method": "copy_only", "delete_status": dstatus}) else: failures.append({"temp_id": temp_id, "displayName": display, "status": cstatus, "error": cbody[:200]}) except Exception as e: failures.append({"temp_id": temp_id, "displayName": display, "status": "exception", "error": str(e)[:200]}) # Progress every 25 if (i + 1) % 25 == 0 or (i + 1) == total: elapsed = time.time() - start_time rate = (i + 1) / elapsed if elapsed > 0 else 0 print(f" [{i+1}/{total}] OK={len(successes)} FAIL={len(failures)} ({rate:.1f}/sec)") elapsed = time.time() - start_time results = { "operation": "move_unique_contacts", "method": method_used, "total": total, "successes": len(successes), "failures": len(failures), "elapsed_seconds": round(elapsed, 1), "successful_contacts": successes, "failed_contacts": failures } with open(OUTPUT_FILE, "w") as f: json.dump(results, f, indent=2) print(f"\n[SUCCESS] Operation 2 complete") print(f" Moved: {len(successes)}/{total}") print(f" Failed: {len(failures)}/{total}") print(f" Method: {method_used}") print(f" Time: {elapsed:.1f}s") print(f" Results: {OUTPUT_FILE}") if __name__ == "__main__": main()