← 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()