import json, os, sys, time, urllib.parse, urllib.request, urllib.error import jwt import base64 import mimetypes from email import encoders from email.mime.base import MIMEBase from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText TOKEN_URL = "https://oauth2.googleapis.com/token" def _cfg(): return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) def _token(scopes, subject=None): cfg = _cfg() raw = cfg.get("service_account_json", "") sa = json.loads(raw) if isinstance(raw, str) else raw if not sa.get("client_email") or not sa.get("private_key"): raise Exception("service_account_json must contain client_email and private_key") now = int(time.time()) aud = sa.get("token_uri") or TOKEN_URL payload = { "iss": sa["client_email"], "scope": " ".join(scopes), "aud": aud, "iat": now, "exp": now + 3600, } sub = subject or cfg.get("user_id") or "" if sub: payload["sub"] = sub assertion = jwt.encode(payload, sa["private_key"], algorithm="RS256") data = urllib.parse.urlencode({ "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "assertion": assertion, }).encode("utf-8") req = urllib.request.Request(aud, data=data, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST") with urllib.request.urlopen(req, timeout=60) as r: tok = json.loads(r.read()) if not tok.get("access_token"): raise Exception("Token request failed: " + json.dumps(tok)) return tok["access_token"] def request(method, url, scopes, params=None, body=None, subject=None): q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} if q: url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) data = json.dumps(body).encode("utf-8") if body is not None else None headers = {"Accept": "application/json", "Authorization": "Bearer " + _token(scopes, subject)} if data is not None: headers["Content-Type"] = "application/json" req = urllib.request.Request(url, data=data, headers=headers, method=method) with urllib.request.urlopen(req, timeout=90) as r: raw = r.read() return json.loads(raw) if raw else {} SCOPES = [ "https://www.googleapis.com/auth/gmail.compose", "https://www.googleapis.com/auth/gmail.send", ] def _build_message(inputs, mailbox): body = str(inputs.get("body") or "") html_body = str(inputs.get("html_body") or "") att_name = str(inputs.get("attachment_name") or "") att_b64 = str(inputs.get("attachment_base64") or "") if html_body or att_b64: message = MIMEMultipart("mixed" if att_b64 else "alternative") if body: message.attach(MIMEText(body, "plain", "utf-8")) if html_body: message.attach(MIMEText(html_body, "html", "utf-8")) if att_b64: name = att_name or "attachment.bin" ctype = mimetypes.guess_type(name)[0] or "application/octet-stream" main_type, sub_type = ctype.split("/", 1) part = MIMEBase(main_type, sub_type) part.set_payload(base64.b64decode(att_b64)) encoders.encode_base64(part) part.add_header("Content-Disposition", "attachment", filename=name) message.attach(part) else: message = MIMEText(body, "plain", "utf-8") message["From"] = mailbox message["To"] = str(inputs.get("to") or "") if inputs.get("cc"): message["Cc"] = str(inputs["cc"]) if inputs.get("bcc"): message["Bcc"] = str(inputs["bcc"]) message["Subject"] = str(inputs.get("subject") or "") if inputs.get("reply_to"): message["Reply-To"] = str(inputs["reply_to"]) if inputs.get("in_reply_to"): message["In-Reply-To"] = str(inputs["in_reply_to"]) if inputs.get("references"): message["References"] = str(inputs["references"]) return message def main(): inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) mailbox = str(inputs.get("user_id") or _cfg().get("user_id") or "me") base = "https://gmail.googleapis.com/gmail/v1/users/" + urllib.parse.quote(mailbox, safe="") if not str(inputs.get("to") or "").strip(): raise Exception("to is required") if not str(inputs.get("subject") or "").strip(): raise Exception("subject is required") message = _build_message(inputs, mailbox) raw = base64.urlsafe_b64encode(message.as_bytes()).decode("ascii") res = request("POST", base + "/messages/send", SCOPES, body={"raw": raw}, subject=mailbox) print(json.dumps(res)) try: main() except urllib.error.HTTPError as e: print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) sys.exit(1) except Exception as e: print(json.dumps({"error": str(e)})) sys.exit(1)