> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://cla-demo.postmansolutions.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://cla-demo.postmansolutions.com/_mcp/server.

# Exchange API Core Features Implementation

## Story and Technical Design Document

---

## Table of Contents

1. [Overview](#overview)

2. [User Stories](#user-stories)

3. [Technical Architecture](#technical-architecture)

4. [API Endpoints Design](#api-endpoints-design)

5. [Data Models & Schemas](#data-models--schemas)

6. [Service Layer Design](#service-layer-design)

7. [Database Schema](#database-schema)

8. [Security & Authentication](#security--authentication)

9. [Error Handling](#error-handling)

10. [Implementation Checklist](#implementation-checklist)

---

## Overview

### Purpose

Implement the core Exchange/Outlook monitoring API endpoints following the existing Node.js/TypeScript architecture pattern established in `/src/api/exchange`. This stage ports functionality from the Python FastAPI implementation to match the Fastify-based structure.

### Scope

* OAuth Authorization flow (GET /auth, GET /auth/callback)

* Webhook notification handling (POST /webhook/inbox)

* Inbox subscription management (GET /list\_inboxes, DELETE /unsubscribe/inbox/:id)

* Blocked senders management (GET, POST, DELETE /blocked\_senders)

* Exchange options/configuration (GET, POST /exchange\_options)

### Dependencies

* Existing architecture in `/src/api/exchange`

* Python implementation reference in `/temp/test.py`

* Microsoft Graph API integration

* SQL Server database with existing schema

---

## User Stories

### US-2.1: OAuth Authorization Initiation

**As a** system administrator

**I want to** initiate OAuth authorization for monitoring an Exchange inbox

**So that** I can connect my organization's email account to the monitoring system

**Acceptance Criteria:**

* GET `/exchange/auth` endpoint generates Microsoft OAuth URL

* State parameter is generated and stored with 10-minute expiration

* Optional email\_hint query parameter pre-fills login

* Returns authorization URL or redirects to Microsoft login

* Requires Administrator role and EXCHANGE\_READ scope

* State includes client\_id, initiated\_by\_user, requested\_inbox, expires\_at

**Technical Notes:**

* Store state in memory (Map) with cleanup mechanism

* Generate UUID v4 for state parameter

* Build OAuth URL with proper scopes and tenant configuration

---

### US-2.2: OAuth Authorization Callback

**As a** system administrator

**I want to** complete the OAuth flow after Microsoft login

**So that** the system can obtain access tokens and create inbox subscriptions

**Acceptance Criteria:**

* GET `/exchange/auth/callback` receives code and state parameters

* Validates state parameter exists and hasn't expired

* Exchanges authorization code for access tokens

* Extracts monitored inbox email from ID token

* Validates requested inbox matches authenticated inbox (if specified)

* Creates Microsoft Graph subscription for inbox monitoring

* Stores subscription details in `exchange_inbox_subscriptions` table

* Encrypts and stores access/refresh tokens in `exchange_inbox_tokens` table

* Redirects user to frontend settings page on success

* Properly handles errors (invalid state, token failure, subscription creation failure)

**Technical Notes:**

* Use MD5 hash of client\_name for client\_id storage

* Subscription expires in 4230 minutes (configurable)

* Webhook URL points to `/exchange/webhook/inbox`

* Monitor `/me/mailFolders('inbox')/messages` resource

* Token expiration calculated from `expires_in` response

---

### US-2.3: Webhook Notification Processing

**As a** system

**I want to** receive and process webhook notifications from Microsoft Graph

**So that** new emails can be detected and processed automatically

**Acceptance Criteria:**

* POST `/exchange/webhook/inbox` handles validation token requests

* Processes notification payloads with 'value' array

* Extracts clientState, subscriptionId, and message ID from notifications

* Validates required fields are present

* Retrieves access token using subscription\_id

* Fetches email details from Microsoft Graph API

* Checks sender against blocked senders list

* Fetches and decodes PDF attachments

* Classifies attachments using Azure Document Intelligence

* Applies event actions (mark as read, move to folder) based on classification

* Processes invoices through workflow engine

* Returns 200 status for successful processing

* Handles errors gracefully without blocking webhook acknowledgment

**Technical Notes:**

* Return validation token immediately as plain text

* Process notifications asynchronously if possible

* Use background tasks for email processing

* Implement proper timeout handling (30s for Graph API calls)

* Log all processing steps for debugging

---

### US-2.4: List Inbox Subscriptions

**As a** system administrator

**I want to** view all monitored inbox subscriptions

**So that** I can see which inboxes are currently being monitored

**Acceptance Criteria:**

* GET `/exchange/list_inboxes` returns list of subscriptions

* Filters results by authenticated client\_name

* Returns inbox\_address and subscription\_id for each subscription

* Requires Administrator role and EXCHANGE\_READ scope

* Returns empty array if no subscriptions exist

* Handles database errors gracefully

**Technical Notes:**

* Join with Clients table to get ClientId from client\_name

* Return standardized response format: `{ result: InboxListItem[] }`

---

### US-2.5: Unsubscribe from Inbox

**As a** system administrator

**I want to** remove an inbox subscription

**So that** emails from that inbox are no longer monitored

**Acceptance Criteria:**

* DELETE `/exchange/unsubscribe/inbox/:subscription_id` removes subscription

* Deletes subscription from Microsoft Graph (if still active)

* Removes subscription from local database

* Removes associated tokens from database

* Returns success message on completion

* Returns 404 if subscription doesn't exist

* Handles expired subscriptions (skip Graph deletion, still delete from DB)

* Requires Administrator role

**Technical Notes:**

* Check subscription expiration before attempting Graph API deletion

* Always delete from local database even if Graph deletion fails

* Use refresh token to get access token for Graph deletion

---

### US-2.6: List Blocked Senders

**As a** system administrator

**I want to** view all blocked email senders/domains

**So that** I can see which senders are being filtered

**Acceptance Criteria:**

* GET `/exchange/blocked_senders` returns list of blocked senders

* Filters by authenticated client\_name

* Returns id, value, reason, created\_on for each entry

* Supports both email addresses and domain patterns (@domain.com)

* Orders results by created\_on DESC

* Requires Administrator role and EXCHANGE\_READ scope

**Technical Notes:**

* Use MD5 hash of client\_name for client\_id filtering

* Return standardized format: `{ result: BlockedSenderItem[] }`

---

### US-2.7: Add Blocked Sender

**As a** system administrator

**I want to** add an email address or domain to the blocked list

**So that** emails from that sender are automatically filtered

**Acceptance Criteria:**

* POST `/exchange/blocked_senders` adds new blocked sender

* Validates value is either valid email or domain pattern (@domain.com)

* Requires 'value' field in request body

* Optional 'reason' field for documentation

* Associates with authenticated client

* Returns success message on completion

* Returns 400 for invalid email/domain format

* Prevents duplicate entries

* Requires Administrator role and EXCHANGE\_WRITE scope

**Technical Notes:**

* Email regex: `^[^@\s]+@[^@\s]+\.[^@\s]+$`

* Domain regex: `^@[^@\s]+\.[^@\s]+$`

* Store created\_on timestamp automatically

---

### US-2.8: Remove Blocked Sender

**As a** system administrator

**I want to** remove a sender from the blocked list

**So that** emails from that sender are no longer filtered

**Acceptance Criteria:**

* DELETE `/exchange/blocked_senders/:sender_id` removes blocked sender

* Validates sender\_id is valid integer

* Returns success message on completion

* Returns 404 if sender\_id doesn't exist

* Requires Administrator role and EXCHANGE\_WRITE scope

**Technical Notes:**

* Consider adding client\_id check to prevent cross-client deletion

---

### US-2.9: Get Exchange Options

**As a** system administrator

**I want to** retrieve configuration for a specific inbox subscription

**So that** I can view how emails are being processed

**Acceptance Criteria:**

* GET `/exchange/exchange_options?subscription_id=xxx` returns configuration

* Returns classifier parameters and classified event actions

* Returns invoice and other email handling rules

* Includes markAs (read/unread) and moveTo (folder path) settings

* Returns 404 if subscription doesn't exist

* Requires Administrator role

**Technical Notes:**

* Load from both database and Cosmos DB (client config)

* Merge classifier parameters with event actions

* Return format: `{ classified: {invoice: {...}, other: {...}}, classifier: {...} }`

---

### US-2.10: Update Exchange Options

**As a** system administrator

**I want to** update email processing rules for an inbox

**So that** I can control how invoices and other emails are handled

**Acceptance Criteria:**

* POST `/exchange/exchange_options` updates configuration

* Requires subscription\_id and classified object in request body

* Updates event\_actions fields in exchange\_inbox\_subscriptions table

* Supports invoice and other email actions configuration

* Returns success message on completion

* Validates classified object structure

* Returns 400 for missing/invalid data

* Requires Administrator role

**Technical Notes:**

* Update fields: event\_actions\_invoice\_mark\_as\_read, event\_actions\_invoice\_move\_to, event\_actions\_other\_mark\_as\_read, event\_actions\_other\_move\_to

* Convert markAs string to boolean for database storage

---

## Technical Architecture

### Directory Structure

```

/src/api/exchange/

├── exchanges.route.ts       # Fastify route definitions

├── exchanges.controller.ts  # Request/response handling

├── exchanges.service.ts     # Business logic

├── exchanges.model.ts       # Database operations

├── exchanges.schema.ts      # Zod schemas & OpenAPI docs

└── types/

    └── exchange.types.ts    # TypeScript interfaces

```

### Layer Responsibilities

#### Route Layer (`exchanges.route.ts`)

* Define HTTP routes and methods

* Apply authentication/authorization middleware

* Map requests to controller methods

* Configure OpenAPI schema from exchanges.schema.ts

#### Controller Layer (`exchanges.controller.ts`)

* Extract and validate request parameters

* Call service layer methods

* Format responses

* Handle HTTP-specific concerns (status codes, headers)

* Catch and format errors

#### Service Layer (`exchanges.service.ts`)

* Implement business logic

* Orchestrate operations across multiple models

* Handle Microsoft Graph API integration

* Manage token refresh and encryption

* Process email attachments and classification

* Apply email actions (mark as read, move to folder)

#### Model Layer (`exchanges.model.ts`)

* Execute database queries

* Map database results to TypeScript types

* Handle database errors

* Use parameterized queries for security

#### Schema Layer (`exchanges.schema.ts`)

* Define Zod schemas for validation

* Define OpenAPI documentation

* Export reusable type definitions

---

## API Endpoints Design

### 1. OAuth Authorization Initiation

**Endpoint:** `GET /exchange/auth`

**Query Parameters:**

```typescript

{

  email_hint?: string // Optional email to pre-fill login

}

```

**Headers:**

```

Authorization: Bearer <jwt_token>

```

**Response:** `200 OK`

```typescript

{

  authorization_url: string

}

```

**Security:**

* Roles: Administrator

* Scopes: EXCHANGE\_READ

**Implementation Notes:**

* Generate UUID v4 for state

* Store state with 10-minute expiration

* Build Microsoft OAuth URL with configured client\_id, redirect\_uri, scopes

* Include state parameter in OAuth URL

---

### 2. OAuth Authorization Callback

**Endpoint:** `GET /exchange/auth/callback`

**Query Parameters:**

```typescript

{

  code: string      // Authorization code from Microsoft

  state: string     // State parameter for CSRF protection

}

```

**Response:** `302 Redirect`

```

Location: {FRONTEND_PUBLIC_URL}/settings

```

**Error Responses:**

* `400 Bad Request` - Invalid or expired state, authorization failed

* `409 Conflict` - Inbox mismatch (requested vs authenticated)

* `504 Gateway Timeout` - Token request or subscription creation timeout

**Implementation Notes:**

* Validate state parameter

* Exchange code for tokens with Microsoft token endpoint

* Decode ID token to extract email

* Create Graph API subscription

* Store subscription and encrypted tokens

* Clean up state after use

---

### 3. Webhook Notification Handler

**Endpoint:** `POST /exchange/webhook/inbox`

**Query Parameters (for validation):**

```typescript

{

  validationToken?: string // Microsoft sends this for webhook registration

}

```

**Request Body (notification):**

```typescript

{

  value: Array<{

    clientState: string

    subscriptionId: string

    resourceData: {

      id: string // message_id

    }

  }>

}

```

**Response:** `200 OK`

```typescript

{

  status: "received"

}

```

**Validation Response:** `200 OK (plain text)`

```

{validationToken}

```

**Error Responses:**

* `400 Bad Request` - Invalid JSON, missing required fields

* `403 Forbidden` - Access token not found

* `500 Internal Server Error` - Processing error

**Implementation Notes:**

* Handle validation token immediately

* Process notifications asynchronously

* Fetch email details from Graph API

* Check blocked senders

* Classify attachments

* Apply event actions

* Trigger workflow for invoices

---

### 4. List Inbox Subscriptions

**Endpoint:** `GET /exchange/list_inboxes`

**Headers:**

```

Authorization: Bearer <jwt_token>

```

**Response:** `200 OK`

```typescript

{

  result: Array<{

    inbox_address: string

    subscription_id: string

  }>

}

```

**Security:**

* Roles: Administrator

* Scopes: EXCHANGE\_READ

---

### 5. Unsubscribe from Inbox

**Endpoint:** `DELETE /exchange/unsubscribe/inbox/:subscription_id`

**Path Parameters:**

```typescript

{

  subscription_id: string

}

```

**Headers:**

```

Authorization: Bearer <jwt_token>

```

**Response:** `200 OK`

```typescript

{

  message: "Subscription deleted successfully"

}

```

**Error Responses:**

* `404 Not Found` - Subscription doesn't exist

* `504 Gateway Timeout` - Microsoft Graph deletion timeout

**Security:**

* Roles: Administrator

---

### 6. List Blocked Senders

**Endpoint:** `GET /exchange/blocked_senders`

**Headers:**

```

Authorization: Bearer <jwt_token>

```

**Response:** `200 OK`

```typescript

{

  result: Array<{

    id: number

    value: string

    reason?: string

    created_on: string // ISO 8601 datetime

  }>

}

```

**Security:**

* Roles: Administrator

* Scopes: EXCHANGE\_READ

---

### 7. Add Blocked Sender

**Endpoint:** `POST /exchange/blocked_senders`

**Headers:**

```

Authorization: Bearer <jwt_token>

Content-Type: application/json

```

**Request Body:**

```typescript

{

  value: string        // Email or domain pattern

  reason?: string      // Optional reason for blocking

}

```

**Response:** `200 OK`

```typescript

{

  message: "Sender blocked"

}

```

**Error Responses:**

* `400 Bad Request` - Missing value, invalid email/domain format

* `500 Internal Server Error` - Database error

**Validation Rules:**

* value must be valid email: `user@example.com`

* OR value must be valid domain: `@example.com`

**Security:**

* Roles: Administrator

* Scopes: EXCHANGE\_WRITE

---

### 8. Remove Blocked Sender

**Endpoint:** `DELETE /exchange/blocked_senders/:sender_id`

**Path Parameters:**

```typescript

{

  sender_id: number

}

```

**Headers:**

```

Authorization: Bearer <jwt_token>

```

**Response:** `200 OK`

```typescript

{

  message: "Sender unblocked"

}

```

**Error Responses:**

* `404 Not Found` - Sender ID doesn't exist

* `500 Internal Server Error` - Database error

**Security:**

* Roles: Administrator

* Scopes: EXCHANGE\_WRITE

---

### 9. Get Exchange Options

**Endpoint:** `GET /exchange/exchange_options`

**Query Parameters:**

```typescript

{

  subscription_id: string

}

```

**Headers:**

```

Authorization: Bearer <jwt_token>

```

**Response:** `200 OK`

```typescript

{

  classified: {

    invoice: {

      markAs: "read" | "unread"

      moveTo: string // folder path

    }

    other: {

      markAs: "read" | "unread"

      moveTo: string // folder path

    }

  }

  classifier: {

    min_confidence: number

    min_required_fields: number

    critical_fields: string[]

    required_id_fields: string[]

    required_context_fields: string[]

  }

}

```

**Error Responses:**

* `404 Not Found` - Subscription not found

* `500 Internal Server Error` - Configuration load failure

**Security:**

* Roles: Administrator

---

### 10. Update Exchange Options

**Endpoint:** `POST /exchange/exchange_options`

**Headers:**

```

Authorization: Bearer <jwt_token>

Content-Type: application/json

```

**Request Body:**

```typescript

{

  subscription_id: string

  classified: {

    invoice: {

      markAs: "read" | "unread"

      moveTo: string

    }

    other: {

      markAs: "read" | "unread"

      moveTo: string

    }

  }

}

```

**Response:** `200 OK`

```typescript

{

  message: "Inbox settings updated"

}

```

**Error Responses:**

* `400 Bad Request` - Missing or invalid subscription\_id or classified object

* `500 Internal Server Error` - Update failed

**Security:**

* Roles: Administrator

---

## Data Models & Schemas

### TypeScript Interfaces

```typescript

// types/exchange.types.ts

  

export interface OAuthState {

  client_id: string;

  initiated_by_user: string;

  requested_inbox?: string;

  expires_at: Date;

}

  

export interface TokenResponse {

  access_token: string;

  refresh_token: string;

  expires_in: number;

  id_token: string;

}

  

export interface InboxSubscription {

  client_id: string;

  inbox_address: string;

  subscription_id: string;

  created_at: Date;

  expires_at: Date;

  event_actions_invoice_mark_as_read: boolean;

  event_actions_invoice_move_to?: string;

  event_actions_other_mark_as_read: boolean;

  event_actions_other_move_to?: string;

}

  

export interface InboxToken {

  subscription_id: string;

  access_token: string; // encrypted

  refresh_token: string; // encrypted

  created_at: Date;

  expires_at: Date;

}

  

export interface BlockedSender {

  id: number;

  client_id: string;

  value: string;

  reason?: string;

  created_on: Date;

}

  

export interface WebhookNotification {

  value: Array<{

    clientState: string;

    subscriptionId: string;

    resourceData: {

      id: string;

      '@odata.type': string;

    };

  }>;

}

  

export interface EmailData {

  id: string;

  subject: string;

  bodyPreview: string;

  sender: {

    emailAddress: {

      address: string;

    };

  };

  hasAttachments: boolean;

}

  

export interface EmailAttachment {

  '@odata.type': string;

  id: string;

  name: string;

  contentType: string;

  contentBytes: string; // base64

}

  

export interface EventActions {

  invoice: {

    markAs: 'read' | 'unread';

    moveTo: string;

  };

  other: {

    markAs: 'read' | 'unread';

    moveTo: string;

  };

}

  

export interface ClassifierConfig {

  min_confidence: number;

  min_required_fields: number;

  critical_fields: string[];

  required_id_fields: string[];

  required_context_fields: string[];

}

```

### Zod Schemas

```typescript

// exchanges.schema.ts additions

  

import { z } from 'zod';

  

export const AuthQuerySchema = z.object({

  email_hint: z.string().email().optional()

});

  

export const AuthCallbackQuerySchema = z.object({

  code: z.string(),

  state: z.string().uuid()

});

  

export const BlockedSenderBodySchema = z.object({

  value: z.string().refine(

    (val) => {

      const emailRegex = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;

      const domainRegex = /^@[^@\s]+\.[^@\s]+$/;

      return emailRegex.test(val) || domainRegex.test(val);

    },

    { message: "Must be a valid email or domain pattern (@example.com)" }

  ),

  reason: z.string().optional()

});

  

export const ExchangeOptionsQuerySchema = z.object({

  subscription_id: z.string().uuid()

});

  

export const ExchangeOptionsBodySchema = z.object({

  subscription_id: z.string().uuid(),

  classified: z.object({

    invoice: z.object({

      markAs: z.enum(['read', 'unread']),

      moveTo: z.string()

    }),

    other: z.object({

      markAs: z.enum(['read', 'unread']),

      moveTo: z.string()

    })

  })

});

  

export const SubscriptionIdParamSchema = z.object({

  subscription_id: z.string().uuid()

});

  

export const SenderIdParamSchema = z.object({

  sender_id: z.coerce.number().int().positive()

});

```

---

## Service Layer Design

### Core Service Methods

```typescript

// exchanges.service.ts

  

class ExchangesService {

  // OAuth & Token Management

  static async generateAuthUrl(clientName: string, userEmail: string, emailHint?: string): Promise<string>

  static async handleAuthCallback(code: string, state: string): Promise<string>

  static async refreshAccessToken(subscriptionId: string): Promise<string>

  static async encryptToken(token: string): Promise<string>

  static async decryptToken(encryptedToken: string): Promise<string>

  // Subscription Management

  static async createGraphSubscription(accessToken: string, clientId: string, inboxAddress: string): Promise<string>

  static async deleteGraphSubscription(subscriptionId: string): Promise<void>

  static async getClientIdFromSubscription(subscriptionId: string): Promise<string>

  // Webhook Processing

  static async processWebhookNotification(notification: WebhookNotification): Promise<void>

  static async fetchEmailAndAttachments(accessToken: string, messageId: string, subscriptionId: string, clientId: string): Promise<void>

  static async fetchAttachments(emailData: EmailData, accessToken: string, clientId: string, promptConfig: ClassifierConfig): Promise<Buffer[]>

  // Email Actions

  static async handleEmailActions(subscriptionId: string, messageId: string, eventActions: EventActions, attachmentType: 'invoice' | 'other'): Promise<void>

  static async markEmailsAsReadOrUnread(subscriptionId: string, emailIds: string[], isRead: boolean): Promise<void>

  static async moveEmailsToFolders(subscriptionId: string, emailFolderMap: Record<string, string>): Promise<void>

  static async getOrCreateFolder(folderPath: string, headers: Record<string, string>): Promise<string>

  // Blocked Senders

  static async isBlockedSender(senderEmail: string, clientId: string): Promise<boolean>

  // Classification

  static async classifyInvoice(document: Buffer, config: ClassifierConfig, clientId: string): Promise<boolean>

  // Configuration

  static async getRelevantConfig(subscriptionId: string): Promise<{ classifier: ClassifierConfig, eventActions: EventActions }>

}

```

### Microsoft Graph API Integration

```typescript

// services/graph.service.ts

  

class GraphService {

  static async getOAuthTokens(code: string): Promise<TokenResponse>

  static async getEmailDetails(accessToken: string, messageId: string): Promise<EmailData>

  static async getEmailAttachments(accessToken: string, messageId: string): Promise<EmailAttachment[]>

  static async createSubscription(accessToken: string, subscriptionData: any): Promise<any>

  static async deleteSubscription(accessToken: string, subscriptionId: string): Promise<void>

  static async updateEmail(accessToken: string, messageId: string, updates: any): Promise<void>

  static async moveEmail(accessToken: string, messageId: string, destinationId: string): Promise<void>

  static async listFolders(accessToken: string, parentId: string): Promise<any[]>

  static async createFolder(accessToken: string, parentId: string, displayName: string): Promise<any>

}

```

### Token Encryption Utilities

```typescript

// utils/token-encryption.ts

  

import crypto from 'crypto';

  

export class TokenEncryption {

  private static readonly ALGORITHM = 'aes-256-cbc';

  private static readonly SECRET_KEY: Buffer = Buffer.from(process.env.EXCHANGE_SECRET_KEY!, 'base64');

  static encrypt(token: string): string {

    const iv = crypto.randomBytes(16);

    const cipher = crypto.createCipheriv(this.ALGORITHM, this.SECRET_KEY, iv);

    const encrypted = Buffer.concat([cipher.update(token, 'utf8'), cipher.final()]);

    return `${iv.toString('hex')}:${encrypted.toString('hex')}`;

  }

  static decrypt(encryptedToken: string): string {

    const [ivHex, encryptedHex] = encryptedToken.split(':');

    const iv = Buffer.from(ivHex, 'hex');

    const encrypted = Buffer.from(encryptedHex, 'hex');

    const decipher = crypto.createDecipheriv(this.ALGORITHM, this.SECRET_KEY, iv);

    const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);

    return decrypted.toString('utf8');

  }

}

```

---

## Database Schema

### Table: exchange\_inbox\_subscriptions

```sql

CREATE TABLE exchange_inbox_subscriptions (

  client_id VARCHAR(32) NOT NULL,

  inbox_address VARCHAR(255) NOT NULL,

  subscription_id VARCHAR(36) NOT NULL PRIMARY KEY,

  created_at DATETIME2 NOT NULL,

  expires_at DATETIME2 NOT NULL,

  event_actions_invoice_mark_as_read BIT NOT NULL DEFAULT 0,

  event_actions_invoice_move_to VARCHAR(255) NULL,

  event_actions_other_mark_as_read BIT NOT NULL DEFAULT 0,

  event_actions_other_move_to VARCHAR(255) NULL,

  CONSTRAINT UK_client_inbox UNIQUE (client_id, inbox_address)

);

  

CREATE INDEX IX_subscriptions_client ON exchange_inbox_subscriptions(client_id);

CREATE INDEX IX_subscriptions_expires ON exchange_inbox_subscriptions(expires_at);

```

### Table: exchange\_inbox\_tokens

```sql

CREATE TABLE exchange_inbox_tokens (

  subscription_id VARCHAR(36) NOT NULL PRIMARY KEY,

  access_token VARCHAR(MAX) NOT NULL, -- encrypted

  refresh_token VARCHAR(MAX) NOT NULL, -- encrypted

  created_at DATETIME2 NOT NULL,

  expires_at DATETIME2 NOT NULL,

  CONSTRAINT FK_tokens_subscription FOREIGN KEY (subscription_id)

    REFERENCES exchange_inbox_subscriptions(subscription_id) ON DELETE CASCADE

);

  

CREATE INDEX IX_tokens_expires ON exchange_inbox_tokens(expires_at);

```

### Table: exchange\_blocked\_senders

```sql

CREATE TABLE exchange_blocked_senders (

  id INT IDENTITY(1,1) PRIMARY KEY,

  client_id VARCHAR(32) NOT NULL,

  value VARCHAR(255) NOT NULL,

  reason VARCHAR(500) NULL,

  created_on DATETIME2 NOT NULL DEFAULT GETUTCDATE(),

  CONSTRAINT UK_client_value UNIQUE (client_id, value)

);

  

CREATE INDEX IX_blocked_client ON exchange_blocked_senders(client_id);

```

### Database Queries

```typescript

// Model layer query examples

  

// Get subscriptions for client

const GET_SUBSCRIPTIONS = `

  SELECT s.inbox_address, s.subscription_id

  FROM exchange_inbox_subscriptions s

  INNER JOIN Clients c ON s.client_id = c.ClientId

  WHERE c.ClientName = @clientName

`;

  

// Get blocked senders

const GET_BLOCKED_SENDERS = `

  SELECT id, value, reason, created_on

  FROM exchange_blocked_senders

  WHERE client_id = @clientId

  ORDER BY created_on DESC

`;

  

// Add blocked sender

const ADD_BLOCKED_SENDER = `

  INSERT INTO exchange_blocked_senders (client_id, value, reason, created_on)

  VALUES (@clientId, @value, @reason, @createdOn)

`;

  

// Delete blocked sender

const DELETE_BLOCKED_SENDER = `

  DELETE FROM exchange_blocked_senders

  WHERE id = @senderId

`;

  

// Get subscription config

const GET_SUBSCRIPTION_CONFIG = `

  SELECT

    event_actions_invoice_mark_as_read,

    event_actions_invoice_move_to,

    event_actions_other_mark_as_read,

    event_actions_other_move_to

  FROM exchange_inbox_subscriptions

  WHERE subscription_id = @subscriptionId

`;

  

// Update subscription config

const UPDATE_SUBSCRIPTION_CONFIG = `

  UPDATE exchange_inbox_subscriptions

  SET

    event_actions_invoice_mark_as_read = @invoiceMarkAsRead,

    event_actions_invoice_move_to = @invoiceMoveTo,

    event_actions_other_mark_as_read = @otherMarkAsRead,

    event_actions_other_move_to = @otherMoveTo

  WHERE subscription_id = @subscriptionId

`;

```

---

## Security & Authentication

### Authentication Flow

1. Client authenticates with JWT token

2. JWT contains client\_name and roles

3. Fastify middleware validates JWT and extracts user

4. Authorization middleware checks roles and scopes

### Required Roles & Scopes

\| Endpoint | Roles | Scopes |

\|----------|-------|--------|

\| GET /auth | Administrator | EXCHANGE\_READ |

\| GET /auth/callback | None (public) | - |

\| POST /webhook/inbox | None (public) | - |

\| GET /list\_inboxes | Administrator | EXCHANGE\_READ |

\| DELETE /unsubscribe/inbox/:id | Administrator | - |

\| GET /blocked\_senders | Administrator | EXCHANGE\_READ |

\| POST /blocked\_senders | Administrator | EXCHANGE\_WRITE |

\| DELETE /blocked\_senders/:id | Administrator | EXCHANGE\_WRITE |

\| GET /exchange\_options | Administrator | - |

\| POST /exchange\_options | Administrator | - |

### Token Security

* Access tokens encrypted at rest using AES-256-CBC

* Refresh tokens encrypted at rest using AES-256-CBC

* Encryption key stored as base64-encoded environment variable

* OAuth state parameters have 10-minute expiration

* State parameters validated to prevent CSRF attacks

### Input Validation

* All request bodies validated with Zod schemas

* Email addresses validated with regex

* Domain patterns validated with regex

* UUIDs validated with Zod

* Path parameters sanitized

---

## Error Handling

### Error Response Format

```typescript

{

  error: string,

  details?: any,

  code?: string

}

```

### HTTP Status Codes

\| Status | Usage |

\|--------|-------|

\| 200 OK | Successful operation |

\| 201 Created | Resource created (rare in this API) |

\| 204 No Content | Successful deletion |

\| 400 Bad Request | Validation error, invalid input |

\| 401 Unauthorized | Missing or invalid JWT token |

\| 403 Forbidden | Insufficient permissions |

\| 404 Not Found | Resource doesn't exist |

\| 409 Conflict | State conflict (inbox mismatch) |

\| 500 Internal Server Error | Unexpected server error |

\| 504 Gateway Timeout | External API timeout |

### Error Handling Strategy

```typescript

// Controller pattern

try {

  const result = await ExchangesService.someMethod();

  return reply.send(result);

} catch (error) {

  request.log.error('Operation failed:', error);

  if (error instanceof ValidationError) {

    return reply.status(400).send({ error: error.message });

  }

  if (error instanceof NotFoundError) {

    return reply.status(404).send({ error: error.message });

  }

  if (error instanceof GraphApiError) {

    return reply.status(error.statusCode).send({ error: error.message });

  }

  return reply.status(500).send({

    error: `Operation failed: ${(error as Error).message}`

  });

}

```

### Logging Requirements

* Log all OAuth flow events with state tracking

* Log all webhook notifications received

* Log all email processing steps

* Log all Graph API calls with timing

* Log all database operations

* Log all errors with full context

* Use structured logging (Pino)

* Include correlation IDs for tracing

---

## Implementation Checklist

### Task 2.1 - OAuth Authorization Endpoints

* [ ] Create state store with expiration (Map or Redis)

* [ ] Implement state generation (UUID v4)

* [ ] Implement GET /auth endpoint

* [ ] Build Microsoft OAuth URL with proper parameters

* [ ] Add Zod schema for query parameters

* [ ] Implement GET /auth/callback endpoint

* [ ] Add state validation logic

* [ ] Implement token exchange with Microsoft

* [ ] Add ID token decoding

* [ ] Implement inbox validation logic

* [ ] Add error handling for all failure cases

* [ ] Update exchanges.schema.ts with OpenAPI docs

* [ ] Test OAuth flow end-to-end

### Task 2.2 - Webhook Endpoint

* [ ] Implement POST /webhook/inbox endpoint

* [ ] Handle validation token requests

* [ ] Parse webhook notification payload

* [ ] Validate required fields (clientState, subscriptionId, resourceData.id)

* [ ] Implement email fetching from Graph API

* [ ] Implement attachment fetching and decoding

* [ ] Add PDF validation (check %PDF header)

* [ ] Integrate blocked sender checking

* [ ] Implement email actions (mark as read, move to folder)

* [ ] Add background task processing

* [ ] Add comprehensive error handling

* [ ] Update exchanges.schema.ts with OpenAPI docs

* [ ] Test webhook with real notifications

### Task 2.3 - Inbox Management Endpoints

* [ ] Implement GET /list\_inboxes endpoint

* [ ] Add client\_name to ClientId resolution

* [ ] Query exchange\_inbox\_subscriptions table

* [ ] Implement DELETE /unsubscribe/inbox/:id endpoint

* [ ] Add subscription expiration checking

* [ ] Implement Graph API subscription deletion

* [ ] Implement database subscription deletion

* [ ] Add cascade token deletion

* [ ] Update exchanges.schema.ts with OpenAPI docs

* [ ] Test subscription lifecycle

### Task 2.4 - Blocked Senders Endpoints

* [ ] Implement GET /blocked\_senders endpoint

* [ ] Query exchange\_blocked\_senders table

* [ ] Implement POST /blocked\_senders endpoint

* [ ] Add email/domain validation with Zod

* [ ] Implement database insertion with duplicate handling

* [ ] Implement DELETE /blocked\_senders/:id endpoint

* [ ] Add blocked sender checking utility

* [ ] Update exchanges.schema.ts with OpenAPI docs

* [ ] Test blocked sender filtering

### Task 2.5 - Exchange Options Endpoints

* [ ] Implement GET /exchange\_options endpoint

* [ ] Query exchange\_inbox\_subscriptions for event actions

* [ ] Load classifier config from Cosmos DB (or config file)

* [ ] Merge configuration objects

* [ ] Implement POST /exchange\_options endpoint

* [ ] Validate classified object structure

* [ ] Update event\_actions fields in database

* [ ] Update exchanges.schema.ts with OpenAPI docs

* [ ] Test configuration updates

### Task 2.6 - Service Layer Implementation

* [ ] Create GraphService for Microsoft Graph API calls

* [ ] Implement token encryption/decryption utilities

* [ ] Implement token refresh logic

* [ ] Create email processing service methods

* [ ] Implement attachment fetching and decoding

* [ ] Implement folder creation/retrieval logic

* [ ] Implement email update operations (mark as read)

* [ ] Implement email move operations

* [ ] Add proper timeout handling (30s for Graph API)

* [ ] Add retry logic for transient failures

### Task 2.7 - Zod Schemas & Validation

* [ ] Create AuthQuerySchema

* [ ] Create AuthCallbackQuerySchema

* [ ] Create BlockedSenderBodySchema with custom validation

* [ ] Create ExchangeOptionsQuerySchema

* [ ] Create ExchangeOptionsBodySchema

* [ ] Create SubscriptionIdParamSchema

* [ ] Create SenderIdParamSchema

* [ ] Add custom email/domain regex validators

* [ ] Update all schemas in exchanges.schema.ts

### Task 2.8 - Error Handling & Logging

* [ ] Add structured error classes (ValidationError, NotFoundError, etc.)

* [ ] Implement consistent error response format

* [ ] Add comprehensive logging to all service methods

* [ ] Add correlation IDs for request tracing

* [ ] Add timing metrics for Graph API calls

* [ ] Add error context (subscription\_id, message\_id, etc.)

* [ ] Test error scenarios

### Task 2.9 - Integration & Testing

* [ ] Test OAuth flow with real Microsoft account

* [ ] Test webhook validation token response

* [ ] Test webhook notification processing

* [ ] Test blocked sender filtering

* [ ] Test email actions (mark as read, move to folder)

* [ ] Test token refresh logic

* [ ] Test error handling for all endpoints

* [ ] Verify database operations

* [ ] Load test webhook endpoint

### Task 2.10 - Documentation

* [ ] Update OpenAPI schemas in exchanges.schema.ts

* [ ] Add JSDoc comments to all public methods

* [ ] Document environment variables

* [ ] Create Postman collection

* [ ] Document Graph API integration

* [ ] Document token encryption approach

* [ ] Create deployment notes

---

## Environment Variables

```env

# Microsoft OAuth Configuration

EXCHANGE_CLIENT_ID=your-client-id

EXCHANGE_CLIENT_SECRET=your-client-secret

EXCHANGE_TENANT_ID=common

  

# Exchange Configuration

EXCHANGE_REDIRECT_URI=http://localhost:5000/v2/exchange/auth/callback

EXCHANGE_WEBHOOK_URL=http://localhost:5000/v2/exchange/webhook/inbox

EXCHANGE_SUBSCRIPTION_EXPIRATION_MINUTES=4230

FRONTEND_PUBLIC_URL=http://localhost:8085

  

# Token Encryption

EXCHANGE_SECRET_KEY=base64-encoded-32-byte-key

  

# Azure Document Intelligence (for classification)

COGNITIVE_SERVICES_ENDPOINT=https://your-resource.cognitiveservices.azure.com/

COGNITIVE_SERVICES_KEY=your-key

  

# Database

DB_HOST=localhost

DB_PORT=1433

DB_NAME=your-database

DB_USER=your-user

DB_PASSWORD=your-password

```

---

## Dependencies

### Required npm Packages

```json

{

  "dependencies": {

    "fastify": "^4.x",

    "@fastify/jwt": "^7.x",

    "zod": "^3.x",

    "axios": "^1.x",

    "jsonwebtoken": "^9.x",

    "crypto": "built-in",

    "uuid": "^9.x",

    "mssql": "^10.x",

    "@azure/ai-form-recognizer": "^5.x",

    "@azure/storage-blob": "^12.x"

  }

}

```

---

## Testing Strategy

### Unit Tests

* Test token encryption/decryption

* Test email/domain validation

* Test state management

* Test blocked sender checking

* Test configuration merging

### Integration Tests

* Test OAuth flow end-to-end

* Test webhook processing

* Test Graph API integration

* Test database operations

* Test email actions

### Manual Testing

* Test with real Microsoft account

* Test webhook with ngrok

* Test various email scenarios

* Test blocked sender filtering

* Test folder creation

---

## Next Steps

1. Review and approve this design document

2. Create detailed implementation tickets for each task

3. Set up development environment with required credentials

4. Begin implementation with Task 2.1 (OAuth endpoints)

5. Implement tasks sequentially with testing after each

6. Conduct code review after each major component

7. Perform end-to-end integration testing

8. Update documentation with any design changes

---

*Document Version: 1.0*

*Created: January 9, 2026*

*Author: GitHub Copilot*