Files
claudetools/temp/bardach_create_missing_contacts.py
Mike Swanson fa15b03180 sync: Auto-sync from ACG-M-L5090 at 2026-03-10 19:11:00
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>
2026-03-10 19:59:08 -07:00

285 lines
9.0 KiB
Python

#!/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()