● Zion Boggan
repos/Zion Pm/archive/old-conductor-sketch/bin/intake-email.py
zionboggan.com ↗
88 lines · python
History for this file →
1
"""Zion PM - email intake (a sense). Polls each Gmail account's `PM` label over IMAP and turns
2
new messages into tasks. Label-gated on purpose: set a Gmail filter to label what you want the PM
3
to handle (outreach replies, client mail) as `PM`, and ONLY those reach the agent. No flooding.
4
 
5
Run periodically (cron/systemd timer):  python3 bin/intake-email.py
6
Needs each account's Gmail APP PASSWORD in /shared/projects/.env.secrets under its app_pw_var.
7
"""
8
import imaplib, email, json, os, time, uuid, re
9
from email.header import decode_header
10
 
11
PM = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
12
ST = os.path.join(PM, "state"); INBOX = os.path.join(PM, "inbox")
13
LABEL = os.environ.get("PM_EMAIL_LABEL", "PM")
14
 
15
def secrets():
16
    d = {}
17
    p = "/shared/projects/.env.secrets"
18
    if os.path.exists(p):
19
        for line in open(p):
20
            line = line.strip().replace("\r", "")
21
            if "=" in line and not line.startswith("#"):
22
                k, _, v = line.partition("="); d[k.strip()] = v.strip().strip('"').strip("'")
23
    return d
24
 
25
def policy_accounts():
26
    import yaml
27
    pol = yaml.safe_load(open(os.path.join(PM, "policy.yaml")))
28
    return (pol.get("sender_identities", {}) or {}).get("accounts", {})
29
 
30
def _dec(s):
31
    if not s: return ""
32
    out = []
33
    for part, enc in decode_header(s):
34
        out.append(part.decode(enc or "utf-8", "ignore") if isinstance(part, bytes) else part)
35
    return "".join(out)
36
 
37
def add_task(title, project, ttype):
38
    p = os.path.join(ST, "tasks.json")
39
    t = json.load(open(p)) if os.path.exists(p) else {"tasks": []}
40
    tid = "t-" + uuid.uuid4().hex[:8]
41
    t["tasks"].append({"id": tid, "title": title, "project": project, "type": ttype,
42
                       "status": "open", "created": time.strftime("%Y-%m-%dT%H:%M:%S"),
43
                       "artifacts": [], "worker": None, "source": "email"})
44
    json.dump(t, open(p, "w"), indent=2)
45
    open(os.path.join(INBOX, tid + ".task"), "w").write(title + "\n")
46
    with open(os.path.join(ST, "activity.jsonl"), "a") as f:
47
        f.write(json.dumps({"ts": time.strftime("%Y-%m-%dT%H:%M:%S"), "event": "intake_email", "id": tid, "title": title}) + "\n")
48
    return tid
49
 
50
def poll_account(name, acct, sec):
51
    addr = (acct.get("address") or "").strip()
52
    pwvar = acct.get("app_pw_var", "")
53
    pw = sec.get(pwvar, "")
54
    if not addr or not pw:
55
        return f"  skip {name}: no address or {pwvar} not set"
56
    use = acct.get("use", "")
57
    ttype = "outreach" if "outreach" in use or "recruit" in use else "client" if "client" in use else "misc"
58
    try:
59
        M = imaplib.IMAP4_SSL("imap.gmail.com")
60
        M.login(addr, pw)
61
        typ, _ = M.select(f'"{LABEL}"', readonly=False)
62
        if typ != "OK":
63
            M.logout(); return f"  {name}: no '{LABEL}' label (create a Gmail filter -> label '{LABEL}')"
64
        typ, data = M.search(None, "UNSEEN")
65
        ids = data[0].split() if data and data[0] else []
66
        made = 0
67
        for i in ids:
68
            typ, msg_data = M.fetch(i, "(RFC822)")
69
            if typ != "OK": continue
70
            msg = email.message_from_bytes(msg_data[0][1])
71
            frm = _dec(msg.get("From")); subj = _dec(msg.get("Subject"))
72
            title = f"Email to {name} from {frm}: {subj}".strip()
73
            add_task(title, project=None, ttype=ttype)
74
            M.store(i, "+FLAGS", "\\Seen")
75
            made += 1
76
        M.logout()
77
        return f"  {name} ({addr}): {made} new -> tasks"
78
    except Exception as e:
79
        return f"  {name} ERROR: {e}"
80
 
81
def main():
82
    sec = secrets(); accts = policy_accounts()
83
    print(f"[intake-email] polling '{LABEL}' label across {len(accts)} accounts")
84
    for name, acct in accts.items():
85
        print(poll_account(name, acct, sec))
86
 
87
if __name__ == "__main__":
88
    main()