Files
hermes-skills/skills/email/email-workflows/references/imap-email-triage-direct-imap-triage-pattern.md

2.9 KiB

Direct IMAP triage implementation pattern

Use this reference when Himalaya is not available or a minimal Python stdlib path is preferable.

Credential handling

  • Store the mailbox password outside the script, e.g. /root/.config/himalaya/<account>.pass or a secret-manager command.
  • Set password-file mode to 0600 and verify existence/mode before login.
  • Never hardcode passwords into the script or skill.

IMAP verification

Use imaplib.IMAP4_SSL(host, 993, ssl_context=ssl.create_default_context()), then:

  1. login(user, password)
  2. list() to verify folder names
  3. select('INBOX', readonly=True)
  4. uid('SEARCH', None, 'UNSEEN') for unread-only triage, or ALL when requested

Folder names with spaces

Quote mailbox names explicitly. Some IMAP servers will interpret an unquoted Suspected Spam as separate atoms and create a folder named Suspected.

def q(name: str) -> str:
    return '"' + name.replace('\\', '\\\\').replace('"', '\\"') + '"'

M._simple_command('CREATE', q('Suspected Spam'))
M.uid('COPY', uid, q('Suspected Spam'))

After any create operation, re-run list() and remove accidental test folders only after selecting them and confirming they contain zero messages.

Moving without deleting permanently

For high-confidence spam/phishing only:

M.select('INBOX', readonly=False)
M.uid('COPY', uid, q('Suspected Spam'))
M.uid('STORE', uid, '+FLAGS', '(\\Deleted)')
M.expunge()

This moves by copy-then-delete-original. It is not permanent deletion because the message exists in the quarantine folder first. Log every move.

State and audit log

Keep local state under ~/.hermes/email_triage/:

  • state.json: processed UIDs keyed by UID with decision, reason, moved_to, timestamp.
  • actions.jsonl: append-only audit log with timestamp, UID, sender/subject if available, decision, and reason.

Use UID or Message-ID to avoid reprocessing. UID is adequate for one mailbox/folder; Message-ID is safer across folders.

Unlimited collection semantics

If the user controls the mail server or explicitly authorizes full processing, do not impose arbitrary caps such as 10 messages per run. Let --max 0 mean unlimited:

selected = list(reversed(uids)) if max_messages <= 0 else list(reversed(uids))[:max_messages]

If the resulting JSON is too large for an LLM run, the issue is model context, not IMAP server load. Use chunking or local deterministic pre-classification while preserving the user's permission to process the full mailbox.

Cron integration

Use a small collection wrapper in ~/.hermes/scripts/ and pass it to Hermes cron by relative filename, not absolute path:

# ~/.hermes/scripts/imap_triage_collect.sh
#!/usr/bin/env bash
set -euo pipefail
python3 /root/.hermes/scripts/imap_triage.py --collect

Cron create/update expects script="imap_triage_collect.sh"; absolute script paths are rejected for scripts already under ~/.hermes/scripts/.