diff --git a/mail_visible_email/README.rst b/mail_visible_email/README.rst new file mode 100644 index 000000000..cc89e358a --- /dev/null +++ b/mail_visible_email/README.rst @@ -0,0 +1,115 @@ +====================================== +Make emails for to, cc and bcc visible +====================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:53373ba10ebe61d982a5d796725296eee83924dc9d4d6aa7e54e0ec9839f59a5 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fmail-lightgray.png?logo=github + :target: https://github.com/OCA/mail/tree/18.0/mail_visible_email + :alt: OCA/mail +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/mail-18-0/mail-18-0-mail_visible_email + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/mail&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +In Odoo mails it is often unclear who were the other recipients of mails +received, or what the actual mail addresses were of mails sent. + +This module adds the following fields to mail_message: + +- email_to +- email_cc +- email_bcc + +For both incoming and outgoing mails, the actual to and cc headers from +the mails will be stored here. For outgoing mails also the bcc header. + +In case we receive a mail because we received it on an address that was +in the bcc of the email sent, the address will actually be shown on the +email_to field. This is because there is no bcc header in an incoming +mail, we will have the address in the Delivered-To header. + +Note that we will only store the unadorned email (without partner name), +as this will be the relevant part, and the partner names are visible on +other fields. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +This module has no separate configuration options. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Therp BV + +Contributors +------------ + +- `Therp BV `__: + + - Ronald Portier + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-NL66278| image:: https://github.com/NL66278.png?size=40px + :target: https://github.com/NL66278 + :alt: NL66278 + +Current `maintainer `__: + +|maintainer-NL66278| + +This module is part of the `OCA/mail `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/mail_visible_email/__init__.py b/mail_visible_email/__init__.py new file mode 100644 index 000000000..31660d6a9 --- /dev/null +++ b/mail_visible_email/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import models diff --git a/mail_visible_email/__manifest__.py b/mail_visible_email/__manifest__.py new file mode 100644 index 000000000..4e6ec44fb --- /dev/null +++ b/mail_visible_email/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright 2025 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Make emails for to, cc and bcc visible", + "summary": "Save and show the actual email addresses used in mail.message.", + "version": "18.0.1.0.0", + "development_status": "Alpha", + "category": "Email", + "website": "https://github.com/OCA/mail", + "author": "Therp BV, Odoo Community Association (OCA)", + "maintainers": ["NL66278"], + "license": "AGPL-3", + "application": False, + "installable": True, + "depends": [ + "mail", + "mail_composer_cc_bcc", + "test_mail", + ], + "data": [ + "views/mail_message_views.xml", + ], +} diff --git a/mail_visible_email/models/__init__.py b/mail_visible_email/models/__init__.py new file mode 100644 index 000000000..c54733443 --- /dev/null +++ b/mail_visible_email/models/__init__.py @@ -0,0 +1,5 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import mail_thread +from . import mail_message +from . import mail_mail diff --git a/mail_visible_email/models/mail_mail.py b/mail_visible_email/models/mail_mail.py new file mode 100644 index 000000000..455a8ede2 --- /dev/null +++ b/mail_visible_email/models/mail_mail.py @@ -0,0 +1,57 @@ +# Copyright 2025 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import api, models, tools + + +def list_emails(partners): + return [ + tools.mail.email_normalize(p.email) + for p in partners + if p.email and tools.mail.email_normalize(p.email) + ] + + +class MailMail(models.Model): + _inherit = "mail.mail" + + @api.model_create_multi + def create(self, values_list): + mails = super().create(values_list) + for mail in mails: + message_vals = {} + email_values = mail._get_email_values() + for fname in ("email_to", "email_cc", "email_bcc"): + emails = email_values.get(fname, []) + if emails: + message_vals[fname] = mail._append_email(fname, emails) + if message_vals: + mail.mail_message_id.write(message_vals) + return mails + + def _get_email_values(self): + """Return normalized email lists for to, cc and bcc. + - Composer send: recipient_cc_ids/recipient_bcc_ids are set on mail.mail + at create time by the composer's _prepare_mail_values. + - Template send (send_mail): email_to, email_cc, email_bcc + are written directly to mail.mail at create time from the rendered + template values. + """ + self.ensure_one() + cc_bcc = self.recipient_cc_ids + self.recipient_bcc_ids + to_partners = self.recipient_ids - cc_bcc + return { + "email_to": list_emails(to_partners) + or tools.mail.email_normalize_all(self.email_to or ""), + "email_cc": list_emails(self.recipient_cc_ids) + or tools.mail.email_normalize_all(self.email_cc or ""), + "email_bcc": list_emails(self.recipient_bcc_ids) + or tools.mail.email_normalize_all(self.email_bcc or ""), + } + + def _append_email(self, fieldname, emails): + """Merge new emails with any already stored on mail.message.""" + self.ensure_one() + existing = self.mail_message_id[fieldname] + if existing: + emails += existing.split(",") + return ",".join(dict.fromkeys(filter(None, emails))) diff --git a/mail_visible_email/models/mail_message.py b/mail_visible_email/models/mail_message.py new file mode 100644 index 000000000..5fc183716 --- /dev/null +++ b/mail_visible_email/models/mail_message.py @@ -0,0 +1,24 @@ +# Copyright 2025 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class MailMessage(models.Model): + _inherit = "mail.message" + + email_to = fields.Char( + string="To", + readonly=True, + help="original email addresses in 'to' header", + ) + email_cc = fields.Char( + string="Cc", + readonly=True, + help="original email addresses in 'cc' header", + ) + email_bcc = fields.Char( + string="Bcc", + readonly=True, + help="original email addresses in 'bcc' header", + ) diff --git a/mail_visible_email/models/mail_thread.py b/mail_visible_email/models/mail_thread.py new file mode 100644 index 000000000..43f8df124 --- /dev/null +++ b/mail_visible_email/models/mail_thread.py @@ -0,0 +1,32 @@ +# Copyright 2025 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, models + +_VISIBLE_EMAIL_FIELDS = {"email_to", "email_cc", "email_bcc"} + + +class MailThread(models.AbstractModel): + _inherit = "mail.thread" + + @api.model + def _message_route_process(self, message, message_dict, routes): + """Intercept message_dict to write 'to' and 'cc' to mail.message. + + Smtp does not deliver messages with a bcc header. If a message is + received from a bcc address, this address will be in the raw + Delivered-To header, which message_parse adds to the 'to' key + of message_dict. + """ + message_dict["email_to"] = message_dict.get("to", False) + message_dict["email_cc"] = message_dict.get("cc", False) + return super()._message_route_process(message, message_dict, routes) + + def _get_message_create_ignore_field_names(self): + return super()._get_message_create_ignore_field_names() | _VISIBLE_EMAIL_FIELDS + + def _message_post_after_hook(self, message, msg_values): + vals = {k: msg_values[k] for k in _VISIBLE_EMAIL_FIELDS if msg_values.get(k)} + if vals: + message.write(vals) + return super()._message_post_after_hook(message, msg_values) diff --git a/mail_visible_email/pyproject.toml b/mail_visible_email/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/mail_visible_email/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/mail_visible_email/readme/CONFIGURE.md b/mail_visible_email/readme/CONFIGURE.md new file mode 100644 index 000000000..c233bc2c8 --- /dev/null +++ b/mail_visible_email/readme/CONFIGURE.md @@ -0,0 +1,2 @@ +This module has no separate configuration options. + \ No newline at end of file diff --git a/mail_visible_email/readme/CONTRIBUTORS.md b/mail_visible_email/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..a184afa07 --- /dev/null +++ b/mail_visible_email/readme/CONTRIBUTORS.md @@ -0,0 +1,4 @@ +- [Therp BV](https://therp.nl): + + > - Ronald Portier \<\> + \ No newline at end of file diff --git a/mail_visible_email/readme/DESCRIPTION.md b/mail_visible_email/readme/DESCRIPTION.md new file mode 100644 index 000000000..284aa9e56 --- /dev/null +++ b/mail_visible_email/readme/DESCRIPTION.md @@ -0,0 +1,21 @@ +In Odoo mails it is often unclear who were the other recipients of mails +received, or what the actual mail addresses were of mails sent. + +This module adds the following fields to mail_message: + +- email_to +- email_cc +- email_bcc + +For both incoming and outgoing mails, the actual to and cc headers +from the mails will be stored here. For outgoing mails also the bcc +header. + +In case we receive a mail because we received it on an address that was +in the bcc of the email sent, the address will actually be shown on the +email_to field. This is because there is no bcc header in an incoming mail, +we will have the address in the Delivered-To header. + +Note that we will only store the unadorned email (without partner name), +as this will be the relevant part, and the partner names are visible on +other fields. diff --git a/mail_visible_email/static/description/icon.png b/mail_visible_email/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/mail_visible_email/static/description/icon.png differ diff --git a/mail_visible_email/static/description/index.html b/mail_visible_email/static/description/index.html new file mode 100644 index 000000000..f28cd7f74 --- /dev/null +++ b/mail_visible_email/static/description/index.html @@ -0,0 +1,458 @@ + + + + + +Make emails for to, cc and bcc visible + + + +
+

Make emails for to, cc and bcc visible

+ + +

Alpha License: AGPL-3 OCA/mail Translate me on Weblate Try me on Runboat

+

In Odoo mails it is often unclear who were the other recipients of mails +received, or what the actual mail addresses were of mails sent.

+

This module adds the following fields to mail_message:

+
    +
  • email_to
  • +
  • email_cc
  • +
  • email_bcc
  • +
+

For both incoming and outgoing mails, the actual to and cc headers from +the mails will be stored here. For outgoing mails also the bcc header.

+

In case we receive a mail because we received it on an address that was +in the bcc of the email sent, the address will actually be shown on the +email_to field. This is because there is no bcc header in an incoming +mail, we will have the address in the Delivered-To header.

+

Note that we will only store the unadorned email (without partner name), +as this will be the relevant part, and the partner names are visible on +other fields.

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Configuration

+

This module has no separate configuration options.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Therp BV
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

NL66278

+

This module is part of the OCA/mail project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/mail_visible_email/tests/__init__.py b/mail_visible_email/tests/__init__.py new file mode 100644 index 000000000..76b133608 --- /dev/null +++ b/mail_visible_email/tests/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_mail_receive +from . import test_mail_send diff --git a/mail_visible_email/tests/test_mail_receive.py b/mail_visible_email/tests/test_mail_receive.py new file mode 100644 index 000000000..e24defe4f --- /dev/null +++ b/mail_visible_email/tests/test_mail_receive.py @@ -0,0 +1,63 @@ +# Copyright 2025 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo.tests.common import TransactionCase, tagged +from odoo.tools import mute_logger + +from odoo.addons.test_mail.data.test_mail_data import MAIL_TEMPLATE + + +@tagged("-at_install", "post_install") +class TestMailReceive(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.Alias = cls.env["mail.alias"] + cls.Partner = cls.env["res.partner"] + cls.MailThread = cls.env["mail.thread"] + cls.Message = cls.env["mail.message"] + cls.alias_domain = cls.env["mail.alias.domain"].create( + { + "name": "fsf.org", + "catchall_alias": "catchall", + } + ) + cls.env.company.alias_domain_id = cls.alias_domain + + cls.partner_model = cls.env["ir.model"].search([("model", "=", "res.partner")]) + cls.mail_alias_test = cls.Alias.create( + { + "alias_name": "test_alias", + "alias_domain_id": cls.alias_domain.id, + "alias_model_id": cls.partner_model.id, + "alias_defaults": "{'name': 'Test Alias', 'is_company': True}", + } + ) + + @mute_logger("odoo.addons.mail.models.mail_thread", "odoo.models") + def test_incoming_email(self): + # Imitate what self.server.fetch_mail() would do + thread_id = self.MailThread.message_process( + self.Partner._name, + MAIL_TEMPLATE.format( + return_path="spambot@example.com", + email_from="spambot@example.com", + to="test_alias@fsf.org", + cc="nobody@fsf.org, anybody@fsf.org", + subject="I'm a robot, hello", + extra="", + msg_id="", + ), + ) + self.assertTrue(thread_id) + partner = self.Partner.browse(thread_id) + self.assertEqual(partner.name, "Test Alias") + message = self.Message.search( + [ + ("model", "=", partner._name), + ("res_id", "=", partner.id), + ] + ) + self.assertTrue(message) + self.assertEqual(message.email_to, "test_alias@fsf.org") + self.assertIn("anybody@fsf.org", message.email_cc) + self.assertIn("nobody@fsf.org", message.email_cc) diff --git a/mail_visible_email/tests/test_mail_send.py b/mail_visible_email/tests/test_mail_send.py new file mode 100644 index 000000000..182fd0189 --- /dev/null +++ b/mail_visible_email/tests/test_mail_send.py @@ -0,0 +1,59 @@ +# Copyright 2025 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo.tests import Form, tagged + +from odoo.addons.mail.tests.common import MailCommon + + +@tagged("-at_install", "post_install") +class TestMailSend(MailCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env.ref("base.res_partner_address_31") + cls.partner_cc = cls.env.ref("base.partner_demo") + cls.partner_cc2 = cls.env.ref("base.partner_demo_portal") + cls.partner_bcc = cls.env.ref("base.res_partner_main1") + cls.mail_template = cls._create_template( + "res.partner", + template_values={ + "auto_delete": False, + "email_to": cls.partner.email, + "email_cc": cls.partner_cc.email, + "email_bcc": cls.partner_bcc.email, + }, + ) + + def open_mail_composer_form(self): + ctx = { + "default_partner_ids": self.partner.ids, + "default_model": self.partner._name, + "default_res_ids": self.partner.ids, + "mail_notify_force_send": True, + } + form = Form(self.env["mail.compose.message"].with_context(**ctx)) + form.body = "

Hello

" + return form + + def test_email_to_cc_bcc_via_composer(self): + """Sending via composer populates email_to, cc, bcc on mail.message.""" + form = self.open_mail_composer_form() + composer = form.save() + composer.partner_cc_ids = self.partner_cc | self.partner_cc2 + composer.partner_bcc_ids = self.partner_bcc + with self.mock_mail_gateway(): + composer._action_send_mail() + message = self.partner.message_ids[0] + self.assertEqual(message.email_to, self.partner.email) # add this + self.assertIn(self.partner_cc.email, message.email_cc) + self.assertIn(self.partner_cc2.email, message.email_cc) + self.assertEqual(message.email_bcc, self.partner_bcc.email) + + def test_email_to_cc_via_template(self): + """Sending via template populates email_to and email_cc on mail.message.""" + Mail = self.env["mail.mail"] + mail_id = self.mail_template.send_mail(self.partner.id, force_send=True) + mail = Mail.browse(mail_id) + message = mail.mail_message_id + self.assertEqual(message.email_to, self.partner.email) + self.assertEqual(message.email_cc, self.partner_cc.email) diff --git a/mail_visible_email/views/mail_message_views.xml b/mail_visible_email/views/mail_message_views.xml new file mode 100644 index 000000000..735d76763 --- /dev/null +++ b/mail_visible_email/views/mail_message_views.xml @@ -0,0 +1,15 @@ + + + + mail.message.form - mail_visible_email + mail.message + + + + + + + + + +