[18.0] mail_composer_cc_bcc: IndexError in _prepare_outgoing_list when a recipient partner has no email — the whole email fails, not just that recipient

Open Beginner friendly
#252 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
76/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
python
Domain
backend

Research direction

Start with mail_composer_cc_bcc/models/mail_mail.py at line 76 and compare its recipient handling with core's odoo/addons/mail/models/mail_mail.py _prepare_outgoing_list. Reproduce with one partner with an email and one without, then add a regression test for the mixed-recipient case. Done means the invalid recipient is skipped without preventing valid recipients from receiving the mail.

Written by the indexing model from the issue text.

Description

Summary

When a message is sent through the mail composer and any recipient partner has no email address, MailMail._prepare_outgoing_list() raises

IndexError: list index out of range

at mail_composer_cc_bcc/models/mail_mail.py:76 (branch 18.0, 18.0.1.0.3). The exception escapes core's per-recipient handling, so the whole mail.mail fails: none of the valid recipients receives anything and the mail is left in exception state. Without this module installed, core handles the same situation gracefully — it skips the invalid recipient and delivers to everybody else.

Root cause

Core builds one outgoing entry per recipient partner, and for a partner without an email address it deliberately produces a placeholder (odoo/addons/mail/models/mail_mail.py, _prepare_outgoing_list):

email_to_normalized = tools.mail.email_normalize_all(partner.email)
email_to = [
    tools.formataddr((partner.name or "", email or "False"))
    for email in email_to_normalized or [partner.email]
]

With partner.email falsy, email_normalize_all() returns [], the comprehension iterates over [False] and formats the literal string "False". formataddr then splits on the last @ and reassembles, which for "False" gives an empty local part:

>>> tools.formataddr(("Recipient Without Email", "False"))
'"Recipient Without Email" <@False>'
>>> extract_rfc2822_addresses('"Recipient Without Email" <@False>')
[]

So the entry has a non-empty email_to list — it survives the if m["email_to"] check — but no extractable address, and

rcpt_to = extract_rfc2822_addresses(m["email_to"][0])[0]   # <-- IndexError

raises. The @ is inserted by core's formataddr, not by this module.

Steps to reproduce

Clean database with only mail_composer_cc_bcc installed (it only depends on mail):

Partner = env["res.partner"]
p_ok = Partner.create({"name": "Recipient With Email", "email": "[email protected]"})
p_ko = Partner.create({"name": "Recipient Without Email"})  # no email

mail = env["mail.mail"].create({
    "subject": "test",
    "body_html": "<p>test</p>",
    "recipient_ids": [(6, 0, (p_ok + p_ko).ids)],
})
mail.with_context(is_from_composer=True)._prepare_outgoing_list()
# -> IndexError: list index out of range

Through the UI: add both partners as followers of any record with a chatter, open the full composer (this module only acts in composition mode comment) and send. Nobody receives the message, including Recipient With Email.

Current vs expected behaviour

Current (module installed) Expected (= core behaviour)
Recipient without email IndexError skipped, notification classified mail_email_invalid
Other recipients of the same mail.mail not delivered delivered
mail.mail state exception sent

Core's contract for this case is explicit: IrMailServer._prepare_smtp_to_list() returns an empty list, assert smtp_to_list, self.NO_VALID_RECIPIENT fires, and mail/models/mail_mail.py::_send() catches that AssertionError specifically in order to "ignore error to avoid blocking delivery to next recipients, if any" (its own comment). This module short-circuits that path before core ever gets there.

Proposed fix

Leave the entry untouched instead of indexing blindly. That is not merely defensive — it is what restores the core behaviour, because core then finds no SMTP recipient for that entry, raises NO_VALID_RECIPIENT and skips only that recipient:

-            rcpt_to = extract_rfc2822_addresses(m["email_to"][0])[0]
+            addresses = [
+                address
+                for address in extract_rfc2822_addresses(m["email_to"][0])
+                if tools.mail.email_normalize(address)
+            ]
+            if not addresses:
+                continue
+            rcpt_to = addresses[0]

The email_normalize() filter is a second belt: extract_rfc2822_addresses() alone already returns [] for the <@False> shape, but normalizing makes the criterion explicit and covers any other malformed value matching the extraction pattern.

Worth noting for whoever fixes it: do not give rcpt_to a fallback value instead. The entry would stay alive and get the whole audience written into its To header while its normalized recipient list stays empty — i.e. no SMTP envelope restriction.

Affected versions

  • 18.0 (18.0.1.0.3): line 76.
  • 19.0: the module is not on the 19.0 branch yet, but the open migrations carry the same construct — #163 has it twice (email_to and email_cc), and #104 / #189 once. I reproduced it on 19.0 running the code of the open migration, where core builds the identical <@False> placeholder, and pushed the fix above to the branch of #104 (camptocamp/mail#2), with a test.

Happy to open the equivalent PR for 18.0 if the approach is agreed.

Dominant language
Python
Stars
36
Forks
180
Avg merge
4d 2h
Merged PRs (30d)
14

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from OCA/mail

All issues in OCA/mail

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.