A privacy-first macOS daemon that watches your IMAP mailbox, classifies incoming messages with a fully local LLM (Qwen2.5-14B-Instruct), silently discards spam, and automatically creates Calendar events for work-related emails — no cloud API, no third-party data sharing, ever.
Optimized for Apple Silicon. Sentinella is built primarily around MLX, Apple's native ML framework, to get the best inference speed and memory efficiency out of the Neural Engine/GPU on M-series chips. Intel Macs are also supported via a
llama.cppfallback, but Apple Silicon is the reference, best-performing platform this project is designed for.
Built for a personal Italian mailbox: the scheduling logic understands Italian public holidays and skips weekends when auto-picking task due dates.
- 🔒 100% local LLM inference — no email content ever leaves your machine (Qwen2.5-14B-Instruct via MLX or llama.cpp, no external API calls)
- 🧠 Smart LAVORO/SPAM classification with structured JSON output (title, summary, due date) used to build the calendar event
- 🚫 Two-layer spam filtering — a deterministic sender/domain blocklist
(
.env+ editableblocklist_mittenti.txt, reloaded before every scan) short-circuits obvious spam before it ever reaches the LLM - 📅 Italian holiday-aware scheduling — fallback due dates automatically
skip weekends and Italian public holidays (via the
holidayslibrary) - 🛡️ Prompt-injection guardrail — the LLM is explicitly instructed to treat email content as untrusted data, never as commands
- 🧹 Automatic data retention — local history and logs older than 15 days are purged automatically; attachments are never written to disk (they already live in the original email)
- 🔐 Hardened by design — restrictive
umask 077,0600/0700permissions enforced on every stored file, AppleScript-injection sanitized inputs, path-traversal-safe file handling, and pinned dependencies (including pinned Hugging Face model revisions for supply-chain safety) - ⚡ Apple Silicon-optimized, adaptive AI backend — automatically picks the native MLX engine on M-series chips for best performance, with a llama.cpp (GGUF) fallback and dynamic CPU thread sizing on Intel Macs
This project is built specifically for macOS, since Calendar integration
relies on AppleScript (osascript) — it will not run on Windows or Linux.
- macOS 13.5 (Ventura) or later with the Calendar app configured
(required by
mlx-lmon Apple Silicon) - Python 3.12+
- One of:
- Apple Silicon (M-series) → uses
mlx-lm, Apple's optimized ML framework - Intel Mac → uses
llama-cpp-pythonwith a quantized GGUF model
- Apple Silicon (M-series) → uses
⚠️ On Intel Macs, a plainpip installbuildsllama-cpp-pythonin slow CPU-only mode. See the comment abovellama-cpp-pythoninrequirements.txtfor the recommendedCMAKE_ARGSbuild flags (hardware-accelerated BLAS backend).
This is not a lightweight script: on every scan cycle it loads a full 14-billion-parameter LLM (Qwen2.5-14B-Instruct) into memory, runs inference, then unloads it. Plan your hardware accordingly.
| Resource | Minimum | Recommended |
|---|---|---|
| RAM / unified memory | 16 GB | 32 GB (comfortable multitasking while the model is loaded) |
| Free disk space | 12 GB | 20 GB (model weights + Python dependencies + headroom for updates) |
| Apple Silicon | Any M-series chip (M1–M4) | — |
| Intel Mac CPU | Multi-core, with llama-cpp-python built against a BLAS backend (see warning above) |
— |
Actual model download size, verified from the Hugging Face repos pinned in
watcher.py:
- MLX 4-bit build (Apple Silicon): ~8.3 GB
- GGUF Q4_K_M build (Intel): ~9.0 GB
With only 16 GB of RAM/unified memory, expect the OS and other running apps
to be tightly squeezed while a scan cycle is in progress; the daemon does
unload the model between cycles (unload_llm()) to free that memory back up
for the rest of the system.
git clone https://github.com/Pharma-Py/Sentinella_mail-IT-.git
cd Sentinella_mail-IT-
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
# then edit .env with your IMAP server and email address
cp blocklist_mittenti.example.txt blocklist_mittenti.txt
# optional: list sender addresses/domains to always treat as SPAMYour email password is never stored in .env or anywhere on disk in
plain text. It is saved through Python's keyring library into your Mac's
local "login" Keychain — the one managed by Keychain Access.app — not
the iCloud Keychain. This is intentional: the login Keychain stays on this
Mac and is never synced to your other Apple devices, which is the correct
scope for a background daemon's credential.
- Make sure
EMAIL_USERis already set in.env(see Installation & Setup above) —set_password.pyreads it from there. - Run:
python3 set_password.py
- Enter your email password when prompted (input is hidden, never echoed to the terminal or written to any file).
- The password is stored under the service name
ai_mail_watcher, keyed to yourEMAIL_USER, and read back bywatcher.pyat every scan cycle.
To verify or revoke it later, open Keychain Access.app → search for
ai_mail_watcher → the entry is listed under the "login" keychain (not
under "iCloud").
Once configured, start the watcher with:
python3 watcher.pyFor continuous background operation, wrap it in a launchd .plist
(recommended, native to macOS) or trigger it via an Automator
application/calendar trigger. The script itself manages a night-time pause
window and a batch scan interval internally — no external scheduler cron
logic is required beyond keeping the process alive.
A self-contained test suite (verify_sentinella.py) validates the core logic
and can optionally audit a live installation:
# Logic tests only (no running instance required)
python3 verify_sentinella.py
# Logic tests + installation audit
python3 verify_sentinella.py /path/to/WatcherThe suite covers: JSON extraction, LLM output validation, date/time resolution (weekends, Italian holidays, night-hour clamping, far-future cap), atomic file writes, email text extraction (plain-text preference, attachment exclusion), file permissions, and environment checks.
Security was treated as a first-class requirement, not an afterthought:
- Restrictive
umask 077at startup ensures every file and folder created by the process (mail data, logs, downloaded models) is readable only by its owner; asecure_permissions()sweep also hardens any pre-existing files to0600/0700. - No attachments on disk — attachment names are passed to the LLM as context, but their content is never written locally, since it already exists in the source email on the IMAP server.
- AppleScript-injection-proof — calendar event values are passed via
osascriptargv (no string interpolation), with control characters stripped from all LLM-generated text. - Prompt-injection guardrail — the system prompt explicitly instructs the model to treat email content as untrusted data and to never execute or emit instructions/commands beyond the requested JSON classification.
- Path-traversal-safe handling of filenames extracted from untrusted email headers.
- Pinned dependencies, including pinned Hugging Face model revisions (commit SHAs) for both the MLX and GGUF model downloads, to prevent silent supply-chain changes.
- Automatic data retention — local history/log entries older than 15 days are purged on every scan cycle.
- Robust JSON extraction — brace-depth-tracking parser replaces brittle string slicing; handles unclosed fences, model preamble, trailing text
- Fail-open LLM validation — unknown
categoriadefaults toLAVORO(false positive > missed task); case-insensitive normalization, type/length checks, control-char stripping - Holiday-aware date resolution — injectable
now=for testing; far-future cap (365 days), night-hour clamp (< 7 or >= 20 → 09:00) - Silent task-loss prevention —
mark_as_processed()only on confirmed success; LLM/calendar failures → retry on next cycle - Argv-based AppleScript — no string interpolation = no injection risk; calendar alarm added; WORK_AI must pre-exist (no auto-create to iCloud)
- IMAP hardening — UID-based commands,
BODY.PEEK[], explicit SSL context, 30s timeout,finallyclose/logout, UIDVALIDITY fallback IDs - Single-instance lock —
fcntl.flockprevents duplicate model loads - Atomic history.log writes — write-tmp +
os.replace()with0600permissions - Email extraction — skips attachment parts, prefers plain text over HTML
wait_for_system_idletimeout — max 120 min to prevent infinite block- Test suite (
verify_sentinella.py) — 28 tests covering all core logic- optional installation audit
MIT