{
  "id": "e-mails-notificacoes-e-entregas",
  "title": "E-mails, notificações e entregas",
  "description": "Contrato atual das notificações internas, mensagens, tentativas e consulta administrativa de e-mails.",
  "type": "technical-reference",
  "status": "in-progress",
  "visibility": "public",
  "tags": [
    "sge/planejamento",
    "sge/email",
    "sge/notificacoes",
    "sge/auditoria"
  ],
  "related": [
    "enums",
    "migrations",
    "dominio-e-modelo-de-dados",
    "fluxos-principais"
  ],
  "sourceRefs": [
    "https://github.com/sge-suite/sge/blob/master/app/Actions/RequestEmailDelivery.php",
    "https://github.com/sge-suite/sge/blob/master/app/Jobs/SendEmailDelivery.php",
    "https://github.com/sge-suite/sge/blob/master/app/Mail/DeliveryMail.php",
    "https://github.com/sge-suite/sge/blob/master/app/Models/EmailMessage.php",
    "https://github.com/sge-suite/sge/blob/master/app/Models/EmailDeliveryAttempt.php",
    "https://github.com/sge-suite/sge/blob/master/app/Policies/EmailDeliveryAttemptPolicy.php",
    "https://github.com/sge-suite/sge/blob/master/app/Support/AdministrativeEmailLogScope.php",
    "https://github.com/sge-suite/sge/blob/master/app/Support/EmailLogAccess.php",
    "https://github.com/sge-suite/sge/blob/master/tests/Feature/EmailDeliveryFlowTest.php",
    "https://github.com/sge-suite/sge/blob/master/tests/Feature/EmailLogInterfaceTest.php"
  ],
  "authors": [],
  "updated": null,
  "diagram": "mensageria-fluxo",
  "body": "> [!abstract] Decisão de planejamento\n> As Migrations 05–07, o backend de envio em fila e as telas de consulta administrativa estão implementados. O Administrador do Sistema consulta mensagens ligadas a contas e vínculos administrativos, inclusive quando o destinatário é externo. A tela mostra o conteúdo salvo e as tentativas, sem ação de reenvio. A integração de notificações operacionais nos fluxos de domínio, os escopos para outros vínculos e a política de retenção continuam pendentes.\n\n## Objetivo e limites\n\nO SGE mantém trilhas separadas para notificações exibidas no sistema, mensagens de e-mail e tentativas de transporte. Assim, reenvios e falhas não sobrescrevem o histórico nem fazem uma notificação parecer enviada quando o provedor recusou a mensagem.\n\nO termo **enviado** neste plano significa que o provedor SMTP aceitou a mensagem. Confirmação de abertura ou leitura do e-mail não faz parte do escopo inicial. A leitura da notificação interna continua independente, controlada exclusivamente por `read_at`.\n\n{{diagram:mensageria-fluxo}}\n\n## Estruturas de persistência\n\n### `notifications`\n\nUsa a tabela nativa plural do Laravel, com `data` em `jsonb`. Cada linha representa uma notificação destinada a uma conta ou a um vínculo dentro do sistema.\n\n| Campo                               | Tipo conceitual    | Finalidade                                                                 |\n| ----------------------------------- | ------------------ | -------------------------------------------------------------------------- |\n| `id`                                | uuid               | Identificador compatível com Notifications do Laravel.                     |\n| `notifiable_type` / `notifiable_id` | morph              | `Affiliation` para operação; `User` somente quando um aviso interno da conta for definido. |\n| `type`                              | string             | Classe/tipo estável da notificação.                                        |\n| `data`                              | jsonb              | JSON convertido pelo cast nativo `array`, com título, texto interno, rota/entidade e metadados não sensíveis. |\n| `read_at`                           | timestamp nullable | Leitura no SGE; não representa leitura do e-mail.                          |\n| `created_at` / `updated_at`         | timestamp          | Auditoria temporal.                                                        |\n\nNotificações de estágio, vínculo, avaliação e demais eventos operacionais serão criadas no vínculo destinatário antes de seguirem por e-mail. A caixa de notificações por vínculo já usa `Affiliation::notifications()`; `AffiliationPolicy::viewNotifications` exige que o vínculo ativo pertença à conta autenticada, e a relação não mistura caixas de vínculos diferentes. Ainda faltam as classes que criam essas notificações nos fluxos de domínio. Recuperação de senha e convite inicial não criam registros internos em `notifications`.\n\nO schema nativo não recebe coluna de deduplicação. Para evento reexecutável, a Action precisa persistir e consultar a fonte de idempotência do domínio. `email_messages.idempotency_key` cobre a mensagem de e-mail; um aviso somente interno que exigir deduplicação durável precisa de um registro operacional próprio antes de o fluxo ser ativado.\n\nO aviso de documento disponível para assinatura será uma exceção controlada por seleção humana: ao mover o documento para `awaiting_signature`, o Setor escolherá os interessados elegíveis. Para cada selecionado com conta, o plano prevê uma `notification` interna e uma `email_message`; para o contato externo da concedente, a modelagem de conteúdo e destinatário ainda precisa ser definida. O Model atual aceita mensagens operacionais ligadas a notificações e avisos de alteração de e-mail da conta; a seleção externa exige entidade, motivo e autorização implementados antes do envio. Não haverá caixa de texto para destinatário livre.\n\n### `email_messages`\n\nGuarda o conteúdo imutável já renderizado das finalidades que têm histórico próprio: notificações operacionais e avisos de conta criada, novo vínculo, alteração do e-mail da conta ou outra alteração administrativa. Inclui `notification_id` quando há notificação, finalidade, assunto, texto/HTML, identificadores opcionais do template e `idempotency_key`. O conteúdo é armazenado sem cast criptografado. Destinatário, solicitante e contexto do registro afetado ficam na tentativa. Recuperação de senha não cria mensagem nem tentativa.\n\n### `email_delivery_attempts`\n\nCada linha representa uma tentativa de transporte. `recipient_email` registra o destinatário efetivo, que pode não pertencer a uma conta do SGE. `purpose` identifica a finalidade. Novos envios das finalidades administrativas têm `email_message_id` ligado ao conteúdo imutável; tentativas antigas de conta criada ou novo vínculo podem permanecer sem mensagem. `scope_context` guarda um snapshot de `user_id`, `affiliation_ids`, `affiliation_types` e `campus_ids` do registro afetado; não representa o último vínculo selecionado. `delivery_key` é uma chave UUID estável do envio; ela permite idempotência e até três tentativas com números únicos. `requested_by_affiliation_id` registra o vínculo solicitante, quando houver; para envio automático, fica nulo. A tentativa mantém número, estado `queued`, `sent` ou `failed`, provedor, identificador do provedor, marcos temporais e motivo técnico sanitizado de falha.\n\nO Model bloqueia exclusão e mudança de uma tentativa finalizada. `RequestEmailDelivery` reserva a tentativa e despacha `SendEmailDelivery` depois do commit; o Job envia com o mailer nativo e registra o resultado. O reprocessamento explícito usa a mesma Action. Mensagens e tentativas não usam `LogsActivity`: são o histórico técnico de entrega. O acesso ao banco deve impedir alterações diretas que contornem essas regras.\n\n## Regras por finalidade\n\n| Finalidade | Persistência | Conteúdo |\n| --- | --- | --- |\n| Recuperação de senha | Nenhuma linha em `email_messages` ou `email_delivery_attempts`. | Token, URL, destinatário e corpo não são registrados nessas tabelas. |\n| Conta criada com primeiro vínculo | Uma mensagem imutável e uma tentativa `account_created`. Registros antigos podem ter `email_message_id` nulo. | Informa a criação da conta e do primeiro vínculo; leva à solicitação de definição da senha sem guardar token ou URL assinada. |\n| Novo vínculo em conta existente | Uma mensagem imutável e uma tentativa `new_affiliation` por endereço avisado. | Avisa o e-mail da conta e o do vínculo, deduplicando quando iguais, e leva ao login. |\n| Alteração de e-mail da conta | Uma mensagem imutável e uma tentativa por endereço avisado. | A tela de Segurança reserva dois avisos, para os e-mails anterior e novo, com o vínculo ativo como solicitante. O conteúdo renderizado inclui os endereços e permanece igual em cada tentativa. |\n| Alteração administrativa | Uma mensagem imutável e uma tentativa `administrative_change` por endereço avisado. | Registra conteúdo dos avisos de desativação/exclusão de vínculo e exclusão de conta; o contexto aponta para a conta ou vínculo afetado. |\n| Notificação operacional | `notifications`, `email_messages` e tentativas. | Guarda assunto e conteúdo renderizado na mensagem; destinatário e transporte na tentativa. |\n| Resumo interno do Setor | Apenas `notifications`. | Conteúdo interno pertinente ao vínculo. |\n\n> [!warning] Segredos não entram no histórico\n> Senhas, tokens, URLs assinadas e credenciais SMTP não devem ser persistidos em mensagens, tentativas, notificações ou Activity Log.\n\nO e-mail de conta criada leva à tela de recuperação com o e-mail preenchido. A pessoa então solicita o link para definir a senha. Essa notificação do Fortify é enfileirada com payload cifrado, pois a fila técnica precisa transportar temporariamente o token; ela não cria histórico nas tabelas de e-mail da aplicação.\n\n## Fluxos planejados\n\n### Recuperação de senha\n\n{{diagram:recuperacao-de-senha-fluxo}}\n\n### Notificação operacional por e-mail\n\n{{diagram:notificacao-operacional-fluxo}}\n\n## Implementação e pendências\n\nAs estruturas e o pipeline abaixo já existem; esta lista mostra o que está implementado e o que falta integrar. A migration de `notifications` não declara FKs porque `notifiable_type`/`notifiable_id` são polimórficos; a FK de `email_messages` depende de `notifications`; as referências de `email_delivery_attempts` dependem de `affiliations`.\n\n1. [x] Implementar os enums de finalidade e status; ambos já têm testes unitários.\n2. [x] Criar a migration nativa de `notifications` pelo gerador do Laravel e adaptar `data` para `jsonb`; adicionar `Notifiable` a `Affiliation` e cobrir a relação e a Policy por testes PostgreSQL.\n3. [x] Criar as migrations de mensagens e tentativas com suas FKs.\n4. [x] Implementar Models, relações, validações e factories próprios de e-mail.\n5. [x] Manter o envio de recuperação do Fortify fora das tabelas de mensagens e tentativas.\n6. [x] Definir o convite inicial sem envio de senha; o link abre a tela de recuperação com o e-mail preenchido.\n7. [x] Criar o backend comum de reserva e envio em fila, com conteúdo persistido apenas nas finalidades que o exigem.\n8. [x] Configurar Jobs, timeout, reprocessamento explícito e motivo de falha sanitizado. O reprocessamento é pela Action; a interface de consulta não oferece reenvio.\n9. [x] Cobrir os fluxos implementados, idempotência, reenvio, renderização e proteção de segredos. Os testes não entregam mensagens ao Mailpit.\n10. [x] Disponibilizar índice e detalhes de envios para o Administrador do Sistema, revalidando o vínculo ativo e selecionado pela Policy em cada requisição.\n11. [x] Filtrar por registro afetado via `scope_context`, incluindo contas e vínculos administrativos sem inferir escopo pelo destinatário ou solicitante.\n12. [x] Mostrar o conteúdo persistido e as tentativas em visualização isolada; não expor segredos nem permitir reenvio na interface.\n13. [ ] Definir escopos de consulta para outros tipos de vínculo e a política institucional de retenção.\n\n## Pendência de retenção\n\nA duração de retenção de `email_messages`, tentativas e conteúdo precisa seguir a política institucional de auditoria e LGPD. Até essa decisão, o acesso deve ser mínimo, auditado e permitido apenas a perfis administrativos autorizados; limpeza automática não deve ser implementada sem a definição formal de prazo.\n\nVeja também [Enums](doc:enums), [Migrations](doc:migrations), [Painel de desenvolvimento](doc:painel-de-desenvolvimento), [Domínio e modelo de dados](doc:dominio-e-modelo-de-dados) e [Fluxos principais](doc:fluxos-principais).",
  "sections": [
    {
      "id": "objetivo-e-limites",
      "level": 2,
      "title": "Objetivo e limites",
      "text": "O SGE mantém trilhas separadas para notificações exibidas no sistema, mensagens de e-mail e tentativas de transporte. Assim, reenvios e falhas não sobrescrevem o histórico nem fazem uma notificação parecer enviada quando o provedor recusou a mensagem.  O termo **enviado** neste plano significa que o provedor SMTP aceitou a mensagem. Confirmação de abertura ou leitura do e-mail não faz parte do escopo inicial. A leitura da notificação interna continua independente, controlada exclusivamente por `read_at`.",
      "line": 4
    },
    {
      "id": "estruturas-de-persistencia",
      "level": 2,
      "title": "Estruturas de persistência",
      "text": "",
      "line": 12
    },
    {
      "id": "notifications",
      "level": 3,
      "title": "notifications",
      "text": "Usa a tabela nativa plural do Laravel, com `data` em `jsonb`. Cada linha representa uma notificação destinada a uma conta ou a um vínculo dentro do sistema.  | Campo                               | Tipo conceitual    | Finalidade                                                                 | | ----------------------------------- | ------------------ | -------------------------------------------------------------------------- | | `id`                                | uuid               | Identificador compatível com Notifications do Laravel.                     | | `notifiable_type` / `notifiable_id` | morph              | `Affiliation` para operação; `User` somente quando um aviso interno da conta for definido. | | `type`                              | string             | Classe/tipo estável da notificação.                                        | | `data`                              | jsonb              | JSON convertido pelo cast nativo `array`, com título, texto interno, rota/entidade e metadados não sensíveis. | | `read_at`                           | timestamp nullable | Leitura no SGE; não representa leitura do e-mail.                          | | `created_at` / `updated_at`         | timestamp          | Auditoria temporal.                                                        |  Notificações de estágio, vínculo, avaliação e demais eventos operacionais serão criadas no vínculo destinatário antes de seguirem por e-mail. A caixa de notificações por vínculo já usa `Affiliation::notifications()`; `AffiliationPolicy::viewNotifications` exige que o vínculo ativo pertença à conta autenticada, e a relação não mistura caixas de vínculos diferentes. Ainda faltam as classes que criam essas notificações nos fluxos de domínio. Recuperação de senha e convite inicial não criam registros internos em `notifications`.  O schema nativo não recebe coluna de deduplicação. Para evento reexecutável, a Action precisa persistir e consultar a fonte de idempotência do domínio. `email_messages.idempotency_key` cobre a mensagem de e-mail; um aviso somente interno que exigir deduplicação durável precisa de um registro operacional próprio antes de o fluxo ser ativado.  O aviso de documento disponível para assinatura será uma exceção controlada por seleção humana: ao mover o documento para `awaiting_signature`, o Setor escolherá os interessados elegíveis. Para cada selecionado com conta, o plano prevê uma `notification` interna e uma `email_message`; para o contato externo da concedente, a modelagem de conteúdo e destinatário ainda precisa ser definida. O Model atual aceita mensagens operacionais ligadas a notificações e avisos de alteração de e-mail da conta; a seleção externa exige entidade, motivo e autorização implementados antes do envio. Não haverá caixa de texto para destinatário livre.",
      "line": 14
    },
    {
      "id": "email-messages",
      "level": 3,
      "title": "email_messages",
      "text": "Guarda o conteúdo imutável já renderizado das finalidades que têm histórico próprio: notificações operacionais e avisos de conta criada, novo vínculo, alteração do e-mail da conta ou outra alteração administrativa. Inclui `notification_id` quando há notificação, finalidade, assunto, texto/HTML, identificadores opcionais do template e `idempotency_key`. O conteúdo é armazenado sem cast criptografado. Destinatário, solicitante e contexto do registro afetado ficam na tentativa. Recuperação de senha não cria mensagem nem tentativa.",
      "line": 33
    },
    {
      "id": "email-delivery-attempts",
      "level": 3,
      "title": "email_delivery_attempts",
      "text": "Cada linha representa uma tentativa de transporte. `recipient_email` registra o destinatário efetivo, que pode não pertencer a uma conta do SGE. `purpose` identifica a finalidade. Novos envios das finalidades administrativas têm `email_message_id` ligado ao conteúdo imutável; tentativas antigas de conta criada ou novo vínculo podem permanecer sem mensagem. `scope_context` guarda um snapshot de `user_id`, `affiliation_ids`, `affiliation_types` e `campus_ids` do registro afetado; não representa o último vínculo selecionado. `delivery_key` é uma chave UUID estável do envio; ela permite idempotência e até três tentativas com números únicos. `requested_by_affiliation_id` registra o vínculo solicitante, quando houver; para envio automático, fica nulo. A tentativa mantém número, estado `queued`, `sent` ou `failed`, provedor, identificador do provedor, marcos temporais e motivo técnico sanitizado de falha.  O Model bloqueia exclusão e mudança de uma tentativa finalizada. `RequestEmailDelivery` reserva a tentativa e despacha `SendEmailDelivery` depois do commit; o Job envia com o mailer nativo e registra o resultado. O reprocessamento explícito usa a mesma Action. Mensagens e tentativas não usam `LogsActivity`: são o histórico técnico de entrega. O acesso ao banco deve impedir alterações diretas que contornem essas regras.",
      "line": 37
    },
    {
      "id": "regras-por-finalidade",
      "level": 2,
      "title": "Regras por finalidade",
      "text": "| Finalidade | Persistência | Conteúdo | | --- | --- | --- | | Recuperação de senha | Nenhuma linha em `email_messages` ou `email_delivery_attempts`. | Token, URL, destinatário e corpo não são registrados nessas tabelas. | | Conta criada com primeiro vínculo | Uma mensagem imutável e uma tentativa `account_created`. Registros antigos podem ter `email_message_id` nulo. | Informa a criação da conta e do primeiro vínculo; leva à solicitação de definição da senha sem guardar token ou URL assinada. | | Novo vínculo em conta existente | Uma mensagem imutável e uma tentativa `new_affiliation` por endereço avisado. | Avisa o e-mail da conta e o do vínculo, deduplicando quando iguais, e leva ao login. | | Alteração de e-mail da conta | Uma mensagem imutável e uma tentativa por endereço avisado. | A tela de Segurança reserva dois avisos, para os e-mails anterior e novo, com o vínculo ativo como solicitante. O conteúdo renderizado inclui os endereços e permanece igual em cada tentativa. | | Alteração administrativa | Uma mensagem imutável e uma tentativa `administrative_change` por endereço avisado. | Registra conteúdo dos avisos de desativação/exclusão de vínculo e exclusão de conta; o contexto aponta para a conta ou vínculo afetado. | | Notificação operacional | `notifications`, `email_messages` e tentativas. | Guarda assunto e conteúdo renderizado na mensagem; destinatário e transporte na tentativa. | | Resumo interno do Setor | Apenas `notifications`. | Conteúdo interno pertinente ao vínculo. |  > [!warning] Segredos não entram no histórico > Senhas, tokens, URLs assinadas e credenciais SMTP não devem ser persistidos em mensagens, tentativas, notificações ou Activity Log.  O e-mail de conta criada leva à tela de recuperação com o e-mail preenchido. A pessoa então solicita o link para definir a senha. Essa notificação do Fortify é enfileirada com payload cifrado, pois a fila técnica precisa transportar temporariamente o token; ela não cria histórico nas tabelas de e-mail da aplicação.",
      "line": 43
    },
    {
      "id": "fluxos-planejados",
      "level": 2,
      "title": "Fluxos planejados",
      "text": "",
      "line": 60
    },
    {
      "id": "recuperacao-de-senha",
      "level": 3,
      "title": "Recuperação de senha",
      "text": "",
      "line": 62
    },
    {
      "id": "notificacao-operacional-por-e-mail",
      "level": 3,
      "title": "Notificação operacional por e-mail",
      "text": "",
      "line": 66
    },
    {
      "id": "implementacao-e-pendencias",
      "level": 2,
      "title": "Implementação e pendências",
      "text": "As estruturas e o pipeline abaixo já existem; esta lista mostra o que está implementado e o que falta integrar. A migration de `notifications` não declara FKs porque `notifiable_type`/`notifiable_id` são polimórficos; a FK de `email_messages` depende de `notifications`; as referências de `email_delivery_attempts` dependem de `affiliations`.  1. [x] Implementar os enums de finalidade e status; ambos já têm testes unitários. 2. [x] Criar a migration nativa de `notifications` pelo gerador do Laravel e adaptar `data` para `jsonb`; adicionar `Notifiable` a `Affiliation` e cobrir a relação e a Policy por testes PostgreSQL. 3. [x] Criar as migrations de mensagens e tentativas com suas FKs. 4. [x] Implementar Models, relações, validações e factories próprios de e-mail. 5. [x] Manter o envio de recuperação do Fortify fora das tabelas de mensagens e tentativas. 6. [x] Definir o convite inicial sem envio de senha; o link abre a tela de recuperação com o e-mail preenchido. 7. [x] Criar o backend comum de reserva e envio em fila, com conteúdo persistido apenas nas finalidades que o exigem. 8. [x] Configurar Jobs, timeout, reprocessamento explícito e motivo de falha sanitizado. O reprocessamento é pela Action; a interface de consulta não oferece reenvio. 9. [x] Cobrir os fluxos implementados, idempotência, reenvio, renderização e proteção de segredos. Os testes não entregam mensagens ao Mailpit. 10. [x] Disponibilizar índice e detalhes de envios para o Administrador do Sistema, revalidando o vínculo ativo e selecionado pela Policy em cada requisição. 11. [x] Filtrar por registro afetado via `scope_context`, incluindo contas e vínculos administrativos sem inferir escopo pelo destinatário ou solicitante. 12. [x] Mostrar o conteúdo persistido e as tentativas em visualização isolada; não expor segredos nem permitir reenvio na interface. 13. [ ] Definir escopos de consulta para outros tipos de vínculo e a política institucional de retenção.",
      "line": 70
    },
    {
      "id": "pendencia-de-retencao",
      "level": 2,
      "title": "Pendência de retenção",
      "text": "A duração de retenção de `email_messages`, tentativas e conteúdo precisa seguir a política institucional de auditoria e LGPD. Até essa decisão, o acesso deve ser mínimo, auditado e permitido apenas a perfis administrativos autorizados; limpeza automática não deve ser implementada sem a definição formal de prazo.  Veja também [Enums](doc:enums), [Migrations](doc:migrations), [Painel de desenvolvimento](doc:painel-de-desenvolvimento), [Domínio e modelo de dados](doc:dominio-e-modelo-de-dados) e [Fluxos principais](doc:fluxos-principais).",
      "line": 88
    }
  ],
  "sourcePath": "content/e-mails-notificacoes-e-entregas.md",
  "visuals": [
    {
      "id": "mensageria-fluxo",
      "kind": "flowchart",
      "title": "Trilha de notificações e e-mails",
      "description": "Como eventos do domínio geram notificações, mensagens preparadas e tentativas de entrega.",
      "summary": "Um evento do domínio pode criar uma notificação e uma mensagem; cada mensagem é enviada por um Job, e o resultado do SMTP fica registrado como tentativa enviada ou falha, com reenvio seguro.",
      "renderMode": "mermaid",
      "api": "api/diagrams/mensageria-fluxo.json",
      "human": "diagrams/mensageria-fluxo.html"
    },
    {
      "id": "notificacao-operacional-fluxo",
      "kind": "flowchart",
      "title": "Fluxo da notificação operacional",
      "description": "Decisão de destinatário, persistência e entrega de um aviso de domínio.",
      "summary": "Após um evento autorizado, o sistema verifica se há conta, cria a notificação interna quando aplicável e prepara a mensagem depois do commit; a entrega passa por uma tentativa rastreável.",
      "renderMode": "mermaid",
      "api": "api/diagrams/notificacao-operacional-fluxo.json",
      "human": "diagrams/notificacao-operacional-fluxo.html"
    },
    {
      "id": "recuperacao-de-senha-fluxo",
      "kind": "flowchart",
      "title": "Fluxo de recuperação de senha",
      "description": "Caminho seguro da solicitação de recuperação até o resultado do envio.",
      "summary": "O Fortify cria o token de uso único, mas o log guarda apenas metadados seguros; a mensagem é preparada, uma tentativa é enfileirada e o SMTP determina sucesso ou falha.",
      "renderMode": "mermaid",
      "api": "api/diagrams/recuperacao-de-senha-fluxo.json",
      "human": "diagrams/recuperacao-de-senha-fluxo.html"
    }
  ],
  "apiVersion": 1
}
