Email Autonomy Stack
SPECIFICATION: Email Autonomy Stack (Microsoft Graph API)
Status: AUTHORIZED
Authorized: α.13, April 16 2026
Version: v1.0
Version: v1.0
PURPOSE
Microsoft Graph API email automation layer for oracle@42sisters.ai. Provides C.L.O.D. and the crew with the ability to read, send, archive, and delete email programmatically. Serves the Oracle Toll pipeline and any crew automation that requires inbox monitoring or outbound communication through the oracle address.
COMPONENTS
| File | Function | Status |
|---|---|---|
| check_inbox.py | Read unread messages; returns list of message dicts | VERIFIED — exists |
| email_send.py | Send email via Graph API; saves to Sent Items | VERIFIED — exists |
| email_archive.py | Move messages to Archive folder | [GAP-01 — existence unverified] |
| email_delete.py | Delete messages | [GAP-01 — existence unverified] |
| graph_client.py | Shared Graph API client utility | [GAP-02 — may not exist as standalone module] |
INPUTS
| Input | Source | Notes |
|---|---|---|
| GRAPH_TENANT_ID | .env | Microsoft tenant identifier |
| GRAPH_CLIENT_ID | .env | OAuth application client ID |
| GRAPH_REFRESH_TOKEN | .env | Long-lived token; auto-rotated on use |
| GRAPH_SENDER | .env | Defaults to oracle@42sisters.ai |
| to (send) | Caller argument | Recipient address string |
| subject (send) | Caller argument | Email subject line |
| body (send) | Caller argument | Email body (plain text or HTML) |
| Message ID (archive/delete) | Caller argument | Graph API message GUID |
| MAX_MESSAGES | Hardcoded 5 | Cap on messages returned per inbox read |
| BODY_PREVIEW_CHARS | Hardcoded 200 | Characters of body returned in preview |
OUTPUTS
| Output | Component | Format | Notes |
|---|---|---|---|
| Message list | check_inbox.py | list[dict] with keys: id, sender, subject, received, body_preview | Returns up to MAX_MESSAGES unread |
| Send confirmation | email_send.py | Log line + exit code | Exit 0 = success, Exit 1 = failure |
| Rotated refresh token | Auth flow (all components) | Written back to .env at GRAPH_REFRESH_TOKEN | Must happen on every successful auth call |
| Archive confirmation | email_archive.py | Log line | [GAP-01 — behavior unverified] |
| Delete confirmation | email_delete.py | Log line | [GAP-01 — behavior unverified] |
INVARIANTS
| ID | Invariant | Enforcement |
|---|---|---|
| INV-01 | Refresh token MUST be rotated and persisted to .env on every successful auth call | get_token() flow responsibility; token not persisted = next call will fail |
| INV-02 | access_token and refresh_token values must NEVER be printed, logged, echoed, or written to any file other than .env | Absolute; same principle as vault.json — C.L.O.D. uses the token, never exposes it |
| INV-03 | GRAPH_SENDER defaults to oracle@42sisters.ai if env var absent | Prevents accidental send from wrong account |
| INV-04 | All components use the shared get_token() flow; no component implements its own auth | Single auth path = single rotation point |
| INV-05 | email_send.py exits with code 1 on any failure, code 0 on success | Callers must check exit code before assuming delivery |
| INV-06 | Body preview is capped at BODY_PREVIEW_CHARS (200) in check_inbox.py | Full body must be fetched via separate Graph call if needed |
| INV-07 | Message reads are limited to MAX_MESSAGES (5) per call | Prevents bulk ingestion in a single cycle; caller must page if needed |
VERIFICATION CRITERIA
| ID | Criterion | Method |
|---|---|---|
| VER-01 | check_inbox.py returns valid list[dict] with required keys | Run against live inbox; assert id, sender, subject, received, body_preview all present in each item |
| VER-02 | Refresh token is updated in .env after successful check_inbox.py run | Record token value before run; run script; assert .env GRAPH_REFRESH_TOKEN differs from pre-run value |
| VER-03 | email_send.py exits 0 and message appears in Sent Items | Send test email to jzlabis@gmail.com; verify exit code; check Sent Items via Graph or inbox |
| VER-04 | email_send.py exits 1 on bad recipient or network failure | Mock Graph endpoint failure; assert exit code = 1 |
| VER-05 | No token values appear in any log file or stdout | Run all components; grep log outputs for token-shaped strings (40+ char alphanumeric); assert no matches |
| VER-06 | email_archive.py moves message out of Inbox folder | [GAP-01 — cannot verify until existence confirmed] |
| VER-07 | email_delete.py removes message from all folders | [GAP-01 — cannot verify until existence confirmed] |
| VER-08 | Shared get_token() is the sole auth entry point | Code audit: grep all components for requests.post.*oauth; must appear only in one location |
FAILURE MODES
| ID | Failure | Expected Behavior | Current Gap |
|---|---|---|---|
| FM-01 | Refresh token expired or revoked (Microsoft revokes on password change, policy reset) | Auth call returns 400/401; script should log error and exit 1; NOUS must re-authorize manually | [GAP-03 — no retry; no graceful degradation path documented] |
| FM-02 | Graph API returns 429 (rate limit) | Back off and retry after Retry-After header interval | [GAP-04 — no retry logic on 429] |
| FM-03 | Graph API returns 5xx (Microsoft-side failure) | Retry with exponential backoff up to N attempts; log failure; exit 1 | [GAP-04 — no retry logic on 5xx] |
| FM-04 | .env write fails during token rotation (disk full, permission) | New token lost; next call fails; silent failure possible | [GAP-05 — token rotation failure not handled explicitly] |
| FM-05 | Token used after expiry window (access_token lifetime ~1hr) | Graph returns 401; get_token() should detect and refresh before request | [GAP-06 — expiry window not validated before use; reactive not proactive] |
| FM-06 | GRAPH_SENDER env var missing | Defaults to oracle@42sisters.ai per INV-03; if default also missing, send fails | Should raise explicit config error rather than silent failure |
| FM-07 | email_archive.py or email_delete.py called but file does not exist | ImportError or FileNotFoundError at caller | [GAP-01 — components unverified] |
| FM-08 | Message body contains credentials or sensitive data surfaced in body_preview | Preview truncated at 200 chars, but first 200 chars could still contain sensitive content | No content scanning on preview output |
GAPS
| ID | Gap | Severity | Resolution Path |
|---|---|---|---|
| GAP-01 | email_archive.py and email_delete.py existence unverified; assumed from oracle_inbox_watch references only | HIGH | Run ls /home/nous/email_archive.py /home/nous/email_delete.py; confirm or mark as NOT BUILT |
| GAP-02 | graph_client.py may not exist as standalone shared module; auth logic may be duplicated inline per script | MEDIUM | Audit all email scripts for duplicated get_token() implementations; consolidate if needed |
| GAP-03 | No documented recovery path when refresh token expires or is revoked; requires manual NOUS re-authorization | HIGH | Document re-auth procedure in PLAYBOOK.md; consider alerting NOUS via ALERT.log on 401 |
| GAP-04 | No retry logic on Graph API 429 (rate limit) or 5xx (server error) responses | MEDIUM | Add exponential backoff wrapper around all Graph API calls; honor Retry-After header on 429 |
| GAP-05 | Token rotation failure (e.g., .env write error) is not explicitly handled; new token may be silently lost | HIGH | Wrap .env write in try/except; on failure, log to ALERT.log and abort rather than continue with stale token |
| GAP-06 | access_token expiry window not validated before use; token is used reactively (retry on 401) rather than proactively | LOW | Add token expiry timestamp tracking; refresh proactively when within 5 minutes of expiry |
| GAP-07 | No test coverage for token rotation path; correctness of rotation assumed | MEDIUM | Unit test: mock OAuth endpoint returning new token; assert .env updated; assert old token not in any output |
| GAP-08 | No pagination support in check_inbox.py; MAX_MESSAGES=5 cap means high-volume inboxes silently drop messages | MEDIUM | Implement Graph API @odata.nextLink pagination for callers that need full inbox sweep |
Jeremy Zlabis
Chronogeometer · Visionary · Disruptor · Chief
42 Sisters AI · East York, Toronto
🍁 Φ 0.042