Menuabrir
Em fluxoAtualizado em 14 de mai. de 2026, 00:06

Este módulo depende de

3
  • crmResolve contatos por email; atualiza status em email_send via contact_id
  • intelligenceEmite eventos opened/clicked para calcular engagement_score
  • trackingWebhook eventos consumidos e persistidos pra auditoria

Módulos que dependem deste

1
  • intelligenceLê email_send para engagement (opens, clicks)

Email

Status: 🟡 Em fluxo (mega-módulo, várias features convivendo) Code: backend/app/modules/email UI: frontend/src/app/(admin)/[slug]/admin/email Última revisão deste doc: 2026-05-13 por Felipe + Claude Dependências fortes: crm (resolve recipient + atualiza status), intelligence (engagement_score), tracking (eventos)


1. Identidade

O que faz (uma frase)

Sistema completo de email marketing + transacional com adapter pattern (Brevo default, Resend, SendGrid) — campanhas broadcast, sequências automatizadas, jornadas visuais, A/B tests, signup forms, supressões, validação DKIM/SPF/DMARC e webhooks de provider para tracking de opens/clicks/bounces.

Por que existe (negócio)

Email continua sendo canal-chave de nutrição (jornadas multi-toque) e transacional (confirmação, recuperação senha, notificações). Sem este módulo, Mandir teria que integrar Brevo/Mailchimp externamente — perdendo o cruzamento com CRM + intelligence.

Com Email integrado:

  • Mais Consciente envia broadcast pra 250 alunos com 1 clique.
  • Sequência "Onboarding 7 dias" automática após signup.
  • Jornada visual com nó condicional ("se abriu email A, mandar B; senão, mandar C").
  • A/B test de assunto antes de send em massa.
  • Engagement score do contato considera open/click rate.

Por que existe (técnico)

  • Adapter pattern (Brevo / Resend / SendGrid) — trocar provider sem refactor.
  • MJML compilation — templates responsivos sem código manual.
  • Worker assíncrono pra dispatch em batch (chunks de 25 + sleep 200ms).
  • Webhook unificado — diferentes providers, mesma tabela email_send.
  • Suppression list global (per-tenant) — opt-out + bounces + complaints.
  • Sender Identity validation — DKIM/SPF/DMARC checked via dnspython.

Status atual

Em produção mas com features em maturidade desigual:

  • Broadcast + transactional: estável.
  • Sequences: estável (worker process_sequence_steps).
  • Journeys (visual flow): estável estrutural (worker process_journey_runs); UI de edição em iteração.
  • A/B tests: schema cravado, lógica de winner_decision manual hoje.
  • Signup forms: estável; sem analytics de conversion ainda.

Próxima mudança: unificar dispatch de email + WhatsApp em pipeline de "campaign" cross-canal; adicionar embeddings em templates pra recommendation.


2. Cases de uso reais

Case 1: Broadcast pra lista (Brevo)

Felipe cria campanha "Aula extra sexta", seleciona lista 250 alunos, scheduling immediate. dispatch_campaign_task enfileira → batch 25 + sleep 200ms → BrevoAdapter.sendemail_send por destinatário → webhook delivered/opened/clicked atualiza status.

Case 2: Sequência onboarding 7 dias

Form signup cria EmailSequenceEnrollment em EmailSequence("Onboarding") com 5 steps (delay_hours: 0, 24, 72, 120, 168). Beat process_sequence_steps (a cada 30min) calcula due_at = enrolled_at + sum(delays[0..current_step]), dispara quando now >= due_at, incrementa current_step.

Case 3: Jornada visual com branch condicional

Felipe edita journey: trigger (tag.added=lead-quente) → email A → delay 24h → condition (opened_email A?) → true: email B / false: email C → end. Beat process_journey_runs avança nó por nó respeitando wait_until em delays.

Case 4: Webhook Brevo opened

Brevo envia POST /api/email/webhooks/brevo com event=opened, message-id=X. Webhook atualiza EmailSend (matched por provider_message_id) → status=opened, opened_at=now. Emite crm.contact.email_opened event → intelligence atualiza engagement_score.


3. Oportunidades de negócio

  • Email-as-a-service standalone: "Mandir Email" sem o resto do Suite (Brevo wrapper + sequences + journeys + A/B). Pricing menor.
  • Vertical templates: templates pré-aprovados por vertical (e-commerce abandoned cart, course re-engagement, etc.).
  • Deliverability score: scoring proprietário (DKIM ok + bounce rate + complaint rate + open rate vs benchmark) — premium feature.
  • Cross-channel orchestration: journey nodes que podem ser email OU WhatsApp OU SMS (já preparado em Hub.1).
  • Embeddings em templates: recomendar template baseado em contexto do contato.

Riscos: dependência Brevo (preços podem subir); deliverability degrada se admin não configura DKIM corretamente; LGPD/CAN-SPAM exige opt-out estrito.


4. Arquitetura interna

Arquivos

ArquivoPropósito
models.py15 tabelas
service.pysend_transactional, schedule_dispatch_campaign, compile_campaign_html, render_template
tasks.pydispatch_campaign_task
sequence_worker.pyprocess_sequence_steps (Beat 30min)
journey_worker.pyprocess_journey_runs (Beat 30min)
*_routes.pySub-routers por sub-recurso
adapters/base.py, brevo.py, resend.py, sendgrid.py

Adapters externos

ProviderDefault?EndpointAuthWebhook events
Brevohttps://api.brevo.com/v3/smtp/emailAPI keydelivered, opened, click, soft/hard_bounce, spam, unsubscribed
Resendhttps://api.resend.com/emailsAPI keydelivered, bounced, complained
SendGridhttps://api.sendgrid.com/v3/mail/sendAPI keyprocessed, delivered, opened, clicked, bounced, dropped, deferred

Decisão via settings.email_provider (env EMAIL_PROVIDER).

Tasks Celery

TaskScheduleIdempotência
email.dispatch_campaignOn-demandLoop por recipient com chunks 25 + sleep 200ms
email.process_sequence_stepsBeat 30mindue_at calculado, status active only, suppression check
email.process_journey_runsBeat 30minwait_until respeitado, status active only, suppression check

5. Tabelas + relacionamentos (15)

Todas com tenant_id indexed. Sem FK cross-módulo (UUIDs soltos).

Campanhas / Sends (2)

  • email_campaign — slug, subject, body_html/mjml, status, campaign_type (broadcast/sequence_step/journey_action/transactional), scheduled_for, sent_at, sent_count/open_count/click_count/etc., template_id, sender_identity_id, list_id, segment_id, ab_test_id.
  • email_send — campaign_id (FK CASCADE), to_email, status (queued/sent/delivered/opened/clicked/bounced/failed), provider, provider_message_id (key pra webhook match), sent_at/opened_at/clicked_at, sequence_enrollment_id, ab_variant.

Templates / Senders (2)

  • email_template — name, subject_default, body_html/mjml/json (Unlayer design), thumbnail_url, is_global.
  • email_sender_identity — from_email, from_name, reply_to, domain, status (pending/verified/failed), is_default, dns_spf_ok/dkim_ok/dmarc_ok, dns_checked_at.

Listas / Membros / Suppressions (3)

  • email_list — name, double_optin, member_count cache.
  • email_list_member — list_id (FK CASCADE), email, status (subscribed/unsubscribed/bounced/complained/pending_confirm), source. UNIQUE (list_id, email).
  • email_suppression — email, reason (bounce_hard/bounce_soft/complaint/unsubscribed/manual). UNIQUE (tenant_id, email).

Sequences (3)

  • email_sequence — name, status (draft/active/paused/archived), trigger_event.
  • email_sequence_step — sequence_id (FK CASCADE), step_order, template_id, delay_hours, condition_json.
  • email_sequence_enrollment — sequence_id, email, current_step, status (active/completed/cancelled/paused), enrolled_at.

Journeys (3)

  • email_journey — name, status, trigger_event, trigger_config.
  • email_journey_node — journey_id (FK CASCADE), node_type (trigger/email/delay/condition/ab_split/end), config (JSONB tipo-específico), next_node_id.
  • email_journey_run — journey_id, email, current_node_id, status, wait_until (pra delays), extra (JSONB com variáveis).

A/B Tests + Forms (2)

  • email_ab_test — name, metric (open_rate/click_rate), sample_pct, winner_variant, decided_at.
  • email_signup_form — name, list_id, double_optin, fields (JSONB), success_message, redirect_url, submit_count.

Relacionamentos cross-módulo

DireçãoOutro móduloComo
↗ Lêcrmemail_send.crm_contact_id (UUID solto), service call get_contact_by_email
↘ Emite eventintelligenceemail.send.opened/clicked → engagement_score
↘ Emite eventcrmcrm.contact.email_opened/clicked/bounced/unsubscribed
↗ Lêtrackingwebhook eventos consumidos pra audit

6. API / Endpoints (~30)

Prefixo /api/email.

Campaigns (7)

MétodoRotaO que faz
GET / POST / PATCH / DELETE/campaignsCRUD
POST/campaigns/{id}/dispatchEnfileira dispatch (Celery)
POST/campaigns/{id}/scheduleAgenda hora específica

Templates / Senders / Lists / Suppressions

  • Templates: GET/POST/GET{id}/PATCH/DELETE.
  • Senders: CRUD + POST /senders/{id}/verify-dns + GET /senders/{id}/dns-status.
  • Lists: CRUD + GET /lists/{id}/members + POST /members + POST /members/bulk + DELETE /members/{id}.
  • Suppressions: GET (filter reason) + POST + bulk + POST /suppressions/check (lookup batch).

Sequences / Journeys / A/B Tests

  • Sequences: CRUD + /{id}/steps + /{id}/enrollments.
  • Journeys: CRUD + /{id}/nodes/batch (criar nós em lote) + /{id}/runs + /{id}/enrollments.
  • A/B Tests: CRUD + /ab-tests/{id}/decide (escolhe winner).

Signup Forms (1 público)

MétodoRotaAuthO que faz
POST/signup-forms/{id}/submitPúblico (sem auth)Submeter form

Reports / Overview / Providers / Webhooks

  • Reports: /reports/campaigns/{id} + /recipients + /lists/{id}.
  • Overview: /overview (stats agregadas).
  • Providers: GET status, GET metadata.
  • Webhooks: POST /webhooks/{brevo,resend,sendgrid} (sem auth, valida via X-Webhook-Token).

7. Configuração

Env vars

VarProvider
EMAIL_PROVIDERbrevo (default) / resend / sendgrid
BREVO_API_KEY / BREVO_SENDER_EMAIL / BREVO_SENDER_NAME / BREVO_WEBHOOK_TOKENBrevo
RESEND_API_KEY / RESEND_SENDER_EMAIL / RESEND_SENDER_NAMEResend
SENDGRID_API_KEY / SENDGRID_SENDER_EMAIL / SENDGRID_SENDER_NAMESendGrid

Kill switches

  • Trocar EMAIL_PROVIDER → todas as envios passam pelo novo provider.
  • Set as keys vazias → _adapter() raises RuntimeError("brevo_not_configured").
  • Pause sequence/journey: status='paused'.

DNS por provider (auto-validation)

ProviderSPF includeDKIM selector
Brevospf.brevo.combrevo
Resendamazonses.comresend
SendGridsendgrid.nets1

8. Operações

Como criar campanha

UI: /admin/email/campaigns → "Nova" → form (subject, body MJML/HTML, list, sender) → save (status=draft) → "Dispatch" (status=sending → sent).

Como configurar sender com DKIM

  1. UI /admin/email/senders → adicionar.
  2. Sistema mostra TXTs (SPF + DKIM + DMARC) pra colar no DNS provider.
  3. Wait DNS propagar (~10-30min).
  4. Click "Verify DNS" → backend chama dnspython.asyncresolver → atualiza dns_*_ok.

Troubleshooting

Sintoma: Campanha em status=sending eternamente

Diagnóstico: verificar docker logs mandir-suite-worker | grep email.dispatch. Causa comum: suppression ate todos recipients; provider rate limit; auth falhou (provider_not_configured).

Sintoma: Webhook Brevo não chega

Diagnóstico: verificar BREVO_WEBHOOK_TOKEN vazio (sem validação) ou correto. Verificar URL configurada no painel Brevo.

Sintoma: Email entra como spam

Causa: DKIM/SPF/DMARC não validado. Fix: completar DNS records + click "Verify DNS".


9. Métricas e observabilidade

Logger keyQuando emite
email.send.sentTransactional/campaign enviado OK
email.send.failedFalhou
email.campaign.dispatch.start/doneWorker iniciou/terminou
email.webhook.{provider}.receivedWebhook chegou
email.suppression.createdAdd to suppression (auto via webhook ou manual)

10. Limitações e débitos técnicos conhecidos

#ItemPlano
1condition_json em sequence_step não implementadoFase 2
2Template vars sem escape (XSS risk se var vem de user input)Adicionar Jinja2 com autoescape
3MJML compile fail = blockerFallback pra body_html
4Sem deduplicação cross-sequenceMesmo contato em N sequences = N emails
5Suppression irreversívelManual remove
6Provider lock-in (DKIM diferente)Trocar exige reconfig DNS
7Cross-module FKs: crm_contact_id UUID soltoOK (convenção Mandir)
8A/B winner manualAuto-decision via beat task TBD

11. Histórico relevante

  • Estável desde Suite v2. Adapters Brevo/Resend/SendGrid maduros.
  • Sequences + Journeys entregues em ondas distintas (sequences primeiro, journeys depois com flow-builder).
  • Signup forms públicos: Sprint pós-monolito.