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)...
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) |