Skip to content

Odoo Email Notification System Internals: How Emails Work

DeployMonkey Team · March 23, 2026 11 min read

How Odoo Sends Emails

Odoo's email pipeline has multiple layers. Understanding each layer helps you debug delivery issues and customize notification behavior:

  1. Message creation: message_post() or mail.mail.create()
  2. Notification generation: Based on followers and subtypes
  3. Email rendering: QWeb templates + mail layout
  4. Mail queue: mail.mail records in outgoing state
  5. SMTP sending: Cron job processes the queue via the outgoing mail server

Outgoing Mail Server

Configure at Settings > Technical > Outgoing Mail Servers:

# Key settings
SMTP Server: smtp.gmail.com (or your provider)
SMTP Port: 587 (TLS) or 465 (SSL)
Username: [email protected]
Password: app-specific password
Connection Security: TLS (STARTTLS)

Odoo selects the outgoing mail server based on the email_from domain. If no server matches, it uses the first available server. If no server is configured, emails queue but never send.

Email Templates

Email templates (mail.template) are QWeb-rendered email bodies:

<record id="email_template_order_confirm" model="mail.template">
    <field name="name">Order Confirmation</field>
    <field name="model_id" ref="sale.model_sale_order"/>
    <field name="email_from">{{ object.company_id.email }}</field>
    <field name="email_to">{{ object.partner_id.email }}</field>
    <field name="subject">Order {{ object.name }} Confirmed</field>
    <field name="body_html" type="html">
        <p>Dear {{ object.partner_id.name }},</p>
        <p>Your order <strong>{{ object.name }}</strong> has been confirmed.</p>
        <table>
            <tr t-foreach="object.order_line" t-as="line">
                <td>{{ line.product_id.name }}</td>
                <td>{{ line.product_uom_qty }}</td>
                <td>{{ format_amount(line.price_subtotal, object.currency_id) }}</td>
            </tr>
        </table>
        <p>Total: {{ format_amount(object.amount_total, object.currency_id) }}</p>
    </field>
    <field name="auto_delete">True</field>
</record>

Sending Emails Programmatically

Via Email Template

template = self.env.ref('my_module.email_template_order_confirm')
template.send_mail(record.id, force_send=True)

# force_send=True: sends immediately (bypasses queue)
# force_send=False (default): adds to mail queue for cron processing

Via mail.mail Directly

mail = self.env['mail.mail'].sudo().create({
    'subject': 'Direct Email',
    'email_from': '[email protected]',
    'email_to': '[email protected]',
    'body_html': '<p>Hello, this is a direct email.</p>',
    'auto_delete': True,
})
mail.send()

Via message_post (Chatter)

# Posts in chatter AND notifies followers via email
record.message_post(
    body='Your request has been processed.',
    message_type='comment',
    subtype_xmlid='mail.mt_comment',
)

Notification Preferences

Each user has a notification preference that controls how they receive messages:

PreferenceBehavior
Handle by EmailsReceives full email for each notification
Handle in OdooSees notifications only in the Odoo inbox (no email)

Set via user preferences or programmatically:

user.notification_type = 'email'  # or 'inbox'

The Mail Queue

Emails are stored as mail.mail records. The mail cron job processes them:

# Check mail queue
pending = self.env['mail.mail'].search([
    ('state', '=', 'outgoing')
])

# Force process queue
self.env['mail.mail'].process_email_queue()

# Check failed emails
failed = self.env['mail.mail'].search([
    ('state', '=', 'exception')
])
for mail in failed:
    print(f'{mail.subject}: {mail.failure_reason}')

Mail states: outgoing (queued), sent (delivered to SMTP), received (acknowledged), exception (failed), cancel (cancelled).

Template Rendering Context

Email templates have access to these variables in Jinja expressions:

  • object: The record being emailed
  • user: Current user
  • ctx: Context dictionary
  • format_amount(amount, currency): Currency formatting
  • format_date(date, format, lang): Date formatting
  • format_datetime(dt, format, lang): Datetime formatting

Debugging Email Delivery

Check SMTP Configuration

# Test SMTP connection
server = self.env['ir.mail_server'].search([], limit=1)
try:
    smtp = server.connect()
    print('SMTP connection successful')
    smtp.quit()
except Exception as e:
    print(f'SMTP error: {e}')

Enable Mail Logging

# In odoo.conf
log_handler = odoo.addons.mail:DEBUG

Common Issues and Fixes

ProblemCauseFix
Emails stuck in outgoingMail cron not runningActivate the "Email Queue Manager" cron
Exception stateSMTP authentication failedCheck server credentials, use app passwords
No emails at allNo outgoing mail serverConfigure SMTP server in settings
Emails in spamMissing SPF/DKIMAdd DNS records for the sending domain
Template not renderingMissing record contextEnsure template model matches the record model

Auto-Delete and Cleanup

Set auto_delete=True on templates to clean up sent emails from the database. Without this, the mail.mail table grows indefinitely:

<field name="auto_delete">True</field>

Best Practices

  • Always configure an outgoing mail server — test it with the built-in test button
  • Use force_send=True only for critical emails — queued sending is more reliable
  • Set auto_delete=True on transactional templates to prevent database bloat
  • Use SPF, DKIM, and DMARC DNS records for the sending domain
  • Monitor the mail queue for stuck or failed emails
  • Use mail.mt_note subtype for internal messages to avoid spamming followers