howl/SETUP.md
lorentz 3bfda9e585 Initial commit: Howl M365 email management daemon
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>
2026-04-01 16:02:24 -04:00

7.8 KiB
Raw Permalink Blame History

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

This uses application permissions — the daemon runs without any user logged in.

  1. Go to portal.azure.comAzure Active DirectoryApp registrationsNew registration
  2. Name it something like howl-email-daemon; leave the redirect URI blank
  3. After creation, note the Application (client) ID and Directory (tenant) ID
  4. Go to Certificates & secretsNew client secret → copy the value immediately
  5. Go to API permissionsAdd a permissionMicrosoft GraphApplication permissions
    • Add Mail.Read
    • Add Mail.ReadWrite
  6. 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.

  1. Follow steps 14 above
  2. Go to API permissionsAdd a permissionMicrosoft GraphDelegated permissions
    • Add Mail.Read
    • Add Mail.ReadWrite
  3. Under AuthenticationAdvanced 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

  1. In Teams, go to the channel → ...ConnectorsIncoming Webhook
  2. Create and copy the webhook URL
  3. Set NOTIFICATION_WEBHOOK_URL=<webhook URL>

Slack

  1. In Slack, go to api.slack.com/apps → create an app → Incoming Webhooks
  2. Activate and add to a channel, copy the URL
  3. 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.bin and re-authenticate with howl run
  • Check that the app registration has the correct permissions for the mailbox

Messages not being processed

  • Check email_log for status = 'failed' rows and read error_message
  • Ensure the mailbox address in GRAPH_MAILBOX exactly matches the M365 UPN or shared mailbox address

Low LLM accuracy

  • Lower LLM_CONFIDENCE_THRESHOLD to move more edge cases to Needs Review
  • Add notes to customer/vendor records — these are included in the LLM context
  • Inspect llm_raw_response in email_log for Claude's reasoning