Full implementation of the Howl email triage system: - Microsoft Graph API integration with MSAL auth (client-credentials and delegated modes) - Claude LLM classification via tool use for structured output - PostgreSQL database with customers, vendors, whitelist, and email_log tables - Alembic migration for full schema - APScheduler daemon with graceful shutdown - Typer CLI (run, dry-run, status commands) - Business rule classifier with overrides (whitelist protection, low-confidence fallback) - Action executor (move to folders, flag, escalate with webhook) - 35 passing unit tests - README, SETUP, and NEXT_STEPS documentation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7.8 KiB
Setup Guide
Prerequisites
- Python 3.11+
- PostgreSQL 14+ database (local or hosted)
- Microsoft 365 account with an Azure AD app registration
- Anthropic API key
1. Azure AD App Registration
For a shared mailbox (recommended for production)
This uses application permissions — the daemon runs without any user logged in.
- Go to portal.azure.com → Azure Active Directory → App registrations → New registration
- Name it something like
howl-email-daemon; leave the redirect URI blank - After creation, note the Application (client) ID and Directory (tenant) ID
- Go to Certificates & secrets → New client secret → copy the value immediately
- Go to API permissions → Add a permission → Microsoft Graph → Application permissions
- Add
Mail.Read - Add
Mail.ReadWrite
- Add
- Click Grant admin consent (requires a Global Admin)
Set in .env:
GRAPH_AUTH_MODE=client_credentials
AZURE_TENANT_ID=<Directory (tenant) ID>
AZURE_CLIENT_ID=<Application (client) ID>
AZURE_CLIENT_SECRET=<client secret value>
GRAPH_MAILBOX=shared@yourcompany.com
For a personal mailbox
This uses delegated permissions — the daemon acts on behalf of a user.
- Follow steps 1–4 above
- Go to API permissions → Add a permission → Microsoft Graph → Delegated permissions
- Add
Mail.Read - Add
Mail.ReadWrite
- Add
- Under Authentication → Advanced settings, enable Allow public client flows
Set in .env:
GRAPH_AUTH_MODE=delegated
AZURE_TENANT_ID=<Directory (tenant) ID>
AZURE_CLIENT_ID=<Application (client) ID>
# No AZURE_CLIENT_SECRET needed for delegated mode
GRAPH_MAILBOX=yourname@yourcompany.com
MSAL_TOKEN_CACHE_PATH=.msal_cache.bin
On first howl run, you'll be shown a device code URL and prompted to log in. The token is cached to MSAL_TOKEN_CACHE_PATH and refreshes automatically.
2. PostgreSQL Database
Create a database for Howl:
CREATE DATABASE howl;
CREATE USER howl_user WITH PASSWORD 'your-password';
GRANT ALL PRIVILEGES ON DATABASE howl TO howl_user;
Set in .env:
DATABASE_URL=postgresql+asyncpg://howl_user:your-password@localhost:5432/howl
Run migrations to create all tables:
alembic upgrade head
3. Environment Configuration
Copy the template and fill in your values:
cp .env.example .env
Required settings
| Variable | Description |
|---|---|
AZURE_TENANT_ID |
Azure AD directory (tenant) ID |
AZURE_CLIENT_ID |
App registration client ID |
AZURE_CLIENT_SECRET |
Client secret (client_credentials mode only) |
GRAPH_MAILBOX |
Email address of the mailbox to monitor |
ANTHROPIC_API_KEY |
Your Anthropic API key |
DATABASE_URL |
PostgreSQL connection string (asyncpg driver) |
Key optional settings
| Variable | Default | Description |
|---|---|---|
GRAPH_AUTH_MODE |
client_credentials |
client_credentials or delegated |
GRAPH_POLL_INTERVAL_SECONDS |
300 |
How often to check for new mail (seconds) |
GRAPH_BATCH_SIZE |
50 |
Messages fetched per poll cycle |
GRAPH_MAX_BODY_CHARS |
4000 |
Email body characters sent to LLM (truncated beyond this) |
ANTHROPIC_MODEL |
claude-sonnet-4-6 |
Claude model to use |
LLM_CONFIDENCE_THRESHOLD |
0.60 |
Below this, email goes to Needs Review |
DRY_RUN |
false |
Set true to classify without touching email |
MAX_RETRY_COUNT |
3 |
Retry attempts for failed messages |
NOTIFICATION_WEBHOOK_URL |
(empty) | Teams or Slack webhook for escalation alerts |
Mail folder names
These are the display names Howl will create/find in the monitored mailbox:
| Variable | Default |
|---|---|
FOLDER_CUSTOMERS |
Customers |
FOLDER_VENDORS |
Vendors |
FOLDER_REVIEW |
Needs Review |
FOLDER_SPAM |
Junk Email |
FOLDER_ESCALATE |
Escalate |
Folders are created automatically on first use if they don't already exist.
4. Populate the Database
Add your contacts before running the daemon. Howl matches sender addresses against these tables to provide context to the LLM.
Adding customers
-- Insert a customer
INSERT INTO customers (name, company, notes)
VALUES ('Alice Johnson', 'Acme Corp', 'Key account — priority response required');
-- Attach email addresses
INSERT INTO customer_emails (customer_id, email_address, is_primary)
VALUES (
(SELECT id FROM customers WHERE name = 'Alice Johnson'),
'alice@acme.com',
TRUE
);
Adding vendors
INSERT INTO vendors (name, company, service_category)
VALUES ('SupplyPro', 'SupplyPro Inc', 'supplier');
INSERT INTO vendor_emails (vendor_id, email_address, is_primary)
VALUES (
(SELECT id FROM vendors WHERE name = 'SupplyPro'),
'billing@supplypro.com',
TRUE
);
Adding whitelist entries
-- Exact email address
INSERT INTO whitelist (email_address, description, added_by)
VALUES ('audit@partnerfirm.com', 'External audit partner', 'admin');
-- Entire domain (matches any @microsoft.com sender)
INSERT INTO whitelist (domain, description, added_by)
VALUES ('microsoft.com', 'Microsoft services', 'admin');
-- Temporary entry (expires in 30 days)
INSERT INTO whitelist (email_address, description, expires_at, added_by)
VALUES ('temp@contractor.com', 'Temp contractor access', NOW() + INTERVAL '30 days', 'admin');
5. Running Howl
Test first with dry-run
# Process up to 10 emails, classify them, but take no action
howl dry-run --limit 10
Check results:
# See what classifications were made
howl status --limit 20
# Or query directly
psql -d howl -c "SELECT sender_address, llm_classification, final_action, status FROM email_log ORDER BY created_at DESC LIMIT 10;"
Start the daemon
howl run
The daemon polls every GRAPH_POLL_INTERVAL_SECONDS (default: 5 minutes). Stop it with Ctrl+C or SIGTERM — it finishes the current batch before exiting.
Running as a systemd service (Linux)
Create /etc/systemd/system/howl.service:
[Unit]
Description=Howl Email Management Daemon
After=network.target postgresql.service
[Service]
Type=simple
User=howl
WorkingDirectory=/opt/projects/howl
EnvironmentFile=/opt/projects/howl/.env
ExecStart=/opt/projects/howl/.venv/bin/howl run
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
sudo systemctl enable howl
sudo systemctl start howl
sudo journalctl -u howl -f # Follow logs
6. Webhook Notifications (optional)
Set NOTIFICATION_WEBHOOK_URL to receive alerts when an email is escalated.
Microsoft Teams
- In Teams, go to the channel → ... → Connectors → Incoming Webhook
- Create and copy the webhook URL
- Set
NOTIFICATION_WEBHOOK_URL=<webhook URL>
Slack
- In Slack, go to api.slack.com/apps → create an app → Incoming Webhooks
- Activate and add to a channel, copy the URL
- Set
NOTIFICATION_WEBHOOK_URL=<webhook URL>
Troubleshooting
Authentication error (client_credentials)
- Verify the client secret hasn't expired in the Azure portal
- Ensure admin consent was granted for the application permissions
401 Unauthorized from Graph
- For delegated mode: delete
.msal_cache.binand re-authenticate withhowl run - Check that the app registration has the correct permissions for the mailbox
Messages not being processed
- Check
email_logforstatus = 'failed'rows and readerror_message - Ensure the mailbox address in
GRAPH_MAILBOXexactly matches the M365 UPN or shared mailbox address
Low LLM accuracy
- Lower
LLM_CONFIDENCE_THRESHOLDto move more edge cases toNeeds Review - Add
notesto customer/vendor records — these are included in the LLM context - Inspect
llm_raw_responseinemail_logfor Claude's reasoning