Microsoft Graph API v1.0

Outlook Email Automation / GrowthSaaS B2B

Modulo TypeScript completo — 3 flujos automatizados para Microsoft 365

📅 Semana 24, 2026
💸 Tenant: a1b2c3d4-e5f6-7890-abcd...
📧 Buzones: maria.lopez@growthsaas.io · sales@growthsaas.io
🛠 Deploy: Azure Functions v4
47
Leads activos pipeline
12
En fase Demo
$380K
Oportunidades abiertas
23%
Tasa de conversion
Flujo 01
Reporte Semanal HTML
Email automatico cada lunes 8am al equipo directivo con resumen del pipeline de ventas y KPIs.
⏰ Lunes 08:00
Flujo 02
Reglas Inbox Enterprise
Regla automatica que mueve emails de clientes @enterprise-*.com a carpeta dedicada y los marca como leidos.
📁 Auto-organizar
Flujo 03
Webhook Demo Requests
Suscripcion en tiempo real para detectar "Demo Request" o "Trial" en sales@growthsaas.io y notificar al equipo.
⚡ Tiempo real
Arquitectura del Modulo
Azure AD
App Registration
📈
Graph API v1.0
Mail.ReadWrite + Send
📧
Outlook 365
growthsaas.io
Azure Function
Timer + HTTP trigger
💬
Slack / CRM
Notificaciones leads
🔑

Autenticacion y Setup

auth.ts
📄 src/auth.ts TypeScript
// GrowthSaaS — Microsoft Graph API Authentication
// Permisos requeridos: Mail.ReadWrite, Mail.Send, MailboxSettings.ReadWrite

import { ClientSecretCredential } from '@azure/identity';
import { Client } from '@microsoft/microsoft-graph-client';
import { TokenCredentialAuthenticationProvider } from
  '@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials';

export const createGraphClient = (): Client => {
  const credential = new ClientSecretCredential(
    process.env.AZURE_TENANT_ID!,   // a1b2c3d4-e5f6-7890-abcd-ef1234567890
    process.env.AZURE_CLIENT_ID!,    // b2c3d4e5-f6a7-8901-bcde-f12345678901
    process.env.AZURE_CLIENT_SECRET!
  );

  const authProvider = new TokenCredentialAuthenticationProvider(credential, {
    scopes: ['https://graph.microsoft.com/.default'],
  });

  return Client.initWithMiddleware({ authProvider });
};

export const SALES_MAILBOX = 'sales@growthsaas.io';
export const OPS_MAILBOX   = 'maria.lopez@growthsaas.io';
📈

Flujo 1 — Reporte Semanal HTML

weeklyReport.ts
📄 src/weeklyReport.ts TypeScript
import { createGraphClient, OPS_MAILBOX } from './auth';

interface PipelineData {
  activeLeads: number;
  demoPhase: number;
  openRevenue: string;
  conversionRate: string;
  weekLabel: string;
}

const buildHTMLReport = (data: PipelineData): string => `
<!DOCTYPE html>
<html><body style="font-family:Segoe UI,sans-serif;max-width:600px;margin:0 auto;padding:24px;">
  <div style="background:#0078d4;color:#fff;padding:20px 24px;border-radius:8px 8px 0 0;">
    <h2 style="margin:0;font-size:20px;">Pipeline Report — ${data.weekLabel}</h2>
    <p style="margin:6px 0 0;opacity:0.8;font-size:13px;">GrowthSaaS Revenue Operations</p>
  </div>
  <table style="width:100%;border-collapse:collapse;border:1px solid #e2e8f0;">
    <tr style="background:#f7fafc;">
      <th style="padding:12px 16px;text-align:left;font-size:13px;color:#718096;">Metrica</th>
      <th style="padding:12px 16px;text-align:right;font-size:13px;color:#718096;">Valor</th>
      <th style="padding:12px 16px;text-align:right;font-size:13px;color:#718096;">vs semana anterior</th>
    </tr>
    <tr><td style="padding:12px 16px;border-top:1px solid #e2e8f0;">Leads activos</td>
        <td style="padding:12px 16px;border-top:1px solid #e2e8f0;text-align:right;font-weight:700;">${data.activeLeads}</td>
        <td style="padding:12px 16px;border-top:1px solid #e2e8f0;text-align:right;color:#38a169;">+5 ↑</td></tr>
    <tr style="background:#f7fafc;">
        <td style="padding:12px 16px;border-top:1px solid #e2e8f0;">En fase Demo</td>
        <td style="padding:12px 16px;border-top:1px solid #e2e8f0;text-align:right;font-weight:700;">${data.demoPhase}</td>
        <td style="padding:12px 16px;border-top:1px solid #e2e8f0;text-align:right;color:#38a169;">+3 ↑</td></tr>
    <tr><td style="padding:12px 16px;border-top:1px solid #e2e8f0;">Oportunidades abiertas</td>
        <td style="padding:12px 16px;border-top:1px solid #e2e8f0;text-align:right;font-weight:700;">${data.openRevenue}</td>
        <td style="padding:12px 16px;border-top:1px solid #e2e8f0;text-align:right;color:#38a169;">+12% ↑</td></tr>
    <tr style="background:#f7fafc;">
        <td style="padding:12px 16px;border-top:1px solid #e2e8f0;">Tasa de conversion</td>
        <td style="padding:12px 16px;border-top:1px solid #e2e8f0;text-align:right;font-weight:700;">${data.conversionRate}</td>
        <td style="padding:12px 16px;border-top:1px solid #e2e8f0;text-align:right;color:#e53e3e;">-1.2% ↓</td></tr>
  </table>
  <p style="font-size:12px;color:#a0aec0;margin-top:16px;">Generado automaticamente por GrowthSaaS RevOps Bot | Microsoft Graph API</p>
</body></html>
`;

export const sendWeeklyPipelineReport = async (): Promise<void> => {
  const graph = createGraphClient();

  const pipeline: PipelineData = {
    activeLeads:      47,
    demoPhase:        12,
    openRevenue:      '$380,000',
    conversionRate:   '23%',
    weekLabel:        'Semana 24 / 2026',
  };

  await graph.api(`/users/${OPS_MAILBOX}/sendMail`)
    .post({
      message: {
        subject:     `Pipeline Report S24 — ${pipeline.activeLeads} leads activos | $${pipeline.openRevenue}`,
        body: {
          contentType: 'HTML',
          content:     buildHTMLReport(pipeline),
        },
        toRecipients: [
          { emailAddress: { address: 'ceo@growthsaas.io',    name: 'Carlos Vega (CEO)' }},
          { emailAddress: { address: 'cfo@growthsaas.io',    name: 'Ana Ruiz (CFO)'    }},
          { emailAddress: { address: 'vpsales@growthsaas.io', name: 'Pedro Saiz (VP Sales)'}},
        ],
        importance:    'normal',
      },
      saveToSentItems: true,
    });

  console.log('[WeeklyReport] Reporte S24 enviado a 3 directivos');
};
PREVIEW DEL EMAIL GENERADO
📁

Flujo 2 — Reglas Inbox Enterprise

inboxRules.ts
📄 src/inboxRules.ts TypeScript
import { createGraphClient, SALES_MAILBOX } from './auth';

export const setupEnterpriseInboxRule = async (): Promise<void> => {
  const graph = createGraphClient();

  // 1. Crear carpeta "Enterprise Accounts" si no existe
  const folders = await graph.api(`/users/${SALES_MAILBOX}/mailFolders`)
    .select('id,displayName').get();

  let enterpriseFolderId: string;
  const existing = folders.value.find((f: any) => f.displayName === 'Enterprise Accounts');

  if (existing) {
    enterpriseFolderId = existing.id;
    console.log('[InboxRules] Carpeta ya existe, id:', enterpriseFolderId);
  } else {
    const newFolder = await graph.api(`/users/${SALES_MAILBOX}/mailFolders`)
      .post({ displayName: 'Enterprise Accounts' });
    enterpriseFolderId = newFolder.id;
    console.log('[InboxRules] Carpeta creada, id:', enterpriseFolderId);
  }

  // 2. Crear regla de bandeja: mover emails de @enterprise-*.com
  const rule = await graph
    .api(`/users/${SALES_MAILBOX}/mailFolders/inbox/messageRules`)
    .post({
      displayName: 'Auto-organizar clientes Enterprise',
      sequence:    1,
      isEnabled:   true,
      conditions: {
        senderContains: ['@enterprise-corp.com', '@enterprise-tech.com', '@enterprise-global.com'],
      },
      actions: {
        moveToFolder: enterpriseFolderId,
        markAsRead:   true,
      },
    });

  console.log(`[InboxRules] Regla '${rule.displayName}' creada (id: ${rule.id})`);
};

Flujo 3 — Webhook Demo Requests

webhook.ts
📄 src/webhook.ts TypeScript
import { createGraphClient, SALES_MAILBOX } from './auth';
import { IncomingMessage, ServerResponse } from 'http';

// Webhook URL expuesto por la Azure Function
const WEBHOOK_URL = 'https://growthsaas-functions.azurewebsites.net/api/email-webhook';
const CLIENT_STATE = process.env.WEBHOOK_SECRET!;

// 1. Crear suscripcion al inbox de ventas
export const createDemoRequestSubscription = async (): Promise<string> => {
  const graph = createGraphClient();

  const expiresAt = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString();

  const sub = await graph.api('/subscriptions').post({
    changeType:          'created',
    notificationUrl:     WEBHOOK_URL,
    resource:            `/users/${SALES_MAILBOX}/mailFolders/inbox/messages`,
    expirationDateTime:  expiresAt,
    clientState:         CLIENT_STATE,
  });

  console.log(`[Webhook] Suscripcion creada. ID: ${sub.id}. Expira: ${sub.expirationDateTime}`);
  return sub.id;
};

// 2. Handler del webhook en Azure Function
export const emailWebhookHandler = async (req: IncomingMessage, res: ServerResponse) => {
  // Validacion inicial de suscripcion
  const { validationToken } = req.query as any;
  if (validationToken) {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    return res.end(validationToken);
  }

  const body = req.body; // JSON parseado por Azure Functions runtime

  for (const notification of body.value) {
    // Verificar clientState para seguridad
    if (notification.clientState !== CLIENT_STATE) continue;

    const graph = createGraphClient();
    const msgId  = notification.resourceData.id;

    const msg = await graph
      .api(`/users/${SALES_MAILBOX}/messages/${msgId}`)
      .select('subject,from,receivedDateTime,bodyPreview').get();

    // Filtrar por asunto: Demo Request o Trial
    const subject = (msg.subject || '').toLowerCase();
    if (subject.includes('demo request') || subject.includes('trial')) {
      await notifySlack({
        from:     msg.from.emailAddress.address,
        subject: msg.subject,
        preview: msg.bodyPreview,
      });
    }
  }

  res.writeHead(202);
  res.end();
};

// 3. Notificacion a Slack
const notifySlack = async (lead: { from: string; subject: string; preview: string }) => {
  await fetch(process.env.SLACK_WEBHOOK_URL!, {
    method: 'POST',
    body: JSON.stringify({
      text: `🚀 *Nuevo lead caliente* en sales@growthsaas.io\n> *De:* ${lead.from}\n> *Asunto:* ${lead.subject}\n> _${lead.preview}_`,
    }),
  });
  console.log(`[Webhook] Lead notificado en Slack: ${lead.from}`);
};

Buenas Practicas Aplicadas

🔍
$select en cada queryLos datos de buzon son pesados — solo se piden los campos necesarios (subject, from, receivedDateTime, bodyPreview).
Webhook con auto-renovacionLas suscripciones expiran en 3 dias — se programa un timer de Azure para renovarlas antes del vencimiento.
📜
HTML con CSS inlineEl reporte semanal usa estilos inline — los clientes de email eliminan bloques <style> externos.
🔒
clientState verificadoEl handler del webhook valida el clientState antes de procesar cada notificacion para evitar spoofing.
📋
saveToSentItems: trueTodos los envios automaticos guardan copia en Enviados para auditoria y trazabilidad.
Service account dedicadoLos flows usan maria.lopez@growthsaas.io como service account — nunca el buzon personal del CEO o VP Sales.
📈
Rate limits respetadosMaximo 10,000 requests/10min por app/tenant — con batch requests para operaciones masivas (mark-read, move).
📃
$search vs $filterBusqueda full-text con $search (OData), filtros estructurados con $filter — sintaxis diferente, no intercambiables.