[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
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 76/100
Direzione di ricerca
Inizia da mail_composer_cc_bcc/models/mail_mail.py alla riga 76 e confronta la sua gestione dei destinatari con _prepare_outgoing_list in odoo/addons/mail/models/mail_mail.py del core. Riproduci il problema con un partner con un'email e uno senza. Poi aggiungi un test di regressione per il caso di destinatari misti. È completato quando il destinatario non valido viene saltato senza impedire ai destinatari validi di ricevere la mail.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
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.0branch yet, but the open migrations carry the same construct — #163 has it twice (email_toandemail_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.
- Lingua principale
- Python
- Stelle
- 36
- Fork
- 180
- Merge medio
- 4g 2h
- PR unite (30g)
- 14
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Nessun modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di OCA/mail
-
[18.0] mail_drop_target: uploading a file to a Discuss channel fails (_get_as_sudo_from_request_or_raise no longer exists)Forse già presa Una pull request collegata a questa issue è aperta o già unita. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
I maintainer di solito rispondono entro 1 giorno
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
I maintainer di solito rispondono entro 1 giorno
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 77/100
I maintainer di solito rispondono entro 1 giorno
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
I maintainer di solito rispondono entro 1 giorno
-
Activity creation without model_idForse già presa @parabol123654 l’ha presa 4 giorni fa. Apertabug
Difficoltà 2/5 1-3 ore Idoneità per principianti 25/100
I maintainer di solito rispondono entro 1 giorno
Issue simili
-
documentation type/bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno
-
add entry to blockAperta
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 65/100
StevenBlack/hosts#3299 ·
-
`garak --report` overwrites the input report when its name has no ".report"Forse già presa @0z5a l’ha presa oggi. Apertaneeds-triage
Difficoltà 2/5 Mezza giornata Idoneità per principianti 72/100
I maintainer di solito rispondono entro 2 giorni
-
trace: Pi's 1-hour cache writes are priced at 1.25x, but billed at 2xForse già presa @Shizoqua l’ha presa oggi. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
microsoft/TokenEconomizer#221 ·
I maintainer di solito rispondono entro 1 giorno
-
plot_graph crashes when a mesh node has no m2m edgeForse già presa @tygvi l’ha presa oggi. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
mllam/neural-lam#883 · 1 commento ·
I maintainer di solito rispondono entro 2 giorni