mailcore 0.1.0.alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +15 -0
  3. data/CONTRIBUTING.md +22 -0
  4. data/LICENSE +21 -0
  5. data/README.md +185 -0
  6. data/SECURITY.md +28 -0
  7. data/app/controllers/action_mailbox/ingresses/mailcore/inbound_emails_controller.rb +59 -0
  8. data/docs/cobertura.md +140 -0
  9. data/docs/integracao-api-interrupcao.json +27 -0
  10. data/docs/integracao-api.json +48 -0
  11. data/docs/rails.md +185 -0
  12. data/docs/ruby-3.3.13-http-padrao.json +22 -0
  13. data/docs/ruby-3.3.13.json +66 -0
  14. data/docs/ruby-3.4.12-http-padrao.json +22 -0
  15. data/docs/ruby-3.4.12.json +66 -0
  16. data/docs/ruby-4.0.9-http-padrao.json +22 -0
  17. data/docs/ruby-4.0.9.json +66 -0
  18. data/docs/sinatra.md +94 -0
  19. data/docs/verificacao.md +100 -0
  20. data/exemplos/enviar.rb +14 -0
  21. data/exemplos/sinatra.rb +65 -0
  22. data/lib/mailcore/action_mailer.rb +124 -0
  23. data/lib/mailcore/assinatura.rb +64 -0
  24. data/lib/mailcore/cliente.rb +27 -0
  25. data/lib/mailcore/configuracao.rb +30 -0
  26. data/lib/mailcore/engine.rb +71 -0
  27. data/lib/mailcore/erros.rb +25 -0
  28. data/lib/mailcore/limite_ingress.rb +51 -0
  29. data/lib/mailcore/railtie.rb +16 -0
  30. data/lib/mailcore/recursos/automacoes.rb +52 -0
  31. data/lib/mailcore/recursos/base.rb +29 -0
  32. data/lib/mailcore/recursos/caixas.rb +199 -0
  33. data/lib/mailcore/recursos/campanhas.rb +72 -0
  34. data/lib/mailcore/recursos/chaves_api.rb +23 -0
  35. data/lib/mailcore/recursos/cli.rb +24 -0
  36. data/lib/mailcore/recursos/contatos.rb +179 -0
  37. data/lib/mailcore/recursos/dominios.rb +35 -0
  38. data/lib/mailcore/recursos/emails.rb +54 -0
  39. data/lib/mailcore/recursos/eventos.rb +31 -0
  40. data/lib/mailcore/recursos/modelos.rb +106 -0
  41. data/lib/mailcore/recursos/recebimento.rb +54 -0
  42. data/lib/mailcore/recursos/webhooks.rb +57 -0
  43. data/lib/mailcore/resposta.rb +22 -0
  44. data/lib/mailcore/transporte.rb +284 -0
  45. data/lib/mailcore/versao.rb +5 -0
  46. data/lib/mailcore.rb +36 -0
  47. metadata +189 -0
data/docs/rails.md ADDED
@@ -0,0 +1,185 @@
1
+ # Rails: envio e recebimento
2
+
3
+ O gem `mailcore` integra o cliente HTTP com Action Mailer e Action Mailbox. Rails
4
+ e Mail são dependências opcionais: o cliente Ruby funciona sem esses componentes.
5
+ A combinação verificada é Ruby 3.4, Rails/Action Mailer/Action Mailbox `8.1.3.1`,
6
+ Mail `2.9.1` e JSON `>= 2.7, < 3`. Consulte o relatório geral do SDK para as outras
7
+ versões Ruby efetivamente exercitadas. Esta faixa de JSON preserva a integração
8
+ com o decodificador do Active Support dessa versão Rails.
9
+
10
+ ## Action Mailer
11
+
12
+ Configure a chave em runtime. O bloco permite um `Proc`, resolvido ao fazer cada
13
+ pedido, para carregar a credencial pelo mecanismo de segredos da aplicação:
14
+
15
+ ```ruby
16
+ # config/initializers/mailcore.rb
17
+ Mailcore.configurar do |configuracao|
18
+ configuracao.chave_api = -> { ENV.fetch('MAILCORE_API_KEY') }
19
+ end
20
+ ```
21
+
22
+ Ative o método de entrega:
23
+
24
+ ```ruby
25
+ # config/environments/production.rb
26
+ config.action_mailer.delivery_method = :mailcore
27
+ ```
28
+
29
+ O carregamento padrão do Rails registra esse método pelo Railtie. Se o cliente
30
+ Ruby foi carregado antes do framework, carregue explicitamente
31
+ `require 'mailcore/railtie'` antes de inicializar a aplicação.
32
+
33
+ Os mailers e templates continuam sendo os do Rails:
34
+
35
+ ```ruby
36
+ class AvisoMailer < ApplicationMailer
37
+ default from: 'Equipe <avisos@seu-dominio.example>'
38
+
39
+ def confirmacao
40
+ headers['mailcore_opcoes'] = {
41
+ chave_idempotencia: "pedido-#{params.fetch(:pedido_id)}-confirmacao",
42
+ tags: { tipo: 'confirmacao' }
43
+ }
44
+ mail(to: params.fetch(:destinatario), subject: 'Pedido confirmado')
45
+ end
46
+ end
47
+ ```
48
+
49
+ Use `deliver_now` para aguardar a aceitação HTTP ou `deliver_later` para entregar a
50
+ execução ao Active Job configurado pela aplicação. O gem não cria uma fila. A
51
+ aceitação da API não comprova entrega SMTP.
52
+
53
+ A conversão inclui remetente com nome, destinatários, cópias, cópias ocultas, um
54
+ endereço de resposta, texto, HTML, multipart, cabeçalhos personalizados e anexos.
55
+ Anexos mantêm os bytes em base64 estrito, o tipo MIME e o Content-ID quando inline.
56
+ Cabeçalhos estruturais e reservados pelo Mailcore são filtrados; os demais seguem
57
+ as validações da API.
58
+
59
+ `mailcore_opcoes` aceita `chave_idempotencia`, `tags` e `agendada_em`. Esta última
60
+ usa o formato ISO 8601 com fuso aceito pela API. Essas opções são metadados do
61
+ adaptador e não se tornam cabeçalhos do email. Uma chave estável identifica a mesma
62
+ intenção de negócio, inclusive quando a aplicação decide repetir um job. O gem não
63
+ repete um envio automaticamente após falhas ambíguas.
64
+
65
+ Para uma configuração específica, use `config.action_mailer.mailcore_settings` ou
66
+ `delivery_method_options` do Rails. São aceitas as opções do cliente
67
+ `chave_api`, `url_base`, `tempo_limite`, `tempo_conexao`, `certificado_ca` e
68
+ `limite_resposta_bytes`, além de `chave_idempotencia`. A opção `cliente` permite
69
+ fornecer uma instância isolada já configurada; ela prevalece sobre as demais opções
70
+ de conexão. Configure cada organização com seu próprio cliente, sem trocar a
71
+ configuração global durante pedidos concorrentes.
72
+
73
+ O identificador retornado pela API é colocado no `message_id` do objeto Mail
74
+ somente após sucesso. Erros do cliente são propagados para o mecanismo normal de
75
+ falhas do Rails, sem substituir o identificador anterior.
76
+
77
+ ## Action Mailbox
78
+
79
+ A aplicação precisa ter Action Mailbox, Active Record, Active Storage e Active
80
+ Job configurados. Faça a instalação e as migrações próprias do Rails no ambiente
81
+ da aplicação. O gem não altera o banco nem escolhe armazenamento ou filas.
82
+
83
+ Carregue o ingress e habilite-o:
84
+
85
+ ```ruby
86
+ # config/application.rb, depois de carregar os componentes Rails
87
+ require 'mailcore/engine'
88
+
89
+ # config/environments/production.rb
90
+ config.action_mailbox.ingress = :mailcore
91
+ ```
92
+
93
+ Cadastre no Mailcore um webhook do evento `email.received` apontando para:
94
+
95
+ ```text
96
+ https://sua-aplicacao.example/rails/action_mailbox/mailcore/inbound_emails
97
+ ```
98
+
99
+ Guarde o segredo de assinatura nas credenciais criptografadas do Rails:
100
+
101
+ ```yaml
102
+ action_mailbox:
103
+ mailcore_segredo: segredo_fornecido_pelo_webhook
104
+ ```
105
+
106
+ Como alternativa, configure `Mailcore.configuracao.segredo_webhook` pelo bloco
107
+ `Mailcore.configurar`; o padrão consulta `MAILCORE_WEBHOOK_SECRET`. A credencial
108
+ Rails tem precedência. Configure também uma chave de API com permissão para ler
109
+ o original recebido. A chave da API e o segredo do webhook têm funções distintas.
110
+
111
+ O endpoint aceita POST e exige assinatura válida do corpo bruto. Para corpos
112
+ dentro do limite, sem o ingress habilitado responde `404`; assinatura inválida resulta em `401`; configuração do
113
+ segredo ausente resulta em `503`. Eventos assinados de outros tipos recebem `204`
114
+ sem buscar conteúdo. O evento de recebimento deve conter `data.email_id` e um
115
+ array não vazio de endereços SMTP em `data.to`; envelope inválido resulta em
116
+ `422`. Corpos acima de 256 KiB são recusados com `413`.
117
+
118
+ Um middleware atua antes dos parsers e da instrumentação do Rails, somente no
119
+ POST desse ingress. Ele lê no máximo 256 KiB mais um byte, inclusive quando a
120
+ requisição não informa Content-Length. Um corpo permitido é recolocado em um
121
+ StringIO com os mesmos bytes e tamanho correto; o controller verifica a assinatura
122
+ desse conteúdo sem normalizar ou reserializar o JSON.
123
+
124
+ Nessa rota, o corpo não é convertido automaticamente em parâmetros Rails antes
125
+ da autenticação. Assim, os metadados de email não entram no campo de parâmetros
126
+ da instrumentação, e o parser Rails não registra o corpo de um JSON malformado.
127
+ Filtros adicionais para `data`, `_json` e credenciais são aplicados somente ao
128
+ ambiente da requisição, preservando os filtros da aplicação. Outras rotas e
129
+ métodos seguem seu processamento habitual.
130
+
131
+ Após validar a assinatura, o ingress baixa o original pela rota autenticada do
132
+ cliente, `recebimento.baixar_original`. Ele não visita URLs fornecidas no webhook
133
+ e não reconstrói anexos a partir de metadados incompletos. A falha desse download
134
+ retorna `503`, preservando a possibilidade de entrega posterior do webhook.
135
+ Falhas internas do banco ou armazenamento seguem o tratamento de erros do Rails.
136
+
137
+ Os bytes MIME do original são preservados. O ingress acrescenta cabeçalhos
138
+ `X-Original-To` com os destinatários validados do envelope assinado. Isso permite
139
+ que o roteamento normal do Action Mailbox também encontre cópias ocultas ou
140
+ destinos de encaminhamento ausentes do cabeçalho `To`. Esses endereços fazem parte
141
+ do conteúdo armazenado pela aplicação e devem receber a mesma proteção dos emails.
142
+
143
+ ```ruby
144
+ class ApplicationMailbox < ActionMailbox::Base
145
+ routing /^suporte@/i => :suporte
146
+ end
147
+ ```
148
+
149
+ O Action Mailbox armazena a mensagem e encaminha o processamento pelo Active Job.
150
+ A deduplicação nativa considera o par Message-ID e checksum do conteúdo. A
151
+ repetição do mesmo original com Message-ID e o mesmo envelope recebe `204` sem
152
+ criar uma nova mensagem. Quando o original não contém Message-ID, o Rails gera um
153
+ identificador que inclui o hostname; não há garantia de deduplicação entre réplicas
154
+ nesse caso. Isso não transforma o webhook em uma entrega exatamente uma vez: um
155
+ original ou envelope diferente pode gerar outro registro, e os efeitos do mailbox
156
+ da aplicação também precisam ser idempotentes. Os prazos de replay e retenção são
157
+ responsabilidade do Mailcore e da aplicação, respectivamente.
158
+
159
+ Configure armazenamento protegido, a retenção do Action Mailbox e filtros de logs
160
+ na aplicação. Não registre o corpo bruto, endereços de destinatários, credenciais
161
+ ou cabeçalhos de assinatura. Os adaptadores não registram esses valores.
162
+
163
+ ## Verificação local
164
+
165
+ Os testes do adaptador usam Action Mailer e Mail reais para converter mensagens,
166
+ com um cliente sintético no ponto de chamada HTTP. Os testes do ingress executam
167
+ uma aplicação Rails em memória, com SQLite, Active Storage em pasta temporária e
168
+ Active Job de teste. Eles verificam a assinatura por um vetor HMAC independente,
169
+ persistência do original, destinatário oculto, replay, acesso recusado e falha de
170
+ consulta. Esses testes não abrem portas, chamam produção ou enviam SMTP.
171
+
172
+ Uma entrada virtual de 4 GiB, sem Content-Length, comprova que a aplicação Rails
173
+ solicita uma única leitura de 262.145 bytes antes de recusar o corpo, sem alocar
174
+ os 4 GiB. Outros casos verificam os bytes assinados com espaços e quebras de linha,
175
+ a ausência de metadados de email nos parâmetros instrumentados, os filtros locais
176
+ e a propagação de erros de I/O posteriores à leitura.
177
+
178
+ Os arquivos `testes/testes_action_mailer.rb` e
179
+ `testes/testes_action_mailbox.rb` são executados em processos separados, usando as
180
+ dependências do ambiente Docker de testes do projeto. A configuração de banco e
181
+ armazenamento desses testes é descartável e exclusiva do processo.
182
+
183
+ Contratos Rails utilizados:
184
+ [Action Mailer](https://guides.rubyonrails.org/action_mailer_basics.html) e
185
+ [Action Mailbox](https://guides.rubyonrails.org/action_mailbox_basics.html).
@@ -0,0 +1,22 @@
1
+ {
2
+ "data_utc": "2026-11-14T14:01:36Z",
3
+ "ruby": "3.3.13",
4
+ "plataforma": "x86_64-linux",
5
+ "imagem": "sha256:e9e5e257301b57271cfde1fcbe2c800bd16b153dd9deb8e5c18aa637e7658b26",
6
+ "resultado": "aprovado",
7
+ "suites": [
8
+ {
9
+ "arquivo": "testes/testes_transporte.rb",
10
+ "aprovado": true,
11
+ "testes": 42,
12
+ "assercoes": 280,
13
+ "falhas": 0,
14
+ "erros": 0,
15
+ "ignorados": 0
16
+ }
17
+ ],
18
+ "perfil": "net_http_padrao_do_ruby",
19
+ "net_http": "0.4.1",
20
+ "total_testes": 42,
21
+ "total_assercoes": 280
22
+ }
@@ -0,0 +1,66 @@
1
+ {
2
+ "data_utc": "2026-11-14T14:01:29Z",
3
+ "ruby": "3.3.13",
4
+ "plataforma": "x86_64-linux",
5
+ "imagem": "sha256:e9e5e257301b57271cfde1fcbe2c800bd16b153dd9deb8e5c18aa637e7658b26",
6
+ "resultado": "aprovado",
7
+ "suites": [
8
+ {
9
+ "arquivo": "testes/testes_action_mailbox.rb",
10
+ "aprovado": true,
11
+ "testes": 13,
12
+ "assercoes": 67,
13
+ "falhas": 0,
14
+ "erros": 0,
15
+ "ignorados": 0
16
+ },
17
+ {
18
+ "arquivo": "testes/testes_action_mailer.rb",
19
+ "aprovado": true,
20
+ "testes": 9,
21
+ "assercoes": 37,
22
+ "falhas": 0,
23
+ "erros": 0,
24
+ "ignorados": 0
25
+ },
26
+ {
27
+ "arquivo": "testes/testes_core.rb",
28
+ "aprovado": true,
29
+ "testes": 5,
30
+ "assercoes": 66,
31
+ "falhas": 0,
32
+ "erros": 0,
33
+ "ignorados": 0
34
+ },
35
+ {
36
+ "arquivo": "testes/testes_recursos.rb",
37
+ "aprovado": true,
38
+ "testes": 156,
39
+ "assercoes": 513,
40
+ "falhas": 0,
41
+ "erros": 0,
42
+ "ignorados": 0
43
+ },
44
+ {
45
+ "arquivo": "testes/testes_sinatra.rb",
46
+ "aprovado": true,
47
+ "testes": 11,
48
+ "assercoes": 250,
49
+ "falhas": 0,
50
+ "erros": 0,
51
+ "ignorados": 0
52
+ },
53
+ {
54
+ "arquivo": "testes/testes_transporte.rb",
55
+ "aprovado": true,
56
+ "testes": 42,
57
+ "assercoes": 280,
58
+ "falhas": 0,
59
+ "erros": 0,
60
+ "ignorados": 0
61
+ }
62
+ ],
63
+ "perfil": "dependencias_lockfile",
64
+ "total_testes": 236,
65
+ "total_assercoes": 1213
66
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "data_utc": "2026-11-14T14:01:46Z",
3
+ "ruby": "3.4.12",
4
+ "plataforma": "x86_64-linux",
5
+ "imagem": "sha256:92daa531a314ef9bef6bd11fcf8fab79f76f3b3a26191785198f1749c5c40989",
6
+ "resultado": "aprovado",
7
+ "suites": [
8
+ {
9
+ "arquivo": "testes/testes_transporte.rb",
10
+ "aprovado": true,
11
+ "testes": 42,
12
+ "assercoes": 280,
13
+ "falhas": 0,
14
+ "erros": 0,
15
+ "ignorados": 0
16
+ }
17
+ ],
18
+ "perfil": "net_http_padrao_do_ruby",
19
+ "net_http": "0.6.0",
20
+ "total_testes": 42,
21
+ "total_assercoes": 280
22
+ }
@@ -0,0 +1,66 @@
1
+ {
2
+ "data_utc": "2026-11-14T14:01:40Z",
3
+ "ruby": "3.4.12",
4
+ "plataforma": "x86_64-linux",
5
+ "imagem": "sha256:92daa531a314ef9bef6bd11fcf8fab79f76f3b3a26191785198f1749c5c40989",
6
+ "resultado": "aprovado",
7
+ "suites": [
8
+ {
9
+ "arquivo": "testes/testes_action_mailbox.rb",
10
+ "aprovado": true,
11
+ "testes": 13,
12
+ "assercoes": 67,
13
+ "falhas": 0,
14
+ "erros": 0,
15
+ "ignorados": 0
16
+ },
17
+ {
18
+ "arquivo": "testes/testes_action_mailer.rb",
19
+ "aprovado": true,
20
+ "testes": 9,
21
+ "assercoes": 37,
22
+ "falhas": 0,
23
+ "erros": 0,
24
+ "ignorados": 0
25
+ },
26
+ {
27
+ "arquivo": "testes/testes_core.rb",
28
+ "aprovado": true,
29
+ "testes": 5,
30
+ "assercoes": 66,
31
+ "falhas": 0,
32
+ "erros": 0,
33
+ "ignorados": 0
34
+ },
35
+ {
36
+ "arquivo": "testes/testes_recursos.rb",
37
+ "aprovado": true,
38
+ "testes": 156,
39
+ "assercoes": 513,
40
+ "falhas": 0,
41
+ "erros": 0,
42
+ "ignorados": 0
43
+ },
44
+ {
45
+ "arquivo": "testes/testes_sinatra.rb",
46
+ "aprovado": true,
47
+ "testes": 11,
48
+ "assercoes": 250,
49
+ "falhas": 0,
50
+ "erros": 0,
51
+ "ignorados": 0
52
+ },
53
+ {
54
+ "arquivo": "testes/testes_transporte.rb",
55
+ "aprovado": true,
56
+ "testes": 42,
57
+ "assercoes": 280,
58
+ "falhas": 0,
59
+ "erros": 0,
60
+ "ignorados": 0
61
+ }
62
+ ],
63
+ "perfil": "dependencias_lockfile",
64
+ "total_testes": 236,
65
+ "total_assercoes": 1213
66
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "data_utc": "2026-11-14T14:01:58Z",
3
+ "ruby": "4.0.9",
4
+ "plataforma": "x86_64-linux",
5
+ "imagem": "sha256:650e4570a7b60591ee3f9ca2d17509e686f798261702b1b92d7e82a086117ab1",
6
+ "resultado": "aprovado",
7
+ "suites": [
8
+ {
9
+ "arquivo": "testes/testes_transporte.rb",
10
+ "aprovado": true,
11
+ "testes": 42,
12
+ "assercoes": 286,
13
+ "falhas": 0,
14
+ "erros": 0,
15
+ "ignorados": 0
16
+ }
17
+ ],
18
+ "perfil": "net_http_padrao_do_ruby",
19
+ "net_http": "0.9.1",
20
+ "total_testes": 42,
21
+ "total_assercoes": 286
22
+ }
@@ -0,0 +1,66 @@
1
+ {
2
+ "data_utc": "2026-11-14T14:01:50Z",
3
+ "ruby": "4.0.9",
4
+ "plataforma": "x86_64-linux",
5
+ "imagem": "sha256:650e4570a7b60591ee3f9ca2d17509e686f798261702b1b92d7e82a086117ab1",
6
+ "resultado": "aprovado",
7
+ "suites": [
8
+ {
9
+ "arquivo": "testes/testes_action_mailbox.rb",
10
+ "aprovado": true,
11
+ "testes": 13,
12
+ "assercoes": 67,
13
+ "falhas": 0,
14
+ "erros": 0,
15
+ "ignorados": 0
16
+ },
17
+ {
18
+ "arquivo": "testes/testes_action_mailer.rb",
19
+ "aprovado": true,
20
+ "testes": 9,
21
+ "assercoes": 37,
22
+ "falhas": 0,
23
+ "erros": 0,
24
+ "ignorados": 0
25
+ },
26
+ {
27
+ "arquivo": "testes/testes_core.rb",
28
+ "aprovado": true,
29
+ "testes": 5,
30
+ "assercoes": 66,
31
+ "falhas": 0,
32
+ "erros": 0,
33
+ "ignorados": 0
34
+ },
35
+ {
36
+ "arquivo": "testes/testes_recursos.rb",
37
+ "aprovado": true,
38
+ "testes": 156,
39
+ "assercoes": 513,
40
+ "falhas": 0,
41
+ "erros": 0,
42
+ "ignorados": 0
43
+ },
44
+ {
45
+ "arquivo": "testes/testes_sinatra.rb",
46
+ "aprovado": true,
47
+ "testes": 11,
48
+ "assercoes": 250,
49
+ "falhas": 0,
50
+ "erros": 0,
51
+ "ignorados": 0
52
+ },
53
+ {
54
+ "arquivo": "testes/testes_transporte.rb",
55
+ "aprovado": true,
56
+ "testes": 42,
57
+ "assercoes": 280,
58
+ "falhas": 0,
59
+ "erros": 0,
60
+ "ignorados": 0
61
+ }
62
+ ],
63
+ "perfil": "dependencias_lockfile",
64
+ "total_testes": 236,
65
+ "total_assercoes": 1213
66
+ }
data/docs/sinatra.md ADDED
@@ -0,0 +1,94 @@
1
+ # Integração com Sinatra
2
+
3
+ O [exemplo Sinatra](../exemplos/sinatra.rb) usa o mesmo cliente `mailcore` da
4
+ integração Ruby. Sinatra é uma dependência da aplicação e do ambiente de testes;
5
+ não é uma dependência de runtime do gem Mailcore. A combinação verificada usa
6
+ Sinatra `4.2.1` e Rack `3.2.8`, compatível com o conjunto Rails `8.1.3.1` usado nos
7
+ testes do projeto.
8
+
9
+ O exemplo é uma aplicação modular `Sinatra::Base`. Ele oferece apenas
10
+ `POST /enviar`, que envia uma mensagem fixa para o endereço configurado no boot.
11
+ O corpo e a query da requisição não escolhem destinatários, conteúdo, chave da
12
+ API, URL da API ou cliente.
13
+
14
+ ## Configurar na aplicação
15
+
16
+ Forneça estas variáveis pelo mecanismo de segredos do seu ambiente Docker:
17
+
18
+ | Variável | Uso |
19
+ | ----------------------- | ------------------------------------------------------------------- |
20
+ | `MAILCORE_API_KEY` | Chave usada pelo SDK para falar com a API |
21
+ | `TOKEN_APLICACAO` | Token separado que autoriza chamadas ao exemplo |
22
+ | `MAILCORE_REMETENTE` | Remetente fixo em domínio autorizado |
23
+ | `MAILCORE_DESTINATARIO` | Destinatário fixo |
24
+ | `MAILCORE_API_URL` | Opcional: URL base HTTPS da API; usa o padrão do SDK quando ausente |
25
+
26
+ O token precisa ser uma String de 32 a 256 caracteres em Base64URL, usando letras,
27
+ números, `_` e `-`, e deve ser diferente da chave da API. Gere-o com uma fonte
28
+ criptograficamente segura, por exemplo `SecureRandom.urlsafe_base64(32)`. O exemplo
29
+ recusa configuração incompleta ou ambígua no boot. Ele guarda um resumo SHA256 do
30
+ token e compara resumos de tamanho fixo com a operação de tempo constante do
31
+ OpenSSL.
32
+
33
+ Em um `config.ru` da aplicação, carregue o exemplo e construa uma instância:
34
+
35
+ ```ruby
36
+ require_relative 'exemplos/sinatra'
37
+
38
+ run ExemploMailcoreSinatra.construir
39
+ ```
40
+
41
+ Execute esse arquivo pelo servidor Rack da sua aplicação, dentro do ambiente
42
+ Docker configurado. O exemplo não inicia um servidor ao ser carregado. Configure
43
+ HTTPS na entrada da aplicação e preserve a comunicação verificada por HTTPS entre
44
+ o SDK e a API. Os testes deste projeto não publicam portas.
45
+
46
+ A fábrica também aceita `ambiente:` e `cliente:` no boot. Isso permite fornecer um
47
+ Hash de configuração isolado e um cliente já configurado, inclusive para testes.
48
+ Esses argumentos não são lidos dos parâmetros HTTP. A configuração global do gem
49
+ e o carregamento do cliente Ruby continuam independentes do Sinatra.
50
+
51
+ ## Chamar a rota
52
+
53
+ ```sh
54
+ curl --request POST "$URL_APLICACAO/enviar" \
55
+ --header "Authorization: Bearer $TOKEN_APLICACAO" \
56
+ --header 'Idempotency-Key: pedido-123-confirmacao' \
57
+ --header 'Content-Type: application/json' \
58
+ --data '{}'
59
+ ```
60
+
61
+ O corpo precisa ser um objeto JSON válido em UTF-8, com até 4 KiB. Seus campos são
62
+ ignorados pelo envio fixo. A chave de idempotência é obrigatória, contém de 1 a 256
63
+ caracteres ASCII visíveis e não aceita vírgulas. Preserve a mesma chave para uma
64
+ repetição explícita da mesma intenção de negócio.
65
+
66
+ A resposta de aceitação é `202`, contendo apenas `id` e `estado`. A aceitação da
67
+ API não comprova entrega SMTP. Autenticação inválida retorna `401`; chave de
68
+ idempotência ou JSON inválido, `400`; conteúdo acima do limite, `413`; tipo de
69
+ conteúdo diferente de JSON, `415`. Rotas e métodos sem implementação recebem
70
+ `404` sem chamar o SDK.
71
+
72
+ Falhas da API preservam um status de erro HTTP aplicável, com mensagem genérica.
73
+ Falhas de comunicação, redirecionamentos recusados e respostas de aceitação
74
+ incompletas resultam em `502`. O exemplo não devolve mensagens privadas de erro,
75
+ cabeçalhos da API, chaves ou detalhes internos, e não faz repetição automática.
76
+
77
+ ## Verificação
78
+
79
+ O arquivo `testes/testes_sinatra.rb`, no repositório de fontes, usa Sinatra e
80
+ `Rack::MockRequest` reais, com um cliente sintético injetado no boot. A verificação
81
+ local concluiu 11 testes e 250 asserções em Ruby 3.4, sem falhas. Ela exercita
82
+ roteamento, autorização, configuração, corpo malicioso, JSON/UTF-8, limites,
83
+ idempotência repassada, respostas sanitizadas e falhas sem repetição.
84
+
85
+ Os testes não abrem um listener, não chamam a API externa e não enviam SMTP. A
86
+ repetição com a mesma chave confirma que o exemplo a preserva; a deduplicação na
87
+ API real depende do contrato do servidor. A matriz geral pode ser executada pelo
88
+ verificador Docker do projeto, `sh scripts/verificar.sh`, sem instalar Ruby ou
89
+ gems no host.
90
+
91
+ Fontes dos componentes:
92
+ [documentação do Sinatra](https://sinatrarb.com/intro.html),
93
+ [Sinatra 4.2.1](https://rubygems.org/gems/sinatra/versions/4.2.1) e
94
+ [Rack](https://github.com/rack/rack).
@@ -0,0 +1,100 @@
1
+ # Verificação da gem Ruby
2
+
3
+ A verificação é local e utiliza dados sintéticos. Não publica pacotes, não faz
4
+ deploy e não envia SMTP. Os relatórios JSON registram resultados observados,
5
+ versões, imagens e alcance de cada execução.
6
+
7
+ Em 14/11/2026, a matriz principal passou em Ruby **3.3.13, 3.4.12 e 4.0.9**.
8
+ O ensaio da API real local também passou nos dez grupos descritos abaixo, com
9
+ 23 migrações, TLS verificado e limpeza concluída. Os números e revisões completos
10
+ permanecem nos relatórios JSON correspondentes.
11
+
12
+ ## Matriz e distribuição
13
+
14
+ Execute na raiz do repositório:
15
+
16
+ ```sh
17
+ sh scripts/verificar.sh
18
+ ```
19
+
20
+ O host precisa de shell POSIX e Docker Linux. Ruby, Bundler, Rails, Sinatra e
21
+ ferramentas de compilação ficam nas imagens. As bases Ruby 3.3, 3.4 e 4.0 estão
22
+ fixadas por digest, e as dependências de desenvolvimento pelo `Gemfile.lock`.
23
+ Não há instalação de ferramentas no host.
24
+
25
+ Além do lockfile, a suíte HTTPS é repetida com o `net-http` original de cada Ruby
26
+ (0.4.1, 0.6.0 e 0.9.1, respectivamente). Seus relatórios terminam em
27
+ `-http-padrao.json`. Isso exercita também o caminho de detecção de resposta
28
+ incompleta nas versões anteriores do transporte HTTP.
29
+
30
+ As suítes são executadas em processos separados, sem rede externa, com checkout
31
+ somente leitura e diretório temporário para Rails/SQLite/Active Storage. O
32
+ servidor HTTPS de teste escuta somente dentro do container e não publica portas.
33
+ Os relatórios `ruby-*.json` contêm as contagens por suíte; uma execução válida
34
+ precisa de `resultado: "aprovado"`, sem falhas, erros ou testes ignorados.
35
+
36
+ - Contrato: método, caminho, filtros e corpo das 151 operações, recursos filhos,
37
+ identificadores escapados e preservação de `nil`/`false`.
38
+ - Protocolo HTTPS real: certificado/hostname, CAs, redirecionamentos, HTTP de erro,
39
+ idempotência, desconexões sem retry, prazo total, limites, respostas truncadas,
40
+ bytes binários, consumo em fluxo e isolamento entre threads.
41
+ - Assinatura: vetor HMAC independente, UTF-8, rotação, alteração do corpo e janela
42
+ temporal de cinco minutos.
43
+ - Rails: Action Mailer/Mail reais e aplicação Action Mailbox com SQLite,
44
+ armazenamento temporário e Active Job de teste. A fronteira HTTP desses
45
+ adaptadores usa um cliente sintético, conforme [o guia Rails](rails.md).
46
+ - Sinatra: aplicação Rack real, autenticação, idempotência, limites de corpo,
47
+ separação de credenciais e tratamento sanitizado dos erros.
48
+
49
+ O mesmo comando constrói a gem em `pkg/`, instala o artefato localmente dentro de
50
+ containers Ruby mínimos e verifica que o cliente puro carrega sem Rails/Sinatra.
51
+ O arquivo SHA-256 acompanha a gem. Os pacotes contêm código/documentação próprios,
52
+ sem testes, credenciais, arquivos de ambiente ou dependências vendorizadas.
53
+
54
+ ## API e PostgreSQL reais locais
55
+
56
+ O ensaio adicional é separado por precisar do checkout backend e de imagens e
57
+ cache de compilação já preparados:
58
+
59
+ ```sh
60
+ sh scripts/verificar_api.sh
61
+ ```
62
+
63
+ São usados o checkout `mailcore` ao lado de `libs-mailcore`, a imagem
64
+ `mailcore/ruby-testes:3.4`, um runtime local da API, uma imagem Rust e o cache
65
+ público Cargo. O controlador compila a API com `--offline --locked` em Docker e
66
+ registra revisão, hash dos fontes e binário. O cache não inclui credenciais Cargo.
67
+ Consulte `python3 testes/integracao_api.py --help` dentro da imagem para informar
68
+ outros caminhos/imagens; `MAILCORE_BACKEND` e `MAILCORE_IMAGEM_CLIENTE` ajustam o
69
+ wrapper. Nenhum compilador deve ser instalado no host para esse ensaio.
70
+
71
+ O controlador usa o socket Docker para criar rede interna exclusiva, configurações
72
+ efêmeras em memória e instâncias novas da API e PostgreSQL. A API usa HTTPS e
73
+ PostgreSQL usa TLS `verify-full`; não são publicadas portas. Chaves de dados,
74
+ certificados, usuários e organização são sintéticos e exclusivos. Não são
75
+ iniciados worker, Postal ou SMTP.
76
+
77
+ Os cenários aprovados incluem chaves Full/Sending e negação por escopo; envio
78
+ composto com Cc/Cco/CID, replay e conflito de idempotência; lote atômico; domínios;
79
+ contatos, catálogos e exportação CSV por fluxo; modelos/pastas/versões/publicação
80
+ com controle de revisão; CRUD de marketing sem disparo; caixas/etiquetas/rascunhos;
81
+ recebimento MIME real pendente de processamento; concessões CLI e revogação.
82
+ O recebimento usa um callback privado sintético assinado, sem iniciar Postal.
83
+
84
+ O resultado e a limpeza ficam em [integracao-api.json](integracao-api.json).
85
+ `resultado: "aprovado"` e `limpeza_concluida: true` são necessários para considerar
86
+ o ensaio aprovado. Credenciais, corpos e mensagens de email não são gravados no
87
+ relatório. O controlador trata SIGINT/SIGTERM e remove apenas seus recursos.
88
+
89
+ A [interrupção por SIGTERM](integracao-api-interrupcao.json) também foi exercitada:
90
+ a saída de falha era esperada e a limpeza foi concluída, sem recursos rotulados
91
+ restantes. Esse ensaio não verifica SIGKILL nem desfaz intenções já aceitas.
92
+
93
+ ## Limites das conclusões
94
+
95
+ Uma API real local comprova os contratos exercitados no binário/backend registrado,
96
+ sem simular entrega SMTP. Os testes Rails não comprovam a configuração da fila,
97
+ banco, armazenamento ou segredos de uma aplicação de produção. A matriz Docker
98
+ não garante todos os sistemas operacionais, versões Rails ou ambientes hospedados.
99
+ Não foi medido desempenho em produção nem equivalência de latência com outro
100
+ serviço. Uma publicação ou implantação futura exige validação própria.
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mailcore'
4
+
5
+ cliente = Mailcore::Cliente.new(chave_api: ENV.fetch('MAILCORE_API_KEY'))
6
+ pedido = {
7
+ remetente: ENV.fetch('MAILCORE_REMETENTE'),
8
+ destinatarios: [ENV.fetch('MAILCORE_DESTINATARIO')],
9
+ assunto: 'Integração Ruby Mailcore',
10
+ texto: 'Sua integração Ruby está pronta.',
11
+ html: '<p>Sua integração Ruby está pronta.</p>'
12
+ }
13
+ resposta = cliente.emails.enviar(pedido, chave_idempotencia: ENV.fetch('CHAVE_IDEMPOTENCIA'))
14
+ puts resposta[:id]