#!/usr/bin/env python3 """Create real person contacts in Barbara's M365 Contacts folder from missing contacts list.""" import json import subprocess import re import time TENANT_ID = "dd4a82e8-85a3-44ac-8800-07945ab4d95f" CLIENT_ID = "fabb3421-8b34-484b-bc17-e46de9703418" CLIENT_SECRET = "~QJ8Q~NyQSs4OcGqHZyPrA2CVnq9KBfKiimntbMO" USER = "barbara@bardach.net" GRAPH_BASE = "https://graph.microsoft.com/v1.0" MIN_MESSAGES = 4 BARBARAS_PHONE = "(520) 275-3867" # Commercial domains to exclude COMMERCIAL_DOMAINS = { "monos.com", "zestypaws.com", "augustinusbader.com", "ella-bella.com", "thefarmersdog.com", "nordprotect.zendesk.com", "hilton.com", "orhp.com", "havenlifestyles.com", "4unature.com", "skyslope.com", "arcisgolf.com", "tucsonrealtors.org" } # Commercial keywords in display_name (case-insensitive) COMMERCIAL_NAME_KEYWORDS = [ "team", "support", "reception", "frontdesk", "nordprotect", "zesty", "monos" ] # Commercial email prefixes COMMERCIAL_EMAIL_PREFIXES = [ "care@", "hello@", "connect@", "contact@", "bark@", "support+", "justchecking", "ticketing@" ] # Title suffixes to drop when parsing names TITLE_SUFFIXES = [ "office manager", "broker", "agent", "realtor", "manager", "director", "assistant", "coordinator", "specialist", "advisor", "consultant" ] def get_token(): """Get OAuth token via client credentials.""" url = f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token" cmd = [ "curl", "-s", "-X", "POST", url, "-H", "Content-Type: application/x-www-form-urlencoded", "-d", f"client_id={CLIENT_ID}&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default&client_secret={CLIENT_SECRET}&grant_type=client_credentials" ] result = subprocess.run(cmd, capture_output=True, text=True) data = json.loads(result.stdout) if "access_token" not in data: print(f"[ERROR] Token failed: {data}") return None return data["access_token"] def is_email_like(name): """Check if display_name is just an email address.""" return "@" in name and "." in name def is_commercial(contact): """Check if a contact is commercial/automated.""" email = contact["email"].lower() name = contact["display_name"].lower() domain = email.split("@")[-1] if "@" in email else "" # Own email if email == "bardach@bardach.net": return True # No-reply patterns if any(x in email for x in ["noreply", "no-reply", "donotreply"]): return True # Commercial domains if domain in COMMERCIAL_DOMAINS: return True # Commercial name keywords for kw in COMMERCIAL_NAME_KEYWORDS: if kw in name: return True # Commercial email prefixes for prefix in COMMERCIAL_EMAIL_PREFIXES: if email.startswith(prefix) or prefix in email: return True return False def parse_name(display_name): """Parse display_name into (givenName, surname).""" name = display_name.strip() # Handle "Last, First" format if "," in name: parts = [p.strip() for p in name.split(",", 1)] if len(parts) == 2 and parts[0] and parts[1]: first = parts[1].split()[0] # Take first word after comma return first, parts[0] # Split into words words = name.split() if len(words) == 0: return "", "" if len(words) == 1: return words[0], "" if len(words) == 2: return words[0], words[1] # 3+ words: check for title suffixes # Try to find where a title suffix starts lower_name = name.lower() for suffix in TITLE_SUFFIXES: idx = lower_name.find(suffix) if idx > 0: # Take everything before the suffix name_part = name[:idx].strip() name_words = name_part.split() if len(name_words) >= 2: return name_words[0], " ".join(name_words[1:]) elif len(name_words) == 1: return name_words[0], "" # Default: first word = given, second word = surname, ignore rest return words[0], words[1] def build_contact_payload(contact): """Build the JSON payload for creating a contact.""" given, surname = parse_name(contact["display_name"]) payload = { "givenName": given, "surname": surname, "displayName": contact["display_name"], "emailAddresses": [ {"address": contact["email"], "name": contact["display_name"]} ] } phone = contact.get("phone") label = (contact.get("phone_label") or "").strip() if phone and phone != BARBARAS_PHONE: label_lower = label.lower() if label_lower == "fax": pass # Skip fax elif label_lower in ("cell", "mobile"): payload["mobilePhone"] = phone elif label_lower in ("home",): payload["homePhones"] = [phone] else: # Office, Direct, Phone, empty -> businessPhones payload["businessPhones"] = [phone] return payload def create_contact(token, payload): """Create a contact via Graph API.""" url = f"{GRAPH_BASE}/users/{USER}/contacts" json_str = json.dumps(payload) cmd = [ "curl", "-s", "-X", "POST", url, "-H", f"Authorization: Bearer {token}", "-H", "Content-Type: application/json", "-d", json_str ] result = subprocess.run(cmd, capture_output=True, text=True) try: data = json.loads(result.stdout) except json.JSONDecodeError: return None, result.stdout return data, None def main(): # Load data with open(r"D:\ClaudeTools\temp\bardach_missing_real_contacts.json", encoding="utf-8") as f: data = json.load(f) contacts = data["contacts"] print(f"[INFO] Loaded {len(contacts)} total missing contacts") # Filter: minimum messages contacts = [c for c in contacts if c["total"] >= MIN_MESSAGES] print(f"[INFO] After >= {MIN_MESSAGES} messages filter: {len(contacts)}") # Filter: remove empty/email-only display names filtered = [] removed_reasons = [] for c in contacts: name = (c["display_name"] or "").strip() email = c["email"].lower() if not name: removed_reasons.append(f" REMOVED (empty name): {email}") continue if is_email_like(name): removed_reasons.append(f" REMOVED (email-as-name): {name} <{email}>") continue if is_commercial(c): removed_reasons.append(f" REMOVED (commercial): {name} <{email}>") continue filtered.append(c) print(f"[INFO] Removed {len(contacts) - len(filtered)} non-person entries:") for r in removed_reasons: print(r) print(f"\n[INFO] Final filtered list: {len(filtered)} real person contacts\n") # Print the filtered list for review print(f"{'#':<4} {'Name':<35} {'Email':<45} {'Phone':<18} {'Msgs':>5}") print("-" * 110) has_phone_count = 0 for i, c in enumerate(filtered, 1): phone = c.get("phone") or "" if phone == BARBARAS_PHONE: phone = "(skipped-own)" if phone and phone != "(skipped-own)": has_phone_count += 1 label = c.get("phone_label") or "" phone_display = f"{phone} [{label}]" if label else phone print(f"{i:<4} {c['display_name']:<35} {c['email']:<45} {phone_display:<18} {c['total']:>5}") print(f"\n[INFO] {has_phone_count} contacts have phone numbers") print(f"[INFO] Starting contact creation...\n") # Get token token = get_token() if not token: print("[ERROR] Could not get token. Aborting.") return created = 0 errors = 0 with_phone = 0 for i, c in enumerate(filtered): # Refresh token every 30 creates if i > 0 and i % 30 == 0: print(f"[INFO] Refreshing token after {i} contacts...") token = get_token() if not token: print("[ERROR] Token refresh failed. Aborting.") return payload = build_contact_payload(c) has_phone = "businessPhones" in payload or "mobilePhone" in payload or "homePhones" in payload resp, err = create_contact(token, payload) if err: print(f"[ERROR] {c['display_name']} <{c['email']}>: curl error: {err}") errors += 1 continue if "id" in resp: phone_note = " (with phone)" if has_phone else "" print(f"[CREATED] {c['display_name']} <{c['email']}>{phone_note}") created += 1 if has_phone: with_phone += 1 else: err_code = resp.get("error", {}).get("code", "unknown") err_msg = resp.get("error", {}).get("message", str(resp)) print(f"[ERROR] {c['display_name']} <{c['email']}>: {err_code} - {err_msg}") errors += 1 print(f"\n{'=' * 60}") print(f"[SUMMARY]") print(f" Total filtered contacts: {len(filtered)}") print(f" Created: {created}") print(f" With phone: {with_phone}") print(f" Errors: {errors}") print(f"{'=' * 60}") if __name__ == "__main__": main()