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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +15 -0
- data/CONTRIBUTING.md +22 -0
- data/LICENSE +21 -0
- data/README.md +185 -0
- data/SECURITY.md +28 -0
- data/app/controllers/action_mailbox/ingresses/mailcore/inbound_emails_controller.rb +59 -0
- data/docs/cobertura.md +140 -0
- data/docs/integracao-api-interrupcao.json +27 -0
- data/docs/integracao-api.json +48 -0
- data/docs/rails.md +185 -0
- data/docs/ruby-3.3.13-http-padrao.json +22 -0
- data/docs/ruby-3.3.13.json +66 -0
- data/docs/ruby-3.4.12-http-padrao.json +22 -0
- data/docs/ruby-3.4.12.json +66 -0
- data/docs/ruby-4.0.9-http-padrao.json +22 -0
- data/docs/ruby-4.0.9.json +66 -0
- data/docs/sinatra.md +94 -0
- data/docs/verificacao.md +100 -0
- data/exemplos/enviar.rb +14 -0
- data/exemplos/sinatra.rb +65 -0
- data/lib/mailcore/action_mailer.rb +124 -0
- data/lib/mailcore/assinatura.rb +64 -0
- data/lib/mailcore/cliente.rb +27 -0
- data/lib/mailcore/configuracao.rb +30 -0
- data/lib/mailcore/engine.rb +71 -0
- data/lib/mailcore/erros.rb +25 -0
- data/lib/mailcore/limite_ingress.rb +51 -0
- data/lib/mailcore/railtie.rb +16 -0
- data/lib/mailcore/recursos/automacoes.rb +52 -0
- data/lib/mailcore/recursos/base.rb +29 -0
- data/lib/mailcore/recursos/caixas.rb +199 -0
- data/lib/mailcore/recursos/campanhas.rb +72 -0
- data/lib/mailcore/recursos/chaves_api.rb +23 -0
- data/lib/mailcore/recursos/cli.rb +24 -0
- data/lib/mailcore/recursos/contatos.rb +179 -0
- data/lib/mailcore/recursos/dominios.rb +35 -0
- data/lib/mailcore/recursos/emails.rb +54 -0
- data/lib/mailcore/recursos/eventos.rb +31 -0
- data/lib/mailcore/recursos/modelos.rb +106 -0
- data/lib/mailcore/recursos/recebimento.rb +54 -0
- data/lib/mailcore/recursos/webhooks.rb +57 -0
- data/lib/mailcore/resposta.rb +22 -0
- data/lib/mailcore/transporte.rb +284 -0
- data/lib/mailcore/versao.rb +5 -0
- data/lib/mailcore.rb +36 -0
- metadata +189 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 48fb7eb69d256776ed0f0fc87263bae696e305d26951713a68b3728e8bd92a2d
|
|
4
|
+
data.tar.gz: 0dc87a1ed77dce322edbe22c410c87644a1a1cde50051ac515b19f85439764fe
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 901090c17a27114d7466eacc4a77c78d91ba21f2951331f2c01941b1e003bab4b8f4691095d291358284a3bf23401e175698c03de33144388a3ecd5420876d4d
|
|
7
|
+
data.tar.gz: c5bdefe691e0a3b5be1cc70127dfe3ecc856170b20391eaba9beb2b73684291eb0780fff1c9b6308dea2e1f3c707a8ca0c9428a944efc3c126f13841f0ab38a3
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Alterações
|
|
2
|
+
|
|
3
|
+
## 0.1.0.alpha.1 — 14/11/2026
|
|
4
|
+
|
|
5
|
+
- Primeira gem Ruby Mailcore, com 151 operações correspondentes ao contrato público
|
|
6
|
+
já integrado pelo SDK Node.
|
|
7
|
+
- Cliente por instância, chave estática ou Proc resolvida por operação, respostas
|
|
8
|
+
com metadados e exceções próprias.
|
|
9
|
+
- TLS verificado, recusa de redirecionamento, prazos totais e limites de resposta,
|
|
10
|
+
com retries implícitos de Net::HTTP desabilitados.
|
|
11
|
+
- Downloads binários e por blocos, HMAC-SHA256 de webhooks e integração Rails
|
|
12
|
+
opcional com Action Mailer e Action Mailbox.
|
|
13
|
+
- Desenvolvimento, matriz de testes e preparação da distribuição em Docker.
|
|
14
|
+
|
|
15
|
+
Esta versão é alpha e não foi publicada no RubyGems.
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Contribuir
|
|
2
|
+
|
|
3
|
+
Use código, comentários e documentação em português do Brasil, preservando nomes
|
|
4
|
+
exigidos pelos protocolos Ruby/Rails/HTTP. Recursos ficam em `lib/mailcore/recursos`;
|
|
5
|
+
o transporte centraliza autenticação, TLS, erros, prazos e limites.
|
|
6
|
+
|
|
7
|
+
O servidor é a autoridade para os DTOs, escopos, permissões e transições. Compare
|
|
8
|
+
novas operações com as rotas públicas do backend antes de implementá-las. Não
|
|
9
|
+
normalize envelopes diferentes nem transforme ausência de campo em `nil` no JSON.
|
|
10
|
+
|
|
11
|
+
Execute `sh scripts/verificar.sh` com Docker Linux. Não é necessário instalar
|
|
12
|
+
ferramentas no host. Mantenha a matriz de runtimes, lockfile de desenvolvimento,
|
|
13
|
+
testes de contrato/protocolo e instalação da gem coerentes. Rails é opcional:
|
|
14
|
+
carregar o cliente puro não pode exigir Action Mailer ou Action Mailbox.
|
|
15
|
+
|
|
16
|
+
Use dados sintéticos em testes. Não inicie workers/SMTP ou publique portas para
|
|
17
|
+
verificar o cliente. A integração local real usa banco e API exclusivos e remove
|
|
18
|
+
seus recursos. Registre o alcance observado sem atribuir ao laboratório prova de
|
|
19
|
+
entrega, desempenho cloud ou configuração de uma conta de produção.
|
|
20
|
+
|
|
21
|
+
Os comandos de verificação não devem publicar pacotes ou fazer deploy. Uma
|
|
22
|
+
implantação posterior no Mailcore precisa ser testada no ambiente de produção.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mailcore
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Mailcore para Ruby
|
|
2
|
+
|
|
3
|
+
[Código-fonte](https://github.com/mailcore-hq/mailcore-ruby) · [Problemas e sugestões](https://github.com/mailcore-hq/mailcore-ruby/issues)
|
|
4
|
+
|
|
5
|
+
Cliente Ruby da API Mailcore, com integração opcional com Rails. Versão
|
|
6
|
+
alpha `0.1.0.alpha.1`. Requer Ruby 3.3 ou superior.
|
|
7
|
+
Cada instância mantém sua configuração; pedidos concorrentes não compartilham
|
|
8
|
+
credenciais ou estado de envio.
|
|
9
|
+
|
|
10
|
+
## Instalação
|
|
11
|
+
|
|
12
|
+
Instale a versão alpha pelo RubyGems:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
gem install mailcore --version 0.1.0.alpha.1 --pre
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Com Bundler, adicione ao Gemfile da aplicação:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
gem 'mailcore', '0.1.0.alpha.1'
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Desenvolvimento e instalação local
|
|
25
|
+
|
|
26
|
+
A verificação Docker gera `pkg/mailcore-0.1.0.alpha.1.gem`. Em uma aplicação Ruby:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
gem install /caminho/mailcore-0.1.0.alpha.1.gem
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
O comando pode buscar dependências no RubyGems. Outra opção é apontar o Gemfile
|
|
33
|
+
da aplicação para este checkout e executar Bundler:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
gem 'mailcore', path: '/caminho/libs-mailcore/mailcore-ruby'
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Não é necessário instalar Rails para utilizar o cliente. As dependências de
|
|
40
|
+
runtime são `net-http`, `json`, `openssl`, `base64` e `timeout`; o pacote não
|
|
41
|
+
inclui um cliente HTTP compilado próprio. A faixa JSON 2.x mantém compatibilidade
|
|
42
|
+
com a versão Rails exercitada pelos testes.
|
|
43
|
+
|
|
44
|
+
## Primeiro envio
|
|
45
|
+
|
|
46
|
+
Configure `MAILCORE_API_KEY` somente no servidor. Use domínio verificado e uma
|
|
47
|
+
chave autorizada para o remetente.
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
require 'mailcore'
|
|
51
|
+
|
|
52
|
+
cliente = Mailcore::Cliente.new(chave_api: ENV.fetch('MAILCORE_API_KEY'))
|
|
53
|
+
pedido = {
|
|
54
|
+
remetente: 'Equipe <equipe@example.com>',
|
|
55
|
+
destinatarios: ['pessoa@example.com'],
|
|
56
|
+
assunto: 'Bem-vindo',
|
|
57
|
+
html: '<p>Sua conta está pronta.</p>',
|
|
58
|
+
texto: 'Sua conta está pronta.'
|
|
59
|
+
}
|
|
60
|
+
resposta = cliente.emails.enviar(pedido, chave_idempotencia: 'pedido-123-confirmacao')
|
|
61
|
+
puts resposta[:id]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`202` confirma a aceitação da intenção; a entrega é acompanhada pelos eventos da
|
|
65
|
+
mensagem ou webhooks. Reutilize a mesma chave e o mesmo conteúdo ao repetir a
|
|
66
|
+
mesma intenção. A biblioteca não repete requisições automaticamente, inclusive
|
|
67
|
+
após timeout, `429` e desconexão. Um timeout não desfaz uma intenção aceita.
|
|
68
|
+
|
|
69
|
+
Anexos usam `nome`, `tipo_conteudo`, `conteudo_base64` e, no envio composto,
|
|
70
|
+
`id_conteudo` opcional para CID. O lote aceita de 1 a 100 mensagens, em uma única
|
|
71
|
+
operação atômica; não divide pedidos nem aceita anexos, Cc/Cco ou cabeçalhos de
|
|
72
|
+
mensagem. Agendamento usa `agendada_em` ISO 8601 com fuso. As regras de domínio,
|
|
73
|
+
permissões e limites do DTO são validadas pelo servidor.
|
|
74
|
+
|
|
75
|
+
## Resultados, erros e opções
|
|
76
|
+
|
|
77
|
+
As operações devolvem `Mailcore::Resposta`: `dados` contém o JSON da API com
|
|
78
|
+
chaves simbolizadas recursivamente; `status` e `cabecalhos` preservam metadados
|
|
79
|
+
HTTP. `resposta[:id]` abrevia `resposta.dados[:id]`. Listas mantêm os envelopes
|
|
80
|
+
originais (`dados`, `proximo`, `proximo_cursor`, `tem_mais`, conforme a rota).
|
|
81
|
+
Nenhuma paginação é percorrida automaticamente.
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
begin
|
|
85
|
+
pagina = cliente.contatos.listar({ limite: 20 })
|
|
86
|
+
proximo = pagina.dados[:proximo_cursor]
|
|
87
|
+
rescue Mailcore::ErroApi => erro
|
|
88
|
+
# erro.codigo, erro.status e erro.cabecalhos['retry-after']
|
|
89
|
+
rescue Mailcore::ErroTransporte => erro
|
|
90
|
+
# Confirme a intenção antes de decidir repetir um envio ambíguo.
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`ErroApi` preserva códigos e mensagens JSON públicos do servidor.
|
|
95
|
+
`ErroResposta` identifica JSON inválido e limites excedidos.
|
|
96
|
+
`ErroTransporte` identifica falhas de comunicação, resposta incompleta ou prazo; configuração inválida
|
|
97
|
+
gera `ErroConfiguracao`. As mensagens de transporte não reproduzem credenciais
|
|
98
|
+
ou corpos recebidos. Erros de API podem conter dados privados; registre somente
|
|
99
|
+
os campos necessários.
|
|
100
|
+
|
|
101
|
+
Todas as operações aceitam as palavras-chave `chave_idempotencia`,
|
|
102
|
+
`tempo_limite` (segundos), `limite_resposta_bytes` e `cabecalhos` adicionais.
|
|
103
|
+
Cabeçalhos de autenticação, protocolo e idempotência são reservados pelo cliente.
|
|
104
|
+
O prazo total padrão é 30 segundos, conexão até 10 segundos e resposta até 16 MiB.
|
|
105
|
+
O teto JSON de pedido é 50 MiB, para comportar o envelope de importação CSV;
|
|
106
|
+
cada rota aplica seu limite específico no servidor.
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
cliente = Mailcore::Cliente.new(
|
|
110
|
+
chave_api: -> { ENV.fetch('MAILCORE_API_KEY') },
|
|
111
|
+
url_base: 'https://mailcore.com.br/v1',
|
|
112
|
+
tempo_limite: 30,
|
|
113
|
+
tempo_conexao: 10
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Uma chave em `Proc` é resolvida a cada operação, permitindo rotação. Use instâncias
|
|
118
|
+
distintas para credenciais distintas; configure o módulo global somente no boot.
|
|
119
|
+
O cliente exige HTTPS, verifica certificado e hostname e recusa redirecionamentos.
|
|
120
|
+
Não herda proxies do ambiente. CAs privadas podem ser informadas por
|
|
121
|
+
`certificado_ca: '/caminho/ca.crt'`; TLS permanece verificado. HTTP só é permitido
|
|
122
|
+
em loopback com `permitir_http_local: true`, para testes explícitos.
|
|
123
|
+
|
|
124
|
+
## Downloads e webhooks
|
|
125
|
+
|
|
126
|
+
Downloads completos devolvem `String` em `Encoding::BINARY`, preservando MIME,
|
|
127
|
+
CSV e anexos. Os métodos com sufixo `_fluxo` exigem bloco e entregam partes de
|
|
128
|
+
forma síncrona, respeitando o ritmo do consumidor e o teto de bytes. A resposta
|
|
129
|
+
final informa o total em `dados`. Prazo e tamanho continuam ativos até terminar.
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
File.open('/caminho/mensagem.eml.parcial', 'wb') do |arquivo|
|
|
133
|
+
cliente.recebimento.baixar_original_fluxo('id-da-mensagem') do |parte|
|
|
134
|
+
arquivo.write(parte)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Só trate o arquivo como completo depois de sucesso; uma falha pode deixar partes
|
|
140
|
+
já escritas. Downloads usam rotas autenticadas da API; URLs temporárias retornadas
|
|
141
|
+
em metadados não são acessadas automaticamente.
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
evento = Mailcore.verificar_assinatura(
|
|
145
|
+
corpo_bruto: corpo_recebido_sem_alteracoes,
|
|
146
|
+
cabecalhos: cabecalhos_recebidos,
|
|
147
|
+
segredo: ENV.fetch('MAILCORE_WEBHOOK_SECRET')
|
|
148
|
+
)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
A verificação usa HMAC-SHA256, cabeçalhos `svix-*`, comparação em tempo constante
|
|
152
|
+
e janela temporal de cinco minutos. Corpo alterado, cabeçalhos ambíguos e
|
|
153
|
+
timestamps vencidos/futuros são recusados. Deduplicação dos eventos e confirmação
|
|
154
|
+
durável pertencem ao consumidor; uma assinatura válida não torna a ação idempotente.
|
|
155
|
+
|
|
156
|
+
## Rails e demais recursos
|
|
157
|
+
|
|
158
|
+
O [guia Rails](docs/rails.md) cobre Action Mailer e Action Mailbox, incluindo
|
|
159
|
+
HTML/texto, anexos/CID e recebimento autenticado. Rails e Mail são opcionais para
|
|
160
|
+
a gem; os adaptadores usam os componentes já instalados na aplicação.
|
|
161
|
+
O [exemplo Sinatra](docs/sinatra.md) demonstra uma rota privada de envio com
|
|
162
|
+
destino fixo, usando o mesmo cliente Ruby e a instalação Sinatra da aplicação.
|
|
163
|
+
|
|
164
|
+
A [cobertura](docs/cobertura.md) lista os recursos e contratos. O cliente inclui
|
|
165
|
+
envio, lote, domínios, chaves, contatos, segmentos, tópicos, propriedades,
|
|
166
|
+
importações/exportações, modelos, campanhas, automações, eventos, webhooks,
|
|
167
|
+
recebimento, caixas e concessões CLI. Não converte métodos privados do dashboard
|
|
168
|
+
em API pública nem cria capacidades ausentes no servidor.
|
|
169
|
+
|
|
170
|
+
## Desenvolver e verificar com Docker
|
|
171
|
+
|
|
172
|
+
Requisitos do host: shell POSIX e Docker Linux acessível ao usuário.
|
|
173
|
+
Ruby, Bundler, Rails e ferramentas de compilação rodam dentro das imagens:
|
|
174
|
+
|
|
175
|
+
```sh
|
|
176
|
+
sh scripts/verificar.sh
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
O comando verifica a matriz Ruby 3.3, 3.4 e 4.0, prepara a gem e confere sua
|
|
180
|
+
instalação. Os testes de protocolo usam dados sintéticos, TLS verificado e nenhuma
|
|
181
|
+
porta publicada. A [verificação](docs/verificacao.md) distingue os testes de
|
|
182
|
+
contrato, HTTPS, Rails e integração com a API real local.
|
|
183
|
+
|
|
184
|
+
Publicação no RubyGems e deploy não são executados por esses comandos.
|
|
185
|
+
Leia [CONTRIBUTING.md](CONTRIBUTING.md), [SECURITY.md](SECURITY.md) e [LICENSE](LICENSE).
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Segurança
|
|
2
|
+
|
|
3
|
+
Mantenha as chaves no servidor, em variáveis protegidas ou gerenciador de segredos.
|
|
4
|
+
Nunca as inclua no cliente de navegador, Git, URLs, relatórios ou mensagens de log.
|
|
5
|
+
Use chaves com os escopos e domínios necessários. Revogação e autorização atual
|
|
6
|
+
continuam sendo controladas pela API.
|
|
7
|
+
|
|
8
|
+
HTTPS é obrigatório, com verificação de cadeia/hostname. O SDK não segue
|
|
9
|
+
redirecionamentos, não envia cookies, não herda proxies do ambiente e não
|
|
10
|
+
permite substituir cabeçalhos reservados. A opção HTTP de teste se limita a
|
|
11
|
+
loopback explicitamente autorizado. Uma CA privada não desabilita a verificação TLS.
|
|
12
|
+
|
|
13
|
+
Valide assinaturas sobre o corpo bruto antes de processar eventos. Preserve
|
|
14
|
+
deduplicação durável, autorização da aplicação e limites no servidor HTTP que
|
|
15
|
+
recebe o webhook. Repetições legítimas podem ocorrer dentro da janela de assinatura.
|
|
16
|
+
No Action Mailbox, configure armazenamento protegido e retenção de mensagens.
|
|
17
|
+
|
|
18
|
+
Não registre corpos MIME/JSON, endereços, URLs temporárias ou segredos para
|
|
19
|
+
diagnosticar falhas. A gem não registra requisições automaticamente. `inspect`
|
|
20
|
+
de configuração, cliente, transporte e resposta omite credenciais/conteúdo.
|
|
21
|
+
|
|
22
|
+
Em caso de falha em download por blocos, descarte o arquivo parcial antes de
|
|
23
|
+
usá-lo. Em caso de timeout de envio, consulte a intenção por idempotência antes de
|
|
24
|
+
repetir. Não há repetição automática no transporte; tarefas Rails podem possuir
|
|
25
|
+
sua própria política de repetição, que deve preservar a chave/intenção.
|
|
26
|
+
|
|
27
|
+
Relate vulnerabilidades pelo canal privado de manutenção do projeto, com uma
|
|
28
|
+
reprodução sintética. Não publique credenciais ou dados de clientes em issues.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionMailbox
|
|
4
|
+
module Ingresses
|
|
5
|
+
module Mailcore
|
|
6
|
+
class InboundEmailsController < ::ActionMailbox::BaseController
|
|
7
|
+
LIMITE_EVENTO_BYTES = ::Mailcore::LimiteIngress::LIMITE_BYTES
|
|
8
|
+
class_attribute :cliente_mailcore, instance_accessor: false, default: nil
|
|
9
|
+
|
|
10
|
+
def create
|
|
11
|
+
return head :content_too_large if request.content_length.to_i > LIMITE_EVENTO_BYTES
|
|
12
|
+
|
|
13
|
+
corpo = request.body.read(LIMITE_EVENTO_BYTES + 1).to_s
|
|
14
|
+
return head :content_too_large if corpo.bytesize > LIMITE_EVENTO_BYTES
|
|
15
|
+
|
|
16
|
+
segredo = segredo_assinatura
|
|
17
|
+
return head :service_unavailable unless segredo.is_a?(String) && !segredo.empty?
|
|
18
|
+
|
|
19
|
+
evento = ::Mailcore.verificar_assinatura(
|
|
20
|
+
corpo_bruto: corpo,
|
|
21
|
+
cabecalhos: {
|
|
22
|
+
'svix-id' => request.headers['svix-id'],
|
|
23
|
+
'svix-timestamp' => request.headers['svix-timestamp'],
|
|
24
|
+
'svix-signature' => request.headers['svix-signature']
|
|
25
|
+
},
|
|
26
|
+
segredo: segredo
|
|
27
|
+
)
|
|
28
|
+
return head :no_content unless evento[:type] == 'email.received'
|
|
29
|
+
|
|
30
|
+
dados = evento[:data]
|
|
31
|
+
unless dados.is_a?(Hash) && dados[:email_id].is_a?(String) && !dados[:email_id].empty?
|
|
32
|
+
return head :unprocessable_entity
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
fonte = ::Mailcore::MensagemActionMailbox.new(
|
|
36
|
+
email_id: dados[:email_id], destinatarios: dados[:to],
|
|
37
|
+
cliente: self.class.cliente_mailcore || ::Mailcore.cliente
|
|
38
|
+
).original
|
|
39
|
+
::ActionMailbox::InboundEmail.create_and_extract_message_id!(fonte)
|
|
40
|
+
head :no_content
|
|
41
|
+
rescue ::Mailcore::ErroAssinatura
|
|
42
|
+
head :unauthorized
|
|
43
|
+
rescue ::Mailcore::ErroEnvelopeActionMailbox
|
|
44
|
+
head :unprocessable_entity
|
|
45
|
+
rescue ::Mailcore::Erro
|
|
46
|
+
head :service_unavailable
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def segredo_assinatura
|
|
52
|
+
segredo = Rails.application.credentials.dig(:action_mailbox, :mailcore_segredo) ||
|
|
53
|
+
::Mailcore.configuracao.segredo_webhook
|
|
54
|
+
segredo.respond_to?(:call) ? segredo.call : segredo
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/docs/cobertura.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Cobertura da gem Mailcore
|
|
2
|
+
|
|
3
|
+
A versão `0.1.0.alpha.1` reúne **16 recursos principais, 30 classes de recursos e
|
|
4
|
+
151 métodos HTTP**. O contrato foi conferido no backend
|
|
5
|
+
`e001da831365c536f5764dc703fb7a03964f807f` e no SDK Node
|
|
6
|
+
`a234dc2d6f22389f8a57046a142d82a4a4feddf5`, em 14/11/2026. A superfície Ruby
|
|
7
|
+
preserva os caminhos, campos e envelopes selecionados nessa conferência.
|
|
8
|
+
|
|
9
|
+
Os nomes abaixo partem de uma instância `cliente = Mailcore::Cliente.new(...)`.
|
|
10
|
+
Pedidos e filtros são `Hash` posicionais; opções da operação são palavras-chave,
|
|
11
|
+
como `chave_idempotencia:`, `tempo_limite:`, `limite_resposta_bytes:` e
|
|
12
|
+
`cabecalhos:`. Os prazos usam segundos. Os métodos síncronos retornam
|
|
13
|
+
`Mailcore::Resposta`, com `dados`, `status` e `cabecalhos`; `resposta[:id]`
|
|
14
|
+
consulta a chave correspondente em `dados`.
|
|
15
|
+
|
|
16
|
+
## Recursos e métodos
|
|
17
|
+
|
|
18
|
+
| Recurso | Métodos |
|
|
19
|
+
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
20
|
+
| `emails` | `enviar`, `listar`, `obter`, `reagendar`, `cancelar`, `eventos`, `obter_por_idempotencia`, `baixar_anexo` |
|
|
21
|
+
| `lotes` | `enviar`, `obter_por_idempotencia` |
|
|
22
|
+
| `dominios` | `criar`, `listar`, `obter`, `atualizar`, `registros_dns`, `verificar`, `remover` |
|
|
23
|
+
| `chaves_api` | `criar`, `listar`, `renomear`, `revogar` |
|
|
24
|
+
| `contatos` | `criar`, `listar`, `obter`, `atualizar`, `remover`, `adicionar_em_lote`, `alterar_em_lote` |
|
|
25
|
+
| `contatos.segmentos` | `listar`, `adicionar`, `remover` |
|
|
26
|
+
| `contatos.topicos` | `listar`, `atualizar` |
|
|
27
|
+
| `contatos.importacoes` | `prever`, `criar`, `obter` |
|
|
28
|
+
| `contatos.exportacoes` | `criar`, `listar`, `obter`, `baixar`, `baixar_fluxo` |
|
|
29
|
+
| `segmentos` | `criar`, `listar`, `obter`, `atualizar`, `remover` |
|
|
30
|
+
| `topicos` | `criar`, `listar`, `obter`, `atualizar`, `remover` |
|
|
31
|
+
| `propriedades_contatos` | `criar`, `listar`, `obter`, `atualizar`, `remover` |
|
|
32
|
+
| `modelos` | `criar`, `listar`, `obter`, `atualizar`, `remover`, `publicar`, `duplicar`, `prever`, `testar`, `obter_previa` |
|
|
33
|
+
| `modelos.versoes` | `listar`, `obter`, `restaurar`, `prever` |
|
|
34
|
+
| `modelos.pastas` | `listar`, `criar`, `atualizar`, `remover` |
|
|
35
|
+
| `webhooks` | `criar`, `listar`, `obter`, `atualizar`, `remover` |
|
|
36
|
+
| `webhooks.eventos` | `listar`, `obter`, `listar_tentativas`, `reenviar` |
|
|
37
|
+
| `campanhas` | `criar`, `listar`, `obter`, `atualizar`, `remover`, `duplicar`, `revisar`, `enviar`, `cancelar`, `prever`, `ler_previa`, `testar`, `obter_metricas`, `listar_destinatarios`, `listar_links_clicados` |
|
|
38
|
+
| `automacoes` | `criar`, `listar`, `obter`, `atualizar`, `remover`, `duplicar`, `parar` |
|
|
39
|
+
| `automacoes.execucoes` | `listar`, `obter` |
|
|
40
|
+
| `eventos` | `criar`, `listar`, `obter`, `atualizar`, `remover`, `enviar` |
|
|
41
|
+
| `recebimento` | `listar`, `obter`, `listar_anexos`, `obter_anexo`, `baixar_original`, `baixar_anexo`, `baixar_original_fluxo`, `baixar_anexo_fluxo` |
|
|
42
|
+
| `caixas` | `criar`, `listar`, `obter`, `atualizar`, `remover` |
|
|
43
|
+
| `caixas.etiquetas` | `criar`, `listar`, `obter`, `atualizar`, `remover` |
|
|
44
|
+
| `caixas.conversas` | `listar`, `buscar`, `obter`, `atualizar`, `remover` |
|
|
45
|
+
| `caixas.conversas.etiquetas` | `adicionar`, `substituir`, `remover` |
|
|
46
|
+
| `caixas.conversas.emails` | `listar`, `obter`, `responder`, `encaminhar` |
|
|
47
|
+
| `caixas.rascunhos` | `criar`, `listar`, `obter`, `atualizar`, `remover`, `enviar` |
|
|
48
|
+
| `cli.concessoes` | `listar`, `revogar` |
|
|
49
|
+
|
|
50
|
+
`cli` é a classe agregadora que expõe `concessoes`, completando as 30 classes.
|
|
51
|
+
A função local `Mailcore.verificar_assinatura` verifica webhooks sem fazer
|
|
52
|
+
requisições HTTP e não integra a contagem dos 151 métodos.
|
|
53
|
+
|
|
54
|
+
## Pedidos, respostas e paginação
|
|
55
|
+
|
|
56
|
+
A gem mantém os nomes em português do contrato HTTP. Campos como
|
|
57
|
+
`remetente`, `destinatarios`, `agendada_em`, `segmento_id` e `revisao` são enviados
|
|
58
|
+
conforme fornecidos. O servidor valida regras de negócio, permissões e os DTOs
|
|
59
|
+
específicos de cada recurso. A gem não converte um pedido de caixa em envio
|
|
60
|
+
transacional nem acrescenta campos para ampliar permissões.
|
|
61
|
+
|
|
62
|
+
JSON admite strings e símbolos como chaves, texto codificável em UTF-8, inteiros,
|
|
63
|
+
floats finitos, booleanos, arrays, hashes e `nil`. Objetos arbitrários e números
|
|
64
|
+
que seriam serializados como texto são recusados. Nomes de chave precisam
|
|
65
|
+
permanecer únicos após a conversão para UTF-8. Respostas JSON usam chaves
|
|
66
|
+
simbolizadas, preservando arrays e valores anuláveis.
|
|
67
|
+
|
|
68
|
+
Omissão de um campo e seu envio explícito com `nil` têm significados diferentes.
|
|
69
|
+
O transporte conserva `nil` e `false` no corpo; não aplica `compact`. Nos filtros
|
|
70
|
+
da URL, `nil` é omitido e `false` permanece como `false`. Identificadores são
|
|
71
|
+
codificados como um segmento de caminho; UUIDs, aliases, nomes de eventos e
|
|
72
|
+
endereços de contato não viram trechos de URL controlados pela aplicação.
|
|
73
|
+
|
|
74
|
+
A paginação é explícita. O cliente consulta uma página por chamada e devolve os
|
|
75
|
+
cursores sem recalcular ou carregar páginas adicionais. O servidor pode aplicar
|
|
76
|
+
limites diferentes por recurso; os envelopes permanecem próprios:
|
|
77
|
+
|
|
78
|
+
| Envelope dentro de `resposta.dados` | Grupos |
|
|
79
|
+
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
80
|
+
| `{ dados:, proximo: }` | E-mails, domínios e concessões CLI |
|
|
81
|
+
| `{ dados:, tem_mais:, proximo_cursor: }` | Contatos, exportações, segmentos, tópicos, propriedades de contatos, modelos/versões, webhooks/eventos/tentativas |
|
|
82
|
+
| `{ dados:, proximo:, tem_mais: }` | Campanhas e suas listas, automações/execuções, eventos personalizados, recebimento e caixas |
|
|
83
|
+
| `{ dados: }` | Chaves de API, pastas de modelos e eventos de entrega de um e-mail |
|
|
84
|
+
|
|
85
|
+
## Contratos por área
|
|
86
|
+
|
|
87
|
+
| Área | Regras relevantes |
|
|
88
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
89
|
+
| Envio transacional | Texto e/ou HTML, Cc/Cco, cabeçalhos e anexos CID conforme o DTO de composição. Até 50 destinatários somados, HTML + texto até 500 KiB, JSON do endpoint até 8 MiB, até 10 anexos com soma decodificada de 5 MiB nos tipos aceitos. A gem não baixa anexos remotos do envio transacional. Aceitação retorna `id` e `estado`; a entrega é acompanhada separadamente. |
|
|
90
|
+
| Envio por modelo | Modelo publicado por ID/alias e variáveis. Usa o DTO específico, com anexos básicos e sem compor simultaneamente conteúdo direto, Cc/Cco, cabeçalhos ou CID. |
|
|
91
|
+
| Lotes | Array de 1 a 100 mensagens, com aceitação atômica e ordenada. Sem anexos, Cc/Cco ou cabeçalhos de mensagem. Uma operação gera uma chamada; a gem não divide o lote ou repete automaticamente seu envio. |
|
|
92
|
+
| Agendamento | `agendada_em` usa ISO 8601 com fuso, dentro da janela de até 30 dias. Reagendar e cancelar dependem do estado atual e da autorização da chave. |
|
|
93
|
+
| Domínios | Capacidades de envio/recebimento `enabled` ou `disabled`, mantendo ao menos uma habilitada. Tracking e diagnóstico seguem os campos do servidor. Remover devolve o estado suspenso do domínio. |
|
|
94
|
+
| Chaves de API | O segredo aparece somente na criação. A alteração pública renomeia a chave; a revogação devolve resposta sem conteúdo. A listagem não usa cursor. |
|
|
95
|
+
| Contatos | CRUD por UUID ou e-mail; associações exigem UUID. Nomes anuláveis, propriedades tipadas e preferências são preservados. Operações em lote admitem até 1.000 contatos. Tópicos usam as assinaturas `opt_in` e `opt_out`. |
|
|
96
|
+
| Importação de contatos | CSV UTF-8 em string JSON, até 8 MiB e 100.000 registros, com mapeamento por índice de coluna. O upload admite JSON até 50 MiB para acomodar os escapes. `prever` não persiste; `criar` inicia processamento e `obter` acompanha o estado. Não há método de listagem de importações. |
|
|
97
|
+
| Exportação de contatos | Criar com filtros, consultar/listar e baixar CSV. Criação e download revalidam o papel administrativo. `baixar` devolve bytes; `baixar_fluxo` entrega os blocos ao consumidor e devolve a contagem total. |
|
|
98
|
+
| Modelos | CRUD e publicação por UUID/alias, com revisão exigida na edição, publicação e restauração. Versões imutáveis, pastas hierárquicas, prévias e testes com destinatários explícitos. `obter_previa(organizacao_id, previa_id)` lê HTML pela rota autenticada. |
|
|
99
|
+
| Campanhas | Marketing para segmento, tópico opcional, revisão e confirmação explícita no envio. Testes usam destinatários explícitos. Cancelamento respeita o estado da campanha; despacho concluído não comprova entrega. Métricas mantêm `nil` quando a telemetria está indisponível. |
|
|
100
|
+
| Automações | Fluxos ativos/inativos, etapas e conexões informadas juntas, condições, esperas, intervalos e ações do contrato. Parar impede novas inscrições e preserva as execuções aceitas. A gem consulta execuções; não oferece mutação individual dessas execuções. |
|
|
101
|
+
| Eventos personalizados | Definição com nome imutável e esquema opcional. Enviar ocorrência exige exatamente `email` ou `contato_id`, com `dados` até 64 KiB. Esquema `nil` difere de `{}`. O nome literal `send` continua consultável e editável pelos métodos de definição. |
|
|
102
|
+
| Webhooks | CRUD, eventos, tentativas e reenvio do evento original. O servidor preserva identificador e payload no replay. Consumidores devem verificar a assinatura sobre o corpo bruto e deduplicar o `svix-id` antes de executar efeitos. |
|
|
103
|
+
| Recebimento | Lista, busca, intervalo temporal e metadados/anexos. `obter` aceita `formato_html: 'cid'` ou `'data_uri'` no Hash de filtros; o formato efetivo pode ser `cid` por limite de expansão. Cabeçalhos repetidos permanecem pares ordenados. MIME original e anexos usam rotas fixas autenticadas. |
|
|
104
|
+
| Caixas | Caixas, etiquetas, conversas, mensagens e rascunhos usam IDs explícitos e revisões conforme o DTO. Responder e encaminhar aceitam a intenção de envio pela fila. Remover conversa a move para a lixeira. Etiquetas podem ser substituídas por `[]` para remover todas as associações. |
|
|
105
|
+
| Anexos de caixas | Resposta/encaminhamento admitem Base64 ou URL e CID conforme o contrato; rascunhos recusam CID fornecido pelo cliente. O servidor resolve URLs com suas próprias proteções. O cliente Ruby não navega nessas URLs. JSON de caixas é limitado a 8 MiB no endpoint. |
|
|
106
|
+
| Concessões CLI | Listagem e revogação das concessões autorizadas do ator atual. Fluxos interativos de autorização e renovação permanecem fora do cliente comum. |
|
|
107
|
+
|
|
108
|
+
O teto de **50 MiB do transporte JSON** acomoda a importação escapada; ele não
|
|
109
|
+
substitui limites menores de cada endpoint, como os 8 MiB do envio e das caixas.
|
|
110
|
+
O limite padrão de leitura da resposta é **16 MiB**, configurável por operação.
|
|
111
|
+
Downloads usam `String` binária. Os métodos com sufixo `_fluxo` exigem um bloco,
|
|
112
|
+
leem de forma síncrona e mantêm o prazo e o teto de bytes durante o consumo.
|
|
113
|
+
|
|
114
|
+
## Recebimento, Rails e limites de superfície
|
|
115
|
+
|
|
116
|
+
O recurso `recebimento` acessa somente as rotas autenticadas de consulta, anexos,
|
|
117
|
+
MIME original e download. URLs temporárias presentes nos metadados são
|
|
118
|
+
capacidades sensíveis; a gem não as segue automaticamente e não envia a chave
|
|
119
|
+
Mailcore a um endereço devolvido pela API. O conteúdo recebido está sujeito à
|
|
120
|
+
retenção e à disponibilidade atuais do servidor. Listagem não garante que MIME
|
|
121
|
+
ou anexos ainda estejam disponíveis.
|
|
122
|
+
|
|
123
|
+
Rails é opcional. As integrações Action Mailer e Action Mailbox usam a mesma gem
|
|
124
|
+
e o mesmo cliente HTTP; o cliente Ruby básico não exige carregar Rails. Templates
|
|
125
|
+
Ruby/Rails podem produzir o HTML e o texto enviados ao Mailcore. A renderização
|
|
126
|
+
React pertence à integração JavaScript e não faz parte desta gem.
|
|
127
|
+
|
|
128
|
+
Esta matriz cobre os 151 métodos selecionados, sem anunciar toda a API do
|
|
129
|
+
produto. As rotas públicas de métricas globais e auditoria existentes no backend
|
|
130
|
+
ficam fora desta versão, assim como no SDK Node usado na conferência. Nenhum
|
|
131
|
+
helper de dashboard, sessão, visualização compartilhada ou token temporário foi
|
|
132
|
+
acrescentado por inferência. Endereços de anexos de caixas com prefixo de dashboard
|
|
133
|
+
continuam como metadados; não há um método público fictício para baixá-los.
|
|
134
|
+
|
|
135
|
+
O teste de contrato exercita os 151 métodos contra um transporte coletor,
|
|
136
|
+
conferindo método, caminho, corpo, filtros, formato e opções. Os testes HTTPS
|
|
137
|
+
reais com servidor sintético cobrem o transporte separadamente. Essa combinação
|
|
138
|
+
não equivale a executar cada endpoint contra uma conta real ou a medir capacidade
|
|
139
|
+
de envio em produção; os resultados efetivamente executados estão no relatório
|
|
140
|
+
de verificação do repositório.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"data_utc": "2026-11-14T13:53:31.104091+00:00",
|
|
3
|
+
"resultado": "falhou",
|
|
4
|
+
"ambiente": "api_postgres_tls_exclusivos_sem_worker_sem_portas",
|
|
5
|
+
"instalacao_host": false,
|
|
6
|
+
"rede_interna": true,
|
|
7
|
+
"portas_publicadas": false,
|
|
8
|
+
"worker_executado": false,
|
|
9
|
+
"smtp_executado": false,
|
|
10
|
+
"deploy_executado": false,
|
|
11
|
+
"tls_api_verificado": false,
|
|
12
|
+
"tls_postgres": "nao_verificado",
|
|
13
|
+
"cenarios": [],
|
|
14
|
+
"etapa": "preparar_identidades",
|
|
15
|
+
"imagens": {
|
|
16
|
+
"cliente": "sha256:149a81a02cb846ea058823aabc578bde130e86c93800e6335b2e89ed44e46d22",
|
|
17
|
+
"api_runtime": "sha256:158e95e9b87f2a126c038e6931b2f956fe6120f3566fd047a29250bd7783ff33",
|
|
18
|
+
"compilador": "sha256:8fa55b2f3ddf97471ab6a767bfa3f37e6bad0986ba823e75fea57e2a2a5c3073",
|
|
19
|
+
"postgres": "sha256:f1ea2a1a4fb7b68d4836be1fdcb8a6466a89b072158bf9d97663600538c294c5"
|
|
20
|
+
},
|
|
21
|
+
"backend_revisao": "52992028e5be8888db699d82e3cc8484140d6a67",
|
|
22
|
+
"backend_fontes_sha256": "d31c825268a64c858276827f47d27e506f543421327f487d7165e511359e248f",
|
|
23
|
+
"backend_fontes_alterados": false,
|
|
24
|
+
"api_compilada_neste_ensaio": true,
|
|
25
|
+
"falha": "Ensaio interrompido; recursos exclusivos serão removidos.",
|
|
26
|
+
"limpeza_concluida": true
|
|
27
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"data_utc": "2026-11-14T13:51:11.583482+00:00",
|
|
3
|
+
"resultado": "aprovado",
|
|
4
|
+
"ambiente": "api_postgres_tls_exclusivos_sem_worker_sem_portas",
|
|
5
|
+
"instalacao_host": false,
|
|
6
|
+
"rede_interna": true,
|
|
7
|
+
"portas_publicadas": false,
|
|
8
|
+
"worker_executado": false,
|
|
9
|
+
"smtp_executado": false,
|
|
10
|
+
"deploy_executado": false,
|
|
11
|
+
"tls_api_verificado": true,
|
|
12
|
+
"tls_postgres": "verify-full",
|
|
13
|
+
"cenarios": [
|
|
14
|
+
"chaves_full_sending_escopo_inspect",
|
|
15
|
+
"envio_composto_idempotencia_409_download_cancelamento",
|
|
16
|
+
"lote_atomico_repeticao_conflito",
|
|
17
|
+
"dominios_capacidades_tracking_crud",
|
|
18
|
+
"contatos_nulos_catalogos_exportacao_csv_fluxo",
|
|
19
|
+
"modelos_pastas_versoes_previa_publicacao_cas",
|
|
20
|
+
"marketing_crud_sem_disparo",
|
|
21
|
+
"caixas_etiquetas_rascunhos_cas_sem_envio",
|
|
22
|
+
"recebimento_mime_real_pendente_bytes_fluxo",
|
|
23
|
+
"cli_revogacao_e_limpeza_crud"
|
|
24
|
+
],
|
|
25
|
+
"etapa": "concluido",
|
|
26
|
+
"imagens": {
|
|
27
|
+
"cliente": "sha256:1b334f043e1073752505db50e5d893d4c691a6d5478125ad5eee0038af2a2967",
|
|
28
|
+
"api_runtime": "sha256:158e95e9b87f2a126c038e6931b2f956fe6120f3566fd047a29250bd7783ff33",
|
|
29
|
+
"compilador": "sha256:8fa55b2f3ddf97471ab6a767bfa3f37e6bad0986ba823e75fea57e2a2a5c3073",
|
|
30
|
+
"postgres": "sha256:f1ea2a1a4fb7b68d4836be1fdcb8a6466a89b072158bf9d97663600538c294c5"
|
|
31
|
+
},
|
|
32
|
+
"backend_revisao": "83c2fa9859e03473e7af56eede97c6a1cad8592c",
|
|
33
|
+
"backend_fontes_sha256": "d31c825268a64c858276827f47d27e506f543421327f487d7165e511359e248f",
|
|
34
|
+
"backend_fontes_alterados": false,
|
|
35
|
+
"api_compilada_neste_ensaio": true,
|
|
36
|
+
"api_binario_sha256": "3cd977ab90dbfcc0067b7d0b85d120ae9132f4e873cc223298f901fc1dbd35bf",
|
|
37
|
+
"migracoes_aplicadas": 23,
|
|
38
|
+
"ruby": "3.4.12",
|
|
39
|
+
"recebimento_processado_por_worker": false,
|
|
40
|
+
"rails_real_exercitado": false,
|
|
41
|
+
"persistencia_sintetica": {
|
|
42
|
+
"mensagens": 3,
|
|
43
|
+
"tarefas_envio": 3,
|
|
44
|
+
"recebidos": 1,
|
|
45
|
+
"recebidos_pendentes": 1
|
|
46
|
+
},
|
|
47
|
+
"limpeza_concluida": true
|
|
48
|
+
}
|