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.
- 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
- 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
- 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.
| Atributo | Valor |
|---|---|
| Identificador | email |
| Categoria | Comunicação |
| Escopo | Tenant (exige organizationId no token) |
| Porta (standalone) | 3026 no host, 3000 dentro do contêiner |
| Path alias | @email |
| Prefixo HTTP | /email |
| Status | Beta desde 2026-05 |
| Depende de | PostgreSQL (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.watchdo 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.listperió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.getcusta 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
| Antes | Depois |
|---|---|
| Uma integração para o Gmail e outra para o Outlook | Um contrato canônico de mensagem, conversa e pasta para os dois |
Cron de renovação de watch e outro de subscription, com relógios distintos | POST /provider-subscriptions e um job que renova o que está para vencer |
| Refresh token expirando em produção às três da tarde | O serviço renova sozinho cinco minutos antes de vencer, na própria requisição |
| Copiar e colar e-mail dentro do CRM | O produto lê, responde e arquiva na caixa do usuário |
| Corpo de e-mail replicado no seu banco, com o problema de LGPD junto | Nenhum 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ério | Catalisa Email | Nylas | Unipile | Aurinko | Gmail + Graph direto |
|---|---|---|---|---|---|
| Preço (consulta 2026-08-16) | Precificação em definição | US$ 15/mês + US$ 2,00/conta | € 49/mês mínimo, € 5,00/conta (11–50) | US$ 1,00–2,00/conta, por volume | Sem custo de licença |
| Provedores | Gmail, Outlook | 25+, com IMAP e Exchange | Gmail, Outlook, IMAP + LinkedIn, WhatsApp | Gmail, Outlook, Exchange on-prem, Zoho | Só os dois, um por vez |
| Leitura de caixa no Gmail | Requer escopo restrito e CASA (ver §15) | Incluída, CASA é do fornecedor | Incluída, CASA é do fornecedor | Incluída, CASA é do fornecedor | Requer escopo restrito e CASA seus |
| Corpo da mensagem no banco do fornecedor | Não é gravado | Sincronizado e armazenado | Sincronizado e armazenado | Sincronizado e armazenado | Não se aplica |
De quem é o client_id OAuth | Do cliente, por organização | Do fornecedor ou do cliente | Do fornecedor | Do cliente | Do cliente |
| Push gerenciado | Sim, com job de renovação | Sim | Sim | Sim | Você constrói |
| Canais além de e-mail | Calendar é building block irmão | Calendário, contatos, notetaker | LinkedIn, WhatsApp, Instagram, Telegram | Calendário, contatos, tarefas, sync de CRM | — |
| Multi-tenant com isolamento por organização | Nativo, do token | Você modela | Você modela | Você modela | Você modela |
| Maturidade | Beta, sem cliente em produção | Anos de produção, BAA e HIPAA | Produção, empresa mais nova | Produção | — |
Nossos diferenciais
- 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.
- 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.
- A mesma anatomia vale para o Calendar.
provider-config,connection,provider-subscription,provider-notificationewebhooksã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. - O tenant vem do token. Nenhuma rota lê
organizationIddo 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
| Driver | Por quê |
|---|---|
| Contas conectadas ativas | Cada uma consome quota do provedor e ocupa uma assinatura de push |
| Chamadas de API por conta | Listagem no Gmail hidrata cada mensagem individualmente — é a chamada mais cara do BB |
| Assinaturas de push ativas | Cada renovação é uma chamada ao provedor; no Gmail custa 100 unidades de quota |
| Entregas de webhook | Cada 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 Email | Nylas (Full Platform) | Unipile | Aurinko | Gmail + Graph direto | |
|---|---|---|---|---|---|
| Base | Precificação em definição | US$ 15/mês, 5 contas inclusas | € 49/mês mínimo | Sem base publicada | US$ 0 |
| Conta adicional | — | US$ 2,00/conta/mês | € 5,00/conta/mês (faixa 11–50) | US$ 1,00 a US$ 2,00/conta/mês | US$ 0 |
| Mensal em 200 contas | — | ≈ US$ 405 | Faixa acima de 50 contas não publicada | ≈ US$ 200 a US$ 400 | US$ 0 de licença |
| Avaliação CASA anual | Do cliente, se usar leitura no Gmail | Do fornecedor | Do fornecedor | Do fornecedor | Sua, anual e obrigatória |
| Engenharia de integração | Uma API | Uma API | Uma API | Uma API | Dois 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
statedo 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 nostate. 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 ostatenã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ópriostatecifrado: quem não tem aEMAIL_CREDENTIAL_MASTER_KEYnão produz umstateválido. - Renovação de token dentro da requisição, não em job.
getProviderForConnectionchecatokenExpiresAte 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.listdo Gmail devolve só identificadores; cada mensagem precisa de ummessages.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 opageSizeser 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
pageTokendo Outlook é uma URL inteira. O Graph devolve@odata.nextLinkcompleto, e o provider detecta que o token começa comhttpe 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_AUDIENCEestiver definida. Sem a variável, a notificação é aceita — modo de desenvolvimento. Em produção, defina a variável. - A chave de
externalIdda 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 umwatchativo —, 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
| Termo | Significa |
|---|---|
| Provider config | As 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. |
| Connection | Uma 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 subscription | A assinatura de push no provedor: subscription do Microsoft Graph ou watch do Gmail. Expira e precisa ser renovada. |
| Webhook subscription | A 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 message | O formato único de mensagem, independente de provedor. Traz corpo em texto e HTML, sinalizadores, rótulos e metadados de anexo. |
| Category | Triagem 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. |
| Folder | Pasta ou rótulo, normalizado em INBOX, SENT, DRAFTS, TRASH, SPAM, ARCHIVE ou CUSTOM. |
| Thread | Conversa. 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 Prisma | Tabela | Propósito | Campos-chave |
|---|---|---|---|
EmailProviderConfig | email.email_provider_configs | Credenciais OAuth da organização | credentials (cifrado), providerType, isDefault, isActive, único (organizationId, name) |
EmailConnection | email.email_connections | Conta autorizada de um usuário | externalEmail, accessToken e refreshToken (cifrados), tokenExpiresAt, status |
EmailProviderSubscription | email.email_provider_subscriptions | Assinatura de push no provedor | externalId (único), resource, clientState, expiresAt, isActive |
EmailWebhookSubscription | email.email_webhook_subscriptions | Para onde entregar o evento | callbackUrl, events[], secret, isActive |
EmailWebhookDeliveryLog | email.email_webhook_delivery_logs | Resultado de cada entrega | status, statusCode, payload, retryCount, nextRetryAt |
Enumerações
| Enum | Valores |
|---|---|
EmailProviderType | GOOGLE · OUTLOOK |
EmailConnectionStatus | ACTIVE · TOKEN_EXPIRED · REVOKED · ERROR |
EmailWebhookEventType | MESSAGE_RECEIVED · MESSAGE_SENT · MESSAGE_UPDATED · MESSAGE_DELETED |
EmailDeliveryStatus | PENDING · SUCCESS · FAILED · RETRYING |
EmailFolderType | INBOX · 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étodo | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /email/api/v1/email/provider-configs | Cadastra credenciais OAuth da organização | EMAIL_ADMIN |
GET | /email/api/v1/email/provider-configs | Lista as configurações | EMAIL_READ |
GET | /email/api/v1/email/provider-configs/:id | Busca uma configuração | EMAIL_READ |
PATCH | /email/api/v1/email/provider-configs/:id | Atualiza nome, credenciais, padrão ou ativação | EMAIL_ADMIN |
DELETE | /email/api/v1/email/provider-configs/:id | Exclusão lógica | EMAIL_ADMIN |
POST | /email/api/v1/email/provider-configs/:id/test | Valida o formato da configuração | EMAIL_ADMIN |
Conexões — /email/api/v1/email/connections
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /email/api/v1/email/connections/connect | Devolve a URL de autorização do provedor | EMAIL_WRITE |
GET | /email/api/v1/email/connections/callback | Callback do OAuth; redireciona ao redirectUrl | Pública — sem authMiddleware |
GET | /email/api/v1/email/connections | Lista as conexões do usuário do token | EMAIL_READ |
DELETE | /email/api/v1/email/connections/:id | Desconecta (marca REVOKED, exclusão lógica) | EMAIL_WRITE |
Mensagens — /email/api/v1/email/messages
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /email/api/v1/email/messages | Lista mensagens; filtros por query string | EMAIL_READ |
POST | /email/api/v1/email/messages/search | Busca com query longa no corpo da requisição | EMAIL_READ |
POST | /email/api/v1/email/messages | Envia mensagem ou salva rascunho | EMAIL_WRITE |
GET | /email/api/v1/email/messages/:id | Busca uma mensagem; exige ?connectionId= | EMAIL_READ |
POST | /email/api/v1/email/messages/:id/reply | Responde, com replyAll opcional | EMAIL_WRITE |
POST | /email/api/v1/email/messages/:id/forward | Encaminha | EMAIL_WRITE |
PATCH | /email/api/v1/email/messages/:id | Marca lida ou favorita, muda rótulos, move de pasta | EMAIL_WRITE |
DELETE | /email/api/v1/email/messages/:id | Move para a lixeira; ?permanent=true apaga de vez | EMAIL_WRITE |
GET | /email/api/v1/email/messages/:id/attachments/:attId | Baixa o anexo em base64; exige ?connectionId= | EMAIL_READ |
Conversas — /email/api/v1/email/threads
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /email/api/v1/email/threads | Lista conversas | EMAIL_READ |
GET | /email/api/v1/email/threads/:id | Conversa com todas as mensagens; exige ?connectionId= | EMAIL_READ |
PATCH | /email/api/v1/email/threads/:id | Aplica a alteração a todas as mensagens da conversa | EMAIL_WRITE |
DELETE | /email/api/v1/email/threads/:id | Apaga todas as mensagens da conversa; exige ?connectionId= | EMAIL_WRITE |
Pastas — /email/api/v1/email/folders
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /email/api/v1/email/folders | Lista pastas e rótulos; exige ?connectionId= | EMAIL_READ |
Webhooks do cliente — /email/api/v1/email/webhooks
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /email/api/v1/email/webhooks | Cria a assinatura de entrega | EMAIL_ADMIN |
GET | /email/api/v1/email/webhooks | Lista as assinaturas | EMAIL_READ |
PATCH | /email/api/v1/email/webhooks/:id | Atualiza URL, eventos, segredo ou ativação | EMAIL_ADMIN |
DELETE | /email/api/v1/email/webhooks/:id | Exclusão lógica | EMAIL_ADMIN |
Assinaturas de push no provedor — /email/api/v1/email/provider-subscriptions
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /email/api/v1/email/provider-subscriptions | Cria subscription no Graph ou watch no Gmail | EMAIL_ADMIN |
POST | /email/api/v1/email/provider-subscriptions/:id/renew | Estende a validade | EMAIL_ADMIN |
DELETE | /email/api/v1/email/provider-subscriptions/:id | Cancela no provedor e apaga a linha | EMAIL_ADMIN |
Notificação do provedor e utilitárias
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /email/api/v1/email/providers/notifications | Recebe o push do Graph e do Pub/Sub | Pública — validação é por provedor |
GET | /email/health | Sonda de saúde do serviço | Pública |
GET | /email/connected | Página de destino padrão pós-OAuth, para desenvolvimento | Pública |
/healthe/connectednã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
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
name | string (1–100) | Sim | Único dentro da organização |
providerType | GOOGLE | OUTLOOK | Sim | Provedor |
credentials | object<string,string> | Sim | client_id, client_secret, redirect_uri; no Outlook aceita tenant_id (padrão common) |
isDefault | boolean | Não | Se omitido, a primeira configuração da organização vira padrão |
isActive | boolean | Não | Padrão true |
settings | object | Não | Ajustes 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
| Status | Quando |
|---|---|
400 | Corpo reprovado no Zod, ou EMAIL_CREDENTIAL_MASTER_KEY ausente/malformada |
403 | Token sem organizationId, ou sem EMAIL_ADMIN |
409 | Já 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"]
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
configId | string (uuid) | Sim | Configuração de provedor a usar |
redirectUrl | string (url) | Sim | Para onde o BB devolve o navegador depois do callback |
scopes | string[] | Não | Sobrepõe os escopos padrão. Leia §15 antes de mexer aqui. |
Escopos padrão
| Provedor | Escopos padrão | Consequência |
|---|---|---|
GOOGLE | gmail.send, userinfo.email, openid | Envio apenas. Listar, ler, modificar e criar watch no Gmail falham por falta de escopo. |
OUTLOOK | User.Read, Mail.ReadWrite, Mail.Send, offline_access | Leitura e escrita completas na caixa. |
Resposta 200 — objeto simples, sem envelope data.
{ "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth?client_id=..." }
Erros
| Status | Quando |
|---|---|
400 | configId inexistente na organização, ou credenciais sem client_id/redirect_uri |
403 | Sem 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ção | Resposta |
|---|---|
| Sucesso | 302 para o redirectUrl |
Provedor devolveu error na query | 400 em text/plain com o erro e a descrição |
code ou state ausente | 400 Missing code or state parameter |
state não decifra ou está incompleto | 400 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âmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
connectionId | string (uuid) | Sim | Conexão a consultar |
folder | string | Não | Gmail: identificador de rótulo (INBOX). Outlook: nome bem conhecido de pasta (inbox, sentitems). |
labelIds | string ou string[] | Não | Aceita lista separada por vírgula. Só faz efeito no Gmail. |
query | string | Não | Sintaxe nativa do provedor — q do Gmail, $search do Graph. Não é traduzida. |
threadId | string | Não | Restringe à conversa |
pageSize | number (1–100) | Não | Padrão 25 |
pageToken | string | Não | Cursor da página anterior. No Outlook é a URL @odata.nextLink inteira. |
includeBody | boolean | Não | Padrã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
| Status | Quando |
|---|---|
400 | Query reprovada no Zod, conexão REVOKED, ou token expirado sem refresh token |
403 | Sem organizationId no token, ou sem EMAIL_READ |
404 | connectionId inexistente na organização do token |
500 | Falha 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
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
connectionId | string (uuid) | Sim | De qual conta sai a mensagem |
to | array (mín. 1) | Sim | Cada item com email e name opcional |
cc, bcc, replyTo | array | Não | Mesmo formato |
subject | string | Sim | Aceita string vazia |
bodyText, bodyHtml | string | Não | Informar os dois gera multipart/alternative |
attachments | array | Não | data em base64; aceita isInline e contentId |
inReplyToMessageId | string | Não | Identificador do provedor para encadear a conversa |
asDraft | boolean | Não | Padrão false. true cria rascunho sem enviar. |
Resposta 201 — { "data": <CanonicalEmailMessage> }.
Erros
| Status | Quando |
|---|---|
400 | Corpo reprovado no Zod, ou conexão REVOKED |
403 | Sem EMAIL_WRITE |
500 | Provedor 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.
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
connectionId | string (uuid) | Sim | Conta a observar |
resource | string | Não | Padrão /me/messages. Só o Outlook usa. |
notificationUrl | string (url) | Outlook | URL HTTPS pública para onde o Graph envia |
topicName | string | Gmail | projects/<projeto>/topics/<topico>; cai para EMAIL_GOOGLE_PUBSUB_TOPIC |
labelIds | string[] | Não | Gmail: restringe o watch. Padrão ["INBOX"]. |
expirationMinutes | number (10–10080) | Não | Outlook: 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.
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
callbackUrl | string (url) | Sim | Onde você recebe |
events | array | Sim | MESSAGE_RECEIVED, MESSAGE_SENT, MESSAGE_UPDATED, MESSAGE_DELETED |
secret | string (mín. 16) | Não | Se omitido, o serviço gera 32 bytes aleatórios |
description | string (máx. 500) | Não | Texto livre |
A entrega é um POST com estes cabeçalhos:
| Cabeçalho | Conteúdo |
|---|---|
X-Webhook-Signature | sha256=<HMAC-SHA256 do corpo bruto, com o segredo da assinatura> |
X-Webhook-Event | O tipo do evento |
X-Webhook-Id | Identificador 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
notificationUrltem que ser HTTPS e alcançável pela internet.localhostnã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
minutese depende deEMAIL_GOOGLE_PUBSUB_TOPICestar configurado — renovar é chamarwatchde novo, e o Google decide a nova expiração (cerca de 7 dias). - A Google recomenda chamar
watchuma 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,$searchdo Graph no Outlook. Não há tradução entre as duas. - O
attachmentsde uma listagem traz só metadados; o conteúdo só vem por este endpoint. - No Gmail, a busca por
has:attachmentexige 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.
PATCHde conversa aceitamarkRead,addLabels,removeLabelsemoveToFolder. Não aceitamarkStarred— favoritar é operação de mensagem.- No Gmail,
addLabelsespera identificadores de rótulo, não os nomes exibidos. Descubra-os emGET /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:
statusda conexão.REVOKEDdevolve400em qualquer operação.TOKEN_EXPIREDsignifica que o refresh token sumiu — o usuário precisa reconectar.- A mensagem do erro
500nas rotas do Gmail. O provider repassa o texto do Gmail, que costuma dizer explicitamente se é escopo insuficiente, quota estourada ou credencial revogada. - 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.
- Quota. Gmail: 6.000 unidades por minuto por usuário, e um
messages.getcusta 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. POST /provider-configs/:id/testvalida 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 oclient_secretestá certo.
12Integração com outros building blocks
| Building block | Como se relaciona | Obrigatório |
|---|---|---|
| IAM | Emite o token, define a organização e concede EMAIL_READ, EMAIL_WRITE e EMAIL_ADMIN | Sim |
| Calendar | Building block irmão: mesma anatomia de provider-config, connection, provider-subscription e provider-notification, com as mesmas credenciais Google e Microsoft | Não |
| File Storage | Destino natural dos anexos baixados; o Email entrega base64, o File Storage guarda | Não |
| Customers | Liga o endereço de e-mail da conversa à pessoa ou empresa cadastrada | Não |
| Webhooks Engine | Distribuição de eventos em escala; o Email tem entrega própria e simples, sem repetição automática (§15) | Não |
| Audit Trail | Registra quem conectou conta, quem leu caixa e quem enviou em nome de quem | Não |
| Data Extraction | Extrai dados estruturados do PDF que chegou anexado | Nã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ável | Descrição | Obrigatória | Padrão |
|---|---|---|---|
EMAIL_CREDENTIAL_MASTER_KEY | Chave AES-256-GCM. Exatamente 64 caracteres hexadecimais (32 bytes). Gere com openssl rand -hex 32. | Sim, para usar o BB | — |
EMAIL_GOOGLE_PUBSUB_TOPIC | Tó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_AUDIENCE | aud esperado no token OIDC que o Pub/Sub anexa. Sem ela a verificação da notificação é pulada — defina em produção. | Recomendada | — |
DATABASE_URL | PostgreSQL com o schema email | Sim | — |
JWT_SECRET | Verificação do token do IAM. Mínimo 44 caracteres. | Sim | — |
MODULE_IAM_URL | Endereço do IAM em standalone | Em standalone | — |
MODULE_EMAIL_URL | Endereço do Email para os outros building blocks | Não | '' |
DEPLOYMENT_MODE | monolith ou standalone | Não | standalone no main.ts |
PORT | Porta de escuta em standalone | Não | 3000 |
Dependências de infraestrutura
| Dependência | Para quê |
|---|---|
| PostgreSQL | Schema email — configurações, conexões, assinaturas e logs de entrega |
| IAM | Emissão e verificação do token com organização e permissões |
| Google Cloud Pub/Sub | Só para push do Gmail: tópico, subscription e permissão de publicação para o Gmail |
| Endpoint HTTPS público | Só 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
| Limite | Valor | Origem |
|---|---|---|
pageSize de mensagens e conversas | 1 a 100, padrão 25 | Schema Zod deste BB |
expirationMinutes de assinatura | 10 a 10.080 | Schema Zod deste BB |
| Padrão de expiração no Outlook | 4.320 minutos (3 dias) | Padrão deste BB |
| Máximo de assinatura para mensagem no Graph | 10.080 minutos; 1.440 com dados do recurso | Microsoft |
Validade do watch do Gmail | 7 dias; renovação diária recomendada | |
| Quota do Gmail | 6.000 unidades/minuto por usuário; messages.get = 20, messages.send = 100, users.watch = 100 | |
| Throttling do Outlook via Graph | 10.000 requisições/10 min e 4 concorrentes por aplicativo e caixa | Microsoft |
| Tempo limite de entrega de webhook | 30 segundos | Código deste BB |
| Comprimento do segredo de webhook | Mínimo 16 caracteres | Schema Zod deste BB |
Catálogo de erros
| Status | Código | Significa | O que fazer |
|---|---|---|---|
400 | VALIDATION | Corpo ou query reprovados no Zod | Confira campos e tipos contra §9 |
400 | VALIDATION | This email connection has been revoked | A conexão foi desconectada; refaça o fluxo OAuth |
400 | VALIDATION | Token expired and no refresh token available | O provedor não devolveu refresh token; reconecte com access_type=offline |
400 | VALIDATION | Invalid callback state | state corrompido, ou a chave mestra mudou desde o início do fluxo |
400 | VALIDATION | Cannot delete the only email provider config | Crie outra configuração antes de apagar a única |
400 | VALIDATION | Invalid notification signature | clientState da notificação não confere; verifique a origem |
403 | — | Organization context required | Autentique com token que carrega organizationId |
403 | FORBIDDEN | Falta EMAIL_READ, EMAIL_WRITE ou EMAIL_ADMIN | Ajuste o papel no IAM e emita token novo |
404 | NOT_FOUND | EmailConnection, EmailProviderConfig ou assinatura inexistente na organização do token | Confira o identificador e a organização |
409 | CONFLICT | Nome de configuração de provedor já usado | Escolha outro nome |
500 | INTERNAL | Failed to list Gmail messages: <texto do Gmail> | Leia o texto repassado: costuma nomear escopo, quota ou token |
500 | INTERNAL | Failed to decrypt connection tokens | EMAIL_CREDENTIAL_MASTER_KEY mudou; os dados cifrados com a chave antiga são irrecuperáveis |
Observabilidade.
GET /email/healthdevolve nome do serviço e versão do build. É uma sonda de processo vivo: não testa banco nem provedor.POST /provider-configs/:id/testvalida 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 subscriptioncomlogger.warnpor 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_logscom 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 gravado | Onde | Por quê |
|---|---|---|
| Endereço de e-mail da conta conectada | email_connections.external_email | Identificar a conexão para o usuário |
| Endereço de e-mail da conta observada | email_provider_subscriptions.external_id, no Gmail | É a chave que reconcilia a notificação com a assinatura |
| Identificadores da notificação do provedor | email_webhook_delivery_logs.payload | Auditar 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ção | Impacto | Situação |
|---|---|---|
| Gmail nasce somente com permissão de envio | Os 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ção | O 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 Outlook | Nã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 sozinho | renewExpiringSubscriptions() 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ática | Entrega 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 limite | email_webhook_delivery_logs não tem expurgo nem particionamento. | Roadmap |
| Reconectar cria uma conexão nova | O 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 provedor | DELETE /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ário | GET /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 Outlook | PATCH 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+1 | messages.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 provedores | O 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 incremental | Nã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ído | EmailConnectionStatus.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 provedor | O 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ções | GET /connections e GET /provider-configs devolvem tudo, sem cursor. | Aceitável no volume atual |
| Anexo trafega inteiro em base64 | Sem streaming nem URL assinada; um anexo de 20 MB vira quase 27 MB de JSON na memória. | Roadmap |
/health não sonda dependência | Devolve 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