import urllib.request, urllib.parse, json, os from collections import defaultdict APP_ID = "fabb3421-8b34-484b-bc17-e46de9703418" TENANT_ID = "dd4a82e8-85a3-44ac-8800-07945ab4d95f" CLIENT_SECRET = os.environ["CLIENT_SECRET"] USER_ID = "41d14430-feb4-4ae2-aed6-2bd4e6384ca7" # Get token token_data = urllib.parse.urlencode({ 'client_id': APP_ID, 'client_secret': CLIENT_SECRET, 'scope': 'https://graph.microsoft.com/.default', 'grant_type': 'client_credentials' }).encode() req = urllib.request.Request(f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token", data=token_data, method='POST') with urllib.request.urlopen(req) as r: token = json.loads(r.read())['access_token'] base = f'https://graph.microsoft.com/v1.0/users/{USER_ID}/contacts' def patch_contact(cid, data): body = json.dumps(data).encode() req = urllib.request.Request(f'{base}/{cid}', data=body, method='PATCH', headers={'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'}) with urllib.request.urlopen(req) as r: return r.status def delete_contact(cid): req = urllib.request.Request(f'{base}/{cid}', method='DELETE', headers={'Authorization': f'Bearer {token}'}) with urllib.request.urlopen(req) as r: return r.status # Fetch all contacts url = f'{base}?$select=id,displayName,emailAddresses,companyName,businessPhones,mobilePhone,jobTitle,givenName,surname&$orderby=displayName&$top=999' all_contacts = [] while url: req = urllib.request.Request(url, headers={'Authorization': f'Bearer {token}'}) with urllib.request.urlopen(req) as r: data = json.loads(r.read()) all_contacts.extend(data.get('value', [])) url = data.get('@odata.nextLink') print(f'Total contacts: {len(all_contacts)}') by_name = defaultdict(list) for c in all_contacts: name = c.get('displayName', '').strip() if name: by_name[name].append(c) dupes = {k: v for k, v in by_name.items() if len(v) > 1} print(f'Duplicate groups: {len(dupes)}') def merge_emails(keeper, donor): keeper_emails = set(e.get('address', '').lower() for e in keeper.get('emailAddresses', []) if e.get('address', '').strip()) new_emails = [e for e in keeper.get('emailAddresses', []) if e.get('address', '').strip()] added = [] for e in donor.get('emailAddresses', []): addr = e.get('address', '') if addr.strip() and addr.lower() not in keeper_emails: new_emails.append(e) added.append(addr) return new_emails, added def merge_phones(keeper, donor): def normalize(p): return ''.join(c for c in p if c.isdigit())[-10:] keeper_phones = set() for p in (keeper.get('businessPhones') or []): keeper_phones.add(normalize(p)) if keeper.get('mobilePhone'): keeper_phones.add(normalize(keeper['mobilePhone'])) new_phones = [] for p in (donor.get('businessPhones') or []): if normalize(p) not in keeper_phones: new_phones.append(p) if donor.get('mobilePhone') and normalize(donor['mobilePhone']) not in keeper_phones: new_phones.append(donor['mobilePhone']) return new_phones def do_merge(name, keeper, donor): new_emails, added_emails = merge_emails(keeper, donor) new_phones = merge_phones(keeper, donor) patch = {} if added_emails: patch['emailAddresses'] = new_emails if new_phones: biz = list(keeper.get('businessPhones') or []) + new_phones patch['businessPhones'] = biz if not keeper.get('companyName') and donor.get('companyName'): patch['companyName'] = donor['companyName'] if not keeper.get('jobTitle') and donor.get('jobTitle'): patch['jobTitle'] = donor['jobTitle'] if patch: status = patch_contact(keeper['id'], patch) extras = [] if added_emails: extras.append(f"emails: {added_emails}") if new_phones: extras.append(f"phones: {new_phones}") if 'companyName' in patch: extras.append(f"company: {patch['companyName']}") if 'jobTitle' in patch: extras.append(f"job: {patch['jobTitle']}") print(f' [OK] {name}: merged {", ".join(extras)} (status {status})') else: print(f' [OK] {name}: no new data to merge') del_status = delete_contact(donor['id']) print(f' Deleted duplicate (status {del_status})') # === EXACT DUPLICATES === print('\n--- EXACT DUPLICATES ---') for name in ['Bardach, Mike', 'Brandon Lopez', 'Judi Carroll', 'Kelly Yang', 'Megan Carroll', 'Winter Williams']: contacts = dupes[name] for c in contacts[1:]: try: status = delete_contact(c['id']) print(f' [OK] Deleted: {name} (status {status})') except Exception as e: print(f' [ERROR] {name}: {e}') # === PATSY SABLE (3 copies) === print('\n--- Patsy Sable (3 copies) ---') patsy = dupes['Patsy Sable'] patsy_personal = [c for c in patsy if any(e.get('address', '') == 'patsy@patsysable.com' for e in c.get('emailAddresses', []))] patsy_work = [c for c in patsy if any(e.get('address', '') == 'psable@longrealty.com' for e in c.get('emailAddresses', []))] if len(patsy_work) >= 2: try: status = delete_contact(patsy_work[1]['id']) print(f' [OK] Deleted exact work dupe (status {status})') except Exception as e: print(f' [ERROR] work dupe: {e}') if patsy_personal and patsy_work: try: do_merge('Patsy Sable', patsy_personal[0], patsy_work[0]) except Exception as e: print(f' [ERROR] merge: {e}') # === MERGE PAIRS === print('\n--- MERGE PAIRS ---') for name in ['Barbara Bardach', 'David Rodriguez', 'Denise Newton', 'Gina Beltran', 'Jessica Bonn', 'Kayla Manley', 'Maria Anemone', 'Mark Crager', 'Paula Williams', 'Randy Bonn', 'Susan Barry']: contacts = dupes[name] try: do_merge(name, contacts[0], contacts[1]) except Exception as e: print(f' [ERROR] {name}: {e}') print('\n=== ALL DONE ===')