Skip to navigation

Exchange API Core Features Implementation

Story and Technical Design Document


Table of Contents

  1. Overview

  2. User Stories

  3. Technical Architecture

  4. API Endpoints Design

  5. Data Models & Schemas

  6. Service Layer Design

  7. Database Schema

  8. Security & Authentication

  9. Error Handling

  10. 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:

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

Headers:

Authorization: Bearer <jwt_token>

Response: 200 OK

{
  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:

{
  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):

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

Request Body (notification):

{
  value: Array<{
    clientState: string
    subscriptionId: string
    resourceData: {
      id: string // message_id
    }
  }>
}

Response: 200 OK

{
  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

{
  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:

{
  subscription_id: string
}

Headers:

Authorization: Bearer <jwt_token>

Response: 200 OK

{
  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

{
  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:

{
  value: string        // Email or domain pattern
  reason?: string      // Optional reason for blocking
}

Response: 200 OK

{
  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:

{
  sender_id: number
}

Headers:

Authorization: Bearer <jwt_token>

Response: 200 OK

{
  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:

{
  subscription_id: string
}

Headers:

Authorization: Bearer <jwt_token>

Response: 200 OK

{
  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:

{
  subscription_id: string
  classified: {
    invoice: {
      markAs: "read" | "unread"
      moveTo: string
    }
    other: {
      markAs: "read" | "unread"
      moveTo: string
    }
  }
}

Response: 200 OK

{
  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

// 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

// 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

// 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

// 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

// 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

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

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

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

// 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

{
  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

// 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

# 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

{
  "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