← back to Rentv Sheet Enrich Refine
infer_emails.py
100 lines
#!/usr/bin/env python3
"""
infer_emails.py — GENERATIVE email inference (Steve 2026-08-13). For each named contact
missing a real email, generate the LIKELY email by LEARNING that company's actual naming
convention from its known emails (e.g. if 'blake.thompson@nmrk.com' exists, Newmark uses
first.last -> infer 'jane.doe@nmrk.com'). Falls back to first.last@domain when a company
has a domain but no detectable pattern. Written to a clearly-labeled "Likely Email
(inferred)" column, GREEN + flagged — never the real Email column, so guesses are never
mistaken for verified. ADD-only; skips contacts that already have a real or inferred email.
"""
import lib, re
GID = 3823360
FREE = {'gmail.com','yahoo.com','hotmail.com','aol.com','outlook.com','icloud.com','me.com',
'comcast.net','sbcglobal.net','att.net','msn.com','ymail.com','live.com','mac.com'}
def alpha(s): return re.sub(r'[^a-z]', '', (s or '').lower())
def parts(name):
toks = [t for t in re.split(r'[\s,]+', name.strip()) if t and t[0].isalpha()]
if not toks: return None, None
first = alpha(toks[0]); last = alpha(toks[-1]) if len(toks) > 1 else ""
return first or None, last or None
# email local-part templates (first, last) -> string
TEMPLATES = {
'first.last': lambda f,l: f"{f}.{l}",
'firstlast': lambda f,l: f"{f}{l}",
'flast': lambda f,l: f"{f[0]}{l}",
'f.last': lambda f,l: f"{f[0]}.{l}",
'first_last': lambda f,l: f"{f}_{l}",
'firstl': lambda f,l: f"{f}{l[0]}",
'lastf': lambda f,l: f"{l}{f[0]}",
'last.first': lambda f,l: f"{l}.{f}",
'first': lambda f,l: f,
'last': lambda f,l: l,
}
def detect(first, last, local):
"""Which template(s) produce this local-part for this name."""
local = alpha(local); out = []
if not first: return out
for name, fn in TEMPLATES.items():
try:
if (last or name in ('first',)) and alpha(fn(first, last or first)) == local:
out.append(name)
except Exception:
pass
return out
def main():
tok = lib.access_token()
title = "Unique Contacts (all tabs)"
rows = lib.read_tab(tok, title); hdr = [h.strip() for h in rows[0]]
c = {v: j for j, v in enumerate(hdr) if v}
def g(r, i): return (r[i] if i is not None and i < len(r) else "").strip()
NAME = c['Contact Name']; EMAIL = c['Email']; CO = c['Company/Venue']; UEM = c['Updated Email (found)']
# ensure the inferred column exists
INF = c.get('Likely Email (inferred)')
if INF is None:
INF = max(c.values()) + 1
lib.batch_fill(tok, GID, [{"row0": 0, "col0": INF, "value": "Likely Email (inferred)"}])
# 1) learn per-company domain + pattern votes from KNOWN emails
dom = {}; votes = {}
for r in rows[1:]:
e = g(r, EMAIL).lower(); co = g(r, CO).lower(); nm = g(r, NAME)
m = re.search(r'([^@\s]+)@([\w.-]+)', e)
if not m or not co: continue
local, d = m.group(1), m.group(2).lower()
if d in FREE: continue
dom.setdefault(co, d)
f, l = parts(nm)
for t in detect(f, l, local):
votes.setdefault(co, {}).setdefault(t, 0)
votes[co][t] += 1
# dominant pattern per company
pat = {co: max(v.items(), key=lambda x: x[1])[0] for co, v in votes.items()}
# 2) infer for named contacts missing any email
cells = []; n_pat = 0; n_default = 0
for ri, r in enumerate(rows[1:], start=1):
nm = g(r, NAME)
if not nm or not nm[0].isalpha(): continue
if g(r, EMAIL) or g(r, UEM) or g(r, INF): continue # already has some email
co = g(r, CO).lower(); d = dom.get(co)
if not d: continue # no company domain -> can't infer
f, l = parts(nm)
if not f or not l: continue
if co in pat:
local = alpha(TEMPLATES[pat[co]](f, l)); n_pat += 1
else:
local = f"{f}.{l}"; n_default += 1 # generic fallback
cells.append({"row0": ri, "col0": INF, "value": f"{local}@{d}"})
lib.batch_fill(tok, GID, cells)
print(f"companies with a detected email pattern: {len(pat)}")
print(f"inferred emails written: {len(cells)} ({n_pat} from company pattern, {n_default} from first.last default)")
# show a few examples
for cell in cells[:8]:
print(" ", g(rows[cell['row0']], NAME), "->", cell['value'])
if __name__ == "__main__":
main()