A privacy-first email digest that runs entirely on your machine. No cloud LLMs, no paid APIs – your emails never leave your Mac.
Every morning and evening it fetches your Gmail, runs it through a local Qwen3 model via Ollama, labels important emails, and sends you a clean digest on Telegram.
- Fetches emails from the last 24h via Gmail API (skips social/spam)
- Skips emails already processed in previous runs
- Sends them to
qwen3:14brunning locally via Ollama - Labels important emails with
AGENT_IMPORTANTin Gmail - Pushes a digest + action items to your Telegram
- macOS (launchd scheduling)
- Python 3.10+
- Ollama installed and running
- Gmail account with API access
- Telegram account
curl -fsSL https://ollama.com/install.sh | sh
ollama pull qwen3:14bgit clone <repo-url>
cd email-agent
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt- Go to console.cloud.google.com
- Create a project → enable Gmail API
- Go to Credentials → Create OAuth 2.0 Client ID → Desktop app
- Download the JSON → save as
credentials.jsonin the project root - Go to OAuth consent screen → Test users → add your Gmail address
On first run a browser window opens for authorization. A token.json is saved and refreshed automatically on subsequent runs.
- Open Telegram → search
@BotFather→/newbot→ follow prompts → copy the token - Start your bot, send any message to it and then open this URL to get your chat ID:
Your chat ID is the
https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates"id"field inside"chat".
cp .env.example .envEdit .env:
TELEGRAM_TOKEN=your_bot_token_here
TELEGRAM_CHAT_ID=your_chat_id_here
source venv/bin/activate
python agent.pyThe first run opens a browser for Gmail authorization. After that it runs silently.
Create ~/Library/LaunchAgents/com.user.email-agent.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.email-agent</string>
<key>ProgramArguments</key>
<array>
<string>/ABSOLUTE/PATH/TO/email-agent/venv/bin/python</string>
<string>/ABSOLUTE/PATH/TO/email-agent/agent.py</string>
<string>--scheduled</string>
</array>
<key>WorkingDirectory</key>
<string>/ABSOLUTE/PATH/TO/email-agent</string>
<key>StartCalendarInterval</key>
<array>
<dict>
<key>Hour</key><integer>8</integer>
<key>Minute</key><integer>30</integer>
</dict>
<dict>
<key>Hour</key><integer>16</integer>
<key>Minute</key><integer>30</integer>
</dict>
</array>
<key>StandardOutPath</key>
<string>/ABSOLUTE/PATH/TO/email-agent/agent.log</string>
<key>StandardErrorPath</key>
<string>/ABSOLUTE/PATH/TO/email-agent/agent.log</string>
<key>RunAtLoad</key>
<false/>
</dict>
</plist>Replace /ABSOLUTE/PATH/TO/email-agent with your actual path (pwd in the project folder).
Load it:
launchctl load ~/Library/LaunchAgents/com.user.email-agent.plistIf your Mac was asleep at the scheduled time, launchd runs the missed job on next wake.
In agent.py, find this line inside analyze_emails():
Mark as important: bills, payment deadlines, calendar invites, anything requiring a response within 48h, legal/official documents, delivery notifications.Add your own rules in plain English – the model understands natural language:
any emails from [email protected], invoices from [email protected]
Edit StartCalendarInterval in the plist to change when it runs. Each <dict> block is one trigger.
MIN_INTERVAL_HOURS = 5 at the top of agent.py prevents accidental double-runs when triggering manually. Scheduled runs via launchd (--scheduled flag) always bypass this.
Change model='qwen3:14b' in analyze_emails() to any model you have pulled in Ollama. Smaller models may produce less reliable JSON output.
email-agent/
├── agent.py # main script
├── requirements.txt
├── .env # secrets – never commit
├── .env.example # template
├── credentials.json # Gmail OAuth client – never commit
├── token.json # auto-generated on first run
├── processed_emails.json # auto-generated, deduplication cache
├── last_run.txt # auto-generated, cooldown timestamp
└── agent.log # auto-generated, rotates at 1MB
credentials.jsonandtoken.jsongrant access to your Gmail – never commit them.envcontains your Telegram token – never commit it- All LLM inference is local via Ollama – email content never reaches any external server
- Ollama binds to
localhostby default – not exposed to the network
