npm package: @pavas-core/email-orchestrator
Description: NestJS library for email orchestration - Google Workspace, Microsoft Outlook, IMAP/SMTP
Latest version: 1.0.3

# @pavas-core/email-orchestrator

[![npm version](https://img.shields.io/npm/v/@pavas-core/email-orchestrator.svg)](https://www.npmjs.com/package/@pavas-core/email-orchestrator)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![GitHub](https://img.shields.io/badge/GitHub-juanpavasgarzon%2Femail--orchestrator-blue)](https://github.com/juanpavasgarzon/email-orchestrator)

Librería NestJS para orquestar correos electrónicos con múltiples proveedores desde una API unificada.

## Características

- **Google Workspace (Gmail)** — OAuth2, Gmail API y push notifications vía Pub/Sub
- **Microsoft Outlook** — OAuth2, Microsoft Graph y webhooks para notificaciones
- **IMAP/SMTP** — Conexión estándar, config por operación (sin `forRoot`)

Interfaces compartidas, paginación por cursor y flujos OAuth delegados al consumidor. La librería **no almacena credenciales**; tú gestionas tokens y configs.

- **Scopes de perfil siempre incluidos** — `email`, `profile`, `openid` (Google) y `User.Read`, `openid`, `profile` (Outlook) se agregan automáticamente para obtener info de la cuenta.
- **Listar conexiones** — `listConnections(connectionKey)` por usuario; `listConnections()` sin key devuelve todas agrupadas por key.

## Requisitos

- NestJS 10 o 11
- Node.js 18+
- **Todas las credenciales las provee el consumidor** (client ID, secret, scopes, tokens). La librería no persiste ni gestiona credenciales.

## Instalación

```bash
npm install @pavas-core/email-orchestrator
```

**Repositorio:** [github.com/juanpavasgarzon/email-orchestrator](https://github.com/juanpavasgarzon/email-orchestrator)

## Quick Start

### Google (Gmail)

**tokenCache** opcional (usa `InMemoryTokenCache` por defecto). **global** opcional: si `true`, `GoogleService` disponible en toda la app; si `false` (por defecto), importa el módulo donde lo necesites.

```typescript
import { GoogleModule, GoogleService } from '@pavas-core/email-orchestrator';

// Opción 1: Sin config (usa InMemoryTokenCache - almacenamiento automático)
@Module({
  imports: [
    GoogleModule.forRoot({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      redirectUri: process.env.GOOGLE_REDIRECT_URI!,
      scopes: [
        'https://www.googleapis.com/auth/gmail.readonly',
        'https://www.googleapis.com/auth/gmail.send',
      ],
      global: true,  // opcional: false por defecto
    }),
  ],
})
export class AppModule {}
```

```typescript
// OAuth: redirigir al login
@Get('auth/google')
redirectToGoogle() {
  return { url: this.googleService.getAuthUrl() };
}

// Callback: tokens se almacenan automáticamente con key userId-google
@Get('auth/google/callback')
async handleCallback(@Query('code') code: string, @Req() req) {
  const userId = req.user?.id ?? 'default';
  await this.googleService.exchangeCodeForTokens(code, userId);
  return { ok: true };
}

// Listar emails (connectionKey = userId del request)
const page = await this.googleService.listEmails(userId, { limit: 20 });
```

**Propagación de state OAuth:** Pasa datos (userId, returnTo, etc.) entre inicio y callback:

```typescript
import {
  GoogleService,
  encodeOAuthState,
  decodeOAuthState,
} from '@pavas-core/email-orchestrator';

// Inicio: incluir state con datos
@Get('auth/google')
redirectToGoogle(@Req() req) {
  const userId = req.user?.id ?? 'default';
  const state = encodeOAuthState({ userId, returnTo: '/dashboard' });
  return { url: this.googleService.getAuthUrl(state) };
}

// Callback: leer state para obtener connectionKey
@Get('auth/google/callback')
async handleCallback(
  @Query('code') code: string,
  @Query('state') state: string,
) {
  const payload = decodeOAuthState(state ?? '');
  const connectionKey = payload?.userId ?? 'default';
  await this.googleService.exchangeCodeForTokens(code, connectionKey);
  return { redirect: payload?.returnTo ?? '/' };
}
```

**tokenCache custom** (DB, Redis, etc.) — los tokens se almacenan automáticamente con key `connectionKey-provider`:

```typescript
import type { TokenCache } from '@pavas-core/email-orchestrator';

const myCache: TokenCache = {
  async get(key) { return db.get(key); },
  async set(key, tokens) { await db.set(key, tokens); },
};

GoogleModule.forRoot({
  clientId: '...',
  clientSecret: '...',
  redirectUri: '...',
  scopes: ['...'],
  tokenCache: myCache,
});
```

### Outlook

**global** opcional (false por defecto). **tokenCache** opcional.

```typescript
import { OutlookModule } from '@pavas-core/email-orchestrator';

OutlookModule.forRoot({
  clientId: process.env.OUTLOOK_CLIENT_ID!,
  clientSecret: process.env.OUTLOOK_CLIENT_SECRET!,
  tenantId: process.env.OUTLOOK_TENANT_ID!,
  redirectUri: process.env.OUTLOOK_REDIRECT_URI!,
  scopes: [
    'https://graph.microsoft.com/Mail.Read',
    'https://graph.microsoft.com/Mail.Send',
  ],
  global: true,      // opcional
  tokenCache: myCache,
});
```

Flujo OAuth: `getAuthUrl()` → redirect → callback con `exchangeCodeForTokens(code)` → tokens guardados en tokenCache.

**Google y Outlook usan la misma configuración:** clientId, clientSecret, redirectUri, scopes, tokenCache. Solo Outlook añade `tenantId`. El cliente provee la persistencia (tokenCache); la librería construye el token provider internamente.

**MSAL (@azure/msal-node):** La librería no incluye MSAL. Instálalo en tu app (`npm install @azure/msal-node`). Implementa un `tokenCache` que lea de MSAL; es el único punto externo de persistencia:

```typescript
import { ConfidentialClientApplication } from '@azure/msal-node';
import type { TokenCache, OAuthTokens } from '@pavas-core/email-orchestrator';

// Tu TokenCache que adapta MSAL
const msalTokenCache: TokenCache<OAuthTokens> = {
  async get(key: string) {
    const connectionKey = key.replace('-outlook', '');
    const integration = await getIntegrationByConnectionKey(connectionKey);
    const homeAccountId = integration.metadata.home_account_id as string;
    const client = createMsalClient(homeAccountId); // ConfidentialClientApplication con cache
    const account = await client.getTokenCache().getAccountByHomeId(homeAccountId);
    const result = await client.acquireTokenSilent({ account, scopes: integration.scopes });
    return {
      accessToken: result.accessToken,
      refreshToken: '',
      expiresAt: result.expiresOn ? result.expiresOn.getTime() : undefined,
    };
  },
  async set(key, tokens) {
    // MSAL ya persiste en acquireTokenByCode; o sincroniza aquí si usas nuestro exchange
  },
};

OutlookModule.forRoot({ ...config, tokenCache: msalTokenCache });
```

En el callback OAuth usa `acquireTokenByCode` de MSAL (no `exchangeCodeForTokens`). MSAL almacena en su cache; tu `tokenCache.get()` lee desde ahí.

### IMAP / SMTP

Sin config de credenciales a nivel módulo. Usa `forRoot()`. Opción `global`: si `true`, `ImapSmtpService` está disponible en toda la app; si `false` (por defecto), importa el módulo en cada módulo que lo use.

```typescript
import { ImapSmtpModule, ImapSmtpService } from '@pavas-core/email-orchestrator';
import type { ImapSmtpConnectionConfig } from '@pavas-core/email-orchestrator';

@Module({
  imports: [ImapSmtpModule.forRoot()],  // forRoot({ global: true }) si quieres ImapSmtpService en toda la app
})
export class AppModule {}

// Registrar conexión (aparece en listConnections)
@Post('imap-smtp/connect')
async register(@Body() config: ImapSmtpConnectionConfig, @Req() req) {
  const userId = req.user?.id;
  await this.imapSmtp.registerConnection(userId, config);
}

// Enviar con config pasada (solo smtp configurado = envío)
await this.imapSmtp.sendEmail(config, { to: '...', subject: '...', text: '...' });

// Listar con config pasada (solo imap configurado = lectura)
await this.imapSmtp.listEmails(config, { limit: 20 });

// Config: imap y/o smtp (al menos uno)
const connection: ImapSmtpConnectionConfig = {
  imap: { host: 'imap.ejemplo.com', port: 993, user: '...', password: '...', tls: true },
  smtp: { host: 'smtp.ejemplo.com', port: 587, user: '...', password: '...', secure: false },
};
```

---

## Listar conexiones establecidas

Con Google, Outlook o ImapSmtp importados, `ConnectionRegistry` está disponible. Inyecta y pasa el `connectionKey` (userId, apiKey, etc.):

```typescript
import { ConnectionRegistry } from '@pavas-core/email-orchestrator';

@Injectable()
export class ConnectionsController {
  constructor(private readonly connections: ConnectionRegistry) {}

  @Get('connections')
  async list(@Req() req, @Query('all') all?: string) {
    if (all === 'true') {
      return this.connections.listConnections();  // todas las conexiones (por key)
    }
    const connectionKey = req.user?.id ?? req.headers['x-api-key'];
    return this.connections.listConnections(connectionKey);  // solo ese usuario
  }
}
```

Cada ítem incluye:
- `provider` — `'google'` | `'outlook'` | `'imap-smtp'`
- `email`, `name`, `picture` — datos de la cuenta
- `requestedScopes` — permisos solicitados en la config
- `grantedScopes` — permisos efectivamente otorgados (pueden ser menos según Workspace/Admin)

**Persistencia sustituible:** Por defecto usa memoria. Para DB, Redis, etc.:

```typescript
import {
  ConnectionRegistry,
  CONNECTION_PERSISTENCE,
  type ConnectionPersistence,
} from '@pavas-core/email-orchestrator';

@Injectable()
class MyDbConnectionPersistence implements ConnectionPersistence {
  async get(connectionKey: string) { /* devuelve ConnectionInfo[] | null */ }
  async set(connectionKey: string, connections: ConnectionInfo[]) { /* almacena */ }
  async getAll() { /* devuelve ConnectionGroup[] = { connectionKey, connections }[] */ }
}

@Module({
  imports: [GoogleModule, OutlookModule],
  providers: [
    { provide: CONNECTION_PERSISTENCE, useClass: MyDbConnectionPersistence },
  ],
})
export class AppModule {}
```

- `listConnections(connectionKey?)` — con key: consulta APIs y actualiza. Sin key: devuelve todas desde cache (`ConnectionGroup[]`).
- `getCachedConnections(connectionKey?)` — con key: conexiones del usuario. Sin key: todas (`ConnectionGroup[]`).
- `mergeConnection(connectionKey, connectionInfo)` — fusiona una conexión en la persistencia (usado por getConnectionInfo de cada provider).

**Reducción de costos:** Los scopes de email (Google Workspace, Microsoft Graph) tienen un costo monetario alto. Para minimizar llamadas a APIs:

```typescript
import {
  ConnectionModule,
  CONNECTION_REGISTRY_OPTIONS,
  type ConnectionRegistryOptions,
} from '@pavas-core/email-orchestrator';

@Module({
  imports: [GoogleModule, OutlookModule],
  providers: [
    {
      provide: CONNECTION_REGISTRY_OPTIONS,
      useValue: {
        connectionInfoCacheTtlMs: 300000, // 5 min cache para listConnections
      } as ConnectionRegistryOptions,
    },
  ],
})
export class AppModule {}
```

- `getConnectionInfo` de cada provider (Google, Outlook, IMAP/SMTP) actualiza automáticamente la cache de ConnectionRegistry.

Ejemplo de respuesta `listConnections(connectionKey)` (un usuario):

```json
[
  {
    "provider": "google",
    "email": "user@gmail.com",
    "name": "Usuario",
    "picture": "https://...",
    "requestedScopes": ["email", "profile", "openid", "https://www.googleapis.com/auth/gmail.readonly", "https://www.googleapis.com/auth/gmail.send"],
    "grantedScopes": ["https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile", "openid", "https://www.googleapis.com/auth/gmail.readonly"]
  },
  {
    "provider": "outlook",
    "email": "user@empresa.com",
    "name": "Usuario Empresa",
    "requestedScopes": ["User.Read", "openid", "profile", "Mail.Read", "Mail.Send"],
    "grantedScopes": ["User.Read", "Mail.Read", "Mail.Send"]
  }
]
```

Ejemplo de respuesta `listConnections()` sin key (todas las conexiones, desde cache):

```json
[
  {
    "connectionKey": "user123",
    "connections": [
      { "provider": "google", "email": "user@gmail.com", "name": "Usuario", "requestedScopes": [...], "grantedScopes": [...] }
    ]
  },
  {
    "connectionKey": "user456",
    "connections": [
      { "provider": "outlook", "email": "otro@empresa.com", "name": "Otro", "requestedScopes": [...], "grantedScopes": [...] }
    ]
  }
]
```

---

## Token Cache

Siempre se usa `tokenCache`. La key es `connectionKey-provider` (ej: `user123-google`).

- **Sin tokenCache:** usa `InMemoryTokenCache`, tokens se guardan en el callback.
- **Con tokenCache:** `exchangeCodeForTokens` y `refreshAccessToken` hacen `set(key, tokens)` automáticamente.

Implementa `TokenCache` para cache custom (DB, Redis, etc.):

```typescript
import type { TokenCache, OAuthTokens } from '@pavas-core/email-orchestrator';

class MyTokenCache implements TokenCache {
  async get(key: string): Promise<OAuthTokens | null> {
    return await db.getTokens(key);
  }
  async set(key: string, tokens: OAuthTokens): Promise<void> {
    await db.saveTokens(key, tokens);
  }
}
```

---

## Helpers sin Nest

Puedes usar los helpers OAuth fuera de Nest en rutas externas o scripts:

```typescript
import {
  getGoogleAuthUrl,
  exchangeGoogleCodeForTokens,
  refreshGoogleAccessToken,
} from '@pavas-core/email-orchestrator/google';

import {
  getOutlookAuthUrl,
  exchangeOutlookCodeForTokens,
  refreshOutlookAccessToken,
} from '@pavas-core/email-orchestrator/outlook';
```

---

## Push Notifications

### Gmail (Pub/Sub)

**1. Google Cloud Console**

- Crea un proyecto y habilita Gmail API.
- Credenciales OAuth con scopes `gmail.readonly` y `gmail.modify`.

**2. Pub/Sub**

```bash
# Crear topic
gcloud pubsub topics create gmail-notifications

# Otorgar permisos a Gmail API
gcloud pubsub topics add-iam-policy-binding gmail-notifications \
  --member="serviceAccount:gmail-api-push@system.gserviceaccount.com" \
  --role="roles/pubsub.publisher"
```

**3. Push subscription**

Crea una suscripción que apunte a tu endpoint HTTPS (ej. `https://tu-app.com/webhooks/gmail`):

```bash
gcloud pubsub subscriptions create gmail-push \
  --topic=gmail-notifications \
  --push-endpoint=https://tu-app.com/webhooks/gmail
```

**4. Código**

```typescript
// Módulo con callbacks configurables (onNewEmail, onReply)
GoogleModule.forRoot({
  // ... clientId, clientSecret, etc.
  pushNotificationCallbacks: {
    onNewEmail: (event) => console.log('Correo nuevo:', event.message?.subject),
    onReply: (event) => console.log('Respondieron:', event.message?.subject),
  },
});

// Iniciar watch (después de OAuth)
const { historyId, expiration } = await this.googleService.setupPushNotifications(
  'projects/TU_PROJECT_ID/topics/gmail-notifications'
);

// Endpoint POST: una sola llamada a handleWebhook invoca los callbacks automáticamente
@Post('webhooks/gmail')
async handleGmailPush(@Body() body: unknown) {
  await this.googleService.handleWebhook(body);
}
```

**Tipos de notificación (ambos pueden estar habilitados):**
- `onNewEmail`: correo nuevo en el buzón (se llama siempre que llega algo)
- `onReply`: alguien respondió un correo (tiene In-Reply-To). Si es reply, se invocan **ambos** callbacks

---

### Outlook (Webhooks)

**1. Endpoint HTTPS**

Tu app debe exponer un endpoint que reciba solicitudes de Microsoft (ej. `https://tu-app.com/webhooks/outlook`).

**2. Validación inicial**

Microsoft envía primero un **GET** con `?validationToken=xxx`. Tu endpoint debe responder **200** con el body = exactamente el `validationToken` (texto plano):

```typescript
// Módulo con callbacks configurables
OutlookModule.forRoot({
  // ...
  pushNotificationCallbacks: {
    onNewEmail: (event) => this.notifyNewEmail(event),
    onReply: (event) => this.notifyReply(event),
  },
});

@All('webhooks/outlook')
async handleOutlookWebhook(@Req() req: Request, @Res() res: Response) {
  if (this.outlookService.isValidationRequest(req.query)) {
    return res.status(200).send(this.outlookService.getValidationResponse(req.query));
  }
  res.status(202).send(); // Responder en < 3 segundos
  void this.outlookService.handleWebhook(req.body); // Procesa e invoca callbacks
}
```

**3. Crear suscripción**

```typescript
const { subscriptionId, expirationDateTime } =
  await this.outlookService.setupPushNotifications(
    'https://tu-app.com/webhooks/outlook'
  );
// Guardar subscriptionId y renovar antes de expirationDateTime
```

**4. Renovar**

```typescript
await this.outlookService.renewPushSubscription(subscriptionId);
```

---

### IMAP IDLE (push-like)

Para IMAP no hay webhooks nativos, pero `startIdleWatcher` simula push:

```typescript
const stop = await this.imapSmtpService.startIdleWatcher(
  connectionConfig,
  (newMail) => console.log('Nuevo email:', newMail)
);
// Cuando quieras desconectar:
stop();
```

---

## Interfaces Comunes

Todos los servicios usan las mismas interfaces y paginación por cursor:

| Tipo                | Descripción                                      |
|---------------------|--------------------------------------------------|
| `EmailMessage`      | Mensaje normalizado (id, subject, from, to, body) |
| `ListEmailsOptions` | `{ limit?, cursor?, filter?, orderBy?, query?, labelIds? }` — filter/orderBy unificados |
| `ListEmailsResult`  | `{ items, nextCursor?, hasMore }`                |
| `SendEmailOptions`  | Opciones para enviar                             |
| `SendEmailResult`   | `{ messageId }`                                  |
| `FetchHistoryOptions` | `{ startHistoryId?, cursor?, limit? }`          |
| `FetchHistoryResult` | `{ events, nextCursor?, hasMore }`              |

```typescript
import type {
  EmailMessage,
  ListEmailsOptions,
  ListEmailsFilter,
  ListEmailsOrderBy,
  ListEmailsResult,
  SendEmailOptions,
  SendEmailResult,
} from '@pavas-core/email-orchestrator';

// Filtro unificado (Google, Outlook, IMAP/SMTP - misma sintaxis)
await googleService.listEmails(userId, {
  filter: { from: 'x@y.com', subject: 'foo', isUnread: true },
  orderBy: { field: 'date', direction: 'desc' },
  limit: 20,
});
```

---

## Varios Proveedores

Puedes importar varios módulos en el mismo proyecto:

```typescript
@Injectable()
export class EmailOrchestrator {
  constructor(
    private readonly googleService: GoogleService,
    private readonly outlookService: OutlookService,
    private readonly imapSmtpService: ImapSmtpService,
  ) {}

  async sendViaGoogle(connectionKey: string, opts: SendEmailOptions) {
    return this.googleService.sendEmail(connectionKey, opts);
  }

  async sendViaImap(config: ImapSmtpConnectionConfig, opts: SendEmailOptions) {
    return this.imapSmtpService.sendEmail(config, opts);
  }
}
```

---

## Arquitectura de servicios (SOLID)

Cada provider expone **2 servicios** con responsabilidades separadas:

| Provider | Auth/Connection | Utilidad |
|----------|-----------------|----------|
| Google | `GoogleAuthService` — OAuth, tokens, getAuthUrl | `GoogleService` — email ops, push (extiende Auth) |
| Outlook | `OutlookAuthService` — OAuth, tokens, MSAL | `OutlookService` — email ops, webhooks (extiende Auth) |
| IMAP/SMTP | `ImapSmtpConnectionService` — registerConnection, getConnectionInfo | `ImapSmtpService` — sendEmail, listEmails (delega conexiones) |

Inyecta `GoogleAuthService` o `OutlookAuthService` cuando solo necesitas auth. `GoogleService` y `OutlookService` heredan los métodos de auth y añaden los de correo.

---

## API Reference — Métodos y Scopes

### GoogleModule

`forRoot` y `forRootAsync` aceptan `global?: boolean` (false por defecto). Exporta `GoogleAuthService` y `GoogleService`.

### GoogleService / GoogleAuthService

`GoogleService` extiende `GoogleAuthService`. Todos los métodos reciben `connectionKey` como primer argumento (excepto `getAuthUrl`).

| Método | Descripción | Scope requerido |
|--------|-------------|-----------------|
| `getAuthUrl()` | URL de autorización OAuth | Scopes configurados |
| `exchangeCodeForTokens(code, connectionKey)` | Intercambiar code por tokens | Scopes configurados |
| `refreshAccessToken(refreshToken, connectionKey)` | Refrescar access token | Scopes configurados |
| `getConnectionInfo(connectionKey)` | Info de cuenta + permisos otorgados | `email`, `profile`, `openid` (automático) |
| `sendEmail(connectionKey, options)` | Enviar correo | `https://www.googleapis.com/auth/gmail.send` |
| `listEmails(connectionKey, options)` | Listar correos | `https://www.googleapis.com/auth/gmail.readonly` |
| `getEmail(connectionKey, messageId)` | Obtener correo por ID | `https://www.googleapis.com/auth/gmail.readonly` |
| `setupPushNotifications(connectionKey, topic)` | Configurar push (Pub/Sub) | `https://www.googleapis.com/auth/gmail.modify` |
| `stopPushNotifications(connectionKey)` | Detener push | `https://www.googleapis.com/auth/gmail.modify` |
| `handleWebhook(connectionKey, body)` | Maneja webhook: parsea, obtiene cambios e invoca onNewEmail/onReply | `gmail.readonly` |
| `parsePubSubPayload(body)` | Parsear payload Pub/Sub | — (sin API) |
| `fetchChangesSinceHistory(connectionKey, options)` | Obtener cambios tras push | `https://www.googleapis.com/auth/gmail.readonly` |

**Scopes Google más usados:**
- `gmail.send` — enviar
- `gmail.readonly` — leer
- `gmail.modify` — leer + modificar + push notifications

---

### OutlookModule

`forRoot` y `forRootAsync` aceptan `global?: boolean` (false por defecto). Exporta `OutlookAuthService` y `OutlookService`.

### OutlookService / OutlookAuthService

`OutlookService` extiende `OutlookAuthService`. Todos los métodos reciben `connectionKey` como primer argumento (excepto `getAuthUrl`).

| Método | Descripción | Scope requerido |
|--------|-------------|-----------------|
| `getAuthUrl()` | URL de autorización OAuth | Scopes configurados |
| `exchangeCodeForTokens(code, connectionKey)` | Intercambiar code por tokens | Scopes configurados |
| `refreshAccessToken(refreshToken, connectionKey)` | Refrescar tokens | Scopes configurados |
| `getConnectionInfo(connectionKey)` | Info de cuenta + permisos | `User.Read` (automático) |
| `sendEmail(connectionKey, options)` | Enviar correo | `https://graph.microsoft.com/Mail.Send` |
| `listEmails(connectionKey, options)` | Listar correos | `https://graph.microsoft.com/Mail.Read` |
| `getEmail(connectionKey, messageId)` | Obtener correo por ID | `https://graph.microsoft.com/Mail.Read` |
| `setupPushNotifications(connectionKey, url)` | Configurar webhooks | `https://graph.microsoft.com/Mail.Read` |
| `renewPushSubscription(connectionKey, id)` | Renovar suscripción | `https://graph.microsoft.com/Mail.Read` |
| `deletePushSubscription(connectionKey, id)` | Eliminar suscripción | `https://graph.microsoft.com/Mail.Read` |
| `handleWebhook(connectionKey, body)` | Maneja webhook: parsea, obtiene mensajes e invoca onNewEmail/onReply | `Mail.Read` |
| `parsePushNotification(body)` | Parsear notificación | — (sin API) |
| `fetchFullMessagesForEvents(connectionKey, events)` | Mensajes completos para eventos | `https://graph.microsoft.com/Mail.Read` |
| `isValidationRequest(query)` | Detectar validación webhook | — (estático) |
| `getValidationResponse(query)` | Respuesta para validación | — (estático) |

**Scopes Outlook más usados:**
- `Mail.Send` — enviar
- `Mail.Read` — leer + push notifications

---

### ImapSmtpModule

| Método | Descripción |
|--------|-------------|
| `forRoot(options?)` | Registra el módulo. Exporta `ImapSmtpConnectionService` e `ImapSmtpService`. |

### ImapSmtpConnectionService

Gestiona conexiones: `registerConnection`, `getStoredConnection`, `removeConnection`, `getConnectionInfo`.

### ImapSmtpService

Operaciones de correo. La config se pasa en cada llamada o se registra con `registerConnection` (delega a `ImapSmtpConnectionService`).

| Método | Descripción |
|--------|-------------|
| `registerConnection(connectionKey, config)` | Almacena conexión (aparece en listConnections) |
| `getStoredConnection(connectionKey)` | Obtiene config almacenada |
| `removeConnection(connectionKey)` | Elimina conexión almacenada |
| `sendEmail(connection, options)` | Enviar correo (requiere smtp en config) |
| `listEmails(connection, options)` | Listar correos (requiere imap en config) |
| `getEmail(connection, messageIdOrUid)` | Obtener correo |
| `startIdleWatcher(connection, onNewMail)` | Escuchar nuevos correos (IDLE) |

Config: `imap` y/o `smtp` (al menos uno). `grantedScopes` en listConnections: `['imap']`, `['smtp']` o `['imap','smtp']`.

---

### ConnectionRegistry

| Método | Descripción |
|--------|-------------|
| `listConnections(connectionKey?)` | Con key: consulta APIs, actualiza. Sin key: todas desde cache |
| `getCachedConnections(connectionKey?)` | Con key: cache del usuario. Sin key: todas (`ConnectionGroup[]`) |

La persistencia es sustituible: por defecto `InMemoryConnectionPersistence`. Provee `CONNECTION_PERSISTENCE` con tu implementación de `ConnectionPersistence` (DB, Redis, etc.).

---

### Exports del paquete

| Export | Contenido |
|--------|-----------|
| `./` (root) | Interfaces, ConnectionRegistry, ConnectionModule, InMemoryTokenCache, CONNECTION_PERSISTENCE, IMAP_SMTP_CONNECTION_STORE |
| `./google` | GoogleModule, GoogleService, helpers OAuth |
| `./outlook` | OutlookModule, OutlookService, helpers OAuth |
| `./imap-smtp` | ImapSmtpModule, ImapSmtpService |

`ConnectionRegistry` se obtiene al importar GoogleModule u OutlookModule (incluyen ConnectionModule).

---

## Licencia

MIT © Pavas. Ver [LICENSE](https://mit-license.org/).