Endpoints /api/correo/peticiones · Cola de Encargos de Correo (F2)
Descripción
Conjunto de endpoints que gestionan la cola de peticiones (encargos) que un miembro encola para que la secretaria de correo atienda. Son simples, CRUD sin lógica multiturno.
- Navegador crea peticiones (
POST, CF Access). - Navegador lista su cola (
GET, CF Access). - Pasada desatendida de
/correolas atiende (PUT, Bearer token). - Miembro descarta una suya (
DELETE, CF Access o Bearer).
Autenticación
Dos canales de identidad escopo estricto:
CF Access (Navegador)
resolveTeamMember(request) — extrae la identidad CF Access. Usa X-Forwarded-For o headers CF típicos.
Bearer Token (Pasada Desatendida)
memberFromBearer(request, env) — extrae el token del header Authorization: Bearer <token> y lo mapea a un miembro:
const auth = request.headers.get('Authorization');
if (!auth?.startsWith('Bearer ')) return null;
const token = auth.slice(7);
for (const pair of (env.MCP_TOKENS || '').split(',').filter(Boolean)) {
const [name, t] = pair.split(':');
if (t && t === token) {
const cap = name.trim().charAt(0).toUpperCase() + name.trim().slice(1).toLowerCase();
return isTeamMember(cap) ? cap : null;
}
}
Env var MCP_TOKENS = edu:token123,dani:token456,... (pares de nombre:token).
Resolución Unificada
function resolveMember(request: Request, env: Env): TeamMember | null {
return resolveTeamMember(request) ?? memberFromBearer(request, env);
}
Intenta CF Access primero, luego Bearer. Si ninguno, 403 Forbidden.
GET /api/correo/peticiones
Lista las peticiones del miembro autenticado.
Query params (opcionales):
estado: filtro ('pendiente'|'atendida'|'descartada'). Sin este parámetro, devuelve todas.limit: máx filas (default 20, capped a 50).
Ejemplo:
GET /api/correo/peticiones?estado=pendiente&limit=10
Respuesta (200):
[
{
"id": 1,
"owner": "Edu",
"texto": "Respóndele a Hetzner que confirmen la entrega",
"estado": "pendiente",
"respuesta": null,
"compose_url": null,
"created_at": "2026-08-12 14:30:00",
"atendida_at": null
},
{
"id": 2,
"owner": "Edu",
"texto": "Aviso a los clientes sobre la caída",
"estado": "atendida",
"respuesta": "Estimados, la infraestructura fue afectada...",
"compose_url": "https://mail.zoho.com/...#compose&to=...",
"created_at": "2026-08-12 13:15:00",
"atendida_at": "2026-08-12 14:28:00"
}
]
POST /api/correo/peticiones
Crea una petición nueva. Solo desde navegador (CF Access).
Body:
{
"texto": "Respóndele a X que Z" // ≤1000 chars, obligatorio
}
Validaciones:
textorequerido y no vacío.texto.length ≤ 1000.
Respuesta (201 Created):
{
"id": 3,
"owner": "Edu",
"texto": "Respóndele a X que Z",
"estado": "pendiente",
"respuesta": null,
"compose_url": null,
"created_at": "2026-08-12 14:35:00",
"atendida_at": null
}
Errores:
403 Forbidden: no autenticado.400 Bad Request: body JSON inválido otextovacío.413 Payload Too Large:texto> 1000 chars.
PUT /api/correo/peticiones/:id
Atiende una petición: marca como atendida, añade respuesta y deep-link a Zoho Compose.
Body (todos opcionales, parcial):
{
"estado": "pendiente|atendida|descartada", // estado nuevo
"respuesta": "el borrador que la secretaria generó",
"compose_url": "https://mail.zoho.com/..."
}
Validaciones:
estado, si presente, debe ser uno de['pendiente', 'atendida', 'descartada'].respuesta, si presente, ≤4000 chars.compose_url, si presente, debe comenzar conhttps://mail.zoho..
Lógica:
- Solo el propietario de la petición (verificar
existing.owner == member) puede actualizar. - Si
estado == 'atendida', estableceatendida_at = now(). - Usa
COALESCEpara parciales: solo los campos presentes se actualizan, el resto quedan intactos.
Respuesta (200):
{
"id": 2,
"owner": "Edu",
"texto": "Aviso a los clientes sobre la caída",
"estado": "atendida",
"respuesta": "Estimados, se produjo un corte...",
"compose_url": "https://mail.zoho.com/...",
"created_at": "2026-08-12 13:15:00",
"atendida_at": "2026-08-12 14:28:00"
}
Errores:
404 Not Found: petición no existe.403 Forbidden: petición es de otro miembro.400 Bad Request: campos inválidos (estado fuera de enum, respuesta muy larga, etc.).
DELETE /api/correo/peticiones/:id
Descarta una petición (elimina permanentemente).
Validación:
- Solo el propietario puede eliminar.
Respuesta (204 No Content).
Errores:
404 Not Found: petición no existe.403 Forbidden: petición es de otro miembro.
Flujo de Integración (F2)
Paso 1: Navegador Crea
const response = await fetch('/api/correo/peticiones', {
method: 'POST',
body: JSON.stringify({ texto: "Respóndele a Hetzner que confirmen..." })
});
→ Crea fila con estado='pendiente' en correo_peticiones.
Paso 2: Pasada Desatendida Atiende
La pasada de /correo (parte de Paso 7 de claude-method) corre periódicamente y:
- Autentica con
Bearer {miembro_token}(delMCP_TOKENS). - Llama
GET /api/correo/peticiones?estado=pendiente&limit=5→ obtiene hasta 5 peticiones pendientes. - Para cada petición, genera un borrador de correo (en claude-method, Paso 7).
- Produce un
compose_urlde Zoho Compose con el borrador pre-rellenado. - Llama
PUT /api/correo/peticiones/:idcon{"estado":"atendida", "respuesta":"...", "compose_url":"..."}.
Paso 3: Navegador ve el Resultado
El miembro abre /correo, ve la petición ahora con estado='atendida' + botón compose_url para ir a Zoho y enviar manualmente.
Tabla Underlying (correo_peticiones)
Ver [[entity—correo—table—peticiones]] para schema e índices.
Diseño Aplazado
- Envío directo: toggle F3 que permite que la pasada enviara el correo directamente en lugar de dejar solo el borrador.
- Hoy: 100% manual — miembro recibe el borrador y hace clic en el deep-link a Zoho para revisar y enviar.
Véase también
- [[feature—workspace—correo-menu-f0-f1-f2]]
- [[entity—correo—table—peticiones]]
- [[entity—correo—endpoint—ask]]
- [[concept—saas—multi-tenancy]]
- [[decision—20260403—multi-tenancy-rls]]