Un Email es un único mensaje entrante recibido a través de AWS SES, registrado en Postgres y vinculado a un inbox. El .eml original se conserva en S3 y es accesible mediante la API.
Propósito
- Persistir todo mensaje que ingresa a la plataforma, incluyendo los rechazados por política.
- Llevar el estado de procesamiento (
received → approved | rejected → processing → processed | failed). - Servir como fila de unión para agent runs, system events y webhook deliveries.
Importante: sin escrituras directas
Los correos nunca se modifican vía la API pública, excepto mediante la acción explícita reprocess. Los escriben el handler de ingesta de SES y el pipeline del worker; la única forma de crear uno mediante la API es POST /emails, que envía un .eml en bruto a ese mismo pipeline (metadata.source queda en api). DELETE /emails/{id} hace soft-delete.
Schema
| Campo | Tipo | Descripción |
|---|---|---|
id | UUID | ID interno. |
workspace_id | UUID | Workspace propietario. |
inbox_id | UUID | Inbox que coincidió. |
agent_id | UUID | null | Agente asociado al inbox en el momento del procesamiento. |
message_id | string | messageId de SES. Único — garantiza idempotencia. |
raw_s3_key | string | Clave del .eml original en el bucket raw. |
subject | string | null | Subject decodificado. Rellenado por el worker tras el parseo. |
from | string | Remitente del envelope. |
to | string[] | Destinatarios del payload de SES. |
cc | string[] | Rellenado por el worker. |
bcc | string[] | Rellenado por el worker (rara vez con datos — el Bcc se elimina en tránsito). |
status | enum | Ver ciclo de vida. |
rejection_reason | enum | null | Establecido cuando status=rejected. |
spam_verdict | string | null | De SES (PASS, FAIL, GRAY, etc.). |
virus_verdict | string | null | De SES. |
aws_ses_metadata | object | SPF/DKIM/DMARC crudos + info de recepción de SES. |
custom_metadata | object | Formato libre, establecido por los clientes. |
agent_metadata | object | Salida escrita por el agent run (campos extraídos, tags, etc.). |
metadata | object | Metadata del sistema. |
sent_at | datetime | null | Del header Date del mensaje. |
received_at | datetime | Cuándo lo recibió SES. |
processed_at | datetime | null | Cuándo terminó el agente. |
created_at / updated_at / deleted_at | datetime |
Estado
| Estado | Significado |
|---|---|
received | El handler de ingesta escribió la fila; el worker aún no se ha ejecutado. |
approved | El worker parseó el .eml, coincidió con filtros y destinatarios permitidos. En cola para el agente. |
rejected | El worker rechazó el correo. rejection_reason está establecido. El agente no se ejecuta. |
processing | El agent run se inició. |
processed | El agente terminó exitosamente. |
failed | El agent run falló. Se permite reprocess manual. |
Razones de rechazo
| Razón | Significado |
|---|---|
recipient_unknown | Ningún Inbox coincidió. |
recipients | allowed_recipients rechazó la dirección to. |
attachments | Se excedió la cantidad, el tamaño por archivo o el tamaño total de adjuntos. |
filters | Al menos una regla de filtro falló. |
encoding | El .eml no pudo parsearse. |
spam | SES spamVerdict=FAIL (y la política del inbox trata FAIL como bloqueo). |
virus | SES virusVerdict=FAIL. |
sender_rate_limit | El remitente excedió el tope por inbox. |
Ejemplo
{
"id": "e5f6a7b8-9c0d-4e1f-2a3b-4c5d6e7f8a9b",
"workspace_id": "a8b2c1d4-9e7f-4a3b-8c5d-1f2e3a4b5c6d",
"inbox_id": "b3c4d5e6-7a89-4b0c-9d1e-2f3a4b5c6d7e",
"agent_id": "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f",
"message_id": "01HZ9X8K4N0P2Q5R7S9T1V3W5X",
"raw_s3_key": "incoming/01HZ9X8K4N0P2Q5R7S9T1V3W5X.eml",
"subject": "Invoice 4123",
"from": "billing@acme.com",
"to": ["triage-3f4a@inbox.kobana.com.br"],
"cc": [],
"bcc": [],
"status": "processed",
"rejection_reason": null,
"spam_verdict": "PASS",
"virus_verdict": "PASS",
"aws_ses_metadata": {
"spfVerdict": { "status": "PASS" },
"dkimVerdict": { "status": "PASS" },
"dmarcVerdict": { "status": "PASS" }
},
"custom_metadata": {},
"agent_metadata": {
"type": "invoice",
"amount": 4123.5,
"due_date": "2026-06-01"
},
"metadata": {},
"sent_at": "2026-05-17T10:00:00.000Z",
"received_at": "2026-05-17T10:00:14.123Z",
"processed_at": "2026-05-17T10:00:18.927Z",
"created_at": "2026-05-17T10:00:14.123Z",
"updated_at": "2026-05-17T10:00:18.927Z",
"deleted_at": null
}
Descargando el .eml
GET /v1/emails/{id}/eml devuelve un redirect 302 a una URL presigned de S3 de corta duración. La URL es válida por 15 minutos y sirve el mensaje original con headers, partes MIME y adjuntos preservados.
Descargando el .eml desde un webhook
Todo payload de webhook de evento email.* también trae un data.email.download_url listo para usar — un enlace que quien recibe el webhook puede seguir directamente, sin ninguna de las credenciales anteriores:
https://app.inbox.kobana.com.br/eml/{id}/{token}
- No forma parte de
/v1y no se sirve enapi.inbox.*: ese host exige certificado de cliente mTLS en cada request, y quien recibe el webhook no tiene forma de presentarlo. Este enlace vive en el host de la app (sin mTLS). {token}es una credencial emitida una sola vez, de corta duración (30 minutos), generada específicamente para esa entrega — no es tu token de API, y da acceso solo a ese correo.- Seguirlo redirige (
302) directo a una URL presigned de S3 válida por 5 minutos — el mismo archivo que sirveGET /v1/emails/{id}/eml, solo que alcanzado sin mTLS/JWT. - Pasados los 30 minutos el enlace deja de funcionar; vuelve a obtener el correo (
GET /v1/emails/{id}, mTLS+JWT) y descárgalo por la vía habitual.
Reprocesamiento
POST /v1/emails/{id}/reprocess vuelve a encolar el correo para el agente. Solo son elegibles los correos con status=approved, processed o failed — los correos aún en received, processing o rejected devuelven 422 unprocessable_entity.