See an important follow up to this post first!
I wanted a simple system: watch Gmail for messages that actually matter—especially job application replies—and send a WhatsApp alert when something actionable shows up.
The first version worked. It was also unnecessarily complicated.
What I really needed was something predictable, cheap to run, and easy to extend—not a clever system I’d have to debug at 2am.
What I Actually Wanted
Instead of chasing real-time perfection, the real requirements were:
- Predictable behavior
- Cheap filtering before calling AI
- Simple, readable rules
- No raw email stored in git
- No full mailbox sync
- Something other people could copy
The final system delivers exactly that.
The Original Architecture (Too Clever)
The first version used Gmail push events:
Gmail push event
→ local watcher
→ OpenClaw gateway
→ session artifact
→ systemd path trigger
→ scanner
→ classifier
→ WhatsApp alert
It worked—but it raised too many operational questions:
- Who owns the Gmail watcher?
- What actually sends the alert?
- Where do failures happen?
- Why didn’t something trigger?
For something that can wait 12 hours, this was overkill.
The First Pivot: Local Sync (Also Wrong)
Next idea: sync Gmail locally.
Gmail IMAP
→ mbsync
→ local Maildir
→ scanner
→ rules + classifier
→ alert
This looked clean on paper.
Reality check:
- Inbox had ~125,000 emails
- Syncing that just to read “last day” messages is wasteful
- Even partial syncs still pay the cost of a huge mailbox
Lesson: “not AI” doesn’t mean “cheap.”
The Real Solution: Query Gmail Directly
The final design skips syncing entirely:
systemd timer
→ mail-alert script
→ Gmail IMAP SEARCH SINCE <date>
→ fetch recent messages only
→ YAML rules
→ deterministic prefilter
→ optional classifier
→ WhatsApp alert
→ metadata-only state
This gives you:
- Bounded cost
- Simpler debugging
- No local mailbox
Yes, latency is worse—but clarity is much better.
How It Runs in Practice
- Runs twice a day (~08:00 and 20:00)
- Looks back 24 hours
- Each message is processed at most twice
Simple, predictable, boring—and that’s a good thing.
Rule Design: One YAML File Per Alert
Instead of a giant config, each rule lives in its own file:
config/mail-alert-rules/
google-security-alert.yaml
job-application-response.yaml
This makes it easy to:
- Add a rule
- Disable a rule
- Copy and tweak
Example: Deterministic Rule
id: google-security-alert
enabled: true
window: 1d
folders:
- INBOX
match:
all:
from_contains:
- google
subject_contains:
- Security alert
alert:
channel: whatsapp
title: Google security alert
Example: Rule + AI Classification
id: job-application-response
enabled: true
window: 1d
folders:
- INBOX
match:
any:
subject_contains:
- application
- interview
- recruiter
- hiring
body_contains:
- thank you for applying
- schedule
classify:
prompt: job_application_response
positive_key: is_job_application_response
alert:
channel: whatsapp
title: Possible job application reply
Key Design Principle
- Use cheap string matching first
- Only call AI when necessary
- Keep everything human-readable
If your prefilter is too strict, AI never runs—and accuracy doesn’t matter.
Duplicate Prevention (Without Storing Emails)
Polling means overlap.
Instead of storing full emails:
- Store metadata only
- Track what’s been evaluated
- Allow manual bypass for testing
Clean and privacy-friendly.
The Final Operational Model
systemd timer
→ mail-alert service
→ Gmail IMAP
→ YAML rules
→ optional classification
→ WhatsApp alert
That’s it.
No sync. No push events. No hidden state.
What I’d Copy If You Build This
Use direct IMAP if:
- Latency isn’t critical
- Mailbox is large
- You only need recent messages
- You want predictable cost
Use local sync if:
- You truly need offline mail
- Mailbox is small
- You want full local access
For this use case, IMAP search wins.
The Core Pattern
This is the part worth reusing:
- Poll on a schedule that matches urgency
- Search before fetching
- Keep rules human-readable
- Run deterministic checks before AI
- Store metadata-only state
- Hash rule behavior for re-evaluation
- Make duplicate bypass explicit
- Keep secrets out of git
What Went Wrong (And Why It Helped)
Mistakes along the way:
- Built a push system when polling was enough
- Assumed sync would be cheap
- Tried syncing a huge inbox
- Made prefilter rules too narrow
- Ignored “no-match” evaluations
None of these were catastrophic—they were useful.
The End Result
The system is now:
- Simple
- Cheap
- Understandable
- Easy to extend
Adding a new alert is a small, human-scale task:
scripts/mail-alert-rule new bank-security-alert
$EDITOR config/mail-alert-rules/bank-security-alert.yaml
scripts/mail-alert-rule validate bank-security-alert
scripts/mail-alert --dry-run --rule bank-security-alert
That’s the goal:
A system you actually use.
Leave a Reply