276 lines
7.8 KiB
Markdown
276 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
|