# 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. 1. Go to [portal.azure.com](https://portal.azure.com) → **Azure Active Directory** → **App registrations** → **New 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 & secrets** → **New client secret** → copy the value immediately 5. Go to **API permissions** → **Add a permission** → **Microsoft Graph** → **Application 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= AZURE_CLIENT_ID= AZURE_CLIENT_SECRET= GRAPH_MAILBOX=shared@yourcompany.com ``` ### For a personal mailbox This uses **delegated permissions** — the daemon acts on behalf of a user. 1. Follow steps 1–4 above 2. Go to **API permissions** → **Add a permission** → **Microsoft Graph** → **Delegated permissions** - Add `Mail.Read` - Add `Mail.ReadWrite` 3. Under **Authentication** → **Advanced settings**, enable **Allow public client flows** Set in `.env`: ``` GRAPH_AUTH_MODE=delegated AZURE_TENANT_ID= AZURE_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: ```sql 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: ```bash alembic upgrade head ``` --- ## 3. Environment Configuration Copy the template and fill in your values: ```bash 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 ```sql -- 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 ```sql 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 ```sql -- 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 ```bash # Process up to 10 emails, classify them, but take no action howl dry-run --limit 10 ``` Check results: ```bash # 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 ```bash 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`: ```ini [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 ``` ```bash 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 → **...** → **Connectors** → **Incoming Webhook** 2. Create and copy the webhook URL 3. Set `NOTIFICATION_WEBHOOK_URL=` ### Slack 1. In Slack, go to [api.slack.com/apps](https://api.slack.com/apps) → create an app → **Incoming Webhooks** 2. Activate and add to a channel, copy the URL 3. Set `NOTIFICATION_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