"""Create a Gmail DRAFT (never send) in the sender's mailbox via domain-wide delegation with the gmail.compose scope. Lets an approved Outreach draft land in the user's Gmail (and therefore Superhuman) Drafts — as an in-thread reply when there's an active thread, or a fresh email otherwise. The human reviews and sends from Gmail (guardrails #4, #6 — our code only ever creates a draft, it never sends). """ import base64 import email.message import json as _json import urllib.error import urllib.parse import urllib.request from . import config as _cfg from . import credentials as _creds def _parse_subject_body(draft_text): """Split a draft of the form 'Subject: ...\\n\\n' into (subject, body).""" text = (draft_text or "").strip() lines = text.split("\n") if lines and lines[0].lower().startswith("subject:"): subject = lines[0].split(":", 1)[1].strip() body = "\n".join(lines[1:]).lstrip("\n") return subject, body return "", text def _reply_target(conn, investor_id): """LP address + active-thread headers for an in-thread reply, from the most recent matched email with this investor. Returns {to, thread_id, in_reply_to} or None.""" try: row = conn.execute( "SELECT e.rfc_message_id, e.gmail_thread_id, l.matched_address " "FROM emails e JOIN email_investor_links l ON l.email_id = e.id " "WHERE l.fundraising_investor_id = ? AND e.is_matched = 1 " "ORDER BY e.sent_at DESC LIMIT 1", (investor_id,)).fetchone() except Exception: return None if not row or not row["matched_address"]: return None return {"to": row["matched_address"], "thread_id": row["gmail_thread_id"], "in_reply_to": row["rfc_message_id"]} def _build_raw(from_addr, to_addr, subject, body, in_reply_to=None): msg = email.message.EmailMessage() msg["From"] = from_addr msg["To"] = to_addr msg["Subject"] = subject or "(no subject)" if in_reply_to: msg["In-Reply-To"] = in_reply_to msg["References"] = in_reply_to msg.set_content(body or "") return base64.urlsafe_b64encode(msg.as_bytes()).decode("ascii") def create_outreach_draft(conn, sender_email, investor_id, draft_text): """Create a Gmail draft in `sender_email`'s mailbox addressed to the investor. Returns {status, ...}. Never sends.""" if not sender_email: return {"status": "no_sender"} if not _cfg.CONFIG.enabled: return {"status": "integration_disabled"} subject, body = _parse_subject_body(draft_text) if not body.strip(): return {"status": "empty"} target = _reply_target(conn, investor_id) if not target: return {"status": "no_recipient"} # no email history -> no LP address to draft to try: provider = _creds.build_provider(lambda: conn) token = provider.access_token_for(sender_email, _creds.GMAIL_COMPOSE_SCOPE).token except Exception as exc: return {"status": "auth_error", "reason": str(exc)} raw = _build_raw(sender_email, target["to"], subject, body, target.get("in_reply_to")) payload = {"message": {"raw": raw}} if target.get("thread_id"): payload["message"]["threadId"] = target["thread_id"] url = f"https://gmail.googleapis.com/gmail/v1/users/{urllib.parse.quote(sender_email)}/drafts" req = urllib.request.Request(url, data=_json.dumps(payload).encode("utf-8"), method="POST", headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}) try: with urllib.request.urlopen(req, timeout=20) as resp: _json.loads(resp.read()) except urllib.error.HTTPError as e: return {"status": "gmail_error", "reason": e.read().decode("utf-8", "replace")[:300]} except Exception as exc: return {"status": "gmail_error", "reason": str(exc)} return {"status": "ok", "to": target["to"], "threaded": bool(target.get("thread_id")), "gmail_url": "https://mail.google.com/mail/u/0/#drafts"}