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>
275 lines
7.8 KiB
Markdown
275 lines
7.8 KiB
Markdown
# 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=<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 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=<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:
|
||
|
||
```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=<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=<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
|