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" 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 # Fetch all contacts with blank displayName url = f'{base}?$select=id,displayName,givenName,surname,companyName,emailAddresses,businessPhones,mobilePhone&$top=999' blanks = [] while url: req = urllib.request.Request(url, headers={'Authorization': f'Bearer {token}'}) with urllib.request.urlopen(req) as r: data = json.loads(r.read()) for c in data.get('value', []): dn = (c.get('displayName') or '').strip() if not dn: blanks.append(c) url = data.get('@odata.nextLink') print(f'Found {len(blanks)} contacts with blank displayName\n') fixed = 0 skipped = 0 for c in blanks: given = (c.get('givenName') or '').strip() surname = (c.get('surname') or '').strip() company = (c.get('companyName') or '').strip() emails = [e.get('address', '') for e in c.get('emailAddresses', []) if e.get('address', '').strip()] phones = list(filter(None, (c.get('businessPhones') or []) + [c.get('mobilePhone')])) # Build display name from best available info if given and surname: new_name = f'{given} {surname}' elif given: new_name = given elif surname: new_name = surname elif company: new_name = company elif emails: # Use email local part as name new_name = emails[0].split('@')[0].replace('.', ' ').replace('_', ' ').title() else: # Nothing useful - skip skipped += 1 continue try: status = patch_contact(c['id'], {'displayName': new_name}) src = 'name' if (given or surname) else ('company' if company else 'email') print(f' [OK] "{new_name}" (from {src}, status {status})') fixed += 1 except Exception as e: print(f' [ERROR] {new_name}: {e}') print(f'\n=== DONE: Fixed {fixed}, Skipped {skipped} (no usable data) ===')