Files
Guillaume BOURGEOIS 27615636e3 feat(gmail): new Gmail integration
Gmail API + Directory API (27 commands): mailbox search/read/send/
trash/label/move, attachments retrieval, vacation auto-reply, filters,
forwarding addresses, delegates, and Workspace user administration;
message ingestion (get_incidents) with a bundled OCSF mapper.
Service-account auth with domain-wide delegation (JWT RS256), runs on
a remote engine (requires PyJWT + cryptography).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:18:07 +02:00

134 lines
4.9 KiB
Python

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)