Catalisa.
Building blocks/ComunicaçãoBeta

Email

A caixa de entrada do seu usuário, Gmail ou Outlook, atrás de uma única API

32
Endpoints
5
Entidades
2
Provedores
Tenant
Escopo
3026
Porta

O seu produto passa a ler, organizar e responder e-mail dentro da conta do próprio cliente — Gmail ou Outlook — sem que o seu time precise aprender OAuth do Google, Microsoft Graph, Pub/Sub e renovação de assinatura de push.

Para quem é
  • CRMs e plataformas de vendas que precisam registrar a conversa por e-mail junto do negócio
  • Operações de crédito e cobrança que recebem documento e resposta de cliente por e-mail
  • Times de atendimento que querem responder do endereço do próprio atendente, não de um no-reply
Substitui
  • Assinatura de uma API unificada de e-mail (Nylas, Unipile, Aurinko)
  • Integração própria e direta com Gmail API e Microsoft Graph, com a manutenção que vem junto
  • Rotina caseira de renovação de token OAuth e de assinatura de push notification
O que não é
  • Serviço de disparo transacional em massa (isso é SendGrid, Amazon SES, Resend — ver §5)
  • Servidor de e-mail, caixa postal hospedada ou domínio de envio
  • Ferramenta de marketing com métrica de abertura, clique e descadastro

01Resumo executivo

O Email conecta a conta de e-mail do usuário final — a dele mesmo, no Gmail ou no Outlook — ao seu produto. Depois de conectada, o seu sistema lê a caixa, procura mensagem, abre a conversa inteira, baixa anexo, marca como lida, move de pasta e responde do endereço da pessoa, com uma única API. Não é um serviço de disparo: é acesso à caixa de entrada que já existe.

Na prática, é a diferença entre o vendedor ter que copiar e colar o e-mail do cliente no CRM e o CRM já mostrar a conversa completa ao lado da proposta — sem que ninguém do seu time precise aprender que o watch do Gmail morre em sete dias e que o Microsoft Graph limita quatro requisições concorrentes por caixa.

Está em beta desde maio de 2026. Roda em monolito e em standalone, tem cobertura de testes unitários e de integração, e ainda não tem cliente em produção. A restrição relevante para planejamento é de escopo do Google, não de código: por padrão a conexão Gmail nasce somente com permissão de envio, porque leitura de caixa no Gmail é escopo restrito e exige avaliação de segurança anual — o detalhe completo está em §5 e §15. No Outlook a leitura funciona por padrão.

AtributoValor
Identificadoremail
CategoriaComunicação
EscopoTenant (exige organizationId no token)
Porta (standalone)3026 no host, 3000 dentro do contêiner
Path alias@email
Prefixo HTTP/email
StatusBeta desde 2026-05
Depende dePostgreSQL (schema email), IAM, Google e/ou Microsoft

02O problemanegócio

O cenário. Um produto B2B quer mostrar, dentro da própria tela, a conversa por e-mail que já acontece fora dele. O vendedor negocia por e-mail. O analista de crédito recebe o comprovante por e-mail. O cobrador combina o pagamento por e-mail. Nada disso entra no sistema, e o sistema fica sabendo da operação sempre por último.

O que trava hoje.

  • São dois mundos, não um. O Gmail organiza por label e conversa; o Outlook organiza por pasta e conversationId. Threading, paginação, busca e anexo funcionam de formas diferentes. Você escreve a integração duas vezes e normaliza os dois modelos por conta própria.
  • O push expira, e em ritmos diferentes. O users.watch do Gmail precisa ser chamado pelo menos a cada sete dias, e a própria Google recomenda chamá-lo uma vez por dia para não perder notificação (Gmail API — Push Notifications). No Microsoft Graph, a assinatura de mensagem de caixa expira em no máximo 10.080 minutos, e cai para 1.440 minutos se você pedir os dados do recurso junto (Graph — subscription). São dois cron jobs, com dois relógios, e reconciliação por conta quando um deles falha.
  • Push não dispensa polling. A documentação do Gmail admite que, em situações extremas, notificações podem atrasar ou ser descartadas, e recomenda um history.list periódico como rede de segurança. Ou seja: você constrói os dois caminhos, não um.
  • Os limites de quota moldam a arquitetura. O Gmail dá 6.000 unidades de quota por minuto por usuário, e um messages.get custa 20 unidades (Gmail API — Usage limits). São 300 mensagens por minuto por caixa: carregar um histórico de 50 mil mensagens leva quase três horas, só de limite. O Graph impõe 10.000 requisições por 10 minutos e apenas 4 concorrentes por combinação de aplicativo e caixa (Graph — throttling limits), o que impede paralelizar a carga dentro da mesma caixa.
  • Ler caixa no Gmail é um processo, não uma configuração. Os escopos de leitura do Gmail são restritos e exigem uma avaliação de segurança independente, renovada a cada doze meses, feita por um laboratório credenciado do CASA (Google — Restricted scope verification, App Defense Alliance — CASA). Google e App Defense Alliance não publicam o custo dessa avaliação.
  • Do lado da Microsoft, sem publisher verification a adoção corporativa quebra. Aplicativos multitenant registrados depois de 08/11/2020 que pedem mais que login e leitura de perfil aparecem como editor não verificado, e o usuário corporativo simplesmente não consegue consentir (Entra — Publisher verification). A verificação em si não é cobrada pela Microsoft.

O custo de não resolver. Os cinco fatos acima são oficiais e verificáveis, e já bastam: dois relógios de renovação, dois modelos de dados, dois regimes de quota, uma avaliação anual paga e um processo de verificação de editor. Estimativas de esforço para construir Gmail e Outlook do zero circulam na faixa de 12 a 18 dev-months e US$ 240 mil a US$ 480 mil de custo inicial, com 0,5 a 1 FTE permanente de manutenção — números publicados pela Unipile, que vende a alternativa, e por isso citados aqui como material de fornecedor, não como referência neutra (Unipile — Build vs Buy, consultado em 2026-08-16).


03Proposta de valornegócio

AntesDepois
Uma integração para o Gmail e outra para o OutlookUm contrato canônico de mensagem, conversa e pasta para os dois
Cron de renovação de watch e outro de subscription, com relógios distintosPOST /provider-subscriptions e um job que renova o que está para vencer
Refresh token expirando em produção às três da tardeO serviço renova sozinho cinco minutos antes de vencer, na própria requisição
Copiar e colar e-mail dentro do CRMO produto lê, responde e arquiva na caixa do usuário
Corpo de e-mail replicado no seu banco, com o problema de LGPD juntoNenhum corpo de mensagem é gravado — a API é passagem

Um modelo canônico, dois provedores. CanonicalEmailMessage traz remetente, destinatários, assunto, corpo em texto e HTML, sinalizadores de lida e favorita, rótulos e anexos com o mesmo formato, venha de onde vier. Até a triagem nativa é normalizada: as abas do Gmail (CATEGORY_PROMOTIONS, CATEGORY_SOCIAL, ...) e o Focused Inbox do Outlook viram um único campo category, então você exibe "Principal" e "Outros" sem inventar classificador.

O token se renova sozinho. Toda operação passa por getProviderForConnection, que checa a validade do access token e o renova se faltarem menos de cinco minutos para expirar. Não existe caminho em que o integrador precise pensar em refresh.

As credenciais OAuth são do cliente. Cada organização cadastra o próprio client_id e client_secret do Google ou da Microsoft. O consentimento do usuário final é dado ao aplicativo do cliente, não a um intermediário — e a tela de consentimento mostra o nome dele.

Nada do conteúdo fica no banco. Assunto, corpo e anexo trafegam do provedor para o chamador e não são persistidos. O que existe no PostgreSQL são credenciais cifradas, o endereço da conta conectada e metadados de assinatura.


04Casos de uso reaisnegócio

Caso 1 — Um CRM passa a mostrar a negociação inteira, e não só o que o vendedor lembrou de anotar Cenário ilustrativo

Contexto. Plataforma de CRM para times comerciais de médio porte, entre 20 e 200 vendedores por cliente. Metade dos clientes usa Google Workspace, metade usa Microsoft 365.

A dor. A negociação acontece no e-mail e o CRM só sabe o que o vendedor anota. Quando o vendedor sai da empresa, o histórico sai junto — está na caixa pessoal dele. O gestor não consegue auditar o que foi prometido ao cliente, e o time de produto ouve o mesmo pedido em toda pesquisa: "puxe meu e-mail para cá".

A solução com o BB. O cliente cadastra as credenciais OAuth em POST /email/api/v1/email/provider-configs. Cada vendedor conecta a própria conta por POST /email/api/v1/email/connections/connect e volta autorizado. O CRM busca a conversa com o contato usando POST /email/api/v1/email/messages/search, com a sintaxe nativa do provedor no campo query, e abre a conversa completa em GET /email/api/v1/email/threads/:id. Responder é POST /email/api/v1/email/messages/:id/reply — a resposta sai do endereço do vendedor, com o In-Reply-To correto, e aparece na caixa "Enviados" dele.

O resultado. O histórico deixa de ser propriedade da caixa de correio individual. E como o corpo não é persistido pelo building block, o CRM decide o que guardar do lado dele — o que muda a conversa com o jurídico do cliente.

Caso 2 — Uma financeira para de perder documento anexado em resposta de cliente Cenário ilustrativo

Contexto. Financeira de crédito com esteira digital. O cliente recebe a solicitação de documento por e-mail e responde anexando o comprovante de renda.

A dor. O anexo chega numa caixa compartilhada e alguém precisa baixar, renomear e subir no sistema. A esteira fica parada esperando um humano fazer download e upload, e o SLA de análise estoura por um motivo que não tem nada a ver com análise.

A solução com o BB. Uma conexão Outlook para a caixa compartilhada. POST /email/api/v1/email/provider-subscriptions cria a assinatura de push no Graph apontando para /me/messages. Quando chega mensagem, o Graph avisa o endpoint público de notificação, o serviço valida o clientState, traduz o evento e entrega MESSAGE_RECEIVED no webhook do cliente. A esteira busca a mensagem, lê os metadados de anexo e baixa o conteúdo em GET /email/api/v1/email/messages/:id/attachments/:attId, que devolve base64 pronto para gravar no File Storage. Depois marca como lida com PATCH /email/api/v1/email/messages/:id.

O resultado. O caminho do anexo até o cofre de documentos vira código. O tempo entre "cliente respondeu" e "documento na esteira" deixa de depender de alguém abrir o Outlook.

Caso 3 — Um time de atendimento responde do endereço da empresa sem sair do sistema Cenário ilustrativo

Contexto. Operação de atendimento de uma seguradora, com uma caixa atendimento@ e doze atendentes.

A dor. Os doze abrem a mesma caixa no navegador. Dois respondem o mesmo cliente. Um marca como lida e some do radar dos outros. Não há como saber quem respondeu o quê, e a auditoria da seguradora pede exatamente isso.

A solução com o BB. O sistema de atendimento passa a ser a única interface: lista com GET /email/api/v1/email/messages, filtra por pasta e por category, atribui internamente, e responde por POST /email/api/v1/email/messages/:id/reply. Cada operação chega ao provedor autenticada pela conexão da caixa, e cada chamada carrega o token do atendente que a disparou — então o registro de quem fez o quê está no Audit Trail, não na memória do time.

O resultado. Uma caixa compartilhada com fila, atribuição e trilha, sem trocar o Outlook por outro produto e sem migrar endereço.

Caso 4 — O mercado já decidiu que essa integração se compra, não se constrói Referência de mercado

Contexto. Existe uma categoria inteira de fornecedores vivendo exclusivamente de intermediar Gmail e Outlook: Nylas, Unipile e Aurinko, com preço público entre US$ 1,00 e € 5,00 por conta conectada por mês (consulta em 2026-08-16). Uma categoria só se sustenta quando o problema é caro o bastante para valer um fornecedor.

A dor do mercado. As restrições que criam essa categoria são documentadas pelos próprios provedores: watch do Gmail expirando em sete dias com recomendação de renovação diária, assinatura do Graph limitada a 10.080 minutos, quota de 6.000 unidades por minuto por usuário no Gmail, teto de 4 requisições concorrentes por caixa no Graph e a avaliação de segurança CASA anual obrigatória para escopos restritos do Gmail. Todas com fonte oficial, listadas em §2.

Como a Catalisa endereça. O building block resolve o mesmo problema com uma diferença de posição: as credenciais OAuth são do cliente, o dado não passa a residir no fornecedor, e a peça encaixa nos outros 31 building blocks em vez de virar mais um contrato para gerenciar.

O resultado. O mesmo trabalho que a categoria vende, sem o intermediário no caminho do dado do seu cliente e sem uma fatura por conta que cresce junto com o seu time de vendas.


05Mercado e diferenciaisnegócio

Panorama. Há duas coisas muito diferentes que as pessoas chamam de "API de e-mail", e confundir as duas é o erro comercial mais caro desta categoria.

A primeira é envio transacional: SendGrid, Amazon SES, Postmark, Resend, Mailgun e Brevo pegam a sua mensagem e a entregam a partir do domínio da sua aplicação, cobrando por milheiro enviado. Servem para recuperação de senha, confirmação de pedido e nota fiscal. Este building block não faz isso e não compete com eles.

A segunda é acesso à caixa de entrada: ler, organizar e responder dentro da conta que o usuário já tem, no Gmail ou no Outlook, com o endereço dele. É onde vivem Nylas, Unipile e Aurinko — e é aqui que o Email da Catalisa está. A alternativa é integrar Gmail API e Microsoft Graph direto, o que não custa licença nenhuma e custa tudo o mais.

CritérioCatalisa EmailNylasUnipileAurinkoGmail + Graph direto
Preço (consulta 2026-08-16)Precificação em definiçãoUS$ 15/mês + US$ 2,00/conta€ 49/mês mínimo, € 5,00/conta (11–50)US$ 1,00–2,00/conta, por volumeSem custo de licença
ProvedoresGmail, Outlook25+, com IMAP e ExchangeGmail, Outlook, IMAP + LinkedIn, WhatsAppGmail, Outlook, Exchange on-prem, ZohoSó os dois, um por vez
Leitura de caixa no GmailRequer escopo restrito e CASA (ver §15)Incluída, CASA é do fornecedorIncluída, CASA é do fornecedorIncluída, CASA é do fornecedorRequer escopo restrito e CASA seus
Corpo da mensagem no banco do fornecedorNão é gravadoSincronizado e armazenadoSincronizado e armazenadoSincronizado e armazenadoNão se aplica
De quem é o client_id OAuthDo cliente, por organizaçãoDo fornecedor ou do clienteDo fornecedorDo clienteDo cliente
Push gerenciadoSim, com job de renovaçãoSimSimSimVocê constrói
Canais além de e-mailCalendar é building block irmãoCalendário, contatos, notetakerLinkedIn, WhatsApp, Instagram, TelegramCalendário, contatos, tarefas, sync de CRM
Multi-tenant com isolamento por organizaçãoNativo, do tokenVocê modelaVocê modelaVocê modelaVocê modela
MaturidadeBeta, sem cliente em produçãoAnos de produção, BAA e HIPAAProdução, empresa mais novaProdução

Nossos diferenciais

  1. O conteúdo não vira cópia. Nenhum corpo, assunto ou anexo é gravado no nosso PostgreSQL — as rotas de mensagem chamam o provedor e devolvem. É difícil de copiar porque não é uma funcionalidade e sim uma decisão de arquitetura: quem já construiu um índice de busca próprio sobre a caixa sincronizada não consegue voltar atrás.
  2. A credencial OAuth é do cliente, por organização. O usuário final consente ao aplicativo do cliente. Isso muda quem responde pela avaliação de segurança do Google, quem aparece na tela de consentimento e de quem é o relacionamento com o provedor.
  3. A mesma anatomia vale para o Calendar. provider-config, connection, provider-subscription, provider-notification e webhook são a mesma estrutura nos dois building blocks. Quem integrou um integra o outro no mesmo dia — e é isso que torna "e-mail e agenda do seu vendedor" uma entrega, não dois projetos.
  4. O tenant vem do token. Nenhuma rota lê organizationId do corpo. Numa plataforma que atende dezenas de empresas, isso é a diferença entre isolamento garantido e isolamento combinado.

Quando escolher o concorrente. Se você precisa de IMAP genérico, Yahoo, iCloud ou Exchange on-premises, este building block não atende: são só Gmail e Outlook, e a Nylas e a Aurinko cobrem esse terreno hoje. Se você quer e-mail, WhatsApp e LinkedIn no mesmo lugar e na mesma fatura, a Unipile entrega isso e nós não. Se o requisito é BAA, HIPAA ou um histórico longo de produção auditável, a Nylas tem o programa de compliance montado e nós estamos em beta. Se você já tem time dedicado, volume que justifica e paciência para a avaliação CASA anual, integrar Gmail API e Microsoft Graph direto é mais barato em licença — custa zero — e dá acesso a cem por cento da superfície de cada provedor. E se o seu problema é disparar cem mil e-mails transacionais por mês do domínio da sua aplicação, nenhum dos citados serve: use Amazon SES, Postmark ou Resend. O Email da Catalisa ganha quando o problema é acessar a caixa do usuário dentro de uma plataforma B2B multi-tenant que já usa outros building blocks — e perde em quase todo o resto.


06Modelo de cobrança e ROInegócio

Unidade de cobrança. A conta de e-mail conectada por mês — a mesma unidade que Nylas, Unipile e Aurinko usam. É justa porque cresce com o valor entregue: uma conta conectada é um vendedor, um analista ou uma caixa compartilhada que passou a operar dentro do seu produto. Quem conectou zero conta não paga por nada.

Precificação em definição. Não há preço fechado para o Email. O que está definido são os drivers.

O que dispara custo

DriverPor quê
Contas conectadas ativasCada uma consome quota do provedor e ocupa uma assinatura de push
Chamadas de API por contaListagem no Gmail hidrata cada mensagem individualmente — é a chamada mais cara do BB
Assinaturas de push ativasCada renovação é uma chamada ao provedor; no Gmail custa 100 unidades de quota
Entregas de webhookCada notificação recebida vira uma ou mais entregas HTTP com registro em log

Comparação de custo — cenário: operação com 200 contas conectadas, uso contínuo.

Catalisa EmailNylas (Full Platform)UnipileAurinkoGmail + Graph direto
BasePrecificação em definiçãoUS$ 15/mês, 5 contas inclusas€ 49/mês mínimoSem base publicadaUS$ 0
Conta adicionalUS$ 2,00/conta/mês€ 5,00/conta/mês (faixa 11–50)US$ 1,00 a US$ 2,00/conta/mêsUS$ 0
Mensal em 200 contas≈ US$ 405Faixa acima de 50 contas não publicada≈ US$ 200 a US$ 400US$ 0 de licença
Avaliação CASA anualDo cliente, se usar leitura no GmailDo fornecedorDo fornecedorDo fornecedorSua, anual e obrigatória
Engenharia de integraçãoUma APIUma APIUma APIUma APIDois provedores, permanente

Estimativa a partir dos preços públicos consultados em 2026-08-16 nas páginas oficiais de Nylas, Unipile e Aurinko. Preço de fornecedor muda sem aviso e admite negociação por volume — confirme na data da sua análise. Nada aqui é proposta comercial.

ROI. A conta não fecha na linha de licença, porque o caminho direto custa zero de licença. Ela fecha em três lugares: (1) a avaliação de segurança CASA anual, obrigatória para ler caixa no Gmail, cujo custo Google e App Defense Alliance não publicam — a faixa de US$ 15 mil a US$ 75 mil que circula é estimativa da Unipile e deve ser tratada como tal; (2) o esforço de construção, estimado pelo mesmo material de fornecedor em 12 a 18 dev-months para Gmail e Outlook em produção; (3) o custo permanente de manutenção, que não some depois do lançamento — são dois relógios de expiração de push, dois regimes de quota e dois processos de verificação que continuam existindo no ano dois. Se a sua plataforma já usa o IAM e o Webhooks Engine, o Email entra como mais um building block, não como um sistema novo para operar.


07Arquitetura

                    HTTP
                      │
  ┌───────────────────┴─────────────────────────────────────────────┐
  │ Hono app  basePath('/email')                                    │
  │                                                                 │
  │  /api/v1/email/provider-configs      providerConfigRouter       │
  │  /api/v1/email/connections           connectionRouter           │
  │  /api/v1/email/messages              messagesRouter             │
  │  /api/v1/email/threads               threadsRouter              │
  │  /api/v1/email/folders               foldersRouter              │
  │  /api/v1/email/webhooks              webhookRouter              │
  │  /api/v1/email/provider-subscriptions  providerSubscriptionRtr  │
  │  /api/v1/email/providers/notifications providerNotificationRtr  │◀── público
  │  /health   /connected                                           │
  └───────────────────┬─────────────────────────────────────────────┘
                      │  Zod parse → ResultAsync<T, AppError>
  ┌───────────────────┴─────────────────────────────────────────────┐
  │ services/                                                       │
  │   EmailProviderConfigService  credenciais OAuth por organização │
  │   EmailConnectionService      OAuth, refresh, fábrica de provider│
  │   EmailService                mensagens, conversas, pastas      │
  │   EmailProviderSubscriptionService  push do provedor            │
  │   EmailWebhookService         assinatura e entrega ao cliente   │
  └───────┬──────────────────────────────────────┬──────────────────┘
          │                                      │
  ┌───────┴────────────────────┐   ┌─────────────┴───────────────────┐
  │ repositories/ (Prisma)     │   │ providers/  EmailProvider       │
  │   PostgreSQL schema "email"│   │   GoogleEmailProvider           │
  │   credenciais e tokens     │   │   OutlookEmailProvider          │
  │   CIFRADOS (AES-256-GCM)   │   └─────────────┬───────────────────┘
  │   NENHUM corpo de mensagem │                 │ HTTPS
  └────────────────────────────┘   ┌─────────────┴───────────────────┐
                                   │ Gmail API  ·  Microsoft Graph   │
                                   └─────────────────────────────────┘

O fluxo de conexão OAuth — é o caminho que todo integrador percorre primeiro.

 Usuário          Seu app            Email BB              Google / Microsoft
   │                 │                   │                          │
   │  "conectar"     │                   │                          │
   ├────────────────▶│                   │                          │
   │                 │ POST /connections/connect                    │
   │                 │  {configId, redirectUrl, scopes?}            │
   │                 ├──────────────────▶│                          │
   │                 │                   │ 1. lê credenciais da org │
   │                 │                   │    e as decifra          │
   │                 │                   │ 2. cifra o `state`:      │
   │                 │                   │    org + user + config   │
   │                 │                   │    + redirectUrl         │
   │                 │  {authorizationUrl}│                         │
   │                 │◀──────────────────┤                          │
   │  redirect       │                   │                          │
   │◀────────────────┤                   │                          │
   │                 │                   │                          │
   │        consentimento na tela do provedor                       │
   ├───────────────────────────────────────────────────────────────▶│
   │                                                                │
   │        GET /connections/callback?code=...&state=...            │
   │◀───────────────────────────────────────────────────────────────┤
   │                 │                   │                          │
   │                 │                   │ 3. decifra o `state`     │
   │                 │                   │ 4. troca code por tokens ├─────▶
   │                 │                   │ 5. lê o e-mail da conta  ├─────▶
   │                 │                   │ 6. grava EmailConnection │
   │                 │                   │    com tokens cifrados   │
   │  302 para o redirectUrl do passo 1  │                          │
   │◀────────────────────────────────────┤                          │

Decisões não óbvias.

  • O state do OAuth é um blob cifrado, não um identificador. Em vez de guardar o contexto da autorização numa tabela e mandar uma chave, o serviço cifra {organizationId, userId, configId, redirectUrl} com AES-256-GCM e manda tudo no state. O callback decifra e sabe quem começou o fluxo. A vantagem é não precisar de estado compartilhado entre réplicas — em standalone com várias instâncias, qualquer uma atende o callback. O custo é que o state não caduca sozinho, e é por isso que ele não carrega nada além do necessário para reconstruir o contexto.
  • O callback é rota pública, e tem que ser. O provedor redireciona o navegador do usuário, sem Authorization. A autenticação é o próprio state cifrado: quem não tem a EMAIL_CREDENTIAL_MASTER_KEY não produz um state válido.
  • Renovação de token dentro da requisição, não em job. getProviderForConnection checa tokenExpiresAt e renova se faltarem menos de 5 minutos. A alternativa — um job varrendo tokens — precisaria acordar mais rápido que o menor tempo de vida de token de qualquer provedor, e ainda assim perderia a corrida. Renovar no caminho quente custa uma chamada extra ocasional e elimina a classe inteira de bug "token venceu entre o job e o uso".
  • Uma listagem no Gmail é N+1, e isso é do provedor. O messages.list do Gmail devolve só identificadores; cada mensagem precisa de um messages.get. Uma página de 25 mensagens são 26 chamadas — 505 unidades de quota. O Graph devolve as mensagens completas na própria listagem. Essa assimetria é a razão de o pageSize ser limitado a 100.
  • Uma mensagem que some não derruba a página inteira. No Gmail, entre listar identificadores e hidratá-los, uma mensagem pode ser apagada. O código captura a falha individual e devolve a página sem ela, registrando um aviso. A alternativa — falhar tudo — travava permanentemente quem paginava histórico antigo, porque o identificador morto continuava lá.
  • O pageToken do Outlook é uma URL inteira. O Graph devolve @odata.nextLink completo, e o provider detecta que o token começa com http e o usa direto. Não tente construir esse cursor à mão.
  • A verificação da notificação do Gmail só liga quando configurada. O Pub/Sub anexa um token OIDC às entregas autenticadas, e o serviço o valida contra o JWKS do Google somente se EMAIL_GOOGLE_PUBSUB_AUDIENCE estiver definida. Sem a variável, a notificação é aceita — modo de desenvolvimento. Em produção, defina a variável.
  • A chave de externalId da assinatura difere por provedor. O Graph devolve um identificador de assinatura próprio. O Gmail não devolve nenhum — cada usuário tem no máximo um watch ativo —, então o serviço usa o endereço de e-mail da conta como chave, que é o que a notificação do Pub/Sub carrega de volta.

Monolito e standalone. Em monolito o app do Email é montado no app principal e responde em localhost:3000/email. Em standalone ele sobe pelo src/email/main.ts, escuta a porta de PORT (3000 dentro do contêiner, publicada em 3026 no host pelo docker-compose.yaml) e alcança o IAM por MODULE_IAM_URL. Nenhuma rota muda de caminho entre os dois modos.


08Conceitos e modelo de dados

Glossário

TermoSignifica
Provider configAs credenciais OAuth de uma organização para um provedor: client_id, client_secret, redirect_uri e, no Outlook, tenant_id. Guardadas cifradas. Uma organização pode ter várias, com uma marcada como padrão.
ConnectionUma conta de e-mail de uma pessoa, já autorizada. Guarda os tokens cifrados e o endereço. É o objeto que toda operação de mensagem exige, pelo connectionId.
Provider subscriptionA assinatura de push no provedor: subscription do Microsoft Graph ou watch do Gmail. Expira e precisa ser renovada.
Webhook subscriptionA assinatura de push do seu lado: para onde o BB entrega o evento já traduzido, com assinatura HMAC. Não confunda com a de cima — são as duas pontas da mesma corrente.
Canonical messageO formato único de mensagem, independente de provedor. Traz corpo em texto e HTML, sinalizadores, rótulos e metadados de anexo.
CategoryTriagem nativa normalizada: primary, other, promotions, social, updates, forums. Vem das abas do Gmail ou do Focused Inbox do Outlook. Ausente quando o provedor não dá sinal.
FolderPasta ou rótulo, normalizado em INBOX, SENT, DRAFTS, TRASH, SPAM, ARCHIVE ou CUSTOM.
ThreadConversa. No Gmail é o threadId nativo; no Outlook é o conversationId, e o provider monta a conversa consultando as mensagens que o compartilham.

Modelo de dados — schema email no PostgreSQL. Cinco modelos.

Modelo PrismaTabelaPropósitoCampos-chave
EmailProviderConfigemail.email_provider_configsCredenciais OAuth da organizaçãocredentials (cifrado), providerType, isDefault, isActive, único (organizationId, name)
EmailConnectionemail.email_connectionsConta autorizada de um usuárioexternalEmail, accessToken e refreshToken (cifrados), tokenExpiresAt, status
EmailProviderSubscriptionemail.email_provider_subscriptionsAssinatura de push no provedorexternalId (único), resource, clientState, expiresAt, isActive
EmailWebhookSubscriptionemail.email_webhook_subscriptionsPara onde entregar o eventocallbackUrl, events[], secret, isActive
EmailWebhookDeliveryLogemail.email_webhook_delivery_logsResultado de cada entregastatus, statusCode, payload, retryCount, nextRetryAt

Enumerações

EnumValores
EmailProviderTypeGOOGLE · OUTLOOK
EmailConnectionStatusACTIVE · TOKEN_EXPIRED · REVOKED · ERROR
EmailWebhookEventTypeMESSAGE_RECEIVED · MESSAGE_SENT · MESSAGE_UPDATED · MESSAGE_DELETED
EmailDeliveryStatusPENDING · SUCCESS · FAILED · RETRYING
EmailFolderTypeINBOX · SENT · DRAFTS · TRASH · SPAM · ARCHIVE · CUSTOM

Ciclo de vida de uma conexão

   POST /connections/connect
            │
            ▼
     (aguardando consentimento — nada é gravado ainda)
            │
            │ GET /connections/callback com code válido
            ▼
      ┌──────────┐   token vence e não há refresh   ┌───────────────┐
      │  ACTIVE  │ ───────────────────────────────▶ │ TOKEN_EXPIRED │
      └────┬─────┘                                  └───────────────┘
           │  DELETE /connections/:id
           ▼
      ┌──────────┐
      │ REVOKED  │   deletedAt preenchido; a linha permanece
      └──────────┘

   Operação sobre conexão REVOKED devolve 400.
   `ERROR` existe no enum e hoje não é atribuído por nenhum caminho de código.

Ciclo de vida de uma assinatura de push

  POST /provider-subscriptions          expiresAt vindo do provedor
            │                            Gmail  ≈ 7 dias (watch)
            ▼                            Graph  ≤ 10.080 min p/ mensagem
      ┌───────────┐
      │ isActive  │◀──── POST /provider-subscriptions/:id/renew ────┐
      │   true    │                                                 │
      └─────┬─────┘      job renewExpiringSubscriptions() varre     │
            │            o que vence nas próximas 6 horas ──────────┘
            │  DELETE /provider-subscriptions/:id
            ▼
      remove no provedor e apaga a linha (hard delete)

09Referência da API

Prefixo: /email. Atenção ao caminho: o basePath é /email e os routers ficam sob /api/v1/email, então a rota completa repete o segmento — /email/api/v1/email/messages. Está correto; confira contra src/email/app.ts.

Todas as rotas autenticadas exigem Authorization: Bearer <token> e passam por requireOrganization, exceto onde indicado. As três permissões do vocabulário são EMAIL_READ, EMAIL_WRITE e EMAIL_ADMIN.

Configurações de provedor — /email/api/v1/email/provider-configs

MétodoRotaDescriçãoPermissão
POST/email/api/v1/email/provider-configsCadastra credenciais OAuth da organizaçãoEMAIL_ADMIN
GET/email/api/v1/email/provider-configsLista as configuraçõesEMAIL_READ
GET/email/api/v1/email/provider-configs/:idBusca uma configuraçãoEMAIL_READ
PATCH/email/api/v1/email/provider-configs/:idAtualiza nome, credenciais, padrão ou ativaçãoEMAIL_ADMIN
DELETE/email/api/v1/email/provider-configs/:idExclusão lógicaEMAIL_ADMIN
POST/email/api/v1/email/provider-configs/:id/testValida o formato da configuraçãoEMAIL_ADMIN

Conexões — /email/api/v1/email/connections

MétodoRotaDescriçãoPermissão
POST/email/api/v1/email/connections/connectDevolve a URL de autorização do provedorEMAIL_WRITE
GET/email/api/v1/email/connections/callbackCallback do OAuth; redireciona ao redirectUrlPública — sem authMiddleware
GET/email/api/v1/email/connectionsLista as conexões do usuário do tokenEMAIL_READ
DELETE/email/api/v1/email/connections/:idDesconecta (marca REVOKED, exclusão lógica)EMAIL_WRITE

Mensagens — /email/api/v1/email/messages

MétodoRotaDescriçãoPermissão
GET/email/api/v1/email/messagesLista mensagens; filtros por query stringEMAIL_READ
POST/email/api/v1/email/messages/searchBusca com query longa no corpo da requisiçãoEMAIL_READ
POST/email/api/v1/email/messagesEnvia mensagem ou salva rascunhoEMAIL_WRITE
GET/email/api/v1/email/messages/:idBusca uma mensagem; exige ?connectionId=EMAIL_READ
POST/email/api/v1/email/messages/:id/replyResponde, com replyAll opcionalEMAIL_WRITE
POST/email/api/v1/email/messages/:id/forwardEncaminhaEMAIL_WRITE
PATCH/email/api/v1/email/messages/:idMarca lida ou favorita, muda rótulos, move de pastaEMAIL_WRITE
DELETE/email/api/v1/email/messages/:idMove para a lixeira; ?permanent=true apaga de vezEMAIL_WRITE
GET/email/api/v1/email/messages/:id/attachments/:attIdBaixa o anexo em base64; exige ?connectionId=EMAIL_READ

Conversas — /email/api/v1/email/threads

MétodoRotaDescriçãoPermissão
GET/email/api/v1/email/threadsLista conversasEMAIL_READ
GET/email/api/v1/email/threads/:idConversa com todas as mensagens; exige ?connectionId=EMAIL_READ
PATCH/email/api/v1/email/threads/:idAplica a alteração a todas as mensagens da conversaEMAIL_WRITE
DELETE/email/api/v1/email/threads/:idApaga todas as mensagens da conversa; exige ?connectionId=EMAIL_WRITE

Pastas — /email/api/v1/email/folders

MétodoRotaDescriçãoPermissão
GET/email/api/v1/email/foldersLista pastas e rótulos; exige ?connectionId=EMAIL_READ

Webhooks do cliente — /email/api/v1/email/webhooks

MétodoRotaDescriçãoPermissão
POST/email/api/v1/email/webhooksCria a assinatura de entregaEMAIL_ADMIN
GET/email/api/v1/email/webhooksLista as assinaturasEMAIL_READ
PATCH/email/api/v1/email/webhooks/:idAtualiza URL, eventos, segredo ou ativaçãoEMAIL_ADMIN
DELETE/email/api/v1/email/webhooks/:idExclusão lógicaEMAIL_ADMIN

Assinaturas de push no provedor — /email/api/v1/email/provider-subscriptions

MétodoRotaDescriçãoPermissão
POST/email/api/v1/email/provider-subscriptionsCria subscription no Graph ou watch no GmailEMAIL_ADMIN
POST/email/api/v1/email/provider-subscriptions/:id/renewEstende a validadeEMAIL_ADMIN
DELETE/email/api/v1/email/provider-subscriptions/:idCancela no provedor e apaga a linhaEMAIL_ADMIN

Notificação do provedor e utilitárias

MétodoRotaDescriçãoPermissão
POST/email/api/v1/email/providers/notificationsRecebe o push do Graph e do Pub/SubPública — validação é por provedor
GET/email/healthSonda de saúde do serviçoPública
GET/email/connectedPágina de destino padrão pós-OAuth, para desenvolvimentoPública

/health e /connected não entram na contagem de 32 endpoints, que cobre apenas as rotas de negócio dos oito routers.


POST /email/api/v1/email/provider-configs

Cadastra as credenciais OAuth da organização. Aceita o corpo direto ou no envelope {"data":{"attributes":{...}}}.

Request

{
  "name": "Google Workspace corporativo",
  "providerType": "GOOGLE",
  "credentials": {
    "client_id": "1234567890-abc.apps.googleusercontent.com",
    "client_secret": "GOCSPX-...",
    "redirect_uri": "https://email.example.com/email/api/v1/email/connections/callback"
  },
  "isDefault": true
}
CampoTipoObrigatórioDescrição
namestring (1–100)SimÚnico dentro da organização
providerTypeGOOGLE | OUTLOOKSimProvedor
credentialsobject<string,string>Simclient_id, client_secret, redirect_uri; no Outlook aceita tenant_id (padrão common)
isDefaultbooleanNãoSe omitido, a primeira configuração da organização vira padrão
isActivebooleanNãoPadrão true
settingsobjectNãoAjustes específicos do provedor

Resposta 201 — o campo credentials nunca é devolvido.

{
  "data": {
    "type": "email-provider-config",
    "id": "0f5a...",
    "links": { "self": "/api/v1/email/provider-configs/0f5a..." },
    "attributes": {
      "name": "Google Workspace corporativo",
      "providerType": "GOOGLE",
      "isDefault": true,
      "isActive": true,
      "createdAt": "2026-08-16T12:00:00.000Z",
      "updatedAt": "2026-08-16T12:00:00.000Z"
    }
  }
}

Erros

StatusQuando
400Corpo reprovado no Zod, ou EMAIL_CREDENTIAL_MASTER_KEY ausente/malformada
403Token sem organizationId, ou sem EMAIL_ADMIN
409Já existe configuração com esse name na organização

POST /email/api/v1/email/connections/connect

Devolve a URL para onde você redireciona o usuário. Nada é gravado neste passo.

Request

{
  "configId": "0f5a...",
  "redirectUrl": "https://app.seucliente.com.br/integracoes/email/ok",
  "scopes": ["https://www.googleapis.com/auth/gmail.send", "openid"]
}
CampoTipoObrigatórioDescrição
configIdstring (uuid)SimConfiguração de provedor a usar
redirectUrlstring (url)SimPara onde o BB devolve o navegador depois do callback
scopesstring[]NãoSobrepõe os escopos padrão. Leia §15 antes de mexer aqui.

Escopos padrão

ProvedorEscopos padrãoConsequência
GOOGLEgmail.send, userinfo.email, openidEnvio apenas. Listar, ler, modificar e criar watch no Gmail falham por falta de escopo.
OUTLOOKUser.Read, Mail.ReadWrite, Mail.Send, offline_accessLeitura e escrita completas na caixa.

Resposta 200 — objeto simples, sem envelope data.

{ "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth?client_id=..." }

Erros

StatusQuando
400configId inexistente na organização, ou credenciais sem client_id/redirect_uri
403Sem organizationId no token, ou sem EMAIL_WRITE

GET /email/api/v1/email/connections/callback

Rota pública, chamada pelo navegador do usuário. Recebe code e state, troca o código por tokens, cria a EmailConnection e responde 302 para o redirectUrl informado no connect.

SituaçãoResposta
Sucesso302 para o redirectUrl
Provedor devolveu error na query400 em text/plain com o erro e a descrição
code ou state ausente400 Missing code or state parameter
state não decifra ou está incompleto400 OAuth callback failed: Invalid callback state

O redirect_uri cadastrado nas credenciais precisa apontar para esta rota, e ser idêntico ao registrado no console do Google ou no portal do Entra. Divergência de um caractere resulta em redirect_uri_mismatch na tela do provedor, antes de chegar aqui.


GET /email/api/v1/email/messages

Lista mensagens de uma conexão. Todos os filtros vêm por query string.

ParâmetroTipoObrigatórioDescrição
connectionIdstring (uuid)SimConexão a consultar
folderstringNãoGmail: identificador de rótulo (INBOX). Outlook: nome bem conhecido de pasta (inbox, sentitems).
labelIdsstring ou string[]NãoAceita lista separada por vírgula. Só faz efeito no Gmail.
querystringNãoSintaxe nativa do provedorq do Gmail, $search do Graph. Não é traduzida.
threadIdstringNãoRestringe à conversa
pageSizenumber (1–100)NãoPadrão 25
pageTokenstringNãoCursor da página anterior. No Outlook é a URL @odata.nextLink inteira.
includeBodybooleanNãoPadrão false. Sem ele o Gmail retorna só metadados.

Resposta 200 — sem envelope data.

{
  "messages": [
    {
      "id": "19f2b0c1a2d3e4f5",
      "threadId": "19f2b0c1a2d3e400",
      "providerMessageId": "19f2b0c1a2d3e4f5",
      "rfc822MessageId": "<CADk...@mail.gmail.com>",
      "from": { "email": "cliente@empresa.com.br", "name": "Maria Silva" },
      "to": [{ "email": "vendedor@financeira.com.br" }],
      "cc": [], "bcc": [], "replyTo": [],
      "subject": "Proposta 4471",
      "snippet": "Segue o comprovante solicitado...",
      "isRead": false, "isStarred": false, "isDraft": false, "isSent": false,
      "hasAttachments": true,
      "labels": ["INBOX", "UNREAD", "CATEGORY_PERSONAL"],
      "category": "primary",
      "folder": "INBOX",
      "attachments": [
        { "id": "ANGjdJ...", "filename": "comprovante.pdf",
          "mimeType": "application/pdf", "size": 184320 }
      ],
      "receivedAt": "2026-08-16T11:42:00.000Z"
    }
  ],
  "hasMore": true,
  "nextPageToken": "08...="
}

Erros

StatusQuando
400Query reprovada no Zod, conexão REVOKED, ou token expirado sem refresh token
403Sem organizationId no token, ou sem EMAIL_READ
404connectionId inexistente na organização do token
500Falha do provedor. A mensagem do Gmail é repassada — costuma dizer se é escopo, quota ou token morto.

POST /email/api/v1/email/messages

Envia uma mensagem ou salva um rascunho.

Request

{
  "connectionId": "7c1e...",
  "to": [{ "email": "cliente@empresa.com.br", "name": "Maria Silva" }],
  "cc": [{ "email": "gerente@financeira.com.br" }],
  "subject": "Proposta 4471 — documentação pendente",
  "bodyText": "Bom dia, Maria. Falta o comprovante de renda.",
  "bodyHtml": "<p>Bom dia, Maria. Falta o comprovante de renda.</p>",
  "attachments": [
    { "filename": "checklist.pdf", "mimeType": "application/pdf", "data": "JVBERi0x..." }
  ],
  "asDraft": false
}
CampoTipoObrigatórioDescrição
connectionIdstring (uuid)SimDe qual conta sai a mensagem
toarray (mín. 1)SimCada item com email e name opcional
cc, bcc, replyToarrayNãoMesmo formato
subjectstringSimAceita string vazia
bodyText, bodyHtmlstringNãoInformar os dois gera multipart/alternative
attachmentsarrayNãodata em base64; aceita isInline e contentId
inReplyToMessageIdstringNãoIdentificador do provedor para encadear a conversa
asDraftbooleanNãoPadrão false. true cria rascunho sem enviar.

Resposta 201{ "data": <CanonicalEmailMessage> }.

Erros

StatusQuando
400Corpo reprovado no Zod, ou conexão REVOKED
403Sem EMAIL_WRITE
500Provedor recusou o envio — anexo grande, destinatário inválido, quota estourada

POST /email/api/v1/email/provider-subscriptions

Cria a assinatura de push no provedor. Os campos são mutuamente relevantes: o Outlook usa notificationUrl e resource, o Gmail usa topicName e labelIds.

CampoTipoObrigatórioDescrição
connectionIdstring (uuid)SimConta a observar
resourcestringNãoPadrão /me/messages. Só o Outlook usa.
notificationUrlstring (url)OutlookURL HTTPS pública para onde o Graph envia
topicNamestringGmailprojects/<projeto>/topics/<topico>; cai para EMAIL_GOOGLE_PUBSUB_TOPIC
labelIdsstring[]NãoGmail: restringe o watch. Padrão ["INBOX"].
expirationMinutesnumber (10–10080)NãoOutlook: padrão 4.320 minutos (3 dias)

O schema exige pelo menos um entre notificationUrl, topicName e a variável de ambiente do tópico — sem nenhum deles, 400.

Resposta 201

{
  "data": {
    "type": "email-provider-subscription",
    "id": "b9e0...",
    "attributes": {
      "connectionId": "7c1e...",
      "providerType": "OUTLOOK",
      "externalId": "8f2c-...",
      "resource": "/me/messages",
      "expiresAt": "2026-08-19T12:00:00.000Z",
      "isActive": true,
      "createdAt": "2026-08-16T12:00:00.000Z"
    }
  }
}

No Gmail, o externalId é o endereço de e-mail da conta, não um identificador opaco — é assim que a notificação do Pub/Sub é reconciliada com a assinatura.


POST /email/api/v1/email/webhooks

Assina os eventos que o BB entrega ao seu sistema.

CampoTipoObrigatórioDescrição
callbackUrlstring (url)SimOnde você recebe
eventsarraySimMESSAGE_RECEIVED, MESSAGE_SENT, MESSAGE_UPDATED, MESSAGE_DELETED
secretstring (mín. 16)NãoSe omitido, o serviço gera 32 bytes aleatórios
descriptionstring (máx. 500)NãoTexto livre

A entrega é um POST com estes cabeçalhos:

CabeçalhoConteúdo
X-Webhook-Signaturesha256=<HMAC-SHA256 do corpo bruto, com o segredo da assinatura>
X-Webhook-EventO tipo do evento
X-Webhook-IdIdentificador da entrega, o mesmo do registro em email_webhook_delivery_logs

Corpo entregue: { "eventType": "...", "data": { ... }, "timestamp": "..." }. Tempo limite de 30 segundos.


10Início rápido

Do zero à primeira mensagem enviada pela conta do usuário. Ambiente local, modo monolito na porta 3000 — em standalone troque por http://localhost:3026.

Não executados. O building block ainda não está publicado em staging (não consta em AMBIENTES.md), então os comandos abaixo foram escritos a partir do código e não foram executados contra um ambiente. Os passos 2 e 3 exigem consentimento humano no navegador e não são automatizáveis.

1. Autenticar no IAM

TOKEN=$(curl -s -X POST http://localhost:3000/iam/api/v1/users/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "admin@catalisa.app",
    "password": "root123456",
    "organizationId": "b0000000-0000-0000-0000-000000000001"
  }' | jq -r .accessToken)

O token precisa carregar EMAIL_ADMIN, EMAIL_READ e EMAIL_WRITE, além do organizationId.

2. Cadastrar as credenciais OAuth da organização

Registre antes um aplicativo no Google Cloud Console ou no portal do Entra, e aponte o redirect_uri para a rota de callback do building block.

CONFIG=$(curl -s -X POST http://localhost:3000/email/api/v1/email/provider-configs \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "name": "Workspace de teste",
    "providerType": "GOOGLE",
    "credentials": {
      "client_id": "SEU_CLIENT_ID.apps.googleusercontent.com",
      "client_secret": "SEU_CLIENT_SECRET",
      "redirect_uri": "http://localhost:3000/email/api/v1/email/connections/callback"
    },
    "isDefault": true
  }')
CONFIG_ID=$(echo "$CONFIG" | jq -r '.data.id')

Resposta esperada: 201, com attributes.providerType igual a GOOGLE e sem o campo credentials.

3. Conectar a conta

curl -s -X POST http://localhost:3000/email/api/v1/email/connections/connect \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{
    \"configId\": \"$CONFIG_ID\",
    \"redirectUrl\": \"http://localhost:3000/email/connected\"
  }" | jq -r .authorizationUrl

Abra a URL no navegador, consinta, e o provedor devolve para a rota de callback, que grava a conexão e redireciona para /email/connected — a página "Email connected". Não há como pular esta etapa: OAuth exige um humano.

4. Descobrir o connectionId

CONN_ID=$(curl -s http://localhost:3000/email/api/v1/email/connections \
  -H "Authorization: Bearer $TOKEN" | jq -r '.data[0].id')

curl -s http://localhost:3000/email/api/v1/email/connections \
  -H "Authorization: Bearer $TOKEN" | jq '.data[0].attributes'

Resposta esperada:

{
  "providerType": "GOOGLE",
  "externalEmail": "usuario@seudominio.com.br",
  "status": "ACTIVE",
  "configId": "0f5a..."
}

5. Enviar a primeira mensagem

curl -s -X POST http://localhost:3000/email/api/v1/email/messages \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{
    \"connectionId\": \"$CONN_ID\",
    \"to\": [{\"email\": \"voce@exemplo.com.br\"}],
    \"subject\": \"Teste do building block Email\",
    \"bodyText\": \"Se você está lendo isto, a conexão funciona.\"
  }" | jq '.data.id, .data.isSent'

Resposta esperada: 201. A mensagem aparece na caixa "Enviados" da conta conectada — não de um remetente da Catalisa.

6. Listar a caixa de entrada

curl -s "http://localhost:3000/email/api/v1/email/messages?connectionId=$CONN_ID&folder=INBOX&pageSize=5" \
  -H "Authorization: Bearer $TOKEN" | jq '.messages[] | {subject, from, isRead}'

Com uma conexão Outlook, isto lista. Com uma conexão Gmail nos escopos padrão, isto falha com 500 e a mensagem de erro do Gmail sobre escopo insuficiente — comportamento esperado, explicado em §15.

Credenciais de desenvolvimento local. Nunca cole segredo de produção em documentação ou script — veja AMBIENTES.md.


11Receitas

Receber notificação de mensagem nova no Outlook

Objetivo. Fazer o seu sistema ser avisado em menos de um minuto quando chega e-mail na caixa conectada.

# 1. Assinar o evento do seu lado — para onde o BB entrega
WEBHOOK=$(curl -s -X POST http://localhost:3000/email/api/v1/email/webhooks \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "callbackUrl": "https://api.seusistema.com.br/hooks/email",
    "events": ["MESSAGE_RECEIVED", "MESSAGE_UPDATED"],
    "description": "Esteira de documentos"
  }')
echo "$WEBHOOK" | jq '.data.id'

# 2. Criar a assinatura de push no Microsoft Graph
curl -s -X POST http://localhost:3000/email/api/v1/email/provider-subscriptions \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{
    \"connectionId\": \"$CONN_ID\",
    \"resource\": \"/me/messages\",
    \"notificationUrl\": \"https://email.suaempresa.com.br/email/api/v1/email/providers/notifications\",
    \"expirationMinutes\": 4320
  }" | jq '.data.attributes'

Ao criar a assinatura, o Graph chama a notificationUrl com ?validationToken= e espera o valor de volta em text/plain — o router já faz isso. Se essa etapa falhar, a assinatura não nasce.

Armadilhas.

  • A notificationUrl tem que ser HTTPS e alcançável pela internet. localhost não funciona: use um túnel em desenvolvimento.
  • Você precisa das duas assinaturas. Só a do provedor faz o BB receber, mas não faz ele entregar; só a sua faz ele querer entregar, mas nada chega.
  • O segredo do webhook aparece na criação e não é devolvido nas listagens. Guarde-o na hora.
  • A validação da entrega é HMAC-SHA256 sobre o corpo bruto. Se o seu framework reserializar o JSON antes de você calcular o hash, a assinatura não bate.
  • A latência oficial do Graph para message é média abaixo de 1 minuto, máxima 3 (Graph — throttling e latência). Não prometa segundos ao usuário final.

Manter o push vivo

Objetivo. Evitar que a assinatura expire e o produto emudeça sem ninguém perceber.

# Renovação manual de uma assinatura específica
curl -s -X POST \
  "http://localhost:3000/email/api/v1/email/provider-subscriptions/$SUB_ID/renew" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"minutes": 4320}' | jq '.data.attributes.expiresAt'

Para a renovação automática, agende renewExpiringSubscriptions() de src/email/jobs/subscription-renewal.job.ts. Ele varre o que vence nas próximas 6 horas e renova para 3 dias.

Armadilhas.

  • O job não é agendado por este building block. Ele existe como função exportada e precisa de um cron ou de uma fila do lado de fora. Sem isso, toda assinatura morre no prazo do provedor.
  • No Gmail, a renovação ignora o parâmetro minutes e depende de EMAIL_GOOGLE_PUBSUB_TOPIC estar configurado — renovar é chamar watch de novo, e o Google decide a nova expiração (cerca de 7 dias).
  • A Google recomenda chamar watch uma vez por dia, não a cada sete. Uma janela de 6 horas atende folgado, desde que o cron rode.
  • Renovação com token vencido falha silenciosamente no log do job, com logger.warn. Monitore esse aviso.

Baixar o anexo que o cliente respondeu

Objetivo. Levar o PDF que chegou por e-mail até o File Storage, sem intervenção humana.

# 1. Localizar a mensagem (sintaxe nativa do provedor no campo query)
MSG_ID=$(curl -s -X POST http://localhost:3000/email/api/v1/email/messages/search \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{
    \"connectionId\": \"$CONN_ID\",
    \"query\": \"has:attachment from:cliente@empresa.com.br\",
    \"pageSize\": 5
  }" | jq -r '.messages[0].id')

# 2. Descobrir os anexos
ATT_ID=$(curl -s "http://localhost:3000/email/api/v1/email/messages/$MSG_ID?connectionId=$CONN_ID" \
  -H "Authorization: Bearer $TOKEN" | jq -r '.data.attachments[0].id')

# 3. Baixar e gravar em disco
curl -s "http://localhost:3000/email/api/v1/email/messages/$MSG_ID/attachments/$ATT_ID?connectionId=$CONN_ID" \
  -H "Authorization: Bearer $TOKEN" | jq -r '.data.data' | base64 -d > comprovante.pdf

Armadilhas.

  • O conteúdo vem inteiro em base64 dentro do JSON. Um anexo de 20 MB vira quase 27 MB de resposta na memória. Não use este endpoint para arquivo grande sem pensar no consumo.
  • A busca usa a sintaxe do provedor: has:attachment from:... no Gmail, $search do Graph no Outlook. Não há tradução entre as duas.
  • O attachments de uma listagem traz só metadados; o conteúdo só vem por este endpoint.
  • No Gmail, a busca por has:attachment exige escopo de leitura — ver §15.

Marcar a conversa inteira como lida

Objetivo. Encerrar o atendimento sem percorrer mensagem por mensagem.

curl -s -X PATCH "http://localhost:3000/email/api/v1/email/threads/$THREAD_ID" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{\"connectionId\": \"$CONN_ID\", \"markRead\": true, \"addLabels\": [\"Resolvido\"]}" \
  | jq '.data | {id, unreadCount}'

Armadilhas.

  • No Outlook a operação não é atômica. O provider busca as mensagens da conversa e aplica a alteração uma a uma. Com o teto de 4 requisições concorrentes por caixa do Graph, uma conversa de 40 mensagens leva tempo e pode falhar no meio, deixando parte alterada.
  • PATCH de conversa aceita markRead, addLabels, removeLabels e moveToFolder. Não aceita markStarred — favoritar é operação de mensagem.
  • No Gmail, addLabels espera identificadores de rótulo, não os nomes exibidos. Descubra-os em GET /folders.

Diagnosticar por que uma chamada ao provedor falhou

Objetivo. Separar erro de escopo, de quota e de token morto — que se parecem muito.

# A conexão está viva?
curl -s http://localhost:3000/email/api/v1/email/connections \
  -H "Authorization: Bearer $TOKEN" | jq '.data[] | {id, externalEmail, status: .attributes.status}'

# A configuração do provedor é válida em forma?
curl -s -X POST "http://localhost:3000/email/api/v1/email/provider-configs/$CONFIG_ID/test" \
  -H "Authorization: Bearer $TOKEN" | jq

Ordem de diagnóstico:

  1. status da conexão. REVOKED devolve 400 em qualquer operação. TOKEN_EXPIRED significa que o refresh token sumiu — o usuário precisa reconectar.
  2. A mensagem do erro 500 nas rotas do Gmail. O provider repassa o texto do Gmail, que costuma dizer explicitamente se é escopo insuficiente, quota estourada ou credencial revogada.
  3. Escopo. Se a conexão Gmail foi criada com os escopos padrão, tudo que não é envio falha. Não há conserto sem reconectar com escopos maiores — ver §15.
  4. Quota. Gmail: 6.000 unidades por minuto por usuário, e um messages.get custa 20. Graph: 10.000 requisições por 10 minutos e 4 concorrentes por caixa. Uma listagem de 100 mensagens no Gmail consome cerca de 2.005 unidades.
  5. POST /provider-configs/:id/test valida apenas a forma da configuração — que o tipo de provedor existe e que a credencial decifra. Não chama o provedor nem confirma que o client_secret está certo.

12Integração com outros building blocks

Building blockComo se relacionaObrigatório
IAMEmite o token, define a organização e concede EMAIL_READ, EMAIL_WRITE e EMAIL_ADMINSim
CalendarBuilding block irmão: mesma anatomia de provider-config, connection, provider-subscription e provider-notification, com as mesmas credenciais Google e MicrosoftNão
File StorageDestino natural dos anexos baixados; o Email entrega base64, o File Storage guardaNão
CustomersLiga o endereço de e-mail da conversa à pessoa ou empresa cadastradaNão
Webhooks EngineDistribuição de eventos em escala; o Email tem entrega própria e simples, sem repetição automática (§15)Não
Audit TrailRegistra quem conectou conta, quem leu caixa e quem enviou em nome de quemNão
Data ExtractionExtrai dados estruturados do PDF que chegou anexadoNão

A simetria com o Calendar é o argumento, não a coincidência. Os dois building blocks compartilham a mesma anatomia de integração OAuth:

   ┌──────────────────────────┐        ┌──────────────────────────┐
   │        EMAIL             │        │       CALENDAR           │
   ├──────────────────────────┤        ├──────────────────────────┤
   │ provider-configs         │        │ provider-configs         │
   │ connections/connect      │        │ connections/connect      │
   │ connections/callback     │  mesma │ connections/callback     │
   │ provider-subscriptions   │◀─────▶ │ provider-notifications   │
   │ providers/notifications  │ estrut.│ webhooks                 │
   │ webhooks                 │        │ calendars · events·slots │
   │ messages·threads·folders │        │                          │
   └────────────┬─────────────┘        └────────────┬─────────────┘
                │                                   │
                └───────────────┬───────────────────┘
                                ▼
             mesmas credenciais OAuth do cliente no Google
             e na Microsoft · mesmo padrão de cifra AES-256-GCM
             · mesmo modelo de conexão por usuário

Na prática: quem já conectou a agenda do vendedor conecta o e-mail dele com o mesmo consentimento, o mesmo aplicativo registrado e o mesmo código de front-end. Vender "a agenda e a caixa do seu time dentro do seu produto" deixa de ser dois projetos e vira uma entrega.

E a cadeia completa, que é o que nenhum fornecedor isolado de API de e-mail entrega:

  ┌─────────┐   MESSAGE_RECEIVED   ┌──────────────┐   anexo base64  ┌──────────────┐
  │  Email  │────────────────────▶ │  Seu sistema │───────────────▶ │ File Storage │
  └─────────┘                      └──────┬───────┘                 └──────┬───────┘
       │ remetente                        │                                │
       ▼                                  ▼                                ▼
  ┌───────────┐                  ┌──────────────────┐            ┌──────────────────┐
  │ Customers │                  │ Decision Platform│            │ Data Extraction  │
  │ quem é    │                  │ segue a esteira  │            │ lê o PDF         │
  └───────────┘                  └──────────────────┘            └──────────────────┘

13Configuração e operação

Variáveis de ambiente

VariávelDescriçãoObrigatóriaPadrão
EMAIL_CREDENTIAL_MASTER_KEYChave AES-256-GCM. Exatamente 64 caracteres hexadecimais (32 bytes). Gere com openssl rand -hex 32.Sim, para usar o BB
EMAIL_GOOGLE_PUBSUB_TOPICTópico do Pub/Sub no formato projects/<projeto>/topics/<topico>. Necessária para push do Gmail e obrigatória para renovar o watch.Só com push do Gmail
EMAIL_GOOGLE_PUBSUB_AUDIENCEaud esperado no token OIDC que o Pub/Sub anexa. Sem ela a verificação da notificação é pulada — defina em produção.Recomendada
DATABASE_URLPostgreSQL com o schema emailSim
JWT_SECRETVerificação do token do IAM. Mínimo 44 caracteres.Sim
MODULE_IAM_URLEndereço do IAM em standaloneEm standalone
MODULE_EMAIL_URLEndereço do Email para os outros building blocksNão''
DEPLOYMENT_MODEmonolith ou standaloneNãostandalone no main.ts
PORTPorta de escuta em standaloneNão3000

Dependências de infraestrutura

DependênciaPara quê
PostgreSQLSchema email — configurações, conexões, assinaturas e logs de entrega
IAMEmissão e verificação do token com organização e permissões
Google Cloud Pub/SubSó para push do Gmail: tópico, subscription e permissão de publicação para o Gmail
Endpoint HTTPS públicoSó para push do Outlook: o Graph precisa alcançar a rota de notificação

O Email não usa Redis e não usa S3.

Limites e quotas

LimiteValorOrigem
pageSize de mensagens e conversas1 a 100, padrão 25Schema Zod deste BB
expirationMinutes de assinatura10 a 10.080Schema Zod deste BB
Padrão de expiração no Outlook4.320 minutos (3 dias)Padrão deste BB
Máximo de assinatura para mensagem no Graph10.080 minutos; 1.440 com dados do recursoMicrosoft
Validade do watch do Gmail7 dias; renovação diária recomendadaGoogle
Quota do Gmail6.000 unidades/minuto por usuário; messages.get = 20, messages.send = 100, users.watch = 100Google
Throttling do Outlook via Graph10.000 requisições/10 min e 4 concorrentes por aplicativo e caixaMicrosoft
Tempo limite de entrega de webhook30 segundosCódigo deste BB
Comprimento do segredo de webhookMínimo 16 caracteresSchema Zod deste BB

Catálogo de erros

StatusCódigoSignificaO que fazer
400VALIDATIONCorpo ou query reprovados no ZodConfira campos e tipos contra §9
400VALIDATIONThis email connection has been revokedA conexão foi desconectada; refaça o fluxo OAuth
400VALIDATIONToken expired and no refresh token availableO provedor não devolveu refresh token; reconecte com access_type=offline
400VALIDATIONInvalid callback statestate corrompido, ou a chave mestra mudou desde o início do fluxo
400VALIDATIONCannot delete the only email provider configCrie outra configuração antes de apagar a única
400VALIDATIONInvalid notification signatureclientState da notificação não confere; verifique a origem
403Organization context requiredAutentique com token que carrega organizationId
403FORBIDDENFalta EMAIL_READ, EMAIL_WRITE ou EMAIL_ADMINAjuste o papel no IAM e emita token novo
404NOT_FOUNDEmailConnection, EmailProviderConfig ou assinatura inexistente na organização do tokenConfira o identificador e a organização
409CONFLICTNome de configuração de provedor já usadoEscolha outro nome
500INTERNALFailed to list Gmail messages: <texto do Gmail>Leia o texto repassado: costuma nomear escopo, quota ou token
500INTERNALFailed to decrypt connection tokensEMAIL_CREDENTIAL_MASTER_KEY mudou; os dados cifrados com a chave antiga são irrecuperáveis

Observabilidade.

  • GET /email/health devolve nome do serviço e versão do build. É uma sonda de processo vivo: não testa banco nem provedor.
  • POST /provider-configs/:id/test valida a forma da configuração — que o provedor existe e que a credencial decifra. Não confirma que o segredo está correto no Google ou na Microsoft.
  • O job de renovação registra Failed to renew email provider subscription com logger.warn por assinatura que falha. É o sinal mais importante de monitorar: sem ele, o push morre em silêncio.
  • Listagens do Gmail que perdem mensagem no meio registram [gmail] listMessages: skipped N/M unfetchable message(s) — é o único rastro de página que voltou incompleta.
  • Cada entrega de webhook grava uma linha em email_webhook_delivery_logs com status, código HTTP e mensagem de erro. É por ali que se responde "o evento saiu daqui?".

14Segurança e compliance

Isolamento entre tenants. O organizationId vem do claim assinado do JWT. Todos os oito routers de negócio aplicam requireOrganization, que devolve 403 quando o claim falta. Nenhuma rota lê organizationId do corpo ou da query. No acesso ao dado, EmailConnectionRepository.findById(id, organizationId) e EmailProviderConfigRepository.findById(id, organizationId) filtram por organização na cláusula SQL, junto com deletedAt: null — um connectionId de outra organização resulta em 404, não em dado. Como toda operação de mensagem passa obrigatoriamente por getProviderForConnection, que resolve a conexão com esse filtro, não existe caminho de leitura de caixa que escape do escopo da organização.

Credenciais OAuth e tokens. EmailProviderConfig.credentials, EmailConnection.accessToken e EmailConnection.refreshToken são cifrados com AES-256-GCM — chave de 32 bytes, IV aleatório de 12 bytes por operação e auth tag verificada na decifra. O formato gravado é iv:authTag:ciphertext, tudo em hexadecimal. A chave vem de EMAIL_CREDENTIAL_MASTER_KEY e é validada no uso: 64 caracteres hexadecimais, ou a operação falha. O client_secret do provedor nunca volta em nenhuma resposta da API — as rotas de configuração serializam apenas nome, tipo, sinalizadores e datas.

O state do OAuth. É cifrado com a mesma chave mestra e a mesma cifra autenticada. Quem não tem a chave não forja um state válido, e um state adulterado falha na verificação do auth tag, produzindo Invalid callback state.

Autenticação das notificações do provedor. No Outlook, cada assinatura nasce com um clientState aleatório (UUID v4) que o Graph devolve em toda notificação; o serviço exige que todos os itens do lote tragam o valor correto, e recusa lote vazio. No Gmail, o Pub/Sub anexa um token OIDC assinado pelo Google, verificado contra o JWKS oficial quando EMAIL_GOOGLE_PUBSUB_AUDIENCE está configurada — configure-a em produção, porque sem ela o endpoint aceita a notificação sem verificar.

Integridade da entrega ao cliente. Cada webhook sai com X-Webhook-Signature: sha256=<HMAC> calculado sobre o corpo exato enviado. Valide a assinatura antes de confiar no conteúdo.

Dado pessoal e LGPD. E-mail é dado pessoal, e o corpo de uma mensagem pode conter dado pessoal sensível. A decisão de arquitetura que mais importa aqui é esta: o building block não persiste conteúdo de mensagem. Assunto, corpo, destinatários e anexos passam do provedor para o chamador e não tocam o PostgreSQL. Concretamente, o que fica gravado é:

Dado pessoal gravadoOndePor quê
Endereço de e-mail da conta conectadaemail_connections.external_emailIdentificar a conexão para o usuário
Endereço de e-mail da conta observadaemail_provider_subscriptions.external_id, no GmailÉ a chave que reconcilia a notificação com a assinatura
Identificadores da notificação do provedoremail_webhook_delivery_logs.payloadAuditar a entrega. No Gmail contém o endereço e o historyId; no Outlook, o identificador da mensagem e o tipo de alteração — nunca o conteúdo.

Ou seja: o BB registra que houve mensagem e em qual caixa, nunca o que estava escrito. Quem decide o que reter do conteúdo é o sistema que consome a API — e é lá que a política de retenção precisa existir.

Retenção e exclusão. Configurações de provedor, conexões e assinaturas de webhook usam exclusão lógica (deletedAt), preservando a linha para auditoria. Assinaturas de push são removidas de fato, no provedor e no banco. Não há expurgo automatizado de logs de entrega — ver §15.

Consentimento. O acesso à caixa depende de consentimento explícito do titular na tela do próprio provedor, com os escopos listados. A credencial OAuth é da organização cliente, então o consentimento é dado ao aplicativo dela — o que também significa que a responsabilidade pela verificação junto a Google e Microsoft é dela.

Autenticação e permissões. Duas rotas são públicas por necessidade de protocolo: o callback do OAuth, autenticado pelo state cifrado, e o endpoint de notificação, autenticado pelo mecanismo de cada provedor. Todas as demais exigem token do IAM, organizationId e uma das três permissões. Operações destrutivas ou de configuração exigem EMAIL_ADMIN.


15Limitações conhecidas

LimitaçãoImpactoSituação
Gmail nasce somente com permissão de envioOs escopos padrão da conexão Google são gmail.send, userinfo.email e openid. Listar, ler, modificar mensagem e criar watch no Gmail falham por falta de escopo. Ler caixa no Gmail exige escopos restritos, que exigem avaliação de segurança CASA anual.Por design, até que haja avaliação aprovada. Dá para passar scopes maiores no connect, mas o aplicativo do cliente precisa estar verificado — não anuncie leitura de Gmail sem confirmar isso.
Sem cliente em produçãoO BB é beta: tem testes unitários e de integração, roda em monolito e em standalone, mas não tem histórico de carga real.Beta
Só Gmail e OutlookNão há IMAP genérico, Exchange on-premises, Yahoo, iCloud nem Zoho.Por design; a arquitetura de providers/ aceita novos, o registro é em auth-helpers.ts
O job de renovação não se agenda sozinhorenewExpiringSubscriptions() existe como função e nada a chama. Sem um cron externo, toda assinatura de push expira no prazo do provedor e o produto emudece.Precisa de agendamento na plataforma
Webhook sem repetição automáticaEntrega que falha grava FAILED ou RETRYING e preenche nextRetryAt, mas nenhum processo consome esse campo. Não há segunda tentativa.Roadmap; para entrega crítica, use o Webhooks Engine
Logs de entrega crescem sem limiteemail_webhook_delivery_logs não tem expurgo nem particionamento.Roadmap
Reconectar cria uma conexão novaO callback sempre insere uma EmailConnection. Reconectar a mesma conta gera outra linha, e GET /connections passa a listar as duas. O repositório tem findByExternalEmail, que o fluxo de callback não usa.Roadmap
Desconectar não revoga no provedorDELETE /connections/:id marca REVOKED e apaga logicamente, mas não chama a revogação de token no Google ou na Microsoft, nem cancela as assinaturas de push da conexão.Roadmap — cancele a assinatura antes de desconectar
Acesso é por organização, não por usuárioGET /connections lista só as conexões do usuário do token, mas as rotas de mensagem resolvem a conexão apenas pela organização. Qualquer usuário da mesma organização com EMAIL_READ e o connectionId em mãos acessa aquela caixa.Por design atual; trate connectionId como informação restrita
Operação de conversa não é atômica no OutlookPATCH e DELETE de conversa iteram mensagem a mensagem. Falha no meio deixa a conversa parcialmente alterada, e o teto de 4 concorrentes por caixa do Graph torna conversa longa lenta.Limitação do provedor
Listagem no Gmail é N+1messages.list devolve identificadores e cada mensagem exige um messages.get. Uma página de 25 são 26 chamadas, cerca de 505 unidades de quota.Limitação do provedor
Busca não é traduzida entre provedoresO query vai cru para o q do Gmail ou o $search do Graph. A mesma string produz resultados diferentes.Por design — traduzir sintaxe de busca esconde comportamento e piora o resultado
Sem sincronização incrementalNão há history.list do Gmail nem delta query do Graph. A notificação avisa que algo mudou; descobrir o que mudou é trabalho do consumidor.Roadmap
ERROR no enum de status nunca é atribuídoEmailConnectionStatus.ERROR existe no schema e nenhum caminho de código o define. Não trate esse valor como sinal.Schema à frente do código
MESSAGE_SENT não é emitido pelo provedorO tipo de evento existe e pode ser assinado, mas as notificações do Gmail viram MESSAGE_UPDATED e as do Graph seguem o changeType (created, updated, deleted).Schema à frente do código
Sem paginação para conexões e configuraçõesGET /connections e GET /provider-configs devolvem tudo, sem cursor.Aceitável no volume atual
Anexo trafega inteiro em base64Sem streaming nem URL assinada; um anexo de 20 MB vira quase 27 MB de JSON na memória.Roadmap
/health não sonda dependênciaDevolve serviço e versão. Não testa PostgreSQL nem provedor.Roadmap

16Perguntas frequentes

Isso serve para disparar e-mail transacional, tipo confirmação de pedido?

Não. Para disparo em volume a partir do domínio da sua aplicação, use Amazon SES, Postmark, SendGrid ou Resend — é outra categoria de produto, com outra infraestrutura de entregabilidade. O Email da Catalisa envia pela conta do usuário, uma mensagem por vez, com o endereço dele no remetente. Serve para a resposta que uma pessoa daria; não para a notificação que um sistema dispara.

O conteúdo dos e-mails do meu cliente fica guardado na Catalisa?

Não. Assunto, corpo e anexo passam pela API e não são gravados. O banco guarda credenciais cifradas, o endereço da conta conectada e metadados de assinatura e de entrega — os detalhes estão em §14. Quem decide o que reter do conteúdo é o seu sistema, e é lá que a política de retenção precisa existir.

Por que a leitura funciona no Outlook e não funciona no Gmail?

Porque os escopos padrão da conexão Google são de envio apenas. Ler caixa no Gmail exige escopos restritos, e o Google condiciona escopo restrito a uma avaliação de segurança independente, renovada a cada doze meses. A conexão nasce sem esses escopos justamente para não prometer o que a verificação ainda não autoriza. Dá para pedir escopos maiores no connect, mas o aplicativo do cliente precisa estar aprovado — veja §15.

Quem é o dono da relação com o Google e a Microsoft?

O cliente. Cada organização cadastra o próprio client_id e client_secret, e é o nome do aplicativo dela que aparece na tela de consentimento do usuário final. Isso é uma vantagem — a marca certa na tela, o dado sem intermediário — e uma responsabilidade: a verificação de editor na Microsoft e a avaliação de escopo restrito no Google são dela.

Preciso mesmo de duas assinaturas para receber uma notificação?

Precisa. A provider-subscription faz o Google ou a Microsoft avisarem o building block. A webhook faz o building block avisar você. Só a primeira e nada chega ao seu sistema; só a segunda e nada chega ao building block. É o erro de integração mais comum aqui.

O que acontece se a assinatura de push expirar?

O produto emudece sem erro. Não há exceção, não há alerta — as notificações simplesmente param. Por isso o item mais importante de operação deste BB é agendar renewExpiringSubscriptions() em um cron e monitorar o aviso Failed to renew email provider subscription. O job renova o que vence nas próximas 6 horas; ele existe, mas ninguém o chama por padrão (§15).

O token OAuth do usuário expira. Preciso tratar isso?

Não. Toda operação verifica a validade antes de chamar o provedor e renova se faltarem menos de cinco minutos. O único caso que exige ação humana é quando o provedor não devolveu refresh token ou o usuário revogou o acesso: a conexão vai para TOKEN_EXPIRED e a pessoa precisa reconectar.

Dá para conectar uma caixa compartilhada, e não a de uma pessoa?

Dá, desde que a caixa tenha credenciais próprias de acesso ao Outlook e alguém consinta por ela. A EmailConnection guarda o userId de quem iniciou o fluxo, mas as operações de mensagem resolvem a conexão pela organização — então outros usuários da mesma organização operam a mesma caixa com o connectionId. Leia a linha sobre escopo de acesso em §15 antes de desenhar isso.

Como isso se relaciona com o building block Calendar?

São irmãos por construção: mesma estrutura de configuração de provedor, conexão, assinatura de push e notificação, e as mesmas credenciais OAuth no Google e na Microsoft. Quem integrou um integra o outro no mesmo dia, e o usuário consente uma vez pelo mesmo aplicativo. É o diagrama de §12.

Por que a rota tem email duas vezes no caminho?

Porque o basePath do app é /email e os routers ficam sob /api/v1/email. O caminho real é /email/api/v1/email/messages. Está correto e é assim que precisa ser chamado — confira em src/email/app.ts se tiver dúvida.


Padrão: PADRAO-DOCUMENTACAO.md · Índice: INDEX.md

Building blocks relacionados