Headless University of Toronto Outlook email access via IMAP/SMTP with OAuth2. Uses Thunderbird's pre-authorized client ID to bypass admin consent requirements (AADSTS65002). Device code flow for initial auth, macOS Keychain for token cache.
Headless access to University of Toronto alumni/student Outlook via IMAP/SMTP with OAuth2.
Trit: -1 (MINUS - validator/consumer)
Principle: Thunderbird Client ID → Device Code Auth → Keychain Cache → IMAP/SMTP
Implementation: IMAP OAuth2 (XOAUTH2) + Thunderbird Pre-Authorized Client ID
University tenants block third-party OAuth apps:
AADSTS65002: Consent between first party application and first party resource
must be configured via preauthorization
Solution: Use Thunderbird's pre-authorized client ID 9e5f94bc-e8a4-4e73-b8be-63364c29d753 which Microsoft has pre-approved for IMAP/SMTP access on all tenants.
┌─────────────────────────────────────────────────────────────────────┐
│ THUNDERBIRD CLIENT ID BYPASS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ [Problem: Graph API blocked] │
│ ┌──────────┐ Graph API ┌───────────────┐ │
│ │ Agent │ ────────────────▶ │ MS Entra ID │ │
│ └──────────┘ └───────────────┘ │
│ │ │ │
│ │ ▼ │
│ │ ❌ AADSTS65002 Error │
│ │ "Admin consent required" │
│ │
│ [Solution: Thunderbird IMAP] │
│ ┌──────────┐ Thunderbird ID ┌───────────────┐ │
│ │ Agent │ ─────────────────▶ │ MS Entra ID │ │
│ └──────────┘ 9e5f94bc-... └───────────────┘ │
│ │ │ │
│ │ Device code flow │ Pre-authorized ✓ │
│ ▼ ▼ │
│ "Enter code XXXXXX at microsoft.com/devicelogin" │
│ │ │
│ │ User authenticates (one-time) │
│ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ macOS Keychain (secure storage) │ │
│ │ outlook-university: access + refresh tokens │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ │ XOAUTH2 authentication │
│ ▼ │
│ ┌───────────────────────────────────────────────┐ │
│ │ outlook.office365.com:993 (IMAP) │ │
│ │ smtp.office365.com:587 (SMTP) │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
# Thunderbird's pre-authorized client ID (public, safe to commit)
THUNDERBIRD_CLIENT_ID = "9e5f94bc-e8a4-4e73-b8be-63364c29d753"
# IMAP OAuth2 scopes (NOT Graph API scopes!)
IMAP_SCOPES = [
"https://outlook.office.com/IMAP.AccessAsUser.All",
"https://outlook.office.com/SMTP.Send",
"offline_access",
"openid", "profile", "email"
]
# Servers
IMAP_SERVER = "outlook.office365.com" # Port 993 SSL
SMTP_SERVER = "smtp.office365.com" # Port 587 STARTTLS
cd ~/.claude/skills/utoronto-outlook
uv run python outlook_university.py auth
# Output:
# ============================================================
# OUTLOOK UNIVERSITY - DEVICE CODE AUTHENTICATION
# ============================================================
# Code: XXXXXXXXX
# Go to: https://microsoft.com/devicelogin
# (Uses Thunderbird's pre-authorized client ID)
# ============================================================
# Check login
uv run python outlook_university.py whoami
# Logged in as: yulia.zubak@alumni.utoronto.ca
# List messages
uv run python outlook_university.py list 10
# Read message
uv run python outlook_university.py read 42
# Search
uv run python outlook_university.py search "professor"
# List folders
uv run python outlook_university.py folders
from outlook_university import OutlookClient
client = OutlookClient()
# List recent emails
messages = client.list_messages(limit=10)
# Get unread
unread = client.get_unread()
# Read full message (body truncated to 2000 chars for context safety)
msg = client.get_message("42")
# Search
results = client.search("grades")
# Send email
client.send(
to=["recipient@example.com"],
subject="Test",
body="Hello from headless Outlook!"
)
client.close()
The critical implementation detail for IMAP OAuth2:
# Build XOAUTH2 string per RFC 7628
auth_string = f"user={email}\x01auth=Bearer {access_token}\x01\x01"
# IMAP authenticate callback returns raw bytes
conn.authenticate("XOAUTH2", lambda x: auth_string.encode())
| Operation | Trit | Description |
|-----------|------|-------------|
| list_messages | -1 | Consume/read inbox (MINUS) |
| get_message | -1 | Read specific message (MINUS) |
| get_unread | -1 | Query unread (MINUS) |
| search | -1 | Query messages (MINUS) |
| list_folders | 0 | Metadata access (ERGODIC) |
| send | +1 | Generate output (PLUS) |
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer