Appearance
Local Testing Guide — Mail Poller on Mac
Before deploying to Z2, test mail-poller locally on your Mac to verify:
- Credential loading
- IMAP and Graph API connectivity
- State persistence
- Archive format
Prerequisites
- Python 3.11+
- Credentials available (from ct103 or local copies)
- Network access to Gmail IMAP and Microsoft Graph API
Step 1: Prepare Test Environment
Create a local testing directory:
bash
mkdir -p /tmp/mail-poller-test/{archive,logs}
# Copy credentials
cp -r /path/to/itachi-credentials /tmp/mail-poller-test/credentials
chmod 600 /tmp/mail-poller-test/credentials/*.json
# Copy script
cp projects/brain/mail-poller-z2/mail-poller.py /tmp/mail-poller-test/Step 2: Modify Script for Testing (temporary)
Edit /tmp/mail-poller-test/mail-poller.py to use test directories:
python
# Line 50: Change to test paths
STATE_DIR = Path("/tmp/mail-poller-test")
ARCHIVE_DIR = STATE_DIR / "archive"
CREDENTIALS_PATH = "/tmp/mail-poller-test/credentials"Or use environment variables (preferred):
bash
# Modify script to read from env:
# CREDENTIALS_PATH = os.getenv("CREDENTIALS_PATH", "/opt/itachi/credentials")
# STATE_DIR = Path(os.getenv("STATE_DIR", "/opt/hinata/mail-poller"))Step 3: Test Individual Accounts
Test Gmail (IMAP)
Dry-run (no writes):
bash
cd /tmp/mail-poller-test
python3 mail-poller.py \
--account gmail \
--dry-run \
--verboseExpected output:
[...] [INFO] mail-poller: [gmail] Starting IMAP poll...
[...] [INFO] mail-poller: [gmail] Connected to imap.gmail.com
[...] [INFO] mail-poller: [gmail/INBOX] Found N new messages
[...] [INFO] mail-poller: [gmail] Poll complete: N new messagesFull run (with writes):
bash
cd /tmp/mail-poller-test
python3 mail-poller.py --account gmail --verboseCheck output:
bash
# Should create state.json
cat state.json | python3 -m json.tool
# Should create archive structure
find archive/gmail -name "*.json" | head -5
# Sample message
find archive/gmail -name "*.json" | head -1 | xargs cat | python3 -m json.toolTest Outlook (Graph API)
Dry-run:
bash
python3 mail-poller.py \
--account hotmail-michael-asolo \
--dry-run \
--verboseExpected output:
[...] [INFO] mail-poller: [hotmail-michael-asolo] Starting Graph API poll...
[...] [INFO] mail-poller: [hotmail-michael-asolo] Token refreshed
[...] [INFO] mail-poller: [hotmail-michael-asolo] Found N new messages
[...] [INFO] mail-poller: [hotmail-michael-asolo] Poll complete: N new messagesCheck if token was updated:
bash
# Should have 'access_token', 'refresh_token', 'updated'
cat /tmp/mail-poller-test/credentials/outlook-tokens-hotmail-michael-asolo.json | python3 -m json.toolStep 4: Test All Accounts (Full Run)
bash
python3 mail-poller.py --verboseExpected output:
[...] [INFO] mail-poller: Starting mail poller (dry_run=False)
[...] [INFO] mail-poller: [gmail] Starting IMAP poll...
[...] [INFO] mail-poller: [gmail] Poll complete: N new messages
[...] [INFO] mail-poller: [hotmail-michael-asolo] Starting Graph API poll...
[...] [INFO] mail-poller: [hotmail-michael-asolo] Poll complete: N new messages
[...] [INFO] mail-poller: [outlook-michael-nnamah] Starting Graph API poll...
[...] [INFO] mail-poller: [outlook-michael-nnamah] Poll complete: N new messages
[...] [INFO] mail-poller: [outlook-n-nnamah] Starting Graph API poll...
[...] [INFO] mail-poller: [outlook-n-nnamah] Poll complete: N new messages
[...] [INFO] mail-poller: Archived M messages
[...] [INFO] mail-poller: Mail poller completeStep 5: Verify State Persistence
Run again (should have zero new messages, since cursors were just updated):
bash
python3 mail-poller.py --verboseExpected output:
[...] [INFO] mail-poller: [gmail/INBOX] No new messages
[...] [INFO] mail-poller: [gmail] Poll complete: 0 new messages
[...] [INFO] mail-poller: [hotmail-michael-asolo] Poll complete: 0 new messages
[...] [INFO] mail-poller: No new messages
[...] [INFO] mail-poller: Mail poller completeIf you got new messages on second run, the state cursor is not working. Check:
bash
jq .gmail.folders /tmp/mail-poller-test/state.json
jq .hotmail-michael-asolo.last_received /tmp/mail-poller-test/state.jsonStep 6: Verify Archive Format
bash
# Count messages per account
find /tmp/mail-poller-test/archive -type f -name "*.json" | \
sed 's/.*archive\///;s/\/202.*//' | \
sort | uniq -c
# Verify all messages have required fields
python3 << 'PYTHON'
import json
from pathlib import Path
archive_dir = Path("/tmp/mail-poller-test/archive")
required_fields = {"account", "email", "message_id", "message_hash", "date", "subject", "from", "to", "body_text", "year_month"}
for json_file in archive_dir.rglob("*.json"):
with open(json_file) as f:
msg = json.load(f)
missing = required_fields - set(msg.keys())
if missing:
print(f"MISSING FIELDS in {json_file}: {missing}")
else:
print(f"OK: {json_file.name}")
PYTHONSample valid message:
json
{
"account": "gmail",
"email": "michael.asolo1@gmail.com",
"message_id": "<abc123@gmail.com>",
"message_hash": "abcd1234efgh5678",
"date": "2026-06-05T10:30:45+00:00",
"subject": "Test Subject",
"from": "sender@example.com",
"to": "michael.asolo1@gmail.com",
"body_text": "Plain text body...",
"body_html": "",
"year_month": "2026/06"
}Step 7: Test Error Handling
Simulate Invalid Credential
Edit a credential file with bad password:
bash
python3 << 'PYTHON'
import json
cred = json.load(open("/tmp/mail-poller-test/credentials/mail_imap_credential.json"))
cred["password"] = "invalid"
json.dump(cred, open("/tmp/mail-poller-test/credentials/mail_imap_credential.json", "w"))
PYTHON
python3 mail-poller.py --account gmail --verboseExpected output:
[...] [ERROR] [gmail] IMAP poll failed: b'[AUTHENTICATIONFAILED] Invalid credentials (Failure)'
[...] [INFO] mail-poller: Mail poller completeScript should continue and try other accounts (error gracefully handled).
Simulate Missing Credential File
bash
rm /tmp/mail-poller-test/credentials/mail_imap_credential.json
python3 mail-poller.py --account gmail --verboseExpected output:
[...] [ERROR] [gmail] Credential file not found: /tmp/mail-poller-test/credentials/mail_imap_credential.json
[...] [ERROR] [gmail] IMAP poll failed: ...Step 8: Performance Check
Time a full run:
bash
time python3 mail-poller.py --verboseExpected performance:
- Total time: 1–5 seconds (depending on account size)
- IMAP polling: ~300ms per account
- Graph API polling: ~500ms per account
- Archive I/O: ~100ms
If significantly slower, check:
- Network latency (ping imap.gmail.com, graph.microsoft.com)
- Disk I/O (check /tmp is not on slow network mount)
- Python startup time
Step 9: Cleanup
After testing, remove temporary files:
bash
rm -rf /tmp/mail-poller-testOr keep for repeated testing (state.json will be preserved).
Common Issues
"urllib.error.HTTPError: 401 Client Error: Unauthorized"
Microsoft token expired or invalid. Regenerate tokens (see INSTALLATION.md Step 1c).
"imaplib.IMAP4.error: [AUTHENTICATIONFAILED] Invalid credentials"
Gmail app password incorrect or expired. Regenerate at https://myaccount.google.com/apppasswords.
"ConnectionRefusedError: [Errno 61] Connection refused"
Cannot reach Gmail IMAP or Microsoft Graph servers. Check:
- Network connectivity:
ping imap.gmail.com - Firewall rules:
nc -zv imap.gmail.com 993
"state.json persists old cursor (fetches old emails)"
State file not being updated. Check:
- File permissions:
ls -la state.json(should be writable) - Disk space:
df /tmp - Script errors in logs (verbose mode)
Reset cursor:
bash
rm /tmp/mail-poller-test/state.json
python3 mail-poller.py # Will refetch everythingNext Steps
Once local testing passes:
- Copy
mail-poller.pyto vault (no modifications) - Deploy to Z2:
./deploy.sh - Run manual test on ct102:
ssh z2 'pct exec 102 -- /opt/hinata/mail-poller/mail-poller.py --verbose' - Verify systemd timer:
ssh z2 'pct exec 102 -- systemctl list-timers'