Um Email é uma mensagem individual recebida via AWS SES, registrada no Postgres e vinculada a uma inbox. O .eml original é preservado no S3 e acessível pela API.
Propósito
- Persistir toda mensagem que entra na plataforma, inclusive as rejeitadas por política.
- Carregar o estado de processamento (
received → approved | rejected → processing → processed | failed). - Servir de pivot para agent runs, system events e webhook deliveries.
Importante: sem escrita direta
E-mails nunca são modificados via API pública, exceto pela ação explícita de reprocess. São escritos pelo handler de ingestão SES e pelo pipeline de worker; a única forma de criar um pela API é POST /emails, que envia um .eml bruto para esse mesmo pipeline (metadata.source fica api). DELETE /emails/{id} faz soft-delete.
Schema
| Campo | Tipo | Descrição |
|---|---|---|
id | UUID | ID interno. |
workspace_id | UUID | Workspace dono. |
inbox_id | UUID | Inbox que casou. |
agent_id | UUID | null | Agent associado à inbox no momento do processamento. |
message_id | string | messageId do SES. Unique — garante idempotência. |
raw_s3_key | string | Chave do .eml original no bucket raw. |
subject | string | null | Subject decodificado. Preenchido pelo worker após parse. |
from | string | Sender do envelope. |
to | string[] | Destinatários do payload SES. |
cc | string[] | Preenchido pelo worker. |
bcc | string[] | Preenchido pelo worker (raramente populado — Bcc é removido em trânsito). |
status | enum | Veja o ciclo de vida. |
rejection_reason | enum | null | Definido quando status=rejected. |
spam_verdict | string | null | Do SES (PASS, FAIL, GRAY, etc.). |
virus_verdict | string | null | Do SES. |
aws_ses_metadata | object | SPF/DKIM/DMARC + receipt info crus do SES. |
custom_metadata | object | Free-form, setado por clientes. |
agent_metadata | object | Output escrito pelo agent run (campos extraídos, tags, etc.). |
metadata | object | Metadata do sistema. |
sent_at | datetime | null | Do header Date da mensagem. |
received_at | datetime | Quando o SES recebeu. |
processed_at | datetime | null | Quando o agente terminou. |
created_at / updated_at / deleted_at | datetime |
Status
| Status | Significado |
|---|---|
received | Handler de ingestão gravou a linha; worker ainda não rodou. |
approved | Worker fez parse do .eml, casou filtros e destinatários permitidos. Enfileirado pro agente. |
rejected | Worker rejeitou. rejection_reason setado. Agente não roda. |
processing | Agent run começou. |
processed | Agente terminou com sucesso. |
failed | Agent run falhou. reprocess manual permitido. |
Razões de rejeição
| Razão | Significado |
|---|---|
recipient_unknown | Nenhuma Inbox casou. |
recipients | allowed_recipients rejeitou o endereço to. |
attachments | Quantidade, tamanho-por-arquivo ou total de anexos excedidos. |
filters | Pelo menos uma regra de filtro falhou. |
encoding | .eml não pôde ser parseado. |
spam | SES spamVerdict=FAIL (e política da inbox bloqueia FAIL). |
virus | SES virusVerdict=FAIL. |
sender_rate_limit | Sender excedeu cap por-inbox. |
Exemplo
{
"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
}
Baixando o .eml
GET /v1/emails/{id}/eml retorna um redirect 302 para uma URL S3 presigned de curta duração. A URL é válida por 15 minutos e serve a mensagem original com headers, partes MIME e anexos preservados.
Baixando o .eml a partir de um webhook
Todo payload de webhook de evento email.* também traz um data.email.download_url pronto pra uso — um link que quem recebe o webhook pode seguir direto, sem nenhuma das credenciais acima:
https://app.inbox.kobana.com.br/eml/{id}/{token}
- Ele não faz parte do
/v1e não é servido emapi.inbox.*: aquele host exige certificado de cliente mTLS em toda requisição, e quem recebe o webhook não tem como apresentar um. Esse link vive no host do app (sem mTLS). {token}é uma credencial emitida uma única vez, de curta duração (30 minutos), gerada especificamente para aquela entrega — não é o seu token de API, e dá acesso só àquele e-mail.- Seguir o link redireciona (
302) direto para uma URL presigned do S3 válida por 5 minutos — o mesmo arquivo queGET /v1/emails/{id}/emlserve, só que alcançado sem mTLS/JWT. - Depois de 30 minutos o link para de funcionar; busque o e-mail de novo (
GET /v1/emails/{id}, mTLS+JWT) e baixe pelo caminho normal.
Reprocessamento
POST /v1/emails/{id}/reprocess reenfileira o e-mail para o agente. Só são elegíveis e-mails com status=approved, processed ou failed — qualquer outro status retorna 422 unprocessable_entity.