# Microsoft Exchange Connector — Setup Guide

## Prerequisites

- [ ] Azure AD / Microsoft Entra tenant with Exchange Online
- [ ] App registration in Azure AD (Portal → App registrations → New registration)
- [ ] API permissions granted: `Mail.Read` and `Mail.ReadWrite` (Application type, not Delegated)
- [ ] Admin consent granted for the API permissions
- [ ] Client secret created for the app registration
- [ ] The `[microsoft]` extra installed: `uv add entr-adapter-core[microsoft]`

## Configuration

Add these settings to your adapter's `config.py` and `.env`:

| Env Var | Type | Required | Default | Example | Description |
|---------|------|----------|---------|---------|-------------|
| `TENANT_NAME` | str | Yes | — | `Acme` | Tenant identifier (for logging) |
| `ENTRA_TENANT_ID` | str | Yes | — | `abc123-def456-...` | Azure AD tenant (directory) ID |
| `ENTRA_CLIENT_ID` | str | Yes | — | `def456-ghi789-...` | OAuth2 application (client) ID |
| `ENTRA_CLIENT_SECRET` | str | Yes | — | `~secret~` | OAuth2 client secret |

**In `config.py`:**

```python
from pydantic_settings import BaseSettings
from entr_adapter_core.config import CoreSettings

class Settings(CoreSettings):
    TENANT_NAME: str
    ENTRA_TENANT_ID: str
    ENTRA_CLIENT_ID: str
    ENTRA_CLIENT_SECRET: str
```

**In `.env`:**

```env
TENANT_NAME=Acme
ENTRA_TENANT_ID=abc123-def456-ghi789
ENTRA_CLIENT_ID=def456-ghi789-jkl012
ENTRA_CLIENT_SECRET=~your-client-secret~
```

### Using MicrosoftConnectorConfig (alternative)

```python
from entr_adapter_core.connectors.microsoft import MicrosoftConnectorConfig

config = MicrosoftConnectorConfig(
    tenant_name="Acme",
    entra_tenant_id="abc123-def456-ghi789",
    entra_client_id="def456-ghi789-jkl012",
    entra_client_secret="~your-client-secret~",
    mailbox="invoices@acme.com",
    # Optional:
    folder_path=["Inbox", "Invoices"],    # Default: ["Inbox"]
    delay_days=1,                          # Default: 0 (immediate)
    move_to_folder_path=["Inbox", "Processed"],  # Default: None (don't move)
    require_pdf_attachment=True,           # Default: False
)
```

### Configuration Parameters (MicrosoftConnectorConfig)

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `mailbox` | str | Required | Email address of the mailbox to monitor |
| `folder_path` | list[str] | `["Inbox"]` | Folder path to traverse (e.g., `["Inbox", "Invoices"]`) |
| `delay_days` | int | `0` | Days to wait before emails become visible. Useful for giving senders time to correct emails |
| `move_to_folder_path` | list[str] | `None` | Move emails here after download. `None` = don't move |
| `require_pdf_attachment` | bool | `False` | Only list emails with PDF attachments. Two-stage filter: server-side `hasAttachments` then client-side PDF check |

## Registration

Wire the Microsoft connector as a retrieval handler in your adapter's `main.py`:

```python
from entr_adapter_core.app import create_adapter_app
from entr_adapter_core.connectors.microsoft import MicrosoftEntraRetrievalHandler
from app.config import Settings, get_settings
from app.handlers.my_handler import MyHandler

settings = get_settings()

# Create the retrieval handler
exchange_retriever = MicrosoftEntraRetrievalHandler(
    settings,
    mailbox="invoices@acme.com",
    folder_path=["Inbox", "Invoices"],
    require_pdf_attachment=True,
    move_to_folder_path=["Inbox", "Processed"],
)

# Register with your process handler
handlers = {
    "my-adapter": MyHandler(
        settings=settings,
        retrieval_handler=exchange_retriever,
    ),
}

app = create_adapter_app(handlers=handlers, settings=settings)
```

## Verification

**1. Check connector initialization (startup logs):**

```
INFO  Initialized MicrosoftEntraRetrievalHandler for tenant: Acme
INFO  Entra Mailbox: invoices@acme.com
INFO  Target folder path: Inbox / Invoices
INFO  Require PDF attachment: True
INFO  Move after download to: Inbox / Processed
```

**2. Trigger a list_documents call via the API:**

```bash
curl -X POST http://localhost:8096/list_documents \
  -H "Content-Type: application/json" \
  -d '{"adapter_identifier": "my-adapter", "source_identifier": "exchange", "date_from": "2026-01-01"}'
```

**3. Expected success response:**

```json
{"status": "success", "documents": [{"document_identifier": "AAMk...", "filename": "Invoice #42.eml"}]}
```

**4. Common failure modes:**

| Error | Cause | Fix |
|-------|-------|-----|
| `Failed to acquire access token` | Invalid credentials or tenant ID | Verify `ENTRA_TENANT_ID`, `ENTRA_CLIENT_ID`, `ENTRA_CLIENT_SECRET` |
| `Could not find folder 'Inbox / Invoices'` | Folder path doesn't exist | Check folder names in Outlook (case-insensitive). Dutch mailboxes use `Postvak IN` instead of `Inbox` |
| `403 Forbidden` | Missing API permissions or admin consent | Grant `Mail.Read` (Application) and click "Grant admin consent" in Azure portal |
| `ImportError: ...httpx...` | Missing extra | Run `uv add entr-adapter-core[microsoft]` |
| `Results are paginated` (warning) | More than 999 emails match the filter | Narrow the `date_from` range or increase `delay_days` |

## Common Pitfalls

1. **Dutch folder names**: Dutch Exchange mailboxes use `Postvak IN` instead of `Inbox`. Check the actual folder names via Graph Explorer or Outlook.
2. **Application vs Delegated permissions**: The client credentials flow requires **Application** permissions (`Mail.Read`), not Delegated. Delegated permissions won't work without a user login flow.
3. **Admin consent required**: Application permissions always require admin consent. After adding permissions, an admin must click "Grant admin consent for [org]" in the Azure portal.
4. **Token caching**: Access tokens are cached for their lifetime minus 5 minutes. If you see frequent "Acquiring new access token" log messages, check that `expires_in` is returned correctly.
5. **sentDateTime vs receivedDateTime**: The connector filters by `sentDateTime` (immutable) rather than `receivedDateTime`. This ensures consistent filtering even when emails are moved between folders.
6. **PDF filtering overhead**: When `require_pdf_attachment=True`, the connector makes an additional API call per email to check attachments. For high-volume mailboxes, this can be slow. The server-side `hasAttachments` pre-filter reduces the number of checks.
7. **Move-after-download failures**: If `move_to_folder_path` is configured but the folder doesn't exist, the download still succeeds — only the move fails (logged as a warning). Create the archive folder before deploying.
8. **Rate limits**: Microsoft Graph API has throttling limits (~10,000 requests per 10 minutes per app). The connector doesn't implement retry-after handling — if you hit limits, reduce polling frequency.

## Client Onboarding Template

Send this checklist to the client's IT team before integration:

- [ ] **Azure AD tenant ID** — the directory (tenant) ID from Azure Portal → Azure Active Directory → Overview
- [ ] **App registration** — create an app registration for ENTR in Azure AD, or provide client ID + secret for an existing one
- [ ] **API permissions** — grant `Mail.Read` (Application type) to the app registration
- [ ] **Admin consent** — an Azure AD admin must grant consent for the API permissions
- [ ] **Client secret** — create a client secret for the app registration and share it securely (expires: recommend 24 months)
- [ ] **Mailbox address** — which email address should ENTR monitor? (e.g., `invoices@acme.com`)
- [ ] **Folder structure** — which folder(s) contain the documents to process? (e.g., Inbox, Inbox/Invoices)
- [ ] **Archive behavior** — should processed emails be moved to a specific folder? If so, which folder path?
- [ ] **Attachment filtering** — should only emails with PDF attachments be processed, or all emails?
- [ ] **Processing delay** — should there be a delay before emails are processed? (e.g., 1 day delay for corrections)
