Export Outlook emails as markdown and JSON without Microsoft Graph API or admin approval. Scan, search, and filter your Mac Outlook desktop emails using the macOS Accessibility API -- no OAuth, no API keys, no IT ticket required.
Built for developers, data scientists, and power users who need their Outlook emails accessible to AI tools like Claude Code, Cursor, Copilot, or any LLM that reads local files.
Microsoft Graph API requires tenant admin consent, OAuth setup, and weeks of IT approval. This tool bypasses all of that by reading Outlook's UI directly through the macOS Accessibility tree. It's ~500x faster than screenshot-based automation and costs nothing.
| Microsoft Graph API | Outlook Scanner | |
|---|---|---|
| Admin approval | Required | Not needed |
| Setup time | Days to weeks | 2 minutes |
| OAuth / API keys | Required | None |
| Rate limits | Yes | None |
| Works offline | No | Yes |
| Data leaves machine | Yes | Never |
- Scan up to 30 emails per run
- Single-account scanning
- Markdown output -- clean, structured files with sender, recipients, date, body
- Smart deduplication -- persistent across runs, never re-scans the same email
- Claude Code skill -- say "check my inbox" and Claude reads your emails
- 100% local and private -- emails never leave your machine
- Scan up to 500 emails per run
- Multi-account auto-discovery -- detects all Outlook accounts automatically
- Multi-folder scanning -- Inbox, Sent Items, Drafts, or any custom folder
- Keyword search -- search your Outlook via the scanner, save results as markdown + JSON
- Date range filtering -- scan emails from specific time periods
- Sender and domain filtering -- include/exclude specific senders or domains
- Unread-only mode -- scan only unread emails
- JSON output -- structured JSON alongside markdown for programmatic use
- Scan manifests -- metadata files tracking every scan run
- Persistent scan logging -- full scan history at
~/.outlook-scanner/scan_log.json - Attachment detection -- identifies attached files with names and types
- YAML configuration -- customize behavior via
~/.outlook-scanner/config.yaml
- macOS 14+ (Sonoma or later)
- Microsoft Outlook for Mac (version 16.x)
- Python 3.10+
- Accessibility permissions for your terminal app
git clone https://gitlab.com/timo2026/outlook-email-scanner.git
cd outlook-email-scanner
pip install -e .System Settings > Privacy & Security > Accessibility -- add your terminal app (Terminal, iTerm2, Warp) or AI coding tool (Cursor, VS Code).
This single permission covers both reading Outlook's UI tree and mouse/keyboard control for scrolling and account switching.
from src.scanner import scan_outlook
stats = scan_outlook()
print(stats)
# {'found': 15, 'processed': 10, 'skipped': 5, 'errors': 0}Emails are saved to ~/Desktop/outlook-emails/default/ as markdown files.
from src.scanner import scan_outlook
# Scan the currently active folder
stats = scan_outlook()from src.scanner import scan_outlook
# Search for specific emails, save matching results
stats = scan_outlook(search_query="quarterly report")from src.scanner import scan_outlook
# Scan Inbox, Sent Items, and Drafts
stats = scan_outlook(
account="work",
folders=["Inbox", "Sent Items", "Drafts"],
)
# Per-folder results available in stats["folders"]
for folder, folder_stats in stats["folders"].items():
print(f"{folder}: {folder_stats}")from src.scanner import scan_all_outlook_accounts
# Auto-discovers all accounts from Outlook sidebar
results = scan_all_outlook_accounts()
for account, stats in results.items():
print(f"{account}: {stats}")from src.scanner import discover_accounts
accounts = discover_accounts()
print(accounts)
# ['alice@company.com', 'bob@gmail.com']from datetime import datetime
from src.scanner import scan_outlook
from src.controller import OutlookController
from src.reader import OutlookReader
reader = OutlookReader()
reader.connect()
controller = OutlookController(
reader,
account="work",
unread_only=True, # Only unread emails
since=datetime(2026, 3, 1), # From March 1st
until=datetime(2026, 3, 31), # To March 31st
include_domains=["company.com"], # Only from company.com
exclude_senders=["noreply@company.com"], # Skip automated emails
)
stats = controller.run()
print(stats)
# {'found': 50, 'processed': 12, 'skipped': 3, 'filtered': 35, 'errors': 0}# Quick scan
python -c "from src.scanner import scan_outlook; print(scan_outlook())"
# Search scan
python -c "from src.scanner import scan_outlook; print(scan_outlook(search_query='invoice'))"This repo includes a SKILL.md file that works as a Claude Code skill. Install it and say "check my inbox":
mkdir -p ~/.claude/skills/outlook-email-scan
cp SKILL.md ~/.claude/skills/outlook-email-scan/SKILL.mdThe scanner saves emails as plain markdown files to ~/Desktop/outlook-emails/. Point any AI tool at that directory to read, search, summarize, or analyze your emails.
Each email is saved as YYYY-MM-DD-Subject.md:
# Meeting Tomorrow
**From:** Alice Smith <alice@company.com>
**To:** Team <team@company.com>
**Date:** 2026-03-18 09:30
## Body
Hi team,
Just a reminder about our meeting tomorrow at 2 PM.
Best,
AliceEach email also saved as YYYY-MM-DD-Subject.json for programmatic access:
{
"subject": "Meeting Tomorrow",
"sender": "Alice Smith <alice@company.com>",
"recipients": "Team <team@company.com>",
"timestamp": "2026-03-18 09:30",
"body": "Hi team...",
"account": "work",
"folder": "Inbox",
"saved_at": "2026-03-18T10:00:00Z",
"has_attachments": false
}Scan manifests (scan-{date}.json) are also generated with metadata for each scan run.
Create ~/.outlook-scanner/config.yaml to customize behavior:
# Timing (seconds)
screenshot_wait: 1.5
scroll_wait: 1.0
scan_timeout: 900
# Limits (overridden by Pro license)
max_emails: 30
max_scrolls: 5
# Output
output_dir: ~/Desktop/outlook-emails
# Folders to skip when scanning all folders
blacklisted_folders:
- Archive
- Junk Email
- Deleted Items
- Trash
- SpamThe scanner uses a modular architecture with 8 components:
| Module | Purpose |
|---|---|
scanner.py |
High-level entry point. Multi-account and multi-folder orchestration |
controller.py |
Scan engine. Iterates emails, applies filters, saves output, logs stats |
reader.py |
Low-level Accessibility API interface via atomacos |
config.py |
YAML-based configuration with license-tier awareness |
filters.py |
Date range, sender, and domain filtering |
folders.py |
Folder navigation and blacklist management |
dedup.py |
Persistent hash-based deduplication across runs |
structured.py |
JSON output and scan manifest generation |
license.py |
Pro license activation and validation |
Window ("Inbox - user@company.com")
+-- SplitGroup (main)
+-- SplitGroup (inner)
+-- SplitGroup (content)
|-- Group [0] -> message list
| +-- ScrollArea -> Table -> Rows
|-- Splitter [1]
|-- Group [2] -> email headers (subject, from, to)
|-- ScrollArea [3]
+-- Group [4] -> email body
+-- ... -> WebArea -> StaticText elements
All scanner data lives in ~/.outlook-scanner/:
| File | Purpose |
|---|---|
config.yaml |
User configuration overrides |
seen_hashes.json |
Deduplication hashes (persistent across runs) |
scan_log.json |
Scan history (last 1000 runs) |
license.json |
Pro license token |
Email output goes to ~/Desktop/outlook-emails/<account>/.
- macOS only -- uses the macOS Accessibility API
- Outlook for Mac only -- tested with Outlook 16.x
- Requires Outlook to be open -- this is desktop automation, not an API
- Reading pane must be visible -- the scanner reads from the reading pane
- No attachment download -- detects attachments but doesn't download them (planned)
pip install -e ".[dev]"
pytestAll tests mock the Accessibility API so they run without Outlook.
MIT -- see LICENSE for details.
Built by Timo Labs.