Your CRM Is Full of Leads That Never Hear From You Again
Most Odoo CRM implementations stop at lead capture. Salespeople manually follow up with the hottest prospects, while hundreds of warm leads sit in the pipeline untouched. No nurture sequence. No re-engagement campaign. No systematic way to move a "not ready yet" lead toward a buying decision. The leads go cold, and the marketing budget that generated them is wasted.
The business impact compounds over time. Studies consistently show that 80% of sales require five or more follow-ups, yet 44% of salespeople give up after one. The gap between "lead captured" and "deal closed" is where email marketing automation lives—and Odoo 19 ships with everything you need to fill it.
This guide covers the complete email marketing integration in Odoo 19: setting up the Email Marketing module alongside CRM, segmenting mailing lists from pipeline data, building emails with the drag-and-drop editor, running A/B tests, creating automated drip campaigns with Marketing Automation, configuring SPF/DKIM/DMARC for deliverability, and measuring attribution from first touch to closed deal.
Setting Up the Odoo 19 Email Marketing Module with CRM Integration
Odoo 19 ships Email Marketing as a standalone app, but its real power emerges when it connects to CRM, Contacts, and Marketing Automation. Here's the correct installation order and configuration to ensure all three modules communicate properly.
Required Modules
| Module | Technical Name | Purpose | Required? |
|---|---|---|---|
| Email Marketing | mass_mailing | Mailing lists, email builder, campaign management | Yes |
| CRM | crm | Lead/opportunity pipeline, contact enrichment | Yes |
| Marketing Automation | marketing_automation | Drip campaigns, workflow triggers, timed sequences | Recommended |
| Link Tracker | link_tracker | UTM tracking, click attribution, shortened URLs | Auto-installed |
Installation and Initial Configuration
# Install all three modules in one pass
./odoo-bin -d your_database -i mass_mailing,crm,marketing_automation --stop-after-init
# Verify the modules are loaded
./odoo-bin shell -d your_database <<PYEOF
modules = env['ir.module.module'].search([
('name', 'in', ['mass_mailing', 'crm', 'marketing_automation']),
('state', '=', 'installed'),
])
for m in modules:
print(f" {{m.name}}: {{m.state}}")
PYEOFOutgoing Mail Server Configuration
Before sending a single campaign, configure a dedicated outgoing mail server for bulk email. Never use the same SMTP server for transactional emails (order confirmations, password resets) and marketing emails. If your marketing sends get flagged as spam, your transactional emails go down with them.
# Recommended setup: two outgoing mail servers
#
# Server 1: Transactional (default)
# - SMTP: smtp.yourcompany.com:587
# - From: noreply@yourcompany.com
# - Priority: 10 (higher = used first)
#
# Server 2: Marketing (dedicated)
# - SMTP: smtp.sendgrid.net:587 (or Mailgun, SES, etc.)
# - From: marketing@yourcompany.com
# - Priority: 20
#
# In Email Marketing settings, set the dedicated server
# under: Email Marketing → Configuration → Settings
# → Dedicated Server: "Marketing (SendGrid)" Use a subdomain for marketing emails (e.g., mail.yourcompany.com) instead of your root domain. If your marketing subdomain builds a poor reputation, your main domain's sender score stays clean. This is a one-time DNS setup that protects your entire email infrastructure.
Building Mailing List Segments from CRM Pipeline Data
The difference between email marketing that converts and email marketing that gets unsubscribed is segmentation. Odoo 19 lets you build mailing lists dynamically from CRM data—so the right leads get the right message at the right stage of the buying process.
Static vs. Dynamic Mailing Lists
Odoo supports two types of mailing lists, and choosing the wrong one is the most common segmentation mistake:
| Type | How It Works | Best For | CRM Integration |
|---|---|---|---|
| Static List | Contacts are added manually or via import. List membership doesn't change unless you update it. | Event attendees, one-time promotions, imported lists | Weak — snapshot in time |
| Dynamic Filter | Mailing targets are defined by a domain filter. Recipients are calculated at send time. | Stage-based nurturing, activity-driven campaigns | Strong — always current |
Creating CRM-Based Dynamic Segments
When creating a mailing, set the Recipients model to Lead/Opportunity instead of the default Mailing Contact. This unlocks CRM-specific filters:
# Segment 1: New leads that haven't been contacted in 7+ days
[
("type", "=", "lead"),
("stage_id.sequence", "<=", 1),
("activity_date_deadline", "<=", (context_today() - timedelta(days=7)).strftime("%Y-%m-%d")),
]
# Segment 2: Qualified opportunities stuck in proposal stage
[
("type", "=", "opportunity"),
("stage_id.name", "ilike", "Proposition"),
("date_last_stage_update", "<=", (context_today() - timedelta(days=14)).strftime("%Y-%m-%d")),
]
# Segment 3: Lost deals from last quarter (win-back campaign)
[
("active", "=", False),
("probability", "=", 0),
("date_closed", ">=", (context_today() - timedelta(days=90)).strftime("%Y-%m-%d")),
]
# Segment 4: High-value opportunities (expected revenue > 50k)
[
("type", "=", "opportunity"),
("expected_revenue", ">=", 50000),
("stage_id.is_won", "=", False),
]Syncing CRM Tags to Mailing Lists
For more permanent segmentation, use a server action to auto-subscribe leads to static mailing lists based on CRM tags. This is useful when salespeople tag leads during qualification and you want those tags to drive marketing campaigns:
# Automated Action: on write of crm.lead (tag_ids field)
# Action Type: Execute Python Code
for lead in records:
if not lead.partner_id or not lead.partner_id.email:
continue
for tag in lead.tag_ids:
# Map CRM tags to mailing list names
list_name = f"CRM - {{tag.name}}"
mailing_list = env["mailing.list"].search(
[("name", "=", list_name)], limit=1
)
if not mailing_list:
mailing_list = env["mailing.list"].create({{
"name": list_name,
"is_public": False,
}})
# Subscribe the contact (idempotent: won't duplicate)
existing = env["mailing.contact.subscription"].search([
("list_id", "=", mailing_list.id),
("contact_id.email", "=", lead.partner_id.email),
])
if not existing:
contact = env["mailing.contact"].search(
[("email", "=", lead.partner_id.email)], limit=1
)
if not contact:
contact = env["mailing.contact"].create({{
"name": lead.partner_id.name,
"email": lead.partner_id.email,
}})
contact.write({{
"subscription_list_ids": [(0, 0, {{
"list_id": mailing_list.id,
}})],
}}) Auto-subscribing CRM leads to mailing lists is only legal if you have a legitimate interest basis (B2B in most jurisdictions) or explicit consent. If you operate under GDPR, add a consent checkbox to your web forms and only sync leads where partner_id.email_marketing_consent is True. Odoo 19's mailing module respects opt-out flags—but it won't check consent on initial subscription.
Designing Emails with the Odoo 19 Drag-and-Drop Builder
Odoo 19's email builder has matured significantly from earlier versions. It now includes a block-based drag-and-drop editor with pre-built sections, dynamic content blocks, and responsive rendering that works across Gmail, Outlook, and Apple Mail. But there are important constraints to understand.
Builder Capabilities and Limitations
| Feature | Supported? | Notes |
|---|---|---|
| Drag-and-drop blocks | Yes | Text, image, button, divider, social icons, columns |
| Dynamic placeholders | Yes | {{object.partner_id.name}}, {{object.name}}, etc. |
| Conditional blocks | Partial | Use t-if in the HTML source, not in the visual editor |
| Custom HTML | Yes | Switch to code view for full control |
| Responsive design | Auto | Columns stack on mobile; images scale to 100% width |
| Dark mode support | No | No automatic dark mode CSS; test manually |
Email Template with Dynamic CRM Data
When sending to CRM leads, you can personalize emails using lead fields. Switch to the code view and use Jinja-style placeholders:
<!-- Personalized subject line (set in the Subject field):
{{object.partner_id.name}}, here's how we can help
with {{object.name}}
-->
<div style="max-width: 600px; margin: 0 auto;
font-family: Arial, sans-serif;">
<!-- Header with company logo -->
<div style="background: #1a5c2e; padding: 24px;
text-align: center;">
<img src="/web/image/res.company/1/logo"
alt="Company Logo"
style="max-height: 48px;" />
</div>
<!-- Body -->
<div style="padding: 32px 24px;">
<p>Hi {{object.partner_id.name or 'there'}},</p>
<p>I noticed your inquiry about
<strong>{{object.name}}</strong>
came in on {{object.create_date.strftime('%B %d')}}.
I wanted to share a resource that might help.</p>
<!-- CTA Button -->
<div style="text-align: center; margin: 32px 0;">
<a href="/resources"
style="background: #1a5c2e; color: #ffffff;
padding: 14px 32px; text-decoration: none;
border-radius: 6px; font-weight: bold;">
View Our Implementation Guide
</a>
</div>
<p>If you'd like to discuss your project directly,
you can book a call with
{{object.user_id.name or 'our team'}}.</p>
</div>
<!-- Footer (required: unsubscribe link) -->
<div style="padding: 16px 24px; font-size: 12px;
color: #888; border-top: 1px solid #eee;">
<a href="/mail/mailing/{{mailing.id}}/unsubscribe">
Unsubscribe</a> |
{{mailing.user_id.company_id.name}}
</div>
</div>Use the Send Test button to send yourself a preview. Then check it in Gmail (web), Outlook (desktop), and Apple Mail (mobile). The Odoo builder produces clean HTML, but email clients are notorious for rendering inconsistencies—especially Outlook, which uses the Word rendering engine and ignores most CSS.
A/B Testing Email Campaigns in Odoo 19
Odoo 19 includes native A/B testing for email campaigns. You can test subject lines, sender names, email content, and send times—then automatically send the winning variant to the remaining audience. Here's how to set it up correctly.
Configuring an A/B Test
When creating a mailing, enable A/B Testing in the settings tab. This creates a percentage split of your audience:
# A/B Test Configuration
# ─────────────────────
# Variant A (this mailing):
# Subject: "Your Odoo implementation roadmap"
# Test percentage: 20%
#
# Variant B (create alternate mailing):
# Subject: "3 mistakes that delay Odoo go-live by months"
# Test percentage: 20%
#
# Winning criteria: Best open rate
# Winner selection: Automatic after 24 hours
# Remaining 60%: Sent the winning variant
#
# IMPORTANT: Both variants must target the SAME mailing
# list or domain filter. Odoo deduplicates automatically
# so no recipient receives both variants.What to Test (and What Not To)
- Subject lines — highest-impact variable. Test curiosity vs. specificity, questions vs. statements, with and without the recipient's name.
- Sender name — "Sarah from Octura" vs. "Octura Solutions." Personal names typically win for B2B.
- Send time — Tuesday 10 AM vs. Thursday 2 PM. Schedule variants at different times targeting the same list.
- Do NOT test multiple variables at once. If you change the subject AND the CTA button, you can't attribute the result. One variable per test.
A/B tests need statistical significance. With a typical email open rate of 20-25%, you need at least 1,000 recipients per variant to detect a meaningful difference. If your list is under 2,000, skip A/B testing and instead test across sequential campaigns—send variant A this week and variant B next week to the same segment.
Building Automated Drip Campaigns with Odoo Marketing Automation
Single emails don't nurture leads. Sequences do. Odoo's Marketing Automation module lets you build multi-step workflows that send timed emails based on lead behavior—opened the previous email, clicked a link, moved to a new CRM stage, or stayed inactive for a set period.
Anatomy of a Lead Nurture Workflow
Here's a proven 5-email nurture sequence for new CRM leads, configured in the Marketing Automation module:
# Campaign: New Lead Nurture
# Model: Lead/Opportunity (crm.lead)
# Filter: type = 'lead' AND stage = 'New'
# ──────────────────────────────────────────
# ACTIVITY 1: Welcome Email
# Trigger: 1 hour after entering campaign
# Type: Email
# Subject: "Welcome — here's what to expect"
# Content: Introduce your company, set expectations,
# link to a valuable resource (guide, case study)
# ACTIVITY 2: Value Email (child of Activity 1)
# Trigger: 3 days after Activity 1
# Condition: Opened Activity 1
# Subject: "How [similar company] solved [their problem]"
# Content: Case study relevant to the lead's industry
# ACTIVITY 2b: Re-engage (child of Activity 1)
# Trigger: 3 days after Activity 1
# Condition: Did NOT open Activity 1
# Subject: "Did you miss this? [different subject line]"
# Content: Same core content, new subject + preview text
# ACTIVITY 3: Soft CTA
# Trigger: 5 days after Activity 2 or 2b
# Condition: Clicked any link in previous email
# Subject: "Quick question about your project"
# Content: Personalized ask — offer a 15-min call
# ACTIVITY 3b: Server Action (no click)
# Trigger: 5 days after Activity 2 or 2b
# Condition: Did NOT click any link
# Type: Server Action
# Action: Add tag "Low Engagement" to lead
# Move to "Nurture" stage in CRM
# ACTIVITY 4: Final CTA
# Trigger: 7 days after Activity 3
# Subject: "Last chance: free implementation audit"
# Content: Urgency-based offer with calendar link
# ACTIVITY 5: Server Action (end of sequence)
# Trigger: 3 days after Activity 4
# Type: Server Action
# Action: Update lead tag to "Nurture Complete"
# Create activity for salesperson:
# "Follow up — lead completed nurture sequence"Creating the Workflow in Python (Programmatic Setup)
For teams that manage campaigns as code (version-controlled, reproducible across environments), here's how to create a marketing automation campaign programmatically:
# Run in Odoo shell or as a migration script
campaign = env["marketing.campaign"].create({{
"name": "New Lead Nurture Sequence",
"model_id": env.ref("crm.model_crm_lead").id,
"domain": '[("type","=","lead"),("stage_id.sequence","<=",1)]',
}})
# Activity 1: Welcome email (1 hour delay)
welcome = env["marketing.activity"].create({{
"name": "Welcome Email",
"campaign_id": campaign.id,
"activity_type": "email",
"mass_mailing_id": env.ref("my_module.mailing_welcome").id,
"trigger_type": "begin",
"interval_number": 1,
"interval_type": "hours",
}})
# Activity 2: Case study (3 days, if opened)
env["marketing.activity"].create({{
"name": "Case Study (Opened)",
"campaign_id": campaign.id,
"activity_type": "email",
"mass_mailing_id": env.ref("my_module.mailing_case_study").id,
"parent_id": welcome.id,
"trigger_type": "mail_open",
"interval_number": 3,
"interval_type": "days",
}})
# Activity 2b: Re-engage (3 days, if NOT opened)
env["marketing.activity"].create({{
"name": "Re-engage (Not Opened)",
"campaign_id": campaign.id,
"activity_type": "email",
"mass_mailing_id": env.ref("my_module.mailing_reengage").id,
"parent_id": welcome.id,
"trigger_type": "mail_not_open",
"interval_number": 3,
"interval_type": "days",
}})
campaign.action_start_campaign() Marketing Automation activities are processed by the marketing.campaign cron job, which runs every 4 hours by default. If your drip campaigns need tighter timing (e.g., "send 1 hour after"), reduce the cron interval: Settings → Technical → Automation → Scheduled Actions → "Execute Marketing Campaigns". Set it to run every hour. Be aware this increases server load proportionally.
Email Deliverability: SPF, DKIM, and DMARC Configuration for Odoo
You can build the perfect email sequence, but none of it matters if your emails land in spam. Email deliverability is a technical problem that requires DNS configuration before you send your first campaign. Here's the complete setup for Odoo 19.
The Three DNS Records You Need
| Record | What It Does | What Happens Without It |
|---|---|---|
| SPF | Declares which servers are authorized to send email for your domain | Emails fail SPF checks; Gmail/Outlook soft-fail or reject |
| DKIM | Adds a cryptographic signature to each email proving it wasn't tampered with | No authentication; emails look suspicious to receiving servers |
| DMARC | Tells receiving servers what to do when SPF or DKIM fails (none, quarantine, reject) | No policy enforcement; spoofed emails from your domain go unchecked |
DNS Record Examples
# SPF Record (TXT on mail.yourcompany.com)
# Include your SMTP provider (e.g., SendGrid, Mailgun, SES)
v=spf1 include:sendgrid.net include:_spf.google.com ~all
# DKIM Record (TXT on selector._domainkey.mail.yourcompany.com)
# Your SMTP provider gives you this value
s1._domainkey.mail.yourcompany.com TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqG..."
# DMARC Record (TXT on _dmarc.mail.yourcompany.com)
# Start with p=none to monitor, then move to p=quarantine
v=DMARC1; p=none; rua=mailto:dmarc-reports@yourcompany.com; pct=100; adkim=r; aspf=rVerifying Your Setup
# Check SPF
dig TXT mail.yourcompany.com +short
# Expected: "v=spf1 include:sendgrid.net ... ~all"
# Check DKIM
dig TXT s1._domainkey.mail.yourcompany.com +short
# Expected: "v=DKIM1; k=rsa; p=..."
# Check DMARC
dig TXT _dmarc.mail.yourcompany.com +short
# Expected: "v=DMARC1; p=none; ..."
# Send a test email and check headers in Gmail:
# Open the email → "Show original" → look for:
# SPF: PASS
# DKIM: PASS
# DMARC: PASS Never start with p=reject. Begin with p=none and monitor DMARC reports for 2-4 weeks. You'll discover legitimate email sources you forgot about (old CRM, help desk, etc.). Once reports are clean, move to p=quarantine for 2 weeks, then p=reject. Skipping this progression means legitimate emails from forgotten systems get silently rejected.
Email Analytics and CRM Attribution: Measuring What Actually Drives Revenue
Open rates and click rates are vanity metrics unless you connect them to revenue. Odoo 19's built-in link tracker and UTM system let you attribute closed deals back to specific email campaigns. Here's how to set up end-to-end attribution.
Built-In Metrics per Mailing
| Metric | How Odoo Tracks It | What to Watch For |
|---|---|---|
| Sent / Delivered | SMTP response codes | Delivery rate below 95% signals list hygiene issues |
| Opened | Tracking pixel (1x1 image) | Apple MPP inflates opens; don't rely on this alone |
| Clicked | Link redirect via link.tracker | Most reliable engagement signal |
| Bounced | Bounce email parsing | Hard bounce rate above 2% requires list cleaning |
| Replied | Incoming mail matching | High-value signal; lead replied to your campaign |
UTM Attribution: Connecting Emails to Closed Deals
Every mailing in Odoo automatically generates UTM parameters. When a lead clicks a link in your email and later converts, the UTM source is stored on the opportunity. To report on this:
# Custom report: Revenue attributed to email campaigns
# Run in Odoo shell or as a scheduled report
from collections import defaultdict
won_opps = env["crm.lead"].search([
("type", "=", "opportunity"),
("stage_id.is_won", "=", True),
("source_id", "!=", False),
("date_closed", ">=", "2026-01-01"),
])
attribution = defaultdict(lambda: {{"count": 0, "revenue": 0}})
for opp in won_opps:
source = opp.source_id.name
attribution[source]["count"] += 1
attribution[source]["revenue"] += opp.expected_revenue
print("\n Email Campaign Attribution Report")
print(" " + "=" * 50)
for source, data in sorted(
attribution.items(),
key=lambda x: x[1]["revenue"],
reverse=True,
):
print(f" {{source}}:")
print(f" Won deals: {{data['count']}}")
print(f" Revenue: ${{data['revenue']:,.0f}}")Since iOS 15, Apple Mail pre-fetches tracking pixels, making open rates unreliable for ~40% of email recipients. Focus on click-through rate and reply rate as your primary engagement metrics. Open rate is now only useful as a directional indicator, not an absolute measurement. Build your dashboards around clicks and downstream CRM actions.
3 Email Marketing Mistakes That Kill Your Deliverability and Conversions
Sending Your First Campaign to Your Entire List
You install Email Marketing, import 10,000 contacts, and send a campaign to all of them on day one. Your sending domain has zero reputation with Gmail, Outlook, and Yahoo. Sending 10,000 emails from a cold domain triggers spam filters immediately. Your domain gets throttled, emails land in spam, and your sender reputation starts in a hole you'll spend months climbing out of.
Warm up your sending domain. Start with 50-100 emails per day to your most engaged contacts (people who've recently interacted with your sales team). Increase volume by 50% every 3-4 days. Over 2-3 weeks, ramp up to full volume. This gradual increase builds positive sender reputation with ISPs. Odoo doesn't have built-in warm-up scheduling, so use the mail.batch.size system parameter to control daily send limits.
Using the Default "From" Address Without Authentication
Odoo's default configuration sends emails from whatever address is set in the outgoing mail server—often admin@yourcompany.com or worse, the OdooBot address. If SPF, DKIM, and DMARC aren't configured for that exact sending address, every email fails authentication. Gmail shows a "via" warning next to the sender name. Outlook routes the email to Junk. Enterprise spam filters reject it outright.
Configure SPF, DKIM, and DMARC (as detailed in the Deliverability section above) before sending any campaign. Set the "From" address in your mailing to match the authenticated domain exactly. In Odoo 19, go to Email Marketing → Configuration → Settings and set a dedicated "Email From" that matches your DNS records. Test with mail-tester.com—aim for a score of 9/10 or higher.
Building Drip Campaigns Without Exit Conditions
You set up a 5-email nurture sequence for new leads. A lead enters the sequence, then the salesperson closes the deal after the first email. The lead—now a paying customer—continues receiving emails saying "still interested?" and "last chance for a free consultation." Nothing damages credibility faster than sending nurture emails to someone who already bought.
Add domain filters to every activity in your marketing automation workflow. Each activity should include a filter like [("stage_id.is_won", "=", False), ("active", "=", True)] to exclude won deals and archived leads. Alternatively, use a server action at the start of your workflow that checks the lead's current stage and removes it from the campaign if it's already been converted. Review your active campaigns weekly for participants who should have exited.
What Email Marketing Automation Delivers for Your Pipeline
Email marketing integrated with CRM isn't a brand-awareness play. It's a revenue acceleration engine:
Nurtured leads produce 20% more sales opportunities than non-nurtured leads. A 5-email drip sequence keeps your company top-of-mind during the buying cycle without manual follow-up.
Automated email sequences replace manual salesperson follow-ups for early-stage leads. Your sales team focuses on warm opportunities while automation handles the nurturing.
Email marketing consistently delivers the highest ROI of any digital channel. With Odoo's built-in tooling, there's no additional SaaS cost—the module is included in your Odoo license.
For a B2B company generating 200 leads per month with a 10% close rate and $15,000 average deal value, increasing the close rate to 13% through email nurturing adds $54,000 in monthly revenue—$648,000 annually from a system that runs automatically once configured.
Optimization Metadata
Complete guide to Odoo 19 email marketing with CRM integration. Set up mailing lists, build drip campaigns, configure SPF/DKIM/DMARC, and measure attribution from lead to closed deal.
1. "Setting Up the Odoo 19 Email Marketing Module with CRM Integration"
2. "Building Mailing List Segments from CRM Pipeline Data"
3. "Building Automated Drip Campaigns with Odoo Marketing Automation"
4. "Email Deliverability: SPF, DKIM, and DMARC Configuration for Odoo"
5. "3 Email Marketing Mistakes That Kill Your Deliverability and Conversions"