Integração com o Postfix
O Papéli deve atuar exclusivamente nos serviços de envio de usuários autenticados —
as portas submission (587) e submissions (465). Nunca na porta 25,
para não alterar emails recebidos de terceiros.
Fluxo recomendado
| Porta | Serviço | Milters (em ordem) | Responsabilidade |
|---|---|---|---|
25 |
smtp (entrada) | sem papeli | Emails externos recebidos |
587 |
submission | papeli.sock → opendkim |
Envio autenticado STARTTLS |
465 |
submissions (SMTPS) | papeli.sock → opendkim |
Envio autenticado SSL/TLS |
Configuração do master.cf
# Porta 587 — STARTTLS Submission
submission inet n - y - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
-o smtpd_client_restrictions=permit_sasl_authenticated,reject
-o smtpd_milters=unix:/run/papeli/papeli.sock,inet:localhost:8891
-o milter_default_action=accept
# Porta 465 — SMTPS Implícito
submissions inet n - y - - smtpd
-o syslog_name=postfix/submissions
-o smtpd_tls_wrappermode=yes
-o smtpd_sasl_auth_enable=yes
-o smtpd_client_restrictions=permit_sasl_authenticated,reject
-o smtpd_milters=unix:/run/papeli/papeli.sock,inet:localhost:8891
-o milter_default_action=accept
25 (smtp inet) sem
o unix:/run/papeli/papeli.sock, garantindo que emails externos
recebidos não sejam alterados.
Permissões do Socket UNIX
sudo mkdir -p /run/papeli /etc/papeli/templates
sudo chown -R postfix:postfix /run/papeli /etc/papeli
sudo chmod 770 /run/papeli
/var/spool/postfix/papeli/papeli.sock, ou desative
o chroot mudando a coluna chroot de y para n no master.cf.
Instalação do serviço systemd
sudo cp target/release/papeli /usr/local/bin/papeli
sudo cp deploy/papeli.example.toml /etc/papeli/papeli.toml
sudo cp deploy/papeli.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now papeli
sudo postfix reload
Validando com swaks
swaks --to destinatario@externo.com \
--from carlos@suaempresa.com \
--server localhost \
--port 587 \
--auth LOGIN \
--auth-user carlos@suaempresa.com \
--auth-password 'suasenha' \
--tls \
--body "Olá, este é um teste do Papéli com Postfix!"
Configuração TOML Hierárquica
O Papéli resolve variáveis em 3 camadas de precedência descendente.
Se o remetente for de um domínio não gerenciado (ex: @gmail.com),
o daemon ignora a mensagem sem alterá-la.
Arquivo papeli.toml completo
[server]
# Socket de escuta: UNIX socket ou TCP inet
listen = "unix:/run/papeli/papeli.sock"
# listen = "inet:127.0.0.1:8899"
templates_dir = "/etc/papeli/templates"
[default]
company_name = "Organização Geral"
logo_url = "https://empresa.com/assets/logo-padrao.png"
template_html = "default.mjml"
template_txt = "default.txt.j2"
disclaimer = "Esta mensagem contém informações confidenciais."
[domains."acme.com"]
company_name = "ACME Corporation"
logo_url = "https://acme.com/logo.png"
disclaimer = "Mensagem protegida pela política de confidencialidade da ACME."
[senders."diretoria@acme.com"]
sender_name = "Carlos Silva"
sender_role = "Diretor Executivo"
sender_phone = "+55 11 99999-9999"
[senders."suporte@acme.com"]
sender_name = "Suporte ao Cliente"
sender_role = "Central de Atendimento 24/7"
sender_phone = "0800 123 4567"
Referência das variáveis de template
| Variável | Fonte | Descrição |
|---|---|---|
company_name | config | Nome da empresa/organização |
logo_url | config | URL absoluta do logotipo |
template_html | config | Arquivo MJML do papel de carta |
template_txt | config | Template texto puro Jinja2 |
disclaimer | config | Aviso legal de rodapé |
sender_name | config / header | Nome completo do remetente |
sender_role | config | Cargo/função |
sender_phone | config | Telefone de contato |
body_html | mensagem | Corpo HTML sanitizado |
body_txt | mensagem | Corpo texto puro |
from | header | Endereço de email do remetente |
date | header | Data/hora da mensagem |
papeli.toml entram em vigor instantaneamente
para o próximo email processado. A estrutura interna é protegida por Arc<RwLock<Config>>.
Sem necessidade de reiniciar o serviço systemd ou o Postfix.
Templates MJML
O Papéli compila templates MJML para HTML responsivo usando a biblioteca
mrml (Rust puro). As variáveis são interpoladas com MiniJinja
antes da compilação MJML.
/etc/papeli/templates/. O nome do arquivo é configurado
por domínio ou remetente no papeli.toml.
Template MJML mínimo (default.mjml)
<mjml>
<mj-head>
<mj-attributes>
<mj-all font-family="Arial, sans-serif" font-size="14px" />
</mj-attributes>
</mj-head>
<mj-body background-color="#f4f6f8">
<!-- Cabeçalho com logotipo -->
<mj-section background-color="#ffffff" padding="20px 30px">
<mj-column>
<mj-image src="{{ logo_url }}" width="180px" alt="{{ company_name }}" />
</mj-column>
</mj-section>
<!-- Corpo da mensagem -->
<mj-section background-color="#ffffff" padding="10px 30px 20px">
<mj-column>
<mj-text>
{{ body_html }}
</mj-text>
</mj-column>
</mj-section>
<!-- Assinatura -->
<mj-section background-color="#fafafa" border-top="1px solid #e0e0e0"
padding="15px 30px">
<mj-column>
<mj-text font-size="12px" color="#666666">
<div class="papeli-sig-long" data-papeli="signature"
data-sender="{{ from }}">
<strong>{{ sender_name }}</strong><br>
{{ sender_role }} | {{ company_name }}<br>
Tel: {{ sender_phone }}<br>
<small>{{ disclaimer }}</small>
</div>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
r#"..."#
em strings que contenham # (como cores CSS #ffffff ou
background-color="#f4f6f8"). Use r##"..."## nesses casos.
Sistema de Assinatura Dupla
O módulo src/signatures.rs implementa o mecanismo de dupla assinatura
que resolve o problema clássico de inchaço em threads longas.
Marcadores HTML
Assinatura Longa (mensagem ativa recente):
<div class="papeli-sig-long"
data-papeli="signature"
data-sender="carlos@acme.com">
<strong>Carlos Silva</strong><br>
Diretor de Tecnologia | ACME Corp<br>
Tel: +55 11 99999-9999 | www.acme.com<br>
<small>Aviso de confidencialidade: ...</small>
</div>
Assinatura Curta (substituída no histórico citado):
<div class="papeli-sig-short" data-papeli="signature">
<p style="color: #666; font-size: 11px;">— Carlos Silva (ACME Corp)</p>
</div>
Delimitadores em Texto Puro
Assinatura Longa:
--
<!--papeli:sig:start:long-->
Carlos Silva
Diretor de Tecnologia | ACME Corp
Tel: +55 11 99999-9999
[Aviso legal...]
<!--papeli:sig:end:long-->
Assinatura Curta:
<!--papeli:sig:start:short-->
— Carlos Silva (ACME Corp)
<!--papeli:sig:end:short-->
Algoritmo de Compactação
- O filtro detecta a presença de citações (
<blockquote>,.gmail_quote, etc.). - Varre o DOM localizando instâncias antigas de
.papeli-sig-long. - Substitui cada bloco longo pela versão compacta equivalente (
.papeli-sig-short). - Aplica a nova assinatura longa apenas no final da mensagem atual do remetente.
Processamento MIME
O módulo src/mime.rs é o coração do Papéli: analisa toda a árvore MIME
da mensagem e a reconstrói de forma cirúrgica, preservando 100% dos anexos.
Casos suportados
| Tipo de Email | Estrutura MIME | Comportamento |
|---|---|---|
| Texto puro | text/plain |
Promovido para multipart/alternative com HTML gerado |
| HTML simples | text/html |
Sanitizado e envelopado no template MJML |
| Alternativo | multipart/alternative |
Ambas as partes processadas independentemente |
| Com anexos | multipart/mixed |
Corpo processado; anexos copiados bit-a-bit (SHA-256 verificado) |
| Imagens inline | multipart/related + CID |
CIDs preservados; imagens permanecem como partes da mensagem |
Sanitização HTML
O módulo src/sanitizer.rs usa Ammonia e Scraper
para limpar semanticamente o HTML sem destruir o conteúdo significativo.
O que é removido
class="WordSection1"class="MsoNormal"Condicionais VML (
<!--[if mso]-->)Estilos
mso-* inline
"Enviado do meu iPhone"
"Sent from my Android"
"Get Outlook for iOS"
"Enviado via Gmail"
O que é preservado
- Formatação semântica: negrito, itálico, listas, tabelas, links
- Imagens inline (CID e URLs absolutas)
- Estrutura de cabeçalhos (
h1–h6) - Blocos de código e citações (
blockquote) - Todo o conteúdo textual do email
Protocolo Milter
O src/milter.rs implementa um servidor Milter em Rust puro,
compatível com o protocolo Sendmail Milter v6.
Modos de socket
# papeli.toml
[server]
listen = "unix:/run/papeli/papeli.sock"
# master.cf do Postfix
-o smtpd_milters=unix:/run/papeli/papeli.sock,inet:localhost:8891
Socket UNIX é recomendado: comunicação local mais rápida, sem overhead de rede, sem exposição de porta TCP.
# papeli.toml
[server]
listen = "inet:127.0.0.1:8899"
# master.cf do Postfix
-o smtpd_milters=inet:127.0.0.1:8899,inet:localhost:8891
Modo TCP útil quando o Postfix está em container separado ou quando há necessidade de depuração remota.
Bypass automático
Se o domínio do remetente não estiver configurado no papeli.toml
(ex: @gmail.com, @hotmail.com), o daemon retorna
SMFIS_ACCEPT imediatamente, passando a mensagem inalterada para o próximo milter.
Suite de Testes
O Papéli possui mais de 30 testes automatizados cobrindo todos os cenários de processamento MIME, sanitização e assinatura.
Executar os testes
# Rodar todos os testes
cargo test
# Com output detalhado
cargo test -- --nocapture
# Filtrar por módulo
cargo test mime
cargo test sanitizer
cargo test signatures
Cobertura de testes por módulo
| Módulo | Cenários testados |
|---|---|
mime.rs |
text/plain, text/html, multipart/alternative, multipart/mixed, multipart/related, CID inline, anexos binários (SHA-256), promoção de texto puro |
sanitizer.rs |
remoção de classes MSO/Word, condicionais VML, rodapés de iPhone/Android/Outlook, preservação de links e formatação semântica |
signatures.rs |
injeção de assinatura longa, colapso de assinaturas históricas, delimitadores ASCII em texto puro, múltiplas threads |
templates.rs |
compilação MJML, interpolação de variáveis MiniJinja, templates com todas as variáveis de config |
config.rs |
resolução hierárquica (sender → domain → default), hot-reload, domínios não gerenciados (bypass) |
Fixtures de teste
Emails reais de diferentes clientes (Outlook 365, Apple Mail, Thunderbird, Gmail Web,
Android Gmail, iPhone Mail) estão disponíveis como fixtures em tests/fixtures/
para testes de regressão end-to-end.
docs/FIXTURES.md para a lista completa de fixtures disponíveis
e como adicionar novos cenários de teste.