---
id: MOD-INFRAESTRUTURA-MAILAUDIT
tipo: modulo
onda: 4
status: concluido
unidades: [MOD-INFRAESTRUTURA-MAILAUDIT]
fontes:
  - V2/Modules/Infraestrutura/MailAudit/
depende_de: [CORE-AUTH, CORE-CMS, SVC-MAILSERVICE]
consumidores: []
criticidade: media
dono: Infra
---

# Infraestrutura/MailAudit

> Leitor somente-leitura do log operacional de e-mails (`v2_mail_logs`) produzido pelo
> `SVC-MAILSERVICE`. Não envia, reenvia, edita nem apaga e-mails: lista os envios do
> sistema, filtra em memória e exibe o corpo integral da mensagem — inclusive dados
> sensíveis — para quem tiver `MailAudit:read`.

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Um operador autorizado vê os últimos envios de e-mail do sistema, com destinatário, assunto, status, erro e corpo completo, e abre um registro para detalhe. |
| Entrada | `GET /cms/mail-audit` com `search`, `status`, `profile`; `GET /cms/mail-audit/detail/{id}`. |
| Saída | 2 telas HTML; estatísticas globais (total, enviados, falhas, taxa); tabela de até 50 registros; detalhe com metadados, erro e corpo; `302` (ou `403` genérico em AJAX) do middleware e `404` com mensagem exata. |
| Efeitos colaterais | Nenhum: o módulo só lê `v2_mail_logs` e não grava auditoria, cache ou arquivo. |
| Particularidade central | O detalhe busca `getHistory(1)` e depois `firstWhere('id', $id)`: **somente o registro mais recente** pode ser aberto; qualquer outro id responde `404`, mesmo existindo na listagem. |
| Como se integra | Rotas `v2.mailaudit.*`; view namespace `v2-mailaudit`; controller injeta `MailService` e chama `getHistory($limit, $profile)`. |
| Se removido, quebra | A única visualização do histórico de e-mails no CMS; `v2_mail_logs` continua sendo gravada pelo `MailService`, mas fica inacessível pela UI. |
| Superfície pública | `MailAuditCmsController` (`index`, `detail`), `ModuleProvider::getConfig()`, 2 rotas, 2 blades. |

## 1. Efeito

### 1.1 O que o usuário consegue fazer

- **Ver o painel** com quatro indicadores: total de envios (`sent+failed`), enviados,
  com falha e taxa de sucesso (`Views/cms/index.blade.php:44-101`).
- **Filtrar** por status (`sent`/`failed`), por profile SMTP e por texto livre em
  destinatário, assunto ou remetente (`MailAuditCmsController.php:44-65`).
- **Listar** até 50 registros (após o filtro) dos 200 mais recentes, com data,
  destinatário, assunto, status e link para detalhe (`:47-81`).
- **Abrir o detalhe** de um envio: remetente, destinatário, profile, data de envio,
  Message ID, erro e corpo da mensagem (prévia + HTML completo), além de ID e datas
  de criação/atualização (`Views/cms/detail.blade.php:45-134`).
- **Ver o erro inline** na listagem para registros `failed`, em uma linha extra da
  tabela (`Views/cms/index.blade.php:204-211`).

### 1.2 O que muda no sistema

| Efeito | Onde é produzido | Fonte |
|---|---|---|
| Nenhuma escrita: apenas `SELECT` em `v2_mail_logs` | `MailService::getHistory` | `V2/Services/MailService/MailService.php:277-289` |
| Nenhuma auditoria da consulta (não há `activity_log`) | ausência de chamada | `MailAuditCmsController.php:38-112` |
| Nenhum cache de página (rotas `cms*` são excluídas do `page.cache`) | `PageCacheMiddleware::$except` | `V2/Core/Middleware/PageCacheMiddleware.php:15-21` |
| Nenhum e-mail, fila, arquivo ou índice de busca | ausência de chamadas | `MailAuditCmsController.php` |

O módulo é estritamente passivo. A escrita em `v2_mail_logs` é do `MailService`
(`logMail`), inclusive em falha (`V2/Services/MailService/MailService.php:117-132,360-378`).

### 1.3 Contrato observável (entrada → saída)

| Entrada | Resultado |
|---|---|
| `GET /cms/mail-audit` com `MailAudit:read` | `200` com estatísticas e até 50 registros. |
| Sem `MailAudit:read` (perfil **comum** com `none`) | O middleware bloqueia antes: `302` dashboard + `Acesso negado. Seu perfil não possui permissão para acessar o módulo: Auditoria de E-mails`; AJAX/JSON: `403` `Forbidden. Access denied for this module.` (`V2PermissionMiddleware.php:84-99`). Suporte e `super-admin` não são bloqueados. |
| `MailAudit:read` ausente **alcançando o controller** | `403` `Acesso negado. Você não tem permissão para acessar a auditoria de e-mails.` — **inalcançável por HTTP normal** (defesa em profundidade): `none` é bloqueado no middleware e `write` implica `read`; só ocorre se o middleware não casar a rota (`MailAuditCmsController.php:40-42`). |
| `?status=sent` | Filtra a coleção por `status === 'sent'`. |
| `?status=failed` | Filtra por `status === 'failed'` e exibe a linha de erro. |
| `?status=0` | `!empty('0')` é falso em PHP, então o filtro **não** é aplicado e a listagem normal é devolvida. |
| `?status=qualquer` (não vazio e ≠ `sent`/`failed`) | Nenhum resultado (comparação exata). |
| `?profile=contact` | Filtra no SQL por `profile = 'contact'`; o select da tela só oferece `default`. |
| `?search=foo` | Filtra em PHP por substring case-insensitive em `to_address`, `subject` e `from_address`. |
| `?search=` vazio | Não filtra. |
| Mais de 200 registros | A listagem só considera os 200 mais recentes e depois corta em 50. |
| Filtro/status sem resultados | Estado vazio “Nenhum envio encontrado” / “Nenhum resultado para os filtros aplicados.” (`Views/cms/index.blade.php:141-152`). |
| `GET /cms/mail-audit/detail/{id}` do registro mais recente | `200` com o detalhe. |
| `GET /cms/mail-audit/detail/{id}` de qualquer outro registro | `404` `Registro de e-mail não encontrado.` (`MailAuditCmsController.php:97-102`). |
| `GET /cms/mail-audit/detail/{id}` com perfil `none` | Mesmo bloqueio do middleware (`302` dashboard ou `403` AJAX); sem `read` o controller não é alcançado (`V2PermissionMiddleware.php:84-99`). |
| `GET /cms/mail-audit/detail/{id}` alcançando o controller sem `read` | `403` `Acesso negado. Você não tem permissão para acessar a auditoria de e-mails.` — inalcançável por HTTP normal (defesa em profundidade), mesma ressalva do index (`:93-95`). |
| Registro sem `body` | “Corpo da mensagem não disponível para este registro.” (`Views/cms/detail.blade.php:104-109`). |
| Registro sem `message_id` | O bloco “Message ID” não é renderizado (`:64-69`). |
| Registro com `error_message` | Bloco “Erro” em alerta vermelho (`:71-79`). |

### 1.4 O que este módulo NÃO faz

- **Não envia, reenvia nem exclui** e-mails; não há POST, DELETE, botão de ação ou
  endpoint de reenvio (`Routes/cms.php:13-21`).
- **Não pagina**: a tela mostra no máximo 50 linhas e não tem links de paginação.
- **Não filtra por data** (o `filter-card` é chamado com `showDateRange => false`).
- **Não mascara dados sensíveis**: exibe o corpo integral, incluindo links, tokens e
  PII que o remetente tenha colocado na mensagem.
- **Não mostra anexos** nem headers SMTP; apenas as colunas de `v2_mail_logs`.
- **Não identifica o template**: não há coluna de template/view; o “conteúdo” exibido é
  o `body` já renderizado que o chamador passou ao `MailService::send`.
- **Não valida o `id`** como UUID; qualquer string é comparada com `firstWhere`.
- **Não registra quem consultou** o histórico.
- **Não tem retenção/limpeza**: nunca apaga registros e não há job de expurgo no módulo.

## 2. Abordagem

### 2.1 Estratégia em uma página

O módulo é um **viewer sobre o query builder** do `MailService`. `getHistory($limit,
$profile)` devolve uma `Collection` de `stdClass` já ordenada por `created_at DESC`;
o controller aplica status e busca em PHP (compatibilidade com PHP 7.2), corta em 50
e calcula estatísticas com duas chamadas adicionais de `getHistory(999999)`. Não há
model Eloquent, policy ou repositório próprio.

```mermaid
flowchart LR
  A[GET /cms/mail-audit] --> B[CmsAuthenticate]
  B --> C[V2PermissionMiddleware]
  C -->|none| E[302 dashboard / 403 AJAX]
  C -->|read ou write| D{v2_can MailAudit read?}
  D -->|não: defesa inalcançável| E2[403 do controller]
  D -->|sim| F[MailService::getHistory 200]
  F --> G[DB v2_mail_logs]
  G --> H[filtro status em PHP]
  H --> I[filtro search em PHP]
  I --> J[take 50]
  J --> K[estatísticas getHistory 999999 x2]
  K --> L[v2-mailaudit::cms.index]
```

### 2.2 Mecânica passo a passo

1. **Autenticação**: `CmsAuthenticate` exige Sentinel + ≥1 role
   (`V2/Core/Middleware/CmsAuthenticate.php:18-44`).
2. **Autorização de módulo**: `V2PermissionMiddleware` casa `cms/mail-audit*` e aplica
   `none/read/write` (`V2PermissionMiddleware.php:40-48,79-127`).
3. **Autorização da action (defesa em profundidade)**: o controller exige
   `v2_can('MailAudit','read')` em `index` e `detail` (`MailAuditCmsController.php:40-42,93-95`).
   Com o middleware ativo, esse `403` é inalcançável: `none` já foi barrado e `write`
   implica `read` (`PermissionService.php:54-59`).
4. **Leitura base**: `getHistory(200, $profile)` → `SELECT * FROM v2_mail_logs WHERE
   deleted_at IS NULL [AND profile = ?] ORDER BY created_at DESC LIMIT 200`
   (`MailService.php:277-289`).
5. **Filtro de status**: `Collection::where('status', $status)` (comparação estrita)
   (`MailAuditCmsController.php:52-54`).
6. **Filtro de busca**: `strtolower` + `strpos` em `to_address`, `subject` e
   `from_address` (`:56-63`).
7. **Corte**: `take(50)` (`:65`).
8. **Estatísticas**: `getHistory(999999)->where('status','sent')->count()` e idem
   `failed` — duas consultas que trazem até 999.999 linhas para a memória
   (`:67-73`).
9. **Renderização**: a view calcula `total = sent + failed` e a taxa; o total exibido
   ignora registros `pending` (`Views/cms/index.blade.php:53-95`).
10. **Detalhe**: `getHistory(1)` → coleção com **um** registro; `firstWhere('id',$id)`;
    se não achar, `abort(404)` (`MailAuditCmsController.php:97-102`).
11. **Atividade**: o controller passa `history => collect()` e a view inclui o
    `activity-footer`, que não renderiza nada (`:104-110`; `Views/cms/detail.blade.php:149-150`).

### 2.3 Decisões de design e por quê

- **Reutilizar o `MailService` em vez de consultar a tabela** `[C]`: o módulo chama
  `getHistory` e herda o filtro `deleted_at`/ordenação (`MailService.php:277-289`).
- **Filtragem em PHP** `[C]`: o comentário justifica “compatível com PHP 7.2”; evita
  query dinâmica, mas limita o universo antes do filtro (200) (`MailAuditCmsController.php:49-65`).
- **Estatísticas sem agregação SQL** `[I]`: duas consultas com limite 999.999 e
  `count()` na coleção; funciona, mas é O(n) em memória e ignora `pending`.
- **Detalhe por `getHistory(1)`** `[C]`: provável tentativa de “buscar por id sem
  método específico”; o efeito é o bug descrito em §3.3 (`:97-98`).
- **Sem model** `[C]`: o histórico não é um `Model` Eloquent com ActivityLog; o próprio
  comentário diz que a auditoria do envio já está na view (`:104-106`).

### 2.4 Trechos de código pivô

```php
// V2/Modules/Infraestrutura/MailAudit/Controllers/MailAuditCmsController.php:44-65
// Universo de 200, filtros em memória, corte final em 50 — não há paginação.
$profile = $request->input('profile');
$status = $request->input('status');
$search = $request->input('search');
$limit = 50;
$history = $this->mailService->getHistory(200, $profile);
if (!empty($status)) {
    $history = $history->where('status', $status);
}
if (!empty($search)) {
    $history = $history->filter(function ($log) use ($search) {
        $searchLower = strtolower($search);
        return strpos(strtolower($log->to_address), $searchLower) !== false
            || strpos(strtolower($log->subject), $searchLower) !== false
            || strpos(strtolower($log->from_address), $searchLower) !== false;
    });
}
$history = $history->take($limit);
```

Um registro na posição 201 jamais aparece, mesmo que corresponda ao filtro; a busca
não consulta `body` nem `error_message`.

```php
// .../Controllers/MailAuditCmsController.php:97-102
// O bug de detalhe: busca apenas o registro mais recente e tenta achar o id nele.
$history = $this->mailService->getHistory(1);
$log = $history->firstWhere('id', $id);

if (!$log) {
    abort(404, 'Registro de e-mail não encontrado.');
}
```

Como a listagem traz 50 ids, 49 deles (no mínimo) levam a `404`; a correção mínima
seria `getHistory(999999)` ou um `getById` no serviço.

```php
// V2/Services/MailService/MailService.php:277-289
// A consulta real: soft-deleted fora, profile opcional, ordem decrescente e limite.
$query = DB::table('v2_mail_logs')
    ->whereNull('deleted_at');

if ($profile) {
    $query->where('profile', $profile);
}

return $query->orderBy('created_at', 'desc')
    ->limit($limit)
    ->get();
```

Não há `select` de colunas: o módulo recebe `body`, `error_message`, `message_id` e
`transaction_snapshot_id` mesmo quando não os usa.

```php
// .../Controllers/MailAuditCmsController.php:67-73
// Estatísticas globais: duas varreduras de até 999.999 linhas, sem filtro de profile/status na query.
$totalSent = $this->mailService->getHistory(999999)
    ->where('status', 'sent')
    ->count();
$totalFailed = $this->mailService->getHistory(999999)
    ->where('status', 'failed')
    ->count();
```

Os cartões não refletem os filtros aplicados; são sempre globais. O total do primeiro
cartão (`sent+failed`) exclui `pending`.

```php
// V2/Services/MailService/MailService.php:360-378
// Quem escreve a tabela: o log é best-effort; falha de log não derruba o envio.
protected function logMail($profile, $from, $to, $subject, $body = null, $status = 'sent', $messageId = null, $errorMessage = null)
{
    try {
        DB::table('v2_mail_logs')->insert([
            'id' => v2_uuid(),
            'profile' => $profile,
            'from_address' => $from,
            'to_address' => $to,
            'subject' => $subject,
            'body' => $body,
            'status' => $status,
            'sent_at' => $status === 'sent' ? date('Y-m-d H:i:s') : null,
            // ... message_id, error_message, created_at, updated_at
        ]);
    } catch (\Exception $e) {
        Log::error('[MailService] Erro ao salvar log de e-mail: ' . $e->getMessage());
    }
}
```

O `body` é gravado **em claro** e sem truncamento; é a origem do dado sensível exibido
no detalhe.

### 2.5 Algoritmos, consultas e custos

- `index`: 3 queries (200 registros + 2× até 999.999) e filtragem O(n) em PHP; o custo
  cresce com o histórico e pode estourar memória em instalações grandes.
- `detail`: 1 query `LIMIT 1` (barata, porém funcionalmente errada).
- Sem N+1: a listagem não carrega relações.
- Sem cache: cada request relê o banco; o `page.cache` ignora `cms*`
  (`PageCacheMiddleware.php:15-21`).
- Não usa os índices `status`/`profile` para as estatísticas, porque a query não filtra
  status no SQL.

## 3. Particularidades

### 3.1 O que só existe aqui

- **É a única UI de leitura de `v2_mail_logs`**; `SVC-MAILSERVICE` grava, este módulo lê.
- **Exibe o corpo integral do e-mail** (prévia + `pre`), algo que nenhum outro módulo
  faz com o log de e-mail. O corpo é o HTML final; não há vínculo com o template Blade
  que o gerou nem com o `MailResult` do envio.
- **O total do painel soma apenas `sent` + `failed`**, ignorando `pending` — o schema
  admite `pending`, mas o `MailService` só grava `sent`/`failed`.
- **O detalhe é deliberadamente (ou acidentalmente) limitado ao registro mais recente**;
  não há outro ponto do corpus com esse padrão.

### 3.2 Invariantes e regras não óbvias

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| RN-MAILAUDIT-01 | O middleware é o gate efetivo: para perfil comum, `none` é bloqueado antes e `read`/`write` passam; suporte e `super-admin` têm bypass. O controller repete `v2_can('MailAudit','read')` como defesa | Com `none` (perfil comum): `302` dashboard (ou `403` AJAX) do middleware; o `403` do controller só ocorre se o middleware não casar a rota | `V2PermissionMiddleware.php:69-72,79-127`; `MailAuditCmsController.php:40-42,93-95` |
| RN-MAILAUDIT-02 | Só os 200 registros mais recentes entram nos filtros | Registros antigos não são encontráveis pela busca | `:49-65` |
| RN-MAILAUDIT-03 | A listagem exibe no máximo 50 linhas | Sem paginação, o restante fica invisível | `:47,65` |
| RN-MAILAUDIT-04 | O filtro de status é comparação exata (`sent`/`failed`) e só roda se `!empty($status)` | `status=0` é ignorado (`empty('0')` é `true`) e devolve a listagem normal; os demais valores não vazios e diferentes de `sent`/`failed` zeram a lista | `:52-54` |
| RN-MAILAUDIT-05 | O filtro de profile vai ao SQL; o de status/busca não | `profile` reduz o universo antes do limite 200 | `:49`; `MailService.php:282-284` |
| RN-MAILAUDIT-06 | O detalhe só resolve o registro mais recente | Demais ids → `404` | `:97-102` |
| RN-MAILAUDIT-07 | O corpo é exibido sem máscara | PII/tokens ficam visíveis a quem tem `read` | `Views/cms/detail.blade.php:93-103` |
| RN-MAILAUDIT-08 | As estatísticas ignoram os filtros da tela | Sempre globais | `:67-73` |
| RN-MAILAUDIT-09 | Registros soft-deleted não aparecem | `getHistory` filtra `deleted_at` | `MailService.php:279-280` |
| RN-MAILAUDIT-10 | Consultar não gera auditoria | Não há `ActivityLogger` no módulo | `MailAuditCmsController.php` |

### 3.3 Casos-limite e comportamentos surpreendentes

- **Detalhe inacessível**: o link “Ver detalhes” de qualquer registro que não seja o
  mais recente leva a `404`. Se o usuário filtrar por `failed`, os links continuam
  apontando para ids que o `detail` não resolve.
- **Estatísticas divergentes da lista**: com filtro `failed`, os cartões continuam
  mostrando o total global; o usuário pode interpretar que “há 10 falhas” quando a
  lista filtrada tem 1.
- **`profile` desconhecido**: o select só tem `default`, mas `?profile=contact` funciona
  e filtra; o valor não é validado.
- **Corpo nulo**: e-mails logados sem `body` (o parâmetro é opcional) mostram o estado
  vazio, mas o restante do detalhe permanece.
- **Registro `pending`**: aparece na lista com badge “Pendente”, mas não entra em
  nenhum dos cartões.
- **`from_address` nulo**: a busca faz `strtolower(null)`; no PHP 7.2 isso vira string
  vazia sem erro, então o campo simplesmente não contribui para o `strpos`.
- **Múltiplos destinatários**: `to_address` é varchar(500) e pode conter vários
  separados por `;`; a tela exibe o valor cru e a busca encontra qualquer substring.
- **`updated_at`**: exibido no detalhe, mas o `MailService` grava `sent_at` e
  `updated_at` iguais; não há updates posteriores.

### 3.4 Armadilhas de reimplementação

- **Não “corrigir” o detalhe consultando por id sem revisar o contrato**: a UI atual
  depende de `getHistory(1)`; uma reimplementação deve criar um `getById` e ajustar o
  link, mas o comportamento observável legado é o `404`.
- **Não usar `count()` da coleção filtrada para os cartões**: os cartões são globais;
  mudar isso altera o contrato visual.
- **Não assumir paginação**: não há `paginate()` nem `links()`; adicionar paginação
  muda a contagem “X registro(s) exibido(s)” e o limite de 50.
- **Não exibir o corpo como HTML cru**: a view usa `{{ }}`/`<pre><code>`; trocar por
  `{!! !!}` abriria XSS no CMS.
- **Não reimplementar `MailService::getHistory` sem `whereNull('deleted_at')`**: o
  módulo espera não ver registros removidos.

### 3.5 Divergências documentação × código e dívidas

- O comentário do controller diz que “o histórico de auditoria do envio já é exibido na
  própria view” (`:104-106`), mas não há timeline de auditoria no detalhe; o
  `activity-footer` recebe `history` vazio e não renderiza.
- O `@compliance WCAG AAA` no cabeçalho das views não é verificável: não há testes de
  acessibilidade; a tabela usa `scope`, `caption` e `aria-label`, mas o detalhe não
  tem `aria-live` para estados dinâmicos.
- O schema admite `pending`, mas o módulo só estiliza `sent`/`failed` e trata o resto
  como “Pendente”; o `MailService` nunca grava `pending` (`MailService.php:122,131`).
- O `PageCacheMiddleware` exclui `cms*`, então não há cache; o `PublicCacheMiddleware`
  também não se aplica a `/cms` (`PublicCacheMiddleware.php:33-38`).

## 4. Integrabilidade

### 4.1 Superfície pública

| Símbolo | Tipo | Assinatura | Precondição | Consumidores |
|---|---|---|---|---|
| `MailAuditCmsController@index` | action | `index(Request): View` | `MailAudit:read` | rota `v2.mailaudit.index` |
| `MailAuditCmsController@detail` | action | `detail(string $id): View` | `MailAudit:read` | rota `v2.mailaudit.detail` |
| `MailService::getHistory` | método | `getHistory($limit = 50, $profile = null): Collection` | tabela existente | este módulo (e outros consumidores do serviço) |
| `ModuleProvider::getConfig` | provider | `array` | — | `V2ServiceProvider`, sidebar |

O módulo não expõe helpers, classes de domínio ou hooks.

### 4.2 Contrato de UI/JS

- **Namespace**: `v2-mailaudit`; layout `v2-cms::layouts.app`.
- **Filtros**: `search`, `status` (select `sent`/`failed`), `profile` (select apenas
  `default`), todos via query string no `filter-card` (`Views/cms/index.blade.php:103-125`).
- **Tabela**: `aria-label="Histórico de envios de e-mail"`, `caption` visually hidden;
  colunas Data, Destinatário, Assunto, Status, Detalhes (`:154-164`).
- **Linha de erro**: `<tr class="table-danger">` com `colspan="5"` quando `failed` e
  `error_message` (`:204-211`).
- **Detalhe**: `details/summary` para “Visualizar HTML completo” (`Views/cms/detail.blade.php:95-103`).
- **Sem JS próprio**: nenhum `@push('scripts')`, nenhum AJAX, nenhum modal.
- **Acessibilidade**: labels nos selects, `aria-label` no botão de detalhe, `role="alert"`
  no estado vazio; contraste depende do tema do CMS.

### 4.3 Ordem e ciclo de vida

1. `V2ServiceProvider` descobre o módulo, registra `v2-mailaudit` e injeta
   `group='Infraestrutura'` (`V2/Core/Providers/V2ServiceProvider.php:90-128`).
2. Rotas CMS com `web` + `module.active:MailAudit` (`:303-307`).
3. `CmsAuthenticate` → `V2PermissionMiddleware` → controller.
4. `MailService` é resolvido por auto-wiring; ele lê `v2_system_mail_configs` apenas
   para enviar, não para `getHistory`.
5. A tabela `v2_mail_logs` é criada pela migration do serviço
   (`V2/Services/MailService/Migrations/2026_06_30_000000_create_v2_mail_logs_table.php:24-67`),
   carregada no boot de `V2/Services`.
6. Não há evento, observer ou invalidação de cache.

### 4.4 Receitas de uso

```php
// Ler o histórico como a tela faz
$history = app(\V2\Services\MailService\MailService::class)->getHistory(200, 'default');
$falhas = $history->where('status', 'failed');
```

Resultado: coleção de `stdClass` ordenada por `created_at DESC`, sem soft-deleted.

```php
// Buscar um registro específico corretamente (correção recomendada)
$log = \DB::table('v2_mail_logs')->whereNull('deleted_at')->where('id', $id)->first();
abort_unless($log, 404, 'Registro de e-mail não encontrado.');
```

Resultado: qualquer id existente é resolvido; é o que o `detail` deveria fazer.

```blade
{{-- Exibir o corpo com escape, como o detalhe faz --}}
<div class="mail-body-preview">{{ $log->body }}</div>
```

Resultado: o HTML aparece como texto; o operador pode inspecionar o conteúdo sem
executá-lo no navegador do CMS.

### 4.5 Anti-uso

- **Usar `MailAudit` como caixa de entrada/reenvio**: não há `send` nem rota POST; um
  reenvio precisa chamar `MailService::send` e criará um **novo** log.
- **Expor o body em API/JSON sem autorização**: o módulo atual só renderiza HTML
  autenticado; serializar `v2_mail_logs` para fora do CMS vaza tokens/PII.
- **Confiar no filtro de status para estatística**: os cartões são globais; usar o
  filtro como métrica produz leitura errada.

### 4.6 Custos e limites da integração

| Limite | Valor | Efeito |
|---|---|---|
| Universo da listagem | 200 registros | filtros não alcançam registros mais antigos |
| Linhas exibidas | 50 | sem paginação |
| Estatísticas | 999.999 por consulta | 2 varreduras completas por request |
| Busca | `to_address`, `subject`, `from_address` | não busca `body` |
| Tamanho do corpo | `text` sem limite no schema | detalhe pode renderizar HTML muito grande (prévia limitada a 120 px, completo a 400 px) |
| Retenção | indefinida | crescimento contínuo da tabela |

## 5. Rotas e contratos HTTP

| Tipo | Método | URI | Nome | Middleware | Controller@ação | Permissão | Efeito | Fonte |
|---|---|---|---|---|---|---|---|---|
| endpoint | GET | `/cms/mail-audit` | `v2.mailaudit.index` | `web; module.active:MailAudit; CmsAuthenticate; V2PermissionMiddleware` | `MailAuditCmsController@index` | `MailAudit:read` | Lista até 50 envios + estatísticas | `Routes/cms.php:18` |
| endpoint | GET | `/cms/mail-audit/detail/{id}` | `v2.mailaudit.detail` | idem | `MailAuditCmsController@detail` | `MailAudit:read` | Detalhe (apenas o mais recente) | `Routes/cms.php:19` |

Não há CSRF relevante (somente GET), throttle, cache ou API. O parâmetro `id` não tem
constraint de formato.

## 6. Dados e modelo

O módulo não tem migrations nem model próprios. A tabela é do `SVC-MAILSERVICE`; o
schema completo está em `../../../../02_servicos-base/mail-service.md`.

### 6.1 `v2_mail_logs`

| Coluna | Tipo | Null/default | Papel no módulo |
|---|---|---|---|
| `id` | uuid | PK | link do detalhe |
| `profile` | varchar(50) | default `default` | filtro e badge |
| `from_address` | varchar(255) | sim | exibição e busca |
| `to_address` | varchar(500) | não | exibição e busca |
| `subject` | varchar(500) | não | exibição e busca |
| `body` | text | sim | prévia e HTML completo |
| `status` | varchar(20) | default `pending` | badge e filtro |
| `message_id` | varchar(255) | sim | exibição condicional |
| `error_message` | text | sim | linha de erro/alert |
| `sent_at` | timestamp | sim | exibido **somente no detalhe** (`sent_at ?? created_at`); a listagem não o usa |
| `transaction_snapshot_id` | uuid | sim | **não exibido** |
| `created_at`, `updated_at`, `deleted_at` | timestamps | sim | `created_at` é a data da listagem e a ordenação; `updated_at` só no detalhe; `deleted_at` filtra |

Índices: `profile`, `status`, `sent_at`, `to_address`
(`V2/Services/MailService/Migrations/2026_06_30_000000_create_v2_mail_logs_table.php:24-67`).

### 6.2 Ausência de model

Não há `MailLog` Eloquent; `getHistory` devolve `stdClass`. Por isso o
`activity-footer` recebe `history` vazio em vez de consultar auditoria
(`MailAuditCmsController.php:104-110`).

```mermaid
erDiagram
    v2_mail_logs {
      uuid id PK
      varchar profile
      varchar from_address
      varchar to_address
      varchar subject
      text body
      varchar status
      varchar message_id
      text error_message
      timestamp sent_at
      timestamp created_at
      timestamp deleted_at
    }
```

## 7. Fluxos e estados

```mermaid
stateDiagram-v2
    [*] --> Pending: default do schema
    [*] --> Sent: logMail(status=sent)
    [*] --> Failed: logMail(status=failed)
    Sent --> [*]
    Failed --> [*]
    Pending --> [*]
```

O módulo não altera estado; apenas lê. Um registro `sent`/`failed` é imutável na
prática (não há update de `v2_mail_logs` no código). O soft delete existe no schema,
mas nenhuma rota o aciona.

```mermaid
sequenceDiagram
    participant O as Operador
    participant C as MailAuditCmsController
    participant M as MailService
    participant DB as Postgres
    O->>C: GET /cms/mail-audit?status=failed
    C->>C: v2_can('MailAudit','read')
    C->>M: getHistory(200, profile)
    M->>DB: SELECT ... WHERE deleted_at IS NULL LIMIT 200
    M-->>C: Collection
    C->>C: where(status) + search + take(50)
    C->>M: getHistory(999999) x2
    M->>DB: SELECT ... LIMIT 999999
    C-->>O: HTML com cartões + tabela
```

## 8. UI CMS

### 8.1 `index` — Auditoria de E-mails

| Item | Valor |
|---|---|
| Rota/permissão | `GET /cms/mail-audit`; `MailAudit:read` |
| Layout | `v2-cms::layouts.app`; breadcrumb “Infraestrutura › Auditoria de E-mail” |
| Objetivo | monitorar envios e diagnosticar falhas |
| Estados | vazio sem histórico; vazio com filtros; badge “Filtrado” quando há filtro |
| Cartões | Total de Envios, Enviados, Com Falha, Taxa de Sucesso |
| Filtros | busca textual, status, profile; sem intervalo de datas |
| Colunas | Data (`created_at`, `d/m/Y H:i`, `index.blade.php:171`), Destinatário, Assunto, Status, Detalhes |
| Ordenação | `created_at DESC` |
| Paginação | nenhuma (máximo 50) |
| Ações | “Ver detalhes” (olho) |
| JS | nenhum |

Mensagens: “Nenhum envio encontrado”; “O histórico será populado automaticamente à
medida que e-mails forem enviados.”; “Nenhum resultado para os filtros aplicados.”
(`Views/cms/index.blade.php:141-152`).

### 8.2 `detail` — Detalhes do Envio

| Item | Valor |
|---|---|
| Rota/permissão | `GET /cms/mail-audit/detail/{id}`; `MailAudit:read` |
| Objetivo | inspecionar um envio específico |
| Cabeçalho | assunto + badge de status |
| Metadados | Remetente, Destinatário, Profile SMTP, Enviado em (`sent_at ?? created_at`, `detail.blade.php:61`), Message ID (condicional), Erro (condicional) |
| Corpo | prévia `.mail-body-preview` (máx. 120 px) + `details` com `<pre><code>` (máx. 400 px) |
| Lateral | ID do Registro, Criado em, Última atualização, “Voltar ao Histórico” |
| Estados | corpo ausente; erro presente; sem loading |
| JS | nenhum |

O corpo é escapado por `{{ }}`; o “HTML completo” é exibido como texto, não renderizado
(`Views/cms/detail.blade.php:93-103`). No detalhe, `sent_at` tem fallback para
`created_at` (`:58-62`); a listagem não consulta `sent_at`.

## 9. UI Portal

**Não se aplica.** Não há rota pública, view de portal, SEO, mídia ou cache de página.

## 10. Configuração, permissões e flags

### 10.1 Configuração

**Não se aplica.** O módulo não lê `config()`; usa apenas o `MailService` e a tabela.
O comportamento de `profile` depende dos valores gravados pelo serviço, não de config.

### 10.2 Permissões

| Slug | Ação | Middleware | Onde é aplicada |
|---|---|---|---|
| `MailAudit` | `read` | `V2PermissionMiddleware` (gate efetivo) | GET de `index`/`detail`; perfil comum com `none` recebe `302` dashboard (ou `403` AJAX) antes do controller; suporte/`super-admin` passam |
| `MailAudit` | `read` | `v2_can()` no controller (defesa em profundidade) | `index`/`detail`, inalcançável por HTTP normal se o middleware casar a rota |

Não há uso de `write`: o módulo não escreve. Um perfil com `write` passa em `read`
porque `write` implica leitura (`PermissionService.php:54-59`). O `none` bloqueia
**perfil comum**; o e-mail de `config('v2.support.email')` e o Sentinel `super-admin`
têm bypass total e alcançam as duas actions mesmo sem a chave `MailAudit` na matriz
(`V2PermissionMiddleware.php:69-72`; `PermissionService.php:46-49`; `helpers.php:20-23`).

### 10.3 Flag de módulo

`module.active:MailAudit` bloqueia as duas rotas quando a flag está desativada; sem
linha em `v2_module_flags`, o módulo está ativo (`ModuleFlagService.php:72-95`). Como o
prefixo do grupo é `cms/mail-audit` (não exatamente `cms`), o bloqueio é `404`
`Modulo desativado.`; AJAX/JSON recebe `404` com `Modulo desativado: MailAudit`
(`ModuleAccessMiddleware.php:46-62`).

### 10.4 `ModuleProvider::getConfig()`

```php
[
  'title' => 'Auditoria de E-mails',
  'description' => 'Consulta e monitoramento do histórico de envios de e-mail do sistema.',
  'icon' => 'bi bi-envelope-paper',
  'active_pattern' => 'cms/mail-audit*',
  'subModulesLevel1' => [
    [
      'title' => 'Histórico de Envios',
      'icon' => 'bi bi-clock-history',
      'route' => 'v2.mailaudit.index',
      'position' => 1,
      'subModulesLevel2' => [],
    ],
  ],
]
```

Não define `position` no módulo (a sidebar ordena por título dentro do grupo);
`group` é injetado pelo Core (`V2/Modules/Infraestrutura/MailAudit/ModuleProvider.php:14-31`).

## 11. Observabilidade, erros e limites

| Situação | Resposta/registro |
|---|---|
| Perfil comum com `none` | `302` dashboard + `Acesso negado. Seu perfil não possui permissão para acessar o módulo: Auditoria de E-mails`; AJAX/JSON: `403` `Forbidden. Access denied for this module.` (middleware); suporte/`super-admin` não são bloqueados |
| Sem `read` alcançando o controller | `403` `Acesso negado. Você não tem permissão para acessar a auditoria de e-mails.` — defesa em profundidade, inalcançável por HTTP normal |
| Detalhe inexistente/não-recente | `404` `Registro de e-mail não encontrado.` |
| Módulo desativado | `404` `Modulo desativado.` (`abort`), pois o prefixo da rota é `cms/mail-audit` (≠ `cms`); AJAX/JSON: `404` `{"error":"Modulo desativado: MailAudit","code":404}`. O `302` dashboard só ocorre se `getPrefix() === 'cms'` (`ModuleAccessMiddleware.php:46-62`). |
| Sem autenticação | `302` login (ou `401` em AJAX) |
| Consulta | nenhum log de acesso |
| Falha de escrita do log (serviço) | `Log::error('[MailService] Erro ao salvar log de e-mail: ...')` |

O módulo não registra auditoria de leitura; não há mascaramento de `body`,
`to_address`, `from_address` ou `error_message`. Quem tem `read` vê tudo o que o
serviço gravou.

## 12. Seeds e dados iniciais

**Não se aplica.** Não há seeder de logs; a tabela nasce vazia e é populada pelo
`MailService` a cada envio. O estado vazio da tela é o esperado em instalação nova.

## 13. Lacunas

| ID | Lacuna | Impacto | Onde procurar |
|---|---|---|---|
| LG-MAILAUDIT-01 | O bug do detalhe (`getHistory(1)`) não tem teste que o caracterize | Reimplementações podem “corrigir” sem saber o contrato legado | `sucesu-cms/tests` |
| LG-MAILAUDIT-02 | Não há política de retenção nem mascaramento de dados sensíveis | Conformidade LGPD e crescimento da tabela | ausência de job/coluna; `MailService::logMail` |
| LG-MAILAUDIT-03 | Não foi possível determinar se todos os callers de `MailService::send` passam `body` | Registros sem corpo aparecem como “não disponível” | `V2/Modules/Institucional/{FaleConosco,AssocieSe}` |
| LG-MAILAUDIT-04 | O comportamento de `strtolower(null)` em `from_address` varia com a versão de PHP | Em PHP ≥ 8.1 pode gerar depreciação em logs | `MailAuditCmsController.php:56-62` |

## 14. Checklist de paridade

- [ ] `PC-MAILAUDIT-01` — Acessar `/cms/mail-audit` com perfil **comum** `none` retorna `302` dashboard + `Acesso negado...` (ou `403` genérico em AJAX) do middleware; suporte e `super-admin` passam; o `403` do controller é defesa inalcançável. Fonte: `V2PermissionMiddleware.php:69-72,84-99`; `MailAuditCmsController.php:40-42`.
- [ ] `PC-MAILAUDIT-02` — A listagem mostra no máximo 50 registros dos 200 mais recentes. Fonte: `:49,65`.
- [ ] `PC-MAILAUDIT-03` — O filtro `status=failed` mostra apenas falhas e a linha de erro. Fonte: `:52-54`; `Views/cms/index.blade.php:204-211`.
- [ ] `PC-MAILAUDIT-04` — O filtro `search` encontra por destinatário, assunto ou remetente, sem diferenciar caixa. Fonte: `:56-63`.
- [ ] `PC-MAILAUDIT-05` — O filtro `profile` é aplicado no SQL antes do limite de 200. Fonte: `:49`; `MailService.php:282-284`.
- [ ] `PC-MAILAUDIT-06` — Os cartões exibem totais globais, não filtrados. Fonte: `:67-73`.
- [ ] `PC-MAILAUDIT-07` — O detalhe do registro mais recente abre com corpo e metadados. Fonte: `:97-110`.
- [ ] `PC-MAILAUDIT-08` — O detalhe de qualquer outro id retorna `404`. Fonte: `:97-102`.
- [ ] `PC-MAILAUDIT-09` — O corpo do e-mail é exibido escapado, com prévia e HTML completo. Fonte: `Views/cms/detail.blade.php:93-103`.
- [ ] `PC-MAILAUDIT-10` — Registros soft-deleted não aparecem em nenhuma consulta. Fonte: `MailService.php:279-280`.
- [ ] `PC-MAILAUDIT-11` — O módulo não escreve em `v2_mail_logs` nem em `activity_log`. Fonte: `MailAuditCmsController.php:38-112`.

## 15. Evidências consultadas

- `V2/Modules/Infraestrutura/MailAudit/Controllers/MailAuditCmsController.php:1-113`
- `V2/Modules/Infraestrutura/MailAudit/ModuleProvider.php:1-32`
- `V2/Modules/Infraestrutura/MailAudit/Routes/cms.php:1-21`
- `V2/Modules/Infraestrutura/MailAudit/Views/cms/index.blade.php:1-219`
- `V2/Modules/Infraestrutura/MailAudit/Views/cms/detail.blade.php:1-151`
- `V2/Services/MailService/MailService.php:110-135,250-289,347-378`
- `V2/Services/MailService/Migrations/2026_06_30_000000_create_v2_mail_logs_table.php:24-67`
- `V2/Core/Middleware/CmsAuthenticate.php:18-44`; `V2/Core/Middleware/V2PermissionMiddleware.php:79-127`
- `V2/Core/Middleware/PageCacheMiddleware.php:11-21`; `V2/Core/Middleware/PublicCacheMiddleware.php:25-93`
- `V2/Core/Providers/V2ServiceProvider.php:90-128,303-307`
- `V2/layouts/cms/partials/activity-footer.blade.php:1-15`
