Skip to content

[19.0][FIX] mail_composer_cc_bcc: one email per recipient, no Bcc disclosure - #2

Open
skanndar wants to merge 3 commits into
camptocamp:19.0-mail_composer_cc_bccfrom
skanndar:19.0-fix-mail_composer_cc_bcc-recipients
Open

[19.0][FIX] mail_composer_cc_bcc: one email per recipient, no Bcc disclosure#2
skanndar wants to merge 3 commits into
camptocamp:19.0-mail_composer_cc_bccfrom
skanndar:19.0-fix-mail_composer_cc_bcc-recipients

Conversation

@skanndar

@skanndar skanndar commented Aug 5, 2026

Copy link
Copy Markdown

Follow-up to my review on OCA#104, pushed as commits as @NICO-SOLUTIONS suggested.

This branch builds on @Borruso's #1 — his commit is included unchanged, because his two fixes (renaming the override to _prepare_email_message__, which 19.0 renamed, and copying the headers dict) are what make the Bcc emails reach the SMTP server at all. Merging this brings both; if you would rather keep them separate, tell me and I will rebase onto 19.0-mail_composer_cc_bcc once #1 is in.

My commit adds two things on top.

1. One email per recipient (currently the last one is silently lost)

When mail.email_to is empty Odoo builds an extra Cc-only email (odoo/odoo@46bad8f0). Here it is a duplicate: every Cc partner is a recipient and already gets its own email. It was being dropped by comparing lengths:

if len(res) > len(recipients):
    res.pop()

recipients is a set, and a Cc partner is counted once for the Cc-only entry and once for its own, so the count is short by one and pop() removes the last entry — the last recipient's email — instead of the duplicate. With one To, two Cc and two Bcc partners on 19.0:

before after
emails handed to the SMTP server 5 5
cc1@ 2 copies 1
bcc2@ nothing 1

Filtering the Cc-only entry explicitly (res = [m for m in res if m["email_to"]]) fixes both, and the length heuristic goes away.

2. Drop X-Msg-To-Add

New in 19.0: MailThread._notify_by_email_get_headers() fills that header with every external recipient, and IrMailServer._alter_message__() merges it into the To header at send time to enable Reply-All. Since this module builds the whole To/Cc itself, the header has to go — otherwise the Cc and Bcc addresses end up in the To of every email. I verified this A/B: with the header kept, every recipient (including the plain To one) receives To: to@…, cc1@…, bcc1@…, bcc2@….

Tests

Two tests, both failing before this commit and passing after:

  • test_email_cc_bcc_one_email_per_recipient — 5 recipients → 5 emails, same To/Cc on all of them, no Bcc address visible in any To/Cc, and each Bcc recipient gets its own email.
  • test_email_cc_bcc_no_reply_all_header — no X-Msg-To-Add survives.

The existing test_email_cc_bcc asserted len(self._mails) == 5 and passed either way, because the count was right while the recipients were not.

How this was checked

Official odoo:19 image (build 19.0.20260723), fresh database, only this module installed, and the SMTP session replaced by a stub recording exactly what Odoo hands to the server (send_message(message, smtp_from, smtp_to_list)) — so the envelopes and headers above are the real ones, not a reading of the code. Full module test suite green (9 tests).

Borruso and others added 2 commits July 30, 2026 16:14
Two issues show up on 19.0 when sending from the composer with Cc/Bcc:

* the Cc-only email Odoo builds when mail.email_to is empty (odoo/odoo@46bad8f0)
  is a duplicate here, since every Cc partner is a recipient and already gets
  its own email. It was dropped by comparing lengths and popping the last
  entry, which actually removed the *last recipient's* email instead: with two
  Bcc recipients, the second one never received anything. Filter that entry out
  explicitly instead.

* 19.0 fills the 'X-Msg-To-Add' header with every external recipient
  (MailThread._notify_by_email_get_headers) and merges it into the To header at
  send time (IrMailServer._alter_message__) to enable Reply-All. This module
  builds the whole To / Cc itself, so the header has to be dropped: otherwise
  the Cc and Bcc addresses end up in the To of every email.

Both are covered by tests.

@Borruso Borruso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@NICO-SOLUTIONS

Copy link
Copy Markdown

@skanndar
Have you tested BCC defined directly on mail templates (instead of res.company default BCCs) across different modules (e.g. invoices)?

After a quick test, it seems to work for Sale Orders, but I could not get it to work for invoices.

@NICO-SOLUTIONS

Copy link
Copy Markdown

@skanndar

The current BCC handling seems to be tied to the mail composer flow because it relies on:

context.get("is_from_composer")

This works for composer-based flows (e.g. Sale Orders), but it seems that other template-based sending flows like invoice sending (account.move.send) do not go through this path, so the BCC customization is not applied there.

I also noticed this comment in account.move.send:

# TDE FIXME: this should use standard composer / template code to be sure
# it is aligned with standard recipients management. Todo later

This might be worth considering, as invoice sending appears to use a different recipient generation flow compared to the mail composer.

Since the module is named mail_composer_cc_bcc, it may already be doing what it was designed for. However, if the intention is to support BCC configured on mail templates globally, maybe the implementation should be extended to follow the standard mail.templatemail.mail generation flow instead of depending only on the composer context.

A possible direction could be to propagate the template email_bcc value through the normal template generation flow, similar to how email_cc and email_to are handled, and ensure that it is available on the generated mail.mail record independently of the sending flow.

This would make template BCC work consistently for different modules (e.g. invoices, sales orders, etc.) without introducing module-specific dependencies.

I only did a quick review/test, so I may be missing some details, but this seems to explain why it works for Sale Orders but not for invoices.

@NICO-SOLUTIONS NICO-SOLUTIONS left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@skanndar
Sorry for the noise. I was initially irritated because I noticed that BCC is not applied when sending invoices.

After reviewing the implementation more closely, it seems the current behavior is correct for the intended scope of mail_composer_cc_bcc. The module extends the Mail Composer flow, and the usage of is_from_composer makes sense in this context. Composer-based flows, such as Sale Orders using the email composer wizard, are covered, and the overall flow and scope look correct.

I also verified the actual email delivery with Sale Orders. The BCC recipient receives the email without being exposed in the visible To/Cc headers, and the regular recipient does not see the BCC recipient. So the BCC handling itself looks correct.

Invoice sending (account.move.send) follows a different template/recipient generation path in core and does not go through the Mail Composer, so it is expected that this module does not affect that flow. The comment in account.move.send about aligning with the standard composer/template recipient management is still worth considering, but this would be a separate improvement to the general template-based email generation flow rather than an issue with this module.

If BCC support is also intended for direct template-based sending flows, a separate module or a more general mail_template_cc_bcc approach might make sense here. That could provide BCC support independently of the Mail Composer flow.

Maybe a small note in the documentation could clarify that the current module covers composer-based emails only and that direct template-based sending flows are not covered.

From my side, code review completed and functional testing performed with Sale Orders. LGTM 👍

Sending flows that do not go through mail.compose.message — invoices via
account.move.send being the main one, which calls message_post() directly —
are not covered by this module. Requested during review.
@skanndar

skanndar commented Aug 9, 2026

Copy link
Copy Markdown
Author

Thanks both for the reviews, and no noise at all @NICO-SOLUTIONS — the invoice question was worth asking, and your reading is right. I checked it on 19.0: account.move.send._send_mail() calls move.message_post(...) directly (addons/account/models/account_move_send.py), so mail.compose.message is never involved and is_from_composer is never set. That is also why it works for sale orders, which do go through the composer wizard. Carrying the template's Bcc through the standard mail.templatemail.mail generation, as you suggest, would make it work everywhere — but it is a different feature from what this module does today, so I agree it belongs in a separate module rather than here.

I pushed your documentation suggestion as 0d84265: a short Scope section in readme/DESCRIPTION.md stating that the module covers composer-based emails and that template-only flows such as invoice sending are not, with the pointer to account.move.send (only the readme fragment — README.rst is regenerated by the bot).

@Borruso I see you closed #1 — thanks. This branch still carries your commit unchanged, so merging here brings both fixes.

@cyrilmanuel this now has two approvals; whenever you merge it into 19.0-mail_composer_cc_bcc it will show up in OCA#104 and I can drop my "changes requested" there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants