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>
This commit is contained in:
284
temp/bardach_create_missing_contacts.py
Normal file
284
temp/bardach_create_missing_contacts.py
Normal file
@@ -0,0 +1,284 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user